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
57 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.
58 const AI_DEFAULT_PROMPT_FOR_WEBPAGE = 'You are a website editor. Return all HTML content inside a section tag. Do not add explanation.';
59 const AI_DEFAULT_PROMPT_FOR_TEXT_TRANSLATION = 'You are a translator, answer with one and only one translation with no comment and explanation.';
60 const AI_DEFAULT_PROMPT_FOR_TEXT_SUMMARIZE = 'You are a writer, make the answer in the same language than the original text to summarize.';
61 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 adding one, just fix spelling and grammar errors. Answer with the corrected text and only the corrected text with no comment and explanation.';
62 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.';
63 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.';
64 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.';
65
66
72 public function __construct($db)
73 {
74 $this->db = $db;
75
76 // Get API key according to enabled AI
77 $this->apiService = getDolGlobalString('AI_API_SERVICE', 'chatgpt');
78 $this->apiKey = getDolGlobalString('AI_API_'.strtoupper($this->apiService).'_KEY');
79 }
80
86 public function getApiService()
87 {
88 return $this->apiService;
89 }
90
102 public function generateContent($instructions, $model = 'auto', $function = 'textgeneration', $format = '', $moreheaders = array(), $moreendpoint = '')
103 {
104 global $dolibarr_main_data_root;
105
106 $arrayofai = getListOfAIServices();
107
108 // TODO Can store the need for a key into array returned by getListOfAIServices()
109 if (empty($this->apiKey) && in_array($this->apiService, array('chatgpt', 'groq', 'mistral'))) {
110 return array('error' => true, 'message' => 'API key is not defined for the AI enabled service ('.$this->apiService.')');
111 }
112
113 // $this->apiEndpoint is already set here only if it was previously forced.
114
115 if (empty($this->apiEndpoint) && $this->apiService == 'custom' && !getDolGlobalString('AI_API_CUSTOM_URL')) {
116 return array('error' => true, 'message' => 'API URL is not defined for the AI enabled service ('.$this->apiService.')');
117 }
118
119 // In most cases, it is empty and we must get it from $function and $this->apiService
120 if (empty($this->apiEndpoint)) {
121 // Return the endpoint from $this->apiService.
122 if ($function == 'imagegeneration') {
123 $this->apiEndpoint = getDolGlobalString('AI_API_'.strtoupper($this->apiService).'_URL', $arrayofai[$this->apiService]['url']);
124 $this->apiEndpoint .= (preg_match('/\/$/', $this->apiEndpoint) ? '' : '/').'images/generations';
125 } elseif ($function == 'audiogeneration') {
126 $this->apiEndpoint = getDolGlobalString('AI_API_'.strtoupper($this->apiService).'_URL', $arrayofai[$this->apiService]['url']);
127 $this->apiEndpoint .= (preg_match('/\/$/', $this->apiEndpoint) ? '' : '/').'audio/speech';
128 } elseif ($function == 'transcription') {
129 $this->apiEndpoint = getDolGlobalString('AI_API_'.strtoupper($this->apiService).'_URL', $arrayofai[$this->apiService]['url']);
130 $this->apiEndpoint .= (preg_match('/\/$/', $this->apiEndpoint) ? '' : '/').'transcriptions';
131 } elseif ($function == 'file') {
132 $this->apiEndpoint = getDolGlobalString('AI_API_'.strtoupper($this->apiService).'_URL', $arrayofai[$this->apiService]['url']);
133 $this->apiEndpoint .= (preg_match('/\/$/', $this->apiEndpoint) ? '' : '/').'files';
134 } elseif ($function == 'assistant') {
135 $this->apiEndpoint = getDolGlobalString('AI_API_'.strtoupper($this->apiService).'_URL', $arrayofai[$this->apiService]['url']);
136 $this->apiEndpoint .= (preg_match('/\/$/', $this->apiEndpoint) ? '' : '/').'assistans';
137 } elseif ($function == 'thread') {
138 $this->apiEndpoint = getDolGlobalString('AI_API_'.strtoupper($this->apiService).'_URL', $arrayofai[$this->apiService]['url']);
139 $this->apiEndpoint .= (preg_match('/\/$/', $this->apiEndpoint) ? '' : '/').'threads';
140 } else { // if $function == 'docparsing', 'text...', ...
141 $this->apiEndpoint = getDolGlobalString('AI_API_'.strtoupper($this->apiService).'_URL', $arrayofai[$this->apiService]['url']);
142 if ($this->apiService == 'google') {
143 // Google Gemini native API: the /models/<model>:generateContent suffix is
144 // appended later (once $model has been resolved). The OpenAI-style
145 // /chat/completions does not exist on the native Gemini endpoint.
146 $this->apiEndpoint = rtrim($this->apiEndpoint, '/');
147 } else {
148 $this->apiEndpoint .= (preg_match('/\/$/', $this->apiEndpoint) ? '' : '/').'chat/completions';
149 }
150 }
151 }
152 if ($moreendpoint) {
153 $this->apiEndpoint .= '/'.$moreendpoint;
154 }
155
156
157 // $model may be undefined or 'auto'.
158 // If this is the case, we must get it from $function and $this->apiService
159 if (empty($model) || $model == 'auto') {
160 // Return the model from $this->apiService.
161 if (in_array($function, array('file', 'assistant', 'thread'))) {
162 $model = '';
163 } elseif ($function == 'imagegeneration') {
164 $model = getDolGlobalString('AI_API_'.strtoupper($this->apiService).'_MODEL_IMAGE', $arrayofai[$this->apiService][$function]['default']);
165 } elseif ($function == 'audiogeneration') {
166 $model = getDolGlobalString('AI_API_'.strtoupper($this->apiService).'_MODEL_AUDIO', $arrayofai[$this->apiService][$function]['default']);
167 } elseif ($function == 'transcription') {
168 $model = getDolGlobalString('AI_API_'.strtoupper($this->apiService).'_MODEL_TRANSCRIPT', $arrayofai[$this->apiService][$function]['default']);
169 } elseif ($function == 'translation') {
170 $model = getDolGlobalString('AI_API_'.strtoupper($this->apiService).'_MODEL_TRANSLATE', $arrayofai[$this->apiService][$function]['default']);
171 } elseif ($function == 'docparsing') {
172 $model = getDolGlobalString('AI_API_'.strtoupper($this->apiService).'_MODEL_DOCPARSING', $arrayofai[$this->apiService][$function]['default']);
173 } else {
174 // else 'textgenerationemail', 'textgenerationwebpage', 'textgeneration', 'texttranslation', 'textsummarize', 'textrephraser', 'textspellchecker', ...
175 $model = getDolGlobalString('AI_API_'.strtoupper($this->apiService).'_MODEL_TEXT', $arrayofai[$this->apiService]['textgeneration']['default']);
176 }
177 }
178
179 // Google Gemini: append /models/<model>:generateContent now that $model is resolved.
180 if ($this->apiService == 'google' && !in_array($function, array('file', 'assistant', 'thread'))
181 && strpos($this->apiEndpoint, ':generateContent') === false) {
182 $this->apiEndpoint .= '/models/'.rawurlencode($model).':generateContent';
183 }
184
185 dol_syslog("Call API for apiKey=".substr($this->apiKey, 0, 5).'***********, apiEndpoint='.$this->apiEndpoint.", model=".$model.", format=".$format);
186 if (getDolGlobalString("AI_DEBUG")) {
187 if (@is_writable($dolibarr_main_data_root)) { // Avoid fatal error on fopen with open_basedir
188 $outputfile = $dolibarr_main_data_root."/dolibarr_ai.log";
189 $fp = fopen($outputfile, "w"); // overwrite
190
191 if ($fp) {
192 fwrite($fp, "Call API for apiKey=".substr($this->apiKey, 0, 5).'***********, apiEndpoint='.$this->apiEndpoint.", model=".$model.", format=".$format."\n");
193 fclose($fp);
194 dolChmod($outputfile);
195 }
196 }
197 }
198
199 $response = null;
200
201 try {
202 if (empty($this->apiEndpoint)) {
203 throw new Exception('The AI service '.$this->apiService.' is not yet supported for the type of request '.$function);
204 }
205
206 $configurationsJson = getDolGlobalString('AI_CONFIGURATIONS_PROMPT');
207 $configurations = json_decode($configurationsJson, true);
208
209 $prePrompt = '';
210 $postPrompt = '';
211
212 if (isset($configurations[$function])) {
213 if (isset($configurations[$function]['prePrompt'])) {
214 $prePrompt = $configurations[$function]['prePrompt'];
215 }
216
217 if (isset($configurations[$function]['postPrompt'])) {
218 $postPrompt = $configurations[$function]['postPrompt'];
219 }
220 }
221 //var_dump($prePrompt);
222
223 // Get the default value of prePrompt if not defined
224 if (empty($prePrompt) && $function == 'textgenerationemail') {
225 $prePrompt = self::AI_DEFAULT_PROMPT_FOR_EMAIL;
226 if ($format === 'html') {
227 $prePrompt .= ' Return all HTML content inside a section tag';
228 } else {
229 $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.';
230 }
231 }
232 if (empty($prePrompt) && $function == 'textgenerationwebpage') {
233 $prePrompt = self::AI_DEFAULT_PROMPT_FOR_WEBPAGE;
234 }
235 if (empty($prePrompt) && $function == 'textgenerationextrafield') {
236 $prePrompt = self::AI_DEFAULT_PROMPT_FOR_EXTRAFIELD_FILLER;
237 }
238 if (empty($prePrompt) && $function == 'texttranslation') {
239 $prePrompt = self::AI_DEFAULT_PROMPT_FOR_TEXT_TRANSLATION;
240 }
241 if (empty($prePrompt) && $function == 'textsummarize') {
242 $prePrompt = self::AI_DEFAULT_PROMPT_FOR_TEXT_SUMMARIZE;
243 }
244 if (empty($prePrompt) && $function == 'textrephraser') {
245 $prePrompt = self::AI_DEFAULT_PROMPT_FOR_TEXT_REPHRASER;
246 }
247 if (empty($prePrompt) && $function == 'textspellchecker') {
248 $prePrompt = self::AI_DEFAULT_PROMPT_FOR_TEXT_SPELLCHECKER;
249 }
250 if (empty($prePrompt) && $function == 'docparsing') {
251 $prePrompt = self::AI_DEFAULT_PROMPT_FOR_DOC_PARSING;
252 }
253
254 if (is_array($instructions)) {
255 $arrayforpayload = $instructions;
256 $fullInstructions = '';
257 } else {
258 $fullInstructions = $instructions.($postPrompt ? (preg_match('/[\.\!\?]$/', $instructions) ? '' : '.').' '.$postPrompt : '');
259
260 // Set payload string
261 /*{
262 "messages": [
263 {
264 "content": "You are a helpful assistant.",
265 "role": "system"
266 },
267 {
268 "content": "Hello!",
269 "role": "user"
270 }
271 ],
272 "model": "tinyllama-1.1b",
273 "stream": true,
274 "max_tokens": 2048,
275 "stop": [
276 "hello"
277 ],
278 "frequency_penalty": 0,
279 "presence_penalty": 0,
280 "temperature": 0.7,
281 "top_p": 0.95
282 }*/
283
284 // Add a system message
285 $addDateTimeContext = false;
286 if ($addDateTimeContext) { // @phpstan-ignore-line
287 $prePrompt = ($prePrompt ? $prePrompt.(preg_match('/[\.\!\?]$/', $prePrompt) ? '' : '.').' ' : '').'Today we are '.dol_print_date(dol_now(), 'dayhourtext');
288 }
289
290 if ($this->apiService == 'google') {
291 // Google Gemini native payload format (different from OpenAI's "messages").
292 $arrayforpayload = array(
293 'contents' => array(
294 array('role' => 'user', 'parts' => array(array('text' => $fullInstructions)))
295 )
296 );
297 if ($prePrompt) {
298 $arrayforpayload['system_instruction'] = array(
299 'parts' => array(array('text' => $prePrompt))
300 );
301 }
302 } else {
303 // OpenAI-compatible payload format (chatgpt, mistral, groq, anthropic-compat, custom, ...)
304 $arrayforpayload = array(
305 'messages' => array(array('role' => 'user', 'content' => $fullInstructions)),
306 'model' => $model,
307 );
308 if ($prePrompt) {
309 $arrayforpayload['messages'][] = array('role' => 'system', 'content' => $prePrompt);
310 }
311 }
312 }
313
314 /*
315 $arrayforpayload['temperature'] = 0.7;
316 $arrayforpayload['max_tokens'] = -1;
317 $arrayforpayload['stream'] = false;
318 */
319
320 if ($function == 'thread') {
321 $payload = $instructions;
322 } else {
323 $payload = json_encode($arrayforpayload);
324 }
325
326 if ($this->apiService == 'google') {
327 // Google Gemini uses the x-goog-api-key header (Bearer is not accepted by the native API).
328 $headers = array(
329 'x-goog-api-key: ' . $this->apiKey,
330 );
331 } else {
332 $headers = array(
333 'Authorization: Bearer ' . $this->apiKey,
334 );
335 }
336 if ($function != 'file') {
337 $headers[] = 'Content-Type: application/json';
338 }
339 if (!empty($moreheaders)) {
340 foreach ($moreheaders as $morekey => $moreval) {
341 $headers[] = $morekey.': '.$moreval;
342 }
343 }
344
345 if (getDolGlobalString("AI_DEBUG")) {
346 if (@is_writable($dolibarr_main_data_root)) { // Avoid fatal error on fopen with open_basedir
347 $outputfile = $dolibarr_main_data_root."/dolibarr_ai.log";
348 $fp = fopen($outputfile, "a");
349
350 if ($fp) {
351 if ($function == 'docparsing') {
352 fwrite($fp, "Call endpoint ".$this->apiEndpoint." with POST and the following file to upload:\n");
353 fwrite($fp, $instructions."\n");
354 } else {
355 fwrite($fp, "Call endpoint ".$this->apiEndpoint." with POST and the following message:\n");
356 fwrite($fp, $fullInstructions."\n");
357 fwrite($fp, "And prepompt:\n");
358 fwrite($fp, $prePrompt."\n");
359 }
360 fwrite($fp, "HTTP Header\n");
361 fwrite($fp, var_export($headers, true)."\n");
362 fwrite($fp, "Payload\n");
363 fwrite($fp, var_export($payload, true)."\n");
364
365 fclose($fp);
366 dolChmod($outputfile);
367 }
368 }
369 }
370
371 // By default, we accept only external endpoints ($dolibarr_ai_allow_local_endpoints is not set).
372 // To allow local endpoints, we must set $dolibarr_ai_allow_local_endpoints to 1 or 2 in conf.php.
373 global $dolibarr_ai_allow_local_endpoints;
374 $localurl = $dolibarr_ai_allow_local_endpoints ?? 0;
375
376 $response = getURLContent($this->apiEndpoint, 'POST', $payload, 1, $headers, array('http', 'https'), $localurl);
377
378 if (empty($response['http_code'])) {
379 throw new Exception('API request failed. No http received');
380 }
381 if (!empty($response['http_code']) && $response['http_code'] != 200) {
382 if (in_array($response['http_code'], array(400, 401, 403, 429)) && !empty($response['content'])) {
383 $tmp = json_decode($response['content'], true);
384 if (!empty($tmp['message'])) {
385 return array(
386 'error' => true,
387 'message' => $tmp['message'],
388 'code' => (empty($response['http_code']) ? 0 : $response['http_code']),
389 'curl_error_no' => (empty($response['curl_error_no']) ? 0 : $response['curl_error_no']),
390 'format' => $format,
391 'service' => $this->apiService,
392 'function' => $function
393 );
394 }
395 }
396 throw new Exception('API request on AI endpoint '.$this->apiEndpoint.' failed with status code '.$response['http_code']);
397 }
398
399 if (getDolGlobalString("AI_DEBUG")) {
400 if (@is_writable($dolibarr_main_data_root)) { // Avoid fatal error on fopen with open_basedir
401 $outputfile = $dolibarr_main_data_root."/dolibarr_ai.log";
402 $fp = fopen($outputfile, "a");
403
404 if ($fp) {
405 fwrite($fp, "Answer\n");
406 fwrite($fp, var_export((empty($response['content']) ? 'No content result' : $response['content']), true)."\n");
407
408 fclose($fp);
409 dolChmod($outputfile);
410 }
411 }
412 }
413
414
415 // Decode JSON response
416 $decodedResponse = json_decode($response['content'], true);
417
418 // Extraction content
419 if (!empty($decodedResponse['error'])) {
420 if (is_scalar($decodedResponse['error'])) {
421 $generatedContent = $decodedResponse['error'];
422 } else {
423 $generatedContent = var_export($decodedResponse['error'], true);
424 }
425 } elseif ($this->apiService == 'google') {
426 // Google Gemini response shape: candidates[0].content.parts[*].text
427 // (parts is an array because Gemini can return mixed-modality output;
428 // we concatenate the textual parts.)
429 $generatedContent = '';
430 if (!empty($decodedResponse['candidates'][0]['content']['parts'])) {
431 foreach ($decodedResponse['candidates'][0]['content']['parts'] as $part) {
432 if (isset($part['text'])) {
433 $generatedContent .= $part['text'];
434 }
435 }
436 }
437 } else {
438 $generatedContent = $decodedResponse['choices'][0]['message']['content'];
439 }
440 dol_syslog("ai->generatedContent returned: ".dol_trunc($generatedContent, 50));
441
442 // If content is not HTML, we convert it into HTML
443 if ($format == 'html') {
444 if (!dol_textishtml($generatedContent)) {
445 dol_syslog("Result was detected as not HTML so we convert it into HTML.");
446 $generatedContent = dol_nl2br($generatedContent);
447 } else {
448 dol_syslog("Result was detected as already HTML. Do nothing.");
449 }
450
451 // TODO If content is for website module, we must
452 // - clan html header, keep body only and remove ``` ticks added by AI
453 // - add tags <section contenEditable="true"> </section>
454 }
455
456 return $generatedContent;
457 } catch (Exception $e) {
458 $errormessage = $e->getMessage();
459 $errormessagelog = $e->getMessage();
460 if (!empty($response['content'])) {
461 $decodedResponse = json_decode($response['content'], true);
462 $errormessagelog .= ' - '.$response['content'];
463
464 if (!empty($decodedResponse['error']['message'])) {
465 // With OpenAI, error is into an object error into the content
466 $errormessage .= ' - '.$decodedResponse['error']['message'];
467 } else {
468 $errormessage .= ' - '.$response['content'];
469 }
470 }
471
472 if (getDolGlobalString("AI_DEBUG")) {
473 if (@is_writable($dolibarr_main_data_root)) { // Avoid fatal error on fopen with open_basedir
474 $outputfile = $dolibarr_main_data_root."/dolibarr_ai.log";
475 $fp = fopen($outputfile, "a");
476
477 if ($fp) {
478 fwrite($fp, "Error: ".$errormessagelog."\n");
479
480 fclose($fp);
481 dolChmod($outputfile);
482 }
483 }
484 }
485
486 return array(
487 'error' => true,
488 'message' => $errormessage,
489 'code' => (empty($response['http_code']) ? 0 : $response['http_code']),
490 'curl_error_no' => (empty($response['curl_error_no']) ? 0 : $response['curl_error_no']),
491 'format' => $format,
492 'service' => $this->apiService,
493 'function' => $function
494 );
495 }
496 }
497
505 public function decodeJsonIntoArray($json, $type)
506 {
507 $tmparray = array();
508
509 //var_dump($json['items']);
510 if ($type == 'supplier_invoice') {
511 // Invoice info
512 if (!empty($json['document_info']['reference'])) {
513 $tmparray['supplierref'] = $json['document_info']['reference'];
514 } elseif (!empty($json['document_info']['invoice_number'])) {
515 $tmparray['supplierref'] = $json['document_info']['invoice_number'];
516 }
517
518 if (!empty($json['document_info']['title'])) {
519 $tmparray['title'] = $json['document_info']['title'];
520 }
521
522 if (!empty($json['document_info']['issue_date']) && preg_match('/^([0-9]{4})-([0-9]{2})-([0-9]{2})$/', $json['document_info']['issue_date'])) {
523 $tmparray['issue_date'] = dol_stringtotime($json['document_info']['issue_date'], 'tzuserrel');
524 }
525 if (!empty($json['document_info']['due_date']) && preg_match('/^([0-9]{4})-([0-9]{2})-([0-9]{2})$/', $json['document_info']['due_date'])) {
526 $tmparray['due_date'] = dol_stringtotime($json['document_info']['due_date'], 'tzuserrel');
527 }
528 // Currency
529 if ($json['summary']['currency'] == '€') {
530 $tmparray['currency_code'] = 'EUR';
531 } elseif (strlen($json['summary']['currency']) == 3) {
532 $tmparray['currency_code'] = $json['summary']['currency'];
533 }
534
535 // Vendor
536 if (!empty($json['document_info']['vendor'])) {
537 if (!empty($json['document_info']['vendor']['name'])) {
538 $tmparray['vendor_name'] = $json['document_info']['vendor']['name'];
539 }
540 if (!empty($json['document_info']['vendor']['siren'])) {
541 $tmparray['vendor_profid1'] = $json['document_info']['vendor']['siren'];
542 }
543 if (!empty($json['document_info']['vendor']['siret'])) {
544 $tmparray['vendor_profid2'] = $json['document_info']['vendor']['siret'];
545 }
546 if (!empty($json['document_info']['vendor']['email'])) {
547 $tmparray['vendor_email'] = $json['document_info']['vendor']['email'];
548 }
549 if (!empty($json['document_info']['vendor']['professional_id'])) {
550 $tmparray['vendor_profid1'] = $json['document_info']['vendor']['professional_id']['siren'];
551 }
552 if (!empty($json['document_info']['vendor']['vat_number'])) {
553 $tmparray['vendor_vat_number'] = $json['document_info']['vendor']['vat_number'];
554 }
555 }
556
557 if (empty($json['items'])) {
558 if (!empty($json['summary']['subtotal_excluding_tax'])) {
559 $tmparray['description'] = 'Undefined';
560 $tmparray['total_ht'] = (float) $json['summary']['subtotal_excluding_tax'];
561 $tmparray['vat_rate'] = (float) $json['summary']['tax']['rate'];
562 }
563 } else {
564 $i = 0;
565 foreach ($json['items'] as $item) {
566 $i++;
567 $tmparray['lines'][$i] = array();
568
569 if (!empty($item['description'])) {
570 $tmparray['lines'][$i]['desc'] = $item['description'];
571 } elseif (!empty($item['service'] && is_string($item['service']))) {
572 $tmparray['lines'][$i]['desc'] = $item['service'];
573 }
574
575 if (!empty($item['service'])) {
576 $tmparray['lines'][$i]['qty'] = $item['quantity'];
577 $tmparray['lines'][$i]['vat_rate'] = $item['tax']['vat_rate'];
578 //$tmparray['lines'][$i]['vat_amount'] = $item['tax']['amount'];
579 $tmparray['lines'][$i]['subprice'] = $item['unit_price'];
580 $tmparray['lines'][$i]['total_ht'] = $item['total_excluding_tax'];
581 $tmparray['lines'][$i]['total_ttc'] = $item['total_including_tax'];
582 } else {
583 $tmparray['lines'][$i]['qty'] = $item['quantity'];
584 $tmparray['lines'][$i]['vat_rate'] = $item['tax']['rate'];
585 $tmparray['lines'][$i]['vat_amount'] = $item['tax']['amount'];
586 $tmparray['lines'][$i]['subprice'] = $item['unit_price'];
587 $tmparray['lines'][$i]['total_ht'] = $item['total_excluding_tax'];
588 $tmparray['lines'][$i]['total_ttc'] = $item['total_including_tax'];
589 }
590 if (!empty($item['period_start'])) {
591 $tmparray['lines'][$i]['date_start'] = dol_stringtotime($item['period_start'], 'tzuserrel');
592 }
593 if (!empty($item['period_end'])) {
594 $tmparray['lines'][$i]['date_end'] = dol_stringtotime($item['period_end'], 'tzuserrel');
595 }
596 if (!empty($item['period'])) {
597 $tmparray['lines'][$i]['date_start'] = dol_stringtotime($item['period']['start_date'], 'tzuserrel');
598 $tmparray['lines'][$i]['date_end'] = dol_stringtotime($item['period']['end_date'], 'tzuserrel');
599 }
600 }
601 }
602 }
603
604 return $tmparray;
605 }
606}
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:86
generateContent($instructions, $model='auto', $function='textgeneration', $format='', $moreheaders=array(), $moreendpoint='')
Generate the response of an AI prompt.
Definition ai.class.php:102
__construct($db)
Constructor.
Definition ai.class.php:72
decodeJsonIntoArray($json, $type)
Decode JSON into array.
Definition ai.class.php:505
dol_stringtotime($string, $gm=1)
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:436
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.
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).