[FIX] stock_landed_costs: account/valuation inconsistencies

When stock landed costs are divided per product unit, inconsistencies
may arise between the real stock valuation and the stock valuation
account. This is likely to happen when several products are bought, but
these products leave the stock one at a time.

A numerical example is the following: a landed cost of 15.00 is applied
to a purchase of 13 units. An amount of 15.00 is recorded when the
products enter the stock. If the product leave the stock one at a time,
13 entries of 1.15 are recorded (15.00/13 = 1.153846... ≈ 1.15), which
is then equal to 13 * 1.15 = 14.95. In this case, All the products have
left the stock (stock valuation is zero), but 5 cents remain on the
account.

This is of course even worse the higher the ratio is. For example, a
landed cost of 4.00 split into 1000 units sold piece by piece will never
be recorded when a product leaves the stock.

The fix is to record the rounding difference on a specific quant. In the
previous example, instead of adding 1.153846... on the unit cost of the
13 units, we do the following:
- 12 units to which we add 1.15 on unit cost
- 1 unit to which we add 1.20 on unit cost

opw-675222
This commit is contained in:
Nicolas Martinelli 2016-04-20 10:54:18 +02:00
parent 262d98bb69
commit eb26694e0f
3 changed files with 210 additions and 6 deletions

View File

@ -43,7 +43,8 @@ This module allows you to easily add extra costs on pickings and decide the spli
'stock_landed_costs_data.xml',
],
'test': [
'test/stock_landed_costs.yml'
'test/stock_landed_costs.yml',
'test/stock_landed_costs_rounding.yml',
],
'installable': True,
'auto_install': False,

View File

@ -232,13 +232,41 @@ class stock_landed_cost(osv.osv):
per_unit = line.final_cost / line.quantity
# FORWARDPORT UP TO SAAS-10
diff = per_unit - (line.former_cost / line.quantity if line.quantity else 1.0)
quants = [quant for quant in line.move_id.quant_ids]
# If the precision required for the variable diff is larger than the accounting
# precision, inconsistencies between the stock valuation and the accounting entries
# may arise.
# For example, a landed cost of 15 divided in 13 units. If the products leave the
# stock one unit at a time, the amount related to the landed cost will correspond to
# round(15/13, 2)*13 = 14.95. To avoid this case, we split the quant in 12 + 1, then
# record the difference on the new quant.
# We need to make sure to able to extract at least one unit of the product. There is
# an arbitrary minimum quantity set to 2.0 from which we consider we can extract a
# unit and adapt the cost.
curr_rounding = line.move_id.company_id.currency_id.rounding
diff_rounded = float_round(diff, precision_rounding=curr_rounding)
diff_correct = diff_rounded
quants = line.move_id.quant_ids.sorted(key=lambda r: r.qty, reverse=True)
quant_correct = False
if quants\
and float_compare(quants[0].product_id.uom_id.rounding, 1.0, precision_digits=1) == 0\
and float_compare(line.quantity * diff, line.quantity * diff_rounded, precision_rounding=curr_rounding) != 0\
and float_compare(quants[0].qty, 2.0, precision_rounding=quants[0].product_id.uom_id.rounding) >= 0:
# Search for existing quant of quantity = 1.0 to avoid creating a new one
quant_correct = quants.filtered(lambda r: float_compare(r.qty, 1.0, precision_rounding=quants[0].product_id.uom_id.rounding) == 0)
if not quant_correct:
quant_correct = quant_obj._quant_split(cr, uid, quants[0], quants[0].qty - 1.0, context=context)
else:
quant_correct = quant_correct[0]
quants = quants - quant_correct
diff_correct += (line.quantity * diff) - (line.quantity * diff_rounded)
diff = diff_rounded
quant_dict = {}
for quant in quants:
if quant.id not in quant_dict:
quant_dict[quant.id] = quant.cost + diff
else:
quant_dict[quant.id] += diff
quant_dict[quant.id] = quant.cost + diff
if quant_correct:
quant_dict[quant_correct.id] = quant_correct.cost + diff_correct
for key, value in quant_dict.items():
quant_obj.write(cr, SUPERUSER_ID, key, {'cost': value}, context=context)
qty_out = 0

View File

@ -0,0 +1,175 @@
-
In order to test the rounding in landed costs feature of stock, I create 2 landed cost
-
Define undivisible units
-
!record {model: product.uom, id: product_uom_unit_round_1}:
category_id: product.product_uom_categ_unit
name: Undivisible Unit(s)
factor: 1.0
rounding: 1.0
-
I create 2 products with different cost prices and configure them for real_time valuation and real price costing method
-
!record {model: product.product, id: product_landed_cost_3}:
name: "LC product 3"
cost_method: real
uom_id: product_uom_unit_round_1
valuation: real_time
property_stock_account_input: account.o_expense
property_stock_account_output: account.o_income
-
!record {model: product.product, id: product_landed_cost_4}:
name: "LC product 4"
cost_method: real
uom_id: product_uom_unit_round_1
valuation: real_time
property_stock_account_input: account.o_expense
property_stock_account_output: account.o_income
-
I create 2 pickings moving those products
-
!record {model: stock.picking, id: picking_landed_cost_3}:
name: 'LC_pick_3'
picking_type_id: stock.picking_type_in
move_lines:
- name: move 3
product_id: product_landed_cost_3
product_uom_qty: 13
product_uom: product_uom_unit_round_1
product_uos_qty: 13
product_uos: product_uom_unit_round_1
location_id: stock.stock_location_customers
location_dest_id: stock.stock_location_stock
-
!record {model: stock.picking, id: picking_landed_cost_4}:
name: 'LC_pick_4'
picking_type_id: stock.picking_type_in
move_lines:
- name: move 4
product_id: product_landed_cost_4
product_uom_qty: 1
product_uom: product.product_uom_dozen
product_uos_qty: 1
product_uos: product.product_uom_dozen
location_id: stock.stock_location_customers
location_dest_id: stock.stock_location_stock
price_unit: !eval 17.00/12.00
-
We perform all the tests for LC_pick_3
-
I receive picking LC_pick_3, and check how many quants are created
-
!python {model: stock.picking}: |
self.action_confirm(cr, uid, [ref("picking_landed_cost_3")], context=context)
self.action_assign(cr, uid, [ref("picking_landed_cost_3")], context=context)
self.action_done(cr, uid, [ref("picking_landed_cost_3")], context=context)
pick = self.browse(cr, uid, [ref("picking_landed_cost_3")], context=context)
quants = pick.move_lines.quant_ids
assert len(quants) == 1
assert quants.qty == 13
assert quants.cost == 0.0
-
I create a landed cost for picking 3
-
!record {model: stock.landed.cost, id: stock_landed_cost_2}:
picking_ids: [picking_landed_cost_3]
account_journal_id: account.expenses_journal
cost_lines:
- name: 'equal split'
split_method: 'equal'
price_unit: 15
product_id: product.product_product_1
valuation_adjustment_lines: []
-
I compute the landed cost using Compute button
-
!python {model: stock.landed.cost}: |
self.compute_landed_cost(cr, uid, [ref("stock_landed_cost_2")])
-
I check the valuation adjustment lines
-
!python {model: stock.landed.cost}: |
landed_cost = self.browse(cr, uid, ref("stock_landed_cost_2"))
for valuation in landed_cost.valuation_adjustment_lines:
assert valuation.additional_landed_cost == 15
-
I confirm the landed cost
-
!python {model: stock.landed.cost}: |
self.button_validate(cr, uid, [ref("stock_landed_cost_2")])
-
I check that the landed cost is now "Closed" and that it has an accounting entry
-
!assert {model: stock.landed.cost, id: stock_landed_cost_2}:
- state == 'done'
- account_move_id
-
I check the quants quantity and cost
-
!python {model: stock.landed.cost}: |
landed_cost = self.browse(cr, uid, ref("stock_landed_cost_2"))
for valuation in landed_cost.valuation_adjustment_lines:
quants = valuation.move_id.quant_ids
assert quants.mapped('qty') == [12.0, 1.0]
assert quants.mapped('cost') == [1.15, 1.2]
-
We perform all the tests for LC_pick_4
-
I receive picking LC_pick_4, and check how many quants are created
-
!python {model: stock.picking}: |
self.action_confirm(cr, uid, [ref("picking_landed_cost_4")], context=context)
self.action_assign(cr, uid, [ref("picking_landed_cost_4")], context=context)
self.action_done(cr, uid, [ref("picking_landed_cost_4")], context=context)
pick = self.browse(cr, uid, [ref("picking_landed_cost_4")], context=context)
quants = pick.move_lines.quant_ids
assert len(quants) == 2
assert quants.mapped('qty') == [11.0, 1.0]
assert [round(c, 2) for c in quants.mapped('cost')] == [1.42, 1.38]
-
I create a landed cost for picking 4
-
!record {model: stock.landed.cost, id: stock_landed_cost_3}:
picking_ids: [picking_landed_cost_4]
account_journal_id: account.expenses_journal
cost_lines:
- name: 'equal split'
split_method: 'equal'
price_unit: 11
product_id: product.product_product_1
valuation_adjustment_lines: []
-
I compute the landed cost using Compute button
-
!python {model: stock.landed.cost}: |
self.compute_landed_cost(cr, uid, [ref("stock_landed_cost_3")])
-
I check the valuation adjustment lines
-
!python {model: stock.landed.cost}: |
landed_cost = self.browse(cr, uid, ref("stock_landed_cost_3"))
for valuation in landed_cost.valuation_adjustment_lines:
assert valuation.additional_landed_cost == 11
-
I confirm the landed cost
-
!python {model: stock.landed.cost}: |
self.button_validate(cr, uid, [ref("stock_landed_cost_3")])
-
I check that the landed cost is now "Closed" and that it has an accounting entry
-
!assert {model: stock.landed.cost, id: stock_landed_cost_3}:
- state == 'done'
- account_move_id
-
I check the quants quantity and cost
-
!python {model: stock.landed.cost}: |
landed_cost = self.browse(cr, uid, ref("stock_landed_cost_3"))
for valuation in landed_cost.valuation_adjustment_lines:
quants = valuation.move_id.quant_ids
assert quants.mapped('qty') == [11.0, 1.0]
assert [round(c, 2) for c in quants.mapped('cost')] == [2.34, 2.26]