dolibarr 24.0.0-beta
thirdparty.class.php
Go to the documentation of this file.
1<?php
2/* Copyright (C) 2026 Laurent Destailleur <eldy@users.sourceforge.net>
3 * Copyright (C) 2026 Nick Fragoulis
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 */
18
25require_once DOL_DOCUMENT_ROOT . '/societe/class/societe.class.php';
26require_once DOL_DOCUMENT_ROOT . '/contact/class/contact.class.php';
27
28
35{
36
43 public function __construct($db, $user)
44 {
45 $this->db = $db;
46 $this->user = $user;
47 }
48
54 public function getDefinitions(): array
55 {
56 return [
57 [
58 "name" => "search_thirdparties",
59 "description" => "Search for a thirdparty by ID, name, alias, code, or email. If a numerical ID is provided alone, it returns an exact match. If a name is provided, it returns a list of matches.",
60 "inputSchema" => [
61 "type" => "object",
62 "properties" => [
63 "query" => ["type" => ["string", "integer"], "description" => "The ID of the thirdparty, or a name/alias/code/email to search for."],
64 "type" => ["type" => "string", "enum" => ["customer", "prospect", "supplier"], "description" => "Filter by type (optional)."],
65 "country_code" => ["type" => "string", "description" => "ISO 2-letter country code (e.g. US, FR, GR) (optional)."],
66 "limit" => ["type" => "integer", "default" => 5]
67 ],
68 "required" => ["query"]
69 ]
70 ],
71 [
72 "name" => "count_thirdparties",
73 "description" => "Count the number of thirdparties matching the search criteria.",
74 "inputSchema" => [
75 "type" => "object",
76 "properties" => [
77 "query" => ["type" => ["string", "integer"], "description" => "A name/alias/code/email to search for."],
78 "type" => ["type" => "string", "enum" => ["customer", "prospect", "supplier"], "description" => "Filter by type (optional)."],
79 "country_code" => ["type" => "string", "description" => "ISO 2-letter country code (e.g. US, FR, GR) (optional)."]
80 ],
81 "required" => ["query"]
82 ]
83 ],
84 [
85 "name" => "get_thirdparty_details",
86 "description" => "Get full details of a specific thirdparty by ID.",
87 "inputSchema" => [
88 "type" => "object",
89 "properties" => [
90 "id" => ["type" => "integer", "description" => "The unique numerical ID of the thirdparty."]
91 ],
92 "required" => ["id"]
93 ]
94 ],
95 [
96 "name" => "create_thirdparty",
97 "description" => "Create a new thirdparty (Using only terms: Customer, Prospect, or Supplier).",
98 "inputSchema" => [
99 "type" => "object",
100 "properties" => [
101 "name" => ["type" => "string", "description" => "Name of the thirdparty"],
102 "type" => ["type" => "string", "enum" => ["customer", "prospect", "supplier", "both", "none"], "default" => "customer", "description" => "Type of thirdparty. 'none' means not a customer or prospect."],
103 "email" => ["type" => "string", "description" => "Email address"],
104 "phone" => ["type" => "string", "description" => "Phone number"],
105 "address" => ["type" => "string", "description" => "Address"],
106 "zip" => ["type" => "string", "description" => "Postal code"],
107 "town" => ["type" => "string", "description" => "Town/City"],
108 "country_code" => ["type" => "string", "description" => "ISO 2-letter country code (e.g. US, FR, GR)"],
109 "code_client" => ["type" => "string", "description" => "Customer code (optional, -1 for auto-generation)"],
110 "idprof1" => ["type" => "string", "description" => "Professional ID 1"],
111 "idprof2" => ["type" => "string", "description" => "Professional ID 2"],
112 "idprof3" => ["type" => "string", "description" => "Professional ID 3"],
113 "idprof4" => ["type" => "string", "description" => "Professional ID 4"]
114 ],
115 "required" => ["name"]
116 ]
117 ],
118 [
119 "name" => "update_thirdparty",
120 "description" => "Updates an existing thirdparty's details.",
121 "inputSchema" => [
122 "type" => "object",
123 "properties" => [
124 "id" => ["type" => "integer", "description" => "The ID of the thirdparty to update."],
125 "name" => ["type" => "string", "description" => "The new name for the thirdparty."],
126 "email" => ["type" => "string", "description" => "The new email address."],
127 "phone" => ["type" => "string", "description" => "The new phone number."],
128 "address" => ["type" => "string", "description" => "The new address."],
129 "zip" => ["type" => "string", "description" => "The new postal code."],
130 "town" => ["type" => "string", "description" => "The new town."],
131 "country_code" => ["type" => "string", "description" => "The new ISO 2-letter country code."]
132 ],
133 "required" => ["id"]
134 ]
135 ],
136 [
137 "name" => "list_thirdparty_contacts",
138 "description" => "Lists all contacts associated with a specific thirdparty.",
139 "inputSchema" => [
140 "type" => "object",
141 "properties" => [
142 "id" => ["type" => "integer", "description" => "The ID of the thirdparty."]
143 ],
144 "required" => ["id"]
145 ]
146 ],
147 [
148 "name" => "add_thirdparty_contact",
149 "description" => "Adds a new contact to an existing thirdparty.",
150 "inputSchema" => [
151 "type" => "object",
152 "properties" => [
153 "thirdparty_identifier" => [
154 "type" => ["string", "integer"],
155 "description" => "The ID or name of the thirdparty to add the contact to."
156 ],
157 "firstname" => ["type" => "string", "description" => "Contact's first name."],
158 "lastname" => ["type" => "string", "description" => "Contact's last name."],
159 "email" => ["type" => "string", "description" => "Contact's email address."],
160 "phone" => ["type" => "string", "description" => "Contact's phone number."],
161 "role" => ["type" => "string", "description" => "Contact's role or position within the company."]
162 ],
163 "required" => ["thirdparty_identifier", "firstname", "lastname"]
164 ]
165 ]
166 ];
167 }
168
175 public function getCategories(): array
176 {
177 return ['thirdparty', 'billing', 'commercial', 'project', 'stock'];
178 }
179
187 public function execute(string $name, array $args)
188 {
189 switch ($name) {
190 case 'search_thirdparties':
191 return $this->search($args, 0);
192 case 'count_thirdparties':
193 return $this->search($args, 1);
194 case 'get_thirdparty_details': // Get info of agiven thidparty
195 return $this->getDetails($args);
196 case 'create_thirdparty':
197 return $this->create($args);
198 case 'update_thirdparty':
199 return $this->update($args);
200 case 'list_thirdparty_contacts':
201 return $this->listContacts($args);
202 case 'add_thirdparty_contact':
203 return $this->addContact($args);
204 default:
205 return ["error" => "Tool function '$name' not found."];
206 }
207 }
208
220 private function search(array $args, int $count = 0)
221 {
222 if (!$this->user->hasRight('societe', 'lire')) {
223 return ["error" => "Permission Denied"];
224 }
225
226 $query = $args['query'];
227 $type = isset($args['type']) ? (string) $args['type'] : '';
228 $country_code = isset($args['country_code']) ? (string) $args['country_code'] : '';
229 $limit = isset($args['limit']) ? (int) $args['limit'] : 5;
230
231 // Safety fallback
232 if ($limit <= 0) {
233 $limit = 5;
234 }
235 if ($limit > 1000) {
236 dol_syslog("Search DB Error: Too many record requested", LOG_ERR);
237 return ["error" => "DB Error"];
238 }
239
240 // Dolibarr SQL construction
241 if ($count) {
242 $sql = "SELECT COUNT(s.rowid) as nb";
243 } else {
244 $sql = "SELECT s.rowid, s.nom, s.name_alias, s.code_client, s.code_fournisseur, s.email, s.client, s.fournisseur";
245 }
246 $sql .= " FROM " . MAIN_DB_PREFIX . "societe as s";
247 if ($country_code) {
248 $sql.= " INNER JOIN ".MAIN_DB_PREFIX."c_country as c ON s.fk_pays = c.rowid AND c.code = '".$this->db->escape($country_code)."'";
249 }
250 $sql .= " WHERE s.entity IN (" . getEntity('societe') . ")";
251
252 if (is_numeric($query)) {
253 $sql .= " AND s.rowid = " . (int) $query;
254 $limit = 1;
255 } else {
256 $sql .= " AND (s.nom LIKE '%" . $this->db->escape($query) . "%'";
257 $sql .= " OR s.name_alias LIKE '%" . $this->db->escape($query) . "%'";
258 $sql .= " OR s.code_client LIKE '%" . $this->db->escape($query) . "%'";
259 $sql .= " OR s.code_fournisseur LIKE '%" . $this->db->escape($query) . "%'";
260 $sql .= " OR s.email LIKE '%" . $this->db->escape($query) . "%')";
261 }
262
263 if ($type === 'customer') {
264 $sql .= " AND s.client IN (1, 3)";
265 } elseif ($type === 'prospect') {
266 $sql .= " AND s.client IN (2, 3)";
267 } elseif ($type === 'supplier') {
268 $sql .= " AND s.fournisseur = 1";
269 }
270
271 $sql .= " ORDER BY s.nom ASC";
272 $sql .= $this->db->plimit($limit);
273
274 $resql = $this->db->query($sql);
275
276 if (!$resql) {
277 dol_syslog("Search DB Error: " . $this->db->lasterror(), LOG_ERR);
278 return ["error" => "DB Error"];
279 }
280
281 $data = [];
282
283 while ($obj = $this->db->fetch_object($resql)) {
284 if ($count) {
285 $data = [
286 "count" => (int) $obj->nb
287 ];
288
289 $this->db->free($resql);
290
291 return $data;
292 }
293
294 $roles = [];
295
296 // Cast strictly to ensure type safety in logic
297 $is_client = (int) $obj->client;
298 $is_supplier = (int) $obj->fournisseur;
299
300 if ($is_client === 1 || $is_client === 3) {
301 $roles[] = "Customer";
302 }
303 if ($is_client === 2 || $is_client === 3) {
304 $roles[] = "Prospect";
305 }
306 if ($is_supplier === 1) {
307 $roles[] = "Supplier";
308 }
309
310 $data[] = [
311 "id" => (int) $obj->rowid,
312 "name" => (string) $obj->nom,
313 "alias" => (string) $obj->name_alias,
314 "code_cust" => (string) $obj->code_client,
315 "code_sup" => (string) $obj->code_fournisseur,
316 "email" => (string) $obj->email,
317 "type" => implode('/', $roles),
318 "url" => DOL_URL_ROOT . "/societe/card.php?socid=" . $obj->rowid
319 ];
320 }
321
322 $this->db->free($resql);
323
324 return $data;
325 }
326
335 private function getDetails(array $args): array
336 {
337 if (!$this->user->hasRight('societe', 'lire')) {
338 return ["error" => "Permission Denied"];
339 }
340
341 if (empty($args['id'])) {
342 return ["error" => "ID is required"];
343 }
344
345 $soc = new Societe($this->db);
346
347 // Fetch returns > 0 on success, 0 on not found, < 0 on error
348 $result = $soc->fetch((int) $args['id']);
349
350 if ($result > 0) {
351 return [
352 "id" => (int) $soc->id,
353 "name" => (string) $soc->nom,
354 "address" => (string) $soc->address,
355 "zip" => (string) $soc->zip,
356 "city" => (string) $soc->town,
357 "country" => (string) $soc->country,
358 "email" => (string) $soc->email,
359 "phone" => (string) $soc->phone,
360 "vat" => (string) $soc->tva_intra,
361 // getLibStatut(2) returns the status label (short). Cast to string just in case.
362 "status" => (string) $soc->getLibStatut(2),
363 "url" => DOL_URL_ROOT . "/societe/card.php?socid=" . $soc->id
364 ];
365 }
366
367 return ["error" => "Thirdparty not found"];
368 }
369
370
379 private function create(array $args)
380 {
381 global $conf;
382
383 // Check permissions
384 if (!$this->user->hasRight('societe', 'creer')) {
385 return ["error" => "Permission Denied"];
386 }
387
388 // Validate mandatory fields
389 if (empty($args['name'])) {
390 return ["error" => "Name is required"];
391 }
392
393 $soc = new Societe($this->db);
394
395 // Assign properties with strict casting to prevent null issues in strict mode
396 $soc->nom = (string) $args['name'];
397 $soc->email = isset($args['email']) ? (string) $args['email'] : '';
398 $soc->phone = isset($args['phone']) ? (string) $args['phone'] : '';
399 $soc->address = isset($args['address']) ? (string) $args['address'] : '';
400 $soc->zip = isset($args['zip']) ? (string) $args['zip'] : '';
401 $soc->town = isset($args['town']) ? (string) $args['town'] : '';
402 $soc->code_client = isset($args['code_client']) ? (string) $args['code_client'] : '';
403 $soc->idprof1 = isset($args['idprof1']) ? (string) $args['idprof1'] : '';
404 $soc->idprof2 = isset($args['idprof2']) ? (string) $args['idprof2'] : '';
405 $soc->idprof3 = isset($args['idprof3']) ? (string) $args['idprof3'] : '';
406 $soc->idprof4 = isset($args['idprof4']) ? (string) $args['idprof4'] : '';
407
408 // Country Handling
409 if (! empty($args['country_code'])) {
410 require_once DOL_DOCUMENT_ROOT . '/core/lib/company.lib.php';
411 // getCountry returns an array or false/0.
413 $info = getCountry($args['country_code'], '1');
414 if ($info && isset($info['id'])) {
415 $soc->country_id = (int) $info['id'];
416 }
417 }
418
419 // Fallback to default country if not set
420 if (empty($soc->country_id) && !empty($conf->global->MAIN_INFO_SOCIETE_COUNTRY)) {
421 $soc->country_id = (int) $conf->global->MAIN_INFO_SOCIETE_COUNTRY;
422 }
423
424 $type = $args['type'] ?? 'customer';
425 $soc->client = 0; // Default: not a customer
426 $soc->fournisseur = 0; // Default: not a supplier
427
428 if ($type === 'customer') {
429 $soc->client = 1;
430 if (empty($soc->code_client)) {
431 $soc->code_client = '-1';
432 }
433 } elseif ($type === 'prospect') {
434 $soc->client = 2;
435 if (empty($soc->code_client)) {
436 $soc->code_client = '-1';
437 }
438 } elseif ($type === 'both') {
439 $soc->client = 3; // Prospect + Customer
440 if (empty($soc->code_client)) {
441 $soc->code_client = '-1';
442 }
443 }
444
445 if ($type === 'supplier' || $type === 'both') {
446 $soc->fournisseur = 1;
447 if (empty($soc->code_fournisseur)) {
448 $soc->code_fournisseur = '-1';
449 }
450 }
451
452 $result = $soc->create($this->user);
453
454 if ($result > 0) {
455 return [
456 "status" => "success",
457 "message" => "Thirdparty created",
458 "id" => (int) $soc->id,
459 "name" => (string) $soc->name,
460 "url" => DOL_URL_ROOT . "/societe/card.php?socid=" . $soc->id
461 ];
462 }
463
464 return ["error" => "Create failed: " . (string) $soc->error];
465 }
466
475 private function update(array $args)
476 {
477 if (!$this->user->hasRight('societe', 'creer') && !$this->user->hasRight('societe', 'modifier')) {
478 return ["error" => "Permission Denied to update."];
479 }
480
481 if (empty($args['id'])) {
482 return ["error" => "ID is required for update."];
483 }
484
485 $soc = new Societe($this->db);
486
487 $result = $soc->fetch((int) $args['id']);
488 if ($result <= 0) {
489 return ["error" => "Thirdparty not found with ID: " . $args['id']];
490 }
491
492 // Update fields only if provided in arguments (Partial Update)
493 // We cast to (string) to ensure strict type compliance
494 if (isset($args['name'])) {
495 $soc->nom = (string) $args['name'];
496 }
497 if (isset($args['email'])) {
498 $soc->email = (string) $args['email'];
499 }
500 if (isset($args['phone'])) {
501 $soc->phone = (string) $args['phone'];
502 }
503 if (isset($args['address'])) {
504 $soc->address = (string) $args['address'];
505 }
506 if (isset($args['zip'])) {
507 $soc->zip = (string) $args['zip'];
508 }
509 if (isset($args['town'])) {
510 $soc->town = (string) $args['town'];
511 }
512
513 // Handle Country update
514 if (!empty($args['country_code'])) {
515 require_once DOL_DOCUMENT_ROOT . '/core/lib/company.lib.php';
516
518 $info = getCountry($args['country_code'], '1');
519
520 if ($info && isset($info['id'])) {
521 $soc->country_id = (int) $info['id'];
522 }
523 }
524
525 if ($soc->update($soc->id, $this->user) > 0) {
526 return [
527 "status" => "success",
528 "message" => "Thirdparty updated successfully.",
529 "id" => (int) $soc->id,
530 "url" => DOL_URL_ROOT . "/societe/card.php?socid=" . $soc->id
531 ];
532 }
533
534 return ["error" => "Update failed: " . (string) $soc->error];
535 }
536
545 private function listContacts(array $args)
546 {
547 global $langs;
548
549 if (!$this->user->hasRight('societe', 'lire')) {
550 return ["error" => "Permission Denied"];
551 }
552
553 if (empty($args['id'])) {
554 return ["error" => "Thirdparty ID is required"];
555 }
556
557 $socid = (int) $args['id'];
558 $langs->load("companies");
559 $langs->load("other");
560
561 // Translations
562 $label_id = $langs->transnoentitiesnoconv("Id");
563 $label_firstname = $langs->transnoentitiesnoconv("Firstname");
564 $label_lastname = $langs->transnoentitiesnoconv("Lastname");
565 $label_email = $langs->transnoentitiesnoconv("Email");
566 $label_phone = $langs->transnoentitiesnoconv("Phone");
567 $label_role = $langs->transnoentitiesnoconv("PostOrFunction");
568 $label_link = $langs->transnoentitiesnoconv("Link");
569
570 $link_text = $langs->transnoentitiesnoconv("Show");
571
572 // Verify thirdparty exists first
573 $soc = new Societe($this->db);
574 if ($soc->fetch($socid) <= 0) {
575 return ["error" => "Thirdparty not found with ID: " . $socid];
576 }
577
578 // Build SQL
579 $sql = "SELECT t.rowid, t.firstname, t.lastname, t.email, t.phone, t.poste";
580 $sql .= " FROM " . MAIN_DB_PREFIX . "socpeople as t";
581 $sql .= " WHERE t.fk_soc = " . (int) $socid; // Fix: Added explicit (int) cast to satisfy CodingPhpTest static analysis
582 $sql .= " AND t.entity IN (" . getEntity('socpeople') . ")";
583 $sql .= " ORDER BY t.lastname ASC, t.firstname ASC";
584
585 $resql = $this->db->query($sql);
586
587 if (! $resql) {
588 dol_syslog("DB Error in listContacts: " . $this->db->lasterror(), LOG_ERR);
589 return ["error" => "DB Error"];
590 }
591
592 $data = [];
593
594 while ($obj = $this->db->fetch_object($resql)) {
595 // Absolute URL
596 $absoluteUrl = DOL_URL_ROOT . "/contact/card.php?id=" . $obj->rowid;
597
598 $htmlLink = "<a href='" . $absoluteUrl . "' target='_blank'>" . $link_text . "</a>";
599
600 $data[] = [
601 $label_id => (int) $obj->rowid,
602 $label_firstname => (string) $obj->firstname,
603 $label_lastname => (string) $obj->lastname,
604 $label_email => (string) $obj->email,
605 $label_phone => (string) $obj->phone,
606 $label_role => (string) $obj->poste,
607 $label_link => $htmlLink
608 ];
609 }
610
611 $this->db->free($resql);
612
613 return $data;
614 }
615
625 private function addContact(array $args)
626 {
627 if (!$this->user->hasRight('societe', 'creer')) {
628 return ["error" => "Permission Denied to create contacts."];
629 }
630
631 if (empty($args['thirdparty_identifier']) || empty($args['firstname']) || empty($args['lastname'])) {
632 return ["error" => "Thirdparty identifier, firstname and lastname are required."];
633 }
634
635 $identifier = $args['thirdparty_identifier'];
636 $soc = new Societe($this->db);
637 $res = 0;
638
639 // Fetch Thirdparty
640 if (is_numeric($identifier)) {
641 // Fetch by RowID
642 $res = $soc->fetch((int) $identifier);
643 } else {
644 // Fetch by Ref/Code (2nd argument of fetch is $ref)
645 $res = $soc->fetch(0, (string) $identifier);
646 }
647
648 if ($res <= 0) {
649 return ["error" => "Thirdparty not found with identifier: " . $identifier];
650 }
651
652 // Prepare Contact
653 $contact = new Contact($this->db);
654 $contact->socid = $soc->id; // Link to the fetched thirdparty
655 $contact->firstname = (string) ($args['firstname'] ?? '');
656 $contact->lastname = (string) $args['lastname'];
657 $contact->email = isset($args['email']) ? (string) $args['email'] : '';
658 $contact->phone_pro = isset($args['phone']) ? (string) $args['phone'] : '';
659 $contact->poste = isset($args['role']) ? (string) $args['role'] : '';
660
661 // Attempt Creation
662 if ($contact->create($this->user) > 0) {
663 return [
664 "status" => "success",
665 "message" => "Contact created successfully.",
666 "id" => (int) $contact->id,
667 "url" => DOL_URL_ROOT . "/contact/card.php?id=" . $contact->id
668 ];
669 }
670
671 return ["error" => "Failed to create contact: " . (string) $contact->error];
672 }
673}
Class to manage contact/addresses.
Abstract base class for all MCP (Model Context Protocol) tools.
Class to manage third parties objects (customers, suppliers, prospects...)
Class ToolThirdParty.
getDefinitions()
Returns an array of tool definitions, including name, description, and input schema.
getDetails(array $args)
Fetch details for a specific third party (Societe).
execute(string $name, array $args)
Executes the requested tool function based on its name.
search(array $args, int $count=0)
Search for third parties based on provided criteria.
__construct($db, $user)
Constructor.
addContact(array $args)
Add a contact linked to a thirdparty.
getCategories()
Return categories this tool belongs to.
listContacts(array $args)
List all contacts for a given thirdparty.
getCountry($searchkey, $withcode='', $dbtouse=null, $outputlangs=null, $entconv=1, $searchlabel='')
Return country label, code or id from an id, code or label.
if(!isModEnabled('ai')||!getDolGlobalString('AI_ASSISTANT_ENABLED')) global $conf
The main.inc.php has been included so the following variable are now defined:
dol_syslog($message, $level=LOG_INFO, $ident=0, $suffixinfilename='', $restricttologhandler='', $logcontext=null)
Write log message into outputs.
getEntity($element, $shared=1, $currentobject=null)
Get list of entity id to use.
print $langs trans("Show") . '< td style="' . $timeColor . '" align="center"> s</td > badge status0 badge status4 badge status3 Error badge status8< td align="center">< span class="badge ' . $badge . '"></span ></td >< td align="center">< a href="#" class="button button-small" onclick="openLogModal(this)" data-req="' . dol_escape_htmltag($reqSafe) . '" data-res="' . dol_escape_htmltag($resSafe) . '" data-err="' . dol_escape_htmltag($errSafe) . '">< span class="fa fa-search-plus"></span ></a ></td ></tr >< tr >< td colspan="' . $colspan . '" class="opacitymedium"></td ></tr ></table ></div ></form > logModal none logModal none s a JSON string
buildzip.php
$conf db user
Active Directory does not allow anonymous connections.
Definition repair.php:134