[IMP] mrp: immediate production
This commit introduce the immediate production mechanism. As it works on stock.picking, marking a (some) production(s) as done without consuming anything will pop a wizard allowing the user to transfer all the reserve component quantities as done quantities. Task : 2278147
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
'wizard/stock_warn_insufficient_qty_views.xml',
|
||||
'wizard/mrp_production_backorder.xml',
|
||||
'wizard/mrp_consumption_warning_views.xml',
|
||||
'wizard/mrp_immediate_production_views.xml',
|
||||
'views/mrp_views_menus.xml',
|
||||
'views/stock_move_views.xml',
|
||||
'views/mrp_workorder_views.xml',
|
||||
|
||||
@@ -1054,6 +1054,19 @@ class MrpProduction(models.Model):
|
||||
if self.product_id.tracking == 'serial':
|
||||
self._set_qty_producing()
|
||||
|
||||
def _action_generate_immediate_wizard(self):
|
||||
view = self.env.ref('mrp.view_immediate_production')
|
||||
return {
|
||||
'name': _('Immediate Production?'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'mrp.immediate.production',
|
||||
'views': [(view.id, 'form')],
|
||||
'view_id': view.id,
|
||||
'target': 'new',
|
||||
'context': dict(self.env.context, default_mo_ids=[(4, mo.id) for mo in self]),
|
||||
}
|
||||
|
||||
def action_confirm(self):
|
||||
self._check_company()
|
||||
for production in self:
|
||||
@@ -1416,6 +1429,9 @@ class MrpProduction(models.Model):
|
||||
|
||||
def button_mark_done(self):
|
||||
self._button_mark_done_sanity_checks()
|
||||
|
||||
if not self.env.context.get('button_mark_done_production_ids'):
|
||||
self = self.with_context(button_mark_done_production_ids=self.ids)
|
||||
res = self._pre_button_mark_done()
|
||||
if res is not True:
|
||||
return res
|
||||
@@ -1481,6 +1497,14 @@ class MrpProduction(models.Model):
|
||||
return action
|
||||
|
||||
def _pre_button_mark_done(self):
|
||||
productions_to_immediate = self._check_immediate()
|
||||
if productions_to_immediate:
|
||||
return productions_to_immediate._action_generate_immediate_wizard()
|
||||
|
||||
for production in self:
|
||||
if float_is_zero(production.qty_producing, precision_rounding=production.product_uom_id.rounding):
|
||||
raise UserError(_('The quantity to produce must be positive!'))
|
||||
|
||||
consumption_issues = self._get_consumption_issues()
|
||||
if consumption_issues:
|
||||
return self._action_generate_consumption_wizard(consumption_issues)
|
||||
@@ -1686,3 +1710,15 @@ class MrpProduction(models.Model):
|
||||
duplicates = co_prod_move_lines.filtered(lambda ml: ml.qty_done and ml.lot_id == move_line.lot_id) - move_line
|
||||
if duplicates:
|
||||
raise UserError(message)
|
||||
|
||||
def _check_immediate(self):
|
||||
immediate_productions = self.browse()
|
||||
if self.env.context.get('skip_immediate'):
|
||||
return immediate_productions
|
||||
pd = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
for production in self:
|
||||
if all(float_is_zero(ml.qty_done, precision_digits=pd) for
|
||||
ml in production.move_raw_ids.move_line_ids.filtered(lambda m: m.state not in ('done', 'cancel'))
|
||||
) and float_is_zero(production.qty_producing, precision_digits=pd):
|
||||
immediate_productions |= production
|
||||
return immediate_productions
|
||||
|
||||
@@ -61,3 +61,5 @@ access_mrp_production_backorder,access.mrp.production.backorder,model_mrp_produc
|
||||
access_mrp_production_backorder_line,access.mrp.production.backorder.line,model_mrp_production_backorder_line,mrp.group_mrp_user,1,1,1,0
|
||||
access_mrp_consumption_warning,access.mrp.consumption.warning,model_mrp_consumption_warning,mrp.group_mrp_user,1,1,1,0
|
||||
access_mrp_consumption_warning_line,access.mrp.consumption.warning.line,model_mrp_consumption_warning_line,mrp.group_mrp_user,1,1,1,0
|
||||
access_mrp_immediate_production,access.mrp.immediate.production,model_mrp_immediate_production,mrp.group_mrp_user,1,1,1,0
|
||||
access_mrp_immediate_production_line,access.mrp.immediate.production.line,model_mrp_immediate_production_line,mrp.group_mrp_user,1,1,1,0
|
||||
|
||||
|
@@ -1155,3 +1155,160 @@ class TestMrpOrder(TestMrpCommon):
|
||||
|
||||
# Check Mo is created or not
|
||||
self.assertTrue(mo, "Mo is created")
|
||||
|
||||
def test_immediate_validate_1(self):
|
||||
""" In a production with a single available move raw, clicking on mark as done without filling any
|
||||
quantities should open a wizard asking to process all the reservation (so, the whole move).
|
||||
"""
|
||||
mo, bom, p_final, p1, p2 = self.generate_mo(qty_final=1, qty_base_1=1, qty_base_2=1)
|
||||
self.env['stock.quant']._update_available_quantity(p1, self.stock_location_components, 5.0)
|
||||
self.env['stock.quant']._update_available_quantity(p2, self.stock_location_components, 5.0)
|
||||
mo.action_assign()
|
||||
res_dict = mo.button_mark_done()
|
||||
self.assertEqual(res_dict.get('res_model'), 'mrp.immediate.production')
|
||||
wizard = Form(self.env[res_dict['res_model']].with_context(res_dict['context'])).save()
|
||||
wizard.process()
|
||||
self.assertEqual(mo.move_raw_ids.mapped('state'), ['done', 'done'])
|
||||
self.assertEqual(mo.move_raw_ids.mapped('quantity_done'), [1, 1])
|
||||
self.assertEqual(mo.move_finished_ids.state, 'done')
|
||||
self.assertEqual(mo.move_finished_ids.quantity_done, 1)
|
||||
|
||||
def test_immediate_validate_2(self):
|
||||
""" In a production with a single available move raw, clicking on mark as done after filling quantity
|
||||
for a stock move only will trigger an error as qty_producing is left to 0."""
|
||||
mo, bom, p_final, p1, p2 = self.generate_mo(qty_final=1, qty_base_1=1, qty_base_2=1)
|
||||
self.env['stock.quant']._update_available_quantity(p1, self.stock_location_components, 5.0)
|
||||
self.env['stock.quant']._update_available_quantity(p2, self.stock_location_components, 5.0)
|
||||
mo.action_assign()
|
||||
details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.new() as ml:
|
||||
ml.qty_done = 1
|
||||
details_operation_form.save()
|
||||
with self.assertRaises(UserError):
|
||||
res_dict = mo.button_mark_done()
|
||||
|
||||
def test_immediate_validate_3(self):
|
||||
""" In a production with a serial number tracked product. Check that the immediate production only creates
|
||||
one unit of finished product. Test with reservation."""
|
||||
mo, bom, p_final, p1, p2 = self.generate_mo(tracking_final='serial', qty_final=2, qty_base_1=1, qty_base_2=1)
|
||||
self.env['stock.quant']._update_available_quantity(p1, self.stock_location_components, 5.0)
|
||||
self.env['stock.quant']._update_available_quantity(p2, self.stock_location_components, 5.0)
|
||||
mo.action_assign()
|
||||
action = mo.button_mark_done()
|
||||
self.assertEqual(action.get('res_model'), 'mrp.immediate.production')
|
||||
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
|
||||
action = wizard.process()
|
||||
self.assertEqual(action.get('res_model'), 'mrp.production.backorder')
|
||||
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
|
||||
action = wizard.action_backorder()
|
||||
self.assertEqual(mo.qty_producing, 1)
|
||||
self.assertEqual(mo.move_raw_ids.mapped('quantity_done'), [1, 1])
|
||||
self.assertEqual(len(mo.procurement_group_id.mrp_production_ids), 2)
|
||||
mo_backorder = mo.procurement_group_id.mrp_production_ids[-1]
|
||||
self.assertEqual(mo_backorder.product_qty, 1)
|
||||
self.assertEqual(mo_backorder.move_raw_ids.mapped('product_uom_qty'), [1, 1])
|
||||
|
||||
def test_immediate_validate_4(self):
|
||||
""" In a production with a serial number tracked product. Check that the immediate production only creates
|
||||
one unit of finished product. Test without reservation."""
|
||||
mo, bom, p_final, p1, p2 = self.generate_mo(tracking_final='serial', qty_final=2, qty_base_1=1, qty_base_2=1)
|
||||
self.env['stock.quant']._update_available_quantity(p1, self.stock_location_components, 5.0)
|
||||
self.env['stock.quant']._update_available_quantity(p2, self.stock_location_components, 5.0)
|
||||
action = mo.button_mark_done()
|
||||
self.assertEqual(action.get('res_model'), 'mrp.immediate.production')
|
||||
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
|
||||
action = wizard.process()
|
||||
self.assertEqual(action.get('res_model'), 'mrp.production.backorder')
|
||||
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
|
||||
action = wizard.action_backorder()
|
||||
self.assertEqual(mo.qty_producing, 1)
|
||||
self.assertEqual(mo.move_raw_ids.mapped('quantity_done'), [1, 1])
|
||||
self.assertEqual(len(mo.procurement_group_id.mrp_production_ids), 2)
|
||||
mo_backorder = mo.procurement_group_id.mrp_production_ids[-1]
|
||||
self.assertEqual(mo_backorder.product_qty, 1)
|
||||
self.assertEqual(mo_backorder.move_raw_ids.mapped('product_uom_qty'), [1, 1])
|
||||
|
||||
def test_immediate_validate_5(self):
|
||||
"""Validate three productions at once."""
|
||||
mo1, bom, p_final, p1, p2 = self.generate_mo(qty_final=1, qty_base_1=1, qty_base_2=1)
|
||||
self.env['stock.quant']._update_available_quantity(p1, self.stock_location_components, 5.0)
|
||||
self.env['stock.quant']._update_available_quantity(p2, self.stock_location_components, 5.0)
|
||||
mo1.action_assign()
|
||||
mo2_form = Form(self.env['mrp.production'])
|
||||
mo2_form.product_id = p_final
|
||||
mo2_form.bom_id = bom
|
||||
mo2_form.product_qty = 1
|
||||
mo2 = mo2_form.save()
|
||||
mo2.action_confirm()
|
||||
mo2.action_assign()
|
||||
mo3_form = Form(self.env['mrp.production'])
|
||||
mo3_form.product_id = p_final
|
||||
mo3_form.bom_id = bom
|
||||
mo3_form.product_qty = 1
|
||||
mo3 = mo3_form.save()
|
||||
mo3.action_confirm()
|
||||
mo3.action_assign()
|
||||
mos = mo1 | mo2 | mo3
|
||||
res_dict = mos.button_mark_done()
|
||||
self.assertEqual(res_dict.get('res_model'), 'mrp.immediate.production')
|
||||
wizard = Form(self.env[res_dict['res_model']].with_context(res_dict['context'])).save()
|
||||
wizard.process()
|
||||
self.assertEqual(mos.move_raw_ids.mapped('state'), ['done'] * 6)
|
||||
self.assertEqual(mos.move_raw_ids.mapped('quantity_done'), [1] * 6)
|
||||
self.assertEqual(mos.move_finished_ids.mapped('state'), ['done'] * 3)
|
||||
self.assertEqual(mos.move_finished_ids.mapped('quantity_done'), [1] * 3)
|
||||
|
||||
def test_immediate_validate_6(self):
|
||||
"""In a production for a tracked product, clicking on mark as done without filling any quantities should
|
||||
pop up the immediate transfer wizard. Processing should choose a new lot for the finished product. """
|
||||
mo, bom, p_final, p1, p2 = self.generate_mo(qty_final=1, qty_base_1=1, qty_base_2=1, tracking_final='lot')
|
||||
self.env['stock.quant']._update_available_quantity(p1, self.stock_location_components, 5.0)
|
||||
self.env['stock.quant']._update_available_quantity(p2, self.stock_location_components, 5.0)
|
||||
mo.action_assign()
|
||||
res_dict = mo.button_mark_done()
|
||||
self.assertEqual(res_dict.get('res_model'), 'mrp.immediate.production')
|
||||
wizard = Form(self.env[res_dict['res_model']].with_context(res_dict['context'])).save()
|
||||
wizard.process()
|
||||
self.assertEqual(mo.move_raw_ids.mapped('state'), ['done'] * 2)
|
||||
self.assertEqual(mo.move_raw_ids.mapped('quantity_done'), [1] * 2)
|
||||
self.assertEqual(mo.move_finished_ids.state, 'done')
|
||||
self.assertEqual(mo.move_finished_ids.quantity_done, 1)
|
||||
self.assertTrue(mo.move_finished_ids.move_line_ids.lot_id != False)
|
||||
|
||||
def test_immediate_validate_uom(self):
|
||||
"""In a production with a different uom than the finished product one, the
|
||||
immediate production wizard should fill the correct quantities. """
|
||||
p_final = self.env['product.product'].create({
|
||||
'name': 'final',
|
||||
'type': 'product',
|
||||
})
|
||||
component = self.env['product.product'].create({
|
||||
'name': 'component',
|
||||
'type': 'product',
|
||||
})
|
||||
bom = self.env['mrp.bom'].create({
|
||||
'product_id': p_final.id,
|
||||
'product_tmpl_id': p_final.product_tmpl_id.id,
|
||||
'product_uom_id': self.uom_unit.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'consumption': 'flexible',
|
||||
'bom_line_ids': [(0, 0, {'product_id': component.id, 'product_qty': 1})]
|
||||
})
|
||||
self.env['stock.quant']._update_available_quantity(component, self.stock_location_components, 25.0)
|
||||
mo_form = Form(self.env['mrp.production'])
|
||||
mo_form.bom_id = bom
|
||||
mo_form.product_uom_id = self.uom_dozen
|
||||
mo_form.product_qty = 1
|
||||
mo = mo_form.save()
|
||||
mo.action_confirm()
|
||||
mo.action_assign()
|
||||
res_dict = mo.button_mark_done()
|
||||
self.assertEqual(res_dict.get('res_model'), 'mrp.immediate.production')
|
||||
wizard = Form(self.env[res_dict['res_model']].with_context(res_dict['context'])).save()
|
||||
wizard.process()
|
||||
self.assertEqual(mo.move_raw_ids.state, 'done')
|
||||
self.assertEqual(mo.move_raw_ids.quantity_done, 12)
|
||||
self.assertEqual(mo.move_finished_ids.state, 'done')
|
||||
self.assertEqual(mo.move_finished_ids.quantity_done, 1)
|
||||
self.assertEqual(component.qty_available, 13)
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
if records:
|
||||
res = records.filtered(lambda mo: mo.state in {'to_close', 'progress'}).button_mark_done()
|
||||
res = records.filtered(lambda mo: mo.state in {'confirmed', 'to_close', 'progress'}).button_mark_done()
|
||||
if res is not True:
|
||||
action = res
|
||||
</field>
|
||||
@@ -69,8 +69,13 @@
|
||||
<header>
|
||||
<field name="confirm_cancel" invisible="1"/>
|
||||
<field name="show_lock" invisible="1"/>
|
||||
<button name="button_mark_done" attrs="{'invisible': ['|', ('state', '!=', 'progress'), ('qty_producing', '=', 0)]}" string="Validate" type="object" class="oe_highlight"/>
|
||||
<button name="button_mark_done" attrs="{'invisible': [('state', '!=', 'to_close')]}" string="Mark as Done" type="object" class="oe_highlight"/>
|
||||
<button name="button_mark_done" attrs="{'invisible': ['|', ('state', 'in', ('draft', 'cancel', 'done', 'to_close')), ('qty_producing', '=', 0)]}" string="Validate" type="object" class="oe_highlight"/>
|
||||
<button name="button_mark_done" attrs="{'invisible': [
|
||||
'&',
|
||||
'|',
|
||||
('state', 'not in', ('confirmed', 'progress')),
|
||||
('qty_producing', '!=', 0),
|
||||
('state', '!=', 'to_close')]}" string="Mark as Done" type="object" class="oe_highlight"/>
|
||||
<button name="action_confirm" attrs="{'invisible': [('state', '!=', 'draft')]}" string="Confirm" type="object" class="oe_highlight"/>
|
||||
<button name="action_assign" attrs="{'invisible': ['|', ('state', 'in', ('draft', 'done', 'cancel')), ('reserve_visible', '=', False)]}" string="Check availability" type="object"/>
|
||||
<button name="button_plan" attrs="{'invisible': ['|', '|', ('state', 'not in', ('confirmed', 'progress')), ('workorder_ids', '=', []), ('is_planned', '=', True)]}" type="object" string="Plan" class="oe_highlight"/>
|
||||
|
||||
@@ -5,3 +5,4 @@ from . import change_production_qty
|
||||
from . import stock_warn_insufficient_qty
|
||||
from . import mrp_production_backorder
|
||||
from . import mrp_consumption_warning
|
||||
from . import mrp_immediate_production
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import float_compare
|
||||
|
||||
|
||||
class MrpImmediateProductionLine(models.TransientModel):
|
||||
_name = 'mrp.immediate.production.line'
|
||||
_description = 'Immediate Production Line'
|
||||
|
||||
immediate_production_id = fields.Many2one('mrp.immediate.production', 'Immediate Production', required=True)
|
||||
production_id = fields.Many2one('mrp.production', 'Production', required=True)
|
||||
to_immediate = fields.Boolean('To Process')
|
||||
|
||||
|
||||
class MrpImmediateProduction(models.TransientModel):
|
||||
_name = 'mrp.immediate.production'
|
||||
_description = 'Immediate Production'
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
res = super().default_get(fields)
|
||||
if 'immediate_production_line_ids' in fields:
|
||||
if self.env.context.get('default_mo_ids'):
|
||||
res['mo_ids'] = self.env.context['default_mo_ids']
|
||||
res['immediate_production_line_ids'] = [(0, 0, {'to_immediate': True, 'production_id': mo_id[1]}) for mo_id in res['mo_ids']]
|
||||
return res
|
||||
|
||||
mo_ids = fields.Many2many('mrp.production', 'mrp_production_production_rel')
|
||||
show_productions = fields.Boolean(compute='_compute_show_production')
|
||||
immediate_production_line_ids = fields.One2many(
|
||||
'mrp.immediate.production.line',
|
||||
'immediate_production_id',
|
||||
string="Immediate Production Lines")
|
||||
|
||||
@api.depends('immediate_production_line_ids')
|
||||
def _compute_show_production(self):
|
||||
for wizard in self:
|
||||
wizard.show_productions = len(wizard.immediate_production_line_ids.production_id) > 1
|
||||
|
||||
def process(self):
|
||||
productions_to_do = self.env['mrp.production']
|
||||
productions_not_to_do = self.env['mrp.production']
|
||||
for line in self.immediate_production_line_ids:
|
||||
if line.to_immediate is True:
|
||||
productions_to_do |= line.production_id
|
||||
else:
|
||||
productions_not_to_do |= line.production_id
|
||||
|
||||
for production in productions_to_do:
|
||||
error_msg = ""
|
||||
if production.product_tracking in ('lot', 'serial') and not production.lot_producing_id:
|
||||
production.action_generate_serial()
|
||||
if production.product_tracking == 'serial' and float_compare(production.qty_producing, 1, precision_rounding=production.product_uom_id.rounding) == 1:
|
||||
production.qty_producing = 1
|
||||
else:
|
||||
production.qty_producing = production.product_qty - production.qty_produced
|
||||
production._set_qty_producing()
|
||||
for move in production.move_raw_ids.filtered(lambda m: m.state not in ['done', 'cancel']):
|
||||
rounding = move.product_uom.rounding
|
||||
for move_line in move.move_line_ids:
|
||||
if move_line.product_uom_qty:
|
||||
move_line.qty_done = min(move_line.product_uom_qty, move_line.move_id.should_consume_qty)
|
||||
if float_compare(move.quantity_done, move.should_consume_qty, precision_rounding=rounding) >= 0:
|
||||
break
|
||||
if float_compare(move.product_uom_qty, move.quantity_done, precision_rounding=move.product_uom.rounding) == 1:
|
||||
if move.has_tracking in ('serial', 'lot'):
|
||||
error_msg += "\n - %s" % move.product_id.display_name
|
||||
|
||||
if error_msg:
|
||||
error_msg = _('You need to supply Lot/Serial Number for products:') + error_msg
|
||||
raise UserError(error_msg)
|
||||
|
||||
productions_to_validate = self.env.context.get('button_mark_done_production_ids')
|
||||
if productions_to_validate:
|
||||
productions_to_validate = self.env['mrp.production'].browse(productions_to_validate)
|
||||
productions_to_validate = productions_to_validate - productions_not_to_do
|
||||
return productions_to_validate.with_context(skip_immediate=True).button_mark_done()
|
||||
return True
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="view_immediate_production" model="ir.ui.view">
|
||||
<field name="name">mrp.immediate.production.view.form</field>
|
||||
<field name="model">mrp.immediate.production</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Immediate production?">
|
||||
<group>
|
||||
<p>You have not recorded <i>produced</i> quantities yet, by clicking on <i>apply</i> Odoo will produce all the finished products and consume all components.</p>
|
||||
</group>
|
||||
|
||||
<field name="show_productions" invisible="1"/>
|
||||
<field name="immediate_production_line_ids" nolabel="1" attrs="{'invisible': [('show_productions', '=', False)]}">>
|
||||
<tree create="0" delete="0" editable="top">
|
||||
<field name="production_id"/>
|
||||
<field name="to_immediate" widget="boolean_toggle"/>
|
||||
</tree>
|
||||
</field>
|
||||
|
||||
<footer>
|
||||
<button name="process" string="Apply" type="object" class="btn-primary"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user