From e5eb5406daf90e06881ff77ebf1882bbe249b527 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Wed, 25 Feb 2026 16:30:30 +0530 Subject: [PATCH 01/53] fix(tds-report): correct party type filtering and refactor --- .../tax_withholding_details.py | 216 ++++++++++-------- 1 file changed, 126 insertions(+), 90 deletions(-) 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..2706a3f8d6e 100644 --- a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py +++ b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py @@ -19,18 +19,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 +37,24 @@ 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 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 @@ -152,9 +146,12 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_ 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,39 @@ 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 {} + + voucher_types = set() + voucher_nos = set() + + for doctype, voucher_no in net_total_map.keys(): + voucher_types.add(doctype) + voucher_nos.add(voucher_no) + + gle = frappe.qb.DocType("GL Entry") + 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(gle.voucher_type.isin(voucher_types)) + .where(gle.voucher_no.isin(voucher_nos)) + ).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_no, []).append(d) return gle_map @@ -308,14 +322,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 +343,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 +361,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 +396,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 +426,78 @@ 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) + value.party = party_list[0] if party_list else None + 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 From 314881645162a19efb03c4d4fd035bd3e299e5bd Mon Sep 17 00:00:00 2001 From: Jatin3128 Date: Fri, 6 Mar 2026 15:08:35 +0530 Subject: [PATCH 02/53] fix: correct payment terms fetching and recalculation logic --- .../accounts_settings/accounts_settings.json | 8 +-- erpnext/controllers/accounts_controller.py | 31 +++++---- erpnext/public/js/controllers/transaction.js | 1 + .../selling/doctype/quotation/quotation.py | 11 +++- .../doctype/quotation/test_quotation.py | 65 ++++++++++++++++++- .../doctype/sales_order/sales_order.json | 16 ++++- .../doctype/sales_order/sales_order.py | 26 ++------ 7 files changed, 113 insertions(+), 45 deletions(-) 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/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 008402eeb53..e53299ac2b7 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,21 @@ 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" 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 + prev_doc = self.get("items")[0].get("prevdoc_docname") + prev_doctype = "Quotation" + prev_doctype_name = "prevdoc_docname" + 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): 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/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 7a31854d259..593a80d92bc 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 @@ -441,6 +441,11 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False, ar filtered_items = args.get("filtered_children", []) 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", @@ -449,6 +454,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", @@ -465,6 +471,9 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False, ar 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..6f33a753c4e 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -174,6 +174,11 @@ class TestQuotation(FrappeTestCase): quotation.insert() 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 +322,7 @@ 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 +360,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): @@ -1057,6 +1065,57 @@ 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.json b/erpnext/selling/doctype/sales_order/sales_order.json index 4bbdb20d311..27e2f72bc4f 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,22 @@ "fieldtype": "Data", "is_virtual": 1, "label": "Last Scanned Warehouse" + }, + { + "default": "0", + "fetch_from": "customer.is_internal_customer", + "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:03:35.717402", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", @@ -1750,4 +1760,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..268219e3c2e 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -52,21 +52,17 @@ class SalesOrder(SellingController): from typing import TYPE_CHECKING if TYPE_CHECKING: - from frappe.types import DF - from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail - from erpnext.accounts.doctype.sales_taxes_and_charges.sales_taxes_and_charges import ( - SalesTaxesandCharges, - ) + from erpnext.accounts.doctype.sales_taxes_and_charges.sales_taxes_and_charges import SalesTaxesandCharges from erpnext.selling.doctype.sales_order_item.sales_order_item import SalesOrderItem from erpnext.selling.doctype.sales_team.sales_team import SalesTeam from erpnext.stock.doctype.packed_item.packed_item import PackedItem + from frappe.types import DF 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"] @@ -100,9 +96,7 @@ class SalesOrder(SellingController): customer_group: DF.Link | None customer_name: DF.Data | None delivery_date: DF.Date | None - delivery_status: DF.Literal[ - "Not Delivered", "Fully Delivered", "Partly Delivered", "Closed", "Not Applicable" - ] + delivery_status: DF.Literal["Not Delivered", "Fully Delivered", "Partly Delivered", "Closed", "Not Applicable"] disable_rounded_total: DF.Check discount_amount: DF.Currency dispatch_address: DF.SmallText | None @@ -111,6 +105,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 @@ -154,18 +149,7 @@ class SalesOrder(SellingController): shipping_rule: DF.Link | None skip_delivery_note: DF.Check source: DF.Link | None - status: DF.Literal[ - "", - "Draft", - "On Hold", - "To Pay", - "To Deliver and Bill", - "To Bill", - "To Deliver", - "Completed", - "Cancelled", - "Closed", - ] + status: DF.Literal["", "Draft", "On Hold", "To Deliver and Bill", "To Bill", "To Deliver", "Completed", "Cancelled", "Closed"] tax_category: DF.Link | None tax_id: DF.Data | None taxes: DF.Table[SalesTaxesandCharges] From 93ebec90ef2d6e007eab2341db2a78f8378f2931 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Mon, 9 Mar 2026 18:50:42 +0530 Subject: [PATCH 03/53] fix: enhance sorting and optimize GL entry retrieval --- .../tax_withholding_details.py | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) 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 2706a3f8d6e..bf6d67c6d98 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 @@ -141,7 +142,7 @@ def get_result(filters, tds_accounts, tax_category_map, net_total_map): 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 @@ -174,14 +175,9 @@ def get_gle_map(net_total_map): if not net_total_map: return {} - voucher_types = set() - voucher_nos = set() - - for doctype, voucher_no in net_total_map.keys(): - voucher_types.add(doctype) - voucher_nos.add(voucher_no) - gle = frappe.qb.DocType("GL Entry") + voucher_pairs = list(net_total_map.keys()) + rows = ( frappe.qb.from_(gle) .select( @@ -196,8 +192,7 @@ def get_gle_map(net_total_map): gle.party_type, ) .where(gle.is_cancelled == 0) - .where(gle.voucher_type.isin(voucher_types)) - .where(gle.voucher_no.isin(voucher_nos)) + .where(Tuple(gle.voucher_type, gle.voucher_no).isin(voucher_pairs)) ).run(as_dict=True) gle_map = {} @@ -492,8 +487,11 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None, filter value.grand_total = entry.paid_amount_after_tax value.base_total = entry.base_paid_amount else: - party_list = journal_entry_party_map.get(entry.name) - value.party = party_list[0] if party_list else None + 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 From c2e67599f5708efe72f5b6f4a8f0b2258b867b63 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Mon, 9 Mar 2026 19:14:21 +0530 Subject: [PATCH 04/53] fix: refactor GL entry mapping to include voucher type --- .../tax_withholding_details/tax_withholding_details.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 bf6d67c6d98..d93c60b2cf4 100644 --- a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py +++ b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py @@ -46,13 +46,12 @@ def get_result(filters, tds_accounts, tax_category_map, net_total_map): precision = get_currency_precision() 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 = "", "" posting_date = entry.posting_date - voucher_type = entry.voucher_type values = net_total_map.get((voucher_type, name)) party = values.party if values else (entry.party or entry.against) @@ -197,7 +196,7 @@ def get_gle_map(net_total_map): gle_map = {} for d in rows: - gle_map.setdefault(d.voucher_no, []).append(d) + gle_map.setdefault((d.voucher_type, d.voucher_no), []).append(d) return gle_map From 94972da845f9de4d316bc7963116d9f54e3854df Mon Sep 17 00:00:00 2001 From: ljain112 Date: Mon, 9 Mar 2026 19:25:38 +0530 Subject: [PATCH 05/53] fix: correct function syntax in TDS Computation Report --- .../tds_computation_summary/tds_computation_summary.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) 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 From c5796fed4a484cc05b9e2fcc83cdfaffbcc15111 Mon Sep 17 00:00:00 2001 From: Poovitha Palanivelu Date: Fri, 6 Mar 2026 12:27:24 +0530 Subject: [PATCH 06/53] fix: update user status depends on employee status (cherry picked from commit 194d060f1398481f4debdd5c849b3a5955389222) # Conflicts: # erpnext/setup/doctype/employee/employee.py --- erpnext/setup/doctype/employee/employee.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/setup/doctype/employee/employee.py b/erpnext/setup/doctype/employee/employee.py index 14724ead051..f7da64e8e6f 100755 --- a/erpnext/setup/doctype/employee/employee.py +++ b/erpnext/setup/doctype/employee/employee.py @@ -185,13 +185,17 @@ class Employee(NestedSet): throw(_("Please enter relieving date.")) def validate_for_enabled_user_id(self, enabled): +<<<<<<< HEAD if not self.status == "Active": return +======= +>>>>>>> 194d060f13 (fix: update user status depends on employee status) 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") From 55a0603356f308a67d3dec947ef79c6a9c808359 Mon Sep 17 00:00:00 2001 From: Navin-S-R Date: Tue, 10 Mar 2026 17:58:57 +0530 Subject: [PATCH 07/53] chore: resolve conflict --- erpnext/setup/doctype/employee/employee.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/erpnext/setup/doctype/employee/employee.py b/erpnext/setup/doctype/employee/employee.py index f7da64e8e6f..543a8a194b8 100755 --- a/erpnext/setup/doctype/employee/employee.py +++ b/erpnext/setup/doctype/employee/employee.py @@ -185,12 +185,6 @@ class Employee(NestedSet): throw(_("Please enter relieving date.")) def validate_for_enabled_user_id(self, enabled): -<<<<<<< HEAD - if not self.status == "Active": - return - -======= ->>>>>>> 194d060f13 (fix: update user status depends on employee status) if enabled is None: frappe.throw(_("User {0} does not exist").format(self.user_id)) From 79b04826d9ebfedb17a53c8262e392658a46f0f6 Mon Sep 17 00:00:00 2001 From: Jatin3128 Date: Fri, 6 Mar 2026 15:11:13 +0530 Subject: [PATCH 08/53] fix: correct payment terms fetching and recalculation logic --- erpnext/controllers/accounts_controller.py | 8 +++++-- .../selling/doctype/quotation/quotation.py | 4 +--- .../doctype/quotation/test_quotation.py | 12 ++++++---- .../doctype/sales_order/sales_order.json | 3 +-- .../doctype/sales_order/sales_order.py | 23 +++++++++++++++---- 5 files changed, 34 insertions(+), 16 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index e53299ac2b7..ec2b5caf9d2 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2590,10 +2590,12 @@ class AccountsController(TransactionBase): prev_doc = self.get("items")[0].get("purchase_order") prev_doctype = "Purchase Order" prev_doctype_name = "purchase_order" - else: + elif self.doctype == "Sales Order": prev_doc = self.get("items")[0].get("prevdoc_docname") prev_doctype = "Quotation" prev_doctype_name = "prevdoc_docname" + else: + return None, None, None return prev_doc, prev_doctype, prev_doctype_name def linked_order_has_payment_terms(self, po_or_so, fieldname, doctype): @@ -2690,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/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 593a80d92bc..b4e433ac805 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -441,12 +441,11 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False, ar filtered_items = args.get("filtered_children", []) 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, @@ -464,7 +463,6 @@ 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, diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 6f33a753c4e..4d4d485c71a 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -174,12 +174,11 @@ class TestQuotation(FrappeTestCase): quotation.insert() 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 @@ -322,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,"automatically_fetch_payment_terms": 1,}, + { + "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 @@ -1065,7 +1068,7 @@ class TestQuotation(FrappeTestCase): quotation.reload() self.assertEqual(quotation.status, "Open") - + @change_settings( "Accounts Settings", {"automatically_fetch_payment_terms": 1}, @@ -1117,7 +1120,6 @@ class TestQuotation(FrappeTestCase): 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.json b/erpnext/selling/doctype/sales_order/sales_order.json index 27e2f72bc4f..e649b8e9383 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -1669,7 +1669,6 @@ }, { "default": "0", - "fetch_from": "customer.is_internal_customer", "fieldname": "ignore_default_payment_terms_template", "fieldtype": "Check", "hidden": 1, @@ -1681,7 +1680,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2026-03-06 15:03:35.717402", + "modified": "2026-03-06 15:33:49.059029", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 268219e3c2e..dbd7f406432 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -52,13 +52,16 @@ class SalesOrder(SellingController): from typing import TYPE_CHECKING if TYPE_CHECKING: + from frappe.types import DF + from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail - from erpnext.accounts.doctype.sales_taxes_and_charges.sales_taxes_and_charges import SalesTaxesandCharges + from erpnext.accounts.doctype.sales_taxes_and_charges.sales_taxes_and_charges import ( + SalesTaxesandCharges, + ) from erpnext.selling.doctype.sales_order_item.sales_order_item import SalesOrderItem from erpnext.selling.doctype.sales_team.sales_team import SalesTeam from erpnext.stock.doctype.packed_item.packed_item import PackedItem - from frappe.types import DF additional_discount_percentage: DF.Float address_display: DF.SmallText | None @@ -96,7 +99,9 @@ class SalesOrder(SellingController): customer_group: DF.Link | None customer_name: DF.Data | None delivery_date: DF.Date | None - delivery_status: DF.Literal["Not Delivered", "Fully Delivered", "Partly Delivered", "Closed", "Not Applicable"] + delivery_status: DF.Literal[ + "Not Delivered", "Fully Delivered", "Partly Delivered", "Closed", "Not Applicable" + ] disable_rounded_total: DF.Check discount_amount: DF.Currency dispatch_address: DF.SmallText | None @@ -149,7 +154,17 @@ class SalesOrder(SellingController): shipping_rule: DF.Link | None skip_delivery_note: DF.Check source: DF.Link | None - status: DF.Literal["", "Draft", "On Hold", "To Deliver and Bill", "To Bill", "To Deliver", "Completed", "Cancelled", "Closed"] + status: DF.Literal[ + "", + "Draft", + "On Hold", + "To Deliver and Bill", + "To Bill", + "To Deliver", + "Completed", + "Cancelled", + "Closed", + ] tax_category: DF.Link | None tax_id: DF.Data | None taxes: DF.Table[SalesTaxesandCharges] From e3e9d7b19e073290054f198cb653630eea1b3491 Mon Sep 17 00:00:00 2001 From: creative-paramu Date: Wed, 11 Mar 2026 11:14:33 +0530 Subject: [PATCH 09/53] fix: update item description in Production Plan Assembly Items table (cherry picked from commit 19533551f44bd0527892734d3d9716a598d52e9c) --- .../manufacturing/doctype/production_plan/production_plan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 351ba27a43e..91ae43edb6c 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") } From ef6fd7dcb5a00364cdd01a199e374b42ee6cc371 Mon Sep 17 00:00:00 2001 From: Parameshwari Palanisamy <101092028+creative-paramu@users.noreply.github.com> Date: Wed, 11 Mar 2026 11:41:24 +0530 Subject: [PATCH 10/53] Update production_plan.py (cherry picked from commit 39e68a9ce7e89583f3c39895c5537fc742b70620) --- .../manufacturing/doctype/production_plan/production_plan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 91ae43edb6c..8dac30f175e 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"), } From 3e7d2c6f11216f998000c43556c0cf164910e788 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 11 Mar 2026 14:26:16 +0530 Subject: [PATCH 11/53] refactor: make cost center editable in payment entry deduction (cherry picked from commit 078b22d985b5d35025f3ee68258491f791a52108) # Conflicts: # erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json --- .../payment_entry_deduction/payment_entry_deduction.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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", From b73d9700d0e0ee12ec96f49fa706ad8e3a12a396 Mon Sep 17 00:00:00 2001 From: Nihantra Patel Date: Wed, 11 Mar 2026 14:40:41 +0530 Subject: [PATCH 12/53] fix: Append existing ignored doctypes in Journal Entry on_cancel instead of overwriting (cherry picked from commit 39e10c4ab0c17fccd20e1d70c9d0533f7d2e4778) --- erpnext/accounts/doctype/journal_entry/journal_entry.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 2ed9881772c..e949aafb888 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() From db251c6e11025b3967125cd5dc06d5156298eb38 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 12 Mar 2026 05:15:12 +0000 Subject: [PATCH 13/53] fix: re-calculate taxes and totals after resetting bundle item rate (backport #53342) (#53349) Co-authored-by: V Shankar fix: re-calculate taxes and totals after resetting bundle item rate (#53342) --- erpnext/stock/doctype/packed_item/packed_item.py | 8 ++++++++ 1 file changed, 8 insertions(+) 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(): From fd94cd0e7cd9194bfc43e87d2da6ca8074dec28a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 12 Mar 2026 05:40:00 +0000 Subject: [PATCH 14/53] Feat/shipment default contact (backport #53029) (#53354) Co-authored-by: David <52141166+sdavidbastos@users.noreply.github.com> --- .../stock/doctype/delivery_note/delivery_note.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index b2dd23f80bd..782d6a89c5e 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 @@ -1169,18 +1170,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: From 034d460ae1d852f8dbfede93b8e80a550b006a17 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 12 Mar 2026 05:40:33 +0000 Subject: [PATCH 15/53] fix(delivery note): avoid maintaining si_detail on return delivery note (backport #52456) (#53352) Co-authored-by: NaviN <118178330+Navin-S-R@users.noreply.github.com> fix(delivery note): avoid maintaining si_detail on return delivery note (#52456) --- erpnext/controllers/sales_and_purchase_return.py | 1 - erpnext/stock/doctype/delivery_note/delivery_note.py | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) 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/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 782d6a89c5e..f1fc54c751d 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -393,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") ) From 8497d1f8cf065a7b6fae7f2dbe79c0fc5b21e335 Mon Sep 17 00:00:00 2001 From: Ejaaz Khan <67804911+iamejaaz@users.noreply.github.com> Date: Thu, 12 Mar 2026 10:41:53 +0530 Subject: [PATCH 16/53] fix: remove redundant pos print format (#53348) (cherry picked from commit e4d79c62463653bc7cf151921230af090314d155) --- .../print_format/point_of_sale/__init__.py | 0 .../point_of_sale/point_of_sale.json | 23 ------------------- 2 files changed, 23 deletions(-) delete mode 100644 erpnext/accounts/print_format/point_of_sale/__init__.py delete mode 100644 erpnext/accounts/print_format/point_of_sale/point_of_sale.json 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\n\t\t\t\n\t\t\t\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\n\t\t\t\n\t\t\n\t\t{% endfor %}\n\t\n
{{ __(\"Item\") }}{{ __(\"Qty\") }}{{ __(\"Amount\") }}
\n\t\t\t\t{{ item.item_name }}\n\t\t\t{{ format_number(item.qty, null,precision(\"difference\")) }}
@ {{ format_currency(item.rate, currency) }}
{{ format_currency(item.amount, currency) }}
\n\n\n\t\n\t\t\n\t\t\t\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\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\n\t\t\n\t\t{% endif %}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\n
\n\t\t\t\t{{ __(\"Net Total\") }}\n\t\t\t\n\t\t\t\t{{ format_currency(total, currency) }}\n\t\t\t
\n\t\t\t\t{{ row.description }}\n\t\t\t\n\t\t\t\t{{ format_currency(row.tax_amount, currency) }}\n\t\t\t
\n\t\t\t\t{{ __(\"Discount\") }}\n\t\t\t\n\t\t\t\t{{ format_currency(discount_amount, currency) }}\n\t\t\t
\n\t\t\t\t{{ __(\"Grand Total\") }}\n\t\t\t\n\t\t\t\t{{ format_currency(grand_total, currency) }}\n\t\t\t
\n\t\t\t\t{{ __(\"Paid Amount\") }}\n\t\t\t\n\t\t\t\t{{ format_currency(paid_amount, currency) }}\n\t\t\t
\n\t\t\t\t{{ __(\"Qty Total\") }}\n\t\t\t\n\t\t\t\t{{ qty_total }}\n\t\t\t
\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 From dbed426725a6444a5ebc6847678f9b92055f6027 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 12 Mar 2026 06:19:37 +0000 Subject: [PATCH 17/53] =?UTF-8?q?refactor:=20supplier=20quotation=20compar?= =?UTF-8?q?ision=20report=20button=20should=20start=20f=E2=80=A6=20(backpo?= =?UTF-8?q?rt=20#53361)=20(#53362)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mihir Kandoi --- .../request_for_quotation/request_for_quotation.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) 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"); From 0612f1c9412afe64ec77282af18044300afab6ff Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 12 Mar 2026 06:49:53 +0000 Subject: [PATCH 18/53] fix: NoneType error when template description is to be copied to variant (backport #53358) (#53365) Co-authored-by: Mihir Kandoi fix: NoneType error when template description is to be copied to variant (#53358) --- erpnext/controllers/item_variant.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From 6d476604dfecb62cb29bafcbb60d89dfedf720ba Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Thu, 12 Mar 2026 14:24:59 +0530 Subject: [PATCH 19/53] fix: use completion_date not posting date --- erpnext/assets/doctype/asset_repair/asset_repair.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index 646a7eee7ef..13d0bb8b481 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: frm.doc.completion_date, to_date: moment(frm.doc.modified).format("YYYY-MM-DD"), company: frm.doc.company, categorize_by: "", From 05d614eb04616aec0f0382523a5d48ca5f43fed4 Mon Sep 17 00:00:00 2001 From: Khushi Rawat <142375893+khushi8112@users.noreply.github.com> Date: Thu, 12 Mar 2026 14:41:23 +0530 Subject: [PATCH 20/53] fix: coderebbit review --- erpnext/assets/doctype/asset_repair/asset_repair.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index 13d0bb8b481..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.completion_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: "", From 0e00ab88655d5c38ccc1b0aafcc31f05835a6ebe Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 12 Mar 2026 09:20:39 +0000 Subject: [PATCH 21/53] fix: do not modify rate in the child item merely for comparison (backport #53301) (#53375) Co-authored-by: Mihir Kandoi fix: do not modify rate in the child item merely for comparison (#53301) --- .../stock_reconciliation/stock_reconciliation.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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) From 5737d2afa3a55e1d7dad9790bfea61f7f3387bde Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 12 Mar 2026 09:32:09 +0000 Subject: [PATCH 22/53] fix: precision issue in production plan (backport #53370) (#53373) Co-authored-by: Mihir Kandoi fix: precision issue in production plan (#53370) --- .../manufacturing/doctype/production_plan/production_plan.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 8dac30f175e..d2ca6ff73fd 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -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), } ) ) From 85c4cc3e1bec898de9e5ff03d80b806705ba3ed0 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 12 Mar 2026 20:37:52 +0530 Subject: [PATCH 23/53] fix: update delivery date in line items (#53331) --- .../doctype/purchase_order/purchase_order.js | 4 ---- erpnext/public/js/controllers/buying.js | 23 +++++++++++++++++++ .../material_request/material_request.js | 4 ---- 3 files changed, 23 insertions(+), 8 deletions(-) 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/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/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); From 2a70203cabc664dccadfe421050b3cb8648b0007 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 12 Mar 2026 15:20:22 +0000 Subject: [PATCH 24/53] fix(regional): rename duplicate Customer fields in Italy setup (backport #50921) (#53397) * fix(regional): rename duplicate Customer fields in Italy setup (#50921) Co-authored-by: Claude Co-authored-by: Claude Opus 4.5 Co-authored-by: Claude Opus 4.6 (cherry picked from commit c6efc403cdae696dafcf6fe4f295e7c668af61e2) # Conflicts: # erpnext/patches.txt * chore: resolve conflicts * chore: resolve conflicts Removed obsolete patches for older versions. --------- Co-authored-by: Solede Co-authored-by: Mihir Kandoi --- erpnext/patches.txt | 1 + .../rename_italy_customer_name_fields.py | 65 +++++ erpnext/regional/italy/e-invoice.xml | 4 +- erpnext/regional/italy/setup.py | 6 +- erpnext/tests/test_italy_regional_patch.py | 259 ++++++++++++++++++ 5 files changed, 330 insertions(+), 5 deletions(-) create mode 100644 erpnext/patches/v15_0/rename_italy_customer_name_fields.py create mode 100644 erpnext/tests/test_italy_regional_patch.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 7d2c1757bda..575ae6cce15 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -432,3 +432,4 @@ erpnext.patches.v16_0.set_ordered_qty_in_quotation_item erpnext.patches.v15_0.replace_http_with_https_in_sales_partner erpnext.patches.v16_0.add_portal_redirects erpnext.patches.v16_0.update_order_qty_and_requested_qty_based_on_mr_and_po +erpnext.patches.v15_0.rename_italy_customer_name_fields diff --git a/erpnext/patches/v15_0/rename_italy_customer_name_fields.py b/erpnext/patches/v15_0/rename_italy_customer_name_fields.py new file mode 100644 index 00000000000..e56adc8765e --- /dev/null +++ b/erpnext/patches/v15_0/rename_italy_customer_name_fields.py @@ -0,0 +1,65 @@ +import frappe + + +def execute(): + """Rename Italy regional custom fields to avoid conflict with standard Customer fields. + + The Italy regional setup created custom fields 'first_name' and 'last_name' on Customer + which conflict with the standard read-only fields that fetch from customer_primary_contact. + This patch renames them to 'italy_customer_first_name' and 'italy_customer_last_name'. + """ + # Check if old fields exist and are the Italy regional ones + old_first_name_exists = frappe.db.exists("Custom Field", "Customer-first_name") + old_last_name_exists = frappe.db.exists("Custom Field", "Customer-last_name") + + is_italy_first_name = False + is_italy_last_name = False + + if old_first_name_exists: + field_doc = frappe.get_doc("Custom Field", "Customer-first_name") + is_italy_first_name = field_doc.depends_on and "customer_type" in field_doc.depends_on + + if old_last_name_exists: + field_doc = frappe.get_doc("Custom Field", "Customer-last_name") + is_italy_last_name = field_doc.depends_on and "customer_type" in field_doc.depends_on + + # If neither field is the Italy regional one, nothing to do + if not is_italy_first_name and not is_italy_last_name: + return + + # Step 1: Delete old Custom Field documents FIRST (to avoid duplicate field validation error) + if is_italy_first_name: + frappe.delete_doc("Custom Field", "Customer-first_name", force=True) + + if is_italy_last_name: + frappe.delete_doc("Custom Field", "Customer-last_name", force=True) + + # Step 2: Create the new fields and sync database schema + from erpnext.regional.italy.setup import make_custom_fields + + make_custom_fields(update=True) + + # Step 3: Migrate data from old columns to new columns (if old columns still exist in DB) + # Note: We do NOT drop the first_name/last_name columns because they are standard fields + # in Customer doctype (Read Only fields that fetch from customer_primary_contact). + # The Italy regional setup incorrectly created Custom Fields with the same names. + # We only migrate the data and leave the standard columns intact. + if is_italy_first_name and frappe.db.has_column("Customer", "first_name"): + frappe.db.sql( + """ + UPDATE `tabCustomer` + SET italy_customer_first_name = first_name + WHERE first_name IS NOT NULL AND first_name != '' + AND (italy_customer_first_name IS NULL OR italy_customer_first_name = '') + """ + ) + + if is_italy_last_name and frappe.db.has_column("Customer", "last_name"): + frappe.db.sql( + """ + UPDATE `tabCustomer` + SET italy_customer_last_name = last_name + WHERE last_name IS NOT NULL AND last_name != '' + AND (italy_customer_last_name IS NULL OR italy_customer_last_name = '') + """ + ) diff --git a/erpnext/regional/italy/e-invoice.xml b/erpnext/regional/italy/e-invoice.xml index 7c436a2b449..ed132d9998c 100644 --- a/erpnext/regional/italy/e-invoice.xml +++ b/erpnext/regional/italy/e-invoice.xml @@ -97,8 +97,8 @@ {%- if doc.customer_data.customer_type == "Individual" %} {{ doc.customer_data.fiscal_code }} - {{ doc.customer_data.first_name }} - {{ doc.customer_data.last_name }} + {{ doc.customer_data.italy_customer_first_name }} + {{ doc.customer_data.italy_customer_last_name }} {%- else %} diff --git a/erpnext/regional/italy/setup.py b/erpnext/regional/italy/setup.py index 23406ea85a6..b40cd38b2da 100644 --- a/erpnext/regional/italy/setup.py +++ b/erpnext/regional/italy/setup.py @@ -232,7 +232,7 @@ def make_custom_fields(update=True): depends_on='eval:doc.customer_type=="Company"', ), dict( - fieldname="first_name", + fieldname="italy_customer_first_name", label="First Name", fieldtype="Data", insert_after="salutation", @@ -240,10 +240,10 @@ def make_custom_fields(update=True): depends_on='eval:doc.customer_type!="Company"', ), dict( - fieldname="last_name", + fieldname="italy_customer_last_name", label="Last Name", fieldtype="Data", - insert_after="first_name", + insert_after="italy_customer_first_name", print_hide=1, depends_on='eval:doc.customer_type!="Company"', ), diff --git a/erpnext/tests/test_italy_regional_patch.py b/erpnext/tests/test_italy_regional_patch.py new file mode 100644 index 00000000000..53034716d0e --- /dev/null +++ b/erpnext/tests/test_italy_regional_patch.py @@ -0,0 +1,259 @@ +"""Test for Italy regional patch: rename_italy_customer_name_fields. + +This test is completely DB-based to avoid dependencies on ERPNext test fixtures. +""" + +import unittest + +import frappe +from frappe.utils import now + + +class TestRenameItalyCustomerNameFields(unittest.TestCase): + """Test the patch that renames Italy regional custom fields on Customer.""" + + OLD_FIRST_NAME_FIELD = "Customer-first_name" + OLD_LAST_NAME_FIELD = "Customer-last_name" + NEW_FIRST_NAME_FIELD = "Customer-italy_customer_first_name" + NEW_LAST_NAME_FIELD = "Customer-italy_customer_last_name" + + @classmethod + def setUpClass(cls): + # Connect to the site + if not frappe.db: + frappe.connect() + cls.test_customer_name = "_Test Italy Patch Customer" + + def setUp(self): + """Set up test scenario: create old fields and test customer.""" + self._cleanup_fields() + self._cleanup_test_customer() + self._create_old_custom_fields_direct() + self._add_old_columns_to_db() + self._create_test_customer_direct() + + def tearDown(self): + """Clean up after test.""" + self._cleanup_test_customer() + self._cleanup_fields() + self._drop_old_columns_if_exist() + # Restore new fields from Italy setup + try: + from erpnext.regional.italy.setup import make_custom_fields + + make_custom_fields(update=True) + except (ImportError, AttributeError, ValueError) as e: + # Ignore setup failures in tearDown, but log for debugging + frappe.logger().warning(f"Failed to restore Italy setup in tearDown: {e}") + frappe.db.rollback() + + def _cleanup_fields(self): + """Remove both old and new custom fields.""" + for field_name in [ + self.OLD_FIRST_NAME_FIELD, + self.OLD_LAST_NAME_FIELD, + self.NEW_FIRST_NAME_FIELD, + self.NEW_LAST_NAME_FIELD, + ]: + if frappe.db.exists("Custom Field", field_name): + frappe.db.delete("Custom Field", {"name": field_name}) + + def _cleanup_test_customer(self): + """Remove test customer if exists.""" + if frappe.db.exists("Customer", self.test_customer_name): + # Delete directly from DB to avoid controller validation + frappe.db.delete("Customer", {"name": self.test_customer_name}) + + def _create_old_custom_fields_direct(self): + """Create the old custom fields directly in DB to bypass validation. + + This simulates the legacy state where Italy regional setup created + fields with names that now conflict with standard Customer fields. + """ + current_time = now() + + # Insert old first_name custom field directly + frappe.db.sql( + """ + INSERT INTO `tabCustom Field` + (name, creation, modified, modified_by, owner, docstatus, + dt, fieldname, fieldtype, label, insert_after, depends_on) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + """, + ( + self.OLD_FIRST_NAME_FIELD, + current_time, + current_time, + "Administrator", + "Administrator", + 0, + "Customer", + "first_name", + "Data", + "First Name", + "customer_name", + "eval:doc.customer_type == 'Individual'", + ), + ) + + # Insert old last_name custom field directly + frappe.db.sql( + """ + INSERT INTO `tabCustom Field` + (name, creation, modified, modified_by, owner, docstatus, + dt, fieldname, fieldtype, label, insert_after, depends_on) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + """, + ( + self.OLD_LAST_NAME_FIELD, + current_time, + current_time, + "Administrator", + "Administrator", + 0, + "Customer", + "last_name", + "Data", + "Last Name", + "first_name", + "eval:doc.customer_type == 'Individual'", + ), + ) + + frappe.db.commit() # nosemgrep: frappe-manual-commit -- required after raw SQL INSERT in test setup + + def _add_old_columns_to_db(self): + """Ensure old columns exist in the database table.""" + frappe.clear_cache() # Clear cache to get fresh column info + if not frappe.db.has_column("Customer", "first_name"): + frappe.db.sql_ddl("ALTER TABLE `tabCustomer` ADD COLUMN `first_name` VARCHAR(140)") + if not frappe.db.has_column("Customer", "last_name"): + frappe.db.sql_ddl("ALTER TABLE `tabCustomer` ADD COLUMN `last_name` VARCHAR(140)") + frappe.clear_cache() # Clear cache after adding columns + + def _drop_old_columns_if_exist(self): + """Drop old columns if they still exist.""" + frappe.clear_cache() # Clear cache to get fresh column info + try: + if frappe.db.has_column("Customer", "first_name"): + frappe.db.sql_ddl("ALTER TABLE `tabCustomer` DROP COLUMN `first_name`") + except frappe.db.InternalError as e: + # Column might already be dropped or locked + frappe.logger().debug(f"Could not drop first_name column: {e}") + try: + if frappe.db.has_column("Customer", "last_name"): + frappe.db.sql_ddl("ALTER TABLE `tabCustomer` DROP COLUMN `last_name`") + except frappe.db.InternalError as e: + # Column might already be dropped or locked + frappe.logger().debug(f"Could not drop last_name column: {e}") + frappe.clear_cache() # Clear cache after dropping columns + + def _create_test_customer_direct(self): + """Create a test customer directly in DB to avoid controller dependencies.""" + current_time = now() + + # Insert customer directly into DB + frappe.db.sql( + """ + INSERT INTO `tabCustomer` + (name, creation, modified, modified_by, owner, docstatus, + customer_name, customer_type, first_name, last_name) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + """, + ( + self.test_customer_name, + current_time, + current_time, + "Administrator", + "Administrator", + 0, + self.test_customer_name, + "Individual", + "Mario", + "Rossi", + ), + ) + frappe.db.commit() # nosemgrep: frappe-manual-commit -- required after raw SQL INSERT in test setup + + def test_patch_renames_fields_and_migrates_data(self): + """Test that the patch renames fields and migrates data correctly.""" + # Verify old fields exist before patch + self.assertTrue(frappe.db.exists("Custom Field", self.OLD_FIRST_NAME_FIELD)) + self.assertTrue(frappe.db.exists("Custom Field", self.OLD_LAST_NAME_FIELD)) + + # Verify old data exists + old_first_name = frappe.db.get_value("Customer", self.test_customer_name, "first_name") + old_last_name = frappe.db.get_value("Customer", self.test_customer_name, "last_name") + self.assertEqual(old_first_name, "Mario") + self.assertEqual(old_last_name, "Rossi") + + # Execute the patch + from erpnext.patches.v15_0.rename_italy_customer_name_fields import execute + + execute() + + # Verify old Custom Field documents are deleted + self.assertFalse(frappe.db.exists("Custom Field", self.OLD_FIRST_NAME_FIELD)) + self.assertFalse(frappe.db.exists("Custom Field", self.OLD_LAST_NAME_FIELD)) + + # Verify new Custom Field documents exist + self.assertTrue(frappe.db.exists("Custom Field", self.NEW_FIRST_NAME_FIELD)) + self.assertTrue(frappe.db.exists("Custom Field", self.NEW_LAST_NAME_FIELD)) + + # Verify data was migrated to new columns + new_first_name = frappe.db.get_value("Customer", self.test_customer_name, "italy_customer_first_name") + new_last_name = frappe.db.get_value("Customer", self.test_customer_name, "italy_customer_last_name") + self.assertEqual(new_first_name, "Mario") + self.assertEqual(new_last_name, "Rossi") + + # Note: first_name/last_name columns are NOT dropped because they are + # standard Customer fields (Read Only, fetch from customer_primary_contact) + + def test_patch_skips_non_italy_fields(self): + """Test that the patch skips fields that are not Italy regional fields.""" + # Delete the Italy regional fields created in setUp + self._cleanup_fields() + self._drop_old_columns_if_exist() + self._cleanup_test_customer() + + current_time = now() + + # Create a custom field with same name but without Italy's depends_on + frappe.db.sql( + """ + INSERT INTO `tabCustom Field` + (name, creation, modified, modified_by, owner, docstatus, + dt, fieldname, fieldtype, label, insert_after) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + """, + ( + self.OLD_FIRST_NAME_FIELD, + current_time, + current_time, + "Administrator", + "Administrator", + 0, + "Customer", + "first_name", + "Data", + "First Name", + "customer_name", + ), + ) + frappe.db.commit() # nosemgrep: frappe-manual-commit -- required after raw SQL INSERT in test setup + + # Execute the patch + from erpnext.patches.v15_0.rename_italy_customer_name_fields import execute + + execute() + + # The non-Italy field should still exist (not renamed) + self.assertTrue(frappe.db.exists("Custom Field", self.OLD_FIRST_NAME_FIELD)) + + # Verify new Italy fields were NOT created (since this wasn't an Italy field) + self.assertFalse(frappe.db.exists("Custom Field", self.NEW_FIRST_NAME_FIELD)) + self.assertFalse(frappe.db.exists("Custom Field", self.NEW_LAST_NAME_FIELD)) + + +if __name__ == "__main__": + unittest.main() From 908e185cfecefc259b41f0dea8f332009964dd0b Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Fri, 13 Mar 2026 11:59:42 +0530 Subject: [PATCH 25/53] fix: update label on company change --- erpnext/assets/doctype/asset/asset.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index f81b26b2c8d..dff9092189f 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(["net_purchase_amount"], erpnext.get_currency(frm.doc.company)); + }, + set_depr_posting_failure_alert: function (frm) { const alert = `
@@ -309,7 +315,7 @@ frappe.ui.form.on("Asset", { }, }); }, - + render_depreciation_schedule_view: function (frm, asset_depr_schedule_doc) { let wrapper = $(frm.fields_dict["depreciation_schedule_view"].wrapper).empty(); @@ -389,6 +395,7 @@ frappe.ui.form.on("Asset", { datatable.style.setStyle(`.dt-cell--col-3`, { "font-weight": 600 }); }, + setup_chart_and_depr_schedule_view: async function (frm) { if (frm.doc.finance_books.length > 1) { return; From f702a71126632107280c03d36cd31c9c5f65d95c Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Fri, 13 Mar 2026 12:05:12 +0530 Subject: [PATCH 26/53] chore: linters check --- erpnext/assets/doctype/asset/asset.js | 5 ++--- erpnext/assets/doctype/asset/asset.json | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index dff9092189f..a8802c1d67e 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -224,7 +224,7 @@ frappe.ui.form.on("Asset", { }, set_dynamic_labels: function (frm) { - frm.set_currency_labels(["net_purchase_amount"], erpnext.get_currency(frm.doc.company)); + frm.set_currency_labels(["gross_purchase_amount"], erpnext.get_currency(frm.doc.company)); }, set_depr_posting_failure_alert: function (frm) { @@ -315,7 +315,7 @@ frappe.ui.form.on("Asset", { }, }); }, - + render_depreciation_schedule_view: function (frm, asset_depr_schedule_doc) { let wrapper = $(frm.fields_dict["depreciation_schedule_view"].wrapper).empty(); @@ -395,7 +395,6 @@ frappe.ui.form.on("Asset", { datatable.style.setStyle(`.dt-cell--col-3`, { "font-weight": 600 }); }, - setup_chart_and_depr_schedule_view: async function (frm) { if (frm.doc.finance_books.length > 1) { return; 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", From db9dc86694d26b694dede5c028223682cb232834 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 13 Mar 2026 07:47:07 +0000 Subject: [PATCH 27/53] Revert "fix(regional): rename duplicate Customer fields in Italy setup" (backport #53409) (#53410) * Revert "fix(regional): rename duplicate Customer fields in Italy setup" (#53409) (cherry picked from commit bd87a7e612864e5fea2a4178fc92d929d998bb3e) # Conflicts: # erpnext/patches.txt * chore: resolve conflicts --------- Co-authored-by: Mihir Kandoi --- erpnext/patches.txt | 1 - .../rename_italy_customer_name_fields.py | 65 ----- erpnext/regional/italy/e-invoice.xml | 4 +- erpnext/regional/italy/setup.py | 6 +- erpnext/tests/test_italy_regional_patch.py | 259 ------------------ 5 files changed, 5 insertions(+), 330 deletions(-) delete mode 100644 erpnext/patches/v15_0/rename_italy_customer_name_fields.py delete mode 100644 erpnext/tests/test_italy_regional_patch.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 575ae6cce15..7d2c1757bda 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -432,4 +432,3 @@ erpnext.patches.v16_0.set_ordered_qty_in_quotation_item erpnext.patches.v15_0.replace_http_with_https_in_sales_partner erpnext.patches.v16_0.add_portal_redirects erpnext.patches.v16_0.update_order_qty_and_requested_qty_based_on_mr_and_po -erpnext.patches.v15_0.rename_italy_customer_name_fields diff --git a/erpnext/patches/v15_0/rename_italy_customer_name_fields.py b/erpnext/patches/v15_0/rename_italy_customer_name_fields.py deleted file mode 100644 index e56adc8765e..00000000000 --- a/erpnext/patches/v15_0/rename_italy_customer_name_fields.py +++ /dev/null @@ -1,65 +0,0 @@ -import frappe - - -def execute(): - """Rename Italy regional custom fields to avoid conflict with standard Customer fields. - - The Italy regional setup created custom fields 'first_name' and 'last_name' on Customer - which conflict with the standard read-only fields that fetch from customer_primary_contact. - This patch renames them to 'italy_customer_first_name' and 'italy_customer_last_name'. - """ - # Check if old fields exist and are the Italy regional ones - old_first_name_exists = frappe.db.exists("Custom Field", "Customer-first_name") - old_last_name_exists = frappe.db.exists("Custom Field", "Customer-last_name") - - is_italy_first_name = False - is_italy_last_name = False - - if old_first_name_exists: - field_doc = frappe.get_doc("Custom Field", "Customer-first_name") - is_italy_first_name = field_doc.depends_on and "customer_type" in field_doc.depends_on - - if old_last_name_exists: - field_doc = frappe.get_doc("Custom Field", "Customer-last_name") - is_italy_last_name = field_doc.depends_on and "customer_type" in field_doc.depends_on - - # If neither field is the Italy regional one, nothing to do - if not is_italy_first_name and not is_italy_last_name: - return - - # Step 1: Delete old Custom Field documents FIRST (to avoid duplicate field validation error) - if is_italy_first_name: - frappe.delete_doc("Custom Field", "Customer-first_name", force=True) - - if is_italy_last_name: - frappe.delete_doc("Custom Field", "Customer-last_name", force=True) - - # Step 2: Create the new fields and sync database schema - from erpnext.regional.italy.setup import make_custom_fields - - make_custom_fields(update=True) - - # Step 3: Migrate data from old columns to new columns (if old columns still exist in DB) - # Note: We do NOT drop the first_name/last_name columns because they are standard fields - # in Customer doctype (Read Only fields that fetch from customer_primary_contact). - # The Italy regional setup incorrectly created Custom Fields with the same names. - # We only migrate the data and leave the standard columns intact. - if is_italy_first_name and frappe.db.has_column("Customer", "first_name"): - frappe.db.sql( - """ - UPDATE `tabCustomer` - SET italy_customer_first_name = first_name - WHERE first_name IS NOT NULL AND first_name != '' - AND (italy_customer_first_name IS NULL OR italy_customer_first_name = '') - """ - ) - - if is_italy_last_name and frappe.db.has_column("Customer", "last_name"): - frappe.db.sql( - """ - UPDATE `tabCustomer` - SET italy_customer_last_name = last_name - WHERE last_name IS NOT NULL AND last_name != '' - AND (italy_customer_last_name IS NULL OR italy_customer_last_name = '') - """ - ) diff --git a/erpnext/regional/italy/e-invoice.xml b/erpnext/regional/italy/e-invoice.xml index ed132d9998c..7c436a2b449 100644 --- a/erpnext/regional/italy/e-invoice.xml +++ b/erpnext/regional/italy/e-invoice.xml @@ -97,8 +97,8 @@ {%- if doc.customer_data.customer_type == "Individual" %} {{ doc.customer_data.fiscal_code }} - {{ doc.customer_data.italy_customer_first_name }} - {{ doc.customer_data.italy_customer_last_name }} + {{ doc.customer_data.first_name }} + {{ doc.customer_data.last_name }} {%- else %} diff --git a/erpnext/regional/italy/setup.py b/erpnext/regional/italy/setup.py index b40cd38b2da..23406ea85a6 100644 --- a/erpnext/regional/italy/setup.py +++ b/erpnext/regional/italy/setup.py @@ -232,7 +232,7 @@ def make_custom_fields(update=True): depends_on='eval:doc.customer_type=="Company"', ), dict( - fieldname="italy_customer_first_name", + fieldname="first_name", label="First Name", fieldtype="Data", insert_after="salutation", @@ -240,10 +240,10 @@ def make_custom_fields(update=True): depends_on='eval:doc.customer_type!="Company"', ), dict( - fieldname="italy_customer_last_name", + fieldname="last_name", label="Last Name", fieldtype="Data", - insert_after="italy_customer_first_name", + insert_after="first_name", print_hide=1, depends_on='eval:doc.customer_type!="Company"', ), diff --git a/erpnext/tests/test_italy_regional_patch.py b/erpnext/tests/test_italy_regional_patch.py deleted file mode 100644 index 53034716d0e..00000000000 --- a/erpnext/tests/test_italy_regional_patch.py +++ /dev/null @@ -1,259 +0,0 @@ -"""Test for Italy regional patch: rename_italy_customer_name_fields. - -This test is completely DB-based to avoid dependencies on ERPNext test fixtures. -""" - -import unittest - -import frappe -from frappe.utils import now - - -class TestRenameItalyCustomerNameFields(unittest.TestCase): - """Test the patch that renames Italy regional custom fields on Customer.""" - - OLD_FIRST_NAME_FIELD = "Customer-first_name" - OLD_LAST_NAME_FIELD = "Customer-last_name" - NEW_FIRST_NAME_FIELD = "Customer-italy_customer_first_name" - NEW_LAST_NAME_FIELD = "Customer-italy_customer_last_name" - - @classmethod - def setUpClass(cls): - # Connect to the site - if not frappe.db: - frappe.connect() - cls.test_customer_name = "_Test Italy Patch Customer" - - def setUp(self): - """Set up test scenario: create old fields and test customer.""" - self._cleanup_fields() - self._cleanup_test_customer() - self._create_old_custom_fields_direct() - self._add_old_columns_to_db() - self._create_test_customer_direct() - - def tearDown(self): - """Clean up after test.""" - self._cleanup_test_customer() - self._cleanup_fields() - self._drop_old_columns_if_exist() - # Restore new fields from Italy setup - try: - from erpnext.regional.italy.setup import make_custom_fields - - make_custom_fields(update=True) - except (ImportError, AttributeError, ValueError) as e: - # Ignore setup failures in tearDown, but log for debugging - frappe.logger().warning(f"Failed to restore Italy setup in tearDown: {e}") - frappe.db.rollback() - - def _cleanup_fields(self): - """Remove both old and new custom fields.""" - for field_name in [ - self.OLD_FIRST_NAME_FIELD, - self.OLD_LAST_NAME_FIELD, - self.NEW_FIRST_NAME_FIELD, - self.NEW_LAST_NAME_FIELD, - ]: - if frappe.db.exists("Custom Field", field_name): - frappe.db.delete("Custom Field", {"name": field_name}) - - def _cleanup_test_customer(self): - """Remove test customer if exists.""" - if frappe.db.exists("Customer", self.test_customer_name): - # Delete directly from DB to avoid controller validation - frappe.db.delete("Customer", {"name": self.test_customer_name}) - - def _create_old_custom_fields_direct(self): - """Create the old custom fields directly in DB to bypass validation. - - This simulates the legacy state where Italy regional setup created - fields with names that now conflict with standard Customer fields. - """ - current_time = now() - - # Insert old first_name custom field directly - frappe.db.sql( - """ - INSERT INTO `tabCustom Field` - (name, creation, modified, modified_by, owner, docstatus, - dt, fieldname, fieldtype, label, insert_after, depends_on) - VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) - """, - ( - self.OLD_FIRST_NAME_FIELD, - current_time, - current_time, - "Administrator", - "Administrator", - 0, - "Customer", - "first_name", - "Data", - "First Name", - "customer_name", - "eval:doc.customer_type == 'Individual'", - ), - ) - - # Insert old last_name custom field directly - frappe.db.sql( - """ - INSERT INTO `tabCustom Field` - (name, creation, modified, modified_by, owner, docstatus, - dt, fieldname, fieldtype, label, insert_after, depends_on) - VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) - """, - ( - self.OLD_LAST_NAME_FIELD, - current_time, - current_time, - "Administrator", - "Administrator", - 0, - "Customer", - "last_name", - "Data", - "Last Name", - "first_name", - "eval:doc.customer_type == 'Individual'", - ), - ) - - frappe.db.commit() # nosemgrep: frappe-manual-commit -- required after raw SQL INSERT in test setup - - def _add_old_columns_to_db(self): - """Ensure old columns exist in the database table.""" - frappe.clear_cache() # Clear cache to get fresh column info - if not frappe.db.has_column("Customer", "first_name"): - frappe.db.sql_ddl("ALTER TABLE `tabCustomer` ADD COLUMN `first_name` VARCHAR(140)") - if not frappe.db.has_column("Customer", "last_name"): - frappe.db.sql_ddl("ALTER TABLE `tabCustomer` ADD COLUMN `last_name` VARCHAR(140)") - frappe.clear_cache() # Clear cache after adding columns - - def _drop_old_columns_if_exist(self): - """Drop old columns if they still exist.""" - frappe.clear_cache() # Clear cache to get fresh column info - try: - if frappe.db.has_column("Customer", "first_name"): - frappe.db.sql_ddl("ALTER TABLE `tabCustomer` DROP COLUMN `first_name`") - except frappe.db.InternalError as e: - # Column might already be dropped or locked - frappe.logger().debug(f"Could not drop first_name column: {e}") - try: - if frappe.db.has_column("Customer", "last_name"): - frappe.db.sql_ddl("ALTER TABLE `tabCustomer` DROP COLUMN `last_name`") - except frappe.db.InternalError as e: - # Column might already be dropped or locked - frappe.logger().debug(f"Could not drop last_name column: {e}") - frappe.clear_cache() # Clear cache after dropping columns - - def _create_test_customer_direct(self): - """Create a test customer directly in DB to avoid controller dependencies.""" - current_time = now() - - # Insert customer directly into DB - frappe.db.sql( - """ - INSERT INTO `tabCustomer` - (name, creation, modified, modified_by, owner, docstatus, - customer_name, customer_type, first_name, last_name) - VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) - """, - ( - self.test_customer_name, - current_time, - current_time, - "Administrator", - "Administrator", - 0, - self.test_customer_name, - "Individual", - "Mario", - "Rossi", - ), - ) - frappe.db.commit() # nosemgrep: frappe-manual-commit -- required after raw SQL INSERT in test setup - - def test_patch_renames_fields_and_migrates_data(self): - """Test that the patch renames fields and migrates data correctly.""" - # Verify old fields exist before patch - self.assertTrue(frappe.db.exists("Custom Field", self.OLD_FIRST_NAME_FIELD)) - self.assertTrue(frappe.db.exists("Custom Field", self.OLD_LAST_NAME_FIELD)) - - # Verify old data exists - old_first_name = frappe.db.get_value("Customer", self.test_customer_name, "first_name") - old_last_name = frappe.db.get_value("Customer", self.test_customer_name, "last_name") - self.assertEqual(old_first_name, "Mario") - self.assertEqual(old_last_name, "Rossi") - - # Execute the patch - from erpnext.patches.v15_0.rename_italy_customer_name_fields import execute - - execute() - - # Verify old Custom Field documents are deleted - self.assertFalse(frappe.db.exists("Custom Field", self.OLD_FIRST_NAME_FIELD)) - self.assertFalse(frappe.db.exists("Custom Field", self.OLD_LAST_NAME_FIELD)) - - # Verify new Custom Field documents exist - self.assertTrue(frappe.db.exists("Custom Field", self.NEW_FIRST_NAME_FIELD)) - self.assertTrue(frappe.db.exists("Custom Field", self.NEW_LAST_NAME_FIELD)) - - # Verify data was migrated to new columns - new_first_name = frappe.db.get_value("Customer", self.test_customer_name, "italy_customer_first_name") - new_last_name = frappe.db.get_value("Customer", self.test_customer_name, "italy_customer_last_name") - self.assertEqual(new_first_name, "Mario") - self.assertEqual(new_last_name, "Rossi") - - # Note: first_name/last_name columns are NOT dropped because they are - # standard Customer fields (Read Only, fetch from customer_primary_contact) - - def test_patch_skips_non_italy_fields(self): - """Test that the patch skips fields that are not Italy regional fields.""" - # Delete the Italy regional fields created in setUp - self._cleanup_fields() - self._drop_old_columns_if_exist() - self._cleanup_test_customer() - - current_time = now() - - # Create a custom field with same name but without Italy's depends_on - frappe.db.sql( - """ - INSERT INTO `tabCustom Field` - (name, creation, modified, modified_by, owner, docstatus, - dt, fieldname, fieldtype, label, insert_after) - VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) - """, - ( - self.OLD_FIRST_NAME_FIELD, - current_time, - current_time, - "Administrator", - "Administrator", - 0, - "Customer", - "first_name", - "Data", - "First Name", - "customer_name", - ), - ) - frappe.db.commit() # nosemgrep: frappe-manual-commit -- required after raw SQL INSERT in test setup - - # Execute the patch - from erpnext.patches.v15_0.rename_italy_customer_name_fields import execute - - execute() - - # The non-Italy field should still exist (not renamed) - self.assertTrue(frappe.db.exists("Custom Field", self.OLD_FIRST_NAME_FIELD)) - - # Verify new Italy fields were NOT created (since this wasn't an Italy field) - self.assertFalse(frappe.db.exists("Custom Field", self.NEW_FIRST_NAME_FIELD)) - self.assertFalse(frappe.db.exists("Custom Field", self.NEW_LAST_NAME_FIELD)) - - -if __name__ == "__main__": - unittest.main() From 53e3bfbf22eb12e1516ca73db88b51ea6ce8f099 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 13 Mar 2026 14:36:00 +0530 Subject: [PATCH 28/53] fix: broke cost center filter in get outstanding reference docs (cherry picked from commit 7dfe36fdce9c84e0c6baf54c7f0eff009223dc8d) --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) 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)}'" From 56ffd52335b766cf84e9d9ecaa88fa1bb5ce7609 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 13 Mar 2026 13:53:41 +0530 Subject: [PATCH 29/53] refactor: disable total row in trends report (cherry picked from commit 4dbc72b30179cd9f71193fcd90de78373b8924d9) --- .../purchase_invoice_trends/purchase_invoice_trends.json | 6 +++--- .../report/sales_invoice_trends/sales_invoice_trends.json | 4 ++-- .../report/purchase_order_trends/purchase_order_trends.json | 4 ++-- .../selling/report/quotation_trends/quotation_trends.json | 4 ++-- .../report/sales_order_trends/sales_order_trends.json | 4 ++-- .../report/delivery_note_trends/delivery_note_trends.json | 4 ++-- .../purchase_receipt_trends/purchase_receipt_trends.json | 4 ++-- 7 files changed, 15 insertions(+), 15 deletions(-) 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/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/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/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", From a6cf31edad101d6649051300d89c72c251aa4e6c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 15 Mar 2026 09:46:07 +0530 Subject: [PATCH 30/53] =?UTF-8?q?fix:=20sales=20order=20indicator=20should?= =?UTF-8?q?=20be=20based=20on=20available=20qty=20rather=20th=E2=80=A6=20(?= =?UTF-8?q?backport=20#53456)=20(#53457)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- erpnext/selling/doctype/sales_order/sales_order.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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"; From 7a7c4a03f0224527f06a215d157a5a6c5f531846 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 15 Mar 2026 07:45:00 +0000 Subject: [PATCH 31/53] fix(serial_and_batch_bundle_selector): handle CSV attachment properly (backport #53460) (#53461) Co-authored-by: diptanilsaha fix(serial_and_batch_bundle_selector): handle CSV attachment properly (#53460) --- .../serial_and_batch_bundle.py | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) 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) From 81244a84e75391b7eface371fc7f302f47332f60 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 15 Mar 2026 14:53:39 +0100 Subject: [PATCH 32/53] chore: add docs to project URLs (backport #53467) (#53468) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) 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" From b63b5320f2fd4b707fbfc10dccab8f128e7f32b8 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 12:53:12 +0530 Subject: [PATCH 33/53] fix(p&l_statement): disable accumulated value filter by default (backport #53488) (#53489) Co-authored-by: diptanilsaha fix(p&l_statement): disable accumulated value filter by default (#53488) --- .../profit_and_loss_statement/profit_and_loss_statement.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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", From ac6c06daf95df1f9640832b845c6e5f7bd97cb94 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 16 Mar 2026 14:44:31 +0530 Subject: [PATCH 34/53] fix: stock adjustment entry (cherry picked from commit af3067ee2324c15265f97fe48ec532deb9016d56) --- erpnext/stock/stock_ledger.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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( From 30fe711c44561c1e5752276bdfc7af44ab2ac695 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 12:12:04 +0000 Subject: [PATCH 35/53] fix(support-settings): disable the auto-close tickets feature if `close_issue_after_days` is set to 0 (backport #53499) (#53504) Co-authored-by: diptanilsaha fix(support-settings): disable the auto-close tickets feature if `close_issue_after_days` is set to 0 (#53499) --- erpnext/support/doctype/issue/issue.py | 12 ++++++++---- .../doctype/support_settings/support_settings.json | 6 ++++-- 2 files changed, 12 insertions(+), 6 deletions(-) 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", From 284ccd1defef1aac73f5652c30f1a80fd46cb766 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 16 Mar 2026 23:08:55 +0530 Subject: [PATCH 36/53] fix: do not set valuation rate for invoice without update stock (cherry picked from commit bec9e48435b6294138002f3b5f864ce631ce500a) --- erpnext/controllers/selling_controller.py | 3 +++ 1 file changed, 3 insertions(+) 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( From eec8cf8a71ef0af194442669d68f1d523e7e1e1c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 18:33:11 +0000 Subject: [PATCH 37/53] fix: change "Date" label to "Posting Date" in Sales Invoice and Purchase Invoice (backport #53503) (#53516) * fix: change "Date" label to "Posting Date" in Sales Invoice and Purchase Invoice (#53503) (cherry picked from commit 4cd150ba7a6b546366cc83bb8fcf2a3f2f51e1f7) # Conflicts: # erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json # erpnext/accounts/doctype/sales_invoice/sales_invoice.json * chore: resolve conflicts * chore: resolve conflicts --------- Co-authored-by: Abdus Samad <120767334+Samad-11@users.noreply.github.com> Co-authored-by: Mihir Kandoi --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json | 2 +- erpnext/accounts/doctype/sales_invoice/sales_invoice.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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", From ab9d960aa8fcb74473bc14fdea3cb5d629861808 Mon Sep 17 00:00:00 2001 From: Nikhil Kothari Date: Tue, 17 Mar 2026 10:17:32 +0530 Subject: [PATCH 38/53] fix(banking): include paid purchase invoices in reports and bank clearance (#52675) * fix(banking): include paid purchase invoices in reports and bank clearance * fix: condition for amounts not reflected in system * fix: set Sales Invoice to be the payment document in bank rec * fix: add additional filter for `is_paid` * fix: added is_paid * fix: added invoice number in bank clearance tool * chore: make requested changes * fix: exclude opening JEs * fix: bring back banking icon in desktop (cherry picked from commit ef3262216651515548a61c1a0d870bb3a3aa16f9) # Conflicts: # erpnext/accounts/doctype/bank_clearance/bank_clearance.py # erpnext/desktop_icon/banking.json --- .../doctype/bank_clearance/bank_clearance.py | 239 ++++++++++------ .../bank_clearance_summary.py | 119 +++++--- .../bank_reconciliation_statement.py | 256 +++++++++++++----- erpnext/desktop_icon/banking.json | 21 ++ 4 files changed, 447 insertions(+), 188 deletions(-) create mode 100644 erpnext/desktop_icon/banking.json diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py index 4ff2a13eae2..35030f5828f 100644 --- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py +++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py @@ -5,8 +5,14 @@ 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 +<<<<<<< HEAD from frappe.utils import flt, fmt_money, get_link_to_form, getdate +======= +from frappe.query_builder.functions import Coalesce, Sum +from frappe.utils import cint, flt, fmt_money, getdate +>>>>>>> ef32622166 (fix(banking): include paid purchase invoices in reports and bank clearance (#52675)) from pypika import Order import erpnext @@ -136,65 +142,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 +320,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/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/desktop_icon/banking.json b/erpnext/desktop_icon/banking.json new file mode 100644 index 00000000000..6e55cfa0d02 --- /dev/null +++ b/erpnext/desktop_icon/banking.json @@ -0,0 +1,21 @@ +{ + "app": "erpnext", + "creation": "2025-11-12 14:51:14.307331", + "docstatus": 0, + "doctype": "Desktop Icon", + "hidden": 0, + "icon": "dollar-sign", + "icon_type": "Link", + "idx": 5, + "label": "Banking", + "link_to": "Banking", + "link_type": "Workspace Sidebar", + "modified": "2026-02-12 12:29:48.687545", + "modified_by": "Administrator", + "name": "Banking", + "owner": "Administrator", + "parent_icon": "Accounting", + "restrict_removal": 0, + "roles": [], + "standard": 1 +} From d262a65b00078ef2f3b8e56102820366c79aae02 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 10:42:51 +0530 Subject: [PATCH 39/53] fix: correct overlap detection in JobCard.has_overlap (backport #53473) (#53522) Co-authored-by: Sanjesh-Raju Co-authored-by: Sanjesh Co-authored-by: Tridots Tech fix: correct overlap detection in JobCard.has_overlap (#53473) --- erpnext/manufacturing/doctype/job_card/job_card.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 From 0e770c0bbd057c4d3c1688512952cd1c3dba95ab Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 05:42:21 +0000 Subject: [PATCH 40/53] fix: add item_name to quick entry fields in Item doctype (backport #53530) (#53532) Co-authored-by: Abdus Samad <120767334+Samad-11@users.noreply.github.com> fix: add item_name to quick entry fields in Item doctype (#53530) --- erpnext/stock/doctype/item/item.json | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index ecd11a4fcd6..b7777b4298d 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", From a85aeb2f9b962451adfc93df6dc42e56e8e9b5d9 Mon Sep 17 00:00:00 2001 From: Nikhil Kothari Date: Tue, 17 Mar 2026 11:28:37 +0530 Subject: [PATCH 41/53] chore: resolve conflicts --- .../doctype/bank_clearance/bank_clearance.py | 6 +----- erpnext/desktop_icon/banking.json | 21 ------------------- 2 files changed, 1 insertion(+), 26 deletions(-) delete mode 100644 erpnext/desktop_icon/banking.json diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py index 35030f5828f..36cbb321518 100644 --- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py +++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py @@ -7,12 +7,8 @@ from frappe import _, msgprint from frappe.model.document import Document from frappe.query_builder import Case from frappe.query_builder.custom import ConstantColumn -<<<<<<< HEAD -from frappe.utils import flt, fmt_money, get_link_to_form, getdate -======= from frappe.query_builder.functions import Coalesce, Sum -from frappe.utils import cint, flt, fmt_money, getdate ->>>>>>> ef32622166 (fix(banking): include paid purchase invoices in reports and bank clearance (#52675)) +from frappe.utils import flt, fmt_money, get_link_to_form, getdate from pypika import Order import erpnext diff --git a/erpnext/desktop_icon/banking.json b/erpnext/desktop_icon/banking.json deleted file mode 100644 index 6e55cfa0d02..00000000000 --- a/erpnext/desktop_icon/banking.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "app": "erpnext", - "creation": "2025-11-12 14:51:14.307331", - "docstatus": 0, - "doctype": "Desktop Icon", - "hidden": 0, - "icon": "dollar-sign", - "icon_type": "Link", - "idx": 5, - "label": "Banking", - "link_to": "Banking", - "link_type": "Workspace Sidebar", - "modified": "2026-02-12 12:29:48.687545", - "modified_by": "Administrator", - "name": "Banking", - "owner": "Administrator", - "parent_icon": "Accounting", - "restrict_removal": 0, - "roles": [], - "standard": 1 -} From a5d1afe304b15345d1cfd91afab13d1fdb7ecbb7 Mon Sep 17 00:00:00 2001 From: Sakthivel Murugan S <129778327+ssakthivelmurugan@users.noreply.github.com> Date: Tue, 17 Mar 2026 11:31:22 +0530 Subject: [PATCH 42/53] fix(minor): filter bank accounts in bank statement import (#53481) --- .../bank_statement_import/bank_statement_import.js | 9 +++++++++ 1 file changed, 9 insertions(+) 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; From ca6872c768c3269f5c7f985f351ee791923411cd Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 16 Mar 2026 16:28:38 +0530 Subject: [PATCH 43/53] fix: valuation rate for no Use Batch wise Valuation batches (cherry picked from commit 4befa1519807236e8f093ddc0be7480d029143c2) --- erpnext/stock/deprecated_serial_batch.py | 36 +++++ .../test_serial_and_batch_bundle.py | 130 ++++++++++++++++++ .../doctype/stock_settings/stock_settings.py | 16 +++ 3 files changed, 182 insertions(+) diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py index 69443e3a608..fdb2d9c78af 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, self.sle.company) 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_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"] From c3845643141f8df1d016db70fdf48761b3a7f474 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 17 Mar 2026 15:05:46 +0530 Subject: [PATCH 44/53] fix: test case Removed company parameter from get_valuation_method call. --- erpnext/stock/deprecated_serial_batch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py index fdb2d9c78af..6c30b087de8 100644 --- a/erpnext/stock/deprecated_serial_batch.py +++ b/erpnext/stock/deprecated_serial_batch.py @@ -372,4 +372,4 @@ class DeprecatedBatchNoValuation: def get_valuation_method(self, item_code): from erpnext.stock.utils import get_valuation_method - return get_valuation_method(item_code, self.sle.company) + return get_valuation_method(item_code) From f232024fa453b2c562d02c0ee5abd9dac1f4a54c Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 17 Mar 2026 15:23:05 +0530 Subject: [PATCH 45/53] chore: remove incorrect import (cherry picked from commit fc2edfbdedb59077d105b82d4bd1b00784cec57a) # Conflicts: # erpnext/controllers/queries.py --- erpnext/controllers/queries.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 12d5229c9f3..97b2680d968 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -607,9 +607,13 @@ def get_blanket_orders(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs +<<<<<<< HEAD def get_income_account(doctype, txt, searchfield, start, page_len, filters): from erpnext.controllers.queries import get_match_cond +======= +def get_income_account(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict): +>>>>>>> fc2edfbded (chore: remove incorrect import) # 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" @@ -695,9 +699,13 @@ def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters, @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs +<<<<<<< HEAD def get_expense_account(doctype, txt, searchfield, start, page_len, filters): from erpnext.controllers.queries import get_match_cond +======= +def get_expense_account(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict): +>>>>>>> fc2edfbded (chore: remove incorrect import) if not filters: filters = {} From 03f09222cf3601370815ac107fd47918567652a9 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 17 Mar 2026 15:48:47 +0530 Subject: [PATCH 46/53] fix: use qb to prevent incorrect sql due to user permissions (cherry picked from commit 04b967bd6de59a2041b06d8acf33f9b92d098432) # Conflicts: # erpnext/controllers/queries.py --- erpnext/controllers/queries.py | 94 ++++++++++++++++++---------------- 1 file changed, 51 insertions(+), 43 deletions(-) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 97b2680d968..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 @@ -607,39 +608,38 @@ def get_blanket_orders(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs -<<<<<<< HEAD def get_income_account(doctype, txt, searchfield, start, page_len, filters): - from erpnext.controllers.queries import get_match_cond - -======= -def get_income_account(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict): ->>>>>>> fc2edfbded (chore: remove incorrect import) # 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() ) @@ -699,31 +699,39 @@ def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters, @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs -<<<<<<< HEAD def get_expense_account(doctype, txt, searchfield, start, page_len, filters): - from erpnext.controllers.queries import get_match_cond - -======= -def get_expense_account(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict): ->>>>>>> fc2edfbded (chore: remove incorrect import) 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() From 3bc9190795f133da1844dde64afa4e6cfdeb3b41 Mon Sep 17 00:00:00 2001 From: Saeed Kola Date: Tue, 17 Mar 2026 17:08:29 +0530 Subject: [PATCH 47/53] fix: skip validate_stock_accounts when perpetual inventory is disabled When perpetual inventory is disabled, stock transactions produce no GL entries, so blocking manual Journal Entries against stock accounts is incorrect. Added an early return guard in validate_stock_accounts() to skip the check when is_perpetual_inventory_enabled() returns False. --- erpnext/accounts/doctype/journal_entry/journal_entry.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index e949aafb888..502a4f9e015 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -269,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( From 239728e4d95ba0b2dcc9cf92eadc4502c57ece81 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 20:17:32 +0530 Subject: [PATCH 48/53] fix(sales_invoice): reset payment methods on `pos_profile` change (backport #53514) (#53560) Co-authored-by: diptanilsaha fix(sales_invoice): reset payment methods on `pos_profile` change (#53514) --- .../accounts/doctype/sales_invoice/sales_invoice.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) 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( From 526ffc11761473f4eeb49e20247314ae3d7ded3a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 14:51:13 +0000 Subject: [PATCH 49/53] fix: Creating new item price incase of changes in expired item price (backport #53534) (#53544) Co-authored-by: Nishka Gosalia Co-authored-by: Nishka Gosalia <58264710+nishkagosalia@users.noreply.github.com> fix: Creating new item price incase of changes in expired item price (#53534) --- .../doctype/sales_order/test_sales_order.py | 39 +++++++++++++ erpnext/stock/get_item_details.py | 58 +++++++++++++++---- 2 files changed, 87 insertions(+), 10 deletions(-) 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/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)) From b9c8e8d478ba1778ecdec649d9b16d9b862c233a Mon Sep 17 00:00:00 2001 From: Arturo Date: Tue, 17 Mar 2026 16:18:14 +0100 Subject: [PATCH 50/53] fix(italy): fix e-invoice ScontoMaggiorazione structure and included_in_print_rate support (#53334) --- erpnext/regional/italy/e-invoice.xml | 34 ++++++++++++++++------------ 1 file changed, 19 insertions(+), 15 deletions(-) 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 %} From 57815a07acc8aae37649d5379520b541bb28b18e Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 22:07:46 +0530 Subject: [PATCH 51/53] fix(stock): fix the property setter (backport #53422) (#53573) Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com> fix(stock): fix the property setter (#53422) --- erpnext/public/js/utils/landed_taxes_and_charges_common.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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: { From 9771ed4c572510ec51586606f9d57ab6459717f1 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 22:09:00 +0530 Subject: [PATCH 52/53] fix(manufacturing): update working hours validation (backport #53559) (#53566) Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com> fix(manufacturing): update working hours validation (#53559) --- erpnext/manufacturing/doctype/workstation/workstation.py | 3 --- 1 file changed, 3 deletions(-) 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)) From 526dc68c72366766702884cb1e03a4693c66a398 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 16:50:12 +0000 Subject: [PATCH 53/53] chore: add documentation link in valuation method field (backport #53564) (#53570) * chore: add documentation link in valuation method field (#53564) (cherry picked from commit f319857939f2aa5466357bbbe158a1cdf169c49c) # Conflicts: # erpnext/stock/doctype/item/item.json * chore: resolve conflicts --------- Co-authored-by: Mihir Kandoi --- erpnext/stock/doctype/item/item.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index b7777b4298d..10c771d1415 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -340,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", @@ -896,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",