From 3751379f1e9a4c215fb6eb898b4ccc67659b9ace Mon Sep 17 00:00:00 2001 From: stephanchrst Date: Tue, 10 May 2022 21:51:50 +0700 Subject: initial commit 2 --- addons/mrp/wizard/__init__.py | 8 ++ addons/mrp/wizard/change_production_qty.py | 103 +++++++++++++++++++++ addons/mrp/wizard/change_production_qty_views.xml | 33 +++++++ addons/mrp/wizard/mrp_consumption_warning.py | 60 ++++++++++++ .../mrp/wizard/mrp_consumption_warning_views.xml | 57 ++++++++++++ addons/mrp/wizard/mrp_immediate_production.py | 82 ++++++++++++++++ .../mrp/wizard/mrp_immediate_production_views.xml | 27 ++++++ addons/mrp/wizard/mrp_production_backorder.py | 38 ++++++++ addons/mrp/wizard/mrp_production_backorder.xml | 44 +++++++++ addons/mrp/wizard/mrp_workcenter_block_view.xml | 41 ++++++++ addons/mrp/wizard/stock_warn_insufficient_qty.py | 19 ++++ .../wizard/stock_warn_insufficient_qty_views.xml | 13 +++ 12 files changed, 525 insertions(+) create mode 100644 addons/mrp/wizard/__init__.py create mode 100644 addons/mrp/wizard/change_production_qty.py create mode 100644 addons/mrp/wizard/change_production_qty_views.xml create mode 100644 addons/mrp/wizard/mrp_consumption_warning.py create mode 100644 addons/mrp/wizard/mrp_consumption_warning_views.xml create mode 100644 addons/mrp/wizard/mrp_immediate_production.py create mode 100644 addons/mrp/wizard/mrp_immediate_production_views.xml create mode 100644 addons/mrp/wizard/mrp_production_backorder.py create mode 100644 addons/mrp/wizard/mrp_production_backorder.xml create mode 100644 addons/mrp/wizard/mrp_workcenter_block_view.xml create mode 100644 addons/mrp/wizard/stock_warn_insufficient_qty.py create mode 100644 addons/mrp/wizard/stock_warn_insufficient_qty_views.xml (limited to 'addons/mrp/wizard') diff --git a/addons/mrp/wizard/__init__.py b/addons/mrp/wizard/__init__.py new file mode 100644 index 00000000..d90ee8f5 --- /dev/null +++ b/addons/mrp/wizard/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +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/change_production_qty.py b/addons/mrp/wizard/change_production_qty.py new file mode 100644 index 00000000..4a58bc7b --- /dev/null +++ b/addons/mrp/wizard/change_production_qty.py @@ -0,0 +1,103 @@ +# -*- 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_is_zero, float_round + + +class ChangeProductionQty(models.TransientModel): + _name = 'change.production.qty' + _description = 'Change Production Qty' + + mo_id = fields.Many2one('mrp.production', 'Manufacturing Order', + required=True, ondelete='cascade') + product_qty = fields.Float( + 'Quantity To Produce', + digits='Product Unit of Measure', required=True) + + @api.model + def default_get(self, fields): + res = super(ChangeProductionQty, self).default_get(fields) + if 'mo_id' in fields and not res.get('mo_id') and self._context.get('active_model') == 'mrp.production' and self._context.get('active_id'): + res['mo_id'] = self._context['active_id'] + if 'product_qty' in fields and not res.get('product_qty') and res.get('mo_id'): + res['product_qty'] = self.env['mrp.production'].browse(res['mo_id']).product_qty + return res + + @api.model + def _update_finished_moves(self, production, new_qty, old_qty): + """ Update finished product and its byproducts. This method only update + the finished moves not done or cancel and just increase or decrease + their quantity according the unit_ratio. It does not use the BoM, BoM + modification during production would not be taken into consideration. + """ + modification = {} + for move in production.move_finished_ids: + if move.state in ('done', 'cancel'): + continue + qty = (new_qty - old_qty) * move.unit_factor + modification[move] = (move.product_uom_qty + qty, move.product_uom_qty) + move.write({'product_uom_qty': move.product_uom_qty + qty}) + return modification + + def change_prod_qty(self): + precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') + for wizard in self: + production = wizard.mo_id + produced = sum(production.move_finished_ids.filtered(lambda m: m.product_id == production.product_id).mapped('quantity_done')) + if wizard.product_qty < produced: + format_qty = '%.{precision}f'.format(precision=precision) + raise UserError(_( + "You have already processed %(quantity)s. Please input a quantity higher than %(minimum)s ", + quantity=format_qty % produced, + minimum=format_qty % produced + )) + old_production_qty = production.product_qty + new_production_qty = wizard.product_qty + done_moves = production.move_finished_ids.filtered(lambda x: x.state == 'done' and x.product_id == production.product_id) + qty_produced = production.product_id.uom_id._compute_quantity(sum(done_moves.mapped('product_qty')), production.product_uom_id) + + factor = (new_production_qty - qty_produced) / (old_production_qty - qty_produced) + update_info = production._update_raw_moves(factor) + documents = {} + for move, old_qty, new_qty in update_info: + iterate_key = production._get_document_iterate_key(move) + if iterate_key: + document = self.env['stock.picking']._log_activity_get_documents({move: (new_qty, old_qty)}, iterate_key, 'UP') + for key, value in document.items(): + if documents.get(key): + documents[key] += [value] + else: + documents[key] = [value] + production._log_manufacture_exception(documents) + finished_moves_modification = self._update_finished_moves(production, new_production_qty - qty_produced, old_production_qty - qty_produced) + if finished_moves_modification: + production._log_downside_manufactured_quantity(finished_moves_modification) + production.write({'product_qty': new_production_qty}) + + for wo in production.workorder_ids: + operation = wo.operation_id + wo.duration_expected = wo._get_duration_expected(ratio=new_production_qty / old_production_qty) + quantity = wo.qty_production - wo.qty_produced + if production.product_id.tracking == 'serial': + quantity = 1.0 if not float_is_zero(quantity, precision_digits=precision) else 0.0 + else: + quantity = quantity if (quantity > 0 and not float_is_zero(quantity, precision_digits=precision)) else 0 + wo._update_qty_producing(quantity) + if wo.qty_produced < wo.qty_production and wo.state == 'done': + wo.state = 'progress' + if wo.qty_produced == wo.qty_production and wo.state == 'progress': + wo.state = 'done' + if wo.next_work_order_id.state == 'pending': + wo.next_work_order_id.state = 'ready' + # assign moves; last operation receive all unassigned moves + # TODO: following could be put in a function as it is similar as code in _workorders_create + # TODO: only needed when creating new moves + moves_raw = production.move_raw_ids.filtered(lambda move: move.operation_id == operation and move.state not in ('done', 'cancel')) + if wo == production.workorder_ids[-1]: + moves_raw |= production.move_raw_ids.filtered(lambda move: not move.operation_id) + moves_finished = production.move_finished_ids.filtered(lambda move: move.operation_id == operation) #TODO: code does nothing, unless maybe by_products? + moves_raw.mapped('move_line_ids').write({'workorder_id': wo.id}) + (moves_finished + moves_raw).write({'workorder_id': wo.id}) + return {} diff --git a/addons/mrp/wizard/change_production_qty_views.xml b/addons/mrp/wizard/change_production_qty_views.xml new file mode 100644 index 00000000..8d5004d7 --- /dev/null +++ b/addons/mrp/wizard/change_production_qty_views.xml @@ -0,0 +1,33 @@ + + + + + + + Change Quantity To Produce + change.production.qty + +
+ + + + +
+
+
+
+
+ + + Change Quantity To Produce + ir.actions.act_window + change.production.qty + form + new + + +
+
diff --git a/addons/mrp/wizard/mrp_consumption_warning.py b/addons/mrp/wizard/mrp_consumption_warning.py new file mode 100644 index 00000000..bae87759 --- /dev/null +++ b/addons/mrp/wizard/mrp_consumption_warning.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models, api + + +class MrpConsumptionWarning(models.TransientModel): + _name = 'mrp.consumption.warning' + _description = "Wizard in case of consumption in warning/strict and more component has been used for a MO (related to the bom)" + + mrp_production_ids = fields.Many2many('mrp.production') + mrp_production_count = fields.Integer(compute="_compute_mrp_production_count") + + consumption = fields.Selection([ + ('flexible', 'Allowed'), + ('warning', 'Allowed with warning'), + ('strict', 'Blocked')], compute="_compute_consumption") + mrp_consumption_warning_line_ids = fields.One2many('mrp.consumption.warning.line', 'mrp_consumption_warning_id') + + @api.depends("mrp_production_ids") + def _compute_mrp_production_count(self): + for wizard in self: + wizard.mrp_production_count = len(wizard.mrp_production_ids) + + @api.depends("mrp_consumption_warning_line_ids.consumption") + def _compute_consumption(self): + for wizard in self: + consumption_map = set(wizard.mrp_consumption_warning_line_ids.mapped("consumption")) + wizard.consumption = "strict" in consumption_map and "strict" or "warning" in consumption_map and "warning" or "flexible" + + def action_confirm(self): + action_from_do_finish = False + if self.env.context.get('from_workorder'): + if self.env.context.get('active_model') == 'mrp.workorder': + action_from_do_finish = self.env['mrp.workorder'].browse(self.env.context.get('active_id')).do_finish() + action_from_mark_done = self.mrp_production_ids.with_context(skip_consumption=True).button_mark_done() + return action_from_do_finish or action_from_mark_done + + def action_cancel(self): + if self.env.context.get('from_workorder') and len(self.mrp_production_ids) == 1: + return { + 'type': 'ir.actions.act_window', + 'res_model': 'mrp.production', + 'views': [[self.env.ref('mrp.mrp_production_form_view').id, 'form']], + 'res_id': self.mrp_production_ids.id, + 'target': 'main', + } + +class MrpConsumptionWarningLine(models.TransientModel): + _name = 'mrp.consumption.warning.line' + _description = "Line of issue consumption" + + mrp_consumption_warning_id = fields.Many2one('mrp.consumption.warning', "Parent Wizard", readonly=True, required=True, ondelete="cascade") + mrp_production_id = fields.Many2one('mrp.production', "Manufacturing Order", readonly=True, required=True, ondelete="cascade") + consumption = fields.Selection(related="mrp_production_id.consumption") + + product_id = fields.Many2one('product.product', "Product", readonly=True, required=True) + product_uom_id = fields.Many2one('uom.uom', "Unit of Measure", related="product_id.uom_id", readonly=True) + product_consumed_qty_uom = fields.Float("Consumed", readonly=True) + product_expected_qty_uom = fields.Float("To Consume", readonly=True) diff --git a/addons/mrp/wizard/mrp_consumption_warning_views.xml b/addons/mrp/wizard/mrp_consumption_warning_views.xml new file mode 100644 index 00000000..a09d66db --- /dev/null +++ b/addons/mrp/wizard/mrp_consumption_warning_views.xml @@ -0,0 +1,57 @@ + + + + + + + Consumption Warning + mrp.consumption.warning + +
+ + + +
+ You consumed a different quantity than expected for the following products. + + Please confirm it has been done on purpose. + + + Please review your component consumption or ask a manager to validate + this manufacturing order + these manufacturing orders. + +
+ + + + + + + + + + +
+
+ +
+
+ + + Consumption Warning + ir.actions.act_window + mrp.consumption.warning + form + new + + +
+
diff --git a/addons/mrp/wizard/mrp_immediate_production.py b/addons/mrp/wizard/mrp_immediate_production.py new file mode 100644 index 00000000..d1e1b27d --- /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 00000000..9c98f67c --- /dev/null +++ b/addons/mrp/wizard/mrp_immediate_production_views.xml @@ -0,0 +1,27 @@ + + + + mrp.immediate.production.view.form + mrp.immediate.production + +
+ +

You have not recorded produced quantities yet, by clicking on apply Odoo will produce all the finished products and consume all components.

+
+ + + > + + + + + + +
+
+ +
+
+
diff --git a/addons/mrp/wizard/mrp_production_backorder.py b/addons/mrp/wizard/mrp_production_backorder.py new file mode 100644 index 00000000..681b3fff --- /dev/null +++ b/addons/mrp/wizard/mrp_production_backorder.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class MrpProductionBackorderLine(models.TransientModel): + _name = 'mrp.production.backorder.line' + _description = "Backorder Confirmation Line" + + mrp_production_backorder_id = fields.Many2one('mrp.production.backorder', 'MO Backorder', required=True, ondelete="cascade") + mrp_production_id = fields.Many2one('mrp.production', 'Manufacturing Order', required=True, ondelete="cascade", readonly=True) + to_backorder = fields.Boolean('To Backorder') + + +class MrpProductionBackorder(models.TransientModel): + _name = 'mrp.production.backorder' + _description = "Wizard to mark as done or create back order" + + mrp_production_ids = fields.Many2many('mrp.production') + + mrp_production_backorder_line_ids = fields.One2many( + 'mrp.production.backorder.line', + 'mrp_production_backorder_id', + string="Backorder Confirmation Lines") + show_backorder_lines = fields.Boolean("Show backorder lines", compute="_compute_show_backorder_lines") + + @api.depends('mrp_production_backorder_line_ids') + def _compute_show_backorder_lines(self): + for wizard in self: + wizard.show_backorder_lines = len(wizard.mrp_production_backorder_line_ids) > 1 + + def action_close_mo(self): + return self.mrp_production_ids.with_context(skip_backorder=True).button_mark_done() + + def action_backorder(self): + mo_ids_to_backorder = self.mrp_production_backorder_line_ids.filtered(lambda l: l.to_backorder).mrp_production_id.ids + return self.mrp_production_ids.with_context(skip_backorder=True, mo_ids_to_backorder=mo_ids_to_backorder).button_mark_done() diff --git a/addons/mrp/wizard/mrp_production_backorder.xml b/addons/mrp/wizard/mrp_production_backorder.xml new file mode 100644 index 00000000..7b116eb3 --- /dev/null +++ b/addons/mrp/wizard/mrp_production_backorder.xml @@ -0,0 +1,44 @@ + + + + + + + Create Backorder + mrp.production.backorder + +
+ +

+ Create a backorder if you expect to process the remaining products later. Do not create a backorder if you will not process the remaining products. +

+
+ + + + + + + +
+
+ +
+
+ + + You produced less than initial demand + ir.actions.act_window + mrp.production.backorder + form + new + + +
+
diff --git a/addons/mrp/wizard/mrp_workcenter_block_view.xml b/addons/mrp/wizard/mrp_workcenter_block_view.xml new file mode 100644 index 00000000..8104b8ea --- /dev/null +++ b/addons/mrp/wizard/mrp_workcenter_block_view.xml @@ -0,0 +1,41 @@ + + + + + mrp.workcenter.productivity.form + mrp.workcenter.productivity + +
+ + + + + + +
+
+
+
+
+ + + Block Workcenter + ir.actions.act_window + mrp.workcenter.productivity + form + {'default_workcenter_id': active_id} + + new + + + + Block Workcenter + ir.actions.act_window + mrp.workcenter.productivity + form + + new + +
diff --git a/addons/mrp/wizard/stock_warn_insufficient_qty.py b/addons/mrp/wizard/stock_warn_insufficient_qty.py new file mode 100644 index 00000000..335c5424 --- /dev/null +++ b/addons/mrp/wizard/stock_warn_insufficient_qty.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class StockWarnInsufficientQtyUnbuild(models.TransientModel): + _name = 'stock.warn.insufficient.qty.unbuild' + _inherit = 'stock.warn.insufficient.qty' + _description = 'Warn Insufficient Unbuild Quantity' + + unbuild_id = fields.Many2one('mrp.unbuild', 'Unbuild') + + def _get_reference_document_company_id(self): + return self.unbuild_id.company_id + + def action_done(self): + self.ensure_one() + return self.unbuild_id.action_unbuild() diff --git a/addons/mrp/wizard/stock_warn_insufficient_qty_views.xml b/addons/mrp/wizard/stock_warn_insufficient_qty_views.xml new file mode 100644 index 00000000..63df4691 --- /dev/null +++ b/addons/mrp/wizard/stock_warn_insufficient_qty_views.xml @@ -0,0 +1,13 @@ + + + + stock.warn.insufficient.qty.unbuild + stock.warn.insufficient.qty.unbuild + + + + Do you confirm you want to unbuild from location ? This may lead to inconsistencies in your inventory. + + + + -- cgit v1.2.3