mirror of
https://github.com/ForgeFlow/stock-rma.git
synced 2025-01-21 12:57:49 +02:00
Merge pull request #314 from ForgeFlow/15.0-rma_account-anglo-saxon
[15.0][FIX] include anglo-saxon price unit calculation in refunds.
This commit is contained in:
@@ -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"]
|
||||
|
||||
@@ -5,3 +5,4 @@ from . import account_move
|
||||
from . import account_move_line
|
||||
from . import procurement
|
||||
from . import stock_move
|
||||
from . import stock_valuation_layer
|
||||
|
||||
@@ -121,6 +121,19 @@ class AccountMove(models.Model):
|
||||
line.update({"rma_line_id": find_with_label_rma.id})
|
||||
return res
|
||||
|
||||
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"
|
||||
@@ -203,3 +216,35 @@ 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
|
||||
|
||||
@@ -307,3 +307,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_valuation_layer.py
Normal file
16
rma_account/models/stock_valuation_layer.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# Copyright 2017-22 ForgeFlow S.L. (www.forgeflow.com)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class StockValuationLayer(models.Model):
|
||||
_inherit = "stock.valuation.layer"
|
||||
|
||||
def _validate_accounting_entries(self):
|
||||
res = super(StockValuationLayer, self)._validate_accounting_entries()
|
||||
for svl in self:
|
||||
# Eventually reconcile together the stock interim accounts
|
||||
if svl.company_id.anglo_saxon_accounting:
|
||||
svl.stock_move_id.rma_line_id._stock_account_anglo_saxon_reconcile_valuation()
|
||||
return res
|
||||
@@ -42,8 +42,8 @@ class TestAccountMoveLineRmaOrderLine(common.TransactionCase):
|
||||
|
||||
# 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")
|
||||
|
||||
@@ -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,104 @@ 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)
|
||||
|
||||
101
rma_account_unreconciled/tests/test_rma_account_unreconciled.py
Normal file
101
rma_account_unreconciled/tests/test_rma_account_unreconciled.py
Normal file
@@ -0,0 +1,101 @@
|
||||
# Copyright 2022 ForgeFlow S.L.
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
|
||||
|
||||
|
||||
from odoo.addons.rma.tests.test_rma import TestRma
|
||||
|
||||
|
||||
class TestRmaAccountUnreconciled(TestRma):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.rma_refund_wiz = cls.env["rma.refund"]
|
||||
cls.g_account_manager = cls.env.ref("account.group_account_manager")
|
||||
cls.rma_manager_user_account = cls._create_user(
|
||||
"rma manager account",
|
||||
[cls.g_stock_manager, cls.g_rma_manager, cls.g_account_manager],
|
||||
cls.company,
|
||||
)
|
||||
for categ in cls.rma_customer_id.with_user(cls.rma_manager_user_account).mapped(
|
||||
"rma_line_ids.product_id.categ_id"
|
||||
):
|
||||
categ.write(
|
||||
{
|
||||
"property_valuation": "real_time",
|
||||
"property_cost_method": "fifo",
|
||||
}
|
||||
)
|
||||
categ.property_stock_account_input_categ_id.write(
|
||||
{
|
||||
"reconcile": True,
|
||||
}
|
||||
)
|
||||
categ.property_stock_account_output_categ_id.write(
|
||||
{
|
||||
"reconcile": True,
|
||||
}
|
||||
)
|
||||
for product in cls.rma_customer_id.with_user(
|
||||
cls.rma_manager_user_account
|
||||
).mapped("rma_line_ids.product_id"):
|
||||
product.write(
|
||||
{
|
||||
"standard_price": 10.0,
|
||||
}
|
||||
)
|
||||
|
||||
def test_unreconciled_moves(self):
|
||||
for rma_line in self.rma_customer_id.rma_line_ids:
|
||||
rma_line.write(
|
||||
{
|
||||
"refund_policy": "received",
|
||||
}
|
||||
)
|
||||
rma_line.action_rma_approve()
|
||||
self.assertFalse(rma_line.unreconciled)
|
||||
self.rma_customer_id.rma_line_ids.action_rma_to_approve()
|
||||
wizard = self.rma_make_pickingwith_context(
|
||||
**{
|
||||
"active_ids": self.rma_customer_id.rma_line_ids.ids,
|
||||
"active_model": "rma.order.line",
|
||||
"picking_type": "incoming",
|
||||
"active_id": 1,
|
||||
}
|
||||
).create({})
|
||||
wizard._create_picking()
|
||||
res = self.rma_customer_id.rma_line_ids.action_view_in_shipments()
|
||||
picking = self.env["stock.picking"].browse(res["res_id"])
|
||||
picking.action_assign()
|
||||
for mv in picking.move_lines:
|
||||
mv.quantity_done = mv.product_uom_qty
|
||||
picking.button_validate()
|
||||
for rma_line in self.rma_customer_id.rma_line_ids:
|
||||
rma_line._compute_unreconciled()
|
||||
self.assertTrue(rma_line.unreconciled)
|
||||
make_refund = self.rma_refund_wiz.with_context(
|
||||
**{
|
||||
"customer": True,
|
||||
"active_ids": self.rma_customer_id.rma_line_ids.ids,
|
||||
"active_model": "rma.order.line",
|
||||
}
|
||||
).create(
|
||||
{
|
||||
"description": "Test refund",
|
||||
}
|
||||
)
|
||||
for item in make_refund.item_ids:
|
||||
item.write(
|
||||
{
|
||||
"qty_to_refund": item.product_qty,
|
||||
}
|
||||
)
|
||||
make_refund.invoice_refund()
|
||||
self.rma_customer_id.with_user(
|
||||
self.rma_manager_user_account
|
||||
).rma_line_ids.refund_line_ids.move_id.filtered(
|
||||
lambda x: x.state != "posted"
|
||||
).action_post()
|
||||
for rma_line in self.rma_customer_id.rma_line_ids:
|
||||
# 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