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:
Marica
2022-06-21 20:13:23 +05:30
committed by GitHub
4 changed files with 121 additions and 11 deletions

View File

@@ -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

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

@@ -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)

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
@@ -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(
"""