[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:
co-authored by
Arnold Moyaux
parent
28c2e642cb
commit
c76fcd87c5
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.")
|
||||
@@ -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"/>
|
||||
|
||||
Reference in New Issue
Block a user