dolibarr 25.0.0-alpha
ai.class.php
Go to the documentation of this file.
1<?php
2/* Copyright (C) 2024 Laurent Destailleur <eldy@users.sourceforge.net>
3 * Copyright (C) 2024 Frédéric France <frederic.france@free.fr>
4 * Copyright (C) 2024-2026 MDW <mdeweerd@users.noreply.github.com>
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 * or see https://www.gnu.org/
19 */
20
27require_once DOL_DOCUMENT_ROOT."/core/lib/admin.lib.php";
28require_once DOL_DOCUMENT_ROOT.'/core/lib/geturl.lib.php';
29require_once DOL_DOCUMENT_ROOT."/ai/lib/ai.lib.php";
30
31
35class Ai
36{
40 protected $db;
41
45 private $apiService;
46
50 private $apiKey;
51
55 private $apiEndpoint;
56
60 public $tokenUsage;
61
62
63 const AI_DEFAULT_PROMPT_FOR_EMAIL = 'You are an email editor. Return only the content of the message. Do not add explanation.'; // Note: This instruction will also be completed by generateContent() to manage text versus HTML content.
64 const AI_DEFAULT_PROMPT_FOR_WEBPAGE = 'You are a website editor. Return all HTML content inside a section tag. Do not add explanation.';
65 const AI_DEFAULT_PROMPT_FOR_TEXT_TRANSLATION = 'You are a translator, answer with one and only one translation with no comment and explanation.';
66 const AI_DEFAULT_PROMPT_FOR_TEXT_SUMMARIZE = 'You are a writer, make the answer in the same language than the original text to summarize.';
67 const AI_DEFAULT_PROMPT_FOR_TEXT_SPELLCHECKER = 'You are a proofreader, write your response in the same language as the original text in order to correct spelling and grammar errors. If there is carriage return or line feed in original message, keep them. Keep also any HTML or markdown formatting without changing it or adding one, just fix spelling and grammar errors in text content. Answer with the corrected text and the original HTML formatting if there was, with no additional comment and explanation even to highlight the fixed errors.';
68 const AI_DEFAULT_PROMPT_FOR_TEXT_REPHRASER = 'You are a writer, write your response in the same language as the original text to rephrase. Give only one answer with no comment and explanation. If there is carriage return or line feed in original message, keep them. Keep also any HTML or markdown formatting without adding one.';
69 const AI_DEFAULT_PROMPT_FOR_EXTRAFIELD_FILLER = 'Give only one answer with no comment and explanation, I want the text to be ready to copy and paste.';
70 const AI_DEFAULT_PROMPT_FOR_DOC_PARSING = 'You are an assistant to analyze documents. Return your answer with a JSON string and only a JSON string, do not add any other comment.';
71
72
78 public function __construct($db)
79 {
80 $this->db = $db;
81
82 // Get API key according to enabled AI
83 $this->apiService = getDolGlobalString('AI_API_SERVICE', 'chatgpt');
84 $this->apiKey = getDolGlobalString('AI_API_'.strtoupper($this->apiService).'_KEY');
85 }
86
92 public function getApiService()
93 {
94 return $this->apiService;
95 }
96
108 public function generateContent($instructions, $model = 'auto', $function = 'textgeneration', $format = '', $moreheaders = array(), $moreendpoint = '')
109 {
110 global $dolibarr_main_data_root;
111
112 $arrayofai = getListOfAIServices();
113
114 // TODO Can store the need for a key into array returned by getListOfAIServices()
115 if (empty($this->apiKey) && in_array($this->apiService, array('chatgpt', 'groq', 'mistral'))) {
116 return array('error' => true, 'message' => 'API key is not defined for the AI enabled service ('.$this->apiService.')');
117 }
118
119 // $this->apiEndpoint is already set here only if it was previously forced.
120
121 if (empty($this->apiEndpoint) && $this->apiService == 'custom' && !getDolGlobalString('AI_API_CUSTOM_URL')) {
122 return array('error' => true, 'message' => 'API URL is not defined for the AI enabled service ('.$this->apiService.')');
123 }
124
125 // In most cases, it is empty and we must get it from $function and $this->apiService
126 if (empty($this->apiEndpoint)) {
127 // Return the endpoint from $this->apiService.
128 if ($function == 'imagegeneration') {
129 $this->apiEndpoint = getDolGlobalString('AI_API_'.strtoupper($this->apiService).'_URL', $arrayofai[$this->apiService]['url']);
130 $this->apiEndpoint .= (preg_match('/\/$/', $this->apiEndpoint) ? '' : '/').'images/generations';
131 } elseif ($function == 'audiogeneration') {
132 $this->apiEndpoint = getDolGlobalString('AI_API_'.strtoupper($this->apiService).'_URL', $arrayofai[$this->apiService]['url']);
133 $this->apiEndpoint .= (preg_match('/\/$/', $this->apiEndpoint) ? '' : '/').'audio/speech';
134 } elseif ($function == 'transcription') {
135 $this->apiEndpoint = getDolGlobalString('AI_API_'.strtoupper($this->apiService).'_URL', $arrayofai[$this->apiService]['url']);
136 $this->apiEndpoint .= (preg_match('/\/$/', $this->apiEndpoint) ? '' : '/').'transcriptions';
137 } elseif ($function == 'file') {
138 $this->apiEndpoint = getDolGlobalString('AI_API_'.strtoupper($this->apiService).'_URL', $arrayofai[$this->apiService]['url']);
139 $this->apiEndpoint .= (preg_match('/\/$/', $this->apiEndpoint) ? '' : '/').'files';
140 } elseif ($function == 'assistant') {
141 $this->apiEndpoint = getDolGlobalString('AI_API_'.strtoupper($this->apiService).'_URL', $arrayofai[$this->apiService]['url']);
142 $this->apiEndpoint .= (preg_match('/\/$/', $this->apiEndpoint) ? '' : '/').'assistans';
143 } elseif ($function == 'thread') {
144 $this->apiEndpoint = getDolGlobalString('AI_API_'.strtoupper($this->apiService).'_URL', $arrayofai[$this->apiService]['url']);
145 $this->apiEndpoint .= (preg_match('/\/$/', $this->apiEndpoint) ? '' : '/').'threads';
146 } else { // if $function == 'docparsing', 'text...', ...
147 $this->apiEndpoint = getDolGlobalString('AI_API_'.strtoupper($this->apiService).'_URL', $arrayofai[$this->apiService]['url']);
148 if ($this->apiService == 'google') {
149 // Google Gemini native API: the /models/<model>:generateContent suffix is
150 // appended later (once $model has been resolved). The OpenAI-style
151 // /chat/completions does not exist on the native Gemini endpoint.
152 $this->apiEndpoint = rtrim($this->apiEndpoint, '/');
153 } else {
154 $this->apiEndpoint .= (preg_match('/\/$/', $this->apiEndpoint) ? '' : '/').'chat/completions';
155 }
156 }
157 }
158 if ($moreendpoint) {
159 $this->apiEndpoint .= '/'.$moreendpoint;
160 }
161
162
163 // $model may be undefined or 'auto'.
164 // If this is the case, we must get it from $function and $this->apiService
165 if (empty($model) || $model == 'auto') {
166 // Return the model from $this->apiService.
167 if (in_array($function, array('file', 'assistant', 'thread'))) {
168 $model = '';
169 } elseif ($function == 'imagegeneration') {
170 $model = getDolGlobalString('AI_API_'.strtoupper($this->apiService).'_MODEL_IMAGE', $arrayofai[$this->apiService][$function]['default']);
171 } elseif ($function == 'audiogeneration') {
172 $model = getDolGlobalString('AI_API_'.strtoupper($this->apiService).'_MODEL_AUDIO', $arrayofai[$this->apiService][$function]['default']);
173 } elseif ($function == 'transcription') {
174 $model = getDolGlobalString('AI_API_'.strtoupper($this->apiService).'_MODEL_TRANSCRIPT', $arrayofai[$this->apiService][$function]['default']);
175 } elseif ($function == 'translation') {
176 $model = getDolGlobalString('AI_API_'.strtoupper($this->apiService).'_MODEL_TRANSLATE', $arrayofai[$this->apiService][$function]['default']);
177 } elseif ($function == 'docparsing') {
178 $model = getDolGlobalString('AI_API_'.strtoupper($this->apiService).'_MODEL_DOCPARSING', $arrayofai[$this->apiService][$function]['default']);
179 } else {
180 // else 'textgenerationemail', 'textgenerationwebpage', 'textgeneration', 'texttranslation', 'textsummarize', 'textrephraser', 'textspellchecker', ...
181 $model = getDolGlobalString('AI_API_'.strtoupper($this->apiService).'_MODEL_TEXT', $arrayofai[$this->apiService]['textgeneration']['default']);
182 }
183 }
184
185 // Google Gemini: append /models/<model>:generateContent now that $model is resolved.
186 if ($this->apiService == 'google' && !in_array($function, array('file', 'assistant', 'thread'))
187 && strpos($this->apiEndpoint, ':generateContent') === false) {
188 $this->apiEndpoint .= '/models/'.rawurlencode($model).':generateContent';
189 }
190
191 dol_syslog("Call API for apiKey=".substr($this->apiKey, 0, 5).'***********, apiEndpoint='.$this->apiEndpoint.", model=".$model.", format=".$format);
192 if (getDolGlobalString("AI_DEBUG")) {
193 if (@is_writable($dolibarr_main_data_root)) { // Avoid fatal error on fopen with open_basedir
194 $outputfile = $dolibarr_main_data_root."/dolibarr_ai.log";
195 $fp = fopen($outputfile, "w"); // overwrite
196
197 if ($fp) {
198 fwrite($fp, "Call API for apiKey=".substr($this->apiKey, 0, 5).'***********, apiEndpoint='.$this->apiEndpoint.", model=".$model.", format=".$format."\n");
199 fclose($fp);
200 dolChmod($outputfile);
201 }
202 }
203 }
204
205 $response = null;
206
207 try {
208 if (empty($this->apiEndpoint)) {
209 throw new Exception('The AI service '.$this->apiService.' is not yet supported for the type of request '.$function);
210 }
211
212 $configurationsJson = getDolGlobalString('AI_CONFIGURATIONS_PROMPT');
213 $configurations = json_decode($configurationsJson, true);
214
215 $prePrompt = '';
216 $postPrompt = '';
217
218 if (isset($configurations[$function])) {
219 if (isset($configurations[$function]['prePrompt'])) {
220 $prePrompt = $configurations[$function]['prePrompt'];
221 }
222
223 if (isset($configurations[$function]['postPrompt'])) {
224 $postPrompt = $configurations[$function]['postPrompt'];
225 }
226 }
227 //var_dump($prePrompt);
228
229 // Get the default value of prePrompt if not defined
230 if (empty($prePrompt) && $function == 'textgenerationemail') {
231 $prePrompt = self::AI_DEFAULT_PROMPT_FOR_EMAIL;
232 if ($format === 'html') {
233 $prePrompt .= ' Return all HTML content inside a section tag';
234 } else {
235 $prePrompt .= ' Return content in UTF8 text. Use Linux carriage return if you need to split a line. Do not include any HTML tag neither HTML entities.';
236 }
237 }
238 if (empty($prePrompt) && $function == 'textgenerationwebpage') {
239 $prePrompt = self::AI_DEFAULT_PROMPT_FOR_WEBPAGE;
240 }
241 if (empty($prePrompt) && $function == 'textgenerationextrafield') {
242 $prePrompt = self::AI_DEFAULT_PROMPT_FOR_EXTRAFIELD_FILLER;
243 }
244 if (empty($prePrompt) && $function == 'texttranslation') {
245 $prePrompt = self::AI_DEFAULT_PROMPT_FOR_TEXT_TRANSLATION;
246 }
247 if (empty($prePrompt) && $function == 'textsummarize') {
248 $prePrompt = self::AI_DEFAULT_PROMPT_FOR_TEXT_SUMMARIZE;
249 }
250 if (empty($prePrompt) && $function == 'textrephraser') {
251 $prePrompt = self::AI_DEFAULT_PROMPT_FOR_TEXT_REPHRASER;
252 }
253 if (empty($prePrompt) && $function == 'textspellchecker') {
254 $prePrompt = self::AI_DEFAULT_PROMPT_FOR_TEXT_SPELLCHECKER;
255 }
256 if (empty($prePrompt) && $function == 'docparsing') {
257 $prePrompt = self::AI_DEFAULT_PROMPT_FOR_DOC_PARSING;
258 }
259
260 if (is_array($instructions)) {
261 $arrayforpayload = $instructions;
262 $fullInstructions = '';
263 } else {
264 $fullInstructions = $instructions.($postPrompt ? (preg_match('/[\.\!\?]$/', $instructions) ? '' : '.').' '.$postPrompt : '');
265
266 // Set payload string
267 /*{
268 "messages": [
269 {
270 "content": "You are a helpful assistant.",
271 "role": "system"
272 },
273 {
274 "content": "Hello!",
275 "role": "user"
276 }
277 ],
278 "model": "tinyllama-1.1b",
279 "stream": true,
280 "max_tokens": 2048,
281 "stop": [
282 "hello"
283 ],
284 "frequency_penalty": 0,
285 "presence_penalty": 0,
286 "temperature": 0.7,
287 "top_p": 0.95
288 }*/
289
290 // Add a system message
291 $addDateTimeContext = false;
292 if ($addDateTimeContext) { // @phpstan-ignore-line
293 $prePrompt = ($prePrompt ? $prePrompt.(preg_match('/[\.\!\?]$/', $prePrompt) ? '' : '.').' ' : '').'Today we are '.dol_print_date(dol_now(), 'dayhourtext');
294 }
295
296 if ($this->apiService == 'google') {
297 // Google Gemini native payload format (different from OpenAI's "messages").
298 $arrayforpayload = array(
299 'contents' => array(
300 array('role' => 'user', 'parts' => array(array('text' => $fullInstructions)))
301 )
302 );
303 if ($prePrompt) {
304 $arrayforpayload['system_instruction'] = array(
305 'parts' => array(array('text' => $prePrompt))
306 );
307 }
308 } else {
309 // OpenAI-compatible payload format (chatgpt, mistral, groq, anthropic-compat, custom, ...)
310 $arrayforpayload = array(
311 'messages' => array(array('role' => 'user', 'content' => $fullInstructions)),
312 'model' => $model,
313 );
314 if ($prePrompt) {
315 $arrayforpayload['messages'][] = array('role' => 'system', 'content' => $prePrompt);
316 }
317 }
318 }
319
320 /*
321 $arrayforpayload['temperature'] = 0.7;
322 $arrayforpayload['max_tokens'] = -1;
323 $arrayforpayload['stream'] = false;
324 */
325
326 if ($function == 'thread') {
327 $payload = $instructions;
328 } else {
329 $payload = json_encode($arrayforpayload);
330 }
331
332 if ($this->apiService == 'google') {
333 // Google Gemini uses the x-goog-api-key header (Bearer is not accepted by the native API).
334 $headers = array(
335 'x-goog-api-key: ' . $this->apiKey,
336 );
337 } else {
338 $headers = array(
339 'Authorization: Bearer ' . $this->apiKey,
340 );
341 }
342 if ($function != 'file') {
343 $headers[] = 'Content-Type: application/json';
344 }
345 if (!empty($moreheaders)) {
346 foreach ($moreheaders as $morekey => $moreval) {
347 $headers[] = $morekey.': '.$moreval;
348 }
349 }
350
351 if (getDolGlobalString("AI_DEBUG")) {
352 if (@is_writable($dolibarr_main_data_root)) { // Avoid fatal error on fopen with open_basedir
353 $outputfile = $dolibarr_main_data_root."/dolibarr_ai.log";
354 $fp = fopen($outputfile, "a");
355
356 if ($fp) {
357 if ($function == 'docparsing') {
358 fwrite($fp, "Call endpoint ".$this->apiEndpoint." with POST and the following file to upload:\n");
359 fwrite($fp, $instructions."\n");
360 } else {
361 fwrite($fp, "Call endpoint ".$this->apiEndpoint." with POST and the following message:\n");
362 fwrite($fp, $fullInstructions."\n");
363 fwrite($fp, "And prepompt:\n");
364 fwrite($fp, $prePrompt."\n");
365 }
366 fwrite($fp, "HTTP Header\n");
367 fwrite($fp, var_export($headers, true)."\n");
368 fwrite($fp, "Payload\n");
369 fwrite($fp, var_export($payload, true)."\n");
370
371 fclose($fp);
372 dolChmod($outputfile);
373 }
374 }
375 }
376
377 // By default, we accept only external endpoints ($dolibarr_ai_allow_local_endpoints is not set).
378 // To allow local endpoints, we must set $dolibarr_ai_allow_local_endpoints to 1 or 2 in conf.php.
379 global $dolibarr_ai_allow_local_endpoints;
380 $localurl = $dolibarr_ai_allow_local_endpoints ?? 0;
381
382 $response = getURLContent($this->apiEndpoint, 'POST', $payload, 1, $headers, array('http', 'https'), $localurl);
383
384 if (empty($response['http_code'])) {
385 throw new Exception('API request failed. No http received');
386 }
387 if (!empty($response['http_code']) && $response['http_code'] != 200) {
388 if (in_array($response['http_code'], array(400, 401, 403, 429)) && !empty($response['content'])) {
389 $tmp = json_decode($response['content'], true);
390 if (!empty($tmp['message'])) {
391 return array(
392 'error' => true,
393 'message' => $tmp['message'],
394 'code' => (empty($response['http_code']) ? 0 : $response['http_code']),
395 'curl_error_no' => (empty($response['curl_error_no']) ? 0 : $response['curl_error_no']),
396 'format' => $format,
397 'service' => $this->apiService,
398 'function' => $function
399 );
400 }
401 }
402 throw new Exception('API request on AI endpoint '.$this->apiEndpoint.' failed with status code '.$response['http_code']);
403 }
404
405 if (getDolGlobalString("AI_DEBUG")) {
406 if (@is_writable($dolibarr_main_data_root)) { // Avoid fatal error on fopen with open_basedir
407 $outputfile = $dolibarr_main_data_root."/dolibarr_ai.log";
408 $fp = fopen($outputfile, "a");
409
410 if ($fp) {
411 fwrite($fp, "Answer\n");
412 fwrite($fp, var_export((empty($response['content']) ? 'No content result' : $response['content']), true)."\n");
413
414 fclose($fp);
415 dolChmod($outputfile);
416 }
417 }
418 }
419
420
421 // Decode JSON response
422 $decodedResponse = json_decode($response['content'], true);
423
424 // Extraction content
425 if (!empty($decodedResponse['error'])) {
426 if (is_scalar($decodedResponse['error'])) {
427 $generatedContent = $decodedResponse['error'];
428 } else {
429 $generatedContent = var_export($decodedResponse['error'], true);
430 }
431 } elseif ($this->apiService == 'google') {
432 // Google Gemini response shape: candidates[0].content.parts[*].text
433 // (parts is an array because Gemini can return mixed-modality output;
434 // we concatenate the textual parts.)
435 $generatedContent = '';
436 if (!empty($decodedResponse['candidates'][0]['content']['parts'])) {
437 foreach ($decodedResponse['candidates'][0]['content']['parts'] as $part) {
438 if (isset($part['text'])) {
439 $generatedContent .= $part['text'];
440 }
441 }
442 }
443 } else {
444 $generatedContent = $decodedResponse['choices'][0]['message']['content'];
445 }
446 dol_syslog("ai->generatedContent returned: ".dol_trunc($generatedContent, 50));
447
448 $this->tokenUsage = $decodedResponse['usage'] ?? null;
449
450 // If content is not HTML, we convert it into HTML
451 if ($format == 'html') {
452 if (!dol_textishtml($generatedContent)) {
453 dol_syslog("Result was detected as not HTML so we convert it into HTML.");
454 $generatedContent = dol_nl2br($generatedContent);
455 } else {
456 dol_syslog("Result was detected as already HTML. Do nothing.");
457 }
458
459 // TODO If content is for website module, we must
460 // - clan html header, keep body only and remove ``` ticks added by AI
461 // - add tags <section contenEditable="true"> </section>
462 }
463
464 return $generatedContent;
465 } catch (Exception $e) {
466 $errormessage = $e->getMessage();
467 $errormessagelog = $e->getMessage();
468 if (!empty($response['content'])) {
469 $decodedResponse = json_decode($response['content'], true);
470 $errormessagelog .= ' - '.$response['content'];
471
472 if (!empty($decodedResponse['error']['message'])) {
473 // With OpenAI, error is into an object error into the content
474 $errormessage .= ' - '.$decodedResponse['error']['message'];
475 } else {
476 $errormessage .= ' - '.$response['content'];
477 }
478 }
479
480 if (getDolGlobalString("AI_DEBUG")) {
481 if (@is_writable($dolibarr_main_data_root)) { // Avoid fatal error on fopen with open_basedir
482 $outputfile = $dolibarr_main_data_root."/dolibarr_ai.log";
483 $fp = fopen($outputfile, "a");
484
485 if ($fp) {
486 fwrite($fp, "Error: ".$errormessagelog."\n");
487
488 fclose($fp);
489 dolChmod($outputfile);
490 }
491 }
492 }
493
494 return array(
495 'error' => true,
496 'message' => $errormessage,
497 'code' => (empty($response['http_code']) ? 0 : $response['http_code']),
498 'curl_error_no' => (empty($response['curl_error_no']) ? 0 : $response['curl_error_no']),
499 'format' => $format,
500 'service' => $this->apiService,
501 'function' => $function
502 );
503 }
504 }
505
513 public function decodeJsonIntoArray($json, $type)
514 {
515 $tmparray = array();
516
517 //var_dump($json['items']);
518 if ($type == 'supplier_invoice') {
519 // Invoice info
520 if (!empty($json['document_info']['reference'])) {
521 $tmparray['supplierref'] = $json['document_info']['reference'];
522 } elseif (!empty($json['document_info']['invoice_number'])) {
523 $tmparray['supplierref'] = $json['document_info']['invoice_number'];
524 }
525
526 if (!empty($json['document_info']['title'])) {
527 $tmparray['title'] = $json['document_info']['title'];
528 }
529
530 // Issue date
531 if (!empty($json['document_info']['issue_date']) && preg_match('/^[0-9]{4}-[0-9]{2}-[0-9]{2}((\s|T)[0-9]{2}:[0-9]{2}:[0-9]{2}Z?)?$/', $json['document_info']['issue_date'])) {
532 $tmparray['issue_date'] = dol_stringtotime($json['document_info']['issue_date'], 'tzuserrel');
533 } elseif (!empty($json['document_info']['submission_date']) && preg_match('/^[0-9]{4}-[0-9]{2}-[0-9]{2}((\s|T)[0-9]{2}:[0-9]{2}:[0-9]{2}Z?)?$/', $json['document_info']['submission_date'])) {
534 $tmparray['issue_date'] = dol_stringtotime($json['document_info']['submission_date'], 'tzuserrel');
535 } elseif (!empty($json['document_info']['date']) && preg_match('/^[0-9]{4}-[0-9]{2}-[0-9]{2}((\s|T)[0-9]{2}:[0-9]{2}:[0-9]{2}Z?)?$/', $json['document_info']['date'])) {
536 $tmparray['issue_date'] = dol_stringtotime($json['document_info']['date'], 'tzuserrel');
537 }
538
539 // Due date
540 if (!empty($json['document_info']['due_date']) && preg_match('/^([0-9]{4})-([0-9]{2})-([0-9]{2})$/', $json['document_info']['due_date'])) {
541 $tmparray['due_date'] = dol_stringtotime($json['document_info']['due_date'], 'tzuserrel');
542 }
543
544 // Currency
545 if ($json['summary']['currency'] == '€') {
546 $tmparray['currency_code'] = 'EUR';
547 } elseif (strlen($json['summary']['currency']) == 3) {
548 $tmparray['currency_code'] = $json['summary']['currency'];
549 } elseif (strlen($json['document_info']['currency_code']) == 3) {
550 $tmparray['currency_code'] = $json['document_info']['currency_code'];
551 }
552
553 // Note
554 if (!empty($json['notes'])) {
555 if (is_scalar($json['notes'])) {
556 $tmparray['note_public'] = $json['notes'];
557 } elseif (is_array($json['notes'])) {
558 // Loop on each note
559 $tmparray['note_public'] = '';
560 foreach ($json['notes'] as $val) {
561 if (is_scalar($val)) {
562 $tmparray['note_public'] = dol_concat($tmparray['note_public'], $val);
563 } elseif (is_array($val)) {
564 foreach ($val as $val2) {
565 if (is_scalar($val2)) {
566 $tmparray['note_public'] = dol_concat($tmparray['note_public'], $val2);
567 }
568 }
569 }
570 }
571 }
572 }
573
574 // Vendor
575 if (!empty($json['document_info']['vendor'])) {
576 $arrayforthirdparty = $json['document_info']['vendor'];
577 } elseif (!empty($json['vendor'])) {
578 $arrayforthirdparty = $json['vendor'];
579 } elseif (!empty($json['issuer'])) {
580 $arrayforthirdparty = $json['issuer'];
581 }
582 if (!empty($arrayforthirdparty)) {
583 if (!empty($arrayforthirdparty['name'])) {
584 $tmparray['vendor_name'] = $arrayforthirdparty['name'];
585 }
586 if (!empty($arrayforthirdparty['siren'])) {
587 $tmparray['vendor_profid1'] = $arrayforthirdparty['siren'];
588 }
589 if (!empty($arrayforthirdparty['siret'])) {
590 $tmparray['vendor_profid2'] = $arrayforthirdparty['siret'];
591 }
592 if (!empty($arrayforthirdparty['email'])) {
593 $tmparray['vendor_email'] = $arrayforthirdparty['email'];
594 }
595 if (!empty($arrayforthirdparty['professional_id'])) {
596 $tmparray['vendor_profid1'] = $arrayforthirdparty['professional_id']['siren'];
597 }
598 if (!empty($arrayforthirdparty['vat_number'])) {
599 $tmparray['vendor_vat_number'] = $arrayforthirdparty['vat_number'];
600 } elseif (!empty($arrayforthirdparty['tva_num'])) {
601 $tmparray['vendor_vat_number'] = $arrayforthirdparty['tva_num'];
602 }
603 }
604
605 // Invoice
606 if (!empty($json['recipient']['description'])) {
607 $tmparray['invoice_label'] = $json['recipient']['description'];
608 }
609
610 // Items
611 if (empty($json['items'])) {
612 if (!empty($json['summary']['subtotal_excluding_tax'])) {
613 $tmparray['description'] = 'Undefined';
614 $tmparray['total_ht'] = (float) $json['summary']['subtotal_excluding_tax'];
615 $tmparray['vat_rate'] = (float) $json['summary']['tax']['rate'];
616 }
617 } else {
618 $i = 0;
619 foreach ($json['items'] as $item) {
620 $i++;
621 $tmparray['lines'][$i] = array();
622
623 if (!empty($item['description'])) {
624 $tmparray['lines'][$i]['desc'] = $item['description'];
625 } elseif (!empty($item['service'] && is_string($item['service']))) {
626 $tmparray['lines'][$i]['desc'] = $item['service'];
627 }
628
629 $tmparray['lines'][$i]['qty'] = $item['quantity'] ?? 1;
630 $tmparray['lines'][$i]['vat_rate'] = $item['tax']['vat_rate'] ?? null;
631 $tmparray['lines'][$i]['total_vat'] = $item['tax']['amount'] ?? null;
632 $tmparray['lines'][$i]['subprice'] = $item['unit_price'] ?? null;
633 $tmparray['lines'][$i]['total_ht'] = $item['total_excluding_tax'] ?? null;
634 $tmparray['lines'][$i]['total_ttc'] = $item['total_including_tax'] ?? null;
635
636 if (!empty($item['period_start'])) {
637 $tmparray['lines'][$i]['date_start'] = dol_stringtotime($item['period_start'], 'tzuserrel');
638 }
639 if (!empty($item['period_end'])) {
640 $tmparray['lines'][$i]['date_end'] = dol_stringtotime($item['period_end'], 'tzuserrel');
641 }
642 if (!empty($item['period']) && !empty($item['period']['start_date'])) {
643 $tmparray['lines'][$i]['date_start'] = dol_stringtotime($item['period']['start_date'], 'tzuserrel');
644 }
645 if (!empty($item['period']) && !empty($item['period']['end_date'])) {
646 $tmparray['lines'][$i]['date_end'] = dol_stringtotime($item['period']['end_date'], 'tzuserrel');
647 }
648 }
649 }
650 }
651
652 return $tmparray;
653 }
654}
getListOfAIServices()
Get list of available ai services.
Definition ai.lib.php:69
Class for AI feature.
Definition ai.class.php:36
getApiService()
get API Service
Definition ai.class.php:92
generateContent($instructions, $model='auto', $function='textgeneration', $format='', $moreheaders=array(), $moreendpoint='')
Generate the response of an AI prompt.
Definition ai.class.php:108
__construct($db)
Constructor.
Definition ai.class.php:78
decodeJsonIntoArray($json, $type)
Decode JSON into array.
Definition ai.class.php:513
dol_stringtotime($string, $gm=1, $processnotimeasnoon=0)
Convert a string date into a GM Timestamps date Warning: YYYY-MM-DDTHH:MM:SS+02:00 (RFC3339) is not s...
Definition date.lib.php:442
dol_now($mode='gmt')
Return date for now.
dol_nl2br($stringtoencode, $nl2brmode=0, $forxml=false)
Replace CRLF in string with a HTML BR tag.
dolChmod($filepath, $newmask='')
Change mod of a file.
dol_textishtml($msg, $option=0)
Return if a text is a html content.
dol_print_date($time, $format='', $tzoutput='auto', $outputlangs=null, $encodetooutput=false, $decorate=0)
Output date in a string format according to outputlangs (or langs if not defined).
dol_trunc($string, $size=40, $trunc='right', $stringencoding='UTF-8', $nodot=0, $display=0)
Truncate a string to a particular length adding '...' if string larger than length.
dol_concat($text1, $text2)
Concat 2 strings.
getDolGlobalString($key, $default='')
Return a Dolibarr global constant string value.
dol_syslog($message, $level=LOG_INFO, $ident=0, $suffixinfilename='', $restricttologhandler='', $logcontext=null)
Write log message into outputs.
getURLContent($url, $postorget='GET', $param='', $followlocation=1, $addheaders=array(), $allowedschemes=array('http', 'https'), $localurl=0, $ssl_verifypeer=-1, $timeoutconnect=0, $timeoutresponse=0, $otherCurlOptions=array(), $morelogsuffix='')
Function to get a content from an URL (use proxy if proxy defined).