[FIX] purchase_stock: product price accuracy higher than currency accuracy
Steps to reproduce: . Change the decimal accuracy of the product price to 6 . Create a storable product and set the Vendor Tax . Create a purchase order with that item. . Set the unit price to have 6 decimal places. E.g: 9.406250 . Change the demand quantities to 10 . Confirm the purchase order. . Edit the purchase order and change the demand quantities to 7 (<10) . When you save, a new return order will be created Bug: Merging the two stock moves fail because the unit price is different and the unit price is different because in the case of taxes we recompute the unit price from untaxed amount which is rounded using the currency price_precision Fix: removed the unit_price from the keys used in matching which require an exact equality and added a comparaison on the lowest precision opw-3011342 closes odoo/odoo#110206 X-original-commit: f9ee1a22eb94e7ffcefd0023fc89150f8de73006 Signed-off-by: William Henrotin (whe) <whe@odoo.com> Signed-off-by: Walid Hanniche (waha) <waha@odoo.com>
This commit is contained in:
@@ -376,6 +376,54 @@ class TestPurchaseOrder(ValuationReconciliationTestCommon):
|
||||
picking.button_validate()
|
||||
self.assertEqual(po.order_line.mapped('qty_received'), [4.0], 'Purchase: no conversion error on receipt in different uom"')
|
||||
|
||||
def test_05_po_update_qty_stock_move_merge(self):
|
||||
""" This test ensures that changing product quantity when unit price has high decimal precision
|
||||
merged with the original instead of creating a new return
|
||||
"""
|
||||
|
||||
unit_price_precision = self.env['decimal.precision'].search([('name', '=', 'Product Price')])
|
||||
unit_price_precision.digits = 6
|
||||
|
||||
tax = self.env["account.tax"].create({
|
||||
"name": "Dummy Tax",
|
||||
"amount": "5.00",
|
||||
"type_tax_use": "purchase",
|
||||
})
|
||||
|
||||
super_product = self.env['product.product'].create({
|
||||
'name': 'Super Product',
|
||||
'type': 'product',
|
||||
'categ_id': self.stock_account_product_categ.id,
|
||||
'standard_price': 9.876543,
|
||||
})
|
||||
|
||||
purchase_order = self.env['purchase.order'].create({
|
||||
'partner_id': self.env.ref('base.res_partner_3').id,
|
||||
'order_line': [(0, 0, {
|
||||
'name': super_product.name,
|
||||
'product_id': super_product.id,
|
||||
'product_qty': 7,
|
||||
'product_uom': super_product.uom_id.id,
|
||||
'price_unit': super_product.standard_price,
|
||||
'taxes_id': [(4, tax.id)],
|
||||
})],
|
||||
})
|
||||
|
||||
purchase_order.button_confirm()
|
||||
self.assertEqual(purchase_order.state, 'purchase')
|
||||
self.assertEqual(len(purchase_order.picking_ids), 1)
|
||||
self.assertEqual(len(purchase_order.picking_ids.move_line_ids), 1)
|
||||
self.assertEqual(purchase_order.picking_ids.move_line_ids.reserved_qty, 7)
|
||||
|
||||
|
||||
purchase_order.order_line.product_qty = 4
|
||||
# updating quantity shouldn't create a seperate stock move
|
||||
# the new stock move (-3) should be merged with the previous
|
||||
purchase_order.button_confirm()
|
||||
self.assertEqual(len(purchase_order.picking_ids), 1)
|
||||
self.assertEqual(len(purchase_order.picking_ids.move_line_ids), 1)
|
||||
self.assertEqual(purchase_order.picking_ids.move_line_ids.reserved_qty, 4)
|
||||
|
||||
def test_message_qty_already_received(self):
|
||||
self.env.user.write({'company_id': self.company_data['company'].id})
|
||||
|
||||
|
||||
@@ -894,7 +894,7 @@ Please change the quantity done or the rounding precision of your unit of measur
|
||||
|
||||
@api.model
|
||||
def _prepare_merge_negative_moves_excluded_distinct_fields(self):
|
||||
return ['description_picking']
|
||||
return ['description_picking', 'price_unit']
|
||||
|
||||
def _clean_merged(self):
|
||||
"""Cleanup hook used when merging moves"""
|
||||
@@ -932,6 +932,7 @@ Please change the quantity done or the rounding precision of your unit of measur
|
||||
neg_qty_moves.picking_id = False
|
||||
excluded_fields = self._prepare_merge_negative_moves_excluded_distinct_fields()
|
||||
neg_key = itemgetter(*[field for field in distinct_fields if field not in excluded_fields])
|
||||
price_unit_prec = self.env['decimal.precision'].precision_get('Product Price')
|
||||
|
||||
for candidate_moves in candidate_moves_set:
|
||||
# First step find move to merge.
|
||||
@@ -953,21 +954,27 @@ Please change the quantity done or the rounding precision of your unit of measur
|
||||
for neg_move in neg_qty_moves:
|
||||
# Check all the candidates that matches the same limited key, and adjust their quantities to absorb negative moves
|
||||
for pos_move in moves_by_neg_key.get(neg_key(neg_move), []):
|
||||
# If quantity can be fully absorbed by a single move, update its quantity and remove the negative move
|
||||
if float_compare(pos_move.product_uom_qty, abs(neg_move.product_uom_qty), precision_rounding=pos_move.product_uom.rounding) >= 0:
|
||||
pos_move.product_uom_qty += neg_move.product_uom_qty
|
||||
pos_move.write({
|
||||
'move_dest_ids': [Command.link(m.id) for m in neg_move.mapped('move_dest_ids') if m.location_id == pos_move.location_dest_id],
|
||||
'move_orig_ids': [Command.link(m.id) for m in neg_move.mapped('move_orig_ids') if m.location_dest_id == pos_move.location_id],
|
||||
})
|
||||
merged_moves |= pos_move
|
||||
moves_to_unlink |= neg_move
|
||||
if float_is_zero(pos_move.product_uom_qty, precision_rounding=pos_move.product_uom.rounding):
|
||||
moves_to_cancel |= pos_move
|
||||
break
|
||||
neg_move.product_uom_qty += pos_move.product_uom_qty
|
||||
pos_move.product_uom_qty = 0
|
||||
moves_to_cancel |= pos_move
|
||||
currency_prec = pos_move.product_id.currency_id.decimal_places
|
||||
rounding = min(currency_prec, price_unit_prec)
|
||||
if float_compare(pos_move.price_unit, neg_move.price_unit, precision_rounding=rounding) == 0:
|
||||
new_total_value = pos_move.product_qty * pos_move.price_unit + neg_move.product_qty * neg_move.price_unit
|
||||
# If quantity can be fully absorbed by a single move, update its quantity and remove the negative move
|
||||
if float_compare(pos_move.product_uom_qty, abs(neg_move.product_uom_qty), precision_rounding=pos_move.product_uom.rounding) >= 0:
|
||||
pos_move.product_uom_qty += neg_move.product_uom_qty
|
||||
pos_move.write({
|
||||
'price_unit': float_round(new_total_value / pos_move.product_qty, precision_digits=price_unit_prec) if pos_move.product_qty else 0,
|
||||
'move_dest_ids': [Command.link(m.id) for m in neg_move.mapped('move_dest_ids') if m.location_id == pos_move.location_dest_id],
|
||||
'move_orig_ids': [Command.link(m.id) for m in neg_move.mapped('move_orig_ids') if m.location_dest_id == pos_move.location_id],
|
||||
})
|
||||
merged_moves |= pos_move
|
||||
moves_to_unlink |= neg_move
|
||||
if float_is_zero(pos_move.product_uom_qty, precision_rounding=pos_move.product_uom.rounding):
|
||||
moves_to_cancel |= pos_move
|
||||
break
|
||||
neg_move.product_uom_qty += pos_move.product_uom_qty
|
||||
neg_move.price_unit = float_round(new_total_value / neg_move.product_qty, precision_digits=price_unit_prec)
|
||||
pos_move.product_uom_qty = 0
|
||||
moves_to_cancel |= pos_move
|
||||
|
||||
if moves_to_unlink:
|
||||
# We are using propagate to False in order to not cancel destination moves merged in moves[0]
|
||||
|
||||
Reference in New Issue
Block a user