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