Merge pull request #31393 from frappe/mergify/bp/version-13-hotfix/pr-30837
fix: Respect system precision for user facing balance qty values (backport #30837)
This commit is contained in:
@@ -712,6 +712,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,
|
||||
@@ -727,6 +728,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
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -7,7 +7,7 @@ import frappe
|
||||
from frappe.core.page.permission_manager.permission_manager import reset
|
||||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, add_to_date, today
|
||||
from frappe.utils import add_days, add_to_date, flt, today
|
||||
|
||||
from erpnext.accounts.doctype.gl_entry.gl_entry import rename_gle_sle_docs
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
@@ -40,6 +40,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 assertSLEs(self, doc, expected_sles, sle_filters=None):
|
||||
"""Compare sorted SLEs, useful for vouchers that create multiple SLEs for same line"""
|
||||
|
||||
@@ -754,6 +757,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)
|
||||
|
||||
@@ -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
|
||||
@@ -360,7 +360,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
|
||||
@@ -371,10 +371,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):
|
||||
@@ -571,7 +571,7 @@ class update_entries_after(object):
|
||||
)
|
||||
|
||||
# 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
|
||||
@@ -595,6 +595,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!
|
||||
@@ -1347,7 +1348,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(
|
||||
@@ -1365,7 +1367,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(
|
||||
@@ -1379,6 +1381,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(
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user