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 824251dc87a..eeac8b1ebc3 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 @@ -82,29 +82,31 @@ frappe.ui.form.on("Opening Invoice Creation Tool", { }, setup_company_filters: function (frm) { - frm.set_query("cost_center", "invoices", function (doc, cdt, cdn) { - return { - filters: { - company: doc.company, - }, - }; + frm.events.apply_company_query_filter(frm, "cost_center", "invoices", { is_group: 0 }); + frm.events.apply_company_query_filter(frm, "project", "invoices"); + frm.events.apply_company_query_filter(frm, "project"); + frm.events.apply_company_query_filter(frm, "cost_center", undefined, { is_group: 0 }); + frm.events.apply_company_query_filter(frm, "temporary_opening_account", "invoices", { + account_type: "Temporary", + is_group: 0, }); + }, - frm.set_query("cost_center", function (doc) { + apply_company_query_filter: function (frm, field_name, child_doctype = null, filters = {}) { + const query = function (doc) { return { filters: { company: doc.company, + ...filters, }, }; - }); + }; - frm.set_query("temporary_opening_account", "invoices", function (doc, cdt, cdn) { - return { - filters: { - company: doc.company, - }, - }; - }); + if (child_doctype) { + frm.set_query(field_name, child_doctype, query); + } else { + frm.set_query(field_name, query); + } }, company: function (frm) { @@ -128,10 +130,7 @@ 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 = ""; - }); + frm.clear_table("invoices"); frm.refresh_fields(); }, @@ -172,7 +171,19 @@ frappe.ui.form.on("Opening Invoice Creation Tool", { }); frappe.ui.form.on("Opening Invoice Creation Tool Item", { - invoices_add: (frm) => { + invoices_add: (frm, cdt, cdn) => { + const row = frappe.get_doc(cdt, cdn); + const field_copy = []; + + ["project", "cost_center"].forEach((fieldname) => { + if (frm.doc[fieldname]) { + frappe.model.set_value(cdt, cdn, fieldname, frm.doc[fieldname]); + } else { + field_copy.push(fieldname); + } + }); + + frm.script_manager.copy_from_first_row("invoices", row, field_copy); frm.trigger("update_invoice_table"); }, }); 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 32d2c40cd9e..48551eab3b2 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 @@ -122,6 +122,17 @@ class OpeningInvoiceCreationTool(Document): if not row.get(scrub(d)): frappe.throw(mandatory_error_msg.format(row.idx, d, self.invoice_type)) + self.validate_temporary_opening_account(row) + + def validate_temporary_opening_account(self, row): + account_type = frappe.get_cached_value("Account", row.temporary_opening_account, "account_type") + if account_type != "Temporary": + frappe.throw( + _("Row #{0}: {1} account is not of type {2}").format( + row.idx, row.temporary_opening_account, "Temporary" + ) + ) + def get_invoices(self): invoices = [] for row in self.invoices: @@ -191,6 +202,7 @@ class OpeningInvoiceCreationTool(Document): "description": row.item_name or "Opening Invoice Item", income_expense_account_field: row.temporary_opening_account, "cost_center": cost_center, + "project": row.get("project") or self.get("project"), } ) 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 bfab823f495..9bb69cacd33 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,6 +3,7 @@ import frappe from frappe.tests.utils import FrappeTestCase +from frappe.utils import add_days, today from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import ( create_dimension, @@ -11,6 +12,7 @@ from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension imp from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import ( get_temporary_opening_account, ) +from erpnext.projects.doctype.project.test_project import make_project test_dependencies = ["Customer", "Supplier", "Accounting Dimension"] @@ -27,21 +29,26 @@ class TestOpeningInvoiceCreationTool(FrappeTestCase): self, invoice_type="Sales", company=None, - party_1=None, - party_2=None, - invoice_number=None, + invoices=None, + project=None, + cost_center=None, department=None, + return_doc=False, ): doc = frappe.get_single("Opening Invoice Creation Tool") args = get_opening_invoice_creation_dict( invoice_type=invoice_type, company=company, - party_1=party_1, - party_2=party_2, - invoice_number=invoice_number, + invoices=invoices, + project=project, + cost_center=cost_center, department=department, ) doc.update(args) + + if return_doc: + return doc + return doc.make_invoices() def test_opening_sales_invoice_creation(self): @@ -50,8 +57,8 @@ class TestOpeningInvoiceCreationTool(FrappeTestCase): self.assertEqual(len(invoices), 2) expected_value = { "keys": ["customer", "outstanding_amount", "status"], - 0: ["_Test Customer", 300, "Overdue"], - 1: ["_Test Customer 1", 250, "Overdue"], + 0: ["_Test Customer", 200, "Overdue"], + 1: ["_Test Customer 1", 200, "Overdue"], } self.check_expected_values(invoices, expected_value) @@ -68,48 +75,34 @@ class TestOpeningInvoiceCreationTool(FrappeTestCase): for field_idx, field in enumerate(expected_value["keys"]): self.assertEqual(si.get(field, ""), expected_value[invoice_idx][field_idx]) + def test_opening_invoice_requires_temporary_account_type(self): + doc = self.make_invoices(company="_Test Opening Invoice Company", return_doc=True) + doc.invoices[0].temporary_opening_account = "Sales - _TOIC" + self.assertRaises(frappe.ValidationError, doc.make_invoices) + def test_opening_purchase_invoice_creation(self): invoices = self.make_invoices(invoice_type="Purchase", company="_Test Opening Invoice Company") self.assertEqual(len(invoices), 2) expected_value = { "keys": ["supplier", "outstanding_amount", "status"], - 0: ["_Test Supplier", 300, "Overdue"], - 1: ["_Test Supplier 1", 250, "Overdue"], + 0: ["_Test Supplier", 200, "Overdue"], + 1: ["_Test Supplier 1", 200, "Overdue"], } self.check_expected_values(invoices, expected_value, "Purchase") def test_opening_sales_invoice_creation_with_missing_debit_account(self): - company = "_Test Opening Invoice Company" party_1, party_2 = make_customer("Customer A"), make_customer("Customer B") - old_default_receivable_account = frappe.db.get_value("Company", company, "default_receivable_account") - frappe.db.set_value("Company", company, "default_receivable_account", "") + old_default_receivable_account = frappe.db.get_value( + "Company", "_Test Opening Invoice Company", "default_receivable_account" + ) + frappe.db.set_value("Company", "_Test Opening Invoice Company", "default_receivable_account", "") - if not frappe.db.exists("Cost Center", "_Test Opening Invoice Company - _TOIC"): - cc = frappe.get_doc( - { - "doctype": "Cost Center", - "cost_center_name": "_Test Opening Invoice Company", - "is_group": 1, - "company": "_Test Opening Invoice Company", - } - ) - cc.insert(ignore_mandatory=True) - cc2 = frappe.get_doc( - { - "doctype": "Cost Center", - "cost_center_name": "Main", - "is_group": 0, - "company": "_Test Opening Invoice Company", - "parent_cost_center": cc.name, - } - ) - cc2.insert() - - frappe.db.set_value("Company", company, "cost_center", "Main - _TOIC") - - self.make_invoices(company="_Test Opening Invoice Company", party_1=party_1, party_2=party_2) + self.make_invoices( + company="_Test Opening Invoice Company", + invoices=[{"party": party_1}, {"party": party_2}], + ) # Check if missing debit account error raised error_log = frappe.db.exists( @@ -119,74 +112,113 @@ class TestOpeningInvoiceCreationTool(FrappeTestCase): self.assertTrue(error_log) # teardown - frappe.db.set_value("Company", company, "default_receivable_account", old_default_receivable_account) - - def test_renaming_of_invoice_using_invoice_number_field(self): - company = "_Test Opening Invoice Company" - party_1, party_2 = make_customer("Customer A"), make_customer("Customer B") - self.make_invoices( - company=company, party_1=party_1, party_2=party_2, invoice_number="TEST-NEW-INV-11" + frappe.db.set_value( + "Company", + "_Test Opening Invoice Company", + "default_receivable_account", + old_default_receivable_account, ) - sales_inv1 = frappe.get_all("Sales Invoice", filters={"customer": "Customer A"})[0].get("name") - sales_inv2 = frappe.get_all("Sales Invoice", filters={"customer": "Customer B"})[0].get("name") - self.assertEqual(sales_inv1, "TEST-NEW-INV-11") + def test_renaming_of_invoice_using_invoice_number_field(self): + party_1, party_2 = make_customer("Customer A"), make_customer("Customer B") + inv_num = f"TEST-NEW-INV-{frappe.generate_hash(length=8)}" + invoices = self.make_invoices( + company="_Test Opening Invoice Company", + invoices=[ + {"party": party_1, "invoice_number": inv_num}, + {"party": party_2}, + ], + ) - # teardown - for inv in [sales_inv1, sales_inv2]: - doc = frappe.get_doc("Sales Invoice", inv) - doc.cancel() + self.assertEqual(invoices[0], inv_num) def test_opening_invoice_with_accounting_dimension(self): invoices = self.make_invoices( invoice_type="Sales", company="_Test Opening Invoice Company", department="Sales - _TOIC" ) - expected_value = { - "keys": ["customer", "outstanding_amount", "status", "department"], - 0: ["_Test Customer", 300, "Overdue", "Sales - _TOIC"], - 1: ["_Test Customer 1", 250, "Overdue", "Sales - _TOIC"], - } - self.check_expected_values(invoices, expected_value, invoice_type="Sales") + for invoice in invoices: + self.assertEqual(frappe.db.get_value("Sales Invoice", invoice, "department"), "Sales - _TOIC") - def tearDown(self): + def test_opening_entry_project_linking(self): + doc = self.make_invoices( + company="_Test Opening Invoice Company", invoice_type="Sales", return_doc=True + ) + project_1 = make_project( + {"project_name": "Test Opening Invoice projecty 01", "company": "_Test Opening Invoice Company"} + ) + project_2 = make_project( + {"project_name": "Test Opening Invoice projecty 02", "company": "_Test Opening Invoice Company"} + ) + doc.invoices[0].project = project_1.name + doc.invoices[1].project = project_2.name + invoices = doc.make_invoices() + sales_invoice_1 = frappe.get_doc("Sales Invoice", invoices[0]) + sales_invoice_2 = frappe.get_doc("Sales Invoice", invoices[1]) + + self.assertEqual(sales_invoice_1.items[0].project, project_1.name) + self.assertEqual(sales_invoice_2.items[0].project, project_2.name) + + @classmethod + def tearDownClass(cls): disable_dimension() + super().tearDownClass() def get_opening_invoice_creation_dict(**args): party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier" company = args.get("company", "_Test Company") + default_invoices = [] + default_invoice_rows = [ + { + "qty": 1.0, + "outstanding_amount": 200, + "party": f"_Test {party}", + "item_name": "Opening Item", + "due_date": add_days(today(), -10), + "posting_date": add_days(today(), -15), + "temporary_opening_account": get_temporary_opening_account(company), + }, + { + "qty": 1.0, + "outstanding_amount": 200, + "party": f"_Test {party} 1", + "item_name": "Opening Item", + "due_date": add_days(today(), -10), + "posting_date": add_days(today(), -15), + "temporary_opening_account": get_temporary_opening_account(company), + }, + ] + + for row in args.get("invoices") or default_invoice_rows: + default_invoices.append( + { + "qty": row.get("qty") or 1.0, + "outstanding_amount": row.get("outstanding_amount") or 200, + "party": row.get("party") or f"_Test {party}", + "item_name": row.get("item_name") or "Opening Item", + "due_date": row.get("due_date") or add_days(today(), -10), + "posting_date": row.get("posting_date") or add_days(today(), -15), + "temporary_opening_account": row.get("temporary_opening_account") + or get_temporary_opening_account(company), + "invoice_number": row.get("invoice_number"), + "project": row.get("project"), + "cost_center": row.get("cost_center"), + } + ) invoice_dict = frappe._dict( { "company": company, "invoice_type": args.get("invoice_type", "Sales"), - "invoices": [ - { - "qty": 1.0, - "outstanding_amount": 300, - "party": args.get("party_1") or f"_Test {party}", - "item_name": "Opening Item", - "due_date": "2016-09-10", - "posting_date": "2016-09-05", - "temporary_opening_account": get_temporary_opening_account(company), - "invoice_number": args.get("invoice_number"), - }, - { - "qty": 2.0, - "outstanding_amount": 250, - "party": args.get("party_2") or f"_Test {party} 1", - "item_name": "Opening Item", - "due_date": "2016-09-10", - "posting_date": "2016-09-05", - "temporary_opening_account": get_temporary_opening_account(company), - "invoice_number": None, - }, - ], + "project": args.get("project"), + "cost_center": args.get("cost_center"), + "invoices": default_invoices, } ) invoice_dict.update(args) + invoice_dict.invoices = default_invoices return invoice_dict 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 d1d65dd9185..f8aa72fbf21 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 @@ -19,7 +19,8 @@ "qty", "accounting_dimensions_section", "cost_center", - "dimension_col_break" + "dimension_col_break", + "project" ], "fields": [ { @@ -112,11 +113,17 @@ "fieldname": "invoice_number", "fieldtype": "Data", "label": "Invoice Number" + }, + { + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" } ], "istable": 1, "links": [], - "modified": "2026-07-02 15:17:11.938499", + "modified": "2026-07-03 15:17:11.938499", "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 bc48300286f..38df591b727 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 @@ -25,6 +25,7 @@ class OpeningInvoiceCreationToolItem(Document): party: DF.DynamicLink party_type: DF.Link | None posting_date: DF.Date | None + project: DF.Link | None qty: DF.Data | None temporary_opening_account: DF.Link | None # end: auto-generated types