fix: check future negative stock for batches

batch's ledger is only maintained in form of `actual_qty` on batch's
SLEs. To validate if batch has any negative qty in future, cumulative
total of `actual_qty` is required to ensure it never goes negative.
This commit is contained in:
Ankush Menat
2021-12-07 23:03:52 +05:30
committed by Ankush Menat
parent 1cbeba5f1d
commit 5eba57528c

View File

@@ -1089,17 +1089,36 @@ def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
allow_negative_stock = cint(allow_negative_stock) \ allow_negative_stock = cint(allow_negative_stock) \
or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
if (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation") and not allow_negative_stock: if allow_negative_stock:
sle = get_future_sle_with_negative_qty(args) return
if sle: if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"):
message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format( return
abs(sle[0]["qty_after_transaction"]),
frappe.get_desk_link('Item', args.item_code), neg_sle = get_future_sle_with_negative_qty(args)
frappe.get_desk_link('Warehouse', args.warehouse), if neg_sle:
sle[0]["posting_date"], sle[0]["posting_time"], message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
frappe.get_desk_link(sle[0]["voucher_type"], sle[0]["voucher_no"])) abs(neg_sle[0]["qty_after_transaction"]),
frappe.get_desk_link('Item', args.item_code),
frappe.get_desk_link('Warehouse', args.warehouse),
neg_sle[0]["posting_date"], neg_sle[0]["posting_time"],
frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]))
frappe.throw(message, NegativeStockError, title='Insufficient Stock')
if not args.batch_no:
return
neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
if neg_batch_sle:
message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
abs(neg_batch_sle[0]["cumulative_total"]),
frappe.get_desk_link('Batch', args.batch_no),
frappe.get_desk_link('Warehouse', args.warehouse),
neg_batch_sle[0]["posting_date"], neg_batch_sle[0]["posting_time"],
frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]))
frappe.throw(message, NegativeStockError, title="Insufficient Stock for Batch")
frappe.throw(message, NegativeStockError, title='Insufficient Stock')
def get_future_sle_with_negative_qty(args): def get_future_sle_with_negative_qty(args):
return frappe.db.sql(""" return frappe.db.sql("""
@@ -1118,6 +1137,29 @@ def get_future_sle_with_negative_qty(args):
limit 1 limit 1
""", args, as_dict=1) """, args, as_dict=1)
def get_future_sle_with_negative_batch_qty(args):
return frappe.db.sql("""
with batch_ledger as (
select
posting_date, posting_time, voucher_type, voucher_no,
sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total
from `tabStock Ledger Entry`
where
item_code = %(item_code)s
and warehouse = %(warehouse)s
and batch_no=%(batch_no)s
and is_cancelled = 0
order by posting_date, posting_time, creation
)
select * from batch_ledger
where
cumulative_total < 0.0
and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
limit 1
""", args, as_dict=1)
def _round_off_if_near_zero(number: float, precision: int = 6) -> float: def _round_off_if_near_zero(number: float, precision: int = 6) -> float:
""" Rounds off the number to zero only if number is close to zero for decimal """ Rounds off the number to zero only if number is close to zero for decimal
specified in precision. Precision defaults to 6. specified in precision. Precision defaults to 6.