[FIX] mrp: fix negative values in subcontracting BoM Overview

A subcontracting BoM sometimes shows negative values in Overview, if
components are available in subcontractor's location.

closes odoo/odoo#144702

Task: 3607854
Signed-off-by: William Henrotin (whe) <whe@odoo.com>
This commit is contained in:
Paweł Fertyk
2024-03-13 09:14:08 +00:00
parent e71b1a27c0
commit 3e169c0c69
6 changed files with 149 additions and 12 deletions
@@ -37,7 +37,7 @@ class ReportBomStructure(models.AbstractModel):
@api.model
def _compute_production_capacities(self, bom_qty, bom_data):
date_today = self.env.context.get('from_date', fields.date.today())
lead_time = bom_data['lead_time'] - bom_data['bom'].days_to_prepare_mo
lead_time = bom_data['manufacture_delay']
same_delay = lead_time == bom_data['availability_delay']
res = {}
if bom_data.get('producible_qty', 0):
@@ -317,6 +317,7 @@ class ReportBomStructure(models.AbstractModel):
availabilities = self._get_availabilities(product, current_quantity, product_info, bom_key, quantities_info, level, ignore_stock, components, report_line=bom_report_line)
# in case of subcontracting, lead_time will be calculated with components availability delay
bom_report_line['lead_time'] = route_info.get('lead_time', False)
bom_report_line['manufacture_delay'] = route_info.get('manufacture_delay', False)
bom_report_line.update(availabilities)
if level == 0:
@@ -371,6 +372,7 @@ class ReportBomStructure(models.AbstractModel):
'route_name': route_info.get('route_name', ''),
'route_detail': route_info.get('route_detail', ''),
'lead_time': route_info.get('lead_time', False),
'manufacture_delay': route_info.get('manufacture_delay', False),
'stock_avail_state': availabilities['stock_avail_state'],
'resupply_avail_delay': availabilities['resupply_avail_delay'],
'availability_display': availabilities['availability_display'],
@@ -511,6 +513,7 @@ class ReportBomStructure(models.AbstractModel):
'route_name': bom_line['route_name'],
'route_detail': bom_line['route_detail'],
'lead_time': bom_line['lead_time'],
'manufacture_delay': bom_line['manufacture_delay'],
'level': bom_line['level'],
'code': bom_line['code'],
'availability_state': bom_line['availability_state'],
@@ -11,7 +11,7 @@
<h6 t-if="data.bom_code">Reference: <t t-esc="data.bom_code"/></h6>
</div>
<div class="text-center">
<h3><t t-if="data.quantity_available" t-esc="formatFloat(data.quantity_available, {'digits': [false, precision]})"/> <t t-if="showUom" t-esc="props.uomName"/></h3>
<h3><t t-esc="formatFloat(data.quantity_available, {'digits': [false, precision]})"/> <t t-if="showUom" t-esc="props.uomName"/></h3>
<span>Free to Use</span>
</div>
<div t-if="data.hasOwnProperty('earliest_capacity')" class="ps-5 text-center">
@@ -119,7 +119,8 @@ class ReportBomStructure(models.AbstractModel):
vendor_lead_time = route_info['supplier'].delay
manufacture_lead_time = route_info['bom'].produce_delay
subcontract_delay = resupply_delay if resupply_delay else 0
subcontract_delay += max(vendor_lead_time, manufacture_lead_time + max_component_delay)
subcontract_delay += max(vendor_lead_time, manufacture_lead_time) + max_component_delay
route_info['manufacture_delay'] = route_info['lead_time'] + max(vendor_lead_time, manufacture_lead_time)
route_info['lead_time'] += max(vendor_lead_time, manufacture_lead_time + route_info['bom'].days_to_prepare_mo)
return ('estimated', subcontract_delay)
return (resupply_state, resupply_delay)
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from freezegun import freeze_time
from odoo import Command
from odoo.exceptions import AccessError, UserError
from odoo.tests import Form
@@ -944,6 +946,60 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
{'qty_producing': 10.0, 'product_qty': 10.0, 'state': 'to_close'},
])
@freeze_time('2024-01-01')
def test_bom_overview_availability(self):
# Create routes for components and the main product
self.env['product.supplierinfo'].create({
'product_tmpl_id': self.finished.product_tmpl_id.id,
'partner_id': self.subcontractor_partner1.id,
'price': 1.0,
'delay': 10
})
self.env['product.supplierinfo'].create({
'product_tmpl_id': self.comp1.product_tmpl_id.id,
'partner_id': self.subcontractor_partner1.id,
'price': 648.0,
'delay': 5
})
self.env['product.supplierinfo'].create({
'product_tmpl_id': self.comp2.product_tmpl_id.id,
'partner_id': self.subcontractor_partner1.id,
'price': 648.0,
'delay': 5
})
self.bom.produce_delay = 1
self.bom.days_to_prepare_mo = 3
# Add 4 units of each component to subcontractor's location
subcontractor_location = self.env.company.subcontracting_location_id
self.env['stock.quant']._update_available_quantity(self.comp1, subcontractor_location, 4)
self.env['stock.quant']._update_available_quantity(self.comp2, subcontractor_location, 4)
# Generate a report for 3 products: all products should be ready for production
bom_data = self.env['report.mrp.report_bom_structure']._get_report_data(self.bom.id, 3)
self.assertTrue(bom_data['lines']['components_available'])
for component in bom_data['lines']['components']:
self.assertEqual(component['quantity_on_hand'], 4)
self.assertEqual(component['availability_state'], 'available')
self.assertEqual(bom_data['lines']['earliest_capacity'], 3)
self.assertEqual(bom_data['lines']['earliest_date'], '01/11/2024')
self.assertTrue('leftover_capacity' not in bom_data['lines']['earliest_date'])
self.assertTrue('leftover_date' not in bom_data['lines']['earliest_date'])
# Generate a report for 5 products: only 4 products should be ready for production
bom_data = self.env['report.mrp.report_bom_structure']._get_report_data(self.bom.id, 5)
self.assertFalse(bom_data['lines']['components_available'])
for component in bom_data['lines']['components']:
self.assertEqual(component['quantity_on_hand'], 4)
self.assertEqual(component['availability_state'], 'estimated')
self.assertEqual(bom_data['lines']['earliest_capacity'], 4)
self.assertEqual(bom_data['lines']['earliest_date'], '01/11/2024')
self.assertEqual(bom_data['lines']['leftover_capacity'], 1)
self.assertEqual(bom_data['lines']['leftover_date'], '01/16/2024')
@tagged('post_install', '-at_install')
class TestSubcontractingTracking(TransactionCase):
@@ -14,10 +14,11 @@ class ReportBomStructure(models.AbstractModel):
@api.model
def _get_resupply_availability(self, route_info, components):
resupply_state, resupply_delay = super()._get_resupply_availability(route_info, components)
if route_info.get('route_type') == 'subcontract':
if route_info.get('route_type') == 'subcontract' and resupply_delay:
# always add `Purchase security lead days` and `Days to Purchase`
extra_delay = route_info['bom'].company_id.po_lead + route_info['bom'].company_id.days_to_purchase
route_info['lead_time'] += extra_delay
subcontract_delay = (resupply_delay if resupply_delay else 0) + extra_delay
route_info['manufacture_delay'] += extra_delay
subcontract_delay = resupply_delay + extra_delay
return ('estimated', subcontract_delay)
return (resupply_state, resupply_delay)
@@ -3,6 +3,8 @@
import logging
from freezegun import freeze_time
from odoo import Command
from odoo.exceptions import UserError
from odoo.fields import Date
@@ -535,23 +537,41 @@ class MrpSubcontractingPurchaseTest(TestMrpSubcontractingCommon):
bom_data = self.env['report.mrp.report_bom_structure']._get_bom_data(self.bom, self.warehouse, self.finished)
self.assertEqual(bom_data['lead_time'], 15 + 5 + 5 + 0,
"Lead time = Purchase lead time(finished) + Days to Purchase + Purchase security lead time + DTPMO on BOM")
self.assertEqual(bom_data['resupply_avail_delay'], 10 + 5 + 5 + 20,
"Component avail delay = Purchase lead time(comp1) + Days to Purchase + Purchase security lead time + Calculated DTPMO")
# Resupply delay = 0 (received from MRP, where route type != "manufacture")
# Vendor lead time = 15 (finished product supplier delay)
# Manufacture lead time = 10 (BoM.produce_delay)
# Max purchase component delay = max delay(comp1, comp2) + po_lead + days_to_purchase = 20
self.assertEqual(bom_data['resupply_avail_delay'], 0 + 15 + 20 + 5 + 5,
'Resupply avail delay = Resupply delay + Max(Vendor lead time, Manufacture lead time)'
' + Max purchase component delay + Purchase security lead time + Days to Purchase'
)
# Case 2: Vendor lead time < Manufacturing lead time + DTPMO on BOM
self.bom.action_compute_bom_days()
self.assertEqual(self.bom.days_to_prepare_mo, 10 + 5 + 5,
"DTPMO = Purchase lead time(comp1) + Days to Purchase + Purchase security lead time")
self.bom.days_to_prepare_mo = 10
# Temp increase BoM.produce_delay, to check if it is now used in the final calculation
self.bom.produce_delay = 30
bom_data = self.env['report.mrp.report_bom_structure']._get_bom_data(self.bom, self.warehouse, self.finished)
self.assertEqual(bom_data['lead_time'], 10 + 5 + 5 + 10,
self.assertEqual(bom_data['lead_time'], 30 + 5 + 5 + 10,
"Lead time = Manufacturing lead time + Days to Purchase + Purchase security lead time + DTPMO on BOM")
self.assertEqual(bom_data['resupply_avail_delay'], 10 + 5 + 5 + 20,
"Component avail delay = Manufacturing lead time + Days to Purchase + Purchase security lead time + Calculated DTPMO")
# Resupply delay = 0 (received from MRP, where route type != "manufacture")
# Vendor lead time = 15 (finished product supplier delay)
# Manufacture lead time = 30 (BoM.produce_delay)
# Max purchase component delay = max delay(comp1, comp2) + po_lead + days_to_purchase = 20
self.assertEqual(bom_data['resupply_avail_delay'], 0 + 30 + 20 + 5 + 5,
'Resupply avail delay = Resupply delay + Max(Vendor lead time, Manufacture lead time)'
' + Max purchase component delay + Purchase security lead time + Days to Purchase'
)
# Continue the test with the original produce_delay
self.bom.produce_delay = 10
# Update stock for components, calculate DTPMO should be 0
self.env['stock.quant']._update_available_quantity(self.comp1, self.warehouse.lot_stock_id, 100)
self.env['stock.quant']._update_available_quantity(self.comp2, self.warehouse.lot_stock_id, 100)
self.env['stock.quant']._update_available_quantity(self.comp1, self.env.company.subcontracting_location_id, 100)
self.env['stock.quant']._update_available_quantity(self.comp2, self.env.company.subcontracting_location_id, 100)
self.env.invalidate_all() # invalidate cache to get updated qty_available
# Case 1: Vendor lead time >= Manufacturing lead time + DTPMO on BOM
self.bom.days_to_prepare_mo = 2
@@ -734,3 +754,59 @@ class MrpSubcontractingPurchaseTest(TestMrpSubcontractingCommon):
report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom.id, searchQty=search_qty_more_than_total, searchVariant=False)
self.assertEqual(report_values['lines']['components'][0]['stock_avail_state'], 'unavailable')
@freeze_time('2024-01-01')
def test_bom_overview_availability_po_lead(self):
# Create routes for components and the main product
self.env['product.supplierinfo'].create({
'product_tmpl_id': self.finished.product_tmpl_id.id,
'partner_id': self.subcontractor_partner1.id,
'delay': 10
})
self.env['product.supplierinfo'].create({
'product_tmpl_id': self.comp1.product_tmpl_id.id,
'partner_id': self.subcontractor_partner1.id,
'delay': 5
})
self.env['product.supplierinfo'].create({
'product_tmpl_id': self.comp2.product_tmpl_id.id,
'partner_id': self.subcontractor_partner1.id,
'delay': 5
})
self.bom.produce_delay = 1
self.bom.days_to_prepare_mo = 3
# Security Lead Time for Purchase should always be added
self.env.company.po_lead = 2
# Add 4 units of each component to subcontractor's location
subcontractor_location = self.env.company.subcontracting_location_id
self.env['stock.quant']._update_available_quantity(self.comp1, subcontractor_location, 4)
self.env['stock.quant']._update_available_quantity(self.comp2, subcontractor_location, 4)
# Generate a report for 3 products: all products should be ready for production
bom_data = self.env['report.mrp.report_bom_structure']._get_report_data(self.bom.id, 3)
self.assertTrue(bom_data['lines']['components_available'])
for component in bom_data['lines']['components']:
self.assertEqual(component['quantity_on_hand'], 4)
self.assertEqual(component['availability_state'], 'available')
self.assertEqual(bom_data['lines']['earliest_capacity'], 3)
# 01/11 + 2 days of Security Lead Time = 01/13
self.assertEqual(bom_data['lines']['earliest_date'], '01/13/2024')
self.assertTrue('leftover_capacity' not in bom_data['lines']['earliest_date'])
self.assertTrue('leftover_date' not in bom_data['lines']['earliest_date'])
# Generate a report for 5 products: only 4 products should be ready for production
bom_data = self.env['report.mrp.report_bom_structure']._get_report_data(self.bom.id, 5)
self.assertFalse(bom_data['lines']['components_available'])
for component in bom_data['lines']['components']:
self.assertEqual(component['quantity_on_hand'], 4)
self.assertEqual(component['availability_state'], 'estimated')
self.assertEqual(bom_data['lines']['earliest_capacity'], 4)
# 01/11 + 2 days of Security Lead Time = 01/13
self.assertEqual(bom_data['lines']['earliest_date'], '01/13/2024')
self.assertEqual(bom_data['lines']['leftover_capacity'], 1)
# 01/16 + 2 x 2 days (for components and for final product) = 01/20
self.assertEqual(bom_data['lines']['leftover_date'], '01/20/2024')