diff options
| author | svs-odoo <svs@odoo.com> | 2020-07-14 08:48:45 +0000 |
|---|---|---|
| committer | svs-odoo <svs@odoo.com> | 2020-07-31 12:36:54 +0000 |
| commit | 832af921156adf9951e533397e8a1b6e384c711b (patch) | |
| tree | 7803d143938accc4d04454b0388666da86e82dda | |
| parent | 288fdacaf7c39747df5a571994bae9a944013f28 (diff) | |
[IMP] mrp: extend replenishment report for mrp
Adds the Manufacturing Order as replenishment/consuming document for the
replenishment report.
task-2058495
| -rw-r--r-- | addons/mrp/__manifest__.py | 1 | ||||
| -rw-r--r-- | addons/mrp/models/stock_move.py | 4 | ||||
| -rw-r--r-- | addons/mrp/report/__init__.py | 1 | ||||
| -rw-r--r-- | addons/mrp/report/report_stock_forecasted.py | 37 | ||||
| -rw-r--r-- | addons/mrp/report/report_stock_forecasted.xml | 19 | ||||
| -rw-r--r-- | addons/mrp/tests/__init__.py | 1 | ||||
| -rw-r--r-- | addons/mrp/tests/test_stock_report.py | 153 |
7 files changed, 216 insertions, 0 deletions
diff --git a/addons/mrp/__manifest__.py b/addons/mrp/__manifest__.py index a5fdb6e3ebc..786cd178e28 100644 --- a/addons/mrp/__manifest__.py +++ b/addons/mrp/__manifest__.py @@ -40,6 +40,7 @@ 'report/mrp_report_views_main.xml', 'report/mrp_report_bom_structure.xml', 'report/mrp_production_templates.xml', + 'report/report_stock_forecasted.xml', 'report/report_stock_rule.xml', 'report/mrp_zebra_production_templates.xml', ], diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py index 170c69e1611..ea3b578c165 100644 --- a/addons/mrp/models/stock_move.py +++ b/addons/mrp/models/stock_move.py @@ -317,6 +317,10 @@ class StockMove(models.Model): vals['state'] = 'assigned' return vals + def _get_source_document(self): + res = super()._get_source_document() + return res or self.production_id or self.raw_material_production_id + def _get_upstream_documents_and_responsibles(self, visited): if self.production_id and self.production_id.state not in ('done', 'cancel'): return [(self.production_id, self.production_id.user_id, visited)] diff --git a/addons/mrp/report/__init__.py b/addons/mrp/report/__init__.py index c2e895dd1f6..b2dc5ecce3c 100644 --- a/addons/mrp/report/__init__.py +++ b/addons/mrp/report/__init__.py @@ -2,4 +2,5 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import mrp_report_bom_structure +from . import report_stock_forecasted from . import report_stock_rule diff --git a/addons/mrp/report/report_stock_forecasted.py b/addons/mrp/report/report_stock_forecasted.py new file mode 100644 index 00000000000..0e403f97ef7 --- /dev/null +++ b/addons/mrp/report/report_stock_forecasted.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models + + +class ReplenishmentReport(models.AbstractModel): + _inherit = 'report.stock.report_product_product_replenishment' + + def _move_draft_domain(self, product_template_ids, product_variant_ids, wh_location_ids): + in_domain, out_domain = super()._move_draft_domain(product_template_ids, product_variant_ids, wh_location_ids) + in_domain += [('production_id', '=', False)] + out_domain += [('raw_material_production_id', '=', False)] + return in_domain, out_domain + + def _compute_draft_quantity_count(self, product_template_ids, product_variant_ids, wh_location_ids): + res = super()._compute_draft_quantity_count(product_template_ids, product_variant_ids, wh_location_ids) + res['draft_production_qty'] = {} + domain = self._product_domain(product_template_ids, product_variant_ids) + domain += [('state', '=', 'draft')] + + # Pending incoming quantity. + mo_domain = domain + [('location_dest_id', 'in', wh_location_ids)] + grouped_mo = self.env['mrp.production'].read_group(mo_domain, ['product_qty:sum'], 'product_id') + res['draft_production_qty']['in'] = sum(mo['product_qty'] for mo in grouped_mo) + + # Pending outgoing quantity. + move_domain = domain + [ + ('raw_material_production_id', '!=', False), + ('location_id', 'in', wh_location_ids), + ] + grouped_moves = self.env['stock.move'].read_group(move_domain, ['product_qty:sum'], 'product_id') + res['draft_production_qty']['out'] = sum(move['product_qty'] for move in grouped_moves) + res['qty']['in'] += res['draft_production_qty']['in'] + res['qty']['out'] += res['draft_production_qty']['out'] + + return res diff --git a/addons/mrp/report/report_stock_forecasted.xml b/addons/mrp/report/report_stock_forecasted.xml new file mode 100644 index 00000000000..649158ee5df --- /dev/null +++ b/addons/mrp/report/report_stock_forecasted.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <template id="mrp_report_product_product_replenishment" inherit_id="stock.report_product_product_replenishment"> + <xpath expr="//tr[@name='draft_picking_in']" position="after"> + <tr t-if="docs['draft_production_qty']['in']" name="draft_mo_in"> + <td colspan="2">Draft MO</td> + <td t-esc="docs['draft_production_qty']['in']" class="text-right"/> + <td t-esc="docs['uom']" groups="uom.group_uom"/> + </tr> + </xpath> + <xpath expr="//tr[@name='draft_picking_out']" position="after"> + <tr t-if="docs['draft_production_qty']['out']" name="draft_mo_out"> + <td colspan="2">Draft MO</td> + <td t-esc="docs['draft_production_qty']['out']" class="text-right"/> + <td t-esc="docs['uom']" groups="uom.group_uom"/> + </tr> + </xpath> + </template> +</odoo> diff --git a/addons/mrp/tests/__init__.py b/addons/mrp/tests/__init__.py index 2af9d670b87..73cf6398cf0 100644 --- a/addons/mrp/tests/__init__.py +++ b/addons/mrp/tests/__init__.py @@ -5,6 +5,7 @@ from . import test_byproduct from . import test_cancel_mo from . import test_order from . import test_stock +from . import test_stock_report from . import test_warehouse_multistep_manufacturing from . import test_procurement from . import test_unbuild diff --git a/addons/mrp/tests/test_stock_report.py b/addons/mrp/tests/test_stock_report.py new file mode 100644 index 00000000000..ce97fad278e --- /dev/null +++ b/addons/mrp/tests/test_stock_report.py @@ -0,0 +1,153 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.tests.common import Form +from odoo.addons.stock.tests.test_report import TestReportsCommon + + +class TestSaleStockReports(TestReportsCommon): + def test_report_forecast_1_mo_count(self): + """ Creates and configures a product who could be produce and could be a component. + Plans some producing and consumming MO and check the report values. + """ + # Create a variant attribute. + product_chocolate = self.env['product.product'].create({ + 'name': 'Chocolate', + 'type': 'consu', + }) + product_chococake = self.env['product.product'].create({ + 'name': 'Choco Cake', + 'type': 'product', + }) + product_double_chococake = self.env['product.product'].create({ + 'name': 'Double Choco Cake', + 'type': 'product', + }) + + # Creates two BOM: one creating a regular slime, one using regular slimes. + bom_chococake = self.env['mrp.bom'].create({ + 'product_id': product_chococake.id, + 'product_tmpl_id': product_chococake.product_tmpl_id.id, + 'product_uom_id': product_chococake.uom_id.id, + 'product_qty': 1.0, + 'type': 'normal', + 'bom_line_ids': [ + (0, 0, {'product_id': product_chocolate.id, 'product_qty': 4}), + ], + }) + bom_double_chococake = self.env['mrp.bom'].create({ + 'product_id': product_double_chococake.id, + 'product_tmpl_id': product_double_chococake.product_tmpl_id.id, + 'product_uom_id': product_double_chococake.uom_id.id, + 'product_qty': 1.0, + 'type': 'normal', + 'bom_line_ids': [ + (0, 0, {'product_id': product_chococake.id, 'product_qty': 2}), + ], + }) + + # Creates two MO: one for each BOM. + mo_form = Form(self.env['mrp.production']) + mo_form.product_id = product_chococake + mo_form.bom_id = bom_chococake + mo_form.product_qty = 10 + mo_1 = mo_form.save() + mo_form = Form(self.env['mrp.production']) + mo_form.product_id = product_double_chococake + mo_form.bom_id = bom_double_chococake + mo_form.product_qty = 2 + mo_2 = mo_form.save() + + report_values, docs, lines = self.get_report_forecast(product_template_ids=product_chococake.product_tmpl_id.ids) + draft_picking_qty = docs['draft_picking_qty'] + draft_production_qty = docs['draft_production_qty'] + self.assertEqual(len(lines), 0, "Must have 0 line.") + self.assertEqual(draft_picking_qty['in'], 0) + self.assertEqual(draft_picking_qty['out'], 0) + self.assertEqual(draft_production_qty['in'], 10) + self.assertEqual(draft_production_qty['out'], 4) + + # Confirms the MO and checks the report lines. + mo_1.action_confirm() + mo_2.action_confirm() + report_values, docs, lines = self.get_report_forecast(product_template_ids=product_chococake.product_tmpl_id.ids) + draft_picking_qty = docs['draft_picking_qty'] + draft_production_qty = docs['draft_production_qty'] + self.assertEqual(len(lines), 2, "Must have two line.") + line_1 = lines[0] + line_2 = lines[1] + self.assertEqual(line_1['document_in'].id, mo_1.id) + self.assertEqual(line_1['quantity'], 4) + self.assertEqual(line_1['document_out'].id, mo_2.id) + self.assertEqual(line_2['document_in'].id, mo_1.id) + self.assertEqual(line_2['quantity'], 6) + self.assertEqual(line_2['document_out'], False) + self.assertEqual(draft_picking_qty['in'], 0) + self.assertEqual(draft_picking_qty['out'], 0) + self.assertEqual(draft_production_qty['in'], 0) + self.assertEqual(draft_production_qty['out'], 0) + + def test_report_forecast_2_production_backorder(self): + """ Creates a manufacturing order and produces half the quantity. + Then creates a backorder and checks the report. + """ + # Configures the warehouse. + warehouse = self.env.ref('stock.warehouse0') + warehouse.manufacture_steps = 'pbm_sam' + # Configures a product. + product_apple_pie = self.env['product.product'].create({ + 'name': 'Apple Pie', + 'type': 'product', + }) + product_apple = self.env['product.product'].create({ + 'name': 'Apple', + 'type': 'consu', + }) + bom = self.env['mrp.bom'].create({ + 'product_id': product_apple_pie.id, + 'product_tmpl_id': product_apple_pie.product_tmpl_id.id, + 'product_uom_id': product_apple_pie.uom_id.id, + 'product_qty': 1.0, + 'type': 'normal', + 'bom_line_ids': [ + (0, 0, {'product_id': product_apple.id, 'product_qty': 5}), + ], + }) + # Creates a MO and validates the pick components. + mo_form = Form(self.env['mrp.production']) + mo_form.product_id = product_apple_pie + mo_form.bom_id = bom + mo_form.product_qty = 4 + mo_1 = mo_form.save() + mo_1.action_confirm() + pick = mo_1.move_raw_ids.move_orig_ids.picking_id + pick_form = Form(pick) + with pick_form.move_line_ids_without_package.edit(0) as move_line: + move_line.qty_done = 20 + pick = pick_form.save() + pick.button_validate() + # Produces 3 products then creates a backorder for the remaining product. + mo_form = Form(mo_1) + mo_form.qty_producing = 3 + mo_1 = mo_form.save() + action = mo_1.button_mark_done() + backorder_form = Form(self.env['mrp.production.backorder'].with_context(**action['context'])) + backorder = backorder_form.save() + backorder.action_backorder() + + mo_2 = (mo_1.procurement_group_id.mrp_production_ids - mo_1) + # 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), 1, "Must have only one line about the backorder") + self.assertEqual(lines[0]['document_in'].id, mo_2.id) + self.assertEqual(lines[0]['quantity'], 1) + self.assertEqual(lines[0]['document_out'], False) + + # Produces the last unit. + mo_form = Form(mo_2) + mo_form.qty_producing = 1 + mo_2 = mo_form.save() + mo_2.button_mark_done() + # 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") |
