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,
});