diff --git a/addons/mrp/data/mrp_demo.xml b/addons/mrp/data/mrp_demo.xml index 6b0e3ff6927..374a68d90c0 100644 --- a/addons/mrp/data/mrp_demo.xml +++ b/addons/mrp/data/mrp_demo.xml @@ -37,6 +37,7 @@ 1 + 10 diff --git a/addons/mrp/models/mrp_bom.py b/addons/mrp/models/mrp_bom.py index 2ff22019fdf..c0ea486f000 100644 --- a/addons/mrp/models/mrp_bom.py +++ b/addons/mrp/models/mrp_bom.py @@ -82,6 +82,13 @@ class MrpBom(models.Model): allow_operation_dependencies = fields.Boolean('Operation Dependencies', help="Create operation level dependencies that will influence both planning and the status of work orders upon MO confirmation. If this feature is ticked, and nothing is specified, Odoo will assume that all operations can be started simultaneously." ) + produce_delay = fields.Float( + 'Manufacturing Lead Time', default=0.0, + help="Average lead time in days to manufacture this product. In the case of multi-level BOM, the manufacturing lead times of the components will be added. In case the product is subcontracted, this can be used to determine the date at which components should be sent to the subcontractor.") + days_to_prepare_mo = fields.Float( + string="Days to prepare Manufacturing Order", default=0.0, + help="Create and confirm Manufacturing Orders these many days in advance, to have enough time to replenish components or manufacture semi-finished products.\n" + "Note that this does not affect the MO scheduled date, which still respects the just-in-time mechanism.") _sql_constraints = [ ('qty_positive', 'check (product_qty > 0)', 'The quantity to produce must be positive!'), @@ -215,6 +222,14 @@ class MrpBom(models.Model): def name_get(self): return [(bom.id, '%s%s' % (bom.code and '%s: ' % bom.code or '', bom.product_tmpl_id.display_name)) for bom in self] + def action_compute_bom_days(self): + company_id = self.env.context.get('default_company_id', self.env.company.id) + warehouse = self.env['stock.warehouse'].search([('company_id', '=', company_id)], limit=1) + for bom in self: + bom_data = self.env['report.mrp.report_bom_structure'].with_context(minimized=True)._get_bom_data(bom, warehouse, bom.product_id, ignore_stock=True) + availability_delay = bom_data.get('resupply_avail_delay') + bom.days_to_prepare_mo = availability_delay - bom_data.get('lead_time', 0) if availability_delay else 0 + @api.constrains('product_tmpl_id', 'product_id', 'type') def check_kit_has_not_orderpoint(self): product_ids = [pid for bom in self.filtered(lambda bom: bom.type == "phantom") diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index 054f6557a96..594fa559ee2 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -683,7 +683,7 @@ class MrpProduction(models.Model): for production in self: if not production.date_planned_start or production.is_planned: continue - days_delay = production.product_id.produce_delay + days_delay = production.bom_id.produce_delay date_planned_finished = production.date_planned_start + relativedelta(days=days_delay) if date_planned_finished == production.date_planned_start: date_planned_finished = date_planned_finished + relativedelta(hours=1) @@ -950,7 +950,7 @@ class MrpProduction(models.Model): } def _get_date_planned_finished(self): - date_planned_finished = self.date_planned_start + relativedelta(days=self.product_id.produce_delay) + date_planned_finished = self.date_planned_start + relativedelta(days=self.bom_id.produce_delay) if date_planned_finished == self.date_planned_start: date_planned_finished = date_planned_finished + relativedelta(hours=1) return date_planned_finished diff --git a/addons/mrp/models/product.py b/addons/mrp/models/product.py index fb53828474d..de60614180c 100644 --- a/addons/mrp/models/product.py +++ b/addons/mrp/models/product.py @@ -29,14 +29,7 @@ class ProductTemplate(models.Model): compute='_compute_used_in_bom_count', compute_sudo=False) mrp_product_qty = fields.Float('Manufactured', digits='Product Unit of Measure', compute='_compute_mrp_product_qty', compute_sudo=False) - produce_delay = fields.Float( - 'Manufacturing Lead Time', default=0.0, - help="Average lead time in days to manufacture this product. In the case of multi-level BOM, the manufacturing lead times of the components will be added. In case the product is subcontracted, this can be used to determine the date at which components should be sent to the subcontractor.") is_kits = fields.Boolean(compute='_compute_is_kits', compute_sudo=False) - days_to_prepare_mo = fields.Float( - string="Days to prepare Manufacturing Order", default=0.0, - help="Create and confirm Manufacturing Orders these many days in advance, to have enough time to replenish components or manufacture semi-finished products.\n" - "Note that this does not affect the MO scheduled date, which still respects the just-in-time mechanism.") def _compute_bom_count(self): for product in self: @@ -87,11 +80,6 @@ class ProductTemplate(models.Model): } return action - def action_compute_bom_days(self): - templates = self.filtered(lambda t: t.bom_count > 0) - if templates: - return templates.mapped('product_variant_id').action_compute_bom_days() - def action_archive(self): filtered_products = self.env['mrp.bom.line'].search([('product_id', 'in', self.product_variant_ids.ids)]).product_id.mapped('display_name') res = super().action_archive() @@ -302,15 +290,6 @@ class ProductProduct(models.Model): res['context'].pop('default_product_tmpl_id', None) return res - def action_compute_bom_days(self): - bom_by_products = self.env['mrp.bom']._bom_find(self) - company_id = self.env.context.get('default_company_id', self.env.company.id) - warehouse = self.env['stock.warehouse'].search([('company_id', '=', company_id)], limit=1) - for product in self: - bom_data = self.env['report.mrp.report_bom_structure'].with_context(minimized=True)._get_bom_data(bom_by_products[product], warehouse, product, ignore_stock=True) - availability_delay = bom_data.get('resupply_avail_delay') - product.days_to_prepare_mo = availability_delay - bom_data.get('lead_time', 0) if availability_delay else 0 - def _match_all_variant_values(self, product_template_attribute_value_ids): """ It currently checks that all variant values (`product_template_attribute_value_ids`) are in the product (`self`). diff --git a/addons/mrp/models/stock_orderpoint.py b/addons/mrp/models/stock_orderpoint.py index 4ac29fa48b1..6ce564a1e33 100644 --- a/addons/mrp/models/stock_orderpoint.py +++ b/addons/mrp/models/stock_orderpoint.py @@ -65,7 +65,8 @@ class StockWarehouseOrderpoint(models.Model): res = super()._compute_days_to_order() for orderpoint in self: if 'manufacture' in orderpoint.rule_ids.mapped('action'): - orderpoint.days_to_order = orderpoint.product_id.days_to_prepare_mo + boms = (orderpoint.product_id.variant_bom_ids or orderpoint.product_id.bom_ids) + orderpoint.days_to_order = boms and boms[0].days_to_prepare_mo or 0 return res def _quantity_in_progress(self): diff --git a/addons/mrp/models/stock_rule.py b/addons/mrp/models/stock_rule.py index 44403e8086a..9ae73e48ab3 100644 --- a/addons/mrp/models/stock_rule.py +++ b/addons/mrp/models/stock_rule.py @@ -117,8 +117,8 @@ class StockRule(models.Model): return self.env['mrp.bom']._bom_find(product_id, picking_type=self.picking_type_id, bom_type='normal', company_id=company_id.id)[product_id] def _prepare_mo_vals(self, product_id, product_qty, product_uom, location_dest_id, name, origin, company_id, values, bom): - date_planned = self._get_date_planned(product_id, company_id, values) - date_deadline = values.get('date_deadline') or date_planned + relativedelta(days=product_id.produce_delay) + date_planned = self._get_date_planned(bom, values) + date_deadline = values.get('date_deadline') or date_planned + relativedelta(days=bom.produce_delay) mo_values = { 'origin': origin, 'product_id': product_id.id, @@ -150,9 +150,9 @@ class StockRule(models.Model): }) return mo_values - def _get_date_planned(self, product_id, company_id, values): + def _get_date_planned(self, bom_id, values): format_date_planned = fields.Datetime.from_string(values['date_planned']) - date_planned = format_date_planned - relativedelta(days=product_id.produce_delay) + date_planned = format_date_planned - relativedelta(days=bom_id.produce_delay) if date_planned == format_date_planned: date_planned = date_planned - relativedelta(hours=1) return date_planned @@ -167,7 +167,8 @@ class StockRule(models.Model): if not manufacture_rule: return delay, delay_description manufacture_rule.ensure_one() - manufacture_delay = product.produce_delay + bom = (product.variant_bom_ids or product.bom_ids)[:1] + manufacture_delay = bom.produce_delay delay += manufacture_delay if not bypass_delay_description: delay_description.append((_('Manufacturing Lead Time'), _('+ %d day(s)', manufacture_delay))) @@ -175,7 +176,7 @@ class StockRule(models.Model): delay += security_delay if not bypass_delay_description: delay_description.append((_('Manufacture Security Lead Time'), _('+ %d day(s)', security_delay))) - days_to_order = values.get('days_to_order', product.product_tmpl_id.days_to_prepare_mo) + days_to_order = values.get('days_to_order', bom.days_to_prepare_mo) if not bypass_delay_description: delay_description.append((_('Days to Supply Components'), _('+ %d day(s)', days_to_order))) return delay + days_to_order, delay_description diff --git a/addons/mrp/populate/mrp.py b/addons/mrp/populate/mrp.py index 0472ab701c6..45fc2316f66 100644 --- a/addons/mrp/populate/mrp.py +++ b/addons/mrp/populate/mrp.py @@ -21,15 +21,6 @@ class ResCompany(models.Model): ] -class ProductProduct(models.Model): - _inherit = 'product.product' - - def _populate_factories(self): - return super()._populate_factories() + [ - ('produce_delay', populate.randint(1, 4)), - ] - - class Warehouse(models.Model): _inherit = 'stock.warehouse' @@ -75,6 +66,7 @@ class MrpBom(models.Model): ('sequence', populate.randint(1, 1000)), ('code', populate.constant("R{counter}")), ('ready_to_produce', populate.randomize(['all_available', 'asap'])), + ('produce_delay', populate.randint(1, 4)), ] diff --git a/addons/mrp/report/mrp_report_bom_structure.py b/addons/mrp/report/mrp_report_bom_structure.py index 87d7cb61814..f9748d9e73a 100644 --- a/addons/mrp/report/mrp_report_bom_structure.py +++ b/addons/mrp/report/mrp_report_bom_structure.py @@ -499,8 +499,8 @@ class ReportBomStructure(models.AbstractModel): 'route_type': 'manufacture', 'route_name': manufacture_rules[0].route_id.display_name, 'route_detail': bom.display_name, - 'lead_time': product.produce_delay + rules_delay, - 'manufacture_delay': product.produce_delay + rules_delay, + 'lead_time': bom.produce_delay + rules_delay, + 'manufacture_delay': bom.produce_delay + rules_delay, } return {} diff --git a/addons/mrp/views/mrp_bom_views.xml b/addons/mrp/views/mrp_bom_views.xml index 0b60938a0d1..a7ff060dff7 100644 --- a/addons/mrp/views/mrp_bom_views.xml +++ b/addons/mrp/views/mrp_bom_views.xml @@ -141,6 +141,17 @@ + diff --git a/addons/mrp/views/product_views.xml b/addons/mrp/views/product_views.xml index 35c639b2a91..824048f290f 100644 --- a/addons/mrp/views/product_views.xml +++ b/addons/mrp/views/product_views.xml @@ -7,21 +7,6 @@ product.template - - - diff --git a/addons/mrp_subcontracting/models/stock_picking.py b/addons/mrp_subcontracting/models/stock_picking.py index 0df9a6145b9..6e4849793c5 100644 --- a/addons/mrp_subcontracting/models/stock_picking.py +++ b/addons/mrp_subcontracting/models/stock_picking.py @@ -139,7 +139,7 @@ class StockPicking(models.Model): 'location_dest_id': subcontract_move.picking_id.partner_id.with_company(subcontract_move.company_id).property_stock_subcontractor.id, 'product_qty': subcontract_move.product_uom_qty, 'picking_type_id': warehouse.subcontracting_type_id.id, - 'date_planned_start': subcontract_move.date - relativedelta(days=product.produce_delay) + 'date_planned_start': subcontract_move.date - relativedelta(days=bom.produce_delay) } return vals diff --git a/addons/mrp_subcontracting/report/mrp_report_bom_structure.py b/addons/mrp_subcontracting/report/mrp_report_bom_structure.py index c753f0c1243..9a167ea4370 100644 --- a/addons/mrp_subcontracting/report/mrp_report_bom_structure.py +++ b/addons/mrp_subcontracting/report/mrp_report_bom_structure.py @@ -79,7 +79,7 @@ class ReportBomStructure(models.AbstractModel): 'route_detail': supplier.display_name, 'lead_time': supplier.delay + rules_delay, 'supplier_delay': supplier.delay + rules_delay, - 'manufacture_delay': product.produce_delay, + 'manufacture_delay': bom.produce_delay, 'supplier': supplier, } diff --git a/addons/mrp_subcontracting/tests/test_subcontracting.py b/addons/mrp_subcontracting/tests/test_subcontracting.py index a738cb26b70..f8467b5d8bc 100644 --- a/addons/mrp_subcontracting/tests/test_subcontracting.py +++ b/addons/mrp_subcontracting/tests/test_subcontracting.py @@ -113,7 +113,7 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon): self.subcontractor_partner1.property_stock_subcontractor = partner_subcontract_location.id # Add a manufacturing lead time to check that the resupply delivery is correctly planned 2 days # before the subcontracting receipt - self.finished.produce_delay = 2 + self.bom.produce_delay = 2 # Create a receipt picking from the subcontractor picking_form = Form(self.env['stock.picking']) @@ -140,7 +140,7 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon): # The picking should be a delivery order self.assertEqual(picking.picking_type_id, wh.subcontracting_resupply_type_id) # The date planned should be correct - self.assertEqual(picking_receipt.scheduled_date, picking.scheduled_date + relativedelta(days=self.finished.produce_delay)) + self.assertEqual(picking_receipt.scheduled_date, picking.scheduled_date + relativedelta(days=mo.bom_id.produce_delay)) self.assertEqual(mo.picking_type_id, wh.subcontracting_type_id) self.assertFalse(mo.picking_type_id.active) diff --git a/addons/sale_mrp/tests/test_sale_mrp_lead_time.py b/addons/sale_mrp/tests/test_sale_mrp_lead_time.py index 0d2a2bfd88d..6572ed21ae2 100644 --- a/addons/sale_mrp/tests/test_sale_mrp_lead_time.py +++ b/addons/sale_mrp/tests/test_sale_mrp_lead_time.py @@ -20,7 +20,6 @@ class TestSaleMrpLeadTime(TestStockCommon): # `type` is invisible in the view, # and it's a compute field based on `detailed_type` which is the field visible in the view p1.detailed_type = 'product' - p1.produce_delay = 5.0 p1.sale_delay = 5.0 p1.route_ids.clear() p1.route_ids.add(cls.warehouse_1.manufacture_pull_id.route_id) @@ -36,6 +35,7 @@ class TestSaleMrpLeadTime(TestStockCommon): with Form(cls.env['mrp.bom']) as bom: bom.product_tmpl_id = cls.product_1.product_tmpl_id bom.product_qty = 2 + bom.produce_delay = 5.0 with bom.bom_line_ids.new() as line: line.product_id = cls.product_2 line.product_qty = 4 @@ -80,7 +80,7 @@ class TestSaleMrpLeadTime(TestStockCommon): ) # Check schedule date and deadline of manufacturing order - mo_scheduled = out_date - timedelta(days=self.product_1.produce_delay) - timedelta(days=company.manufacturing_lead) + mo_scheduled = out_date - timedelta(days=manufacturing_order.bom_id.produce_delay) - timedelta(days=company.manufacturing_lead) self.assertAlmostEqual( fields.Datetime.from_string(manufacturing_order.date_planned_start), mo_scheduled, delta=timedelta(seconds=1), @@ -154,7 +154,7 @@ class TestSaleMrpLeadTime(TestStockCommon): ) # Check schedule date and deadline date of manufacturing order - mo_scheduled = out_date - timedelta(days=self.product_1.produce_delay) - timedelta(days=self.warehouse_1.delivery_route_id.rule_ids[0].delay) - timedelta(days=self.env.ref('base.main_company').manufacturing_lead) + mo_scheduled = out_date - timedelta(days=manufacturing_order.bom_id.produce_delay) - timedelta(days=self.warehouse_1.delivery_route_id.rule_ids[0].delay) - timedelta(days=self.env.ref('base.main_company').manufacturing_lead) self.assertAlmostEqual( fields.Datetime.from_string(manufacturing_order.date_planned_start), mo_scheduled, delta=timedelta(seconds=1),