feat: reserve stock for SO on PR submission
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index b91002e..d382162 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -541,7 +541,7 @@
create_stock_reservation_entries_for_so_items as create_stock_reservation_entries,
)
- create_stock_reservation_entries(so=self, items_details=items_details, notify=notify)
+ create_stock_reservation_entries(sales_order=self, items_details=items_details, notify=notify)
@frappe.whitelist()
def cancel_stock_reservation_entries(self, sre_list=None, notify=True) -> None:
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index 2fcd102..8c9d03c 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -242,7 +242,7 @@
for so, locations in so_details.items():
so_doc = frappe.get_doc("Sales Order", so)
create_stock_reservation_entries_for_so_items(
- so=so_doc, items_details=locations, against_pick_list=True, notify=notify
+ sales_order=so_doc, items_details=locations, against_pick_list=True, notify=notify
)
@frappe.whitelist()
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index de0db1a..fc88dd8 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -264,6 +264,7 @@
self.make_gl_entries()
self.repost_future_sle_and_gle()
self.set_consumed_qty_in_subcontract_order()
+ self.reserve_stock_for_sales_order()
def check_next_docstatus(self):
submit_rv = frappe.db.sql(
@@ -829,6 +830,31 @@
self.load_from_db()
+ def reserve_stock_for_sales_order(self):
+ if self.is_return or not cint(
+ frappe.db.get_single_value("Stock Settings", "auto_reserve_stock_for_sales_order_on_purchase")
+ ):
+ return
+
+ self.reload() # reload to get the Serial and Batch Bundle Details
+
+ so_items_details_map = {}
+ for item in self.items:
+ if item.sales_order and item.sales_order_item:
+ item_details = {
+ "name": item.sales_order_item,
+ "item_code": item.item_code,
+ "warehouse": item.warehouse,
+ "qty_to_reserve": item.stock_qty,
+ "serial_and_batch_bundle": item.get("serial_and_batch_bundle"),
+ }
+ so_items_details_map.setdefault(item.sales_order, []).append(item_details)
+
+ if so_items_details_map:
+ for so, items_details in so_items_details_map.items():
+ so_doc = frappe.get_doc("Sales Order", so)
+ so_doc.create_stock_reservation_entries(items_details)
+
def update_billed_amount_based_on_po(po_details, update_modified=True):
po_billed_amt_details = get_billed_amount_against_po(po_details)
diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py
index 936be3f..effad7d 100644
--- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py
+++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py
@@ -761,7 +761,7 @@
def create_stock_reservation_entries_for_so_items(
- so: object,
+ sales_order: object,
items_details: list[dict] = None,
against_pick_list: bool = False,
notify=True,
@@ -771,15 +771,17 @@
from erpnext.selling.doctype.sales_order.sales_order import get_unreserved_qty
if not against_pick_list and (
- so.get("_action") == "submit"
- and so.set_warehouse
- and cint(frappe.get_cached_value("Warehouse", so.set_warehouse, "is_group"))
+ sales_order.get("_action") == "submit"
+ and sales_order.set_warehouse
+ and cint(frappe.get_cached_value("Warehouse", sales_order.set_warehouse, "is_group"))
):
return frappe.msgprint(
- _("Stock cannot be reserved in the group warehouse {0}.").format(frappe.bold(so.set_warehouse))
+ _("Stock cannot be reserved in the group warehouse {0}.").format(
+ frappe.bold(sales_order.set_warehouse)
+ )
)
- validate_stock_reservation_settings(so)
+ validate_stock_reservation_settings(sales_order)
allow_partial_reservation = frappe.db.get_single_value(
"Stock Settings", "allow_partial_reservation"
@@ -787,29 +789,28 @@
items = []
if items_details:
+ item_field = "sales_order_item" if against_pick_list else "name"
+
for item in items_details:
- so_item = frappe.get_doc(
- "Sales Order Item", item.get("sales_order_item") if against_pick_list else item.get("name")
- )
- so_item.reserve_stock = 1
+ so_item = frappe.get_doc("Sales Order Item", item.get(item_field))
so_item.warehouse = item.get("warehouse")
so_item.qty_to_reserve = (
item.get("picked_qty") - item.get("stock_reserved_qty", 0)
if against_pick_list
else (flt(item.get("qty_to_reserve")) * flt(so_item.conversion_factor, 1))
)
+ so_item.serial_and_batch_bundle = item.get("serial_and_batch_bundle")
if against_pick_list:
so_item.pick_list = item.get("parent")
so_item.pick_list_item = item.get("name")
- so_item.pick_list_sbb = item.get("serial_and_batch_bundle")
items.append(so_item)
sre_count = 0
- reserved_qty_details = get_sre_reserved_qty_details_for_voucher("Sales Order", so.name)
+ reserved_qty_details = get_sre_reserved_qty_details_for_voucher("Sales Order", sales_order.name)
- for item in items if items_details else so.get("items"):
+ for item in items if items_details else sales_order.get("items"):
# Skip if `Reserved Stock` is not checked for the item.
if not item.get("reserve_stock"):
continue
@@ -817,9 +818,9 @@
# Stock should be reserved from the Pick List if has Picked Qty.
if not against_pick_list and flt(item.picked_qty) > 0:
frappe.throw(
- _(
- "Row #{0}: Item {1} has been picked, please create a Stock Reservation from the Pick List."
- ).format(item.idx, frappe.bold(item.item_code))
+ _("Row #{0}: Item {1} has been picked, please reserve stock from the Pick List.").format(
+ item.idx, frappe.bold(item.item_code)
+ )
)
is_stock_item, has_serial_no, has_batch_no = frappe.get_cached_value(
@@ -915,33 +916,33 @@
sre.warehouse = item.warehouse
sre.has_serial_no = has_serial_no
sre.has_batch_no = has_batch_no
- sre.voucher_type = so.doctype
- sre.voucher_no = so.name
+ sre.voucher_type = sales_order.doctype
+ sre.voucher_no = sales_order.name
sre.voucher_detail_no = item.name
sre.available_qty = available_qty_to_reserve
sre.voucher_qty = item.stock_qty
sre.reserved_qty = qty_to_be_reserved
- sre.company = so.company
+ sre.company = sales_order.company
sre.stock_uom = item.stock_uom
- sre.project = so.project
+ sre.project = sales_order.project
if against_pick_list:
sre.against_pick_list = item.pick_list
sre.against_pick_list_item = item.pick_list_item
- if item.pick_list_sbb:
- sbb = frappe.get_doc("Serial and Batch Bundle", item.pick_list_sbb)
- sre.reservation_based_on = "Serial and Batch"
- for entry in sbb.entries:
- sre.append(
- "sb_entries",
- {
- "serial_no": entry.serial_no,
- "batch_no": entry.batch_no,
- "qty": 1 if has_serial_no else abs(entry.qty),
- "warehouse": entry.warehouse,
- },
- )
+ if item.serial_and_batch_bundle:
+ sbb = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle)
+ sre.reservation_based_on = "Serial and Batch"
+ for entry in sbb.entries:
+ sre.append(
+ "sb_entries",
+ {
+ "serial_no": entry.serial_no,
+ "batch_no": entry.batch_no,
+ "qty": 1 if has_serial_no else abs(entry.qty),
+ "warehouse": entry.warehouse,
+ },
+ )
sre.save()
sre.submit()