From 00500a9d477c909044ea6932ab5772c9d2c321b3 Mon Sep 17 00:00:00 2001 From: DavidJForgeFlow Date: Thu, 1 Dec 2022 16:47:21 +0100 Subject: [PATCH] [14.0][ADD] mrp_component_operation --- mrp_component_operation/README.rst | 0 mrp_component_operation/__init__.py | 4 + mrp_component_operation/__manifest__.py | 21 ++ mrp_component_operation/models/__init__.py | 3 + .../models/mrp_component_operation.py | 60 ++++ .../models/mrp_production.py | 25 ++ .../models/stock_location_route.py | 9 + .../readme/CONTRIBUTORS.rst | 1 + .../readme/DESCRIPTION.rst | 2 + mrp_component_operation/readme/USAGE.rst | 6 + .../security/ir.model.access.csv | 5 + mrp_component_operation/tests/__init__.py | 1 + .../tests/test_component_operate.py | 322 ++++++++++++++++++ .../views/mrp_component_operation_views.xml | 63 ++++ .../views/mrp_production_views.xml | 18 + mrp_component_operation/views/stock_view.xml | 16 + mrp_component_operation/wizards/__init__.py | 1 + .../wizards/mrp_component_operate.py | 160 +++++++++ .../wizards/mrp_component_operate_wizard.xml | 50 +++ .../odoo/addons/mrp_component_operation | 1 + setup/mrp_component_operation/setup.py | 6 + 21 files changed, 774 insertions(+) create mode 100644 mrp_component_operation/README.rst create mode 100644 mrp_component_operation/__init__.py create mode 100644 mrp_component_operation/__manifest__.py create mode 100644 mrp_component_operation/models/__init__.py create mode 100644 mrp_component_operation/models/mrp_component_operation.py create mode 100644 mrp_component_operation/models/mrp_production.py create mode 100644 mrp_component_operation/models/stock_location_route.py create mode 100644 mrp_component_operation/readme/CONTRIBUTORS.rst create mode 100644 mrp_component_operation/readme/DESCRIPTION.rst create mode 100644 mrp_component_operation/readme/USAGE.rst create mode 100644 mrp_component_operation/security/ir.model.access.csv create mode 100644 mrp_component_operation/tests/__init__.py create mode 100644 mrp_component_operation/tests/test_component_operate.py create mode 100644 mrp_component_operation/views/mrp_component_operation_views.xml create mode 100644 mrp_component_operation/views/mrp_production_views.xml create mode 100644 mrp_component_operation/views/stock_view.xml create mode 100644 mrp_component_operation/wizards/__init__.py create mode 100644 mrp_component_operation/wizards/mrp_component_operate.py create mode 100644 mrp_component_operation/wizards/mrp_component_operate_wizard.xml create mode 120000 setup/mrp_component_operation/odoo/addons/mrp_component_operation create mode 100644 setup/mrp_component_operation/setup.py diff --git a/mrp_component_operation/README.rst b/mrp_component_operation/README.rst new file mode 100644 index 000000000..e69de29bb diff --git a/mrp_component_operation/__init__.py b/mrp_component_operation/__init__.py new file mode 100644 index 000000000..c89926b23 --- /dev/null +++ b/mrp_component_operation/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2022 ForgeFlow S.L. (https://www.forgeflow.com) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). +from . import models +from . import wizards diff --git a/mrp_component_operation/__manifest__.py b/mrp_component_operation/__manifest__.py new file mode 100644 index 000000000..62efb5d47 --- /dev/null +++ b/mrp_component_operation/__manifest__.py @@ -0,0 +1,21 @@ +# Copyright 2022 ForgeFlow S.L. (https://www.forgeflow.com) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). + +{ + "name": "MRP Components Operations", + "version": "14.0.1.0.0", + "author": "ForgeFlow, Odoo Community Association (OCA)", + "summary": "Allows to operate the components from a MO", + "website": "https://github.com/OCA/manufacture", + "category": "Manufacturing", + "depends": ["mrp", "stock_move_forced_lot"], + "data": [ + "security/ir.model.access.csv", + "views/mrp_component_operation_views.xml", + "views/mrp_production_views.xml", + "views/stock_view.xml", + "wizards/mrp_component_operate_wizard.xml", + ], + "license": "LGPL-3", + "installable": True, +} diff --git a/mrp_component_operation/models/__init__.py b/mrp_component_operation/models/__init__.py new file mode 100644 index 000000000..360abba99 --- /dev/null +++ b/mrp_component_operation/models/__init__.py @@ -0,0 +1,3 @@ +from . import mrp_component_operation +from . import mrp_production +from . import stock_location_route diff --git a/mrp_component_operation/models/mrp_component_operation.py b/mrp_component_operation/models/mrp_component_operation.py new file mode 100644 index 000000000..5332e5229 --- /dev/null +++ b/mrp_component_operation/models/mrp_component_operation.py @@ -0,0 +1,60 @@ +# Copyright 2022 ForgeFlow S.L. (https://www.forgeflow.com) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). +from odoo import fields, models + + +class MrpComponentOperation(models.Model): + _name = "mrp.component.operation" + _description = "Component Operation" + + name = fields.Char(help="Component Operation Reference", required=True) + + source_location_id = fields.Many2one( + "stock.location", + "Source Location", + help="The Location where the components are.", + ) + + source_route_id = fields.Many2one( + comodel_name="stock.location.route", + string="Source Route", + help="The Route used to pick the components.", + domain=[("mo_component_selectable", "=", True)], + ) + + destination_location_id = fields.Many2one( + "stock.location", + "Destination Location", + help="The Location where the components are going to be transferred.", + ) + + destination_route_id = fields.Many2one( + comodel_name="stock.location.route", + string="Destination Route", + help="The Route used to transfer the components to the destination location.", + domain=[("mo_component_selectable", "=", True)], + ) + + scrap_location_id = fields.Many2one( + "stock.location", + "Scrap Location", + ) + + incoming_operation = fields.Selection( + selection=[ + ("no", "No"), + ("replace", "Pick Component from Source Route"), + ], + default="no", + required=True, + ) + + outgoing_operation = fields.Selection( + selection=[ + ("no", "No"), + ("move", "Move to Destination Location"), + ("scrap", "Make a Scrap"), + ], + default="no", + required=True, + ) diff --git a/mrp_component_operation/models/mrp_production.py b/mrp_component_operation/models/mrp_production.py new file mode 100644 index 000000000..ca4e09e43 --- /dev/null +++ b/mrp_component_operation/models/mrp_production.py @@ -0,0 +1,25 @@ +# Copyright 2022 ForgeFlow S.L. (https://www.forgeflow.com) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). +from odoo import _, models + + +class MrpProduction(models.Model): + + _inherit = "mrp.production" + + def button_operate_components(self): + return { + "name": _("Operate Component"), + "view_mode": "form", + "res_model": "mrp.component.operate", + "view_id": self.env.ref( + "mrp_component_operation.view_mrp_component_operate_form" + ).id, + "type": "ir.actions.act_window", + "context": { + "default_mo_id": self.id, + "product_ids": self.move_raw_ids.move_line_ids.product_id.mapped("id"), + "lot_ids": self.move_raw_ids.move_line_ids.lot_id.mapped("id"), + }, + "target": "new", + } diff --git a/mrp_component_operation/models/stock_location_route.py b/mrp_component_operation/models/stock_location_route.py new file mode 100644 index 000000000..434951925 --- /dev/null +++ b/mrp_component_operation/models/stock_location_route.py @@ -0,0 +1,9 @@ +# Copyright 2022 ForgeFlow S.L. (https://www.forgeflow.com) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). +from odoo import fields, models + + +class StockLocationRoute(models.Model): + _inherit = "stock.location.route" + + mo_component_selectable = fields.Boolean(string="Selectable on MO Components") diff --git a/mrp_component_operation/readme/CONTRIBUTORS.rst b/mrp_component_operation/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..fcd081018 --- /dev/null +++ b/mrp_component_operation/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* David Jiménez diff --git a/mrp_component_operation/readme/DESCRIPTION.rst b/mrp_component_operation/readme/DESCRIPTION.rst new file mode 100644 index 000000000..8e46b748e --- /dev/null +++ b/mrp_component_operation/readme/DESCRIPTION.rst @@ -0,0 +1,2 @@ +This module allows to operate the components from a MO, being able to move it to another location or making an scrap +and take another from a desired location and link it to the current MO. diff --git a/mrp_component_operation/readme/USAGE.rst b/mrp_component_operation/readme/USAGE.rst new file mode 100644 index 000000000..23481e293 --- /dev/null +++ b/mrp_component_operation/readme/USAGE.rst @@ -0,0 +1,6 @@ +Go to Manufacture -> Configuration -> Settings -> Component Operations. +Create a new Operation, select the desired outgoing operation and the desired incoming operation. +Then fill the Locations and Routes (check in the routes that are selectable for mo operations in the route page). +Once saved, go to the Manufacture Order and click on the button "Operate Component". +Select the desired product (lot if needed) and the operation wanted. +When Confirmed, in the MO will be linked the pickings/scraps done. diff --git a/mrp_component_operation/security/ir.model.access.csv b/mrp_component_operation/security/ir.model.access.csv new file mode 100644 index 000000000..1b735c204 --- /dev/null +++ b/mrp_component_operation/security/ir.model.access.csv @@ -0,0 +1,5 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_mrp_component_operation_user,mrp.component.operation.user,model_mrp_component_operation,mrp.group_mrp_user,1,0,0,0 +access_mrp_component_operation_manager,mrp.component.operation.manager,model_mrp_component_operation,mrp.group_mrp_manager,1,1,1,1 +access_mrp_component_operate_user,mrp.component.operate.user,model_mrp_component_operate,mrp.group_mrp_user,1,0,0,0 +access_mrp_component_operate_manager,mrp.component.operate.manager,model_mrp_component_operate,mrp.group_mrp_manager,1,1,1,1 diff --git a/mrp_component_operation/tests/__init__.py b/mrp_component_operation/tests/__init__.py new file mode 100644 index 000000000..b5d3c9eba --- /dev/null +++ b/mrp_component_operation/tests/__init__.py @@ -0,0 +1 @@ +from . import test_component_operate diff --git a/mrp_component_operation/tests/test_component_operate.py b/mrp_component_operation/tests/test_component_operate.py new file mode 100644 index 000000000..546382af1 --- /dev/null +++ b/mrp_component_operation/tests/test_component_operate.py @@ -0,0 +1,322 @@ +from odoo import api +from odoo.tests import common + + +class TestComponentOperation(common.SavepointCase): + @classmethod + def setUpClass(cls): + super(TestComponentOperation, cls).setUpClass() + cls.user_admin = cls.env.ref("base.user_admin") + cls.env = api.Environment(cls.cr, cls.user_admin.id, {}) + cls.ProcurementGroup = cls.env["procurement.group"] + cls.MrpProduction = cls.env["mrp.production"] + cls.env.user.company_id.manufacturing_lead = 0 + cls.env.user.tz = False # Make sure there's no timezone in user + + cls.picking_type = cls.env["stock.picking.type"].search( + [ + ("code", "=", "mrp_operation"), + ("sequence_id.company_id", "=", cls.env.user.company_id.id), + ], + limit=1, + ) + cls.product1 = cls.env["product.product"].create( + { + "name": "TEST Parent", + "route_ids": [ + (6, 0, [cls.env.ref("mrp.route_warehouse0_manufacture").id]) + ], + "type": "product", + "produce_delay": 0, + } + ) + cls.product2 = cls.env["product.product"].create( + {"name": "TEST Child", "type": "product"} + ) + cls.product3 = cls.env["product.product"].create( + {"name": "TEST Child Serial", "type": "product", "tracking": "serial"} + ) + cls.bom = cls.env["mrp.bom"].create( + { + "product_id": cls.product1.id, + "product_tmpl_id": cls.product1.product_tmpl_id.id, + "type": "normal", + "bom_line_ids": [ + (0, 0, {"product_id": cls.product2.id, "product_qty": 2}), + (0, 0, {"product_id": cls.product3.id, "product_qty": 1}), + ], + } + ) + cls.stock_picking_type = cls.env.ref("stock.picking_type_out") + cls.warehouse = cls.env["stock.warehouse"].search( + [("company_id", "=", cls.env.user.company_id.id)], limit=1 + ) + cls.warehouse.manufacture_steps = "pbm" + cls.ressuply_loc1 = cls.warehouse.lot_stock_id + cls.source_location = cls.env.ref("stock.stock_location_stock") + cls.destination_location = cls.env.ref("stock.stock_location_output") + stock_location_locations_virtual = cls.env["stock.location"].create( + {"name": "Virtual Locations", "usage": "view", "posz": 1} + ) + cls.scrapped_location = cls.env["stock.location"].create( + { + "name": "Scrapped", + "location_id": stock_location_locations_virtual.id, + "scrap_location": True, + "usage": "inventory", + } + ) + cls.source_route = cls.env["stock.location.route"].create( + { + "name": "Source Route", + "mo_component_selectable": True, + "sequence": 10, + } + ) + + cls.destination_route = cls.env["stock.location.route"].create( + { + "name": "Destination Route", + "mo_component_selectable": True, + "sequence": 10, + } + ) + + cls.env["stock.rule"].create( + { + "name": "Transfer", + "route_id": cls.source_route.id, + "location_src_id": cls.ressuply_loc1.id, + "location_id": cls.source_location.id, + "action": "pull", + "picking_type_id": cls.warehouse.int_type_id.id, + "procure_method": "make_to_stock", + "warehouse_id": cls.warehouse.id, + } + ) + + cls.env["stock.rule"].create( + { + "name": "Transfer 2", + "route_id": cls.destination_route.id, + "location_src_id": cls.source_location.id, + "location_id": cls.destination_location.id, + "action": "pull", + "picking_type_id": cls.warehouse.int_type_id.id, + "procure_method": "make_to_stock", + "warehouse_id": cls.warehouse.id, + "propagate_warehouse_id": cls.warehouse.id, + } + ) + + cls.operation_scrap_replace = cls.env["mrp.component.operation"].create( + { + "name": "Operation Scrap and Replace", + "incoming_operation": "replace", + "outgoing_operation": "scrap", + "source_location_id": cls.source_location.id, + "source_route_id": cls.source_route.id, + "scrap_location_id": cls.scrapped_location.id, + } + ) + + cls.operation_no = cls.env["mrp.component.operation"].create( + { + "name": "Operation Scrap and Replace", + "incoming_operation": "no", + "outgoing_operation": "no", + "source_location_id": cls.source_location.id, + } + ) + + cls.operation_move_replace = cls.env["mrp.component.operation"].create( + { + "name": "Operation Move", + "incoming_operation": "replace", + "outgoing_operation": "move", + "source_location_id": cls.source_location.id, + "source_route_id": cls.source_route.id, + "destination_location_id": cls.destination_location.id, + "destination_route_id": cls.destination_route.id, + } + ) + + def test_01_scrap_and_replace(self): + nb_product_todo = 5 + serials_p2 = [] + for i in range(nb_product_todo): + serials_p2.append( + self.env["stock.production.lot"].create( + { + "name": f"lot_consumed_2_{i}", + "product_id": self.product3.id, + "company_id": self.env.company.id, + } + ) + ) + self.env["stock.quant"]._update_available_quantity( + self.product3, self.ressuply_loc1, 1, lot_id=serials_p2[-1] + ) + self.env["stock.quant"]._update_available_quantity( + self.product2, self.ressuply_loc1, 10 + ) + mo = self.MrpProduction.create( + { + "bom_id": self.bom.id, + "product_id": self.product1.id, + "product_qty": 2, + "product_uom_id": self.product1.uom_id.id, + "date_deadline": "2023-01-01 15:00:00", + "date_planned_start": "2023-01-01 15:00:00", + } + ) + mo._onchange_move_raw() + mo._onchange_move_finished() + mo.action_confirm() + mo.action_assign() + self.assertEqual(mo.move_raw_ids[0].move_line_ids.product_uom_qty, 4) + self.assertEqual(len(mo.move_raw_ids[1].move_line_ids), 2) + wizard = self.env["mrp.component.operate"].create( + { + "product_id": mo.move_raw_ids[1].product_id.id, + "lot_id": mo.move_raw_ids[1].move_line_ids[0].lot_id.id, + "operation_id": self.operation_scrap_replace.id, + "mo_id": mo.id, + } + ) + self.assertEqual(wizard.product_qty, 1) + self.assertEqual(wizard.product_id, self.product3) + lot = wizard.lot_id + wizard.action_operate_component() + self.assertEqual(len(mo.picking_ids), 1) + self.assertEqual(mo.scrap_ids.product_id, self.product3) + self.assertEqual(mo.scrap_ids.lot_id, lot) + self.assertEqual(mo.scrap_ids.state, "done") + self.assertEqual(len(mo.move_raw_ids[1].move_line_ids), 1) + self.assertEqual(len(mo.move_raw_ids[1].move_orig_ids.move_line_ids), 0) + self.assertEqual(mo.picking_ids.product_id, self.product3) + mo.picking_ids.action_assign() + mo.picking_ids.button_validate() + self.assertEqual(len(mo.move_raw_ids[1].move_line_ids), 1) + self.assertEqual(len(mo.move_raw_ids[1].move_orig_ids.move_line_ids), 1) + + def test_02_move_and_replace(self): + nb_product_todo = 5 + serials_p2 = [] + for i in range(nb_product_todo): + serials_p2.append( + self.env["stock.production.lot"].create( + { + "name": f"lot_consumed_2_{i}", + "product_id": self.product3.id, + "company_id": self.env.company.id, + } + ) + ) + self.env["stock.quant"]._update_available_quantity( + self.product3, self.ressuply_loc1, 1, lot_id=serials_p2[-1] + ) + self.env["stock.quant"]._update_available_quantity( + self.product2, self.ressuply_loc1, 10 + ) + mo = self.MrpProduction.create( + { + "bom_id": self.bom.id, + "product_id": self.product1.id, + "product_qty": 1, + "product_uom_id": self.product1.uom_id.id, + "date_deadline": "2023-01-01 15:00:00", + "date_planned_start": "2023-01-01 15:00:00", + } + ) + mo._onchange_move_raw() + mo._onchange_move_finished() + mo.action_confirm() + mo.action_assign() + self.assertEqual(mo.move_raw_ids[0].move_line_ids.product_uom_qty, 2) + self.assertEqual(len(mo.move_raw_ids[1].move_line_ids), 1) + wizard = self.env["mrp.component.operate"].create( + { + "product_id": mo.move_raw_ids[1].product_id.id, + "lot_id": mo.move_raw_ids[1].move_line_ids[0].lot_id.id, + "operation_id": self.operation_move_replace.id, + "mo_id": mo.id, + } + ) + self.assertEqual(wizard.product_qty, 1) + self.assertEqual(wizard.product_id, self.product3) + lot = wizard.lot_id + wizard.action_operate_component() + self.assertEqual(len(mo.picking_ids), 2) + self.assertEqual(mo.picking_ids[1].location_dest_id, self.destination_location) + self.assertEqual(mo.picking_ids[1].move_lines.product_id, self.product3) + self.assertEqual(mo.picking_ids[0].location_dest_id, self.source_location) + self.assertEqual(mo.picking_ids[0].move_lines.product_id, self.product3) + self.assertEqual(len(mo.move_raw_ids[1].move_line_ids), 0) + self.assertEqual(len(mo.move_raw_ids[1].move_orig_ids.move_line_ids), 0) + mo.picking_ids[1].action_assign() + mo.picking_ids[1].button_validate() + self.assertEqual( + mo.picking_ids[1].move_line_ids.location_dest_id, self.destination_location + ) + self.assertEqual(mo.picking_ids[1].move_line_ids.lot_id, lot) + self.assertEqual(len(mo.move_raw_ids[1].move_line_ids), 0) + self.assertEqual(len(mo.move_raw_ids[1].move_orig_ids.move_line_ids), 0) + mo.picking_ids[0].action_assign() + mo.picking_ids[0].button_validate() + self.assertEqual( + mo.picking_ids[0].move_line_ids.location_dest_id, self.source_location + ) + self.assertEqual(mo.picking_ids[0].move_line_ids.product_id, self.product3) + self.assertEqual(len(mo.move_raw_ids[1].move_line_ids), 0) + self.assertEqual(len(mo.move_raw_ids[1].move_orig_ids.move_line_ids), 1) + + def test_03_nothing_and_nothing(self): + nb_product_todo = 5 + serials_p2 = [] + for i in range(nb_product_todo): + serials_p2.append( + self.env["stock.production.lot"].create( + { + "name": f"lot_consumed_2_{i}", + "product_id": self.product3.id, + "company_id": self.env.company.id, + } + ) + ) + self.env["stock.quant"]._update_available_quantity( + self.product3, self.ressuply_loc1, 1, lot_id=serials_p2[-1] + ) + self.env["stock.quant"]._update_available_quantity( + self.product2, self.ressuply_loc1, 10 + ) + mo = self.MrpProduction.create( + { + "bom_id": self.bom.id, + "product_id": self.product1.id, + "product_qty": 2, + "product_uom_id": self.product1.uom_id.id, + "date_deadline": "2023-01-01 15:00:00", + "date_planned_start": "2023-01-01 15:00:00", + } + ) + mo._onchange_move_raw() + mo._onchange_move_finished() + mo.action_confirm() + mo.action_assign() + self.assertEqual(mo.move_raw_ids[0].move_line_ids.product_uom_qty, 4) + self.assertEqual(len(mo.move_raw_ids[1].move_line_ids), 2) + wizard = self.env["mrp.component.operate"].create( + { + "product_id": mo.move_raw_ids[1].product_id.id, + "lot_id": mo.move_raw_ids[1].move_line_ids[0].lot_id.id, + "operation_id": self.operation_no.id, + "mo_id": mo.id, + } + ) + self.assertEqual(wizard.product_qty, 1) + self.assertEqual(wizard.product_id, self.product3) + wizard.action_operate_component() + self.assertEqual(len(mo.picking_ids), 0) + self.assertEqual(mo.move_raw_ids[0].move_line_ids.product_uom_qty, 4) + self.assertEqual(len(mo.move_raw_ids[1].move_line_ids), 2) diff --git a/mrp_component_operation/views/mrp_component_operation_views.xml b/mrp_component_operation/views/mrp_component_operation_views.xml new file mode 100644 index 000000000..cfac9a50c --- /dev/null +++ b/mrp_component_operation/views/mrp_component_operation_views.xml @@ -0,0 +1,63 @@ + + + + view_mrp_component_operation_form + mrp.component.operation + +
+ +

+ +

+ + + + + + + + + + + +
+
+
+
+ + + Component Operation + mrp.component.operation + ir.actions.act_window + tree,form + + + + + +
diff --git a/mrp_component_operation/views/mrp_production_views.xml b/mrp_component_operation/views/mrp_production_views.xml new file mode 100644 index 000000000..ae9b7b5d6 --- /dev/null +++ b/mrp_component_operation/views/mrp_production_views.xml @@ -0,0 +1,18 @@ + + + + mrp.production.form + mrp.production + + + + + + diff --git a/mrp_component_operation/views/stock_view.xml b/mrp_component_operation/views/stock_view.xml new file mode 100644 index 000000000..6fc71d680 --- /dev/null +++ b/mrp_component_operation/views/stock_view.xml @@ -0,0 +1,16 @@ + + + + stock.location.route.form - mrp.component + + stock.location.route + + + + + + + diff --git a/mrp_component_operation/wizards/__init__.py b/mrp_component_operation/wizards/__init__.py new file mode 100644 index 000000000..b7be0886e --- /dev/null +++ b/mrp_component_operation/wizards/__init__.py @@ -0,0 +1 @@ +from . import mrp_component_operate diff --git a/mrp_component_operation/wizards/mrp_component_operate.py b/mrp_component_operation/wizards/mrp_component_operate.py new file mode 100644 index 000000000..c6ce7de76 --- /dev/null +++ b/mrp_component_operation/wizards/mrp_component_operate.py @@ -0,0 +1,160 @@ +# Copyright 2022 ForgeFlow S.L. (https://www.forgeflow.com) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from odoo import _, api, fields, models +from odoo.exceptions import UserError, ValidationError + + +class MrpComponentOperate(models.Model): + _name = "mrp.component.operate" + _description = "Component Operate" + + product_id = fields.Many2one("product.product", required=True) + + tracking = fields.Selection( + string="Product Tracking", readonly=True, related="product_id.tracking" + ) + + product_qty = fields.Float( + "Quantity", default=1.0, required=True, digits="Product Unit of Measure" + ) + + lot_id = fields.Many2one("stock.production.lot") + + mo_id = fields.Many2one("mrp.production", ondelete="cascade", required=True) + + operation_id = fields.Many2one("mrp.component.operation", required=True) + + incoming_operation = fields.Selection( + related="operation_id.incoming_operation", + required=True, + ) + + outgoing_operation = fields.Selection( + related="operation_id.outgoing_operation", + required=True, + ) + + @api.onchange("operation_id") + def _onchange_operation_id(self): + for rec in self: + rec.incoming_operation = rec.operation_id.incoming_operation + rec.outgoing_operation = rec.operation_id.outgoing_operation + + def _run_incoming_operations(self): + res = [] + if self.incoming_operation == "replace": + res = self._run_procurement( + self.operation_id.source_route_id, self.operation_id.source_location_id + ) + move = self.mo_id.move_raw_ids.filtered( + lambda x: x.product_id == self.product_id + ) + filtered_pickings = self.mo_id.picking_ids.filtered( + lambda x: x.location_dest_id == self.operation_id.source_location_id + ) + move.move_orig_ids |= filtered_pickings[ + len(filtered_pickings) - 1 + ].move_ids_without_package + elif self.incoming_operation == "no": + res = [] + return res + + def _run_outgoing_operations(self): + res = [] + if self.outgoing_operation == "scrap": + res = self._create_scrap() + elif self.outgoing_operation == "move": + res = self._run_procurement( + self.operation_id.destination_route_id, + self.operation_id.destination_location_id, + ) + move = self.mo_id.move_raw_ids.move_line_ids.filtered( + lambda x: x.product_id == self.product_id + and (x.lot_id == self.lot_id or self.lot_id is False) + ) + if move.product_uom_qty == self.product_qty: + move.unlink() + else: + move.write( + { + "product_uom_qty": (move.product_uom_qty - self.product_qty), + } + ) + move.move_id._recompute_state() + elif self.outgoing_operation == "no": + res = [] + return res + + def _create_scrap(self): + scrap = self.env["stock.scrap"].create( + { + "origin": self.mo_id.name, + "product_id": self.product_id.id, + "lot_id": self.lot_id.id, + "scrap_qty": self.product_qty, + "product_uom_id": self.product_id.product_tmpl_id.uom_id.id, + "location_id": self.operation_id.source_location_id.id, + "scrap_location_id": self.operation_id.scrap_location_id.id, + "create_date": fields.Datetime.now(), + "company_id": self.env.company.id, + } + ) + self.mo_id.scrap_ids |= scrap + scrap.action_validate() + return scrap + + def _run_procurement(self, route, dest_location): + """Method called when the user clicks on create picking""" + procurements = [] + errors = [] + procurement = self._prepare_procurement(route, dest_location) + procurements.append(procurement) + try: + self.env["procurement.group"].run(procurements) + except UserError as error: + errors.append(error.args[0]) + if errors: + raise UserError("\n".join(errors)) + return procurements + + @api.model + def _get_procurement_data(self, route, dest_location): + if not route: + raise ValidationError(_("No route specified")) + procurement_data = { + "name": self.mo_id and self.mo_id.name, + "group_id": self.mo_id.procurement_group_id, + "origin": self.mo_id.name, + "date_planned": fields.Datetime.now(), + "product_id": self.product_id.id, + "product_qty": self.product_qty, + "product_uom": self.product_id.product_tmpl_id.uom_id.id, + "location_id": dest_location.id, + "route_ids": route, + "company_id": self.env.company.id, + "mrp_production_ids": self.mo_id.id, + } + if self.lot_id and route != self.operation_id.source_route_id: + procurement_data["lot_id"] = self.lot_id.id + return procurement_data + + @api.model + def _prepare_procurement(self, route, dest_location): + values = self._get_procurement_data(route, dest_location) + procurement = self.env["procurement.group"].Procurement( + self.product_id, + self.product_qty, + self.product_id.product_tmpl_id.uom_id, + dest_location, + values.get("origin"), + values.get("origin"), + self.env.company, + values, + ) + return procurement + + def action_operate_component(self): + self._run_outgoing_operations() + self._run_incoming_operations() + return diff --git a/mrp_component_operation/wizards/mrp_component_operate_wizard.xml b/mrp_component_operation/wizards/mrp_component_operate_wizard.xml new file mode 100644 index 000000000..cf481d434 --- /dev/null +++ b/mrp_component_operation/wizards/mrp_component_operate_wizard.xml @@ -0,0 +1,50 @@ + + + + view_mrp_component_operate_form + mrp.component.operate + +
+ + + + + + + + + + + + + + + +
+
+
+
diff --git a/setup/mrp_component_operation/odoo/addons/mrp_component_operation b/setup/mrp_component_operation/odoo/addons/mrp_component_operation new file mode 120000 index 000000000..148191eca --- /dev/null +++ b/setup/mrp_component_operation/odoo/addons/mrp_component_operation @@ -0,0 +1 @@ +../../../../mrp_component_operation \ No newline at end of file diff --git a/setup/mrp_component_operation/setup.py b/setup/mrp_component_operation/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/mrp_component_operation/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)