Compare commits

...

3 Commits

Author SHA1 Message Date
mergify[bot]
f1fc9e3261 fix: added doctype filter validation for sales person wise transaction summary report (backport #55812) (#55817)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
fix: added doctype filter validation for sales person wise transaction summary report (#55812)
2026-06-11 07:08:59 +00:00
Nabin Hait
2905669af4 Merge pull request #55794 from frappe/mergify/bp/version-15-hotfix/pr-55789
fix: prefetch batchwise valuations before streaming SLEs in stock ageing (backport #55789)
2026-06-10 12:14:37 +05:30
Mihir Kandoi
c6176500d2 fix: prefetch batchwise valuations before streaming SLEs in stock ageing
Stock Ageing iterates stock ledger entries through an unbuffered
(streaming) cursor. _get_batchwise_valuation() lazily queried
Batch.use_batchwise_valuation from inside that loop whenever a row
carried the legacy batch_no field, and the nested query invalidated
the active streaming result set — crashing the report (or silently
dropping the remaining rows, depending on the driver version).

Resolve the valuation flags in a single query before entering the
unbuffered cursor block; the lazy lookup now only serves callers that
pass stock ledger entries in directly, where no streaming is active.

Fixes https://github.com/frappe/erpnext/issues/55786

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 060a5c4eeb)
2026-06-10 06:07:48 +00:00
3 changed files with 115 additions and 1 deletions

View File

@@ -13,6 +13,8 @@ def execute(filters=None):
if not filters:
filters = {}
validate_filters(filters)
columns = get_columns(filters)
entries = get_entries(filters)
item_details = get_item_details()
@@ -49,10 +51,17 @@ def execute(filters=None):
return columns, data
def get_columns(filters):
def validate_filters(filters):
ALLOWED_DOCTYPES = ["Sales Order", "Sales Invoice", "Delivery Note"]
if not filters.get("doc_type"):
msgprint(_("Please select the document type first"), raise_exception=1)
if filters.get("doc_type") not in ALLOWED_DOCTYPES:
frappe.throw(_("{0}, {1} or {2} are the only allowed options.").format(*ALLOWED_DOCTYPES))
def get_columns(filters):
columns = [
{
"label": _(filters["doc_type"]),

View File

@@ -306,6 +306,11 @@ class FIFOSlots:
# prepare single sle voucher detail lookup
self.prepare_stock_reco_voucher_wise_count()
if stock_ledger_entries is None:
# nested queries invalidate the streaming cursor below,
# so batchwise valuation flags must be resolved beforehand
self._prefetch_batchwise_valuations()
with frappe.db.unbuffered_cursor():
if stock_ledger_entries is None:
stock_ledger_entries = self._get_stock_ledger_entries()
@@ -423,12 +428,38 @@ class FIFOSlots:
def _get_batchwise_valuation(self, batch_no: str):
if batch_no not in self.batchwise_valuation_by_batch:
# only reachable when stock ledger entries are passed in directly;
# the streaming path prefetches all flags before iteration
self.batchwise_valuation_by_batch[batch_no] = frappe.db.get_value(
"Batch", batch_no, "use_batchwise_valuation"
)
return self.batchwise_valuation_by_batch[batch_no]
def _prefetch_batchwise_valuations(self) -> None:
sle = frappe.qb.DocType("Stock Ledger Entry")
batch = frappe.qb.DocType("Batch")
to_date = get_datetime(self.filters.get("to_date") + " 23:59:59")
query = (
frappe.qb.from_(sle)
.left_join(batch)
.on(sle.batch_no == batch.name)
.select(sle.batch_no, batch.use_batchwise_valuation)
.distinct()
.where(
(sle.batch_no.isnotnull())
& (sle.company == self.filters.get("company"))
& (sle.posting_datetime <= to_date)
& (sle.is_cancelled != 1)
)
)
query = self._apply_filter(query, sle, "item_code")
for batch_no, use_batchwise_valuation in query.run():
self.batchwise_valuation_by_batch[batch_no] = use_batchwise_valuation
def _init_key_stores(self, row: dict) -> tuple:
"Initialise keys and FIFO Queue."

View File

@@ -1438,6 +1438,80 @@ class TestStockAgeing(FrappeTestCase):
item_result["fifo_queue"], [[batch_no.upper(), 1, 5.0, getdate(add_days(base_date, -2)), 50.0]]
)
def test_legacy_batch_no_sle_with_streaming_cursor(self):
"""SLEs carrying the legacy batch_no field must not trigger nested
queries while entries stream through an unbuffered cursor."""
from unittest.mock import patch
from frappe.utils import add_days, nowdate
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
get_batch_from_bundle,
)
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation,
)
suffix = frappe.generate_hash(length=8).upper()
item_code = make_item(
f"Test Stock Ageing Legacy Batch {suffix}",
{
"is_stock_item": 1,
"has_batch_no": 1,
"create_new_batch": 1,
"batch_number_series": f"SA-LEG-{suffix}-.###",
"valuation_method": "FIFO",
},
).name
warehouse = "_Test Warehouse - _TC"
base_date = nowdate()
reco = create_stock_reconciliation(
item_code=item_code,
warehouse=warehouse,
qty=10,
rate=10,
posting_date=add_days(base_date, -2),
posting_time="10:00:00",
)
batch_no = get_batch_from_bundle(reco.items[0].serial_and_batch_bundle)
frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", 1)
create_stock_reconciliation(
item_code=item_code,
warehouse=warehouse,
qty=5,
rate=10,
batch_no=batch_no,
posting_date=add_days(base_date, -1),
posting_time="10:00:00",
)
# mimic pre-bundle data where SLEs carry batch_no directly
frappe.db.set_value(
"Stock Ledger Entry",
{"item_code": item_code},
"batch_no",
batch_no,
)
filters = frappe._dict(
company="_Test Company",
to_date=base_date,
ranges=["30", "60", "90"],
item_code=item_code,
)
fifo_slots = FIFOSlots(filters)
# fetch row by row so the streaming result set is still active
# while each stock ledger entry is processed
with patch("frappe.database.database.SQL_ITERATOR_BATCH_SIZE", 1):
slots = fifo_slots.generate()
self.assertEqual(fifo_slots.batchwise_valuation_by_batch.get(batch_no), 1)
self.assertEqual(slots[item_code]["total_qty"], 5.0)
def generate_item_and_item_wh_wise_slots(filters, sle):
"Return results with and without 'show_warehouse_wise_stock'"