diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
index 15badf105f8..25e62e20f3e 100644
--- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
+++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
@@ -216,7 +216,7 @@
"description": "Payment Terms from orders will be fetched into the invoices as is",
"fieldname": "automatically_fetch_payment_terms",
"fieldtype": "Check",
- "label": "Automatically Fetch Payment Terms from Order"
+ "label": "Automatically Fetch Payment Terms from Order/Quotation"
},
{
"description": "The percentage you are allowed to bill more against the amount ordered. For example, if the order value is $100 for an item and tolerance is set as 10%, then you are allowed to bill up to $110 ",
@@ -307,7 +307,7 @@
},
{
"default": "0",
- "description": "Learn about Common Party",
+ "description": "Learn about Common Party",
"fieldname": "enable_common_party_accounting",
"fieldtype": "Check",
"label": "Enable Common Party Accounting"
@@ -671,7 +671,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2025-12-26 19:46:55.093717",
+ "modified": "2026-03-06 14:49:11.467716",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",
@@ -701,4 +701,4 @@
"sort_order": "ASC",
"states": [],
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py
index 4ff2a13eae2..36cbb321518 100644
--- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py
+++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py
@@ -5,7 +5,9 @@
import frappe
from frappe import _, msgprint
from frappe.model.document import Document
+from frappe.query_builder import Case
from frappe.query_builder.custom import ConstantColumn
+from frappe.query_builder.functions import Coalesce, Sum
from frappe.utils import flt, fmt_money, get_link_to_form, getdate
from pypika import Order
@@ -136,65 +138,162 @@ def get_payment_entries_for_bank_clearance(
):
entries = []
- condition = ""
- pe_condition = ""
+ journal_entry = frappe.qb.DocType("Journal Entry")
+ journal_entry_account = frappe.qb.DocType("Journal Entry Account")
+
+ journal_entry_query = (
+ frappe.qb.from_(journal_entry_account)
+ .inner_join(journal_entry)
+ .on(journal_entry_account.parent == journal_entry.name)
+ .select(
+ ConstantColumn("Journal Entry").as_("payment_document"),
+ journal_entry.name.as_("payment_entry"),
+ journal_entry.cheque_no.as_("cheque_number"),
+ journal_entry.cheque_date,
+ Sum(journal_entry_account.debit_in_account_currency).as_("debit"),
+ Sum(journal_entry_account.credit_in_account_currency).as_("credit"),
+ journal_entry.posting_date,
+ journal_entry_account.against_account,
+ journal_entry.clearance_date,
+ journal_entry_account.account_currency,
+ )
+ .where(
+ (journal_entry_account.account == account)
+ & (journal_entry.docstatus == 1)
+ & (journal_entry.posting_date >= from_date)
+ & (journal_entry.posting_date <= to_date)
+ & (journal_entry.is_opening == "No")
+ )
+ )
+
if not include_reconciled_entries:
- condition = "and (clearance_date IS NULL or clearance_date='0000-00-00')"
- pe_condition = "and (pe.clearance_date IS NULL or pe.clearance_date='0000-00-00')"
+ journal_entry_query = journal_entry_query.where(
+ (journal_entry.clearance_date.isnull()) | (journal_entry.clearance_date == "0000-00-00")
+ )
- journal_entries = frappe.db.sql(
- f"""
- select
- "Journal Entry" as payment_document, t1.name as payment_entry,
- t1.cheque_no as cheque_number, t1.cheque_date,
- sum(t2.debit_in_account_currency) as debit, sum(t2.credit_in_account_currency) as credit,
- t1.posting_date, t2.against_account, t1.clearance_date, t2.account_currency
- from
- `tabJournal Entry` t1, `tabJournal Entry Account` t2
- where
- t2.parent = t1.name and t2.account = %(account)s and t1.docstatus=1
- and t1.posting_date >= %(from)s and t1.posting_date <= %(to)s
- and ifnull(t1.is_opening, 'No') = 'No' {condition}
- group by t2.account, t1.name
- order by t1.posting_date ASC, t1.name DESC
- """,
- {"account": account, "from": from_date, "to": to_date},
- as_dict=1,
+ journal_entries = (
+ journal_entry_query.groupby(journal_entry_account.account, journal_entry.name)
+ .orderby(journal_entry.posting_date)
+ .orderby(journal_entry.name, order=Order.desc)
+ ).run(as_dict=True)
+
+ pe = frappe.qb.DocType("Payment Entry")
+ company = frappe.qb.DocType("Company")
+ payment_entry_query = (
+ frappe.qb.from_(pe)
+ .join(company)
+ .on(pe.company == company.name)
+ .select(
+ ConstantColumn("Payment Entry").as_("payment_document"),
+ pe.name.as_("payment_entry"),
+ pe.reference_no.as_("cheque_number"),
+ pe.reference_date.as_("cheque_date"),
+ (
+ Case()
+ .when(
+ pe.paid_from == account,
+ (
+ pe.paid_amount
+ + (
+ Case()
+ .when(
+ (pe.payment_type == "Pay")
+ & (company.default_currency == pe.paid_from_account_currency),
+ pe.base_total_taxes_and_charges,
+ )
+ .else_(pe.total_taxes_and_charges)
+ )
+ ),
+ )
+ .else_(0)
+ ).as_("credit"),
+ (
+ Case()
+ .when(pe.paid_from == account, 0)
+ .else_(
+ pe.received_amount
+ + (
+ Case()
+ .when(
+ company.default_currency == pe.paid_to_account_currency,
+ pe.base_total_taxes_and_charges,
+ )
+ .else_(pe.total_taxes_and_charges)
+ )
+ )
+ ).as_("debit"),
+ pe.posting_date,
+ Coalesce(pe.party, Case().when(pe.paid_from == account, pe.paid_to).else_(pe.paid_from)).as_(
+ "against_account"
+ ),
+ pe.clearance_date,
+ (
+ Case()
+ .when(pe.paid_to == account, pe.paid_to_account_currency)
+ .else_(pe.paid_from_account_currency)
+ ).as_("account_currency"),
+ )
+ .where(
+ ((pe.paid_from == account) | (pe.paid_to == account))
+ & (pe.docstatus == 1)
+ & (pe.posting_date >= from_date)
+ & (pe.posting_date <= to_date)
+ )
)
- payment_entries = frappe.db.sql(
- f"""
- select
- "Payment Entry" as payment_document, pe.name as payment_entry,
- pe.reference_no as cheque_number, pe.reference_date as cheque_date,
- if(pe.paid_from=%(account)s, pe.paid_amount + if(pe.payment_type = 'Pay' and c.default_currency = pe.paid_from_account_currency, pe.base_total_taxes_and_charges, pe.total_taxes_and_charges) , 0) as credit,
- if(pe.paid_from=%(account)s, 0, pe.received_amount + pe.total_taxes_and_charges) as debit,
- pe.posting_date, ifnull(pe.party,if(pe.paid_from=%(account)s,pe.paid_to,pe.paid_from)) as against_account, pe.clearance_date,
- if(pe.paid_to=%(account)s, pe.paid_to_account_currency, pe.paid_from_account_currency) as account_currency
- from `tabPayment Entry` as pe
- join `tabCompany` c on c.name = pe.company
- where
- (pe.paid_from=%(account)s or pe.paid_to=%(account)s) and pe.docstatus=1
- and pe.posting_date >= %(from)s and pe.posting_date <= %(to)s
- {pe_condition}
- order by
- pe.posting_date ASC, pe.name DESC
- """,
- {
- "account": account,
- "from": from_date,
- "to": to_date,
- },
- as_dict=1,
+ if not include_reconciled_entries:
+ payment_entry_query = payment_entry_query.where(
+ (pe.clearance_date.isnull()) | (pe.clearance_date == "0000-00-00")
+ )
+
+ payment_entries = (payment_entry_query.orderby(pe.posting_date).orderby(pe.name, order=Order.desc)).run(
+ as_dict=True
)
- pos_sales_invoices, pos_purchase_invoices = [], []
+ acc = frappe.qb.DocType("Account")
+
+ pi = frappe.qb.DocType("Purchase Invoice")
+
+ paid_purchase_invoices_query = (
+ frappe.qb.from_(pi)
+ .inner_join(acc)
+ .on(pi.cash_bank_account == acc.name)
+ .select(
+ ConstantColumn("Purchase Invoice").as_("payment_document"),
+ pi.name.as_("payment_entry"),
+ pi.paid_amount.as_("credit"),
+ pi.posting_date,
+ pi.supplier.as_("against_account"),
+ pi.bill_no.as_("cheque_number"),
+ pi.clearance_date,
+ acc.account_currency,
+ ConstantColumn(0).as_("debit"),
+ )
+ .where(
+ (pi.docstatus == 1)
+ & (pi.is_paid == 1)
+ & (pi.cash_bank_account == account)
+ & (pi.posting_date >= from_date)
+ & (pi.posting_date <= to_date)
+ )
+ )
+
+ if not include_reconciled_entries:
+ paid_purchase_invoices_query = paid_purchase_invoices_query.where(
+ (pi.clearance_date.isnull()) | (pi.clearance_date == "0000-00-00")
+ )
+
+ paid_purchase_invoices = (
+ paid_purchase_invoices_query.orderby(pi.posting_date).orderby(pi.name, order=Order.desc)
+ ).run(as_dict=True)
+
+ pos_sales_invoices = []
+
if include_pos_transactions:
si_payment = frappe.qb.DocType("Sales Invoice Payment")
si = frappe.qb.DocType("Sales Invoice")
- acc = frappe.qb.DocType("Account")
- pos_sales_invoices = (
+ pos_sales_invoices_query = (
frappe.qb.from_(si_payment)
.inner_join(si)
.on(si_payment.parent == si.name)
@@ -217,38 +316,22 @@ def get_payment_entries_for_bank_clearance(
& (si.posting_date >= from_date)
& (si.posting_date <= to_date)
)
- .orderby(si.posting_date)
- .orderby(si.name, order=Order.desc)
- ).run(as_dict=True)
+ )
- pi = frappe.qb.DocType("Purchase Invoice")
+ if not include_reconciled_entries:
+ pos_sales_invoices_query = pos_sales_invoices_query.where(
+ (si_payment.clearance_date.isnull()) | (si_payment.clearance_date == "0000-00-00")
+ )
- pos_purchase_invoices = (
- frappe.qb.from_(pi)
- .inner_join(acc)
- .on(pi.cash_bank_account == acc.name)
- .select(
- ConstantColumn("Purchase Invoice").as_("payment_document"),
- pi.name.as_("payment_entry"),
- pi.paid_amount.as_("credit"),
- pi.posting_date,
- pi.supplier.as_("against_account"),
- pi.clearance_date,
- acc.account_currency,
- ConstantColumn(0).as_("debit"),
- )
- .where(
- (pi.docstatus == 1)
- & (pi.cash_bank_account == account)
- & (pi.posting_date >= from_date)
- & (pi.posting_date <= to_date)
- )
- .orderby(pi.posting_date)
- .orderby(pi.name, order=Order.desc)
+ pos_sales_invoices = (
+ pos_sales_invoices_query.orderby(si.posting_date).orderby(si.name, order=Order.desc)
).run(as_dict=True)
entries = (
- list(payment_entries) + list(journal_entries) + list(pos_sales_invoices) + list(pos_purchase_invoices)
+ list(payment_entries)
+ + list(journal_entries)
+ + list(pos_sales_invoices)
+ + list(paid_purchase_invoices)
)
return entries
diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js
index 7361cb2fc1c..a186f419108 100644
--- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js
+++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js
@@ -2,6 +2,15 @@
// For license information, please see license.txt
frappe.ui.form.on("Bank Statement Import", {
+ onload(frm) {
+ frm.set_query("bank_account", function (doc) {
+ return {
+ filters: {
+ company: doc.company,
+ },
+ };
+ });
+ },
setup(frm) {
frappe.realtime.on("data_import_refresh", ({ data_import }) => {
frm.import_in_progress = false;
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index 2ed9881772c..502a4f9e015 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -214,6 +214,8 @@ class JournalEntry(AccountsController):
def on_cancel(self):
# References for this Journal are removed on the `on_cancel` event in accounts_controller
super().on_cancel()
+
+ from_doc_events = getattr(self, "ignore_linked_doctypes", ())
self.ignore_linked_doctypes = (
"GL Entry",
"Stock Ledger Entry",
@@ -226,6 +228,10 @@ class JournalEntry(AccountsController):
"Unreconcile Payment Entries",
"Advance Payment Ledger Entry",
)
+
+ if from_doc_events and from_doc_events != self.ignore_linked_doctypes:
+ self.ignore_linked_doctypes = self.ignore_linked_doctypes + from_doc_events
+
self.make_gl_entries(1)
self.unlink_advance_entry_reference()
self.unlink_asset_reference()
@@ -263,6 +269,9 @@ class JournalEntry(AccountsController):
frappe.throw(_("Journal Entry type should be set as Depreciation Entry for asset depreciation"))
def validate_stock_accounts(self):
+ if not erpnext.is_perpetual_inventory_enabled(self.company):
+ return
+
stock_accounts = get_stock_accounts(self.company, accounts=self.accounts)
for account in stock_accounts:
account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index 585ec41ffac..262dc89d44f 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -2556,14 +2556,9 @@ def get_orders_to_be_billed(
if not voucher_type:
return []
- # Add cost center condition
- doc = frappe.get_doc({"doctype": voucher_type})
- condition = ""
- if doc and hasattr(doc, "cost_center") and doc.cost_center:
- condition = " and cost_center='%s'" % cost_center
-
# dynamic dimension filters
- active_dimensions = get_dimensions()[0]
+ condition = ""
+ active_dimensions = get_dimensions(True)[0]
for dim in active_dimensions:
if filters.get(dim.fieldname):
condition += f" and {dim.fieldname}='{filters.get(dim.fieldname)}'"
diff --git a/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json b/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json
index e47b51ae028..735d6b02857 100644
--- a/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json
+++ b/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json
@@ -22,6 +22,7 @@
"reqd": 1
},
{
+ "allow_on_submit": 1,
"fieldname": "cost_center",
"fieldtype": "Link",
"in_list_view": 1,
@@ -59,7 +60,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2024-11-05 16:07:47.307971",
+ "modified": "2026-03-11 14:26:11.312950",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry Deduction",
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
index efaeff12279..81c9ff09ccb 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
@@ -312,7 +312,7 @@
"fieldname": "posting_date",
"fieldtype": "Date",
"in_list_view": 1,
- "label": "Date",
+ "label": "Posting Date",
"oldfieldname": "posting_date",
"oldfieldtype": "Date",
"print_hide": 1,
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index 8f04f173c1f..f207b2079ab 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -373,7 +373,7 @@
"fieldtype": "Date",
"hide_days": 1,
"hide_seconds": 1,
- "label": "Date",
+ "label": "Posting Date",
"no_copy": 1,
"oldfieldname": "posting_date",
"oldfieldtype": "Date",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index cf4377bd0df..96eca4f52cd 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -804,11 +804,9 @@ class SalesInvoice(SellingController):
if self.pos_profile:
pos = frappe.get_doc("POS Profile", self.pos_profile)
- if not self.get("payments") and not for_validate:
- update_multi_mode_option(self, pos)
-
if pos:
if not for_validate:
+ update_multi_mode_option(self, pos)
self.tax_category = pos.get("tax_category")
if not for_validate and not self.customer:
@@ -2747,6 +2745,8 @@ def update_multi_mode_option(doc, pos_profile):
payment.account = payment_mode.default_account
payment.type = payment_mode.type
+ mop_refetched = bool(doc.payments) and not doc.is_created_using_pos
+
doc.set("payments", [])
invalid_modes = []
mode_of_payments = [d.mode_of_payment for d in pos_profile.get("payments")]
@@ -2768,6 +2768,12 @@ def update_multi_mode_option(doc, pos_profile):
msg = _("Please set default Cash or Bank account in Mode of Payments {}")
frappe.throw(msg.format(", ".join(invalid_modes)), title=_("Missing Account"))
+ if mop_refetched:
+ frappe.toast(
+ _("Payment methods refreshed. Please review before proceeding."),
+ indicator="orange",
+ )
+
def get_all_mode_of_payments(doc):
return frappe.db.sql(
diff --git a/erpnext/accounts/print_format/point_of_sale/__init__.py b/erpnext/accounts/print_format/point_of_sale/__init__.py
deleted file mode 100644
index e69de29bb2d..00000000000
diff --git a/erpnext/accounts/print_format/point_of_sale/point_of_sale.json b/erpnext/accounts/print_format/point_of_sale/point_of_sale.json
deleted file mode 100644
index c0c50cb4e26..00000000000
--- a/erpnext/accounts/print_format/point_of_sale/point_of_sale.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "align_labels_right": 0,
- "creation": "2016-05-05 17:16:18.564460",
- "custom_format": 1,
- "disabled": 0,
- "doc_type": "Sales Invoice",
- "docstatus": 0,
- "doctype": "Print Format",
- "font": "Default",
- "html": "\n\n
\n\t{{ company }}
\n\t{{ __(\"POS No : \") }} {{ offline_pos_name }}
\n
\n\n\t{{ __(\"Customer\") }}: {{ customer }}
\n
\n\n\n\t{{ __(\"Date\") }}: {{ dateutil.global_date_format(posting_date) }}
\n
\n\n
\n\n\t\n\t\t\n\t\t\t| {{ __(\"Item\") }} | \n\t\t\t{{ __(\"Qty\") }} | \n\t\t\t{{ __(\"Amount\") }} | \n\t\t
\n\t\n\t\n\t\t{% for item in items %}\n\t\t\n\t\t\t| \n\t\t\t\t{{ item.item_name }}\n\t\t\t | \n\t\t\t{{ format_number(item.qty, null,precision(\"difference\")) }} @ {{ format_currency(item.rate, currency) }} | \n\t\t\t{{ format_currency(item.amount, currency) }} | \n\t\t
\n\t\t{% endfor %}\n\t\n
\n\n\n\t\n\t\t\n\t\t\t| \n\t\t\t\t{{ __(\"Net Total\") }}\n\t\t\t | \n\t\t\t\n\t\t\t\t{{ format_currency(total, currency) }}\n\t\t\t | \n\t\t
\n\t\t{% for row in taxes %}\n\t\t{% if not row.included_in_print_rate %}\n\t\t\n\t\t\t| \n\t\t\t\t{{ row.description }}\n\t\t\t | \n\t\t\t\n\t\t\t\t{{ format_currency(row.tax_amount, currency) }}\n\t\t\t | \n\t\t
\n\t\t{% endif %}\n\t\t{% endfor %}\n\t\t{% if discount_amount %}\n\t\t\n\t\t\t| \n\t\t\t\t{{ __(\"Discount\") }}\n\t\t\t | \n\t\t\t\n\t\t\t\t{{ format_currency(discount_amount, currency) }}\n\t\t\t | \n\t\t
\n\t\t{% endif %}\n\t\t\n\t\t\t| \n\t\t\t\t{{ __(\"Grand Total\") }}\n\t\t\t | \n\t\t\t\n\t\t\t\t{{ format_currency(grand_total, currency) }}\n\t\t\t | \n\t\t
\n\t\t\n\t\t\t| \n\t\t\t\t{{ __(\"Paid Amount\") }}\n\t\t\t | \n\t\t\t\n\t\t\t\t{{ format_currency(paid_amount, currency) }}\n\t\t\t | \n\t\t
\n\t\t\n\t\t\t| \n\t\t\t\t{{ __(\"Qty Total\") }}\n\t\t\t | \n\t\t\t\n\t\t\t\t{{ qty_total }}\n\t\t\t | \n\t\t
\n\t\n
\n\n\n
\n{{ terms }}
\n{{ __(\"Thank you, please visit again.\") }}
",
- "idx": 0,
- "line_breaks": 0,
- "modified": "2019-09-05 17:20:30.726659",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Point of Sale",
- "owner": "Administrator",
- "print_format_builder": 0,
- "print_format_type": "JS",
- "raw_printing": 0,
- "show_section_headings": 0,
- "standard": "Yes"
-}
\ No newline at end of file
diff --git a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py
index ae675670446..5320de2b66c 100644
--- a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py
+++ b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py
@@ -4,7 +4,10 @@
import frappe
from frappe import _
-from frappe.utils import getdate, nowdate
+from frappe.query_builder import Case
+from frappe.query_builder.custom import ConstantColumn
+from frappe.utils import getdate
+from pypika import Order
def execute(filters=None):
@@ -48,17 +51,6 @@ def get_columns():
return columns
-def get_conditions(filters):
- conditions = ""
-
- if filters.get("from_date"):
- conditions += " and posting_date>=%(from_date)s"
- if filters.get("to_date"):
- conditions += " and posting_date<=%(to_date)s"
-
- return conditions
-
-
def get_entries(filters):
entries = []
@@ -73,41 +65,90 @@ def get_entries(filters):
return sorted(
entries,
- key=lambda k: k[2].strftime("%H%M%S") or getdate(nowdate()),
+ key=lambda k: getdate(k[2]),
)
def get_entries_for_bank_clearance_summary(filters):
entries = []
- conditions = get_conditions(filters)
+ je = frappe.qb.DocType("Journal Entry")
+ jea = frappe.qb.DocType("Journal Entry Account")
- journal_entries = frappe.db.sql(
- f"""SELECT
- "Journal Entry", jv.name, jv.posting_date, jv.cheque_no,
- jv.clearance_date, jvd.against_account, jvd.debit - jvd.credit
- FROM
- `tabJournal Entry Account` jvd, `tabJournal Entry` jv
- WHERE
- jvd.parent = jv.name and jv.docstatus=1 and jvd.account = %(account)s {conditions}
- order by posting_date DESC, jv.name DESC""",
- filters,
- as_list=1,
- )
+ journal_entries = (
+ frappe.qb.from_(jea)
+ .inner_join(je)
+ .on(jea.parent == je.name)
+ .select(
+ ConstantColumn("Journal Entry").as_("payment_document"),
+ je.name.as_("payment_entry"),
+ je.posting_date,
+ je.cheque_no,
+ je.clearance_date,
+ jea.against_account,
+ jea.debit_in_account_currency - jea.credit_in_account_currency,
+ )
+ .where(
+ (jea.account == filters.account)
+ & (je.docstatus == 1)
+ & (je.posting_date >= filters.from_date)
+ & (je.posting_date <= filters.to_date)
+ & ((je.is_opening == "No") | (je.is_opening.isnull()))
+ )
+ .orderby(je.posting_date, order=Order.desc)
+ .orderby(je.name, order=Order.desc)
+ ).run(as_list=True)
- payment_entries = frappe.db.sql(
- f"""SELECT
- "Payment Entry", name, posting_date, reference_no, clearance_date, party,
- if(paid_from=%(account)s, ((paid_amount * -1) - total_taxes_and_charges) , received_amount)
- FROM
- `tabPayment Entry`
- WHERE
- docstatus=1 and (paid_from = %(account)s or paid_to = %(account)s) {conditions}
- order by posting_date DESC, name DESC""",
- filters,
- as_list=1,
- )
+ pe = frappe.qb.DocType("Payment Entry")
+ payment_entries = (
+ frappe.qb.from_(pe)
+ .select(
+ ConstantColumn("Payment Entry").as_("payment_document"),
+ pe.name.as_("payment_entry"),
+ pe.posting_date,
+ pe.reference_no.as_("cheque_no"),
+ pe.clearance_date,
+ pe.party.as_("against_account"),
+ Case()
+ .when(
+ (pe.paid_from == filters.account),
+ ((pe.paid_amount * -1) - pe.total_taxes_and_charges),
+ )
+ .else_(pe.received_amount),
+ )
+ .where((pe.paid_from == filters.account) | (pe.paid_to == filters.account))
+ .where(
+ (pe.docstatus == 1)
+ & (pe.posting_date >= filters.from_date)
+ & (pe.posting_date <= filters.to_date)
+ )
+ .orderby(pe.posting_date, order=Order.desc)
+ .orderby(pe.name, order=Order.desc)
+ ).run(as_list=True)
- entries = journal_entries + payment_entries
+ pi = frappe.qb.DocType("Purchase Invoice")
+ purchase_invoices = (
+ frappe.qb.from_(pi)
+ .select(
+ ConstantColumn("Purchase Invoice").as_("payment_document"),
+ pi.name.as_("payment_entry"),
+ pi.posting_date,
+ pi.bill_no.as_("cheque_no"),
+ pi.clearance_date,
+ pi.supplier.as_("against_account"),
+ (pi.paid_amount * -1).as_("amount"),
+ )
+ .where(
+ (pi.docstatus == 1)
+ & (pi.is_paid == 1)
+ & (pi.cash_bank_account == filters.account)
+ & (pi.posting_date >= filters.from_date)
+ & (pi.posting_date <= filters.to_date)
+ )
+ .orderby(pi.posting_date, order=Order.desc)
+ .orderby(pi.name, order=Order.desc)
+ ).run(as_list=True)
+
+ entries = journal_entries + payment_entries + purchase_invoices
return entries
diff --git a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py
index bfc2f2d56ff..474e25c5474 100644
--- a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py
+++ b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py
@@ -4,7 +4,11 @@
import frappe
from frappe import _
+from frappe.query_builder import Case
+from frappe.query_builder.custom import ConstantColumn
+from frappe.query_builder.functions import Coalesce, Sum
from frappe.utils import flt, getdate
+from pypika import Order
from erpnext.accounts.utils import get_balance_on
@@ -123,73 +127,143 @@ def get_entries_for_bank_reconciliation_statement(filters):
payment_entries = get_payment_entries(filters)
+ purchase_invoices = get_purchase_invoices(filters)
+
pos_entries = []
if filters.include_pos_transactions:
pos_entries = get_pos_entries(filters)
- return list(journal_entries) + list(payment_entries) + list(pos_entries)
+ return list(journal_entries) + list(payment_entries) + list(pos_entries) + list(purchase_invoices)
def get_journal_entries(filters):
- return frappe.db.sql(
- """
- select "Journal Entry" as payment_document, jv.posting_date,
- jv.name as payment_entry, jvd.debit_in_account_currency as debit,
- jvd.credit_in_account_currency as credit, jvd.against_account,
- jv.cheque_no as reference_no, jv.cheque_date as ref_date, jv.clearance_date, jvd.account_currency
- from
- `tabJournal Entry Account` jvd, `tabJournal Entry` jv
- where jvd.parent = jv.name and jv.docstatus=1
- and jvd.account = %(account)s and jv.posting_date <= %(report_date)s
- and ifnull(jv.clearance_date, '4000-01-01') > %(report_date)s
- and ifnull(jv.is_opening, 'No') = 'No'
- and jv.company = %(company)s """,
- filters,
- as_dict=1,
- )
+ je = frappe.qb.DocType("Journal Entry")
+ jea = frappe.qb.DocType("Journal Entry Account")
+ return (
+ frappe.qb.from_(jea)
+ .join(je)
+ .on(jea.parent == je.name)
+ .select(
+ ConstantColumn("Journal Entry").as_("payment_document"),
+ je.name.as_("payment_entry"),
+ je.posting_date,
+ jea.debit_in_account_currency.as_("debit"),
+ jea.credit_in_account_currency.as_("credit"),
+ jea.against_account,
+ je.cheque_no.as_("reference_no"),
+ je.cheque_date.as_("ref_date"),
+ je.clearance_date,
+ jea.account_currency,
+ )
+ .where(
+ (je.docstatus == 1)
+ & (jea.account == filters.account)
+ & (je.posting_date <= filters.report_date)
+ & (je.clearance_date.isnull() | (je.clearance_date > filters.report_date))
+ & (je.company == filters.company)
+ & ((je.is_opening.isnull()) | (je.is_opening == "No"))
+ )
+ .orderby(je.posting_date)
+ .orderby(je.name, order=Order.desc)
+ ).run(as_dict=True)
def get_payment_entries(filters):
- return frappe.db.sql(
- """
- select
- "Payment Entry" as payment_document, name as payment_entry,
- reference_no, reference_date as ref_date,
- if(paid_to=%(account)s, received_amount_after_tax, 0) as debit,
- if(paid_from=%(account)s, paid_amount_after_tax, 0) as credit,
- posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date,
- if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency
- from `tabPayment Entry`
- where
- (paid_from=%(account)s or paid_to=%(account)s) and docstatus=1
- and posting_date <= %(report_date)s
- and ifnull(clearance_date, '4000-01-01') > %(report_date)s
- and company = %(company)s
- """,
- filters,
- as_dict=1,
- )
+ pe = frappe.qb.DocType("Payment Entry")
+ return (
+ frappe.qb.from_(pe)
+ .select(
+ ConstantColumn("Payment Entry").as_("payment_document"),
+ pe.name.as_("payment_entry"),
+ pe.reference_no.as_("reference_no"),
+ pe.reference_date.as_("ref_date"),
+ Case().when(pe.paid_to == filters.account, pe.received_amount_after_tax).else_(0).as_("debit"),
+ Case().when(pe.paid_from == filters.account, pe.paid_amount_after_tax).else_(0).as_("credit"),
+ pe.posting_date,
+ Coalesce(
+ pe.party, Case().when(pe.paid_from == filters.account, pe.paid_to).else_(pe.paid_from)
+ ).as_("against_account"),
+ pe.clearance_date,
+ (
+ Case()
+ .when(pe.paid_to == filters.account, pe.paid_to_account_currency)
+ .else_(pe.paid_from_account_currency)
+ ).as_("account_currency"),
+ )
+ .where(
+ (pe.docstatus == 1)
+ & ((pe.paid_from == filters.account) | (pe.paid_to == filters.account))
+ & (pe.posting_date <= filters.report_date)
+ & (pe.clearance_date.isnull() | (pe.clearance_date > filters.report_date))
+ & (pe.company == filters.company)
+ )
+ .orderby(pe.posting_date)
+ .orderby(pe.name, order=Order.desc)
+ ).run(as_dict=True)
+
+
+def get_purchase_invoices(filters):
+ pi = frappe.qb.DocType("Purchase Invoice")
+ acc = frappe.qb.DocType("Account")
+ return (
+ frappe.qb.from_(pi)
+ .inner_join(acc)
+ .on(pi.cash_bank_account == acc.name)
+ .select(
+ ConstantColumn("Purchase Invoice").as_("payment_document"),
+ pi.name.as_("payment_entry"),
+ pi.bill_no.as_("reference_no"),
+ pi.posting_date.as_("ref_date"),
+ Case().when(pi.paid_amount < 0, pi.paid_amount * -1).else_(0).as_("debit"),
+ Case().when(pi.paid_amount > 0, pi.paid_amount).else_(0).as_("credit"),
+ pi.posting_date,
+ pi.supplier.as_("against_account"),
+ pi.clearance_date,
+ acc.account_currency,
+ )
+ .where(
+ (pi.docstatus == 1)
+ & (pi.is_paid == 1)
+ & (pi.cash_bank_account == filters.account)
+ & (pi.posting_date <= filters.report_date)
+ & (pi.clearance_date.isnull() | (pi.clearance_date > filters.report_date))
+ & (pi.company == filters.company)
+ )
+ .orderby(pi.posting_date)
+ .orderby(pi.name, order=Order.desc)
+ ).run(as_dict=True)
def get_pos_entries(filters):
- return frappe.db.sql(
- """
- select
- "Sales Invoice Payment" as payment_document, sip.name as payment_entry, sip.amount as debit,
- si.posting_date, si.debit_to as against_account, sip.clearance_date,
- account.account_currency, 0 as credit
- from `tabSales Invoice Payment` sip, `tabSales Invoice` si, `tabAccount` account
- where
- sip.account=%(account)s and si.docstatus=1 and sip.parent = si.name
- and account.name = sip.account and si.posting_date <= %(report_date)s and
- ifnull(sip.clearance_date, '4000-01-01') > %(report_date)s
- and si.company = %(company)s
- order by
- si.posting_date ASC, si.name DESC
- """,
- filters,
- as_dict=1,
- )
+ si = frappe.qb.DocType("Sales Invoice")
+ si_payment = frappe.qb.DocType("Sales Invoice Payment")
+ acc = frappe.qb.DocType("Account")
+ return (
+ frappe.qb.from_(si_payment)
+ .join(si)
+ .on(si_payment.parent == si.name)
+ .join(acc)
+ .on(si_payment.account == acc.name)
+ .select(
+ ConstantColumn("Sales Invoice").as_("payment_document"),
+ si.name.as_("payment_entry"),
+ si_payment.amount.as_("debit"),
+ si.posting_date,
+ si.debit_to.as_("against_account"),
+ si_payment.clearance_date,
+ acc.account_currency,
+ ConstantColumn(0).as_("credit"),
+ )
+ .where(
+ (si_payment.account == filters.account)
+ & (si.docstatus == 1)
+ & (si.posting_date <= filters.report_date)
+ & (si_payment.clearance_date.isnull() | (si_payment.clearance_date > filters.report_date))
+ & (si.company == filters.company)
+ )
+ .orderby(si.posting_date)
+ .orderby(si_payment.name, order=Order.desc)
+ ).run(as_dict=True)
def get_amounts_not_reflected_in_system(filters):
@@ -205,30 +279,66 @@ def get_amounts_not_reflected_in_system(filters):
def get_amounts_not_reflected_in_system_for_bank_reconciliation_statement(filters):
- je_amount = frappe.db.sql(
- """
- select sum(jvd.debit_in_account_currency - jvd.credit_in_account_currency)
- from `tabJournal Entry Account` jvd, `tabJournal Entry` jv
- where jvd.parent = jv.name and jv.docstatus=1 and jvd.account=%(account)s
- and jv.posting_date > %(report_date)s and jv.clearance_date <= %(report_date)s
- and ifnull(jv.is_opening, 'No') = 'No' """,
- filters,
+ je = frappe.qb.DocType("Journal Entry")
+ jea = frappe.qb.DocType("Journal Entry Account")
+
+ je_amount = (
+ frappe.qb.from_(jea)
+ .inner_join(je)
+ .on(jea.parent == je.name)
+ .select(
+ Sum(jea.debit_in_account_currency - jea.credit_in_account_currency).as_("amount"),
+ )
+ .where(
+ (je.docstatus == 1)
+ & (jea.account == filters.account)
+ & (je.posting_date > filters.report_date)
+ & (je.clearance_date <= filters.report_date)
+ & (je.company == filters.company)
+ & ((je.is_opening.isnull()) | (je.is_opening == "No"))
+ )
+ .run(as_dict=True)
)
+ je_amount = flt(je_amount[0].amount) if je_amount else 0.0
- je_amount = flt(je_amount[0][0]) if je_amount else 0.0
-
- pe_amount = frappe.db.sql(
- """
- select sum(if(paid_from=%(account)s, paid_amount, received_amount))
- from `tabPayment Entry`
- where (paid_from=%(account)s or paid_to=%(account)s) and docstatus=1
- and posting_date > %(report_date)s and clearance_date <= %(report_date)s""",
- filters,
+ pe = frappe.qb.DocType("Payment Entry")
+ pe_amount = (
+ frappe.qb.from_(pe)
+ .select(
+ Sum(Case().when(pe.paid_from == filters.account, pe.paid_amount).else_(pe.received_amount)).as_(
+ "amount"
+ ),
+ )
+ .where(
+ ((pe.paid_from == filters.account) | (pe.paid_to == filters.account))
+ & (pe.docstatus == 1)
+ & (pe.posting_date > filters.report_date)
+ & (pe.clearance_date <= filters.report_date)
+ & (pe.company == filters.company)
+ )
+ .run(as_dict=True)
)
+ pe_amount = flt(pe_amount[0].amount) if pe_amount else 0.0
- pe_amount = flt(pe_amount[0][0]) if pe_amount else 0.0
+ pi = frappe.qb.DocType("Purchase Invoice")
+ pi_amount = (
+ frappe.qb.from_(pi)
+ .select(
+ Sum(pi.paid_amount).as_("amount"),
+ )
+ .where(
+ (pi.docstatus == 1)
+ & (pi.is_paid == 1)
+ & (pi.cash_bank_account == filters.account)
+ & (pi.posting_date > filters.report_date)
+ & (pi.clearance_date <= filters.report_date)
+ & (pi.company == filters.company)
+ )
+ ).run(as_dict=True)
- return je_amount + pe_amount
+ pi_amount = flt(pi_amount[0].amount) if pi_amount else 0.0
+
+ return je_amount + pe_amount + pi_amount
def get_balance_row(label, amount, account_currency):
diff --git a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js
index 84ffcaf4a36..21449be5e84 100644
--- a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js
+++ b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js
@@ -22,7 +22,7 @@ frappe.query_reports["Profit and Loss Statement"]["filters"].push(
fieldname: "accumulated_values",
label: __("Accumulated Values"),
fieldtype: "Check",
- default: 1,
+ default: 0,
},
{
fieldname: "include_default_book_entries",
diff --git a/erpnext/accounts/report/purchase_invoice_trends/purchase_invoice_trends.json b/erpnext/accounts/report/purchase_invoice_trends/purchase_invoice_trends.json
index 2080f51933a..37556b6b4c2 100644
--- a/erpnext/accounts/report/purchase_invoice_trends/purchase_invoice_trends.json
+++ b/erpnext/accounts/report/purchase_invoice_trends/purchase_invoice_trends.json
@@ -7,10 +7,10 @@
"docstatus": 0,
"doctype": "Report",
"filters": [],
- "idx": 3,
+ "idx": 4,
"is_standard": "Yes",
- "letterhead": null,
- "modified": "2025-11-05 11:55:49.950442",
+ "letter_head": null,
+ "modified": "2026-03-13 17:35:39.703838",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Trends",
diff --git a/erpnext/accounts/report/sales_invoice_trends/sales_invoice_trends.json b/erpnext/accounts/report/sales_invoice_trends/sales_invoice_trends.json
index 1ed34ff4c36..93aa6567f0c 100644
--- a/erpnext/accounts/report/sales_invoice_trends/sales_invoice_trends.json
+++ b/erpnext/accounts/report/sales_invoice_trends/sales_invoice_trends.json
@@ -9,8 +9,8 @@
"filters": [],
"idx": 3,
"is_standard": "Yes",
- "letterhead": null,
- "modified": "2025-11-05 11:55:50.070651",
+ "letter_head": null,
+ "modified": "2026-03-13 17:36:13.725601",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Trends",
diff --git a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py
index 9fb40938d59..d93c60b2cf4 100644
--- a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py
+++ b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py
@@ -5,6 +5,7 @@
import frappe
from frappe import _
from frappe.utils import flt, getdate
+from pypika import Tuple
from erpnext.accounts.utils import get_currency_precision
@@ -19,18 +20,14 @@ def execute(filters=None):
validate_filters(filters)
(
- tds_docs,
tds_accounts,
tax_category_map,
- journal_entry_party_map,
net_total_map,
) = get_tds_docs(filters)
columns = get_columns(filters)
- res = get_result(
- filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, net_total_map
- )
+ res = get_result(filters, tds_accounts, tax_category_map, net_total_map)
return columns, res
@@ -41,27 +38,23 @@ def validate_filters(filters):
frappe.throw(_("From Date must be before To Date"))
-def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, net_total_map):
- party_map = get_party_pan_map(filters.get("party_type"))
+def get_result(filters, tds_accounts, tax_category_map, net_total_map):
+ party_names = {v.party for v in net_total_map.values() if v.party}
+ party_map = get_party_pan_map(filters.get("party_type"), party_names)
tax_rate_map = get_tax_rate_map(filters)
- gle_map = get_gle_map(tds_docs)
+ gle_map = get_gle_map(net_total_map)
precision = get_currency_precision()
- out = []
entries = {}
- for name, details in gle_map.items():
+ for (voucher_type, name), details in gle_map.items():
for entry in details:
tax_amount, total_amount, grand_total, base_total, base_tax_withholding_net_total = 0, 0, 0, 0, 0
tax_withholding_category, rate = None, None
bill_no, bill_date = "", ""
- party = entry.party or entry.against
posting_date = entry.posting_date
- voucher_type = entry.voucher_type
- if voucher_type == "Journal Entry":
- party_list = journal_entry_party_map.get(name)
- if party_list:
- party = party_list[0]
+ values = net_total_map.get((voucher_type, name))
+ party = values.party if values else (entry.party or entry.against)
if entry.account in tds_accounts.keys():
tax_amount += entry.credit - entry.debit
@@ -76,12 +69,13 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
rate = get_tax_withholding_rates(tax_rate_map.get(tax_withholding_category, []), posting_date)
- values = net_total_map.get((voucher_type, name))
-
if values:
if voucher_type == "Journal Entry" and tax_amount and rate:
# back calculate total amount from rate and tax_amount
- base_total = min(flt(tax_amount / (rate / 100), precision=precision), values[0])
+ base_total = min(
+ flt(tax_amount / (rate / 100), precision=precision),
+ values.base_tax_withholding_net_total,
+ )
total_amount = grand_total = base_total
base_tax_withholding_net_total = total_amount
@@ -90,16 +84,16 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
# back calculate total amount from rate and tax_amount
total_amount = flt((tax_amount * 100) / rate, precision=precision)
else:
- total_amount = values[0]
+ total_amount = values.base_tax_withholding_net_total
- grand_total = values[1]
- base_total = values[2]
+ grand_total = values.grand_total
+ base_total = values.base_total
base_tax_withholding_net_total = total_amount
if voucher_type == "Purchase Invoice":
- base_tax_withholding_net_total = values[0]
- bill_no = values[3]
- bill_date = values[4]
+ base_tax_withholding_net_total = values.base_tax_withholding_net_total
+ bill_no = values.bill_no
+ bill_date = values.bill_date
else:
total_amount += entry.credit
@@ -147,14 +141,17 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
else:
entries[key] = row
out = list(entries.values())
- out.sort(key=lambda x: (x["section_code"], x["transaction_date"]))
+ out.sort(key=lambda x: (x["section_code"], x["transaction_date"], x["ref_no"]))
return out
-def get_party_pan_map(party_type):
+def get_party_pan_map(party_type, party_names):
party_map = frappe._dict()
+ if not party_names:
+ return party_map
+
fields = ["name", "tax_withholding_category"]
if party_type == "Supplier":
fields += ["supplier_type", "supplier_name"]
@@ -164,7 +161,7 @@ def get_party_pan_map(party_type):
if frappe.db.has_column(party_type, "pan"):
fields.append("pan")
- party_details = frappe.db.get_all(party_type, fields=fields)
+ party_details = frappe.db.get_all(party_type, filters={"name": ("in", list(party_names))}, fields=fields)
for party in party_details:
party.party_type = party_type
@@ -173,22 +170,33 @@ def get_party_pan_map(party_type):
return party_map
-def get_gle_map(documents):
- # create gle_map of the form
- # {"purchase_invoice": list of dict of all gle created for this invoice}
+def get_gle_map(net_total_map):
+ if not net_total_map:
+ return {}
+
+ gle = frappe.qb.DocType("GL Entry")
+ voucher_pairs = list(net_total_map.keys())
+
+ rows = (
+ frappe.qb.from_(gle)
+ .select(
+ gle.credit,
+ gle.debit,
+ gle.account,
+ gle.voucher_no,
+ gle.posting_date,
+ gle.voucher_type,
+ gle.against,
+ gle.party,
+ gle.party_type,
+ )
+ .where(gle.is_cancelled == 0)
+ .where(Tuple(gle.voucher_type, gle.voucher_no).isin(voucher_pairs))
+ ).run(as_dict=True)
+
gle_map = {}
-
- gle = frappe.db.get_all(
- "GL Entry",
- {"voucher_no": ["in", documents], "is_cancelled": 0},
- ["credit", "debit", "account", "voucher_no", "posting_date", "voucher_type", "against", "party"],
- )
-
- for d in gle:
- if d.voucher_no not in gle_map:
- gle_map[d.voucher_no] = [d]
- else:
- gle_map[d.voucher_no].append(d)
+ for d in rows:
+ gle_map.setdefault((d.voucher_type, d.voucher_no), []).append(d)
return gle_map
@@ -308,14 +316,9 @@ def get_columns(filters):
def get_tds_docs(filters):
- tds_documents = []
- purchase_invoices = []
- sales_invoices = []
- payment_entries = []
- journal_entries = []
+ vouchers = frappe._dict()
tax_category_map = frappe._dict()
net_total_map = frappe._dict()
- journal_entry_party_map = frappe._dict()
bank_accounts = frappe.get_all("Account", {"is_group": 0, "account_type": "Bank"}, pluck="name")
_tds_accounts = frappe.get_all(
@@ -334,35 +337,14 @@ def get_tds_docs(filters):
tds_docs = get_tds_docs_query(filters, bank_accounts, list(tds_accounts.keys())).run(as_dict=True)
for d in tds_docs:
- if d.voucher_type == "Purchase Invoice":
- purchase_invoices.append(d.voucher_no)
- if d.voucher_type == "Sales Invoice":
- sales_invoices.append(d.voucher_no)
- elif d.voucher_type == "Payment Entry":
- payment_entries.append(d.voucher_no)
- elif d.voucher_type == "Journal Entry":
- journal_entries.append(d.voucher_no)
+ vouchers.setdefault(d.voucher_type, set()).add(d.voucher_no)
- tds_documents.append(d.voucher_no)
-
- if purchase_invoices:
- get_doc_info(purchase_invoices, "Purchase Invoice", tax_category_map, net_total_map)
-
- if sales_invoices:
- get_doc_info(sales_invoices, "Sales Invoice", tax_category_map, net_total_map)
-
- if payment_entries:
- get_doc_info(payment_entries, "Payment Entry", tax_category_map, net_total_map)
-
- if journal_entries:
- journal_entry_party_map = get_journal_entry_party_map(journal_entries)
- get_doc_info(journal_entries, "Journal Entry", tax_category_map, net_total_map)
+ for voucher_type, docs in vouchers.items():
+ get_doc_info(docs, voucher_type, tax_category_map, net_total_map, filters)
return (
- tds_documents,
tds_accounts,
tax_category_map,
- journal_entry_party_map,
net_total_map,
)
@@ -373,11 +355,16 @@ def get_tds_docs_query(filters, bank_accounts, tds_accounts):
_("No {0} Accounts found for this company.").format(frappe.bold(_("Tax Withholding"))),
title=_("Accounts Missing Error"),
)
+
+ invoice_voucher = "Purchase Invoice" if filters.get("party_type") == "Supplier" else "Sales Invoice"
+ voucher_types = {"Payment Entry", "Journal Entry", invoice_voucher}
+
gle = frappe.qb.DocType("GL Entry")
query = (
frappe.qb.from_(gle)
.select("voucher_no", "voucher_type", "against", "party")
.where(gle.is_cancelled == 0)
+ .where(gle.voucher_type.isin(voucher_types))
)
if filters.get("from_date"):
@@ -403,25 +390,27 @@ def get_tds_docs_query(filters, bank_accounts, tds_accounts):
return query
-def get_journal_entry_party_map(journal_entries):
+def get_journal_entry_party_map(journal_entries, party_type):
journal_entry_party_map = {}
for d in frappe.db.get_all(
"Journal Entry Account",
{
"parent": ("in", journal_entries),
- "party_type": ("in", ("Supplier", "Customer")),
+ "party_type": party_type,
"party": ("is", "set"),
},
["parent", "party"],
):
- if d.parent not in journal_entry_party_map:
- journal_entry_party_map[d.parent] = []
- journal_entry_party_map[d.parent].append(d.party)
+ journal_entry_party_map.setdefault(d.parent, []).append(d.party)
return journal_entry_party_map
-def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
+def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None, filters=None):
+ journal_entry_party_map = {}
+ party_type = filters.get("party_type") if filters else None
+ party = filters.get("party") if filters else None
+
common_fields = ["name"]
fields_dict = {
"Purchase Invoice": [
@@ -431,37 +420,81 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
"base_total",
"bill_no",
"bill_date",
+ "supplier",
],
- "Sales Invoice": ["base_net_total", "grand_total", "base_total"],
+ "Sales Invoice": ["base_net_total", "grand_total", "base_total", "customer"],
"Payment Entry": [
"tax_withholding_category",
"paid_amount",
"paid_amount_after_tax",
"base_paid_amount",
+ "party",
+ "party_type",
],
"Journal Entry": ["tax_withholding_category", "total_debit"],
}
+ party_field = {
+ "Purchase Invoice": "supplier",
+ "Sales Invoice": "customer",
+ "Payment Entry": "party",
+ }
- entries = frappe.get_all(
- doctype, filters={"name": ("in", vouchers)}, fields=common_fields + fields_dict[doctype]
- )
+ doc_filters = {"name": ("in", vouchers)}
+
+ if party and party_field.get(doctype):
+ doc_filters[party_field[doctype]] = party
+
+ if doctype == "Payment Entry":
+ doc_filters["party_type"] = party_type
+
+ entries = frappe.get_all(doctype, filters=doc_filters, fields=common_fields + fields_dict[doctype])
+
+ if doctype == "Journal Entry":
+ journal_entry_party_map = get_journal_entry_party_map(vouchers, party_type=party_type)
for entry in entries:
tax_category_map[(doctype, entry.name)] = entry.tax_withholding_category
+
+ value = frappe._dict(
+ party=None,
+ party_type=party_type,
+ base_tax_withholding_net_total=0,
+ grand_total=0,
+ base_total=0,
+ bill_no="",
+ bill_date="",
+ )
+
if doctype == "Purchase Invoice":
- value = [
- entry.base_tax_withholding_net_total,
- entry.grand_total,
- entry.base_total,
- entry.bill_no,
- entry.bill_date,
- ]
+ value.party = entry.supplier
+ value.party_type = "Supplier"
+ value.base_tax_withholding_net_total = entry.base_tax_withholding_net_total
+ value.grand_total = entry.grand_total
+ value.base_total = entry.base_total
+ value.bill_no = entry.bill_no
+ value.bill_date = entry.bill_date
elif doctype == "Sales Invoice":
- value = [entry.base_net_total, entry.grand_total, entry.base_total]
+ value.party = entry.customer
+ value.party_type = "Customer"
+ value.base_tax_withholding_net_total = entry.base_net_total
+ value.grand_total = entry.grand_total
+ value.base_total = entry.base_total
elif doctype == "Payment Entry":
- value = [entry.paid_amount, entry.paid_amount_after_tax, entry.base_paid_amount]
+ value.party = entry.party
+ value.party_type = entry.party_type
+ value.base_tax_withholding_net_total = entry.paid_amount
+ value.grand_total = entry.paid_amount_after_tax
+ value.base_total = entry.base_paid_amount
else:
- value = [entry.total_debit] * 3
+ party_list = journal_entry_party_map.get(entry.name, [])
+ if party and party in party_list:
+ value.party = party
+ elif party_list:
+ value.party = sorted(party_list)[0]
+ value.party_type = party_type
+ value.base_tax_withholding_net_total = entry.total_debit
+ value.grand_total = entry.total_debit
+ value.base_total = entry.total_debit
net_total_map[(doctype, entry.name)] = value
diff --git a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py
index 1b5292f6bde..cbceaeed092 100644
--- a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py
+++ b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py
@@ -20,16 +20,12 @@ def execute(filters=None):
columns = get_columns(filters)
(
- tds_docs,
tds_accounts,
tax_category_map,
- journal_entry_party_map,
- invoice_total_map,
+ net_total_map,
) = get_tds_docs(filters)
- res = get_result(
- filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, invoice_total_map
- )
+ res = get_result(filters, tds_accounts, tax_category_map, net_total_map)
final_result = group_by_party_and_category(res, filters)
return columns, final_result
diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js
index f81b26b2c8d..a8802c1d67e 100644
--- a/erpnext/assets/doctype/asset/asset.js
+++ b/erpnext/assets/doctype/asset/asset.js
@@ -36,6 +36,7 @@ frappe.ui.form.on("Asset", {
},
company: function (frm) {
+ frm.trigger("set_dynamic_labels");
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
@@ -87,6 +88,7 @@ frappe.ui.form.on("Asset", {
},
refresh: function (frm) {
+ frm.trigger("set_dynamic_labels");
frappe.ui.form.trigger("Asset", "is_existing_asset");
frm.toggle_display("next_depreciation_date", frm.doc.docstatus < 1);
@@ -221,6 +223,10 @@ frappe.ui.form.on("Asset", {
}
},
+ set_dynamic_labels: function (frm) {
+ frm.set_currency_labels(["gross_purchase_amount"], erpnext.get_currency(frm.doc.company));
+ },
+
set_depr_posting_failure_alert: function (frm) {
const alert = `
diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json
index e797415ee3e..79a5099b730 100644
--- a/erpnext/assets/doctype/asset/asset.json
+++ b/erpnext/assets/doctype/asset/asset.json
@@ -229,7 +229,7 @@
"fieldtype": "Currency",
"label": "Net Purchase Amount",
"mandatory_depends_on": "eval:(!doc.is_composite_asset || doc.docstatus==1)",
- "options": "Company:company:default_currency",
+ "options": "currency",
"read_only_depends_on": "eval: doc.is_composite_asset"
},
{
@@ -597,7 +597,7 @@
"link_fieldname": "target_asset"
}
],
- "modified": "2025-12-23 16:01:10.195932",
+ "modified": "2026-03-13 12:15:25.734623",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js
index 646a7eee7ef..7f7d045ac92 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.js
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.js
@@ -131,7 +131,7 @@ frappe.ui.form.on("Asset Repair", {
function () {
frappe.route_options = {
voucher_no: frm.doc.name,
- from_date: frm.doc.posting_date,
+ from_date: moment(frm.doc.completion_date).format("YYYY-MM-DD"),
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
company: frm.doc.company,
categorize_by: "",
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js
index 85095e66a57..e55ebb52b58 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.js
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.js
@@ -769,10 +769,6 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
items_on_form_rendered() {
set_schedule_date(this.frm);
}
-
- schedule_date() {
- set_schedule_date(this.frm);
- }
};
// for backward compatibility: combine new and previous states
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
index d9d4d7ea1cc..670231653cf 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
@@ -165,14 +165,10 @@ frappe.ui.form.on("Request for Quotation", {
},
show_supplier_quotation_comparison(frm) {
- const today = new Date();
- const oneMonthAgo = new Date(today);
- oneMonthAgo.setMonth(today.getMonth() - 1);
-
frappe.route_options = {
company: frm.doc.company,
- from_date: moment(oneMonthAgo).format("YYYY-MM-DD"),
- to_date: moment(today).format("YYYY-MM-DD"),
+ from_date: moment(frm.doc.transaction_date).format("YYYY-MM-DD"),
+ to_date: moment(new Date()).format("YYYY-MM-DD"),
request_for_quotation: frm.doc.name,
};
frappe.set_route("query-report", "Supplier Quotation Comparison");
diff --git a/erpnext/buying/report/purchase_order_trends/purchase_order_trends.json b/erpnext/buying/report/purchase_order_trends/purchase_order_trends.json
index 0047d6ecbe5..e53b8e6d669 100644
--- a/erpnext/buying/report/purchase_order_trends/purchase_order_trends.json
+++ b/erpnext/buying/report/purchase_order_trends/purchase_order_trends.json
@@ -9,8 +9,8 @@
"filters": [],
"idx": 3,
"is_standard": "Yes",
- "letterhead": null,
- "modified": "2025-11-05 11:55:50.058154",
+ "letter_head": null,
+ "modified": "2026-03-13 17:36:05.561765",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Trends",
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 008402eeb53..ec2b5caf9d2 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -2502,13 +2502,14 @@ class AccountsController(TransactionBase):
grand_total = self.get("rounded_total") or self.grand_total
automatically_fetch_payment_terms = 0
- if self.doctype in ("Sales Invoice", "Purchase Invoice"):
- base_grand_total = base_grand_total - flt(self.base_write_off_amount)
- grand_total = grand_total - flt(self.write_off_amount)
+ if self.doctype in ("Sales Invoice", "Purchase Invoice", "Sales Order"):
po_or_so, doctype, fieldname = self.get_order_details()
automatically_fetch_payment_terms = cint(
frappe.db.get_single_value("Accounts Settings", "automatically_fetch_payment_terms")
)
+ if self.doctype != "Sales Order":
+ base_grand_total = base_grand_total - flt(self.base_write_off_amount)
+ grand_total = grand_total - flt(self.write_off_amount)
if self.get("total_advance"):
if party_account_currency == self.company_currency:
@@ -2524,7 +2525,7 @@ class AccountsController(TransactionBase):
if not self.get("payment_schedule"):
if (
- self.doctype in ["Sales Invoice", "Purchase Invoice"]
+ self.doctype in ["Sales Invoice", "Purchase Invoice", "Sales Order"]
and automatically_fetch_payment_terms
and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype)
):
@@ -2579,17 +2580,23 @@ class AccountsController(TransactionBase):
self.ignore_default_payment_terms_template = 1
def get_order_details(self):
+ if not self.get("items"):
+ return None, None, None
if self.doctype == "Sales Invoice":
- po_or_so = self.get("items") and self.get("items")[0].get("sales_order")
- po_or_so_doctype = "Sales Order"
- po_or_so_doctype_name = "sales_order"
-
+ prev_doc = self.get("items")[0].get("sales_order")
+ prev_doctype = "Sales Order"
+ prev_doctype_name = "sales_order"
+ elif self.doctype == "Purchase Invoice":
+ prev_doc = self.get("items")[0].get("purchase_order")
+ prev_doctype = "Purchase Order"
+ prev_doctype_name = "purchase_order"
+ elif self.doctype == "Sales Order":
+ prev_doc = self.get("items")[0].get("prevdoc_docname")
+ prev_doctype = "Quotation"
+ prev_doctype_name = "prevdoc_docname"
else:
- po_or_so = self.get("items") and self.get("items")[0].get("purchase_order")
- po_or_so_doctype = "Purchase Order"
- po_or_so_doctype_name = "purchase_order"
-
- return po_or_so, po_or_so_doctype, po_or_so_doctype_name
+ return None, None, None
+ return prev_doc, prev_doctype, prev_doctype_name
def linked_order_has_payment_terms(self, po_or_so, fieldname, doctype):
if po_or_so and self.all_items_have_same_po_or_so(po_or_so, fieldname):
@@ -2685,7 +2692,9 @@ class AccountsController(TransactionBase):
for d in self.get("payment_schedule"):
d.validate_from_to_dates("discount_date", "due_date")
- if self.doctype == "Sales Order" and getdate(d.due_date) < getdate(self.transaction_date):
+ if self.doctype in ["Sales Order", "Quotation"] and getdate(d.due_date) < getdate(
+ self.transaction_date
+ ):
frappe.throw(
_("Row {0}: Due Date in the Payment Terms table cannot be before Posting Date").format(
d.idx
diff --git a/erpnext/controllers/item_variant.py b/erpnext/controllers/item_variant.py
index 5dace4af884..0ba090956ca 100644
--- a/erpnext/controllers/item_variant.py
+++ b/erpnext/controllers/item_variant.py
@@ -360,13 +360,13 @@ def copy_attributes_to_variant(item, variant):
else:
if item.variant_based_on == "Item Attribute":
if variant.attributes:
- attributes_description = item.description + " "
+ attributes_description = item.description or ""
for d in variant.attributes:
attributes_description += (
"
" + d.attribute + ": " + cstr(d.attribute_value) + "
"
)
- if attributes_description not in variant.description:
+ if attributes_description not in (variant.description or ""):
variant.description = attributes_description
diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py
index 12d5229c9f3..75fbd5d60a5 100644
--- a/erpnext/controllers/queries.py
+++ b/erpnext/controllers/queries.py
@@ -15,6 +15,7 @@ from frappe.utils import cint, nowdate, today, unique
from pypika import Order
import erpnext
+from erpnext.accounts.utils import build_qb_match_conditions
from erpnext.stock.get_item_details import _get_item_tax_template
@@ -608,34 +609,37 @@ def get_blanket_orders(doctype, txt, searchfield, start, page_len, filters):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_income_account(doctype, txt, searchfield, start, page_len, filters):
- from erpnext.controllers.queries import get_match_cond
-
# income account can be any Credit account,
# but can also be a Asset account with account_type='Income Account' in special circumstances.
# Hence the first condition is an "OR"
+
if not filters:
filters = {}
- doctype = "Account"
- condition = ""
+ dt = "Account"
+
+ acc = qb.DocType(dt)
+ condition = [
+ (acc.report_type.eq("Profit and Loss") | acc.account_type.isin(["Income Account", "Temporary"])),
+ acc.is_group.eq(0),
+ acc.disabled.eq(0),
+ ]
+ if txt:
+ condition.append(acc.name.like(f"%{txt}%"))
+
if filters.get("company"):
- condition += "and tabAccount.company = %(company)s"
+ condition.append(acc.company.eq(filters.get("company")))
- condition += " and tabAccount.disabled = %(disabled)s"
+ user_perms = build_qb_match_conditions(dt)
+ condition.extend(user_perms)
- return frappe.db.sql(
- f"""select tabAccount.name from `tabAccount`
- where (tabAccount.report_type = "Profit and Loss"
- or tabAccount.account_type in ("Income Account", "Temporary"))
- and tabAccount.is_group=0
- and tabAccount.`{searchfield}` LIKE %(txt)s
- {condition} {get_match_cond(doctype)}
- order by idx desc, name""",
- {
- "txt": "%" + txt + "%",
- "company": filters.get("company", ""),
- "disabled": cint(filters.get("disabled", 0)),
- },
+ return (
+ qb.from_(acc)
+ .select(acc.name)
+ .where(Criterion.all(condition))
+ .orderby(acc.idx, order=Order.desc)
+ .orderby(acc.name)
+ .run()
)
@@ -696,26 +700,38 @@ def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters,
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_expense_account(doctype, txt, searchfield, start, page_len, filters):
- from erpnext.controllers.queries import get_match_cond
-
if not filters:
filters = {}
- doctype = "Account"
- condition = ""
- if filters.get("company"):
- condition += "and tabAccount.company = %(company)s"
+ dt = "Account"
- return frappe.db.sql(
- f"""select tabAccount.name from `tabAccount`
- where (tabAccount.report_type = "Profit and Loss"
- or tabAccount.account_type in ("Expense Account", "Fixed Asset", "Temporary", "Asset Received But Not Billed", "Capital Work in Progress"))
- and tabAccount.is_group=0
- and tabAccount.disabled = 0
- and tabAccount.{searchfield} LIKE %(txt)s
- {condition} {get_match_cond(doctype)}""",
- {"company": filters.get("company", ""), "txt": "%" + txt + "%"},
- )
+ acc = qb.DocType(dt)
+ condition = [
+ (
+ acc.report_type.eq("Profit and Loss")
+ | acc.account_type.isin(
+ [
+ "Expense Account",
+ "Fixed Asset",
+ "Temporary",
+ "Asset Received But Not Billed",
+ "Capital Work in Progress",
+ ]
+ )
+ ),
+ acc.is_group.eq(0),
+ acc.disabled.eq(0),
+ ]
+ if txt:
+ condition.append(acc.name.like(f"%{txt}%"))
+
+ if filters.get("company"):
+ condition.append(acc.company.eq(filters.get("company")))
+
+ user_perms = build_qb_match_conditions(dt)
+ condition.extend(user_perms)
+
+ return qb.from_(acc).select(acc.name).where(Criterion.all(condition)).run()
@frappe.whitelist()
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index f1184851c20..15e595a3ca1 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -531,7 +531,6 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
target_doc.against_sales_order = source_doc.against_sales_order
target_doc.against_sales_invoice = source_doc.against_sales_invoice
target_doc.so_detail = source_doc.so_detail
- target_doc.si_detail = source_doc.si_detail
target_doc.expense_account = source_doc.expense_account
target_doc.dn_detail = source_doc.name
if default_warehouse_for_sales_return:
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index acdea69cf22..20c8a72290b 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -511,6 +511,9 @@ class SellingController(StockController):
if self.doctype not in ("Delivery Note", "Sales Invoice"):
return
+ if self.doctype == "Sales Invoice" and not self.update_stock and not self.is_internal_transfer():
+ return
+
from erpnext.stock.serial_batch_bundle import get_batch_nos, get_serial_nos
allow_at_arms_length_price = frappe.get_cached_value(
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index 549493d4b92..ae60bcb1ca8 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -281,9 +281,8 @@ class JobCard(Document):
# if key number reaches/crosses to production_capacity means capacity is full and overlap error generated
# this will store last to_time of sequential job cards
alloted_capacity = {1: time_logs[0]["to_time"]}
- # flag for sequential Job card found
- sequential_job_card_found = False
for i in range(1, len(time_logs)):
+ sequential_job_card_found = False
# scanning for all Existing keys
for key in alloted_capacity.keys():
# if current Job Card from time is greater than last to_time in that key means these job card are sequential
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 351ba27a43e..d2ca6ff73fd 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -1784,7 +1784,7 @@ def get_item_data(item_code):
return {
"bom_no": item_details.get("bom_no"),
"stock_uom": item_details.get("stock_uom"),
- # "description": item_details.get("description")
+ "description": item_details.get("description"),
}
@@ -1800,6 +1800,7 @@ def get_sub_assembly_items(
skip_available_sub_assembly_item=False,
):
data = get_bom_children(parent=bom_no)
+ precision = frappe.get_precision("Production Plan Sub Assembly Item", "qty")
for d in data:
if d.expandable:
parent_item_code = frappe.get_cached_value("BOM", bom_no, "item")
@@ -1837,7 +1838,7 @@ def get_sub_assembly_items(
"is_sub_contracted_item": d.is_sub_contracted_item,
"bom_level": indent,
"indent": indent,
- "stock_qty": stock_qty,
+ "stock_qty": flt(stock_qty, precision),
}
)
)
diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py
index 326a8b37efc..510e69cd272 100644
--- a/erpnext/manufacturing/doctype/workstation/workstation.py
+++ b/erpnext/manufacturing/doctype/workstation/workstation.py
@@ -79,9 +79,6 @@ class Workstation(Document):
self.total_working_hours += row.hours
def validate_working_hours(self, row):
- if not (row.start_time and row.end_time):
- frappe.throw(_("Row #{0}: Start Time and End Time are required").format(row.idx))
-
if get_time(row.start_time) >= get_time(row.end_time):
frappe.throw(_("Row #{0}: Start Time must be before End Time").format(row.idx))
diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js
index c8a61a0ca79..37c83483c18 100644
--- a/erpnext/public/js/controllers/buying.js
+++ b/erpnext/public/js/controllers/buying.js
@@ -140,6 +140,7 @@ erpnext.buying = {
this.toggle_subcontracting_fields();
super.refresh();
+ this.prevent_past_schedule_dates(this.frm);
}
toggle_subcontracting_fields() {
@@ -183,6 +184,28 @@ erpnext.buying = {
erpnext.utils.set_letter_head(this.frm)
}
+ schedule_date(doc, cdt, cdn) {
+ if (doc.schedule_date && !cdt.endsWith(" Item")) {
+ doc.items.forEach((d) => {
+ frappe.model.set_value(d.doctype, d.name, "schedule_date", doc.schedule_date);
+ });
+ }
+ }
+
+ transaction_date() {
+ super.transaction_date();
+ this.frm.set_value("schedule_date", "");
+ this.prevent_past_schedule_dates(this.frm);
+ }
+
+ prevent_past_schedule_dates(frm) {
+ if (frm.doc.transaction_date && frm.fields_dict["schedule_date"]) {
+ frm.fields_dict["schedule_date"].datepicker?.update({
+ minDate: new Date(frm.doc.transaction_date),
+ });
+ }
+ }
+
supplier_address() {
erpnext.utils.get_address_display(this.frm);
erpnext.utils.set_taxes_from_address(this.frm, "supplier_address", "supplier_address", "supplier_address");
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index ef727eec8d8..b0b1281aeff 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -1052,6 +1052,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
if (this.frm.doc.transaction_date) {
this.frm.transaction_date = this.frm.doc.transaction_date;
frappe.ui.form.trigger(this.frm.doc.doctype, "currency");
+ this.recalculate_terms();
}
}
diff --git a/erpnext/public/js/utils/landed_taxes_and_charges_common.js b/erpnext/public/js/utils/landed_taxes_and_charges_common.js
index 7d801ca91e6..751d831a6f7 100644
--- a/erpnext/public/js/utils/landed_taxes_and_charges_common.js
+++ b/erpnext/public/js/utils/landed_taxes_and_charges_common.js
@@ -42,9 +42,9 @@ erpnext.landed_cost_taxes_and_charges = {
if (row.account_currency == company_currency) {
row.exchange_rate = 1;
- frm.set_df_property("taxes", "hidden", 1, row.name, "exchange_rate");
+ frm.set_df_property("taxes", "hidden", 1, frm.docname, "exchange_rate", cdn);
} else if (!row.exchange_rate || row.exchange_rate == 1) {
- frm.set_df_property("taxes", "hidden", 0, row.name, "exchange_rate");
+ frm.set_df_property("taxes", "hidden", 0, frm.docname, "exchange_rate", cdn);
frappe.call({
method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_exchange_rate",
args: {
diff --git a/erpnext/regional/italy/e-invoice.xml b/erpnext/regional/italy/e-invoice.xml
index 7c436a2b449..ef1e94ff27b 100644
--- a/erpnext/regional/italy/e-invoice.xml
+++ b/erpnext/regional/italy/e-invoice.xml
@@ -18,25 +18,27 @@
{{ address.country_code }}
{%- endmacro %}
-{%- macro render_discount_or_margin(item) -%}
-{%- if (item.discount_percentage and item.discount_percentage > 0.0) or item.margin_type %}
+{%- macro render_discount_or_margin(item, tax_divisor) -%}
+{%- if item.discount_percentage and item.discount_percentage > 0.0 %}
- {%- if item.discount_percentage > 0.0 %}
SC
{{ format_float(item.discount_percentage) }}
- {%- endif %}
- {%- if item.margin_rate_or_amount > 0.0 -%}
- MG
- {%- if item.margin_type == "Percentage" -%}
- {{ format_float(item.margin_rate_or_amount) }}
- {%- elif item.margin_type == "Amount" -%}
- {{ format_float(item.margin_rate_or_amount) }}
- {%- endif -%}
- {%- endif %}
-{%- endif -%}
+{%- endif %}
+{%- if item.margin_rate_or_amount and item.margin_rate_or_amount > 0.0 %}
+
+ MG
+ {%- if item.margin_type == "Percentage" -%}
+ {{ format_float(item.margin_rate_or_amount) }}
+ {%- elif item.margin_type == "Amount" -%}
+ {{ format_float(item.margin_rate_or_amount / tax_divisor) }}
+ {%- endif -%}
+
+{%- endif %}
{%- endmacro -%}
+{%- set has_inclusive_tax = doc.taxes | selectattr("included_in_print_rate") | list | length > 0 -%}
+
{%- for item in doc.e_invoice_items %}
+ {%- set tax_divisor = (1 + item.tax_rate / 100) if has_inclusive_tax and item.tax_rate else 1 %}
{{ item.idx }}
@@ -188,8 +191,9 @@
{{ html2text(item.description or '') or item.item_name }}
{{ format_float(item.qty) }}
{{ item.stock_uom }}
- {{ format_float(item.net_rate or item.price_list_rate or item.rate, item_meta.get_field("rate").precision) }}
- {{ render_discount_or_margin(item) }}
+ {%- set item_unit_net_price = (item.price_list_rate / tax_divisor) or (item.net_rate) or (item.rate / tax_divisor) %}
+ {{ format_float(item_unit_net_price, item_meta.get_field("rate").precision) }}
+ {{ render_discount_or_margin(item, tax_divisor) }}
{{ format_float(item.net_amount, item_meta.get_field("amount").precision) }}
{{ format_float(item.tax_rate, item_meta.get_field("tax_rate").precision) }}
{%- if item.tax_exemption_reason %}
diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py
index 7a31854d259..b4e433ac805 100644
--- a/erpnext/selling/doctype/quotation/quotation.py
+++ b/erpnext/selling/doctype/quotation/quotation.py
@@ -7,7 +7,7 @@ import json
import frappe
from frappe import _
from frappe.model.mapper import get_mapped_doc
-from frappe.utils import flt, getdate, nowdate
+from frappe.utils import cint, flt, getdate, nowdate
from erpnext.controllers.selling_controller import SellingController
@@ -442,6 +442,10 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False, ar
child_filter = d.name in filtered_items if filtered_items else True
return child_filter
+ automatically_fetch_payment_terms = cint(
+ frappe.get_single_value("Accounts Settings", "automatically_fetch_payment_terms")
+ )
+
doclist = get_mapped_doc(
"Quotation",
source_name,
@@ -449,6 +453,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False, ar
"Quotation": {
"doctype": "Sales Order",
"validation": {"docstatus": ["=", 1]},
+ "field_no_map": ["payment_terms_template"],
},
"Quotation Item": {
"doctype": "Sales Order Item",
@@ -458,13 +463,15 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False, ar
},
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True},
"Sales Team": {"doctype": "Sales Team", "add_if_empty": True},
- "Payment Schedule": {"doctype": "Payment Schedule", "add_if_empty": True},
},
target_doc,
set_missing_values,
ignore_permissions=ignore_permissions,
)
+ if automatically_fetch_payment_terms:
+ doclist.set_payment_schedule()
+
return doclist
diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py
index 2d1da049653..4d4d485c71a 100644
--- a/erpnext/selling/doctype/quotation/test_quotation.py
+++ b/erpnext/selling/doctype/quotation/test_quotation.py
@@ -175,6 +175,10 @@ class TestQuotation(FrappeTestCase):
self.assertTrue(quotation.payment_schedule)
+ @change_settings(
+ "Accounts Settings",
+ {"automatically_fetch_payment_terms": 1},
+ )
def test_make_sales_order_terms_copied(self):
from erpnext.selling.doctype.quotation.quotation import make_sales_order
@@ -317,7 +321,11 @@ class TestQuotation(FrappeTestCase):
@change_settings(
"Accounts Settings",
- {"add_taxes_from_item_tax_template": 0, "add_taxes_from_taxes_and_charges_template": 0},
+ {
+ "add_taxes_from_item_tax_template": 0,
+ "add_taxes_from_taxes_and_charges_template": 0,
+ "automatically_fetch_payment_terms": 1,
+ },
)
def test_make_sales_order_with_terms(self):
from erpnext.selling.doctype.quotation.quotation import make_sales_order
@@ -355,10 +363,13 @@ class TestQuotation(FrappeTestCase):
sales_order.save()
self.assertEqual(sales_order.payment_schedule[0].payment_amount, 8906.00)
- self.assertEqual(sales_order.payment_schedule[0].due_date, getdate(quotation.transaction_date))
+ self.assertEqual(
+ getdate(sales_order.payment_schedule[0].due_date), getdate(quotation.transaction_date)
+ )
self.assertEqual(sales_order.payment_schedule[1].payment_amount, 8906.00)
self.assertEqual(
- sales_order.payment_schedule[1].due_date, getdate(add_days(quotation.transaction_date, 30))
+ getdate(sales_order.payment_schedule[1].due_date),
+ getdate(add_days(quotation.transaction_date, 30)),
)
def test_valid_till_before_transaction_date(self):
@@ -1058,6 +1069,56 @@ class TestQuotation(FrappeTestCase):
quotation.reload()
self.assertEqual(quotation.status, "Open")
+ @change_settings(
+ "Accounts Settings",
+ {"automatically_fetch_payment_terms": 1},
+ )
+ def test_make_sales_order_with_payment_terms(self):
+ from erpnext.selling.doctype.quotation.quotation import make_sales_order
+
+ template = frappe.get_doc(
+ {
+ "doctype": "Payment Terms Template",
+ "template_name": "_Test Payment Terms Template for Quotation",
+ "terms": [
+ {
+ "doctype": "Payment Terms Template Detail",
+ "invoice_portion": 50.00,
+ "credit_days_based_on": "Day(s) after invoice date",
+ "credit_days": 0,
+ },
+ {
+ "doctype": "Payment Terms Template Detail",
+ "invoice_portion": 50.00,
+ "credit_days_based_on": "Day(s) after invoice date",
+ "credit_days": 10,
+ },
+ ],
+ }
+ ).save()
+
+ quotation = make_quotation(qty=10, rate=1000, do_not_submit=1)
+ quotation.transaction_date = add_days(nowdate(), -2)
+ quotation.valid_till = add_days(nowdate(), 10)
+ quotation.update({"payment_terms_template": template.name, "payment_schedule": []})
+ quotation.save()
+ quotation.submit()
+
+ self.assertEqual(quotation.payment_schedule[0].payment_amount, 5000)
+ self.assertEqual(quotation.payment_schedule[1].payment_amount, 5000)
+ self.assertEqual(quotation.payment_schedule[0].due_date, quotation.transaction_date)
+ self.assertEqual(quotation.payment_schedule[1].due_date, add_days(quotation.transaction_date, 10))
+
+ sales_order = make_sales_order(quotation.name)
+ sales_order.transaction_date = nowdate()
+ sales_order.delivery_date = nowdate()
+ sales_order.save()
+
+ self.assertEqual(sales_order.payment_schedule[0].due_date, sales_order.transaction_date)
+ self.assertEqual(sales_order.payment_schedule[1].due_date, add_days(sales_order.transaction_date, 10))
+ self.assertEqual(sales_order.payment_schedule[0].payment_amount, 5000)
+ self.assertEqual(sales_order.payment_schedule[1].payment_amount, 5000)
+
test_records = frappe.get_test_records("Quotation")
diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js
index bbad2fe4fae..38334cc29bc 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.js
+++ b/erpnext/selling/doctype/sales_order/sales_order.js
@@ -26,7 +26,7 @@ frappe.ui.form.on("Sales Order", {
let color;
if (!doc.qty && frm.doc.has_unit_price_items) {
color = "yellow";
- } else if (doc.stock_qty <= doc.delivered_qty) {
+ } else if (doc.stock_qty <= doc.actual_qty) {
color = "green";
} else {
color = "orange";
diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json
index 4bbdb20d311..e649b8e9383 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.json
+++ b/erpnext/selling/doctype/sales_order/sales_order.json
@@ -160,6 +160,7 @@
"language",
"additional_info_section",
"is_internal_customer",
+ "ignore_default_payment_terms_template",
"represents_company",
"column_break_152",
"source",
@@ -1484,9 +1485,9 @@
},
{
"default": "0",
- "depends_on": "eval:doc.order_type == 'Maintenance';",
"fieldname": "skip_delivery_note",
"fieldtype": "Check",
+ "hidden": 1,
"hide_days": 1,
"hide_seconds": 1,
"label": "Skip Delivery Note",
@@ -1665,13 +1666,21 @@
"fieldtype": "Data",
"is_virtual": 1,
"label": "Last Scanned Warehouse"
+ },
+ {
+ "default": "0",
+ "fieldname": "ignore_default_payment_terms_template",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "Ignore Default Payment Terms Template",
+ "read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 105,
"is_submittable": 1,
"links": [],
- "modified": "2026-02-06 11:06:16.092658",
+ "modified": "2026-03-06 15:33:49.059029",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order",
@@ -1750,4 +1759,4 @@
"title_field": "customer_name",
"track_changes": 1,
"track_seen": 1
-}
+}
\ No newline at end of file
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index 7ceba32232f..dbd7f406432 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -66,7 +66,6 @@ class SalesOrder(SellingController):
additional_discount_percentage: DF.Float
address_display: DF.SmallText | None
advance_paid: DF.Currency
- advance_payment_status: DF.Literal["Not Requested", "Requested", "Partially Paid", "Fully Paid"]
amended_from: DF.Link | None
amount_eligible_for_commission: DF.Currency
apply_discount_on: DF.Literal["", "Grand Total", "Net Total"]
@@ -111,6 +110,7 @@ class SalesOrder(SellingController):
grand_total: DF.Currency
group_same_items: DF.Check
has_unit_price_items: DF.Check
+ ignore_default_payment_terms_template: DF.Check
ignore_pricing_rule: DF.Check
in_words: DF.Data | None
incoterm: DF.Link | None
@@ -158,7 +158,6 @@ class SalesOrder(SellingController):
"",
"Draft",
"On Hold",
- "To Pay",
"To Deliver and Bill",
"To Bill",
"To Deliver",
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index 4fffd1f801e..d7a2f9d5e26 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -57,6 +57,45 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
def tearDown(self):
frappe.set_user("Administrator")
+ @change_settings(
+ "Stock Settings",
+ {
+ "auto_insert_price_list_rate_if_missing": 1,
+ "update_existing_price_list_rate": 1,
+ "update_price_list_based_on": "Rate",
+ },
+ )
+ def test_sales_order_expired_item_price(self):
+ price_list = "_Test Price List"
+
+ item_1 = make_item("_Test Expired Item 1", {"is_stock_item": 1})
+
+ frappe.db.delete("Item Price", {"item_code": item_1.item_code})
+
+ item_price = frappe.new_doc("Item Price")
+ item_price.item_code = item_1.item_code
+ item_price.price_list = price_list
+ item_price.price_list_rate = 100
+ item_price.valid_from = add_days(today(), -10)
+ item_price.valid_upto = add_days(today(), -5)
+ item_price.save()
+
+ so = make_sales_order(
+ item_code=item_1.item_code, qty=1, rate=1000, selling_price_list=price_list, do_not_save=True
+ )
+ so.save()
+ so.reload()
+
+ self.assertEqual(frappe.db.get_value("Item Price", item_price.name, "price_list_rate"), 100)
+ self.assertEqual(
+ frappe.db.count("Item Price", {"item_code": item_1.item_code, "price_list": price_list}),
+ 2,
+ )
+ all_item_prices = frappe.get_all(
+ "Item Price", filters={"item_code": item_1.item_code}, order_by="valid_from desc"
+ )
+ self.assertEqual(frappe.db.get_value("Item Price", all_item_prices[0].name, "price_list_rate"), 1000)
+
def test_sales_order_skip_delivery_note(self):
so = make_sales_order(do_not_submit=True)
so.order_type = "Maintenance"
diff --git a/erpnext/selling/report/quotation_trends/quotation_trends.json b/erpnext/selling/report/quotation_trends/quotation_trends.json
index a4011db4041..8722bf61fd7 100644
--- a/erpnext/selling/report/quotation_trends/quotation_trends.json
+++ b/erpnext/selling/report/quotation_trends/quotation_trends.json
@@ -9,8 +9,8 @@
"filters": [],
"idx": 3,
"is_standard": "Yes",
- "letterhead": null,
- "modified": "2025-11-05 11:55:50.127020",
+ "letter_head": null,
+ "modified": "2026-03-13 17:36:37.619715",
"modified_by": "Administrator",
"module": "Selling",
"name": "Quotation Trends",
diff --git a/erpnext/selling/report/sales_order_trends/sales_order_trends.json b/erpnext/selling/report/sales_order_trends/sales_order_trends.json
index dedec06bcf9..26758f5ab3f 100644
--- a/erpnext/selling/report/sales_order_trends/sales_order_trends.json
+++ b/erpnext/selling/report/sales_order_trends/sales_order_trends.json
@@ -9,8 +9,8 @@
"filters": [],
"idx": 3,
"is_standard": "Yes",
- "letterhead": null,
- "modified": "2025-11-05 11:55:50.096303",
+ "letter_head": null,
+ "modified": "2026-03-13 17:36:21.440118",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order Trends",
diff --git a/erpnext/setup/doctype/employee/employee.py b/erpnext/setup/doctype/employee/employee.py
index 14724ead051..543a8a194b8 100755
--- a/erpnext/setup/doctype/employee/employee.py
+++ b/erpnext/setup/doctype/employee/employee.py
@@ -185,13 +185,11 @@ class Employee(NestedSet):
throw(_("Please enter relieving date."))
def validate_for_enabled_user_id(self, enabled):
- if not self.status == "Active":
- return
-
if enabled is None:
frappe.throw(_("User {0} does not exist").format(self.user_id))
- if enabled == 0:
- frappe.throw(_("User {0} is disabled").format(self.user_id), EmployeeUserDisabledError)
+
+ if self.status != "Active" and enabled or self.status == "Active" and enabled == 0:
+ frappe.set_value("User", self.user_id, "enabled", not enabled)
def validate_duplicate_user_id(self):
Employee = frappe.qb.DocType("Employee")
diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py
index 69443e3a608..6c30b087de8 100644
--- a/erpnext/stock/deprecated_serial_batch.py
+++ b/erpnext/stock/deprecated_serial_batch.py
@@ -211,6 +211,7 @@ class DeprecatedBatchNoValuation:
.select(
sle.batch_no,
Sum(sle.actual_qty).as_("batch_qty"),
+ Sum(sle.stock_value_difference).as_("batch_value"),
)
.where(
(sle.item_code == self.sle.item_code)
@@ -227,9 +228,24 @@ class DeprecatedBatchNoValuation:
if self.sle.name:
query = query.where(sle.name != self.sle.name)
+ # Moving Average items with no Use Batch wise Valuation but want to use batch wise valuation
+ moving_avg_item_non_batch_value = False
+ if valuation_method := self.get_valuation_method(self.sle.item_code):
+ if valuation_method == "Moving Average" and not frappe.db.get_single_value(
+ "Stock Settings", "do_not_use_batchwise_valuation"
+ ):
+ query = query.where(batch.use_batchwise_valuation == 0)
+ moving_avg_item_non_batch_value = True
+
batch_data = query.run(as_dict=True)
for d in batch_data:
self.available_qty[d.batch_no] += flt(d.batch_qty)
+ if moving_avg_item_non_batch_value:
+ self.non_batchwise_balance_qty[d.batch_no] += flt(d.batch_qty)
+ self.non_batchwise_balance_value[d.batch_no] += flt(d.batch_value)
+
+ if moving_avg_item_non_batch_value:
+ return
for d in batch_data:
if self.available_qty.get(d.batch_no):
@@ -327,9 +343,24 @@ class DeprecatedBatchNoValuation:
query = query.where(bundle.voucher_type != "Pick List")
+ # Moving Average items with no Use Batch wise Valuation but want to use batch wise valuation
+ moving_avg_item_non_batch_value = False
+ if valuation_method := self.get_valuation_method(self.sle.item_code):
+ if valuation_method == "Moving Average" and not frappe.db.get_single_value(
+ "Stock Settings", "do_not_use_batchwise_valuation"
+ ):
+ query = query.where(batch.use_batchwise_valuation == 0)
+ moving_avg_item_non_batch_value = True
+
batch_data = query.run(as_dict=True)
for d in batch_data:
self.available_qty[d.batch_no] += flt(d.batch_qty)
+ if moving_avg_item_non_batch_value:
+ self.non_batchwise_balance_qty[d.batch_no] += flt(d.batch_qty)
+ self.non_batchwise_balance_value[d.batch_no] += flt(d.batch_value)
+
+ if moving_avg_item_non_batch_value:
+ return
if not self.last_sle:
return
@@ -337,3 +368,8 @@ class DeprecatedBatchNoValuation:
for batch_no in self.available_qty:
self.non_batchwise_balance_value[batch_no] = flt(self.last_sle.stock_value)
self.non_batchwise_balance_qty[batch_no] = flt(self.last_sle.qty_after_transaction)
+
+ def get_valuation_method(self, item_code):
+ from erpnext.stock.utils import get_valuation_method
+
+ return get_valuation_method(item_code)
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index b2dd23f80bd..f1fc54c751d 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -7,6 +7,7 @@ import json
import frappe
from frappe import _
from frappe.contacts.doctype.address.address import get_company_address
+from frappe.contacts.doctype.contact.contact import get_default_contact
from frappe.desk.notifications import clear_doctype_notifications
from frappe.model.mapper import get_mapped_doc
from frappe.model.utils import get_fetch_values
@@ -392,6 +393,9 @@ class DeliveryNote(SellingController):
)
def validate_sales_invoice_references(self):
+ if self.is_return:
+ return
+
self._validate_dependent_item_fields(
"against_sales_invoice", "si_detail", _("References to Sales Invoices are Incomplete")
)
@@ -1169,18 +1173,24 @@ def make_shipment(source_name, target_doc=None):
# As we are using session user details in the pickup_contact then pickup_contact_person will be session user
target.pickup_contact_person = frappe.session.user
- if source.contact_person:
+ contact_person = source.contact_person or get_default_contact("Customer", source.customer)
+ if contact_person:
contact = frappe.db.get_value(
- "Contact", source.contact_person, ["email_id", "phone", "mobile_no"], as_dict=1
+ "Contact", contact_person, ["email_id", "phone", "mobile_no"], as_dict=1
)
- delivery_contact_display = f"{source.contact_display}"
- if contact:
+
+ delivery_contact_display = source.contact_display or contact_person or ""
+ if contact and not source.contact_display:
if contact.email_id:
delivery_contact_display += "
" + contact.email_id
if contact.phone:
delivery_contact_display += "
" + contact.phone
if contact.mobile_no and not contact.phone:
delivery_contact_display += "
" + contact.mobile_no
+
+ target.delivery_contact_name = contact_person
+ if contact and contact.email_id and not target.delivery_contact_email:
+ target.delivery_contact_email = contact.email_id
target.delivery_contact = delivery_contact_display
if source.shipping_address_name:
diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json
index ecd11a4fcd6..10c771d1415 100644
--- a/erpnext/stock/doctype/item/item.json
+++ b/erpnext/stock/doctype/item/item.json
@@ -151,6 +151,7 @@
"set_only_once": 1
},
{
+ "allow_in_quick_entry": 1,
"bold": 1,
"fieldname": "item_name",
"fieldtype": "Data",
@@ -339,6 +340,7 @@
},
{
"depends_on": "is_stock_item",
+ "documentation_url": "https://docs.frappe.io/erpnext/change-valuation-method",
"fieldname": "valuation_method",
"fieldtype": "Select",
"label": "Valuation Method",
@@ -895,7 +897,7 @@
"image_field": "image",
"links": [],
"make_attachments_public": 1,
- "modified": "2025-12-15 20:08:35.634046",
+ "modified": "2026-03-17 20:39:05.218344",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",
diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js
index 89dba460809..7642133fe3e 100644
--- a/erpnext/stock/doctype/material_request/material_request.js
+++ b/erpnext/stock/doctype/material_request/material_request.js
@@ -642,10 +642,6 @@ erpnext.buying.MaterialRequestController = class MaterialRequestController exten
set_schedule_date(this.frm);
}
- schedule_date() {
- set_schedule_date(this.frm);
- }
-
qty(doc, cdt, cdn) {
var row = frappe.get_doc(cdt, cdn);
row.amount = flt(row.qty) * flt(row.rate);
diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py
index 65bec2d7ea3..d8412dd9dbf 100644
--- a/erpnext/stock/doctype/packed_item/packed_item.py
+++ b/erpnext/stock/doctype/packed_item/packed_item.py
@@ -339,11 +339,19 @@ def update_product_bundle_rate(parent_items_price, pi_row, item_row):
def set_product_bundle_rate_amount(doc, parent_items_price):
"Set cumulative rate and amount in bundle item."
+ rate_updated = False
for item in doc.get("items"):
bundle_rate = parent_items_price.get((item.item_code, item.name))
if bundle_rate and bundle_rate != item.rate:
item.rate = bundle_rate
item.amount = flt(bundle_rate * item.qty)
+ item.margin_rate_or_amount = 0
+ item.discount_percentage = 0
+ item.discount_amount = 0
+ rate_updated = True
+ if rate_updated:
+ doc.calculate_taxes_and_totals()
+ doc.set_total_in_words()
def on_doctype_update():
diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
index b0936ec1381..b5618bda08e 100644
--- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
@@ -1684,17 +1684,34 @@ def upload_csv_file(item_code, file_path):
def get_serial_batch_from_csv(item_code, file_path):
- if "private" in file_path:
- file_path = frappe.get_site_path() + file_path
- else:
- file_path = frappe.get_site_path() + "/public" + file_path
+ from frappe.utils.csvutils import read_csv_content
serial_nos = []
batch_nos = []
- with open(file_path) as f:
- reader = csv.reader(f)
- serial_nos, batch_nos = parse_csv_file_to_get_serial_batch(reader)
+ if not file_path:
+ return serial_nos, batch_nos
+
+ try:
+ file = frappe.get_doc("File", {"file_url": file_path})
+ except frappe.DoesNotExistError:
+ frappe.msgprint(
+ _("File '{0}' not found").format(frappe.bold(file_path)),
+ alert=True,
+ indicator="red",
+ raise_exception=FileNotFoundError,
+ )
+
+ if file.file_type != "CSV":
+ frappe.msgprint(
+ _("{0} is not a CSV file.").format(frappe.bold(file.file_name)),
+ alert=True,
+ indicator="red",
+ raise_exception=frappe.ValidationError,
+ )
+
+ csv_data = read_csv_content(file.get_content())
+ serial_nos, batch_nos = parse_csv_file_to_get_serial_batch(csv_data)
if serial_nos:
make_serial_nos(item_code, serial_nos)
@@ -2644,7 +2661,7 @@ def get_auto_batch_nos(kwargs):
)
if kwargs.based_on == "Expiry":
- available_batches = sorted(available_batches, key=lambda x: (x.expiry_date or getdate("9999-12-31")))
+ available_batches = sorted(available_batches, key=lambda x: x.expiry_date or getdate("9999-12-31"))
if not kwargs.get("do_not_check_future_batches") and available_batches and kwargs.get("posting_date"):
filter_zero_near_batches(available_batches, kwargs)
diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py
index 64563625297..51b939c343d 100644
--- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py
@@ -358,6 +358,136 @@ class TestSerialandBatchBundle(FrappeTestCase):
self.assertFalse(json.loads(sle.stock_queue or "[]"))
self.assertEqual(flt(sle.stock_value), 0.0)
+ def test_old_moving_avg_item_with_without_batchwise_valuation(self):
+ frappe.flags.ignore_serial_batch_bundle_validation = True
+ frappe.flags.use_serial_and_batch_fields = True
+ batch_item_code = "Old Batch Item Valuation 2"
+ make_item(
+ batch_item_code,
+ {
+ "has_batch_no": 1,
+ "batch_number_series": "TEST-OLD2-BAT-VAL-.#####",
+ "create_new_batch": 1,
+ "is_stock_item": 1,
+ "valuation_method": "Moving Average",
+ },
+ )
+
+ non_batchwise_val_batches = [
+ "TEST-OLD2-BAT-VAL-00001",
+ "TEST-OLD2-BAT-VAL-00002",
+ "TEST-OLD2-BAT-VAL-00003",
+ "TEST-OLD2-BAT-VAL-00004",
+ ]
+
+ for batch_id in non_batchwise_val_batches:
+ if not frappe.db.exists("Batch", batch_id):
+ batch_doc = frappe.get_doc(
+ {
+ "doctype": "Batch",
+ "batch_id": batch_id,
+ "item": batch_item_code,
+ "use_batchwise_valuation": 0,
+ }
+ ).insert(ignore_permissions=True)
+
+ self.assertTrue(batch_doc.use_batchwise_valuation)
+ batch_doc.db_set(
+ {
+ "use_batchwise_valuation": 0,
+ "batch_qty": 20,
+ }
+ )
+
+ qty_after_transaction = 0
+ balance_value = 0
+ i = 0
+ for batch_id in non_batchwise_val_batches:
+ i += 1
+ qty = 20
+ valuation = 100 * i
+ qty_after_transaction += qty
+ balance_value += qty * valuation
+
+ doc = frappe.get_doc(
+ {
+ "doctype": "Stock Ledger Entry",
+ "posting_date": today(),
+ "posting_time": nowtime(),
+ "batch_no": batch_id,
+ "incoming_rate": valuation,
+ "qty_after_transaction": qty_after_transaction,
+ "stock_value_difference": valuation * qty,
+ "stock_value": balance_value,
+ "balance_value": balance_value,
+ "valuation_rate": balance_value / qty_after_transaction,
+ "actual_qty": qty,
+ "item_code": batch_item_code,
+ "warehouse": "_Test Warehouse - _TC",
+ }
+ )
+
+ doc.set_posting_datetime()
+ doc.flags.ignore_permissions = True
+ doc.flags.ignore_mandatory = True
+ doc.flags.ignore_links = True
+ doc.flags.ignore_validate = True
+ doc.submit()
+ doc.reload()
+
+ frappe.flags.ignore_serial_batch_bundle_validation = False
+ frappe.flags.use_serial_and_batch_fields = False
+
+ se = make_stock_entry(
+ item_code=batch_item_code,
+ target="_Test Warehouse - _TC",
+ qty=30,
+ rate=355,
+ use_serial_batch_fields=True,
+ )
+
+ se = make_stock_entry(
+ item_code=batch_item_code,
+ source="_Test Warehouse - _TC",
+ qty=70,
+ use_serial_batch_fields=True,
+ )
+
+ sle = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"item_code": batch_item_code, "is_cancelled": 0, "voucher_no": se.name},
+ ["qty_after_transaction", "stock_value"],
+ as_dict=True,
+ )
+
+ self.assertEqual(flt(sle.stock_value), 14000.0)
+ self.assertEqual(flt(sle.qty_after_transaction), 40.0)
+
+ se = make_stock_entry(
+ item_code=batch_item_code,
+ target="_Test Warehouse - _TC",
+ qty=10,
+ rate=200,
+ use_serial_batch_fields=True,
+ )
+
+ se = make_stock_entry(
+ item_code=batch_item_code,
+ source="_Test Warehouse - _TC",
+ qty=50,
+ use_serial_batch_fields=True,
+ )
+
+ sle = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"item_code": batch_item_code, "is_cancelled": 0, "voucher_no": se.name},
+ ["qty_after_transaction", "stock_value"],
+ as_dict=True,
+ )
+
+ self.assertEqual(flt(sle.stock_value), 0.0)
+ self.assertEqual(flt(sle.qty_after_transaction), 0.0)
+
def test_old_serial_no_valuation(self):
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index c8b559a525d..00b5c9bd5a1 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -525,11 +525,11 @@ class StockReconciliation(StockController):
return True
rate_precision = item.precision("valuation_rate")
- item_dict["rate"] = flt(item_dict.get("rate"), rate_precision)
- item.valuation_rate = flt(item.valuation_rate, rate_precision) if item.valuation_rate else None
+ rate = flt(item_dict.get("rate"), rate_precision)
+ valuation_rate = flt(item.valuation_rate, rate_precision) if item.valuation_rate else None
if (
(item.qty is None or item.qty == item_dict.get("qty"))
- and (item.valuation_rate is None or item.valuation_rate == item_dict.get("rate"))
+ and (valuation_rate is None or valuation_rate == rate)
and (not item.serial_no or (item.serial_no == item_dict.get("serial_nos")))
):
return False
@@ -999,9 +999,9 @@ class StockReconciliation(StockController):
def set_total_qty_and_amount(self):
for d in self.get("items"):
- d.amount = flt(d.qty, d.precision("qty")) * flt(d.valuation_rate, d.precision("valuation_rate"))
- d.current_amount = flt(d.current_qty, d.precision("current_qty")) * flt(
- d.current_valuation_rate, d.precision("current_valuation_rate")
+ d.amount = flt(flt(d.qty) * flt(d.valuation_rate), d.precision("amount"))
+ d.current_amount = flt(
+ flt(d.current_qty) * flt(d.current_valuation_rate), d.precision("current_amount")
)
d.quantity_difference = flt(d.qty) - flt(d.current_qty)
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py
index 0ea1738b60e..69e626db1ba 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.py
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.py
@@ -110,6 +110,22 @@ class StockSettings(Document):
self.validate_auto_insert_price_list_rate_if_missing()
self.change_precision_for_for_sales()
self.change_precision_for_purchase()
+ self.validate_do_not_use_batchwise_valuation()
+
+ def validate_do_not_use_batchwise_valuation(self):
+ doc_before_save = self.get_doc_before_save()
+ if not doc_before_save:
+ return
+
+ if not frappe.get_all("Serial and Batch Bundle", filters={"docstatus": 1}, limit=1, pluck="name"):
+ return
+
+ if doc_before_save.do_not_use_batchwise_valuation and not self.do_not_use_batchwise_valuation:
+ frappe.throw(
+ _("Cannot disable {0} as it may lead to incorrect stock valuation.").format(
+ frappe.bold(_("Do Not Use Batchwise Valuation"))
+ )
+ )
def validate_warehouses(self):
warehouse_fields = ["default_warehouse", "sample_retention_warehouse"]
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index 312b8e129f8..65d04cfd80c 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -10,7 +10,7 @@ from frappe.model import child_table_fields, default_fields
from frappe.model.meta import get_field_precision
from frappe.model.utils import get_fetch_values
from frappe.query_builder.functions import IfNull, Sum
-from frappe.utils import add_days, add_months, cint, cstr, flt, getdate, parse_json
+from frappe.utils import add_days, add_months, cint, cstr, flt, get_link_to_form, getdate, parse_json
from erpnext import get_company_currency
from erpnext.accounts.doctype.pricing_rule.pricing_rule import (
@@ -972,16 +972,30 @@ def insert_item_price(args):
):
return
- item_price = frappe.db.get_value(
+ transaction_date = (
+ getdate(args.get("posting_date") or args.get("transaction_date") or args.get("posting_datetime"))
+ or getdate()
+ )
+
+ item_prices = frappe.get_all(
"Item Price",
- {
+ filters={
"item_code": args.item_code,
"price_list": args.price_list,
"currency": args.currency,
"uom": args.stock_uom,
},
- ["name", "price_list_rate"],
- as_dict=1,
+ fields=["name", "price_list_rate", "valid_from", "valid_upto"],
+ order_by="valid_from desc, creation desc",
+ )
+ item_price = next(
+ (
+ row
+ for row in item_prices
+ if (not row.valid_from or getdate(row.valid_from) <= transaction_date)
+ and (not row.valid_upto or getdate(row.valid_upto) >= transaction_date)
+ ),
+ item_prices[0] if item_prices else None,
)
update_based_on_price_list_rate = stock_settings.update_price_list_based_on == "Price List Rate"
@@ -996,11 +1010,35 @@ def insert_item_price(args):
if not price_list_rate or item_price.price_list_rate == price_list_rate:
return
- frappe.db.set_value("Item Price", item_price.name, "price_list_rate", price_list_rate)
- frappe.msgprint(
- _("Item Price updated for {0} in Price List {1}").format(args.item_code, args.price_list),
- alert=True,
- )
+ is_price_valid_for_transaction = (
+ not item_price.valid_from or getdate(item_price.valid_from) <= transaction_date
+ ) and (not item_price.valid_upto or getdate(item_price.valid_upto) >= transaction_date)
+ if is_price_valid_for_transaction:
+ frappe.db.set_value("Item Price", item_price.name, "price_list_rate", price_list_rate)
+ frappe.msgprint(
+ _("Item Price updated for {0} in Price List {1}").format(
+ get_link_to_form("Item", args.item_code), args.price_list
+ ),
+ alert=True,
+ )
+ else:
+ # if price is not valid for the transaction date, insert a new price list rate with updated price and future validity
+
+ item_price = frappe.new_doc(
+ "Item Price",
+ item_code=args.item_code,
+ price_list_rate=price_list_rate,
+ currency=args.currency,
+ uom=args.stock_uom,
+ price_list=args.price_list,
+ )
+ item_price.insert()
+ frappe.msgprint(
+ _("Item Price Added for {0} in Price List {1}").format(
+ get_link_to_form("Item", args.item_code), args.price_list
+ ),
+ alert=True,
+ )
else:
rate_to_consider = (
(flt(args.price_list_rate) or flt(args.rate))
diff --git a/erpnext/stock/report/delivery_note_trends/delivery_note_trends.json b/erpnext/stock/report/delivery_note_trends/delivery_note_trends.json
index cef82c5912d..b4da5b466c3 100644
--- a/erpnext/stock/report/delivery_note_trends/delivery_note_trends.json
+++ b/erpnext/stock/report/delivery_note_trends/delivery_note_trends.json
@@ -9,8 +9,8 @@
"filters": [],
"idx": 3,
"is_standard": "Yes",
- "letterhead": null,
- "modified": "2025-11-05 11:55:50.114173",
+ "letter_head": null,
+ "modified": "2026-03-13 17:36:31.552712",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note Trends",
diff --git a/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.json b/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.json
index 03c2a09f3bb..6743b359e30 100644
--- a/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.json
+++ b/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.json
@@ -9,8 +9,8 @@
"filters": [],
"idx": 3,
"is_standard": "Yes",
- "letterhead": null,
- "modified": "2025-11-05 11:55:49.983683",
+ "letter_head": null,
+ "modified": "2026-03-13 17:35:57.060786",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Trends",
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 69c94afe30d..5134bd29116 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -919,7 +919,10 @@ class update_entries_after:
if (
sle.is_adjustment_entry
and flt(sle.qty_after_transaction, self.flt_precision) == 0
- and flt(sle.stock_value, self.currency_precision) != 0
+ and (
+ flt(sle.stock_value, self.currency_precision) != 0
+ or flt(sle.stock_value_difference, self.currency_precision) == 0
+ )
):
sle.stock_value_difference = (
get_stock_value_difference(
diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py
index b6023b47e76..70ced430257 100644
--- a/erpnext/support/doctype/issue/issue.py
+++ b/erpnext/support/doctype/issue/issue.py
@@ -227,10 +227,14 @@ def set_status(name, status):
def auto_close_tickets():
- """Auto-close replied support tickets after 7 days"""
- auto_close_after_days = (
- frappe.db.get_value("Support Settings", "Support Settings", "close_issue_after_days") or 7
- )
+ """
+ Auto-close replied support tickets as defined on `close_issue_after_days` in Support Settings.
+ Disables the feature if `close_issue_after_days` is set to 0.
+ """
+ auto_close_after_days = frappe.db.get_single_value("Support Settings", "close_issue_after_days")
+
+ if not auto_close_after_days:
+ return
table = frappe.qb.DocType("Issue")
issues = (
diff --git a/erpnext/support/doctype/support_settings/support_settings.json b/erpnext/support/doctype/support_settings/support_settings.json
index bf1daa16f86..cbc727309bb 100644
--- a/erpnext/support/doctype/support_settings/support_settings.json
+++ b/erpnext/support/doctype/support_settings/support_settings.json
@@ -37,9 +37,11 @@
},
{
"default": "7",
+ "description": "Set this value to 0 to disable the feature.",
"fieldname": "close_issue_after_days",
"fieldtype": "Int",
- "label": "Close Issue After Days"
+ "label": "Close Issue After Days",
+ "non_negative": 1
},
{
"fieldname": "portal_sb",
@@ -163,7 +165,7 @@
],
"issingle": 1,
"links": [],
- "modified": "2021-10-14 13:08:38.473616",
+ "modified": "2026-03-16 16:33:45.859541",
"modified_by": "Administrator",
"module": "Support",
"name": "Support Settings",
diff --git a/pyproject.toml b/pyproject.toml
index fc872af4d61..f5bc11693a4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -72,4 +72,5 @@ docstring-code-format = true
[project.urls]
Homepage = "https://frappe.io/erpnext"
Repository = "https://github.com/frappe/erpnext.git"
+Documentation = "https://docs.frappe.io/erpnext"
"Bug Reports" = "https://github.com/frappe/erpnext/issues"