[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
acc_type = cls._create_account_type("expense", "other", "expense")
name = "Cost of Goods Sold"
code = "cogs"
name = "Goods Delivered Not Invoiced"
code = "gdni"
cls.account_cogs = cls._create_account(acc_type, name, code, cls.company)
# Create account for Inventory
acc_type = cls._create_account_type("asset", "other", "asset")

View File

@@ -58,6 +58,18 @@ class RmaOrderLine(models.Model):
pickings |= move.picking_id
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
def _get_out_pickings(self):
pickings = self.env["stock.picking"]

View File

@@ -3,3 +3,4 @@ from . import rma_order_line
from . import rma_operation
from . import account_move
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
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):
_inherit = "account.move.line"
@@ -179,3 +192,37 @@ class AccountMoveLine(models.Model):
ondelete="set null",
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
else:
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.account_model = cls.env["account.account"]
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
# running FIFO
cls.product_fifo_1 = cls._create_product("product_fifo1")
cls.product_fifo_2 = cls._create_product("product_fifo2")
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)]})
# The product category created in the base module is not automated valuation
# we have to create a new category here
@@ -27,11 +32,11 @@ class TestRmaStockAccount(TestRma):
name = "Goods Received Not Invoiced"
code = "grni"
cls.account_grni = cls._create_account(acc_type, name, code, cls.company, True)
# Create account for Cost of Goods Sold
acc_type = cls._create_account_type("expense", "other")
name = "Cost of Goods Sold"
code = "cogs"
cls.account_cogs = cls._create_account(acc_type, name, code, cls.company, False)
# Create account for Goods Delievered
acc_type = cls._create_account_type("asset", "other")
name = "Goods Delivered Not Invoiced"
code = "gdni"
cls.account_gdni = cls._create_account(acc_type, name, code, cls.company, True)
# Create account for Inventory
acc_type = cls._create_account_type("asset", "other")
name = "Inventory"
@@ -45,9 +50,9 @@ class TestRmaStockAccount(TestRma):
"property_stock_valuation_account_id": cls.account_inventory.id,
"property_valuation": "real_time",
"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_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,
"property_cost_method": "fifo",
}
@@ -103,7 +108,7 @@ class TestRmaStockAccount(TestRma):
self.assertEqual(picking.move_lines.stock_valuation_layer_ids.value, 15.0)
account_move = picking.move_lines.stock_valuation_layer_ids.account_move_id
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):
@@ -130,8 +135,9 @@ class TestRmaStockAccount(TestRma):
dropship=False,
)
# Set an incorrect price in the RMA (this should not affect cost)
rma_customer_id.rma_line_ids.price_unit = 999
rma_customer_id.rma_line_ids.action_rma_to_approve()
rma_lines = rma_customer_id.rma_line_ids
rma_lines.price_unit = 999
rma_lines.action_rma_to_approve()
picking = self._receive_rma(rma_customer_id.rma_line_ids)
# Test the value in the layers of the incoming stock move is used
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
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"
).action_post()
for rma_line in self.rma_customer_id.rma_line_ids:
rma_line._compute_unreconciled()
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()
# The debits and credits are reconciled automatically
rma_line._compute_unreconciled()
self.assertFalse(rma_line.unreconciled)

View File

@@ -14,40 +14,40 @@ class TestRmaStockAccountPurchase(TestRmaStockAccount):
super(TestRmaStockAccountPurchase, cls).setUpClass()
cls.pol_model = cls.env["purchase.order.line"]
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):
"""
Test the price unit is taken from the cost of the stock move associated to
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
supplier_view = self.env.ref("rma_purchase.view_rma_line_form")
rma_line = Form(
self.rma_line.with_context(supplier=1).with_user(self.rma_basic_user),
view=supplier_view.id,
)
rma_line.partner_id = self.po.partner_id
rma_line.purchase_order_line_id = self.pol_1
rma_line.partner_id = po.partner_id
rma_line.purchase_order_line_id = pol_1
rma_line.price_unit = 4356
rma_line = rma_line.save()
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
# of the PO
rma_move_value = picking.move_lines.stock_valuation_layer_ids.value
po_move_value = self.po.picking_ids.mapped(
"move_lines.stock_valuation_layer_ids"
)[-1].value
po_move_value = po.picking_ids.mapped("move_lines.stock_valuation_layer_ids")[
-1
].value
self.assertEqual(-rma_move_value, po_move_value)
# Test the accounts used
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
account_move = picking.move_lines.stock_valuation_layer_ids.account_move_id
self.check_accounts_used(
account_move, debit_account="inventory", credit_account="cogs"
account_move, debit_account="inventory", credit_account="gdni"
)