diff --git a/repair_picking/README.rst b/repair_picking/README.rst new file mode 100644 index 000000000..6f286f38d --- /dev/null +++ b/repair_picking/README.rst @@ -0,0 +1,105 @@ +============== +Repair Picking +============== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:7a770249391cf216da563d0a64febf0abd30c9fdae7325803988570d96d8d2f3 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fmanufacture-lightgray.png?logo=github + :target: https://github.com/OCA/manufacture/tree/14.0/repair_picking + :alt: OCA/manufacture +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/manufacture-14-0/manufacture-14-0-repair_picking + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/manufacture&target_branch=14.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +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. + +The main features are: + +- 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. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +#. Navigate to Inventory > Configuration > Warehouses, and select a warehouse. +#. In the "Repair Steps" field, choose between "Repair", "Pick component, repair", or "Pick component, repair, store removed component" to define the repair process. +#. Define the "Repair Location", "Add Component to Repair" picking type, "Remove component from Repair" picking type, and "Repair Route" as needed. + +Usage +===== + +#. Navigate to Repair > Repair Orders and create a new repair order. +#. In the Operations tab, add components to be added or removed during the repair process. +#. Confirm the repair order. This will automatically generate the necessary + pickings based on the configured repair steps. +#. Process the pickings as required during the repair process. +#. If the repair order needs to be canceled, all associated pickings that are not + in "cancel" or "done" state will also be canceled automatically. + +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 to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* ForgeFlow + +Contributors +~~~~~~~~~~~~ + +* Joan Sisquella + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/manufacture `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. 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/CONFIGURE.rst b/repair_picking/readme/CONFIGURE.rst new file mode 100644 index 000000000..9ac6f605d --- /dev/null +++ b/repair_picking/readme/CONFIGURE.rst @@ -0,0 +1,3 @@ +#. Navigate to Inventory > Configuration > Warehouses, and select a warehouse. +#. In the "Repair Steps" field, choose between "Repair", "Pick component, repair", or "Pick component, repair, store removed component" to define the repair process. +#. Define the "Repair Location", "Add Component to Repair" picking type, "Remove component from Repair" picking type, and "Repair Route" as needed. 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..996f1311b --- /dev/null +++ b/repair_picking/readme/DESCRIPTION.rst @@ -0,0 +1,12 @@ +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. + +The main features are: + +- 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..e07e5799d --- /dev/null +++ b/repair_picking/readme/USAGE.rst @@ -0,0 +1,7 @@ +#. Navigate to Repair > Repair Orders and create a new repair order. +#. In the Operations tab, add components to be added or removed during the repair process. +#. Confirm the repair order. This will automatically generate the necessary + pickings based on the configured repair steps. +#. Process the pickings as required during the repair process. +#. 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/static/description/index.html b/repair_picking/static/description/index.html new file mode 100644 index 000000000..7c956335b --- /dev/null +++ b/repair_picking/static/description/index.html @@ -0,0 +1,453 @@ + + + + + +Repair Picking + + + +
+

Repair Picking

+ + +

Beta License: LGPL-3 OCA/manufacture Translate me on Weblate Try me on Runboat

+

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.

+

The main features are:

+
    +
  • 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.
  • +
+

Table of contents

+ +
+

Configuration

+
    +
  1. Navigate to Inventory > Configuration > Warehouses, and select a warehouse.
  2. +
  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. +
  5. Define the “Repair Location”, “Add Component to Repair” picking type, “Remove component from Repair” picking type, and “Repair Route” as needed.
  6. +
+
+
+

Usage

+
    +
  1. Navigate to Repair > Repair Orders and create a new repair order.
  2. +
  3. In the Operations tab, add components to be added or removed during the repair process.
  4. +
  5. Confirm the repair order. This will automatically generate the necessary +pickings based on the configured repair steps.
  6. +
  7. Process the pickings as required during the repair process.
  8. +
  9. If the repair order needs to be canceled, all associated pickings that are not +in “cancel” or “done” state will also be canceled automatically.
  10. +
+
+
+

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 to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • ForgeFlow
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/manufacture project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + 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..52f3b71f4 --- /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 setUpClass(cls): + super().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.ref("base.main_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() + repair_order._compute_picking_ids() + 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() + repair_order._compute_picking_ids() + 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() + repair_order._compute_picking_ids() + 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, + } + ) + repair_order._compute_picking_ids() + 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, +)