From 78e44dc5a3dee67956f380fa468bab02692ce5ac Mon Sep 17 00:00:00 2001 From: yhu-odoo Date: Tue, 12 Sep 2023 14:52:23 +0200 Subject: [PATCH] [IMP] mrp{,_subcontracting_purchase}, {purchase_,}stock: add DTPMO to lead days For subcontracting, we need to consider both vendor lead time and manufacturing lead time, and DTPMO (Days To Prepare MO) on the BOM. Subcontracting delay = max(Vendor lead time, Manufacturing lead time + DTPMO) + Days to Purchase + Purchase security lead time Same thing applied to Bom overview, except: 1. Availability state will be computed based on the delay time of it's components. DTPMO on the bom won't be take into account. 2. Lead time will use the DTPMO on the BOM. DTPMO will be added to the lead time when it's a manufacturing bom or when it's a subcontracting bom with Manufacturing Lead Time + DTPMO > Vendor Lead Time Task-3081481 Part-of: odoo/odoo#137810 --- addons/mrp/models/mrp_bom.py | 3 +- addons/mrp/models/res_company.py | 5 - addons/mrp/models/stock_rule.py | 20 ++-- addons/mrp/report/mrp_report_bom_structure.py | 13 ++- .../report/mrp_report_bom_structure.py | 20 ++-- .../models/__init__.py | 5 +- .../models/stock_rule.py | 50 +++++++++ .../report/mrp_report_bom_structure.py | 11 ++ .../tests/test_mrp_subcontracting_purchase.py | 100 ++++++++++++++++++ addons/purchase_stock/models/res_company.py | 6 -- addons/purchase_stock/models/stock_rule.py | 22 ++-- addons/stock/models/product.py | 11 +- addons/stock/models/res_company.py | 3 - addons/stock/models/stock_move.py | 7 +- addons/stock/models/stock_orderpoint.py | 12 ++- addons/stock/models/stock_rule.py | 8 +- .../stock/wizard/stock_replenishment_info.py | 2 +- 17 files changed, 235 insertions(+), 63 deletions(-) create mode 100644 addons/mrp_subcontracting_purchase/models/stock_rule.py diff --git a/addons/mrp/models/mrp_bom.py b/addons/mrp/models/mrp_bom.py index 2b8ee43d5af..601d281b924 100644 --- a/addons/mrp/models/mrp_bom.py +++ b/addons/mrp/models/mrp_bom.py @@ -297,8 +297,7 @@ class MrpBom(models.Model): 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 + bom.days_to_prepare_mo = self.env['report.mrp.report_bom_structure']._get_max_component_delay(bom_data['components']) @api.constrains('product_tmpl_id', 'product_id', 'type') def check_kit_has_not_orderpoint(self): diff --git a/addons/mrp/models/res_company.py b/addons/mrp/models/res_company.py index 0a26c44c757..bbfdfd84d14 100644 --- a/addons/mrp/models/res_company.py +++ b/addons/mrp/models/res_company.py @@ -36,8 +36,3 @@ class Company(models.Model): def _create_per_company_sequences(self): super(Company, self)._create_per_company_sequences() self._create_unbuild_sequence() - - def _get_security_by_rule_action(self): - res = super()._get_security_by_rule_action() - res['manufacture'] = self.manufacturing_lead - return res diff --git a/addons/mrp/models/stock_rule.py b/addons/mrp/models/stock_rule.py index 7624d4b7804..82e7b96719b 100644 --- a/addons/mrp/models/stock_rule.py +++ b/addons/mrp/models/stock_rule.py @@ -190,25 +190,29 @@ class StockRule(models.Model): """Add the product and company manufacture delay to the cumulative delay and cumulative description. """ - delay, delay_description = super()._get_lead_days(product, **values) + delays, delay_description = super()._get_lead_days(product, **values) bypass_delay_description = self.env.context.get('bypass_delay_description') manufacture_rule = self.filtered(lambda r: r.action == 'manufacture') if not manufacture_rule: - return delay, delay_description + return delays, delay_description manufacture_rule.ensure_one() bom = values.get('bom') or self.env['mrp.bom']._bom_find(product, picking_type=manufacture_rule.picking_type_id, company_id=manufacture_rule.company_id.id)[product] manufacture_delay = bom.produce_delay - delay += manufacture_delay + delays['total_delay'] += manufacture_delay + delays['manufacture_delay'] += manufacture_delay if not bypass_delay_description: delay_description.append((_('Manufacturing Lead Time'), _('+ %d day(s)', manufacture_delay))) - security_delay = manufacture_rule.picking_type_id.company_id.manufacturing_lead - delay += security_delay - if not bypass_delay_description: - delay_description.append((_('Manufacture Security Lead Time'), _('+ %d day(s)', security_delay))) + if bom.type == 'normal': + security_delay = self.picking_type_id.company_id.manufacturing_lead + delays['total_delay'] += security_delay + delays['security_lead_days'] += 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', bom.days_to_prepare_mo) + delays['total_delay'] += days_to_order 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 + return delays, delay_description def _push_prepare_move_copy_values(self, move_to_copy, new_date): new_move_vals = super(StockRule, self)._push_prepare_move_copy_values(move_to_copy, new_date) diff --git a/addons/mrp/report/mrp_report_bom_structure.py b/addons/mrp/report/mrp_report_bom_structure.py index 615c8f50d94..ebe8b593c25 100644 --- a/addons/mrp/report/mrp_report_bom_structure.py +++ b/addons/mrp/report/mrp_report_bom_structure.py @@ -37,18 +37,19 @@ 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()) - same_delay = bom_data['lead_time'] == bom_data['availability_delay'] + lead_time = bom_data['lead_time'] - bom_data['bom'].days_to_prepare_mo + same_delay = lead_time == bom_data['availability_delay'] res = {} if bom_data.get('producible_qty', 0): # Check if something is producible today, at the earliest time possible considering product's lead time. res['earliest_capacity'] = bom_data['producible_qty'] - res['earliest_date'] = format_date(self.env, date_today + timedelta(days=bom_data['lead_time'])) + res['earliest_date'] = format_date(self.env, date_today + timedelta(days=lead_time)) if bom_data['availability_state'] != 'unavailable': if same_delay: # Means that stock will be resupplied at date_today, so the whole manufacture can start at date_today. res['earliest_capacity'] = bom_qty - res['earliest_date'] = format_date(self.env, date_today + timedelta(days=bom_data['lead_time'])) + res['earliest_date'] = format_date(self.env, date_today + timedelta(days=bom_data['availability_delay'])) else: res['leftover_capacity'] = bom_qty - bom_data.get('producible_qty', 0) res['leftover_date'] = format_date(self.env, date_today + timedelta(days=bom_data['availability_delay'])) @@ -247,7 +248,6 @@ class ReportBomStructure(models.AbstractModel): 'route_type': route_info.get('route_type', ''), 'route_name': route_info.get('route_name', ''), 'route_detail': route_info.get('route_detail', ''), - 'lead_time': route_info.get('lead_time', False), 'currency': company.currency_id, 'currency_id': company.currency_id.id, 'product': product, @@ -314,6 +314,8 @@ class ReportBomStructure(models.AbstractModel): bom_report_line['bom_cost'] *= bom_report_line['cost_share'] 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.update(availabilities) if level == 0: @@ -588,8 +590,9 @@ class ReportBomStructure(models.AbstractModel): 'route_type': 'manufacture', 'route_name': manufacture_rules[0].route_id.display_name, 'route_detail': bom.display_name, - 'lead_time': bom.produce_delay + rules_delay + manufacturing_lead, + 'lead_time': bom.produce_delay + rules_delay + manufacturing_lead + bom.days_to_prepare_mo, 'manufacture_delay': bom.produce_delay + rules_delay + manufacturing_lead, + 'bom': bom, } return {} diff --git a/addons/mrp_subcontracting/report/mrp_report_bom_structure.py b/addons/mrp_subcontracting/report/mrp_report_bom_structure.py index 9a167ea4370..e048d2c562e 100644 --- a/addons/mrp_subcontracting/report/mrp_report_bom_structure.py +++ b/addons/mrp_subcontracting/report/mrp_report_bom_structure.py @@ -72,15 +72,16 @@ class ReportBomStructure(models.AbstractModel): subcontract_rules = [rule for rule in rules if rule.action == 'buy' and bom and bom.type == 'subcontract'] if subcontract_rules: supplier = product._select_seller(quantity=quantity, uom_id=product.uom_id, params={'subcontractor_ids': bom.subcontractor_ids}) + # for subcontracting, we can't decide the lead time without component's resupply availability + # we only return necessary info and calculate the lead time late when we have component's data if supplier: return { 'route_type': 'subcontract', 'route_name': subcontract_rules[0].route_id.display_name, 'route_detail': supplier.display_name, - 'lead_time': supplier.delay + rules_delay, - 'supplier_delay': supplier.delay + rules_delay, - 'manufacture_delay': bom.produce_delay, + 'lead_time': rules_delay, 'supplier': supplier, + 'bom': bom, } return res @@ -106,11 +107,16 @@ 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': max_component_delay = self._get_max_component_delay(components) if max_component_delay is False: return ('unavailable', False) - produce_delay = route_info.get('manufacture_delay', 0) + max_component_delay - supplier_delay = route_info.get('supplier_delay', 0) - return ('estimated', max(produce_delay, supplier_delay)) - return super()._get_resupply_availability(route_info, components) + # Calculate the lead time for subcontracting, keep same as `_get_lead_days` + 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) + 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_purchase/models/__init__.py b/addons/mrp_subcontracting_purchase/models/__init__.py index d72772bf68b..6576bfb375b 100644 --- a/addons/mrp_subcontracting_purchase/models/__init__.py +++ b/addons/mrp_subcontracting_purchase/models/__init__.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import account_move_line -from . import stock_picking -from . import stock_valuation_layer from . import purchase_order +from . import stock_picking +from . import stock_rule +from . import stock_valuation_layer diff --git a/addons/mrp_subcontracting_purchase/models/stock_rule.py b/addons/mrp_subcontracting_purchase/models/stock_rule.py new file mode 100644 index 00000000000..b0c3d40d6b4 --- /dev/null +++ b/addons/mrp_subcontracting_purchase/models/stock_rule.py @@ -0,0 +1,50 @@ +from odoo import models, _ + + +class StockRule(models.Model): + _inherit = 'stock.rule' + + def _get_lead_days(self, product, **values): + """For subcontracting, we need to consider both vendor lead time and + manufacturing lead time, and DTPMO (Days To Prepare MO). + Subcontracting delay = + max(Vendor lead time, Manufacturing lead time + DTPMO) + Days to Purchase + Purchase security lead time + """ + bypass_delay_description = self.env.context.get('bypass_delay_description') + buy_rule = self.filtered(lambda r: r.action == 'buy') + seller = 'supplierinfo' in values and values['supplierinfo'] or product.with_company(buy_rule.company_id)._select_seller(quantity=None) + if not buy_rule or not seller: + return super()._get_lead_days(product, **values) + seller = seller[0] + bom = self.env['mrp.bom'].sudo()._bom_subcontract_find( + product, + company_id=buy_rule.picking_type_id.company_id.id, + bom_type='subcontract', + subcontractor=seller.partner_id) + if not bom: + return super()._get_lead_days(product, **values) + + delays, delay_description = super(StockRule, self - buy_rule)._get_lead_days(product, **values) + extra_delays, extra_delay_description = super(StockRule, buy_rule.with_context(ignore_vendor_lead_time=True))._get_lead_days(product, **values) + if seller.delay >= bom.produce_delay + bom.days_to_prepare_mo: + delays['total_delay'] += seller.delay + delays['purchase_delay'] += seller.delay + if not bypass_delay_description: + delay_description.append((_('Vendor Lead Time'), _('+ %d day(s)', seller.delay))) + else: + manufacture_delay = bom.produce_delay + delays['total_delay'] += manufacture_delay + # set manufacture_delay to purchase_delay so that PO can be created with correct date + delays['purchase_delay'] += manufacture_delay + if not bypass_delay_description: + delay_description.append((_('Manufacturing Lead Time'), _('+ %d day(s)', manufacture_delay))) + days_to_order = bom.days_to_prepare_mo + delays['total_delay'] += days_to_order + # add dtpmo to purchase_delay so that PO can be created with correct date + delays['purchase_delay'] += days_to_order + if not bypass_delay_description: + extra_delay_description.append((_('Days to Supply Components'), _('+ %d day(s)', days_to_order))) + + for key, value in extra_delays.items(): + delays[key] += value + return delays, delay_description + extra_delay_description 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 41b6b766c99..b37d60826be 100644 --- a/addons/mrp_subcontracting_purchase/report/mrp_report_bom_structure.py +++ b/addons/mrp_subcontracting_purchase/report/mrp_report_bom_structure.py @@ -10,3 +10,14 @@ class ReportBomStructure(models.AbstractModel): @api.model def _is_buy_route(self, rules, product, bom): return super()._is_buy_route(rules, product, bom) and (not bom or bom.type != 'subcontract') + + @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': + # 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 + 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 84b4fd9990d..63a299d20c5 100644 --- a/addons/mrp_subcontracting_purchase/tests/test_mrp_subcontracting_purchase.py +++ b/addons/mrp_subcontracting_purchase/tests/test_mrp_subcontracting_purchase.py @@ -382,3 +382,103 @@ class MrpSubcontractingPurchaseTest(TestMrpSubcontractingCommon): {'location_dest_id': stock_location_id.id, 'quantity': 10.0, 'state': 'done'}, {'location_dest_id': subco_location_id.id, 'quantity': 1.0, 'state': 'done'}, ]) + + def test_subcontracting_lead_days(self): + """ Test the lead days computation for subcontracting. Subcontracting delay = + max(Vendor lead time, Manufacturing lead time + DTPMO) + Days to Purchase + Purchase security lead time + """ + rule = self.env['stock.rule'].search([('action', '=', 'buy')], limit=1) + self.env.company.manufacturing_lead = 114514 # should never be used + + self.env.company.po_lead = 1 + self.env.company.days_to_purchase = 2 + # Case 1 Vendor lead time >= Manufacturing lead time + DTPMO + seller = self.env['product.supplierinfo'].create({ + 'product_tmpl_id': self.finished.product_tmpl_id.id, + 'partner_id': self.subcontractor_partner1.id, + 'price': 12.0, + 'delay': 10 + }) + self.bom.produce_delay = 3 + self.bom.days_to_prepare_mo = 4 + delays, _ = rule._get_lead_days(self.finished, supplierinfo=seller) + self.assertEqual(delays['total_delay'], seller.delay + self.env.company.po_lead + self.env.company.days_to_purchase) + # Case 2 Vendor lead time < Manufacturing lead time + DTPMO + self.bom.produce_delay = 5 + self.bom.days_to_prepare_mo = 6 + delays, _ = rule._get_lead_days(self.finished, supplierinfo=seller) + self.assertEqual(delays['total_delay'], self.bom.produce_delay + self.bom.days_to_prepare_mo + self.env.company.po_lead + self.env.company.days_to_purchase) + + def test_subcontracting_lead_days_on_overview(self): + """Test on the BOM overview, the lead days and resupply availability are + correctly computed. The dtpmo on the bom should be used for the lead days, + while the resupply availability should be based on the calculated dtpmo. + """ + # should never be used + self.env.company.manufacturing_lead = 114514 + # should be added in all cases + self.env.company.po_lead = 5 + self.env.company.days_to_purchase = 5 + + rule = self.env['stock.rule'].search([('action', '=', 'buy')], limit=1) + (self.finished | self.comp1 | self.comp2).route_ids = [(6, None, [rule.route_id.id])] + self.comp2_bom.active = False + self.env['product.supplierinfo'].create({ + 'product_tmpl_id': self.finished.product_tmpl_id.id, + 'partner_id': self.subcontractor_partner1.id, + 'price': 648.0, + 'delay': 15 + }) + self.env['product.supplierinfo'].create({ + 'product_tmpl_id': self.comp1.product_tmpl_id.id, + 'partner_id': self.subcontractor_partner1.id, + 'price': 648.0, + 'delay': 10 + }) + self.env['product.supplierinfo'].create({ + 'product_tmpl_id': self.comp2.product_tmpl_id.id, + 'partner_id': self.subcontractor_partner1.id, + 'price': 648.0, + 'delay': 6 + }) + self.bom.produce_delay = 10 + self.bom.days_to_prepare_mo = 0 + + # Case 1: Vendor lead time >= Manufacturing lead time + DTPMO on BOM + 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") + + # 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 + 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, + "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") + + # 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.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 + 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, + "Lead time = Purchase lead time(finished) + Days to Purchase + Purchase security lead time") + for component in bom_data['components']: + self.assertEqual(component['availability_state'], 'available') + # 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") + 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 + 20, + "Lead time = Manufacturing lead time + Days to Purchase + Purchase security lead time + DTPMO on BOM") + for component in bom_data['components']: + self.assertEqual(component['availability_state'], 'available') diff --git a/addons/purchase_stock/models/res_company.py b/addons/purchase_stock/models/res_company.py index 40b810ab185..b49d8cf9259 100644 --- a/addons/purchase_stock/models/res_company.py +++ b/addons/purchase_stock/models/res_company.py @@ -10,9 +10,3 @@ class ResCompany(models.Model): days_to_purchase = fields.Float( string='Days to Purchase', help="Days needed to confirm a PO, define when a PO should be validated") - - - def _get_security_by_rule_action(self): - res = super()._get_security_by_rule_action() - res['buy'] = self.po_lead - return res diff --git a/addons/purchase_stock/models/stock_rule.py b/addons/purchase_stock/models/stock_rule.py index e2d4bba9238..06426255691 100644 --- a/addons/purchase_stock/models/stock_rule.py +++ b/addons/purchase_stock/models/stock_rule.py @@ -161,23 +161,29 @@ class StockRule(models.Model): and cumulative description. The company lead time is always displayed for onboarding purpose in order to indicate that those options are available. """ - delay, delay_description = super()._get_lead_days(product, **values) + delays, delay_description = super()._get_lead_days(product, **values) bypass_delay_description = self.env.context.get('bypass_delay_description') buy_rule = self.filtered(lambda r: r.action == 'buy') seller = 'supplierinfo' in values and values['supplierinfo'] or product.with_company(buy_rule.company_id)._select_seller(quantity=None) if not buy_rule or not seller: - return delay, delay_description + return delays, delay_description buy_rule.ensure_one() - supplier_delay = seller[0].delay - if supplier_delay and not bypass_delay_description: - delay_description.append((_('Vendor Lead Time'), _('+ %d day(s)', supplier_delay))) + if not self.env.context.get('ignore_vendor_lead_time'): + supplier_delay = seller[0].delay + delays['total_delay'] += supplier_delay + delays['purchase_delay'] += supplier_delay + if not bypass_delay_description: + delay_description.append((_('Vendor Lead Time'), _('+ %d day(s)', supplier_delay))) security_delay = buy_rule.picking_type_id.company_id.po_lead + delays['total_delay'] += security_delay + delays['security_lead_days'] += security_delay if not bypass_delay_description: delay_description.append((_('Purchase Security Lead Time'), _('+ %d day(s)', security_delay))) - days_to_order = values.get('days_to_order', buy_rule.company_id.days_to_purchase) + days_to_order = buy_rule.company_id.days_to_purchase + delays['total_delay'] += days_to_order if not bypass_delay_description: delay_description.append((_('Days to Purchase'), _('+ %d day(s)', days_to_order))) - return delay + supplier_delay + security_delay + days_to_order, delay_description + return delays, delay_description @api.model def _get_procurements_to_merge_groupby(self, procurement): @@ -266,7 +272,7 @@ class StockRule(models.Model): params values: values of procurements params origins: procuremets origins to write on the PO """ - purchase_date = min([fields.Datetime.from_string(value['date_planned']) - relativedelta(days=int(value['supplier'].delay)) for value in values]) + purchase_date = min([value.get('date_order') or fields.Datetime.from_string(value['date_planned']) - relativedelta(days=int(value['supplier'].delay)) for value in values]) # Since the procurements are grouped if they share the same domain for # PO but the PO does not exist. In this case it will create the PO from diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py index 53078cddbd0..0bfb825e5aa 100644 --- a/addons/stock/models/product.py +++ b/addons/stock/models/product.py @@ -595,12 +595,13 @@ class Product(models.Model): else: return self._get_rules_from_location(rule.location_src_id, seen_rules=seen_rules | rule) - def _get_date_with_security_lead_days(self, date, location, route_ids=False): + def _get_dates_info(self, date, location, route_ids=False): rules = self._get_rules_from_location(location, route_ids=route_ids) - for action, days in location.company_id._get_security_by_rule_action().items(): - if action in rules.mapped('action'): - date -= relativedelta(days=days) - return date + delays, _ = rules.with_context(bypass_delay_description=True)._get_lead_days(self) + return { + 'date_planned': date - relativedelta(days=delays['security_lead_days']), + 'date_order': date - relativedelta(days=delays['security_lead_days'] + delays['purchase_delay']), + } def _get_only_qty_available(self): """ Get only quantities available, it is equivalent to read qty_available diff --git a/addons/stock/models/res_company.py b/addons/stock/models/res_company.py index 0d1e9ab27e1..cadace55d4c 100644 --- a/addons/stock/models/res_company.py +++ b/addons/stock/models/res_company.py @@ -206,6 +206,3 @@ class Company(models.Model): 'partner_id': company.partner_id.id } for company in companies]) return companies - - def _get_security_by_rule_action(self): - return {} diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index 6fe8123459e..557eeb37a95 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -1407,12 +1407,13 @@ Please change the quantity done or the rounding precision of your unit of measur elif self.rule_id.group_propagation_option == 'none': group_id = False product_id = self.product_id.with_context(lang=self._get_lang()) - date = self._get_mto_procurement_date() + dates_info = {'date_planned': self._get_mto_procurement_date()} if self.location_id.warehouse_id and self.location_id.warehouse_id.lot_stock_id.parent_path in self.location_id.parent_path: - date = self.product_id._get_date_with_security_lead_days(self.date, self.location_id, route_ids=self.route_ids) + dates_info = self.product_id._get_dates_info(self.date, self.location_id, route_ids=self.route_ids) return { 'product_description_variants': self.description_picking and self.description_picking.replace(product_id._get_description(self.picking_type_id), ''), - 'date_planned': date, + 'date_planned': dates_info.get('date_planned'), + 'date_order': dates_info.get('date_order'), 'date_deadline': self.date_deadline, 'move_dest_ids': self, 'group_id': group_id, diff --git a/addons/stock/models/stock_orderpoint.py b/addons/stock/models/stock_orderpoint.py index a3757648ed4..ac8c1e53770 100644 --- a/addons/stock/models/stock_orderpoint.py +++ b/addons/stock/models/stock_orderpoint.py @@ -79,6 +79,7 @@ class StockWarehouseOrderpoint(models.Model): qty_forecast = fields.Float('Forecast', readonly=True, compute='_compute_qty', digits='Product Unit of Measure') qty_to_order = fields.Float('To Order', compute='_compute_qty_to_order', store=True, readonly=False, digits='Product Unit of Measure') + #TODO: remove this field in master days_to_order = fields.Float(compute='_compute_days_to_order', help="Numbers of days in advance that replenishments demands are created.") visibility_days = fields.Float( compute='_compute_visibility_days', inverse='_set_visibility_days', readonly=False, @@ -113,7 +114,7 @@ class StockWarehouseOrderpoint(models.Model): continue values = orderpoint._get_lead_days_values() lead_days, dummy = orderpoint.rule_ids._get_lead_days(orderpoint.product_id, **values) - lead_days_date = fields.Date.today() + relativedelta.relativedelta(days=lead_days) + lead_days_date = fields.Date.today() + relativedelta.relativedelta(days=lead_days['total_delay']) orderpoint.lead_days_date = lead_days_date @api.depends('route_id', 'product_id', 'location_id', 'company_id', 'warehouse_id', 'product_id.route_ids') @@ -352,7 +353,7 @@ class StockWarehouseOrderpoint(models.Model): # group product by lead_days and location in order to read virtual_available # in batch rules = product._get_rules_from_location(loc) - lead_days = rules.with_context(bypass_delay_description=True)._get_lead_days(product)[0] + lead_days = rules.with_context(bypass_delay_description=True)._get_lead_days(product)[0]['total_delay'] ploc_per_day[(lead_days, loc)].add(product.id) # recompute virtual_available with lead days @@ -481,11 +482,12 @@ class StockWarehouseOrderpoint(models.Model): comming from an orderpoint. This method could be override in order to add other custom key that could be used in move/po creation. """ - date_planned = date or fields.Date.today() - date_planned = self.product_id._get_date_with_security_lead_days(date_planned, self.location_id, route_ids=self.route_id) + date_deadline = date or fields.Date.today() + dates_info = self.product_id._get_dates_info(date_deadline, self.location_id, route_ids=self.route_id) return { 'route_ids': self.route_id, - 'date_planned': date_planned, + 'date_planned': dates_info['date_planned'], + 'date_order': dates_info['date_order'], 'date_deadline': date or False, 'warehouse_id': self.warehouse_id, 'orderpoint_id': self, diff --git a/addons/stock/models/stock_rule.py b/addons/stock/models/stock_rule.py index 9b9e7a593ef..82c6954aa42 100644 --- a/addons/stock/models/stock_rule.py +++ b/addons/stock/models/stock_rule.py @@ -359,12 +359,14 @@ class StockRule(models.Model): :param product: the product of the procurement :type product: :class:`~odoo.addons.product.models.product.ProductProduct` :return: the cumulative delay and cumulative delay's description - :rtype: tuple[int, list[str, str]] + :rtype: tuple[defaultdict(float), list[str, str]] """ + delays = defaultdict(float) delay = sum(self.filtered(lambda r: r.action in ['pull', 'pull_push']).mapped('delay')) + delays['total_delay'] += delay global_visibility_days = self.env['ir.config_parameter'].sudo().get_param('stock.visibility_days') if global_visibility_days: - delay += int(global_visibility_days) + delays['total_delay'] += int(global_visibility_days) if self.env.context.get('bypass_delay_description'): delay_description = [] else: @@ -375,7 +377,7 @@ class StockRule(models.Model): ] if global_visibility_days: delay_description.append((_('Global Visibility Days'), _('+ %d day(s)', int(global_visibility_days)))) - return delay, delay_description + return delays, delay_description class ProcurementGroup(models.Model): diff --git a/addons/stock/wizard/stock_replenishment_info.py b/addons/stock/wizard/stock_replenishment_info.py index f711c606fdf..2d5f4e9374e 100644 --- a/addons/stock/wizard/stock_replenishment_info.py +++ b/addons/stock/wizard/stock_replenishment_info.py @@ -118,7 +118,7 @@ class StockReplenishmentOption(models.TransientModel): @api.depends('replenishment_info_id') def _compute_lead_time(self): for record in self: - lead_time = record.route_id.rule_ids._get_lead_days(record.product_id)[0] + lead_time = record.route_id.rule_ids._get_lead_days(record.product_id)[0]['total_delay'] #TO FIX: use _get_rule to avoid singleton issue record.lead_time = str(lead_time) + " days" @api.depends('warehouse_id', 'free_qty', 'uom', 'qty_to_order')