diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index 6701673cc7f..37d8363beaa 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -43,3 +43,6 @@ jobs: - name: Run Semgrep rules run: semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness + + - name: Semgrep for Test Correctness + run: semgrep ci --include=**/test_*.py --config ./semgrep/test-correctness.yml diff --git a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py index 81e639dc6b2..250442a3cd4 100644 --- a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py @@ -10,9 +10,6 @@ from erpnext.tests.utils import ERPNextTestSuite class TestAccountingDimension(ERPNextTestSuite): - def setUp(self): - create_dimension() - def test_dimension_against_sales_invoice(self): si = create_sales_invoice(do_not_save=1) @@ -77,63 +74,3 @@ class TestAccountingDimension(ERPNextTestSuite): si.save() self.assertRaises(frappe.ValidationError, si.submit) - - -def create_dimension(): - frappe.set_user("Administrator") - - if not frappe.db.exists("Accounting Dimension", {"document_type": "Department"}): - dimension = frappe.get_doc( - { - "doctype": "Accounting Dimension", - "document_type": "Department", - } - ) - dimension.append( - "dimension_defaults", - { - "company": "_Test Company", - "reference_document": "Department", - "default_dimension": "_Test Department - _TC", - }, - ) - dimension.insert() - dimension.save() - else: - dimension = frappe.get_doc("Accounting Dimension", "Department") - dimension.disabled = 0 - dimension.save() - - if not frappe.db.exists("Accounting Dimension", {"document_type": "Location"}): - dimension1 = frappe.get_doc( - { - "doctype": "Accounting Dimension", - "document_type": "Location", - } - ) - - dimension1.append( - "dimension_defaults", - { - "company": "_Test Company", - "reference_document": "Location", - "default_dimension": "Block 1", - }, - ) - - dimension1.insert() - dimension1.save() - else: - dimension1 = frappe.get_doc("Accounting Dimension", "Location") - dimension1.disabled = 0 - dimension1.save() - - -def disable_dimension(): - dimension1 = frappe.get_doc("Accounting Dimension", "Department") - dimension1.disabled = 1 - dimension1.save() - - dimension2 = frappe.get_doc("Accounting Dimension", "Location") - dimension2.disabled = 1 - dimension2.save() diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py index 9bed10824bb..fe7d4706967 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py @@ -5,10 +5,6 @@ import unittest import frappe -from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import ( - create_dimension, - disable_dimension, -) from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError from erpnext.tests.utils import ERPNextTestSuite @@ -16,7 +12,6 @@ from erpnext.tests.utils import ERPNextTestSuite class TestAccountingDimensionFilter(ERPNextTestSuite): def setUp(self): - create_dimension() create_accounting_dimension_filter() self.invoice_list = [] diff --git a/erpnext/accounts/doctype/bank_account/bank_account.py b/erpnext/accounts/doctype/bank_account/bank_account.py index c0dc6467f8f..9fe5b4ba3fb 100644 --- a/erpnext/accounts/doctype/bank_account/bank_account.py +++ b/erpnext/accounts/doctype/bank_account/bank_account.py @@ -116,6 +116,7 @@ def get_default_company_bank_account(company, party_type, party): @frappe.whitelist() def get_bank_account_details(bank_account): + frappe.has_permission("Bank Account", doc=bank_account, ptype="read", throw=True) return frappe.get_cached_value( "Bank Account", bank_account, ["account", "bank", "bank_account_no"], as_dict=1 ) diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 3abfa176622..452164c728c 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -489,4 +489,5 @@ def rename_temporarily_named_docs(doctype): for hook in frappe.get_hooks(hook_type): frappe.call(hook, newname=newname, oldname=oldname) - frappe.db.commit() + if not frappe.in_test: + frappe.db.commit() diff --git a/erpnext/accounts/doctype/ledger_merge/ledger_merge.py b/erpnext/accounts/doctype/ledger_merge/ledger_merge.py index 008b4115f5f..dc3fd5a9d04 100644 --- a/erpnext/accounts/doctype/ledger_merge/ledger_merge.py +++ b/erpnext/accounts/doctype/ledger_merge/ledger_merge.py @@ -71,14 +71,16 @@ def start_merge(docname): ledger_merge.account, ) row.db_set("merged", 1) - frappe.db.commit() + if not frappe.in_test: + frappe.db.commit() successful_merges += 1 frappe.publish_realtime( "ledger_merge_progress", {"ledger_merge": ledger_merge.name, "current": successful_merges, "total": total}, ) except Exception: - frappe.db.rollback() + if not frappe.in_test: + frappe.db.rollback() ledger_merge.log_error("Ledger merge failed") finally: if successful_merges == total: diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js index 4938e6690e5..466b38126d7 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js @@ -50,6 +50,7 @@ frappe.ui.form.on("Opening Invoice Creation Tool", { refresh: function (frm) { frm.disable_save(); + frm.trigger("create_missing_party"); !frm.doc.import_in_progress && frm.trigger("make_dashboard"); frm.page.set_primary_action(__("Create Invoices"), () => { let btn_primary = frm.page.btn_primary.get(0); @@ -123,7 +124,8 @@ frappe.ui.form.on("Opening Invoice Creation Tool", { invoice_type: function (frm) { $.each(frm.doc.invoices, (idx, row) => { row.party_type = frm.doc.invoice_type == "Sales" ? "Customer" : "Supplier"; - row.party = ""; + frappe.model.set_value(row.doctype, row.name, "party", ""); + frappe.model.set_value(row.doctype, row.name, "party_name", ""); }); frm.refresh_fields(); }, @@ -162,9 +164,35 @@ frappe.ui.form.on("Opening Invoice Creation Tool", { row.party_type = frm.doc.invoice_type == "Sales" ? "Customer" : "Supplier"; }); }, + + create_missing_party: function (frm) { + if (frm.doc.create_missing_party) { + frm.fields_dict["invoices"].grid.update_docfield_property("party", "reqd", 0); + frm.fields_dict["invoices"].grid.update_docfield_property("party_name", "read_only", 0); + } else { + frm.fields_dict["invoices"].grid.update_docfield_property("party", "reqd", 1); + frm.fields_dict["invoices"].grid.update_docfield_property("party_name", "read_only", 1); + } + frm.refresh_field("invoices"); + }, }); frappe.ui.form.on("Opening Invoice Creation Tool Item", { + party: function (frm, cdt, cdn) { + let row = locals[cdt][cdn]; + if (!row.party) { + frappe.model.set_value(cdt, cdn, "party_name", ""); + return; + } + + let party_type = frm.doc.invoice_type == "Sales" ? "Customer" : "Supplier"; + let name_field = party_type === "Customer" ? "customer_name" : "supplier_name"; + + frappe.db.get_value(party_type, row.party, name_field, (r) => { + frappe.model.set_value(cdt, cdn, "party_name", r?.[name_field] || ""); + }); + }, + invoices_add: (frm) => { frm.trigger("update_invoice_table"); }, diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json index deeee72c18a..8d1c3e87ba1 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json @@ -8,9 +8,9 @@ "engine": "InnoDB", "field_order": [ "company", - "create_missing_party", "column_break_3", "invoice_type", + "create_missing_party", "accounting_dimensions_section", "cost_center", "dimension_col_break", @@ -29,7 +29,7 @@ }, { "default": "0", - "description": "Create missing customer or supplier.", + "description": "If party does not exist, create it using the Party Name field.", "fieldname": "create_missing_party", "fieldtype": "Check", "label": "Create Missing Party" @@ -65,10 +65,10 @@ "options": "Cost Center" }, { - "fieldname": "project", - "fieldtype": "Link", - "label": "Project", - "options": "Project" + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" }, { "collapsible": 1, @@ -84,7 +84,7 @@ "hide_toolbar": 1, "issingle": 1, "links": [], - "modified": "2024-03-27 13:10:06.564397", + "modified": "2026-03-23 00:32:15.600086", "modified_by": "Administrator", "module": "Accounts", "name": "Opening Invoice Creation Tool", @@ -101,8 +101,9 @@ } ], "quick_entry": 1, + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py index 2f3a893a73f..3949e242567 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py @@ -5,7 +5,7 @@ import frappe from frappe import _, scrub from frappe.model.document import Document -from frappe.utils import flt, nowdate +from frappe.utils import escape_html, flt, nowdate from frappe.utils.background_jobs import enqueue, is_job_enqueued from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( @@ -32,6 +32,7 @@ class OpeningInvoiceCreationTool(Document): create_missing_party: DF.Check invoice_type: DF.Literal["Sales", "Purchase"] invoices: DF.Table[OpeningInvoiceCreationToolItem] + project: DF.Link | None # end: auto-generated types def onload(self): @@ -85,6 +86,11 @@ class OpeningInvoiceCreationTool(Document): ) prepare_invoice_summary(doctype, invoices) + invoices_summary_companies = list(invoices_summary.keys()) + + for company in invoices_summary_companies: + invoices_summary[escape_html(company)] = invoices_summary.pop(company) + return invoices_summary, max_count def validate_company(self): @@ -102,10 +108,20 @@ class OpeningInvoiceCreationTool(Document): row.due_date = row.due_date or nowdate() def validate_mandatory_invoice_fields(self, row): - if not frappe.db.exists(row.party_type, row.party): - if self.create_missing_party: - self.add_party(row.party_type, row.party) - else: + if self.create_missing_party: + if not row.party and not row.party_name: + frappe.throw(_("Row #{}: Either Party ID or Party Name is required").format(row.idx)) + + if not row.party and row.party_name: + row.party = self.add_party(row.party_type, row.party_name) + + if row.party and not frappe.db.exists(row.party_type, row.party): + row.party = self.add_party(row.party_type, row.party) + + else: + if not row.party: + frappe.throw(_("Row #{}: Party ID is required").format(row.idx)) + if not frappe.db.exists(row.party_type, row.party): frappe.throw( _("Row #{}: {} {} does not exist.").format( row.idx, frappe.bold(row.party_type), frappe.bold(row.party) @@ -113,7 +129,7 @@ class OpeningInvoiceCreationTool(Document): ) mandatory_error_msg = _("Row #{0}: {1} is required to create the Opening {2} Invoices") - for d in ("Party", "Outstanding Amount", "Temporary Opening Account"): + for d in ("Outstanding Amount", "Temporary Opening Account"): if not row.get(scrub(d)): frappe.throw(mandatory_error_msg.format(row.idx, d, self.invoice_type)) @@ -159,6 +175,7 @@ class OpeningInvoiceCreationTool(Document): party_doc.flags.ignore_mandatory = True party_doc.save(ignore_permissions=True) + return party_doc.name def get_invoice_dict(self, row=None): def get_item_dict(): @@ -262,7 +279,8 @@ def start_import(invoices): doc.flags.ignore_mandatory = True doc.insert(set_name=invoice_number) doc.submit() - frappe.db.commit() + if not frappe.in_test: + frappe.db.commit() names.append(doc.name) except Exception: errors += 1 diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool_dashboard.html b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool_dashboard.html index afbcfa5602a..43be52717c3 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool_dashboard.html +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool_dashboard.html @@ -1,5 +1,5 @@ {% $.each(data, (company, summary) => { %} -
| {{ format_currency(summary[doctype].outstanding_amount, summary.currency, 2) }} | - + {% endif %} {% }); %} diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py index 3d57c781983..c01ada6d317 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py @@ -3,10 +3,6 @@ import frappe -from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import ( - create_dimension, - disable_dimension, -) from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import ( get_temporary_opening_account, ) @@ -14,11 +10,6 @@ from erpnext.tests.utils import ERPNextTestSuite class TestOpeningInvoiceCreationTool(ERPNextTestSuite): - def setUp(self): - if not frappe.db.exists("Company", "_Test Opening Invoice Company"): - make_company() - create_dimension() - def make_invoices( self, invoice_type="Sales", @@ -183,19 +174,6 @@ def get_opening_invoice_creation_dict(**args): return invoice_dict -def make_company(): - if frappe.db.exists("Company", "_Test Opening Invoice Company"): - return frappe.get_doc("Company", "_Test Opening Invoice Company") - - company = frappe.new_doc("Company") - company.company_name = "_Test Opening Invoice Company" - company.abbr = "_TOIC" - company.default_currency = "INR" - company.country = "Pakistan" - company.insert() - return company - - def make_customer(customer=None): customer_name = customer or "Opening Customer" customer = frappe.get_doc( diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json b/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json index 29daab42439..74ce2a6fb67 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json @@ -8,6 +8,7 @@ "invoice_number", "party_type", "party", + "party_name", "temporary_opening_account", "column_break_3", "posting_date", @@ -35,9 +36,9 @@ "fieldname": "party", "fieldtype": "Dynamic Link", "in_list_view": 1, - "label": "Party", - "options": "party_type", - "reqd": 1 + "label": "Party ID", + "mandatory_depends_on": "eval: !parent.create_missing_party", + "options": "party_type" }, { "fieldname": "temporary_opening_account", @@ -118,11 +119,17 @@ "fieldname": "supplier_invoice_date", "fieldtype": "Date", "label": "Supplier Invoice Date" + }, + { + "fieldname": "party_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Party Name" } ], "istable": 1, "links": [], - "modified": "2025-12-01 16:18:07.997594", + "modified": "2026-03-20 02:11:42.023575", "modified_by": "Administrator", "module": "Accounts", "name": "Opening Invoice Creation Tool Item", diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.py b/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.py index 1ea025322b4..38e97672c96 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.py @@ -22,7 +22,8 @@ class OpeningInvoiceCreationToolItem(Document): parent: DF.Data parentfield: DF.Data parenttype: DF.Data - party: DF.DynamicLink + party: DF.DynamicLink | None + party_name: DF.Data | None party_type: DF.Link | None posting_date: DF.Date | None qty: DF.Data | None diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 289c19a5885..d60f77120b2 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -2376,9 +2376,7 @@ def get_outstanding_reference_documents(args, validate=False): vouchers=args.get("vouchers") or None, ) - outstanding_invoices = split_invoices_based_on_payment_terms( - outstanding_invoices, args.get("company") - ) + outstanding_invoices = split_refdocs_based_on_payment_terms(outstanding_invoices, args.get("company")) for d in outstanding_invoices: d["exchange_rate"] = 1 @@ -2416,6 +2414,8 @@ def get_outstanding_reference_documents(args, validate=False): filters=args, ) + orders_to_be_billed = split_refdocs_based_on_payment_terms(orders_to_be_billed, args.get("company")) + data = negative_outstanding_invoices + outstanding_invoices + orders_to_be_billed if not data: @@ -2438,13 +2438,13 @@ def get_outstanding_reference_documents(args, validate=False): return data -def split_invoices_based_on_payment_terms(outstanding_invoices, company) -> list: +def split_refdocs_based_on_payment_terms(refdocs, company) -> list: """Split a list of invoices based on their payment terms.""" - exc_rates = get_currency_data(outstanding_invoices, company) + exc_rates = get_currency_data(refdocs, company) - outstanding_invoices_after_split = [] - for entry in outstanding_invoices: - if entry.voucher_type in ["Sales Invoice", "Purchase Invoice"]: + outstanding_refdoc_after_split = [] + for entry in refdocs: + if entry.voucher_type in ["Sales Invoice", "Purchase Invoice", "Sales Order", "Purchase Order"]: if payment_term_template := frappe.db.get_value( entry.voucher_type, entry.voucher_no, "payment_terms_template" ): @@ -2459,25 +2459,25 @@ def split_invoices_based_on_payment_terms(outstanding_invoices, company) -> list ), alert=True, ) - outstanding_invoices_after_split += split_rows + outstanding_refdoc_after_split += split_rows continue # If not an invoice or no payment terms template, add as it is - outstanding_invoices_after_split.append(entry) + outstanding_refdoc_after_split.append(entry) - return outstanding_invoices_after_split + return outstanding_refdoc_after_split -def get_currency_data(outstanding_invoices: list, company: str | None = None) -> dict: +def get_currency_data(outstanding_refdocs: list, company: str | None = None) -> dict: """Get currency and conversion data for a list of invoices.""" exc_rates = frappe._dict() company_currency = frappe.db.get_value("Company", company, "default_currency") if company else None - for doctype in ["Sales Invoice", "Purchase Invoice"]: - invoices = [x.voucher_no for x in outstanding_invoices if x.voucher_type == doctype] + for doctype in ["Sales Invoice", "Purchase Invoice", "Sales Order", "Purchase Order"]: + refdoc = [x.voucher_no for x in outstanding_refdocs if x.voucher_type == doctype] for x in frappe.db.get_all( doctype, - filters={"name": ["in", invoices]}, + filters={"name": ["in", refdoc]}, fields=["name", "currency", "conversion_rate", "party_account_currency"], ): exc_rates[x.name] = frappe._dict( diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 12f5276fbfb..9d2890f5e79 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -2019,6 +2019,92 @@ class TestPaymentEntry(ERPNextTestSuite): self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, pe.doctype, pe.name) self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, "Journal Entry", jv[0]) + def test_outstanding_orders_split_by_payment_terms(self): + create_payment_terms_template() + + so = make_sales_order(do_not_save=1, qty=1, rate=200) + so.payment_terms_template = "Test Receivable Template" + so.save().submit() + + args = { + "posting_date": nowdate(), + "company": so.company, + "party_type": "Customer", + "payment_type": "Receive", + "party": so.customer, + "party_account": "Debtors - _TC", + "get_orders_to_be_billed": True, + } + + references = get_outstanding_reference_documents(args) + + self.assertEqual(len(references), 2) + self.assertEqual(references[0].voucher_no, so.name) + self.assertEqual(references[1].voucher_no, so.name) + self.assertEqual(references[0].payment_term, "Basic Amount Receivable") + self.assertEqual(references[1].payment_term, "Tax Receivable") + + def test_outstanding_orders_no_split_when_allocate_disabled(self): + create_payment_terms_template() + + template = frappe.get_doc("Payment Terms Template", "Test Receivable Template") + template.allocate_payment_based_on_payment_terms = 0 + template.save() + + so = make_sales_order(do_not_save=1, qty=1, rate=200) + so.payment_terms_template = "Test Receivable Template" + so.save().submit() + + args = { + "posting_date": nowdate(), + "company": so.company, + "party_type": "Customer", + "payment_type": "Receive", + "party": so.customer, + "party_account": "Debtors - _TC", + "get_orders_to_be_billed": True, + } + + references = get_outstanding_reference_documents(args) + + self.assertEqual(len(references), 1) + self.assertIsNone(references[0].payment_term) + + template.allocate_payment_based_on_payment_terms = 1 + template.save() + + def test_outstanding_multicurrency_sales_order_split(self): + create_payment_terms_template() + + so = make_sales_order( + customer="_Test Customer USD", + currency="USD", + qty=1, + rate=100, + do_not_submit=True, + ) + so.payment_terms_template = "Test Receivable Template" + so.conversion_rate = 50 + so.save().submit() + + args = { + "posting_date": nowdate(), + "company": so.company, + "party_type": "Customer", + "payment_type": "Receive", + "party": so.customer, + "party_account": "Debtors - _TC", + "get_orders_to_be_billed": True, + } + + references = get_outstanding_reference_documents(args) + + # Should split without throwing currency errors + self.assertEqual(len(references), 2) + for ref in references: + self.assertEqual(ref.voucher_no, so.name) + self.assertIsNotNone(ref.payment_term) + def create_payment_entry(**args): payment_entry = frappe.new_doc("Payment Entry") diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index c95945bf6e2..e16e132957f 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -750,7 +750,8 @@ def make_payment_request(**args): pr.submit() if args.order_type == "Shopping Cart": - frappe.db.commit() + if not frappe.in_test: + frappe.db.commit() frappe.local.response["type"] = "redirect" frappe.local.response["location"] = pr.get_payment_url() diff --git a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py index af5f73a39ec..05e24d16a3a 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py +++ b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py @@ -4,10 +4,6 @@ import unittest import frappe -from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import ( - create_dimension, - disable_dimension, -) from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import ( make_closing_entry_from_opening, ) @@ -162,7 +158,6 @@ class TestPOSClosingEntry(ERPNextTestSuite): test case to check whether we can create POS Closing Entry without mandatory accounting dimension """ - create_dimension() location = frappe.get_doc("Accounting Dimension", "Location") location.dimension_defaults[0].mandatory_for_bs = True location.save() @@ -198,7 +193,6 @@ class TestPOSClosingEntry(ERPNextTestSuite): ) accounting_dimension_department.mandatory_for_bs = 0 accounting_dimension_department.save() - disable_dimension() def test_merging_into_sales_invoice_for_batched_item(self): frappe.flags.print_message = False diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index 9cf27216b1e..a40cd03240e 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -563,10 +563,10 @@ def send_emails(document_name, from_scheduler=False, posting_date=None): new_from_date = add_months(new_to_date, -1 * doc.filter_duration) doc.add_comment("Comment", "Emails sent on: " + frappe.utils.format_datetime(frappe.utils.now())) if doc.report == "General Ledger": - doc.db_set("to_date", new_to_date, commit=True) - doc.db_set("from_date", new_from_date, commit=True) + frappe.db.set_value(doc.doctype, doc.name, "to_date", new_to_date) + frappe.db.set_value(doc.doctype, doc.name, "from_date", new_from_date) else: - doc.db_set("posting_date", new_to_date, commit=True) + frappe.db.set_value(doc.doctype, doc.name, "posting_date", new_to_date) return True else: return False diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index d2021e0f9a4..adb7dad6726 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -8,7 +8,6 @@ "email_append_to": 1, "engine": "InnoDB", "field_order": [ - "title", "naming_series", "supplier", "supplier_name", @@ -209,16 +208,6 @@ "connections_tab" ], "fields": [ - { - "allow_on_submit": 1, - "default": "{supplier_name}", - "fieldname": "title", - "fieldtype": "Data", - "hidden": 1, - "label": "Title", - "no_copy": 1, - "print_hide": 1 - }, { "fieldname": "naming_series", "fieldtype": "Select", @@ -1693,7 +1682,7 @@ "idx": 204, "is_submittable": 1, "links": [], - "modified": "2026-03-17 20:44:00.221219", + "modified": "2026-03-25 11:45:38.696888", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", @@ -1756,6 +1745,6 @@ "sort_order": "DESC", "states": [], "timeline_field": "supplier", - "title_field": "title", + "title_field": "supplier_name", "track_changes": 1 } diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 527c33225c6..7c076e197a5 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -203,7 +203,6 @@ class PurchaseInvoice(BuyingController): taxes_and_charges_deducted: DF.Currency tc_name: DF.Link | None terms: DF.TextEditor | None - title: DF.Data | None to_date: DF.Date | None total: DF.Currency total_advance: DF.Currency @@ -617,12 +616,13 @@ class PurchaseInvoice(BuyingController): frappe.db.set_value(self.doctype, self.name, "against_expense_account", self.against_expense_account) def po_required(self): - if frappe.db.get_single_value("Buying Settings", "po_required") == "Yes": - if frappe.get_value( + if ( + frappe.db.get_single_value("Buying Settings", "po_required") == "Yes" + and not self.is_internal_transfer() + and not frappe.get_value( "Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_order" - ): - return - + ) + ): for d in self.get("items"): if not d.purchase_order: msg = _("Purchase Order Required for item {}").format(frappe.bold(d.item_code)) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 33117c639dc..09febdfd915 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -2189,11 +2189,6 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin): def test_offsetting_entries_for_accounting_dimensions(self): from erpnext.accounts.doctype.account.test_account import create_account - from erpnext.accounts.report.trial_balance.test_trial_balance import ( - clear_dimension_defaults, - create_accounting_dimension, - disable_dimension, - ) create_account( account_name="Offsetting", @@ -2201,7 +2196,16 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin): parent_account="Temporary Accounts - _TC", ) - create_accounting_dimension(company="_Test Company", offsetting_account="Offsetting - _TC") + dim = frappe.get_doc("Accounting Dimension", "Branch") + dim.append( + "dimension_defaults", + { + "company": "_Test Company", + "reference_document": "Branch", + "offsetting_account": "Offsetting - _TC", + }, + ) + dim.save() branch1 = frappe.new_doc("Branch") branch1.branch = "Location 1" @@ -2238,8 +2242,6 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin): voucher_type="Purchase Invoice", additional_columns=["branch"], ) - clear_dimension_defaults("Branch") - disable_dimension() def test_repost_accounting_entries(self): # update repost settings diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 3616f196bb7..002cdc4a43c 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2246,13 +2246,6 @@ class TestSalesInvoice(ERPNextTestSuite): @ERPNextTestSuite.change_settings("Selling Settings", {"allow_multiple_items": True}) def test_rounding_adjustment_3(self): - from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import create_dimension - - # Dimension creates custom field, which does an implicit DB commit as it is a DDL command - # Ensure dimension don't have any mandatory fields - create_dimension() - - # rollback from tearDown() happens till here si = create_sales_invoice(do_not_save=True) si.items = [] for d in [(1122, 2), (1122.01, 1), (1122.01, 1)]: diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py index 0b3da559e39..642f918c3b1 100644 --- a/erpnext/accounts/doctype/subscription/subscription.py +++ b/erpnext/accounts/doctype/subscription/subscription.py @@ -772,7 +772,8 @@ def process_all(subscription: list, posting_date: DateTimeLikeObject | None = No try: subscription = frappe.get_doc("Subscription", subscription_name) subscription.process(posting_date) - frappe.db.commit() + if not frappe.in_test: + frappe.db.commit() except frappe.ValidationError: frappe.db.rollback() subscription.log_error("Subscription failed") diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index 66dc090f7c7..bd633c94dc9 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -18,7 +18,6 @@ class TestTaxWithholdingCategory(ERPNextTestSuite): # create relevant supplier, etc create_records() create_tax_withholding_category_records() - make_pan_no_field() def validate_tax_withholding_entries(self, doctype, docname, expected_entries): """Validate tax withholding entries for a document""" @@ -3998,18 +3997,3 @@ def create_lower_deduction_certificate( "certificate_limit": limit, } ).insert() - - -def make_pan_no_field(): - pan_field = { - "Supplier": [ - { - "fieldname": "pan", - "label": "PAN", - "fieldtype": "Data", - "translatable": 0, - } - ] - } - - create_custom_fields(pan_field, update=1) diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py index caa464c5447..22fabeabca6 100644 --- a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py +++ b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py @@ -48,6 +48,9 @@ class Deferred_Item: Generate report data for output """ ret_data = frappe._dict({"name": self.item_name}) + ret_data.service_start_date = self.service_start_date + ret_data.service_end_date = self.service_end_date + ret_data.amount = self.base_net_amount for period in self.period_total: ret_data[period.key] = period.total ret_data.indent = 1 @@ -205,6 +208,9 @@ class Deferred_Invoice: for item in self.uniq_items: self.items.append(Deferred_Item(item, self, [x for x in items if x.item == item])) + # roll-up amount from all deferred items + self.amount_total = sum(item.base_net_amount for item in self.items) + def calculate_invoice_revenue_expense_for_period(self): """ calculate deferred revenue/expense for all items in invoice @@ -232,7 +238,7 @@ class Deferred_Invoice: generate report data for invoice, includes invoice total """ ret_data = [] - inv_total = frappe._dict({"name": self.name}) + inv_total = frappe._dict({"name": self.name, "amount": self.amount_total}) for x in self.period_total: inv_total[x.key] = x.total inv_total.indent = 0 @@ -386,6 +392,24 @@ class Deferred_Revenue_and_Expense_Report: def get_columns(self): columns = [] columns.append({"label": _("Name"), "fieldname": "name", "fieldtype": "Data", "read_only": 1}) + columns.append( + { + "label": _("Service Start Date"), + "fieldname": "service_start_date", + "fieldtype": "Date", + "read_only": 1, + } + ) + columns.append( + { + "label": _("Service End Date"), + "fieldname": "service_end_date", + "fieldtype": "Date", + "read_only": 1, + } + ) + columns.append({"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "read_only": 1}) + for period in self.period_list: columns.append( { @@ -415,6 +439,8 @@ class Deferred_Revenue_and_Expense_Report: elif self.filters.type == "Expense": total_row = frappe._dict({"name": "Total Deferred Expense"}) + total_row["amount"] = sum(inv.amount_total for inv in self.deferred_invoices) + for idx, period in enumerate(self.period_list, 0): total_row[period.key] = self.period_total[idx].total ret.append(total_row) diff --git a/erpnext/accounts/report/trial_balance/test_trial_balance.py b/erpnext/accounts/report/trial_balance/test_trial_balance.py index 42cf62af0a0..c37f9d5a46a 100644 --- a/erpnext/accounts/report/trial_balance/test_trial_balance.py +++ b/erpnext/accounts/report/trial_balance/test_trial_balance.py @@ -14,7 +14,6 @@ class TestTrialBalance(ERPNextTestSuite): from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center from erpnext.accounts.utils import get_fiscal_year - self.company = create_company() create_cost_center( cost_center_name="Test Cost Center", company="Trial Balance Company", @@ -26,7 +25,16 @@ class TestTrialBalance(ERPNextTestSuite): parent_account="Temporary Accounts - TBC", ) self.fiscal_year = get_fiscal_year(today(), company="Trial Balance Company")[0] - create_accounting_dimension() + dim = frappe.get_doc("Accounting Dimension", "Branch") + dim.append( + "dimension_defaults", + { + "company": "Trial Balance Company", + "automatically_post_balancing_accounting_entry": 1, + "offsetting_account": "Offsetting - TBC", + }, + ) + dim.save() def test_offsetting_entries_for_accounting_dimensions(self): """ @@ -45,7 +53,7 @@ class TestTrialBalance(ERPNextTestSuite): branch2.insert(ignore_if_duplicate=True) si = create_sales_invoice( - company=self.company, + company="Trial Balance Company", debit_to="Debtors - TBC", cost_center="Test Cost Center - TBC", income_account="Sales - TBC", @@ -57,60 +65,7 @@ class TestTrialBalance(ERPNextTestSuite): si.submit() filters = frappe._dict( - {"company": self.company, "fiscal_year": self.fiscal_year, "branch": ["Location 1"]} + {"company": "Trial Balance Company", "fiscal_year": self.fiscal_year, "branch": ["Location 1"]} ) total_row = execute(filters)[1][-1] self.assertEqual(total_row["debit"], total_row["credit"]) - - -def create_company(**args): - args = frappe._dict(args) - company = frappe.get_doc( - { - "doctype": "Company", - "company_name": args.company_name or "Trial Balance Company", - "country": args.country or "India", - "default_currency": args.currency or "INR", - "parent_company": args.get("parent_company"), - "is_group": args.get("is_group"), - } - ) - company.insert(ignore_if_duplicate=True) - return company.name - - -def create_accounting_dimension(**args): - args = frappe._dict(args) - document_type = args.document_type or "Branch" - if frappe.db.exists("Accounting Dimension", document_type): - accounting_dimension = frappe.get_doc("Accounting Dimension", document_type) - accounting_dimension.disabled = 0 - else: - accounting_dimension = frappe.new_doc("Accounting Dimension") - accounting_dimension.document_type = document_type - accounting_dimension.insert() - - accounting_dimension.set("dimension_defaults", []) - accounting_dimension.append( - "dimension_defaults", - { - "company": args.company or "Trial Balance Company", - "automatically_post_balancing_accounting_entry": 1, - "offsetting_account": args.offsetting_account or "Offsetting - TBC", - }, - ) - accounting_dimension.save() - - -def disable_dimension(**args): - args = frappe._dict(args) - document_type = args.document_type or "Branch" - dimension = frappe.get_doc("Accounting Dimension", document_type) - dimension.disabled = 1 - dimension.save() - - -def clear_dimension_defaults(dimension_name): - accounting_dimension = frappe.get_doc("Accounting Dimension", dimension_name) - accounting_dimension.dimension_defaults = [] - accounting_dimension.save() diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index 832e3736e7d..1a9788be48e 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -61,7 +61,9 @@ def book_depreciation_entries(date): accounting_dimensions, ) - frappe.db.commit() + if not frappe.in_test: + frappe.db.commit() + except Exception as e: frappe.db.rollback() failed_assets.append(asset_name) @@ -71,7 +73,8 @@ def book_depreciation_entries(date): if failed_assets: set_depr_entry_posting_status_for_failed_assets(failed_assets) notify_depr_entry_posting_error(failed_assets, error_logs) - frappe.db.commit() + if not frappe.in_test: + frappe.db.commit() def get_depreciable_assets_data(date): diff --git a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py index f245ac4f0a2..e37ac4c2bf3 100644 --- a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py @@ -380,6 +380,7 @@ class TestAssetCapitalization(ERPNextTestSuite): "asset_type": "Composite Component", "purchase_date": pr.posting_date, "available_for_use_date": pr.posting_date, + "location": "Test Location", } ) consumed_asset_doc.save() diff --git a/erpnext/assets/doctype/asset_movement/test_asset_movement.py b/erpnext/assets/doctype/asset_movement/test_asset_movement.py index 88dd93a93cb..76d37d3abb4 100644 --- a/erpnext/assets/doctype/asset_movement/test_asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/test_asset_movement.py @@ -16,7 +16,6 @@ class TestAssetMovement(ERPNextTestSuite): frappe.db.set_value( "Company", "_Test Company", "capital_work_in_progress_account", "CWIP Account - _TC" ) - make_location() def test_movement(self): pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location") @@ -40,10 +39,6 @@ class TestAssetMovement(ERPNextTestSuite): if asset.docstatus == 0: asset.submit() - # check asset movement is created - if not frappe.db.exists("Location", "Test Location 2"): - frappe.get_doc({"doctype": "Location", "location_name": "Test Location 2"}).insert() - create_asset_movement( purpose="Transfer", company=asset.company, @@ -122,9 +117,6 @@ class TestAssetMovement(ERPNextTestSuite): if asset.docstatus == 0: asset.submit() - if not frappe.db.exists("Location", "Test Location 2"): - frappe.get_doc({"doctype": "Location", "location_name": "Test Location 2"}).insert() - movement = frappe.get_doc({"doctype": "Asset Movement", "reference_name": pr.name}) self.assertRaises(frappe.ValidationError, movement.cancel) @@ -150,9 +142,6 @@ class TestAssetMovement(ERPNextTestSuite): asset = create_asset(item_code="Macbook Pro", do_not_save=1) asset.save().submit() - if not frappe.db.exists("Location", "Test Location 2"): - frappe.get_doc({"doctype": "Location", "location_name": "Test Location 2"}).insert() - asset_creation_date = frappe.db.get_value( "Asset Movement", [["Asset Movement Item", "asset", "=", asset.name], ["docstatus", "=", 1]], @@ -197,9 +186,3 @@ def create_asset_movement(**args): movement.submit() return movement - - -def make_location(): - for location in ["Pune", "Mumbai", "Nagpur"]: - if not frappe.db.exists("Location", location): - frappe.get_doc({"doctype": "Location", "location_name": location}).insert(ignore_permissions=True) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 2a1b37aae2a..260dc52ceac 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -9,7 +9,6 @@ "engine": "InnoDB", "field_order": [ "supplier_section", - "title", "naming_series", "supplier", "supplier_name", @@ -172,17 +171,6 @@ "fieldtype": "Section Break", "options": "fa fa-user" }, - { - "allow_on_submit": 1, - "default": "{supplier_name}", - "fieldname": "title", - "fieldtype": "Data", - "hidden": 1, - "label": "Title", - "no_copy": 1, - "print_hide": 1, - "reqd": 1 - }, { "fieldname": "naming_series", "fieldtype": "Select", @@ -1328,7 +1316,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2026-03-09 17:15:29.184682", + "modified": "2026-03-25 11:46:18.748951", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index a0daeca51f2..2cb285c14f3 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -159,7 +159,6 @@ class PurchaseOrder(BuyingController): taxes_and_charges_deducted: DF.Currency tc_name: DF.Link | None terms: DF.TextEditor | None - title: DF.Data to_date: DF.Date | None total: DF.Currency total_net_weight: DF.Float @@ -780,7 +779,8 @@ def make_purchase_invoice_from_portal(purchase_order_name): if frappe.session.user not in frappe.get_all("Portal User", {"parent": doc.supplier}, pluck="user"): frappe.throw(_("Not Permitted"), frappe.PermissionError) doc.save() - frappe.db.commit() + if not frappe.in_test: + frappe.db.commit() frappe.response["type"] = "redirect" frappe.response.location = "/purchase-invoices/" + doc.name @@ -802,18 +802,18 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions target.set_payment_schedule() target.credit_to = get_party_account("Supplier", source.supplier, source.company) + def get_billed_qty(po_item_name): + from frappe.query_builder.functions import Sum + + table = frappe.qb.DocType("Purchase Invoice Item") + query = ( + frappe.qb.from_(table) + .select(Sum(table.qty).as_("qty")) + .where((table.docstatus == 1) & (table.po_detail == po_item_name)) + ) + return query.run(pluck="qty")[0] or 0 + def update_item(obj, target, source_parent): - def get_billed_qty(po_item_name): - from frappe.query_builder.functions import Sum - - table = frappe.qb.DocType("Purchase Invoice Item") - query = ( - frappe.qb.from_(table) - .select(Sum(table.qty).as_("qty")) - .where((table.docstatus == 1) & (table.po_detail == po_item_name)) - ) - return query.run(pluck="qty")[0] or 0 - billed_qty = flt(get_billed_qty(obj.name)) target.qty = flt(obj.qty) - billed_qty @@ -853,7 +853,11 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions "wip_composite_asset": "wip_composite_asset", }, "postprocess": update_item, - "condition": lambda doc: (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount)) + "condition": lambda doc: ( + doc.base_amount == 0 + or abs(doc.billed_amt) < abs(doc.amount) + or doc.qty > flt(get_billed_qty(doc.name)) + ) and select_item(doc), }, "Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "reset_value": True}, diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index fe4bb12c3db..e6956111ea0 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -1386,6 +1386,34 @@ class TestPurchaseOrder(ERPNextTestSuite): self.assertEqual(pi_2.status, "Paid") self.assertEqual(po.status, "Completed") + def test_purchase_order_over_billing_missing_item(self): + item1 = make_item( + "_Test Item for Overbilling", + ).name + + item2 = make_item( + "_Test Item for Overbilling 2", + ).name + + po = create_purchase_order(qty=10, rate=1000, item_code=item1, do_not_save=1) + po.append("items", {"item_code": item2, "qty": 5, "rate": 20, "warehouse": "_Test Warehouse - _TC"}) + po.taxes = [] + po.insert() + po.submit() + + pi1 = make_pi_from_po(po.name) + pi1.items[0].qty = 8 + pi1.items[0].rate = 1250 + pi1.remove(pi1.items[1]) + pi1.insert() + pi1.submit() + + self.assertEqual(pi1.grand_total, 10000.0) + self.assertTrue(len(pi1.items) == 1) + + pi2 = make_pi_from_po(po.name) + self.assertEqual(len(pi2.items), 2) + def create_po_for_sc_testing(): from erpnext.controllers.tests.test_subcontracting_controller import ( 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 b71d0dd3006..8baeba950b9 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js @@ -100,6 +100,7 @@ frappe.ui.form.on("Request for Quotation", { fieldname: "print_format", options: "Print Format", placeholder: "Standard", + default: frappe.get_meta("Request for Quotation").default_print_format || "", get_query: () => { return { filters: { diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json index 18e1356b263..de8b4d28547 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json @@ -9,8 +9,6 @@ "field_order": [ "naming_series", "company", - "billing_address", - "billing_address_display", "vendor", "column_break1", "transaction_date", @@ -43,7 +41,13 @@ "select_print_heading", "letter_head", "more_info", - "opportunity" + "opportunity", + "address_and_contact_tab", + "billing_address", + "billing_address_display", + "column_break_czul", + "shipping_address", + "shipping_address_display" ], "fields": [ { @@ -346,6 +350,27 @@ "fieldtype": "Check", "hidden": 1, "label": "Use HTML" + }, + { + "fieldname": "address_and_contact_tab", + "fieldtype": "Tab Break", + "label": "Address & Contact" + }, + { + "fieldname": "column_break_czul", + "fieldtype": "Column Break" + }, + { + "fieldname": "shipping_address", + "fieldtype": "Link", + "label": "Company Shipping Address", + "options": "Address" + }, + { + "fieldname": "shipping_address_display", + "fieldtype": "Text Editor", + "label": "Shipping Address Details", + "read_only": 1 } ], "grid_page_length": 50, @@ -353,7 +378,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2026-03-09 17:15:29.774614", + "modified": "2026-03-19 15:27:56.730649", "modified_by": "Administrator", "module": "Buying", "name": "Request for Quotation", diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index 38ab9af4eab..1fc2cdf3386 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -56,6 +56,8 @@ class RequestforQuotation(BuyingController): select_print_heading: DF.Link | None send_attached_files: DF.Check send_document_print: DF.Check + shipping_address: DF.Link | None + shipping_address_display: DF.TextEditor | None status: DF.Literal["", "Draft", "Submitted", "Cancelled"] subject: DF.Data suppliers: DF.Table[RequestforQuotationSupplier] diff --git a/erpnext/manufacturing/report/bom_stock_calculated/__init__.py b/erpnext/buying/print_format/request_for_quotation_with_item_image/__init__.py similarity index 100% rename from erpnext/manufacturing/report/bom_stock_calculated/__init__.py rename to erpnext/buying/print_format/request_for_quotation_with_item_image/__init__.py diff --git a/erpnext/buying/print_format/request_for_quotation_with_item_image/request_for_quotation_with_item_image.json b/erpnext/buying/print_format/request_for_quotation_with_item_image/request_for_quotation_with_item_image.json new file mode 100644 index 00000000000..26f131aec5b --- /dev/null +++ b/erpnext/buying/print_format/request_for_quotation_with_item_image/request_for_quotation_with_item_image.json @@ -0,0 +1,33 @@ +{ + "absolute_value": 0, + "align_labels_right": 0, + "creation": "2026-03-19 15:17:39.094444", + "custom_format": 1, + "default_print_language": "en", + "disabled": 0, + "doc_type": "Request for Quotation", + "docstatus": 0, + "doctype": "Print Format", + "font_size": 14, + "html": "{%- macro add_header(page_num, max_pages, doc, letter_head, no_letterhead, footer, print_settings=None, print_heading_template=None) -%}\n\n{% if letter_head and not no_letterhead %}\n
| \n\t\t\t\t\t \n\t\t\t\t\t\t \n\t\t\t\t\t{{ _(\"Supplier Name:\") }} \n\t\t\t\t\t\t{{ _(\"Shipping Address:\") }} \n\t\t\t\t\t\n\t\t\t\t\t\t \n\t\t\t\t{{ doc.vendor }} \n\t\t\t\t\t\t\n \t\t\t\t\t{% if doc.shipping_address %}\n \t\t\t\t\t\t{% set shipping_address = frappe.db.get_value(\"Address\", doc.shipping_address, [\"address_line1\", \"address_line2\", \"city\", \"state\", \"pincode\", \"country\"], as_dict=True) %}\n {{ doc.shipping_address }} \n\n\t\t\t\t\t\n \t\t\t\t\t\t{{ shipping_address.address_line1 or \"\" }} \n \t\t\t\t\t\t{% if shipping_address.address_line2 %}{{ shipping_address.address_line2 }} {% endif %}\n \t\t\t\t\t\t{{ shipping_address.city or \"\" }} {{ shipping_address.state or \"\" }} {{ shipping_address.pincode or \"\" }} {{ shipping_address.country or \"\" }} \n \t\t\t\t\t{% endif %}\n\t\t\t\t\t\t | \n\n\t\t\t\t\n\t\t\t\t\t \n\t\t\t\t\t\t \n\t\t\t\t\t{{ _(\"Order Date:\") }} \n\t\t\t\t\t\n\t\t\t\t\t\t \n\t\t\t\t\t{{ frappe.utils.format_date(doc.transaction_date) }} \n\t\t\t\t\t\n\t\t\t\t\t\t \n\t\t\t\t\t{{ _(\"Required By:\") }} \n\t\t\t\t\t\n\t\t\t\t\t\t \n\t\t\t\t{{ frappe.utils.format_date(doc.schedule_date) }} \n\t\t\t\t\t | \n\t\t\t
| {{ _(\"No\") }} | \n\t\t\t\t\t{{ _(\"Item\") }} | \n\t\t\t\t\t{% if item_naming_by != \"Item Code\" %}\n\t\t\t\t\t\t{{ _(\"Item Code\") }} | \n\t\t\t\t\t{% endif %}\n\t\t\t\t\t{{ _(\"Quantity\") }} | \n\t\t\t\t||
| {{ loop.index }} | \n\t\t\t\t\t\n\t\t\t\t\t\t
| \n\t\t\t\t\t{% if item_naming_by != \"Item Code\" %}\n\t\t\t\t\t\t{{ item.item_code }} | \n\t\t\t\t\t{% endif %}\n\n\t\t\t\t\t{{ item.get_formatted(\"qty\", 0) }} {{ item.uom }} | \n\t\t\t\t
| {%= __("Item") %} | -{%= __("Description") %} | -{%= __("Required Qty") %} | -{%= __("In Stock Qty") %} | -{%= __("Enough Parts to Build") %} | -{%= data[i][ __("Item")] %} | -{%= data[i][ __("Description")] %} | -{%= data[i][ __("Required Qty")] %} | -{%= data[i][ __("In Stock Qty")] %} | -{%= data[i][ __("Enough Parts to Build")] %} | - - {% } %} -
|---|