diff --git a/addons/mrp/report/mrp_report_bom_structure.py b/addons/mrp/report/mrp_report_bom_structure.py index 0e447b1e480..d7f4cd29256 100644 --- a/addons/mrp/report/mrp_report_bom_structure.py +++ b/addons/mrp/report/mrp_report_bom_structure.py @@ -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'], diff --git a/addons/mrp/static/src/components/bom_overview_table/mrp_bom_overview_table.xml b/addons/mrp/static/src/components/bom_overview_table/mrp_bom_overview_table.xml index 7a299a595a3..2385630a3f2 100644 --- a/addons/mrp/static/src/components/bom_overview_table/mrp_bom_overview_table.xml +++ b/addons/mrp/static/src/components/bom_overview_table/mrp_bom_overview_table.xml @@ -11,7 +11,7 @@
Reference:
-

+

Free to Use
diff --git a/addons/mrp_subcontracting/report/mrp_report_bom_structure.py b/addons/mrp_subcontracting/report/mrp_report_bom_structure.py index 9aa4a550593..fcddd506fe2 100644 --- a/addons/mrp_subcontracting/report/mrp_report_bom_structure.py +++ b/addons/mrp_subcontracting/report/mrp_report_bom_structure.py @@ -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) diff --git a/addons/mrp_subcontracting/tests/test_subcontracting.py b/addons/mrp_subcontracting/tests/test_subcontracting.py index 878288b54d6..1048d902197 100644 --- a/addons/mrp_subcontracting/tests/test_subcontracting.py +++ b/addons/mrp_subcontracting/tests/test_subcontracting.py @@ -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): diff --git a/addons/mrp_subcontracting_purchase/report/mrp_report_bom_structure.py b/addons/mrp_subcontracting_purchase/report/mrp_report_bom_structure.py index b37d60826be..d2b79cc9235 100644 --- a/addons/mrp_subcontracting_purchase/report/mrp_report_bom_structure.py +++ b/addons/mrp_subcontracting_purchase/report/mrp_report_bom_structure.py @@ -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) diff --git a/addons/mrp_subcontracting_purchase/tests/test_mrp_subcontracting_purchase.py b/addons/mrp_subcontracting_purchase/tests/test_mrp_subcontracting_purchase.py index b0480a2bc6c..df3950a0d7c 100644 --- a/addons/mrp_subcontracting_purchase/tests/test_mrp_subcontracting_purchase.py +++ b/addons/mrp_subcontracting_purchase/tests/test_mrp_subcontracting_purchase.py @@ -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')