fix: Respect system precision for user facing balance qty values (#30837)

* fix: Respect system precision for user facing balance qty values

- `get_precision` -> `set_precision`
- Use system wide currency precision for `stock_value`
- Round of qty defiiciency as per user defined precision (system flt precision), so that it is WYSIWYG for users

* fix: Consider system precision when validating future negative qty

* test: Immediate Negative Qty precision test

- Test for Immediate Negative Qty precision
- Stock Entry Negative Qty message: Format available qty in system precision
- Pass `stock_uom` as confugrable option in `make_item`

* test: Future Negative Qty validation with precision

* fix: Use `get_field_precision` for currency precision as it used to

- `get_field_precision` defaults to number format for precision (maintain old behaviour)
- Don't pass `currency` to `get_field_precision` as its not used anymore
This commit is contained in:
Marica 2022-06-17 15:13:13 +05:30 committed by GitHub
parent 74007c8e91
commit d6078aa911
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 120 additions and 10 deletions

View File

@ -800,6 +800,7 @@ def create_item(
item_code,
is_stock_item=1,
valuation_rate=0,
stock_uom="Nos",
warehouse="_Test Warehouse - _TC",
is_customer_provided_item=None,
customer=None,
@ -815,6 +816,7 @@ def create_item(
item.item_name = item_code
item.description = item_code
item.item_group = "All Item Groups"
item.stock_uom = stock_uom
item.is_stock_item = is_stock_item
item.is_fixed_asset = is_fixed_asset
item.asset_category = asset_category

View File

@ -590,7 +590,7 @@ class StockEntry(StockController):
)
+ "<br><br>"
+ _("Available quantity is {0}, you need {1}").format(
frappe.bold(d.actual_qty), frappe.bold(d.transfer_qty)
frappe.bold(flt(d.actual_qty, d.precision("actual_qty"))), frappe.bold(d.transfer_qty)
),
NegativeStockError,
title=_("Insufficient Stock"),

View File

@ -42,6 +42,9 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
"delete from `tabBin` where item_code in (%s)" % (", ".join(["%s"] * len(items))), items
)
def tearDown(self):
frappe.db.rollback()
def test_item_cost_reposting(self):
company = "_Test Company"
@ -1230,6 +1233,93 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
)
self.assertEqual(abs(sles[0].stock_value_difference), sles[1].stock_value_difference)
@change_settings("System Settings", {"float_precision": 4})
def test_negative_qty_with_precision(self):
"Test if system precision is respected while validating negative qty."
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.utils import get_stock_balance
item_code = "ItemPrecisionTest"
warehouse = "_Test Warehouse - _TC"
create_item(item_code, is_stock_item=1, stock_uom="Kg")
create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=559.8327, rate=100)
make_stock_entry(item_code=item_code, source=warehouse, qty=470.84, rate=100)
self.assertEqual(get_stock_balance(item_code, warehouse), 88.9927)
settings = frappe.get_doc("System Settings")
settings.float_precision = 3
settings.save()
# To deliver 100 qty we fall short of 11.0073 qty (11.007 with precision 3)
# Stock up with 11.007 (balance in db becomes 99.9997, on UI it will show as 100)
make_stock_entry(item_code=item_code, target=warehouse, qty=11.007, rate=100)
self.assertEqual(get_stock_balance(item_code, warehouse), 99.9997)
# See if delivery note goes through
# Negative qty error should not be raised as 99.9997 is 100 with precision 3 (system precision)
dn = create_delivery_note(
item_code=item_code,
qty=100,
rate=150,
warehouse=warehouse,
company="_Test Company",
expense_account="Cost of Goods Sold - _TC",
cost_center="Main - _TC",
do_not_submit=True,
)
dn.submit()
self.assertEqual(flt(get_stock_balance(item_code, warehouse), 3), 0.000)
@change_settings("System Settings", {"float_precision": 4})
def test_future_negative_qty_with_precision(self):
"""
Ledger:
| Voucher | Qty | Balance
-------------------
| Reco | 559.8327| 559.8327
| SE | -470.84 | [Backdated] (new bal: 88.9927)
| SE | 11.007 | 570.8397 (new bal: 99.9997)
| DN | -100 | 470.8397 (new bal: -0.0003)
Check if future negative qty is asserted as per precision 3.
-0.0003 should be considered as 0.000
"""
from erpnext.stock.doctype.item.test_item import create_item
item_code = "ItemPrecisionTest"
warehouse = "_Test Warehouse - _TC"
create_item(item_code, is_stock_item=1, stock_uom="Kg")
create_stock_reconciliation(
item_code=item_code,
warehouse=warehouse,
qty=559.8327,
rate=100,
posting_date=add_days(today(), -2),
)
make_stock_entry(item_code=item_code, target=warehouse, qty=11.007, rate=100)
create_delivery_note(
item_code=item_code,
qty=100,
rate=150,
warehouse=warehouse,
company="_Test Company",
expense_account="Cost of Goods Sold - _TC",
cost_center="Main - _TC",
)
settings = frappe.get_doc("System Settings")
settings.float_precision = 3
settings.save()
# Make backdated SE and make sure SE goes through as per precision (no negative qty error)
make_stock_entry(
item_code=item_code, source=warehouse, qty=470.84, rate=100, posting_date=add_days(today(), -1)
)
def create_repack_entry(**args):
args = frappe._dict(args)

View File

@ -1,4 +1,4 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import copy
@ -370,7 +370,7 @@ class update_entries_after(object):
self.args["name"] = self.args.sle_id
self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company")
self.get_precision()
self.set_precision()
self.valuation_method = get_valuation_method(self.item_code)
self.new_items_found = False
@ -381,10 +381,10 @@ class update_entries_after(object):
self.initialize_previous_data(self.args)
self.build()
def get_precision(self):
company_base_currency = frappe.get_cached_value("Company", self.company, "default_currency")
self.precision = get_field_precision(
frappe.get_meta("Stock Ledger Entry").get_field("stock_value"), currency=company_base_currency
def set_precision(self):
self.flt_precision = cint(frappe.db.get_default("float_precision")) or 2
self.currency_precision = get_field_precision(
frappe.get_meta("Stock Ledger Entry").get_field("stock_value")
)
def initialize_previous_data(self, args):
@ -581,7 +581,7 @@ class update_entries_after(object):
self.update_queue_values(sle)
# rounding as per precision
self.wh_data.stock_value = flt(self.wh_data.stock_value, self.precision)
self.wh_data.stock_value = flt(self.wh_data.stock_value, self.currency_precision)
if not self.wh_data.qty_after_transaction:
self.wh_data.stock_value = 0.0
stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
@ -605,6 +605,7 @@ class update_entries_after(object):
will not consider cancelled entries
"""
diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty)
diff = flt(diff, self.flt_precision) # respect system precision
if diff < 0 and abs(diff) > 0.0001:
# negative stock!
@ -1405,7 +1406,8 @@ def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
return
neg_sle = get_future_sle_with_negative_qty(args)
if neg_sle:
if is_negative_with_precision(neg_sle):
message = _(
"{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
).format(
@ -1423,7 +1425,7 @@ def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
return
neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
if neg_batch_sle:
if is_negative_with_precision(neg_batch_sle, is_batch=True):
message = _(
"{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
).format(
@ -1437,6 +1439,22 @@ def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
def is_negative_with_precision(neg_sle, is_batch=False):
"""
Returns whether system precision rounded qty is insufficient.
E.g: -0.0003 in precision 3 (0.000) is sufficient for the user.
"""
if not neg_sle:
return False
field = "cumulative_total" if is_batch else "qty_after_transaction"
precision = cint(frappe.db.get_default("float_precision")) or 2
qty_deficit = flt(neg_sle[0][field], precision)
return qty_deficit < 0 and abs(qty_deficit) > 0.0001
def get_future_sle_with_negative_qty(args):
return frappe.db.sql(
"""