diff --git a/htdocs/comm/remx.php b/htdocs/comm/remx.php index c25b2fbc4e336..1b02b59f58112 100644 --- a/htdocs/comm/remx.php +++ b/htdocs/comm/remx.php @@ -6,6 +6,7 @@ * Copyright (C) 2024-2026 MDW * Copyright (C) 2025 Anthony Damhet * Copyright (C) 2026 Vincent de Grandpré + * Copyright (C) 2026 José MARTINEZ * * 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 @@ -173,6 +174,8 @@ $amount_ttc_1 = price2num($amount_ttc_1); $amount_ttc_2 = GETPOST('amount_ttc_2', 'alpha'); $amount_ttc_2 = price2num($amount_ttc_2); + $mc_amount_ttc_1 = price2num(GETPOST('mc_amount_ttc_1', 'alpha')); + $mc_amount_ttc_2 = price2num(GETPOST('mc_amount_ttc_2', 'alpha')); $error = 0; $remid = (GETPOSTINT("remid") ? GETPOSTINT("remid") : 0); @@ -186,6 +189,11 @@ $error++; setEventMessages($langs->trans("TotalOfTwoDiscountMustEqualsOriginal"), null, 'errors'); } + if (!$error && ((float) $mc_amount_ttc_1 + (float) $mc_amount_ttc_2) != 0 + && price2num((float) $mc_amount_ttc_1 + (float) $mc_amount_ttc_2, 'MT') != price2num($discount->multicurrency_amount_ttc, 'MT')) { + $error++; + setEventMessages($langs->trans("TotalOfTwoDiscountMustEqualsOriginal"), null, 'errors'); + } if (!$error && $discount->fk_facture_line) { $error++; setEventMessages($langs->trans("ErrorCantSplitAUsedDiscount"), null, 'errors'); @@ -196,6 +204,16 @@ $newdiscount1 = $newDiscounts[0]; $newdiscount2 = $newDiscounts[1]; + // Foreign-currency discount: honour the foreign amounts entered (the user may split directly in that currency) rather than the rate-derived ones + if (((float) $mc_amount_ttc_1 + (float) $mc_amount_ttc_2) != 0) { + $newdiscount1->multicurrency_amount_ttc = (float) $mc_amount_ttc_1; + $newdiscount1->multicurrency_amount_ht = price2num((float) $mc_amount_ttc_1 / (1 + (float) $newdiscount1->tva_tx / 100), 'MT'); + $newdiscount1->multicurrency_amount_tva = price2num((float) $mc_amount_ttc_1 - (float) $newdiscount1->multicurrency_amount_ht); + $newdiscount2->multicurrency_amount_ttc = (float) $mc_amount_ttc_2; + $newdiscount2->multicurrency_amount_ht = price2num((float) $mc_amount_ttc_2 / (1 + (float) $newdiscount2->tva_tx / 100), 'MT'); + $newdiscount2->multicurrency_amount_tva = price2num((float) $mc_amount_ttc_2 - (float) $newdiscount2->multicurrency_amount_ht); + } + $db->begin(); $discount->fk_facture_source = 0; // This is to delete only the require record (that we will recreate with two records) and not all family with same fk_facture_source @@ -546,21 +564,21 @@ print ''.dol_print_date($db->jdate($obj->dc), 'dayhour', 'tzuserrel').''; if (preg_match('/\(CREDIT_NOTE\)/', $obj->description)) { - print ''; + print ''; $facturestatic->id = $obj->fk_facture_source; $facturestatic->ref = $obj->ref; $facturestatic->type = $obj->type; print preg_replace('/\(CREDIT_NOTE\)/', $langs->trans("CreditNote"), $obj->description).'
'.$facturestatic->getNomURl(1); print ''; } elseif (preg_match('/\(DEPOSIT\)/', $obj->description)) { - print ''; + print ''; $facturestatic->id = $obj->fk_facture_source; $facturestatic->ref = $obj->ref; $facturestatic->type = $obj->type; print preg_replace('/\(DEPOSIT\)/', $langs->trans("InvoiceDeposit"), $obj->description).'
'.$facturestatic->getNomURl(1); print ''; } elseif (preg_match('/\(EXCESS RECEIVED\)/', $obj->description)) { - print ''; + print ''; $facturestatic->id = $obj->fk_facture_source; $facturestatic->ref = $obj->ref; $facturestatic->type = $obj->type; @@ -584,7 +602,7 @@ if (isModEnabled('multicurrency')) { print ''.price($obj->multicurrency_amount_ttc).''; } - print ''; + print ''; //print ''.img_object($langs->trans("ShowUser"), 'user').' '.$obj->login.''; print $tmpuser->getNomUrl(-1); print ''; @@ -771,7 +789,31 @@ function calcRemainToPay(){ 1 => array('type' => 'text', 'name' => 'amount_ttc_2', 'label' => $langs->trans("AmountTTC").' 2', 'value' => $amount2, 'size' => '5') ); $langs->load("dict"); - print $form->formconfirm($_SERVER["PHP_SELF"].'?id='.$object->id.'&remid='.$showconfirminfo['rowid'].($backtopage ? '&backtopage='.urlencode($backtopage) : ''), $langs->trans('SplitDiscount'), $langs->trans('ConfirmSplitDiscount', price($showconfirminfo['amount_ttc']), $langs->transnoentities("Currency".$conf->currency)), 'confirm_split', $formquestion, '', 0); + $discountforsplit = new DiscountAbsolute($db); + $discountforsplit->fetch($showconfirminfo['rowid']); + $ismcsplit = (isModEnabled('multicurrency') && (float) $discountforsplit->multicurrency_amount_ttc != 0 && abs((float) $discountforsplit->multicurrency_amount_ttc - (float) $discountforsplit->amount_ttc) > 0.01 && $discountforsplit->multicurrency_code != $conf->currency); + $mcsplitcode = !empty($discountforsplit->multicurrency_code) ? $discountforsplit->multicurrency_code : $langs->trans("Currency"); + $splitamount1 = price2num($showconfirminfo['amount_ttc'] / 2, 'MT'); + $splitamount2 = price2num((float) $showconfirminfo['amount_ttc'] - (float) $splitamount1, 'MT'); + $splitmc1 = price2num($discountforsplit->multicurrency_amount_ttc / 2, 'MT'); + $splitmc2 = price2num((float) $discountforsplit->multicurrency_amount_ttc - (float) $splitmc1, 'MT'); + print '
'; + print ''; + print ''; + print '
'; + print ''; + if ($ismcsplit) { print ''; } + print ''; + print ''; + if ($ismcsplit) { print ''; } + print ''; + print ''; + if ($ismcsplit) { print ''; } + print ''; + print '
'.img_picto('', 'split', 'class="pictofixedwidth"').$langs->trans('SplitDiscount').''.$langs->trans('AmountTTC').' ('.dol_escape_htmltag($mcsplitcode).')'.$langs->trans('AmountTTC').' ('.$langs->trans("Currency".$conf->currency).')
'.$langs->trans('Part').' 1
'.$langs->trans('Part').' 2
'; + print '
'.$langs->trans('ConfirmSplitDiscount', price($showconfirminfo['amount_ttc']), $langs->transnoentities("Currency".$conf->currency)).' '.$form->selectyesno('confirm', 'no', 0).'  
'; + print '
'; + print ''; } } } else { @@ -844,21 +886,21 @@ function calcRemainToPay(){ print ''; print ''.dol_print_date($db->jdate($obj->dc), 'dayhour', 'tzuserrel').''; if (preg_match('/\(CREDIT_NOTE\)/', $obj->description)) { - print ''; + print ''; $facturefournstatic->id = $obj->fk_invoice_supplier_source; $facturefournstatic->ref = $obj->ref; $facturefournstatic->type = $obj->type; print preg_replace('/\(CREDIT_NOTE\)/', $langs->trans("CreditNote"), $obj->description).'
'.$facturefournstatic->getNomURl(1); print ''; } elseif (preg_match('/\(DEPOSIT\)/', $obj->description)) { - print ''; + print ''; $facturefournstatic->id = $obj->fk_invoice_supplier_source; $facturefournstatic->ref = $obj->ref; $facturefournstatic->type = $obj->type; print preg_replace('/\(DEPOSIT\)/', $langs->trans("InvoiceDeposit"), $obj->description).'
'.$facturefournstatic->getNomURl(1); print ''; } elseif (preg_match('/\(EXCESS PAID\)/', $obj->description)) { - print ''; + print ''; $facturefournstatic->id = $obj->fk_invoice_supplier_source; $facturefournstatic->ref = $obj->ref; $facturefournstatic->type = $obj->type; @@ -879,7 +921,7 @@ function calcRemainToPay(){ if (isModEnabled('multicurrency')) { print ''.price($obj->multicurrency_amount_ttc).''; } - print ''; + print ''; print $tmpuser->getNomUrl(-1); print ''; @@ -1065,7 +1107,41 @@ function calcRemainToPay(){ 1 => array('type' => 'text', 'name' => 'amount_ttc_2', 'label' => $langs->trans("AmountTTC").' 2', 'value' => $amount2, 'size' => '5') ); $langs->load("dict"); - print $form->formconfirm($_SERVER["PHP_SELF"].'?id='.$object->id.'&remid='.$showconfirminfo['rowid'].($backtopage ? '&backtopage='.urlencode($backtopage) : ''), $langs->trans('SplitDiscount'), $langs->trans('ConfirmSplitDiscount', price($showconfirminfo['amount_ttc']), $langs->transnoentities("Currency".$conf->currency)), 'confirm_split', $formquestion, 0, 0); + $discountforsplit = new DiscountAbsolute($db); + $discountforsplit->fetch($showconfirminfo['rowid']); + $ismcsplit = (isModEnabled('multicurrency') && (float) $discountforsplit->multicurrency_amount_ttc != 0 && abs((float) $discountforsplit->multicurrency_amount_ttc - (float) $discountforsplit->amount_ttc) > 0.01 && $discountforsplit->multicurrency_code != $conf->currency); + if ($ismcsplit) { + $mcsplitcode = !empty($discountforsplit->multicurrency_code) ? $discountforsplit->multicurrency_code : $langs->trans("Currency"); + $mcamount1 = price2num($discountforsplit->multicurrency_amount_ttc / 2, 'MT'); + $mcamount2 = ($discountforsplit->multicurrency_amount_ttc - (float) $mcamount1); + $formquestion[2] = array('type' => 'text', 'name' => 'mc_amount_ttc_1', 'label' => $langs->trans("AmountTTC").' 1 ('.$mcsplitcode.')', 'value' => $mcamount1, 'size' => '5'); + $formquestion[3] = array('type' => 'text', 'name' => 'mc_amount_ttc_2', 'label' => $langs->trans("AmountTTC").' 2 ('.$mcsplitcode.')', 'value' => $mcamount2, 'size' => '5'); + } + $discountforsplit = new DiscountAbsolute($db); + $discountforsplit->fetch($showconfirminfo['rowid']); + $ismcsplit = (isModEnabled('multicurrency') && (float) $discountforsplit->multicurrency_amount_ttc != 0 && abs((float) $discountforsplit->multicurrency_amount_ttc - (float) $discountforsplit->amount_ttc) > 0.01 && $discountforsplit->multicurrency_code != $conf->currency); + $mcsplitcode = !empty($discountforsplit->multicurrency_code) ? $discountforsplit->multicurrency_code : $langs->trans("Currency"); + $splitamount1 = price2num($showconfirminfo['amount_ttc'] / 2, 'MT'); + $splitamount2 = price2num((float) $showconfirminfo['amount_ttc'] - (float) $splitamount1, 'MT'); + $splitmc1 = price2num($discountforsplit->multicurrency_amount_ttc / 2, 'MT'); + $splitmc2 = price2num((float) $discountforsplit->multicurrency_amount_ttc - (float) $splitmc1, 'MT'); + print '
'; + print ''; + print ''; + print '
'; + print ''; + if ($ismcsplit) { print ''; } + print ''; + print ''; + if ($ismcsplit) { print ''; } + print ''; + print ''; + if ($ismcsplit) { print ''; } + print ''; + print '
'.img_picto('', 'split', 'class="pictofixedwidth"').$langs->trans('SplitDiscount').''.$langs->trans('AmountTTC').' ('.dol_escape_htmltag($mcsplitcode).')'.$langs->trans('AmountTTC').' ('.$langs->trans("Currency".$conf->currency).')
'.$langs->trans('Part').' 1
'.$langs->trans('Part').' 2
'; + print '
'.$langs->trans('ConfirmSplitDiscount', price($showconfirminfo['amount_ttc']), $langs->transnoentities("Currency".$conf->currency)).' '.$form->selectyesno('confirm', 'no', 0).'  
'; + print '
'; + print ''; } } } else { @@ -1192,21 +1268,21 @@ function calcRemainToPay(){ print ''; print ''.dol_print_date($db->jdate($obj->dc), 'dayhour').''; if (preg_match('/\(CREDIT_NOTE\)/', $obj->description)) { - print ''; + print ''; $facturestatic->id = $obj->fk_facture_source; $facturestatic->ref = $obj->invoice_source_ref; $facturestatic->type = $obj->type; print preg_replace('/\(CREDIT_NOTE\)/', $langs->trans("CreditNote"), $obj->description).'
'.$facturestatic->getNomURl(1); print ''; } elseif (preg_match('/\(DEPOSIT\)/', $obj->description)) { - print ''; + print ''; $facturestatic->id = $obj->fk_facture_source; $facturestatic->ref = $obj->invoice_source_ref; $facturestatic->type = $obj->type; print preg_replace('/\(DEPOSIT\)/', $langs->trans("InvoiceDeposit"), $obj->description).'
'.$facturestatic->getNomURl(1); print ''; } elseif (preg_match('/\(EXCESS RECEIVED\)/', $obj->description)) { - print ''; + print ''; $facturestatic->id = $obj->fk_facture_source; $facturestatic->ref = $obj->invoice_source_ref; $facturestatic->type = $obj->type; @@ -1219,7 +1295,9 @@ function calcRemainToPay(){ } print ''; if ($obj->invoiceid) { - print ''.img_object($langs->trans("ShowBill"), 'bill').' '.$obj->ref.''; + $facturestatic->id = $obj->invoiceid; + $facturestatic->ref = $obj->ref; + print $facturestatic->getNomUrl(1); } print ''; print ''.price($obj->amount_ht).''; @@ -1231,7 +1309,7 @@ function calcRemainToPay(){ if (isModEnabled('multicurrency')) { print ''.price($obj->multicurrency_amount_ttc).''; } - print ''; + print ''; print $tmpuser->getNomUrl(-1); print ''; @@ -1362,21 +1440,21 @@ function calcRemainToPay(){ print ''; print ''.dol_print_date($db->jdate($obj->dc), 'dayhour').''; if (preg_match('/\(CREDIT_NOTE\)/', $obj->description)) { - print ''; + print ''; $facturefournstatic->id = $obj->fk_invoice_supplier_source; $facturefournstatic->ref = $obj->invoice_source_ref; $facturefournstatic->type = $obj->type; print preg_replace('/\(CREDIT_NOTE\)/', $langs->trans("CreditNote"), $obj->description).'
'.$facturefournstatic->getNomURl(1); print ''; } elseif (preg_match('/\(DEPOSIT\)/', $obj->description)) { - print ''; + print ''; $facturefournstatic->id = $obj->fk_invoice_supplier_source; $facturefournstatic->ref = $obj->invoice_source_ref; $facturefournstatic->type = $obj->type; print preg_replace('/\(DEPOSIT\)/', $langs->trans("InvoiceDeposit"), $obj->description).'
'.$facturefournstatic->getNomURl(1); print ''; } elseif (preg_match('/\(EXCESS PAID\)/', $obj->description)) { - print ''; + print ''; $facturefournstatic->id = $obj->fk_invoice_supplier_source; $facturefournstatic->ref = $obj->invoice_source_ref; $facturefournstatic->type = $obj->type; @@ -1389,7 +1467,9 @@ function calcRemainToPay(){ } print ''; if ($obj->invoiceid) { - print ''.img_object($langs->trans("ShowBill"), 'bill').' '.$obj->ref.''; + $facturefournstatic->id = $obj->invoiceid; + $facturefournstatic->ref = $obj->ref; + print $facturefournstatic->getNomUrl(1); } print ''; print ''.price($obj->amount_ht).''; @@ -1401,7 +1481,7 @@ function calcRemainToPay(){ if (isModEnabled('multicurrency')) { print ''.price($obj->multicurrency_amount_ttc).''; } - print ''; + print ''; print $tmpuser->getNomUrl(-1); print ''; diff --git a/htdocs/compta/facture/card.php b/htdocs/compta/facture/card.php index 3d18cb63b0cf5..8b0d321f8e806 100644 --- a/htdocs/compta/facture/card.php +++ b/htdocs/compta/facture/card.php @@ -23,6 +23,7 @@ * Copyright (C) 2026 Vincent de Grandpré * Copyright (C) 2026 Joachim Küter * Copyright (C) 2026 Lionel Vessiller + * Copyright (C) 2026 José MARTINEZ * * 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 @@ -1106,6 +1107,9 @@ $discount->fk_soc = $object->socid; $discount->socid = $object->socid; $discount->fk_facture_source = $object->id; + // Carry the currency and rate of the source invoice so the resulting discount keeps its foreign-currency identity + $discount->multicurrency_code = $object->multicurrency_code; + $discount->multicurrency_tx = $object->multicurrency_tx; $error = 0; @@ -6341,6 +6345,7 @@ function js_recalculate_revenuestamp(){ $creditnoteamount = 0; $depositamount = 0; $sql = "SELECT re.rowid, re.amount_ht, re.amount_tva, re.amount_ttc,"; + $sql .= " re.multicurrency_amount_ttc, re.multicurrency_code, re.multicurrency_tx,"; $sql .= " re.description, re.fk_facture_source"; $sql .= " FROM ".MAIN_DB_PREFIX."societe_remise_except as re"; $sql .= " WHERE fk_facture = ".((int) $object->id); @@ -6362,6 +6367,14 @@ function js_recalculate_revenuestamp(){ print $langs->trans("Deposit").' '; } print $invoice->getNomUrl(0); + // Foreign-currency amount and rate, kept on a single line in the (wide) label cell (option MULTICURRENCY_PAYMENT_USE_REAL_AMOUNTS) + if (getDolGlobalInt('MULTICURRENCY_PAYMENT_USE_REAL_AMOUNTS') && isModEnabled('multicurrency') && !empty($obj->multicurrency_code)) { + print ' '.price($obj->multicurrency_amount_ttc, 0, $langs, 1, -1, -1, $obj->multicurrency_code); + if (!empty($obj->multicurrency_tx) && $obj->multicurrency_tx > 0) { + print ' - '.$langs->trans('Rate').' : '.price2num($obj->multicurrency_tx, 'MU'); + } + print ''; + } print ''; print ''; // Delete discount diff --git a/htdocs/core/class/discount.class.php b/htdocs/core/class/discount.class.php index dfbcfdaece32d..cf3371332e909 100644 --- a/htdocs/core/class/discount.class.php +++ b/htdocs/core/class/discount.class.php @@ -6,6 +6,7 @@ * Copyright (C) 2024-2026 MDW * Copyright (C) 2024 Noé Cendrier * Copyright (C) 2026 Vincent de Grandpré + * Copyright (C) 2026 José MARTINEZ * * 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 @@ -110,6 +111,16 @@ class DiscountAbsolute extends CommonObject */ public $multicurrency_subprice; + /** + * @var string Currency code of the credit (invoice currency) + */ + public $multicurrency_code; + + /** + * @var float Exchange rate of the credit + */ + public $multicurrency_tx; + /** * @var int */ @@ -235,6 +246,7 @@ public function fetch($rowid, $fk_facture_source = 0, $fk_invoice_supplier_sourc $sql .= " sr.fk_user,"; $sql .= " sr.amount_ht, sr.amount_tva, sr.amount_localtax1, sr.amount_localtax2, sr.amount_ttc, sr.tva_tx, sr.localtax1_tx, sr.localtax1_type, sr.localtax2_tx, sr.localtax2_type, sr.vat_src_code,"; $sql .= " sr.multicurrency_amount_ht, sr.multicurrency_amount_tva, sr.multicurrency_amount_ttc,"; + $sql .= " sr.multicurrency_code, sr.multicurrency_tx,"; $sql .= " sr.fk_facture_line, sr.fk_facture, sr.fk_facture_source, sr.fk_invoice_supplier_line, sr.fk_invoice_supplier, sr.fk_invoice_supplier_source, sr.description,"; $sql .= " sr.datec,"; $sql .= " f.ref as ref_facture_source, f.type as type_facture_source,"; @@ -281,6 +293,8 @@ public function fetch($rowid, $fk_facture_source = 0, $fk_invoice_supplier_sourc $this->multicurrency_amount_ht = $this->multicurrency_total_ht; $this->multicurrency_amount_tva = $this->multicurrency_total_tva; $this->multicurrency_amount_ttc = $this->multicurrency_total_ttc; + $this->multicurrency_code = $obj->multicurrency_code; + $this->multicurrency_tx = $obj->multicurrency_tx; $this->tva_tx = $obj->tva_tx; $this->localtax1_tx = $obj->localtax1_tx; @@ -1127,6 +1141,11 @@ public function splitAmount($amount_ttc1, $amount_ttc2) $newdiscount2->localtax2_tx = $this->localtax2_tx; $newdiscount1->vat_src_code = $this->vat_src_code; $newdiscount2->vat_src_code = $this->vat_src_code; + // Carry the currency and rate so generateFromAmount() recomputes the foreign-currency amount of each part (was lost on split) + $newdiscount1->multicurrency_code = $this->multicurrency_code; + $newdiscount2->multicurrency_code = $this->multicurrency_code; + $newdiscount1->multicurrency_tx = $this->multicurrency_tx; + $newdiscount2->multicurrency_tx = $this->multicurrency_tx; $newdiscount1->generateFromAmount($amount_ttc1, 1, $newdiscount1->tva_tx, $newdiscount1->localtax1_tx, $newdiscount1->localtax2_tx, $this->localtax1_type, $this->localtax2_type); $newdiscount2->generateFromAmount($amount_ttc2, 1, $newdiscount2->tva_tx, $newdiscount2->localtax1_tx, $newdiscount2->localtax2_tx, $this->localtax1_type, $this->localtax2_type); diff --git a/htdocs/core/class/html.form.class.php b/htdocs/core/class/html.form.class.php index 6f6726069f7bd..d9d0565ba9bdf 100644 --- a/htdocs/core/class/html.form.class.php +++ b/htdocs/core/class/html.form.class.php @@ -2538,6 +2538,7 @@ public function select_remises($selected, $htmlname, $filter, $socid, $maxvalue // On recherche les remises $sql = "SELECT re.rowid, re.amount_ht, re.amount_tva, re.amount_ttc,"; + $sql .= " re.multicurrency_amount_ttc, re.multicurrency_code, re.multicurrency_tx,"; $sql .= " re.description, re.fk_facture_source"; $sql .= " FROM " . $this->db->prefix() . "societe_remise_except as re"; $sql .= " WHERE re.fk_soc = " . (int) $socid; @@ -2593,7 +2594,17 @@ public function select_remises($selected, $htmlname, $filter, $socid, $maxvalue } } - print ''; + // Under MULTICURRENCY_PAYMENT_USE_REAL_AMOUNTS, append the foreign amount and rate of the credit + $multicurrencyinfo = ''; + if (getDolGlobalInt('MULTICURRENCY_PAYMENT_USE_REAL_AMOUNTS') && isModEnabled('multicurrency') && !empty($obj->multicurrency_code)) { + $multicurrencyinfo = ' (' . price($obj->multicurrency_amount_ttc, 0, $langs, 1, -1, -1, $obj->multicurrency_code); + if (!empty($obj->multicurrency_tx) && $obj->multicurrency_tx > 0) { + $multicurrencyinfo .= ' - ' . $langs->trans('Rate') . ' : ' . price2num($obj->multicurrency_tx, 'MU'); + } + $multicurrencyinfo .= ')'; + } + + print ''; $i++; } } @@ -7591,6 +7602,8 @@ public function form_remise_dispo($page, $selected, $htmlname, $socid, $amount, } } print $langs->trans($translationKey, price($amount, 0, $langs, 0, 0, -1, $conf->currency)); + // Under MULTICURRENCY_PAYMENT_USE_REAL_AMOUNTS, also show the foreign-currency value and rate of the available credits + print $this->getMulticurrencyCreditsInfo($socid, $discount_type, $filter); if (empty($hidelist)) { print ' '; } @@ -7637,6 +7650,63 @@ public function form_remise_dispo($page, $selected, $htmlname, $socid, $amount, } } + /** + * Build a short HTML label with the foreign-currency value and the exchange rate of the available + * multicurrency credits of a third party, to enrich the "available discounts/credits" messages. + * Returns an empty string when the option MULTICURRENCY_PAYMENT_USE_REAL_AMOUNTS is off, the + * multicurrency module is disabled, or no available credit carries a currency code. + * + * @param int $socid Third party id + * @param int $discount_type 0 = customer discount, 1 = supplier discount + * @param string $filter Same SQL filter used to compute the company-currency total + * @return string HTML fragment (empty string if nothing to show) + */ + public function getMulticurrencyCreditsInfo($socid, $discount_type = 0, $filter = '') + { + global $langs; + + if (!getDolGlobalInt('MULTICURRENCY_PAYMENT_USE_REAL_AMOUNTS') || !isModEnabled('multicurrency')) { + return ''; + } + + $sql = "SELECT multicurrency_code, SUM(multicurrency_amount_ttc) as mc_total, MIN(multicurrency_tx) as mintx, MAX(multicurrency_tx) as maxtx"; + $sql .= " FROM ".MAIN_DB_PREFIX."societe_remise_except"; + $sql .= " WHERE fk_soc = ".((int) $socid); + $sql .= " AND discount_type = ".((int) $discount_type); + if (!empty($discount_type)) { + $sql .= " AND fk_invoice_supplier IS NULL AND fk_invoice_supplier_line IS NULL"; + } else { + $sql .= " AND fk_facture IS NULL AND fk_facture_line IS NULL"; + } + if ($filter) { + $sql .= " AND (".$filter.")"; // @phan-suppress-current-line SqlInjection + } + $sql .= " AND multicurrency_code IS NOT NULL AND multicurrency_code <> ''"; + $sql .= " GROUP BY multicurrency_code"; + + $resql = $this->db->query($sql); + if (!$resql) { + return ''; + } + + $tmparray = array(); + while ($obj = $this->db->fetch_object($resql)) { + $txt = price($obj->mc_total, 0, $langs, 1, -1, -1, $obj->multicurrency_code); + if ($obj->mintx > 0 && $obj->mintx == $obj->maxtx) { + $txt .= ' - '.$langs->trans('Rate').' : '.price2num($obj->mintx, 'MU'); + } + $tmparray[] = $txt; + } + $this->db->free($resql); + + if (empty($tmparray)) { + return ''; + } + + // On its own line: appended inline it can push the card left column into the right one + return '
('.implode(', ', $tmparray).')'; + } + // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps diff --git a/htdocs/core/tpl/object_discounts.tpl.php b/htdocs/core/tpl/object_discounts.tpl.php index 163bb376e2878..11ecf10f9e1ae 100644 --- a/htdocs/core/tpl/object_discounts.tpl.php +++ b/htdocs/core/tpl/object_discounts.tpl.php @@ -101,6 +101,7 @@ if (!empty($cannotApplyDiscount) || !$isInvoice || $isNewObject || $object->statut > $objclassname::STATUS_DRAFT || $object->type == $objclassname::TYPE_CREDIT_NOTE || $object->type == $objclassname::TYPE_DEPOSIT) { $translationKey = empty($discount_type) ? 'CompanyHasDownPaymentOrCommercialDiscount' : 'HasDownPaymentOrCommercialDiscountFromSupplier'; $text = $langs->trans($translationKey, price($absolute_discount, 0, $langs, 1, -1, -1, $conf->currency)); + $text .= $form->getMulticurrencyCreditsInfo($thirdparty->id, $discount_type, $filterabsolutediscount); if ($isInvoice && !$isNewObject && $object->statut > $objclassname::STATUS_DRAFT && $object->type != $objclassname::TYPE_CREDIT_NOTE && $object->type != $objclassname::TYPE_DEPOSIT) { $text = $form->textwithpicto($text, $langs->trans('AbsoluteDiscountUse')); @@ -131,6 +132,7 @@ if (!empty($cannotApplyDiscount) || !$isInvoice || $isNewObject || $object->statut != $objclassname::STATUS_VALIDATED || $object->type == $objclassname::TYPE_CREDIT_NOTE) { $translationKey = empty($discount_type) ? 'CompanyHasCreditNote' : 'HasCreditNoteFromSupplier'; $text = $langs->trans($translationKey, price($absolute_creditnote, 0, $langs, 1, -1, -1, $conf->currency)); + $text .= $form->getMulticurrencyCreditsInfo($thirdparty->id, $discount_type, $filtercreditnote); if ($isInvoice && !$isNewObject && $object->statut == $objclassname::STATUS_DRAFT && $object->type != $objclassname::TYPE_DEPOSIT) { $text = $form->textwithpicto($text, $langs->trans('CreditNoteDepositUse')); diff --git a/htdocs/fourn/facture/card.php b/htdocs/fourn/facture/card.php index 1a76b5462a165..c328aed583da8 100644 --- a/htdocs/fourn/facture/card.php +++ b/htdocs/fourn/facture/card.php @@ -16,6 +16,7 @@ * Copyright (C) 2024-2026 MDW * Copyright (C) 2026 Vincent de Grandpré * Copyright (C) 2026 Lionel Vessiller + * Copyright (C) 2026 José MARTINEZ * * 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 @@ -560,25 +561,83 @@ //var_dump($object->getRemainToPay(0)); //var_dump($discount->amount_ttc);exit; - $remaintopay = $object->getRemainToPay(0); - if (price2num($discount->amount_ttc) > price2num($remaintopay)) { - // TODO Split the discount in 2 automatically - $error++; - setEventMessages($langs->trans("ErrorDiscountLargerThanRemainToPaySplitItBefore"), null, 'errors'); + // Under MULTICURRENCY_PAYMENT_USE_REAL_AMOUNTS, when the credit is in the invoice currency, compare and settle + // in that currency (the company-currency amounts may use different exchange rates and would compare wrongly). + $usemccompare = (getDolGlobalInt('MULTICURRENCY_PAYMENT_USE_REAL_AMOUNTS') && isModEnabled('multicurrency') + && !empty($discount->multicurrency_code) && !empty($object->multicurrency_code) + && $discount->multicurrency_code == $object->multicurrency_code && $object->multicurrency_code != $conf->currency); + $remaintopay = $usemccompare ? $object->getRemainToPay(1) : $object->getRemainToPay(0); + $discountamountforcompare = $usemccompare ? $discount->multicurrency_amount_ttc : $discount->amount_ttc; + $discounttolink = $discount; + $depositwassplit = false; + $splitappliedmsg = ''; + $splitremainmsg = ''; + if (price2num($discountamountforcompare, 'MT') > price2num($remaintopay, 'MT')) { + if ((float) $remaintopay <= 0) { + $error++; + setEventMessages($langs->trans("ErrorDiscountLargerThanRemainToPaySplitItBefore"), null, 'errors'); + } else { + // Credit larger than the remaining: split it automatically, apply up to the remaining amount (max, in the invoice currency) and keep the rest available + $depositeur = (float) $discount->amount_ttc; + $depositdev = (float) $discount->multicurrency_amount_ttc; + $remaindev = 0.0; + if ($usemccompare && $depositdev != 0) { + $applydev = (float) $remaintopay; + $applyeur = (float) price2num($applydev / $depositdev * $depositeur, 'MT'); + } else { + $applyeur = (float) $remaintopay; + $applydev = ($depositeur != 0 ? (float) price2num($applyeur / $depositeur * $depositdev, 'MT') : 0); + } + $splitparts = $discount->splitAmount($applyeur, (float) price2num($depositeur - $applyeur, 'MT')); + $applypart = $splitparts[0]; + $remainpart = $splitparts[1]; + if (!empty($discount->multicurrency_code) && $depositdev != 0) { + $remaindev = (float) price2num($depositdev - $applydev, 'MT'); + $applypart->multicurrency_amount_ttc = $applydev; + $applypart->multicurrency_amount_ht = price2num($applydev / (1 + (float) $applypart->tva_tx / 100), 'MT'); + $applypart->multicurrency_amount_tva = price2num($applydev - (float) $applypart->multicurrency_amount_ht); + $remainpart->multicurrency_amount_ttc = $remaindev; + $remainpart->multicurrency_amount_ht = price2num($remaindev / (1 + (float) $remainpart->tva_tx / 100), 'MT'); + $remainpart->multicurrency_amount_tva = price2num($remaindev - (float) $remainpart->multicurrency_amount_ht); + } + $discount->fk_facture_source = 0; + $discount->fk_invoice_supplier_source = 0; + $resdelete = $discount->delete($user); + $newidapply = $applypart->create($user); + $newidremain = $remainpart->create($user); + if ($resdelete > 0 && $newidapply > 0 && $newidremain > 0) { + $discounttolink = new DiscountAbsolute($db); + $discounttolink->fetch($newidapply); + // Build a positive feedback message about the automatic split (amount applied / amount kept available) + $depositwassplit = true; + $splitappliedmsg = price($applyeur, 0, $langs, 1, -1, -1, $conf->currency); + $splitremainmsg = price((float) price2num($depositeur - $applyeur, 'MT'), 0, $langs, 1, -1, -1, $conf->currency); + if (!empty($discount->multicurrency_code) && $depositdev != 0) { + $splitappliedmsg .= ' / '.price($applydev, 0, $langs, 1, -1, -1, $discount->multicurrency_code); + $splitremainmsg .= ' / '.price($remaindev, 0, $langs, 1, -1, -1, $discount->multicurrency_code); + } + } else { + $error++; + setEventMessages($langs->trans("Error"), null, 'errors'); + } + } } if (!$error) { - $result = $discount->link_to_invoice(0, $id); + $result = $discounttolink->link_to_invoice(0, $id); if ($result < 0) { $error++; - setEventMessages($discount->error, $discount->errors, 'errors'); + setEventMessages($discounttolink->error, $discounttolink->errors, 'errors'); } } if (!$error) { - $newremaintopay = $object->getRemainToPay(0); + $newremaintopay = $usemccompare ? $object->getRemainToPay(1) : $object->getRemainToPay(0); if ($newremaintopay == 0) { $object->setPaid($user); } + if ($depositwassplit) { + setEventMessages($langs->trans('DepositSplitAutomaticallyApplied', $splitappliedmsg, $splitremainmsg), null, 'warnings'); + } } } if (!$error) { @@ -3989,6 +4048,7 @@ function setRadioForTypeOfInvoice() { $depositamount = 0; $sql = "SELECT re.rowid, re.amount_ht, re.amount_tva, re.amount_ttc,"; + $sql .= " re.multicurrency_amount_ttc, re.multicurrency_code, re.multicurrency_tx,"; $sql .= " re.description, re.fk_invoice_supplier_source"; $sql .= " FROM ".MAIN_DB_PREFIX."societe_remise_except as re"; $sql .= " WHERE fk_invoice_supplier = ".((int) $object->id); @@ -4016,7 +4076,16 @@ function setRadioForTypeOfInvoice() { print ''; print ''; // Amount - print ''.price($obj->amount_ttc).''; + print ''.price($obj->amount_ttc); + // Amount in the invoice currency and rate as a sub-line, consistent with payments (option MULTICURRENCY_PAYMENT_USE_REAL_AMOUNTS) + if (getDolGlobalInt('MULTICURRENCY_PAYMENT_USE_REAL_AMOUNTS') && isModEnabled('multicurrency') && !empty($obj->multicurrency_code)) { + print '
'.price($obj->multicurrency_amount_ttc, 0, $langs, 1, -1, -1, $obj->multicurrency_code); + if (!empty($obj->multicurrency_tx) && $obj->multicurrency_tx > 0) { + print '
'.$langs->trans('Rate').' : '.price2num($obj->multicurrency_tx, 'MU'); + } + print '
'; + } + print ''; print ''; $i++; if ($invoice->type == FactureFournisseur::TYPE_CREDIT_NOTE) { diff --git a/htdocs/langs/en_US/bills.lang b/htdocs/langs/en_US/bills.lang index 7ece5af6c3c5f..9b81b25e035ce 100644 --- a/htdocs/langs/en_US/bills.lang +++ b/htdocs/langs/en_US/bills.lang @@ -752,3 +752,7 @@ CreditNoteNotCreatedErrorOnDiscount=Credit note for %s not created: error CreditNoteNotCreatedAlreadyConverted=Credit note for %s not created: invoice already converted CreditNotesCreated=%s credit note(s) created ThisMassActionIsOnlyForInvoices=This mass action is only available for invoices + +# Deposit/credit split in the invoice currency (option MULTICURRENCY_PAYMENT_USE_REAL_AMOUNTS) +Part=Part +DepositSplitAutomaticallyApplied=The available credit was larger than the remaining amount to pay and was split automatically: %s applied to this invoice, %s kept as an available credit for later use.