72 'path' =>
'/comm/propal/class/propal.class.php',
73 'card' =>
'/comm/propal/card.php',
74 'date_field' =>
'datep',
75 'soc_field' =>
'socid'
78 'class' =>
'Commande',
79 'path' =>
'/commande/class/commande.class.php',
80 'card' =>
'/commande/card.php',
81 'date_field' =>
'date_commande',
82 'soc_field' =>
'socid'
86 'path' =>
'/compta/facture/class/facture.class.php',
87 'card' =>
'/compta/facture/card.php',
88 'date_field' =>
'date',
89 'soc_field' =>
'socid'
92 'supplier_proposal' => [
93 'class' =>
'SupplierProposal',
94 'path' =>
'/supplier_proposal/class/supplier_proposal.class.php',
95 'card' =>
'/supplier_proposal/card.php',
96 'date_field' =>
'date',
97 'soc_field' =>
'socid'
100 'class' =>
'CommandeFournisseur',
101 'path' =>
'/fourn/class/fournisseur.commande.class.php',
102 'card' =>
'/fourn/commande/card.php',
103 'date_field' =>
'date_commande',
104 'soc_field' =>
'socid'
106 'supplier_invoice' => [
107 'class' =>
'FactureFournisseur',
108 'path' =>
'/fourn/class/fournisseur.facture.class.php',
109 'card' =>
'/fourn/facture/card.php',
110 'date_field' =>
'date',
111 'soc_field' =>
'socid'
115 'class' =>
'Expedition',
116 'path' =>
'/expedition/class/expedition.class.php',
117 'card' =>
'/expedition/card.php',
118 'date_field' =>
'date_expedition',
119 'soc_field' =>
'socid'
122 'class' =>
'Reception',
123 'path' =>
'/reception/class/reception.class.php',
124 'card' =>
'/reception/card.php',
125 'date_field' =>
'date_reception',
126 'soc_field' =>
'socid'
136 private const PERM_MAP = [
137 'proposal' => [
'propal',
'creer'],
138 'order' => [
'commande',
'creer'],
139 'invoice' => [
'facture',
'creer'],
140 'supplier_proposal' => [
'supplier_proposal',
'creer'],
141 'supplier_order' => [
'fournisseur',
'commande'],
142 'supplier_invoice' => [
'fournisseur',
'facture'],
143 'shipment' => [
'expedition',
'creer'],
144 'reception' => [
'reception',
'creer'],
157 "name" =>
"create_sales_order",
158 "description" =>
"Create a CUSTOMER SALES ORDER. This is specifically for creating ORDERS that customers place with you. USE THIS TOOL whenever user mentions: 'create', 'new' or 'add' with 'order', 'customer order' or 'sales order'. This is NOT for invoices or supplier orders. Examples of when to use this tool:
159- 'create order for customer X'
161- 'add order from customer Z'
162- 'add order for X with 5 items'
163If user says 'order' without any qualifier, they mean a SALES ORDER - use this tool.",
169 "description" =>
"Customer ID (Thirdparty ID) - REQUIRED"
173 "description" =>
"Order date (YYYY-MM-DD format, optional, defaults to today)"
177 "description" =>
"Order notes (optional)"
181 "description" =>
"Products being ordered by the customer",
185 "product_id" => [
"type" =>
"integer",
"default" => 0,
"description" =>
"Product ID (0 if not found)"],
186 "description" => [
"type" =>
"string",
"description" =>
"Product name or description"],
187 "quantity" => [
"type" =>
"number",
"default" => 1,
"description" =>
"Quantity ordered"],
188 "unit_price" => [
"type" =>
"number",
"description" =>
"Selling price per unit (optional)"],
189 "vat_rate" => [
"type" =>
"number",
"description" =>
"VAT rate (optional, auto-calculated if not provided)"]
191 "required" => [
"quantity"]
195 "required" => [
"socid"]
200 "name" =>
"create_customer_invoice",
201 "description" =>
"Create a customer invoice (bill). Do NOT use this for orders - use create_sales_order instead. Do NOT use this for payments - use pay_invoice instead. Examples: 'create invoice for customer X', 'new bill customer Y'",
207 "enum" => [
"invoice"],
208 "default" =>
"invoice"
212 "description" =>
"Invoice header data. Must include 'socid' (Customer ID).",
214 "socid" => [
"type" =>
"integer",
"description" =>
"Customer ID (Thirdparty ID)"],
215 "date" => [
"type" =>
"string",
"description" =>
"Invoice date (YYYY-MM-DD)"],
216 "note_public" => [
"type" =>
"string",
"description" =>
"Public note"],
217 "note_private" => [
"type" =>
"string",
"description" =>
"Private note"]
219 "required" => [
"socid"]
223 "description" =>
"Invoice line items.",
227 "product_id" => [
"type" =>
"integer",
"default" => 0],
228 "description" => [
"type" =>
"string"],
229 "quantity" => [
"type" =>
"number",
"default" => 1],
230 "unit_price" => [
"type" =>
"number"],
231 "vat_rate" => [
"type" =>
"number"],
232 "fk_unit" => [
"type" =>
"integer"]
234 "required" => [
"quantity"]
238 "required" => [
"header"]
243 "name" =>
"create_other_document",
244 "description" =>
"Create documents other than orders and invoices. Use this for: 'proposal', 'supplier_order', 'supplier_invoice', 'supplier_proposal'. DO NOT use for 'order' or 'invoice' - they have dedicated tools.",
250 "enum" => [
'proposal',
'supplier_order',
'supplier_invoice',
'supplier_proposal'],
251 "description" =>
"Document type. Cannot be 'order' or 'invoice'."
255 "description" =>
"Header data. Must include 'socid'.",
257 "socid" => [
"type" =>
"integer",
"description" =>
"Thirdparty ID (Customer for proposal, Supplier for supplier_*)"],
258 "date" => [
"type" =>
"string",
"description" =>
"Document date (YYYY-MM-DD)"],
259 "duree_validite" => [
"type" =>
"integer",
"description" =>
"Validity in days (proposal only)"],
260 "note_public" => [
"type" =>
"string",
"description" =>
"Public note"],
261 "note_private" => [
"type" =>
"string",
"description" =>
"Private note"]
263 "required" => [
"socid"]
267 "description" =>
"Array of line items.",
271 "product_id" => [
"type" =>
"integer",
"default" => 0],
272 "description" => [
"type" =>
"string"],
273 "quantity" => [
"type" =>
"number",
"default" => 1],
274 "unit_price" => [
"type" =>
"number"],
275 "vat_rate" => [
"type" =>
"number"],
276 "fk_unit" => [
"type" =>
"integer"]
278 "required" => [
"quantity"]
282 "required" => [
"object_type",
"header"]
286 "name" =>
"add_line_item",
287 "description" =>
"Add a single line to an existing draft document.",
291 "object_type" => [
"type" =>
"string",
"enum" => array_keys($this->map)],
292 "parent_id" => [
"type" =>
"integer"],
293 "product_id" => [
"type" =>
"integer",
"default" => 0],
294 "description" => [
"type" =>
"string"],
295 "quantity" => [
"type" =>
"number",
"default" => 1],
296 "unit_price" => [
"type" =>
"number"],
297 "vat_rate" => [
"type" =>
"number"]
299 "required" => [
"object_type",
"parent_id",
"quantity"]
303 "name" =>
"delete_object",
304 "description" =>
"Delete a Draft document.",
308 "object_type" => [
"type" =>
"string",
"enum" => array_keys($this->map)],
309 "id" => [
"type" =>
"integer"]
311 "required" => [
"object_type",
"id"]
325 return [
'commercial',
'billing',
'thirdparty'];
335 public function execute(
string $name, array $args)
337 global $langs, $conf,
$mysoc;
344 if (!is_object($this->
user) || empty($this->
user->id)) {
346 if (is_object($user) && !empty($user->id)) {
349 return [
"error" =>
"User not authenticated."];
358 $langs->loadLangs([
"main",
"bills",
"companies",
"orders",
"propal",
"products",
"supplier_orders",
"supplier_proposals",
"sendings",
"receptions"]);
362 case 'create_sales_order':
364 $args[
'object_type'] =
'order';
366 if (!isset($args[
'header']) && isset($args[
'socid'])) {
368 'socid' => $args[
'socid'],
369 'date_commande' => $args[
'date_commande'] ??
null,
370 'note' => $args[
'note'] ?? null
372 unset($args[
'socid'], $args[
'date_commande'], $args[
'note']);
374 return $this->createDocument($args);
376 case 'create_customer_invoice':
377 $args[
'object_type'] =
'invoice';
378 return $this->createDocument($args);
380 case 'create_other_document':
382 return $this->createDocument($args);
384 case 'add_line_item':
387 case 'delete_object':
391 return [
"error" =>
"Unknown tool: $name"];
394 return [
"error" =>
"Exception: " . $e->getMessage()];
409 private function createDocument(array $args)
411 $type = (
string) $args[
'object_type'];
414 if (! isset($this->map[$type])) {
415 return [
"error" =>
"Configuration not found for object type: " . $type];
420 if ($permError !==
null) {
425 $confMap = $this->map[$type];
432 foreach ($args[
'header'] as $k => $v) {
436 if ($key ===
'date' && isset($confMap[
'date_field'])) {
437 $key = $confMap[
'date_field'];
440 if ($key ===
'socid' && isset($confMap[
'soc_field'])) {
441 $key = $confMap[
'soc_field'];
446 if (strpos($key,
'date') !==
false && ! is_numeric($v) && is_string($v)) {
447 $timestamp = strtotime($v);
448 if ($timestamp !==
false) {
460 $dateField = $confMap[
'date_field'] ??
'date';
462 if (empty($obj->{$dateField})) {
464 $obj->{$dateField} =
dol_now();
468 if ($type ===
'proposal' && empty($obj->duree_validite)) {
469 $obj->duree_validite = 15;
473 $id = $obj->create($this->
user);
476 $err = (
string) $obj->error;
477 if (! empty($obj->errors)) {
478 $err .=
" " . json_encode($obj->errors);
480 return [
"error" =>
"Creation failed ($type): " . $err];
487 if (! empty($args[
'lines']) && is_array($args[
'lines'])) {
488 foreach ($args[
'lines'] as $line) {
489 $line[
'object_type'] = $type;
490 $line[
'parent_id'] =
$id;
494 $res = $this->processAddLine($obj, $line);
496 if (! empty($res[
'success'])) {
499 $lineErrors[] = isset($res[
'error']) ? (
string) $res[
'error'] :
'Unknown line error';
507 "ref" => (
string) $obj->ref,
508 "lines_added" => $linesAdded,
509 "line_errors" => $lineErrors,
510 "url" => DOL_URL_ROOT . $confMap[
'card'] .
"?id=" .
$id
537 return [
"success" =>
false,
"error" =>
"Document is not in draft status"];
541 if (empty(
$object->thirdparty)) {
546 $companyDefaultVAT = 0.0;
547 if (! empty(
$conf->global->MAIN_VAT_DEFAULT)) {
548 $companyDefaultVAT = (float)
$conf->global->MAIN_VAT_DEFAULT;
552 $productIdentifier = isset($args[
'product']) ? (
string) $args[
'product'] : (isset($args[
'description']) ? (
string) $args[
'description'] :
'');
554 $qtyInput = $args[
'qty'] ?? $args[
'quantity'] ?? 1;
555 $qty = (float) $qtyInput;
557 $priceInput = isset($args[
'price']) ? $args[
'price'] : ($args[
'unit_price'] ??
null);
558 $price = ($priceInput !==
null) ? (
float) $priceInput :
null;
560 $vat = isset($args[
'vat_rate']) ? (float) $args[
'vat_rate'] : null;
561 $discount = isset($args[
'discount']) ? (float) $args[
'discount'] : 0.0;
564 return [
"success" =>
false,
"error" =>
"Quantity must be positive."];
571 if ($productIdentifier !==
'') {
572 $findResult = $this->findProduct($productIdentifier);
573 if (is_array($findResult) && isset($findResult[
'error'])) {
581 if (isset($args[
'product'])) {
582 return array_merge([
'success' =>
false], $findResult);
586 if (is_object($findResult)) {
592 if ($price ===
null) {
597 $vat = (
$prod && isset(
$prod->tva_tx) &&
$prod->tva_tx !==
'') ? (
float)
$prod->tva_tx : $companyDefaultVAT;
601 $userDesc = isset($args[
'description']) ? (
string) $args[
'description'] :
'';
604 if ($userDesc !==
'') {
606 if (strtolower($userDesc) === strtolower(
$prod->label)) {
607 $desc =
$prod->label;
609 $desc =
$prod->label .
' - ' . $userDesc;
616 $desc =
$prod->label;
622 if (! empty(
$conf->global->PRODUCT_USE_UNITS) &&
$prod && ! empty(
$prod->fk_unit)) {
623 $fk_unit = (int)
$prod->fk_unit;
628 $docType = (
string) $args[
'object_type'];
632 if ($docType ===
'invoice') {
634 $res =
$object->addline($desc, $price, $qty, $vat, 0, 0, $fkProduct, $discount,
'',
'', 0, 0,
'',
'HT', 0, $prodType, -1, 0,
'', 0);
635 } elseif ($docType ===
'order') {
637 $res =
$object->addline($desc, $price, $qty, $vat, 0, 0, $fkProduct, $discount, 0, 0,
'HT', 0,
'',
'', $prodType);
638 } elseif ($docType ===
'proposal') {
640 $res =
$object->addline($desc, $price, $qty, $vat, 0, 0, $fkProduct, $discount,
'HT', 0, 0, $prodType);
641 } elseif ($docType ===
'supplier_invoice') {
650 $res =
$object->addline($desc, $price, $vat, 0, 0, $qty, $fkProduct, $discount,
'',
'', 0, 0,
'HT', $prodType);
651 } elseif ($docType ===
'supplier_order') {
653 $res =
$object->addline($desc, $price, $qty, $vat, 0, 0, $fkProduct, $discount, 0, 0,
'HT', 0,
'',
'', $prodType);
654 } elseif ($docType ===
'supplier_proposal') {
656 $res =
$object->addline($desc, $price, $qty, $vat, 0, 0, $fkProduct, $discount, 0, 0,
'HT', 0,
'',
'', $prodType);
657 } elseif ($docType ===
'shipment') {
660 return [
"success" =>
false,
"error" =>
"Shipment standalone mode required to add lines manually."];
664 $res =
$object->addlinefree($qty,
'shipping', $fkProduct, $fk_unit, 0, $desc, 0);
665 } elseif ($docType ===
'reception') {
668 return [
"success" =>
false,
"error" =>
"Reception standalone mode required to add lines manually."];
670 require_once DOL_DOCUMENT_ROOT .
'/reception/class/receptionlinebatch.class.php';
673 $res =
$object->addlinefree($qty,
'reception', $fkProduct, $fk_unit, 0, $desc, 0);
675 return [
"success" =>
false,
"error" =>
"Type $docType not supported for lines"];
680 $commercialDocs = [
'invoice',
'order',
'proposal',
'supplier_invoice',
'supplier_order',
'supplier_proposal'];
681 if (in_array($docType, $commercialDocs,
true) && $res > 0 && $fk_unit > 0 && ! empty(
$conf->global->PRODUCT_USE_UNITS)) {
682 $this->updateLineUnit($docType, $res, $fk_unit);
688 "line_id" => (int) $res,
690 "product_identifier" => $productIdentifier,
691 "final_description" => $desc
697 return [
"success" =>
false,
"error" => $errorMsg];
710 $type = (
string) $args[
'object_type'];
713 $permError = $this->checkPermission($type);
714 if ($permError !==
null) {
717 'error' => $permError[
'error'] ??
'Permission denied'
721 $parentId = (int) $args[
'parent_id'];
725 $obj = $this->instantiate($type);
727 return [
"success" =>
false,
"error" => $e->getMessage()];
730 if (! method_exists($obj,
'fetch')) {
731 return [
"success" =>
false,
"error" =>
"Object does not support fetching"];
734 $result = $obj->fetch($parentId);
736 return [
"success" =>
false,
"error" =>
"Parent document not found with ID: " . $parentId];
741 if (! empty($args[
'product_id'])) {
742 $args[
'product'] = (
string) $args[
'product_id'];
747 return $this->processAddLine($obj, $args);
759 $product =
new Product($this->db);
761 $searchString = trim((
string) $identifier);
765 if (preg_match(
'/^(?:id)[:\s]+(\d+)$/i', $searchString, $matches)) {
766 $searchString = $matches[1];
770 if (is_numeric($searchString)) {
771 if ($product->fetch((
int) $searchString) > 0) {
777 if ($product->fetch(0, $searchString) > 0) {
783 $sql =
"SELECT rowid FROM " . MAIN_DB_PREFIX .
"product";
784 $sql .=
" WHERE (barcode = '" . $this->db->escape($searchString) .
"' OR label = '" . $this->db->escape($searchString) .
"' OR ref = '" . $this->db->escape($searchString) .
"')";
785 $sql .=
" AND entity IN (" .
getEntity(
'product') .
")";
788 $res = $this->db->query($sql);
790 $numRows = $this->db->num_rows($res);
792 $row = $this->db->fetch_object($res);
794 $product->fetch((
int) $row->rowid);
795 $this->db->free($res);
799 $this->db->free($res);
803 $sql =
"SELECT rowid, ref, label FROM " . MAIN_DB_PREFIX .
"product";
804 $sql .=
" WHERE (ref LIKE '%" . $this->db->escape($searchString) .
"%' OR label LIKE '%" . $this->db->escape($searchString) .
"%')";
805 $sql .=
" AND entity IN (" .
getEntity(
'product') .
")";
806 $sql .=
" AND tosell = 1";
809 $res = $this->db->query($sql);
812 $numRows = $this->db->num_rows($res);
816 $row = $this->db->fetch_object($res);
818 $product->fetch((
int) $row->rowid);
819 $this->db->free($res);
826 while ($row = $this->db->fetch_object($res)) {
827 $matchList[] = $row->ref .
" - " . $row->label;
829 $this->db->free($res);
832 "error" =>
"Multiple products found for '" . $searchString .
"'",
833 "matches" => $matchList
836 $this->db->free($res);
839 return [
"error" =>
"Product '" . $searchString .
"' not found."];
851 $type = (
string) $args[
'object_type'];
852 $id = (int) $args[
'id'];
855 $permError = $this->checkPermission($type);
856 if ($permError !==
null) {
861 $obj = $this->instantiate($type);
864 if ($obj->fetch(
$id) <= 0) {
865 return [
"error" =>
"Object not found with ID: " .
$id];
870 $status = isset($obj->statut) ? (int) $obj->statut : -1;
873 return [
"error" =>
"Can only delete drafts (status 0). Current status: " . $status];
877 if ($obj->delete($this->user) > 0) {
878 return [
"success" =>
true];
882 $errorMsg = ! empty($obj->error) ? (
string) $obj->error :
'Unknown error';
884 return [
"error" =>
"Delete failed: " . $errorMsg];
896 if (! isset(self::PERM_MAP[$type])) {
897 return [
"error" =>
"Unknown type for permission check: " . $type];
899 [$module, $perm] = self::PERM_MAP[$type];
900 if (! $this->
user->hasRight($module, $perm)) {
901 return [
"error" =>
"Permission denied for action on " . $type];
916 if (! isset($this->map[$type])) {
917 throw new Exception(
"Unknown type: " . $type);
920 $config = $this->map[$type];
921 $path = (
string) $config[
'path'];
922 $className = (
string) $config[
'class'];
925 require_once DOL_DOCUMENT_ROOT .
'/core/class/commonobject.class.php';
926 require_once DOL_DOCUMENT_ROOT . $path;
928 if (! class_exists($className)) {
929 throw new Exception(
"Class '$className' not found for type '$type'");
932 return new $className($this->db);
944 private function updateLineUnit(
string $type,
int $lineId,
int $unitId): void
949 'invoice' =>
'facturedet',
950 'order' =>
'commandedet',
951 'proposal' =>
'propaldet',
952 'supplier_invoice' =>
'facture_fourn_det',
953 'supplier_order' =>
'commande_fournisseurdet',
954 'supplier_proposal' =>
'supplier_proposaldet'
957 if (! isset($tableMap[$type])) {
961 $table = $tableMap[$type];
963 $sql =
"UPDATE " . MAIN_DB_PREFIX . $this->db->sanitize($table);
964 $sql .=
" SET fk_unit = " . (int) $unitId;
965 $sql .=
" WHERE rowid = " . (int) $lineId;
967 $resql = $this->db->query($sql);
970 dol_syslog(
"Error updating unit for line $lineId: " . $this->db->lasterror(), LOG_ERR);