[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:
Walid HANNICHE (waha)
2023-01-18 00:02:00 +01:00
parent 45dd909fae
commit a7f08f0b4c
2 changed files with 71 additions and 16 deletions
@@ -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})
+23 -16
View File
@@ -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]