summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsvs-odoo <svs@odoo.com>2019-05-27 06:26:56 +0000
committersvs-odoo <svs@odoo.com>2019-07-31 12:37:38 +0000
commitc76fcd87c513b74cebea3a318c33f06fb03cafa9 (patch)
tree66ffde4646b3c73ccc7c2f47d3c063afd1142bc7
parent28c2e642cbf02d65e0f50ec45c590cd0eba0dcfb (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.py61
-rw-r--r--addons/mrp/models/stock_move.py6
-rw-r--r--addons/mrp/tests/__init__.py1
-rw-r--r--addons/mrp/tests/test_cancel_mo.py117
-rw-r--r--addons/mrp/views/mrp_production_views.xml7
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"/>