Skip to content

Commit

Permalink
fix: consider reserved serial nos while cancelling a stock transaction
Browse files Browse the repository at this point in the history
  • Loading branch information
s-aga-r committed Nov 4, 2023
1 parent 56e9a46 commit d9e2843
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,35 @@ def get_sre_reserved_qty_for_voucher_detail_no(
return flt(reserved_qty[0][0])


def get_sre_reserved_serial_nos_details(
item_code: str, warehouse: str, serial_nos: list = None
) -> dict:
"""Returns a dict of `Serial No` reserved in Stock Reservation Entry. The dict is like {serial_no: sre_name, ...}"""

sre = frappe.qb.DocType("Stock Reservation Entry")
sb_entry = frappe.qb.DocType("Serial and Batch Entry")
query = (
frappe.qb.from_(sre)
.inner_join(sb_entry)
.on(sre.name == sb_entry.parent)
.select(sb_entry.serial_no, sre.name)
.where(
(sre.docstatus == 1)
& (sre.item_code == item_code)
& (sre.warehouse == warehouse)
& (sre.reserved_qty > sre.delivered_qty)
& (sre.status.notin(["Delivered", "Cancelled"]))
& (sre.reservation_based_on == "Serial and Batch")
)
.orderby(sb_entry.creation)
)

if serial_nos:
query = query.where(sb_entry.serial_no.isin(serial_nos))

return frappe._dict(query.run())


def get_sre_details_for_voucher(voucher_type: str, voucher_no: str) -> list[dict]:
"""Returns a list of SREs for the provided voucher."""

Expand Down
74 changes: 59 additions & 15 deletions erpnext/stock/stock_ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
get_sre_reserved_qty_for_item_and_warehouse as get_reserved_stock,
)
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
get_sre_reserved_serial_nos_details,
)
from erpnext.stock.utils import (
get_incoming_outgoing_rate_for_cancel,
get_or_make_bin,
Expand Down Expand Up @@ -1719,22 +1722,22 @@ def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):

frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))

if not args.batch_no:
return
if args.batch_no:
neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
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(
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"))

neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
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(
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"))
validate_reserved_stock(args)


def is_negative_with_precision(neg_sle, is_batch=False):
Expand Down Expand Up @@ -1801,6 +1804,47 @@ def get_future_sle_with_negative_batch_qty(args):
)


def validate_reserved_stock(kwargs):
if kwargs.serial_no:
serial_nos = kwargs.serial_no.split("\n")
validate_reserved_serial_nos(kwargs.item_code, kwargs.warehouse, serial_nos)

elif kwargs.serial_and_batch_bundle:
sbb_entries = frappe.db.get_all(
"Serial and Batch Entry",
{
"parenttype": "Serial and Batch Bundle",
"parent": kwargs.serial_and_batch_bundle,
"docstatus": 1,
},
["batch_no", "serial_no", "qty"],
)
serial_nos = [entry.serial_no for entry in sbb_entries if entry.serial_no]

if serial_nos:
validate_reserved_serial_nos(kwargs.item_code, kwargs.warehouse, serial_nos)


def validate_reserved_serial_nos(item_code, warehouse, serial_nos):
if reserved_serial_nos_details := get_sre_reserved_serial_nos_details(
item_code, warehouse, serial_nos
):
if common_serial_nos := list(
set(serial_nos).intersection(set(reserved_serial_nos_details.keys()))
):
msg = _(
"Serial Nos are reserved in Stock Reservation Entries, you need to unreserve them before proceeding."
)
msg += "<br />"
msg += _("Example: Serial No {0} reserved in {1}.").format(
frappe.bold(common_serial_nos[0]),
frappe.get_desk_link(
"Stock Reservation Entry", reserved_serial_nos_details[common_serial_nos[0]]
),
)
frappe.throw(msg, title=_("Reserved Serial No."))


def is_negative_stock_allowed(*, item_code: Optional[str] = None) -> bool:
if cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock", cache=True)):
return True
Expand Down

0 comments on commit d9e2843

Please sign in to comment.