From 9e4de892693f35a04286a60c92bf510ef81d1525 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sun, 19 Apr 2026 12:53:02 -0400 Subject: [PATCH] feat(fusion_accounting_bank_rec): mirror Enterprise OWL batch 2 (action + edit components) Mirrors 5 OWL components from account_accountant for Phase 1 structural parity: - button/ (single action button) - button_list/ (toolbar of buttons + dropdown + hotkeys) - line_to_reconcile/ (editable matched-line editor) - list_view/ (list view + many2one multi-edit field) - apply_amount/ (amount application html field) Renames applied per spec (template names, module IDs, CSS classes). Notes / deferred to fusion-only Tasks 34-36: - list_view extends @web ListController instead of Enterprise's AttachmentPreviewListController; setSelectedRecord is a no-op pending the previewer pane mirror. - View/field registry IDs prefixed with `fusion_` to coexist with Enterprise's account_accountant when both modules are installed (`fusion_bank_rec_list`, `fusion_bank_rec_dialog_list`, `fusion_apply_amount_html`, `fusion_bank_rec_list_many2one_multi_id`, `fusion_bankrec_edit_line`). - button_list still references Enterprise view_refs in dialog contexts (`account_accountant.view_account_list_bank_rec_widget` etc.) for parity; the `set_*` ORM methods on account.bank.statement.line are Enterprise-only too. These call sites only fire when the mirrored components are actually rendered, which Phase 1 does not exercise. Manifest version bumped to 19.0.1.0.13. Module upgrade succeeds, 134 logical tests still pass. Made-with: Cursor --- fusion_accounting_bank_rec/__manifest__.py | 14 +- .../apply_amount/apply_amount.js | 82 +++ .../apply_amount/apply_amount.xml | 6 + .../bank_reconciliation/button/button.js | 29 + .../bank_reconciliation/button/button.xml | 15 + .../button_list/button_list.js | 603 ++++++++++++++++++ .../button_list/button_list.xml | 56 ++ .../line_to_reconcile/line_to_reconcile.js | 204 ++++++ .../line_to_reconcile/line_to_reconcile.xml | 49 ++ .../bank_reconciliation/list_view/list.js | 88 +++ .../list_view_many2one_multi_edit.js | 30 + .../list_view_many2one_multi_edit.xml | 6 + 12 files changed, 1181 insertions(+), 1 deletion(-) create mode 100644 fusion_accounting_bank_rec/static/src/components/bank_reconciliation/apply_amount/apply_amount.js create mode 100644 fusion_accounting_bank_rec/static/src/components/bank_reconciliation/apply_amount/apply_amount.xml create mode 100644 fusion_accounting_bank_rec/static/src/components/bank_reconciliation/button/button.js create mode 100644 fusion_accounting_bank_rec/static/src/components/bank_reconciliation/button/button.xml create mode 100644 fusion_accounting_bank_rec/static/src/components/bank_reconciliation/button_list/button_list.js create mode 100644 fusion_accounting_bank_rec/static/src/components/bank_reconciliation/button_list/button_list.xml create mode 100644 fusion_accounting_bank_rec/static/src/components/bank_reconciliation/line_to_reconcile/line_to_reconcile.js create mode 100644 fusion_accounting_bank_rec/static/src/components/bank_reconciliation/line_to_reconcile/line_to_reconcile.xml create mode 100644 fusion_accounting_bank_rec/static/src/components/bank_reconciliation/list_view/list.js create mode 100644 fusion_accounting_bank_rec/static/src/components/bank_reconciliation/list_view/list_view_many2one_multi_edit.js create mode 100644 fusion_accounting_bank_rec/static/src/components/bank_reconciliation/list_view/list_view_many2one_multi_edit.xml diff --git a/fusion_accounting_bank_rec/__manifest__.py b/fusion_accounting_bank_rec/__manifest__.py index d81cede4..6f4a67e3 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.12', + 'version': '19.0.1.0.13', 'category': 'Accounting/Accounting', 'sequence': 28, 'summary': 'Native V19 bank reconciliation widget with AI confidence scoring + behavioural learning.', @@ -56,6 +56,18 @@ Built by Nexa Systems Inc. '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', + # Batch 2 (Task 31) — action + edit components + 'fusion_accounting_bank_rec/static/src/components/bank_reconciliation/button/button.js', + 'fusion_accounting_bank_rec/static/src/components/bank_reconciliation/button/button.xml', + 'fusion_accounting_bank_rec/static/src/components/bank_reconciliation/button_list/button_list.js', + 'fusion_accounting_bank_rec/static/src/components/bank_reconciliation/button_list/button_list.xml', + 'fusion_accounting_bank_rec/static/src/components/bank_reconciliation/line_to_reconcile/line_to_reconcile.js', + 'fusion_accounting_bank_rec/static/src/components/bank_reconciliation/line_to_reconcile/line_to_reconcile.xml', + 'fusion_accounting_bank_rec/static/src/components/bank_reconciliation/list_view/list.js', + 'fusion_accounting_bank_rec/static/src/components/bank_reconciliation/list_view/list_view_many2one_multi_edit.js', + 'fusion_accounting_bank_rec/static/src/components/bank_reconciliation/list_view/list_view_many2one_multi_edit.xml', + 'fusion_accounting_bank_rec/static/src/components/bank_reconciliation/apply_amount/apply_amount.js', + 'fusion_accounting_bank_rec/static/src/components/bank_reconciliation/apply_amount/apply_amount.xml', ], }, 'installable': True, diff --git a/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/apply_amount/apply_amount.js b/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/apply_amount/apply_amount.js new file mode 100644 index 00000000..4eba3f9b --- /dev/null +++ b/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/apply_amount/apply_amount.js @@ -0,0 +1,82 @@ +/** @odoo-module **/ + +/** + * Mirrored from `account_accountant/.../apply_amount/apply_amount.js`. + * Phase 1 structural parity. + */ + +import { Component } from "@odoo/owl"; +import { standardFieldProps } from "@web/views/fields/standard_field_props"; +import { registry } from "@web/core/registry"; +import { useService } from "@web/core/utils/hooks"; + +class BankRecWidgetApplyAmountHtmlField extends Component { + static props = standardFieldProps; + static template = "fusion_accounting_bank_rec.BankRecWidgetApplyAmountHtmlField"; + + setup() { + this.action = useService("action"); + this.orm = useService("orm"); + } + + get value() { + return this.props.record.data[this.props.name]; + } + + async switchApplyAmount(ev) { + const root = this.env.model.root; + const fetchReconciledLines = async (fields = []) => { + return await this.orm.searchRead( + "account.move.line", + [ + [ + "id", + "in", + ...root.data.reconciled_lines_excluding_exchange_diff_ids._currentIds, + ], + ], + fields + ); + }; + + const fetchStatementLines = async (fields = []) => { + return await this.orm.searchRead( + "account.move.line", + [["move_id", "=", root.data.move_id.id]], + fields + ); + }; + + if (ev.target.attributes.name?.value === "action_redirect_to_move") { + const [line] = await fetchReconciledLines(["amount_currency", "balance", "move_id"]); + await this.openMove(line.move_id[0]); + } else if (ev.target.attributes.name?.value === "apply_full_amount") { + const [line] = await fetchReconciledLines(["amount_currency", "balance"]); + await root.update({ + balance: -line.balance, + amount_currency: -line.amount_currency, + }); + } else if (ev.target.attributes.name?.value === "apply_partial_amount") { + const lines = await fetchStatementLines(["amount_currency", "balance"]); + // We have all the lines of the entry, we want the suspense line. + await root.update({ + balance: lines.at(-1).balance, + amount_currency: lines.at(-1).amount_currency, + }); + } + } + + openMove(moveId) { + this.action.doAction({ + type: "ir.actions.act_window", + res_model: "account.move", + res_id: moveId, + views: [[false, "form"]], + target: "current", + }); + } +} + +const fusionBankRecWidgetApplyAmountHtmlField = { component: BankRecWidgetApplyAmountHtmlField }; + +registry.category("fields").add("fusion_apply_amount_html", fusionBankRecWidgetApplyAmountHtmlField); diff --git a/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/apply_amount/apply_amount.xml b/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/apply_amount/apply_amount.xml new file mode 100644 index 00000000..784cb753 --- /dev/null +++ b/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/apply_amount/apply_amount.xml @@ -0,0 +1,6 @@ + + + +
+ + diff --git a/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/button/button.js b/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/button/button.js new file mode 100644 index 00000000..11f4a802 --- /dev/null +++ b/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/button/button.js @@ -0,0 +1,29 @@ +/** @odoo-module **/ + +/** + * Mirrored from `account_accountant/.../button/button.js`. + * Phase 1 structural parity. + */ + +import { Component } from "@odoo/owl"; +import { useService } from "@web/core/utils/hooks"; + +export class BankRecButton extends Component { + static template = "fusion_accounting_bank_rec.BankRecButton"; + static props = { + label: { type: String, optional: true }, + action: { type: Function, optional: true }, + count: { type: [Number, { value: null }], optional: true }, + primary: { type: Boolean, optional: true }, + toReview: { type: Boolean, optional: true }, + classes: { type: String, optional: true }, + }; + static defaultProps = { + primary: false, + classes: "", + }; + + setup() { + this.ui = useService("ui"); + } +} diff --git a/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/button/button.xml b/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/button/button.xml new file mode 100644 index 00000000..99a60743 --- /dev/null +++ b/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/button/button.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/button_list/button_list.js b/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/button_list/button_list.js new file mode 100644 index 00000000..8c0f662b --- /dev/null +++ b/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/button_list/button_list.js @@ -0,0 +1,603 @@ +/** @odoo-module **/ + +/** + * Mirrored from `account_accountant/.../button_list/button_list.js`. + * Phase 1 structural parity. Behaviour delegates to the + * Enterprise-compat surface in our `fusion_bank_reconciliation` service. + */ + +import { BankRecButton } from "@fusion_accounting_bank_rec/components/bank_reconciliation/button/button"; +import { BankRecFileUploader } from "@fusion_accounting_bank_rec/components/bank_reconciliation/file_uploader/file_uploader"; +import { Component } from "@odoo/owl"; +import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog"; +import { Dropdown } from "@web/core/dropdown/dropdown"; +import { DropdownItem } from "@web/core/dropdown/dropdown_item"; +import { SelectCreateDialog } from "@web/views/view_dialogs/select_create_dialog"; +import { BankRecSelectCreateDialog } from "@fusion_accounting_bank_rec/components/bank_reconciliation/search_dialog/search_dialog"; +import { _t } from "@web/core/l10n/translation"; +import { getCurrency } from "@web/core/currency"; +import { useOwnedDialogs, useService } from "@web/core/utils/hooks"; +import { useBankReconciliation } from "@fusion_accounting_bank_rec/components/bank_reconciliation/bank_reconciliation_service"; +import { useHotkey } from "@web/core/hotkeys/hotkey_hook"; + +export class BankRecButtonList extends Component { + static template = "fusion_accounting_bank_rec.BankRecButtonList"; + static components = { + Dropdown, + DropdownItem, + BankRecButton, + BankRecFileUploader, + }; + static props = { + statementLineRootRef: { type: Object }, + statementLine: { type: Object }, + suspenseAccountLine: { type: Object, optional: true }, + reconcileLineCount: { type: [Number, { value: null }], optional: true }, + reconcileModels: Array, + preSelectedReconciliationModel: { type: Object, optional: true }, + }; + static defaultProps = { + reconcileLineCount: 0, + }; + + setup() { + this.action = useService("action"); + this.ui = useService("ui"); + this.orm = useService("orm"); + + this.addDialog = useOwnedDialogs(); + this.currencyDigits = getCurrency(this.statementLineData.currency_id.id)?.digits || 2; + this.bankReconciliation = useBankReconciliation(); + + this.registerHotkeys(); + } + + restoreFocus() { + if (this.isLineSelected) { + this.props.statementLineRootRef.el.focus(); + } + } + + /** + * Displays a search dialog (no create option) for selecting a `res.partner` record. + */ + setPartnerOnReconcileLine() { + this.addDialog( + SelectCreateDialog, + { + title: _t("Search: Partner"), + noCreate: false, + multiSelect: false, + resModel: "res.partner", + context: { default_name: this.statementLineData.partner_name }, + onSelected: async (partner) => { + await this.orm.call( + "account.bank.statement.line", + "set_partner_bank_statement_line", + [this.statementLineData.id, partner[0]] + ); + const recordsToLoad = []; + if (this.statementLineData.partner_name) { + // Reload all impacted statement lines if we have a partner_name + recordsToLoad.push( + ...this.env.model.root.records.filter( + (record) => + record.data.partner_name === this.statementLineData.partner_name + ) + ); + } else { + recordsToLoad.push(this.props.statementLine); + } + await this.bankReconciliation.reloadRecords(recordsToLoad); + await this.bankReconciliation.computeReconcileLineCountPerPartnerId( + this.env.model.root.records + ); + this.bankReconciliation.reloadChatter(); + this.restoreFocus(); + }, + }, + { + onClose: () => { + this.restoreFocus(); + }, + } + ); + } + + /** + * Opens a dialog to select an account and assigns it to the current reconcile line. + */ + setAccountOnReconcileLine() { + const context = { + list_view_ref: "account_accountant.view_account_list_bank_rec_widget", + search_view_ref: "account_accountant.view_account_search_bank_rec_widget", + ...(this.statementLineData.amount > 0 + ? { preferred_account_type: "income" } + : { preferred_account_type: "expense" }), + }; + + this.addDialog( + SelectCreateDialog, + { + title: _t("Search: Account"), + noCreate: true, + multiSelect: false, + domain: [ + [ + "id", + "not in", + [ + this.statementLineData.journal_id.suspense_account_id.id, + this.statementLineData.journal_id.default_account_id.id, + ], + ], + ], + context: context, + resModel: "account.account", + onSelected: async (account) => { + const linesToLoad = await this._setAccountOnReconcileLine( + this.lastAccountMoveLine.data.id, + account[0], + { context: { account_default_taxes: true } } + ); + const recordsToLoad = [ + ...this.env.model.root.records.filter((record) => + linesToLoad.includes(record.data.id) + ), + this.props.statementLine, + ]; + await this.bankReconciliation.reloadRecords(recordsToLoad); + this.bankReconciliation.reloadChatter(); + this.restoreFocus(); + }, + }, + { + onClose: () => { + this.restoreFocus(); + }, + } + ); + } + + async _setAccountOnReconcileLine(amlId, accountId, context = {}) { + return await this.orm.call( + "account.bank.statement.line", + "set_account_bank_statement_line", + [this.statementLineData.id, amlId, accountId], + context + ); + } + + async setAccountReceivableOnReconcileLine() { + let accountId; + if (this.statementLineData.partner_id.property_account_receivable_id.id) { + accountId = this.statementLineData.partner_id.property_account_receivable_id.id; + } else { + accountId = await this.orm.webSearchRead("account.account", [ + ["account_type", "=", "asset_receivable"], + ]); + } + await this._setAccountOnReconcileLine(this.lastAccountMoveLine.data.id, accountId); + this.props.statementLine.load(); + this.bankReconciliation.reloadChatter(); + } + + async setAccountPayableOnReconcileLine() { + let accountId; + if (this.statementLineData.partner_id.property_account_payable_id.id) { + accountId = this.statementLineData.partner_id.property_account_payable_id.id; + } else { + accountId = await this.orm.webSearchRead("account.account", [ + ["account_type", "=", "liability_payable"], + ]); + } + await this._setAccountOnReconcileLine(this.lastAccountMoveLine.data.id, accountId); + this.props.statementLine.load(); + this.bankReconciliation.reloadChatter(); + } + + /** + * Opens a dialog to search and select journal items to reconcile with the current bank statement line. + */ + reconcileOnReconcileLine() { + const context = { + list_view_ref: "account_accountant.view_account_move_line_list_bank_rec_widget", + search_view_ref: "account_accountant.view_account_move_line_search_bank_rec_widget", + preferred_aml_value: -this.props.suspenseAccountLine.amount_currency, + preferred_aml_currency_id: this.props.suspenseAccountLine.currency_id.id, + ...(this.statementLineData.partner_id + ? { search_default_partner_id: this.statementLineData.partner_id.id } + : { search_default_posted: 1 }), + }; + + this.addDialog( + BankRecSelectCreateDialog, + { + title: _t("Search: Journal Items to Match"), + noCreate: true, + domain: this.getReconcileButtonDomain(), + resModel: "account.move.line", + size: "xl", + context: context, + onSelected: async (moveLines) => { + await this.orm.call( + "account.bank.statement.line", + "set_line_bank_statement_line", + [this.statementLineData.id, moveLines] + ); + await this.bankReconciliation.computeReconcileLineCountPerPartnerId( + this.env.model.root.records + ); + this.props.statementLine.load(); + this.bankReconciliation.reloadChatter(); + this.restoreFocus(); + }, + suspenseAccountLine: this.props.suspenseAccountLine, + reference: this.statementLineData.payment_ref, + date: this.statementLineData.date, + }, + { + onClose: () => { + this.restoreFocus(); + }, + } + ); + } + + getReconcileButtonDomain() { + return [ + ["parent_state", "in", ["draft", "posted"]], + ["company_id", "child_of", this.statementLineData.company_id.id], + ["search_account_id.reconcile", "=", true], + ["display_type", "not in", ["line_section", "line_note"]], + ["reconciled", "=", false], + "|", + ["search_account_id.account_type", "not in", ["asset_receivable", "liability_payable"]], + ["payment_id", "=", false], + ["statement_line_id", "!=", this.statementLineData.id], + ]; + } + + /** + * Deletes the current bank statement line. + */ + async deleteTransaction() { + this.addDialog(ConfirmationDialog, { + body: _t("Are you sure you want to delete this statement line?"), + confirm: async () => { + await this.orm.unlink("account.bank.statement.line", [this.statementLineData.id]); + this.env.model.load(); + }, + cancel: () => {}, + }); + } + + /** + * Set the move of the statement line as to check + */ + async setStatementLineAsReviewed() { + await this.orm.call("account.move", "set_moves_checked", [ + this.statementLineData.move_id.id, + ]); + this.props.statementLine.load(); + this.bankReconciliation.reloadChatter(); + } + + // ----------------------------------------------------------------------------- + // Reconciliation Model + // ----------------------------------------------------------------------------- + async triggerReconciliationModel(reconciliationModelId) { + await this.orm.call("account.reconcile.model", "trigger_reconciliation_model", [ + reconciliationModelId, + this.statementLineData.id, + ]); + await this.bankReconciliation.computeReconcileLineCountPerPartnerId( + this.env.model.root.records + ); + this.props.statementLine.load(); + this.bankReconciliation.reloadChatter(); + } + + getKeyAction(key) { + const keyActions = { + 1: { + condition: + this.props.statementLineRootRef.el.querySelector(".set-partner-btn") && + this.isLineSelected, + action: async () => this.setPartnerOnReconcileLine(), + buttonElement: this.props.statementLineRootRef.el.querySelector(".set-partner-btn"), + }, + 2: { + condition: + this.props.statementLineRootRef.el.querySelector(".reconcile-btn") && + this.isLineSelected, + action: async () => this.reconcileOnReconcileLine(), + buttonElement: this.props.statementLineRootRef.el.querySelector(".reconcile-btn"), + }, + 3: { + condition: + this.props.statementLineRootRef.el.querySelector(".set-account-btn") && + this.isLineSelected, + action: () => this.setAccountOnReconcileLine(), + buttonElement: this.props.statementLineRootRef.el.querySelector(".set-account-btn"), + }, + 4: { + condition: + this.props.statementLineRootRef.el.querySelector(".set-payable-btn") && + this.isLineSelected, + action: () => this.setAccountPayableOnReconcileLine(), + buttonElement: this.props.statementLineRootRef.el.querySelector(".set-payable-btn"), + }, + 5: { + condition: + this.props.statementLineRootRef.el.querySelector(".set-receivable-btn") && + this.isLineSelected, + action: () => this.setAccountReceivableOnReconcileLine(), + buttonElement: + this.props.statementLineRootRef.el.querySelector(".set-receivable-btn"), + }, + 6: { + condition: + this.props.statementLineRootRef.el.querySelector( + ".reconciliation-model-btn-0" + ) && this.isLineSelected, + action: () => { + const buttonElement = this.props.statementLineRootRef.el.querySelector( + ".reconciliation-model-btn-0" + ); + if (buttonElement) { + buttonElement.click(); + } + }, + buttonElement: this.props.statementLineRootRef.el.querySelector( + ".reconciliation-model-btn-0" + ), + }, + 7: { + condition: + this.props.statementLineRootRef.el.querySelector( + ".reconciliation-model-btn-1" + ) && this.isLineSelected, + action: () => { + const buttonElement = this.props.statementLineRootRef.el.querySelector( + ".reconciliation-model-btn-1" + ); + if (buttonElement) { + buttonElement.click(); + } + }, + buttonElement: this.props.statementLineRootRef.el.querySelector( + ".reconciliation-model-btn-1" + ), + }, + 8: { + condition: + this.props.statementLineRootRef.el.querySelector( + ".reconciliation-model-btn-2" + ) && this.isLineSelected, + action: () => { + const buttonElement = this.props.statementLineRootRef.el.querySelector( + ".reconciliation-model-btn-2" + ); + if (buttonElement) { + buttonElement.click(); + } + }, + buttonElement: this.props.statementLineRootRef.el.querySelector( + ".reconciliation-model-btn-2" + ), + }, + Enter: { + condition: + this.props.statementLineRootRef.el.querySelector(".btn-primary") && + this.isLineSelected, + action: () => { + const primaryButtons = + this.props.statementLineRootRef.el.querySelectorAll(".btn-primary"); + if (primaryButtons.length > 0) { + primaryButtons[0].click(); + } + }, + buttonElement: this.props.statementLineRootRef.el.querySelector(".btn-primary"), + }, + }; + return keyActions[key]; + } + + registerHotkeys() { + const hotkeyConfigs = [ + { key: "1", trigger: "alt+shift+1" }, + { key: "2", trigger: "alt+shift+2" }, + { key: "3", trigger: "alt+shift+3" }, + { key: "4", trigger: "alt+shift+4" }, + { key: "5", trigger: "alt+shift+5" }, + { key: "6", trigger: "alt+shift+6" }, + { key: "7", trigger: "alt+shift+7" }, + { key: "8", trigger: "alt+shift+8" }, + { key: "Enter", trigger: "alt+shift+enter" }, + ]; + hotkeyConfigs.forEach(({ key, trigger }) => { + useHotkey( + trigger, + ({ target }) => { + const { condition, action } = this.getKeyAction(key); + if (condition) { + action(); + } + }, + { + area: () => this.props.statementLineRootRef.el.parentElement, + withOverlay: () => { + const { buttonElement, condition } = this.getKeyAction(key); + return condition ? buttonElement : null; + }, + isAvailable: () => { + const { condition } = this.getKeyAction(key); + return condition; + }, + } + ); + }); + } + + // ----------------------------------------------------------------------------- + // File Uploader + // ----------------------------------------------------------------------------- + get bankRecFileUploaderRecord() { + return { + statementLineId: this.statementLineData.id, + }; + } + + // ----------------------------------------------------------------------------- + // ACTION + // ----------------------------------------------------------------------------- + actionViewRecoModels() { + return this.action.doAction("account.action_account_reconcile_model"); + } + + // ----------------------------------------------------------------------------- + // GETTER + // ----------------------------------------------------------------------------- + get statementLineData() { + return this.props.statementLine.data; + } + + get isLineSelected() { + return this.statementLineData.id === this.bankReconciliation.statementLine?.data.id; + } + + get lastAccountMoveLine() { + return this.statementLineData.line_ids.records.at(-1); + } + + get isCustomerRankHigher() { + return ( + this.statementLineData.partner_id.customer_rank > + this.statementLineData.partner_id.supplier_rank + ); + } + + get isSetPartnerButtonShown() { + return !this.statementLineData.partner_id; + } + + get isSetAccountButtonShown() { + return !this.statementLineData.account_id; + } + + get isSetReceivableButtonShown() { + return ( + !this.isSetPartnerButtonShown && + ((this.statementLineData.partner_id.customer_rank && this.isCustomerRankHigher) || + this.statementLineData.amount > 0) + ); + } + + get isSetPayableButtonShown() { + return ( + !this.isSetPartnerButtonShown && + ((this.statementLineData.partner_id.supplier_rank && !this.isCustomerRankHigher) || + this.statementLineData.amount < 0) + ); + } + + get isReconcileButtonShown() { + return this.props.reconcileLineCount === null || this.props.reconcileLineCount; + } + + get reconcileModelsInDropdown() { + if (this.ui.isSmall) { + return this.props.reconcileModels; + } + return this.props.reconcileModels.filter( + (model) => model.id !== this.props?.preSelectedReconciliationModel?.id + ); + } + + get buttons() { + const buttonsToDisplay = {}; + if (this.isSetPartnerButtonShown) { + buttonsToDisplay.partner = { + label: _t("Set Partner"), + action: this.setPartnerOnReconcileLine.bind(this), + classes: "set-partner-btn", + }; + } else { + buttonsToDisplay.receivable = { + label: _t("Receivable"), + action: this.setAccountReceivableOnReconcileLine.bind(this), + classes: "set-receivable-btn", + }; + buttonsToDisplay.payable = { + label: _t("Payable"), + action: this.setAccountPayableOnReconcileLine.bind(this), + classes: "set-payable-btn", + }; + } + + if (this.isReconcileButtonShown) { + buttonsToDisplay.reconcile = { + label: _t("Reconcile"), + action: this.reconcileOnReconcileLine.bind(this), + count: this.props.reconcileLineCount, + classes: "reconcile-btn", + }; + } + + if (this.isSetAccountButtonShown) { + buttonsToDisplay.account = { + label: _t("Set Account"), + action: this.setAccountOnReconcileLine.bind(this), + classes: "set-account-btn", + }; + } + + if (this.statementLineData.is_reconciled && !this.statementLineData.checked) { + buttonsToDisplay.toReview = { + label: _t("Reviewed"), + action: this.setStatementLineAsReviewed.bind(this), + toReview: true, + }; + } + + return buttonsToDisplay; + } + + get buttonsToDisplay() { + const buttons = this.buttons || {}; + + let primaryButtonKeys = []; + let secondaryButtonKeys = []; + if (buttons?.partner && buttons?.account) { + primaryButtonKeys = ["partner", "account"]; + } else if (buttons?.reconcile && !!buttons.reconcile?.count) { + primaryButtonKeys = ["reconcile"]; + if (this.isSetReceivableButtonShown) { + secondaryButtonKeys = ["receivable"]; + } else { + secondaryButtonKeys = ["payable"]; + } + } else if (this.isSetReceivableButtonShown) { + primaryButtonKeys = ["receivable"]; + } else if (this.isSetPayableButtonShown) { + primaryButtonKeys = ["payable"]; + } + + return [ + ...primaryButtonKeys.map((key) => ({ ...buttons[key], primary: true })), + ...secondaryButtonKeys.map((key) => ({ ...buttons[key] })), + ]; + } + + get buttonsInDropdown() { + const buttons = this.buttons || {}; + if (this.props.preSelectedReconciliationModel) { + return Object.values(buttons); + } + const buttonToDisplayClasses = this.buttonsToDisplay.map((button) => button.classes) || []; + return Object.values(buttons).filter( + (button) => !buttonToDisplayClasses.includes(button.classes) + ); + } +} diff --git a/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/button_list/button_list.xml b/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/button_list/button_list.xml new file mode 100644 index 00000000..1312ff54 --- /dev/null +++ b/fusion_accounting_bank_rec/static/src/components/bank_reconciliation/button_list/button_list.xml @@ -0,0 +1,56 @@ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + Upload Bills + + + +