[FIX] product, decimal_precision, stock, mrp: Prevent quant reservation issue
One can set a Unit of Measure precision rounding more precise than the overall Decimal Accuracy. This can trigger the error 'It is not possible to unreserve more products', as there will be inconsistencies on reserved quantities due to decimal roundings. We add 2 warnings on Units of Measure and Decimal Accuracy. Therefore, users are warned when their UOM/decimal changes can cause inconsistencies in quants reservation. Also, we want to prevent the issue at the root by ensuring that both the move and the quant have the same reserved quantity. The contrary can happen e.g. when a product is defined in Liters (rounding .001) but used in an operation in ml (rounding .01, so e.g. 187.5ml). The conversions between the 2 UOM can cause the differences of reservations. In _prepare_move_line_vals we make sure uom_quantity_back_to_product_uom is not more precise than the overall Decimal Accuracy. That way, if the reserved quantity changes between UOM conversions, the move line created is in the quant/product UOM, and we do not have move line's reserved quantity > quantity in stock. We do the same for _update_reserved_quantity (stock.move), when updating a move line. In _update_move_lines, because of UOM conversions, new_quantity_done can differ from ml.product_uom_qty, which triggers the creation of an extra move line, leading to reserved quantity > quantity in stock. To prevent that, we compare the reserved quantities in the product UOM. opw 2206902 (Example 1) opw 2171541 opw 2221227 opw 2233937 opw 2198775 and many more closes odoo/odoo#62279 X-original-commit: 478a42beb5876171afeab7df27fa69548edbf849 Signed-off-by: Arnold Moyaux <amoyaux@users.noreply.github.com>
This commit is contained in:
@@ -1403,3 +1403,107 @@ class TestMrpOrder(TestMrpCommon):
|
||||
mo_copy = mo_form.save()
|
||||
mo_copy.button_mark_done()
|
||||
self.assertEqual(mo_copy.state, 'done')
|
||||
|
||||
def test_product_produce_different_uom(self):
|
||||
""" Check that for products tracked by lots,
|
||||
with component product UOM different from UOM used in the BOM,
|
||||
we do not create a new move line due to extra reserved quantity
|
||||
caused by decimal rounding conversions.
|
||||
"""
|
||||
|
||||
# the overall decimal accuracy is set to 3 digits
|
||||
precision = self.env.ref('product.decimal_product_uom')
|
||||
precision.digits = 3
|
||||
|
||||
# define L and ml, L has rounding .001 but ml has rounding .01
|
||||
# when producing e.g. 187.5ml, it will be rounded to .188L
|
||||
categ_test = self.env['uom.category'].create({'name': 'Volume Test'})
|
||||
|
||||
uom_L = self.env['uom.uom'].create({
|
||||
'name': 'Test Liters',
|
||||
'category_id': categ_test.id,
|
||||
'uom_type': 'reference',
|
||||
'rounding': 0.001
|
||||
})
|
||||
|
||||
uom_ml = self.env['uom.uom'].create({
|
||||
'name': 'Test ml',
|
||||
'category_id': categ_test.id,
|
||||
'uom_type': 'smaller',
|
||||
'rounding': 0.01,
|
||||
'factor_inv': 0.001,
|
||||
})
|
||||
|
||||
# create a product component and the final product using the component
|
||||
product_comp = self.env['product.product'].create({
|
||||
'name': 'Product Component',
|
||||
'type': 'product',
|
||||
'tracking': 'lot',
|
||||
'categ_id': self.env.ref('product.product_category_all').id,
|
||||
'uom_id': uom_L.id,
|
||||
'uom_po_id': uom_L.id,
|
||||
})
|
||||
|
||||
product_final = self.env['product.product'].create({
|
||||
'name': 'Product Final',
|
||||
'type': 'product',
|
||||
'tracking': 'lot',
|
||||
'categ_id': self.env.ref('product.product_category_all').id,
|
||||
'uom_id': uom_L.id,
|
||||
'uom_po_id': uom_L.id,
|
||||
})
|
||||
|
||||
# the products are tracked by lot, so we go through _generate_consumed_move_line
|
||||
lot_final = self.env['stock.production.lot'].create({
|
||||
'name': 'Lot Final',
|
||||
'product_id': product_final.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
|
||||
lot_comp = self.env['stock.production.lot'].create({
|
||||
'name': 'Lot Component',
|
||||
'product_id': product_comp.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
|
||||
# update the quantity on hand for Component, in a lot
|
||||
self.stock_location = self.env.ref('stock.stock_location_stock')
|
||||
self.env['stock.quant']._update_available_quantity(product_comp, self.stock_location, 1, lot_id=lot_comp)
|
||||
|
||||
# create a BOM for Final, using Component
|
||||
test_bom = self.env['mrp.bom'].create({
|
||||
'product_id': product_final.id,
|
||||
'product_tmpl_id': product_final.product_tmpl_id.id,
|
||||
'product_uom_id': uom_L.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [(0, 0, {
|
||||
'product_id': product_comp.id,
|
||||
'product_qty': 375.00,
|
||||
'product_uom_id': uom_ml.id
|
||||
})],
|
||||
})
|
||||
|
||||
# create a MO for this BOM
|
||||
mo_product_final_form = Form(self.env['mrp.production'])
|
||||
mo_product_final_form.product_id = product_final
|
||||
mo_product_final_form.product_uom_id = uom_L
|
||||
mo_product_final_form.bom_id = test_bom
|
||||
mo_product_final_form.product_qty = 0.5
|
||||
mo_product_final_form = mo_product_final_form.save()
|
||||
|
||||
mo_product_final_form.action_confirm()
|
||||
mo_product_final_form.action_assign()
|
||||
self.assertEqual(mo_product_final_form.reservation_state, 'assigned')
|
||||
|
||||
# produce
|
||||
res_dict = mo_product_final_form.button_mark_done()
|
||||
self.assertEqual(res_dict.get('res_model'), 'mrp.immediate.production')
|
||||
wizard = Form(self.env[res_dict['res_model']].with_context(res_dict['context'])).save()
|
||||
wizard.process()
|
||||
|
||||
# check that in _generate_consumed_move_line,
|
||||
# we do not create an extra move line because
|
||||
# of a conversion 187.5ml = 0.188L
|
||||
# thus creating an extra line with 'product_uom_qty': 0.5
|
||||
self.assertEqual(len(mo_product_final_form.move_raw_ids.move_line_ids), 1, 'One move line should exist for the MO.')
|
||||
|
||||
@@ -2340,6 +2340,15 @@ msgid ""
|
||||
"current partner"
|
||||
msgstr ""
|
||||
|
||||
#. module: product
|
||||
#: code:addons/product/models/uom_uom.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"This rounding precision is higher than the Decimal Accuracy (%s digits).\n"
|
||||
"This may cause inconsistencies in computations.\n"
|
||||
"Please set a precision between %s and 1."
|
||||
msgstr ""
|
||||
|
||||
#. module: product
|
||||
#: model:ir.model.fields,help:product.field_product_supplierinfo__product_code
|
||||
msgid ""
|
||||
@@ -2661,6 +2670,13 @@ msgstr ""
|
||||
msgid "Volume unit of measure label"
|
||||
msgstr ""
|
||||
|
||||
#. module: product
|
||||
#: code:addons/product/models/decimal_precision.py:0
|
||||
#: code:addons/product/models/uom_uom.py:0
|
||||
#, python-format
|
||||
msgid "Warning!"
|
||||
msgstr ""
|
||||
|
||||
#. module: product
|
||||
#: model_terms:ir.ui.view,arch_db:product.product_template_search_view
|
||||
msgid "Warnings"
|
||||
@@ -2694,6 +2710,15 @@ msgstr ""
|
||||
msgid "White"
|
||||
msgstr ""
|
||||
|
||||
#. module: product
|
||||
#: code:addons/product/models/decimal_precision.py:34
|
||||
#, python-format
|
||||
msgid "You are setting a Decimal Accuracy less precise than the UOMs:\n"
|
||||
"%s\n"
|
||||
"This may cause inconsistencies in computations.\n"
|
||||
"Please increase the rounding of those units of measure, or the digits of this Decimal Accuracy."
|
||||
msgstr ""
|
||||
|
||||
#. module: product
|
||||
#: model_terms:ir.actions.act_window,help:product.product_pricelist_action2
|
||||
msgid ""
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
from . import res_config_settings
|
||||
from . import decimal_precision
|
||||
from . import uom_uom
|
||||
|
||||
# don't try to be a good boy and sort imports alphabetically.
|
||||
# `product.template` should be initialised before `product.product`
|
||||
|
||||
@@ -15,3 +15,26 @@ class DecimalPrecision(models.Model):
|
||||
for precision in self):
|
||||
raise ValidationError(_("You cannot define the decimal precision of 'Account' as greater than the rounding factor of the company's main currency"))
|
||||
return True
|
||||
|
||||
@api.onchange('digits')
|
||||
def _onchange_digits(self):
|
||||
if self.name != "Product Unit of Measure": # precision_get() relies on this name
|
||||
return
|
||||
# We are changing the precision of UOM fields; check whether the
|
||||
# precision is equal or higher than existing units of measure.
|
||||
rounding = 1.0 / 10.0**self.digits
|
||||
dangerous_uom = self.env['uom.uom'].search([('rounding', '<', rounding)])
|
||||
if dangerous_uom:
|
||||
uom_descriptions = [
|
||||
" - %s (id=%s, precision=%s)" % (uom.name, uom.id, uom.rounding)
|
||||
for uom in dangerous_uom
|
||||
]
|
||||
return {'warning': {
|
||||
'title': _('Warning!'),
|
||||
'message': _(
|
||||
"You are setting a Decimal Accuracy less precise than the UOMs:\n"
|
||||
"%s\n"
|
||||
"This may cause inconsistencies in computations.\n"
|
||||
"Please increase the rounding of those units of measure, or the digits of this Decimal Accuracy."
|
||||
) % ('\n'.join(uom_descriptions)),
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, models, _
|
||||
|
||||
|
||||
class UoM(models.Model):
|
||||
_inherit = 'uom.uom'
|
||||
|
||||
@api.onchange('rounding')
|
||||
def _onchange_rounding(self):
|
||||
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
if self.rounding < 1.0 / 10.0**precision:
|
||||
return {'warning': {
|
||||
'title': _('Warning!'),
|
||||
'message': _(
|
||||
"This rounding precision is higher than the Decimal Accuracy"
|
||||
" (%s digits).\nThis may cause inconsistencies in computations.\n"
|
||||
"Please set a precision between %s and 1."
|
||||
) % (precision, 1.0 / 10.0**precision),
|
||||
}}
|
||||
@@ -1167,9 +1167,10 @@ class StockMove(models.Model):
|
||||
'company_id': self.company_id.id,
|
||||
}
|
||||
if quantity:
|
||||
uom_quantity = self.product_id.uom_id._compute_quantity(quantity, self.product_uom, rounding_method='HALF-UP')
|
||||
uom_quantity_back_to_product_uom = self.product_uom._compute_quantity(uom_quantity, self.product_id.uom_id, rounding_method='HALF-UP')
|
||||
rounding = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
uom_quantity = self.product_id.uom_id._compute_quantity(quantity, self.product_uom, rounding_method='HALF-UP')
|
||||
uom_quantity = float_round(uom_quantity, precision_digits=rounding)
|
||||
uom_quantity_back_to_product_uom = self.product_uom._compute_quantity(uom_quantity, self.product_id.uom_id, rounding_method='HALF-UP')
|
||||
if float_compare(quantity, uom_quantity_back_to_product_uom, precision_digits=rounding) == 0:
|
||||
vals = dict(vals, product_uom_qty=uom_quantity)
|
||||
else:
|
||||
@@ -1211,9 +1212,9 @@ class StockMove(models.Model):
|
||||
taken_quantity = self.product_uom._compute_quantity(taken_quantity_move_uom, self.product_id.uom_id, rounding_method='HALF-UP')
|
||||
|
||||
quants = []
|
||||
rounding = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
|
||||
if self.product_id.tracking == 'serial':
|
||||
rounding = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
if float_compare(taken_quantity, int(taken_quantity), precision_digits=rounding) != 0:
|
||||
taken_quantity = 0
|
||||
|
||||
@@ -1231,7 +1232,11 @@ class StockMove(models.Model):
|
||||
for reserved_quant, quantity in quants:
|
||||
to_update = self.move_line_ids.filtered(lambda ml: ml._reservation_is_updatable(quantity, reserved_quant))
|
||||
if to_update:
|
||||
to_update[0].with_context(bypass_reservation_update=True).product_uom_qty += self.product_id.uom_id._compute_quantity(quantity, to_update[0].product_uom_id, rounding_method='HALF-UP')
|
||||
uom_quantity = self.product_id.uom_id._compute_quantity(quantity, to_update[0].product_uom_id, rounding_method='HALF-UP')
|
||||
uom_quantity = float_round(uom_quantity, precision_digits=rounding)
|
||||
uom_quantity_back_to_product_uom = to_update[0].product_uom_id._compute_quantity(uom_quantity, self.product_id.uom_id, rounding_method='HALF-UP')
|
||||
if to_update and float_compare(quantity, uom_quantity_back_to_product_uom, precision_digits=rounding) == 0:
|
||||
to_update[0].with_context(bypass_reservation_update=True).product_uom_qty += uom_quantity
|
||||
else:
|
||||
if self.product_id.tracking == 'serial':
|
||||
for i in range(0, int(quantity)):
|
||||
|
||||
@@ -6,6 +6,9 @@ from datetime import timedelta
|
||||
from odoo.addons.stock.tests.common import TestStockCommon
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
from odoo.tests import Form
|
||||
from odoo.tools import float_is_zero, float_compare
|
||||
|
||||
from odoo.tests.common import Form
|
||||
|
||||
class TestPickShip(TestStockCommon):
|
||||
@@ -2159,6 +2162,189 @@ class TestStockUOM(TestStockCommon):
|
||||
self.assertEqual(len(back_order_in), 1.00, 'There should be one back order created')
|
||||
self.assertEqual(back_order_in.move_lines.product_qty, 91640.00, 'There should be one back order created')
|
||||
|
||||
def test_move_product_with_different_uom(self):
|
||||
""" Product defined in g with 0.01 rounding
|
||||
Decimal Accuracy (DA) 3 digits.
|
||||
Quantity on hand: 149.88g
|
||||
Picking of 1kg
|
||||
kg has 0.0001 rounding
|
||||
Due to conversions, we may end up reserving 150g
|
||||
(more than the quantity in stock), we check that
|
||||
we reserve less than the quantity in stock
|
||||
"""
|
||||
precision = self.env.ref('product.decimal_product_uom')
|
||||
precision.digits = 3
|
||||
precision_digits = precision.digits
|
||||
|
||||
self.uom_kg.rounding = 0.0001
|
||||
self.uom_gm.rounding = 0.01
|
||||
|
||||
product_G = self.env['product.product'].create({
|
||||
'name': 'Product G',
|
||||
'type': 'product',
|
||||
'categ_id': self.env.ref('product.product_category_all').id,
|
||||
'uom_id': self.uom_gm.id,
|
||||
'uom_po_id': self.uom_gm.id,
|
||||
})
|
||||
|
||||
stock_location = self.env['stock.location'].browse(self.stock_location)
|
||||
self.env['stock.quant']._update_available_quantity(product_G, stock_location, 149.88)
|
||||
self.assertEqual(len(product_G.stock_quant_ids), 1, 'One quant should exist for the product.')
|
||||
quant = product_G.stock_quant_ids
|
||||
|
||||
# transfer 1kg of product_G
|
||||
picking = self.env['stock.picking'].create({
|
||||
'location_id': self.stock_location,
|
||||
'location_dest_id': self.customer_location,
|
||||
'picking_type_id': self.env.ref('stock.picking_type_out').id,
|
||||
})
|
||||
|
||||
move = self.env['stock.move'].create({
|
||||
'name': 'test_reserve_product_G',
|
||||
'location_id': self.stock_location,
|
||||
'location_dest_id': self.customer_location,
|
||||
'picking_id': picking.id,
|
||||
'product_id': product_G.id,
|
||||
'product_uom': self.uom_kg.id,
|
||||
'product_uom_qty': 1,
|
||||
})
|
||||
|
||||
self.assertEqual(move.product_uom.id, self.uom_kg.id)
|
||||
self.assertEqual(move.product_uom_qty, 1.0)
|
||||
|
||||
picking.action_confirm()
|
||||
picking.action_assign()
|
||||
|
||||
self.assertEqual(product_G.uom_id.rounding, 0.01)
|
||||
self.assertEqual(move.product_uom.rounding, 0.0001)
|
||||
|
||||
self.assertEqual(len(picking.move_line_ids), 1, 'One move line should exist for the picking.')
|
||||
move_line = picking.move_line_ids
|
||||
# check that we do not reserve more (in the same UOM) than the quantity in stock
|
||||
self.assertEqual(float_compare(move_line.product_qty, quant.quantity, precision_digits=precision_digits), -1, "We do not reserve more (in the same UOM) than the quantity in stock")
|
||||
# check that we reserve the same quantity in the ml and the quant
|
||||
self.assertTrue(float_is_zero(move_line.product_qty - quant.reserved_quantity, precision_digits=precision_digits))
|
||||
|
||||
def test_update_product_move_line_with_different_uom(self):
|
||||
""" Check that when the move line and corresponding
|
||||
product have different UOM with possibly conflicting
|
||||
precisions, we do not reserve more than the quantity
|
||||
in stock. Similar initial configuration as
|
||||
test_move_product_with_different_uom.
|
||||
"""
|
||||
precision = self.env.ref('product.decimal_product_uom')
|
||||
precision.digits = 3
|
||||
precision_digits = precision.digits
|
||||
|
||||
self.uom_kg.rounding = 0.0001
|
||||
self.uom_gm.rounding = 0.01
|
||||
|
||||
product_LtDA = self.env['product.product'].create({
|
||||
'name': 'Product Less than DA',
|
||||
'type': 'product',
|
||||
'categ_id': self.env.ref('product.product_category_all').id,
|
||||
'uom_id': self.uom_gm.id,
|
||||
'uom_po_id': self.uom_gm.id,
|
||||
})
|
||||
|
||||
product_GtDA = self.env['product.product'].create({
|
||||
'name': 'Product Greater than DA',
|
||||
'type': 'product',
|
||||
'categ_id': self.env.ref('product.product_category_all').id,
|
||||
'uom_id': self.uom_gm.id,
|
||||
'uom_po_id': self.uom_gm.id,
|
||||
})
|
||||
|
||||
stock_location = self.env['stock.location'].browse(self.stock_location)
|
||||
|
||||
# quantity in hand converted to kg is not more precise than the DA
|
||||
self.env['stock.quant']._update_available_quantity(product_LtDA, stock_location, 149)
|
||||
# quantity in hand converted to kg is more precise than the DA
|
||||
self.env['stock.quant']._update_available_quantity(product_GtDA, stock_location, 149.88)
|
||||
|
||||
self.assertEqual(len(product_LtDA.stock_quant_ids), 1, 'One quant should exist for the product.')
|
||||
self.assertEqual(len(product_GtDA.stock_quant_ids), 1, 'One quant should exist for the product.')
|
||||
quant_LtDA = product_LtDA.stock_quant_ids
|
||||
quant_GtDA = product_GtDA.stock_quant_ids
|
||||
|
||||
# create 2 moves of 1kg
|
||||
move_LtDA = self.env['stock.move'].create({
|
||||
'name': 'test_reserve_product_LtDA',
|
||||
'location_id': self.stock_location,
|
||||
'location_dest_id': self.customer_location,
|
||||
'product_id': product_LtDA.id,
|
||||
'product_uom': self.uom_kg.id,
|
||||
'product_uom_qty': 1,
|
||||
})
|
||||
|
||||
move_GtDA = self.env['stock.move'].create({
|
||||
'name': 'test_reserve_product_GtDA',
|
||||
'location_id': self.stock_location,
|
||||
'location_dest_id': self.customer_location,
|
||||
'product_id': product_GtDA.id,
|
||||
'product_uom': self.uom_kg.id,
|
||||
'product_uom_qty': 1,
|
||||
})
|
||||
|
||||
self.assertEqual(move_LtDA.state, 'draft')
|
||||
self.assertEqual(move_GtDA.state, 'draft')
|
||||
move_LtDA._action_confirm()
|
||||
move_GtDA._action_confirm()
|
||||
self.assertEqual(move_LtDA.state, 'confirmed')
|
||||
self.assertEqual(move_GtDA.state, 'confirmed')
|
||||
# check availability, less than initial demand
|
||||
move_LtDA._action_assign()
|
||||
move_GtDA._action_assign()
|
||||
self.assertEqual(move_LtDA.state, 'partially_available')
|
||||
self.assertEqual(move_GtDA.state, 'partially_available')
|
||||
# the initial demand is 1kg
|
||||
self.assertEqual(move_LtDA.product_uom.id, self.uom_kg.id)
|
||||
self.assertEqual(move_GtDA.product_uom.id, self.uom_kg.id)
|
||||
self.assertEqual(move_LtDA.product_uom_qty, 1.0)
|
||||
self.assertEqual(move_GtDA.product_uom_qty, 1.0)
|
||||
# one move line is created
|
||||
self.assertEqual(len(move_LtDA.move_line_ids), 1)
|
||||
self.assertEqual(len(move_GtDA.move_line_ids), 1)
|
||||
|
||||
# increase quantity by 0.14988 kg (more precise than DA)
|
||||
self.env['stock.quant']._update_available_quantity(product_LtDA, stock_location, 149.88)
|
||||
self.env['stock.quant']._update_available_quantity(product_GtDA, stock_location, 149.88)
|
||||
|
||||
# _update_reserved_quantity is called on a move only in _action_assign
|
||||
move_LtDA._action_assign()
|
||||
move_GtDA._action_assign()
|
||||
|
||||
# as the move line for LtDA and its corresponding quant can be
|
||||
# in different UOMs, a new move line can be created
|
||||
# from _update_reserved_quantity
|
||||
move_lines_LtDA = self.env["stock.move.line"].search([
|
||||
('product_id', '=', quant_LtDA.product_id.id),
|
||||
('location_id', '=', quant_LtDA.location_id.id),
|
||||
('lot_id', '=', quant_LtDA.lot_id.id),
|
||||
('package_id', '=', quant_LtDA.package_id.id),
|
||||
('owner_id', '=', quant_LtDA.owner_id.id),
|
||||
('product_qty', '!=', 0)
|
||||
])
|
||||
reserved_on_move_lines_LtDA = sum(move_lines_LtDA.mapped('product_qty'))
|
||||
|
||||
move_lines_GtDA = self.env["stock.move.line"].search([
|
||||
('product_id', '=', quant_GtDA.product_id.id),
|
||||
('location_id', '=', quant_GtDA.location_id.id),
|
||||
('lot_id', '=', quant_GtDA.lot_id.id),
|
||||
('package_id', '=', quant_GtDA.package_id.id),
|
||||
('owner_id', '=', quant_GtDA.owner_id.id),
|
||||
('product_qty', '!=', 0)
|
||||
])
|
||||
reserved_on_move_lines_GtDA = sum(move_lines_GtDA.mapped('product_qty'))
|
||||
|
||||
# check that we do not reserve more (in the same UOM) than the quantity in stock
|
||||
self.assertEqual(float_compare(reserved_on_move_lines_LtDA, quant_LtDA.quantity, precision_digits=precision_digits), -1, "We do not reserve more (in the same UOM) than the quantity in stock")
|
||||
self.assertEqual(float_compare(reserved_on_move_lines_GtDA, quant_GtDA.quantity, precision_digits=precision_digits), -1, "We do not reserve more (in the same UOM) than the quantity in stock")
|
||||
|
||||
# check that we reserve the same quantity in the ml and the quant
|
||||
self.assertTrue(float_is_zero(reserved_on_move_lines_LtDA - quant_LtDA.reserved_quantity, precision_digits=precision_digits))
|
||||
self.assertTrue(float_is_zero(reserved_on_move_lines_GtDA - quant_GtDA.reserved_quantity, precision_digits=precision_digits))
|
||||
|
||||
|
||||
class TestRoutes(TestStockCommon):
|
||||
def setUp(self):
|
||||
|
||||
Reference in New Issue
Block a user