diff --git a/rma_account/models/rma_operation.py b/rma_account/models/rma_operation.py
index 727bab40..26702309 100644
--- a/rma_account/models/rma_operation.py
+++ b/rma_account/models/rma_operation.py
@@ -27,6 +27,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):
@@ -39,3 +49,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 f27e460e..fe30ddda 100644
--- a/rma_account/models/rma_order_line.py
+++ b/rma_account/models/rma_order_line.py
@@ -357,3 +357,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 c5872fa4..763c0297 100644
--- a/rma_account/models/stock_move.py
+++ b/rma_account/models/stock_move.py
@@ -28,4 +28,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 1e160fee..8db54d37 100644
--- a/rma_account/tests/test_rma_stock_account.py
+++ b/rma_account/tests/test_rma_stock_account.py
@@ -25,6 +25,9 @@ class TestRmaStockAccount(TestRma):
"rma_account.rma_operation_customer_refund"
)
cls.rma_basic_user.write({"groups_id": [(4, cls.g_account_user.id)]})
+ cls.customer_route = cls.env.ref("rma.route_rma_customer")
+ cls.input_location = cls.env.ref("stock.stock_location_company")
+ cls.output_location = cls.env.ref("stock.stock_location_output")
# The product category created in the base module is not automated valuation
# we have to create a new category here
# Create account for Goods Received Not Invoiced
@@ -248,3 +251,110 @@ 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_08_cost_from_move_multi_step(self):
+ """
+ Receive a product and then return it using a multi-step route.
+ The Goods Delivered Not Invoiced should result in 0
+ """
+ # Alter the customer RMA route to make it multi-step
+ # Get rid of the duplicated rule
+ self.env.ref("rma.rule_rma_customer_out_pull").active = False
+ self.env.ref("rma.rule_rma_customer_in_pull").active = False
+ cust_in_pull_rule = self.customer_route.rule_ids.filtered(
+ lambda r: r.location_id == self.stock_rma_location
+ )
+ cust_in_pull_rule.location_id = self.input_location
+ cust_out_pull_rule = self.customer_route.rule_ids.filtered(
+ lambda r: r.location_src_id == self.env.ref("rma.location_rma")
+ )
+ cust_out_pull_rule.location_src_id = self.output_location
+ cust_out_pull_rule.procure_method = "make_to_order"
+ self.env["stock.rule"].create(
+ {
+ "name": "RMA->Output",
+ "action": "pull",
+ "warehouse_id": self.wh.id,
+ "location_src_id": self.env.ref("rma.location_rma").id,
+ "location_id": self.output_location.id,
+ "procure_method": "make_to_stock",
+ "route_id": self.customer_route.id,
+ "picking_type_id": self.env.ref("stock.picking_type_internal").id,
+ }
+ )
+ self.env["stock.rule"].create(
+ {
+ "name": "Customers->RMA",
+ "action": "pull",
+ "warehouse_id": self.wh.id,
+ "location_src_id": self.customer_location.id,
+ "location_id": self.env.ref("rma.location_rma").id,
+ "procure_method": "make_to_order",
+ "route_id": self.customer_route.id,
+ "picking_type_id": self.env.ref("stock.picking_type_in").id,
+ }
+ )
+ # Set a standard price on the products
+ self.product_fifo_1.standard_price = 10
+ self._create_inventory(
+ self.product_fifo_1, 20.0, self.env.ref("stock.stock_location_customers")
+ )
+ products2move = [
+ (self.product_fifo_1, 3),
+ ]
+ self.product_fifo_1.categ_id.rma_customer_operation_id = (
+ self.rma_cust_replace_op_id
+ )
+ rma_customer_id = self._create_rma_from_move(
+ products2move,
+ "customer",
+ self.env.ref("base.res_partner_2"),
+ dropship=False,
+ )
+ # Set an incorrect price in the RMA (this should not affect cost)
+ rma = rma_customer_id.rma_line_ids
+ rma.price_unit = 999
+ rma.action_rma_to_approve()
+ self._receive_rma(rma)
+ layers = rma.move_ids.sudo().stock_valuation_layer_ids
+ gdni_amls = layers.account_move_id.line_ids.filtered(
+ lambda l: l.account_id == self.account_gdni
+ )
+ gdni_balance = sum(gdni_amls.mapped("balance"))
+ self.assertEqual(len(gdni_amls), 1)
+ # Balance should be -30, as we have only received
+ self.assertEqual(gdni_balance, -30.0)
+ self._deliver_rma(rma)
+ layers = rma.move_ids.sudo().stock_valuation_layer_ids
+ gdni_amls = layers.account_move_id.line_ids.filtered(
+ lambda l: l.account_id == self.account_gdni
+ )
+ gdni_balance = sum(gdni_amls.mapped("balance"))
+ self.assertEqual(len(gdni_amls), 2)
+ # Balance should be 0, as we have received and shipped
+ 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 dfe569fb..825046a3 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: