73 'path' =>
'/comm/propal/class/propal.class.php',
74 'card' =>
'/comm/propal/card.php',
75 'date_field' =>
'datep',
76 'soc_field' =>
'socid'
79 'class' =>
'Commande',
80 'path' =>
'/commande/class/commande.class.php',
81 'card' =>
'/commande/card.php',
82 'date_field' =>
'date_commande',
83 'soc_field' =>
'socid'
87 'path' =>
'/compta/facture/class/facture.class.php',
88 'card' =>
'/compta/facture/card.php',
89 'date_field' =>
'date',
90 'soc_field' =>
'socid'
93 'supplier_proposal' => [
94 'class' =>
'SupplierProposal',
95 'path' =>
'/supplier_proposal/class/supplier_proposal.class.php',
96 'card' =>
'/supplier_proposal/card.php',
97 'date_field' =>
'date',
98 'soc_field' =>
'socid'
100 'supplier_order' => [
101 'class' =>
'CommandeFournisseur',
102 'path' =>
'/fourn/class/fournisseur.commande.class.php',
103 'card' =>
'/fourn/commande/card.php',
104 'date_field' =>
'date_commande',
105 'soc_field' =>
'socid'
107 'supplier_invoice' => [
108 'class' =>
'FactureFournisseur',
109 'path' =>
'/fourn/class/fournisseur.facture.class.php',
110 'card' =>
'/fourn/facture/card.php',
111 'date_field' =>
'date',
112 'soc_field' =>
'socid'
116 'class' =>
'Expedition',
117 'path' =>
'/expedition/class/expedition.class.php',
118 'card' =>
'/expedition/card.php',
119 'date_field' =>
'date_expedition',
120 'soc_field' =>
'socid'
123 'class' =>
'Reception',
124 'path' =>
'/reception/class/reception.class.php',
125 'card' =>
'/reception/card.php',
126 'date_field' =>
'date_reception',
127 'soc_field' =>
'socid'
143 private const PERM_MAP = [
144 'proposal' => [[
'propal',
'creer']],
145 'order' => [[
'commande',
'creer']],
146 'invoice' => [[
'facture',
'creer']],
147 'supplier_proposal' => [[
'supplier_proposal',
'creer']],
148 'supplier_order' => [[
'fournisseur',
'commande',
'creer'], [
'supplier_order',
'creer']],
149 'supplier_invoice' => [[
'fournisseur',
'facture',
'creer'], [
'supplier_invoice',
'creer']],
150 'shipment' => [[
'expedition',
'creer']],
151 'reception' => [[
'reception',
'creer']],
161 private const DELETE_PERM_MAP = [
162 'proposal' => [[
'propal',
'supprimer']],
163 'order' => [[
'commande',
'supprimer']],
164 'invoice' => [[
'facture',
'supprimer']],
165 'supplier_proposal' => [[
'supplier_proposal',
'supprimer']],
166 'supplier_order' => [[
'fournisseur',
'commande',
'supprimer'], [
'supplier_order',
'supprimer']],
167 'supplier_invoice' => [[
'fournisseur',
'facture',
'supprimer'], [
'supplier_invoice',
'supprimer']],
168 'shipment' => [[
'expedition',
'supprimer']],
169 'reception' => [[
'reception',
'supprimer']],
183 private const ACCESS_MAP = [
184 'proposal' => [
'feature' =>
'propal',
'tableandshare' =>
'propal'],
185 'order' => [
'feature' =>
'commande',
'tableandshare' =>
'commande'],
186 'invoice' => [
'feature' =>
'facture',
'tableandshare' =>
'facture'],
187 'supplier_proposal' => [
'feature' =>
'supplier_proposal',
'tableandshare' =>
'supplier_proposal'],
188 'supplier_order' => [
'feature' =>
'fournisseur',
'tableandshare' =>
'commande_fournisseur'],
189 'supplier_invoice' => [
'feature' =>
'fournisseur',
'tableandshare' =>
'facture_fourn'],
190 'shipment' => [
'feature' =>
'expedition',
'tableandshare' =>
'expedition'],
191 'reception' => [
'feature' =>
'reception',
'tableandshare' =>
'reception'],
209 private const ALLOWED_HEADER_FIELDS = [
216 'create_missing_products',
234 "name" =>
"create_sales_order",
235 "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:
236- 'create order for customer X'
238- 'add order from customer Z'
239- 'add order for X with 5 items'
240If user says 'order' without any qualifier, they mean a SALES ORDER - use this tool.",
246 "description" =>
"Customer ID (Thirdparty ID) - REQUIRED"
250 "description" =>
"Order date (YYYY-MM-DD format, optional, defaults to today)"
254 "description" =>
"Order notes (optional)"
258 "description" =>
"Products being ordered by the customer",
262 "product_id" => [
"type" =>
"integer",
"default" => 0,
"description" =>
"Product ID (0 if not found)"],
263 "product_ref" => [
"type" =>
"string",
"description" =>
"Product reference/SKU as written on the source document (preferred when the numeric product_id is unknown; matched against catalog refs, barcodes and labels)"],
264 "barcode" => [
"type" =>
"string",
"description" =>
"Product barcode (EAN13/UPC) as written on the source document; used to enrich a product created on the fly"],
265 "description" => [
"type" =>
"string",
"description" =>
"Product name or description"],
266 "quantity" => [
"type" =>
"number",
"default" => 1,
"description" =>
"Quantity ordered"],
267 "unit_price" => [
"type" =>
"number",
"description" =>
"Unit selling price. OMIT this field to take the price from the product catalog; send 0 only for a deliberately free line."],
268 "vat_rate" => [
"type" =>
"number",
"description" =>
"VAT rate (optional, auto-calculated if not provided)"]
270 "required" => [
"quantity"]
274 "required" => [
"socid"]
279 "name" =>
"create_customer_invoice",
280 "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'",
286 "enum" => [
"invoice"],
287 "default" =>
"invoice"
291 "description" =>
"Invoice header data. Must include 'socid' (Customer ID).",
293 "socid" => [
"type" =>
"integer",
"description" =>
"Customer ID (Thirdparty ID)"],
294 "date" => [
"type" =>
"string",
"description" =>
"Invoice date (YYYY-MM-DD)"],
295 "note_public" => [
"type" =>
"string",
"description" =>
"Public note"],
296 "note_private" => [
"type" =>
"string",
"description" =>
"Private note"]
298 "required" => [
"socid"]
302 "description" =>
"Invoice line items.",
306 "product_id" => [
"type" =>
"integer",
"default" => 0],
307 "product_ref" => [
"type" =>
"string",
"description" =>
"Product reference/SKU as written on the source document (preferred when the numeric product_id is unknown; matched against catalog refs, barcodes and labels)"],
308 "barcode" => [
"type" =>
"string",
"description" =>
"Product barcode (EAN13/UPC) as written on the source document; used to enrich a product created on the fly"],
309 "description" => [
"type" =>
"string"],
310 "quantity" => [
"type" =>
"number",
"default" => 1],
311 "unit_price" => [
"type" =>
"number",
"description" =>
"Unit selling price. OMIT this field to take the price from the product catalog; send 0 only for a deliberately free line."],
312 "vat_rate" => [
"type" =>
"number"],
313 "fk_unit" => [
"type" =>
"integer"]
315 "required" => [
"quantity"]
319 "required" => [
"header"]
324 "name" =>
"create_other_document",
325 "description" =>
"Create documents other than orders and invoices. Use this for: 'proposal', 'supplier_order', 'supplier_invoice', 'supplier_proposal', 'reception', 'shipment'. DO NOT use for 'order' or 'invoice' - they have dedicated tools.",
331 "enum" => [
'proposal',
'supplier_order',
'supplier_invoice',
'supplier_proposal',
'reception',
'shipment'],
332 "description" =>
"Document type. Cannot be 'order' or 'invoice'. 'reception' and 'shipment' create a standalone document (with no source order) and require the RECEPTION_STANDALONE / SHIPMENT_STANDALONE option to be enabled."
336 "description" =>
"Header data. Must include 'socid'.",
338 "socid" => [
"type" =>
"integer",
"description" =>
"Thirdparty ID (Customer for proposal, Supplier for supplier_*)"],
339 "date" => [
"type" =>
"string",
"description" =>
"Document date (YYYY-MM-DD)"],
340 "duree_validite" => [
"type" =>
"integer",
"description" =>
"Validity in days (proposal only)"],
341 "ref_supplier" => [
"type" =>
"string",
"description" =>
"Supplier's own document reference (delivery note number, supplier order number...) as written on the source document"],
342 "create_missing_products" => [
"type" =>
"boolean",
"default" =>
false,
"description" =>
"When a line's product_ref does not match any catalog product, create the product on the fly (ref=product_ref, label=description, buying price=unit_price) instead of adding a free-text line"],
343 "products_category" => [
"type" =>
"string",
"description" =>
"Optional product category/tag label assigned to every product created on the fly (the category is created when missing), e.g. 'A TRAITER' so the new products can be reviewed as a batch. Only used with create_missing_products."],
344 "note_public" => [
"type" =>
"string",
"description" =>
"Public note"],
345 "note_private" => [
"type" =>
"string",
"description" =>
"Private note"]
347 "required" => [
"socid"]
351 "description" =>
"Array of line items.",
355 "product_id" => [
"type" =>
"integer",
"default" => 0],
356 "product_ref" => [
"type" =>
"string",
"description" =>
"Product reference/SKU as written on the source document (preferred when the numeric product_id is unknown; matched against catalog refs, barcodes and labels)"],
357 "barcode" => [
"type" =>
"string",
"description" =>
"Product barcode (EAN13/UPC) as written on the source document; used to enrich a product created on the fly"],
358 "description" => [
"type" =>
"string"],
359 "quantity" => [
"type" =>
"number",
"default" => 1],
360 "unit_price" => [
"type" =>
"number",
"description" =>
"Unit selling price. OMIT this field to take the price from the product catalog; send 0 only for a deliberately free line."],
361 "vat_rate" => [
"type" =>
"number"],
362 "fk_unit" => [
"type" =>
"integer"]
364 "required" => [
"quantity"]
368 "required" => [
"object_type",
"header"]
372 "name" =>
"add_line_item",
373 "description" =>
"Add a single line to an existing draft document.",
377 "object_type" => [
"type" =>
"string",
"enum" => array_keys($this->map)],
378 "parent_id" => [
"type" =>
"integer"],
379 "product_id" => [
"type" =>
"integer",
"default" => 0],
380 "product_ref" => [
"type" =>
"string",
"description" =>
"Product reference/SKU as written on the source document (preferred when the numeric product_id is unknown; matched against catalog refs, barcodes and labels)"],
381 "barcode" => [
"type" =>
"string",
"description" =>
"Product barcode (EAN13/UPC) as written on the source document; used to enrich a product created on the fly"],
382 "description" => [
"type" =>
"string"],
383 "quantity" => [
"type" =>
"number",
"default" => 1],
384 "unit_price" => [
"type" =>
"number",
"description" =>
"Unit selling price. OMIT this field to take the price from the product catalog; send 0 only for a deliberately free line."],
385 "vat_rate" => [
"type" =>
"number"]
387 "required" => [
"object_type",
"parent_id",
"quantity"]
391 "name" =>
"delete_object",
392 "description" =>
"Delete a Draft document.",
396 "object_type" => [
"type" =>
"string",
"enum" => array_keys($this->map)],
397 "id" => [
"type" =>
"integer"]
399 "required" => [
"object_type",
"id"]
414 return self::RIGHTS_ENFORCED_DOWNSTREAM;
428 $langs->load(
"other");
430 $header = isset($args[
'header']) && is_array($args[
'header']) ? $args[
'header'] : array();
431 $lines = isset($args[
'lines']) && is_array($args[
'lines']) ? $args[
'lines'] : array();
432 $socid = (int) ($header[
'socid'] ?? ($args[
'socid'] ?? 0));
436 require_once DOL_DOCUMENT_ROOT.
'/societe/class/societe.class.php';
438 if ($soc->fetch($socid) > 0) {
439 $who =
' '.$langs->trans(
"AIPreviewForThirdparty", $soc->name);
452 foreach ($lines as $line) {
453 if (!isset($line[
'unit_price']) || $line[
'unit_price'] ===
'') {
458 $total += ((float) ($line[
'quantity'] ?? 1)) * ((
float) $line[
'unit_price']);
461 $what = $langs->trans(count($lines) === 1 ?
"AIPreviewLine" :
"AIPreviewLines", (string) count($lines));
463 $what .=
', '.price($total);
465 $what = $langs->trans(
"AIPreviewPlusCatalogue", $what);
467 } elseif ($derived) {
468 $what .=
', '.$langs->trans(
"AIPreviewFromCatalogue");
472 case 'create_customer_invoice':
473 return $langs->trans(
"AIPreviewCreateInvoice", $who, $what);
474 case 'create_sales_order':
475 return $langs->trans(
"AIPreviewCreateOrder", $who, $what);
476 case 'create_other_document':
477 return $langs->trans(
"AIPreviewCreateDocument", (
string) ($args[
'object_type'] ??
'document'), $who, $what);
478 case 'add_line_item':
479 return $langs->trans(
"AIPreviewAddLine", (
string) ($args[
'object_type'] ??
'document'), (
string) ((
int) ($args[
'parent_id'] ?? 0)));
480 case 'delete_object':
481 return $langs->trans(
"AIPreviewDelete", (
string) ($args[
'object_type'] ??
'record'), (
string) ((
int) ($args[
'id'] ?? 0)));
495 return [
'commercial',
'billing',
'thirdparty'];
505 public function execute(
string $name, array $args)
507 global $langs, $conf,
$mysoc;
514 if (!is_object($this->
user) || empty($this->
user->id)) {
516 if (is_object($user) && !empty($user->id)) {
519 return [
"error" =>
"User not authenticated."];
528 $langs->loadLangs([
"main",
"bills",
"companies",
"orders",
"propal",
"products",
"supplier_orders",
"supplier_proposals",
"sendings",
"receptions"]);
532 case 'create_sales_order':
534 $args[
'object_type'] =
'order';
536 if (!isset($args[
'header']) && isset($args[
'socid'])) {
538 'socid' => $args[
'socid'],
539 'date_commande' => $args[
'date_commande'] ??
null,
540 'note' => $args[
'note'] ?? null
542 unset($args[
'socid'], $args[
'date_commande'], $args[
'note']);
544 return $this->createDocument($args);
546 case 'create_customer_invoice':
547 $args[
'object_type'] =
'invoice';
548 return $this->createDocument($args);
550 case 'create_other_document':
552 return $this->createDocument($args);
554 case 'add_line_item':
557 case 'delete_object':
561 return [
"error" =>
"Unknown tool: $name"];
564 return [
"error" =>
"Exception: " . $e->getMessage()];
579 private function createDocument(array $args)
585 if (empty($args[
'header']) && !empty($args[
'socid'])) {
586 $args[
'header'] = [
'socid' => (int) $args[
'socid']];
587 unset($args[
'socid']);
593 if (empty($args[
'header'][
'socid'])) {
595 foreach (array(
'customer',
'customer_name',
'thirdparty',
'company',
'name') as $ck) {
596 if (!empty($args[
'header'][$ck]) && is_string($args[
'header'][$ck])) {
597 $custName = trim($args[
'header'][$ck]);
600 if (!empty($args[$ck]) && is_string($args[$ck])) {
601 $custName = trim($args[$ck]);
605 if ($custName !==
'' && !is_numeric($custName)) {
606 if (empty($args[
'header']) || !is_array($args[
'header'])) {
607 $args[
'header'] = array();
609 $sqlc =
"SELECT rowid, nom FROM ".MAIN_DB_PREFIX.
"societe WHERE entity IN (".
getEntity(
'societe').
") AND nom = '".$this->db->escape($custName).
"'";
610 $resc = $this->db->query($sqlc);
611 if ($resc && $this->db->num_rows($resc) == 1) {
612 $args[
'header'][
'socid'] = (int) $this->db->fetch_object($resc)->rowid;
614 $sqlc =
"SELECT rowid, nom FROM ".MAIN_DB_PREFIX.
"societe WHERE entity IN (".
getEntity(
'societe').
") AND nom LIKE '%".$this->db->escape($custName).
"%' LIMIT 6";
615 $resc = $this->db->query($sqlc);
617 while ($resc && ($oc = $this->db->fetch_object($resc))) {
618 $found[$oc->rowid] = $oc->nom;
620 if (count($found) == 1) {
621 $args[
'header'][
'socid'] = (int) array_key_first($found);
622 } elseif (count($found) > 1) {
623 $candidatesTxt = array();
624 foreach ($found as $fid => $fname) {
625 $candidatesTxt[] = $fname.
" (id ".((int) $fid).
")";
628 return [
"error" =>
"Several thirdparties match '".$custName.
"': ".implode(
', ', $candidatesTxt).
". Ask the user which one, then retry with that socid."];
633 if (!empty($args[
'lines']) && is_array($args[
'lines'])) {
634 $aliases = [
'ref' =>
'product_ref',
'product' =>
'product_ref',
'sku' =>
'product_ref',
'qty' =>
'quantity',
'price' =>
'unit_price',
'unitprice' =>
'unit_price',
'price_ht' =>
'unit_price',
'vat' =>
'vat_rate'];
635 foreach ($args[
'lines'] as $k => $line) {
636 if (!is_array($line)) {
639 foreach ($aliases as $from => $to) {
640 if (isset($line[$from]) && !isset($line[$to])) {
641 $args[
'lines'][$k][$to] = $line[$from];
642 unset($args[
'lines'][$k][$from]);
648 if (isset($line[
'label'])) {
649 $to = (empty($line[
'product_ref']) && empty($line[
'product_id'])) ?
'product_ref' :
'description';
650 if (!isset($args[
'lines'][$k][$to])) {
651 $args[
'lines'][$k][$to] = $line[
'label'];
653 unset($args[
'lines'][$k][
'label']);
660 $type = (string) $args[
'object_type'];
663 if (! isset($this->map[$type])) {
664 return [
"error" =>
"Configuration not found for object type: " . $type];
669 if ($permError !==
null) {
676 return [
"error" =>
"Reception standalone mode (RECEPTION_STANDALONE) must be enabled to create a reception without a supplier order."];
679 return [
"error" =>
"Shipment standalone mode (SHIPMENT_STANDALONE) must be enabled to create a shipment without an order."];
683 $confMap = $this->map[$type];
685 if (empty($args[
'header']) || ! is_array($args[
'header'])) {
686 return [
"error" =>
"Missing or invalid header for object type: " . $type];
692 $header = array_intersect_key($args[
'header'], array_flip(self::ALLOWED_HEADER_FIELDS));
694 $rejectedfields = array_diff(array_keys($args[
'header']), array_keys($header));
695 if (! empty($rejectedfields)) {
697 '[ToolCrudObjects] Ignored non allowed header fields for ' . $type .
': '
698 . implode(
', ', array_map(
'strval', $rejectedfields)),
704 if (empty($header[
'socid'])) {
705 return [
"error" =>
"Missing socid in header for object type: " . $type];
709 if ($this->
user->socid > 0 && $this->user->socid != (
int) $header[
'socid']) {
711 '[ToolCrudObjects] User id=' . $this->
user->id .
' (socid=' . $this->user->socid
712 .
') tried to create a ' . $type .
' for socid=' . ((
int) $header[
'socid']) .
'.',
715 return [
"error" =>
"Access denied to this thirdparty."];
721 require_once DOL_DOCUMENT_ROOT.
'/societe/class/societe.class.php';
722 $ctrlSoc =
new Societe($this->db);
723 if ($ctrlSoc->fetch((
int) $header[
'socid']) <= 0) {
724 return [
"error" =>
"Thirdparty with id ".((int) $header[
'socid']).
" does not exist. Use find/search to resolve the thirdparty first."];
732 $createMissingProducts = ! empty($header[
'create_missing_products']);
733 $productsCategory = trim((
string) ($header[
'products_category'] ??
''));
734 foreach ($header as $k => $v) {
738 if ($key ===
'create_missing_products' || $key ===
'products_category') {
743 if ($key ===
'date' && isset($confMap[
'date_field'])) {
744 $key = $confMap[
'date_field'];
747 if ($key ===
'socid' && isset($confMap[
'soc_field'])) {
748 $key = $confMap[
'soc_field'];
749 $obj->fk_soc = (int) $v;
753 if (strpos($key,
'date') !==
false && ! is_numeric($v) && is_string($v)) {
754 $timestamp = strtotime($v);
755 if ($timestamp !==
false) {
767 $dateField = $confMap[
'date_field'] ??
'date';
769 if (empty($obj->{$dateField})) {
771 $obj->{$dateField} =
dol_now();
775 if ($type ===
'proposal' && empty($obj->duree_validite)) {
776 $obj->duree_validite = 15;
784 $obj->entity =
$conf->entity;
787 $id = $obj->create($this->
user);
790 $err = (string) $obj->error;
791 if (! empty($obj->errors)) {
792 $err .=
" " . json_encode($obj->errors);
794 return [
"error" =>
"Creation failed ($type): " . $err];
801 if (! empty($args[
'lines']) && is_array($args[
'lines'])) {
802 foreach ($args[
'lines'] as $line) {
803 $line[
'object_type'] = $type;
804 $line[
'parent_id'] =
$id;
805 if ($createMissingProducts) {
806 $line[
'create_missing_products'] =
true;
807 if ($productsCategory !==
'') {
808 $line[
'products_category'] = $productsCategory;
814 $res = $this->processAddLine($obj, $line);
816 if (! empty($res[
'success'])) {
819 $lineErrors[] = isset($res[
'error']) ? (string) $res[
'error'] :
'Unknown line error';
828 if (empty($obj->ref) && method_exists($obj,
'fetch')) {
835 "ref" => (
string) $obj->ref,
836 "lines_added" => $linesAdded,
837 "line_errors" => $lineErrors,
838 "url" => DOL_URL_ROOT . $confMap[
'card'] .
"?id=" .
$id
865 return [
"success" =>
false,
"error" =>
"Document is not in draft status"];
869 if (empty(
$object->thirdparty)) {
874 $companyDefaultVAT = 0.0;
883 if (!empty($args[
'product_id'])) {
884 $productIdentifier = (string) ((
int) $args[
'product_id']);
885 } elseif (isset($args[
'product'])) {
886 $productIdentifier = (string) $args[
'product'];
887 } elseif (isset($args[
'product_ref'])) {
888 $productIdentifier = (string) $args[
'product_ref'];
889 } elseif (isset($args[
'description'])) {
890 $productIdentifier = (string) $args[
'description'];
892 $productIdentifier =
'';
895 $qtyInput = $args[
'qty'] ?? $args[
'quantity'] ?? 1;
896 $qty = (float) $qtyInput;
898 $priceInput = isset($args[
'price']) ? $args[
'price'] : ($args[
'unit_price'] ??
null);
899 $price = ($priceInput !==
null) ? (
float) $priceInput :
null;
901 $vat = isset($args[
'vat_rate']) ? (float) $args[
'vat_rate'] : null;
902 $discount = isset($args[
'discount']) ? (float) $args[
'discount'] : 0.0;
905 return [
"success" =>
false,
"error" =>
"Quantity must be positive."];
912 if ($productIdentifier !==
'') {
913 $findResult = $this->findProduct($productIdentifier);
914 if (is_array($findResult) && isset($findResult[
'error'])) {
922 if (isset($args[
'product'])) {
923 return array_merge([
'success' =>
false], $findResult);
927 if (is_object($findResult)) {
936 if (
$prod ===
null && ! empty($args[
'create_missing_products']) && ! empty($args[
'product_ref'])
937 && $this->
user && $this->
user->hasRight(
'produit',
'creer')) {
938 $newprod =
new Product($this->db);
939 $newprod->ref = trim((
string) $args[
'product_ref']);
940 $newprod->label = ! empty($args[
'description']) ? (string) $args[
'description'] : $newprod->
ref;
942 $newprod->status = 1;
943 $newprod->status_buy = 1;
946 $newprod->import_key =
'AI'.dol_print_date(
dol_now(),
'%y%m%d');
947 if (!empty($args[
'barcode'])) {
948 $newprod->barcode = trim((
string) $args[
'barcode']);
952 if ($defbctype > 0) {
953 $newprod->barcode_type = $defbctype;
956 if ($price !==
null) {
957 $newprod->cost_price = (float) $price;
959 if ($newprod->create($this->user) > 0) {
962 $this->db->query(
"UPDATE ".MAIN_DB_PREFIX.
"product SET import_key='".$this->db->escape($newprod->import_key).
"' WHERE rowid=".(int) $newprod->id);
967 $this->completeCreatedProduct($newprod,
$object, $args, ($price !==
null ? (
float) $price : null), ($vat !== null ? (float) $vat : 0.0));
973 if ($price ===
null) {
978 $vat = (
$prod && isset(
$prod->tva_tx) &&
$prod->tva_tx !==
'') ? (
float)
$prod->tva_tx : $companyDefaultVAT;
985 $userDesc = isset($args[
'description']) ? trim(preg_replace(
'/\s+/',
' ', (
string) $args[
'description'])) :
'';
988 if ($userDesc !==
'') {
990 if (strtolower($userDesc) === strtolower(
$prod->label)) {
991 $desc =
$prod->label;
993 $desc =
$prod->label .
' - ' . $userDesc;
1000 $desc =
$prod->label;
1007 $fk_unit = (int)
$prod->fk_unit;
1012 $docType = (string) $args[
'object_type'];
1016 if ($docType ===
'invoice') {
1018 $res =
$object->addline($desc, $price, $qty, $vat, 0, 0, $fkProduct, $discount,
'',
'', 0, 0,
'',
'HT', 0, $prodType, -1, 0,
'', 0);
1019 } elseif ($docType ===
'order') {
1021 $res =
$object->addline($desc, $price, $qty, $vat, 0, 0, $fkProduct, $discount, 0, 0,
'HT', 0,
'',
'', $prodType);
1022 } elseif ($docType ===
'proposal') {
1024 $res =
$object->addline($desc, $price, $qty, $vat, 0, 0, $fkProduct, $discount,
'HT', 0, 0, $prodType);
1025 } elseif ($docType ===
'supplier_invoice') {
1034 $res =
$object->addline($desc, $price, $vat, 0, 0, $qty, $fkProduct, $discount,
'',
'', 0, 0,
'HT', $prodType);
1035 } elseif ($docType ===
'supplier_order') {
1043 $res =
$object->addline($desc, $price, $qty, $vat, 0, 0, $fkProduct, 0,
'', $discount,
'HT', 0, $prodType);
1044 } elseif ($docType ===
'supplier_proposal') {
1051 $res =
$object->addline($desc, $price, $qty, $vat, 0, 0, $fkProduct, $discount,
'HT', 0, 0, $prodType);
1052 } elseif ($docType ===
'shipment') {
1055 return [
"success" =>
false,
"error" =>
"Shipment standalone mode required to add lines manually."];
1059 $res =
$object->addlinefree($qty,
'shipping', $fkProduct, $fk_unit, 0, $desc, 0);
1060 } elseif ($docType ===
'reception') {
1063 return [
"success" =>
false,
"error" =>
"Reception standalone mode required to add lines manually."];
1065 require_once DOL_DOCUMENT_ROOT .
'/reception/class/receptionlinebatch.class.php';
1073 $recWarehouse = (int) ($args[
'warehouse_id'] ?? 0);
1074 if ($recWarehouse <= 0) {
1082 if ($recWarehouse <= 0) {
1083 $resqlw = $this->db->query(
"SELECT rowid FROM " . MAIN_DB_PREFIX .
"entrepot WHERE entity IN (" .
getEntity(
'stock') .
") AND statut = 1 ORDER BY rowid ASC");
1084 if ($resqlw && ($objw = $this->db->fetch_object($resqlw))) {
1085 $recWarehouse = (int) $objw->rowid;
1090 $res =
$object->addlinefree($qty,
'reception', $fkProduct, $fk_unit, 0, $desc, [], (
float) $price, (
string) ($args[
'product_ref'] ??
''), $recWarehouse);
1092 return [
"success" =>
false,
"error" =>
"Type $docType not supported for lines"];
1097 $commercialDocs = [
'invoice',
'order',
'proposal',
'supplier_invoice',
'supplier_order',
'supplier_proposal'];
1098 if (in_array($docType, $commercialDocs,
true) && $res > 0 && $fk_unit > 0 &&
getDolGlobalInt(
'PRODUCT_USE_UNITS')) {
1099 $this->updateLineUnit($docType, $res, $fk_unit);
1105 "line_id" => (int) $res,
1107 "product_identifier" => $productIdentifier,
1108 "final_description" => $desc
1113 $errorMsg = isset(
$object->error) ? (string)
$object->error :
'Unknown error adding line';
1114 return [
"success" =>
false,
"error" => $errorMsg];
1127 $type = (string) $args[
'object_type'];
1130 $permError = $this->checkPermission($type);
1131 if ($permError !==
null) {
1134 'error' => $permError[
'error'] ??
'Permission denied'
1138 $parentId = (int) $args[
'parent_id'];
1142 $obj = $this->instantiate($type);
1144 return [
"success" =>
false,
"error" => $e->getMessage()];
1147 if (! method_exists($obj,
'fetch')) {
1148 return [
"success" =>
false,
"error" =>
"Object does not support fetching"];
1151 $result = $obj->fetch($parentId);
1153 return [
"success" =>
false,
"error" =>
"Parent document not found with ID: " . $parentId];
1158 $accessError = $this->checkAccessToObject($type, $obj);
1159 if ($accessError !==
null) {
1160 return [
"success" =>
false,
"error" => $accessError[
'error']];
1165 if (! empty($args[
'product_id'])) {
1166 $args[
'product'] = (string) $args[
'product_id'];
1171 return $this->processAddLine($obj, $args);
1199 $supplierSideTypes = array(
'reception',
'supplier_order',
'supplier_invoice',
'supplier_proposal');
1202 if ($price !==
null && $price > 0 && $socid > 0 && in_array((
string) $args[
'object_type'], $supplierSideTypes,
true)) {
1203 require_once DOL_DOCUMENT_ROOT.
'/fourn/class/fournisseur.product.class.php';
1205 $supplier =
new Societe($this->db);
1206 if ($pf->fetch($newprod->id) > 0 && $supplier->fetch($socid) > 0) {
1207 $reffourn = trim((
string) ($args[
'product_ref'] ??
''));
1212 $supplierbarcode = trim((
string) ($args[
'barcode'] ??
''));
1213 $supplierbarcodetype = ($supplierbarcode !==
'') ?
getDolGlobalInt(
'PRODUIT_DEFAULT_BARCODE_TYPE') : 0;
1217 if ($pf->update_buyprice(1, (
float) $price, $this->user,
'HT', $supplier, 0, $reffourn, (
float) $vat, 0, 0, 0, 0, 0,
'', array(),
'', (
float) $price,
'HT', 1, (
string)
$conf->currency,
'', $supplierbarcode, $supplierbarcodetype) < 0) {
1218 dol_syslog(
'[ToolCrudObjects] update_buyprice failed for new product '.$newprod->id.
': '.$pf->error, LOG_WARNING);
1224 $catlabel = trim((
string) ($args[
'products_category'] ??
''));
1226 require_once DOL_DOCUMENT_ROOT.
'/categories/class/categorie.class.php';
1228 if ($cat->fetch(0, $catlabel, Categorie::TYPE_PRODUCT) <= 0) {
1229 if (! $this->
user->hasRight(
'categorie',
'creer')) {
1230 dol_syslog(
'[ToolCrudObjects] category "'.$catlabel.
'" not found and user lacks categorie->creer', LOG_WARNING);
1234 $cat->label = $catlabel;
1235 $cat->type = Categorie::TYPE_PRODUCT;
1236 if ($cat->create($this->user) <= 0) {
1237 dol_syslog(
'[ToolCrudObjects] category creation failed ('.$catlabel.
'): '.$cat->error, LOG_WARNING);
1241 if ($cat->add_type($newprod, Categorie::TYPE_PRODUCT) < 0 && $cat->error !=
'DB_ERROR_RECORD_ALREADY_EXISTS') {
1242 dol_syslog(
'[ToolCrudObjects] category assignment failed for product '.$newprod->id.
': '.$cat->error, LOG_WARNING);
1256 $product =
new Product($this->db);
1258 $searchString = trim((
string) $identifier);
1262 if (preg_match(
'/^(?:id)[:\s]+(\d+)$/i', $searchString, $matches)) {
1263 $searchString = $matches[1];
1267 if (is_numeric($searchString)) {
1268 if ($product->fetch((
int) $searchString) > 0) {
1274 if ($product->fetch(0, $searchString) > 0) {
1280 $sql =
"SELECT rowid FROM " . MAIN_DB_PREFIX .
"product";
1281 $sql .=
" WHERE (barcode = '" . $this->db->escape($searchString) .
"' OR label = '" . $this->db->escape($searchString) .
"' OR ref = '" . $this->db->escape($searchString) .
"')";
1282 $sql .=
" AND entity IN (" .
getEntity(
'product') .
")";
1285 $res = $this->db->query($sql);
1287 $numRows = $this->db->num_rows($res);
1289 $row = $this->db->fetch_object($res);
1291 $product->fetch((
int) $row->rowid);
1292 $this->db->free($res);
1296 $this->db->free($res);
1300 $sql =
"SELECT rowid, ref, label FROM " . MAIN_DB_PREFIX .
"product";
1301 $sql .=
" WHERE (ref LIKE '%" . $this->db->escape($searchString) .
"%' OR label LIKE '%" . $this->db->escape($searchString) .
"%')";
1302 $sql .=
" AND entity IN (" .
getEntity(
'product') .
")";
1303 $sql .=
" AND tosell = 1";
1306 $res = $this->db->query($sql);
1309 $numRows = $this->db->num_rows($res);
1312 if ($numRows == 1) {
1313 $row = $this->db->fetch_object($res);
1315 $product->fetch((
int) $row->rowid);
1316 $this->db->free($res);
1323 while ($row = $this->db->fetch_object($res)) {
1324 $matchList[] = $row->ref .
" - " . $row->label;
1326 $this->db->free($res);
1329 "error" =>
"Multiple products found for '" . $searchString .
"'",
1330 "matches" => $matchList
1333 $this->db->free($res);
1336 return [
"error" =>
"Product '" . $searchString .
"' not found."];
1348 $type = (string) $args[
'object_type'];
1349 $id = (int) $args[
'id'];
1352 $permError = $this->checkPermission($type,
'delete');
1353 if ($permError !==
null) {
1358 $obj = $this->instantiate($type);
1361 if ($obj->fetch(
$id) <= 0) {
1362 return [
"error" =>
"Object not found with ID: " .
$id];
1367 $accessError = $this->checkAccessToObject($type, $obj);
1368 if ($accessError !==
null) {
1369 return $accessError;
1374 $status = isset($obj->statut) ? (int) $obj->statut : -1;
1376 if ($status !== 0) {
1377 return [
"error" =>
"Can only delete drafts (status 0). Current status: " . $status];
1381 if ($obj->delete($this->user) > 0) {
1382 return [
"success" =>
true];
1386 $errorMsg = ! empty($obj->error) ? (string) $obj->error :
'Unknown error';
1388 return [
"error" =>
"Delete failed: " . $errorMsg];
1402 $map = ($mode ===
'delete' ? self::DELETE_PERM_MAP : self::PERM_MAP);
1404 if (! isset($map[$type])) {
1405 return [
"error" =>
"Unknown type for permission check: " . $type];
1409 foreach ($map[$type] as $perm) {
1410 if ($this->
user->hasRight($perm[0], $perm[1], $perm[2] ??
'')) {
1415 return [
"error" =>
"Permission denied for action on " . $type];
1433 if (! isset(self::ACCESS_MAP[$type])) {
1434 return [
"error" =>
"Unknown type for access check: " . $type];
1437 $access = self::ACCESS_MAP[$type];
1445 '[ToolCrudObjects] Access denied to ' . $type .
' id=' .
$object->id
1446 .
' for user id=' . $this->user->id .
' (entity or thirdparty restriction).',
1449 return [
"error" =>
"Access denied to this " . $type .
"."];
1465 if (! isset($this->map[$type])) {
1466 throw new Exception(
"Unknown type: " . $type);
1469 $config = $this->map[$type];
1470 $path = (string) $config[
'path'];
1471 $className = (string) $config[
'class'];
1474 require_once DOL_DOCUMENT_ROOT .
'/core/class/commonobject.class.php';
1475 require_once DOL_DOCUMENT_ROOT . $path;
1477 if (! class_exists($className)) {
1478 throw new Exception(
"Class '$className' not found for type '$type'");
1481 return new $className($this->db);
1493 private function updateLineUnit(
string $type,
int $lineId,
int $unitId): void
1498 'invoice' =>
'facturedet',
1499 'order' =>
'commandedet',
1500 'proposal' =>
'propaldet',
1501 'supplier_invoice' =>
'facture_fourn_det',
1502 'supplier_order' =>
'commande_fournisseurdet',
1503 'supplier_proposal' =>
'supplier_proposaldet'
1506 if (! isset($tableMap[$type])) {
1510 $table = $tableMap[$type];
1512 $sql =
"UPDATE " . MAIN_DB_PREFIX . $this->db->sanitize($table);
1513 $sql .=
" SET fk_unit = " . (int) $unitId;
1514 $sql .=
" WHERE rowid = " . (int) $lineId;
1516 $resql = $this->db->query($sql);
1519 dol_syslog(
"Error updating unit for line $lineId: " . $this->db->lasterror(), LOG_ERR);