From eb26694e0f30445419e58e88eabae3db1aa18865 Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli Date: Wed, 20 Apr 2016 10:54:18 +0200 Subject: [PATCH] [FIX] stock_landed_costs: account/valuation inconsistencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- addons/stock_landed_costs/__openerp__.py | 3 +- .../stock_landed_costs/stock_landed_costs.py | 38 +++- .../test/stock_landed_costs_rounding.yml | 175 ++++++++++++++++++ 3 files changed, 210 insertions(+), 6 deletions(-) create mode 100644 addons/stock_landed_costs/test/stock_landed_costs_rounding.yml diff --git a/addons/stock_landed_costs/__openerp__.py b/addons/stock_landed_costs/__openerp__.py index 8429d2ed097..9d98b3cf917 100644 --- a/addons/stock_landed_costs/__openerp__.py +++ b/addons/stock_landed_costs/__openerp__.py @@ -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, diff --git a/addons/stock_landed_costs/stock_landed_costs.py b/addons/stock_landed_costs/stock_landed_costs.py index 0401cbcb83f..f6f5a621218 100644 --- a/addons/stock_landed_costs/stock_landed_costs.py +++ b/addons/stock_landed_costs/stock_landed_costs.py @@ -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 diff --git a/addons/stock_landed_costs/test/stock_landed_costs_rounding.yml b/addons/stock_landed_costs/test/stock_landed_costs_rounding.yml new file mode 100644 index 00000000000..d0dd1ca9eb3 --- /dev/null +++ b/addons/stock_landed_costs/test/stock_landed_costs_rounding.yml @@ -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]