diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 403477b375..d492f718e5 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -622,42 +622,7 @@ def make_project(source_name, target_doc=None): @frappe.whitelist() def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False): - from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( - get_stock_reservation_entries_for_voucher, - has_reserved_stock, - ) - def set_missing_values(source, target): - if not target.items and has_reserved_stock("Sales Order", source_name): - sre_list = get_stock_reservation_entries_for_voucher("Sales Order", source_name) - sre_dict = {d.pop("voucher_detail_no"): d for d in sre_list} - - for item in source.get("items"): - if item.name in sre_dict: - reserved_qty, delivered_qty, warehouse = ( - sre_dict[item.name]["reserved_qty"], - sre_dict[item.name]["delivered_qty"], - sre_dict[item.name]["warehouse"], - ) - qty_to_deliver = (reserved_qty - delivered_qty) / item.conversion_factor - - row = frappe.new_doc("Delivery Note Item") - row.against_sales_order = source.name - row.against_sre = sre_dict[item.name]["name"] - row.so_detail = item.name - row.item_code = item.item_code - row.item_name = item.item_name - row.description = item.description - row.qty = qty_to_deliver - row.stock_uom = item.stock_uom - row.uom = item.uom - row.conversion_factor = item.conversion_factor - - if not frappe.get_cached_value("Warehouse", warehouse, "is_group"): - row.warehouse = warehouse - - target.append("items", row) - target.run_method("set_missing_values") target.run_method("set_po_nos") target.run_method("calculate_taxes_and_totals") @@ -686,9 +651,6 @@ def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False): or item_group.get("buying_cost_center") ) - if has_reserved_stock("Sales Order", source_name): - skip_item_mapping = True - mapper = { "Sales Order": {"doctype": "Delivery Note", "validation": {"docstatus": ["=", 1]}}, "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True}, diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index 53b35761b5..ae56645b73 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -77,19 +77,6 @@ frappe.ui.form.on("Delivery Note", { } }); - frm.set_query("against_sre", "items", (doc, cdt, cdn) => { - var row = locals[cdt][cdn]; - return { - filters: { - "docstatus": 1, - "status": ["not in", ["Delivered", "Cancelled"]], - "voucher_type": "Sales Order", - "voucher_no": row.against_sales_order, - "voucher_detail_no": row.so_detail, - } - } - }); - frm.set_df_property('packed_items', 'cannot_add_rows', true); frm.set_df_property('packed_items', 'cannot_delete_rows', true); }, diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index b8d2186c32..f45f8bd6df 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_sre() + self.validate_against_stock_reservation() self.reset_default_field_value("set_warehouse", "items", "warehouse") def validate_with_previous_doc(self): @@ -262,8 +262,6 @@ class DeliveryNote(SellingController): self.update_prevdoc_status() self.update_billing_status() - self.update_stock_reservation_entry() - # Updating stock ledger should always be called after updating prevdoc status, # because updating reserved qty in bin depends upon updated delivered qty in SO self.update_stock_ledger() @@ -275,105 +273,87 @@ class DeliveryNote(SellingController): self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation") def update_stock_reservation_entry(self): - if not self.is_return: - from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( - update_sre_delivered_qty, - ) - - for item in self.get("items"): - if item.against_sre: - update_sre_delivered_qty(item.doctype, item.against_sre) - - def validate_against_sre(self): - from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( - get_stock_reservation_entries_for_items, - has_reserved_stock, - ) - - sre_details = get_stock_reservation_entries_for_items(self.items) + if self.is_return or self._action != "submit": + return for item in self.items: - if item.against_sre: - sre = sre_details[item.against_sre] + if not item.against_sales_order or not item.so_detail: + continue - # SRE `docstatus` should be `1` (submitted) - if sre.docstatus == 0: - frappe.throw( - _("Row #{0}: Stock Reservation Entry {1} is not submitted").format( - item.idx, item.against_sre - ) - ) - elif sre.docstatus == 2: - frappe.throw( - _("Row #{0}: Stock Reservation Entry {0} is cancelled").format(item.idx, item.against_sre) - ) + sre_list = frappe.db.get_all( + "Stock Reservation Entry", + { + "docstatus": 1, + "voucher_type": "Sales Order", + "voucher_no": item.against_sales_order, + "voucher_detail_no": item.so_detail, + "warehouse": item.warehouse, + "status": ["not in", ["Delivered", "Cancelled"]], + }, + order_by="creation", + ) - # SRE `status` should not be `Delivered` - if sre.status == "Delivered": - frappe.throw( - _("Row #{0}: Cannot deliver more against Stock Reservation Entry {1}").format( - item.idx, item.against_sre - ) - ) + if not sre_list: + continue - if not frappe.get_cached_value("Warehouse", sre.warehouse, "is_group"): - if item.warehouse != sre.warehouse: + available_qty_to_deliver = item.stock_qty + for sre in sre_list: + if available_qty_to_deliver <= 0: + break + + sre_doc = frappe.get_doc("Stock Reservation Entry", sre) + 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() + sre_doc.update_status() + + available_qty_to_deliver -= qty_to_be_deliver + + def validate_against_stock_reservation(self): + from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( + get_sre_reserved_qty_details_for_voucher_detail_no, + ) + + if self.is_return: + return + + for item in self.items: + if not item.against_sales_order or not item.so_detail: + continue + + sre_data = get_sre_reserved_qty_details_for_voucher_detail_no( + "Sales Order", item.against_sales_order, item.so_detail + ) + + if not sre_data: + continue + + is_group_warehouse = frappe.get_cached_value("Warehouse", sre_data[0], "is_group") + + if not item.warehouse: + if not is_group_warehouse: + item.warehouse = sre_data[0] + else: + frappe.throw(_("Row #{0}: Warehouse is mandatory").format(item.idx, item.item_code)) + else: + if not is_group_warehouse: + if item.warehouse != sre_data[0]: frappe.throw( - _("Row #{0}: Warehouse {1} does not match with Stock Reservation Entry {2}").format( - item.idx, item.warehouse, item.against_sre - ) + _("Row #{0}: Stock is reserved for Warehouse {1}").format(item.idx, sre_data[0]), + title="Stock Reservation Warehouse Mismatch", ) else: from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses - warehouses = get_child_warehouses(sre.warehouse) - + warehouses = get_child_warehouses(sre_data[0]) if item.warehouse not in warehouses: frappe.throw( - _("Row #{0}: Warehouse {1} should be a child of Warehouse {2}").format( - item.idx, item.warehouse, sre.warehouse - ) + _( + "Row #{0}: Stock is reserved for Group Warehouse {1}, please select its child Warehouse" + ).format(item.idx, sre_data[0]), + title="Stock Reservation Group Warehouse", ) - for field in ( - "item_code", - ("against_sales_order", "voucher_no"), - ("so_detail", "voucher_detail_no"), - ): - item_field = sre_field = None - - if isinstance(field, tuple): - item_field, sre_field = field[0], field[1] - else: - item_field = sre_field = field - - if item.get(item_field) != sre.get(sre_field): - frappe.throw( - _("Row #{0}: {1} {2} does not match with Stock Reservation Entry {3}").format( - item.idx, - frappe.get_meta(item.doctype).get_label(item_field), - item.get(item_field), - item.against_sre, - ) - ) - - max_delivered_qty = (sre.reserved_qty - sre.delivered_qty) / item.conversion_factor - if item.qty > max_delivered_qty: - frappe.throw( - _("Row #{0}: Cannot deliver more than {1} {2} against Stock Reservation Entry {3}").format( - item.idx, max_delivered_qty, item.uom, item.against_sre - ) - ) - elif item.against_sales_order: - if not item.so_detail: - frappe.throw(_("Row #{0}: Sales Order Item reference is required").format(item.idx)) - elif has_reserved_stock("Sales Order", item.against_sales_order, item.so_detail): - frappe.throw( - _("Row #{0}: Cannot deliver against Sales Order {1} without Stock Reservation Entry").format( - item.idx, item.against_sales_order - ) - ) - def check_credit_limit(self): from erpnext.selling.doctype.customer.customer import check_credit_limit diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index faa7748c31..d3ed493714 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -76,7 +76,6 @@ "si_detail", "dn_detail", "pick_list_item", - "against_sre", "section_break_40", "batch_no", "serial_no", @@ -833,22 +832,13 @@ "fieldname": "material_request_item", "fieldtype": "Data", "label": "Material Request Item" - }, - { - "fieldname": "against_sre", - "fieldtype": "Link", - "label": "Against Stock Reservation Entry", - "no_copy": 1, - "options": "Stock Reservation Entry", - "print_hide": 1, - "read_only": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-03-26 16:53:08.283469", + "modified": "2023-03-30 23:27:30.943175", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", 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 c47049319d..c80ae577f3 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py +++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py @@ -152,6 +152,24 @@ 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") + return ( + 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)[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"] @@ -161,56 +179,6 @@ def has_reserved_stock(voucher_type: str, voucher_no: str, voucher_detail_no: st return False -def update_sre_delivered_qty( - doctype: str, sre_name: str, sre_field: str = "against_sre", qty_field: str = "stock_qty" -) -> None: - table = frappe.qb.DocType(doctype) - delivered_qty = ( - frappe.qb.from_(table) - .select(Sum(table[qty_field])) - .where((table.docstatus == 1) & (table[sre_field] == sre_name)) - ).run(as_list=True)[0][0] or 0.0 - - sre_doc = frappe.get_doc("Stock Reservation Entry", sre_name) - sre_doc.delivered_qty = delivered_qty - sre_doc.db_update() - sre_doc.update_status() - - -def get_stock_reservation_entries_for_items( - items: list[dict | object], sre_field: str = "against_sre" -) -> dict[dict]: - sre_details = {} - - if items: - sre_list = [item.get(sre_field) for item in items if item.get(sre_field)] - - if sre_list: - sre = frappe.qb.DocType("Stock Reservation Entry") - sre_data = ( - frappe.qb.from_(sre) - .select( - sre.name, - sre.status, - sre.docstatus, - sre.item_code, - sre.warehouse, - sre.voucher_type, - sre.voucher_no, - sre.voucher_detail_no, - sre.reserved_qty, - sre.delivered_qty, - sre.stock_uom, - ) - .where(sre.name.isin(sre_list)) - .orderby(sre.creation) - ).run(as_dict=True) - - sre_details = {d.name: d for d in sre_data} - - return sre_details - - def get_sre_reserved_qty_details(item_code: str | list, warehouse: str | list) -> dict: sre_details = {}