[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>
This commit is contained in:
svs-odoo
2019-07-31 12:37:38 +00:00
co-authored by Arnold Moyaux
parent 28c2e642cb
commit c76fcd87c5
5 changed files with 166 additions and 24 deletions
+42 -17
View File
@@ -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()
documents[key] += [value]
if documents:
documents_by_production[production] = documents
if 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):
-6
View File
@@ -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:
+1
View File
@@ -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
+117
View File
@@ -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.")
+6 -1
View File
@@ -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"/>