Skip to content

Commit

Permalink
fix: consider reserved batches 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 d9e2843 commit e1a87a8
Show file tree
Hide file tree
Showing 2 changed files with 77 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,39 @@ def get_sre_reserved_serial_nos_details(
return frappe._dict(query.run())


def get_sre_reserved_batch_nos_details(
item_code: str, warehouse: str, batch_nos: list = None
) -> dict:
"""Returns a dict of `Batch Qty` reserved in Stock Reservation Entry. The dict is like {batch_no: qty, ...}"""

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.batch_no,
Sum(sb_entry.qty - sb_entry.delivered_qty),
)
.where(
(sre.docstatus == 1)
& (sre.item_code == item_code)
& (sre.warehouse == warehouse)
& ((sre.reserved_qty - sre.delivered_qty) > 0)
& (sre.status.notin(["Delivered", "Cancelled"]))
& (sre.reservation_based_on == "Serial and Batch")
)
.groupby(sb_entry.batch_no)
.orderby(sb_entry.creation)
)

if batch_nos:
query = query.where(sb_entry.batch_no.isin(batch_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
48 changes: 44 additions & 4 deletions erpnext/stock/stock_ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,17 @@
from frappe.model.meta import get_field_precision
from frappe.query_builder import Case
from frappe.query_builder.functions import CombineDatetime, Sum
from frappe.utils import cint, flt, get_link_to_form, getdate, now, nowdate, parse_json
from frappe.utils import cint, flt, get_link_to_form, getdate, now, nowdate, nowtime, parse_json

import erpnext
from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
get_available_batches,
)
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
get_sre_reserved_batch_nos_details,
)
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
get_sre_reserved_qty_for_item_and_warehouse as get_reserved_stock,
)
Expand Down Expand Up @@ -1809,6 +1815,9 @@ def validate_reserved_stock(kwargs):
serial_nos = kwargs.serial_no.split("\n")
validate_reserved_serial_nos(kwargs.item_code, kwargs.warehouse, serial_nos)

elif kwargs.batch_no:
validate_reserved_batch_nos(kwargs.item_code, kwargs.warehouse, [kwargs.batch_no])

elif kwargs.serial_and_batch_bundle:
sbb_entries = frappe.db.get_all(
"Serial and Batch Entry",
Expand All @@ -1817,12 +1826,13 @@ def validate_reserved_stock(kwargs):
"parent": kwargs.serial_and_batch_bundle,
"docstatus": 1,
},
["batch_no", "serial_no", "qty"],
["batch_no", "serial_no"],
)
serial_nos = [entry.serial_no for entry in sbb_entries if entry.serial_no]

if serial_nos:
if serial_nos := [entry.serial_no for entry in sbb_entries if entry.serial_no]:
validate_reserved_serial_nos(kwargs.item_code, kwargs.warehouse, serial_nos)
elif batch_nos := [entry.batch_no for entry in sbb_entries if entry.batch_no]:
validate_reserved_batch_nos(kwargs.item_code, kwargs.warehouse, batch_nos)


def validate_reserved_serial_nos(item_code, warehouse, serial_nos):
Expand All @@ -1845,6 +1855,36 @@ def validate_reserved_serial_nos(item_code, warehouse, serial_nos):
frappe.throw(msg, title=_("Reserved Serial No."))


def validate_reserved_batch_nos(item_code, warehouse, batch_nos):
if reserved_batches_map := get_sre_reserved_batch_nos_details(item_code, warehouse, batch_nos):
available_batches = get_available_batches(
frappe._dict(
{
"item_code": item_code,
"warehouse": warehouse,
"posting_date": nowdate(),
"posting_time": nowtime(),
}
)
)
available_batches_map = {row.batch_no: row.qty for row in available_batches}
precision = cint(frappe.db.get_default("float_precision")) or 2

for batch_no in batch_nos:
diff = flt(
available_batches_map.get(batch_no, 0) - reserved_batches_map.get(batch_no, 0), precision
)
if diff < 0 and abs(diff) > 0.0001:
msg = _("{0} units of {1} needed in {2} on {3} {4} to complete this transaction.").format(
abs(diff),
frappe.get_desk_link("Batch", batch_no),
frappe.get_desk_link("Warehouse", warehouse),
nowdate(),
nowtime(),
)
frappe.throw(msg, title=_("Reserved Stock for Batch"))


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 e1a87a8

Please sign in to comment.