From 1634ecd4f6f39dac4397060e0a2e404d004f1cf9 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sun, 19 Apr 2026 12:51:38 -0400 Subject: [PATCH] feat(fusion_accounting_bank_rec): mirror Enterprise OWL batch 1 (display components) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors 4 OWL components from account_accountant for Phase 1 structural parity: - statement_line/ (display + interactivity for one bank line) - statement_summary/ (header summary card per statement) - line_info_pop_over/ (popover with extra info on hover) - reconciled_line_name/ (label for already-reconciled lines) Plus the Enterprise-compat surface added to fusion_bank_reconciliation service: - useBankReconciliation() hook export - chatterState reactive (visible, statementLine) - reconcileCountPerPartnerId / reconcileModelPerStatementLineId - selectStatementLine, openChatter, toggleChatter, reloadChatter - computeReconcileLineCountPerPartnerId (no-op stub) - computeAvailableReconcileModels (no-op stub) - updateAvailableReconcileModels (no-op stub) - reloadRecords helper - statementLine{,MoveId,Move,Id} getters Service now also depends on `orm`. A components/bank_reconciliation/bank_reconciliation_service.js re-export shim lets mirrored components keep their relative `../bank_reconciliation_service` imports verbatim. Renames applied per spec: - account_accountant.* -> fusion_accounting_bank_rec.* (template names) - @account_accountant/... -> @fusion_accounting_bank_rec/... (module IDs) - useService("bank_reconciliation_service") -> useService("fusion_bank_reconciliation") Forward imports to batch 2 components (button_list, line_to_reconcile) resolve lazily — files are on disk and bundled in subsequent batches. Phase 1 prioritizes structural parity; behaviour wired up in fusion-only Tasks 34-36. Manifest version bumped to 19.0.1.0.12. Module upgrade succeeds, 134 logical tests still pass. Made-with: Cursor --- fusion_accounting_bank_rec/__manifest__.py | 15 +- .../bank_reconciliation_service.js | 14 + .../line_info_pop_over/line_info_pop_over.js | 80 +++++ .../line_info_pop_over/line_info_pop_over.xml | 24 ++ .../reconciled_line_name.js | 40 +++ .../reconciled_line_name.xml | 16 + .../statement_line/statement_line.js | 305 ++++++++++++++++++ .../statement_line/statement_line.xml | 115 +++++++ .../statement_summary/statement_summary.js | 42 +++ .../statement_summary/statement_summary.xml | 24 ++ .../services/bank_reconciliation_service.js | 147 ++++++++- 11 files changed, 819 insertions(+), 3 deletions(-) create mode 100644 fusion_accounting_bank_rec/static/src/components/bank_reconciliation/bank_reconciliation_service.js create mode 100644 fusion_accounting_bank_rec/static/src/components/bank_reconciliation/line_info_pop_over/line_info_pop_over.js create mode 100644 fusion_accounting_bank_rec/static/src/components/bank_reconciliation/line_info_pop_over/line_info_pop_over.xml create mode 100644 fusion_accounting_bank_rec/static/src/components/bank_reconciliation/reconciled_line_name/reconciled_line_name.js create mode 100644 fusion_accounting_bank_rec/static/src/components/bank_reconciliation/reconciled_line_name/reconciled_line_name.xml create mode 100644 fusion_accounting_bank_rec/static/src/components/bank_reconciliation/statement_line/statement_line.js create mode 100644 fusion_accounting_bank_rec/static/src/components/bank_reconciliation/statement_line/statement_line.xml create mode 100644 fusion_accounting_bank_rec/static/src/components/bank_reconciliation/statement_summary/statement_summary.js create mode 100644 fusion_accounting_bank_rec/static/src/components/bank_reconciliation/statement_summary/statement_summary.xml diff --git a/fusion_accounting_bank_rec/__manifest__.py b/fusion_accounting_bank_rec/__manifest__.py index 80a07454..d81cede4 100644 --- a/fusion_accounting_bank_rec/__manifest__.py +++ b/fusion_accounting_bank_rec/__manifest__.py @@ -1,6 +1,6 @@ { 'name': 'Fusion Accounting — Bank Reconciliation', - 'version': '19.0.1.0.11', + 'version': '19.0.1.0.12', 'category': 'Accounting/Accounting', 'sequence': 28, 'summary': 'Native V19 bank reconciliation widget with AI confidence scoring + behavioural learning.', @@ -43,6 +43,19 @@ Built by Nexa Systems Inc. 'fusion_accounting_bank_rec/static/src/views/kanban/bank_rec_kanban_renderer.js', 'fusion_accounting_bank_rec/static/src/views/kanban/bank_rec_kanban_view.js', 'fusion_accounting_bank_rec/static/src/views/kanban/bank_rec_kanban.xml', + # OWL component mirror — Enterprise account_accountant bank-rec. + # Re-export shim so mirrored components can use the relative + # `../bank_reconciliation_service` import unchanged. + 'fusion_accounting_bank_rec/static/src/components/bank_reconciliation/bank_reconciliation_service.js', + # Batch 1 (Task 30) — display components + 'fusion_accounting_bank_rec/static/src/components/bank_reconciliation/statement_line/statement_line.js', + 'fusion_accounting_bank_rec/static/src/components/bank_reconciliation/statement_line/statement_line.xml', + 'fusion_accounting_bank_rec/static/src/components/bank_reconciliation/statement_summary/statement_summary.js', + 'fusion_accounting_bank_rec/static/src/components/bank_reconciliation/statement_summary/statement_summary.xml', + 'fusion_accounting_bank_rec/static/src/components/bank_reconciliation/line_info_pop_over/line_info_pop_over.js', + 'fusion_accounting_bank_rec/static/src/components/bank_reconciliation/line_info_pop_over/line_info_pop_over.xml', + 'fusion_accounting_bank_rec/static/src/components/bank_reconciliation/reconciled_line_name/reconciled_line_name.js', + 'fusion_accounting_bank_rec/static/src/components/bank_reconciliation/reconciled_line_name/reconciled_line_name.xml', ], }, 'installable': True, diff --git a/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/bank_reconciliation_service.js b/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/bank_reconciliation_service.js new file mode 100644 index 00000000..cd02c726 --- /dev/null +++ b/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/bank_reconciliation_service.js @@ -0,0 +1,14 @@ +/** @odoo-module **/ + +/** + * Re-export shim so mirrored Enterprise components can use the relative + * import `../bank_reconciliation_service` unchanged. The real + * implementation lives in + * `@fusion_accounting_bank_rec/services/bank_reconciliation_service`. + */ + +export { + BankReconciliationService, + bankReconciliationService, + useBankReconciliation, +} from "@fusion_accounting_bank_rec/services/bank_reconciliation_service"; diff --git a/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/line_info_pop_over/line_info_pop_over.js b/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/line_info_pop_over/line_info_pop_over.js new file mode 100644 index 00000000..c30fe520 --- /dev/null +++ b/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/line_info_pop_over/line_info_pop_over.js @@ -0,0 +1,80 @@ +/** @odoo-module **/ + +/** + * Mirrored from + * `account_accountant/.../line_info_pop_over/line_info_pop_over.js`. + * Phase 1 structural parity. + */ + +import { Component } from "@odoo/owl"; +import { formatMonetary } from "@web/views/fields/formatters"; +import { useService } from "@web/core/utils/hooks"; + +export class BankRecLineInfoPopOver extends Component { + static template = "fusion_accounting_bank_rec.BankRecLineInfoPopOver"; + static props = { + lineData: { type: Object, optional: true }, + statementLineData: { type: Object, optional: true }, + exchangeMove: { type: Object, optional: true }, + isPartiallyReconciled: { type: Boolean, optional: true }, + close: { type: Function, optional: true }, + }; + + setup() { + this.action = useService("action"); + } + + openExchangeMove() { + this.action.doAction({ + type: "ir.actions.act_window", + res_model: "account.move", + res_id: this.props.exchangeMove.id, + views: [[false, "form"]], + target: "current", + }); + } + + openReconciledMove() { + this.action.doAction({ + type: "ir.actions.act_window", + res_model: "account.move", + res_id: this.reconciledLineData.move_id.id, + views: [[false, "form"]], + target: "current", + }); + } + + get reconciledMoveName() { + return this.reconciledLineData.move_name; + } + + get formattedReconciledMoveAmountCurrency() { + return formatMonetary(this.reconciledLineData.amount_currency, { + currencyId: this.reconciledLineData.currency_id.id, + }); + } + + get reconciledLineData() { + return this.props.lineData.reconciled_lines_ids.records[0].data; + } + + get formattedLineDataAmountCurrency() { + return formatMonetary(this.props.lineData.amount_currency, { + currencyId: this.props.lineData.currency_id.id, + }); + } + + get exchangeDiffMoveName() { + return this.props.exchangeMove.display_name; + } + + get exchangeMoveBalance() { + return this.props.exchangeMove.line_ids[0].balance; + } + + get formattedExchangeMoveBalance() { + return formatMonetary(this.exchangeMoveBalance, { + currencyId: this.props.statementLineData.company_id.currency_id?.id, + }); + } +} diff --git a/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/line_info_pop_over/line_info_pop_over.xml b/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/line_info_pop_over/line_info_pop_over.xml new file mode 100644 index 00000000..b932b14d --- /dev/null +++ b/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/line_info_pop_over/line_info_pop_over.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + +
+ + +
+ + + + +
+
+
diff --git a/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/reconciled_line_name/reconciled_line_name.js b/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/reconciled_line_name/reconciled_line_name.js new file mode 100644 index 00000000..e1d8ef97 --- /dev/null +++ b/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/reconciled_line_name/reconciled_line_name.js @@ -0,0 +1,40 @@ +/** @odoo-module **/ + +/** + * Mirrored from + * `account_accountant/.../reconciled_line_name/reconciled_line_name.js`. + * Phase 1 structural parity. + */ + +import { Component } from "@odoo/owl"; +import { useBankReconciliation } from "@fusion_accounting_bank_rec/components/bank_reconciliation/bank_reconciliation_service"; +import { useService } from "@web/core/utils/hooks"; +import { x2ManyCommands } from "@web/core/orm_service"; + +export class BankRecReconciledLineName extends Component { + static template = "fusion_accounting_bank_rec.BankRecReconciledLineName"; + static props = { + statementLine: { type: Object }, + linesToReconcile: { type: Object }, + moveLineId: { type: String }, + valueToDisplay: { type: Object }, + }; + + setup() { + this.orm = useService("orm"); + this.bankReconciliation = useBankReconciliation(); + } + + async deleteTax(lineId, taxChanged) { + const lineData = this.props.linesToReconcile.filter((line) => { + return line.id === parseInt(lineId); + })[0]; + await this.orm.call("account.bank.statement.line", "edit_reconcile_line", [ + this.props.statementLine.data.id, + lineData.id, + { tax_ids: [[x2ManyCommands.UNLINK, taxChanged.data.id]] }, + ]); + this.props.statementLine.load(); + this.bankReconciliation.reloadChatter(); + } +} diff --git a/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/reconciled_line_name/reconciled_line_name.xml b/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/reconciled_line_name/reconciled_line_name.xml new file mode 100644 index 00000000..58a814de --- /dev/null +++ b/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/reconciled_line_name/reconciled_line_name.xml @@ -0,0 +1,16 @@ + + + +
+ + +
+ + +
+
+
+ +
+
+
diff --git a/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/statement_line/statement_line.js b/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/statement_line/statement_line.js new file mode 100644 index 00000000..04463ba1 --- /dev/null +++ b/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/statement_line/statement_line.js @@ -0,0 +1,305 @@ +/** @odoo-module **/ + +/** + * Mirrored from + * `account_accountant/static/src/components/bank_reconciliation/statement_line/statement_line.js` + * + * Phase 1 structural parity. Module IDs / template names / CSS classes + * rebranded to `fusion_accounting_bank_rec`. Behaviour delegates to the + * Enterprise-compat surface in our `fusion_bank_reconciliation` service. + */ + +import { BankRecButtonList } from "@fusion_accounting_bank_rec/components/bank_reconciliation/button_list/button_list"; +import { BankRecLineToReconcile } from "@fusion_accounting_bank_rec/components/bank_reconciliation/line_to_reconcile/line_to_reconcile"; +import { BankRecReconciledLineName } from "@fusion_accounting_bank_rec/components/bank_reconciliation/reconciled_line_name/reconciled_line_name"; +import { DropdownItem } from "@web/core/dropdown/dropdown_item"; +import { formatMonetary } from "@web/views/fields/formatters"; +import { KanbanRecord } from "@web/views/kanban/kanban_record"; +import { user } from "@web/core/user"; +import { useService } from "@web/core/utils/hooks"; +import { onWillStart, useState, useRef } from "@odoo/owl"; +import { useBankReconciliation } from "@fusion_accounting_bank_rec/components/bank_reconciliation/bank_reconciliation_service"; + +export class BankRecStatementLine extends KanbanRecord { + static template = "fusion_accounting_bank_rec.BankRecStatementLine"; + static components = { + BankRecLineToReconcile, + BankRecButtonList, + DropdownItem, + BankRecReconciledLineName, + }; + static props = [...KanbanRecord.props]; + + setup() { + super.setup(); + this.orm = useService("orm"); + this.ui = useService("ui"); + this.bankReconciliation = useBankReconciliation(); + this.state = useState({ + isUnfolded: false, + }); + this.statementLineRootRef = useRef("root"); + if (this.env.model.config.context?.default_st_line_id === this.props.record.resId) { + this.state.isUnfolded = true; + this.bankReconciliation.selectStatementLine(this.props.record); + } + onWillStart(async () => { + this.userCanReview = await user.hasGroup("account.group_account_user"); + }); + } + + getRecordClasses() { + let classes = super.getRecordClasses(); + if (this.hasStatementLine === 1) { + classes += " mt-3"; + } + return classes; + } + + // ----------------------------------------------------------------------------- + // ACTION + // ----------------------------------------------------------------------------- + + openStatementCreate() { + this.action.doAction("account_accountant.action_bank_statement_form_bank_rec_widget", { + additionalContext: { + split_line_id: this.recordData.id, + default_journal_id: this.recordData.journal_id.id, + }, + onClose: async () => { + this.env.model.load(); + }, + }); + } + + openPartner() { + this.action.doAction({ + type: "ir.actions.act_window", + res_model: "res.partner", + res_id: this.partner.id, + views: [[false, "form"]], + target: "current", + }); + } + + async removePartner() { + await this.orm.write("account.bank.statement.line", [this.recordData.id], { + partner_id: false, + }); + this.record.load(); + } + + // ----------------------------------------------------------------------------- + // HELPER + // ----------------------------------------------------------------------------- + get reconciledLineName() { + const reconciledLine = {}; + for (const line of this.linesToReconcile) { + if ( + line.reconciled_lines_excluding_exchange_diff_ids.records.length === 1 && + line.reconciled_lines_excluding_exchange_diff_ids.records[0].data.move_name + ) { + reconciledLine[line.id] = { + move: line.reconciled_lines_excluding_exchange_diff_ids.records[0].data + .move_name, + }; + } else if (line.tax_ids.count) { + reconciledLine[line.id] = { tax: line.tax_ids.records }; + } else { + reconciledLine[line.id] = { account: line.account_id.display_name }; + } + } + return reconciledLine; + } + + get record() { + return this.props.record; + } + + get recordData() { + return this.props.record.data; + } + + fold() { + if (this.state.isUnfolded) { + this.toggleUnfold(); + } + this.selectStatementLine(); + } + + unfold() { + if (!this.state.isUnfolded) { + this.toggleUnfold(); + } + this.selectStatementLine(); + } + + toggleUnfold() { + this.state.isUnfolded = !this.isUnfolded; + this.selectStatementLine(); + } + + selectStatementLine() { + // Update the chatter with the last selected element + this.bankReconciliation.selectStatementLine(this.record); + } + + openChatter() { + this.selectStatementLine(); + this.bankReconciliation.openChatter(); + } + + get hasInvalidAnalytics() { + return this.linesToReconcile.some((line) => line.has_invalid_analytics); + } + + get isUnfolded() { + return this.state.isUnfolded; + } + + get hasStatementLine() { + return this.env.model.root.count; + } + + get formattedAmount() { + return formatMonetary(this.recordData.amount, { + currencyId: this.recordData.currency_id.id, + }); + } + + get formattedDate() { + return this.recordData.date.toLocaleString({ + month: "short", + day: "2-digit", + }); + } + + get formattedFullDate() { + return this.recordData.date.toLocaleString({ + month: "long", + day: "numeric", + year: "numeric", + }); + } + + get partner() { + return this.recordData.partner_id; + } + + get linesToReconcile() { + return this.accountMoveLines.filter((line) => { + return ( + line.account_id.id !== this.recordData.journal_id?.suspense_account_id.id && + line.account_id.id !== this.recordData.journal_id?.default_account_id.id + ); + }); + } + + get suspenseAccountLine() { + return this.accountMoveLines.filter((line) => { + return line.account_id.id === this.recordData.journal_id.suspense_account_id.id; + })?.[0]; + } + + get accountMoveLines() { + return [...this.recordData.line_ids.records.map((line) => line.data)]; + } + + get hasForeignCurrencyAndSameCurrencyForAllLines() { + return ( + this.recordData.foreign_currency_id && + this.linesToReconcile && + this.linesToReconcile.filter((line) => { + return line.currency_id.id !== this.recordData.foreign_currency_id.id; + }).length === 0 + ); + } + + get suspenseAccountLineFormattedAmount() { + return formatMonetary(this.suspenseAccountLine.amount_currency, { + currencyId: this.suspenseAccountLine?.currency_id.id, + }); + } + + get activityNumber() { + return this.recordData.activity_ids.count; + } + + /** + * Checks if there is at least one attachment associated with the bank + * statement line or its related records. Aggregates attachment counts from + * the move, the related move lines, and the lines reconciled with them. + * + * @returns {number} Total attachments. > 0 indicates presence. + */ + get hasAttachment() { + const statementAttachment = this.recordData.bank_statement_attachment_ids.records.map( + (attachment) => attachment.data.id + ); + + return ( + this.recordData.attachment_ids.records.length + + this.linesToReconcile + .flatMap((line) => line.reconciled_lines_ids.records) + .filter((line) => line.data.move_attachment_ids?.count) + .reduce( + (accumulator, line) => + parseInt(accumulator) + parseInt(line.data.move_attachment_ids.count), + 0 + ) + + this.linesToReconcile + .filter( + (line) => + line.move_attachment_ids?.count && + !line.move_attachment_ids.records + .map((attachment) => attachment.data.id) + .every((id) => statementAttachment.includes(id)) + ) + .reduce( + (accumulator, line) => + parseInt(accumulator) + parseInt(line.move_attachment_ids.count), + 0 + ) + ); + } + + get amountClasses() { + const classes = this.recordData.foreign_currency_id ? "w-50" : "w-100"; + if (this.recordData.amount > 0) { + return `${classes} fw-bold`; + } + if (this.recordData.amount < 0) { + return `${classes} text-danger fw-bold`; + } + return `${classes} text-secondary`; + } + + get buttonListProps() { + return { + statementLineRootRef: this.statementLineRootRef, + statementLine: this.record, + reconcileLineCount: + this.bankReconciliation.reconcileCountPerPartnerId[this.recordData.partner_id.id] ?? + null, + reconcileModels: + this.bankReconciliation.reconcileModelPerStatementLineId[this.recordData.id] ?? [], + preSelectedReconciliationModel: this.accountMoveLines + .filter((line) => line.reconcile_model_id.id) + .map((line) => line.reconcile_model_id)?.[0], + }; + } + + get formattedAmountCurrencyInForeign() { + return formatMonetary(this.recordData.amount_currency, { + currencyId: this.recordData.foreign_currency_id.id, + }); + } + + get isSelected() { + return this.recordData.move_id.id === this.bankReconciliation.statementLineMoveId; + } + + get isChatterOpen() { + return this.bankReconciliation.chatterState.visible; + } +} diff --git a/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/statement_line/statement_line.xml b/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/statement_line/statement_line.xml new file mode 100644 index 00000000..82ea7f0c --- /dev/null +++ b/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/statement_line/statement_line.xml @@ -0,0 +1,115 @@ + + + + +
+
+ +
+
+
+
+ +
+ +
+
+ + + + + +
+ +
+ + + + + + Reconciled + + + + + + , + + +
+
+ + + + +
+
+ +
+
+
+ + +
+ +
+ + + + +
+
+ +
+
+
+
+
+ + +
+
+
+
+
+
+
+
diff --git a/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/statement_summary/statement_summary.js b/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/statement_summary/statement_summary.js new file mode 100644 index 00000000..8a4ebb71 --- /dev/null +++ b/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/statement_summary/statement_summary.js @@ -0,0 +1,42 @@ +/** @odoo-module **/ + +/** + * Mirrored from + * `account_accountant/.../statement_summary/statement_summary.js`. + * Phase 1 structural parity. + */ + +import { Component } from "@odoo/owl"; + +export class BankRecStatementSummary extends Component { + static template = "fusion_accounting_bank_rec.BankRecStatementSummary"; + + static props = { + label: { type: String }, + amount: { type: String, optional: true }, + action: { type: Function }, + journalId: { type: Number, optional: true }, + isValid: { type: Boolean, optional: true }, + journalIsInvalid: { type: Boolean, optional: true }, + }; + static defaultProps = { + isValid: true, + }; + + actionApplyInvalidStatement() { + const facets = this.env.searchModel.facets; + const searchItems = this.env.searchModel.searchItems; + const invalidStatementFilter = Object.values(searchItems).find( + (i) => i.name == "invalid_statement" + ); + const invalidStatementFacet = facets.filter( + (i) => i.groupId == invalidStatementFilter.groupId + ); + if ( + invalidStatementFacet.length == 0 || + !invalidStatementFacet[0].values.includes(invalidStatementFilter.description) + ) { + this.env.searchModel.toggleSearchItem(invalidStatementFilter.id); + } + } +} diff --git a/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/statement_summary/statement_summary.xml b/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/statement_summary/statement_summary.xml new file mode 100644 index 00000000..7f6a9acc --- /dev/null +++ b/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/statement_summary/statement_summary.xml @@ -0,0 +1,24 @@ + + + +
+
+

+

+
+

+ Invalid Statement(s) +

+
+ + + diff --git a/fusion_accounting_bank_rec/static/src/services/bank_reconciliation_service.js b/fusion_accounting_bank_rec/static/src/services/bank_reconciliation_service.js index b8607fcd..60236e11 100644 --- a/fusion_accounting_bank_rec/static/src/services/bank_reconciliation_service.js +++ b/fusion_accounting_bank_rec/static/src/services/bank_reconciliation_service.js @@ -11,7 +11,9 @@ */ import { registry } from "@web/core/registry"; -import { reactive } from "@odoo/owl"; +import { reactive, useState, EventBus } from "@odoo/owl"; +import { useService } from "@web/core/utils/hooks"; +import { browser } from "@web/core/browser/browser"; const ENDPOINT_BASE = "/fusion/bank_rec"; @@ -20,6 +22,22 @@ export class BankReconciliationService { this.env = env; this.rpc = services.rpc; this.notification = services.notification; + this.orm = services.orm; + + // ============================================================ + // Enterprise-compat surface (mirrored OWL components rely on this) + // ============================================================ + // Mirrored components from account_accountant expect these + // attributes/methods on the service. Most are implemented as + // stubs that no-op or return sensible defaults; structural + // parity now, behaviour wired up in fusion-only Tasks 34-36. + this.bus = new EventBus(); + this.chatterState = reactive({ + visible: this._readChatterPref(), + statementLine: null, + }); + this.reconcileCountPerPartnerId = reactive({}); + this.reconcileModelPerStatementLineId = reactive({}); // Reactive state — components depend on it via useState/reactive this.state = reactive({ @@ -265,13 +283,138 @@ export class BankReconciliationService { getBandClass(line) { return `band-${line.fusion_confidence_band || "none"}`; } + + // ============================================================ + // Enterprise-compat methods (stubs — wired up later) + // ============================================================ + // The following surface is required by mirrored components from + // account_accountant. They are primarily no-ops or thin wrappers + // around the legacy/V19 ORM. Phase 1 prioritizes structural parity; + // fusion-only Tasks 34-36 will replace these with native + // implementations driven by our JSON-RPC endpoints. + + _readChatterPref() { + try { + return ( + JSON.parse( + browser.sessionStorage.getItem("isFusionBankRecChatterOpened") + ) ?? false + ); + } catch { + return false; + } + } + + toggleChatter() { + this.chatterState.visible = !this.chatterState.visible; + try { + browser.sessionStorage.setItem( + "isFusionBankRecChatterOpened", + this.chatterState.visible + ); + } catch { + // Session storage unavailable — non-fatal. + } + } + + openChatter() { + this.chatterState.visible = true; + } + + selectStatementLine(statementLine) { + this.chatterState.statementLine = statementLine; + } + + reloadChatter() { + this.bus.trigger("MAIL:RELOAD-THREAD", { + model: "account.move", + id: this.statementLineMoveId, + }); + } + + async computeReconcileLineCountPerPartnerId(records) { + // Stub: real impl to be added in fusion-only task. + // Components call this after partner edits to refresh the per-partner + // count badge. Returning empty here keeps the badge silent. + if (!this.orm) { + return; + } + try { + const partnerIds = (records || []) + .map((r) => r?.data?.partner_id?.id) + .filter(Boolean); + if (!partnerIds.length) { + this.reconcileCountPerPartnerId = {}; + return; + } + // Best-effort: keep a zero map so templates don't blow up. + const out = {}; + for (const pid of partnerIds) { + out[pid] = this.reconcileCountPerPartnerId[pid] ?? 0; + } + this.reconcileCountPerPartnerId = out; + } catch { + // Non-fatal; templates fall back to defaults. + } + } + + async computeAvailableReconcileModels(records) { + // Stub: components show these as quick-action buttons. Empty for now. + const out = {}; + for (const r of records || []) { + const id = r?.data?.id; + if (id) { + out[id] = []; + } + } + this.reconcileModelPerStatementLineId = out; + } + + async updateAvailableReconcileModels(recordId) { + if (recordId) { + this.reconcileModelPerStatementLineId[recordId] = []; + } + } + + async reloadRecords(records) { + await Promise.all( + (records || []).map((record) => record?.load ? record.load() : null) + ); + } + + get statementLineMove() { + return this.chatterState.statementLine?.data?.move_id; + } + + get statementLineMoveId() { + return this.statementLineMove?.id; + } + + get statementLine() { + return this.chatterState.statementLine; + } + + get statementLineId() { + return this.statementLine?.data?.id; + } } export const bankReconciliationService = { - dependencies: ["rpc", "notification"], + dependencies: ["rpc", "notification", "orm"], start(env, services) { return new BankReconciliationService(env, services); }, }; registry.category("services").add("fusion_bank_reconciliation", bankReconciliationService); + +/** + * Hook for OWL components mirrored from Enterprise. + * + * Enterprise's components import `useBankReconciliation` from + * `../bank_reconciliation_service`; we expose the same hook here so + * mirrored code works unmodified after the relative-import rewrite. + */ +export function useBankReconciliation() { + return useState(useService("fusion_bank_reconciliation")); +}