diff --git a/mrp_workorder_moves_view/README.rst b/mrp_workorder_moves_view/README.rst new file mode 100644 index 000000000..1b7ec798d --- /dev/null +++ b/mrp_workorder_moves_view/README.rst @@ -0,0 +1,43 @@ +================== +Moves in workorder +================== + +Adds the moves related to the operations + +.. |badge1| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +|badge1| + +Adds a new level in products with the custom values specified in a order +line, manufacturing order or a lot. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* AvanzOSC + +Contributors +~~~~~~~~~~~~ + +* Mikel Arregi +* Ana Juaristi diff --git a/mrp_workorder_moves_view/__init__.py b/mrp_workorder_moves_view/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/mrp_workorder_moves_view/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/mrp_workorder_moves_view/__manifest__.py b/mrp_workorder_moves_view/__manifest__.py new file mode 100644 index 000000000..1d9d525da --- /dev/null +++ b/mrp_workorder_moves_view/__manifest__.py @@ -0,0 +1,22 @@ +# Copyright 2020 Mikel Arregi Etxaniz - AvanzOSC +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html +{ + "name": "Workorder moves in view", + "version": "12.0.1.0.0", + "license": "AGPL-3", + "depends": [ + "mrp", + ], + "author": "AvanzOSC", + "website": "http://www.avanzosc.es", + "category": "Manufacturing", + "data": [ + "views/mrp_workorder_view.xml", + "views/stock_production_lot_view.xml", + "views/mrp_production_view.xml", + "views/stock_move_line_view.xml", + ], + 'demo': [], + 'installable': True, + 'auto_install': False, +} diff --git a/mrp_workorder_moves_view/models/__init__.py b/mrp_workorder_moves_view/models/__init__.py new file mode 100644 index 000000000..f853e1667 --- /dev/null +++ b/mrp_workorder_moves_view/models/__init__.py @@ -0,0 +1,5 @@ +from . import mrp_workorder +from . import mrp_production +from . import mrp_production_lot +from . import stock_location +from . import res_config diff --git a/mrp_workorder_moves_view/models/mrp_production.py b/mrp_workorder_moves_view/models/mrp_production.py new file mode 100644 index 000000000..ff8ad805e --- /dev/null +++ b/mrp_workorder_moves_view/models/mrp_production.py @@ -0,0 +1,42 @@ +# Copyright 2020 Mikel Arregi Etxaniz - AvanzOSC +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import api, fields, models +from odoo.tools.safe_eval import safe_eval +from odoo.models import expression + + +class MrpProduction(models.Model): + _inherit = "mrp.production" + + + @api.multi + def button_open_lots(self): + self.ensure_one() + location_obj = self.env['stock.location'] + physical_location = location_obj.search( + [('name', '=', 'Physical Locations')]) + virtual_location = location_obj.search( + [('name', '=', 'Virtual Locations')]) + move_lines = self.env['stock.move.line'] + for workorder in self.env['mrp.workorder'].search( + [('production_id', '=', self.id)]): + move_lines |= workorder.active_move_line_ids + move_lines |= workorder.move_line_ids + produce_lines = move_lines.filtered( + lambda x: x.location_id._has_parent(virtual_location) and + x.location_dest_id._has_parent(physical_location)) + produce_lots = produce_lines.mapped('lot_id') + action = self.env.ref('stock.action_production_lot_form') + action_dict = action.read()[0] if action else {} + action_dict['context'] = safe_eval( + action_dict.get('context', '{}')) + action_dict['context'].pop('search_default_group_by_product', False) + action_dict['context'].update({ + 'default_workorder_id': self.id, + }) + action_dict['ids'] = produce_lots + domain = expression.AND([ + [('id', 'in', produce_lots.ids)], + safe_eval(action.domain or '[]')]) + action_dict.update({'domain': domain}) + return action_dict diff --git a/mrp_workorder_moves_view/models/mrp_production_lot.py b/mrp_workorder_moves_view/models/mrp_production_lot.py new file mode 100644 index 000000000..c7be2fe37 --- /dev/null +++ b/mrp_workorder_moves_view/models/mrp_production_lot.py @@ -0,0 +1,34 @@ +# Copyright 2020 Mikel Arregi Etxaniz - AvanzOSC +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import api, fields, models + + +class StockProductionLot(models.Model): + _inherit = "stock.production.lot" + + produce_move_line_ids = fields.One2many( + comodel_name="stock.move.line", inverse_name='lot_id', + string="Produced Lines") + consume_move_line_ids = fields.One2many( + comodel_name="stock.move.line", inverse_name='lot_produced_id', + string="Consumed Lines") + wo_produce_move_line_ids = fields.One2many( + comodel_name="stock.move.line", compute="_compute_lot_moves", + string="Produced Lines") + wo_consume_move_line_ids = fields.One2many( + comodel_name="stock.move.line", compute="_compute_lot_moves", + string="Consumed Lines") + + def _compute_lot_moves(self): + for lot in self: + workorder_id = self._context.get('moves_workorder_id', False) + if workorder_id: + workorder_id = self._context.get('active_id', False) + workorder_moves = self.env['stock.move.line'].search([( + 'workorder_id', '=', workorder_id)]) + lot.wo_produce_move_line_ids = \ + lot.produce_move_line_ids.filtered( + lambda x: x.id in workorder_moves.ids) + lot.wo_consume_move_line_ids = \ + lot.consume_move_line_ids.filtered( + lambda x: x.id in workorder_moves.ids) diff --git a/mrp_workorder_moves_view/models/mrp_workorder.py b/mrp_workorder_moves_view/models/mrp_workorder.py new file mode 100644 index 000000000..a000f021a --- /dev/null +++ b/mrp_workorder_moves_view/models/mrp_workorder.py @@ -0,0 +1,295 @@ +# Copyright 2020 Mikel Arregi Etxaniz - AvanzOSC +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import api, models, _ +from odoo.tools.safe_eval import safe_eval +from odoo.models import expression +from odoo.exceptions import UserError +from odoo.tools import float_compare, float_round + + +class MrpWorkorder(models.Model): + _inherit = "mrp.workorder" + + def _generate_final_lot_ids(self, final_lot): + """ Generate stock move lines """ + self.ensure_one() + MoveLine = self.env['stock.move.line'] + tracked_moves = self.move_raw_ids.filtered( + lambda move: move.state not in ('done', 'cancel') and move.product_id.tracking != 'none' and move.product_id != self.production_id.product_id and move.bom_line_id) + for move in tracked_moves: + qty = move.unit_factor * self.qty_producing + if move.product_id.tracking == 'serial': + while float_compare(qty, 0.0, precision_rounding=move.product_uom.rounding) > 0: + MoveLine.create({ + 'move_id': move.id, + 'product_uom_qty': 0, + 'product_uom_id': move.product_uom.id, + 'qty_done': min(1, qty), + 'production_id': self.production_id.id, + 'workorder_id': self.id, + 'product_id': move.product_id.id, + 'done_wo': False, + 'location_id': move.location_id.id, + 'location_dest_id': move.location_dest_id.id, + 'lot_produced_id': final_lot.id, + }) + qty -= 1 + else: + MoveLine.create({ + 'move_id': move.id, + 'product_uom_qty': 0, + 'product_uom_id': move.product_uom.id, + 'qty_done': qty, + 'product_id': move.product_id.id, + 'production_id': self.production_id.id, + 'workorder_id': self.id, + 'done_wo': False, + 'location_id': move.location_id.id, + 'location_dest_id': move.location_dest_id.id, + 'lot_produced_id': final_lot.id, + }) + + @api.multi + def _create_serial_lot(self, suffix): + lot_name = "{}-{}".format(self.production_id.name, suffix) + lot_obj = self.env['stock.production.lot'] + lot_id = lot_obj.search([("name", "=", lot_name)]) + if lot_id: + return lot_id + return self.env['stock.production.lot'].create( + {'name': lot_name, + 'product_id': self.production_id.product_id.id}) + + # @api.multi + # def button_execute_all_moves(self): + # for order in self: + # lot_incremental = 0 + # while (order.qty_production != order.qty_produced or + # order.state != 'done' and not order.final_lot_id): + # lot_incremental += 1 + # serial_lot = order._create_serial_lot(lot_incremental) + # order.final_lot_id = serial_lot + # order.all_record_production() + # order._make_active_moves() + @api.multi + def button_start(self): + res = super().button_start() + self.button_create_all_moves() + return res + + @api.multi + def button_execute_all_moves(self): + for order in self: + order.all_record_production() + order._make_active_moves() + order.final_workorder() + order.qty_produced += order.qty_producing + rounding = order.production_id.product_uom_id.rounding + if float_compare(order.qty_produced, order.production_id.product_qty, + precision_rounding=rounding) >= 0: + order.button_finish() + + + @api.multi + def generate_lots(self): + lot_incremental = 0 + serial_lot = self.env['stock.production.lot'] + i=0 + while (self.qty_production - i > 0): + lot_incremental += 1 + serial_lot |= self._create_serial_lot(lot_incremental) + i += 1 + return serial_lot + + @api.multi + def button_create_all_moves(self): + i = 1 + final_lots = self.generate_lots() + active_moves = self.active_move_line_ids.filtered( + lambda x: x.product_id.tracking != 'none') + if active_moves: + active_moves.lot_produced_id = final_lots[0].id + while (self.qty_production - i > 0): + self._generate_final_lot_ids(final_lots[i]) + i += 1 + + @api.multi + def all_record_production(self): + if not self: + return True + + self.ensure_one() + if self.qty_producing <= 0: + raise UserError(_('Please set the quantity you are currently producing. It should be different from zero.')) + + generated_lots = self.env['stock.production.lot'] + if (self.production_id.product_id.tracking != 'none') and not self.final_lot_id and self.move_raw_ids: + generated_lots = self.generate_lots() + + # Update quantities done on each raw material line + # For each untracked component without any 'temporary' move lines, + # (the new workorder tablet view allows registering consumed quantities for untracked components) + # we assume that only the theoretical quantity was used + for lot in generated_lots: + for move in self.move_raw_ids: + if move.has_tracking == 'none' and (move.state not in ('done', 'cancel')) and move.bom_line_id\ + and move.unit_factor and not move.move_line_ids.filtered(lambda ml: not ml.done_wo): + rounding = move.product_uom.rounding + if self.product_id.tracking != 'none': + qty_to_add = float_round(self.qty_producing * move.unit_factor, precision_rounding=rounding) + move._generate_consumed_move_line(qty_to_add, lot) + elif len(move._get_move_lines()) < 2: + move.quantity_done += float_round(self.qty_producing * move.unit_factor, precision_rounding=rounding) + else: + move._set_quantity_done(move.quantity_done + float_round(self.qty_producing * move.unit_factor, precision_rounding=rounding)) + return True + + + def _make_active_moves(self): + # Transfer quantities from temporary to final move lots or make them final + for move_line in self.active_move_line_ids: + # Check if move_line already exists + if move_line.qty_done <= 0: # rounding... + move_line.sudo().unlink() + continue + if move_line.product_id.tracking != 'none' and not move_line.lot_id: + raise UserError(_( + 'You should provide a lot/serial number for a component.')) + # Search other move_line where it could be added: + lots = self.move_line_ids.filtered( + lambda x: (x.lot_id.id == move_line.lot_id.id) and ( + not x.lot_produced_id) and (not x.done_move) and ( + x.product_id == move_line.product_id)) + if lots: + lots[0].qty_done += move_line.qty_done + #lots[0].lot_produced_id = self.final_lot_id + self._link_to_quality_check(move_line, lots[0]) + move_line.sudo().unlink() + else: + #move_line.lot_produced_id = self.final_lot_id + move_line.done_wo = True + + # self.move_line_ids.filtered( + # lambda move_line: not move_line.done_move and not move_line.lot_produced_id and move_line.qty_done > 0 + # ).write({ + # 'lot_produced_id': self.final_lot_id.id, + # 'lot_produced_qty': self.qty_producing + # }) + + def final_workorder(self): + # If last work order, then post lots used + # TODO: should be same as checking if for every workorder something has been done? + generated_lots = self.generate_lots() + if not self.next_work_order_id: + production_move = self.production_id.move_finished_ids.filtered( + lambda x: (x.product_id.id == self.production_id.product_id.id) and (x.state not in ('done', 'cancel'))) + if production_move.product_id.tracking != 'none': + move_line = production_move.move_line_ids.filtered(lambda x: + x.lot_id.id in generated_lots) + if move_line: + for line in move_line: + move_line.product_uom_qty = self.qty_production + move_line.qty_done = self.qty_production + else: + for lot in generated_lots: + location_dest_id = production_move.location_dest_id.get_putaway_strategy(self.product_id).id or production_move.location_dest_id.id + move_line.create({'move_id': production_move.id, + 'product_id': production_move.product_id.id, + 'lot_id': lot.id, + 'product_uom_qty': self.qty_producing, + 'product_uom_id': production_move.product_uom.id, + 'qty_done': self.qty_producing, + 'workorder_id': self.id, + 'location_id': production_move.location_id.id, + 'location_dest_id': location_dest_id, + }) + else: + production_move._set_quantity_done(self.qty_production) + + if not self.next_work_order_id: + for by_product_move in self._get_byproduct_move_to_update(): + if by_product_move.has_tracking != 'serial': + values = self._get_byproduct_move_line(by_product_move, self.qty_production * by_product_move.unit_factor) + self.env['stock.move.line'].create(values) + elif by_product_move.has_tracking == 'serial': + qty_todo = by_product_move.product_uom._compute_quantity(self.qty_production * by_product_move.unit_factor, by_product_move.product_id.uom_id) + for i in range(0, int(float_round(qty_todo, precision_digits=0))): + values = self._get_byproduct_move_line(by_product_move, 1) + self.env['stock.move.line'].create(values) + + # # Update workorder quantity produced + # self.qty_produced += self.qty_producing + # + # if self.final_lot_id: + # self.final_lot_id.use_next_on_work_order_id = self.next_work_order_id + # self.final_lot_id = False + # + # # One a piece is produced, you can launch the next work order + # self._start_nextworkorder() + # + # # Set a qty producing + # rounding = self.production_id.product_uom_id.rounding + # if float_compare(self.qty_produced, self.production_id.product_qty, precision_rounding=rounding) >= 0: + # self.qty_producing = 0 + # elif self.production_id.product_id.tracking == 'serial': + # self._assign_default_final_lot_id() + # self.qty_producing = 1.0 + # self._generate_lot_ids() + # else: + # self.qty_producing = float_round(self.production_id.product_qty - self.qty_produced, precision_rounding=rounding) + # self._generate_lot_ids() + # + # if self.next_work_order_id and self.next_work_order_id.state not in ['done', 'cancel'] and self.production_id.product_id.tracking != 'none': + # self.next_work_order_id._assign_default_final_lot_id() + + # if float_compare(self.qty_produced, self.production_id.product_qty, precision_rounding=rounding) >= 0: + # self.button_finish() + return True + + @api.multi + def button_open_active_move_lines(self): + self.ensure_one() + self = self.with_context( + default_workorder_id=self.id) + action = self.env.ref('stock.stock_move_line_action') + action_dict = action.read()[0] if action else {} + action_dict['context'] = safe_eval( + action_dict.get('context', '{}')) + action_dict['context'].pop('search_default_done', False) + action_dict['context'].pop('search_default_groupby_product_id', False) + action_dict['context'].update({ + 'default_workorder_id': self.id, + 'search_default_groupby_lot_produced_id': 1, + }) + domain = expression.AND([ + [('workorder_id', '=', self.id)], + safe_eval(action.domain or '[]')]) + action_dict.update({'domain': domain}) + return action_dict + + def button_open_lots(self): + self.ensure_one() + location_obj = self.env['stock.location'] + physical_location = location_obj.search( + [('name', '=', 'Physical Locations')]) + virtual_location = location_obj.search( + [('name', '=', 'Virtual Locations')]) + move_lines = self.active_move_line_ids | self.move_line_ids + produce_lines = move_lines.filtered( + lambda x: x.location_id._has_parent(virtual_location) and + x.location_dest_id._has_parent(physical_location)) + produce_lots = produce_lines.mapped('lot_id') + action = self.env.ref('stock.action_production_lot_form') + action_dict = action.read()[0] if action else {} + action_dict['context'] = safe_eval( + action_dict.get('context', '{}')) + action_dict['context'].pop('search_default_group_by_product', False) + action_dict['context'].update({ + 'default_workorder_id': self.id, + }) + action_dict['ids'] = produce_lots + domain = expression.AND([ + [('id', 'in', produce_lots.ids)], + safe_eval(action.domain or '[]')]) + action_dict.update({'domain': domain}) + return action_dict \ No newline at end of file diff --git a/mrp_workorder_moves_view/models/res_config.py b/mrp_workorder_moves_view/models/res_config.py new file mode 100644 index 000000000..4f24cee8f --- /dev/null +++ b/mrp_workorder_moves_view/models/res_config.py @@ -0,0 +1,28 @@ +# Copyright 2020 Mikel Arregi Etxaniz - AvanzOSC +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import api, fields, models + +PARAMS = [ + ("do_all", "mrp_workorder_moves_view.do_all"), +] + +class ResConfingSettings(models.TransientModel): + _inherit = "res.config.settings" + + do_all = fields.boolean(string="Process whole workorder") + + @api.multi + def set_params(self): + self.ensure_one() + field_name = "mrp_workorder_moves_view.do_all" + key_name = "do_all" + value = getattr(self, field_name, '').strip() + self.env['ir.config_parameter'].set_param(key_name, value) + + @api.multi + def get_default_params(self, cr, uid, fields, context=None): + res = {} + for field_name, key_name in PARAMS: + res[field_name] = self.env['ir.config_parameter'].get_param( + key_name, '').strip() + return res \ No newline at end of file diff --git a/mrp_workorder_moves_view/models/stock_location.py b/mrp_workorder_moves_view/models/stock_location.py new file mode 100644 index 000000000..b5794a927 --- /dev/null +++ b/mrp_workorder_moves_view/models/stock_location.py @@ -0,0 +1,15 @@ +# Copyright 2020 Mikel Arregi Etxaniz - AvanzOSC +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import api, models + + +class StockLocation(models.Model): + _inherit = "stock.location" + + def _has_parent(self, parent_location): + if not self.location_id: + return False + elif self.location_id == parent_location: + return True + else: + return self.location_id._has_parent(parent_location) diff --git a/mrp_workorder_moves_view/views/mrp_production_view.xml b/mrp_workorder_moves_view/views/mrp_production_view.xml new file mode 100644 index 000000000..ed92bd3bc --- /dev/null +++ b/mrp_workorder_moves_view/views/mrp_production_view.xml @@ -0,0 +1,14 @@ + + + + mrp.production.produced.lots.form + mrp.production + + + + + + diff --git a/mrp_workorder_moves_view/views/mrp_workorder_view.xml b/mrp_workorder_moves_view/views/mrp_workorder_view.xml new file mode 100644 index 000000000..9af321022 --- /dev/null +++ b/mrp_workorder_moves_view/views/mrp_workorder_view.xml @@ -0,0 +1,60 @@ + + + + mrp.workorder.moves.form + stock.move.line + + + + + + + + + + workorder.stock.move.form + mrp.workorder + + + + + + + + + {'invisible': [('move_raw_ids', '=', [])]} + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mrp_workorder_moves_view/views/stock_move_line_view.xml b/mrp_workorder_moves_view/views/stock_move_line_view.xml new file mode 100644 index 000000000..64ca4d6ca --- /dev/null +++ b/mrp_workorder_moves_view/views/stock_move_line_view.xml @@ -0,0 +1,17 @@ + + + + stock.move.line.production.lot.form + stock.move.line + + + + top + + + + + + + + \ No newline at end of file diff --git a/mrp_workorder_moves_view/views/stock_production_lot_view.xml b/mrp_workorder_moves_view/views/stock_production_lot_view.xml new file mode 100644 index 000000000..4fc6d4a95 --- /dev/null +++ b/mrp_workorder_moves_view/views/stock_production_lot_view.xml @@ -0,0 +1,18 @@ + + + + stock.production.lot.moves.form + stock.production.lot + + + + + + + + + + + + + \ No newline at end of file