Skip to content

Commit e711280

Browse files
maq-adhocrov-adhoc
authored andcommitted
[FIX] stock_currency_valuation: resolve the valuation currency per record company
Contract of the seam ``account-multicompany-ux`` <-> ``stock-currency-valuation``: the valuation currency lives on the product CATEGORY and is company-dependent, so reading it off the ambient company answers for the wrong one as soon as the record belongs to another — routine with multi-company UX, where a user can have company A selected and operate on a record of company B. ``stock.quant._compute_secondary_value`` read the guard off the ambient company and the price with ``with_company(quant.company_id)``. When the active company has no valuation currency and the quant's does, the guard answers "none" —true for the active one— and the quant is skipped: its secondary value is not wrong, it is missing. The product is now resolved once with the quant's company and reused for both. ``stock.move._set_value`` had the same read on its conversion branch. The three violations this was expected to fix in ``product.product`` do not exist, and were checked one by one before touching anything: that model has no company of its own, so for its company-dependent fields the CONTEXT company is the semantic one. ``_get_last_product_value`` filters by ``self.env.company`` exactly as the core does, and ``write`` / ``_change_standard_price`` read in the very company they are writing to. ``stock.picking`` and ``stock.landed.cost`` already comply. Left out on purpose: ``valuation_currency_id`` is a ``related`` on ``stock.move``, ``stock.quant`` and the AVCO report, and a ``related`` always resolves in the ambient company with no way to apply ``with_company``, so two companies valuing the same category in different currencies can see a right amount under the wrong symbol. The fix is a compute with the record's company, which has consequences for views and searches. Verified in both directions: with the fix the suite is green, and without it the test fails 0.0 != 3500.0. The test uses a category whose valuation currency exists ONLY in company B and re-browses the quant from an env with company A — reading it on the ``with_company(company_b)`` recordset that created it proves nothing, since a company-dependent field resolves off the RECORDSET's context. Part-of: #1004 Related: ingadhoc/account-financial-tools#984 Related: ingadhoc/product#937 Related: ingadhoc/miscellaneous#436 Signed-off-by: Camila Vives <cav@adhoc.inc>
1 parent f67a368 commit e711280

4 files changed

Lines changed: 128 additions & 5 deletions

File tree

stock_currency_valuation/models/stock_move.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,14 @@ def _set_value(self, correction_quantity=None):
102102
base_value_in_currency = base_value * move.picking_id.currency_rate
103103
move.value_in_currency = base_value_in_currency + lc_value_in_currency
104104
else:
105-
base_value_in_currency = move.with_company(move.company_id).company_id.currency_id._convert(
105+
# ``valuation_currency_id`` comes from the product category and is
106+
# company-dependent: resolved off the move's own company, not the
107+
# ambient one, which can be another company entirely under
108+
# account_multicompany_ux.
109+
move_in_company = move.with_company(move.company_id)
110+
base_value_in_currency = move_in_company.company_id.currency_id._convert(
106111
from_amount=base_value,
107-
to_currency=move.valuation_currency_id,
112+
to_currency=move_in_company.valuation_currency_id,
108113
company=move.company_id,
109114
date=move.date,
110115
)

stock_currency_valuation/models/stock_quant.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,20 @@ class StockQuant(models.Model):
2525
def _compute_secondary_value(self):
2626
self.secondary_value = 0.0
2727
for quant in self:
28-
if not quant.valuation_currency_id:
28+
# The product is resolved ONCE with the quant's own company and reused for
29+
# both the guard and the price. The valuation currency lives on the product
30+
# category and is company-dependent, so reading it off the ambient company
31+
# —as ``quant.valuation_currency_id`` does— can disagree with the price read
32+
# below: with account_multicompany_ux a user can have company A selected
33+
# while looking at a quant of company B, and then the guard answered for A
34+
# and the amount for B.
35+
product = quant.product_id.with_company(quant.company_id)
36+
if not product.valuation_currency_id:
2937
continue
3038
if not quant.location_id or not quant.product_id:
3139
continue
3240
if not quant.location_id._should_be_valued() or quant._should_exclude_for_valuation():
3341
continue
3442
if quant.product_id.uom_id.is_zero(quant.quantity):
3543
continue
36-
secondary_price = quant.product_id.with_company(quant.company_id).standard_price_in_currency
37-
quant.secondary_value = quant.quantity * secondary_price
44+
quant.secondary_value = quant.quantity * product.standard_price_in_currency

stock_currency_valuation/tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
from . import test_delivery_and_return
66
from . import test_replenishment_cost_average_in_currency
77
from . import test_avco_report_uom
8+
from . import test_multicompany_currency
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
from odoo import Command
2+
from odoo.tests import tagged
3+
4+
from .common import TestStockCurrencyValuationCommon
5+
6+
7+
@tagged("post_install", "-at_install")
8+
class TestMultiCompanyCurrency(TestStockCurrencyValuationCommon):
9+
"""The valuation currency lives on the product CATEGORY and is company-dependent, so
10+
every backend read has to resolve it with the company of the RECORD and not with the
11+
active one.
12+
13+
Contract of the seam ``account-multicompany-ux`` <-> ``stock-currency-valuation``:
14+
with multi-company UX a user can have company A selected while operating on a record
15+
of company B, and a company-dependent field read off the context then answers for the
16+
wrong company — silently, returning a number instead of failing.
17+
"""
18+
19+
@classmethod
20+
def setUpClass(cls):
21+
super().setUpClass()
22+
cls.company_b = cls.env["res.company"].create({"name": "SCV Company B"})
23+
cls.env.user.company_ids = [Command.link(cls.company_b.id)]
24+
cls.currency_b = cls.env["res.currency"].create(
25+
{"name": "SCB", "symbol": "B$", "rounding": 0.01}
26+
)
27+
cls.env["res.currency.rate"].create(
28+
{
29+
"name": cls.DAY_1,
30+
"rate": cls.RATE_D1,
31+
"currency_id": cls.currency_b.id,
32+
"company_id": cls.company_b.id,
33+
}
34+
)
35+
# SAME category, a different valuation currency in each company.
36+
cls.category.with_company(cls.company_b).valuation_currency_id = cls.currency_b
37+
# And a different secondary cost in each one, so a crossed read is visible.
38+
cls.product.with_company(cls.company).standard_price_in_currency = 100.0
39+
cls.product.with_company(cls.company_b).standard_price_in_currency = 700.0
40+
cls.warehouse_b = cls.env["stock.warehouse"].search(
41+
[("company_id", "=", cls.company_b.id)], limit=1
42+
)
43+
# Category whose valuation currency exists ONLY in company B. This is what makes
44+
# the divergence visible: with both companies configured, a read off the wrong one
45+
# still returns A currency and the amount comes out the same, so the test would
46+
# pass either way and prove nothing.
47+
cls.category_b_only = cls.env["product.category"].create(
48+
{"name": "SCV only in B", "property_cost_method": "average"}
49+
)
50+
cls.category_b_only.with_company(cls.company_b).valuation_currency_id = cls.currency_b
51+
cls.product_b_only = cls.env["product.product"].create(
52+
{
53+
"name": "Producto sólo B",
54+
"is_storable": True,
55+
"standard_price": 0.0,
56+
"categ_id": cls.category_b_only.id,
57+
"uom_id": cls.uom_unit.id,
58+
}
59+
)
60+
cls.product_b_only.with_company(cls.company_b).standard_price_in_currency = 700.0
61+
62+
def test_valuation_currency_is_resolved_per_company(self):
63+
self.assertEqual(
64+
self.product.with_company(self.company).valuation_currency_id,
65+
self.secondary_currency,
66+
)
67+
self.assertEqual(
68+
self.product.with_company(self.company_b).valuation_currency_id,
69+
self.currency_b,
70+
)
71+
72+
def test_valuation_currency_absent_in_the_active_company(self):
73+
"""The category has a valuation currency in B and NONE in A."""
74+
self.assertFalse(self.product_b_only.with_company(self.company).valuation_currency_id)
75+
self.assertEqual(
76+
self.product_b_only.with_company(self.company_b).valuation_currency_id,
77+
self.currency_b,
78+
)
79+
80+
def test_quant_secondary_value_uses_the_quants_company(self):
81+
"""A quant of company B valued while company A is the active one, on a category
82+
whose valuation currency exists only in B.
83+
84+
Reading the guard off the ambient company answers "no valuation currency" —that is
85+
true for A— and the quant is skipped, so its secondary value stays at zero even
86+
though in its own company it is worth 5 x 700. The amount does not come out wrong:
87+
it does not come out at all.
88+
"""
89+
self.assertTrue(self.warehouse_b, "Company B needs a warehouse of its own.")
90+
quant = (
91+
self.env["stock.quant"]
92+
.with_company(self.company_b)
93+
.with_context(inventory_mode=True)
94+
.create(
95+
{
96+
"product_id": self.product_b_only.id,
97+
"location_id": self.warehouse_b.lot_stock_id.id,
98+
"inventory_quantity": 5.0,
99+
}
100+
)
101+
)
102+
quant.action_apply_inventory()
103+
# Re-browsed from an env whose company is A and WITHOUT B in the context: a
104+
# company-dependent field resolves off the RECORDSET's context, so reading it on
105+
# the ``with_company(company_b)`` recordset used to create the quant would answer
106+
# for B either way and the test would prove nothing.
107+
quant_seen_from_a = self.env["stock.quant"].browse(quant.id)
108+
self.assertEqual(quant_seen_from_a.env.company, self.company)
109+
self.assertFalse(quant_seen_from_a.valuation_currency_id)
110+
self.assertAlmostEqual(quant_seen_from_a.secondary_value, 5.0 * 700.0)

0 commit comments

Comments
 (0)