mirror of
https://github.com/ForgeFlow/stock-rma.git
synced 2025-01-21 12:57:49 +02:00
[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:
@@ -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")
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -3,3 +3,4 @@ from . import rma_order_line
|
||||
from . import rma_operation
|
||||
from . import account_move
|
||||
from . import procurement
|
||||
from . import stock_move
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
16
rma_account/models/stock_move.py
Normal file
16
rma_account/models/stock_move.py
Normal 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
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user