From 31319cb6ee3d106be7d36cc2f3560d78d2801a87 Mon Sep 17 00:00:00 2001 From: Nishka Gosalia <58264710+nishkagosalia@users.noreply.github.com> Date: Wed, 8 Apr 2026 17:20:09 +0530 Subject: [PATCH 1/9] fix: quality inspection item code fetch perm issue (#54121) Co-authored-by: Mihir Kandoi --- erpnext/stock/doctype/quality_inspection/quality_inspection.js | 1 + erpnext/stock/doctype/quality_inspection/quality_inspection.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.js b/erpnext/stock/doctype/quality_inspection/quality_inspection.js index 69bc03a8bd4..8d5764d5697 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.js +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.js @@ -58,6 +58,7 @@ frappe.ui.form.on("Quality Inspection", { if (doc.reference_type && doc.reference_name) { let filters = { from: doctype, + parent_doctype: doc.reference_type, inspection_type: doc.inspection_type, }; diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index 127164509a8..a29c9fca3c6 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -370,10 +370,11 @@ def item_query(doctype: Any, txt: str | None, searchfield: Any, start: int, page from frappe.desk.reportview import get_match_cond from_doctype = cstr(filters.get("from")) + parent_doctype = cstr(filters.get("parent_doctype")) if not from_doctype or not frappe.db.exists("DocType", from_doctype): return [] - mcond = get_match_cond(from_doctype) + mcond = get_match_cond(parent_doctype or from_doctype) cond, qi_condition = "", "and (quality_inspection is null or quality_inspection = '')" if filters.get("parent"): From 9d16d06504684596abc5a09ea616deba370fe998 Mon Sep 17 00:00:00 2001 From: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com> Date: Wed, 8 Apr 2026 17:20:49 +0530 Subject: [PATCH 2/9] fix(manufacturing): check remaining qty to calculate operating cost (#53983) --- erpnext/stock/doctype/stock_entry/stock_entry.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 72205ea4cd8..8c7c896c94f 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -3932,9 +3932,12 @@ def get_operating_cost_per_unit(work_order=None, bom_no=None): for d in work_order.get("operations"): if flt(d.completed_qty): - operating_cost_per_unit += flt( - d.actual_operating_cost - get_consumed_operating_cost(work_order.name, bom_no) - ) / flt(d.completed_qty - work_order.produced_qty) + if not (remaining_qty := flt(d.completed_qty - work_order.produced_qty)): + continue + operating_cost_per_unit += ( + flt(d.actual_operating_cost - get_consumed_operating_cost(work_order.name, bom_no)) + / remaining_qty + ) elif work_order.qty: operating_cost_per_unit += flt(d.planned_operating_cost) / flt(work_order.qty) From 6e44b8913e95ecb9b95a5c4e71bb2ae542b24352 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 8 Apr 2026 19:09:56 +0530 Subject: [PATCH 3/9] fix: inventory dimensions should not be mandatory unnecesarily (#54064) --- erpnext/patches.txt | 3 +- .../v16_0/depends_on_inv_dimensions.py | 70 +++++++++++++++++++ .../inventory_dimension.json | 12 +--- .../inventory_dimension.py | 16 +++-- .../test_inventory_dimension.py | 6 +- 5 files changed, 87 insertions(+), 20 deletions(-) create mode 100644 erpnext/patches/v16_0/depends_on_inv_dimensions.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index f6b48bea3ed..2540e83e26e 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -472,4 +472,5 @@ erpnext.patches.v16_0.complete_onboarding_steps_for_older_sites #2 erpnext.patches.v16_0.enable_serial_batch_setting erpnext.patches.v16_0.co_by_product_patch erpnext.patches.v16_0.update_requested_qty_packed_item -erpnext.patches.v16_0.remove_payables_receivables_workspace \ No newline at end of file +erpnext.patches.v16_0.remove_payables_receivables_workspace +erpnext.patches.v16_0.depends_on_inv_dimensions \ No newline at end of file diff --git a/erpnext/patches/v16_0/depends_on_inv_dimensions.py b/erpnext/patches/v16_0/depends_on_inv_dimensions.py new file mode 100644 index 00000000000..114e6e4b725 --- /dev/null +++ b/erpnext/patches/v16_0/depends_on_inv_dimensions.py @@ -0,0 +1,70 @@ +import frappe + + +def get_inventory_dimensions(): + return frappe.get_all( + "Inventory Dimension", + fields=[ + "target_fieldname as fieldname", + "source_fieldname", + "reference_document as doctype", + "reqd", + "mandatory_depends_on", + ], + order_by="creation", + distinct=True, + ) + + +def get_display_depends_on(doctype): + if doctype not in [ + "Stock Entry Detail", + "Sales Invoice Item", + "Delivery Note Item", + "Purchase Invoice Item", + "Purchase Receipt Item", + ]: + return + + display_depends_on = "" + + if doctype in ["Purchase Invoice Item", "Purchase Receipt Item"]: + display_depends_on = "eval:parent.is_internal_supplier == 1" + elif doctype != "Stock Entry Detail": + display_depends_on = "eval:parent.is_internal_customer == 1" + elif doctype == "Stock Entry Detail": + display_depends_on = "eval:doc.t_warehouse" + + return display_depends_on + + +def execute(): + for dimension in get_inventory_dimensions(): + frappe.set_value( + "Custom Field", + {"fieldname": dimension.source_fieldname, "dt": "Stock Entry Detail"}, + "depends_on", + "eval:doc.s_warehouse", + ) + frappe.set_value( + "Custom Field", + {"fieldname": dimension.source_fieldname, "dt": "Stock Entry Detail", "reqd": 1}, + {"mandatory_depends_on": "eval:doc.s_warehouse", "reqd": 0}, + ) + frappe.set_value( + "Custom Field", + { + "fieldname": f"to_{dimension.fieldname}", + "dt": "Stock Entry Detail", + "depends_on": "eval:parent.purpose != 'Material Issue'", + }, + "depends_on", + "eval:doc.t_warehouse", + ) + if display_depends_on := get_display_depends_on(dimension.doctype): + frappe.set_value( + "Custom Field", + {"fieldname": dimension.fieldname, "dt": dimension.doctype}, + "mandatory_depends_on", + display_depends_on if dimension.reqd else dimension.mandatory_depends_on, + ) diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json index 376b09f9370..aae81a29eac 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json @@ -8,9 +8,8 @@ "field_order": [ "dimension_details_tab", "dimension_name", - "reference_document", "column_break_4", - "disabled", + "reference_document", "field_mapping_section", "source_fieldname", "column_break_9", @@ -93,12 +92,6 @@ "fieldtype": "Check", "label": "Apply to All Inventory Documents" }, - { - "default": "0", - "fieldname": "disabled", - "fieldtype": "Check", - "label": "Disabled" - }, { "fieldname": "target_fieldname", "fieldtype": "Data", @@ -159,6 +152,7 @@ "label": "Conditional Rule Examples" }, { + "depends_on": "eval:!doc.apply_to_all_doctypes", "description": "To apply condition on parent field use parent.field_name and to apply condition on child table use doc.field_name. Here field_name could be based on the actual column name of the respective field.", "fieldname": "mandatory_depends_on", "fieldtype": "Small Text", @@ -188,7 +182,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2025-07-07 15:51:29.329064", + "modified": "2026-04-08 10:10:16.884388", "modified_by": "Administrator", "module": "Stock", "name": "Inventory Dimension", diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py index b48b0a5e21b..76023e19778 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py @@ -34,7 +34,6 @@ class InventoryDimension(Document): apply_to_all_doctypes: DF.Check condition: DF.Code | None dimension_name: DF.Data - disabled: DF.Check document_type: DF.Link | None fetch_from_parent: DF.Literal[None] istable: DF.Check @@ -78,7 +77,6 @@ class InventoryDimension(Document): old_doc = self._doc_before_save allow_to_edit_fields = [ - "disabled", "fetch_from_parent", "type_of_transaction", "condition", @@ -122,6 +120,7 @@ class InventoryDimension(Document): def reset_value(self): if self.apply_to_all_doctypes: self.type_of_transaction = "" + self.mandatory_depends_on = "" self.istable = 0 for field in ["document_type", "condition"]: @@ -186,8 +185,12 @@ class InventoryDimension(Document): label=_(label), depends_on="eval:doc.s_warehouse" if doctype == "Stock Entry Detail" else "", search_index=1, - reqd=self.reqd, - mandatory_depends_on=self.mandatory_depends_on, + reqd=1 + if self.reqd and not self.mandatory_depends_on and doctype != "Stock Entry Detail" + else 0, + mandatory_depends_on="eval:doc.s_warehouse" + if self.reqd and doctype == "Stock Entry Detail" + else self.mandatory_depends_on, ), ] @@ -298,12 +301,13 @@ class InventoryDimension(Document): options=self.reference_document, label=label, depends_on=display_depends_on, + mandatory_depends_on=display_depends_on if self.reqd else self.mandatory_depends_on, ), ] ) -def field_exists(doctype, fieldname) -> str or None: +def field_exists(doctype, fieldname) -> str | None: return frappe.db.get_value("DocField", {"parent": doctype, "fieldname": fieldname}, "name") @@ -379,7 +383,6 @@ def get_document_wise_inventory_dimensions(doctype) -> dict: "type_of_transaction", "fetch_from_parent", ], - filters={"disabled": 0}, or_filters={"document_type": doctype, "apply_to_all_doctypes": 1}, ) @@ -396,7 +399,6 @@ def get_inventory_dimensions(): "validate_negative_stock", "name as dimension_name", ], - filters={"disabled": 0}, order_by="creation", distinct=True, ) diff --git a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py index d12462eff27..2756b18cd48 100644 --- a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py @@ -210,9 +210,9 @@ class TestInventoryDimension(ERPNextTestSuite): doc = create_inventory_dimension( reference_document="Pallet", type_of_transaction="Outward", - dimension_name="Pallet", + dimension_name="Pallet 75", apply_to_all_doctypes=0, - document_type="Stock Entry Detail", + document_type="Delivery Note Item", ) doc.reqd = 1 @@ -220,7 +220,7 @@ class TestInventoryDimension(ERPNextTestSuite): self.assertTrue( frappe.db.get_value( - "Custom Field", {"fieldname": "pallet", "dt": "Stock Entry Detail", "reqd": 1}, "name" + "Custom Field", {"fieldname": "pallet_75", "dt": "Delivery Note Item", "reqd": 1}, "name" ) ) From ef454822d7bce5ffdce2ad645ae6322b20d2a291 Mon Sep 17 00:00:00 2001 From: NaviN <118178330+Navin-S-R@users.noreply.github.com> Date: Wed, 8 Apr 2026 20:42:58 +0530 Subject: [PATCH 4/9] fix(sales invoice): toggle Get Items From button based on is_return and POS view (#52594) --- .../doctype/sales_invoice/sales_invoice.js | 172 ++++++++++-------- erpnext/controllers/queries.py | 57 +++--- 2 files changed, 129 insertions(+), 100 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 64728cd1e0a..90c0da74f26 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -165,13 +165,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends ( ); } } - - // Show buttons only when pos view is active - if (cint(doc.docstatus == 0) && this.frm.page.current_view_name !== "pos" && !doc.is_return) { - this.frm.cscript.sales_order_btn(); - this.frm.cscript.delivery_note_btn(); - this.frm.cscript.quotation_btn(); - } + this.toggle_get_items(); this.set_default_print_format(); if (doc.docstatus == 1 && !doc.inter_company_invoice_reference) { @@ -260,6 +254,93 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends ( } } + toggle_get_items() { + const buttons = ["Sales Order", "Quotation", "Timesheet", "Delivery Note"]; + + buttons.forEach((label) => { + this.frm.remove_custom_button(label, "Get Items From"); + }); + + if (cint(this.frm.doc.docstatus) !== 0 || this.frm.page.current_view_name === "pos") { + return; + } + + if (!this.frm.doc.is_return) { + this.frm.cscript.sales_order_btn(); + this.frm.cscript.quotation_btn(); + this.frm.cscript.timesheet_btn(); + } + + this.frm.cscript.delivery_note_btn(); + } + + timesheet_btn() { + var me = this; + + me.frm.add_custom_button( + __("Timesheet"), + function () { + let d = new frappe.ui.Dialog({ + title: __("Fetch Timesheet"), + fields: [ + { + label: __("From"), + fieldname: "from_time", + fieldtype: "Date", + reqd: 1, + }, + { + label: __("Item Code"), + fieldname: "item_code", + fieldtype: "Link", + options: "Item", + get_query: () => { + return { + query: "erpnext.controllers.queries.item_query", + filters: { + is_sales_item: 1, + customer: me.frm.doc.customer, + has_variants: 0, + }, + }; + }, + }, + { + fieldtype: "Column Break", + fieldname: "col_break_1", + }, + { + label: __("To"), + fieldname: "to_time", + fieldtype: "Date", + reqd: 1, + }, + { + label: __("Project"), + fieldname: "project", + fieldtype: "Link", + options: "Project", + default: me.frm.doc.project, + }, + ], + primary_action: function () { + const data = d.get_values(); + me.frm.events.add_timesheet_data(me.frm, { + from_time: data.from_time, + to_time: data.to_time, + project: data.project, + item_code: data.item_code, + }); + d.hide(); + }, + primary_action_label: __("Get Timesheets"), + }); + d.show(); + }, + __("Get Items From") + ); + } + sales_order_btn() { var me = this; @@ -331,6 +412,12 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends ( this.$delivery_note_btn = this.frm.add_custom_button( __("Delivery Note"), function () { + if (!me.frm.doc.customer) { + frappe.throw({ + title: __("Mandatory"), + message: __("Please Select a Customer"), + }); + } erpnext.utils.map_current_doc({ method: "erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice", source_doctype: "Delivery Note", @@ -343,7 +430,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends ( var filters = { docstatus: 1, company: me.frm.doc.company, - is_return: 0, + is_return: me.frm.doc.is_return, }; if (me.frm.doc.customer) filters["customer"] = me.frm.doc.customer; return { @@ -610,6 +697,10 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends ( apply_tds(frm) { this.frm.clear_table("tax_withholding_entries"); } + + is_return() { + this.toggle_get_items(); + } }; // for backward compatibility: combine new and previous states @@ -1061,71 +1152,6 @@ frappe.ui.form.on("Sales Invoice", { }, refresh: function (frm) { - if (frm.doc.docstatus === 0 && !frm.doc.is_return) { - frm.add_custom_button( - __("Timesheet"), - function () { - let d = new frappe.ui.Dialog({ - title: __("Fetch Timesheet"), - fields: [ - { - label: __("From"), - fieldname: "from_time", - fieldtype: "Date", - reqd: 1, - }, - { - label: __("Item Code"), - fieldname: "item_code", - fieldtype: "Link", - options: "Item", - get_query: () => { - return { - query: "erpnext.controllers.queries.item_query", - filters: { - is_sales_item: 1, - customer: frm.doc.customer, - has_variants: 0, - }, - }; - }, - }, - { - fieldtype: "Column Break", - fieldname: "col_break_1", - }, - { - label: __("To"), - fieldname: "to_time", - fieldtype: "Date", - reqd: 1, - }, - { - label: __("Project"), - fieldname: "project", - fieldtype: "Link", - options: "Project", - default: frm.doc.project, - }, - ], - primary_action: function () { - const data = d.get_values(); - frm.events.add_timesheet_data(frm, { - from_time: data.from_time, - to_time: data.to_time, - project: data.project, - item_code: data.item_code, - }); - d.hide(); - }, - primary_action_label: __("Get Timesheets"), - }); - d.show(); - }, - __("Get Items From") - ); - } - if (frm.doc.is_debit_note) { frm.set_df_property("return_against", "label", __("Adjustment Against")); } diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 753cc7e2db1..109f1bc9b64 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -379,39 +379,42 @@ def get_project_name( @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_delivery_notes_to_be_billed( - doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict, as_dict: bool + doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict, as_dict: bool = False ): - doctype = "Delivery Note" + DeliveryNote = frappe.qb.DocType("Delivery Note") + fields = get_fields(doctype, ["name", "customer", "posting_date"]) - return frappe.db.sql( - """ - select {fields} - from `tabDelivery Note` - where `tabDelivery Note`.`{key}` like {txt} and - `tabDelivery Note`.docstatus = 1 - and status not in ('Stopped', 'Closed') {fcond} - and ( - (`tabDelivery Note`.is_return = 0 and `tabDelivery Note`.per_billed < 100) - or (`tabDelivery Note`.grand_total = 0 and `tabDelivery Note`.per_billed < 100) - or ( - `tabDelivery Note`.is_return = 1 - and return_against in (select name from `tabDelivery Note` where per_billed < 100) + original_dn = ( + frappe.qb.from_(DeliveryNote) + .select(DeliveryNote.name) + .where((DeliveryNote.docstatus == 1) & (DeliveryNote.is_return == 0) & (DeliveryNote.per_billed > 0)) + ) + + query = ( + frappe.qb.from_(DeliveryNote) + .select(*[DeliveryNote[f] for f in fields]) + .where( + (DeliveryNote.docstatus == 1) + & (DeliveryNote.status.notin(["Stopped", "Closed"])) + & (DeliveryNote[searchfield].like(f"%{txt}%")) + & ( + ((DeliveryNote.is_return == 0) & (DeliveryNote.per_billed < 100)) + | ((DeliveryNote.grand_total == 0) & (DeliveryNote.per_billed < 100)) + | ( + (DeliveryNote.is_return == 1) + & (DeliveryNote.per_billed < 100) + & (DeliveryNote.return_against.isin(original_dn)) ) ) - {mcond} order by `tabDelivery Note`.`{key}` asc limit {page_len} offset {start} - """.format( - fields=", ".join([f"`tabDelivery Note`.{f}" for f in fields]), - key=searchfield, - fcond=get_filters_cond(doctype, filters, []), - mcond=get_match_cond(doctype), - start=start, - page_len=page_len, - txt="%(txt)s", - ), - {"txt": ("%%%s%%" % txt)}, - as_dict=as_dict, + ) ) + if filters and isinstance(filters, dict): + for key, value in filters.items(): + query = query.where(DeliveryNote[key] == value) + + query = query.orderby(DeliveryNote[searchfield], order=Order.asc).limit(page_len).offset(start) + return query.run(as_dict=as_dict) @frappe.whitelist() From 38ed425ee299e70d8c22b759899f250fd5429393 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 8 Apr 2026 18:22:08 +0530 Subject: [PATCH 5/9] fix: last SLE not updated in the file --- .../doctype/work_order/test_work_order.py | 18 +++++- erpnext/stock/stock_ledger.py | 59 ++++++++++--------- 2 files changed, 48 insertions(+), 29 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 8a13ed11fe2..b2a1eba0232 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -518,9 +518,23 @@ class TestWorkOrder(ERPNextTestSuite): do_not_save=True, ) + operation_name = "_Test Custom Operation" + workstation_name = "_Test Custom Workstation" + + if not frappe.db.exists("Workstation", workstation_name): + doc = frappe.new_doc("Workstation") + doc.workstation_name = workstation_name + doc.save() + + if not frappe.db.exists("Operation", operation_name): + doc = frappe.new_doc("Operation") + doc.name = operation_name + doc.workstation = workstation_name + doc.save() + operation = { - "operation": "_Test Operation 1", - "workstation": "_Test Workstation 1", + "operation": operation_name, + "workstation": workstation_name, "description": "Test Data", "operating_cost": 100, "time_in_mins": 40, diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 9533f5bd03b..93cf9c0bc2a 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -267,17 +267,11 @@ def update_args_in_repost_item_valuation( items_to_be_repost, repost_affected_transaction, item_wh_wise_last_posted_sle=None, - only_affected_transaction=False, ): file_name = "" - has_file = False - if not item_wh_wise_last_posted_sle: item_wh_wise_last_posted_sle = {} - if doc.reposting_data_file: - has_file = True - if doc.reposting_data_file: file_name = get_reposting_file_name(doc.doctype, doc.name) # frappe.delete_doc("File", file_name, ignore_permissions=True, delete_permanently=True) @@ -292,15 +286,14 @@ def update_args_in_repost_item_valuation( file_name, ) - if not only_affected_transaction or not has_file: - doc.db_set( - { - "current_index": index, - "items_to_be_repost": frappe.as_json(items_to_be_repost), - "total_reposting_count": len(items_to_be_repost), - "reposting_data_file": doc.reposting_data_file, - } - ) + doc.db_set( + { + "current_index": index, + "items_to_be_repost": frappe.as_json(items_to_be_repost), + "total_reposting_count": len(items_to_be_repost), + "reposting_data_file": doc.reposting_data_file, + } + ) if not frappe.in_test: frappe.db.commit() @@ -584,13 +577,9 @@ class update_entries_after: self.update_bin() else: self.item_wh_wise_last_posted_sle = self.get_item_wh_wise_last_posted_sle() - _item_wh_sle = self.sort_sles(self.item_wh_wise_last_posted_sle.values()) - - while _item_wh_sle: - self.initialize_reposting() - sle_dict = _item_wh_sle.pop(0) - self.repost_stock_ledgers(sle_dict) - + item_wh_sles = self.sort_sles(self.item_wh_wise_last_posted_sle.values()) + self.initialize_reposting() + self.repost_stock_ledgers(item_wh_sles) self.update_bin() self.reset_vouchers_and_idx() self.update_data_in_repost() @@ -625,8 +614,19 @@ class update_entries_after: ) } - def repost_stock_ledgers(self, sle_dict=None): - self._sles = self.get_future_entries_to_repost(sle_dict) + def _get_future_entries_to_repost(self, item_wh_sles): + sles = [] + + for sle in item_wh_sles: + if (sle.item_code, sle.warehouse) not in self.distinct_dependant_item_wh: + self.distinct_dependant_item_wh.add((sle.item_code, sle.warehouse)) + + sles.extend(self.get_future_entries_to_repost(sle)) + + return self.sort_sles(sles) + + def repost_stock_ledgers(self, item_wh_sles=None): + self._sles = self._get_future_entries_to_repost(item_wh_sles) if not isinstance(self._sles, deque): self._sles = deque(self._sles) @@ -634,10 +634,13 @@ class update_entries_after: i = 0 while self._sles: sle = self._sles.popleft() - i += 1 + if (sle.item_code, sle.warehouse) not in self.distinct_dependant_item_wh: + self.distinct_dependant_item_wh.add((sle.item_code, sle.warehouse)) + if sle.name in self.distinct_sles: continue + i += 1 item_wh_key = (sle.item_code, sle.warehouse) if item_wh_key not in self.prev_sle_dict: self.prev_sle_dict[item_wh_key] = get_previous_sle_of_current_voucher(sle) @@ -651,7 +654,7 @@ class update_entries_after: self.include_dependant_sle_in_reposting(sle) self.update_item_wh_wise_last_posted_sle(sle) - if i % 1000 == 0: + if i % 2000 == 0: self.update_data_in_repost(len(self._sles), i) def sort_sles(self, sles): @@ -733,7 +736,6 @@ class update_entries_after: self.items_to_be_repost, self.repost_affected_transaction, self.item_wh_wise_last_posted_sle, - only_affected_transaction=True, ) if not frappe.in_test: @@ -990,6 +992,9 @@ class update_entries_after: ): return + if not cint(erpnext.is_perpetual_inventory_enabled(sle.company)): + return + if self.args.item_code != sle.item_code or self.args.warehouse != sle.warehouse: self.repost_affected_transaction.add((sle.voucher_type, sle.voucher_no)) From d15cd08e722662f25c594b6491874fa64dabd065 Mon Sep 17 00:00:00 2001 From: diptanilsaha Date: Wed, 8 Apr 2026 23:38:43 +0530 Subject: [PATCH 6/9] refactor(lost_opportunity_report): replaced raw_sql with query builder (#54136) --- .../lost_opportunity/lost_opportunity.py | 82 +++++++++---------- 1 file changed, 39 insertions(+), 43 deletions(-) diff --git a/erpnext/crm/report/lost_opportunity/lost_opportunity.py b/erpnext/crm/report/lost_opportunity/lost_opportunity.py index eb09711667a..cfbee3901e2 100644 --- a/erpnext/crm/report/lost_opportunity/lost_opportunity.py +++ b/erpnext/crm/report/lost_opportunity/lost_opportunity.py @@ -4,6 +4,12 @@ import frappe from frappe import _ +from frappe.query_builder import DocType +from frappe.query_builder.custom import GROUP_CONCAT +from frappe.query_builder.functions import Date + +Opportunity = DocType("Opportunity") +OpportunityLostReasonDetail = DocType("Opportunity Lost Reason Detail") def execute(filters=None): @@ -66,58 +72,48 @@ def get_columns(): def get_data(filters): - return frappe.db.sql( - f""" - SELECT - `tabOpportunity`.name, - `tabOpportunity`.opportunity_from, - `tabOpportunity`.party_name, - `tabOpportunity`.customer_name, - `tabOpportunity`.opportunity_type, - GROUP_CONCAT(`tabOpportunity Lost Reason Detail`.lost_reason separator ', ') lost_reason, - `tabOpportunity`.sales_stage, - `tabOpportunity`.territory - FROM - `tabOpportunity` - {get_join(filters)} - WHERE - `tabOpportunity`.status = 'Lost' and `tabOpportunity`.company = %(company)s - AND DATE(`tabOpportunity`.modified) BETWEEN %(from_date)s AND %(to_date)s - {get_conditions(filters)} - GROUP BY - `tabOpportunity`.name - ORDER BY - `tabOpportunity`.creation asc """, - filters, - as_dict=1, + query = ( + frappe.qb.from_(Opportunity) + .left_join(OpportunityLostReasonDetail) + .on( + (OpportunityLostReasonDetail.parenttype == "Opportunity") + & (OpportunityLostReasonDetail.parent == Opportunity.name) + ) + .select( + Opportunity.name, + Opportunity.opportunity_from, + Opportunity.party_name, + Opportunity.customer_name, + Opportunity.opportunity_type, + GROUP_CONCAT(OpportunityLostReasonDetail.lost_reason, alias="lost_reason").separator(", "), + Opportunity.sales_stage, + Opportunity.territory, + ) + .where( + (Opportunity.status == "Lost") + & (Opportunity.company == filters.get("company")) + & (Date(Opportunity.modified).between(filters.get("from_date"), filters.get("to_date"))) + ) + .groupby(Opportunity.name) + .orderby(Opportunity.creation) ) + query = get_conditions(filters, query) -def get_conditions(filters): - conditions = [] + return query.run(as_dict=1) + +def get_conditions(filters, query): if filters.get("territory"): - conditions.append(" and `tabOpportunity`.territory=%(territory)s") + query = query.where(Opportunity.territory == filters.get("territory")) if filters.get("opportunity_from"): - conditions.append(" and `tabOpportunity`.opportunity_from=%(opportunity_from)s") + query = query.where(Opportunity.opportunity_from == filters.get("opportunity_from")) if filters.get("party_name"): - conditions.append(" and `tabOpportunity`.party_name=%(party_name)s") - - return " ".join(conditions) if conditions else "" - - -def get_join(filters): - join = """LEFT JOIN `tabOpportunity Lost Reason Detail` - ON `tabOpportunity Lost Reason Detail`.parenttype = 'Opportunity' and - `tabOpportunity Lost Reason Detail`.parent = `tabOpportunity`.name""" + query = query.where(Opportunity.party_name == filters.get("party_name")) if filters.get("lost_reason"): - join = """JOIN `tabOpportunity Lost Reason Detail` - ON `tabOpportunity Lost Reason Detail`.parenttype = 'Opportunity' and - `tabOpportunity Lost Reason Detail`.parent = `tabOpportunity`.name and - `tabOpportunity Lost Reason Detail`.lost_reason = '{}' - """.format(filters.get("lost_reason")) + query = query.where(OpportunityLostReasonDetail.lost_reason == filters.get("lost_reason")) - return join + return query From 7ef48a966a8dc32fb963871a4dc2d6de38c86744 Mon Sep 17 00:00:00 2001 From: Nishka Gosalia <58264710+nishkagosalia@users.noreply.github.com> Date: Thu, 9 Apr 2026 07:11:45 +0530 Subject: [PATCH 7/9] feat: Allowing operation level quality inspection check in BOM (#53859) Co-authored-by: Mihir Kandoi --- .../doctype/bom_operation/bom_operation.json | 10 ++++++++- .../doctype/bom_operation/bom_operation.py | 1 + .../doctype/job_card/job_card.py | 22 +++++++++---------- .../doctype/job_card/test_job_card.py | 11 ++++++++++ .../doctype/work_order/work_order.py | 1 + .../work_order_operation.json | 9 +++++++- .../work_order_operation.py | 1 + 7 files changed, 42 insertions(+), 13 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json index 11c704649a3..ad47d4024b4 100644 --- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json +++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json @@ -20,6 +20,7 @@ "is_subcontracted", "is_final_finished_good", "set_cost_based_on_bom_qty", + "quality_inspection_required", "warehouse_section", "skip_material_transfer", "backflush_from_wip_warehouse", @@ -290,13 +291,20 @@ "fieldname": "backflush_from_wip_warehouse", "fieldtype": "Check", "label": "Backflush Materials From WIP Warehouse" + }, + { + "default": "0", + "depends_on": "eval:parent.inspection_required", + "fieldname": "quality_inspection_required", + "fieldtype": "Check", + "label": "Quality Inspection Required" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2026-03-31 17:09:48.771834", + "modified": "2026-04-01 17:09:48.771834", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Operation", diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.py b/erpnext/manufacturing/doctype/bom_operation/bom_operation.py index fd197e89e62..72d7f194fd8 100644 --- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.py +++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.py @@ -35,6 +35,7 @@ class BOMOperation(Document): parent: DF.Data parentfield: DF.Data parenttype: DF.Data + quality_inspection_required: DF.Check sequence_id: DF.Int set_cost_based_on_bom_qty: DF.Check skip_material_transfer: DF.Check diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index f95905facd1..b3d4301addf 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -789,27 +789,27 @@ class JobCard(Document): ["action_if_quality_inspection_is_not_submitted", "action_if_quality_inspection_is_rejected"], ) - item = self.finished_good or self.production_item - bom_inspection_required = frappe.db.get_value( - "BOM", self.semi_fg_bom or self.bom_no, "inspection_required" + bom_inspection_required = frappe.get_value("BOM", self.bom_no, "inspection_required") + operation_inspection_required = frappe.get_value( + "Work Order Operation", self.operation_id, "quality_inspection_required" ) - if bom_inspection_required: + if bom_inspection_required and operation_inspection_required: if not self.quality_inspection: frappe.throw( _( "Quality Inspection is required for the item {0} before completing the job card {1}" - ).format(get_link_to_form("Item", item), bold(self.name)) + ).format(get_link_to_form("Item", self.finished_good), bold(self.name)) ) - qa_status, docstatus = frappe.db.get_value( + + qa_status, docstatus = frappe.get_value( "Quality Inspection", self.quality_inspection, ["status", "docstatus"] ) - if docstatus != 1: if action_submit == "Stop": frappe.throw( _("Quality Inspection {0} is not submitted for the item: {1}").format( get_link_to_form("Quality Inspection", self.quality_inspection), - get_link_to_form("Item", item), + get_link_to_form("Item", self.finished_good), ), title=_("Inspection Submission"), exc=QualityInspectionNotSubmittedError, @@ -818,7 +818,7 @@ class JobCard(Document): frappe.msgprint( _("Quality Inspection {0} is not submitted for the item: {1}").format( get_link_to_form("Quality Inspection", self.quality_inspection), - get_link_to_form("Item", item), + get_link_to_form("Item", self.finished_good), ), alert=True, indicator="orange", @@ -828,7 +828,7 @@ class JobCard(Document): frappe.throw( _("Quality Inspection {0} is rejected for the item: {1}").format( get_link_to_form("Quality Inspection", self.quality_inspection), - get_link_to_form("Item", item), + get_link_to_form("Item", self.finished_good), ), title=_("Inspection Rejected"), exc=QualityInspectionRejectedError, @@ -837,7 +837,7 @@ class JobCard(Document): frappe.msgprint( _("Quality Inspection {0} is rejected for the item: {1}").format( get_link_to_form("Quality Inspection", self.quality_inspection), - get_link_to_form("Item", item), + get_link_to_form("Item", self.finished_good), ), alert=True, indicator="orange", diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index ed0d84c36e5..a6ac3f0a79a 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -87,6 +87,7 @@ class TestJobCard(ERPNextTestSuite): with_operations=1, track_semi_finished_goods=1, company="_Test Company", + inspection_required=1, ) final_bom.append("items", {"item_code": raw.name, "qty": 1}) final_bom.append( @@ -97,6 +98,7 @@ class TestJobCard(ERPNextTestSuite): "bom_no": cut_bom, "skip_material_transfer": 1, "time_in_mins": 60, + "quality_inspection_required": 1, }, ) final_bom.append( @@ -134,6 +136,15 @@ class TestJobCard(ERPNextTestSuite): work_order.submit() job_card = frappe.get_all("Job Card", filters={"work_order": work_order.name, "operation": "Cutting"}) job_card_doc = frappe.get_doc("Job Card", job_card[0].name) + job_card_doc.append( + "time_logs", + { + "from_time": "2024-01-01 08:00:00", + "to_time": "2024-01-01 09:00:00", + "time_in_mins": 60, + "completed_qty": 1, + }, + ) self.assertRaises(frappe.ValidationError, job_card_doc.submit) def test_job_card_operations(self): diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index cae1c2b0c7f..488f00aa9ac 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -1277,6 +1277,7 @@ class WorkOrder(Document): "skip_material_transfer", "backflush_from_wip_warehouse", "set_cost_based_on_bom_qty", + "quality_inspection_required", ], order_by="idx", ) diff --git a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json index 6cbcc855d01..89ed830116b 100644 --- a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json +++ b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json @@ -15,6 +15,7 @@ "workstation_type", "workstation", "sequence_id", + "quality_inspection_required", "section_break_insy", "bom_no", "finished_good", @@ -294,13 +295,19 @@ "fieldtype": "Check", "label": "Backflush Materials From WIP Warehouse", "read_only": 1 + }, + { + "default": "0", + "fieldname": "quality_inspection_required", + "fieldtype": "Check", + "label": "Quality Inspection Required" } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-05-15 15:10:06.885440", + "modified": "2026-03-30 17:20:08.874381", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order Operation", diff --git a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.py b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.py index fb8b3feb4dd..2e45434f94b 100644 --- a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.py +++ b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.py @@ -36,6 +36,7 @@ class WorkOrderOperation(Document): planned_operating_cost: DF.Currency planned_start_time: DF.Datetime | None process_loss_qty: DF.Float + quality_inspection_required: DF.Check sequence_id: DF.Int skip_material_transfer: DF.Check source_warehouse: DF.Link | None From 7f0751539b2fdb4937ba9f8b6472629b487666aa Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 9 Apr 2026 07:15:48 +0530 Subject: [PATCH 8/9] fix: inventory dimension patch (#54141) --- .../v16_0/depends_on_inv_dimensions.py | 49 +++++++++++++------ 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/erpnext/patches/v16_0/depends_on_inv_dimensions.py b/erpnext/patches/v16_0/depends_on_inv_dimensions.py index 114e6e4b725..3ee805df7ef 100644 --- a/erpnext/patches/v16_0/depends_on_inv_dimensions.py +++ b/erpnext/patches/v16_0/depends_on_inv_dimensions.py @@ -40,28 +40,45 @@ def get_display_depends_on(doctype): def execute(): for dimension in get_inventory_dimensions(): - frappe.set_value( - "Custom Field", - {"fieldname": dimension.source_fieldname, "dt": "Stock Entry Detail"}, - "depends_on", - "eval:doc.s_warehouse", - ) - frappe.set_value( - "Custom Field", - {"fieldname": dimension.source_fieldname, "dt": "Stock Entry Detail", "reqd": 1}, - {"mandatory_depends_on": "eval:doc.s_warehouse", "reqd": 0}, - ) - frappe.set_value( + if frappe.db.exists( + "Custom Field", {"fieldname": dimension.source_fieldname, "dt": "Stock Entry Detail"} + ): + frappe.set_value( + "Custom Field", + {"fieldname": dimension.source_fieldname, "dt": "Stock Entry Detail"}, + "depends_on", + "eval:doc.s_warehouse", + ) + if frappe.db.exists( + "Custom Field", {"fieldname": dimension.source_fieldname, "dt": "Stock Entry Detail", "reqd": 1} + ): + frappe.set_value( + "Custom Field", + {"fieldname": dimension.source_fieldname, "dt": "Stock Entry Detail", "reqd": 1}, + {"mandatory_depends_on": "eval:doc.s_warehouse", "reqd": 0}, + ) + if frappe.db.exists( "Custom Field", { "fieldname": f"to_{dimension.fieldname}", "dt": "Stock Entry Detail", "depends_on": "eval:parent.purpose != 'Material Issue'", }, - "depends_on", - "eval:doc.t_warehouse", - ) - if display_depends_on := get_display_depends_on(dimension.doctype): + ): + frappe.set_value( + "Custom Field", + { + "fieldname": f"to_{dimension.fieldname}", + "dt": "Stock Entry Detail", + "depends_on": "eval:parent.purpose != 'Material Issue'", + }, + "depends_on", + "eval:doc.t_warehouse", + ) + if (display_depends_on := get_display_depends_on(dimension.doctype)) and frappe.db.exists( + "Custom Field", + {"fieldname": dimension.fieldname, "dt": dimension.doctype}, + ): frappe.set_value( "Custom Field", {"fieldname": dimension.fieldname, "dt": dimension.doctype}, From 71a17cfda907ff027b33868d0b46289a5f8acc4c Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 9 Apr 2026 07:56:35 +0530 Subject: [PATCH 9/9] fix: inventory dimension patch (#54147) --- .../patches/v16_0/depends_on_inv_dimensions.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/erpnext/patches/v16_0/depends_on_inv_dimensions.py b/erpnext/patches/v16_0/depends_on_inv_dimensions.py index 3ee805df7ef..0de46f68f11 100644 --- a/erpnext/patches/v16_0/depends_on_inv_dimensions.py +++ b/erpnext/patches/v16_0/depends_on_inv_dimensions.py @@ -16,7 +16,7 @@ def get_inventory_dimensions(): ) -def get_display_depends_on(doctype): +def get_display_depends_on(doctype, fieldname): if doctype not in [ "Stock Entry Detail", "Sales Invoice Item", @@ -24,18 +24,20 @@ def get_display_depends_on(doctype): "Purchase Invoice Item", "Purchase Receipt Item", ]: - return + return None, None + fieldname_start_with = "to" display_depends_on = "" if doctype in ["Purchase Invoice Item", "Purchase Receipt Item"]: display_depends_on = "eval:parent.is_internal_supplier == 1" + fieldname_start_with = "from" elif doctype != "Stock Entry Detail": display_depends_on = "eval:parent.is_internal_customer == 1" elif doctype == "Stock Entry Detail": display_depends_on = "eval:doc.t_warehouse" - return display_depends_on + return f"{fieldname_start_with}_{fieldname}", display_depends_on def execute(): @@ -75,13 +77,13 @@ def execute(): "depends_on", "eval:doc.t_warehouse", ) - if (display_depends_on := get_display_depends_on(dimension.doctype)) and frappe.db.exists( - "Custom Field", - {"fieldname": dimension.fieldname, "dt": dimension.doctype}, + fieldname, display_depends_on = get_display_depends_on(dimension.doctype, dimension.fieldname) + if display_depends_on and frappe.db.exists( + "Custom Field", {"fieldname": fieldname, "dt": dimension.doctype} ): frappe.set_value( "Custom Field", - {"fieldname": dimension.fieldname, "dt": dimension.doctype}, + {"fieldname": fieldname, "dt": dimension.doctype}, "mandatory_depends_on", display_depends_on if dimension.reqd else dimension.mandatory_depends_on, )