From 13a5d80af6344375d5b4ba19d5bb4d4d78e67c2d Mon Sep 17 00:00:00 2001 From: DavidJForgeFlow Date: Mon, 20 Mar 2023 11:29:49 +0100 Subject: [PATCH] [IMP] rma_repair: create transfer from RMA to Repair location --- rma_repair/models/__init__.py | 2 + rma_repair/models/rma_operation.py | 5 ++ rma_repair/models/rma_order.py | 24 ++++++ rma_repair/models/rma_order_line.py | 28 ++++++ rma_repair/models/stock_move.py | 19 ++++ rma_repair/models/stock_rule.py | 33 +++++++ rma_repair/tests/test_rma_repair.py | 62 +++++++++++-- rma_repair/views/rma_operation_view.xml | 1 + rma_repair/views/rma_order_line_view.xml | 20 ++++- rma_repair/views/rma_order_view.xml | 14 +++ .../wizards/rma_order_line_make_repair.py | 86 ++++++++++++++++++- .../rma_order_line_make_repair_view.xml | 17 +++- 12 files changed, 294 insertions(+), 17 deletions(-) create mode 100644 rma_repair/models/stock_move.py create mode 100644 rma_repair/models/stock_rule.py diff --git a/rma_repair/models/__init__.py b/rma_repair/models/__init__.py index dc5f3eb0..e98920d9 100644 --- a/rma_repair/models/__init__.py +++ b/rma_repair/models/__init__.py @@ -4,3 +4,5 @@ from . import repair from . import rma_order_line from . import rma_order from . import rma_operation +from . import stock_move +from . import stock_rule diff --git a/rma_repair/models/rma_operation.py b/rma_repair/models/rma_operation.py index c24d5345..6026e7b9 100644 --- a/rma_repair/models/rma_operation.py +++ b/rma_repair/models/rma_operation.py @@ -35,3 +35,8 @@ class RmaOperation(models.Model): "respectively. 'No invoice' means you don't want to generate " "invoice for this repair order.", ) + repair_route_id = fields.Many2one( + comodel_name="stock.route", + string="Repair Route", + domain=[("rma_selectable", "=", True)], + ) diff --git a/rma_repair/models/rma_order.py b/rma_repair/models/rma_order.py index b3091f0c..a3fa6837 100644 --- a/rma_repair/models/rma_order.py +++ b/rma_repair/models/rma_order.py @@ -12,13 +12,37 @@ class RmaOrder(models.Model): repairs = rma.mapped("rma_line_ids.repair_ids") rma.repair_count = len(repairs) + def _compute_repair_transfer_count(self): + for order in self: + pickings = ( + order.mapped("rma_line_ids.move_ids") + .filtered(lambda m: m.is_rma_put_away) + .mapped("picking_id") + ) + order.put_away_count = len(pickings) + repair_count = fields.Integer( compute="_compute_repair_count", string="# of Repairs" ) + repair_transfer_count = fields.Integer( + compute="_compute_repair_transfer_count", string="# Repair Transfers" + ) + def action_view_repair_order(self): action = self.env.ref("repair.action_repair_order_tree") result = action.sudo().read()[0] repair_ids = self.mapped("rma_line_ids.repair_ids").ids result["domain"] = [("id", "in", repair_ids)] return result + + def action_view_repair_transfers(self): + self.ensure_one() + action = self.env.ref("stock.action_picking_tree_all") + result = action.sudo().read()[0] + pickings = self.env["stock.picking"] + for line in self.rma_line_ids: + pickings |= line.move_ids.filtered( + lambda m: m.is_rma_repair_transfer + ).mapped("picking_id") + return self._view_shipments(result, pickings) diff --git a/rma_repair/models/rma_order_line.py b/rma_repair/models/rma_order_line.py index eddc0c3a..d9a313d9 100644 --- a/rma_repair/models/rma_order_line.py +++ b/rma_repair/models/rma_order_line.py @@ -36,6 +36,13 @@ class RmaOrderLine(models.Model): for line in self: line.repair_count = len(line.repair_ids) + def _compute_repair_transfer_count(self): + for line in self: + pickings = line.move_ids.filtered( + lambda m: m.is_rma_repair_transfer + ).mapped("picking_id") + line.repair_transfer_count = len(pickings) + repair_ids = fields.One2many( comodel_name="repair.order", inverse_name="rma_line_id", @@ -80,6 +87,10 @@ class RmaOrderLine(models.Model): compute="_compute_repair_count", string="# of Repairs" ) + repair_transfer_count = fields.Integer( + compute="_compute_repair_transfer_count", string="# of Repair Transfers" + ) + delivery_policy = fields.Selection( selection_add=[("repair", "Based on Repair Quantities")], ondelete={"repair": lambda recs: recs.write({"delivery_policy": "no"})}, @@ -182,3 +193,20 @@ class RmaOrderLine(models.Model): for rec in self.filtered(lambda l: l.delivery_policy == "repair"): rec.qty_to_deliver = rec.qty_repaired - rec.qty_delivered return res + + def action_view_repair_transfers(self): + action = self.env.ref("stock.action_picking_tree_all") + result = action.sudo().read()[0] + pickings = self.env["stock.picking"] + for line in self: + pickings |= line.move_ids.filtered( + lambda m: m.is_rma_repair_transfer + ).mapped("picking_id") + # choose the view_mode accordingly + if len(pickings) != 1: + result["domain"] = "[('id', 'in', " + str(pickings.ids) + ")]" + elif len(pickings) == 1: + res = self.env.ref("stock.view_picking_form", False) + result["views"] = [(res and res.id or False, "form")] + result["res_id"] = pickings.ids[0] + return result diff --git a/rma_repair/models/stock_move.py b/rma_repair/models/stock_move.py new file mode 100644 index 00000000..e1c65cc3 --- /dev/null +++ b/rma_repair/models/stock_move.py @@ -0,0 +1,19 @@ +# Copyright 2022 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" + + is_rma_repair_transfer = fields.Boolean( + string="Is RMA Repair", + help="This Stock Move has been created from a Repair operation in " "the RMA.", + ) + + def _is_in_out_rma_move(self, op, states, location_type): + res = super(StockMove, self)._is_in_out_rma_move(op, states, location_type) + if self.is_rma_repair_transfer: + return False + return res diff --git a/rma_repair/models/stock_rule.py b/rma_repair/models/stock_rule.py new file mode 100644 index 00000000..f80df6ab --- /dev/null +++ b/rma_repair/models/stock_rule.py @@ -0,0 +1,33 @@ +# Copyright 2022 ForgeFlow S.L. +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html) + +from odoo import models + + +class StockRule(models.Model): + _inherit = "stock.rule" + + def _get_stock_move_values( + self, + product_id, + product_qty, + product_uom, + location_id, + name, + origin, + company_id, + values, + ): + res = super()._get_stock_move_values( + product_id, + product_qty, + product_uom, + location_id, + name, + origin, + company_id, + values, + ) + if "is_rma_repair_transfer" in values: + res["is_rma_repair_transfer"] = values.get("is_rma_repair_transfer") + return res diff --git a/rma_repair/tests/test_rma_repair.py b/rma_repair/tests/test_rma_repair.py index 4df8201e..32a9dca0 100644 --- a/rma_repair/tests/test_rma_repair.py +++ b/rma_repair/tests/test_rma_repair.py @@ -28,6 +28,36 @@ class TestRmaRepair(common.SingleTransactionCase): # Create partners cls.customer1 = cls.partner_obj.create({"name": "Customer 1"}) + # Create routes + cls.wh = cls.env.ref("stock.warehouse0") + cls.stock_rma_location = cls.wh.lot_rma_id + cls.repair_loc = cls.env["stock.location"].create( + { + "name": "WH Repair Location", + "location_id": cls.wh.view_location_id.id, + } + ) + cls.repair_route = cls.env["stock.route"].create( + { + "name": "Transfer RMA to Repair", + "rma_selectable": True, + "sequence": 10, + } + ) + + cls.env["stock.rule"].create( + { + "name": "Transfer", + "route_id": cls.repair_route.id, + "location_src_id": cls.stock_rma_location.id, + "location_dest_id": cls.repair_loc.id, + "action": "pull", + "picking_type_id": cls.wh.int_type_id.id, + "procure_method": "make_to_stock", + "warehouse_id": cls.wh.id, + } + ) + # Create RMA group and operation: cls.rma_group_customer = cls.rma_obj.create( {"partner_id": cls.customer1.id, "type": "customer"} @@ -41,6 +71,8 @@ class TestRmaRepair(common.SingleTransactionCase): "repair_type": "received", "in_route_id": cls.rma_route_cust.id, "out_route_id": cls.rma_route_cust.id, + "repair_location_id": cls.repair_loc.id, + "repair_route_id": cls.repair_route.id, } ) cls.operation_2 = cls.rma_op.create( @@ -52,6 +84,8 @@ class TestRmaRepair(common.SingleTransactionCase): "repair_type": "ordered", "in_route_id": cls.rma_route_cust.id, "out_route_id": cls.rma_route_cust.id, + "repair_location_id": cls.repair_loc.id, + "repair_route_id": cls.repair_route.id, } ) cls.operation_3 = cls.rma_op.create( @@ -64,6 +98,8 @@ class TestRmaRepair(common.SingleTransactionCase): "delivery_policy": "repair", "in_route_id": cls.rma_route_cust.id, "out_route_id": cls.rma_route_cust.id, + "repair_location_id": cls.repair_loc.id, + "repair_route_id": cls.repair_route.id, } ) # Create products @@ -154,14 +190,6 @@ class TestRmaRepair(common.SingleTransactionCase): cls.material.product_tmpl_id.standard_price = 10 cls.stock_location = cls.env.ref("stock.stock_location_stock") - cls.env["stock.quant"].create( - { - "product_id": cls.material.id, - "location_id": cls.stock_location.id, - "quantity": 10, - } - ) - def test_01_add_from_invoice_customer(self): """Test wizard to create RMA from a customer invoice.""" add_inv = self.rma_add_invoice_wiz.with_context( @@ -217,6 +245,7 @@ class TestRmaRepair(common.SingleTransactionCase): self.assertEqual(rma.repair_count, 0) self.assertEqual(rma.qty_to_repair, 15.0) self.assertEqual(rma.qty_repaired, 0.0) + self.assertEqual(rma.repair_transfer_count, 0) make_repair = self.rma_make_repair_wiz.with_context( **{ "customer": True, @@ -225,6 +254,14 @@ class TestRmaRepair(common.SingleTransactionCase): } ).new() make_repair.make_repair_order() + rma._compute_repair_transfer_count() + self.assertEqual(rma.repair_transfer_count, 1) + repair_transfer_move = rma.move_ids.filtered( + lambda x: x.location_dest_id == self.repair_loc + ) + self.assertEqual(repair_transfer_move.location_id, self.stock_rma_location) + self.assertEqual(repair_transfer_move.product_qty, 15.0) + self.assertEqual(repair_transfer_move.product_id, rma.product_id) rma.repair_ids.action_repair_confirm() self.assertEqual(rma.repair_count, 1) self.assertEqual(rma.qty_to_repair, 0.0) @@ -263,6 +300,7 @@ class TestRmaRepair(common.SingleTransactionCase): for mv in picking.move_ids: mv.quantity_done = mv.product_uom_qty picking._action_done() + self.assertEqual(rma.repair_transfer_count, 0) self.assertEqual(rma.qty_to_deliver, 0.0) make_repair = self.rma_make_repair_wiz.with_context( **{ @@ -272,6 +310,13 @@ class TestRmaRepair(common.SingleTransactionCase): } ).new() make_repair.make_repair_order() + rma._compute_repair_transfer_count() + self.assertEqual(rma.repair_transfer_count, 1) + repair_transfer_move = rma.move_ids.filtered( + lambda x: x.location_dest_id == self.repair_loc + ) + self.assertEqual(repair_transfer_move.location_id, self.stock_rma_location) + self.assertEqual(repair_transfer_move.product_id, rma.product_id) repair = rma.repair_ids line = self.repair_line_obj.create( { @@ -290,6 +335,7 @@ class TestRmaRepair(common.SingleTransactionCase): repair.invoice_method = "after_repair" repair.action_repair_confirm() repair.action_repair_start() + repair.action_force_availability() repair.action_repair_end() self.assertEqual(rma.qty_to_pay, 0.0) repair.action_repair_invoice_create() diff --git a/rma_repair/views/rma_operation_view.xml b/rma_repair/views/rma_operation_view.xml index 10e29203..7ee81bc3 100644 --- a/rma_repair/views/rma_operation_view.xml +++ b/rma_repair/views/rma_operation_view.xml @@ -21,6 +21,7 @@ + diff --git a/rma_repair/views/rma_order_line_view.xml b/rma_repair/views/rma_order_line_view.xml index ba943994..a6a52716 100644 --- a/rma_repair/views/rma_order_line_view.xml +++ b/rma_repair/views/rma_order_line_view.xml @@ -13,7 +13,7 @@ class="oe_stat_button" icon="fa-wrench" groups="stock.group_stock_user" - attrs="{'invisible':[('repair_count','=',0)]}" + attrs="{'invisible': [('repair_count', '=', 0)]}" > + @@ -51,13 +65,13 @@ name="%(action_rma_order_line_make_repair)d" string="Create Repair Order" class="oe_highlight" - attrs="{'invisible':['|', '|', ('type', '!=', 'customer'), ('qty_to_repair', '=', 0), ('state', '!=', 'approved')]}" + attrs="{'invisible':['|', '|', ('type', '!=', 'customer'), '|', ('qty_to_repair', '=', 0), ('qty_to_repair', '<', 0), ('state', '!=', 'approved')]}" type="action" /> + diff --git a/rma_repair/wizards/rma_order_line_make_repair.py b/rma_repair/wizards/rma_order_line_make_repair.py index 258886c0..2ac26989 100644 --- a/rma_repair/wizards/rma_order_line_make_repair.py +++ b/rma_repair/wizards/rma_order_line_make_repair.py @@ -1,8 +1,10 @@ # Copyright 2020 ForgeFlow S.L. # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +import time from odoo import _, api, fields, models -from odoo.exceptions import ValidationError +from odoo.exceptions import UserError, ValidationError +from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT as DT_FORMAT class RmaLineMakeRepair(models.TransientModel): @@ -56,6 +58,13 @@ class RmaLineMakeRepair(models.TransientModel): data = item._prepare_repair_order(rma_line) repair = repair_obj.create(data) res.append(repair.id) + if rma_line.location_id != repair.location_id: + item._run_procurement( + rma_line.operation_id.repair_route_id, repair.location_id + ) + item._run_procurement( + rma_line.operation_id.repair_route_id, rma_line.location_id + ) return { "domain": [("id", "in", res)], "name": _("Repairs"), @@ -72,7 +81,7 @@ class RmaLineMakeRepairItem(models.TransientModel): _description = "RMA Line Make Repair Item" @api.constrains("product_qty") - def _check_prodcut_qty(self): + def _check_product_qty(self): for rec in self: if rec.product_qty <= 0.0: raise ValidationError(_("Quantity must be positive.")) @@ -138,3 +147,76 @@ class RmaLineMakeRepairItem(models.TransientModel): "partner_invoice_id": addr["invoice"], "lot_id": rma_line.lot_id.id, } + + def _run_procurement(self, route, dest_location): + procurements = [] + errors = [] + procurement = self._prepare_procurement(route, dest_location) + procurements.append(procurement) + try: + self.env["procurement.group"].with_context(picking_type="internal").run( + procurements + ) + except UserError as error: + errors.append(error.args[0]) + if errors: + raise UserError("\n".join(errors)) + return procurements + + def find_procurement_group(self): + if self.line_id.rma_id: + return self.env["procurement.group"].search( + [("rma_id", "=", self.line_id.rma_id.id)], limit=1 + ) + else: + return self.env["procurement.group"].search( + [("rma_line_id", "=", self.line_id.id)], limit=1 + ) + + def _get_procurement_group(self): + group_data = { + "partner_id": self.line_id.partner_id.id, + "name": self.line_id.rma_id.name or self.line_id.name, + "rma_id": self.line_id.rma_id and self.line_id.rma_id.id or False, + "rma_line_id": self.line_id.id if not self.line_id.rma_id else False, + } + return self.env["procurement.group"].create(group_data) + + @api.model + def _get_procurement_data(self, route, dest_location): + if not route: + raise ValidationError(_("No route specified")) + group = self.find_procurement_group() + if not group: + group = self._get_procurement_group() + procurement_data = { + "name": self.line_id and self.line_id.name, + "group_id": group, + "warehouse": dest_location.warehouse_id, + "origin": self.line_id.name, + "date_planned": time.strftime(DT_FORMAT), + "product_id": self.product_id, + "product_qty": self.product_qty, + "product_uom": self.product_id.product_tmpl_id.uom_id.id, + "location_id": dest_location, + "partner_id": self.partner_id.id, + "route_ids": route, + "rma_line_id": self.line_id.id, + "is_rma_repair_transfer": True, + } + 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 diff --git a/rma_repair/wizards/rma_order_line_make_repair_view.xml b/rma_repair/wizards/rma_order_line_make_repair_view.xml index ca619669..3361fb52 100644 --- a/rma_repair/wizards/rma_order_line_make_repair_view.xml +++ b/rma_repair/wizards/rma_order_line_make_repair_view.xml @@ -12,14 +12,23 @@ - - + + - - + +