[FIX] include anglo-saxon price unit calculation in refunds.

Otherwise the anglo saxon entries won't be correct.
For example, the Interim (Delivered) account should balance
after receiving and triggering a refund on a customer rma.
This commit is contained in:
Jordi Ballester Alomar
2022-11-21 15:13:06 +01:00
parent 07ad5f9e03
commit 090905791d
10 changed files with 240 additions and 58 deletions

View File

@@ -42,8 +42,8 @@ class TestAccountMoveLineRmaOrderLine(common.SavepointCase):
# Create account for Cost of Goods Sold # Create account for Cost of Goods Sold
acc_type = cls._create_account_type("expense", "other", "expense") acc_type = cls._create_account_type("expense", "other", "expense")
name = "Cost of Goods Sold" name = "Goods Delivered Not Invoiced"
code = "cogs" code = "gdni"
cls.account_cogs = cls._create_account(acc_type, name, code, cls.company) cls.account_cogs = cls._create_account(acc_type, name, code, cls.company)
# Create account for Inventory # Create account for Inventory
acc_type = cls._create_account_type("asset", "other", "asset") acc_type = cls._create_account_type("asset", "other", "asset")

View File

@@ -58,6 +58,18 @@ class RmaOrderLine(models.Model):
pickings |= move.picking_id pickings |= move.picking_id
return pickings return pickings
@api.model
def _get_in_moves(self):
moves = self.env["stock.move"]
for move in self.move_ids:
first_usage = move._get_first_usage()
last_usage = move._get_last_usage()
if last_usage == "internal" and first_usage != "internal":
moves |= move
elif last_usage == "supplier" and first_usage == "customer":
moves |= moves
return moves
@api.model @api.model
def _get_out_pickings(self): def _get_out_pickings(self):
pickings = self.env["stock.picking"] pickings = self.env["stock.picking"]

View File

@@ -3,3 +3,4 @@ from . import rma_order_line
from . import rma_operation from . import rma_operation
from . import account_move from . import account_move
from . import procurement from . import procurement
from . import stock_move

View File

@@ -97,6 +97,19 @@ class AccountMove(models.Model):
result["res_id"] = rma_ids and rma_ids[0] or False result["res_id"] = rma_ids and rma_ids[0] or False
return result return result
def _stock_account_get_last_step_stock_moves(self):
rslt = super(AccountMove, self)._stock_account_get_last_step_stock_moves()
for invoice in self.filtered(lambda x: x.move_type == "out_invoice"):
rslt += invoice.mapped("line_ids.rma_line_id.move_ids").filtered(
lambda x: x.state == "done" and x.location_dest_id.usage == "customer"
)
for invoice in self.filtered(lambda x: x.move_type == "out_refund"):
# Add refunds generated from the RMA
rslt += invoice.mapped("line_ids.rma_line_id.move_ids").filtered(
lambda x: x.state == "done" and x.location_id.usage == "customer"
)
return rslt
class AccountMoveLine(models.Model): class AccountMoveLine(models.Model):
_inherit = "account.move.line" _inherit = "account.move.line"
@@ -179,3 +192,37 @@ class AccountMoveLine(models.Model):
ondelete="set null", ondelete="set null",
help="This will contain the rma line that originated this line", help="This will contain the rma line that originated this line",
) )
def _stock_account_get_anglo_saxon_price_unit(self):
self.ensure_one()
price_unit = super(
AccountMoveLine, self
)._stock_account_get_anglo_saxon_price_unit()
rma_line = self.rma_line_id or self.env["rma.order.line"]
if rma_line:
is_line_reversing = bool(self.move_id.reversed_entry_id)
qty_to_refund = self.product_uom_id._compute_quantity(
self.quantity, self.product_id.uom_id
)
posted_invoice_lines = rma_line.move_line_ids.filtered(
lambda l: l.move_id.move_type == "out_refund"
and l.move_id.state == "posted"
and bool(l.move_id.reversed_entry_id) == is_line_reversing
)
qty_refunded = sum(
[
x.product_uom_id._compute_quantity(x.quantity, x.product_id.uom_id)
for x in posted_invoice_lines
]
)
product = self.product_id.with_company(self.company_id).with_context(
is_returned=is_line_reversing
)
average_price_unit = product._compute_average_price(
qty_refunded, qty_to_refund, rma_line._get_in_moves()
)
if average_price_unit:
price_unit = self.product_id.uom_id.with_company(
self.company_id
)._compute_price(average_price_unit, self.product_uom_id)
return price_unit

View File

@@ -310,3 +310,29 @@ class RmaOrderLine(models.Model):
return res return res
else: else:
return super(RmaOrderLine, self).name_get() return super(RmaOrderLine, self).name_get()
def _stock_account_anglo_saxon_reconcile_valuation(self):
for rma in self:
prod = rma.product_id
if rma.product_id.valuation != "real_time":
continue
if not rma.company_id.anglo_saxon_accounting:
continue
product_accounts = prod.product_tmpl_id._get_product_accounts()
if rma.type == "customer":
product_interim_account = product_accounts["stock_output"]
else:
product_interim_account = product_accounts["stock_input"]
if product_interim_account.reconcile:
# Get the in and out moves
amls = rma.move_ids.mapped(
"stock_valuation_layer_ids.account_move_id.line_ids"
)
# Search for anglo-saxon lines linked to the product in the journal entry.
amls = amls.filtered(
lambda line: line.product_id == prod
and line.account_id == product_interim_account
and not line.reconciled
)
# Reconcile.
amls.reconcile()

View File

@@ -0,0 +1,16 @@
# Copyright 2017-2022 ForgeFlow S.L.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html)
from odoo import models
class StockMove(models.Model):
_inherit = "stock.move"
def _account_entry_move(self, qty, description, svl_id, cost):
res = super(StockMove, self)._account_entry_move(qty, description, svl_id, cost)
if self.company_id.anglo_saxon_accounting:
# Eventually reconcile together the invoice and valuation accounting
# entries on the stock interim accounts
self.rma_line_id._stock_account_anglo_saxon_reconcile_valuation()
return res

View File

@@ -14,11 +14,16 @@ class TestRmaStockAccount(TestRma):
cls.acc_type_model = cls.env["account.account.type"] cls.acc_type_model = cls.env["account.account.type"]
cls.account_model = cls.env["account.account"] cls.account_model = cls.env["account.account"]
cls.g_account_user = cls.env.ref("account.group_account_user") cls.g_account_user = cls.env.ref("account.group_account_user")
cls.rma_refund_wiz = cls.env["rma.refund"]
# we create new products to ensure previous layers do not affect when # we create new products to ensure previous layers do not affect when
# running FIFO # running FIFO
cls.product_fifo_1 = cls._create_product("product_fifo1") cls.product_fifo_1 = cls._create_product("product_fifo1")
cls.product_fifo_2 = cls._create_product("product_fifo2") cls.product_fifo_2 = cls._create_product("product_fifo2")
cls.product_fifo_3 = cls._create_product("product_fifo3") cls.product_fifo_3 = cls._create_product("product_fifo3")
# Refs
cls.rma_operation_customer_refund_id = cls.env.ref(
"rma_account.rma_operation_customer_refund"
)
cls.rma_basic_user.write({"groups_id": [(4, cls.g_account_user.id)]}) cls.rma_basic_user.write({"groups_id": [(4, cls.g_account_user.id)]})
# The product category created in the base module is not automated valuation # The product category created in the base module is not automated valuation
# we have to create a new category here # we have to create a new category here
@@ -27,11 +32,11 @@ class TestRmaStockAccount(TestRma):
name = "Goods Received Not Invoiced" name = "Goods Received Not Invoiced"
code = "grni" code = "grni"
cls.account_grni = cls._create_account(acc_type, name, code, cls.company, True) cls.account_grni = cls._create_account(acc_type, name, code, cls.company, True)
# Create account for Cost of Goods Sold # Create account for Goods Delievered
acc_type = cls._create_account_type("expense", "other") acc_type = cls._create_account_type("asset", "other")
name = "Cost of Goods Sold" name = "Goods Delivered Not Invoiced"
code = "cogs" code = "gdni"
cls.account_cogs = cls._create_account(acc_type, name, code, cls.company, False) cls.account_gdni = cls._create_account(acc_type, name, code, cls.company, True)
# Create account for Inventory # Create account for Inventory
acc_type = cls._create_account_type("asset", "other") acc_type = cls._create_account_type("asset", "other")
name = "Inventory" name = "Inventory"
@@ -45,9 +50,9 @@ class TestRmaStockAccount(TestRma):
"property_stock_valuation_account_id": cls.account_inventory.id, "property_stock_valuation_account_id": cls.account_inventory.id,
"property_valuation": "real_time", "property_valuation": "real_time",
"property_stock_account_input_categ_id": cls.account_grni.id, "property_stock_account_input_categ_id": cls.account_grni.id,
"property_stock_account_output_categ_id": cls.account_cogs.id, "property_stock_account_output_categ_id": cls.account_gdni.id,
"rma_approval_policy": "one_step", "rma_approval_policy": "one_step",
"rma_customer_operation_id": cls.rma_cust_replace_op_id.id, "rma_customer_operation_id": cls.rma_operation_customer_refund_id.id,
"rma_supplier_operation_id": cls.rma_sup_replace_op_id.id, "rma_supplier_operation_id": cls.rma_sup_replace_op_id.id,
"property_cost_method": "fifo", "property_cost_method": "fifo",
} }
@@ -103,7 +108,7 @@ class TestRmaStockAccount(TestRma):
self.assertEqual(picking.move_lines.stock_valuation_layer_ids.value, 15.0) self.assertEqual(picking.move_lines.stock_valuation_layer_ids.value, 15.0)
account_move = picking.move_lines.stock_valuation_layer_ids.account_move_id account_move = picking.move_lines.stock_valuation_layer_ids.account_move_id
self.check_accounts_used( self.check_accounts_used(
account_move, debit_account="inventory", credit_account="cogs" account_move, debit_account="inventory", credit_account="gdni"
) )
def test_02_cost_from_move(self): def test_02_cost_from_move(self):
@@ -130,8 +135,9 @@ class TestRmaStockAccount(TestRma):
dropship=False, dropship=False,
) )
# Set an incorrect price in the RMA (this should not affect cost) # Set an incorrect price in the RMA (this should not affect cost)
rma_customer_id.rma_line_ids.price_unit = 999 rma_lines = rma_customer_id.rma_line_ids
rma_customer_id.rma_line_ids.action_rma_to_approve() rma_lines.price_unit = 999
rma_lines.action_rma_to_approve()
picking = self._receive_rma(rma_customer_id.rma_line_ids) picking = self._receive_rma(rma_customer_id.rma_line_ids)
# Test the value in the layers of the incoming stock move is used # Test the value in the layers of the incoming stock move is used
for rma_line in rma_customer_id.rma_line_ids: for rma_line in rma_customer_id.rma_line_ids:
@@ -141,3 +147,96 @@ class TestRmaStockAccount(TestRma):
) )
value_used = move_product.stock_valuation_layer_ids.value value_used = move_product.stock_valuation_layer_ids.value
self.assertEqual(value_used, -value_origin) self.assertEqual(value_used, -value_origin)
# Create a refund for the first line
rma = rma_lines[0]
make_refund = self.rma_refund_wiz.with_context(
{"customer": True, "active_ids": rma.ids, "active_model": "rma.order.line"}
).create({"description": "Test refund"})
make_refund.item_ids.qty_to_refund = 1
make_refund.invoice_refund()
rma.refund_line_ids.move_id.action_post()
rma._compute_refund_count()
gdni_amls = rma.refund_line_ids.move_id.line_ids.filtered(
lambda l: l.account_id == self.account_gdni
)
gdni_amls |= (
rma.move_ids.stock_valuation_layer_ids.account_move_id.line_ids.filtered(
lambda l: l.account_id == self.account_gdni
)
)
gdni_balance = sum(gdni_amls.mapped("balance"))
# When we received we Credited to GDNI 30
# When we refund we Debit to GDNI 10
self.assertEqual(gdni_balance, -20.0)
make_refund = self.rma_refund_wiz.with_context(
{"customer": True, "active_ids": rma.ids, "active_model": "rma.order.line"}
).create({"description": "Test refund"})
make_refund.item_ids.qty_to_refund = 2
make_refund.invoice_refund()
rma.refund_line_ids.move_id.filtered(
lambda m: m.state != "posted"
).action_post()
rma._compute_refund_count()
gdni_amls = rma.refund_line_ids.move_id.line_ids.filtered(
lambda l: l.account_id == self.account_gdni
)
gdni_amls |= (
rma.move_ids.stock_valuation_layer_ids.account_move_id.line_ids.filtered(
lambda l: l.account_id == self.account_gdni
)
)
gdni_balance = sum(gdni_amls.mapped("balance"))
# When we received we Credited to GDNI 30
# When we refund we Debit to GDNI 30
self.assertEqual(gdni_balance, 0.0)
# Ensure that the GDNI move lines are all reconciled
self.assertEqual(all(gdni_amls.mapped("reconciled")), True)
def test_03_cost_from_move(self):
"""
Receive a product and then return it. The Goods Delivered Not Invoiced
should result in 0
"""
# 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_customer_id.rma_line_ids)
gdni_amls = (
rma.move_ids.stock_valuation_layer_ids.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_customer_id.rma_line_ids)
gdni_amls = (
rma.move_ids.stock_valuation_layer_ids.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)

View File

@@ -96,25 +96,6 @@ class TestRmaAccountUnreconciled(TestRma):
lambda x: x.state != "posted" lambda x: x.state != "posted"
).action_post() ).action_post()
for rma_line in self.rma_customer_id.rma_line_ids: for rma_line in self.rma_customer_id.rma_line_ids:
rma_line._compute_unreconciled() # The debits and credits are reconciled automatically
self.assertTrue(rma_line.unreconciled)
self.assertEqual(
self.env["rma.order.line"].search_count(
[
("type", "=", "customer"),
("unreconciled", "=", True),
("rma_id", "=", self.rma_customer_id.id),
]
),
3,
)
for rma_line in self.rma_customer_id.rma_line_ids:
aml_domain = rma_line.sudo().action_view_unreconciled().get("domain")
aml_lines = (
aml_domain and self.env["account.move.line"].search(aml_domain) or False
)
if aml_lines:
aml_lines.reconcile()
rma_line._compute_unreconciled() rma_line._compute_unreconciled()
self.assertFalse(rma_line.unreconciled) self.assertFalse(rma_line.unreconciled)

View File

@@ -14,40 +14,40 @@ class TestRmaStockAccountPurchase(TestRmaStockAccount):
super(TestRmaStockAccountPurchase, cls).setUpClass() super(TestRmaStockAccountPurchase, cls).setUpClass()
cls.pol_model = cls.env["purchase.order.line"] cls.pol_model = cls.env["purchase.order.line"]
cls.po_model = cls.env["purchase.order"] cls.po_model = cls.env["purchase.order"]
# Create PO:
cls.product_fifo_1.standard_price = 1234
cls.po = cls.po_model.create(
{
"partner_id": cls.partner_id.id,
}
)
cls.pol_1 = cls.pol_model.create(
{
"name": cls.product_fifo_1.name,
"order_id": cls.po.id,
"product_id": cls.product_fifo_1.id,
"product_qty": 20.0,
"product_uom": cls.product_fifo_1.uom_id.id,
"price_unit": 100.0,
"date_planned": Datetime.now(),
}
)
cls.po.button_confirm()
cls._do_picking(cls.po.picking_ids)
def test_01_cost_from_po_move(self): def test_01_cost_from_po_move(self):
""" """
Test the price unit is taken from the cost of the stock move associated to Test the price unit is taken from the cost of the stock move associated to
the PO the PO
""" """
# Create PO:
self.product_fifo_1.standard_price = 1234
po = self.po_model.create(
{
"partner_id": self.partner_id.id,
}
)
pol_1 = self.pol_model.create(
{
"name": self.product_fifo_1.name,
"order_id": po.id,
"product_id": self.product_fifo_1.id,
"product_qty": 20.0,
"product_uom": self.product_fifo_1.uom_id.id,
"price_unit": 100.0,
"date_planned": Datetime.now(),
}
)
po.button_confirm()
self._do_picking(po.picking_ids)
self.product_fifo_1.standard_price = 1234 # this should not be taken self.product_fifo_1.standard_price = 1234 # this should not be taken
supplier_view = self.env.ref("rma_purchase.view_rma_line_form") supplier_view = self.env.ref("rma_purchase.view_rma_line_form")
rma_line = Form( rma_line = Form(
self.rma_line.with_context(supplier=1).with_user(self.rma_basic_user), self.rma_line.with_context(supplier=1).with_user(self.rma_basic_user),
view=supplier_view.id, view=supplier_view.id,
) )
rma_line.partner_id = self.po.partner_id rma_line.partner_id = po.partner_id
rma_line.purchase_order_line_id = self.pol_1 rma_line.purchase_order_line_id = pol_1
rma_line.price_unit = 4356 rma_line.price_unit = 4356
rma_line = rma_line.save() rma_line = rma_line.save()
rma_line.action_rma_to_approve() rma_line.action_rma_to_approve()
@@ -55,9 +55,9 @@ class TestRmaStockAccountPurchase(TestRmaStockAccount):
# The price is not the standard price, is the value of the incoming layer # The price is not the standard price, is the value of the incoming layer
# of the PO # of the PO
rma_move_value = picking.move_lines.stock_valuation_layer_ids.value rma_move_value = picking.move_lines.stock_valuation_layer_ids.value
po_move_value = self.po.picking_ids.mapped( po_move_value = po.picking_ids.mapped("move_lines.stock_valuation_layer_ids")[
"move_lines.stock_valuation_layer_ids" -1
)[-1].value ].value
self.assertEqual(-rma_move_value, po_move_value) self.assertEqual(-rma_move_value, po_move_value)
# Test the accounts used # Test the accounts used
account_move = picking.move_lines.stock_valuation_layer_ids.account_move_id account_move = picking.move_lines.stock_valuation_layer_ids.account_move_id

View File

@@ -70,5 +70,5 @@ class TestRmaStockAccountSale(TestRmaStockAccount):
# Test the accounts used # Test the accounts used
account_move = picking.move_lines.stock_valuation_layer_ids.account_move_id account_move = picking.move_lines.stock_valuation_layer_ids.account_move_id
self.check_accounts_used( self.check_accounts_used(
account_move, debit_account="inventory", credit_account="cogs" account_move, debit_account="inventory", credit_account="gdni"
) )