* Copyright (C) 2014 Juanjo Menent * Copyright (C) 2015 Florian Henry * Copyright (C) 2015 Raphaël Doursenaud * Copyright (C) 2016 Pierre-Henry Favre * Copyright (C) 2024 Frédéric France * Copyright (C) 2024 MDW * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ /** * \file htdocs/multicurrency/class/multicurrency.class.php * \ingroup multicurrency * \brief This file is a CRUD class file (Create/Read/Update/Delete) for multicurrency */ // Put here all includes required by your class file require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php'; require_once DOL_DOCUMENT_ROOT.'/core/class/commonobjectline.class.php'; /** * Class Currency * * Put here description of your class * @see CommonObject */ class MultiCurrency extends CommonObject { /** * @var string Id to identify managed objects */ public $element = 'multicurrency'; /** * @var string Name of table without prefix where object is stored */ public $table_element = 'multicurrency'; /** * @var string Name of table without prefix where object is stored */ public $table_element_line = "multicurrency_rate"; /** * @var CurrencyRate[] Currency rates */ public $rates = array(); /** * @var int The environment ID when using a multicompany module */ public $id; /** * @var string The currency code */ public $code; /** * @var string The currency name */ public $name; /** * @var int The environment ID when using a multicompany module */ public $entity; /** * @var mixed Sample property 2 */ public $date_create; /** * @var mixed Sample property 2 */ public $fk_user; /** * @var ?CurrencyRate The currency rate */ public $rate; /** * Constructor * * @param DoliDB $db Database handler */ public function __construct(DoliDB $db) { $this->db = $db; } /** * Create object into database * * @param User $user User that creates * @param int $notrigger 0=launch triggers after, 1=disable triggers * @return int Return integer <0 if KO, Id of created object if OK */ public function create(User $user, $notrigger = 0) { global $conf, $langs; dol_syslog('MultiCurrency::create', LOG_DEBUG); $error = 0; if (self::checkCodeAlreadyExists($this->code)) { $error++; $this->errors[] = $langs->trans('multicurrency_code_already_added'); return -1; } if (empty($this->entity) || $this->entity <= 0) { $this->entity = $conf->entity; } $now = dol_now(); // Insert request $sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element."("; $sql .= ' code,'; $sql .= ' name,'; $sql .= ' entity,'; $sql .= ' date_create,'; $sql .= ' fk_user'; $sql .= ') VALUES ('; $sql .= " '".$this->db->escape($this->code)."',"; $sql .= " '".$this->db->escape($this->name)."',"; $sql .= " ".((int) $this->entity).","; $sql .= " '".$this->db->idate($now)."',"; $sql .= " ".((int) $user->id); $sql .= ')'; $this->db->begin(); dol_syslog(__METHOD__, LOG_DEBUG); $resql = $this->db->query($sql); if (!$resql) { $error++; $this->errors[] = 'Error '.$this->db->lasterror(); dol_syslog('MultiCurrency::create '.implode(',', $this->errors), LOG_ERR); } if (!$error) { $this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.$this->table_element); $this->date_create = $now; $this->fk_user = $user->id; if (empty($notrigger)) { $result = $this->call_trigger('CURRENCY_CREATE', $user); if ($result < 0) { $error++; } } } if ($error) { $this->db->rollback(); return -1 * $error; } else { $this->db->commit(); return $this->id; } } /** * Load object in memory from the database * * @param int $id Id object * @param string $code code * @return int Return integer <0 if KO, 0 if not found, >0 if OK */ public function fetch($id, $code = null) { dol_syslog('MultiCurrency::fetch', LOG_DEBUG); global $conf; $sql = "SELECT"; $sql .= ' c.rowid, c.name, c.code, c.entity, c.date_create, c.fk_user'; $sql .= ' FROM '.MAIN_DB_PREFIX.$this->table_element.' AS c'; if (!empty($code)) { $sql .= ' WHERE c.code = \''.$this->db->escape($code).'\' AND c.entity = '.$conf->entity; } else { $sql .= ' WHERE c.rowid = '.((int) $id); } dol_syslog(__METHOD__, LOG_DEBUG); $resql = $this->db->query($sql); if ($resql) { $numrows = $this->db->num_rows($resql); if ($numrows) { $obj = $this->db->fetch_object($resql); $this->id = $obj->rowid; $this->name = $obj->name; $this->code = $obj->code; $this->entity = $obj->entity; $this->date_create = $obj->date_create; $this->fk_user = $obj->fk_user; $this->fetchAllCurrencyRate(); $this->getRate(); } $this->db->free($resql); if ($numrows) { return 1; } else { return 0; } } else { $this->errors[] = 'Error '.$this->db->lasterror(); dol_syslog('MultiCurrency::fetch '.implode(',', $this->errors), LOG_ERR); return -1; } } /** * Load all rates in object from the database * * @return int Return integer <0 if KO, >=0 if OK */ public function fetchAllCurrencyRate() { $sql = "SELECT cr.rowid"; $sql .= ' FROM '.MAIN_DB_PREFIX.$this->table_element_line.' as cr'; $sql .= ' WHERE cr.fk_multicurrency = '.((int) $this->id); $sql .= ' ORDER BY cr.date_sync DESC'; $this->rates = array(); dol_syslog(__METHOD__, LOG_DEBUG); $resql = $this->db->query($sql); if ($resql) { $num = $this->db->num_rows($resql); while ($obj = $this->db->fetch_object($resql)) { $rate = new CurrencyRate($this->db); $rate->fetch($obj->rowid); $this->rates[] = $rate; } $this->db->free($resql); return $num; } else { $this->errors[] = 'Error '.$this->db->lasterror(); dol_syslog('MultiCurrency::fetchAllCurrencyRate '.implode(',', $this->errors), LOG_ERR); return -1; } } /** * Update object into database * * @param User $user User that modifies * @param int $notrigger 0=launch triggers after, 1=disable triggers * @return int Return integer <0 if KO, >0 if OK */ public function update(User $user, $notrigger = 0) { $error = 0; dol_syslog('MultiCurrency::update', LOG_DEBUG); // Clean parameters $this->name = trim($this->name); $this->code = trim($this->code); // Check parameters if (empty($this->code)) { $error++; dol_syslog('MultiCurrency::update $this->code can not be empty', LOG_ERR); return -1; } // Update request $sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET"; $sql .= " name = '".$this->db->escape($this->name)."',"; $sql .= " code = '".$this->db->escape($this->code)."'"; $sql .= " WHERE rowid = ".((int) $this->id); $this->db->begin(); $resql = $this->db->query($sql); if (!$resql) { $error++; $this->errors[] = 'Error '.$this->db->lasterror(); dol_syslog('MultiCurrency::update '.implode(',', $this->errors), LOG_ERR); } if (!$error && empty($notrigger)) { $result = $this->call_trigger('CURRENCY_MODIFY', $user); if ($result < 0) { $error++; } } // Commit or rollback if ($error) { $this->db->rollback(); return -1 * $error; } else { $this->db->commit(); return 1; } } /** * Delete object in database * * @param User $user User making the deletion * @param int $notrigger 0=launch triggers after, 1=disable triggers * @return int Return integer <0 if KO, >0 if OK */ public function delete(User $user, $notrigger = 0) { dol_syslog('MultiCurrency::delete', LOG_DEBUG); $error = 0; $this->db->begin(); if (empty($notrigger)) { $result = $this->call_trigger('CURRENCY_DELETE', $user); if ($result < 0) { $error++; } } if (!$error) { // Delete all rates before if (!$this->deleteRates()) { $error++; $this->errors[] = 'Error '.$this->db->lasterror(); dol_syslog('Currency::delete '.implode(',', $this->errors), LOG_ERR); } $sql = "DELETE FROM ".MAIN_DB_PREFIX.$this->table_element; $sql .= " WHERE rowid = ".((int) $this->id); dol_syslog(__METHOD__, LOG_DEBUG); $resql = $this->db->query($sql); if (!$resql) { $error++; $this->errors[] = 'Error '.$this->db->lasterror(); dol_syslog('MultiCurrency::delete '.implode(',', $this->errors), LOG_ERR); } } // Commit or rollback if ($error) { $this->db->rollback(); return -1 * $error; } else { $this->db->commit(); return 1; } } /** * Delete rates in database * * @return bool false if KO, true if OK */ public function deleteRates() { global $user; foreach ($this->rates as &$rate) { if ($rate->delete($user) <= 0) { return false; } } return true; } /** * Add a Rate into database * * @param double $rate rate value * @return int -1 if KO, 1 if OK */ public function addRate($rate) { global $user; $currencyRate = new CurrencyRate($this->db); $currencyRate->rate = (float) price2num($rate); if ($currencyRate->create($user, $this->id) > 0) { $this->rate = $currencyRate; return 1; } else { $this->rate = null; $this->errors = $currencyRate->errors; return -1; } } /** * Try get label of code in llx_currency then add rate. * * @param string $code currency code * @param double $rate new rate * @return int -1 if KO, 1 if OK, 2 if label found and OK */ public function addRateFromDolibarr($code, $rate) { global $user; $currency = new MultiCurrency($this->db); $currency->code = $code; $currency->name = $code; $sql = "SELECT label FROM ".MAIN_DB_PREFIX."c_currencies WHERE code_iso = '".$this->db->escape($code)."'"; dol_syslog(__METHOD__, LOG_DEBUG); $resql = $this->db->query($sql); if ($resql && ($line = $this->db->fetch_object($resql))) { $currency->name = $line->label; } if ($currency->create($user) > 0) { $currency->addRate($rate); if (!empty($line)) { return 2; } else { return 1; } } return -1; } /** * Add new entry into llx_multicurrency_rate * * @param double $rate rate value * @return int Return integer <0 if KO, >0 if OK */ public function updateRate($rate) { return $this->addRate($rate); } /** * Fetch CurrencyRate object in $this->rate * * @return int Return integer <0 if KO, 0 if not found, >0 if OK */ public function getRate() { $sql = "SELECT cr.rowid"; $sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element_line." as cr"; $sql .= " WHERE cr.fk_multicurrency = ".((int) $this->id); $sql .= " AND cr.date_sync = (SELECT MAX(cr2.date_sync) FROM ".MAIN_DB_PREFIX.$this->table_element_line." AS cr2 WHERE cr2.fk_multicurrency = ".((int) $this->id).")"; dol_syslog(__METHOD__, LOG_DEBUG); $resql = $this->db->query($sql); if ($resql && ($obj = $this->db->fetch_object($resql))) { $this->rate = new CurrencyRate($this->db); return $this->rate->fetch($obj->rowid); } return -1; } /** * Get id of currency from code * * @param DoliDB $dbs object db * @param string $code code value search * * @return int 0 if not found, >0 if OK */ public static function getIdFromCode($dbs, $code) { global $conf; $sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."multicurrency WHERE code = '".$dbs->escape($code)."' AND entity = ".((int) $conf->entity); dol_syslog(__METHOD__, LOG_DEBUG); $resql = $dbs->query($sql); if ($resql && $obj = $dbs->fetch_object($resql)) { return $obj->rowid; } else { return 0; } } /** * Get id and rate of currency from code * * @param DoliDB $dbs Object db * @param string $code Code value search * @param integer|string $date_document Date from document (propal, order, invoice, ...) * * @return array [0] => id currency * [1] => rate */ public static function getIdAndTxFromCode($dbs, $code, $date_document = '') { global $conf; $sql1 = "SELECT m.rowid, mc.rate FROM ".MAIN_DB_PREFIX."multicurrency m"; $sql1 .= ' LEFT JOIN '.MAIN_DB_PREFIX.'multicurrency_rate mc ON (m.rowid = mc.fk_multicurrency)'; $sql1 .= " WHERE m.code = '".$dbs->escape($code)."'"; $sql1 .= " AND m.entity IN (".getEntity('multicurrency').")"; $sql2 = ''; if (getDolGlobalString('MULTICURRENCY_USE_RATE_ON_DOCUMENT_DATE') && !empty($date_document)) { // Use last known rate compared to document date $tmparray = dol_getdate($date_document); $sql2 .= " AND mc.date_sync <= '".$dbs->idate(dol_mktime(23, 59, 59, $tmparray['mon'], $tmparray['mday'], $tmparray['year'], true))."'"; } $sql3 = " ORDER BY mc.date_sync DESC LIMIT 1"; dol_syslog(__METHOD__, LOG_DEBUG); $resql = $dbs->query($sql1.$sql2.$sql3); if ($resql && $obj = $dbs->fetch_object($resql)) { return array($obj->rowid, $obj->rate); } else { if (getDolGlobalString('MULTICURRENCY_USE_RATE_ON_DOCUMENT_DATE')) { $resql = $dbs->query($sql1.$sql3); if ($resql && $obj = $dbs->fetch_object($resql)) { return array($obj->rowid, $obj->rate); } } return array(0, 1); } } /** * Get the conversion of amount with invoice rate * * @param int $fk_facture Id of invoice * @param double $amount amount to convert * @param string $way 'dolibarr' mean the amount is in dolibarr currency * @param string $table 'facture' or 'facture_fourn' * @param float|null $invoice_rate Invoice rate if known (to avoid to make the getInvoiceRate call) * @return float|false amount converted or false if conversion fails */ public static function getAmountConversionFromInvoiceRate($fk_facture, $amount, $way = 'dolibarr', $table = 'facture', $invoice_rate = null) { if (!is_null($invoice_rate)) { $multicurrency_tx = $invoice_rate; } else { $multicurrency_tx = self::getInvoiceRate($fk_facture, $table); } if ($multicurrency_tx) { if ($way == 'dolibarr') { return (float) price2num($amount * $multicurrency_tx, 'MU'); } else { return (float) price2num($amount / $multicurrency_tx, 'MU'); } } else { return false; } } /** * Get current invoite rate * * @param int $fk_facture id of facture * @param string $table facture or facture_fourn * @return float|bool Rate of currency or false if error */ public static function getInvoiceRate($fk_facture, $table = 'facture') { global $db; $sql = "SELECT multicurrency_tx FROM ".MAIN_DB_PREFIX.$table." WHERE rowid = ".((int) $fk_facture); dol_syslog(__METHOD__, LOG_DEBUG); $resql = $db->query($sql); if ($resql && ($line = $db->fetch_object($resql))) { return $line->multicurrency_tx; } return false; } /** * With free account we can't set source then recalcul all rates to force another source. * This modify the array &$TRate. * * @param stdClass $TRate Object containing all currencies rates * @return int -1 if KO, 0 if nothing, 1 if OK */ public function recalculRates(&$TRate) { global $conf; if ($conf->currency != getDolGlobalString('MULTICURRENCY_APP_SOURCE')) { $alternate_source = 'USD'.$conf->currency; if (!empty($TRate->$alternate_source)) { $coef = 1 / $TRate->$alternate_source; foreach ($TRate as $attr => &$rate) { $rate *= $coef; } $TRate->USDUSD = $coef; return 1; } return -1; // Alternate source not found } return 0; // Nothing to do } /** * Sync rates from API * * @param string $key Key to use. Come from getDolGlobalString("MULTICURRENCY_APP_ID") * @param int $addifnotfound Add if not found * @param string $mode "" for standard use, "cron" to use it in a cronjob * @return int Return integer <0 if KO, >0 if OK, if mode = "cron" OK is 0 */ public function syncRates($key, $addifnotfound = 0, $mode = "") { global $conf, $db, $langs; if (getDolGlobalString('MULTICURRENCY_DISABLE_SYNC_CURRENCYLAYER')) { if ($mode == "cron") { $this->output = $langs->trans('Use of API for currency update is disabled by option MULTICURRENCY_DISABLE_SYNC_CURRENCYLAYER'); } else { setEventMessages($langs->trans('Use of API for currency update is disabled by option MULTICURRENCY_DISABLE_SYNC_CURRENCYLAYER'), null, 'errors'); } return -1; } if (empty($key)) { $key = getDolGlobalString("MULTICURRENCY_APP_ID"); } include_once DOL_DOCUMENT_ROOT.'/core/lib/geturl.lib.php'; $urlendpoint = 'http://api.currencylayer.com/live?access_key='.$key; $urlendpoint .= '&source=' . (!getDolGlobalString('MULTICURRENCY_APP_SOURCE') ? 'USD' : $conf->global->MULTICURRENCY_APP_SOURCE); dol_syslog("Call url endpoint ".$urlendpoint); $resget = getURLContent($urlendpoint); if ($resget['content']) { $response = $resget['content']; $response = json_decode($response); if ($response->success) { $TRate = $response->quotes; //$timestamp = $response->timestamp; if ($this->recalculRates($TRate) >= 0) { foreach ($TRate as $currency_code => $rate) { $code = substr($currency_code, 3, 3); $obj = new MultiCurrency($db); if ($obj->fetch(0, $code) > 0) { $obj->updateRate($rate); } elseif ($addifnotfound) { $this->addRateFromDolibarr($code, $rate); } } } if ($mode == "cron") { return 0; } return 1; } else { dol_syslog("Failed to call endpoint ".$response->error->info, LOG_WARNING); if ($mode == "cron") { $this->output = $langs->trans('multicurrency_syncronize_error', $response->error->info); } else { setEventMessages($langs->trans('multicurrency_syncronize_error', $response->error->info), null, 'errors'); } return -1; } } else { return -1; } } /** * Check in database if the current code already exists * * @param string $code current code to search * @return boolean True if exists, false if not exists */ public function checkCodeAlreadyExists($code) { $currencytmp = new MultiCurrency($this->db); if ($currencytmp->fetch('', $code) > 0) { return true; } else { return false; } } } /** * Class CurrencyRate */ class CurrencyRate extends CommonObjectLine { /** * @var string Id to identify managed objects */ public $element = 'multicurrency_rate'; /** * @var string Name of table without prefix where object is stored */ public $table_element = 'multicurrency_rate'; /** * @var int ID */ public $id; /** * @var double Rate */ public $rate; /** * @var double Rate Indirect */ public $rate_indirect; /** * @var integer Date synchronisation */ public $date_sync; /** * @var int Id of currency */ public $fk_multicurrency; /** * @var int Id of entity */ public $entity; /** * Constructor * * @param DoliDB $db Database handler */ public function __construct(DoliDB $db) { $this->db = $db; } /** * Create object into database * * @param User $user User making the deletion * @param int $fk_multicurrency Id of currency * @param int $notrigger 0=launch triggers after, 1=disable triggers * @return int Return integer <0 if KO, Id of created object if OK */ public function create(User $user, int $fk_multicurrency, $notrigger = 0) { global $conf; dol_syslog('CurrencyRate::create', LOG_DEBUG); $error = 0; $this->rate = (float) price2num($this->rate); if (empty($this->entity) || $this->entity <= 0) { $this->entity = $conf->entity; } $now = empty($this->date_sync) ? dol_now() : $this->date_sync; // Insert request $sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element."("; $sql .= ' rate,'; $sql .= ' rate_indirect,'; $sql .= ' date_sync,'; $sql .= ' fk_multicurrency,'; $sql .= ' entity'; $sql .= ') VALUES ('; $sql .= ' '.((float) $this->rate).','; $sql .= ' '.((float) $this->rate_indirect).','; $sql .= " '".$this->db->idate($now)."',"; $sql .= " ".((int) $fk_multicurrency).","; $sql .= " ".((int) $this->entity); $sql .= ')'; $this->db->begin(); dol_syslog(__METHOD__, LOG_DEBUG); $resql = $this->db->query($sql); if (!$resql) { $error++; $this->errors[] = 'Error '.$this->db->lasterror(); dol_syslog('CurrencyRate::create '.implode(',', $this->errors), LOG_ERR); } if (!$error) { $this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.$this->table_element); $this->fk_multicurrency = $fk_multicurrency; $this->date_sync = $now; if (empty($notrigger)) { $result = $this->call_trigger('CURRENCYRATE_CREATE', $user); if ($result < 0) { $error++; } } } if ($error) { $this->db->rollback(); return -1 * $error; } else { $this->db->commit(); return $this->id; } } /** * Load object in memory from the database * * @param int $id Id object * @return int Return integer <0 if KO, 0 if not found, >0 if OK */ public function fetch($id) { dol_syslog('CurrencyRate::fetch', LOG_DEBUG); $sql = "SELECT cr.rowid, cr.rate, cr.rate_indirect, cr.date_sync, cr.fk_multicurrency, cr.entity"; $sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." AS cr"; $sql .= " WHERE cr.rowid = ".((int) $id); dol_syslog(__METHOD__, LOG_DEBUG); $resql = $this->db->query($sql); if ($resql) { $numrows = $this->db->num_rows($resql); if ($numrows) { $obj = $this->db->fetch_object($resql); $this->id = $obj->rowid; $this->rate = $obj->rate; $this->rate_indirect = $obj->rate_indirect; $this->date_sync = $this->db->jdate($obj->date_sync); $this->fk_multicurrency = $obj->fk_multicurrency; $this->entity = $obj->entity; } $this->db->free($resql); if ($numrows) { return 1; } else { return 0; } } else { $this->errors[] = 'Error '.$this->db->lasterror(); dol_syslog('CurrencyRate::fetch '.implode(',', $this->errors), LOG_ERR); return -1; } } /** * Update object into database * * @param User $user User making the deletion * @param int $notrigger 0=launch triggers after, 1=disable triggers * @return int Return integer <0 if KO, >0 if OK */ public function update(User $user, $notrigger = 0) { $error = 0; dol_syslog('CurrencyRate::update', LOG_DEBUG); $this->rate = (float) price2num($this->rate); // Update request $sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element; $sql .= "SET rate = ".((float) $this->rate); if (!empty($this->date_sync)) { $sql .= ", date_sync = '".$this->db->idate($this->date_sync)."'"; } if (!empty($this->fk_multicurrency)) { $sql .= ', fk_multicurrency = '.((int) $this->fk_multicurrency); } $sql .= " WHERE rowid =".((int) $this->id); $this->db->begin(); dol_syslog(__METHOD__, LOG_DEBUG); $resql = $this->db->query($sql); if (!$resql) { $error++; $this->errors[] = 'Error '.$this->db->lasterror(); dol_syslog('CurrencyRate::update '.implode(',', $this->errors), LOG_ERR); } if (!$error && empty($notrigger)) { $result = $this->call_trigger('CURRENCYRATE_MODIFY', $user); if ($result < 0) { $error++; } } // Commit or rollback if ($error) { $this->db->rollback(); return -1 * $error; } else { $this->db->commit(); return 1; } } /** * Delete object in database * * @param User $user User making the deletion * @param int $notrigger 0=launch triggers after, 1=disable triggers * @return int Return integer <0 if KO, >0 if OK */ public function delete(User $user, $notrigger = 0) { dol_syslog('CurrencyRate::delete', LOG_DEBUG); $error = 0; $this->db->begin(); if (empty($notrigger)) { $result = $this->call_trigger('CURRENCYRATE_DELETE', $user); if ($result < 0) { $error++; } } if (!$error) { $sql = 'DELETE FROM '.MAIN_DB_PREFIX.$this->table_element; $sql .= ' WHERE rowid='.((int) $this->id); dol_syslog(__METHOD__, LOG_DEBUG); $resql = $this->db->query($sql); if (!$resql) { $error++; $this->errors[] = 'Error '.$this->db->lasterror(); dol_syslog('CurrencyRate::delete '.implode(',', $this->errors), LOG_ERR); } } // Commit or rollback if ($error) { $this->db->rollback(); return -1 * $error; } else { $this->db->commit(); return 1; } } }