diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index bd6b1f1216..bab6bb7b7b 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2784,16 +2784,16 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil parent.update_billing_percentage() parent.set_status() + # Cancel and Recreate Stock Reservation Entries. if parent_doctype == "Sales Order": from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( cancel_stock_reservation_entries, has_reserved_stock, - reserve_stock_against_sales_order, ) if has_reserved_stock(parent.doctype, parent.name): cancel_stock_reservation_entries(parent.doctype, parent.name) - reserve_stock_against_sales_order(parent.name) + parent.create_stock_reservation_entries() @erpnext.allow_regional diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index a4578bcf2c..37c229417f 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -46,8 +46,6 @@ frappe.ui.form.on("Sales Order", { frm.set_df_property('packed_items', 'cannot_add_rows', true); frm.set_df_property('packed_items', 'cannot_delete_rows', true); - - }, refresh: function(frm) { if(frm.doc.docstatus === 1 && frm.doc.status !== 'Closed' @@ -71,13 +69,11 @@ frappe.ui.form.on("Sales Order", { frappe.db.get_single_value("Stock Settings", "enable_stock_reservation").then((value) => { if (value) { frappe.db.get_single_value("Stock Settings", "reserve_stock_on_sales_order_submission").then((value) => { - if (value) { - frm.set_value("reserve_stock", 1); - } else { - frm.set_value("reserve_stock", 0); - } + // If `Reserve Stock on Sales Order Submission` is enabled in Stock Settings, set Reserve Stock to 1 else 0. + frm.set_value("reserve_stock", value ? 1 : 0); }) } else { + // If `Stock Reservation` is disabled in Stock Settings, set Reserve Stock to 0 and read only. frm.set_value("reserve_stock", 0); frm.set_df_property("reserve_stock", "read_only", 1); } @@ -292,11 +288,12 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex this.frm.page.set_inner_btn_group_as_primary(__('Create')); } - // Stock Reservation + // Stock Reservation > Reserve button will be only visible if the SO has unreserved stock. if (this.frm.doc.__onload && this.frm.doc.__onload.has_unreserved_stock) { - this.frm.add_custom_button(__('Reserve'), () => this.reserve_stock_against_sales_order(), __('Stock Reservation')); + this.frm.add_custom_button(__('Reserve'), () => this.create_stock_reservation_entries(), __('Stock Reservation')); } + // Stock Reservation > Unreserve button will be only visible if the SO has reserved stock. 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')); } @@ -339,14 +336,15 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex this.order_type(doc); } - reserve_stock_against_sales_order() { + create_stock_reservation_entries() { frappe.call({ - method: "erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry.reserve_stock_against_sales_order", + doc: this.frm.doc, + method: 'create_stock_reservation_entries', args: { - sales_order: this.frm.docname + notify: true }, freeze: true, - freeze_message: __("Reserving Stock..."), + freeze_message: __('Reserving Stock...'), callback: (r) => { this.frm.doc.__onload.has_unreserved_stock = false; this.frm.refresh(); @@ -356,13 +354,13 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex cancel_stock_reservation_entries() { frappe.call({ - method: "erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry.cancel_stock_reservation_entries", + 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..."), + freeze_message: __('Unreserving Stock...'), callback: (r) => { this.frm.doc.__onload.has_reserved_stock = false; this.frm.refresh(); diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index d09f7c5556..47bb37c91f 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -1641,17 +1641,20 @@ }, { "default": "0", + "description": "If checked, Stock Reservation Entries will be created on Submit", "fieldname": "reserve_stock", "fieldtype": "Check", - "label": "Reserve Stock on Submit", - "no_copy": 1 + "label": "Reserve Stock", + "no_copy": 1, + "print_hide": 1, + "report_hide": 1 } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2023-03-31 13:04:36.653260", + "modified": "2023-04-04 10:39:34.129343", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index b24e4810a1..9ed57df175 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -30,6 +30,9 @@ from erpnext.manufacturing.doctype.production_plan.production_plan import ( from erpnext.selling.doctype.customer.customer import check_credit_limit from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults from erpnext.stock.doctype.item.item import get_item_defaults +from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( + get_sre_reserved_qty_details_for_voucher, +) from erpnext.stock.get_item_details import get_default_bom, get_price_list_rate from erpnext.stock.stock_balance import get_reserved_qty, update_bin_qty @@ -44,7 +47,7 @@ class SalesOrder(SellingController): def __init__(self, *args, **kwargs): super(SalesOrder, self).__init__(*args, **kwargs) - def onload(self): + def onload(self) -> None: if frappe.get_cached_value("Stock Settings", None, "enable_stock_reservation"): from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( has_reserved_stock, @@ -254,11 +257,7 @@ class SalesOrder(SellingController): update_coupon_code_count(self.coupon_code, "used") if self.get("reserve_stock"): - from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( - reserve_stock_against_sales_order, - ) - - reserve_stock_against_sales_order(self) + self.create_stock_reservation_entries() def on_cancel(self): self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry") @@ -504,34 +503,118 @@ class SalesOrder(SellingController): ).format(item.item_code) ) - def has_unreserved_stock(self): - from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( - get_sre_reserved_qty_details_for_voucher_detail_no, - ) + def has_unreserved_stock(self) -> bool: + """Returns True if there is any unreserved item in the Sales Order.""" - for item in self.items: + reserved_qty_details = get_sre_reserved_qty_details_for_voucher("Sales Order", self.name) + + for item in self.get("items"): if not item.get("reserve_stock"): continue - reserved_qty_details = get_sre_reserved_qty_details_for_voucher_detail_no( - "Sales Order", self.name, item.name - ) - - existing_reserved_qty = 0.0 - if reserved_qty_details: - existing_reserved_qty = reserved_qty_details[1] - - unreserved_qty = ( - item.stock_qty - - flt(item.delivered_qty) * item.get("conversion_factor", 1) - - existing_reserved_qty - ) - + unreserved_qty = get_unreserved_qty(item, reserved_qty_details) if unreserved_qty > 0: return True return False + @frappe.whitelist() + def create_stock_reservation_entries(self, notify=True): + """Creates Stock Reservation Entries for Sales Order Items.""" + + from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( + get_available_qty_to_reserve, + validate_stock_reservation_settings, + ) + + validate_stock_reservation_settings(self) + + allow_partial_reservation = frappe.db.get_single_value( + "Stock Settings", "allow_partial_reservation" + ) + + sre_count = 0 + reserved_qty_details = get_sre_reserved_qty_details_for_voucher("Sales Order", self.name) + for item in self.get("items"): + if not item.get("reserve_stock"): + continue + + unreserved_qty = get_unreserved_qty(item, reserved_qty_details) + + # Stock is already reserved for the item, notify the user and skip the item. + if unreserved_qty <= 0: + frappe.msgprint( + _("Row #{0}: Stock is already reserved for the Item {1}").format( + item.idx, frappe.bold(item.item_code) + ), + title=_("Stock Reservation"), + ) + continue + + available_qty_to_reserve = get_available_qty_to_reserve(item.item_code, item.warehouse) + + # No stock available to reserve, notify the user and skip the item. + if available_qty_to_reserve <= 0: + frappe.msgprint( + _("Row #{0}: No available stock to reserve for the Item {1}").format( + item.idx, frappe.bold(item.item_code) + ), + title=_("Stock Reservation"), + indicator="orange", + ) + continue + + # The quantity which can be reserved. + qty_to_be_reserved = min(unreserved_qty, available_qty_to_reserve) + + # Partial Reservation + if qty_to_be_reserved < unreserved_qty: + frappe.msgprint( + _("Row #{0}: Only {1} available to reserve for the Item {2}").format( + item.idx, + frappe.bold(str(qty_to_be_reserved / item.conversion_factor) + " " + item.uom), + frappe.bold(item.item_code), + ), + title=_("Stock Reservation"), + indicator="orange", + ) + + # Skip the item if `Partial Reservation` is disabled in the Stock Settings. + if not allow_partial_reservation: + continue + + # Create and Submit Stock Reservation Entry + sre = frappe.new_doc("Stock Reservation Entry") + sre.item_code = item.item_code + sre.warehouse = item.warehouse + sre.voucher_type = self.doctype + sre.voucher_no = self.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 = self.company + sre.stock_uom = item.stock_uom + sre.project = self.project + sre.save() + sre.submit() + + sre_count += 1 + + if sre_count and notify: + frappe.msgprint(_("Stock Reservation Entries Created"), alert=True, indicator="green") + + +def get_unreserved_qty(item: object, reserved_qty_details: dict) -> float: + """Returns the unreserved quantity for the Sales Order Item.""" + + existing_reserved_qty = reserved_qty_details.get((item.name, item.warehouse), 0) + return ( + item.stock_qty + - flt(item.delivered_qty) * item.get("conversion_factor", 1) + - existing_reserved_qty + ) + def get_list_context(context=None): from erpnext.controllers.website_list_for_contact import get_list_context diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index ec3d7695d0..5c7e10a232 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -867,7 +867,9 @@ "default": "1", "fieldname": "reserve_stock", "fieldtype": "Check", - "label": "Reserve Stock" + "label": "Reserve Stock", + "print_hide": 1, + "report_hide": 1 }, { "default": "0", @@ -876,13 +878,16 @@ "fieldtype": "Float", "label": "Stock Reserved Qty (in Stock UOM)", "no_copy": 1, - "read_only": 1 + "non_negative": 1, + "print_hide": 1, + "read_only": 1, + "report_hide": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-03-31 21:53:47.431882", + "modified": "2023-04-04 10:44:05.707488", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index f45f8bd6df..3c6286e197 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -148,7 +148,7 @@ class DeliveryNote(SellingController): if not self.installation_status: self.installation_status = "Not Installed" - self.validate_against_stock_reservation() + self.validate_against_stock_reservation_entries() self.reset_default_field_value("set_warehouse", "items", "warehouse") def validate_with_previous_doc(self): @@ -241,7 +241,7 @@ class DeliveryNote(SellingController): self.update_prevdoc_status() self.update_billing_status() - self.update_stock_reservation_entry() + self.update_stock_reservation_entries() if not self.is_return: self.check_credit_limit() @@ -272,11 +272,15 @@ class DeliveryNote(SellingController): self.repost_future_sle_and_gle() self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation") - def update_stock_reservation_entry(self): - if self.is_return or self._action != "submit": + def update_stock_reservation_entries(self) -> None: + """Updates Delivered Qty in Stock Reservation Entries.""" + + # Don't update Delivered Qty on Return or Cancellation. + if self.is_return or self._action == "cancel": return - for item in self.items: + for item in self.get("items"): + # Skip if `Sales Order` or `Sales Order Item` reference is not set. if not item.against_sales_order or not item.so_detail: continue @@ -293,6 +297,7 @@ class DeliveryNote(SellingController): order_by="creation", ) + # Skip if no Stock Reservation Entries. if not sre_list: continue @@ -302,22 +307,31 @@ class DeliveryNote(SellingController): break sre_doc = frappe.get_doc("Stock Reservation Entry", sre) + + # `Delivered Qty` should be less than or equal to `Reserved Qty`. qty_to_be_deliver = min(sre_doc.reserved_qty - sre_doc.delivered_qty, available_qty_to_deliver) + sre_doc.delivered_qty += qty_to_be_deliver sre_doc.db_update() + + # Update Stock Reservation Entry `Status` based on `Delivered Qty`. sre_doc.update_status() available_qty_to_deliver -= qty_to_be_deliver - def validate_against_stock_reservation(self): + def validate_against_stock_reservation_entries(self): + """Validates if Stock Reservation Entries are available for the Sales Order Item reference.""" + from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( get_sre_reserved_qty_details_for_voucher_detail_no, ) + # Don't validate if Return if self.is_return: return - for item in self.items: + for item in self.get("items"): + # Skip if `Sales Order` or `Sales Order Item` reference is not set. if not item.against_sales_order or not item.so_detail: continue @@ -325,6 +339,7 @@ class DeliveryNote(SellingController): "Sales Order", item.against_sales_order, item.so_detail ) + # Skip if stock is not reserved. if not sre_data: continue 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 3e6c84032f..f770059131 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py +++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py @@ -5,7 +5,6 @@ import frappe from frappe import _ from frappe.model.document import Document from frappe.query_builder.functions import Sum -from frappe.utils import flt class StockReservationEntry(Document): @@ -25,6 +24,8 @@ class StockReservationEntry(Document): self.update_status() def validate_mandatory(self) -> None: + """Raises exception if mandatory fields are not set.""" + mandatory = [ "item_code", "warehouse", @@ -42,6 +43,8 @@ class StockReservationEntry(Document): frappe.throw(_("{0} is required").format(self.meta.get_label(d))) def update_status(self, status: str = None, update_modified: bool = True) -> None: + """Updates status based on Voucher Qty, Reserved Qty and Delivered Qty.""" + if not status: if self.docstatus == 2: status = "Cancelled" @@ -62,6 +65,8 @@ class StockReservationEntry(Document): def update_reserved_qty_in_voucher( self, reserved_qty_field: str = "stock_reserved_qty", update_modified: bool = True ) -> None: + """Updates total reserved qty in the voucher.""" + item_doctype = "Sales Order Item" if self.voucher_type == "Sales Order" else None if item_doctype: @@ -87,6 +92,8 @@ class StockReservationEntry(Document): def validate_stock_reservation_settings(voucher: object) -> None: + """Raises an exception if `Stock Reservation` is not enabled or `Voucher Type` is not allowed.""" + if not frappe.db.get_single_value("Stock Settings", "enable_stock_reservation"): frappe.throw( _("Please enable {0} in the {1}.").format( @@ -94,7 +101,9 @@ def validate_stock_reservation_settings(voucher: object) -> None: ) ) + # Voucher types allowed for stock reservation allowed_voucher_types = ["Sales Order"] + if voucher.doctype not in allowed_voucher_types: frappe.throw( _("Stock Reservation can only be created against {0}.").format(", ".join(allowed_voucher_types)) @@ -102,6 +111,8 @@ def validate_stock_reservation_settings(voucher: object) -> None: def get_available_qty_to_reserve(item_code: str, warehouse: str) -> float: + """Returns `Available Qty to Reserve (Actual Qty - Reserved Qty)` for Item and Warehouse combination.""" + from erpnext.stock.get_item_details import get_bin_details available_qty = get_bin_details(item_code, warehouse, include_child_warehouses=True).get( @@ -133,6 +144,8 @@ def get_available_qty_to_reserve(item_code: str, warehouse: str) -> float: def get_stock_reservation_entries_for_voucher( voucher_type: str, voucher_no: str, voucher_detail_no: str = None, fields: list[str] = None ) -> list[dict]: + """Returns list of Stock Reservation Entries against a Voucher.""" + if not fields or not isinstance(fields, list): fields = [ "name", @@ -165,30 +178,11 @@ def get_stock_reservation_entries_for_voucher( return query.run(as_dict=True) -def get_sre_reserved_qty_details_for_voucher_detail_no( - voucher_type: str, voucher_no: str, voucher_detail_no: str -) -> list: - sre = frappe.qb.DocType("Stock Reservation Entry") - reserved_qty_details = ( - frappe.qb.from_(sre) - .select(sre.warehouse, (Sum(sre.reserved_qty) - Sum(sre.delivered_qty)).as_("reserved_qty")) - .where( - (sre.docstatus == 1) - & (sre.voucher_type == voucher_type) - & (sre.voucher_no == voucher_no) - & (sre.voucher_detail_no == voucher_detail_no) - & (sre.status.notin(["Delivered", "Cancelled"])) - ) - .groupby(sre.warehouse) - ).run(as_list=True) +def get_sre_reserved_qty_details_for_item_and_warehouse( + item_code: str | list, warehouse: str | list +) -> dict: + """Returns a dict like {("item_code", "warehouse"): "reserved_qty", ... }.""" - if reserved_qty_details: - return reserved_qty_details[0] - - return reserved_qty_details - - -def get_sre_reserved_qty_details(item_code: str | list, warehouse: str | list) -> dict: sre_details = {} if item_code and warehouse: @@ -220,8 +214,69 @@ def get_sre_reserved_qty_details(item_code: str | list, warehouse: str | list) - return sre_details -@frappe.whitelist() +def get_sre_reserved_qty_details_for_voucher( + voucher_type: str, voucher_no: str, voucher_detail_no: str = None +) -> dict: + """Returns a dict like {("voucher_detail_no", "warehouse"): "reserved_qty", ... }.""" + + reserved_qty_details = {} + + sre = frappe.qb.DocType("Stock Reservation Entry") + query = ( + frappe.qb.from_(sre) + .select( + sre.voucher_detail_no, + sre.warehouse, + (Sum(sre.reserved_qty) - Sum(sre.delivered_qty)).as_("reserved_qty"), + ) + .where( + (sre.docstatus == 1) + & (sre.voucher_type == voucher_type) + & (sre.voucher_no == voucher_no) + & (sre.status.notin(["Delivered", "Cancelled"])) + ) + .groupby(sre.voucher_detail_no, sre.warehouse) + ) + + if voucher_detail_no: + query = query.where(sre.voucher_detail_no == voucher_detail_no) + + data = query.run(as_dict=True) + + for d in data: + reserved_qty_details[(d["voucher_detail_no"], d["warehouse"])] = d["reserved_qty"] + + return reserved_qty_details + + +def get_sre_reserved_qty_details_for_voucher_detail_no( + voucher_type: str, voucher_no: str, voucher_detail_no: str +) -> list: + """Returns a list like ["warehouse", "reserved_qty"].""" + + sre = frappe.qb.DocType("Stock Reservation Entry") + reserved_qty_details = ( + frappe.qb.from_(sre) + .select(sre.warehouse, (Sum(sre.reserved_qty) - Sum(sre.delivered_qty))) + .where( + (sre.docstatus == 1) + & (sre.voucher_type == voucher_type) + & (sre.voucher_no == voucher_no) + & (sre.voucher_detail_no == voucher_detail_no) + & (sre.status.notin(["Delivered", "Cancelled"])) + ) + .groupby(sre.warehouse) + ).run(as_list=True) + + if reserved_qty_details: + return reserved_qty_details[0] + + return reserved_qty_details + + def has_reserved_stock(voucher_type: str, voucher_no: str, voucher_detail_no: str = None) -> bool: + """Returns True if there is any Stock Reservation Entry for the given voucher.""" + if get_stock_reservation_entries_for_voucher( voucher_type, voucher_no, voucher_detail_no, fields=["name"] ): @@ -230,94 +285,12 @@ def has_reserved_stock(voucher_type: str, voucher_no: str, voucher_detail_no: st return False -@frappe.whitelist() -def reserve_stock_against_sales_order(sales_order: object | str) -> None: - if isinstance(sales_order, str): - sales_order = frappe.get_doc("Sales Order", sales_order) - - validate_stock_reservation_settings(sales_order) - - sre_count = 0 - for item in sales_order.get("items"): - if not item.get("reserve_stock"): - continue - - reserved_qty_details = get_sre_reserved_qty_details_for_voucher_detail_no( - "Sales Order", sales_order.name, item.name - ) - - existing_reserved_qty = 0.0 - if reserved_qty_details: - existing_reserved_qty = reserved_qty_details[1] - - unreserved_qty = ( - item.stock_qty - - flt(item.delivered_qty) * item.get("conversion_factor", 1) - - existing_reserved_qty - ) - - if unreserved_qty <= 0: - frappe.msgprint( - _("Row #{0}: Stock is already reserved for the Item {1}").format( - item.idx, frappe.bold(item.item_code) - ), - title=_("Stock Reservation"), - ) - continue - - available_qty_to_reserve = get_available_qty_to_reserve(item.item_code, item.warehouse) - - if available_qty_to_reserve <= 0: - frappe.msgprint( - _("Row #{0}: No available stock to reserve for the Item {1}").format( - item.idx, frappe.bold(item.item_code) - ), - title=_("Stock Reservation"), - indicator="orange", - ) - continue - - qty_to_be_reserved = min(unreserved_qty, available_qty_to_reserve) - - if qty_to_be_reserved < unreserved_qty: - frappe.msgprint( - _("Row #{0}: Only {1} available to reserve for the Item {2}").format( - item.idx, - frappe.bold(str(qty_to_be_reserved / item.conversion_factor) + " " + item.uom), - frappe.bold(item.item_code), - ), - title=_("Stock Reservation"), - indicator="orange", - ) - - if not frappe.db.get_single_value("Stock Settings", "allow_partial_reservation"): - continue - - sre = frappe.new_doc("Stock Reservation Entry") - sre.item_code = item.item_code - sre.warehouse = item.warehouse - 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 = sales_order.company - sre.stock_uom = item.stock_uom - sre.project = sales_order.project - sre.save() - sre.submit() - - sre_count += 1 - - if sre_count: - frappe.msgprint(_("Stock Reservation Entry created"), alert=True, indicator="green") - - @frappe.whitelist() def cancel_stock_reservation_entries( - voucher_type: str, voucher_no: str, voucher_detail_no: str = None + voucher_type: str, voucher_no: str, voucher_detail_no: str = None, notify: bool = True ) -> None: + """Cancel Stock Reservation Entries for the given voucher.""" + sre_list = get_stock_reservation_entries_for_voucher( voucher_type, voucher_no, voucher_detail_no, fields=["name"] ) @@ -326,4 +299,5 @@ def cancel_stock_reservation_entries( for sre in sre_list: frappe.get_doc("Stock Reservation Entry", sre.name).cancel() - frappe.msgprint(_("Stock Reservation Entry cancelled"), alert=True, indicator="red") + if notify: + frappe.msgprint(_("Stock Reservation Entries Cancelled"), alert=True, indicator="red") diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index 170dcb1aab..9ce5e9f599 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -352,6 +352,7 @@ }, { "default": "1", + "description": "Allows to create Stock Reservations against Sales Order", "fieldname": "enable_stock_reservation", "fieldtype": "Check", "label": "Enable Stock Reservation" @@ -359,6 +360,7 @@ { "default": "0", "depends_on": "eval: doc.enable_stock_reservation", + "description": "If enabled, Stock Reservation Entries will be created on submission of Sales Order", "fieldname": "reserve_stock_on_sales_order_submission", "fieldtype": "Check", "label": "Reserve Stock on Sales Order Submission" @@ -370,6 +372,7 @@ { "default": "1", "depends_on": "eval: doc.enable_stock_reservation", + "description": "If enabled, Partial Stock Reservation Entries can be created. For example, If you have a Sales Order of 100 units and the Available Stock is 90 units then a Stock Reservation Entry will be created for 90 units. ", "fieldname": "allow_partial_reservation", "fieldtype": "Check", "label": "Allow Partial Reservation" @@ -380,7 +383,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-04-01 15:52:28.717324", + "modified": "2023-04-04 22:46:42.287425", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py index d761b663f5..f041e796d6 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/stock_settings.py @@ -101,6 +101,8 @@ class StockSettings(Document): check_pending_reposting(self.stock_frozen_upto) def cant_disable_stock_reservation(self): + """Raises an exception if user tries to disable Stock Reservation and there are existing Stock Reservation Entries.""" + if not self.enable_stock_reservation: db_enable_stock_reservation = frappe.db.get_single_value( "Stock Settings", "enable_stock_reservation" diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index b8d6b6c7ce..a6a630fbf5 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -398,8 +398,10 @@ def get_item_warehouse_map(filters: StockBalanceFilter, sle: List[SLEntry]): def get_sre_reserved_qty_details(iwb_map: list) -> dict: + """Returns a dict like {("item_code", "warehouse"): "reserved_qty", ... }.""" + from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( - get_sre_reserved_qty_details as get_reserved_qty_details, + get_sre_reserved_qty_details_for_item_and_warehouse as get_reserved_qty_details, ) item_code_list, warehouse_list = [], []