From d115ddecd319f79d8d6c1a515dec0ac491a09d0d Mon Sep 17 00:00:00 2001 From: 0xD0M1M0 <76812428+0xD0M1M0@users.noreply.github.com> Date: Fri, 15 May 2026 13:08:50 +0000 Subject: [PATCH 1/5] feat: add accounting dimensions to create journal entry --- .../bank_reconciliation_rule.json | 5 +- .../bank_reconciliation_tool_beta.py | 65 +++++++---- .../actions_panel/create_tab.js | 107 +++++++++++++++++- 3 files changed, 153 insertions(+), 24 deletions(-) diff --git a/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json index aa6e6b69..d037cd81 100644 --- a/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json +++ b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json @@ -83,6 +83,7 @@ "fieldtype": "Column Break" }, { + "allow_on_submit": 1, "description": "If multiple rules match a Bank Transaction, only the one with the highest priority is executed.", "fieldname": "priority", "fieldtype": "Int", @@ -95,8 +96,8 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2026-03-28 02:28:06.918430", - "modified_by": "Administrator", + "modified": "2026-05-15 14:33:05.591103", + "modified_by": "dominik@alyf.de", "module": "Klarna Kosma Integration", "name": "Bank Reconciliation Rule", "owner": "Administrator", diff --git a/banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.py b/banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.py index df182a00..869bf6de 100644 --- a/banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.py +++ b/banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.py @@ -6,6 +6,9 @@ import frappe from erpnext import get_company_currency, get_default_cost_center +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( + get_accounting_dimensions, +) from erpnext.accounts.doctype.bank_transaction.bank_transaction import ( get_total_allocated_amount, ) @@ -31,6 +34,28 @@ MAX_QUERY_RESULTS = 150 +def _merge_accounting_dimensions_into_je_accounts( + account_rows: list[dict], accounting_dimensions: str | None +) -> None: + if not accounting_dimensions: + return + + try: + dimensions = json.loads(accounting_dimensions) + except (TypeError, json.JSONDecodeError): + return + + if not isinstance(dimensions, dict): + return + + allowed = set(get_accounting_dimensions(as_list=True)) + for key, value in dimensions.items(): + if key not in allowed or not value: + continue + for row in account_rows: + row[key] = value + + class BankReconciliationToolBeta(Document): # begin: auto-generated types # This code is auto-generated. Do not modify anything in this block. @@ -124,6 +149,7 @@ def create_journal_entry_bts( party_type: str | None = None, party: str | None = None, allow_edit: bool | str = False, + accounting_dimensions: str | None = None, ): """Create a new Journal Entry for Reconciling the Bank Transaction""" if isinstance(allow_edit, str): @@ -163,26 +189,25 @@ def create_journal_entry_bts( "user_remark": bank_transaction.description, } ) - journal_entry.set( - "accounts", - [ - { - "account": second_account, - "credit_in_account_currency": bank_debit_amount, - "debit_in_account_currency": bank_credit_amount, - "party_type": party_type, - "party": party, - "cost_center": get_default_cost_center(company), - }, - { - "account": bank_gl_account, - "bank_account": bank_transaction.bank_account, - "credit_in_account_currency": bank_credit_amount, - "debit_in_account_currency": bank_debit_amount, - "cost_center": get_default_cost_center(company), - }, - ], - ) + account_rows = [ + { + "account": second_account, + "credit_in_account_currency": bank_debit_amount, + "debit_in_account_currency": bank_credit_amount, + "party_type": party_type, + "party": party, + "cost_center": get_default_cost_center(company), + }, + { + "account": bank_gl_account, + "bank_account": bank_transaction.bank_account, + "credit_in_account_currency": bank_credit_amount, + "debit_in_account_currency": bank_debit_amount, + "cost_center": get_default_cost_center(company), + }, + ] + _merge_accounting_dimensions_into_je_accounts(account_rows, accounting_dimensions) + journal_entry.set("accounts", account_rows) company_currency = get_company_currency(company) journal_entry.multi_currency = ( diff --git a/banking/public/js/bank_reconciliation_beta/actions_panel/create_tab.js b/banking/public/js/bank_reconciliation_beta/actions_panel/create_tab.js index 48ed0159..c07b2034 100644 --- a/banking/public/js/bank_reconciliation_beta/actions_panel/create_tab.js +++ b/banking/public/js/bank_reconciliation_beta/actions_panel/create_tab.js @@ -3,18 +3,85 @@ frappe.provide("erpnext.accounts.bank_reconciliation"); erpnext.accounts.bank_reconciliation.CreateTab = class CreateTab { constructor(opts) { Object.assign(this, opts); + this.dimension_fieldnames = []; this.make(); } make() { this.panel_manager.actions_tab = "create_voucher-tab"; + const me = this; + frappe.call({ + method: + "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimensions", + args: {}, + callback: (r) => { + const dimensions = (r.message && r.message[0]) || []; + const company_defaults_map = (r.message && r.message[1]) || {}; + me.dimension_fieldnames = dimensions.map((d) => d.fieldname); + me._build_field_group(dimensions, company_defaults_map); + }, + error: () => { + me.dimension_fieldnames = []; + me._build_field_group([], {}); + }, + }); + } + + _build_field_group(dimensions, company_defaults_map) { this.create_field_group = new frappe.ui.FieldGroup({ - fields: this.get_create_tab_fields(), + fields: this.get_create_tab_fields(dimensions, company_defaults_map), body: this.actions_panel.$tab_content, card_layout: true, }); this.create_field_group.make(); + // Same key as Section (css_class + "-closed"); unset = first visit → start collapsed. + const dim_section_closed_key = + "bank-br-create-accounting-dimensions-closed"; + if (dimensions.length) { + const sec = + this.create_field_group.sections_dict?.accounting_dimensions_section; + if (sec && localStorage.getItem(dim_section_closed_key) === null) { + sec.collapse(true); + } + } + } + + get_accounting_dimension_fields(dimensions, company_defaults_map) { + const defaults_for_company = + company_defaults_map && this.company && company_defaults_map[this.company] + ? company_defaults_map[this.company] + : {}; + return (dimensions || []).map((dimension) => { + const df = { + fieldname: dimension.fieldname, + fieldtype: "Link", + label: __(dimension.label), + options: dimension.document_type, + depends_on: "eval: doc.document_type == 'Journal Entry'", + }; + const default_dim = defaults_for_company[dimension.fieldname]; + if (default_dim) { + df.default = default_dim; + } + return df; + }); + } + + /** Split dimension Link fields across two columns inside the collapsible section. */ + layout_accounting_dimension_fields_two_columns(dimension_fields) { + if (dimension_fields.length <= 1) { + return dimension_fields; + } + const split_at = Math.ceil(dimension_fields.length / 2); + return [ + ...dimension_fields.slice(0, split_at), + { + fieldname: "column_break_accounting_dimensions", + fieldtype: "Column Break", + }, + ...dimension_fields.slice(split_at), + ]; } create_voucher() { @@ -49,6 +116,16 @@ erpnext.accounts.bank_reconciliation.CreateTab = class CreateTab { }); } + get_selected_accounting_dimensions(values) { + const dim_payload = {}; + for (const fn of this.dimension_fieldnames || []) { + if (values[fn]) { + dim_payload[fn] = values[fn]; + } + } + return dim_payload; + } + create_voucher_bts(allow_edit = false, success_callback) { // Create PE or JV and run `success_callback` let values = this.create_field_group.get_values(); @@ -75,10 +152,15 @@ erpnext.accounts.bank_reconciliation.CreateTab = class CreateTab { }; } else { method = method + ".create_journal_entry_bts"; + const dim_payload = this.get_selected_accounting_dimensions(values); args = { ...args, entry_type: values.journal_entry_type, second_account: values.second_account, + accounting_dimensions: + Object.keys(dim_payload).length > 0 + ? JSON.stringify(dim_payload) + : null, }; } @@ -145,10 +227,30 @@ erpnext.accounts.bank_reconciliation.CreateTab = class CreateTab { }); } - get_create_tab_fields() { + get_create_tab_fields(dimensions, company_defaults_map) { let party_type = this.transaction.party_type || (flt(this.transaction.withdrawal) > 0 ? "Supplier" : "Customer"); + const dimension_fields = this.get_accounting_dimension_fields( + dimensions, + company_defaults_map + ); + const dimension_section = + dimension_fields.length > 0 + ? [ + { + fieldtype: "Section Break", + fieldname: "accounting_dimensions_section", + label: __("Accounting Dimensions"), + collapsible: 1, + depends_on: "eval: doc.document_type == 'Journal Entry'", + css_class: "bank-br-create-accounting-dimensions", + }, + ...this.layout_accounting_dimension_fields_two_columns( + dimension_fields + ), + ] + : []; return [ { label: __("Document Type"), @@ -281,6 +383,7 @@ erpnext.accounts.bank_reconciliation.CreateTab = class CreateTab { options: "Cost Center", depends_on: "eval: doc.document_type == 'Payment Entry'", }, + ...dimension_section, { fieldtype: "Section Break", }, From 535f2af645a84b4ddc212f430cb4a045eae06c8b Mon Sep 17 00:00:00 2001 From: 0xD0M1M0 <76812428+0xD0M1M0@users.noreply.github.com> Date: Fri, 15 May 2026 15:50:00 +0000 Subject: [PATCH 2/5] fix: dimensions sync time and GL booking --- .../bank_reconciliation_tool_beta.py | 3 + .../actions_panel/create_tab.js | 95 ++++++++++++++----- 2 files changed, 73 insertions(+), 25 deletions(-) diff --git a/banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.py b/banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.py index 869bf6de..52192c20 100644 --- a/banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.py +++ b/banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.py @@ -53,6 +53,9 @@ def _merge_accounting_dimensions_into_je_accounts( if key not in allowed or not value: continue for row in account_rows: + # Do not tag the bank GL line (like Payment Entry): dimensions belong on the other leg only. + if row.get("bank_account"): + continue row[key] = value diff --git a/banking/public/js/bank_reconciliation_beta/actions_panel/create_tab.js b/banking/public/js/bank_reconciliation_beta/actions_panel/create_tab.js index c07b2034..8cf465cd 100644 --- a/banking/public/js/bank_reconciliation_beta/actions_panel/create_tab.js +++ b/banking/public/js/bank_reconciliation_beta/actions_panel/create_tab.js @@ -10,6 +10,9 @@ erpnext.accounts.bank_reconciliation.CreateTab = class CreateTab { make() { this.panel_manager.actions_tab = "create_voucher-tab"; + this.dimension_fieldnames = []; + this._build_field_group([], {}); + const me = this; frappe.call({ method: @@ -19,11 +22,10 @@ erpnext.accounts.bank_reconciliation.CreateTab = class CreateTab { const dimensions = (r.message && r.message[0]) || []; const company_defaults_map = (r.message && r.message[1]) || {}; me.dimension_fieldnames = dimensions.map((d) => d.fieldname); - me._build_field_group(dimensions, company_defaults_map); + me._append_accounting_dimensions(dimensions, company_defaults_map); }, error: () => { me.dimension_fieldnames = []; - me._build_field_group([], {}); }, }); } @@ -35,18 +37,77 @@ erpnext.accounts.bank_reconciliation.CreateTab = class CreateTab { card_layout: true, }); this.create_field_group.make(); - // Same key as Section (css_class + "-closed"); unset = first visit → start collapsed. + } + + _append_accounting_dimensions(dimensions, company_defaults_map) { + if (!this.create_field_group) { + return; + } + const dimension_section = this._accounting_dimension_section_fields( + dimensions, + company_defaults_map + ); + if (!dimension_section.length) { + return; + } + + this.create_field_group.add_fields(dimension_section); + + const hidden = this.create_field_group.get_field("hidden_field"); + const $footer = + hidden && hidden.$wrapper && hidden.$wrapper.closest(".form-section"); + const $dim = this.create_field_group.wrapper + .find(".bank-br-create-accounting-dimensions") + .closest(".form-section"); + if ($footer && $footer.length && $dim && $dim.length) { + $dim.insertBefore($footer); + } + + this.create_field_group.refresh_dependency(); + const dim_section_closed_key = "bank-br-create-accounting-dimensions-closed"; if (dimensions.length) { - const sec = - this.create_field_group.sections_dict?.accounting_dimensions_section; - if (sec && localStorage.getItem(dim_section_closed_key) === null) { - sec.collapse(true); - } + const collapse_default = () => { + if (localStorage.getItem(dim_section_closed_key) !== null) { + return; + } + const sec = + this.create_field_group.sections_dict + ?.accounting_dimensions_section || + this.create_field_group.sections?.find( + (s) => s.df?.fieldname === "accounting_dimensions_section" + ); + if (sec && typeof sec.collapse === "function") { + sec.collapse(true); + } + }; + collapse_default(); + setTimeout(collapse_default, 0); } } + _accounting_dimension_section_fields(dimensions, company_defaults_map) { + const dimension_fields = this.get_accounting_dimension_fields( + dimensions, + company_defaults_map + ); + if (!dimension_fields.length) { + return []; + } + return [ + { + fieldtype: "Section Break", + fieldname: "accounting_dimensions_section", + label: __("Accounting Dimensions"), + collapsible: 1, + depends_on: "eval: doc.document_type == 'Journal Entry'", + css_class: "bank-br-create-accounting-dimensions", + }, + ...this.layout_accounting_dimension_fields_two_columns(dimension_fields), + ]; + } + get_accounting_dimension_fields(dimensions, company_defaults_map) { const defaults_for_company = company_defaults_map && this.company && company_defaults_map[this.company] @@ -231,26 +292,10 @@ erpnext.accounts.bank_reconciliation.CreateTab = class CreateTab { let party_type = this.transaction.party_type || (flt(this.transaction.withdrawal) > 0 ? "Supplier" : "Customer"); - const dimension_fields = this.get_accounting_dimension_fields( + const dimension_section = this._accounting_dimension_section_fields( dimensions, company_defaults_map ); - const dimension_section = - dimension_fields.length > 0 - ? [ - { - fieldtype: "Section Break", - fieldname: "accounting_dimensions_section", - label: __("Accounting Dimensions"), - collapsible: 1, - depends_on: "eval: doc.document_type == 'Journal Entry'", - css_class: "bank-br-create-accounting-dimensions", - }, - ...this.layout_accounting_dimension_fields_two_columns( - dimension_fields - ), - ] - : []; return [ { label: __("Document Type"), From 3eed95281e85cf4a0d478cfe1f54d461c3b68ce1 Mon Sep 17 00:00:00 2001 From: 0xD0M1M0 <76812428+0xD0M1M0@users.noreply.github.com> Date: Sat, 16 May 2026 20:01:15 +0200 Subject: [PATCH 3/5] chore: add Administrator as modified_by --- .../bank_reconciliation_rule/bank_reconciliation_rule.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json index d037cd81..ad0ce58e 100644 --- a/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json +++ b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json @@ -97,7 +97,7 @@ "is_submittable": 1, "links": [], "modified": "2026-05-15 14:33:05.591103", - "modified_by": "dominik@alyf.de", + "modified_by": "Administrator", "module": "Klarna Kosma Integration", "name": "Bank Reconciliation Rule", "owner": "Administrator", @@ -147,4 +147,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} From 1dee81de8475939135fe35242866519344aa2d93 Mon Sep 17 00:00:00 2001 From: 0xD0M1M0 <76812428+0xD0M1M0@users.noreply.github.com> Date: Sun, 17 May 2026 19:51:31 +0000 Subject: [PATCH 4/5] feat: add standard dimensions and onetime load --- .../bank_reconciliation_rule.json | 1 - .../bank_reconciliation_tool_beta.py | 56 +++++++++-- .../test_bank_reconciliation_tool_beta.py | 40 ++++++++ .../actions_panel/actions_panel_manager.js | 4 + .../actions_panel/create_tab.js | 97 +++++++------------ .../bank_reconciliation_beta/panel_manager.js | 28 ++++-- 6 files changed, 149 insertions(+), 77 deletions(-) diff --git a/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json index ad0ce58e..3860033a 100644 --- a/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json +++ b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json @@ -83,7 +83,6 @@ "fieldtype": "Column Break" }, { - "allow_on_submit": 1, "description": "If multiple rules match a Bank Transaction, only the one with the highest priority is executed.", "fieldname": "priority", "fieldtype": "Int", diff --git a/banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.py b/banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.py index 52192c20..4977f626 100644 --- a/banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.py +++ b/banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.py @@ -33,23 +33,48 @@ MAX_QUERY_RESULTS = 150 +STANDARD_ACCOUNTING_DIMENSION_FIELDS = frozenset({"project", "cost_center"}) -def _merge_accounting_dimensions_into_je_accounts( - account_rows: list[dict], accounting_dimensions: str | None -) -> None: + +def _get_allowed_accounting_dimension_fields() -> set[str]: + """Return whitelisted dimension fieldnames (custom + project + cost_center).""" + return set(get_accounting_dimensions(as_list=True)) | STANDARD_ACCOUNTING_DIMENSION_FIELDS + + +def _parse_accounting_dimensions_json(accounting_dimensions: str | None) -> dict: + """Parse accounting_dimensions JSON from the reco UI; return {} on invalid input.""" if not accounting_dimensions: - return + return {} try: dimensions = json.loads(accounting_dimensions) except (TypeError, json.JSONDecodeError): - return + return {} if not isinstance(dimensions, dict): - return + return {} - allowed = set(get_accounting_dimensions(as_list=True)) - for key, value in dimensions.items(): + return dimensions + + +def _merge_accounting_dimensions_into_payment_entry( + payment_entry: Document, accounting_dimensions: str | None +) -> None: + """Stamp selected accounting dimensions onto Payment Entry header fields.""" + allowed = _get_allowed_accounting_dimension_fields() + for key, value in _parse_accounting_dimensions_json(accounting_dimensions).items(): + if key not in allowed or not value: + continue + if payment_entry.meta.get_field(key): + payment_entry.set(key, value) + + +def _merge_accounting_dimensions_into_je_accounts( + account_rows: list[dict], accounting_dimensions: str | None +) -> None: + """Stamp selected accounting dimensions onto non-bank Journal Entry Account rows.""" + allowed = _get_allowed_accounting_dimension_fields() + for key, value in _parse_accounting_dimensions_json(accounting_dimensions).items(): if key not in allowed or not value: continue for row in account_rows: @@ -154,7 +179,11 @@ def create_journal_entry_bts( allow_edit: bool | str = False, accounting_dimensions: str | None = None, ): - """Create a new Journal Entry for Reconciling the Bank Transaction""" + """Create a new Journal Entry for reconciling the Bank Transaction. + + :param accounting_dimensions: JSON object mapping dimension fieldnames to values + (applied to non-bank account rows only). + """ if isinstance(allow_edit, str): allow_edit = sbool(allow_edit) @@ -252,8 +281,14 @@ def create_payment_entry_bts( mode_of_payment: str | None = None, project: str | None = None, cost_center: str | None = None, + accounting_dimensions: str | None = None, allow_edit: bool = False, ): + """Create a new Payment Entry for reconciling the Bank Transaction. + + :param accounting_dimensions: JSON object mapping dimension fieldnames to values + (applied to Payment Entry header fields). + """ if isinstance(allow_edit, str): allow_edit = sbool(allow_edit) @@ -286,10 +321,13 @@ def create_payment_entry_bts( if mode_of_payment: payment_entry.mode_of_payment = mode_of_payment + + _merge_accounting_dimensions_into_payment_entry(payment_entry, accounting_dimensions) if project: payment_entry.project = project if cost_center: payment_entry.cost_center = cost_center + if payment_type == "Receive": payment_entry.paid_to = company_account else: diff --git a/banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/test_bank_reconciliation_tool_beta.py b/banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/test_bank_reconciliation_tool_beta.py index 01ace1e6..6a6d5a41 100644 --- a/banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/test_bank_reconciliation_tool_beta.py +++ b/banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/test_bank_reconciliation_tool_beta.py @@ -19,6 +19,8 @@ from hrms.hr.doctype.expense_claim.test_expense_claim import make_expense_claim from banking.klarna_kosma_integration.doctype.bank_reconciliation_tool_beta.bank_reconciliation_tool_beta import ( + _merge_accounting_dimensions_into_je_accounts, + _merge_accounting_dimensions_into_payment_entry, auto_reconcile_vouchers, bulk_reconcile_vouchers, create_journal_entry_bts, @@ -265,6 +267,44 @@ def test_pe_against_transaction(self): self.assertEqual(len(bt.payment_entries), 1) self.assertEqual(bt.status, "Reconciled") + def test_merge_accounting_dimensions_into_payment_entry(self): + payment_entry = frappe.new_doc("Payment Entry") + payment_entry.company = "_Test Company" + _merge_accounting_dimensions_into_payment_entry( + payment_entry, + json.dumps( + { + "project": "PROJ-TEST", + "cost_center": "Main - _TC", + "not_a_real_dimension": "ignored", + } + ), + ) + + self.assertEqual(payment_entry.project, "PROJ-TEST") + self.assertEqual(payment_entry.cost_center, "Main - _TC") + + def test_merge_accounting_dimensions_into_je_accounts(self): + account_rows = [ + {"account": "Debtors - _TC"}, + {"account": "Bank - _TC", "bank_account": self.bank_account}, + ] + _merge_accounting_dimensions_into_je_accounts( + account_rows, + json.dumps( + { + "project": "PROJ-TEST", + "cost_center": "Main - _TC", + "not_a_real_dimension": "ignored", + } + ), + ) + + self.assertEqual(account_rows[0]["project"], "PROJ-TEST") + self.assertEqual(account_rows[0]["cost_center"], "Main - _TC") + self.assertNotIn("project", account_rows[1]) + self.assertNotIn("not_a_real_dimension", account_rows[0]) + def test_jv_against_transaction(self): bt = create_bank_transaction(deposit=200, reference_no="abcdef123", bank_account=self.bank_account) create_journal_entry_bts( diff --git a/banking/public/js/bank_reconciliation_beta/actions_panel/actions_panel_manager.js b/banking/public/js/bank_reconciliation_beta/actions_panel/actions_panel_manager.js index 324f147e..cbe05a4b 100644 --- a/banking/public/js/bank_reconciliation_beta/actions_panel/actions_panel_manager.js +++ b/banking/public/js/bank_reconciliation_beta/actions_panel/actions_panel_manager.js @@ -83,6 +83,10 @@ erpnext.accounts.bank_reconciliation.ActionsPanelManager = class ActionsPanelMan transaction: this.transaction, panel_manager: this.panel_manager, company: this.frm.doc.company, + accounting_dimensions: + this.panel_manager.accounting_dimensions || [], + accounting_dimension_defaults: + this.panel_manager.accounting_dimension_defaults || {}, }); }, }, diff --git a/banking/public/js/bank_reconciliation_beta/actions_panel/create_tab.js b/banking/public/js/bank_reconciliation_beta/actions_panel/create_tab.js index 8cf465cd..32e31978 100644 --- a/banking/public/js/bank_reconciliation_beta/actions_panel/create_tab.js +++ b/banking/public/js/bank_reconciliation_beta/actions_panel/create_tab.js @@ -7,43 +7,37 @@ erpnext.accounts.bank_reconciliation.CreateTab = class CreateTab { this.make(); } + /** Build the create form and append cached accounting dimension fields. */ make() { this.panel_manager.actions_tab = "create_voucher-tab"; - this.dimension_fieldnames = []; - this._build_field_group([], {}); + const dimensions = this.accounting_dimensions || []; + const company_defaults_map = this.accounting_dimension_defaults || {}; + this.dimension_fieldnames = dimensions.map((d) => d.fieldname); - const me = this; - frappe.call({ - method: - "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimensions", - args: {}, - callback: (r) => { - const dimensions = (r.message && r.message[0]) || []; - const company_defaults_map = (r.message && r.message[1]) || {}; - me.dimension_fieldnames = dimensions.map((d) => d.fieldname); - me._append_accounting_dimensions(dimensions, company_defaults_map); - }, - error: () => { - me.dimension_fieldnames = []; - }, - }); + this.build_field_group(); + this.append_accounting_dimensions(dimensions, company_defaults_map); } - _build_field_group(dimensions, company_defaults_map) { + /** Render the main create-voucher fields (without accounting dimensions). */ + build_field_group() { this.create_field_group = new frappe.ui.FieldGroup({ - fields: this.get_create_tab_fields(dimensions, company_defaults_map), + fields: this.get_create_tab_fields(), body: this.actions_panel.$tab_content, card_layout: true, }); this.create_field_group.make(); } - _append_accounting_dimensions(dimensions, company_defaults_map) { + /** + * Add the collapsible accounting dimensions section after the base form; + * placed above the footer so Create stays at the bottom. + */ + append_accounting_dimensions(dimensions, company_defaults_map) { if (!this.create_field_group) { return; } - const dimension_section = this._accounting_dimension_section_fields( + const dimension_section = this.build_accounting_dimension_section_fields( dimensions, company_defaults_map ); @@ -87,7 +81,8 @@ erpnext.accounts.bank_reconciliation.CreateTab = class CreateTab { } } - _accounting_dimension_section_fields(dimensions, company_defaults_map) { + /** Field definitions for the accounting dimensions section break and link fields. */ + build_accounting_dimension_section_fields(dimensions, company_defaults_map) { const dimension_fields = this.get_accounting_dimension_fields( dimensions, company_defaults_map @@ -101,13 +96,13 @@ erpnext.accounts.bank_reconciliation.CreateTab = class CreateTab { fieldname: "accounting_dimensions_section", label: __("Accounting Dimensions"), collapsible: 1, - depends_on: "eval: doc.document_type == 'Journal Entry'", css_class: "bank-br-create-accounting-dimensions", }, ...this.layout_accounting_dimension_fields_two_columns(dimension_fields), ]; } + /** Map ERPNext dimension metadata to Link field definitions with company defaults. */ get_accounting_dimension_fields(dimensions, company_defaults_map) { const defaults_for_company = company_defaults_map && this.company && company_defaults_map[this.company] @@ -117,9 +112,8 @@ erpnext.accounts.bank_reconciliation.CreateTab = class CreateTab { const df = { fieldname: dimension.fieldname, fieldtype: "Link", - label: __(dimension.label), + label: __(dimension.label || frappe.model.unscrub(dimension.fieldname)), options: dimension.document_type, - depends_on: "eval: doc.document_type == 'Journal Entry'", }; const default_dim = defaults_for_company[dimension.fieldname]; if (default_dim) { @@ -146,13 +140,16 @@ erpnext.accounts.bank_reconciliation.CreateTab = class CreateTab { } create_voucher() { - var me = this; let values = this.create_field_group.get_values(); let document_type = values.document_type; // Create new voucher and delete or refresh current BT row depending on reconciliation this.create_voucher_bts(false, (message) => - me.actions_panel.after_transaction_reconcile(message, true, document_type) + this.actions_panel.after_transaction_reconcile( + message, + true, + document_type + ) ); } @@ -177,6 +174,7 @@ erpnext.accounts.bank_reconciliation.CreateTab = class CreateTab { }); } + /** Collect non-empty dimension values from the form for the create API call. */ get_selected_accounting_dimensions(values) { const dim_payload = {}; for (const fn of this.dimension_fieldnames || []) { @@ -187,10 +185,16 @@ erpnext.accounts.bank_reconciliation.CreateTab = class CreateTab { return dim_payload; } + /** + * Create Payment Entry or Journal Entry and run `success_callback`. + * Sends selected dimensions as accounting_dimensions JSON for both doctypes. + */ create_voucher_bts(allow_edit = false, success_callback) { - // Create PE or JV and run `success_callback` let values = this.create_field_group.get_values(); let document_type = values.document_type; + const dim_payload = this.get_selected_accounting_dimensions(values); + const accounting_dimensions = + Object.keys(dim_payload).length > 0 ? JSON.stringify(dim_payload) : null; let method = "banking.klarna_kosma_integration.doctype.bank_reconciliation_tool_beta.bank_reconciliation_tool_beta"; let args = { @@ -202,26 +206,17 @@ erpnext.accounts.bank_reconciliation.CreateTab = class CreateTab { posting_date: values.posting_date, mode_of_payment: values.mode_of_payment, allow_edit: allow_edit, + accounting_dimensions: accounting_dimensions, }; if (document_type === "Payment Entry") { method = method + ".create_payment_entry_bts"; - args = { - ...args, - project: values.project, - cost_center: values.cost_center, - }; } else { method = method + ".create_journal_entry_bts"; - const dim_payload = this.get_selected_accounting_dimensions(values); args = { ...args, entry_type: values.journal_entry_type, second_account: values.second_account, - accounting_dimensions: - Object.keys(dim_payload).length > 0 - ? JSON.stringify(dim_payload) - : null, }; } @@ -249,7 +244,6 @@ erpnext.accounts.bank_reconciliation.CreateTab = class CreateTab { // If no response, newly created doc is in draft state // If deleted in response, newly created doc is deleted // If doc object in response, newly created doc is submitted (can be reconciled) - var me = this; frappe.call({ method: "banking.klarna_kosma_integration.doctype.bank_reconciliation_tool_beta.bank_reconciliation_tool_beta.reconcile_voucher", @@ -264,7 +258,7 @@ erpnext.accounts.bank_reconciliation.CreateTab = class CreateTab { frappe.show_alert({ message: __("Failed to reconcile new {0} against {1}", [ doctype, - me.transaction.name, + this.transaction.name, ]), indicator: "red", }); @@ -278,7 +272,7 @@ erpnext.accounts.bank_reconciliation.CreateTab = class CreateTab { return; } - me.actions_panel.after_transaction_reconcile( + this.actions_panel.after_transaction_reconcile( response.message, true, doctype @@ -288,14 +282,10 @@ erpnext.accounts.bank_reconciliation.CreateTab = class CreateTab { }); } - get_create_tab_fields(dimensions, company_defaults_map) { + get_create_tab_fields() { let party_type = this.transaction.party_type || (flt(this.transaction.withdrawal) > 0 ? "Supplier" : "Customer"); - const dimension_section = this._accounting_dimension_section_fields( - dimensions, - company_defaults_map - ); return [ { label: __("Document Type"), @@ -414,21 +404,6 @@ erpnext.accounts.bank_reconciliation.CreateTab = class CreateTab { options: party_type, reqd: 1, }, - { - fieldname: "project", - fieldtype: "Link", - label: "Project", - options: "Project", - depends_on: "eval: doc.document_type == 'Payment Entry'", - }, - { - fieldname: "cost_center", - fieldtype: "Link", - label: "Cost Center", - options: "Cost Center", - depends_on: "eval: doc.document_type == 'Payment Entry'", - }, - ...dimension_section, { fieldtype: "Section Break", }, diff --git a/banking/public/js/bank_reconciliation_beta/panel_manager.js b/banking/public/js/bank_reconciliation_beta/panel_manager.js index 38ae1cdd..5720f41a 100644 --- a/banking/public/js/bank_reconciliation_beta/panel_manager.js +++ b/banking/public/js/bank_reconciliation_beta/panel_manager.js @@ -11,15 +11,19 @@ erpnext.accounts.bank_reconciliation.PanelManager = class PanelManager { } async init_panels() { - const [transactions, document_types] = await Promise.all([ - this.get_bank_transactions(), - frappe.xcall( - "banking.klarna_kosma_integration.doctype.banking_settings.banking_settings.get_doctypes_for_bank_reconciliation" - ), - ]); + const [transactions, document_types, dimensions_message] = + await Promise.all([ + this.get_bank_transactions(), + frappe.xcall( + "banking.klarna_kosma_integration.doctype.banking_settings.banking_settings.get_doctypes_for_bank_reconciliation" + ), + this.get_accounting_dimensions(), + ]); this.transactions = transactions; this.document_types = document_types; + this.accounting_dimensions = dimensions_message?.[0] || []; + this.accounting_dimension_defaults = dimensions_message?.[1] || {}; this.$wrapper.empty(); this.$panel_wrapper = this.$wrapper @@ -33,6 +37,18 @@ erpnext.accounts.bank_reconciliation.PanelManager = class PanelManager { this.render_panels(); } + /** Load dimension metadata once per reco session (incl. project and cost_center). */ + async get_accounting_dimensions() { + const response = await frappe.call({ + method: + "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimensions", + args: { + with_cost_center_and_project: true, + }, + }); + return response.message; + } + async get_bank_transactions() { let transactions = await frappe .call({ From 883a629db7c75b73bb14ab3c39a647607ecc5a31 Mon Sep 17 00:00:00 2001 From: 0xD0M1M0 <76812428+0xD0M1M0@users.noreply.github.com> Date: Fri, 29 May 2026 19:14:58 +0000 Subject: [PATCH 5/5] chore: move project and cost center to fix args --- .../actions_panel/create_tab.js | 144 ++++++++++++------ .../bank_reconciliation_beta/panel_manager.js | 60 ++++++-- 2 files changed, 143 insertions(+), 61 deletions(-) diff --git a/banking/public/js/bank_reconciliation_beta/actions_panel/create_tab.js b/banking/public/js/bank_reconciliation_beta/actions_panel/create_tab.js index 32e31978..be931653 100644 --- a/banking/public/js/bank_reconciliation_beta/actions_panel/create_tab.js +++ b/banking/public/js/bank_reconciliation_beta/actions_panel/create_tab.js @@ -1,5 +1,14 @@ frappe.provide("erpnext.accounts.bank_reconciliation"); +const STANDARD_ACCOUNTING_DIMENSION_FIELDS = [ + { fieldname: "project", label: __("Project"), document_type: "Project" }, + { + fieldname: "cost_center", + label: __("Cost Center"), + document_type: "Cost Center", + }, +]; + erpnext.accounts.bank_reconciliation.CreateTab = class CreateTab { constructor(opts) { Object.assign(this, opts); @@ -11,12 +20,18 @@ erpnext.accounts.bank_reconciliation.CreateTab = class CreateTab { make() { this.panel_manager.actions_tab = "create_voucher-tab"; - const dimensions = this.accounting_dimensions || []; + const custom_dimensions = this.accounting_dimensions || []; const company_defaults_map = this.accounting_dimension_defaults || {}; - this.dimension_fieldnames = dimensions.map((d) => d.fieldname); + this.custom_dimension_fieldnames = custom_dimensions.map( + (d) => d.fieldname + ); + this.dimension_fieldnames = [ + ...STANDARD_ACCOUNTING_DIMENSION_FIELDS.map((d) => d.fieldname), + ...this.custom_dimension_fieldnames, + ]; this.build_field_group(); - this.append_accounting_dimensions(dimensions, company_defaults_map); + this.append_accounting_dimensions(custom_dimensions, company_defaults_map); } /** Render the main create-voucher fields (without accounting dimensions). */ @@ -61,35 +76,60 @@ erpnext.accounts.bank_reconciliation.CreateTab = class CreateTab { const dim_section_closed_key = "bank-br-create-accounting-dimensions-closed"; - if (dimensions.length) { - const collapse_default = () => { - if (localStorage.getItem(dim_section_closed_key) !== null) { - return; - } - const sec = - this.create_field_group.sections_dict - ?.accounting_dimensions_section || - this.create_field_group.sections?.find( - (s) => s.df?.fieldname === "accounting_dimensions_section" - ); - if (sec && typeof sec.collapse === "function") { - sec.collapse(true); - } - }; - collapse_default(); - setTimeout(collapse_default, 0); - } + const collapse_default = () => { + if (localStorage.getItem(dim_section_closed_key) !== null) { + return; + } + const sec = + this.create_field_group.sections_dict?.accounting_dimensions_section || + this.create_field_group.sections?.find( + (s) => s.df?.fieldname === "accounting_dimensions_section" + ); + if (sec && typeof sec.collapse === "function") { + sec.collapse(true); + } + }; + collapse_default(); + setTimeout(collapse_default, 0); } /** Field definitions for the accounting dimensions section break and link fields. */ - build_accounting_dimension_section_fields(dimensions, company_defaults_map) { - const dimension_fields = this.get_accounting_dimension_fields( - dimensions, + build_accounting_dimension_section_fields( + custom_dimensions, + company_defaults_map + ) { + const standard_fields = this.get_accounting_dimension_fields( + STANDARD_ACCOUNTING_DIMENSION_FIELDS, + company_defaults_map + ); + const custom_fields = this.get_accounting_dimension_fields( + custom_dimensions, company_defaults_map ); - if (!dimension_fields.length) { + const left_column = standard_fields[0] ? [standard_fields[0]] : []; + const right_column = standard_fields[1] ? [standard_fields[1]] : []; + + custom_fields.forEach((field, index) => { + if (index % 2 === 0) { + left_column.push(field); + } else { + right_column.push(field); + } + }); + + if (!left_column.length && !right_column.length) { return []; } + + const fields = [...left_column]; + if (right_column.length) { + fields.push({ + fieldname: "column_break_accounting_dimensions", + fieldtype: "Column Break", + }); + fields.push(...right_column); + } + return [ { fieldtype: "Section Break", @@ -98,7 +138,7 @@ erpnext.accounts.bank_reconciliation.CreateTab = class CreateTab { collapsible: 1, css_class: "bank-br-create-accounting-dimensions", }, - ...this.layout_accounting_dimension_fields_two_columns(dimension_fields), + ...fields, ]; } @@ -123,22 +163,6 @@ erpnext.accounts.bank_reconciliation.CreateTab = class CreateTab { }); } - /** Split dimension Link fields across two columns inside the collapsible section. */ - layout_accounting_dimension_fields_two_columns(dimension_fields) { - if (dimension_fields.length <= 1) { - return dimension_fields; - } - const split_at = Math.ceil(dimension_fields.length / 2); - return [ - ...dimension_fields.slice(0, split_at), - { - fieldname: "column_break_accounting_dimensions", - fieldtype: "Column Break", - }, - ...dimension_fields.slice(split_at), - ]; - } - create_voucher() { let values = this.create_field_group.get_values(); let document_type = values.document_type; @@ -174,10 +198,10 @@ erpnext.accounts.bank_reconciliation.CreateTab = class CreateTab { }); } - /** Collect non-empty dimension values from the form for the create API call. */ - get_selected_accounting_dimensions(values) { + /** Collect non-empty dimension values for the given fieldnames. */ + get_selected_accounting_dimensions(values, fieldnames) { const dim_payload = {}; - for (const fn of this.dimension_fieldnames || []) { + for (const fn of fieldnames || []) { if (values[fn]) { dim_payload[fn] = values[fn]; } @@ -185,16 +209,20 @@ erpnext.accounts.bank_reconciliation.CreateTab = class CreateTab { return dim_payload; } + serialize_accounting_dimensions(dim_payload) { + return Object.keys(dim_payload).length > 0 + ? JSON.stringify(dim_payload) + : null; + } + /** * Create Payment Entry or Journal Entry and run `success_callback`. - * Sends selected dimensions as accounting_dimensions JSON for both doctypes. + * Payment Entry: project/cost_center as explicit args; custom dims as JSON. + * Journal Entry: all dimensions (standard + custom) as JSON. */ create_voucher_bts(allow_edit = false, success_callback) { let values = this.create_field_group.get_values(); let document_type = values.document_type; - const dim_payload = this.get_selected_accounting_dimensions(values); - const accounting_dimensions = - Object.keys(dim_payload).length > 0 ? JSON.stringify(dim_payload) : null; let method = "banking.klarna_kosma_integration.doctype.bank_reconciliation_tool_beta.bank_reconciliation_tool_beta"; let args = { @@ -206,17 +234,33 @@ erpnext.accounts.bank_reconciliation.CreateTab = class CreateTab { posting_date: values.posting_date, mode_of_payment: values.mode_of_payment, allow_edit: allow_edit, - accounting_dimensions: accounting_dimensions, }; if (document_type === "Payment Entry") { method = method + ".create_payment_entry_bts"; + const custom_payload = this.get_selected_accounting_dimensions( + values, + this.custom_dimension_fieldnames + ); + args = { + ...args, + project: values.project, + cost_center: values.cost_center, + accounting_dimensions: + this.serialize_accounting_dimensions(custom_payload), + }; } else { method = method + ".create_journal_entry_bts"; + const dim_payload = this.get_selected_accounting_dimensions( + values, + this.dimension_fieldnames + ); args = { ...args, entry_type: values.journal_entry_type, second_account: values.second_account, + accounting_dimensions: + this.serialize_accounting_dimensions(dim_payload), }; } diff --git a/banking/public/js/bank_reconciliation_beta/panel_manager.js b/banking/public/js/bank_reconciliation_beta/panel_manager.js index 5720f41a..88989f07 100644 --- a/banking/public/js/bank_reconciliation_beta/panel_manager.js +++ b/banking/public/js/bank_reconciliation_beta/panel_manager.js @@ -11,19 +11,33 @@ erpnext.accounts.bank_reconciliation.PanelManager = class PanelManager { } async init_panels() { + const dimensions_cached = this.accounting_dimensions != null; + const document_types_cached = this.document_types != null; + const [transactions, document_types, dimensions_message] = await Promise.all([ this.get_bank_transactions(), - frappe.xcall( - "banking.klarna_kosma_integration.doctype.banking_settings.banking_settings.get_doctypes_for_bank_reconciliation" - ), - this.get_accounting_dimensions(), + document_types_cached + ? Promise.resolve(this.document_types) + : frappe.xcall( + "banking.klarna_kosma_integration.doctype.banking_settings.banking_settings.get_doctypes_for_bank_reconciliation" + ), + dimensions_cached + ? Promise.resolve([ + this.accounting_dimensions, + this.accounting_dimension_defaults, + ]) + : this.get_accounting_dimensions(), ]); this.transactions = transactions; - this.document_types = document_types; - this.accounting_dimensions = dimensions_message?.[0] || []; - this.accounting_dimension_defaults = dimensions_message?.[1] || {}; + if (!document_types_cached) { + this.document_types = document_types; + } + if (!dimensions_cached) { + this.accounting_dimensions = dimensions_message?.[0] || []; + this.accounting_dimension_defaults = dimensions_message?.[1] || {}; + } this.$wrapper.empty(); this.$panel_wrapper = this.$wrapper @@ -37,13 +51,38 @@ erpnext.accounts.bank_reconciliation.PanelManager = class PanelManager { this.render_panels(); } - /** Load dimension metadata once per reco session (incl. project and cost_center). */ + /** Re-fetch and re-render the transaction list (e.g. after sort change). */ + async reload_transactions() { + this.transactions = await this.get_bank_transactions(); + const active_name = this.active_transaction?.name; + + if (!this.transactions?.length) { + this.active_transaction = null; + this.$panel_wrapper.empty(); + this.render_no_transactions(); + return; + } + + this.$list_container.empty(); + this.render_transactions_list(); + + const $row = active_name + ? this.$list_container.find("#" + active_name) + : $(); + if ($row.length) { + $row.click(); + } else { + this.$list_container.find(".transaction-row").first().click(); + } + } + + /** Load custom accounting dimension metadata once per reco session. */ async get_accounting_dimensions() { const response = await frappe.call({ method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimensions", args: { - with_cost_center_and_project: true, + with_cost_center_and_project: false, }, }); return response.message; @@ -156,8 +195,7 @@ erpnext.accounts.bank_reconciliation.PanelManager = class PanelManager { me.order_direction = sort_order || me.order_direction || "asc"; me.order = me.order_by + " " + me.order_direction; - // Re-render the list - me.init_panels(); + me.reload_transactions(); }, }); }