From 64497c922892d19ca378b5da75cf6b528f5e52d9 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 19 Oct 2023 14:48:13 +0530 Subject: [PATCH] feat: reserve stock for SO on PR submission --- .../doctype/sales_order/sales_order.py | 2 +- erpnext/stock/doctype/pick_list/pick_list.py | 2 +- .../purchase_receipt/purchase_receipt.py | 26 +++++++ .../stock_reservation_entry.py | 67 ++++++++++--------- 4 files changed, 62 insertions(+), 35 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index b91002eb86..d38216242e 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -541,7 +541,7 @@ class SalesOrder(SellingController): 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 2fcd1025a0..8c9d03c1bd 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -242,7 +242,7 @@ class PickList(Document): 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 de0db1aa8f..fc88dd8d5f 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -264,6 +264,7 @@ class PurchaseReceipt(BuyingController): 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 @@ class PurchaseReceipt(BuyingController): 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 936be3f73b..effad7df98 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 has_reserved_stock(voucher_type: str, voucher_no: str, voucher_detail_no: st 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 @@ def create_stock_reservation_entries_for_so_items( 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 @@ def create_stock_reservation_entries_for_so_items( 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 @@ def create_stock_reservation_entries_for_so_items( # 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 @@ def create_stock_reservation_entries_for_so_items( 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()