From b03e900a40a4bc9c63e449e0df86889915123fc4 Mon Sep 17 00:00:00 2001 From: Nicolas Pierre Date: Thu, 19 Nov 2020 15:17:41 +0000 Subject: [PATCH] [IMP] stock(_*), mrp: highlight PO/SO links on forecast report Replenishment is great to replace MTO but misses the direct link between SO and PO (smart buttons on both SO and PO to link to the corresponding PO and SO). The info is actually there in the forecasted report but it is not accessible directly from the PO and in the case of the SO does not highlight the corresponding line of the SO. This adds a graph icon in the PO, MO and receipt form to directly access the forecasted report (it already exists for SO and delivery). The information linked to the SO/PO/MO/receipt/delivery is also sent to the forecasted report to highlight the corresponding line. Note that in the case of the SO, the information needs to go first through the qty_at_date widget (which contains the link to the forecast report). The graph icon for the PO, receipts and MO turns red when the forecasted quantity at expected date is negative. This is done using the new 'forecasted_issue' field. closes odoo/odoo#62264 Signed-off-by: Arnold Moyaux --- addons/mrp/models/mrp_production.py | 26 ++++++++++ addons/mrp/tests/test_stock_report.py | 27 ++++++++++ addons/mrp/views/mrp_production_views.xml | 3 ++ addons/purchase_stock/models/purchase.py | 27 ++++++++++ .../report/report_stock_forecasted.py | 1 + .../report/report_stock_forecasted.xml | 2 +- .../tests/test_purchase_stock_report.py | 24 +++++++++ .../purchase_stock/views/purchase_views.xml | 5 ++ .../report/report_stock_forecasted.py | 1 + .../report/report_stock_forecasted.xml | 2 +- .../static/src/js/qty_at_date_widget.js | 4 +- .../tests/test_sale_stock_report.py | 24 +++++++++ addons/sale_stock/views/sale_order_views.xml | 1 + addons/stock/models/stock_move.py | 23 ++++++++- .../stock/report/report_stock_forecasted.py | 6 +++ .../stock/report/report_stock_forecasted.xml | 2 +- .../src/scss/report_stock_forecasted.scss | 3 ++ addons/stock/tests/test_report.py | 50 +++++++++++++++++++ addons/stock/views/stock_picking_views.xml | 2 +- 19 files changed, 226 insertions(+), 7 deletions(-) diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index db4fbec72a6..15f25475249 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -255,6 +255,7 @@ class MrpProduction(models.Model): ('expected', 'Expected'), ('late', 'Late')], compute='_compute_components_availability') show_lot_ids = fields.Boolean('Display the serial number shortcut on the moves', compute='_compute_show_lot_ids') + forecasted_issue = fields.Boolean(compute='_compute_forecasted_issue') @api.depends('product_id', 'bom_id', 'company_id') def _compute_allowed_product_ids(self): @@ -516,6 +517,18 @@ class MrpProduction(models.Model): ('qty_positive', 'check (product_qty > 0)', 'The quantity to produce must be positive!'), ] + @api.depends('product_uom_qty', 'date_planned_start') + def _compute_forecasted_issue(self): + for order in self: + warehouse = order.location_dest_id.get_warehouse() + order.forecasted_issue = False + if order.product_id: + virtual_available = order.product_id.with_context(warehouse=warehouse.id, to_date=order.date_planned_start).virtual_available + if order.state == 'draft': + virtual_available += order.product_uom_qty + if virtual_available < 0: + order.forecasted_issue = True + @api.model def _search_delay_alert_date(self, operator, value): late_stock_moves = self.env['stock.move'].search([('delay_alert_date', operator, value)]) @@ -777,6 +790,19 @@ class MrpProduction(models.Model): self.is_locked = not self.is_locked return True + def action_product_forecast_report(self): + self.ensure_one() + action = self.product_id.action_product_forecast_report() + action['context'] = { + 'active_id': self.product_id.id, + 'active_model': 'product.product', + 'move_to_match_ids': self.move_finished_ids.filtered(lambda m: m.product_id == self.product_id).ids + } + warehouse = self.picking_type_id.warehouse_id + if warehouse: + action['context']['warehouse'] = warehouse.id + return action + def _create_workorder(self): for production in self: if not production.bom_id: diff --git a/addons/mrp/tests/test_stock_report.py b/addons/mrp/tests/test_stock_report.py index ce97fad278e..f4da9ff1a34 100644 --- a/addons/mrp/tests/test_stock_report.py +++ b/addons/mrp/tests/test_stock_report.py @@ -151,3 +151,30 @@ class TestSaleStockReports(TestReportsCommon): # Checks the forecast report. report_values, docs, lines = self.get_report_forecast(product_template_ids=product_apple_pie.product_tmpl_id.ids) self.assertEqual(len(lines), 0, "Must have no line") + + def test_report_forecast_3_report_line_corresponding_to_mo_highlighted(self): + """ When accessing the report from a MO, checks if the correct MO is highlighted in the report + """ + product_banana = self.env['product.product'].create({ + 'name': 'Banana', + 'type': 'product', + }) + + # We create 2 identical MO + mo_form = Form(self.env['mrp.production']) + mo_form.product_id = product_banana + mo_form.product_qty = 10 + mo_1 = mo_form.save() + mo_1.button_plan() + mo_2 = mo_1.copy() + mo_2.button_plan() + + # Check for both MO if the highlight (is_matched) corresponds to the correct MO + for mo in [mo_1, mo_2]: + context = mo.action_product_forecast_report()['context'] + _, _, lines = self.get_report_forecast(product_template_ids=product_banana.product_tmpl_id.ids, context=context) + for line in lines: + if line['document_in'] == mo: + self.assertTrue(line['is_matched'], "The corresponding MO line should be matched in the forecast report.") + else: + self.assertFalse(line['is_matched'], "A line of the forecast report not linked to the MO shoud not be matched.") diff --git a/addons/mrp/views/mrp_production_views.xml b/addons/mrp/views/mrp_production_views.xml index 01e1a662e0f..38abb9908d6 100644 --- a/addons/mrp/views/mrp_production_views.xml +++ b/addons/mrp/views/mrp_production_views.xml @@ -143,6 +143,7 @@ +