From 764db059ebf81c215b404f84f560e94624253c96 Mon Sep 17 00:00:00 2001 From: Mateu Griful Date: Mon, 1 Mar 2021 15:22:24 +0100 Subject: [PATCH] [ADD] repair_stock_move --- repair_stock_move/README/CONTRIBUTORS.rst | 1 + repair_stock_move/README/DESCRIPTION.rst | 9 + repair_stock_move/README/USAGE.rst | 2 + repair_stock_move/__init__.py | 5 + repair_stock_move/__manifest__.py | 19 ++ repair_stock_move/hooks.py | 23 +++ repair_stock_move/models/__init__.py | 3 + repair_stock_move/models/repair_line.py | 50 +++++ repair_stock_move/models/repair_order.py | 184 +++++++++++++++++ repair_stock_move/models/stock_move.py | 13 ++ repair_stock_move/tests/__init__.py | 4 + .../tests/test_repair_stock_move.py | 190 ++++++++++++++++++ .../views/repair_order_views.xml | 28 +++ .../odoo/addons/repair_stock_move | 1 + setup/repair_stock_move/setup.py | 6 + 15 files changed, 538 insertions(+) create mode 100644 repair_stock_move/README/CONTRIBUTORS.rst create mode 100644 repair_stock_move/README/DESCRIPTION.rst create mode 100644 repair_stock_move/README/USAGE.rst create mode 100644 repair_stock_move/__init__.py create mode 100644 repair_stock_move/__manifest__.py create mode 100644 repair_stock_move/hooks.py create mode 100644 repair_stock_move/models/__init__.py create mode 100644 repair_stock_move/models/repair_line.py create mode 100644 repair_stock_move/models/repair_order.py create mode 100644 repair_stock_move/models/stock_move.py create mode 100644 repair_stock_move/tests/__init__.py create mode 100644 repair_stock_move/tests/test_repair_stock_move.py create mode 100644 repair_stock_move/views/repair_order_views.xml create mode 120000 setup/repair_stock_move/odoo/addons/repair_stock_move create mode 100644 setup/repair_stock_move/setup.py diff --git a/repair_stock_move/README/CONTRIBUTORS.rst b/repair_stock_move/README/CONTRIBUTORS.rst new file mode 100644 index 000000000..d9a0f8120 --- /dev/null +++ b/repair_stock_move/README/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Mateu Griful diff --git a/repair_stock_move/README/DESCRIPTION.rst b/repair_stock_move/README/DESCRIPTION.rst new file mode 100644 index 000000000..cd1fccfba --- /dev/null +++ b/repair_stock_move/README/DESCRIPTION.rst @@ -0,0 +1,9 @@ +The purpose of this module is to modify the behaviour of the repair standard module. Achieving more flexibility in a repair process. + +The first change that it applies is the modification of the creation of stock moves in the repair flow: +* Confirm Repair: creates draft stock moves on confirmation of the repair order. +* Start Repair: confirms the stock moves. +* End Repair: completes the stock moves and ends the repair order. + +Another feature which this module includes is that it allows to add components during all the different stages of the repair process, which adjusts better to the needs of some businesses. + diff --git a/repair_stock_move/README/USAGE.rst b/repair_stock_move/README/USAGE.rst new file mode 100644 index 000000000..6a28bb3e0 --- /dev/null +++ b/repair_stock_move/README/USAGE.rst @@ -0,0 +1,2 @@ +Follow the Odoo standard repair module flow. + diff --git a/repair_stock_move/__init__.py b/repair_stock_move/__init__.py new file mode 100644 index 000000000..0a185fc16 --- /dev/null +++ b/repair_stock_move/__init__.py @@ -0,0 +1,5 @@ +# Copyright (C) 2017-20 ForgeFlow S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + +from . import models +from .hooks import post_load_hook diff --git a/repair_stock_move/__manifest__.py b/repair_stock_move/__manifest__.py new file mode 100644 index 000000000..ed2dd9e5d --- /dev/null +++ b/repair_stock_move/__manifest__.py @@ -0,0 +1,19 @@ +# Copyright (C) 2021 ForgeFlow S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + +{ + "name": "Repair Stock Move", + "version": "14.0.1.0.0", + "license": "LGPL-3", + "category": "RMA", + "summary": "Ongoing Repair Stock Moves Definition in odoo", + "author": "ForgeFlow, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/manufacture", + "depends": ["repair"], + "data": [ + "views/repair_order_views.xml", + ], + "post_load": "post_load_hook", + "installable": True, + "application": False, +} diff --git a/repair_stock_move/hooks.py b/repair_stock_move/hooks.py new file mode 100644 index 000000000..ec18663e0 --- /dev/null +++ b/repair_stock_move/hooks.py @@ -0,0 +1,23 @@ +# Copyright 2021 ForgeFlow, S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). + +from odoo.addons.repair.models.repair import Repair + + +# flake8: noqa: C901 +def post_load_hook(): + def action_repair_end_new(self): + self.write({"repaired": True}) + self.move_id._action_assign() + self.move_id._action_done() + for operation in self.operations: + operation.move_id._action_assign() + operation.move_id._action_done() + vals = {"state": "done"} + if not self.invoice_id and self.invoice_method == "after_repair": + vals["state"] = "2binvoiced" + self.write(vals) + + if not hasattr(Repair, "action_repair_end_original"): + Repair.action_repair_end_original = Repair.action_repair_end + Repair.action_repair_end = action_repair_end_new diff --git a/repair_stock_move/models/__init__.py b/repair_stock_move/models/__init__.py new file mode 100644 index 000000000..3e8851515 --- /dev/null +++ b/repair_stock_move/models/__init__.py @@ -0,0 +1,3 @@ +from . import repair_order +from . import repair_line +from . import stock_move diff --git a/repair_stock_move/models/repair_line.py b/repair_stock_move/models/repair_line.py new file mode 100644 index 000000000..e21f1febb --- /dev/null +++ b/repair_stock_move/models/repair_line.py @@ -0,0 +1,50 @@ +# Copyright (C) 2021 ForgeFlow S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + +from odoo import api, fields, models + + +class RepairLine(models.Model): + _inherit = "repair.line" + + stock_move_ids = fields.One2many( + comodel_name="stock.move", + inverse_name="repair_line_id", + ) + + def create_stock_move(self): + self.ensure_one() + move = self.env["stock.move"].create( + { + "name": self.repair_id.name, + "product_id": self.product_id.id, + "product_uom_qty": self.product_uom_qty, + "product_uom": self.product_uom.id, + "partner_id": self.repair_id.address_id.id, # TODO: check + "location_id": self.location_id.id, + "location_dest_id": self.location_dest_id.id, + "repair_id": self.repair_id.id, + "origin": self.repair_id.name, + "company_id": self.company_id.id, + } + ) + return move + + @api.model + def create(self, vals): + res = super().create(vals) + if res and res.repair_id.state == "confirmed": + move = res.create_stock_move() + res.move_id = move + move._set_quantity_done(res.product_uom_qty) + if res and res.repair_id.state == "under_repair": + move = res.create_stock_move() + move._action_confirm() + move._set_quantity_done(res.product_uom_qty) + res.move_id = move + return res + + def unlink(self): + for rec in self: + rec.move_id.unlink() + return super().unlink() diff --git a/repair_stock_move/models/repair_order.py b/repair_stock_move/models/repair_order.py new file mode 100644 index 000000000..a73683723 --- /dev/null +++ b/repair_stock_move/models/repair_order.py @@ -0,0 +1,184 @@ +# Copyright (C) 2021 ForgeFlow S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + +from odoo import _, fields, models +from odoo.exceptions import ValidationError +from odoo.tools import float_compare + + +class RepairOrder(models.Model): + _inherit = "repair.order" + + stock_move_ids = fields.One2many( + comodel_name="stock.move", + inverse_name="repair_id", + ) + operations = fields.One2many( + comodel_name="repair.line", + inverse_name="repair_id", + string="Parts", + copy=True, + readonly=True, + states={ + "draft": [("readonly", False)], + "confirmed": [("readonly", False)], + "under_repair": [("readonly", False)], + "ready": [("readonly", False)], + }, + ) + + product_qty = fields.Float( + "Product Quantity", + default=1.0, + digits="Product Unit of Measure", + readonly=True, + required=True, + states={ + "draft": [("readonly", False)], + "confirmed": [("readonly", False)], + "under_repair": [("readonly", False)], + "ready": [("readonly", False)], + }, + ) + + fees_lines = fields.One2many( + comodel_name="repair.fee", + inverse_name="repair_id", + string="Operations", + copy=True, + readonly=True, + states={ + "draft": [("readonly", False)], + "confirmed": [("readonly", False)], + "under_repair": [("readonly", False)], + "ready": [("readonly", False)], + }, + ) + + def action_validate(self): + res = super().action_validate() + self._check_company() + self.operations._check_company() + self.fees_lines._check_company() + res = {} + precision = self.env["decimal.precision"].precision_get( + "Product Unit of Measure" + ) + Move = self.env["stock.move"] + for repair in self: + # Try to create move with the appropriate owner + owner_id = False + available_qty_owner = self.env["stock.quant"]._get_available_quantity( + repair.product_id, + repair.location_id, + repair.lot_id, + strict=True, + ) + if available_qty_owner <= 0.0: + raise ValidationError( + _("There is no stock of product: ") + repair.product_id.display_name + ) + if ( + float_compare( + available_qty_owner, repair.product_qty, precision_digits=precision + ) + >= 0 + ): + owner_id = repair.partner_id.id + + moves = self.env["stock.move"] + for operation in repair.operations: + move = operation.create_stock_move() + product_qty = move.product_uom._compute_quantity( + operation.product_uom_qty, + move.product_id.uom_id, + rounding_method="HALF-UP", + ) + available_quantity = self.env["stock.quant"]._get_available_quantity( + move.product_id, + move.location_id, + lot_id=operation.lot_id, + strict=False, + ) + move._update_reserved_quantity( + product_qty, + available_quantity, + move.location_id, + lot_id=operation.lot_id, + strict=False, + ) + move._set_quantity_done(operation.product_uom_qty) + + if operation.lot_id: + move.move_line_ids.lot_id = operation.lot_id + + moves |= move + operation.write({"move_id": move.id, "state": "draft"}) + move = Move.create( + { + "name": repair.name, + "product_id": repair.product_id.id, + "product_uom": repair.product_uom.id or repair.product_id.uom_id.id, + "product_uom_qty": repair.product_qty, + "partner_id": repair.address_id.id, + "location_id": repair.location_id.id, + "location_dest_id": repair.location_id.id, + "move_line_ids": [ + ( + 0, + 0, + { + "product_id": repair.product_id.id, + "lot_id": repair.lot_id.id, + "product_uom_qty": 0, # bypass reservation here + "product_uom_id": repair.product_uom.id + or repair.product_id.uom_id.id, + "qty_done": repair.product_qty, + "package_id": False, + "result_package_id": False, + "owner_id": owner_id, + "location_id": repair.location_id.id, # TODO:ownerstuff + "company_id": repair.company_id.id, + "location_dest_id": repair.location_id.id, + }, + ) + ], + "repair_id": repair.id, + "origin": repair.name, + "company_id": repair.company_id.id, + } + ) + consumed_lines = moves.mapped("move_line_ids") + produced_lines = move.move_line_ids + moves |= move + produced_lines.write({"consume_line_ids": [(6, 0, consumed_lines.ids)]}) + res[repair.id] = move.id + repair.move_id = move + return res + + def action_repair_start(self): + super().action_repair_start() + (self.move_id | self.operations.mapped("move_id"))._action_confirm() + + def action_open_stock_moves(self): + self.ensure_one() + stock_move_ids = self.move_id.ids + self.operations.move_id.ids + domain = [("id", "in", stock_move_ids)] + action = { + "name": _("Stock Moves"), + "view_type": "tree", + "view_mode": "list,form", + "res_model": "stock.move", + "type": "ir.actions.act_window", + "context": self.env.context, + "domain": domain, + } + return action + + def action_repair_cancel(self): + if self.move_id.state != "draft" or self.operations: + raise ValidationError( + _("Unable to cancel repair order due to already generated stock moves.") + ) + else: + super().action_repair_cancel() diff --git a/repair_stock_move/models/stock_move.py b/repair_stock_move/models/stock_move.py new file mode 100644 index 000000000..88f6f3e9f --- /dev/null +++ b/repair_stock_move/models/stock_move.py @@ -0,0 +1,13 @@ +# Copyright (C) 2021 ForgeFlow S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + +from odoo import fields, models + + +class StockMove(models.Model): + _inherit = "stock.move" + + repair_line_id = fields.Many2one( + comodel_name="repair.line", + string="Repair Line", + ) diff --git a/repair_stock_move/tests/__init__.py b/repair_stock_move/tests/__init__.py new file mode 100644 index 000000000..71b6b8bc6 --- /dev/null +++ b/repair_stock_move/tests/__init__.py @@ -0,0 +1,4 @@ +# Copyright (C) 2021 ForgeFlow S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + +from . import test_repair_stock_move diff --git a/repair_stock_move/tests/test_repair_stock_move.py b/repair_stock_move/tests/test_repair_stock_move.py new file mode 100644 index 000000000..4d0ab8d53 --- /dev/null +++ b/repair_stock_move/tests/test_repair_stock_move.py @@ -0,0 +1,190 @@ +# Copyright (C) 2021 ForgeFlow S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + +from odoo.tests import common + + +class TestRepairStockMove(common.SavepointCase): + @classmethod + def setUpClass(cls): + super(TestRepairStockMove, cls).setUpClass() + # Partners + cls.res_partner_1 = cls.env["res.partner"].create({"name": "Wood Corner"}) + cls.res_partner_address_1 = cls.env["res.partner"].create( + {"name": "Willie Burke", "parent_id": cls.res_partner_1.id} + ) + cls.res_partner_12 = cls.env["res.partner"].create({"name": "Partner 12"}) + + # Products + cls.product_1 = cls.env["product.product"].create( + {"name": "Desk Combination", "type": "product"} + ) + cls.product_2 = cls.env["product.product"].create( + {"name": "Conference Chair", "type": "product"} + ) + cls.product_3 = cls.env["product.product"].create( + {"name": "Large Cabinet", "type": "product"} + ) + cls.service = cls.env["product.product"].create( + { + "name": "Repair Services", + "type": "service", + } + ) + + # Location + cls.stock_warehouse = cls.env["stock.warehouse"].search( + [("company_id", "=", cls.env.company.id)], limit=1 + ) + cls.stock_location_14 = cls.env["stock.location"].create( + { + "name": "Shelf 2", + "location_id": cls.stock_warehouse.lot_stock_id.id, + } + ) + + # Replenish products + cls.env["stock.quant"]._update_available_quantity( + cls.product_1, cls.stock_location_14, 1 + ) + cls.env["stock.quant"]._update_available_quantity( + cls.product_2, cls.stock_location_14, 1 + ) + cls.env["stock.quant"]._update_available_quantity( + cls.product_3, cls.stock_location_14, 1 + ) + + # Repair Orders + cls.repair1 = cls.env["repair.order"].create( + { + "address_id": cls.res_partner_address_1.id, + "guarantee_limit": "2019-01-01", + "invoice_method": "none", + "user_id": False, + "product_id": cls.product_1.id, + "product_uom": cls.env.ref("uom.product_uom_unit").id, + "partner_invoice_id": cls.res_partner_address_1.id, + "location_id": cls.stock_location_14.id, + "operations": [ + ( + 0, + 0, + { + "location_dest_id": cls.product_1.property_stock_production.id, + "location_id": cls.stock_location_14.id, + "name": cls.product_1.get_product_multiline_description_sale(), + "product_id": cls.product_2.id, + "product_uom": cls.env.ref("uom.product_uom_unit").id, + "product_uom_qty": 1.0, + "price_unit": 50.0, + "state": "draft", + "type": "add", + "company_id": cls.env.company.id, + }, + ) + ], + "fees_lines": [ + ( + 0, + 0, + { + "name": cls.service.get_product_multiline_description_sale(), + "product_id": cls.service.id, + "product_uom_qty": 1.0, + "product_uom": cls.env.ref("uom.product_uom_unit").id, + "price_unit": 50.0, + "company_id": cls.env.company.id, + }, + ) + ], + "partner_id": cls.res_partner_12.id, + } + ) + + cls.env.user.groups_id |= cls.env.ref("stock.group_stock_user") + + def test_stock_move_state(self): + # Validate Repair Order + self.repair1.action_validate() + self.assertEqual( + self.repair1.move_id.state, + "draft", + "Generated stock move state should be draft", + ) + for operation in self.repair1.operations: + self.assertEqual( + operation.move_id.state, + "draft", + "Generated stock move state should be draft", + ) + # Start Repair + self.repair1.action_repair_start() + self.assertEqual( + self.repair1.move_id.state, + "confirmed", + "Generated stock move state should be confirmed", + ) + for operation in self.repair1.operations: + self.assertEqual( + operation.move_id.state, + "confirmed", + "Generated stock move state should be confirmed", + ) + # End Repair + self.repair1.action_repair_end() + self.assertEqual( + self.repair1.move_id.state, + "done", + "Generated stock move state should be done", + ) + for operation in self.repair1.operations: + self.assertEqual( + operation.move_id.state, + "done", + "Generated stock move state should be done", + ) + + def _create_simple_repair_order(self, invoice_method): + product_to_repair = self.product_1 + partner = self.res_partner_address_1 + return self.env["repair.order"].create( + { + "product_id": product_to_repair.id, + "product_uom": product_to_repair.uom_id.id, + "address_id": partner.id, + "guarantee_limit": "2019-01-01", + "invoice_method": invoice_method, + "partner_invoice_id": partner.id, + "location_id": self.stock_location_14.id, + "partner_id": self.res_partner_12.id, + } + ) + + def _create_simple_operation(self, repair_id=False, qty=0.0, price_unit=0.0): + product_to_add = self.product_1 + return self.env["repair.line"].create( + { + "name": "Add The product", + "type": "add", + "product_id": product_to_add.id, + "product_uom_qty": qty, + "product_uom": product_to_add.uom_id.id, + "price_unit": price_unit, + "repair_id": repair_id, + "location_id": self.stock_location_14.id, + "location_dest_id": product_to_add.property_stock_production.id, + "company_id": self.env.company.id, + } + ) + + # def _create_simple_fee(self, repair_id=False, qty=0.0, price_unit=0.0): + # service = self.product_1 + # return self.env['repair.fee'].create({ + # 'name': 'PC Assemble + Custom (PC on Demand)', + # 'product_id': service.id, + # 'product_uom_qty': qty, + # 'product_uom': service.uom_id.id, + # 'price_unit': price_unit, + # 'repair_id': repair_id, + # 'company_id': self.env.company.id, + # }) diff --git a/repair_stock_move/views/repair_order_views.xml b/repair_stock_move/views/repair_order_views.xml new file mode 100644 index 000000000..303ccb7a6 --- /dev/null +++ b/repair_stock_move/views/repair_order_views.xml @@ -0,0 +1,28 @@ + + + + + repair.order.form - repair_stock_move + repair.order + + + + + + + + + + + + diff --git a/setup/repair_stock_move/odoo/addons/repair_stock_move b/setup/repair_stock_move/odoo/addons/repair_stock_move new file mode 120000 index 000000000..649711c6d --- /dev/null +++ b/setup/repair_stock_move/odoo/addons/repair_stock_move @@ -0,0 +1 @@ +../../../../repair_stock_move \ No newline at end of file diff --git a/setup/repair_stock_move/setup.py b/setup/repair_stock_move/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/repair_stock_move/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)