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]