diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 42fdc5124bf..60c8e47f8f0 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -839,7 +839,7 @@ frappe.ui.form.on("Payment Entry", { paid_amount: function (frm) { frm.set_value("base_paid_amount", flt(frm.doc.paid_amount) * flt(frm.doc.source_exchange_rate)); let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; - if (frm.doc.paid_amount) { + if (!frm.doc.received_amount) { if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) { frm.set_value("received_amount", frm.doc.paid_amount); } else if (company_currency == frm.doc.paid_to_account_currency) { @@ -860,7 +860,7 @@ frappe.ui.form.on("Payment Entry", { flt(frm.doc.received_amount) * flt(frm.doc.target_exchange_rate) ); - if (frm.doc.received_amount) { + if (!frm.doc.paid_amount) { if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) { frm.set_value("paid_amount", frm.doc.received_amount); if (frm.doc.target_exchange_rate) { diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index 7acb36eca93..e059777dde2 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -731,7 +731,6 @@ "label": "Valuation Rate", "no_copy": 1, "options": "Company:company:default_currency", - "precision": "6", "print_hide": 1, "read_only": 1 }, @@ -984,7 +983,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2025-10-14 13:01:54.441511", + "modified": "2026-04-07 15:41:45.687554", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index aeb993e2399..b4b49828a58 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -166,13 +166,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends ( ); } } - - // Show buttons only when pos view is active - if (cint(doc.docstatus == 0) && cur_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) { @@ -258,6 +252,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; this.$sales_order_btn = this.frm.add_custom_button( @@ -322,6 +403,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", @@ -334,7 +421,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 { @@ -594,6 +681,14 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends ( this.calculate_taxes_and_totals(); } + + apply_tds(frm) { + this.frm.clear_table("tax_withholding_entries"); + } + + is_return() { + this.toggle_get_items(); + } }; // for backward compatibility: combine new and previous states @@ -1039,71 +1134,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/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index a8fb4ab93a0..147bb76064d 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2917,7 +2917,7 @@ class TestSalesInvoice(FrappeTestCase): si.submit() # Check if adjustment entry is created - self.assertTrue( + self.assertFalse( frappe.db.exists( "GL Entry", { diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.js b/erpnext/assets/doctype/asset_movement/asset_movement.js index f56c1e31f27..2e73d0d9df8 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.js +++ b/erpnext/assets/doctype/asset_movement/asset_movement.js @@ -41,7 +41,7 @@ frappe.ui.form.on("Asset Movement", { }); }, - onload: (frm) => { + refresh: (frm) => { frm.trigger("set_required_fields"); }, diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index e14a0265119..73ff7545ca5 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -283,7 +283,7 @@ class RequestforQuotation(BuyingController): } ) user.save(ignore_permissions=True) - update_password_link = user.reset_password() + update_password_link = user._reset_password() return user, update_password_link diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 9e072ee4017..dbfa6e7f9fb 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -356,38 +356,43 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs -def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len, filters, as_dict): - doctype = "Delivery Note" +def get_delivery_notes_to_be_billed( + doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict, as_dict: bool = False +): + 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() diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 20c8a72290b..d5bd3501527 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -616,11 +616,11 @@ class SellingController(StockController): if allow_at_arms_length_price: continue - rate = flt( - flt(d.incoming_rate, d.precision("incoming_rate")) * d.conversion_factor, - d.precision("rate"), - ) - if d.rate != rate: + rate = flt(flt(d.incoming_rate) * flt(d.conversion_factor or 1.0)) + + if flt(d.rate, d.precision("incoming_rate")) != flt( + rate, d.precision("incoming_rate") + ): d.rate = rate frappe.msgprint( _( diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index e75dd3dacd2..4b6fc4f4054 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -186,8 +186,11 @@ class calculate_taxes_and_totals: bill_for_rejected_quantity_in_purchase_invoice = frappe.get_single_value( "Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice" ) + + do_not_round_fields = ["valuation_rate", "incoming_rate"] + for item in self.doc.items: - self.doc.round_floats_in(item) + self.doc.round_floats_in(item, do_not_round_fields=do_not_round_fields) if item.discount_percentage == 100: item.rate = 0.0 diff --git a/erpnext/crm/report/lost_opportunity/lost_opportunity.py b/erpnext/crm/report/lost_opportunity/lost_opportunity.py index eb09711667a..1f826fe227a 100644 --- a/erpnext/crm/report/lost_opportunity/lost_opportunity.py +++ b/erpnext/crm/report/lost_opportunity/lost_opportunity.py @@ -117,7 +117,7 @@ def get_join(filters): 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")) + `tabOpportunity Lost Reason Detail`.lost_reason=%(lost_reason)s + """ return join diff --git a/erpnext/edi/doctype/code_list/code_list.py b/erpnext/edi/doctype/code_list/code_list.py index 8957c6565b9..e723157e7a0 100644 --- a/erpnext/edi/doctype/code_list/code_list.py +++ b/erpnext/edi/doctype/code_list/code_list.py @@ -5,6 +5,7 @@ from typing import TYPE_CHECKING import frappe from frappe.model.document import Document +from frappe.utils import escape_html if TYPE_CHECKING: from lxml.etree import Element @@ -63,14 +64,16 @@ class CodeList(Document): def from_genericode(self, root: "Element"): """Extract Code List details from genericode XML""" - self.title = root.find(".//Identification/ShortName").text + self.title = escape_html(root.find(".//Identification/ShortName").text) self.version = root.find(".//Identification/Version").text self.canonical_uri = root.find(".//CanonicalUri").text # optionals - self.description = getattr(root.find(".//Identification/LongName"), "text", None) - self.publisher = getattr(root.find(".//Identification/Agency/ShortName"), "text", None) + self.description = escape_html(getattr(root.find(".//Identification/LongName"), "text", None)) + self.publisher = escape_html(getattr(root.find(".//Identification/Agency/ShortName"), "text", None)) if not self.publisher: - self.publisher = getattr(root.find(".//Identification/Agency/LongName"), "text", None) + self.publisher = escape_html( + getattr(root.find(".//Identification/Agency/LongName"), "text", None) + ) self.publisher_id = getattr(root.find(".//Identification/Agency/Identifier"), "text", None) self.url = getattr(root.find(".//Identification/LocationUri"), "text", None) diff --git a/erpnext/edi/doctype/code_list/code_list_import.py b/erpnext/edi/doctype/code_list/code_list_import.py index ecabb256026..7368d3c012e 100644 --- a/erpnext/edi/doctype/code_list/code_list_import.py +++ b/erpnext/edi/doctype/code_list/code_list_import.py @@ -3,6 +3,7 @@ import json import frappe import requests from frappe import _ +from frappe.utils import escape_html from lxml import etree URL_PREFIXES = ("http://", "https://") @@ -32,7 +33,12 @@ def import_genericode(): content = f.read() # Parse the xml content - parser = etree.XMLParser(remove_blank_text=True) + parser = etree.XMLParser( + remove_blank_text=True, + resolve_entities=False, + load_dtd=False, + no_network=True, + ) try: root = etree.fromstring(content, parser=parser) except Exception as e: @@ -104,7 +110,7 @@ def get_genericode_columns_and_examples(root): # Get column names for column in root.findall(".//Column"): - column_id = column.get("Id") + column_id = escape_html(column.get("Id")) columns.append(column_id) example_values[column_id] = [] filterable_columns[column_id] = set() @@ -112,7 +118,7 @@ def get_genericode_columns_and_examples(root): # Get all values and count unique occurrences for row in root.findall(".//SimpleCodeList/Row"): for value in row.findall("Value"): - column_id = value.get("ColumnRef") + column_id = escape_html(value.get("ColumnRef")) if column_id not in columns: # Handle undeclared column columns.append(column_id) @@ -123,7 +129,7 @@ def get_genericode_columns_and_examples(root): if simple_value is None: continue - filterable_columns[column_id].add(simple_value.text) + filterable_columns[column_id].add(escape_html(simple_value.text)) # Get example values (up to 3) and filter columns with cardinality <= 5 for row in root.findall(".//SimpleCodeList/Row")[:3]: @@ -133,7 +139,7 @@ def get_genericode_columns_and_examples(root): if simple_value is None: continue - example_values[column_id].append(simple_value.text) + example_values[column_id].append(escape_html(simple_value.text)) filterable_columns = {k: list(v) for k, v in filterable_columns.items() if len(v) <= 5} diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index ed1628ded5f..3895f0bf17a 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -760,6 +760,8 @@ frappe.ui.form.on("BOM Item", "sourced_by_supplier", function (frm, cdt, cdn) { if (d.sourced_by_supplier) { d.rate = 0; refresh_field("rate", d.name, d.parentfield); + } else { + get_bom_material_detail(frm.doc, cdt, cdn, false); } }); diff --git a/erpnext/manufacturing/doctype/sub_operation/sub_operation.json b/erpnext/manufacturing/doctype/sub_operation/sub_operation.json index c7530e41aca..36160fec699 100644 --- a/erpnext/manufacturing/doctype/sub_operation/sub_operation.json +++ b/erpnext/manufacturing/doctype/sub_operation/sub_operation.json @@ -16,7 +16,8 @@ "fieldtype": "Link", "in_list_view": 1, "label": "Operation", - "options": "Operation" + "options": "Operation", + "reqd": 1 }, { "default": "0", @@ -40,7 +41,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-08-04 16:15:11.425349", + "modified": "2026-04-13 12:17:33.776504", "modified_by": "Administrator", "module": "Manufacturing", "name": "Sub Operation", diff --git a/erpnext/manufacturing/doctype/sub_operation/sub_operation.py b/erpnext/manufacturing/doctype/sub_operation/sub_operation.py index f4bb62e8f6c..a34a96fc1a5 100644 --- a/erpnext/manufacturing/doctype/sub_operation/sub_operation.py +++ b/erpnext/manufacturing/doctype/sub_operation/sub_operation.py @@ -16,7 +16,7 @@ class SubOperation(Document): from frappe.types import DF description: DF.SmallText | None - operation: DF.Link | None + operation: DF.Link parent: DF.Data parentfield: DF.Data parenttype: DF.Data diff --git a/erpnext/manufacturing/doctype/workstation/workstation_list.js b/erpnext/manufacturing/doctype/workstation/workstation_list.js index 33722634b96..4c81ab082bf 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation_list.js +++ b/erpnext/manufacturing/doctype/workstation/workstation_list.js @@ -10,6 +10,6 @@ frappe.listview_settings["Workstation"] = { Setup: "blue", }; - return [__(doc.status), color_map[doc.status], true]; + return [__(doc.status), color_map[doc.status], "status,=," + doc.status]; }, }; diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 7d2c1757bda..2dcee807fec 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -432,3 +432,4 @@ erpnext.patches.v16_0.set_ordered_qty_in_quotation_item erpnext.patches.v15_0.replace_http_with_https_in_sales_partner erpnext.patches.v16_0.add_portal_redirects erpnext.patches.v16_0.update_order_qty_and_requested_qty_based_on_mr_and_po +erpnext.patches.v16_0.depends_on_inv_dimensions 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..0de46f68f11 --- /dev/null +++ b/erpnext/patches/v16_0/depends_on_inv_dimensions.py @@ -0,0 +1,89 @@ +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, fieldname): + if doctype not in [ + "Stock Entry Detail", + "Sales Invoice Item", + "Delivery Note Item", + "Purchase Invoice Item", + "Purchase Receipt Item", + ]: + 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 f"{fieldname_start_with}_{fieldname}", display_depends_on + + +def execute(): + for dimension in get_inventory_dimensions(): + 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'", + }, + ): + 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", + ) + 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": fieldname, "dt": dimension.doctype}, + "mandatory_depends_on", + display_depends_on if dimension.reqd else dimension.mandatory_depends_on, + ) 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 c8fcdb4c5a7..3a82642873d 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -759,7 +759,6 @@ "label": "Incoming Rate", "no_copy": 1, "options": "Company:company:default_currency", - "precision": "6", "print_hide": 1, "read_only": 1 }, @@ -952,7 +951,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-05-31 18:51:32.651562", + "modified": "2026-04-07 15:44:20.892151", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json index 20250622fda..3ebd6b6a1fa 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 fbef891b745..b43f2991bad 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py @@ -31,7 +31,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 @@ -75,7 +74,6 @@ class InventoryDimension(Document): old_doc = self._doc_before_save allow_to_edit_fields = [ - "disabled", "fetch_from_parent", "type_of_transaction", "condition", @@ -119,6 +117,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"]: @@ -183,8 +182,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, ), ] @@ -296,12 +299,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") @@ -374,7 +378,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}, ) @@ -397,7 +400,6 @@ def get_inventory_dimensions(): "reference_document as doctype", "validate_negative_stock", ], - filters={"disabled": 0}, ) frappe.local.inventory_dimensions = dimensions diff --git a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py index 96088db1923..29e811ea4c4 100644 --- a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py @@ -220,9 +220,9 @@ class TestInventoryDimension(FrappeTestCase): 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 @@ -230,7 +230,7 @@ class TestInventoryDimension(FrappeTestCase): 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" ) ) diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index 463544a9952..8fda1c44702 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -734,7 +734,6 @@ "oldfieldname": "valuation_rate", "oldfieldtype": "Currency", "options": "Company:company:default_currency", - "precision": "6", "print_hide": 1, "print_width": "80px", "read_only": 1, @@ -1149,7 +1148,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2025-10-14 12:59:20.384056", + "modified": "2026-04-07 15:41:47.032889", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", 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 648836d0f6e..6f5b184ec00 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -364,10 +364,11 @@ def item_query(doctype, txt, searchfield, start, page_len, filters): 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"): diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 4e81c65a58d..d720ff260ae 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -1793,6 +1793,47 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): elif s.id_plant == plant_b.name: self.assertEqual(s.actual_qty, 3) + def test_serial_no_status_with_backdated_stock_reco(self): + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + + item_code = self.make_item( + "Test Item", + { + "is_stock_item": 1, + "has_serial_no": 1, + "serial_no_series": "SERIAL.###", + }, + ).name + + warehouse = "_Test Warehouse - _TC" + + reco = create_stock_reconciliation( + item_code=item_code, + posting_date=add_days(nowdate(), -2), + warehouse=warehouse, + qty=1, + rate=80, + purpose="Opening Stock", + ) + + serial_no = get_serial_nos_from_bundle(reco.items[0].serial_and_batch_bundle)[0] + + create_delivery_note( + item_code=item_code, warehouse=warehouse, qty=1, rate=100, posting_date=nowdate() + ) + + self.assertEqual(frappe.get_value("Serial No", serial_no, "status"), "Delivered") + + reco = create_stock_reconciliation( + item_code=item_code, + posting_date=add_days(nowdate(), -1), + warehouse=warehouse, + qty=1, + rate=90, + ) + + self.assertEqual(frappe.get_value("Serial No", serial_no, "status"), "Delivered") + def create_batch_item_with_batch(item_name, batch_id): batch_item_doc = create_item(item_name, is_stock_item=1) diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py index a1991dd9c07..45d7a459fb5 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.py +++ b/erpnext/stock/doctype/warehouse/warehouse.py @@ -101,49 +101,23 @@ class Warehouse(NestedSet): def warn_about_multiple_warehouse_account(self): "If Warehouse value is split across multiple accounts, warn." - def get_accounts_where_value_is_booked(name): - sle = frappe.qb.DocType("Stock Ledger Entry") - gle = frappe.qb.DocType("GL Entry") - ac = frappe.qb.DocType("Account") - - return ( - frappe.qb.from_(sle) - .join(gle) - .on(sle.voucher_no == gle.voucher_no) - .join(ac) - .on(ac.name == gle.account) - .select(gle.account) - .distinct() - .where((sle.warehouse == name) & (ac.account_type == "Stock")) - .orderby(sle.creation) - .run(as_dict=True) - ) - - if self.is_new(): + if not frappe.db.count("Stock Ledger Entry", {"warehouse": self.name}): return - old_wh_account = frappe.db.get_value("Warehouse", self.name, "account") + doc_before_save = self.get_doc_before_save() + old_wh_account = doc_before_save.account if doc_before_save else None - # WH account is being changed or set get all accounts against which wh value is booked - if self.account != old_wh_account: - accounts = get_accounts_where_value_is_booked(self.name) - accounts = [d.account for d in accounts] + if self.is_new() or (self.account and old_wh_account == self.account): + return - if not accounts or (len(accounts) == 1 and self.account in accounts): - # if same singular account has stock value booked ignore - return - - warning = _("Warehouse's Stock Value has already been booked in the following accounts:") - account_str = "
" + ", ".join(frappe.bold(ac) for ac in accounts) - reason = "

" + _( - "Booking stock value across multiple accounts will make it harder to track stock and account value." - ) - - frappe.msgprint( - warning + account_str + reason, - title=_("Multiple Warehouse Accounts"), - indicator="orange", - ) + frappe.msgprint( + title=_("Warning: Account changed for warehouse"), + indicator="orange", + msg=_( + "Stock entries exist with the old account. Changing the account may lead to a mismatch between the warehouse closing balance and the account closing balance. The overall closing balance will still match, but not for the specific account." + ), + alert=True, + ) def check_if_sle_exists(self): return frappe.db.exists("Stock Ledger Entry", {"warehouse": self.name}) diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py index f895947f503..e5cb69ff816 100644 --- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py +++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py @@ -219,7 +219,7 @@ def get_item_warehouse_batch_map(filters, float_precision): ) qty_dict.bal_qty = flt(qty_dict.bal_qty, float_precision) + flt(d.actual_qty, float_precision) - qty_dict.bal_value += flt(d.stock_value_difference, float_precision) + qty_dict.bal_value += flt(d.stock_value_difference) return iwb_map diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 18e06fd6c10..084b37113f0 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -549,6 +549,16 @@ class update_entries_after: previous_sle = get_previous_sle_of_current_voucher(args) if previous_sle: self.prev_sle_dict[(args.get("item_code"), args.get("warehouse"))] = previous_sle + else: + self.prev_sle_dict[(args.get("item_code"), args.get("warehouse"))] = frappe._dict( + { + "qty_after_transaction": 0.0, + "valuation_rate": 0.0, + "stock_value": 0.0, + "prev_stock_value": 0.0, + "stock_queue": [], + } + ) warehouse_dict.previous_sle = previous_sle @@ -1063,34 +1073,6 @@ class update_entries_after: sabb_doc.voucher_no = None sabb_doc.cancel() - if sle.serial_and_batch_bundle and frappe.get_cached_value("Item", sle.item_code, "has_serial_no"): - self.update_serial_no_status(sle) - - def update_serial_no_status(self, sle): - from erpnext.stock.serial_batch_bundle import get_serial_nos - - serial_nos = get_serial_nos(sle.serial_and_batch_bundle) - if not serial_nos: - return - - warehouse = None - status = "Inactive" - - if sle.actual_qty > 0: - warehouse = sle.warehouse - status = "Active" - - sn_table = frappe.qb.DocType("Serial No") - - query = ( - frappe.qb.update(sn_table) - .set(sn_table.warehouse, warehouse) - .set(sn_table.status, status) - .where(sn_table.name.isin(serial_nos)) - ) - - query.run() - def calculate_valuation_for_serial_batch_bundle(self, sle): if not frappe.db.exists("Serial and Batch Bundle", sle.serial_and_batch_bundle): return