refactor(stock): address layering/robustness review findings (#8, #9, #10)

#8 ledger_preview: wrap the submit-in-memory dry run in a savepoint inside
get_accounting_ledger_preview / get_stock_ledger_preview and roll back to it in a
finally, so the preview never persists entries regardless of caller (previously
only the whitelisted show_*_preview wrappers' full rollback made it safe).

#9 exceptions: move BatchExpiredError and the QualityInspection* errors into a new
erpnext/stock/exceptions.py and re-export them from stock_controller for backward
compatibility (job_card and tests still import from the controller; identity is
preserved). Services now import from the neutral module instead of back from the
controller they were extracted out of.

#10 quality inspection: extract the duplicated doctype->inspection-field map into a
single INSPECTION_FIELDNAME_MAP constant in the service, consumed by both
validate_inspection and check_item_quality_inspection.

Verified: ledger snapshots, quality inspection suite, stock_entry batch-expiry test
stay green; preview smoke-tested to persist nothing and not roll back the caller.
This commit is contained in:
Nabin Hait
2026-06-05 12:29:55 +05:30
parent 3dba21f814
commit 78d5fbaca4
5 changed files with 83 additions and 55 deletions

View File

@@ -23,25 +23,17 @@ from erpnext.setup.doctype.brand.brand import get_brand_defaults
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
from erpnext.stock import get_warehouse_account_map
from erpnext.stock.doctype.item.item import get_item_defaults
# Re-exported for backward compatibility; canonical home is erpnext.stock.exceptions.
from erpnext.stock.exceptions import (
BatchExpiredError,
QualityInspectionNotSubmittedError,
QualityInspectionRejectedError,
QualityInspectionRequiredError,
)
from erpnext.stock.stock_ledger import get_items_to_be_repost
class QualityInspectionRequiredError(frappe.ValidationError):
pass
class QualityInspectionRejectedError(frappe.ValidationError):
pass
class QualityInspectionNotSubmittedError(frappe.ValidationError):
pass
class BatchExpiredError(frappe.ValidationError):
pass
class StockController(AccountsController):
def validate(self):
super().validate()
@@ -674,18 +666,12 @@ def repost_required_for_queue(doc: StockController) -> bool:
@frappe.whitelist()
def check_item_quality_inspection(doctype: str, docstatus: str | int, items: str | list[dict]):
from erpnext.stock.services.quality_inspection import INSPECTION_FIELDNAME_MAP
if isinstance(items, str):
items = json.loads(items)
inspection_fieldname_map = {
"Purchase Receipt": "inspection_required_before_purchase",
"Purchase Invoice": "inspection_required_before_purchase",
"Subcontracting Receipt": "inspection_required_before_purchase",
"Sales Invoice": "inspection_required_before_delivery",
"Delivery Note": "inspection_required_before_delivery",
}
inspection_fieldname = inspection_fieldname_map.get(doctype)
inspection_fieldname = INSPECTION_FIELDNAME_MAP.get(doctype)
if inspection_fieldname is None:
return []