diff --git a/repair_picking/README.rst b/repair_picking/README.rst new file mode 100644 index 000000000..e69de29bb diff --git a/repair_picking/__init__.py b/repair_picking/__init__.py new file mode 100644 index 000000000..0ee8b5073 --- /dev/null +++ b/repair_picking/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import tests diff --git a/repair_picking/__manifest__.py b/repair_picking/__manifest__.py new file mode 100644 index 000000000..e1d9ba86e --- /dev/null +++ b/repair_picking/__manifest__.py @@ -0,0 +1,20 @@ +# Copyright 2023 ForgeFlow S.L. (https://www.forgeflow.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +{ + "name": "Repair Picking", + "version": "14.0.1.0.0", + "author": "ForgeFlow, Odoo Community Association (OCA)", + "category": "Repair", + "website": "https://github.com/OCA/manufacture", + "summary": "Enhanced repair order management with pickings " + "for adding and removing components", + "depends": ["repair", "stock_move_forced_lot", "repair_stock_move"], + "data": [ + "views/stock_warehouse_views.xml", + "views/repair_order_view.xml", + ], + "license": "LGPL-3", + "installable": True, + "auto_install": False, +} diff --git a/repair_picking/models/__init__.py b/repair_picking/models/__init__.py new file mode 100644 index 000000000..c159ea720 --- /dev/null +++ b/repair_picking/models/__init__.py @@ -0,0 +1,3 @@ +from . import stock_warehouse +from . import repair +from . import stock_rule diff --git a/repair_picking/models/repair.py b/repair_picking/models/repair.py new file mode 100644 index 000000000..69c84edbc --- /dev/null +++ b/repair_picking/models/repair.py @@ -0,0 +1,231 @@ +# Copyright 2023 ForgeFlow S.L. (https://www.forgeflow.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import api, fields, models +from odoo.exceptions import UserError + + +class RepairOrder(models.Model): + _inherit = "repair.order" + + @api.model + def _get_default_location_id(self): + warehouse = self.env["stock.warehouse"].search( + [("company_id", "=", self.env.company.id)], limit=1 + ) + return ( + warehouse.repair_location_id.id + if warehouse and warehouse.repair_location_id + else False + ) + + # Changing default value on existing field + location_id = fields.Many2one( + default=_get_default_location_id, + ) + + picking_ids = fields.Many2many( + comodel_name="stock.picking", + compute="_compute_picking_ids", + copy=False, + string="Pickings associated to this repair order", + ) + picking_count = fields.Integer( + string="Transfers", copy=False, compute="_compute_picking_ids" + ) + procurement_group_id = fields.Many2one( + "procurement.group", "Procurement Group", copy=False + ) + + def action_view_pickings(self): + self.ensure_one() + action = self.env["ir.actions.actions"]._for_xml_id( + "stock.action_picking_tree_all" + ) + action["domain"] = [("id", "in", self.picking_ids.ids)] + return action + + def _compute_picking_ids(self): + for order in self: + moves = self.env["stock.move"].search( + [("repair_line_id", "in", order.operations.ids)] + ) + order.picking_ids = moves.mapped("picking_id") + order.picking_count = len(moves.mapped("picking_id")) + + def action_repair_cancel(self): + res = super().action_repair_cancel() + for picking in self.picking_ids: + if picking.state not in ["cancel", "done"]: + picking.action_cancel() + return res + + def _action_launch_stock_rule(self, repair_lines): + for line in repair_lines: + self._run_procurement_repair(line) + return True + + def _run_procurement_repair(self, line): + procurements = [] + errors = [] + procurement = self._prepare_procurement_repair(line) + 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 True + + @api.model + def _get_procurement_data_repair(self, line): + warehouse = self.location_id.get_warehouse() + if not self.procurement_group_id: + group_id = self.env["procurement.group"].create({"name": self.name}) + self.procurement_group_id = group_id + procurement_data = { + "name": self.name, + "group_id": self.procurement_group_id, + "origin": self.name, + "date_planned": fields.Datetime.now(), + "product_id": line.product_id.id, + "product_qty": line.product_uom_qty, + "product_uom": line.product_uom.id, + "company_id": self.company_id, + "warehouse_id": warehouse, + "repair_line_id": line.id, + } + if line.lot_id: + procurement_data["lot_id"] = line.lot_id.id + if line.type == "remove": + procurement_data[ + "source_repair_location_id" + ] = line.repair_id.location_id.id + return procurement_data + + @api.model + def _prepare_procurement_repair(self, line): + values = self._get_procurement_data_repair(line) + warehouse = self.location_id.get_warehouse() + location = ( + self.location_id + if line.type == "add" + else warehouse.remove_c_type_id.default_location_dest_id + ) + procurement = self.env["procurement.group"].Procurement( + line.product_id, + line.product_uom_qty, + line.product_uom, + location, + values.get("origin"), + values.get("origin"), + self.company_id, + values, + ) + return procurement + + def _update_stock_moves_and_picking_state(self): + for repair in self: + for picking in repair.picking_ids: + if picking.location_dest_id.id == self.location_id.id: + for move_line in picking.move_ids_without_package: + stock_moves = repair.stock_move_ids.filtered( + lambda m: m.product_id.id + == repair.operations.filtered( + lambda l: l.type == "add" + and l.product_id.id == m.product_id.id + ).product_id.id + and m.location_id.id == self.location_id.id + ) + if stock_moves: + stock_moves[0].write( + { + "move_orig_ids": [(4, move_line.id)], + "state": "waiting", + } + ) + if picking.location_id.id == self.location_id.id: + for move_line in picking.move_ids_without_package: + stock_moves = repair.stock_move_ids.filtered( + lambda m: m.product_id.id + == repair.operations.filtered( + lambda l: l.type == "remove" + and l.product_id.id == m.product_id.id + ).product_id.id + and m.location_dest_id.id == self.location_id.id + ) + if stock_moves: + move_line.write( + { + "move_orig_ids": [(4, stock_moves[0].id)], + "state": "waiting", + } + ) + # We are using write here because + # the repair_stock_move module does not use stock rules. + # As a result, we manually link the stock moves + # and then recompute the state of the picking. + picking._compute_state() + + def action_repair_confirm(self): + res = super().action_repair_confirm() + for repair in self: + warehouse = repair.location_id.get_warehouse() + if warehouse.repair_steps in ["2_steps", "3_steps"]: + repair._action_launch_stock_rule( + repair.operations.filtered(lambda l: l.type == "add"), + ) + if warehouse.repair_steps == "3_steps": + repair._action_launch_stock_rule( + repair.operations.filtered(lambda l: l.type == "remove"), + ) + repair._update_stock_moves_and_picking_state() + return res + + @api.onchange("location_id") + def _onchange_location_id(self): + warehouse = self.location_id.get_warehouse() + for line in self.operations: + if line.type == "add": + line.location_id = self.location_id + elif line.type == "remove" and warehouse.repair_steps == "3_steps": + line.location_dest_id = self.location_id + + +class RepairLine(models.Model): + _inherit = "repair.line" + + @api.onchange("type", "product_id") + def onchange_operation_type(self): + super().onchange_operation_type() + production_location = self.env["stock.location"].search( + [("usage", "=", "production")], limit=1 + ) + warehouse = self.repair_id.location_id.get_warehouse() + if self.type == "add": + self.write( + { + "location_id": self.repair_id.location_id.id, + "location_dest_id": production_location.id, + } + ) + elif self.type == "remove": + self.write({"location_id": production_location.id}) + if warehouse.repair_steps in ["1_step", "2_steps"]: + scrap_location = self.env["stock.location"].search( + [ + ("scrap_location", "=", True), + ("company_id", "=", warehouse.company_id.id), + ], + limit=1, + ) + self.write({"location_dest_id": scrap_location.id}) + else: + self.write({"location_dest_id": self.repair_id.location_id.id}) + + def create(self, vals): + line = super().create(vals) + if line.repair_id.state in ["confirmed", "under_repair", "ready"]: + line.repair_id._action_launch_stock_rule(line) + return line diff --git a/repair_picking/models/stock_rule.py b/repair_picking/models/stock_rule.py new file mode 100644 index 000000000..76a9edade --- /dev/null +++ b/repair_picking/models/stock_rule.py @@ -0,0 +1,27 @@ +# Copyright 2023 ForgeFlow S.L. (https://www.forgeflow.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import api, models + + +class StockRule(models.Model): + _inherit = "stock.rule" + + def _get_custom_move_fields(self): + fields = super(StockRule, self)._get_custom_move_fields() + # Fields is added on `repair_stock_move` module. + fields += ["repair_line_id"] + return fields + + +class ProcurementGroup(models.Model): + _inherit = "procurement.group" + + @api.model + def _get_rule_domain(self, location, values): + domain = super(ProcurementGroup, self)._get_rule_domain(location, values) + if values.get("source_repair_location_id"): + domain.append( + ("location_src_id", "=", values.get("source_repair_location_id")) + ) + return domain diff --git a/repair_picking/models/stock_warehouse.py b/repair_picking/models/stock_warehouse.py new file mode 100644 index 000000000..cfcbb9fa3 --- /dev/null +++ b/repair_picking/models/stock_warehouse.py @@ -0,0 +1,184 @@ +# Copyright 2023 ForgeFlow S.L. (https://www.forgeflow.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class StockWarehouse(models.Model): + _inherit = "stock.warehouse" + + repair_steps = fields.Selection( + [ + ("1_step", "Repair"), + ("2_steps", "Pick component, repair"), + ("3_steps", "Pick component, repair, store removed component"), + ], + string="Repair Steps", + default="1_step", + ) + add_c_type_id = fields.Many2one( + "stock.picking.type", string="Add Component to Repair" + ) + remove_c_type_id = fields.Many2one( + "stock.picking.type", string="Remove component from Repair" + ) + repair_route_id = fields.Many2one("stock.location.route", string="Repair Route") + repair_location_id = fields.Many2one("stock.location", string="Repair Location") + + def update_picking_types(self, repair_steps, repair_location_id): + if repair_steps in ["2_steps", "3_steps"]: + self.add_c_type_id.active = True + if repair_steps == "1_step": + self.add_c_type_id.active = False + if repair_steps == "3_steps": + self.remove_c_type_id.active = True + if repair_steps in ["1_step", "2_steps"]: + self.remove_c_type_id.active = False + if repair_location_id: + self.add_c_type_id.write({"default_location_dest_id": repair_location_id}) + self.remove_c_type_id.write({"default_location_src_id": repair_location_id}) + + def update_repair_routes(self, repair_steps, repair_location_id): + if repair_steps == "2_steps" or repair_steps == "3_steps": + self.repair_route_id.active = True + existing_rule = ( + self.env["stock.rule"] + .with_context(active_test=False) + .search( + [ + ("picking_type_id", "=", self.add_c_type_id.id), + ("route_id", "=", self.repair_route_id.id), + ], + limit=1, + ) + ) + existing_rule.active = True + if repair_steps == "1_step": + for rule in self.repair_route_id.rule_ids: + rule.active = False + self.repair_route_id.active = False + if repair_location_id: + self.repair_route_id.rule_ids.filtered( + lambda r: r.picking_type_id == self.add_c_type_id + ).write({"location_id": repair_location_id}) + self.repair_route_id.rule_ids.filtered( + lambda r: r.picking_type_id == self.remove_c_type_id + ).write({"location_src_id": repair_location_id}) + if repair_steps in ["1_step", "2_steps"]: + self.repair_route_id.rule_ids.filtered( + lambda r: r.picking_type_id == self.remove_c_type_id + ).active = False + + def write(self, vals): + res = super(StockWarehouse, self).write(vals) + for warehouse in self: + repair_steps = vals.get("repair_steps") + repair_location_id = vals.get("repair_location_id") + if repair_steps: + if repair_steps in ["3_steps", "2_steps"]: + warehouse._create_repair_picking_types() + warehouse._create_repair_route() + if repair_steps == "3_steps": + warehouse._create_remove_rule() + if repair_steps or repair_location_id: + warehouse.update_picking_types(repair_steps, repair_location_id) + warehouse.update_repair_routes(repair_steps, repair_location_id) + return res + + def _create_repair_picking_types(self): + for warehouse in self: + repair_location_id = ( + warehouse.repair_location_id.id or warehouse.lot_stock_id.id + ) + if not warehouse.add_c_type_id: + pbr_type = self.env["stock.picking.type"].create( + { + "name": "Add Component to Repair", + "code": "internal", + "sequence_code": "ACR", + "warehouse_id": warehouse.id, + "default_location_src_id": warehouse.lot_stock_id.id, + "default_location_dest_id": repair_location_id, + "company_id": warehouse.company_id.id, + } + ) + warehouse.add_c_type_id = pbr_type.id + else: + warehouse.add_c_type_id.write( + {"default_location_dest_id": repair_location_id} + ) + if not warehouse.remove_c_type_id: + par_type = self.env["stock.picking.type"].create( + { + "name": "Remove component from Repair", + "code": "internal", + "sequence_code": "RCR", + "warehouse_id": warehouse.id, + "default_location_src_id": repair_location_id, + "default_location_dest_id": warehouse.view_location_id.id, + "company_id": warehouse.company_id.id, + } + ) + warehouse.remove_c_type_id = par_type.id + else: + warehouse.remove_c_type_id.write( + {"default_location_src_id": repair_location_id} + ) + + def _create_repair_route(self): + for warehouse in self: + if not warehouse.repair_route_id: + route = self.env["stock.location.route"].create( + { + "name": "Repair Route for %s" % warehouse.name, + "warehouse_selectable": True, + "product_selectable": False, + "warehouse_ids": [(6, 0, warehouse.ids)], + "company_id": warehouse.company_id.id, + } + ) + warehouse.repair_route_id = route.id + self.env["stock.rule"].create( + { + "name": "Add Component to Repair", + "picking_type_id": warehouse.add_c_type_id.id, + "route_id": route.id, + "location_src_id": warehouse.lot_stock_id.id, + "location_id": warehouse.repair_location_id.id + or warehouse.view_location_id.id, + "action": "pull", + "company_id": warehouse.company_id.id, + "warehouse_id": warehouse.id, + } + ) + + def _create_remove_rule(self): + for warehouse in self: + existing_rule = ( + self.env["stock.rule"] + .with_context(active_test=False) + .search( + [ + ("picking_type_id", "=", warehouse.remove_c_type_id.id), + ("route_id", "=", warehouse.repair_route_id.id), + ], + limit=1, + ) + ) + if not existing_rule: + self.env["stock.rule"].create( + { + "name": "Remove component from Repair", + "picking_type_id": warehouse.remove_c_type_id.id, + "route_id": warehouse.repair_route_id.id, + "location_src_id": warehouse.repair_location_id.id + or warehouse.view_location_id.id, + "location_id": warehouse.view_location_id.id, + "action": "pull", + "company_id": warehouse.company_id.id, + "warehouse_id": warehouse.id, + "active": True, + } + ) + else: + existing_rule.active = True diff --git a/repair_picking/readme/CONTRIBUTORS.rst b/repair_picking/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..1023a5dd6 --- /dev/null +++ b/repair_picking/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Joan Sisquella diff --git a/repair_picking/readme/DESCRIPTION.rst b/repair_picking/readme/DESCRIPTION.rst new file mode 100644 index 000000000..1c1f9b63f --- /dev/null +++ b/repair_picking/readme/DESCRIPTION.rst @@ -0,0 +1,12 @@ +Repair Management Extension for Odoo +===================================== + +This module extends the repair management functionality in Odoo, providing additional options for handling the repair process in a more customizable and efficient way. It allows the configuration of repair steps based on specific business requirements and adds new picking types for managing component addition and removal in repair orders. + +Main Features: +-------------- +- Customize repair steps: Choose between a 1-step, 2-step, or 3-step repair process. +- Add and remove components during the repair process using separate picking types. +- Associate repair orders with pickings for improved traceability. +- Automatic creation of pickings and procurement routes based on the selected repair steps. +- Manage repair locations and routes more efficiently with warehouse settings. diff --git a/repair_picking/readme/USAGE.rst b/repair_picking/readme/USAGE.rst new file mode 100644 index 000000000..19650f3a4 --- /dev/null +++ b/repair_picking/readme/USAGE.rst @@ -0,0 +1,23 @@ +Getting Started +=============== + +1. Install the module in your Odoo instance. + +2. Navigate to Inventory > Configuration > Warehouses, and select a warehouse. + +3. In the "Repair Steps" field, choose between "Repair", "Pick component, repair", or "Pick component, repair, store removed component" to define the repair process. + +4. Define the "Repair Location", "Add Component to Repair" picking type, "Remove component from Repair" picking type, and "Repair Route" as needed. + +Using the Repair Management Extension +===================================== + +1. Navigate to Repair > Repair Orders and create a new repair order. + +2. In the Operations tab, add components to be added or removed during the repair process. + +3. Confirm the repair order. This will automatically generate the necessary pickings based on the configured repair steps. + +4. Process the pickings as required during the repair process. + +5. If the repair order needs to be canceled, all associated pickings that are not in "cancel" or "done" state will also be canceled automatically. diff --git a/repair_picking/tests/__init__.py b/repair_picking/tests/__init__.py new file mode 100644 index 000000000..e6cb600cc --- /dev/null +++ b/repair_picking/tests/__init__.py @@ -0,0 +1,2 @@ +from . import test_stock_repair_warehouse +from . import test_stock_repair_order diff --git a/repair_picking/tests/test_stock_repair_order.py b/repair_picking/tests/test_stock_repair_order.py new file mode 100644 index 000000000..ea9bb71df --- /dev/null +++ b/repair_picking/tests/test_stock_repair_order.py @@ -0,0 +1,266 @@ +# Copyright 2023 ForgeFlow S.L. (https://www.forgeflow.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo.tests import common + + +class TestStockRepairOrder(common.SavepointCase): + @classmethod + def setUp(cls): + super(TestStockRepairOrder, cls).setUpClass() + + cls.repair_model = cls.env["repair.order"] + cls.repair_line_model = cls.env["repair.line"] + cls.product_model = cls.env["product.product"] + cls.stock_location_model = cls.env["stock.location"] + cls.warehouse_model = cls.env["stock.warehouse"] + cls.company = cls.env["res.company"].create( + { + "name": "My Test Company", + } + ) + cls.warehouse = cls.warehouse_model.create( + { + "name": "Test Warehouse", + "code": "TW", + "company_id": cls.company.id, + } + ) + + cls.product1 = cls.product_model.create( + { + "name": "Product 1", + "type": "product", + "company_id": cls.company.id, + } + ) + cls.product2 = cls.product_model.create( + { + "name": "Product 2", + "type": "product", + "company_id": cls.company.id, + } + ) + cls.repair_location = cls.stock_location_model.create( + { + "name": "Repair Location", + "usage": "internal", + "location_id": cls.warehouse.view_location_id.id, + "company_id": cls.company.id, + } + ) + cls.production_location = cls.stock_location_model.create( + { + "name": "Production Location", + "usage": "production", + "company_id": cls.company.id, + } + ) + cls.env["stock.quant"].create( + { + "product_id": cls.product1.id, + "location_id": cls.repair_location.id, + "quantity": 10, + } + ) + cls.env["stock.quant"].create( + { + "product_id": cls.product2.id, + "location_id": cls.warehouse.lot_stock_id.id, + "quantity": 10, + } + ) + + def test_1step_repair_order_flow(self): + self.warehouse.write( + { + "repair_steps": "1_step", + "repair_location_id": self.repair_location.id, + } + ) + repair_order = self.repair_model.create( + { + "product_id": self.product1.id, + "product_uom": self.product1.uom_id.id, + "location_id": self.repair_location.id, + "company_id": self.company.id, + } + ) + self.repair_line_model.create( + { + "name": "Repair Line 1", + "repair_id": repair_order.id, + "product_id": self.product2.id, + "type": "add", + "product_uom_qty": 1, + "product_uom": self.product2.uom_id.id, + "price_unit": 1, + "location_id": self.repair_location.id, + "location_dest_id": self.production_location.id, + } + ) + repair_order.action_repair_confirm() + self.assertEqual(repair_order.state, "confirmed") + repair_order.action_repair_ready() + self.assertEqual(repair_order.state, "ready") + + def test_2steps_repair_order_flow(self): + self.warehouse.write( + { + "repair_steps": "2_steps", + "repair_location_id": self.repair_location.id, + } + ) + self.product2.write( + {"route_ids": [(6, 0, [self.warehouse.repair_route_id.id])]} + ) + repair_order = self.repair_model.create( + { + "product_id": self.product1.id, + "product_uom": self.product1.uom_id.id, + "location_id": self.repair_location.id, + "company_id": self.company.id, + } + ) + self.repair_line_model.create( + { + "name": "Repair Line 2", + "repair_id": repair_order.id, + "product_id": self.product2.id, + "type": "add", + "product_uom_qty": 1, + "product_uom": self.product2.uom_id.id, + "price_unit": 1, + "location_id": self.repair_location.id, + "location_dest_id": self.production_location.id, + } + ) + repair_order.action_repair_confirm() + self.assertEqual(repair_order.state, "confirmed") + self.assertTrue(repair_order.picking_ids) + self.assertEqual(len(repair_order.picking_ids), 1) + + def test_3steps_repair_order_flow(self): + self.warehouse.write( + { + "repair_steps": "3_steps", + "repair_location_id": self.repair_location.id, + } + ) + self.product2.write( + {"route_ids": [(6, 0, [self.warehouse.repair_route_id.id])]} + ) + repair_order = self.repair_model.create( + { + "product_id": self.product1.id, + "product_uom": self.product1.uom_id.id, + "location_id": self.repair_location.id, + "company_id": self.company.id, + } + ) + self.repair_line_model.create( + { + "name": "Repair Line 3", + "repair_id": repair_order.id, + "product_id": self.product2.id, + "type": "add", + "product_uom_qty": 1, + "product_uom": self.product2.uom_id.id, + "price_unit": 1, + "location_id": self.repair_location.id, + "location_dest_id": self.production_location.id, + } + ) + self.repair_line_model.create( + { + "name": "Repair Line 4", + "repair_id": repair_order.id, + "product_id": self.product2.id, + "type": "remove", + "product_uom_qty": 1, + "product_uom": self.product2.uom_id.id, + "price_unit": 1, + "location_id": self.production_location.id, + "location_dest_id": self.repair_location.id, + } + ) + repair_order.action_repair_confirm() + self.assertEqual(repair_order.state, "confirmed") + self.assertTrue(repair_order.picking_ids) + self.assertEqual(len(repair_order.picking_ids), 2) + repair_order.action_repair_cancel() + self.assertEqual(repair_order.state, "cancel") + for picking in repair_order.picking_ids: + self.assertEqual(picking.state, "cancel") + + def test_update_related_pickings(self): + self.warehouse.write( + { + "repair_steps": "3_steps", + "repair_location_id": self.repair_location.id, + } + ) + self.product2.write( + {"route_ids": [(6, 0, [self.warehouse.repair_route_id.id])]} + ) + repair_order = self.repair_model.create( + { + "product_id": self.product1.id, + "product_uom": self.product1.uom_id.id, + "location_id": self.repair_location.id, + "company_id": self.company.id, + } + ) + self.repair_line_model.create( + { + "name": "Repair Line 3", + "repair_id": repair_order.id, + "product_id": self.product2.id, + "type": "add", + "product_uom_qty": 1, + "product_uom": self.product2.uom_id.id, + "price_unit": 1, + "location_id": self.repair_location.id, + "location_dest_id": self.production_location.id, + } + ) + repair_order.action_repair_confirm() + self.assertEqual(repair_order.state, "confirmed") + self.assertTrue(repair_order.picking_ids) + self.assertEqual(len(repair_order.picking_ids), 1) + self.assertEqual(len(repair_order.picking_ids.move_ids_without_package), 1) + self.assertEqual( + repair_order.picking_ids.move_ids_without_package.product_uom_qty, 1 + ) + self.repair_line_model.create( + { + "name": "Repair Line Add", + "repair_id": repair_order.id, + "product_id": self.product2.id, + "type": "add", + "product_uom_qty": 1, + "product_uom": self.product2.uom_id.id, + "price_unit": 1, + "location_id": self.repair_location.id, + "location_dest_id": self.production_location.id, + } + ) + self.assertEqual(len(repair_order.picking_ids), 1) + self.assertEqual(len(repair_order.picking_ids.move_ids_without_package), 1) + self.assertEqual( + repair_order.picking_ids.move_ids_without_package.product_uom_qty, 2 + ) + self.repair_line_model.create( + { + "name": "Repair Line Remove", + "repair_id": repair_order.id, + "product_id": self.product2.id, + "type": "remove", + "product_uom_qty": 1, + "product_uom": self.product2.uom_id.id, + "price_unit": 1, + "location_id": self.production_location.id, + "location_dest_id": self.repair_location.id, + } + ) + self.assertEqual(len(repair_order.picking_ids), 2) diff --git a/repair_picking/tests/test_stock_repair_warehouse.py b/repair_picking/tests/test_stock_repair_warehouse.py new file mode 100644 index 000000000..b872ed8d6 --- /dev/null +++ b/repair_picking/tests/test_stock_repair_warehouse.py @@ -0,0 +1,116 @@ +# Copyright 2023 ForgeFlow S.L. (https://www.forgeflow.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo.tests import common + + +class TestStockRepairWarehouse(common.SavepointCase): + @classmethod + def setUpClass(cls): + super(TestStockRepairWarehouse, cls).setUpClass() + + cls.warehouse_obj = cls.env["stock.warehouse"] + cls.product_obj = cls.env["product.product"] + + cls.product_1 = cls.product_obj.create( + { + "name": "Product 1", + "type": "product", + } + ) + + cls.warehouse_1 = cls.warehouse_obj.create( + { + "name": "Test Warehouse", + "code": "TWH", + "repair_steps": "1_step", + } + ) + + def test_01_warehouse_creation(self): + self.assertEqual(self.warehouse_1.repair_steps, "1_step") + + def test_02_update_repair_steps(self): + self.warehouse_1.repair_steps = "2_steps" + + self.assertEqual(self.warehouse_1.repair_steps, "2_steps") + self.assertTrue(self.warehouse_1.add_c_type_id.active) + self.assertFalse(self.warehouse_1.remove_c_type_id.active) + self.assertTrue(self.warehouse_1.repair_route_id.active) + + def test_03_update_repair_steps_to_3_steps(self): + self.warehouse_1.repair_steps = "3_steps" + + self.assertEqual(self.warehouse_1.repair_steps, "3_steps") + self.assertTrue(self.warehouse_1.add_c_type_id.active) + self.assertTrue(self.warehouse_1.remove_c_type_id.active) + self.assertTrue(self.warehouse_1.repair_route_id.active) + + def test_04_reverse_and_update_repair_steps(self): + self.warehouse_1.repair_steps = "1_step" + self.warehouse_1.repair_steps = "2_steps" + self.assertEqual(self.warehouse_1.repair_steps, "2_steps") + self.assertTrue(self.warehouse_1.add_c_type_id.active) + self.assertFalse(self.warehouse_1.remove_c_type_id.active) + self.assertTrue(self.warehouse_1.repair_route_id.active) + add_rule = self.env["stock.rule"].search( + [ + ("picking_type_id", "=", self.warehouse_1.add_c_type_id.id), + ("route_id", "=", self.warehouse_1.repair_route_id.id), + ] + ) + self.assertTrue(add_rule.active) + + remove_rule = self.env["stock.rule"].search( + [ + ("picking_type_id", "=", self.warehouse_1.remove_c_type_id.id), + ("route_id", "=", self.warehouse_1.repair_route_id.id), + ] + ) + self.assertFalse(remove_rule) + + self.warehouse_1.repair_steps = "3_steps" + self.assertEqual(self.warehouse_1.repair_steps, "3_steps") + self.assertTrue(self.warehouse_1.add_c_type_id.active) + self.assertTrue(self.warehouse_1.remove_c_type_id.active) + self.assertTrue(self.warehouse_1.repair_route_id.active) + add_rule = self.env["stock.rule"].search( + [ + ("picking_type_id", "=", self.warehouse_1.add_c_type_id.id), + ("route_id", "=", self.warehouse_1.repair_route_id.id), + ] + ) + self.assertTrue(add_rule.active) + remove_rule = self.env["stock.rule"].search( + [ + ("picking_type_id", "=", self.warehouse_1.remove_c_type_id.id), + ("route_id", "=", self.warehouse_1.repair_route_id.id), + ] + ) + self.assertTrue(remove_rule.active) + + self.warehouse_1.repair_steps = "2_steps" + self.assertEqual(self.warehouse_1.repair_steps, "2_steps") + self.assertTrue(self.warehouse_1.add_c_type_id.active) + self.assertFalse(self.warehouse_1.remove_c_type_id.active) + self.assertTrue(self.warehouse_1.repair_route_id.active) + add_rule = self.env["stock.rule"].search( + [ + ("picking_type_id", "=", self.warehouse_1.add_c_type_id.id), + ("route_id", "=", self.warehouse_1.repair_route_id.id), + ] + ) + self.assertTrue(add_rule.active) + + remove_rule = self.env["stock.rule"].search( + [ + ("picking_type_id", "=", self.warehouse_1.remove_c_type_id.id), + ("route_id", "=", self.warehouse_1.repair_route_id.id), + ] + ) + self.assertFalse(remove_rule) + + self.warehouse_1.repair_steps = "1_step" + self.assertFalse(self.warehouse_1.add_c_type_id.active) + self.assertFalse(self.warehouse_1.remove_c_type_id.active) + self.assertFalse(self.warehouse_1.repair_route_id.active) diff --git a/repair_picking/views/repair_order_view.xml b/repair_picking/views/repair_order_view.xml new file mode 100644 index 000000000..7c25297b9 --- /dev/null +++ b/repair_picking/views/repair_order_view.xml @@ -0,0 +1,22 @@ + + + + repair.order.form - repair_stock_move - custom + repair.order + + + + + + + + diff --git a/repair_picking/views/stock_warehouse_views.xml b/repair_picking/views/stock_warehouse_views.xml new file mode 100644 index 000000000..1903200d9 --- /dev/null +++ b/repair_picking/views/stock_warehouse_views.xml @@ -0,0 +1,26 @@ + + + + Stock Warehouse Inherit Repair + stock.warehouse + + + + + + + + + + + + + + + diff --git a/setup/repair_picking/odoo/addons/repair_picking b/setup/repair_picking/odoo/addons/repair_picking new file mode 120000 index 000000000..e73932b02 --- /dev/null +++ b/setup/repair_picking/odoo/addons/repair_picking @@ -0,0 +1 @@ +../../../../repair_picking \ No newline at end of file diff --git a/setup/repair_picking/setup.py b/setup/repair_picking/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/repair_picking/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)