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
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
'name': 'Fusion Accounting — Bank Reconciliation',
|
'name': 'Fusion Accounting — Bank Reconciliation',
|
||||||
'version': '19.0.1.0.12',
|
'version': '19.0.1.0.13',
|
||||||
'category': 'Accounting/Accounting',
|
'category': 'Accounting/Accounting',
|
||||||
'sequence': 28,
|
'sequence': 28,
|
||||||
'summary': 'Native V19 bank reconciliation widget with AI confidence scoring + behavioural learning.',
|
'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/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.js',
|
||||||
'fusion_accounting_bank_rec/static/src/components/bank_reconciliation/reconciled_line_name/reconciled_line_name.xml',
|
'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,
|
'installable': True,
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates xml:space="preserve">
|
||||||
|
<t t-name="fusion_accounting_bank_rec.BankRecWidgetApplyAmountHtmlField">
|
||||||
|
<div t-out="value" t-on-click="switchApplyAmount"/>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<templates>
|
||||||
|
<t t-name="fusion_accounting_bank_rec.BankRecButton">
|
||||||
|
<button
|
||||||
|
t-attf-class="d-flex gap-1 btn text-nowrap {{ props.classes }}"
|
||||||
|
t-att-class="{'btn-sm': !ui.isSmall, 'btn-primary': props.primary, 'btn-info': props.toReview, 'btn-secondary': !props.primary}"
|
||||||
|
t-on-click.stop="() => props?.action()"
|
||||||
|
>
|
||||||
|
<span t-esc="props?.label" class="m-auto text-truncate"/>
|
||||||
|
<span class="rounded-pill px-2 o_bg-black-10" t-if="props?.count">
|
||||||
|
<t t-esc="props.count"/>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
@@ -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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<templates>
|
||||||
|
<t t-name="fusion_accounting_bank_rec.BankRecButtonList">
|
||||||
|
<div class="d-flex flex-wrap gap-1">
|
||||||
|
<t t-if="props.preSelectedReconciliationModel and !statementLineData.is_reconciled">
|
||||||
|
<BankRecButton
|
||||||
|
label="props.preSelectedReconciliationModel.display_name"
|
||||||
|
primary="true"
|
||||||
|
action.bind="() => this.triggerReconciliationModel(props.preSelectedReconciliationModel.id)"
|
||||||
|
/>
|
||||||
|
</t>
|
||||||
|
<t t-elif="buttons?.toReview">
|
||||||
|
<BankRecButton t-props="buttons.toReview"/>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<t t-foreach="buttonsToDisplay" t-as="button" t-key="button_index">
|
||||||
|
<BankRecButton t-props="button"/>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<Dropdown t-if="!statementLineData.is_reconciled">
|
||||||
|
<button class="btn btn-secondary" t-att-class="{'btn-sm': !ui.isSmall}">
|
||||||
|
<i class="oi oi-ellipsis-v"/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<t t-set-slot="content">
|
||||||
|
<t t-foreach="buttonsInDropdown" t-as="button" t-key="button_index">
|
||||||
|
<DropdownItem class="'btn btn-link'" onSelected.bind="button.action">
|
||||||
|
<t t-esc="button.label"/>
|
||||||
|
</DropdownItem>
|
||||||
|
</t>
|
||||||
|
<BankRecFileUploader record="bankRecFileUploaderRecord">
|
||||||
|
<t t-set-slot="toggler">
|
||||||
|
<span class="dropdown-item dropdown-item o-navigable btn btn-link">
|
||||||
|
Upload Bills
|
||||||
|
</span>
|
||||||
|
</t>
|
||||||
|
</BankRecFileUploader>
|
||||||
|
<div class="dropdown-divider"/>
|
||||||
|
<t t-foreach="reconcileModelsInDropdown" t-as="model" t-key="model.id">
|
||||||
|
<DropdownItem class="'btn btn-link'" onSelected.bind="() => this.triggerReconciliationModel(model.id)">
|
||||||
|
<t t-esc="model.display_name"/>
|
||||||
|
</DropdownItem>
|
||||||
|
</t>
|
||||||
|
<div t-if="reconcileModelsInDropdown.length" class="dropdown-divider"/>
|
||||||
|
<DropdownItem class="'btn btn-link'" onSelected.bind="actionViewRecoModels">
|
||||||
|
Manage Models
|
||||||
|
</DropdownItem>
|
||||||
|
<DropdownItem class="'btn btn-link'" onSelected.bind="deleteTransaction">
|
||||||
|
Delete Transaction
|
||||||
|
</DropdownItem>
|
||||||
|
</t>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mirrored from
|
||||||
|
* `account_accountant/.../line_to_reconcile/line_to_reconcile.js`.
|
||||||
|
* Phase 1 structural parity.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Component, useRef } from "@odoo/owl";
|
||||||
|
import { _t } from "@web/core/l10n/translation";
|
||||||
|
import { formatMonetary } from "@web/views/fields/formatters";
|
||||||
|
import { useService } from "@web/core/utils/hooks";
|
||||||
|
import { useBankReconciliation } from "@fusion_accounting_bank_rec/components/bank_reconciliation/bank_reconciliation_service";
|
||||||
|
import { usePopover } from "@web/core/popover/popover_hook";
|
||||||
|
import { BankRecFormDialog } from "@fusion_accounting_bank_rec/components/bank_reconciliation/bankrec_form_dialog/bankrec_form_dialog";
|
||||||
|
import { BankRecLineInfoPopOver } from "@fusion_accounting_bank_rec/components/bank_reconciliation/line_info_pop_over/line_info_pop_over";
|
||||||
|
import { x2ManyCommands } from "@web/core/orm_service";
|
||||||
|
|
||||||
|
export class BankRecLineToReconcile extends Component {
|
||||||
|
static template = "fusion_accounting_bank_rec.BankRecLineToReconcile";
|
||||||
|
|
||||||
|
static props = {
|
||||||
|
line: Object,
|
||||||
|
statementLine: Object,
|
||||||
|
};
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.action = useService("action");
|
||||||
|
this.orm = useService("orm");
|
||||||
|
this.dialogService = useService("dialog");
|
||||||
|
this.ui = useService("ui");
|
||||||
|
this.bankReconciliation = useBankReconciliation();
|
||||||
|
|
||||||
|
this.lineInfoRef = useRef("line-info-ref");
|
||||||
|
this.lineInfoPopOver = usePopover(BankRecLineInfoPopOver, {
|
||||||
|
position: "left",
|
||||||
|
closeOnClickAway: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickLine() {
|
||||||
|
if (this.ui.isSmall) {
|
||||||
|
this.toggleEditLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleEditLine() {
|
||||||
|
this.dialogService.add(BankRecFormDialog, {
|
||||||
|
title: _t("Edit Line"),
|
||||||
|
resModel: "account.move.line",
|
||||||
|
resId: this.lineData.id,
|
||||||
|
context: {
|
||||||
|
form_view_ref: "account_accountant.view_bank_rec_edit_line",
|
||||||
|
is_reviewed: this.lineData.move_id.checked,
|
||||||
|
},
|
||||||
|
onRecordSave: async (record) => {
|
||||||
|
await this.orm.call("account.bank.statement.line", "edit_reconcile_line", [
|
||||||
|
this.statementLineData.id,
|
||||||
|
this.lineData.id,
|
||||||
|
await record.getChanges(),
|
||||||
|
]);
|
||||||
|
this.props.statementLine.load();
|
||||||
|
this.bankReconciliation.reloadChatter();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteLine() {
|
||||||
|
await this.orm.call("account.bank.statement.line", "delete_reconciled_line", [
|
||||||
|
this.statementLineData.id,
|
||||||
|
this.lineData.id,
|
||||||
|
]);
|
||||||
|
if (this.lineData.reconciled_lines_ids.records.length) {
|
||||||
|
// Only update the line count per partner if we delete
|
||||||
|
// a line which is reconciled to another move line
|
||||||
|
this.bankReconciliation.computeReconcileLineCountPerPartnerId(
|
||||||
|
this.env.model.root.records
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.props.statementLine.load();
|
||||||
|
this.bankReconciliation.reloadChatter();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// ACTION
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
openMove() {
|
||||||
|
this.action.doAction({
|
||||||
|
type: "ir.actions.act_window",
|
||||||
|
res_model: "account.move",
|
||||||
|
res_id: this.moveData.id,
|
||||||
|
views: [[false, "form"]],
|
||||||
|
target: "current",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
openPartner() {
|
||||||
|
this.action.doAction({
|
||||||
|
type: "ir.actions.act_window",
|
||||||
|
res_model: "res.partner",
|
||||||
|
res_id: this.lineData.partner_id.id,
|
||||||
|
views: [[false, "form"]],
|
||||||
|
target: "current",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
openLineInfoPopOver() {
|
||||||
|
if (this.lineInfoPopOver.isOpen || !this.showLineInfo) {
|
||||||
|
this.lineInfoPopOver.close();
|
||||||
|
} else {
|
||||||
|
this.lineInfoPopOver.open(this.lineInfoRef.el, {
|
||||||
|
statementLineData: this.statementLineData,
|
||||||
|
lineData: this.lineData,
|
||||||
|
exchangeMove: this.exchangeMove,
|
||||||
|
isPartiallyReconciled: this.isPartiallyReconciled,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteTax(taxIndex) {
|
||||||
|
const taxChanged = this.lineDataTaxIds[taxIndex];
|
||||||
|
await this.orm.call("account.bank.statement.line", "edit_reconcile_line", [
|
||||||
|
this.statementLineData.id,
|
||||||
|
this.lineData.id,
|
||||||
|
{ tax_ids: [[x2ManyCommands.UNLINK, taxChanged.data.id]] },
|
||||||
|
]);
|
||||||
|
this.props.statementLine.load();
|
||||||
|
this.bankReconciliation.reloadChatter();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// GETTER
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
get statementLineData() {
|
||||||
|
return this.props.statementLine.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
get lineData() {
|
||||||
|
return this.props.line;
|
||||||
|
}
|
||||||
|
|
||||||
|
get reconciledLineId() {
|
||||||
|
return this.lineData.reconciled_lines_ids.records.length === 1
|
||||||
|
? this.lineData.reconciled_lines_ids.records[0].data
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get reconciledLineExcludingExchangeDiffId() {
|
||||||
|
return this.lineData.reconciled_lines_excluding_exchange_diff_ids.records.length === 1
|
||||||
|
? this.lineData.reconciled_lines_excluding_exchange_diff_ids.records[0].data
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get moveData() {
|
||||||
|
return (
|
||||||
|
this.reconciledLineId?.move_id ||
|
||||||
|
this.reconciledLineExcludingExchangeDiffId?.move_id ||
|
||||||
|
this.lineData.move_id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get isPartiallyReconciled() {
|
||||||
|
if (!this.reconciledLineId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !this.reconciledLineId.full_reconcile_id?.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasDifferentCurrencies() {
|
||||||
|
return this.lineData.currency_id.id !== this.statementLineData.currency_id.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
get formattedAmountCurrencyOfLine() {
|
||||||
|
return formatMonetary(this.lineData.amount_currency, {
|
||||||
|
currencyId: this.lineData.currency_id.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get formattedAmountCurrencyOfStatementLine() {
|
||||||
|
return formatMonetary(this.lineData.amount_currency, {
|
||||||
|
currencyId: this.statementLineData.currency_id.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get exchangeMove() {
|
||||||
|
return (
|
||||||
|
this.lineData.matched_debit_ids.records[0]?.data.exchange_move_id ||
|
||||||
|
this.lineData.matched_credit_ids.records[0]?.data.exchange_move_id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get showLineInfo() {
|
||||||
|
return this.isPartiallyReconciled || this.exchangeMove?.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isTaxLine() {
|
||||||
|
return this.lineData.tax_line_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
get lineDataTaxIds() {
|
||||||
|
return this.lineData.tax_ids.records;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<templates>
|
||||||
|
<t t-name="fusion_accounting_bank_rec.BankRecLineToReconcile">
|
||||||
|
<div class="o_row" t-on-click.stop="onClickLine">
|
||||||
|
<div class="o_line_name d-flex align-items-center gap-1 text-truncate">
|
||||||
|
<a href="#" class="text-truncate fw-bold" t-esc="lineData.partner_id.display_name" t-on-click.stop="openPartner" role="button" t-att-title="lineData.partner_id.display_name" t-if="lineData.partner_id"/>
|
||||||
|
<span t-esc="lineData.account_id.display_name" class="text-truncate" t-att-class="lineData.partner_id ? 'ms-1' : undefined"/>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<t t-foreach="lineDataTaxIds" t-as="tax_id" t-key="tax_id_index">
|
||||||
|
<div class="o_tag d-inline-flex align-items-center badge rounded-pill o_tag_color_0 flex-shrink-0">
|
||||||
|
<span class="o_tag_badge_text text-truncate" t-esc="tax_id.data.display_name"/>
|
||||||
|
<i t-on-click.stop="() => this.deleteTax(tax_id_index)" class="ps-1 opacity-100-hover opacity-75 oi oi-close"/>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span t-if="!!moveData.display_name and moveData.id !== statementLineData.move_id.id" class="d-none d-md-inline">
|
||||||
|
<a t-on-click.stop="openMove" href="#">
|
||||||
|
<t t-esc="moveData.display_name"/>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
<div class="o_line_amount d-flex align-items-center justify-content-between">
|
||||||
|
<span class="text-muted w-50 text-end" t-if="hasDifferentCurrencies">
|
||||||
|
<span t-att-class="{'btn btn-link p-0' : showLineInfo}" t-ref="line-info-ref" t-on-click.stop="openLineInfoPopOver">
|
||||||
|
<i t-if="showLineInfo" class="fa fa-info-circle me-2"/>
|
||||||
|
<t t-out="formattedAmountCurrencyOfLine"/>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span class="text-end w-100" t-if="!hasDifferentCurrencies">
|
||||||
|
<span t-att-class="{'btn btn-link p-0' : showLineInfo}" t-ref="line-info-ref" t-on-click.stop="openLineInfoPopOver">
|
||||||
|
<i t-if="showLineInfo" class="fa fa-info-circle me-2"/>
|
||||||
|
<t t-out="formattedAmountCurrencyOfStatementLine"/>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="o_line_to_reconcile_button d-none d-md-flex justify-content-end gap-2">
|
||||||
|
<button t-if="lineData.has_invalid_analytics" class="btn btn-link p-0 text-600" t-on-click.stop="toggleEditLine">
|
||||||
|
<i class="fa fa-exclamation-triangle text-warning" data-tooltip="This line has invalid analytic distribution"/>
|
||||||
|
</button>
|
||||||
|
<button t-if="!lineData.has_invalid_analytics" class="btn btn-link p-0 text-600" t-on-click.stop="toggleEditLine">
|
||||||
|
<i class="fa fa-pencil"/>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-link p-0 text-600" t-on-click.stop="deleteLine" t-if="!isTaxLine">
|
||||||
|
<i class="fa fa-trash"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mirrored from `account_accountant/.../list_view/list.js`.
|
||||||
|
* Phase 1 structural parity.
|
||||||
|
*
|
||||||
|
* NOTE: Enterprise extends `AttachmentPreviewListController` from
|
||||||
|
* `account_accountant/static/src/components/attachment_preview_list_view/...`.
|
||||||
|
* That helper isn't part of Phase 1 scope; we extend the base
|
||||||
|
* `ListController` directly and TODO-flag the methods that depend on
|
||||||
|
* the previewer state. Behaviour will be wired up in fusion-only
|
||||||
|
* Tasks 34-36 alongside the right-pane preview integration.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ListController } from "@web/views/list/list_controller";
|
||||||
|
import { ListRenderer } from "@web/views/list/list_renderer";
|
||||||
|
import { registry } from "@web/core/registry";
|
||||||
|
import { listView } from "@web/views/list/list_view";
|
||||||
|
import { useChildSubEnv } from "@odoo/owl";
|
||||||
|
import { makeActiveField } from "@web/model/relational_model/utils";
|
||||||
|
|
||||||
|
export class BankRecListController extends ListController {
|
||||||
|
setup() {
|
||||||
|
super.setup(...arguments);
|
||||||
|
|
||||||
|
this.skipKanbanRestore = {};
|
||||||
|
|
||||||
|
useChildSubEnv({
|
||||||
|
skipKanbanRestoreNeeded: (stLineId) => this.skipKanbanRestore[stLineId],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Don't allow bank_rec_form to be restored with previous values since
|
||||||
|
* the statement line has changed.
|
||||||
|
*/
|
||||||
|
async onRecordSaved(record) {
|
||||||
|
this.skipKanbanRestore[record.resId] = true;
|
||||||
|
return super.onRecordSaved(...arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
get previewerStorageKey() {
|
||||||
|
return "fusion.statement_line_pdf_previewer_hidden";
|
||||||
|
}
|
||||||
|
|
||||||
|
get modelParams() {
|
||||||
|
const params = super.modelParams;
|
||||||
|
params.config.activeFields.bank_statement_attachment_ids = makeActiveField();
|
||||||
|
params.config.activeFields.bank_statement_attachment_ids.related = {
|
||||||
|
fields: {
|
||||||
|
mimetype: { name: "mimetype", type: "char" },
|
||||||
|
},
|
||||||
|
activeFields: {
|
||||||
|
mimetype: makeActiveField(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
params.config.activeFields.attachment_ids = makeActiveField();
|
||||||
|
params.config.activeFields.attachment_ids.related = {
|
||||||
|
fields: {
|
||||||
|
mimetype: { name: "mimetype", type: "char" },
|
||||||
|
},
|
||||||
|
activeFields: {
|
||||||
|
mimetype: makeActiveField(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO(fusion task 34-36): wire up attachment preview pane.
|
||||||
|
* Enterprise sets `this.attachmentPreviewState.selectedRecord` and
|
||||||
|
* calls `this.setThread(...)` on the AttachmentPreviewListController.
|
||||||
|
* Until that helper is mirrored, this is a no-op.
|
||||||
|
*/
|
||||||
|
async setSelectedRecord(/* accountBankStatementLineData */) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BankRecListRenderer extends ListRenderer {}
|
||||||
|
|
||||||
|
export const bankRecListView = {
|
||||||
|
...listView,
|
||||||
|
Controller: BankRecListController,
|
||||||
|
Renderer: BankRecListRenderer,
|
||||||
|
};
|
||||||
|
|
||||||
|
registry.category("views").add("fusion_bank_rec_list", bankRecListView);
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mirrored from
|
||||||
|
* `account_accountant/.../list_view/list_view_many2one_multi_edit.js`.
|
||||||
|
* Phase 1 structural parity.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Component } from "@odoo/owl";
|
||||||
|
import { registry } from "@web/core/registry";
|
||||||
|
import { computeM2OProps, Many2One } from "@web/views/fields/many2one/many2one";
|
||||||
|
import { buildM2OFieldDescription, Many2OneField } from "@web/views/fields/many2one/many2one_field";
|
||||||
|
|
||||||
|
export class BankRecMany2OneMultiID extends Component {
|
||||||
|
static template = "fusion_accounting_bank_rec.BankRecMany2OneMultiID";
|
||||||
|
static components = { Many2One };
|
||||||
|
static props = { ...Many2OneField.props };
|
||||||
|
|
||||||
|
get m2oProps() {
|
||||||
|
const props = computeM2OProps(this.props);
|
||||||
|
if (this.props.record.selected && this.props.record.model.multiEdit) {
|
||||||
|
props.context.active_ids = this.env.model.root.selection.map((r) => r.resId);
|
||||||
|
}
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registry.category("fields").add("fusion_bank_rec_list_many2one_multi_id", {
|
||||||
|
...buildM2OFieldDescription(BankRecMany2OneMultiID),
|
||||||
|
});
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates>
|
||||||
|
<t t-name="fusion_accounting_bank_rec.BankRecMany2OneMultiID">
|
||||||
|
<Many2One t-props="m2oProps"/>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
Reference in New Issue
Block a user