diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index d222c3eb31..1bb181b496 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -291,6 +291,11 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex } this.frm.page.set_inner_btn_group_as_primary(__('Create')); } + + // Stock Reservation + if (this.frm.doc.__onload && this.frm.doc.__onload.has_reserved_stock) { + this.frm.add_custom_button(__('Unreserve'), () => this.cancel_stock_reservation_entries(), __('Stock Reservation')); + } } if (this.frm.doc.docstatus===0) { @@ -330,6 +335,22 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex this.order_type(doc); } + cancel_stock_reservation_entries() { + frappe.call({ + method: "erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry.cancel_stock_reservation_entries", + args: { + voucher_type: this.frm.doctype, + voucher_no: this.frm.docname + }, + freeze: true, + freeze_message: __("Unreserving Stock..."), + callback: (r) => { + this.frm.doc.__onload.has_reserved_stock = false; + this.frm.refresh(); + } + }) + } + create_pick_list() { frappe.model.open_mapped_doc({ method: "erpnext.selling.doctype.sales_order.sales_order.create_pick_list", diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index d492f718e5..06c84b0b78 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -44,6 +44,14 @@ class SalesOrder(SellingController): def __init__(self, *args, **kwargs): super(SalesOrder, self).__init__(*args, **kwargs) + def onload(self): + from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( + has_reserved_stock, + ) + + if has_reserved_stock(self.doctype, self.name): + self.set_onload("has_reserved_stock", True) + def validate(self): super(SalesOrder, self).validate() self.validate_delivery_date() 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 c80ae577f3..1b6388d53d 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py +++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py @@ -85,7 +85,6 @@ class StockReservationEntry(Document): ) -@frappe.whitelist() def get_available_qty_to_reserve(item_code, warehouse): from frappe.query_builder.functions import Sum @@ -170,15 +169,6 @@ def get_sre_reserved_qty_details_for_voucher_detail_no( ).run(as_list=True)[0] -def has_reserved_stock(voucher_type: str, voucher_no: str, voucher_detail_no: str = None) -> bool: - if get_stock_reservation_entries_for_voucher( - voucher_type, voucher_no, voucher_detail_no, fields=["name"] - ): - return True - - return False - - def get_sre_reserved_qty_details(item_code: str | list, warehouse: str | list) -> dict: sre_details = {} @@ -209,3 +199,25 @@ def get_sre_reserved_qty_details(item_code: str | list, warehouse: str | list) - sre_details = {(d["item_code"], d["warehouse"]): d["reserved_qty"] for d in sre_data} return sre_details + + +@frappe.whitelist() +def has_reserved_stock(voucher_type: str, voucher_no: str, voucher_detail_no: str = None) -> bool: + if get_stock_reservation_entries_for_voucher( + voucher_type, voucher_no, voucher_detail_no, fields=["name"] + ): + return True + + return False + + +@frappe.whitelist() +def cancel_stock_reservation_entries( + voucher_type: str, voucher_no: str, voucher_detail_no: str = None +) -> None: + sre_list = get_stock_reservation_entries_for_voucher( + voucher_type, voucher_no, voucher_detail_no, fields=["name"] + ) + + for sre in sre_list: + frappe.get_doc("Stock Reservation Entry", sre.name).cancel()