refactor: add Docstrings for functions
				
					
				
			This commit is contained in:
		| @@ -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 | ||||
|   | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -1641,17 +1641,20 @@ | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "description": "If checked, Stock Reservation Entries will be created on <b>Submit</b>", | ||||
|    "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", | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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", | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -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") | ||||
|   | ||||
| @@ -352,6 +352,7 @@ | ||||
|   }, | ||||
|   { | ||||
|    "default": "1", | ||||
|    "description": "Allows to create Stock Reservations against <b>Sales Order</b>", | ||||
|    "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, <b>Stock Reservation Entries</b> will be created on submission of <b>Sales Order</b>", | ||||
|    "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, <b>Partial Stock Reservation Entries</b> can be created. For example, If you have a <b>Sales Order</b> 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", | ||||
|   | ||||
| @@ -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" | ||||
|   | ||||
| @@ -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 = [], [] | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 s-aga-r
					s-aga-r