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 <brad@endurotags.com.au>
Co-authored-by: Laurent Destailleur <eldy@destailleur.fr>
This commit is contained in:
Bradley Jarvis
2025-09-20 09:00:39 +10:00
committed by GitHub
parent 2d76f12dae
commit 7e712cd368
2 changed files with 34 additions and 34 deletions

View File

@@ -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.

View File

@@ -115,6 +115,7 @@ if (getDolGlobalString('MAIN_VIEW_LINE_NUMBER')) {
print '<td class="linecoldescription bomline minwidth300imp tdoverflowmax300">';
print '<div id="line_'.$line->id.'"></div>';
$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 '<td id="costline_'.$line->id.'" class="linecolcost nowrap right">';
$line->qty = (float) $line->qty;
if ($tmpbom->id > 0) $line->qty /= $tmpbom->qty;
$coldisplay++;
if (!empty($line->fk_bom_child)) {
echo '<span class="amount">'.price($tmpbom->total_cost * (float) $line->qty).'</span>';
echo '<span class="amount">'.price(price2num($tmpbom->total_cost * $line->qty, 'MT')).'</span>';
} else {
echo '<span class="amount">'.price($line->total_cost).'</span>';
}
@@ -319,7 +324,7 @@ if ($resql) {
// Qty
$label = $sub_bom_product->getLabelOfUnit('long', $langs);
if ($sub_bom_line->qty_frozen > 0) {
print '<td class="linecolqty nowrap right" id="sub_bom_qty_'.$sub_bom_line->id.'">'.price($sub_bom_line->qty, 0, '', 0, 0).'</td>';
print '<td class="linecolqty nowrap right" id="sub_bom_qty_'.$sub_bom_line->id.'">'.price(price2num($sub_bom_line->qty, 'MS'), 0, '', 0, 0).'</td>';
if (getDolGlobalString('PRODUCT_USE_UNITS')) {
print '<td class="linecoluseunit nowrap left">';
print $label;
@@ -327,7 +332,7 @@ if ($resql) {
}
print '<td class="linecolqtyfrozen nowrap right" id="sub_bom_qty_frozen_'.$sub_bom_line->id.'">'.$langs->trans('Yes').'</td>';
} else {
print '<td class="linecolqty nowrap right" id="sub_bom_qty_'.$sub_bom_line->id.'">'.price($sub_bom_line->qty * (float) $line->qty, 0, '', 0, 0).'</td>';
print '<td class="linecolqty nowrap right" id="sub_bom_qty_'.$sub_bom_line->id.'">'.price(price2num($sub_bom_line->qty * $line->qty, 'MS'), 0, '', 0, 0).'</td>';
if (getDolGlobalString('PRODUCT_USE_UNITS')) {
print '<td class="linecoluseunit nowrap left">';
print $label;
@@ -350,8 +355,7 @@ if ($resql) {
// Cost
if (!empty($sub_bom->id)) {
$sub_bom->calculateCosts();
print '<td class="linecolcost nowrap right" id="sub_bom_cost_'.$sub_bom_line->id.'"><span class="amount">'.price(price2num($sub_bom->total_cost * $sub_bom_line->qty * (float) $line->qty, 'MT')).'</span></td>';
$total_cost += $sub_bom->total_cost * $sub_bom_line->qty * (float) $line->qty;
print '<td class="linecolcost nowrap right" id="sub_bom_cost_'.$sub_bom_line->id.'"><span class="amount">'.price(price2num($sub_bom_line->qty * $line->qty * $sub_bom->unit_cost, 'MS')).'</span></td>';
} 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 '<td class="linecolcost nowrap right" id="sub_bom_cost_'.$sub_bom_line->id.'"><span class="amount">'.price(price2num($sub_bom_line->total_cost, 'MT')).'</span></td>';
$total_cost += $line->total_cost;
} elseif ($sub_bom_product->cost_price > 0) {
print '<td class="linecolcost nowrap right" id="sub_bom_cost_'.$sub_bom_line->id.'">';
print '<span class="amount">'.price(price2num($sub_bom_product->cost_price * $sub_bom_line->qty * (float) $line->qty, 'MT')).'</span></td>';
$total_cost += $sub_bom_product->cost_price * $sub_bom_line->qty * (float) $line->qty;
print '<span class="amount">'.price(price2num($sub_bom_product->cost_price * $sub_bom_line->qty * $line->qty, 'MT')).'</span></td>';
} elseif ($sub_bom_product->pmp > 0) { // PMP if cost price isn't defined
print '<td class="linecolcost nowrap right" id="sub_bom_cost_'.$sub_bom_line->id.'">';
print '<span class="amount">'.price(price2num($sub_bom_product->pmp * $sub_bom_line->qty * (float) $line->qty, 'MT')).'</span></td>';
$total_cost .= $sub_bom_product->pmp * $sub_bom_line->qty * (float) $line->qty;
print '<span class="amount">'.price(price2num($sub_bom_product->pmp * $sub_bom_line->qty * $line->qty, 'MT')).'</span></td>';
} 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 '<td class="linecolcost nowrap right" id="sub_bom_cost_'.$sub_bom_line->id.'"><span class="amount">'.price2num($line_cost, 'MT').'</span></td>';
$total_cost += $line_cost;
}
}