diff options
| author | William Henrotin <whe@odoo.com> | 2020-07-02 14:13:51 +0000 |
|---|---|---|
| committer | William Henrotin <whe@odoo.com> | 2020-08-10 09:40:24 +0000 |
| commit | a6cd4d8c8c73c41e0de6786ba10c0147d9f2a7cb (patch) | |
| tree | 3cd1cd578fbcd0fb3ade837d238c9a1670dbb2c4 /addons/mrp | |
| parent | 53356a5dcfefe11c185ae1443ca33949264a997e (diff) | |
[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
Diffstat (limited to 'addons/mrp')
| -rw-r--r-- | addons/mrp/__manifest__.py | 1 | ||||
| -rw-r--r-- | addons/mrp/models/mrp_production.py | 36 | ||||
| -rw-r--r-- | addons/mrp/security/ir.model.access.csv | 2 | ||||
| -rw-r--r-- | addons/mrp/tests/test_order.py | 157 | ||||
| -rw-r--r-- | addons/mrp/views/mrp_production_views.xml | 11 | ||||
| -rw-r--r-- | addons/mrp/wizard/__init__.py | 1 | ||||
| -rw-r--r-- | addons/mrp/wizard/mrp_immediate_production.py | 82 | ||||
| -rw-r--r-- | addons/mrp/wizard/mrp_immediate_production_views.xml | 27 |
8 files changed, 314 insertions, 3 deletions
diff --git a/addons/mrp/__manifest__.py b/addons/mrp/__manifest__.py index 786cd178e28..11c2573229b 100644 --- a/addons/mrp/__manifest__.py +++ b/addons/mrp/__manifest__.py @@ -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', diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index 34e86616941..4b829bae1af 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -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 diff --git a/addons/mrp/security/ir.model.access.csv b/addons/mrp/security/ir.model.access.csv index 73d4f9c3888..a7bfdbff69d 100644 --- a/addons/mrp/security/ir.model.access.csv +++ b/addons/mrp/security/ir.model.access.csv @@ -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 diff --git a/addons/mrp/tests/test_order.py b/addons/mrp/tests/test_order.py index 5906b45bb73..0b815e2e9f9 100644 --- a/addons/mrp/tests/test_order.py +++ b/addons/mrp/tests/test_order.py @@ -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) diff --git a/addons/mrp/views/mrp_production_views.xml b/addons/mrp/views/mrp_production_views.xml index 670ae733ab3..e13c8503943 100644 --- a/addons/mrp/views/mrp_production_views.xml +++ b/addons/mrp/views/mrp_production_views.xml @@ -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"/> diff --git a/addons/mrp/wizard/__init__.py b/addons/mrp/wizard/__init__.py index 466142a28a5..d90ee8f5717 100644 --- a/addons/mrp/wizard/__init__.py +++ b/addons/mrp/wizard/__init__.py @@ -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 diff --git a/addons/mrp/wizard/mrp_immediate_production.py b/addons/mrp/wizard/mrp_immediate_production.py new file mode 100644 index 00000000000..d1e1b27d9f7 --- /dev/null +++ b/addons/mrp/wizard/mrp_immediate_production.py @@ -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 + diff --git a/addons/mrp/wizard/mrp_immediate_production_views.xml b/addons/mrp/wizard/mrp_immediate_production_views.xml new file mode 100644 index 00000000000..9c98f67c840 --- /dev/null +++ b/addons/mrp/wizard/mrp_immediate_production_views.xml @@ -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> |
