dolibarr 23.0.3
multicurrency.class.php
Go to the documentation of this file.
1<?php
2/* Copyright (C) 2007-2020 Laurent Destailleur <eldy@users.sourceforge.net>
3 * Copyright (C) 2014 Juanjo Menent <jmenent@2byte.es>
4 * Copyright (C) 2015 Florian Henry <florian.henry@open-concept.pro>
5 * Copyright (C) 2015 Raphaël Doursenaud <rdoursenaud@gpcsolutions.fr>
6 * Copyright (C) 2016 Pierre-Henry Favre <phf@atm-consulting.fr>
7 * Copyright (C) 2024-2025 Frédéric France <frederic.france@free.fr>
8 * Copyright (C) 2024-2025 MDW <mdeweerd@users.noreply.github.com>
9 *
10 * This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License as published by
12 * the Free Software Foundation; either version 3 of the License, or
13 * (at your option) any later version.
14 *
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 * GNU General Public License for more details.
19 *
20 * You should have received a copy of the GNU General Public License
21 * along with this program. If not, see <https://www.gnu.org/licenses/>.
22 */
23
30require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';
31require_once DOL_DOCUMENT_ROOT.'/multicurrency/class/currencyrate.class.php';
32
40{
44 public $element = 'multicurrency';
45
49 public $table_element = 'multicurrency';
50
54 public $table_element_line = "multicurrency_rate";
55
59 public $rates = array();
60
64 public $id;
65
69 public $code;
70
74 public $name;
75
79 public $date_create;
80
84 public $fk_user;
85
89 public $rate;
90
94 public $urlendpoint;
95
96
97 const MULTICURRENCY_APP_ENDPOINT_DEFAULT = 'https://api.currencylayer.com/live?access_key=__MULTICURRENCY_APP_KEY__&source=__MULTICURRENCY_APP_SOURCE__';
98
99
105 public function __construct(DoliDB $db)
106 {
107 $this->db = $db;
108
109 $key = getDolGlobalString("MULTICURRENCY_APP_KEY");
110 $source = getDolGlobalString('MULTICURRENCY_APP_SOURCE', 'USD');
111 $urlendpoint = getDolGlobalString("MULTICURRENCY_APP_ENDPOINT", self::MULTICURRENCY_APP_ENDPOINT_DEFAULT);
112
113 $this->urlendpoint = str_replace(array('__MULTICURRENCY_APP_KEY__', '__MULTICURRENCY_APP_SOURCE__'), array($key, $source), $urlendpoint);
114 }
115
123 public function create(User $user, $notrigger = 0)
124 {
125 global $conf, $langs;
126
127 dol_syslog('MultiCurrency::create', LOG_DEBUG);
128
129 $error = 0;
130
131 if (self::checkCodeAlreadyExists($this->code)) {
132 $error++;
133 $this->errors[] = $langs->trans('multicurrency_code_already_added');
134 return -1;
135 }
136
137 if (empty($this->entity) || $this->entity <= 0) {
138 $this->entity = $conf->entity;
139 }
140 $now = dol_now();
141
142 // Insert request
143 $sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element."(";
144 $sql .= ' code,';
145 $sql .= ' name,';
146 $sql .= ' entity,';
147 $sql .= ' date_create,';
148 $sql .= ' fk_user';
149 $sql .= ') VALUES (';
150 $sql .= " '".$this->db->escape($this->code)."',";
151 $sql .= " '".$this->db->escape($this->name)."',";
152 $sql .= " ".((int) $this->entity).",";
153 $sql .= " '".$this->db->idate($now)."',";
154 $sql .= " ".((int) $user->id);
155 $sql .= ')';
156
157 $this->db->begin();
158
159 dol_syslog(__METHOD__, LOG_DEBUG);
160 $resql = $this->db->query($sql);
161 if (!$resql) {
162 $error++;
163 $this->errors[] = 'Error '.$this->db->lasterror();
164 dol_syslog('MultiCurrency::create '.implode(',', $this->errors), LOG_ERR);
165 }
166
167 if (!$error) {
168 $this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.$this->table_element);
169 $this->date_create = $now;
170 $this->fk_user = $user->id;
171
172 if (empty($notrigger)) {
173 $result = $this->call_trigger('CURRENCY_CREATE', $user);
174 if ($result < 0) {
175 $error++;
176 }
177 }
178 }
179
180 if ($error) {
181 $this->db->rollback();
182
183 return -1 * $error;
184 } else {
185 $this->db->commit();
186
187 return $this->id;
188 }
189 }
190
198 public function fetch($id, $code = null)
199 {
200 dol_syslog('MultiCurrency::fetch', LOG_DEBUG);
201
202 $sql = "SELECT c.rowid, c.name, c.code, c.entity, c.date_create, c.fk_user";
203 $sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." AS c";
204 if (!empty($code)) {
205 $sql .= " WHERE c.code = '".$this->db->escape($code)."'";
206 $sql .= " AND c.entity IN (".getEntity($this->element).")";
207 } else {
208 $sql .= ' WHERE c.rowid = '.((int) $id);
209 }
210
211 dol_syslog(__METHOD__, LOG_DEBUG);
212 $resql = $this->db->query($sql);
213
214 if ($resql) {
215 $numrows = $this->db->num_rows($resql);
216 if ($numrows) {
217 $obj = $this->db->fetch_object($resql);
218
219 $this->id = $obj->rowid;
220 $this->name = $obj->name;
221 $this->code = $obj->code;
222 $this->entity = $obj->entity;
223 $this->date_create = $obj->date_create;
224 $this->fk_user = $obj->fk_user;
225
226 $this->fetchAllCurrencyRate();
227 $this->getRate();
228 }
229 $this->db->free($resql);
230
231 if ($numrows) {
232 return 1;
233 } else {
234 return 0;
235 }
236 } else {
237 $this->errors[] = 'Error '.$this->db->lasterror();
238 dol_syslog('MultiCurrency::fetch '.implode(',', $this->errors), LOG_ERR);
239
240 return -1;
241 }
242 }
243
249 public function fetchAllCurrencyRate()
250 {
251 $sql = "SELECT cr.rowid";
252 $sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element_line." as cr";
253 $sql .= " WHERE cr.entity IN (".getEntity($this->element).")";
254 $sql .= " AND cr.fk_multicurrency = ".((int) $this->id);
255 $sql .= " ORDER BY cr.date_sync DESC";
256
257 $this->rates = array();
258
259 dol_syslog(__METHOD__, LOG_DEBUG);
260 $resql = $this->db->query($sql);
261 if ($resql) {
262 $num = $this->db->num_rows($resql);
263
264 while ($obj = $this->db->fetch_object($resql)) {
265 $rate = new CurrencyRate($this->db);
266 $rate->fetch($obj->rowid);
267
268 $this->rates[] = $rate;
269 }
270 $this->db->free($resql);
271
272 return $num;
273 } else {
274 $this->errors[] = 'Error '.$this->db->lasterror();
275 dol_syslog('MultiCurrency::fetchAllCurrencyRate '.implode(',', $this->errors), LOG_ERR);
276
277 return -1;
278 }
279 }
280
288 public function update(User $user, $notrigger = 0)
289 {
290 $error = 0;
291
292 dol_syslog('MultiCurrency::update', LOG_DEBUG);
293
294 // Clean parameters
295 $this->name = trim($this->name);
296 $this->code = trim($this->code);
297
298 // Check parameters
299 if (empty($this->code)) {
300 $error++;
301 dol_syslog('MultiCurrency::update $this->code can not be empty', LOG_ERR);
302
303 return -1;
304 }
305
306 // Update request
307 $sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET";
308 $sql .= " name = '".$this->db->escape($this->name)."',";
309 $sql .= " code = '".$this->db->escape($this->code)."'";
310 $sql .= " WHERE rowid = ".((int) $this->id);
311
312 $this->db->begin();
313
314 $resql = $this->db->query($sql);
315 if (!$resql) {
316 $error++;
317 $this->errors[] = 'Error '.$this->db->lasterror();
318 dol_syslog('MultiCurrency::update '.implode(',', $this->errors), LOG_ERR);
319 }
320
321 if (!$error && empty($notrigger)) {
322 $result = $this->call_trigger('CURRENCY_MODIFY', $user);
323 if ($result < 0) {
324 $error++;
325 }
326 }
327
328 // Commit or rollback
329 if ($error) {
330 $this->db->rollback();
331
332 return -1 * $error;
333 } else {
334 $this->db->commit();
335
336 return 1;
337 }
338 }
339
347 public function delete(User $user, $notrigger = 0)
348 {
349 dol_syslog('MultiCurrency::delete', LOG_DEBUG);
350
351 $error = 0;
352
353 $this->db->begin();
354
355 if (empty($notrigger)) {
356 $result = $this->call_trigger('CURRENCY_DELETE', $user);
357 if ($result < 0) {
358 $error++;
359 }
360 }
361
362 if (!$error) {
363 // Delete all rates before
364 if (!$this->deleteRates()) {
365 $error++;
366 $this->errors[] = 'Error '.$this->db->lasterror();
367 dol_syslog('Currency::delete '.implode(',', $this->errors), LOG_ERR);
368 }
369
370 $sql = "DELETE FROM ".MAIN_DB_PREFIX.$this->table_element;
371 $sql .= " WHERE rowid = ".((int) $this->id);
372
373 dol_syslog(__METHOD__, LOG_DEBUG);
374 $resql = $this->db->query($sql);
375 if (!$resql) {
376 $error++;
377 $this->errors[] = 'Error '.$this->db->lasterror();
378 dol_syslog('MultiCurrency::delete '.implode(',', $this->errors), LOG_ERR);
379 }
380 }
381
382 // Commit or rollback
383 if ($error) {
384 $this->db->rollback();
385
386 return -1 * $error;
387 } else {
388 $this->db->commit();
389
390 return 1;
391 }
392 }
393
399 public function deleteRates()
400 {
401 global $user;
402
403 foreach ($this->rates as &$rate) {
404 if ($rate->delete($user) <= 0) {
405 return false;
406 }
407 }
408
409 return true;
410 }
411
418 public function addRate($rate)
419 {
420 global $user;
421
422 $currencyRate = new CurrencyRate($this->db);
423 $currencyRate->rate = (float) price2num($rate);
424
425 if ($currencyRate->create($user, $this->id) > 0) {
426 $this->rate = $currencyRate;
427 return 1;
428 } else {
429 $this->rate = null;
430 $this->errors = $currencyRate->errors;
431 return -1;
432 }
433 }
434
442 public function addRateFromDolibarr($code, $rate)
443 {
444 global $user;
445
446 $currency = new MultiCurrency($this->db);
447 $currency->code = $code;
448 $currency->name = $code;
449
450 $sql = "SELECT label FROM ".MAIN_DB_PREFIX."c_currencies WHERE code_iso = '".$this->db->escape($code)."'";
451
452 dol_syslog(__METHOD__, LOG_DEBUG);
453 $resql = $this->db->query($sql);
454 if ($resql && ($line = $this->db->fetch_object($resql))) {
455 $currency->name = $line->label;
456 }
457
458 if ($currency->create($user) > 0) {
459 $currency->addRate($rate);
460
461 if (!empty($line)) {
462 return 2;
463 } else {
464 return 1;
465 }
466 }
467
468 return -1;
469 }
470
477 public function updateRate($rate)
478 {
479 return $this->addRate($rate);
480 }
481
487 public function getRate()
488 {
489 $sql = "SELECT cr.rowid";
490 $sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element_line." as cr";
491 $sql .= " WHERE cr.entity IN (".getEntity($this->element).")";
492 $sql .= " AND cr.fk_multicurrency = ".((int) $this->id);
493 $sql .= " AND cr.date_sync = (SELECT MAX(cr2.date_sync) FROM ".MAIN_DB_PREFIX.$this->table_element_line." AS cr2";
494 $sql .= " WHERE cr2.entity IN (".getEntity($this->element).") AND cr2.fk_multicurrency = ".((int) $this->id).")";
495
496 dol_syslog(__METHOD__, LOG_DEBUG);
497
498 $resql = $this->db->query($sql);
499 if ($resql && ($obj = $this->db->fetch_object($resql))) {
500 $this->rate = new CurrencyRate($this->db);
501 return $this->rate->fetch($obj->rowid);
502 }
503
504 return -1;
505 }
506
515 public static function getIdFromCode($dbs, $code)
516 {
517 global $conf;
518
519 $sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."multicurrency WHERE code = '".$dbs->escape($code)."' AND entity = ".((int) $conf->entity);
520
521 dol_syslog(__METHOD__, LOG_DEBUG);
522 $resql = $dbs->query($sql);
523 if ($resql && $obj = $dbs->fetch_object($resql)) {
524 return $obj->rowid;
525 } else {
526 return 0;
527 }
528 }
529
540 public static function getIdAndTxFromCode($dbs, $code, $date_document = 0)
541 {
542 $sql1 = "SELECT m.rowid, mc.rate FROM ".MAIN_DB_PREFIX."multicurrency m";
543 $sql1 .= ' LEFT JOIN '.MAIN_DB_PREFIX.'multicurrency_rate mc ON (m.rowid = mc.fk_multicurrency)';
544 $sql1 .= " WHERE m.code = '".$dbs->escape($code)."'";
545 $sql1 .= " AND m.entity IN (".getEntity('multicurrency').")";
546 $sql2 = '';
547 if (getDolGlobalString('MULTICURRENCY_USE_RATE_ON_DOCUMENT_DATE') && !empty($date_document)) { // Use last known rate compared to document date
548 $tmparray = dol_getdate($date_document);
549 $sql2 .= " AND mc.date_sync <= '".$dbs->idate(dol_mktime(23, 59, 59, $tmparray['mon'], $tmparray['mday'], $tmparray['year'], true))."'";
550 }
551 $sql3 = " ORDER BY mc.date_sync DESC LIMIT 1";
552
553 dol_syslog(__METHOD__, LOG_DEBUG);
554 $resql = $dbs->query($sql1.$sql2.$sql3);
555
556 if ($resql && $obj = $dbs->fetch_object($resql)) {
557 return array($obj->rowid, $obj->rate);
558 } else {
559 if (getDolGlobalString('MULTICURRENCY_USE_RATE_ON_DOCUMENT_DATE')) {
560 $resql = $dbs->query($sql1.$sql3);
561 if ($resql && $obj = $dbs->fetch_object($resql)) {
562 return array($obj->rowid, $obj->rate);
563 }
564 }
565
566 return array(0, 1);
567 }
568 }
569
580 public static function getAmountConversionFromInvoiceRate($fk_facture, $amount, $way = 'dolibarr', $table = 'facture', $invoice_rate = null)
581 {
582 if (!is_null($invoice_rate)) {
583 $multicurrency_tx = $invoice_rate;
584 } else {
585 $tmparray = self::getInvoiceRate($fk_facture, $table);
586 $multicurrency_tx = $tmparray['invoice_multicurrency_tx'];
587 }
588
589 if ($multicurrency_tx) {
590 if ($way == 'dolibarr') {
591 return (float) price2num($amount * $multicurrency_tx, 'MU');
592 } else {
593 return (float) price2num($amount / $multicurrency_tx, 'MU');
594 }
595 } else {
596 return false;
597 }
598 }
599
607 public static function getInvoiceRate($fk_facture, $table = 'facture')
608 {
609 global $db;
610
611 $sql = "SELECT multicurrency_tx, multicurrency_code";
612 $sql .= " FROM ".MAIN_DB_PREFIX.$db->sanitize($table);
613 $sql .= " WHERE rowid = ".((int) $fk_facture);
614
615 dol_syslog(__METHOD__, LOG_DEBUG);
616 $resql = $db->query($sql);
617 if ($resql && ($line = $db->fetch_object($resql))) {
618 return array('invoice_multicurrency_tx' => $line->multicurrency_tx, 'invoice_multicurrency_code' => $line->multicurrency_code);
619 }
620
621 return false;
622 }
623
632 public function recalculRates(&$TRate)
633 {
634 global $conf;
635
636 if (getDolCurrency() != getDolGlobalString('MULTICURRENCY_APP_SOURCE')) {
637 $alternate_source = 'USD'.getDolCurrency();
638 if (!empty($TRate->$alternate_source)) {
639 $coef = 1 / $TRate->$alternate_source;
640 foreach ($TRate as $attr => &$rate) {
641 $rate *= $coef;
642 }
643 $TRate->USDUSD = $coef;
644 return 1;
645 }
646
647 return -1; // Alternate source not found
648 }
649
650 return 0; // Nothing to do
651 }
652
662 public function syncRates($nu = 0, $addifnotfound = 0, $mode = "")
663 {
664 global $db, $langs;
665
666 if (getDolGlobalString('MULTICURRENCY_DISABLE_SYNC_CURRENCYLAYER')) {
667 if ($mode == "cron") {
668 $this->output = $langs->trans('Use of API for currency update is disabled by option MULTICURRENCY_DISABLE_SYNC_CURRENCYLAYER');
669 } else {
670 setEventMessages($langs->trans('Use of API for currency update is disabled by option MULTICURRENCY_DISABLE_SYNC_CURRENCYLAYER'), null, 'errors');
671 }
672 return -1;
673 }
674
675 include_once DOL_DOCUMENT_ROOT.'/core/lib/geturl.lib.php';
676
677 $urlendpoint = $this->urlendpoint;
678
679 dol_syslog("Call url endpoint ".$urlendpoint);
680
681 $addheaders = array('apikey: '.getDolGlobalString('MULTICURRENCY_APP_KEY'));
682
683 $resget = getURLContent($urlendpoint, 'GET', '', 1, $addheaders);
684
685 // Example of result with https://currencylayer.com/live and https://api.apilayer.com/currency_data/live
686 // 'content' => string '{"success":true,"terms":"https:\/\/currencylayer.com\/terms","privacy":"https:\/\/currencylayer.com\/privacy","timestamp":1742562251,"source":"USD","quotes":{"USDAED":3.67302,"USDAFN":70.6213,"USDALL":91.042287,"USDAMD":390.984233,"USDANG":1.802039,"USDAOA":913.498241,"USDARS":1068.745088,"USDAUD":1.591824,"USDAWG":1.8,"USDAZN":1.699323,"USDBAM":1.80224,"USDBBD":2.018881,"USDBDT":121.488567,"USDBGN":1.802745,"USDBHD":0.376878,"USDBIF":2963.403228,"USDBMD":1,"USDBND":1.333573,"USDBOB":6.909262,"USDBRL":5.721'... (length=3337)
687 //var_dump($urlendpoint);
688 //var_dump($resget);
689
690 if (!empty($resget['content'])) {
691 $response = $resget['content'];
692 $response = json_decode($response);
693
694 if ($response->success) {
695 $TRate = $response->quotes;
696 //$timestamp = $response->timestamp;
697
698 // Recalculate rate and update it (or add it) into database
699 if ($this->recalculRates($TRate) >= 0) {
700 foreach ($TRate as $currency_code => $rate) {
701 $code = substr($currency_code, 3, 3);
702 $obj = new MultiCurrency($db);
703 if ($obj->fetch(0, $code) > 0) {
704 $obj->updateRate($rate);
705 } elseif ($addifnotfound) {
706 $this->addRateFromDolibarr($code, $rate);
707 }
708 }
709 }
710
711 if ($mode == "cron") {
712 return 0;
713 }
714 return 1;
715 } else {
716 if (isset($response->error->info)) {
717 $error_info_syslog = $response->error->info; // @phan-suppress-current-line PhanTypeExpectedObjectPropAccess
718 $error_info = $error_info_syslog;
719 } else {
720 $error_info_syslog = json_encode($response);
721 if (empty($resget['content'])) {
722 $error_info = "No error information found (see syslog)";
723 } else {
724 $error_info = $resget['content'];
725 }
726 }
727
728 dol_syslog("Failed to call endpoint ".$error_info_syslog, LOG_WARNING);
729
730 $this->output = $langs->trans('multicurrency_syncronize_error', $error_info);
731
732 return -1;
733 }
734 } else {
735 $this->output = $resget['curl_error_msg'];
736
737 return -1;
738 }
739 }
740
747 public function checkCodeAlreadyExists($code)
748 {
749 $currencytmp = new MultiCurrency($this->db);
750 if ($currencytmp->fetch(0, $code) > 0) {
751 return true;
752 } else {
753 return false;
754 }
755 }
756}
Parent class of all other business classes (invoices, contracts, proposals, orders,...
Class CurrencyRate.
Class to manage Dolibarr database access.
Class Currency.
static getInvoiceRate($fk_facture, $table='facture')
Get current invoite rate.
getRate()
Fetch CurrencyRate object in $this->rate.
static getIdAndTxFromCode($dbs, $code, $date_document=0)
Get id and rate of currency from code.
update(User $user, $notrigger=0)
Update object into database.
syncRates($nu=0, $addifnotfound=0, $mode="")
Sync rates from API.
fetchAllCurrencyRate()
Load all rates in object from the database.
static getAmountConversionFromInvoiceRate($fk_facture, $amount, $way='dolibarr', $table='facture', $invoice_rate=null)
Get the conversion of amount with invoice rate.
checkCodeAlreadyExists($code)
Check in database if the current code already exists.
updateRate($rate)
Add new entry into llx_multicurrency_rate.
static getIdFromCode($dbs, $code)
Get id of currency from code.
__construct(DoliDB $db)
Constructor.
deleteRates()
Delete rates in database.
create(User $user, $notrigger=0)
Create object into database.
addRateFromDolibarr($code, $rate)
Try get label of code in llx_currency then add rate.
recalculRates(&$TRate)
With free account we can't set source to something else than US, to we recalculate all rates to force...
fetch($id, $code=null)
Load object in memory from the database.
addRate($rate)
Add a Rate into database.
Class to manage Dolibarr users.
dol_now($mode='gmt')
Return date for now.
dol_mktime($hour, $minute, $second, $month, $day, $year, $gm='auto', $check=1)
Return a timestamp date built from detailed information (by default a local PHP server timestamp) Rep...
setEventMessages($mesg, $mesgs, $style='mesgs', $messagekey='', $noduplicate=0, $attop=0)
Set event messages in dol_events session object.
price2num($amount, $rounding='', $option=0)
Function that return a number with universal decimal format (decimal separator is '.
getDolCurrency()
Return the main currency ('EUR', 'USD', ...)
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.
dol_getdate($timestamp, $fast=false, $forcetimezone='')
Return an array with locale date info.
getURLContent($url, $postorget='GET', $param='', $followlocation=1, $addheaders=array(), $allowedschemes=array('http', 'https'), $localurl=0, $ssl_verifypeer=-1, $timeoutconnect=0, $timeoutresponse=0, $otherCurlOptions=array())
Function to get a content from an URL (use proxy if proxy defined).
$conf db name
Only used if Module[ID]Name translation string is not found.
Definition repair.php:128