From 6d3dbb3f0b0814993abe42baac1d157b9de513f0 Mon Sep 17 00:00:00 2001 From: AaronHForgeFlow Date: Wed, 4 Jan 2023 11:57:36 +0100 Subject: [PATCH 1/2] [14.0][FIX] rma_account: reconcile GDNI when receiving but refund is expected --- rma_account/models/rma_operation.py | 16 +++++++++ rma_account/models/rma_order_line.py | 36 +++++++++++++++++++++ rma_account/models/stock_move.py | 1 + rma_account/tests/test_rma_stock_account.py | 23 +++++++++++++ rma_account/views/rma_operation_view.xml | 4 +++ rma_account/wizards/rma_refund.py | 6 ++-- 6 files changed, 82 insertions(+), 4 deletions(-) diff --git a/rma_account/models/rma_operation.py b/rma_account/models/rma_operation.py index bf21ab55..14d7b279 100644 --- a/rma_account/models/rma_operation.py +++ b/rma_account/models/rma_operation.py @@ -28,6 +28,16 @@ class RmaOperation(models.Model): comodel_name="account.journal", compute="_compute_domain_valid_journal", ) + automated_refund = fields.Boolean( + help="In the scenario where a company uses anglo-saxon accounting, if " + "you receive products from a customer and don't expect to refund the customer " + "but send a replacement unit, mark this flag to be accounting consistent" + ) + refund_free_of_charge = fields.Boolean( + help="In case of automated refund you should mark this option as long automated" + "refunds mean to compensate Stock Interim accounts only without hitting" + "Accounts receivable" + ) @api.onchange("type") def _compute_domain_valid_journal(self): @@ -40,3 +50,9 @@ class RmaOperation(models.Model): rec.valid_refund_journal_ids = self.env["account.journal"].search( [("type", "=", "purchase")] ) + + @api.onchange("automated_refund") + def _onchange_automated_refund(self): + for rec in self: + if rec.automated_refund: + rec.refund_free_of_charge = True diff --git a/rma_account/models/rma_order_line.py b/rma_account/models/rma_order_line.py index 8c43711b..01535fe8 100644 --- a/rma_account/models/rma_order_line.py +++ b/rma_account/models/rma_order_line.py @@ -360,3 +360,39 @@ class RmaOrderLine(models.Model): # We get the cost from the original invoice line price_unit = self.account_move_line_id.price_unit return price_unit + + def _refund_at_zero_cost(self): + make_refund = ( + self.env["rma.refund"] + .with_context( + { + "customer": True, + "active_ids": self.ids, + "active_model": "rma.order.line", + } + ) + .create({"description": "RMA Anglosaxon Regularisation"}) + ) + for item in make_refund.item_ids: + item.qty_to_refund = item.line_id.qty_received - item.line_id.qty_refunded + action_refund = make_refund.invoice_refund() + refund_id = action_refund.get("res_id", False) + if refund_id: + refund = self.env["account.move"].browse(refund_id) + refund._post() + + def _check_refund_zero_cost(self): + """ + In the scenario where a company uses anglo-saxon accounting, if you receive + products from a customer and don't expect to refund the customer but send a + replacement unit you still need to create a debit entry on the + Stock Interim (Delivered) account. In order to do this the best approach is + to create a customer refund from the RMA, but set as free of charge + (price unit = 0). The refund will be 0, but the Stock Interim (Delivered) + account will be posted anyways. + """ + # For some reason api.depends on qty_received is not working. Using the + # _account_entry_move method in stock move as trigger then + for rec in self.filtered(lambda l: l.operation_id.automated_refund): + if rec.qty_received > rec.qty_refunded: + rec._refund_at_zero_cost() diff --git a/rma_account/models/stock_move.py b/rma_account/models/stock_move.py index 639468fb..18d8e29e 100644 --- a/rma_account/models/stock_move.py +++ b/rma_account/models/stock_move.py @@ -13,4 +13,5 @@ class StockMove(models.Model): # Eventually reconcile together the invoice and valuation accounting # entries on the stock interim accounts self.rma_line_id._stock_account_anglo_saxon_reconcile_valuation() + self.rma_line_id._check_refund_zero_cost() return res diff --git a/rma_account/tests/test_rma_stock_account.py b/rma_account/tests/test_rma_stock_account.py index 6b4d92f1..65b2f302 100644 --- a/rma_account/tests/test_rma_stock_account.py +++ b/rma_account/tests/test_rma_stock_account.py @@ -324,3 +324,26 @@ class TestRmaStockAccount(TestRma): self.assertEqual(gdni_balance, 0.0) # The GDNI entries should be now reconciled self.assertEqual(all(gdni_amls.mapped("reconciled")), True) + + def test_05_reconcile_grni_when_no_refund(self): + """ + Test that receive and send a replacement order leaves GDNI reconciled + """ + self.product_fifo_1.standard_price = 15 + rma_line = Form(self.rma_line) + rma_line.partner_id = self.partner_id + rma_line.product_id = self.product_fifo_1 + rma_line.operation_id.automated_refund = True + rma_line = rma_line.save() + rma_line.action_rma_to_approve() + # receiving should trigger the refund at zero cost + self._receive_rma(rma_line) + gdni_amls = self.env["account.move.line"].search( + [ + ("rma_line_id", "in", rma_line.ids), + ("account_id", "=", self.account_gdni.id), + ] + ) + rma_line.refund_line_ids.filtered( + lambda l: l.account_id == self.account_gdni + ) + self.assertEqual(all(gdni_amls.mapped("reconciled")), True) diff --git a/rma_account/views/rma_operation_view.xml b/rma_account/views/rma_operation_view.xml index 6d2cdbb0..922f52aa 100644 --- a/rma_account/views/rma_operation_view.xml +++ b/rma_account/views/rma_operation_view.xml @@ -21,6 +21,10 @@ + + + + diff --git a/rma_account/wizards/rma_refund.py b/rma_account/wizards/rma_refund.py index b0b86d98..909a833f 100644 --- a/rma_account/wizards/rma_refund.py +++ b/rma_account/wizards/rma_refund.py @@ -92,10 +92,6 @@ class RmaRefund(models.TransientModel): def invoice_refund(self): rma_line_ids = self.env["rma.order.line"].browse(self.env.context["active_ids"]) for line in rma_line_ids: - if line.refund_policy == "no": - raise ValidationError( - _("The operation is not refund for at least one line") - ) if line.state != "approved": raise ValidationError(_("RMA %s is not approved") % line.name) new_invoice = self.compute_refund() @@ -111,6 +107,8 @@ class RmaRefund(models.TransientModel): return result def _get_refund_price_unit(self, rma): + if rma.operation_id.refund_free_of_charge: + return 0.0 price_unit = rma.price_unit # If this references a previous invoice/bill, use the same unit price if rma.account_move_line_id: From deb9c63546c6cc4d6001a15e535ea67d1aea0f5f Mon Sep 17 00:00:00 2001 From: AaronHForgeFlow Date: Thu, 5 Jan 2023 11:11:41 +0100 Subject: [PATCH 2/2] [14.0][FIX] rma_sale: compatibility with refunds free of charge --- rma_sale/wizards/rma_refund.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rma_sale/wizards/rma_refund.py b/rma_sale/wizards/rma_refund.py index 0f789ec7..cc8ea048 100644 --- a/rma_sale/wizards/rma_refund.py +++ b/rma_sale/wizards/rma_refund.py @@ -14,6 +14,8 @@ class RmaRefund(models.TransientModel): def _get_refund_price_unit(self, rma): price_unit = super(RmaRefund, self)._get_refund_price_unit(rma) + if rma.operation_id.refund_free_of_charge: + return price_unit if rma.type == "customer": if rma.account_move_line_id: price_unit = rma.account_move_line_id.price_unit