Merge remote-tracking branch 'upstream/develop' into extrafields-admin-unification

This commit is contained in:
Frédéric FRANCE 2026-08-12 07:06:22 +02:00
commit 8b96cbf1ba
No known key found for this signature in database
GPG key ID: C06E4A9656127239
26 changed files with 2275 additions and 146 deletions

View file

@ -714,7 +714,10 @@ if (empty($reshook)) {
$tva_tx .= ' (' . $lines[$i]->vat_src_code . ')';
}
$result = $object->addline($desc, $lines[$i]->subprice, $lines[$i]->qty, $tva_tx, $lines[$i]->localtax1_tx, $lines[$i]->localtax2_tx, $lines[$i]->fk_product, $lines[$i]->remise_percent, 'HT', 0, $lines[$i]->info_bits, $product_type, $lines[$i]->rang, $lines[$i]->special_code, $fk_parent_line, $lines[$i]->fk_fournprice, $lines[$i]->pa_ht, $label, $date_start, $date_end, $array_options, $lines[$i]->fk_unit);
// Preserve the TTC entry mode of the source line: a line entered including tax must
// stay in TTC so its total is computed from the typed value, without rounding drift.
$line_price_base_type = $lines[$i]->getPriceBaseType();
$result = $object->addline($desc, $lines[$i]->subprice, $lines[$i]->qty, $tva_tx, $lines[$i]->localtax1_tx, $lines[$i]->localtax2_tx, $lines[$i]->fk_product, $lines[$i]->remise_percent, $line_price_base_type, (float) $lines[$i]->subprice_ttc, $lines[$i]->info_bits, $product_type, $lines[$i]->rang, $lines[$i]->special_code, $fk_parent_line, $lines[$i]->fk_fournprice, $lines[$i]->pa_ht, $label, $date_start, $date_end, $array_options, $lines[$i]->fk_unit);
if ($result > 0) {
$lineid = $result;
@ -1072,7 +1075,7 @@ if (empty($reshook)) {
}
if ($line->product_type == 1) { // only service line
// Preserve the original entry mode of the line so the total is not drifted by rounding.
$line_price_base_type = $line->wasEnteredIncludingTax() ? 'TTC' : 'HT';
$line_price_base_type = $line->getPriceBaseType();
$line_pu = ($line_price_base_type === 'TTC') ? (float) $line->subprice_ttc : (float) $line->subprice;
$result = $object->updateline($line->id, $line_pu, $line->qty, $line->remise_percent, $line->tva_tx, $line->localtax1_tx, $line->localtax2_tx, $line->desc, $line_price_base_type, $line->info_bits, $line->special_code, $line->fk_parent_line, 0, $line->fk_fournprice, $line->pa_ht, $line->label, $line->product_type, $alldate_start, $alldate_end, $line->array_options, $line->fk_unit, $line->multicurrency_subprice);
$object->lines[$key] = $object->line;
@ -1089,7 +1092,7 @@ if (empty($reshook)) {
continue;
}
// Preserve the original entry mode of the line so the total is not drifted by rounding.
$line_price_base_type = $line->wasEnteredIncludingTax() ? 'TTC' : 'HT';
$line_price_base_type = $line->getPriceBaseType();
$line_pu = ($line_price_base_type === 'TTC') ? (float) $line->subprice_ttc : (float) $line->subprice;
$result = $object->updateline($line->id, $line_pu, $line->qty, $line->remise_percent, $vat_rate, $localtax1_rate, $localtax2_rate, $line->desc, $line_price_base_type, $line->info_bits, $line->special_code, $line->fk_parent_line, 0, $line->fk_fournprice, $line->pa_ht, $line->label, $line->product_type, $line->date_start, $line->date_end, $line->array_options, $line->fk_unit, $line->multicurrency_subprice);
$object->lines[$key] = $object->line;
@ -1107,7 +1110,7 @@ if (empty($reshook)) {
$tvatx .= ' (' . $line->vat_src_code . ')';
}
// Preserve the original entry mode of the line so the total is not drifted by rounding.
$line_price_base_type = $line->wasEnteredIncludingTax() ? 'TTC' : 'HT';
$line_price_base_type = $line->getPriceBaseType();
$line_pu = ($line_price_base_type === 'TTC') ? (float) $line->subprice_ttc : (float) $line->subprice;
$result = $object->updateline($line->id, $line_pu, $line->qty, (float) $remise_percent, $tvatx, $line->localtax1_tx, $line->localtax2_tx, $line->desc, $line_price_base_type, $line->info_bits, $line->special_code, $line->fk_parent_line, 0, $line->fk_fournprice, $line->pa_ht, $line->label, $line->product_type, $line->date_start, $line->date_end, $line->array_options, $line->fk_unit, $line->multicurrency_subprice);
$object->lines[$key] = $object->line;

View file

@ -1369,17 +1369,19 @@ class Propal extends CommonObject
$origintype = $this->element;
}
// Preserve the original entry mode of the line so the total is computed from the typed value (no rounding drift).
$line_price_base_type = $line->getPriceBaseType();
$result = $this->addline(
$line->desc,
$line->subprice,
(float) $line->subprice,
$line->qty,
$vatrate,
$line->localtax1_tx,
$line->localtax2_tx,
$line->fk_product,
$line->remise_percent,
'HT',
0,
$line_price_base_type,
(float) $line->subprice_ttc,
$line->info_bits,
$line->product_type,
$line->rang,

View file

@ -20,6 +20,7 @@
* Copyright (C) 2022 Gauthier VERDOL <gauthier.verdol@atm-consulting.fr>
* Copyright (C) 2023 William Mead <william.mead@manchenumerique.fr>
* Copyright (C) 2024-2026 MDW <mdeweerd@users.noreply.github.com>
* Copyright (C) 2026 Lionel Vessiller <lvessiller@open-dsi.fr>
*
* 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
@ -365,19 +366,6 @@ class PropaleLigne extends CommonObjectLine
$this->db = $db;
}
/**
* Return true if the unit price was originally entered including tax (TTC mode).
* Useful to preserve the entry mode on no-op edits and to avoid total drift.
* Note: cannot use !empty() because MySQL returns doubles as strings like "0.00000000"
* which empty() treats as non-empty.
*
* @return bool
*/
public function wasEnteredIncludingTax()
{
return isset($this->subprice_ttc) && (float) $this->subprice_ttc != 0;
}
/**
* Retrieve the propal line object
*

View file

@ -17,6 +17,7 @@
* Copyright (C) 2023-2024 Benjamin Falière <benjamin.faliere@altairis.fr>
* Copyright (C) 2024-2026 MDW <mdeweerd@users.noreply.github.com>
* Copyright (C) 2025 Lenin Rivas <lenin.rivas777@gmail.com>
* Copyright (C) 2026 Lionel Vessiller <lvessiller@open-dsi.fr>
*
* 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
@ -494,6 +495,8 @@ if (empty($reshook)) {
$tva_tx .= ' (' . $lines[$i]->vat_src_code . ')';
}
// Preserve the original entry mode of the line so the total is computed from the typed value (no rounding drift).
$line_price_base_type = $lines[$i]->getPriceBaseType();
$result = $object->addline(
$desc,
$lines[$i]->subprice,
@ -505,8 +508,8 @@ if (empty($reshook)) {
$lines[$i]->remise_percent,
$lines[$i]->info_bits,
$lines[$i]->fk_remise_except,
'HT',
0,
$line_price_base_type,
(float) $lines[$i]->subprice_ttc,
$date_start,
$date_end,
$product_type,
@ -766,7 +769,10 @@ if (empty($reshook)) {
continue;
}
if ($line->product_type == 1) { // only service line
$result = $object->updateline($line->id, $line->desc, $line->subprice, $line->qty, $line->remise_percent, $line->tva_tx, $line->localtax1_tx, $line->localtax2_tx, 'HT', $line->info_bits, $alldate_start, $alldate_end, $line->product_type, $line->fk_parent_line, 0, $line->fk_fournprice, $line->pa_ht, $line->label, $line->special_code, $line->array_options, $line->fk_unit, $line->multicurrency_subprice);
// Preserve the original entry mode of the line so the total is not drifted by rounding.
$line_price_base_type = $line->getPriceBaseType();
$line_pu = ($line_price_base_type === 'TTC') ? (float) $line->subprice_ttc : (float) $line->subprice;
$result = $object->updateline($line->id, $line->desc, $line_pu, $line->qty, $line->remise_percent, $line->tva_tx, $line->localtax1_tx, $line->localtax2_tx, $line_price_base_type, $line->info_bits, $alldate_start, $alldate_end, $line->product_type, $line->fk_parent_line, 0, $line->fk_fournprice, $line->pa_ht, $line->label, $line->special_code, $line->array_options, $line->fk_unit, $line->multicurrency_subprice);
}
}
} elseif ($action == 'addline' && GETPOST('submitforalllines', 'alpha') && GETPOST('vatforalllines', 'alpha') !== '' && $usercancreate) {
@ -779,7 +785,10 @@ if (empty($reshook)) {
if ($line->special_code == SUBTOTALS_SPECIAL_CODE) {
continue;
}
$result = $object->updateline($line->id, $line->desc, $line->subprice, $line->qty, $line->remise_percent, $vat_rate, $localtax1_rate, $localtax2_rate, 'HT', $line->info_bits, $line->date_start, $line->date_end, $line->product_type, $line->fk_parent_line, 0, $line->fk_fournprice, $line->pa_ht, $line->label, $line->special_code, $line->array_options, $line->fk_unit, $line->multicurrency_subprice);
// Preserve the original entry mode of the line so the total is not drifted by rounding.
$line_price_base_type = $line->getPriceBaseType();
$line_pu = ($line_price_base_type === 'TTC') ? (float) $line->subprice_ttc : (float) $line->subprice;
$result = $object->updateline($line->id, $line->desc, $line_pu, $line->qty, $line->remise_percent, $vat_rate, $localtax1_rate, $localtax2_rate, $line_price_base_type, $line->info_bits, $line->date_start, $line->date_end, $line->product_type, $line->fk_parent_line, 0, $line->fk_fournprice, $line->pa_ht, $line->label, $line->special_code, $line->array_options, $line->fk_unit, $line->multicurrency_subprice);
}
} elseif ($action == 'addline' && GETPOST('submitforalllines', 'alpha') && GETPOST('remiseforalllines', 'alpha') !== '' && $usercancreate) {
// Define remise_percent
@ -793,7 +802,10 @@ if (empty($reshook)) {
if (!empty($line->vat_src_code)) {
$tvatx .= ' (' . $line->vat_src_code . ')';
}
$result = $object->updateline($line->id, $line->desc, $line->subprice, $line->qty, (float) $remise_percent, $tvatx, $line->localtax1_tx, $line->localtax2_tx, 'HT', $line->info_bits, $line->date_start, $line->date_end, $line->product_type, $line->fk_parent_line, 0, $line->fk_fournprice, $line->pa_ht, $line->label, $line->special_code, $line->array_options, $line->fk_unit, $line->multicurrency_subprice);
// Preserve the original entry mode of the line so the total is not drifted by rounding.
$line_price_base_type = $line->getPriceBaseType();
$line_pu = ($line_price_base_type === 'TTC') ? (float) $line->subprice_ttc : (float) $line->subprice;
$result = $object->updateline($line->id, $line->desc, $line_pu, $line->qty, (float) $remise_percent, $tvatx, $line->localtax1_tx, $line->localtax2_tx, $line_price_base_type, $line->info_bits, $line->date_start, $line->date_end, $line->product_type, $line->fk_parent_line, 0, $line->fk_fournprice, $line->pa_ht, $line->label, $line->special_code, $line->array_options, $line->fk_unit, $line->multicurrency_subprice);
}
} elseif ($action == 'addline' && $usercancreate && (
(GETPOST('submitforallmargins', 'alpha') && GETPOST('marginforalllines', 'alpha') !== '') ||
@ -1247,23 +1259,29 @@ if (empty($reshook)) {
$tmpvat = (float) price2num(preg_replace('/\s*\(.*\)/', '', $tva_tx));
$tmpprodvat = (float) price2num(preg_replace('/\s*\(.*\)/', '', (string) $prod->tva_tx));
// Set unit price to use
// Set unit price to use.
// Force price_base_type to match what the user actually entered, so the total is computed
// from the typed value and not from the converted/rounded value (avoids 0.01 rounding drift).
if (!empty($price_ht) || (string) $price_ht === '0') {
$pu_ht = (float) price2num($price_ht, 'MU');
$pu_ttc = (float) price2num((float) $pu_ht * (1 + ((float) $tmpvat / 100)), 'MU');
$price_base_type = 'HT';
} elseif (!empty($price_ht_devise) || (string) $price_ht_devise === '0') {
$pu_ht_devise = price2num($price_ht_devise, 'MU');
$pu_ttc_devise = (float) price2num((float) $pu_ht_devise * (1 + ((float) $tmpvat / 100)), 'MU');
$pu_ht = '';
$pu_ttc = '';
$price_base_type = 'HT';
} elseif (!empty($price_ttc) || (string) $price_ttc === '0') {
$pu_ttc = (float) price2num($price_ttc, 'MU');
$pu_ht = (float) price2num((float) $pu_ttc / (1 + ((float) $tmpvat / 100)), 'MU');
$price_base_type = 'TTC';
} elseif (!empty($price_ttc_devise) || (string) $price_ttc_devise === '0') {
$pu_ttc_devise = (float) price2num($price_ttc_devise, 'MU');
$pu_ht_devise = (float) price2num((float) $pu_ttc_devise / (1 + ((float) $tmpvat / 100)), 'MU');
$pu_ht = '';
$pu_ttc = '';
$price_base_type = 'TTC';
} elseif ($tmpvat != $tmpprodvat) {
// Is this still used ?
if ($price_base_type != 'HT') {
@ -1653,11 +1671,25 @@ if (empty($reshook)) {
$remise_percent = GETPOST('remise_percent') != '' ? price2num(GETPOST('remise_percent'), '', 2) : 0;
// The form JS clears the other field when the user edits one of them: only the modified field is filled.
// When both fields are submitted, the user did not change the price - we must preserve the original
// storage mode of the line, otherwise a no-op save would shift the total by rounding.
$pu = $pu_ht;
$price_base_type = 'HT';
if (empty($pu) && !empty($pu_ttc)) {
if (empty($pu_ht) && !empty($pu_ttc)) {
$pu = $pu_ttc;
$price_base_type = 'TTC';
} elseif (!empty($pu_ht) && !empty($pu_ttc)) {
foreach ($object->lines as $line_obj) {
if ($line_obj->id == GETPOSTINT('lineid')) {
// Line was originally entered in TTC mode (subprice_ttc filled by addline)
if ($line_obj->wasEnteredIncludingTax()) {
$pu = $pu_ttc;
$price_base_type = 'TTC';
}
break;
}
}
}
// Check minimum price

View file

@ -16,6 +16,7 @@
* Copyright (C) 2024-2026 MDW <mdeweerd@users.noreply.github.com>
* Copyright (C) 2024 William Mead <william.mead@manchenumerique.fr>
* Copyright (C) 2026 Vincent de Grandpré <vincent@de-grandpre.quebec>
* Copyright (C) 2026 Lionel Vessiller <lvessiller@open-dsi.fr>
*
* 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
@ -1127,9 +1128,11 @@ class Commande extends CommonOrder
$line->ref_ext = '';
}
// Preserve the original entry mode of the line so the total is computed from the typed value (no rounding drift).
$line_price_base_type = $line->getPriceBaseType();
$result = $this->addline(
$line->desc,
$line->subprice,
(float) $line->subprice,
$line->qty,
$vatrate,
$line->localtax1_tx,
@ -1138,8 +1141,8 @@ class Commande extends CommonOrder
$line->remise_percent,
$line->info_bits,
$line->fk_remise_except,
'HT',
0,
$line_price_base_type,
(float) $line->subprice_ttc,
$line->date_start,
$line->date_end,
$line->product_type,
@ -1425,6 +1428,7 @@ class Commande extends CommonOrder
$line->desc = $object->lines[$i]->desc;
$line->price = $object->lines[$i]->price;
$line->subprice = $object->lines[$i]->subprice;
$line->subprice_ttc = $object->lines[$i]->subprice_ttc; // Preserve the TTC entry mode so create() keeps the typed value (no rounding drift).
$line->vat_src_code = $object->lines[$i]->vat_src_code;
$line->tva_tx = $object->lines[$i]->tva_tx;
$line->localtax1_tx = $object->lines[$i]->localtax1_tx;
@ -1809,6 +1813,8 @@ class Commande extends CommonOrder
$this->line->fk_remise_except = $fk_remise_except;
$this->line->remise_percent = $remise_percent;
$this->line->subprice = (float) $pu_ht;
// Persist the original entry mode of the line so updateline() can preserve it later.
$this->line->subprice_ttc = ($price_base_type === 'TTC') ? (float) $pu_ttc : 0;
$this->line->rang = $ranktouse;
$this->line->info_bits = $info_bits;
$this->line->total_ht = (float) $total_ht;
@ -3344,6 +3350,8 @@ class Commande extends CommonOrder
$this->line->localtax2_type = empty($localtaxes_type[2]) ? '' : $localtaxes_type[2];
$this->line->remise_percent = $remise_percent;
$this->line->subprice = (float) $pu_ht;
// Persist the original entry mode of the line so a no-op edit can preserve it later.
$this->line->subprice_ttc = ($price_base_type === 'TTC') ? (float) $pu_ttc : 0;
$this->line->info_bits = $info_bits;
$this->line->special_code = $special_code;
$this->line->total_ht = (float) $total_ht;

View file

@ -15,6 +15,7 @@
* Copyright (C) 2022 Gauthier VERDOL <gauthier.verdol@atm-consulting.fr>
* Copyright (C) 2024-2026 MDW <mdeweerd@users.noreply.github.com>
* Copyright (C) 2024 William Mead <william.mead@manchenumerique.fr>
* Copyright (C) 2026 Lionel Vessiller <lvessiller@open-dsi.fr>
*
* 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
@ -224,7 +225,7 @@ class OrderLine extends CommonOrderLine
public function fetch($rowid)
{
$sql = 'SELECT cd.rowid, cd.fk_commande, cd.fk_parent_line, cd.fk_product, cd.product_type, cd.label as custom_label, cd.description, cd.price, cd.qty, cd.tva_tx, cd.localtax1_tx, cd.localtax2_tx,';
$sql .= ' cd.remise, cd.remise_percent, cd.fk_remise_except, cd.subprice, cd.ref_ext,';
$sql .= ' cd.remise, cd.remise_percent, cd.fk_remise_except, cd.subprice, cd.subprice_ttc, cd.ref_ext,';
$sql .= ' cd.info_bits, cd.total_ht, cd.total_tva, cd.total_localtax1, cd.total_localtax2, cd.total_ttc, cd.fk_product_fournisseur_price as fk_fournprice, cd.buy_price_ht as pa_ht, cd.rang, cd.special_code,';
$sql .= ' cd.fk_unit,';
$sql .= ' cd.fk_multicurrency, cd.multicurrency_code, cd.multicurrency_subprice, cd.multicurrency_total_ht, cd.multicurrency_total_tva, cd.multicurrency_total_ttc,';
@ -254,6 +255,7 @@ class OrderLine extends CommonOrderLine
$this->qty = $objp->qty;
$this->price = $objp->price;
$this->subprice = $objp->subprice;
$this->subprice_ttc = $objp->subprice_ttc;
$this->ref_ext = $objp->ref_ext;
$this->vat_src_code = $objp->vat_src_code;
$this->tva_tx = $objp->tva_tx;
@ -481,7 +483,7 @@ class OrderLine extends CommonOrderLine
$sql = 'INSERT INTO '.MAIN_DB_PREFIX.'commandedet';
$sql .= ' (fk_commande, fk_parent_line, label, description, qty, ref_ext,';
$sql .= ' vat_src_code, tva_tx, localtax1_tx, localtax2_tx, localtax1_type, localtax2_type,';
$sql .= ' fk_product, product_type, remise_percent, subprice, price, fk_remise_except,';
$sql .= ' fk_product, product_type, remise_percent, subprice, subprice_ttc, price, fk_remise_except,';
$sql .= ' special_code, rang, fk_product_fournisseur_price, buy_price_ht,';
$sql .= ' info_bits, total_ht, total_tva, total_localtax1, total_localtax2, total_ttc, date_start, date_end,';
$sql .= ' fk_unit,';
@ -503,6 +505,7 @@ class OrderLine extends CommonOrderLine
$sql .= " ".((int) $this->product_type).",";
$sql .= " '".price2num($this->remise_percent)."',";
$sql .= " ".(price2num($this->subprice) !== '' ? price2num($this->subprice) : "null").",";
$sql .= " ".price2num($this->subprice_ttc).",";
$sql .= " ".($this->price != '' ? "'".price2num($this->price)."'" : "null").",";
$sql .= ' '.(!empty($this->fk_remise_except) ? ((int) $this->fk_remise_except) : "null").',';
$sql .= ' '.((int) $this->special_code).',';
@ -661,6 +664,7 @@ class OrderLine extends CommonOrderLine
$sql .= " , qty=".price2num($this->qty);
$sql .= " , ref_ext='".$this->db->escape($this->ref_ext)."'";
$sql .= " , subprice=".price2num($this->subprice);
$sql .= " , subprice_ttc=".price2num($this->subprice_ttc);
$sql .= " , remise_percent=".price2num($this->remise_percent);
$sql .= " , price=".price2num($this->price); // TODO A virer
$sql .= " , remise=".price2num($this->remise); // TODO A virer

View file

@ -22,6 +22,7 @@
* Copyright (C) 2025 Lenin Rivas <lenin.rivas777@gmail.com>
* 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>
*
* 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
@ -1531,6 +1532,7 @@ if (empty($reshook)) {
$line->fk_parent_line = $fk_parent_line;
$line->subprice = -$line->subprice; // invert price for object
$line->subprice_ttc = -$line->subprice_ttc; // keep the TTC entry mode with the inverted sign (no rounding drift)
// $line->pa_ht = $line->pa_ht; // we chose to have buy/cost price always positive, so no revert of sign here
$line->total_ht = -$line->total_ht;
$line->total_tva = -$line->total_tva;
@ -2098,6 +2100,8 @@ if (empty($reshook)) {
$localtax1_tx = get_localtax($tva_tx, 1, $object->thirdparty);
$localtax2_tx = get_localtax($tva_tx, 2, $object->thirdparty);
// Preserve the original entry mode of the line so the total is computed from the typed value (no rounding drift).
$line_price_base_type = $lines[$i]->getPriceBaseType();
$result = $object->addline(
$desc,
$lines[$i]->subprice,
@ -2112,8 +2116,8 @@ if (empty($reshook)) {
0,
(int) $lines[$i]->info_bits,
isset($lines[$i]->fk_remise_except) ? $lines[$i]->fk_remise_except : null,
'HT',
0,
$line_price_base_type,
(float) $lines[$i]->subprice_ttc,
$product_type,
$lines[$i]->rang,
$lines[$i]->special_code,
@ -2401,7 +2405,10 @@ if (empty($reshook)) {
continue;
}
if ($line->product_type == 1) { // only service line
$result = $object->updateline($line->id, $line->desc, $line->subprice, $line->qty, $line->remise_percent, $alldate_start, $alldate_end, $line->tva_tx, $line->localtax1_tx, $line->localtax2_tx, 'HT', $line->info_bits, $line->product_type, $line->fk_parent_line, 0, $line->fk_fournprice, $line->pa_ht, $line->label, $line->special_code, $line->array_options, $line->situation_percent, $line->fk_unit, $line->multicurrency_subprice);
// Preserve the original entry mode of the line so the total is not drifted by rounding.
$line_price_base_type = $line->getPriceBaseType();
$line_pu = ($line_price_base_type === 'TTC') ? (float) $line->subprice_ttc : (float) $line->subprice;
$result = $object->updateline($line->id, $line->desc, $line_pu, $line->qty, $line->remise_percent, $alldate_start, $alldate_end, $line->tva_tx, $line->localtax1_tx, $line->localtax2_tx, $line_price_base_type, $line->info_bits, $line->product_type, $line->fk_parent_line, 0, $line->fk_fournprice, $line->pa_ht, $line->label, $line->special_code, $line->array_options, $line->situation_percent, $line->fk_unit, $line->multicurrency_subprice);
}
}
} elseif ($action == 'addline' && GETPOST('submitforalllines', 'alpha') && GETPOST('vatforalllines', 'alpha') !== '' && $usercancreate) {
@ -2414,7 +2421,10 @@ if (empty($reshook)) {
if ($line->special_code == SUBTOTALS_SPECIAL_CODE) {
continue;
}
$result = $object->updateline($line->id, $line->desc, $line->subprice, $line->qty, $line->remise_percent, $line->date_start, $line->date_end, $vat_rate, $localtax1_rate, $localtax2_rate, 'HT', $line->info_bits, $line->product_type, $line->fk_parent_line, 0, $line->fk_fournprice, $line->pa_ht, $line->label, $line->special_code, $line->array_options, $line->situation_percent, $line->fk_unit, $line->multicurrency_subprice);
// Preserve the original entry mode of the line so the total is not drifted by rounding.
$line_price_base_type = $line->getPriceBaseType();
$line_pu = ($line_price_base_type === 'TTC') ? (float) $line->subprice_ttc : (float) $line->subprice;
$result = $object->updateline($line->id, $line->desc, $line_pu, $line->qty, $line->remise_percent, $line->date_start, $line->date_end, $vat_rate, $localtax1_rate, $localtax2_rate, $line_price_base_type, $line->info_bits, $line->product_type, $line->fk_parent_line, 0, $line->fk_fournprice, $line->pa_ht, $line->label, $line->special_code, $line->array_options, $line->situation_percent, $line->fk_unit, $line->multicurrency_subprice);
}
} elseif ($action == 'addline' && GETPOST('submitforalllines', 'alpha') && GETPOST('remiseforalllines', 'alpha') !== '' && $usercancreate) {
// Define vat_rate
@ -2428,7 +2438,10 @@ if (empty($reshook)) {
if (!empty($line->vat_src_code)) {
$tvatx .= ' ('.$line->vat_src_code.')';
}
$result = $object->updateline($line->id, $line->desc, $line->subprice, $line->qty, (float) $remise_percent, $line->date_start, $line->date_end, $tvatx, $line->localtax1_tx, $line->localtax2_tx, 'HT', $line->info_bits, $line->product_type, $line->fk_parent_line, 0, $line->fk_fournprice, $line->pa_ht, $line->label, $line->special_code, $line->array_options, $line->situation_percent, $line->fk_unit, $line->multicurrency_subprice);
// Preserve the original entry mode of the line so the total is not drifted by rounding.
$line_price_base_type = $line->getPriceBaseType();
$line_pu = ($line_price_base_type === 'TTC') ? (float) $line->subprice_ttc : (float) $line->subprice;
$result = $object->updateline($line->id, $line->desc, $line_pu, $line->qty, (float) $remise_percent, $line->date_start, $line->date_end, $tvatx, $line->localtax1_tx, $line->localtax2_tx, $line_price_base_type, $line->info_bits, $line->product_type, $line->fk_parent_line, 0, $line->fk_fournprice, $line->pa_ht, $line->label, $line->special_code, $line->array_options, $line->situation_percent, $line->fk_unit, $line->multicurrency_subprice);
}
} elseif ($action == 'confirm_addtitleline' && $usercancreate) {
// Handling adding a new title line for subtotals module
@ -2706,24 +2719,29 @@ if (empty($reshook)) {
$tmpvat = (float) price2num(preg_replace('/\s*\(.*\)/', '', $tva_tx));
$tmpprodvat = price2num(preg_replace('/\s*\(.*\)/', '', (string) $prod->tva_tx));
// Set unit price to use
// TODO We should not have this
// Set unit price to use.
// Force price_base_type to match what the user actually entered, so the total is computed
// from the typed value and not from the converted/rounded value (avoids 0.01 rounding drift).
if (!empty($price_ht) || $price_ht === '0') {
$pu_ht = price2num($price_ht, 'MU');
$pu_ttc = price2num((float) $pu_ht * (1 + ($tmpvat / 100)), 'MU');
$price_base_type = 'HT';
} elseif (!empty($price_ht_devise) || $price_ht_devise === '0') {
$pu_ht_devise = price2num($price_ht_devise, 'MU');
$pu_ttc_devise = (float) price2num((float) $pu_ht_devise * (1 + ((float) $tmpvat / 100)), 'MU');
$pu_ht = '';
$pu_ttc = '';
$price_base_type = 'HT';
} elseif (!empty($price_ttc) || $price_ttc === '0') {
$pu_ttc = price2num($price_ttc, 'MU');
$pu_ht = price2num((float) $pu_ttc / (1 + ($tmpvat / 100)), 'MU');
$price_base_type = 'TTC';
} elseif (!empty($price_ttc_devise) || (string) $price_ttc_devise === '0') {
$pu_ttc_devise = (float) price2num($price_ttc_devise, 'MU');
$pu_ht_devise = (float) price2num((float) $pu_ttc_devise / (1 + ((float) $tmpvat / 100)), 'MU');
$pu_ht = '';
$pu_ttc = '';
$price_base_type = 'TTC';
} elseif ($tmpvat != $tmpprodvat) {
// Is this still used ?
if ($price_base_type != 'HT') {
@ -3231,11 +3249,25 @@ if (empty($reshook)) {
$remise_percent = 0;
}
$price_base_type = 'HT';
// The form JS clears the other field when the user edits one of them: only the modified field is filled.
// When both fields are submitted, the user did not change the price - we must preserve the original
// storage mode of the line, otherwise a no-op save would shift the total by rounding.
$pu = $pu_ht;
if (empty($pu) && !empty($pu_ttc)) {
$price_base_type = 'HT';
if (empty($pu_ht) && !empty($pu_ttc)) {
$pu = $pu_ttc;
$price_base_type = 'TTC';
} elseif (!empty($pu_ht) && !empty($pu_ttc)) {
foreach ($object->lines as $line_obj) {
if ($line_obj->id == GETPOSTINT('lineid')) {
// Line was originally entered in TTC mode (subprice_ttc filled by addline)
if ($line_obj->wasEnteredIncludingTax()) {
$pu = $pu_ttc;
$price_base_type = 'TTC';
}
break;
}
}
}
// Check minimum price

View file

@ -25,6 +25,7 @@
* Copyright (C) 2024-2025 Frédéric France <frederic.france@free.fr>
* Copyright (C) 2025 Lenin Rivas <lenin.rivas777@gmail.com>
* Copyright (C) 2026 Vincent de Grandpré <vincent@de-grandpre.quebec>
* Copyright (C) 2026 Lionel Vessiller <lvessiller@open-dsi.fr>
*
* 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
@ -898,9 +899,11 @@ class Facture extends CommonInvoice
$newinvoiceline->fk_remise_except = $discountId;
}
// Preserve the original entry mode of the line so the total is computed from the typed value (no rounding drift).
$line_price_base_type = $newinvoiceline->getPriceBaseType();
$result = $this->addline(
$newinvoiceline->desc,
$newinvoiceline->subprice,
(float) $newinvoiceline->subprice,
$newinvoiceline->qty,
$vatrate,
$newinvoiceline->localtax1_tx,
@ -912,8 +915,8 @@ class Facture extends CommonInvoice
$newinvoiceline->fk_code_ventilation,
$newinvoiceline->info_bits,
$newinvoiceline->fk_remise_except,
'HT',
0,
$line_price_base_type,
(float) $newinvoiceline->subprice_ttc,
$newinvoiceline->product_type,
$newinvoiceline->rang,
$newinvoiceline->special_code,
@ -1246,6 +1249,7 @@ class Facture extends CommonInvoice
$facture->lines[$i]->fk_prev_id = $this->lines[$i]->rowid;
if ($invertdetail) {
$facture->lines[$i]->subprice = -$facture->lines[$i]->subprice;
$facture->lines[$i]->subprice_ttc = -$facture->lines[$i]->subprice_ttc; // Keep the TTC entry mode with the inverted sign so the credit note has no rounding drift.
$facture->lines[$i]->total_ht = -$facture->lines[$i]->total_ht;
$facture->lines[$i]->total_tva = -$facture->lines[$i]->total_tva;
$facture->lines[$i]->total_localtax1 = -$facture->lines[$i]->total_localtax1;
@ -1461,6 +1465,7 @@ class Facture extends CommonInvoice
$line->label = $src_line->label;
$line->desc = $src_line->desc;
$line->subprice = $src_line->subprice;
$line->subprice_ttc = $src_line->subprice_ttc; // Preserve the TTC entry mode so create() keeps the typed value (no rounding drift).
$line->total_ht = $src_line->total_ht;
$line->total_tva = $src_line->total_tva;
$line->total_localtax1 = $src_line->total_localtax1;
@ -1603,6 +1608,7 @@ class Facture extends CommonInvoice
$line->label = $object->lines[$i]->label;
$line->desc = $object->lines[$i]->desc;
$line->subprice = $object->lines[$i]->subprice;
$line->subprice_ttc = $object->lines[$i]->subprice_ttc; // Preserve the TTC entry mode so create() keeps the typed value (no rounding drift).
$line->total_ht = $object->lines[$i]->total_ht;
$line->total_tva = $object->lines[$i]->total_tva;
$line->total_localtax1 = $object->lines[$i]->total_localtax1;
@ -2527,7 +2533,7 @@ class Facture extends CommonInvoice
}
$sql = 'SELECT l.rowid, l.fk_facture, l.fk_product, l.fk_parent_line, l.label as custom_label, l.description, l.product_type, l.price, l.qty, l.vat_src_code, l.tva_tx,';
$sql .= ' l.localtax1_tx, l.localtax2_tx, l.localtax1_type, l.localtax2_type, l.remise_percent, l.fk_remise_except, l.subprice, l.ref_ext,';
$sql .= ' l.localtax1_tx, l.localtax2_tx, l.localtax1_type, l.localtax2_type, l.remise_percent, l.fk_remise_except, l.subprice, l.subprice_ttc, l.ref_ext,';
$sql .= ' l.situation_percent, l.fk_prev_id,';
$sql .= ' l.rang, l.special_code, l.batch, l.fk_warehouse,';
$sql .= ' l.date_start as date_start, l.date_end as date_end,';
@ -2592,6 +2598,7 @@ class Facture extends CommonInvoice
$line->fk_product_type = $objp->fk_product_type; // Type of product
$line->qty = $objp->qty;
$line->subprice = $objp->subprice;
$line->subprice_ttc = $objp->subprice_ttc;
$line->ref_ext = $objp->ref_ext; // line external ref
$line->vat_src_code = $objp->vat_src_code;
@ -4435,6 +4442,8 @@ class Facture extends CommonInvoice
$this->line->qty = ($this->type == self::TYPE_CREDIT_NOTE ? abs((float) $qty) : (float) $qty); // For credit note, quantity is always positive and unit price negative
$this->line->subprice = ($this->type == self::TYPE_CREDIT_NOTE ? -abs((float) $pu_ht) : (float) $pu_ht); // For credit note, unit price always negative, always positive otherwise
// Persist the original entry mode of the line so updateline() can preserve it later.
$this->line->subprice_ttc = ($price_base_type === 'TTC') ? ($this->type == self::TYPE_CREDIT_NOTE ? -abs((float) $pu_ttc) : (float) $pu_ttc) : 0;
$this->line->vat_src_code = $vat_src_code;
$this->line->tva_tx = $txtva;
@ -4777,6 +4786,8 @@ class Facture extends CommonInvoice
$this->line->remise_percent = $remise_percent;
$this->line->subprice = ($apply_abs_price_on_credit_note ? -abs((float) $pu_ht) : (float) $pu_ht); // For credit note, unit price always negative, always positive otherwise
// Persist the original entry mode of the line so a no-op edit can preserve it later.
$this->line->subprice_ttc = ($price_base_type === 'TTC') ? ($apply_abs_price_on_credit_note ? -abs((float) $pu_ttc) : (float) $pu_ttc) : 0;
$this->line->date_start = $date_start;
$this->line->date_end = $date_end;
$this->line->total_ht = (($apply_abs_price_on_credit_note || $qty < 0) ? -abs((float) $total_ht) : (float) $total_ht); // For credit note and if qty is negative, total is negative

View file

@ -22,6 +22,7 @@
* Copyright (C) 2023 Nick Fragoulis
* Copyright (C) 2024-2026 MDW <mdeweerd@users.noreply.github.com>
* Copyright (C) 2024-2025 Frédéric France <frederic.france@free.fr>
* Copyright (C) 2026 Lionel Vessiller <lvessiller@open-dsi.fr>
*
* 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
@ -223,7 +224,7 @@ class FactureLigne extends CommonInvoiceLine
}
$sql = 'SELECT fd.rowid, fd.fk_facture, fd.fk_parent_line, fd.fk_product, fd.product_type, fd.label as custom_label, fd.description, fd.price, fd.qty, fd.vat_src_code, fd.tva_tx,';
$sql .= ' fd.localtax1_tx, fd. localtax2_tx, fd.remise, fd.remise_percent, fd.fk_remise_except, fd.subprice, fd.ref_ext,';
$sql .= ' fd.localtax1_tx, fd. localtax2_tx, fd.remise, fd.remise_percent, fd.fk_remise_except, fd.subprice, fd.subprice_ttc, fd.ref_ext,';
$sql .= ' fd.date_start as date_start, fd.date_end as date_end, fd.fk_product_fournisseur_price as fk_fournprice, fd.buy_price_ht as pa_ht,';
$sql .= ' fd.info_bits, fd.special_code, fd.total_ht, fd.total_tva, fd.total_ttc, fd.total_localtax1, fd.total_localtax2, fd.rang,';
$sql .= ' fd.fk_code_ventilation,';
@ -280,6 +281,7 @@ class FactureLigne extends CommonInvoiceLine
$this->desc = $objp->description;
$this->qty = $objp->qty;
$this->subprice = $objp->subprice;
$this->subprice_ttc = $objp->subprice_ttc;
$this->ref_ext = $objp->ref_ext;
$this->vat_src_code = $objp->vat_src_code;
$this->tva_tx = $objp->tva_tx;
@ -496,7 +498,7 @@ class FactureLigne extends CommonInvoiceLine
$sql = 'INSERT INTO '.MAIN_DB_PREFIX.'facturedet';
$sql .= ' (fk_facture, fk_parent_line, label, description, qty,';
$sql .= ' vat_src_code, tva_tx, localtax1_tx, localtax2_tx, localtax1_type, localtax2_type,';
$sql .= ' fk_product, product_type, remise_percent, subprice, ref_ext, fk_remise_except,';
$sql .= ' fk_product, product_type, remise_percent, subprice, subprice_ttc, ref_ext, fk_remise_except,';
$sql .= ' date_start, date_end, fk_code_ventilation,';
$sql .= ' rang, special_code, fk_product_fournisseur_price, buy_price_ht,';
$sql .= ' info_bits, total_ht, total_tva, total_ttc, total_localtax1, total_localtax2,';
@ -520,6 +522,7 @@ class FactureLigne extends CommonInvoiceLine
$sql .= " ".((int) $this->product_type).",";
$sql .= " ".price2num($this->remise_percent).",";
$sql .= " ".price2num($this->subprice).",";
$sql .= " ".price2num($this->subprice_ttc).",";
$sql .= " '".$this->db->escape($this->ref_ext)."',";
$sql .= ' '.(!empty($this->fk_remise_except) ? ((int) $this->fk_remise_except) : "null").',';
$sql .= " ".(!empty($this->date_start) ? "'".$this->db->idate($this->date_start)."'" : "null").",";
@ -730,6 +733,7 @@ class FactureLigne extends CommonInvoiceLine
$sql .= ", ref_ext='".$this->db->escape($this->ref_ext)."'";
$sql .= ", label=".(!empty($this->label) ? "'".$this->db->escape($this->label)."'" : "null");
$sql .= ", subprice=".price2num($this->subprice);
$sql .= ", subprice_ttc=".price2num($this->subprice_ttc);
$sql .= ", remise_percent=".price2num($this->remise_percent);
if ($this->fk_remise_except) {
$sql .= ", fk_remise_except = ".((int) $this->fk_remise_except);

View file

@ -15,6 +15,7 @@
* Copyright (C) 2024-2025 MDW <mdeweerd@users.noreply.github.com>
* Copyright (C) 2024-2026 Alexandre Spangaro <alexandre@inovea-conseil.com>
* Copyright (C) 2025 William Mead <william@m34d.com>
* Copyright (C) 2026 Lionel Vessiller <lvessiller@open-dsi.fr>
*
* 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
@ -425,6 +426,9 @@ if (empty($reshook)) {
$localtax1_tx = get_localtax($txtva, 1, $object->thirdparty);
$localtax2_tx = get_localtax($txtva, 2, $object->thirdparty);
// Preserve the TTC entry mode of the source line: a line entered including tax must
// stay in TTC so its total is computed from the typed value, without rounding drift.
$line_price_base_type = $lines[$i]->getPriceBaseType();
$result = $object->addline(
$desc,
$lines[$i]->subprice,
@ -436,8 +440,8 @@ if (empty($reshook)) {
$lines[$i]->remise_percent,
$lines[$i]->date_start,
$lines[$i]->date_end,
'HT',
0,
$line_price_base_type,
(float) $lines[$i]->subprice_ttc,
$lines[$i]->info_bits,
$lines[$i]->fk_fournprice,
$lines[$i]->pa_ht,
@ -838,6 +842,11 @@ if (empty($reshook)) {
$objectline->fk_product = GETPOSTINT('idprod');
$objectline->description = GETPOST('product_desc', 'restricthtml');
$objectline->subprice = (float) price2num(GETPOST('elprice'), 'MU');
// The contract line edit form is HT-only: if the user actually changed the HT unit price,
// the line is no longer in TTC entry mode, so drop the stored TTC value.
if (isset($objectline->oldcopy) && (float) $objectline->subprice != (float) $objectline->oldcopy->subprice) {
$objectline->subprice_ttc = 0;
}
$objectline->qty = (float) price2num(GETPOST('elqty'), 'MS');
$objectline->remise_percent = $remise_percent;
$objectline->tva_tx = ($txtva ? $txtva : 0); // Field may be disabled, so we use vat rate 0

View file

@ -15,6 +15,7 @@
* Copyright (C) 2024-2026 MDW <mdeweerd@users.noreply.github.com>
* Copyright (C) 2026 Charlene Benke <charlene@patas-monkey.com>
* Copyright (C) 2026 Alexandre Spangaro <alexandre@inovea-conseil.com
* Copyright (C) 2026 Lionel Vessiller <lvessiller@open-dsi.fr>
*
* 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
@ -859,7 +860,7 @@ class Contrat extends CommonObject
// Selects contract lines related to a product
$sql = "SELECT p.label as product_label, p.description as product_desc, p.ref as product_ref, p.fk_product_type as product_type,";
$sql .= " d.rowid, d.fk_contrat, d.statut as status, d.description, d.subprice, d.vat_src_code, d.tva_tx, d.localtax1_tx, d.localtax2_tx, d.localtax1_type, d.localtax2_type, d.qty, d.remise_percent, d.fk_product_fournisseur_price as fk_fournprice, d.buy_price_ht as pa_ht,";
$sql .= " d.rowid, d.fk_contrat, d.statut as status, d.description, d.subprice, d.subprice_ttc, d.vat_src_code, d.tva_tx, d.localtax1_tx, d.localtax2_tx, d.localtax1_type, d.localtax2_type, d.qty, d.remise_percent, d.fk_product_fournisseur_price as fk_fournprice, d.buy_price_ht as pa_ht,";
$sql .= " d.total_ht,";
$sql .= " d.total_tva,";
$sql .= " d.total_localtax1,";
@ -907,6 +908,7 @@ class Contrat extends CommonObject
$line->localtax1_type = $objp->localtax1_type;
$line->localtax2_type = $objp->localtax2_type;
$line->subprice = $objp->subprice;
$line->subprice_ttc = $objp->subprice_ttc;
$line->statut = $objp->status; // For backward compatibility
$line->status = $objp->status;
$line->remise_percent = $objp->remise_percent;
@ -1564,6 +1566,8 @@ class Contrat extends CommonObject
$total_ttc = $tabprice[2];
$total_localtax1 = $tabprice[9];
$total_localtax2 = $tabprice[10];
$pu_ht = $tabprice[3];
$pu_ttc = $tabprice[5];
if (count($localtaxes_type) > 0) {
$localtax1_type = $localtaxes_type[0];
@ -1580,7 +1584,7 @@ class Contrat extends CommonObject
// if buy price not defined, define buyprice as configured in margin admin
if ($pa_ht == 0) {
$result = $this->defineBuyPrice($pu_ht, $remise_percent, $fk_product);
$result = $this->defineBuyPrice((float) $pu_ht, $remise_percent, $fk_product);
if ($result < 0) {
return -1;
} else {
@ -1591,7 +1595,7 @@ class Contrat extends CommonObject
// Insertion dans la base
$sql = "INSERT INTO ".MAIN_DB_PREFIX."contratdet";
$sql .= " (fk_contrat, label, description, fk_product, qty, tva_tx, vat_src_code,";
$sql .= " localtax1_tx, localtax2_tx, localtax1_type, localtax2_type, remise_percent, subprice,";
$sql .= " localtax1_tx, localtax2_tx, localtax1_type, localtax2_type, remise_percent, subprice, subprice_ttc,";
$sql .= " total_ht, total_tva, total_localtax1, total_localtax2, total_ttc,";
$sql .= " info_bits,";
$sql .= " fk_product_fournisseur_price, buy_price_ht";
@ -1615,6 +1619,7 @@ class Contrat extends CommonObject
$sql .= " '".$this->db->escape($localtax2_type)."',";
$sql .= " ".price2num($remise_percent).",";
$sql .= " ".price2num($pu_ht).",";
$sql .= " ".($price_base_type === 'TTC' ? price2num($pu_ttc) : "0").",";
$sql .= " ".price2num($total_ht).",".price2num($total_tva).",".price2num($total_localtax1).",".price2num($total_localtax2).",".price2num($total_ttc).",";
$sql .= " ".((int) $info_bits).",";
if (isset($fk_fournprice)) {
@ -1712,7 +1717,6 @@ class Contrat extends CommonObject
$qty = trim((string) $qty);
$desc = trim($desc);
$desc = trim($desc);
$subprice = price2num($pu);
$tvatx = price2num($tvatx);
$localtax1tx = price2num($localtax1tx);
$localtax2tx = price2num($localtax2tx);
@ -1747,6 +1751,8 @@ class Contrat extends CommonObject
$total_ttc = $tabprice[2];
$total_localtax1 = $tabprice[9];
$total_localtax2 = $tabprice[10];
$pu_ht = $tabprice[3];
$pu_ttc = $tabprice[5];
$localtax1_type = (empty($localtaxes_type[0]) ? '' : $localtaxes_type[0]);
$localtax2_type = (empty($localtaxes_type[2]) ? '' : $localtaxes_type[2]);
@ -1766,7 +1772,9 @@ class Contrat extends CommonObject
}
$sql = "UPDATE ".MAIN_DB_PREFIX."contratdet SET description = '".$this->db->escape($desc)."'";
$sql .= ",subprice = ".((float) price2num($subprice));
$sql .= ",subprice = ".((float) price2num($pu_ht));
// Persist the original entry mode of the line so a no-op edit can preserve it later.
$sql .= ",subprice_ttc = ".($price_base_type === 'TTC' ? (float) price2num($pu_ttc) : 0);
$sql .= ",remise_percent = ".((float) price2num($remise_percent));
$sql .= ",qty = ".((float) $qty);
$sql .= ",tva_tx = ".((float) price2num($tvatx));
@ -2677,7 +2685,11 @@ class Contrat extends CommonObject
if (!$error) {
foreach ($this->lines as $line) {
$result = $clonedObj->addline($line->description, $line->subprice, $line->qty, $line->tva_tx, $line->localtax1_tx, $line->localtax2_tx, $line->fk_product, $line->remise_percent, $line->date_start, $line->date_cloture, 'HT', 0, $line->info_bits, $line->fk_fournprice, $line->pa_ht, $line->array_options, $line->fk_unit, $line->rang);
// Preserve the original entry mode of the line. Contrat::addline() stores subprice from the
// $pu_ht argument as-is (like the card, which pre-computes it), so we pass the stored HT and
// flag TTC + subprice_ttc so the total is computed from the typed value (no rounding drift).
$line_price_base_type = $line->getPriceBaseType();
$result = $clonedObj->addline($line->description, (float) $line->subprice, $line->qty, $line->tva_tx, $line->localtax1_tx, $line->localtax2_tx, $line->fk_product, $line->remise_percent, $line->date_start, $line->date_cloture, $line_price_base_type, (float) $line->subprice_ttc, $line->info_bits, $line->fk_fournprice, $line->pa_ht, $line->array_options, $line->fk_unit, $line->rang);
if ($result < 0) {
$error++;
$this->setErrorsFromObject($clonedObj);

View file

@ -13,6 +13,7 @@
* Copyright (C) 2015-2018 Ferran Marcet <fmarcet@2byte.es>
* Copyright (C) 2024 William Mead <william.mead@manchenumerique.fr>
* Copyright (C) 2024-2026 MDW <mdeweerd@users.noreply.github.com>
* Copyright (C) 2026 Lionel Vessiller <lvessiller@open-dsi.fr>
*
* 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
@ -493,6 +494,7 @@ class ContratLigne extends CommonObjectLine
$sql .= " t.remise_percent,";
$sql .= " t.fk_remise_except,";
$sql .= " t.subprice,";
$sql .= " t.subprice_ttc,";
$sql .= " t.total_ht,";
$sql .= " t.total_tva,";
$sql .= " t.total_localtax1,";
@ -551,6 +553,7 @@ class ContratLigne extends CommonObjectLine
$this->remise_percent = $obj->remise_percent;
$this->fk_remise_except = $obj->fk_remise_except;
$this->subprice = $obj->subprice;
$this->subprice_ttc = $obj->subprice_ttc;
$this->total_ht = $obj->total_ht;
$this->total_tva = $obj->total_tva;
$this->total_localtax1 = $obj->total_localtax1;
@ -644,7 +647,11 @@ class ContratLigne extends CommonObjectLine
// and this is done at the line level, which has its own VAT rate
$localtaxes_type = getLocalTaxesFromRate($this->tva_tx, 0, $this->thirdparty, $mysoc);
$tabprice = calcul_price_total($this->qty, $this->subprice, $this->remise_percent, (float) $this->tva_tx, $this->localtax1_tx, $this->localtax2_tx, 0, 'HT', 0, 1, $mysoc, $localtaxes_type);
// Compute the total from the value the user actually entered, to avoid a rounding drift.
$line_price_base_type = $this->getPriceBaseType();
$pu_for_calc = $this->wasEnteredIncludingTax() ? (float) $this->subprice_ttc : (float) $this->subprice;
$tabprice = calcul_price_total($this->qty, $pu_for_calc, $this->remise_percent, (float) $this->tva_tx, $this->localtax1_tx, $this->localtax2_tx, 0, $line_price_base_type, 0, 1, $mysoc, $localtaxes_type);
$this->total_ht = (float) $tabprice[0];
$this->total_tva = (float) $tabprice[1];
$this->total_ttc = (float) $tabprice[2];
@ -693,6 +700,7 @@ class ContratLigne extends CommonObjectLine
$sql .= " remise_percent = ".price2num($this->remise_percent).",";
$sql .= " fk_remise_except = ".($this->fk_remise_except > 0 ? ((int) $this->fk_remise_except) : "null").",";
$sql .= " subprice = ".($this->subprice != '' ? ((float) $this->subprice) : "null").",";
$sql .= " subprice_ttc = ".($this->subprice_ttc != '' ? ((float) $this->subprice_ttc) : "0").",";
$sql .= " total_ht = ".((float) $this->total_ht).",";
$sql .= " total_tva = ".((float) $this->total_tva).",";
$sql .= " total_localtax1 = ".((float) $this->total_localtax1).",";
@ -822,7 +830,7 @@ class ContratLigne extends CommonObjectLine
// Insertion dans la base
$sql = "INSERT INTO ".MAIN_DB_PREFIX."contratdet";
$sql .= " (fk_contrat, label, description, fk_product, qty, vat_src_code, tva_tx,";
$sql .= " localtax1_tx, localtax2_tx, localtax1_type, localtax2_type, remise_percent, subprice,";
$sql .= " localtax1_tx, localtax2_tx, localtax1_type, localtax2_type, remise_percent, subprice, subprice_ttc,";
$sql .= " total_ht, total_tva, total_localtax1, total_localtax2, total_ttc,";
$sql .= " info_bits,";
$sql .= " rang,";
@ -842,7 +850,7 @@ class ContratLigne extends CommonObjectLine
$sql .= " '".$this->db->escape($this->localtax2_tx)."',";
$sql .= " '".$this->db->escape($this->localtax1_type)."',";
$sql .= " '".$this->db->escape($this->localtax2_type)."',";
$sql .= " ".price2num($this->remise_percent).",".price2num($this->subprice).",";
$sql .= " ".price2num($this->remise_percent).",".price2num($this->subprice).",".price2num($this->subprice_ttc).",";
$sql .= " ".price2num($this->total_ht).",".price2num($this->total_tva).",".price2num($this->total_localtax1).",".price2num($this->total_localtax2).",".price2num($this->total_ttc).",";
$sql .= " '".$this->db->escape((string) $this->info_bits)."',";
$sql .= " ".(empty($this->rang) ? '0' : (int) $this->rang).",";

View file

@ -3,6 +3,7 @@
* Copyright (C) 2012 Cedric Salvador <csalvador@gpcsolutions.fr>
* Copyright (C) 2024-2026 MDW <mdeweerd@users.noreply.github.com>
* Copyright (C) 2024-2025 Frédéric France <frederic.france@free.fr>
* Copyright (C) 2026 Lionel Vessiller <lvessiller@open-dsi.fr>
*
* 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
@ -417,4 +418,29 @@ abstract class CommonObjectLine extends CommonObject
return $parent_element->getNomUrl($withpicto).' - Line #'.$this->id; // @phan-suppress-current-line PhanPluginUnknownObjectMethodCall
}
/**
* Return true if the unit price was originally entered including tax (TTC mode).
* Useful to preserve the entry mode on no-op edits and to avoid total drift.
* Note: cannot use !empty() because MySQL returns doubles as strings like "0.00000000"
* which empty() treats as non-empty.
*
* @return bool
*/
public function wasEnteredIncludingTax()
{
return isset($this->subprice_ttc) && (float) $this->subprice_ttc != 0;
}
/**
* Return the price base type ('TTC' or 'HT') matching how the unit price was entered.
* Shortcut over wasEnteredIncludingTax() to keep the entry mode when re-adding a line
* (clone, conversion, bulk action) so the total is recomputed from the typed value.
*
* @return string 'TTC' if entered including tax, 'HT' otherwise
*/
public function getPriceBaseType()
{
return $this->wasEnteredIncludingTax() ? 'TTC' : 'HT';
}
}

View file

@ -1702,6 +1702,12 @@ class CommandeFournisseur extends CommonOrder
//$this->special_code = $line->special_code; // TODO : remove this in 9.0 and add special_code param to addline()
// This include test on qty if option SUPPLIER_ORDER_WITH_NOPRICEDEFINED is not set
// Preserve the original entry mode of the line so the total is computed from the typed value (no rounding drift).
// In TTC mode, do not forward the HT currency price as pu_ht_devise: under multicurrency it would
// reset the local price and recompute from the HT currency amount (read as TTC) -> 0.01 drift.
// Like SupplierProposal (which does not pass it), the currency price is re-derived from the local price.
$line_price_base_type = $line->getPriceBaseType();
$line_pu_devise = ($line_price_base_type === 'TTC') ? 0 : (float) $line->multicurrency_subprice;
$result = $this->addline(
(string) $line->desc,
(float) $line->subprice,
@ -1713,7 +1719,7 @@ class CommandeFournisseur extends CommonOrder
0,
(string) ($line->ref_supplier ? $line->ref_supplier : $line->ref_fourn), // $line->ref_fourn comes from field ref into table of lines. Value may be a ref that does not exists anymore, so we first try with value of product
(float) $line->remise_percent,
'HT',
$line_price_base_type,
(float) $line->subprice_ttc,
(int) $line->product_type,
(int) $line->info_bits,
@ -1722,7 +1728,7 @@ class CommandeFournisseur extends CommonOrder
$line->date_end ?? null,
$line->array_options ?? [],
$line->fk_unit ?? null,
(float) $line->multicurrency_subprice, // pu_ht_devise
$line_pu_devise, // pu_ht_devise
(string) $line->origin, // origin
(int) $line->origin_id, // origin_id
(int) ($line->rang ?? -1), // rang
@ -2259,7 +2265,7 @@ class CommandeFournisseur extends CommonOrder
$this->line->product_type = $product_type;
$this->line->remise_percent = $remise_percent;
$this->line->subprice = (float) $pu_ht;
$this->line->subprice_ttc = (float) $pu_ttc;
$this->line->subprice_ttc = ($price_base_type === 'TTC') ? (float) $pu_ttc : 0;
$this->line->rang = $rang;
$this->line->info_bits = $info_bits;
@ -3296,7 +3302,7 @@ class CommandeFournisseur extends CommonOrder
$this->line->multicurrency_total_ttc = (float) $multicurrency_total_ttc;
$this->line->subprice = (float) $pu_ht;
$this->line->subprice_ttc = (float) $pu_ttc;
$this->line->subprice_ttc = ($price_base_type === 'TTC') ? (float) $pu_ttc : 0;
$this->line->price = $this->line->subprice;
$this->line->remise_percent = $remise_percent;

View file

@ -659,16 +659,19 @@ class FactureFournisseur extends CommonInvoice
if ($resql_insert) {
$idligne = $this->db->last_insert_id(MAIN_DB_PREFIX.'facture_fourn_det');
// Preserve the original entry mode of the line so the total is computed from the typed value (no rounding drift).
$line_price_base_type = $this->lines[$i]->getPriceBaseType();
$line_pu = ($line_price_base_type === 'TTC') ? (float) $this->lines[$i]->subprice_ttc : $this->lines[$i]->subprice;
$res = $this->updateline(
$idligne,
$this->lines[$i]->desc ? $this->lines[$i]->desc : $this->lines[$i]->description,
$this->lines[$i]->subprice,
$line_pu,
$this->lines[$i]->tva_tx.($this->lines[$i]->vat_src_code ? ' ('.$this->lines[$i]->vat_src_code.')' : ''),
$this->lines[$i]->localtax1_tx,
$this->lines[$i]->localtax2_tx,
$this->lines[$i]->qty,
$this->lines[$i]->fk_product,
'HT',
$line_price_base_type,
(!empty($this->lines[$i]->info_bits) ? $this->lines[$i]->info_bits : ''),
$this->lines[$i]->product_type,
$this->lines[$i]->remise_percent,
@ -2286,6 +2289,8 @@ class FactureFournisseur extends CommonInvoice
$supplierinvoiceline->qty = ($this->type == self::TYPE_CREDIT_NOTE ? abs((float) $qty) : (float) $qty); // For credit note, quantity is always positive and unit price negative
$supplierinvoiceline->subprice = ($this->type == self::TYPE_CREDIT_NOTE ? -abs((float) $pu_ht) : (float) $pu_ht); // For credit note, unit price always negative, always positive otherwise
// Only keep the TTC unit price when the line was entered including tax, so it acts as a reliable "TTC entry mode" marker.
$supplierinvoiceline->subprice_ttc = ($price_base_type === 'TTC') ? ($this->type == self::TYPE_CREDIT_NOTE ? -abs((float) $tabprice[5]) : (float) $tabprice[5]) : 0;
$supplierinvoiceline->vat_src_code = $vat_src_code;
$supplierinvoiceline->tva_tx = $txtva;
@ -2494,7 +2499,8 @@ class FactureFournisseur extends CommonInvoice
$line->subprice = ($this->type == self::TYPE_CREDIT_NOTE ? -abs((float) $pu_ht) : (float) $pu_ht); // For credit note, unit price always negative, always positive otherwise
$line->pu_ht = $line->subprice; // deprecated
$line->subprice_ttc = ($this->type == self::TYPE_CREDIT_NOTE ? -abs((float) $pu_ttc) : (float) $pu_ttc); // For credit note, unit price always negative, always positive otherwise
// Only keep the TTC unit price when the line was entered including tax, so it acts as a reliable "TTC entry mode" marker.
$line->subprice_ttc = ($price_base_type === 'TTC') ? ($this->type == self::TYPE_CREDIT_NOTE ? -abs((float) $pu_ttc) : (float) $pu_ttc) : 0; // For credit note, unit price always negative, always positive otherwise
$line->pu_ttc = $line->subprice_ttc; // deprecated
$line->remise_percent = $remise_percent;

View file

@ -722,7 +722,8 @@ class SupplierInvoiceLine extends CommonObjectLine
$sql .= " ".price2num($this->remise_percent).",";
$sql .= ' '.(!empty($this->fk_remise_except) ? ((int) $this->fk_remise_except) : "null").',';
$sql .= " ".price2num($this->subprice).",";
$sql .= " ".(!empty($this->qty) ? price2num($this->total_ttc / $this->qty) : price2num($this->total_ttc)).",";
// pu_ttc holds the TTC entry mode: the typed TTC unit price when entered including tax, 0 otherwise (like update()).
$sql .= " ".price2num($this->subprice_ttc).",";
$sql .= " ".(!empty($this->date_start) ? "'".$this->db->idate($this->date_start)."'" : "null").",";
$sql .= " ".(!empty($this->date_end) ? "'".$this->db->idate($this->date_end)."'" : "null").",";
$sql .= ' '.(!empty($this->fk_code_ventilation) ? ((int) $this->fk_code_ventilation) : 0).',';

View file

@ -448,7 +448,10 @@ if (empty($reshook)) {
$alldate_end = dol_mktime(GETPOSTINT('alldate_endhour'), GETPOSTINT('alldate_endmin'), 0, GETPOSTINT('alldate_endmonth'), GETPOSTINT('alldate_endday'), GETPOSTINT('alldate_endyear'));
foreach ($object->lines as $line) {
if ($line->product_type == 1) { // only service line
$result = $object->updateline($line->id, $line->desc, $line->subprice, $line->qty, (float) $line->remise_percent, $line->tva_tx, $line->localtax1_tx, $line->localtax2_tx, 'HT', $line->info_bits, $line->product_type, 0, $alldate_start, $alldate_end, $line->array_options, $line->fk_unit, $line->multicurrency_subprice, $line->ref_supplier);
// Preserve the original entry mode of the line so the total is not drifted by rounding.
$line_price_base_type = $line->getPriceBaseType();
$line_pu = ($line_price_base_type === 'TTC') ? (float) $line->subprice_ttc : (float) $line->subprice;
$result = $object->updateline($line->id, $line->desc, $line_pu, $line->qty, (float) $line->remise_percent, $line->tva_tx, $line->localtax1_tx, $line->localtax2_tx, $line_price_base_type, $line->info_bits, $line->product_type, 0, $alldate_start, $alldate_end, $line->array_options, $line->fk_unit, $line->multicurrency_subprice, $line->ref_supplier);
}
}
} elseif ($action == 'addline' && GETPOST('submitforalllines', 'alpha') && GETPOST('remiseforalllines', 'alpha') !== '' && $usercancreate) {
@ -456,7 +459,10 @@ if (empty($reshook)) {
$remise_percent = (GETPOST('remiseforalllines') ? GETPOST('remiseforalllines') : 0);
$remise_percent = str_replace('*', '', $remise_percent);
foreach ($object->lines as $line) {
$result = $object->updateline($line->id, $line->desc, $line->subprice, $line->qty, (float) $remise_percent, $line->tva_tx, $line->localtax1_tx, $line->localtax2_tx, 'HT', $line->info_bits, $line->product_type, 0, $line->date_start, $line->date_end, $line->array_options, $line->fk_unit, $line->multicurrency_subprice, $line->ref_supplier);
// Preserve the original entry mode of the line so the total is not drifted by rounding.
$line_price_base_type = $line->getPriceBaseType();
$line_pu = ($line_price_base_type === 'TTC') ? (float) $line->subprice_ttc : (float) $line->subprice;
$result = $object->updateline($line->id, $line->desc, $line_pu, $line->qty, (float) $remise_percent, $line->tva_tx, $line->localtax1_tx, $line->localtax2_tx, $line_price_base_type, $line->info_bits, $line->product_type, 0, $line->date_start, $line->date_end, $line->array_options, $line->fk_unit, $line->multicurrency_subprice, $line->ref_supplier);
}
} elseif ($action == 'addline' && GETPOST('submitforalllines', 'aZ09') && GETPOST('vatforalllines', 'alpha') !== '' && $usercancreate) {
// Define new vat_rate for all lines
@ -468,7 +474,10 @@ if (empty($reshook)) {
if ($line->special_code == SUBTOTALS_SPECIAL_CODE) {
continue;
}
$result = $object->updateline($line->id, $line->desc, $line->subprice, $line->qty, (float) $line->remise_percent, $vat_rate, $localtax1_rate, $localtax2_rate, 'HT', $line->info_bits, $line->product_type, 0, $line->date_start, $line->date_end, $line->array_options, $line->fk_unit, $line->multicurrency_subprice, $line->ref_supplier);
// Preserve the original entry mode of the line so the total is not drifted by rounding.
$line_price_base_type = $line->getPriceBaseType();
$line_pu = ($line_price_base_type === 'TTC') ? (float) $line->subprice_ttc : (float) $line->subprice;
$result = $object->updateline($line->id, $line->desc, $line_pu, $line->qty, (float) $line->remise_percent, $vat_rate, $localtax1_rate, $localtax2_rate, $line_price_base_type, $line->info_bits, $line->product_type, 0, $line->date_start, $line->date_end, $line->array_options, $line->fk_unit, $line->multicurrency_subprice, $line->ref_supplier);
}
} elseif ($action == 'confirm_addtitleline' && $usercancreate) {
// Handling adding a new title line for subtotals module
@ -813,15 +822,18 @@ if (empty($reshook)) {
$localtax1_tx = get_localtax($tva_tx, 1, $mysoc, $object->thirdparty);
$localtax2_tx = get_localtax($tva_tx, 2, $mysoc, $object->thirdparty);
// Keep the entry mode chosen by the user so the total is computed from the typed value (no rounding drift).
if (GETPOST('price_ht') != '' || GETPOST('multicurrency_price_ht') != '') {
$price_base_type = 'HT';
$pu_ht = price2num($price_ht, 'MU'); // $pu_ht must be rounded according to settings
$pu_ttc = '';
$pu_ttc = 0;
$pu_ht_devise = price2num($price_ht_devise, 'CU');
} else {
$price_base_type = 'TTC';
$pu_ttc = price2num(GETPOST('price_ttc'), 'MU');
$pu_ht = price2num((float) $pu_ttc / (1 + ((float) $tva_tx / 100)), 'MU'); // $pu_ht must be rounded according to settings
$pu_ht = 0;
$pu_ht_devise = price2num($price_ttc_devise, 'CU');
}
$price_base_type = 'HT';
$pu_ht_devise = price2num($price_ht_devise, 'CU');
$result = $object->addline($desc, (float) $pu_ht, (float) $qty, $tva_tx, $localtax1_tx, $localtax2_tx, 0, 0, $ref_supplier, $remise_percent, $price_base_type, (float) $pu_ttc, $type, 0, 0, $date_start, $date_end, $array_options, $fk_unit, (float) $pu_ht_devise);
}
@ -1014,20 +1026,28 @@ if (empty($reshook)) {
$localtax1_rate = get_localtax($vat_rate, 1, $mysoc, $object->thirdparty);
$localtax2_rate = get_localtax($vat_rate, 2, $mysoc, $object->thirdparty);
if (GETPOST('price_ht') != '') {
$price_base_type = 'HT';
$ht = price2num(GETPOST('price_ht'), '', 2);
} else {
$reg = array();
$vatratecleaned = $vat_rate;
if (preg_match('/^(.*)\s*\((.*)\)$/', $vat_rate, $reg)) { // If vat is "xx (yy)"
$vatratecleaned = trim($reg[1]);
$vatratecode = $reg[2];
}
$pu_ht = price2num(GETPOST('price_ht'), '', 2);
$pu_ttc = price2num(GETPOST('price_ttc'), '', 2);
$ttc = price2num(GETPOST('price_ttc'), '', 2);
$ht = (float) $ttc / (1 + ((float) $vatratecleaned / 100));
$price_base_type = 'HT';
// The form JS clears the other field when the user edits one of them: only the modified field is filled.
// When both fields are submitted, the user did not change the price - we must preserve the original
// storage mode of the line, otherwise a no-op save would shift the total by rounding.
$ht = $pu_ht;
$price_base_type = 'HT';
if (empty($pu_ht) && !empty($pu_ttc)) {
$ht = $pu_ttc;
$price_base_type = 'TTC';
} elseif (!empty($pu_ht) && !empty($pu_ttc)) {
foreach ($object->lines as $line_obj) {
if ($line_obj->id == GETPOSTINT('lineid')) {
// Line was originally entered in TTC mode (subprice_ttc filled by addline)
if ($line_obj->wasEnteredIncludingTax()) {
$ht = $pu_ttc;
$price_base_type = 'TTC';
}
break;
}
}
}
$pu_ht_devise = price2num(GETPOST('multicurrency_subprice'), 'CU', 2);
@ -1045,7 +1065,7 @@ if (empty($reshook)) {
$result = $object->updateline(
$lineid,
GETPOST('product_desc', 'restricthtml'),
$ht,
(float) $ht,
(float) price2num(GETPOST('qty'), 'MS'),
(float) price2num(GETPOST('remise_percent'), '', 2),
$vat_rate,
@ -1686,6 +1706,8 @@ if (empty($reshook)) {
$tva_tx = get_default_tva($soc, $mysoc, $lines[$i]->fk_product, $product_fourn_price_id);
}
// Preserve the original entry mode of the line so the total is computed from the typed value (no rounding drift).
$line_price_base_type = $lines[$i]->getPriceBaseType();
$result = $object->addline(
$desc,
$lines[$i]->subprice,
@ -1697,8 +1719,8 @@ if (empty($reshook)) {
$product_fourn_price_id,
$ref_supplier,
$lines[$i]->remise_percent,
'HT',
0,
$line_price_base_type,
(float) $lines[$i]->subprice_ttc,
$lines[$i]->product_type,
0,
0,

View file

@ -1411,7 +1411,13 @@ if (empty($reshook)) {
$object->special_code = $lines[$i]->special_code;
// FIXME If currency different from main currency, take multicurrency price
if ($object->multicurrency_code != $conf->currency || $object->multicurrency_tx != 1) {
// Preserve the original entry mode of the line so the total is computed from the typed value (no rounding drift).
$line_price_base_type = $lines[$i]->getPriceBaseType();
if ($line_price_base_type === 'TTC') {
// TTC mode: use the local TTC unit price; the currency price is re-derived (no rounding drift).
$pu = (float) $lines[$i]->subprice_ttc;
$pu_currency = 0;
} elseif ($object->multicurrency_code != $conf->currency || $object->multicurrency_tx != 1) {
$pu = 0;
$pu_currency = $lines[$i]->multicurrency_subprice;
} else {
@ -1433,7 +1439,7 @@ if (empty($reshook)) {
(int) $date_end,
0,
$lines[$i]->info_bits,
'HT',
$line_price_base_type,
$product_type,
$lines[$i]->rang,
0,
@ -1513,12 +1519,28 @@ if (empty($reshook)) {
$tva_tx = (GETPOST('tva_tx') ? GETPOST('tva_tx') : 0);
$tva_tx = str_replace('*', '', $tva_tx);
if (GETPOST('price_ht') != '' || GETPOST('multicurrency_subprice') != '') {
$up = price2num(GETPOST('price_ht'), '', 2);
$price_base_type = 'HT';
} else {
$up = price2num(GETPOST('price_ttc'), '', 2);
$pu_ht = price2num(GETPOST('price_ht'), '', 2);
$pu_ttc = price2num(GETPOST('price_ttc'), '', 2);
// The form JS clears the other field when the user edits one of them: only the modified field is filled.
// When both fields are submitted, the user did not change the price - we must preserve the original
// storage mode of the line, otherwise a no-op save would shift the total by rounding.
$up = $pu_ht;
$price_base_type = 'HT';
if (empty($pu_ht) && !empty($pu_ttc)) {
$up = $pu_ttc;
$price_base_type = 'TTC';
} elseif (!empty($pu_ht) && !empty($pu_ttc)) {
foreach ($object->lines as $line_obj) {
if ($line_obj->id == GETPOSTINT('lineid')) {
// Line was originally entered in TTC mode (subprice_ttc filled by addline)
if ($line_obj->wasEnteredIncludingTax()) {
$up = $pu_ttc;
$price_base_type = 'TTC';
}
break;
}
}
}
if (GETPOST('productid') > 0) {
@ -1619,7 +1641,10 @@ if (empty($reshook)) {
$alldate_end = dol_mktime(GETPOSTINT('alldate_endhour'), GETPOSTINT('alldate_endmin'), 0, GETPOSTINT('alldate_endmonth'), GETPOSTINT('alldate_endday'), GETPOSTINT('alldate_endyear'));
foreach ($object->lines as $line) {
if ($line->product_type == 1) { // only service line
$result = $object->updateline($line->id, $line->desc, $line->subprice, $line->tva_tx, $line->localtax1_tx, $line->localtax2_tx, $line->qty, $line->fk_product, 'HT', $line->info_bits, $line->product_type, $line->remise_percent, 0, $alldate_start, $alldate_end, $line->array_options, $line->fk_unit, $line->multicurrency_subprice, $line->ref_supplier, $line->rang);
// Preserve the original entry mode of the line so the total is not drifted by rounding.
$line_price_base_type = $line->getPriceBaseType();
$line_pu = ($line_price_base_type === 'TTC') ? (float) $line->subprice_ttc : (float) $line->subprice;
$result = $object->updateline($line->id, $line->desc, $line_pu, $line->tva_tx, $line->localtax1_tx, $line->localtax2_tx, $line->qty, $line->fk_product, $line_price_base_type, $line->info_bits, $line->product_type, $line->remise_percent, 0, $alldate_start, $alldate_end, $line->array_options, $line->fk_unit, $line->multicurrency_subprice, $line->ref_supplier, $line->rang);
}
}
} elseif ($action == 'addline' && GETPOST('submitforalllines', 'alpha') && GETPOST('remiseforalllines', 'alpha') !== '' && $usercancreate) {
@ -1627,7 +1652,10 @@ if (empty($reshook)) {
$remise_percent = (GETPOST('remiseforalllines') ? GETPOST('remiseforalllines') : 0);
$remise_percent = (float) str_replace('*', '', $remise_percent);
foreach ($object->lines as $line) {
$result = $object->updateline($line->id, $line->desc, $line->subprice, $line->tva_tx, $line->localtax1_tx, $line->localtax2_tx, $line->qty, $line->fk_product, 'HT', $line->info_bits, $line->product_type, $remise_percent, 0, $line->date_start, $line->date_end, $line->array_options, $line->fk_unit, $line->multicurrency_subprice, $line->ref_supplier, $line->rang);
// Preserve the original entry mode of the line so the total is not drifted by rounding.
$line_price_base_type = $line->getPriceBaseType();
$line_pu = ($line_price_base_type === 'TTC') ? (float) $line->subprice_ttc : (float) $line->subprice;
$result = $object->updateline($line->id, $line->desc, $line_pu, $line->tva_tx, $line->localtax1_tx, $line->localtax2_tx, $line->qty, $line->fk_product, $line_price_base_type, $line->info_bits, $line->product_type, $remise_percent, 0, $line->date_start, $line->date_end, $line->array_options, $line->fk_unit, $line->multicurrency_subprice, $line->ref_supplier, $line->rang);
}
} elseif ($action == 'addline' && GETPOST('submitforalllines', 'aZ09') && GETPOST('vatforalllines', 'alpha') != '' && $usercancreate) {
// Define vat_rate
@ -1636,7 +1664,10 @@ if (empty($reshook)) {
$localtax1_rate = get_localtax($vat_rate, 1, $object->thirdparty, $mysoc);
$localtax2_rate = get_localtax($vat_rate, 2, $object->thirdparty, $mysoc);
foreach ($object->lines as $line) {
$result = $object->updateline($line->id, $line->desc, $line->subprice, $vat_rate, $localtax1_rate, $localtax2_rate, $line->qty, $line->fk_product, 'HT', $line->info_bits, $line->product_type, $line->remise_percent, 0, $line->date_start, $line->date_end, $line->array_options, $line->fk_unit, $line->multicurrency_subprice, $line->ref_supplier, $line->rang);
// Preserve the original entry mode of the line so the total is not drifted by rounding.
$line_price_base_type = $line->getPriceBaseType();
$line_pu = ($line_price_base_type === 'TTC') ? (float) $line->subprice_ttc : (float) $line->subprice;
$result = $object->updateline($line->id, $line->desc, $line_pu, $vat_rate, $localtax1_rate, $localtax2_rate, $line->qty, $line->fk_product, $line_price_base_type, $line->info_bits, $line->product_type, $line->remise_percent, 0, $line->date_start, $line->date_end, $line->array_options, $line->fk_unit, $line->multicurrency_subprice, $line->ref_supplier, $line->rang);
}
} elseif ($action == 'addline' && $usercancreate) {
// Add a product line
@ -1923,16 +1954,18 @@ if (empty($reshook)) {
$localtax1_tx = get_localtax($tva_tx, 1, $mysoc, $object->thirdparty);
$localtax2_tx = get_localtax($tva_tx, 2, $mysoc, $object->thirdparty);
// Keep the entry mode chosen by the user so the total is computed from the typed value (no rounding drift).
if (GETPOST('price_ht') != '' || GETPOST('multicurrency_price_ht') != '') {
$pu_ht = price2num($price_ht, 'MU'); // $pu_ht must be rounded according to settings
$price_base_type = 'HT';
$pu = price2num($price_ht, 'MU'); // $pu must be rounded according to settings
$pu_devise = price2num($price_ht_devise, 'CU');
} else {
$pu_ttc = price2num(GETPOST('price_ttc'), 'MU');
$pu_ht = price2num((float) $pu_ttc / (1 + ((float) $tva_tx / 100)), 'MU'); // $pu_ht must be rounded according to settings
$price_base_type = 'TTC';
$pu = price2num(GETPOST('price_ttc'), 'MU');
$pu_devise = price2num($price_ttc_devise, 'CU');
}
$price_base_type = 'HT';
$pu_devise = price2num($price_ht_devise, 'CU');
$result = $object->addline($line_desc, (float) $pu_ht, $tva_tx, $localtax1_tx, $localtax2_tx, (float) $qty, 0, $remise_percent, $date_start, $date_end, 0, $tva_npr, $price_base_type, $type, -1, 0, $array_options, $fk_unit, 0, (float) $pu_devise, $ref_supplier);
$result = $object->addline($line_desc, (float) $pu, $tva_tx, $localtax1_tx, $localtax2_tx, (float) $qty, 0, $remise_percent, $date_start, $date_end, 0, $tva_npr, $price_base_type, $type, -1, 0, $array_options, $fk_unit, 0, (float) $pu_devise, $ref_supplier);
}
//print "xx".$tva_tx; exit;

View file

@ -68,4 +68,14 @@ ALTER TABLE llx_element_element ADD COLUMN tms timestamp DEFAULT CURRENT_TIMESTA
ALTER TABLE llx_c_action_trigger ADD COLUMN enabled varchar(255);
-- Fix #37658 - subprice_ttc (pu_ttc for supplier invoices) now flags a line entered including tax (0 when
-- entered excluding tax). Supplier lines used to store it unconditionally (even for lines entered excluding
-- tax), so reset it on existing supplier lines to avoid them being wrongly treated as entered including tax
-- on clone/edit/bulk actions. A line can be re-entered including tax to set the value again.
-- Guarded on the upgrade source version (MAIN_VERSION_LAST_UPGRADE is still the source version at this point,
-- updated only at the end of step5) so re-running the migration on a 25.x base does NOT wipe values set since.
UPDATE llx_commande_fournisseurdet SET subprice_ttc = 0 WHERE subprice_ttc <> 0 AND EXISTS (SELECT c.rowid FROM llx_const as c WHERE c.name = 'MAIN_VERSION_LAST_UPGRADE' AND c.value < '25.0.0');
UPDATE llx_facture_fourn_det SET pu_ttc = 0 WHERE pu_ttc <> 0 AND EXISTS (SELECT c.rowid FROM llx_const as c WHERE c.name = 'MAIN_VERSION_LAST_UPGRADE' AND c.value < '25.0.0');
UPDATE llx_supplier_proposaldet SET subprice_ttc = 0 WHERE subprice_ttc <> 0 AND EXISTS (SELECT c.rowid FROM llx_const as c WHERE c.name = 'MAIN_VERSION_LAST_UPGRADE' AND c.value < '25.0.0');
-- end of migration

View file

@ -1410,6 +1410,13 @@ foreach ($listofreferent as $key => $value) {
print '</td>';
}
// Additional columns from hooks
$parameters = array('key' => $key, 'value' => $value, 'tablename' => $tablename);
$reshook = $hookmanager->executeHooks('printOverviewDetailTitle', $parameters, $object, $action); // Note that $action and $object may have been modified by hook
if ($reshook < 0) {
setEventMessages($hookmanager->error, $hookmanager->errors, 'errors');
}
print $hookmanager->resPrint;
// Amount HT
//if (empty($value['disableamount']) && ! in_array($tablename, array('projet_task'))) print '<td class="right" width="120">'.$langs->trans("AmountHT").'</td>';
@ -1751,6 +1758,13 @@ foreach ($listofreferent as $key => $value) {
print '</td>';
}
// Additional columns from hooks
$parameters = array('key' => $key, 'value' => $value, 'tablename' => $tablename, 'element' => $element, 'i' => $i, 'qualifiedfortotal' => $qualifiedfortotal);
$reshook = $hookmanager->executeHooks('printOverviewDetailValue', $parameters, $object, $action); // Note that $action and $object may have been modified by hook
if ($reshook < 0) {
setEventMessages($hookmanager->error, $hookmanager->errors, 'errors');
}
print $hookmanager->resPrint;
// Amount without tax
$warning = '';
@ -1977,6 +1991,13 @@ foreach ($listofreferent as $key => $value) {
if ($tablename == 'fichinter') {
print '<td class="left">'.convertSecondToTime($total_duration, 'all', $conf->global->MAIN_DURATION_OF_WORKDAY).'</td>';
}
// Additional total columns from hooks
$parameters = array('key' => $key, 'value' => $value, 'tablename' => $tablename, 'nbelement' => $i);
$reshook = $hookmanager->executeHooks('printOverviewDetailTotal', $parameters, $object, $action); // Note that $action and $object may have been modified by hook
if ($reshook < 0) {
setEventMessages($hookmanager->error, $hookmanager->errors, 'errors');
}
print $hookmanager->resPrint;
print '<td class="right">';
if (empty($value['disableamount'])) {
if ($key == 'loan') {

View file

@ -2700,13 +2700,25 @@ if ($action == 'create' && $permissiontoadd) {
if ($j > 1) {
$htmltooltip .= '<br>';
}
$reception_static->fetch($receptionline_var['reception_id']);
if (empty($conf->cache['reception'][$receptionline_var['reception_id']])) {
$reception_static = new Reception($db);
$reception_static->fetch($receptionline_var['reception_id']);
$conf->cache['reception'][$receptionline_var['reception_id']] = $reception_static;
} else {
$reception_static = $conf->cache['reception'][$receptionline_var['reception_id']];
}
$htmltooltip .= $reception_static->getNomUrl(1, 'nolink', 0, 0, 1);
$htmltooltip .= ' - '.$receptionline_var['qty'];
$htmltext = $langs->trans("DateValidation").' : '.(empty($receptionline_var['date_valid']) ? $langs->trans("Draft") : dol_print_date($receptionline_var['date_valid'], 'dayhour'));
if (isModEnabled('stock') && $receptionline_var['warehouse'] > 0) {
$warehousestatic->fetch($receptionline_var['warehouse']);
if (empty($conf->cache['warehouse'][$receptionline_var['warehouse']])) {
$warehousestatic = new Entrepot($db);
$warehousestatic->fetch($receptionline_var['warehouse']);
$conf->cache['warehouse'][$receptionline_var['warehouse']] = $warehousestatic;
} else {
$warehousestatic = $conf->cache['warehouse'][$receptionline_var['warehouse']];
}
$htmltext .= '<br>'.$langs->trans("From").' : '.$warehousestatic->getNomUrl(1, '', 0, 1);
}
$htmltooltip .= ' '.$form->textwithpicto('', $htmltext, 1);

View file

@ -3,7 +3,7 @@
* Copyright (C) 2024 MDW <mdeweerd@users.noreply.github.com>
* Copyright (C) 2024-2025 Frédéric France <frederic.france@free.fr>
* Copyright (C) 2025 Charlene Benke <charlene@patas-monkey.com>
* Copyright (C) 2026 Lionel Vessiller <lvessiller@open-dsi.fr>
*
* 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
@ -653,10 +653,13 @@ trait CommonSubtotal
}
} else {
if ($current_module == 'facture' && $this instanceof Facture) {
// Preserve the original entry mode of the line so the total is not drifted by rounding.
$line_price_base_type = $this->lines[$i]->getPriceBaseType();
$line_pu = ($line_price_base_type === 'TTC') ? $this->lines[$i]->subprice_ttc : $this->lines[$i]->subprice;
$result = $this->updateline(
$this->lines[$i]->id,
$this->lines[$i]->desc,
$this->lines[$i]->subprice,
$line_pu,
$this->lines[$i]->qty,
$mode == 'discount' ? $value : $this->lines[$i]->remise_percent,
$this->lines[$i]->date_start,
@ -664,7 +667,7 @@ trait CommonSubtotal
$mode == 'tva' ? $value : $this->lines[$i]->tva_tx,
$this->lines[$i]->localtax1_tx,
$this->lines[$i]->localtax2_tx,
'HT',
$line_price_base_type,
$this->lines[$i]->info_bits,
$this->lines[$i]->product_type,
$this->lines[$i]->fk_parent_line,
@ -679,16 +682,19 @@ trait CommonSubtotal
$this->lines[$i]->multicurrency_subprice
);
} elseif ($current_module == 'commande' && $this instanceof Commande) {
// Preserve the original entry mode of the line so the total is not drifted by rounding.
$line_price_base_type = $this->lines[$i]->getPriceBaseType();
$line_pu = ($line_price_base_type === 'TTC') ? $this->lines[$i]->subprice_ttc : $this->lines[$i]->subprice;
$result = $this->updateline(
$this->lines[$i]->id,
$this->lines[$i]->desc,
$this->lines[$i]->subprice,
$line_pu,
$this->lines[$i]->qty,
$mode == 'discount' ? $value : $this->lines[$i]->remise_percent,
$mode == 'tva' ? $value : $this->lines[$i]->tva_tx,
$this->lines[$i]->localtax1_rate,
$this->lines[$i]->localtax2_rate,
'HT',
$line_price_base_type,
$this->lines[$i]->info_bits,
$this->lines[$i]->date_start,
$this->lines[$i]->date_end,
@ -705,7 +711,7 @@ trait CommonSubtotal
);
} elseif ($current_module == 'propal' && $this instanceof Propal) {
// Preserve the original entry mode of the line so the total is not drifted by rounding.
$line_price_base_type = $this->lines[$i]->wasEnteredIncludingTax() ? 'TTC' : 'HT';
$line_price_base_type = $this->lines[$i]->getPriceBaseType();
$line_pu = ($line_price_base_type === 'TTC') ? $this->lines[$i]->subprice_ttc : $this->lines[$i]->subprice;
$result = $this->updateline(
$this->lines[$i]->id,

View file

@ -15,6 +15,7 @@
* Copyright (C) 2022 Gauthier VERDOL <gauthier.verdol@atm-consulting.fr>
* Copyright (C) 2024 Alexandre Spangaro <alexandre@inovea-conseil.com>
* Copyright (C) 2024-2026 MDW <mdeweerd@users.noreply.github.com>
* Copyright (C) 2026 Lionel Vessiller <lvessiller@open-dsi.fr>
*
* 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
@ -470,6 +471,9 @@ if (empty($reshook)) {
$array_options = array();
}
// Preserve the TTC entry mode of the source line: a line entered including tax must
// stay in TTC so its total is computed from the typed value, without rounding drift.
$line_price_base_type = $lines[$i]->getPriceBaseType();
$result = $object->addline(
$desc,
$lines[$i]->subprice,
@ -479,8 +483,8 @@ if (empty($reshook)) {
$lines[$i]->localtax2_tx,
$lines[$i]->fk_product,
$lines[$i]->remise_percent,
'HT',
0,
$line_price_base_type,
(float) $lines[$i]->subprice_ttc,
$lines[$i]->info_bits,
$product_type,
$lines[$i]->rang,
@ -661,7 +665,13 @@ if (empty($reshook)) {
if ($line->special_code == SUBTOTALS_SPECIAL_CODE) {
continue;
}
$result = $object->updateline($line->id, $line->subprice, $line->qty, (float) $line->remise_percent, $vat_rate, $localtax1_rate, $localtax2_rate, $line->desc, 'HT', $line->info_bits, $line->special_code, $line->fk_parent_line, 0, $line->fk_fournprice, $line->pa_ht, $line->label, $line->product_type, $line->array_options, $line->ref_fourn, $line->fk_unit, $line->multicurrency_subprice);
// Preserve the original entry mode of the line so the total is not drifted by rounding.
$line_price_base_type = $line->getPriceBaseType();
$line_pu = ($line_price_base_type === 'TTC') ? (float) $line->subprice_ttc : (float) $line->subprice;
// In TTC mode, do not forward the HT currency price: under multicurrency updateline() would reset
// the local price and recompute from the HT currency amount (read as TTC) -> the TTC value is lost.
$line_pu_devise = ($line_price_base_type === 'TTC') ? 0 : (float) $line->multicurrency_subprice;
$result = $object->updateline($line->id, $line_pu, $line->qty, (float) $line->remise_percent, $vat_rate, $localtax1_rate, $localtax2_rate, $line->desc, $line_price_base_type, $line->info_bits, $line->special_code, $line->fk_parent_line, 0, $line->fk_fournprice, $line->pa_ht, $line->label, $line->product_type, $line->array_options, $line->ref_fourn, $line->fk_unit, $line_pu_devise);
}
} elseif ($action == 'confirm_addtitleline' && $usercancreate) {
// Handling adding a new title line for subtotals module
@ -1014,14 +1024,18 @@ if (empty($reshook)) {
$localtax1_tx = get_localtax($tva_tx, 1, $mysoc, $object->thirdparty);
$localtax2_tx = get_localtax($tva_tx, 2, $mysoc, $object->thirdparty);
// Keep the entry mode chosen by the user so the total is computed from the typed value (no rounding drift).
if (GETPOST('price_ht') != '' || GETPOST('multicurrency_price_ht') != '') {
$price_base_type = 'HT';
$pu_ht = price2num($price_ht, 'MU'); // $pu_ht must be rounded according to settings
$pu_ttc = 0;
$pu_ht_devise = price2num($price_ht_devise, 'CU');
} else {
$price_base_type = 'TTC';
$pu_ttc = price2num(GETPOST('price_ttc'), 'MU');
$pu_ht = price2num((float) $pu_ttc / (1 + ((float) $tva_tx / 100)), 'MU'); // $pu_ht must be rounded according to settings
$pu_ht = 0;
$pu_ht_devise = price2num($price_ttc_devise, 'CU');
}
$price_base_type = 'HT';
$pu_ht_devise = price2num($price_ht_devise, 'CU');
$info_bits = 0;
$result = $object->addline(
@ -1217,6 +1231,8 @@ if (empty($reshook)) {
} elseif ($action == 'updateline' && $usercancreate && GETPOST('save') == $langs->trans("Save")) {
// Update a line within proposal
$vat_rate = (GETPOST('tva_tx') ? GETPOST('tva_tx') : 0);
$pu_ht = price2num(GETPOST('price_ht'), '', 2);
$pu_ttc = price2num(GETPOST('price_ttc'), '', 2);
// Define info_bits
$info_bits = 0;
@ -1232,22 +1248,6 @@ if (empty($reshook)) {
$localtax1_rate = get_localtax($vat_rate, 1, $mysoc, $object->thirdparty);
$localtax2_rate = get_localtax($vat_rate, 2, $mysoc, $object->thirdparty);
if (GETPOST('price_ht') != '') {
$price_base_type = 'HT';
$ht = price2num(GETPOST('price_ht'), '', 2);
} else {
$reg = array();
$vatratecleaned = $vat_rate;
if (preg_match('/^(.*)\s*\((.*)\)$/', $vat_rate, $reg)) { // If vat is "xx (yy)"
$vatratecleaned = trim($reg[1]);
$vatratecode = $reg[2];
}
$ttc = price2num(GETPOST('price_ttc'), '', 2);
$ht = (float) $ttc / (1 + ((float) $vatratecleaned / 100));
$price_base_type = 'HT';
}
$pu_ht_devise = price2num(GETPOST('multicurrency_subprice'), 'CU', 2);
// Add buying price
@ -1270,6 +1270,27 @@ if (empty($reshook)) {
$special_code = 3;
}
// The form JS clears the other field when the user edits one of them: only the modified field is filled.
// When both fields are submitted, the user did not change the price - we must preserve the original
// storage mode of the line, otherwise a no-op save would shift the total by rounding.
$ht = $pu_ht;
$price_base_type = 'HT';
if (empty($pu_ht) && !empty($pu_ttc)) {
$ht = $pu_ttc;
$price_base_type = 'TTC';
} elseif (!empty($pu_ht) && !empty($pu_ttc)) {
foreach ($object->lines as $line_obj) {
if ($line_obj->id == GETPOSTINT('lineid')) {
// Line was originally entered in TTC mode (subprice_ttc filled by addline)
if ($line_obj->wasEnteredIncludingTax()) {
$ht = $pu_ttc;
$price_base_type = 'TTC';
}
break;
}
}
}
// Check minimum price
$productid = GETPOSTINT('productid');
if (!empty($productid)) {
@ -1308,9 +1329,15 @@ if (empty($reshook)) {
$ref_supplier = GETPOST('fourn_ref', 'alpha');
$fk_unit = GETPOSTINT('units');
// In TTC mode, do not forward the HT currency price: under multicurrency updateline() would reset
// the local price and recompute from the HT currency amount (read as TTC) -> the TTC value is lost.
if ($price_base_type === 'TTC') {
$pu_ht_devise = 0;
}
$result = $object->updateline(
GETPOSTINT('lineid'),
$ht,
(float) $ht,
(float) price2num(GETPOST('qty'), 'MS', 2),
(float) price2num(GETPOST('remise_percent'), '', 2),
$vat_rate,

View file

@ -18,6 +18,7 @@
* Copyright (C) 2022 Gauthier VERDOL <gauthier.verdol@atm-consulting.fr>
* 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>
*
* 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
@ -641,6 +642,8 @@ class SupplierProposal extends CommonObject
$this->line->fk_product = $fk_product;
$this->line->remise_percent = $remise_percent;
$this->line->subprice = (float) $pu_ht;
// Persist the original entry mode of the line so updateline() can preserve it later.
$this->line->subprice_ttc = ($price_base_type === 'TTC') ? (float) $pu_ttc : 0;
$this->line->rang = $ranktouse;
$this->line->info_bits = $info_bits;
$this->line->total_ht = (float) $total_ht;
@ -825,11 +828,6 @@ class SupplierProposal extends CommonObject
$multicurrency_total_ttc = $tabprice[18];
$pu_ht_devise = $tabprice[19];
$pu = $pu_ht;
if ($price_base_type == 'TTC') {
$pu = $pu_ttc;
}
// Fetch current line from the database and then clone the object and set it in $oldline property
$line = new SupplierProposalLine($this->db);
$line->fetch($rowid);
@ -863,7 +861,9 @@ class SupplierProposal extends CommonObject
$this->line->localtax1_type = empty($localtaxes_type[0]) ? '' : $localtaxes_type[0];
$this->line->localtax2_type = empty($localtaxes_type[2]) ? '' : $localtaxes_type[2];
$this->line->remise_percent = $remise_percent;
$this->line->subprice = (float) $pu;
$this->line->subprice = (float) $pu_ht;
// Persist the original entry mode of the line so a no-op edit can preserve it later.
$this->line->subprice_ttc = ($price_base_type === 'TTC') ? (float) $pu_ttc : 0;
$this->line->info_bits = $info_bits;
$this->line->total_ht = (float) $total_ht;
$this->line->total_tva = (float) $total_tva;
@ -1097,17 +1097,19 @@ class SupplierProposal extends CommonObject
$fk_parent_line = 0;
}
// Preserve the original entry mode of the line so the total is computed from the typed value (no rounding drift).
$line_price_base_type = $this->lines[$i]->getPriceBaseType();
$result = $this->addline(
$this->lines[$i]->desc,
$this->lines[$i]->subprice,
(float) $this->lines[$i]->subprice,
$this->lines[$i]->qty,
$this->lines[$i]->tva_tx,
$this->lines[$i]->localtax1_tx,
$this->lines[$i]->localtax2_tx,
$this->lines[$i]->fk_product,
$this->lines[$i]->remise_percent,
'HT',
0,
$line_price_base_type,
(float) $this->lines[$i]->subprice_ttc,
0,
$this->lines[$i]->product_type,
$this->lines[$i]->rang,
@ -1383,7 +1385,7 @@ class SupplierProposal extends CommonObject
$this->lines = array();
// Lines of supplier proposals
$sql = "SELECT d.rowid, d.fk_supplier_proposal, d.fk_parent_line, d.label as custom_label, d.description, d.price, d.tva_tx, d.localtax1_tx, d.localtax2_tx, d.qty, d.fk_remise_except, d.remise_percent, d.subprice, d.fk_product,";
$sql = "SELECT d.rowid, d.fk_supplier_proposal, d.fk_parent_line, d.label as custom_label, d.description, d.price, d.tva_tx, d.localtax1_tx, d.localtax2_tx, d.qty, d.fk_remise_except, d.remise_percent, d.subprice, d.subprice_ttc, d.fk_product,";
$sql .= " d.info_bits, d.total_ht, d.total_tva, d.total_localtax1, d.total_localtax2, d.total_ttc, d.fk_product_fournisseur_price as fk_fournprice, d.buy_price_ht as pa_ht, d.special_code, d.rang, d.product_type,";
$sql .= ' p.ref as product_ref, p.description as product_desc, p.fk_product_type, p.label as product_label,';
$sql .= ' d.ref_fourn as ref_produit_fourn, d.extraparams,';
@ -1415,6 +1417,7 @@ class SupplierProposal extends CommonObject
$line->localtax1_tx = $objp->localtax1_tx;
$line->localtax2_tx = $objp->localtax2_tx;
$line->subprice = $objp->subprice;
$line->subprice_ttc = $objp->subprice_ttc;
$line->fk_remise_except = $objp->fk_remise_except;
$line->remise_percent = $objp->remise_percent;

View file

@ -3,6 +3,7 @@
* Copyright (C) 2011-2012 Regis Houssin <regis.houssin@inodbox.com>
* Copyright (C) 2024-2026 MDW <mdeweerd@users.noreply.github.com>
* Copyright (C) 2024-2026 Frédéric France <frederic.france@free.fr>
* Copyright (C) 2026 Lionel Vessiller <lvessiller@open-dsi.fr>
*
* 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
@ -224,6 +225,8 @@ class AllTests
$suite->addTestSuite('FactureRecTest');
require_once dirname(__FILE__).'/FactureTestRounding.php';
$suite->addTestSuite('FactureTestRounding');
require_once dirname(__FILE__).'/TtcRoundingTest.php';
$suite->addTestSuite('TtcRoundingTest');
require_once dirname(__FILE__).'/PaiementTest.php';
$suite->addTestSuite('PaiementTest');
require_once dirname(__FILE__).'/FactureFournisseurTest.php';

File diff suppressed because it is too large Load diff