account_reconcile_oca

This commit is contained in:
Ernad Husremovic 2025-10-25 10:34:41 +02:00
parent 64fdc5b0df
commit a8804cdf59
95 changed files with 17541 additions and 0 deletions

View file

@ -0,0 +1,144 @@
/** @odoo-module */
const {onMounted, onWillStart, useState, useSubEnv} = owl;
import {KanbanController} from "@web/views/kanban/kanban_controller";
import {View} from "@web/views/view";
import {formatMonetary} from "@web/views/fields/formatters";
import {useService} from "@web/core/utils/hooks";
export class ReconcileController extends KanbanController {
async setup() {
super.setup();
this.state = useState({
selectedRecordId: null,
journalBalance: 0,
currency: false,
});
useSubEnv({
parentController: this,
exposeController: this.exposeController.bind(this),
});
this.effect = useService("effect");
this.orm = useService("orm");
this.action = useService("action");
this.router = useService("router");
this.activeActions = this.props.archInfo.activeActions;
this.model.addEventListener("update", () => this.selectRecord(), {once: true});
onWillStart(() => {
this.updateJournalInfo();
});
onMounted(() => {
this.selectRecord();
});
}
get journalId() {
if (this.props.context.active_model === "account.journal") {
return this.props.context.active_id;
}
return false;
}
async updateJournalInfo() {
var journalId = this.journalId;
if (!journalId) {
return;
}
var result = await this.orm.call("account.journal", "read", [
[journalId],
["current_statement_balance", "currency_id", "company_currency_id"],
]);
this.state.journalBalance = result[0].current_statement_balance;
this.state.currency = (result[0].currency_id ||
result[0].company_currency_id)[0];
}
get journalBalanceStr() {
if (!this.state.journalBalance) {
return "";
}
return formatMonetary(this.state.journalBalance, {
currencyId: this.state.currency,
});
}
exposeController(controller) {
this.form_controller = controller;
}
async onClickNewButton() {
const action = await this.orm.call(this.props.resModel, "action_new_line", [], {
context: this.props.context,
});
this.action.doAction(action, {
onClose: async () => {
await this.model.root.load();
await this.updateJournalInfo();
this.render(true);
},
});
}
async setRainbowMan(message) {
this.effect.add({
message,
type: "rainbow_man",
});
}
get viewReconcileInfo() {
return {
resId: this.state.selectedRecordId,
type: "form",
noBreadcrumbs: true,
context: {
...(this.props.context || {}),
form_view_ref: this.props.context.view_ref,
},
display: {controlPanel: false},
mode: this.props.mode || "edit",
resModel: this.props.resModel,
};
}
async selectRecord(record) {
var resId = undefined;
if (record === undefined && this.props.resId) {
resId = this.props.resId;
} else if (record === undefined) {
var records = this.model.root.records.filter(
(modelRecord) =>
!modelRecord.data.is_reconciled || modelRecord.data.to_check
);
if (records.length === 0) {
records = this.model.root.records;
if (records.length === 0) {
this.state.selectedRecordId = false;
return;
}
}
resId = records[0].resId;
} else {
resId = record.resId;
}
if (this.state.selectedRecordId && this.state.selectedRecordId !== resId) {
if (this.form_controller && this.form_controller.model.root.isDirty) {
await this.form_controller.model.root.save({
noReload: true,
stayInEdition: true,
useSaveErrorDialog: true,
});
await this.model.root.load();
await this.render(true);
}
}
if (!this.state.selectedRecordId || this.state.selectedRecordId !== resId) {
this.state.selectedRecordId = resId;
}
this.updateURL(resId);
}
async openRecord(record) {
this.selectRecord(record);
}
updateURL(resId) {
this.router.pushState({id: resId});
}
}
ReconcileController.components = {
...ReconcileController.components,
View,
};
ReconcileController.template = "account_reconcile_oca.ReconcileController";
ReconcileController.defaultProps = {};

View file

@ -0,0 +1,14 @@
/** @odoo-module */
import {KanbanRecord} from "@web/views/kanban/kanban_record";
export class ReconcileKanbanRecord extends KanbanRecord {
getRecordClasses() {
var result = super.getRecordClasses();
if (this.props.selectedRecordId === this.props.record.resId) {
result += " o_kanban_record_reconcile_oca_selected";
}
return result;
}
}
ReconcileKanbanRecord.props = [...KanbanRecord.props, "selectedRecordId?"];

View file

@ -0,0 +1,64 @@
/** @odoo-module */
import {KanbanRenderer} from "@web/views/kanban/kanban_renderer";
import {ReconcileKanbanRecord} from "./reconcile_kanban_record.esm.js";
import {formatMonetary} from "@web/views/fields/formatters";
import {useService} from "@web/core/utils/hooks";
export class ReconcileRenderer extends KanbanRenderer {
setup() {
super.setup();
this.action = useService("action");
this.orm = useService("orm");
}
getAggregates() {
if (
this.env.parentController.props.resModel !== "account.bank.statement.line"
) {
return [];
}
const {list} = this.props;
const aggregates = [];
for (const record of list.records) {
const aggregateId = record.data.aggregate_id && record.data.aggregate_id;
if (
aggregateId &&
(!aggregates.length ||
aggregates[aggregates.length - 1].id !== aggregateId)
) {
aggregates.push({
id: aggregateId,
name: record.data.aggregate_name,
balance: record.data.statement_balance_end_real,
balanceStr: formatMonetary(record.data.statement_balance_end_real, {
currencyId: record.data.currency_id[0],
}),
});
}
}
return aggregates;
}
async onClickStatement(statementId) {
const action = await this.orm.call(
"account.bank.statement",
"action_open_statement",
[[statementId]],
{
context: this.props.context,
}
);
const model = this.props.list.model;
this.action.doAction(action, {
async onClose() {
model.root.load();
},
});
}
}
ReconcileRenderer.components = {
...KanbanRenderer.components,
KanbanRecord: ReconcileKanbanRecord,
};
ReconcileRenderer.template = "account_reconcile_oca.ReconcileRenderer";
ReconcileRenderer.props = [...KanbanRenderer.props, "selectedRecordId?"];

View file

@ -0,0 +1,16 @@
/** @odoo-module */
import {ReconcileController} from "./reconcile_controller.esm.js";
import {ReconcileRenderer} from "./reconcile_renderer.esm.js";
import {kanbanView} from "@web/views/kanban/kanban_view";
import {registry} from "@web/core/registry";
export const reconcileView = {
...kanbanView,
Renderer: ReconcileRenderer,
Controller: ReconcileController,
buttonTemplate: "account_reconcile.ReconcileView.Buttons",
searchMenuTypes: ["filter"],
};
registry.category("views").add("reconcile", reconcileView);

View file

@ -0,0 +1,45 @@
/** @odoo-module */
import {FormController} from "@web/views/form/form_controller";
import {useService} from "@web/core/utils/hooks";
import {useViewButtons} from "@web/views/view_button/view_button_hook";
const {useRef} = owl;
export class ReconcileFormController extends FormController {
setup() {
super.setup(...arguments);
this.env.exposeController(this);
this.orm = useService("orm");
const rootRef = useRef("root");
useViewButtons(this.model, rootRef, {
reload: this.reloadFormController.bind(this),
beforeExecuteAction: this.beforeExecuteActionButton.bind(this),
afterExecuteAction: this.afterExecuteActionButton.bind(this),
});
}
displayName() {
return this.env.config.getDisplayName();
}
async reloadFormController() {
var is_reconciled = this.model.root.data.is_reconciled;
await this.model.root.load();
if (this.env.parentController) {
// We will update the parent controller every time we reload the form.
await this.env.parentController.model.root.load();
await this.env.parentController.render(true);
if (!is_reconciled && this.model.root.data.is_reconciled) {
// This only happens when we press the reconcile button for showing rainbow man
const message = await this.orm.call(
"account.journal",
"get_rainbowman_message",
[[this.model.root.data.journal_id[0]]]
);
if (message) {
this.env.parentController.setRainbowMan(message);
}
// Refreshing
this.env.parentController.selectRecord();
}
}
}
}

View file

@ -0,0 +1,32 @@
/** @odoo-module */
import {Notebook} from "@web/core/notebook/notebook";
import {onWillDestroy} from "@odoo/owl";
export class ReconcileFormNotebook extends Notebook {
setup() {
super.setup(...arguments);
const onPageNavigate = this.onPageNavigate.bind(this);
this.env.bus.addEventListener("RECONCILE_PAGE_NAVIGATE", onPageNavigate);
onWillDestroy(() => {
this.env.bus.removeEventListener("RECONCILE_PAGE_NAVIGATE", onPageNavigate);
});
}
onPageNavigate(ev) {
for (const page of this.pages) {
if (
ev.detail.detail.name === page[1].name &&
this.state.currentPage !== page[0]
) {
ev.preventDefault();
ev.detail.detail.originalEv.preventDefault();
this.state.currentPage = page[0];
return;
}
}
}
}
ReconcileFormNotebook.props = {
...Notebook.props,
};

View file

@ -0,0 +1,11 @@
/** @odoo-module */
import {FormRenderer} from "@web/views/form/form_renderer";
import {ReconcileFormNotebook} from "./reconcile_form_notebook.esm.js";
export class ReconcileFormRenderer extends FormRenderer {}
ReconcileFormRenderer.components = {
...ReconcileFormRenderer.components,
Notebook: ReconcileFormNotebook,
};

View file

@ -0,0 +1,14 @@
/** @odoo-module */
import {ReconcileFormController} from "./reconcile_form_controller.esm.js";
import {ReconcileFormRenderer} from "./reconcile_form_renderer.esm.js";
import {formView} from "@web/views/form/form_view";
import {registry} from "@web/core/registry";
export const ReconcileFormView = {
...formView,
Controller: ReconcileFormController,
Renderer: ReconcileFormRenderer,
};
registry.category("views").add("reconcile_form", ReconcileFormView);

View file

@ -0,0 +1,30 @@
/** @odoo-module */
import {FormController} from "@web/views/form/form_controller";
import {useViewButtons} from "@web/views/view_button/view_button_hook";
const {useRef} = owl;
export class ReconcileManualController extends FormController {
setup() {
super.setup(...arguments);
this.env.exposeController(this);
const rootRef = useRef("root");
useViewButtons(this.model, rootRef, {
reload: this.reloadFormController.bind(this),
beforeExecuteAction: this.beforeExecuteActionButton.bind(this),
afterExecuteAction: this.afterExecuteActionButton.bind(this),
});
}
async reloadFormController() {
try {
await this.model.root.load();
} catch (error) {
// This should happen when we reconcile a line (no more possible data...)
if (this.env.parentController) {
await this.env.parentController.model.root.load();
await this.env.parentController.render(true);
this.env.parentController.selectRecord();
}
}
}
}

View file

@ -0,0 +1,12 @@
/** @odoo-module */
import {ReconcileManualController} from "./reconcile_manual_controller.esm.js";
import {formView} from "@web/views/form/form_view";
import {registry} from "@web/core/registry";
export const FormManualReconcileView = {
...formView,
Controller: ReconcileManualController,
};
registry.category("views").add("reconcile_manual", FormManualReconcileView);

View file

@ -0,0 +1,27 @@
/** @odoo-module */
import {ListController} from "@web/views/list/list_controller";
export class ReconcileMoveLineController extends ListController {
async openRecord(record) {
var data = {};
data[this.props.parentField] = [record.resId, record.display_name];
this.props.parentRecord.update(data);
}
async clickAddAll() {
await this.props.parentRecord.save();
await this.orm.call(this.props.parentRecord.resModel, "add_multiple_lines", [
this.props.parentRecord.resIds,
this.model.root.domain,
]);
await this.props.parentRecord.load();
this.props.parentRecord.model.notify();
}
}
ReconcileMoveLineController.template = `account_reconcile_oca.ReconcileMoveLineController`;
ReconcileMoveLineController.props = {
...ListController.props,
parentRecord: {type: Object, optional: true},
parentField: {type: String, optional: true},
};

View file

@ -0,0 +1,22 @@
/** @odoo-module */
import {ListRenderer} from "@web/views/list/list_renderer";
export class ReconcileMoveLineRenderer extends ListRenderer {
getRowClass(record) {
var classes = super.getRowClass(record);
if (
this.props.parentRecord.data.reconcile_data_info.counterparts.includes(
record.resId
)
) {
classes += " o_field_account_reconcile_oca_move_line_selected";
}
return classes;
}
}
ReconcileMoveLineRenderer.props = [
...ListRenderer.props,
"parentRecord",
"parentField",
];

View file

@ -0,0 +1,16 @@
/** @odoo-module */
import {ReconcileMoveLineController} from "./reconcile_move_line_controller.esm.js";
import {ReconcileMoveLineRenderer} from "./reconcile_move_line_renderer.esm.js";
import {listView} from "@web/views/list/list_view";
import {registry} from "@web/core/registry";
export const ReconcileMoveLineView = {
...listView,
Controller: ReconcileMoveLineController,
Renderer: ReconcileMoveLineRenderer,
buttonTemplate: "reconcile_move_line.ListView.Buttons",
};
registry.category("views").add("reconcile_move_line", ReconcileMoveLineView);

View file

@ -0,0 +1,15 @@
/** @odoo-module **/
import {ChatterContainer} from "@mail/components/chatter_container/chatter_container";
import {registry} from "@web/core/registry";
const {Component} = owl;
export class AccountReconcileChatterWidget extends Component {}
AccountReconcileChatterWidget.template =
"account_reconcile_oca.AccountReconcileChatterWidget";
AccountReconcileChatterWidget.components = {...Component.components, ChatterContainer};
registry
.category("fields")
.add("account_reconcile_oca_chatter", AccountReconcileChatterWidget);

View file

@ -0,0 +1,126 @@
/** @odoo-module **/
import fieldUtils from "web.field_utils";
import {float_is_zero} from "web.utils";
import {registry} from "@web/core/registry";
import session from "web.session";
import {useService} from "@web/core/utils/hooks";
const {Component} = owl;
export class AccountReconcileDataWidget extends Component {
setup() {
super.setup(...arguments);
this.orm = useService("orm");
this.action = useService("action");
this.foreignCurrency =
this.props &&
this.props.record &&
(this.props.record.data.foreign_currency_id ||
this.props.record.data.currency_id[0] !==
this.props.record.data.company_currency_id[0] ||
this.props.record.data[this.props.name].data.some(
(item) => item.line_currency_id !== item.currency_id
));
}
getReconcileLines() {
var data = this.props.record.data[this.props.name].data;
const totals = {debit: 0, credit: 0};
if (!data || !data.length) {
return {lines: [], totals};
}
for (var line in data) {
data[line].amount_format = fieldUtils.format.monetary(
data[line].amount,
undefined,
{
currency: session.get_currency(data[line].currency_id),
}
);
data[line].debit_format = fieldUtils.format.monetary(
data[line].debit,
undefined,
{
currency: session.get_currency(data[line].currency_id),
}
);
data[line].credit_format = fieldUtils.format.monetary(
data[line].credit,
undefined,
{
currency: session.get_currency(data[line].currency_id),
}
);
data[line].amount_currency_format = fieldUtils.format.monetary(
data[line].currency_amount,
undefined,
{
currency: session.get_currency(data[line].line_currency_id),
}
);
if (data[line].original_amount) {
data[line].original_amount_format = fieldUtils.format.monetary(
data[line].original_amount,
{
currency: session.get_currency(data[line].currency_id),
}
);
}
data[line].date_format = fieldUtils.format.date(
fieldUtils.parse.date(data[line].date, undefined, {isUTC: true})
);
totals.debit += data[line].debit || 0;
totals.credit += data[line].credit || 0;
}
totals.balance = totals.debit - totals.credit;
const firstLine = Object.values(data)[0] || {};
const currency = session.get_currency(firstLine.currency_id);
const decimals = currency.digits[1];
const hasOpenBalance = !float_is_zero(totals.balance, decimals);
let openDebitFmt = null;
let openCreditFmt = null;
if (totals.balance < 0) {
openDebitFmt = fieldUtils.format.monetary(Math.abs(totals.balance), {
currency: currency,
});
} else {
openCreditFmt = fieldUtils.format.monetary(totals.balance, {
currency: currency,
});
}
return {lines: data, hasOpenBalance, openDebitFmt, openCreditFmt};
}
onTrashLine(ev, line) {
ev.stopPropagation();
this.props.record.update({
manual_reference: line.reference,
manual_delete: true,
});
}
selectReconcileLine(ev, line) {
this.props.record.update({
manual_reference: line.reference,
});
const triggerEv = new CustomEvent("reconcile-page-navigate", {
detail: {
name: "manual",
originalEv: ev,
},
});
this.env.bus.trigger("RECONCILE_PAGE_NAVIGATE", triggerEv);
}
async openMove(ev, moveId) {
ev.preventDefault();
ev.stopPropagation();
console.log(moveId);
const action = await this.orm.call("account.move", "get_formview_action", [
[moveId],
]);
this.action.doAction(action);
}
}
AccountReconcileDataWidget.template = "account_reconcile_oca.ReconcileDataWidget";
registry
.category("fields")
.add("account_reconcile_oca_data", AccountReconcileDataWidget);

View file

@ -0,0 +1,52 @@
/** @odoo-module **/
import {View} from "@web/views/view";
import {registry} from "@web/core/registry";
const {Component, useSubEnv} = owl;
export class AccountReconcileMatchWidget extends Component {
setup() {
// Necessary in order to avoid a loop
super.setup(...arguments);
useSubEnv({
config: {},
parentController: this.env.parentController,
});
}
get listViewProperties() {
return {
type: "list",
display: {
controlPanel: {
// Hiding the control panel buttons
"top-left": false,
"bottom-left": true,
},
},
resModel: this.props.record.fields[this.props.name].relation,
searchMenuTypes: ["filter"],
domain: this.props.record.getFieldDomain(this.props.name).toList(),
context: {
...this.props.record.getFieldContext(this.props.name),
},
// Disables de selector
allowSelectors: false,
// We need to force the search view in order to show the right one
searchViewId: false,
parentRecord: this.props.record,
parentField: this.props.name,
showButtons: false,
};
}
}
AccountReconcileMatchWidget.template = "account_reconcile_oca.ReconcileMatchWidget";
AccountReconcileMatchWidget.components = {
...AccountReconcileMatchWidget.components,
View,
};
registry
.category("fields")
.add("account_reconcile_oca_match", AccountReconcileMatchWidget);

View file

@ -0,0 +1,29 @@
/** @odoo-module **/
import {
BadgeSelectionField,
preloadSelection,
} from "@web/views/fields/badge_selection/badge_selection_field";
import {registry} from "@web/core/registry";
export class FieldSelectionBadgeUncheck extends BadgeSelectionField {
async onChange(value) {
var old_value = this.props.value;
if (this.props.type === "many2one") {
old_value = old_value[0];
}
if (value === old_value) {
this.props.update(false);
return;
}
super.onChange(...arguments);
}
}
FieldSelectionBadgeUncheck.supportedTypes = ["many2one", "selection"];
FieldSelectionBadgeUncheck.additionalClasses = ["o_field_selection_badge"];
registry.category("fields").add("selection_badge_uncheck", FieldSelectionBadgeUncheck);
registry.category("preloadedData").add("selection_badge_uncheck", {
loadOnTypes: ["many2one"],
preload: preloadSelection,
});

View file

@ -0,0 +1,100 @@
.o_account_reconcile_oca {
display: -webkit-box;
display: -webkit-flex;
display: flex;
-webkit-flex-flow: row wrap;
flex-flow: row wrap;
height: 100%;
.o_kanban_renderer.o_kanban_ungrouped .o_kanban_record {
&:hover {
.o_reconcile_create_statement {
opacity: 100;
}
}
.row {
// We need to add this in order to make remove horizontal scroll
margin: 0;
}
margin: 0 0 0;
min-width: fit-content;
width: 100%;
.o_reconcile_create_statement {
position: absolute;
height: 0px;
margin: 0;
padding: 0 0 0 0;
border: 0;
top: -14px;
opacity: 0;
}
> div {
border-right: thick solid rgba(0, 0, 0, 0);
}
&.o_kanban_record_reconcile_oca_selected > div {
border-right: thick solid $o-brand-primary;
}
}
.o_account_reconcile_oca_selector {
width: 30%;
height: 100%;
padding: 0;
position: relative;
border-right: 1px solid $o-gray-300;
overflow: auto;
}
.o_account_reconcile_oca_info {
width: 70%;
height: 100%;
overflow: auto;
}
.o_form_view {
.btn-info:not(.dropdown-toggle):not(.dropdown-item) {
text-transform: uppercase;
}
.o_form_statusbar.o_account_reconcile_oca_statusbar {
.btn:not(.dropdown-toggle):not(.dropdown-item) {
text-transform: uppercase;
}
height: 40px;
> .o_statusbar_buttons {
height: 100%;
> .btn {
margin: 0;
height: 100%;
padding: 10px;
border-radius: 0;
}
}
}
.o_field_account_reconcile_oca_data {
.o_field_account_reconcile_oca_balance_float {
.o_field_account_reconcile_oca_balance_original_float {
text-decoration: line-through;
}
}
}
.o_field_widget.o_field_account_reconcile_oca_match {
display: inline;
}
.o_field_account_reconcile_oca_move_line_selected {
background-color: rgba($o-brand-primary, 0.2);
color: #000;
}
.o_reconcile_widget_table {
.o_reconcile_widget_line {
&.liquidity {
font-weight: bold;
}
&.selected {
background-color: rgba($o-brand-primary, 0.2);
}
&.suspense {
color: $o-gray-500;
}
}
}
}
}
.o_field_account_reconcile_oca_chatter {
width: 100%;
}

View file

@ -0,0 +1,223 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates>
<t
t-name="account_reconcile_oca.ReconcileRenderer"
t-inherit="web.KanbanRenderer"
t-inherit-mode="primary"
owl="1"
>
<xpath expr="//t[@t-as='groupOrRecord']" position="before">
<div class="m-2 d-flex w-100" t-if="env.parentController.journalId">
<span
class="flex-fill text-900 text-start ps-0 fw-bold fs-4 align-self-center"
>Global Balance</span>
<span
class="pe-0 fw-bold fs-4 align-self-center"
t-esc="env.parentController.journalBalanceStr"
/>
</div>
<t t-set="aggregates" t-value="getAggregates()" />
</xpath>
<xpath expr="//t[@t-else='']/KanbanRecord" position="before">
<t
t-if="aggregates.length and groupOrRecord.record.data.aggregate_id and aggregates[0].id == groupOrRecord.record.data.aggregate_id"
>
<t t-set="aggregate" t-value="aggregates.shift()" />
<div class="m-2 d-flex w-100">
<span
class="flex-fill text-900 text-start ps-0 fw-bold fs-4 align-self-center"
t-esc="aggregate.name"
/>
<span
t-if="groupOrRecord.record.data.reconcile_aggregate == 'statement'"
t-on-click="() => this.onClickStatement(aggregate.id)"
class="pe-0 fw-bold fs-4 align-self-center btn btn-link"
t-esc="aggregate.balanceStr"
/>
<!--
<span
t-if="groupOrRecord.record.data.reconcile_aggregate != 'statement'"
class="pe-0 fw-bold fs-4 align-self-center text-link"
t-esc="aggregate.balanceStr"
/>-->
</div>
</t>
</xpath>
<xpath expr="div[hasclass('o_kanban_renderer')]" position="attributes">
<attribute
name="class"
add="o_account_reconcile_oca_selector"
separator=" "
/>
</xpath>
<!-- Group by selector disabled on the view, so we need to find the one without group,
then we pass to the component the selected record -->
<xpath expr="//KanbanRecord[not(@group)]" position="attributes">
<attribute name="selectedRecordId">props.selectedRecordId</attribute>
</xpath>
</t>
<t
t-name="account_reconcile_oca.ReconcileController"
t-inherit="web.KanbanView"
t-inherit-mode="primary"
owl="1"
>
<!-- we pass to the component the selected record -->
<xpath expr="//Layout/t[2]" position="attributes">
<attribute name="selectedRecordId">state.selectedRecordId</attribute>
</xpath>
<xpath expr="//Layout" position="attributes">
<attribute
name="className"
>model.useSampleModel ? 'o_view_sample_data o_account_reconcile_oca' : 'o_account_reconcile_oca'</attribute>
</xpath>
<xpath expr="//Layout" position="inside">
<div class="o_account_reconcile_oca_info">
<t t-if="state.selectedRecordId">
<View t-props="viewReconcileInfo" t-key="state.selectedRecordId" />
</t>
</div>
</xpath>
</t>
<t t-name="account_reconcile.ReconcileView.Buttons" owl="1">
<button
t-on-click="onClickNewButton"
class="btn btn-primary"
t-if="activeActions.create"
>Create</button>
</t>
<t t-name="account_reconcile_oca.ReconcileMatchWidget" owl="1">
<View t-props="listViewProperties" />
</t>
<t t-name="account_reconcile_oca.ReconcileDataWidget" owl="1">
<table
class="table table-sm position-relative mb-0 table-striped o_reconcile_widget_table"
style="table-layout: auto"
>
<thead>
<th>Account</th>
<th>Partner</th>
<th>Date</th>
<th>Label</th>
<th class="text-end" t-if="foreignCurrency">
Amount in currency
</th>
<th class="text-end">Debit</th>
<th class="text-end">Credit</th>
<th t-if="! props.record.data.is_reconciled" />
</thead>
<t t-set="rec" t-value="getReconcileLines()" />
<tbody>
<t
t-foreach="rec.lines"
t-as="reconcile_line"
t-key="reconcile_line_index"
>
<tr
t-on-click="(ev) => this.selectReconcileLine(ev, reconcile_line)"
t-att-class="'o_reconcile_widget_line ' + reconcile_line.kind + (props.record.data.manual_reference == reconcile_line.reference ? ' selected ' : ' ')"
>
<td>
<div t-esc="reconcile_line.account_id[1]" />
<div t-if="reconcile_line.move_id">
<a
t-att-href="'/web#id=' + reconcile_line.move_id + '&amp;view_type=form&amp;model=account.move'"
class="o_form_uri"
t-on-click="(ev) => this.openMove(ev, reconcile_line.move_id)"
>
<small t-esc="reconcile_line.move" />
</a>
</div>
</td>
<td>
<span
t-esc="reconcile_line.partner_id[1]"
t-if="reconcile_line.partner_id and reconcile_line.partner_id[1]"
/>
</td>
<td t-esc="reconcile_line.date_format" />
<td>
<span
t-esc="reconcile_line.name"
t-if="reconcile_line.name"
/>
</td>
<td
class="text-end o_field_account_reconcile_oca_balance_float"
t-if="foreignCurrency"
>
<span t-esc="reconcile_line.amount_currency_format" />
</td>
<td
class="text-end o_field_account_reconcile_oca_balance_float"
>
<div
t-esc="reconcile_line.debit_format"
t-if="reconcile_line.amount &gt; 0"
/>
<div
class="o_field_account_reconcile_oca_balance_original_float"
t-esc="reconcile_line.original_amount_format"
t-if="reconcile_line.amount &gt; 0 and reconcile_line.original_amount"
/>
</td>
<td
class="text-end o_field_account_reconcile_oca_balance_float"
>
<div
t-esc="reconcile_line.credit_format"
t-if="reconcile_line.amount &lt; 0"
/>
<div
class="o_field_account_reconcile_oca_balance_original_float"
t-esc="reconcile_line.original_amount_format"
t-if="reconcile_line.amount &lt; 0 and reconcile_line.original_amount"
/>
</td>
<td>
<button
class="btn fa fa-trash-o"
role="button"
t-on-click="(ev) => this.onTrashLine(ev, reconcile_line)"
t-if="reconcile_line.kind == 'other' &amp;&amp; ! props.record.data.is_reconciled"
/>
</td>
</tr>
</t>
<tr class="text-muted" t-if="rec.hasOpenBalance">
<td colspan="5">Open Balance</td>
<td class="text-end" t-esc="rec.openDebitFmt" />
<td class="text-end" t-esc="rec.openCreditFmt" />
<td />
</tr>
</tbody>
</table>
</t>
<t t-name="account_reconcile_oca.AccountReconcileChatterWidget" owl="1">
<ChatterContainer
threadModel="this.props.record.fields[this.props.name].relation"
threadId="this.props.value[0]"
/>
</t>
<t
t-name="account_reconcile_oca.ReconcileMoveLineController"
t-inherit="web.ListView"
t-inherit-mode="primary"
owl="1"
>
<xpath expr="//t[@list='model.root']" position="attributes">
<attribute name="parentRecord">props.parentRecord</attribute>
<attribute name="parentField">props.parentField</attribute>
</xpath>
</t>
<t
t-name="reconcile_move_line.ListView.Buttons"
t-inherit="web.ListView.Buttons"
t-inherit-mode="primary"
owl="1"
>
<xpath expr="//div[hasclass('o_list_buttons')]" position="inside">
<button class="btn btn-primary" t-on-click="clickAddAll">Add all</button>
</xpath>
</t>
</templates>