diff --git a/rma_sale_mrp/__manifest__.py b/rma_sale_mrp/__manifest__.py index 6bfa5b76..bfd5aef0 100644 --- a/rma_sale_mrp/__manifest__.py +++ b/rma_sale_mrp/__manifest__.py @@ -10,10 +10,7 @@ "author": "Tecnativa, Odoo Community Association (OCA)", "maintainers": ["chienandalu"], "license": "AGPL-3", - "depends": [ - "rma_sale", - "mrp", - ], + "depends": ["rma_sale", "mrp"], "data": [ "views/sale_order_portal_template.xml", "views/rma_views.xml", diff --git a/rma_sale_mrp/models/account_invoice.py b/rma_sale_mrp/models/account_invoice.py index bf0d04d8..91150fc0 100644 --- a/rma_sale_mrp/models/account_invoice.py +++ b/rma_sale_mrp/models/account_invoice.py @@ -10,10 +10,14 @@ class AccountInvoice(models.Model): def _check_rma_invoice_lines_qty(self): """For those with differences, check if the kit quantity is the same""" precision = self.env["decimal.precision"].precision_get( - "Product Unit of Measure") + "Product Unit of Measure" + ) lines = super()._check_rma_invoice_lines_qty() if lines: return lines.sudo().filtered( - lambda r: (r.rma_id.phantom_bom_product and float_compare( - r.quantity, r.rma_id.kit_qty, precision) < 0)) + lambda r: ( + r.rma_id.phantom_bom_product + and float_compare(r.quantity, r.rma_id.kit_qty, precision) < 0 + ) + ) return lines diff --git a/rma_sale_mrp/models/rma.py b/rma_sale_mrp/models/rma.py index 6111b4e2..45dad61c 100644 --- a/rma_sale_mrp/models/rma.py +++ b/rma_sale_mrp/models/rma.py @@ -2,6 +2,7 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from odoo import _, fields, models from odoo.exceptions import UserError + import odoo.addons.decimal_precision as dp @@ -9,57 +10,57 @@ class Rma(models.Model): _inherit = "rma" phantom_bom_product = fields.Many2one( - comodel_name="product.product", - string="Related kit product", - readonly=True, + comodel_name="product.product", string="Related kit product", readonly=True, ) kit_qty = fields.Float( string="Kit quantity", digits=dp.get_precision("Product Unit of Measure"), readonly=True, help="To how many kits this components corresponds to. Used mainly " - "for refunding the right quantity", + "for refunding the right quantity", ) rma_kit_register = fields.Char(readonly=True) def _get_refund_line_quantity(self): """Refund the kit, not the component""" if self.phantom_bom_product: - uom = ( - self.sale_line_id.product_uom - or self.phantom_bom_product.uom_id - ) + uom = self.sale_line_id.product_uom or self.phantom_bom_product.uom_id return (self.kit_qty, uom) return (self.product_uom_qty, self.product_uom) def action_refund(self): """We want to process them altogether""" phantom_rmas = self.filtered("phantom_bom_product") - phantom_rmas |= self.search([ - ("rma_kit_register", "in", phantom_rmas.mapped("rma_kit_register")), - ("id", "not in", phantom_rmas.ids), - ]) + phantom_rmas |= self.search( + [ + ("rma_kit_register", "in", phantom_rmas.mapped("rma_kit_register")), + ("id", "not in", phantom_rmas.ids), + ] + ) self -= phantom_rmas - for rma_kit_register in phantom_rmas.mapped( - "rma_kit_register"): + for rma_kit_register in phantom_rmas.mapped("rma_kit_register"): # We want to avoid refunding kits that aren't completely processed rmas_by_register = phantom_rmas.filtered( - lambda x: x.rma_kit_register == rma_kit_register) + lambda x: x.rma_kit_register == rma_kit_register + ) if any(rmas_by_register.filtered(lambda x: x.state != "received")): - raise UserError(_( - "You can't refund a kit in wich some RMAs aren't received" - )) + raise UserError( + _("You can't refund a kit in wich some RMAs aren't received") + ) self |= rmas_by_register[0] super().action_refund() # We can just link the line to an RMA but we can link several RMAs # to one invoice line. for rma_kit_register in set(phantom_rmas.mapped("rma_kit_register")): grouped_rmas = phantom_rmas.filtered( - lambda x: x.rma_kit_register == rma_kit_register) + lambda x: x.rma_kit_register == rma_kit_register + ) lead_rma = grouped_rmas.filtered("refund_line_id") grouped_rmas -= lead_rma - grouped_rmas.write({ - "refund_line_id": lead_rma.refund_line_id.id, - "refund_id": lead_rma.refund_id.id, - "state": "refunded", - }) + grouped_rmas.write( + { + "refund_line_id": lead_rma.refund_line_id.id, + "refund_id": lead_rma.refund_id.id, + "state": "refunded", + } + ) diff --git a/rma_sale_mrp/models/sale_order.py b/rma_sale_mrp/models/sale_order.py index fb464d0e..62bc51a0 100644 --- a/rma_sale_mrp/models/sale_order.py +++ b/rma_sale_mrp/models/sale_order.py @@ -18,15 +18,11 @@ class SaleOrder(models.Model): def get_delivery_rma_data(self): """Get the phantom lines we'll be showing in the wizard""" data_list = super().get_delivery_rma_data() - kit_products = set( - [ - ( - x.get("phantom_bom_product"), - x.get("sale_line_id") - ) for x in data_list - if x.get("phantom_bom_product") - ] - ) + kit_products = { + (x.get("phantom_bom_product"), x.get("sale_line_id")) + for x in data_list + if x.get("phantom_bom_product") + } # For every unique phantom product we'll create a phantom line wich # will be using as the control in frontend and for display purposes # in backend @@ -34,35 +30,40 @@ class SaleOrder(models.Model): order_line_obj = self.env["sale.order.line"] product_obj = self.env["product.product"] first_component_dict = next( - x for x in data_list if x.get( - "phantom_bom_product", product_obj - ) == product and x.get( - "sale_line_id", order_line_obj - ) == sale_line_id) + x + for x in data_list + if x.get("phantom_bom_product", product_obj) == product + and x.get("sale_line_id", order_line_obj) == sale_line_id + ) component_index = data_list.index(first_component_dict) # Prevent miscalculation if there partial deliveries - quantity = sum([ - x.get("quantity", 0) for x in data_list - if x.get("sale_line_id") - and x.get("product") == first_component_dict.get("product") - and x.get("sale_line_id") == first_component_dict.get( - "sale_line_id")]) + quantity = sum( + [ + x.get("quantity", 0) + for x in data_list + if x.get("sale_line_id") + and x.get("product") == first_component_dict.get("product") + and x.get("sale_line_id") + == first_component_dict.get("sale_line_id") + ] + ) data_list.insert( - component_index, { + component_index, + { "product": product, "quantity": ( - first_component_dict.get("per_kit_quantity") and ( - quantity - / first_component_dict.get("per_kit_quantity") - ) + first_component_dict.get("per_kit_quantity") + and (quantity / first_component_dict.get("per_kit_quantity")) ), "uom": first_component_dict.get( - "sale_line_id", order_line_obj).product_uom, + "sale_line_id", order_line_obj + ).product_uom, "phantom_kit_line": True, "picking": False, "sale_line_id": first_component_dict.get( - "sale_line_id", order_line_obj), - } + "sale_line_id", order_line_obj + ), + }, ) return data_list @@ -74,12 +75,17 @@ class SaleOrderLine(models.Model): self.ensure_one() if self.product_id and not self.product_id._is_phantom_bom(): return super().get_delivery_move() - return self.move_ids.filtered(lambda m: ( - m.state == "done" and not m.scrapped - and m.location_dest_id.usage == "customer" - and ( - not m.origin_returned_move_id - or (m.origin_returned_move_id and m.to_refund)))) + return self.move_ids.filtered( + lambda m: ( + m.state == "done" + and not m.scrapped + and m.location_dest_id.usage == "customer" + and ( + not m.origin_returned_move_id + or (m.origin_returned_move_id and m.to_refund) + ) + ) + ) def prepare_sale_rma_data(self): """We'll take both the sale order product and the phantom one so we @@ -91,7 +97,7 @@ class SaleOrderLine(models.Model): d.update( { "phantom_bom_product": self.product_id, - "per_kit_quantity": self._get_kit_qty(d.get("product")) + "per_kit_quantity": self._get_kit_qty(d.get("product")), } ) return data @@ -105,6 +111,7 @@ class SaleOrderLine(models.Model): return 0 component_demand = sum( self.move_ids.filtered( - lambda x: x.product_id == product_id - and not x.origin_returned_move_id).mapped("product_uom_qty")) + lambda x: x.product_id == product_id and not x.origin_returned_move_id + ).mapped("product_uom_qty") + ) return component_demand / self.product_uom_qty diff --git a/rma_sale_mrp/tests/test_rma_sale_mrp.py b/rma_sale_mrp/tests/test_rma_sale_mrp.py index b64b3c49..2b00d5e1 100644 --- a/rma_sale_mrp/tests/test_rma_sale_mrp.py +++ b/rma_sale_mrp/tests/test_rma_sale_mrp.py @@ -1,7 +1,7 @@ # Copyright 2020 Tecnativa - David Vidal # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from odoo.tests import Form, SavepointCase from odoo.exceptions import UserError, ValidationError +from odoo.tests import Form, SavepointCase class TestRmaSaleMrp(SavepointCase): @@ -11,39 +11,38 @@ class TestRmaSaleMrp(SavepointCase): cls.res_partner = cls.env["res.partner"] cls.product_product = cls.env["product.product"] cls.sale_order = cls.env["sale.order"] - cls.product_kit = cls.product_product.create({ - "name": "Product test 1", - "type": "consu", - }) - cls.product_kit_comp_1 = cls.product_product.create({ - "name": "Product Component 1", - "type": "product", - }) - cls.product_kit_comp_2 = cls.product_product.create({ - "name": "Product Component 2", - "type": "product", - }) - cls.bom = cls.env["mrp.bom"].create({ - "product_id": cls.product_kit.id, - "product_tmpl_id": cls.product_kit.product_tmpl_id.id, - "type": "phantom", - "bom_line_ids": [ - (0, 0, { - "product_id": cls.product_kit_comp_1.id, - "product_qty": 2, - }), - (0, 0, { - "product_id": cls.product_kit_comp_2.id, - "product_qty": 4, - }) - ]}) - cls.product_2 = cls.product_product.create({ - "name": "Product test 2", - "type": "product", - }) - cls.partner = cls.res_partner.create({ - "name": "Partner test", - }) + cls.product_kit = cls.product_product.create( + {"name": "Product test 1", "type": "consu"} + ) + cls.product_kit_comp_1 = cls.product_product.create( + {"name": "Product Component 1", "type": "product"} + ) + cls.product_kit_comp_2 = cls.product_product.create( + {"name": "Product Component 2", "type": "product"} + ) + cls.bom = cls.env["mrp.bom"].create( + { + "product_id": cls.product_kit.id, + "product_tmpl_id": cls.product_kit.product_tmpl_id.id, + "type": "phantom", + "bom_line_ids": [ + ( + 0, + 0, + {"product_id": cls.product_kit_comp_1.id, "product_qty": 2}, + ), + ( + 0, + 0, + {"product_id": cls.product_kit_comp_2.id, "product_qty": 4}, + ), + ], + } + ) + cls.product_2 = cls.product_product.create( + {"name": "Product test 2", "type": "product"} + ) + cls.partner = cls.res_partner.create({"name": "Partner test"}) order_form = Form(cls.sale_order) order_form.partner_id = cls.partner with order_form.order_line.new() as line_form: @@ -54,7 +53,8 @@ class TestRmaSaleMrp(SavepointCase): # Maybe other modules create additional lines in the create # method in sale.order model, so let's find the correct line. cls.order_line = cls.sale_order.order_line.filtered( - lambda r: r.product_id == cls.product_kit) + lambda r: r.product_id == cls.product_kit + ) cls.order_out_picking = cls.sale_order.picking_ids # Confirm but leave a backorder to split moves so we can test that # the wizard correctly creates the RMAs with the proper quantities @@ -82,8 +82,7 @@ class TestRmaSaleMrp(SavepointCase): self.assertTrue(rma.picking_id in out_pickings) self.assertEqual(rmas.mapped("phantom_bom_product"), self.product_kit) self.assertEqual( - rmas.mapped("product_id"), - self.product_kit_comp_1 + self.product_kit_comp_2 + rmas.mapped("product_id"), self.product_kit_comp_1 + self.product_kit_comp_2 ) rma_1 = rmas.filtered(lambda x: x.product_id == self.product_kit_comp_1) rma_2 = rmas.filtered(lambda x: x.product_id == self.product_kit_comp_2) @@ -93,42 +92,26 @@ class TestRmaSaleMrp(SavepointCase): move_2 = out_pickings.mapped("move_lines").filtered( lambda x: x.product_id == self.product_kit_comp_2 ) - self.assertEqual( - sum(rma_1.mapped("product_uom_qty")), - 8 - ) - self.assertEqual( - rma_1.mapped("product_uom"), - move_1.mapped("product_uom") - ) - self.assertEqual( - sum(rma_2.mapped("product_uom_qty")), - 16 - ) - self.assertEqual( - rma_2.mapped("product_uom"), - move_2.mapped("product_uom") - ) + self.assertEqual(sum(rma_1.mapped("product_uom_qty")), 8) + self.assertEqual(rma_1.mapped("product_uom"), move_1.mapped("product_uom")) + self.assertEqual(sum(rma_2.mapped("product_uom_qty")), 16) + self.assertEqual(rma_2.mapped("product_uom"), move_2.mapped("product_uom")) self.assertEqual(rma.state, "confirmed") self.assertEqual( - rma_1.mapped("reception_move_id.origin_returned_move_id"), - move_1, + rma_1.mapped("reception_move_id.origin_returned_move_id"), move_1, ) self.assertEqual( - rma_2.mapped("reception_move_id.origin_returned_move_id"), - move_2, + rma_2.mapped("reception_move_id.origin_returned_move_id"), move_2, ) self.assertEqual( rmas.mapped("reception_move_id.picking_id") - + self.order_out_picking + self.backorder, + + self.order_out_picking + + self.backorder, order.picking_ids, ) # Refund the RMA user = self.env["res.users"].create( - { - "login": "test_refund_with_so", - "name": "Test", - } + {"login": "test_refund_with_so", "name": "Test"} ) order.user_id = user.id rma.reception_move_id.quantity_done = rma.product_uom_qty @@ -139,15 +122,15 @@ class TestRmaSaleMrp(SavepointCase): rmas_left = rmas - rma for additional_rma in rmas_left: additional_rma.reception_move_id.quantity_done = ( - additional_rma.product_uom_qty) + additional_rma.product_uom_qty + ) additional_rma.reception_move_id.picking_id.action_done() rma.action_refund() self.assertEqual(rma.refund_id.user_id, user) # The component RMAs get automatically refunded self.assertEqual(rma.refund_id, rmas_left.mapped("refund_id")) # The refund product is the kit, not the components - self.assertEqual( - rma.refund_id.invoice_line_ids.product_id, self.product_kit) + self.assertEqual(rma.refund_id.invoice_line_ids.product_id, self.product_kit) rma.refund_id.action_invoice_open() # We can still return another kit wizard_id = order.action_create_rma()["res_id"] diff --git a/rma_sale_mrp/views/report_rma.xml b/rma_sale_mrp/views/report_rma.xml index 55b3b715..c88cb571 100644 --- a/rma_sale_mrp/views/report_rma.xml +++ b/rma_sale_mrp/views/report_rma.xml @@ -1,4 +1,4 @@ - +