From 7e712cd368f26b6c255b06d973a3f2dfd61849ad Mon Sep 17 00:00:00 2001 From: Bradley Jarvis Date: Sat, 20 Sep 2025 09:00:39 +1000 Subject: [PATCH] Fix sub BOM quantities (#35371) * FIX: reqursive bom quantities Correctly handle quantities on BOM recursion. Current behavior is that sub BOM total quantity is multiplied by parent quantity for that line, the expected behaviour is that the parent BOM would be including sub BOM on a per unit base. * Simplified code * Update objectline_view.tpl.php * Update bom.class.php * Update bom.class.php * Update bom.class.php * Update bom.class.php --------- Co-authored-by: brad Co-authored-by: Laurent Destailleur --- htdocs/bom/class/bom.class.php | 44 +++++++++++++------------- htdocs/bom/tpl/objectline_view.tpl.php | 24 +++++++------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/htdocs/bom/class/bom.class.php b/htdocs/bom/class/bom.class.php index 82dabdbdd95..f89172d4989 100644 --- a/htdocs/bom/class/bom.class.php +++ b/htdocs/bom/class/bom.class.php @@ -1416,6 +1416,21 @@ class BOM extends CommonObject $tmpproduct->pmp = 0; $result = $tmpproduct->fetch($line->fk_product, '', '', '', 0, 1, 1); // We discard selling price and language loading + $unit_cost = (float) (is_null($tmpproduct->cost_price) ? $tmpproduct->pmp : $tmpproduct->cost_price); + if (empty($unit_cost)) { // @phpstan-ignore-line phpstan thinks this is always false. No,if unit_cost is 0, it is not. + if ($productFournisseur->find_min_price_product_fournisseur($line->fk_product) > 0) { + if ($productFournisseur->fourn_remise_percent != "0") { + $line->unit_cost = $productFournisseur->fourn_unitprice_with_discount; + } else { + $line->unit_cost = $productFournisseur->fourn_unitprice; + } + } else { + $line->unit_cost = 0; + } + } else { + $line->unit_cost = (float) price2num($unit_cost); + } + if ($tmpproduct->type == $tmpproduct::TYPE_PRODUCT) { if (empty($line->fk_bom_child)) { if ($result < 0) { @@ -1423,22 +1438,7 @@ class BOM extends CommonObject return -1; } - $unit_cost = (float) (is_null($tmpproduct->cost_price) ? $tmpproduct->pmp : $tmpproduct->cost_price); - if (empty($unit_cost)) { // @phpstan-ignore-line phpstan thinks this is always false. No,if unit_cost is 0, it is not. - if ($productFournisseur->find_min_price_product_fournisseur($line->fk_product) > 0) { - if ($productFournisseur->fourn_remise_percent != "0") { - $line->unit_cost = $productFournisseur->fourn_unitprice_with_discount; - } else { - $line->unit_cost = $productFournisseur->fourn_unitprice; - } - } else { - $line->unit_cost = 0; - } - } else { - $line->unit_cost = (float) price2num($unit_cost); - } - - $line->total_cost = (float) price2num($line->qty * $line->unit_cost, 'MT'); + $line->total_cost = (float) price2num($line->unit_cost * $line->qty / $line->efficiency, 'MT'); $this->total_cost += $line->total_cost; } else { @@ -1447,7 +1447,7 @@ class BOM extends CommonObject if ($res > 0) { $bom_child->calculateCosts(); $line->childBom[] = $bom_child; - $this->total_cost += (float) price2num($bom_child->total_cost * $line->qty, 'MT'); + $line->total_cost = (float) price2num($bom_child->unit_cost * $line->qty / $line->efficiency, 'MT'); $this->total_cost += $line->total_cost; } else { $this->error = $bom_child->error; @@ -1482,9 +1482,9 @@ class BOM extends CommonObject } if ($qtyhourservice) { - $line->total_cost = (float) price2num($qtyhourforline / $qtyhourservice * $tmpproduct->cost_price, 'MT'); + $line->total_cost = (float) price2num($qtyhourforline / $qtyhourservice * $line->unit_cost, 'MT'); } else { - $line->total_cost = (float) price2num($line->qty * $tmpproduct->cost_price, 'MT'); + $line->total_cost = (float) price2num($line->qty * $line->unit_cost, 'MT'); } } @@ -1496,7 +1496,7 @@ class BOM extends CommonObject if ($this->qty > 0) { $this->unit_cost = (float) price2num($this->total_cost / $this->qty, 'MU'); - } elseif ($this->qty < 0) { + } elseif ($this->qty < 0) { $this->unit_cost = (float) price2num($this->total_cost * $this->qty, 'MU'); } } @@ -1534,7 +1534,7 @@ class BOM extends CommonObject foreach ($this->lines as $line) { if (!empty($line->childBom)) { foreach ($line->childBom as $childBom) { - $childBom->getNetNeeds($TNetNeeds, $line->qty * $qty); + $childBom->getNetNeeds($TNetNeeds, $line->qty * $qty / $childBom->qty); } } else { if (empty($TNetNeeds[$line->fk_product]['qty'])) { @@ -1570,7 +1570,7 @@ class BOM extends CommonObject //$TNetNeeds[$childBom->id]['fk_unit'] = $line->fk_unit; $TNetNeeds[$childBom->id]['qty'] = $line->qty * $qty; $TNetNeeds[$childBom->id]['level'] = $level; - $childBom->getNetNeedsTree($TNetNeeds, $line->qty * $qty, $level + 1); + $childBom->getNetNeedsTree($TNetNeeds, $line->qty * $qty / $childBom->qty, $level + 1); } } else { // When using nested level (or not), the qty for needs must always use the same unit to be able to be cumulated. diff --git a/htdocs/bom/tpl/objectline_view.tpl.php b/htdocs/bom/tpl/objectline_view.tpl.php index 0eb6729c3f6..227630dd7e9 100644 --- a/htdocs/bom/tpl/objectline_view.tpl.php +++ b/htdocs/bom/tpl/objectline_view.tpl.php @@ -115,6 +115,7 @@ if (getDolGlobalString('MAIN_VIEW_LINE_NUMBER')) { print ''; print '
'; $coldisplay++; + $tmpproduct = new Product($object->db); $tmpproduct->fetch($line->fk_product); $tmpbom = new BOM($object->db); @@ -220,9 +221,13 @@ $total_cost = 0; $tmpbom->calculateCosts(); print ''; + +$line->qty = (float) $line->qty; +if ($tmpbom->id > 0) $line->qty /= $tmpbom->qty; + $coldisplay++; if (!empty($line->fk_bom_child)) { - echo ''.price($tmpbom->total_cost * (float) $line->qty).''; + echo ''.price(price2num($tmpbom->total_cost * $line->qty, 'MT')).''; } else { echo ''.price($line->total_cost).''; } @@ -319,7 +324,7 @@ if ($resql) { // Qty $label = $sub_bom_product->getLabelOfUnit('long', $langs); if ($sub_bom_line->qty_frozen > 0) { - print ''.price($sub_bom_line->qty, 0, '', 0, 0).''; + print ''.price(price2num($sub_bom_line->qty, 'MS'), 0, '', 0, 0).''; if (getDolGlobalString('PRODUCT_USE_UNITS')) { print ''; print $label; @@ -327,7 +332,7 @@ if ($resql) { } print ''.$langs->trans('Yes').''; } else { - print ''.price($sub_bom_line->qty * (float) $line->qty, 0, '', 0, 0).''; + print ''.price(price2num($sub_bom_line->qty * $line->qty, 'MS'), 0, '', 0, 0).''; if (getDolGlobalString('PRODUCT_USE_UNITS')) { print ''; print $label; @@ -350,8 +355,7 @@ if ($resql) { // Cost if (!empty($sub_bom->id)) { $sub_bom->calculateCosts(); - print ''.price(price2num($sub_bom->total_cost * $sub_bom_line->qty * (float) $line->qty, 'MT')).''; - $total_cost += $sub_bom->total_cost * $sub_bom_line->qty * (float) $line->qty; + print ''.price(price2num($sub_bom_line->qty * $line->qty * $sub_bom->unit_cost, 'MS')).''; } elseif ($sub_bom_product->type == Product::TYPE_SERVICE && isModEnabled('workstation') && !empty($sub_bom_product->fk_default_workstation)) { //Convert qty to hour $unit = measuringUnitString($sub_bom_line->fk_unit, '', null, 1); @@ -363,15 +367,12 @@ if ($resql) { } print ''.price(price2num($sub_bom_line->total_cost, 'MT')).''; - $total_cost += $line->total_cost; } elseif ($sub_bom_product->cost_price > 0) { print ''; - print ''.price(price2num($sub_bom_product->cost_price * $sub_bom_line->qty * (float) $line->qty, 'MT')).''; - $total_cost += $sub_bom_product->cost_price * $sub_bom_line->qty * (float) $line->qty; + print ''.price(price2num($sub_bom_product->cost_price * $sub_bom_line->qty * $line->qty, 'MT')).''; } elseif ($sub_bom_product->pmp > 0) { // PMP if cost price isn't defined print ''; - print ''.price(price2num($sub_bom_product->pmp * $sub_bom_line->qty * (float) $line->qty, 'MT')).''; - $total_cost .= $sub_bom_product->pmp * $sub_bom_line->qty * (float) $line->qty; + print ''.price(price2num($sub_bom_product->pmp * $sub_bom_line->qty * $line->qty, 'MT')).''; } else { // Minimum purchase price if cost price and PMP aren't defined $sql_supplier_price = "SELECT MIN(price) AS min_price, quantity AS qty FROM ".MAIN_DB_PREFIX."product_fournisseur_price"; $sql_supplier_price .= " WHERE fk_product = ". (int) $sub_bom_product->id; @@ -380,12 +381,11 @@ if ($resql) { if ($resql_supplier_price) { $obj = $object->db->fetch_object($resql_supplier_price); // Take first value so the ref with the smaller minimum quantity if (!empty($obj->qty) && !empty($sub_bom_line->qty) && !empty($line->qty)) { - $line_cost = $obj->min_price / $obj->qty * $sub_bom_line->qty * (float) $line->qty; + $line_cost = $obj->min_price / $obj->qty * $sub_bom_line->qty * $line->qty; } else { $line_cost = $obj->min_price; } print ''.price2num($line_cost, 'MT').''; - $total_cost += $line_cost; } }