diff options
| author | svs-odoo <svs@odoo.com> | 2019-05-27 06:26:56 +0000 |
|---|---|---|
| committer | svs-odoo <svs@odoo.com> | 2019-07-31 12:37:38 +0000 |
| commit | c76fcd87c513b74cebea3a318c33f06fb03cafa9 (patch) | |
| tree | 66ffde4646b3c73ccc7c2f47d3c063afd1142bc7 | |
| parent | 28c2e642cbf02d65e0f50ec45c590cd0eba0dcfb (diff) | |
[IMP] mrp: can cancel MO in more cases
Manufacturing Order can now be cancelled even if some move lines or Work
Orders are done (in this case, a wizard ask confirmation to the user).
Task #1969034
Co-authored-by: Arnold Moyaux <arm@odoo.com>
| -rw-r--r-- | addons/mrp/models/mrp_production.py | 61 | ||||
| -rw-r--r-- | addons/mrp/models/stock_move.py | 6 | ||||
| -rw-r--r-- | addons/mrp/tests/__init__.py | 1 | ||||
| -rw-r--r-- | addons/mrp/tests/test_cancel_mo.py | 117 | ||||
| -rw-r--r-- | addons/mrp/views/mrp_production_views.xml | 7 |
5 files changed, 167 insertions, 25 deletions
diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index 930ce953948..f336f10293c 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import datetime +from collections import defaultdict from itertools import groupby from odoo import api, fields, models, _ @@ -202,6 +203,27 @@ class MrpProduction(models.Model): production_location_id = fields.Many2one('stock.location', "Production Location", related='product_id.property_stock_production', readonly=False) picking_ids = fields.Many2many('stock.picking', compute='_compute_picking_ids', string='Picking associated to this manufacturing order') delivery_count = fields.Integer(string='Delivery Orders', compute='_compute_picking_ids') + confirm_cancel = fields.Boolean(compute='_compute_confirm_cancel') + + @api.depends('move_raw_ids.state', 'move_finished_ids.state') + def _compute_confirm_cancel(self): + """ If the manufacturing order contains some done move (via an intermediate + post inventory), the user has to confirm the cancellation. + """ + domain = [ + ('state', '=', 'done'), + '|', + ('production_id', 'in', self.ids), + ('raw_material_production_id', 'in', self.ids) + ] + res = self.env['stock.move'].read_group(domain, ['state', 'production_id', 'raw_material_production_id'], ['production_id', 'raw_material_production_id'], lazy=False) + productions_with_done_move = {} + for rec in res: + production_record = rec['production_id'] or rec['raw_material_production_id'] + if production_record: + productions_with_done_move[production_record[0]] = True + for production in self: + production.confirm_cancel = productions_with_done_move.get(production.id, False) @api.depends('procurement_group_id') def _compute_picking_ids(self): @@ -770,33 +792,36 @@ class MrpProduction(models.Model): if not self.move_raw_ids: self.state = 'cancel' return True - if any(workorder.state == 'progress' for workorder in self.mapped('workorder_ids')): - raise UserError(_('You can not cancel production order, a work order is still in progress.')) - documents = {} + self._action_cancel() + return True + + def _action_cancel(self): + documents_by_production = {} for production in self: - for move_raw_id in production.move_raw_ids.filtered(lambda m: m.state not in ('done', 'cancel')): + documents = defaultdict(list) + for move_raw_id in self.move_raw_ids.filtered(lambda m: m.state not in ('done', 'cancel')): iterate_key = self._get_document_iterate_key(move_raw_id) if iterate_key: document = self.env['stock.picking']._log_activity_get_documents({move_raw_id: (move_raw_id.product_uom_qty, 0)}, iterate_key, 'UP') for key, value in document.items(): - if documents.get(key): - documents[key] += [value] - else: - documents[key] = [value] - production.workorder_ids.filtered(lambda x: x.state != 'cancel').action_cancel() - finish_moves = production.move_finished_ids.filtered(lambda x: x.state not in ('done', 'cancel')) - raw_moves = production.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel')) - (finish_moves | raw_moves)._action_cancel() - picking_ids = production.picking_ids.filtered(lambda x: x.state not in ('done', 'cancel')) - picking_ids.action_cancel() - - if documents: + documents[key] += [value] + if documents: + documents_by_production[production] = documents + + self.workorder_ids.filtered(lambda x: x.state not in ['done', 'cancel']).action_cancel() + finish_moves = self.move_finished_ids.filtered(lambda x: x.state not in ('done', 'cancel')) + raw_moves = self.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel')) + (finish_moves | raw_moves)._action_cancel() + picking_ids = self.picking_ids.filtered(lambda x: x.state not in ('done', 'cancel')) + picking_ids.action_cancel() + + for production, documents in documents_by_production.items(): filtered_documents = {} for (parent, responsible), rendering_context in documents.items(): - if not parent or parent._name == 'stock.picking' and parent.state == 'cancel' or parent == self: + if not parent or parent._name == 'stock.picking' and parent.state == 'cancel' or parent == production: continue filtered_documents[(parent, responsible)] = rendering_context - self._log_manufacture_exception(filtered_documents, cancel=True) + production._log_manufacture_exception(filtered_documents, cancel=True) return True def _get_document_iterate_key(self, move_raw_id): diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py index 3289c9aa525..5c0de7462d4 100644 --- a/addons/mrp/models/stock_move.py +++ b/addons/mrp/models/stock_move.py @@ -134,12 +134,6 @@ class StockMove(models.Model): 'workorder_id': move.workorder_id.id,}) return res - def _action_cancel(self): - if any(move.quantity_done and (move.raw_material_production_id or move.production_id) for move in self): - raise exceptions.UserError(_('You cannot cancel a manufacturing order if you have already consumed material.\ - If you want to cancel this MO, please change the consumed quantities to 0.')) - return super(StockMove, self)._action_cancel() - def _action_confirm(self, merge=True, merge_into=False): moves = self.env['stock.move'] for move in self: diff --git a/addons/mrp/tests/__init__.py b/addons/mrp/tests/__init__.py index e1186006cee..9cc1a1ca874 100644 --- a/addons/mrp/tests/__init__.py +++ b/addons/mrp/tests/__init__.py @@ -2,6 +2,7 @@ from . import test_bom from . import test_byproduct +from . import test_cancel_mo from . import test_order from . import test_stock from . import test_warehouse_multistep_manufacturing diff --git a/addons/mrp/tests/test_cancel_mo.py b/addons/mrp/tests/test_cancel_mo.py new file mode 100644 index 00000000000..629f1c2d824 --- /dev/null +++ b/addons/mrp/tests/test_cancel_mo.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.tests import Form +from datetime import datetime, timedelta + +from odoo.fields import Datetime as Dt +from odoo.exceptions import UserError +from odoo.addons.mrp.tests.common import TestMrpCommon + + +class TestMrpCancelMO(TestMrpCommon): + + def test_cancel_mo_without_routing_1(self): + """ Cancel a Manufacturing Order with no routing, no production. + """ + # Create MO + manufacturing_order = self.generate_mo()[0] + # Do nothing, cancel it + manufacturing_order.action_cancel() + # Check the MO and its moves are cancelled + self.assertEqual(manufacturing_order.state, 'cancel', "MO should be in cancel state.") + self.assertEqual(manufacturing_order.move_raw_ids[0].state, 'cancel', + "Cancelled MO raw moves must be cancelled as well.") + self.assertEqual(manufacturing_order.move_raw_ids[1].state, 'cancel', + "Cancelled MO raw moves must be cancelled as well.") + self.assertEqual(manufacturing_order.move_finished_ids.state, 'cancel', + "Cancelled MO finished move must be cancelled as well.") + + def test_cancel_mo_without_routing_2(self): + """ Cancel a Manufacturing Order with no routing but some productions. + """ + # Create MO + manufacturing_order = self.generate_mo()[0] + # Produce some quantity + produce_form = Form(self.env['mrp.product.produce'].with_context(active_id=manufacturing_order.id)) + produce_form.qty_producing = 2 + produce = produce_form.save() + produce.do_produce() + # Cancel it + manufacturing_order.action_cancel() + # Check it's cancelled + self.assertEqual(manufacturing_order.state, 'cancel', "MO should be in cancel state.") + self.assertEqual(manufacturing_order.move_raw_ids[0].state, 'cancel', + "Cancelled MO raw moves must be cancelled as well.") + self.assertEqual(manufacturing_order.move_raw_ids[1].state, 'cancel', + "Cancelled MO raw moves must be cancelled as well.") + self.assertEqual(manufacturing_order.move_finished_ids.state, 'cancel', + "Cancelled MO finished move must be cancelled as well.") + + def test_cancel_mo_without_routing_3(self): + """ Cancel a Manufacturing Order with no routing but some productions + after post inventory. + """ + # Create MO + manufacturing_order = self.generate_mo()[0] + # Produce some quantity (not all to avoid to done the MO when post inventory) + produce_form = Form(self.env['mrp.product.produce'].with_context(active_id=manufacturing_order.id)) + produce_form.qty_producing = 2 + produce = produce_form.save() + produce.do_produce() + # Post Inventory + manufacturing_order.post_inventory() + # Cancel the MO + manufacturing_order.action_cancel() + # Check MO is marked as done and its SML are done or cancelled + self.assertEqual(manufacturing_order.state, 'done', "MO should be in done state.") + self.assertEqual(manufacturing_order.move_raw_ids[0].state, 'done', + "Due to 'post_inventory', some move raw must stay in done state") + self.assertEqual(manufacturing_order.move_raw_ids[1].state, 'done', + "Due to 'post_inventory', some move raw must stay in done state") + self.assertEqual(manufacturing_order.move_raw_ids[2].state, 'cancel', + "The other move raw are cancelled like their MO.") + self.assertEqual(manufacturing_order.move_raw_ids[3].state, 'cancel', + "The other move raw are cancelled like their MO.") + self.assertEqual(manufacturing_order.move_finished_ids[0].state, 'done', + "Due to 'post_inventory', a move finished must stay in done state") + self.assertEqual(manufacturing_order.move_finished_ids[1].state, 'cancel', + "The other move finished is cancelled like its MO.") + + def test_cancel_mo_with_routing(self): + """ Cancel a Manufacturing Order with routing (so generate a Work Order) + and produce some quantities. When cancelled, the MO must be marked as + done and the WO must be cancelled. + """ + # Create MO + mo_data = self.generate_mo() + manufacturing_order = mo_data[0] + bom = mo_data[1] + bom.routing_id = self.routing_1 + + manufacturing_order.button_plan() + workorder = manufacturing_order.workorder_ids + # Produce some quantity + workorder.button_start() + workorder.qty_producing = 2 + workorder._apply_update_workorder_lines() + workorder.record_production() + # Post Inventory + manufacturing_order.post_inventory() + # Cancel it + manufacturing_order.action_cancel() + # Check MO is done, WO is cancelled and its SML are done or cancelled + self.assertEqual(manufacturing_order.state, 'done', "MO should be in done state.") + self.assertEqual(workorder.state, 'cancel', "WO should be cancelled.") + self.assertEqual(manufacturing_order.move_raw_ids[0].state, 'done', + "Due to 'post_inventory', some move raw must stay in done state") + self.assertEqual(manufacturing_order.move_raw_ids[1].state, 'done', + "Due to 'post_inventory', some move raw must stay in done state") + self.assertEqual(manufacturing_order.move_raw_ids[2].state, 'cancel', + "The other move raw are cancelled like their MO.") + self.assertEqual(manufacturing_order.move_raw_ids[3].state, 'cancel', + "The other move raw are cancelled like their MO.") + self.assertEqual(manufacturing_order.move_finished_ids[0].state, 'done', + "Due to 'post_inventory', a move finished must stay in done state") + self.assertEqual(manufacturing_order.move_finished_ids[1].state, 'cancel', + "The other move finished is cancelled like its MO.") diff --git a/addons/mrp/views/mrp_production_views.xml b/addons/mrp/views/mrp_production_views.xml index 2324f76389d..c7858c2f2ba 100644 --- a/addons/mrp/views/mrp_production_views.xml +++ b/addons/mrp/views/mrp_production_views.xml @@ -41,6 +41,7 @@ <field name="arch" type="xml"> <form string="Manufacturing Orders"> <header> + <field name="confirm_cancel" invisible="1"/> <button name="button_mark_done" attrs="{'invisible': [('state', '!=', 'to_close')]}" string="Mark as Done" type="object" class="oe_highlight"/> <button name="action_confirm" attrs="{'invisible': ['|', ('state', '!=', 'draft'), ('is_locked', '=', False)]}" string="Mark as Todo" type="object" class="oe_highlight"/> <button name="action_assign" attrs="{'invisible': ['|', '|', ('is_locked', '=', False), ('state', 'in', ('draft', 'done', 'cancel')), ('reservation_state', '=', 'assigned')]}" string="Check availability" type="object" class="oe_highlight"/> @@ -49,7 +50,11 @@ <button name="open_produce_product" attrs="{'invisible': ['|', '|', '|', ('state', '=', 'to_close'), ('is_locked', '=', False), ('reservation_state', '!=', 'assigned'), ('routing_id', '!=', False)]}" string="Produce" type="object" class="oe_highlight"/> <button name="open_produce_product" attrs="{'invisible': ['|', '|', '|', ('state', '=', 'to_close'), ('is_locked', '=', False), ('reservation_state', 'not in', ('confirmed', 'waiting')), ('routing_id', '!=', False)]}" string="Produce" type="object"/> <button name="post_inventory" string="Post Inventory" type="object" attrs="{'invisible': [('post_visible', '=', False)]}" groups="base.group_no_one"/> - <button name="action_cancel" type="object" string="Cancel" attrs="{'invisible': ['|', '|', ('is_locked', '=', False), ('state', 'in', ('done','cancel'))]}"/> + <button name="action_cancel" type="object" string="Cancel" + attrs="{'invisible': ['|', '|', ('is_locked', '=', False), ('state', 'in', ('done','cancel')), ('confirm_cancel', '=', True)]}"/> + <button name="action_cancel" type="object" string="Cancel" + attrs="{'invisible': ['|', '|', ('is_locked', '=', False), ('state', 'in', ('done','cancel')), ('confirm_cancel', '=', False)]}" + confirm="Some product moves have already been confirmed, this manufacturing order can't be completely cancelled. Are you still sure you want to process ?"/> <button name="button_scrap" type="object" string="Scrap" attrs="{'invisible': ['|', ('state', 'in', ('cancel', 'draft')), ('is_locked', '=', False)]}"/> <button name="button_unreserve" type="object" string="Unreserve" attrs="{'invisible': [('unreserve_visible', '=', False)]}"/> <field name="state" widget="statusbar" statusbar_visible="draft,confirmed,assigned,done"/> |
