60 'path' =>
'/comm/propal/class/propal.class.php',
61 'card' =>
'/comm/propal/card.php',
62 'date_field' =>
'datep',
63 'soc_field' =>
'socid'
66 'class' =>
'Commande',
67 'path' =>
'/commande/class/commande.class.php',
68 'card' =>
'/commande/card.php',
69 'date_field' =>
'date_commande',
70 'soc_field' =>
'socid'
74 'path' =>
'/compta/facture/class/facture.class.php',
75 'card' =>
'/compta/facture/card.php',
76 'date_field' =>
'date',
77 'soc_field' =>
'socid'
80 'supplier_proposal' => [
81 'class' =>
'SupplierProposal',
82 'path' =>
'/supplier_proposal/class/supplier_proposal.class.php',
83 'card' =>
'/supplier_proposal/card.php',
84 'date_field' =>
'date',
85 'soc_field' =>
'socid'
88 'class' =>
'CommandeFournisseur',
89 'path' =>
'/fourn/class/fournisseur.commande.class.php',
90 'card' =>
'/fourn/commande/card.php',
91 'date_field' =>
'date_commande',
92 'soc_field' =>
'socid'
94 'supplier_invoice' => [
95 'class' =>
'FactureFournisseur',
96 'path' =>
'/fourn/class/fournisseur.facture.class.php',
97 'card' =>
'/fourn/facture/card.php',
98 'date_field' =>
'date',
99 'soc_field' =>
'socid'
103 'class' =>
'Expedition',
104 'path' =>
'/expedition/class/expedition.class.php',
105 'card' =>
'/expedition/card.php',
106 'date_field' =>
'date_expedition',
107 'soc_field' =>
'socid'
110 'class' =>
'Reception',
111 'path' =>
'/reception/class/reception.class.php',
112 'card' =>
'/reception/card.php',
113 'date_field' =>
'date_reception',
114 'soc_field' =>
'socid'
124 private const PERM_MAP = [
125 'proposal' => [
'propal',
'creer'],
126 'order' => [
'commande',
'creer'],
127 'invoice' => [
'facture',
'creer'],
128 'supplier_proposal' => [
'supplier_proposal',
'creer'],
129 'supplier_order' => [
'fournisseur',
'commande'],
130 'supplier_invoice' => [
'fournisseur',
'facture'],
131 'shipment' => [
'expedition',
'creer'],
132 'reception' => [
'reception',
'creer'],
145 "name" =>
"create_sales_order",
146 "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:
147- 'create order for customer X'
149- 'add order from customer Z'
150- 'add order for X with 5 items'
151If user says 'order' without any qualifier, they mean a SALES ORDER - use this tool.",
157 "description" =>
"Customer ID (Thirdparty ID) - REQUIRED"
161 "description" =>
"Order date (YYYY-MM-DD format, optional, defaults to today)"
165 "description" =>
"Order notes (optional)"
169 "description" =>
"Products being ordered by the customer",
173 "product_id" => [
"type" =>
"integer",
"default" => 0,
"description" =>
"Product ID (0 if not found)"],
174 "description" => [
"type" =>
"string",
"description" =>
"Product name or description"],
175 "quantity" => [
"type" =>
"number",
"default" => 1,
"description" =>
"Quantity ordered"],
176 "unit_price" => [
"type" =>
"number",
"description" =>
"Selling price per unit (optional)"],
177 "vat_rate" => [
"type" =>
"number",
"description" =>
"VAT rate (optional, auto-calculated if not provided)"]
179 "required" => [
"quantity"]
183 "required" => [
"socid"]
188 "name" =>
"create_customer_invoice",
189 "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'",
195 "enum" => [
"invoice"],
196 "default" =>
"invoice"
200 "description" =>
"Invoice header data. Must include 'socid' (Customer ID).",
202 "socid" => [
"type" =>
"integer",
"description" =>
"Customer ID (Thirdparty ID)"],
203 "date" => [
"type" =>
"string",
"description" =>
"Invoice date (YYYY-MM-DD)"],
204 "note_public" => [
"type" =>
"string",
"description" =>
"Public note"],
205 "note_private" => [
"type" =>
"string",
"description" =>
"Private note"]
207 "required" => [
"socid"]
211 "description" =>
"Invoice line items.",
215 "product_id" => [
"type" =>
"integer",
"default" => 0],
216 "description" => [
"type" =>
"string"],
217 "quantity" => [
"type" =>
"number",
"default" => 1],
218 "unit_price" => [
"type" =>
"number"],
219 "vat_rate" => [
"type" =>
"number"],
220 "fk_unit" => [
"type" =>
"integer"]
222 "required" => [
"quantity"]
226 "required" => [
"header"]
231 "name" =>
"create_other_document",
232 "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.",
238 "enum" => [
'proposal',
'supplier_order',
'supplier_invoice',
'supplier_proposal'],
239 "description" =>
"Document type. Cannot be 'order' or 'invoice'."
243 "description" =>
"Header data. Must include 'socid'.",
245 "socid" => [
"type" =>
"integer",
"description" =>
"Thirdparty ID (Customer for proposal, Supplier for supplier_*)"],
246 "date" => [
"type" =>
"string",
"description" =>
"Document date (YYYY-MM-DD)"],
247 "duree_validite" => [
"type" =>
"integer",
"description" =>
"Validity in days (proposal only)"],
248 "note_public" => [
"type" =>
"string",
"description" =>
"Public note"],
249 "note_private" => [
"type" =>
"string",
"description" =>
"Private note"]
251 "required" => [
"socid"]
255 "description" =>
"Array of line items.",
259 "product_id" => [
"type" =>
"integer",
"default" => 0],
260 "description" => [
"type" =>
"string"],
261 "quantity" => [
"type" =>
"number",
"default" => 1],
262 "unit_price" => [
"type" =>
"number"],
263 "vat_rate" => [
"type" =>
"number"],
264 "fk_unit" => [
"type" =>
"integer"]
266 "required" => [
"quantity"]
270 "required" => [
"object_type",
"header"]
274 "name" =>
"add_line_item",
275 "description" =>
"Add a single line to an existing draft document.",
279 "object_type" => [
"type" =>
"string",
"enum" => array_keys($this->map)],
280 "parent_id" => [
"type" =>
"integer"],
281 "product_id" => [
"type" =>
"integer",
"default" => 0],
282 "description" => [
"type" =>
"string"],
283 "quantity" => [
"type" =>
"number",
"default" => 1],
284 "unit_price" => [
"type" =>
"number"],
285 "vat_rate" => [
"type" =>
"number"]
287 "required" => [
"object_type",
"parent_id",
"quantity"]
291 "name" =>
"delete_object",
292 "description" =>
"Delete a Draft document.",
296 "object_type" => [
"type" =>
"string",
"enum" => array_keys($this->map)],
297 "id" => [
"type" =>
"integer"]
299 "required" => [
"object_type",
"id"]
313 return [
'commercial',
'billing',
'thirdparty'];
323 public function execute(
string $name, array $args)
325 global $user, $langs, $conf,
$mysoc;
331 return [
"error" =>
"User not authenticated."];
339 $langs->loadLangs([
"main",
"bills",
"companies",
"orders",
"propal",
"products",
"supplier_orders",
"supplier_proposals",
"sendings",
"receptions"]);
343 case 'create_sales_order':
345 $args[
'object_type'] =
'order';
347 if (!isset($args[
'header']) && isset($args[
'socid'])) {
349 'socid' => $args[
'socid'],
350 'date_commande' => $args[
'date_commande'] ??
null,
351 'note' => $args[
'note'] ?? null
353 unset($args[
'socid'], $args[
'date_commande'], $args[
'note']);
355 return $this->createDocument($args);
357 case 'create_customer_invoice':
358 $args[
'object_type'] =
'invoice';
359 return $this->createDocument($args);
361 case 'create_other_document':
363 return $this->createDocument($args);
365 case 'add_line_item':
368 case 'delete_object':
372 return [
"error" =>
"Unknown tool: $name"];
375 return [
"error" =>
"Exception: " . $e->getMessage()];
390 private function createDocument(array $args)
392 $type = (
string) $args[
'object_type'];
395 if (! isset($this->map[$type])) {
396 return [
"error" =>
"Configuration not found for object type: " . $type];
401 if ($permError !==
null) {
406 $confMap = $this->map[$type];
413 foreach ($args[
'header'] as $k => $v) {
417 if ($key ===
'date' && isset($confMap[
'date_field'])) {
418 $key = $confMap[
'date_field'];
421 if ($key ===
'socid' && isset($confMap[
'soc_field'])) {
422 $key = $confMap[
'soc_field'];
427 if (strpos($key,
'date') !==
false && ! is_numeric($v) && is_string($v)) {
428 $timestamp = strtotime($v);
429 if ($timestamp !==
false) {
441 $dateField = $confMap[
'date_field'] ??
'date';
443 if (empty($obj->{$dateField})) {
445 $obj->{$dateField} =
dol_now();
449 if ($type ===
'proposal' && empty($obj->duree_validite)) {
450 $obj->duree_validite = 15;
454 $id = $obj->create($this->
user);
457 $err = (
string) $obj->error;
458 if (! empty($obj->errors)) {
459 $err .=
" " . json_encode($obj->errors);
461 return [
"error" =>
"Creation failed ($type): " . $err];
468 if (! empty($args[
'lines']) && is_array($args[
'lines'])) {
469 foreach ($args[
'lines'] as $line) {
470 $line[
'object_type'] = $type;
471 $line[
'parent_id'] =
$id;
475 $res = $this->processAddLine($obj, $line);
477 if (! empty($res[
'success'])) {
480 $lineErrors[] = isset($res[
'error']) ? (
string) $res[
'error'] :
'Unknown line error';
488 "ref" => (
string) $obj->ref,
489 "lines_added" => $linesAdded,
490 "line_errors" => $lineErrors,
491 "url" => DOL_URL_ROOT . $confMap[
'card'] .
"?id=" .
$id
518 return [
"success" =>
false,
"error" =>
"Document is not in draft status"];
522 if (empty(
$object->thirdparty)) {
527 $companyDefaultVAT = 0.0;
528 if (! empty(
$conf->global->MAIN_VAT_DEFAULT)) {
529 $companyDefaultVAT = (float)
$conf->global->MAIN_VAT_DEFAULT;
533 $productIdentifier = isset($args[
'product']) ? (
string) $args[
'product'] : (isset($args[
'description']) ? (
string) $args[
'description'] :
'');
535 $qtyInput = $args[
'qty'] ?? $args[
'quantity'] ?? 1;
536 $qty = (float) $qtyInput;
538 $priceInput = isset($args[
'price']) ? $args[
'price'] : ($args[
'unit_price'] ??
null);
539 $price = ($priceInput !==
null) ? (
float) $priceInput :
null;
541 $vat = isset($args[
'vat_rate']) ? (float) $args[
'vat_rate'] : null;
542 $discount = isset($args[
'discount']) ? (float) $args[
'discount'] : 0.0;
545 return [
"success" =>
false,
"error" =>
"Quantity must be positive."];
552 if ($productIdentifier !==
'') {
553 $findResult = $this->findProduct($productIdentifier);
554 if (is_array($findResult) && isset($findResult[
'error'])) {
562 if (isset($args[
'product'])) {
563 return array_merge([
'success' =>
false], $findResult);
567 if (is_object($findResult)) {
573 if ($price ===
null) {
578 $vat = (
$prod && isset(
$prod->tva_tx) &&
$prod->tva_tx !==
'') ? (
float)
$prod->tva_tx : $companyDefaultVAT;
582 $userDesc = isset($args[
'description']) ? (
string) $args[
'description'] :
'';
585 if ($userDesc !==
'') {
587 if (strtolower($userDesc) === strtolower(
$prod->label)) {
588 $desc =
$prod->label;
590 $desc =
$prod->label .
' - ' . $userDesc;
597 $desc =
$prod->label;
603 if (! empty(
$conf->global->PRODUCT_USE_UNITS) &&
$prod && ! empty(
$prod->fk_unit)) {
604 $fk_unit = (int)
$prod->fk_unit;
609 $docType = (
string) $args[
'object_type'];
613 if ($docType ===
'invoice') {
615 $res =
$object->addline($desc, $price, $qty, $vat, 0, 0, $fkProduct, $discount,
'',
'', 0, 0,
'',
'HT', 0, $prodType, -1, 0,
'', 0);
616 } elseif ($docType ===
'order') {
618 $res =
$object->addline($desc, $price, $qty, $vat, 0, 0, $fkProduct, $discount, 0, 0,
'HT', 0,
'',
'', $prodType);
619 } elseif ($docType ===
'proposal') {
621 $res =
$object->addline($desc, $price, $qty, $vat, 0, 0, $fkProduct, $discount,
'HT', 0, 0, $prodType);
622 } elseif ($docType ===
'supplier_invoice') {
631 $res =
$object->addline($desc, $price, $vat, 0, 0, $qty, $fkProduct, $discount,
'',
'', 0, 0,
'HT', $prodType);
632 } elseif ($docType ===
'supplier_order') {
634 $res =
$object->addline($desc, $price, $qty, $vat, 0, 0, $fkProduct, $discount, 0, 0,
'HT', 0,
'',
'', $prodType);
635 } elseif ($docType ===
'supplier_proposal') {
637 $res =
$object->addline($desc, $price, $qty, $vat, 0, 0, $fkProduct, $discount, 0, 0,
'HT', 0,
'',
'', $prodType);
638 } elseif ($docType ===
'shipment') {
641 return [
"success" =>
false,
"error" =>
"Shipment standalone mode required to add lines manually."];
645 $res =
$object->addlinefree($qty,
'shipping', $fkProduct, $fk_unit, 0, $desc, 0);
646 } elseif ($docType ===
'reception') {
649 return [
"success" =>
false,
"error" =>
"Reception standalone mode required to add lines manually."];
651 require_once DOL_DOCUMENT_ROOT .
'/reception/class/receptionlinebatch.class.php';
654 $res =
$object->addlinefree($qty,
'reception', $fkProduct, $fk_unit, 0, $desc, 0);
656 return [
"success" =>
false,
"error" =>
"Type $docType not supported for lines"];
661 $commercialDocs = [
'invoice',
'order',
'proposal',
'supplier_invoice',
'supplier_order',
'supplier_proposal'];
662 if (in_array($docType, $commercialDocs,
true) && $res > 0 && $fk_unit > 0 && ! empty(
$conf->global->PRODUCT_USE_UNITS)) {
663 $this->updateLineUnit($docType, $res, $fk_unit);
669 "line_id" => (int) $res,
671 "product_identifier" => $productIdentifier,
672 "final_description" => $desc
678 return [
"success" =>
false,
"error" => $errorMsg];
691 $type = (
string) $args[
'object_type'];
694 $permError = $this->checkPermission($type);
695 if ($permError !==
null) {
698 'error' => $permError[
'error'] ??
'Permission denied'
702 $parentId = (int) $args[
'parent_id'];
706 $obj = $this->instantiate($type);
708 return [
"success" =>
false,
"error" => $e->getMessage()];
711 if (! method_exists($obj,
'fetch')) {
712 return [
"success" =>
false,
"error" =>
"Object does not support fetching"];
715 $result = $obj->fetch($parentId);
717 return [
"success" =>
false,
"error" =>
"Parent document not found with ID: " . $parentId];
722 if (! empty($args[
'product_id'])) {
723 $args[
'product'] = (
string) $args[
'product_id'];
728 return $this->processAddLine($obj, $args);
740 $product =
new Product($this->db);
742 $searchString = trim((
string) $identifier);
746 if (preg_match(
'/^(?:id)[:\s]+(\d+)$/i', $searchString, $matches)) {
747 $searchString = $matches[1];
751 if (is_numeric($searchString)) {
752 if ($product->fetch((
int) $searchString) > 0) {
758 if ($product->fetch(0, $searchString) > 0) {
764 $sql =
"SELECT rowid FROM " . MAIN_DB_PREFIX .
"product";
765 $sql .=
" WHERE (barcode = '" . $this->db->escape($searchString) .
"' OR label = '" . $this->db->escape($searchString) .
"' OR ref = '" . $this->db->escape($searchString) .
"')";
766 $sql .=
" AND entity IN (" .
getEntity(
'product') .
")";
769 $res = $this->db->query($sql);
771 $numRows = $this->db->num_rows($res);
773 $row = $this->db->fetch_object($res);
775 $product->fetch((
int) $row->rowid);
776 $this->db->free($res);
780 $this->db->free($res);
784 $sql =
"SELECT rowid, ref, label FROM " . MAIN_DB_PREFIX .
"product";
785 $sql .=
" WHERE (ref LIKE '%" . $this->db->escape($searchString) .
"%' OR label LIKE '%" . $this->db->escape($searchString) .
"%')";
786 $sql .=
" AND entity IN (" .
getEntity(
'product') .
")";
787 $sql .=
" AND tosell = 1";
790 $res = $this->db->query($sql);
793 $numRows = $this->db->num_rows($res);
797 $row = $this->db->fetch_object($res);
799 $product->fetch((
int) $row->rowid);
800 $this->db->free($res);
807 while ($row = $this->db->fetch_object($res)) {
808 $matchList[] = $row->ref .
" - " . $row->label;
810 $this->db->free($res);
813 "error" =>
"Multiple products found for '" . $searchString .
"'",
814 "matches" => $matchList
817 $this->db->free($res);
820 return [
"error" =>
"Product '" . $searchString .
"' not found."];
832 $type = (
string) $args[
'object_type'];
833 $id = (int) $args[
'id'];
836 $permError = $this->checkPermission($type);
837 if ($permError !==
null) {
842 $obj = $this->instantiate($type);
845 if ($obj->fetch(
$id) <= 0) {
846 return [
"error" =>
"Object not found with ID: " .
$id];
851 $status = isset($obj->statut) ? (int) $obj->statut : -1;
854 return [
"error" =>
"Can only delete drafts (status 0). Current status: " . $status];
858 if ($obj->delete($this->user) > 0) {
859 return [
"success" =>
true];
863 $errorMsg = ! empty($obj->error) ? (
string) $obj->error :
'Unknown error';
865 return [
"error" =>
"Delete failed: " . $errorMsg];
877 if (! isset(self::PERM_MAP[$type])) {
878 return [
"error" =>
"Unknown type for permission check: " . $type];
880 [$module, $perm] = self::PERM_MAP[$type];
881 if (! $this->
user->hasRight($module, $perm)) {
882 return [
"error" =>
"Permission denied for action on " . $type];
897 if (! isset($this->map[$type])) {
898 throw new Exception(
"Unknown type: " . $type);
901 $config = $this->map[$type];
902 $path = (
string) $config[
'path'];
903 $className = (
string) $config[
'class'];
906 require_once DOL_DOCUMENT_ROOT .
'/core/class/commonobject.class.php';
907 require_once DOL_DOCUMENT_ROOT . $path;
909 if (! class_exists($className)) {
910 throw new Exception(
"Class '$className' not found for type '$type'");
913 return new $className($this->db);
925 private function updateLineUnit(
string $type,
int $lineId,
int $unitId): void
930 'invoice' =>
'facturedet',
931 'order' =>
'commandedet',
932 'proposal' =>
'propaldet',
933 'supplier_invoice' =>
'facture_fourn_det',
934 'supplier_order' =>
'commande_fournisseurdet',
935 'supplier_proposal' =>
'supplier_proposaldet'
938 if (! isset($tableMap[$type])) {
942 $table = $tableMap[$type];
944 $sql =
"UPDATE " . MAIN_DB_PREFIX . $this->db->escape($table);
945 $sql .=
" SET fk_unit = " . (int) $unitId;
946 $sql .=
" WHERE rowid = " . (int) $lineId;
948 $resql = $this->db->query($sql);
951 dol_syslog(
"Error updating unit for line $lineId: " . $this->db->lasterror(), LOG_ERR);