mirror of
https://github.com/ForgeFlow/stock-rma.git
synced 2025-01-21 12:57:49 +02:00
[14.0][FIX] rma_account: reconcile GDNI when receiving but refund is expected
This commit is contained in:
committed by
AaronHForgeFlow
parent
f6b203f37d
commit
45f5fe0047
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -21,6 +21,10 @@
|
||||
<field name="valid_refund_journal_ids" invisible="1" />
|
||||
<field name="refund_journal_id" />
|
||||
</field>
|
||||
<field name="customer_to_supplier" position="before">
|
||||
<field name="automated_refund" />
|
||||
<field name="refund_free_of_charge" />
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user