Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 100 additions & 20 deletions htdocs/comm/remx.php

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions htdocs/compta/facture/card.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
* Copyright (C) 2026 Vincent de Grandpré <vincent@de-grandpre.quebec>
* Copyright (C) 2026 Joachim Küter <git-jk@bloxera.com>
* Copyright (C) 2026 Lionel Vessiller <lvessiller@open-dsi.fr>
* Copyright (C) 2026 José MARTINEZ <jose.martinez@pichinov.com>
*
* 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
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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);
Expand All @@ -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 ' <span class="small nowraponall">'.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 '</span>';
}
print '</span>';
print '</td>';
// Delete discount
Expand Down
19 changes: 19 additions & 0 deletions htdocs/core/class/discount.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* Copyright (C) 2024-2026 MDW <mdeweerd@users.noreply.github.com>
* Copyright (C) 2024 Noé Cendrier <noe.cendrier@altairis.fr>
* Copyright (C) 2026 Vincent de Grandpré <vincent@de-grandpre.quebec>
* Copyright (C) 2026 José MARTINEZ <jose.martinez@pichinov.com>
*
* 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
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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,";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
72 changes: 71 additions & 1 deletion htdocs/core/class/html.form.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -2593,7 +2594,17 @@ public function select_remises($selected, $htmlname, $filter, $socid, $maxvalue
}
}

print '<option value="' . $obj->rowid . '"' . $selectstring . $disabled . '>' . $desc . ' (' . price($obj->amount_ht) . ' ' . $langs->trans("HT") . ' - ' . price($obj->amount_ttc) . ' ' . $langs->trans("TTC") . ')</option>';
// 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 '<option value="' . $obj->rowid . '"' . $selectstring . $disabled . '>' . $desc . ' (' . price($obj->amount_ht) . ' ' . $langs->trans("HT") . ' - ' . price($obj->amount_ttc) . ' ' . $langs->trans("TTC") . ')' . $multicurrencyinfo . '</option>';
$i++;
}
}
Expand Down Expand Up @@ -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 ' ';
}
Expand Down Expand Up @@ -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 '<br><span class="opacitymedium">('.implode(', ', $tmparray).')</span>';
}


// phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps

Expand Down
2 changes: 2 additions & 0 deletions htdocs/core/tpl/object_discounts.tpl.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand Down Expand Up @@ -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'));
Expand Down
87 changes: 78 additions & 9 deletions htdocs/fourn/facture/card.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
* Copyright (C) 2024-2026 MDW <mdeweerd@users.noreply.github.com>
* Copyright (C) 2026 Vincent de Grandpré <vincent@de-grandpre.quebec>
* Copyright (C) 2026 Lionel Vessiller <lvessiller@open-dsi.fr>
* Copyright (C) 2026 José MARTINEZ <jose.martinez@pichinov.com>
*
* 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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -4016,7 +4076,16 @@ function setRadioForTypeOfInvoice() {
print '</a>';
print '</td>';
// Amount
print '<td class="right">'.price($obj->amount_ttc).'</td>';
print '<td class="right">'.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 '<br><span class="opacitymedium small">'.price($obj->multicurrency_amount_ttc, 0, $langs, 1, -1, -1, $obj->multicurrency_code);
if (!empty($obj->multicurrency_tx) && $obj->multicurrency_tx > 0) {
print '<br>'.$langs->trans('Rate').' : '.price2num($obj->multicurrency_tx, 'MU');
}
print '</span>';
}
print '</td>';
print '</tr>';
$i++;
if ($invoice->type == FactureFournisseur::TYPE_CREDIT_NOTE) {
Expand Down
4 changes: 4 additions & 0 deletions htdocs/langs/en_US/bills.lang
Original file line number Diff line number Diff line change
Expand Up @@ -752,3 +752,7 @@ CreditNoteNotCreatedErrorOnDiscount=Credit note for <b>%s</b> not created: error
CreditNoteNotCreatedAlreadyConverted=Credit note for <b>%s</b> 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.
Loading