mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-11 05:31:48 +00:00
refactor: frappe.db.sql to frappe.qb for update_qty_in_future_sle (#56609)
This commit is contained in:
@@ -1290,6 +1290,107 @@ class TestStockLedgerEntry(ERPNextTestSuite, StockTestMixin):
|
||||
self.assertEqual(sle[0].qty_after_transaction, 105)
|
||||
self.assertEqual(sle[0].actual_qty, 100)
|
||||
|
||||
def test_update_qty_in_future_sle_shifts_same_timestamp_later_entry(self):
|
||||
# update_qty_in_future_sle treats "future" as strictly after the current entry in the
|
||||
# (posting_datetime, creation) order. An entry sharing the exact posting timestamp but created
|
||||
# later must still have its running balance shifted; comparing posting_datetime alone would skip
|
||||
# it. The current entry itself (same timestamp, same creation) must not be shifted.
|
||||
from erpnext.stock.stock_ledger import update_qty_in_future_sle
|
||||
|
||||
item = make_item().name
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
receipt1 = make_purchase_receipt(
|
||||
item_code=item,
|
||||
warehouse=warehouse,
|
||||
qty=10,
|
||||
rate=10,
|
||||
posting_date="2021-01-01",
|
||||
posting_time="02:00:00",
|
||||
)
|
||||
time.sleep(1)
|
||||
receipt2 = make_purchase_receipt(
|
||||
item_code=item,
|
||||
warehouse=warehouse,
|
||||
qty=20,
|
||||
rate=10,
|
||||
posting_date="2021-01-01",
|
||||
posting_time="02:00:00", # identical timestamp, later creation
|
||||
)
|
||||
|
||||
def sle(voucher):
|
||||
return frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_no": voucher.name, "is_cancelled": 0},
|
||||
["name", "posting_date", "posting_time", "creation", "qty_after_transaction"],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
sle1, sle2 = sle(receipt1), sle(receipt2)
|
||||
self.assertEqual(sle1.qty_after_transaction, 10)
|
||||
self.assertEqual(sle2.qty_after_transaction, 30)
|
||||
|
||||
# Simulate a +5 qty shift originating at receipt1's ledger position.
|
||||
args = frappe._dict(
|
||||
{
|
||||
"item_code": item,
|
||||
"warehouse": warehouse,
|
||||
"voucher_type": "Purchase Receipt",
|
||||
"voucher_no": receipt1.name,
|
||||
"posting_date": sle1.posting_date,
|
||||
"posting_time": sle1.posting_time,
|
||||
"creation": sle1.creation,
|
||||
"actual_qty": 5,
|
||||
}
|
||||
)
|
||||
update_qty_in_future_sle(args, allow_negative_stock=True)
|
||||
|
||||
# receipt2 (same timestamp, later creation) is shifted; receipt1 (the current entry) is not.
|
||||
self.assertEqual(frappe.db.get_value("Stock Ledger Entry", sle2.name, "qty_after_transaction"), 35)
|
||||
self.assertEqual(frappe.db.get_value("Stock Ledger Entry", sle1.name, "qty_after_transaction"), 10)
|
||||
|
||||
def test_get_next_stock_reco_respects_creation_order(self):
|
||||
# A stock reco sharing the exact posting timestamp of the current entry must only count as the
|
||||
# "next" reco when it was created after that entry. A reco created before it actually precedes
|
||||
# the entry and must not bound (truncate) the qty-shift range.
|
||||
from erpnext.stock.stock_ledger import get_next_stock_reco
|
||||
|
||||
item = make_item().name
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
reco = create_stock_reconciliation(
|
||||
item_code=item,
|
||||
warehouse=warehouse,
|
||||
qty=10,
|
||||
rate=100,
|
||||
posting_date="2021-01-01",
|
||||
posting_time="02:00:00",
|
||||
)
|
||||
reco_sle = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_no": reco.name, "is_cancelled": 0},
|
||||
["posting_date", "posting_time", "creation"],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
base_kwargs = {
|
||||
"item_code": item,
|
||||
"warehouse": warehouse,
|
||||
"voucher_no": "SOME-OTHER-VOUCHER",
|
||||
"posting_date": reco_sle.posting_date,
|
||||
"posting_time": reco_sle.posting_time,
|
||||
}
|
||||
|
||||
# Current entry created AFTER the reco at the same timestamp -> reco precedes it -> not returned.
|
||||
after = {**base_kwargs, "creation": add_to_date(reco_sle.creation, seconds=5)}
|
||||
self.assertFalse(get_next_stock_reco(after))
|
||||
|
||||
# Current entry created BEFORE the reco at the same timestamp -> reco follows it -> returned.
|
||||
before = {**base_kwargs, "creation": add_to_date(reco_sle.creation, seconds=-5)}
|
||||
result = get_next_stock_reco(before)
|
||||
self.assertTrue(result)
|
||||
self.assertEqual(result[0].voucher_no, reco.name)
|
||||
|
||||
@ERPNextTestSuite.change_settings("System Settings", {"float_precision": 3, "currency_precision": 2})
|
||||
def test_transfer_invariants(self):
|
||||
"""Extact stock value should be transferred."""
|
||||
|
||||
@@ -2126,20 +2126,36 @@ def get_valuation_rate(
|
||||
|
||||
def update_qty_in_future_sle(args, allow_negative_stock=False):
|
||||
"""Recalculate Qty after Transaction in future SLEs based on current SLE."""
|
||||
datetime_limit_condition = ""
|
||||
qty_shift = args.actual_qty
|
||||
|
||||
args["posting_datetime"] = get_combine_datetime(args["posting_date"], args["posting_time"])
|
||||
posting_datetime = get_combine_datetime(args["posting_date"], args["posting_time"])
|
||||
args["posting_datetime"] = posting_datetime
|
||||
|
||||
# find difference/shift in qty caused by stock reconciliation
|
||||
if args.voucher_type == "Stock Reconciliation":
|
||||
qty_shift = get_stock_reco_qty_shift(args)
|
||||
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
|
||||
future_condition = sle.posting_datetime > posting_datetime
|
||||
if args.get("creation") and not args.get("is_cancelled"):
|
||||
future_condition = future_condition | (
|
||||
(sle.posting_datetime == posting_datetime) & (sle.creation > args.get("creation"))
|
||||
)
|
||||
|
||||
query = frappe.qb.update(sle).where(
|
||||
(sle.item_code == args.get("item_code"))
|
||||
& (sle.warehouse == args.get("warehouse"))
|
||||
& (sle.is_cancelled == 0)
|
||||
& future_condition
|
||||
)
|
||||
|
||||
# find the next nearest stock reco so that we only recalculate SLEs till that point
|
||||
next_stock_reco_detail = get_next_stock_reco(args)
|
||||
if next_stock_reco_detail:
|
||||
detail = next_stock_reco_detail[0]
|
||||
datetime_limit_condition = get_datetime_limit_condition(detail)
|
||||
query = query.where(get_datetime_limit_condition(sle, next_stock_reco_detail[0]))
|
||||
|
||||
new_qty = sle.qty_after_transaction + qty_shift
|
||||
|
||||
if get_valuation_method(args.get("item_code"), args.get("company")) == "Standard Cost":
|
||||
# Standard Cost inventory is always carried at the standard rate, so a backdated entry only
|
||||
@@ -2153,38 +2169,13 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
|
||||
get_item_standard_rate(args.get("item_code"), args.get("company"), args.get("posting_date"))
|
||||
)
|
||||
|
||||
frappe.db.sql( # nosemgrep
|
||||
f"""
|
||||
update `tabStock Ledger Entry`
|
||||
set stock_value = (qty_after_transaction + {qty_shift}) * {standard_rate},
|
||||
qty_after_transaction = qty_after_transaction + {qty_shift}
|
||||
where
|
||||
item_code = %(item_code)s
|
||||
and warehouse = %(warehouse)s
|
||||
and is_cancelled = 0
|
||||
and (
|
||||
posting_datetime > %(posting_datetime)s
|
||||
)
|
||||
{datetime_limit_condition}
|
||||
""",
|
||||
args,
|
||||
)
|
||||
else:
|
||||
frappe.db.sql( # nosemgrep
|
||||
f"""
|
||||
update `tabStock Ledger Entry`
|
||||
set qty_after_transaction = qty_after_transaction + {qty_shift}
|
||||
where
|
||||
item_code = %(item_code)s
|
||||
and warehouse = %(warehouse)s
|
||||
and is_cancelled = 0
|
||||
and (
|
||||
posting_datetime > %(posting_datetime)s
|
||||
)
|
||||
{datetime_limit_condition}
|
||||
""",
|
||||
args,
|
||||
)
|
||||
# Set stock_value before qty_after_transaction: MariaDB evaluates SET left-to-right with the
|
||||
# already-updated values, so stock_value must be computed while qty still holds its pre-shift
|
||||
# value. (Postgres uses pre-update values throughout, so the result is the same either way.)
|
||||
query = query.set(sle.stock_value, new_qty * standard_rate)
|
||||
|
||||
query = query.set(sle.qty_after_transaction, new_qty)
|
||||
query.run()
|
||||
|
||||
validate_negative_qty_in_future_sle(args, allow_negative_stock)
|
||||
|
||||
@@ -2219,6 +2210,17 @@ def get_stock_reco_qty_shift(args):
|
||||
return stock_reco_qty_shift
|
||||
|
||||
|
||||
def get_next_reco_datetime_condition(sle, kwargs):
|
||||
current_datetime = get_combine_datetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
|
||||
|
||||
if kwargs.get("is_cancelled"):
|
||||
return sle.posting_datetime >= current_datetime
|
||||
|
||||
return (sle.posting_datetime > current_datetime) | (
|
||||
(sle.posting_datetime == current_datetime) & (sle.creation > kwargs.get("creation"))
|
||||
)
|
||||
|
||||
|
||||
def get_next_stock_reco(kwargs):
|
||||
"""Returns next nearest stock reconciliaton's details."""
|
||||
|
||||
@@ -2244,10 +2246,7 @@ def get_next_stock_reco(kwargs):
|
||||
& (sle.voucher_type == "Stock Reconciliation")
|
||||
& (sle.voucher_no != kwargs.get("voucher_no"))
|
||||
& (sle.is_cancelled == 0)
|
||||
& (
|
||||
sle.posting_datetime
|
||||
>= get_combine_datetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
|
||||
)
|
||||
& get_next_reco_datetime_condition(sle, kwargs)
|
||||
)
|
||||
.orderby(sle.posting_datetime)
|
||||
.orderby(sle.creation)
|
||||
@@ -2260,17 +2259,12 @@ def get_next_stock_reco(kwargs):
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
def get_datetime_limit_condition(detail):
|
||||
def get_datetime_limit_condition(sle, detail):
|
||||
posting_datetime = get_combine_datetime(detail.posting_date, detail.posting_time)
|
||||
|
||||
return f"""
|
||||
and
|
||||
(posting_datetime < '{posting_datetime}'
|
||||
or (
|
||||
posting_datetime = '{posting_datetime}'
|
||||
and creation < '{detail.creation}'
|
||||
)
|
||||
)"""
|
||||
return (sle.posting_datetime < posting_datetime) | (
|
||||
(sle.posting_datetime == posting_datetime) & (sle.creation < detail.creation)
|
||||
)
|
||||
|
||||
|
||||
def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
|
||||
|
||||
Reference in New Issue
Block a user