mirror of
https://github.com/bringout/oca-ocb-accounting.git
synced 2026-04-21 17:42:01 +02:00
Initial commit: Accounting packages
This commit is contained in:
commit
4ef34c2317
2661 changed files with 1709616 additions and 0 deletions
|
|
@ -0,0 +1,119 @@
|
|||
/** @odoo-module */
|
||||
import { camelToSnakeObject, toServerDateString } from "@spreadsheet/helpers/helpers";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { sprintf } from "@web/core/utils/strings";
|
||||
import { deepCopy } from "@web/core/utils/objects";
|
||||
|
||||
import { ServerData } from "@spreadsheet/data_sources/server_data";
|
||||
|
||||
/**
|
||||
* @typedef {import("./accounting_functions").DateRange} DateRange
|
||||
*/
|
||||
|
||||
export class AccountingDataSource {
|
||||
constructor(services) {
|
||||
this.serverData = new ServerData(services.orm, {
|
||||
whenDataIsFetched: () => services.notify(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the total credit for a given account code prefix
|
||||
* @param {string[]} codes prefixes of the accounts codes
|
||||
* @param {DateRange} dateRange start date of the period to look
|
||||
* @param {number} offset end date of the period to look
|
||||
* @param {number} companyId specific company to target
|
||||
* @param {boolean} includeUnposted wether or not select unposted entries
|
||||
* @returns {number | undefined}
|
||||
*/
|
||||
getCredit(codes, dateRange, offset, companyId, includeUnposted) {
|
||||
const data = this._fetchAccountData(codes, dateRange, offset, companyId, includeUnposted);
|
||||
return data.credit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the total debit for a given account code prefix
|
||||
* @param {string[]} codes prefixes of the accounts codes
|
||||
* @param {DateRange} dateRange start date of the period to look
|
||||
* @param {number} offset end date of the period to look
|
||||
* @param {number} companyId specific company to target
|
||||
* @param {boolean} includeUnposted wether or not select unposted entries
|
||||
* @returns {number | undefined}
|
||||
*/
|
||||
getDebit(codes, dateRange, offset, companyId, includeUnposted) {
|
||||
const data = this._fetchAccountData(codes, dateRange, offset, companyId, includeUnposted);
|
||||
return data.debit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Date} date
|
||||
* @param {number | null} companyId
|
||||
* @returns {string}
|
||||
*/
|
||||
getFiscalStartDate(date, companyId) {
|
||||
return this._fetchCompanyData(date, companyId).start;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Date} date
|
||||
* @param {number | null} companyId
|
||||
* @returns {string}
|
||||
*/
|
||||
getFiscalEndDate(date, companyId) {
|
||||
return this._fetchCompanyData(date, companyId).end;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} accountType
|
||||
* @returns {string[]}
|
||||
*/
|
||||
getAccountGroupCodes(accountType) {
|
||||
return this.serverData.batch.get("account.account", "get_account_group", accountType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the account information (credit/debit) for a given account code
|
||||
* @private
|
||||
* @param {string[]} codes prefix of the accounts' codes
|
||||
* @param {DateRange} dateRange start date of the period to look
|
||||
* @param {number} offset end date of the period to look
|
||||
* @param {number | null} companyId specific companyId to target
|
||||
* @param {boolean} includeUnposted wether or not select unposted entries
|
||||
* @returns {{ debit: number, credit: number }}
|
||||
*/
|
||||
_fetchAccountData(codes, dateRange, offset, companyId, includeUnposted) {
|
||||
dateRange = deepCopy(dateRange);
|
||||
dateRange.year += offset;
|
||||
// Excel dates start at 1899-12-30, we should not support date ranges
|
||||
// that do not cover dates prior to it.
|
||||
// Unfortunately, this check needs to be done right before the server
|
||||
// call as a date to low (year <= 1) can raise an error server side.
|
||||
if (dateRange.year < 1900) {
|
||||
throw new Error(sprintf(_t("%s is not a valid year."), dateRange.year));
|
||||
}
|
||||
return this.serverData.batch.get(
|
||||
"account.account",
|
||||
"spreadsheet_fetch_debit_credit",
|
||||
camelToSnakeObject({ dateRange, codes, companyId, includeUnposted })
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the start and end date of the fiscal year enclosing a given date
|
||||
* Defaults on the current user company if not provided
|
||||
* @private
|
||||
* @param {Date} date
|
||||
* @param {number | null} companyId
|
||||
* @returns {{start: string, end: string}}
|
||||
*/
|
||||
_fetchCompanyData(date, companyId) {
|
||||
const result = this.serverData.batch.get("res.company", "get_fiscal_dates", {
|
||||
date: toServerDateString(date),
|
||||
company_id: companyId,
|
||||
});
|
||||
if (result === false) {
|
||||
throw new Error(_t("The company fiscal year could not be found."));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,306 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { sprintf } from "@web/core/utils/strings";
|
||||
|
||||
import spreadsheet from "@spreadsheet/o_spreadsheet/o_spreadsheet_extended";
|
||||
const { functionRegistry } = spreadsheet.registries;
|
||||
const { args, toBoolean, toString, toNumber, toJsDate } = spreadsheet.helpers;
|
||||
|
||||
const QuarterRegexp = /^q([1-4])\/(\d{4})$/i;
|
||||
const MonthRegexp = /^0?([1-9]|1[0-2])\/(\d{4})$/i;
|
||||
|
||||
/**
|
||||
* @typedef {Object} YearDateRange
|
||||
* @property {"year"} rangeType
|
||||
* @property {number} year
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} QuarterDateRange
|
||||
* @property {"quarter"} rangeType
|
||||
* @property {number} year
|
||||
* @property {number} quarter
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} MonthDateRange
|
||||
* @property {"month"} rangeType
|
||||
* @property {number} year
|
||||
* @property {number} month
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} DayDateRange
|
||||
* @property {"day"} rangeType
|
||||
* @property {number} year
|
||||
* @property {number} month
|
||||
* @property {number} day
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {YearDateRange | QuarterDateRange | MonthDateRange | DayDateRange} DateRange
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {string} dateRange
|
||||
* @returns {QuarterDateRange | undefined}
|
||||
*/
|
||||
function parseAccountingQuarter(dateRange) {
|
||||
const found = dateRange.match(QuarterRegexp);
|
||||
return found
|
||||
? {
|
||||
rangeType: "quarter",
|
||||
year: toNumber(found[2]),
|
||||
quarter: toNumber(found[1]),
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} dateRange
|
||||
* @returns {MonthDateRange | undefined}
|
||||
*/
|
||||
function parseAccountingMonth(dateRange) {
|
||||
const found = dateRange.match(MonthRegexp);
|
||||
return found
|
||||
? {
|
||||
rangeType: "month",
|
||||
year: toNumber(found[2]),
|
||||
month: toNumber(found[1]),
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} dateRange
|
||||
* @returns {YearDateRange | undefined}
|
||||
*/
|
||||
function parseAccountingYear(dateRange) {
|
||||
const dateNumber = toNumber(dateRange);
|
||||
// This allows a bit of flexibility for the user if they were to input a
|
||||
// numeric value instead of a year.
|
||||
// Users won't need to fetch accounting info for year 3000 before a long time
|
||||
// And the numeric value 3000 corresponds to 18th march 1908, so it's not an
|
||||
//issue to prevent them from fetching accounting data prior to that date.
|
||||
if (dateNumber < 3000) {
|
||||
return { rangeType: "year", year: dateNumber };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} dateRange
|
||||
* @returns {DayDateRange}
|
||||
*/
|
||||
function parseAccountingDay(dateRange) {
|
||||
const dateNumber = toNumber(dateRange);
|
||||
return {
|
||||
rangeType: "day",
|
||||
year: functionRegistry.get("YEAR").compute(dateNumber),
|
||||
month: functionRegistry.get("MONTH").compute(dateNumber),
|
||||
day: functionRegistry.get("DAY").compute(dateNumber),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string | number} dateRange
|
||||
* @returns {DateRange}
|
||||
*/
|
||||
export function parseAccountingDate(dateRange) {
|
||||
try {
|
||||
dateRange = toString(dateRange).trim();
|
||||
return (
|
||||
parseAccountingQuarter(dateRange) ||
|
||||
parseAccountingMonth(dateRange) ||
|
||||
parseAccountingYear(dateRange) ||
|
||||
parseAccountingDay(dateRange)
|
||||
);
|
||||
} catch (_) {
|
||||
throw new Error(
|
||||
sprintf(
|
||||
_t(
|
||||
`'%s' is not a valid period. Supported formats are "21/12/2022", "Q1/2022", "12/2022", and "2022".`
|
||||
),
|
||||
dateRange
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const ODOO_FIN_ARGS = `
|
||||
account_codes (string) ${_t("The prefix of the accounts.")}
|
||||
date_range (string, date) ${_t(
|
||||
`The date range. Supported formats are "21/12/2022", "Q1/2022", "12/2022", and "2022".`
|
||||
)}
|
||||
offset (number, default=0) ${_t("Year offset applied to date_range.")}
|
||||
company_id (number, optional) ${_t("The company to target (Advanced).")}
|
||||
include_unposted (boolean, default=FALSE) ${_t("Set to TRUE to include unposted entries.")}
|
||||
`;
|
||||
|
||||
functionRegistry.add("ODOO.CREDIT", {
|
||||
description: _t("Get the total credit for the specified account(s) and period."),
|
||||
args: args(ODOO_FIN_ARGS),
|
||||
returns: ["NUMBER"],
|
||||
compute: function (
|
||||
accountCodes,
|
||||
dateRange,
|
||||
offset = 0,
|
||||
companyId = null,
|
||||
includeUnposted = false
|
||||
) {
|
||||
accountCodes = toString(accountCodes)
|
||||
.split(",")
|
||||
.map((code) => code.trim())
|
||||
.sort();
|
||||
offset = toNumber(offset);
|
||||
dateRange = parseAccountingDate(dateRange);
|
||||
includeUnposted = toBoolean(includeUnposted);
|
||||
return this.getters.getAccountPrefixCredit(
|
||||
accountCodes,
|
||||
dateRange,
|
||||
offset,
|
||||
companyId,
|
||||
includeUnposted
|
||||
);
|
||||
},
|
||||
computeFormat: function (
|
||||
accountCodes,
|
||||
dateRange,
|
||||
offset = 0,
|
||||
companyId = null,
|
||||
includeUnposted = false
|
||||
) {
|
||||
return this.getters.getCompanyCurrencyFormat(companyId && companyId.value) || "#,##0.00";
|
||||
},
|
||||
});
|
||||
|
||||
functionRegistry.add("ODOO.DEBIT", {
|
||||
description: _t("Get the total debit for the specified account(s) and period."),
|
||||
args: args(ODOO_FIN_ARGS),
|
||||
returns: ["NUMBER"],
|
||||
compute: function (
|
||||
accountCodes,
|
||||
dateRange,
|
||||
offset = 0,
|
||||
companyId = null,
|
||||
includeUnposted = false
|
||||
) {
|
||||
accountCodes = toString(accountCodes)
|
||||
.split(",")
|
||||
.map((code) => code.trim())
|
||||
.sort();
|
||||
offset = toNumber(offset);
|
||||
dateRange = parseAccountingDate(dateRange);
|
||||
includeUnposted = toBoolean(includeUnposted);
|
||||
return this.getters.getAccountPrefixDebit(
|
||||
accountCodes,
|
||||
dateRange,
|
||||
offset,
|
||||
companyId,
|
||||
includeUnposted
|
||||
);
|
||||
},
|
||||
computeFormat: function (
|
||||
accountCodes,
|
||||
dateRange,
|
||||
offset = 0,
|
||||
companyId = null,
|
||||
includeUnposted = false
|
||||
) {
|
||||
return this.getters.getCompanyCurrencyFormat(companyId && companyId.value) || "#,##0.00";
|
||||
},
|
||||
});
|
||||
|
||||
functionRegistry.add("ODOO.BALANCE", {
|
||||
description: _t("Get the total balance for the specified account(s) and period."),
|
||||
args: args(ODOO_FIN_ARGS),
|
||||
returns: ["NUMBER"],
|
||||
compute: function (
|
||||
accountCodes,
|
||||
dateRange,
|
||||
offset = 0,
|
||||
companyId = null,
|
||||
includeUnposted = false
|
||||
) {
|
||||
accountCodes = toString(accountCodes)
|
||||
.split(",")
|
||||
.map((code) => code.trim())
|
||||
.sort();
|
||||
offset = toNumber(offset);
|
||||
dateRange = parseAccountingDate(dateRange);
|
||||
includeUnposted = toBoolean(includeUnposted);
|
||||
return (
|
||||
this.getters.getAccountPrefixDebit(
|
||||
accountCodes,
|
||||
dateRange,
|
||||
offset,
|
||||
companyId,
|
||||
includeUnposted
|
||||
) -
|
||||
this.getters.getAccountPrefixCredit(
|
||||
accountCodes,
|
||||
dateRange,
|
||||
offset,
|
||||
companyId,
|
||||
includeUnposted
|
||||
)
|
||||
);
|
||||
},
|
||||
computeFormat: function (
|
||||
accountCodes,
|
||||
dateRange,
|
||||
offset = 0,
|
||||
companyId = null,
|
||||
includeUnposted = false
|
||||
) {
|
||||
return this.getters.getCompanyCurrencyFormat(companyId && companyId.value) || "#,##0.00";
|
||||
},
|
||||
});
|
||||
|
||||
functionRegistry.add("ODOO.FISCALYEAR.START", {
|
||||
description: _t("Returns the starting date of the fiscal year encompassing the provided date."),
|
||||
args: args(`
|
||||
day (date) ${_t("The day from which to extract the fiscal year start.")}
|
||||
company_id (number, optional) ${_t("The company.")}
|
||||
`),
|
||||
returns: ["NUMBER"],
|
||||
computeFormat: () => "m/d/yyyy",
|
||||
compute: function (date, companyId = null) {
|
||||
const startDate = this.getters.getFiscalStartDate(
|
||||
toJsDate(date),
|
||||
companyId === null ? null : toNumber(companyId)
|
||||
);
|
||||
return toNumber(startDate);
|
||||
},
|
||||
});
|
||||
|
||||
functionRegistry.add("ODOO.FISCALYEAR.END", {
|
||||
description: _t("Returns the ending date of the fiscal year encompassing the provided date."),
|
||||
args: args(`
|
||||
day (date) ${_t("The day from which to extract the fiscal year end.")}
|
||||
company_id (number, optional) ${_t("The company.")}
|
||||
`),
|
||||
returns: ["NUMBER"],
|
||||
computeFormat: () => "m/d/yyyy",
|
||||
compute: function (date, companyId = null) {
|
||||
const endDate = this.getters.getFiscalEndDate(
|
||||
toJsDate(date),
|
||||
companyId === null ? null : toNumber(companyId)
|
||||
);
|
||||
return toNumber(endDate);
|
||||
},
|
||||
});
|
||||
|
||||
functionRegistry.add("ODOO.ACCOUNT.GROUP", {
|
||||
description: _t("Returns the account ids of a given group."),
|
||||
args: args(`
|
||||
type (string) ${_t("The account type (income, expense, asset_current,...).")}
|
||||
`),
|
||||
returns: ["NUMBER"],
|
||||
computeFormat: () => "m/d/yyyy",
|
||||
compute: function (accountType) {
|
||||
const accountTypes = this.getters.getAccountGroupCodes(toString(accountType));
|
||||
return accountTypes.join(",");
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { _lt } from "@web/core/l10n/translation";
|
||||
import spreadsheet from "@spreadsheet/o_spreadsheet/o_spreadsheet_extended";
|
||||
import AccountingPlugin from "./plugins/accounting_plugin";
|
||||
import { getFirstAccountFunction, getNumberOfAccountFormulas } from "./utils";
|
||||
import { parseAccountingDate } from "./accounting_functions";
|
||||
import { camelToSnakeObject } from "@spreadsheet/helpers/helpers";
|
||||
|
||||
const { cellMenuRegistry, uiPluginRegistry } = spreadsheet.registries;
|
||||
const { astToFormula } = spreadsheet;
|
||||
const { toString, toBoolean } = spreadsheet.helpers;
|
||||
|
||||
uiPluginRegistry.add("odooAccountingAggregates", AccountingPlugin);
|
||||
|
||||
cellMenuRegistry.add("move_lines_see_records", {
|
||||
name: _lt("See records"),
|
||||
sequence: 176,
|
||||
async action(env) {
|
||||
const cell = env.model.getters.getActiveCell();
|
||||
const { args } = getFirstAccountFunction(cell.content);
|
||||
let [codes, date_range, offset, companyId, includeUnposted] = args
|
||||
.map(astToFormula)
|
||||
.map((arg) => env.model.getters.evaluateFormula(arg));
|
||||
codes = toString(codes).split(",");
|
||||
const dateRange = parseAccountingDate(date_range);
|
||||
offset = parseInt(offset) || 0;
|
||||
dateRange.year += offset || 0;
|
||||
companyId = parseInt(companyId) || null;
|
||||
try {
|
||||
includeUnposted = toBoolean(includeUnposted);
|
||||
} catch {
|
||||
includeUnposted = false;
|
||||
}
|
||||
|
||||
const action = await env.services.orm.call(
|
||||
"account.account",
|
||||
"spreadsheet_move_line_action",
|
||||
[camelToSnakeObject({ dateRange, companyId, codes, includeUnposted })]
|
||||
);
|
||||
await env.services.action.doAction(action);
|
||||
},
|
||||
isVisible: (env) => {
|
||||
const cell = env.model.getters.getActiveCell();
|
||||
return (
|
||||
cell &&
|
||||
!cell.evaluated.error &&
|
||||
cell.evaluated.value !== "" &&
|
||||
getNumberOfAccountFormulas(cell.content) === 1
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import spreadsheet from "@spreadsheet/o_spreadsheet/o_spreadsheet_extended";
|
||||
import { AccountingDataSource } from "../accounting_datasource";
|
||||
const DATA_SOURCE_ID = "ACCOUNTING_AGGREGATES";
|
||||
|
||||
/**
|
||||
* @typedef {import("../accounting_functions").DateRange} DateRange
|
||||
*/
|
||||
|
||||
export default class AccountingPlugin extends spreadsheet.UIPlugin {
|
||||
constructor(getters, history, dispatch, config) {
|
||||
super(getters, history, dispatch, config);
|
||||
this.dataSources = config.dataSources;
|
||||
if (this.dataSources) {
|
||||
this.dataSources.add(DATA_SOURCE_ID, AccountingDataSource);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Getters
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Gets the total balance for given account code prefix
|
||||
* @param {string[]} codes prefixes of the accounts' codes
|
||||
* @param {DateRange} dateRange start date of the period to look
|
||||
* @param {number} offset end date of the period to look
|
||||
* @param {number | null} companyId specific company to target
|
||||
* @param {boolean} includeUnposted wether or not select unposted entries
|
||||
* @returns {number}
|
||||
*/
|
||||
getAccountPrefixCredit(codes, dateRange, offset, companyId, includeUnposted) {
|
||||
return (
|
||||
this.dataSources &&
|
||||
this.dataSources
|
||||
.get(DATA_SOURCE_ID)
|
||||
.getCredit(codes, dateRange, offset, companyId, includeUnposted)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the total balance for a given account code prefix
|
||||
* @param {string[]} codes prefixes of the accounts codes
|
||||
* @param {DateRange} dateRange start date of the period to look
|
||||
* @param {number} offset end date of the period to look
|
||||
* @param {number | null} companyId specific company to target
|
||||
* @param {boolean} includeUnposted wether or not select unposted entries
|
||||
* @returns {number}
|
||||
*/
|
||||
getAccountPrefixDebit(codes, dateRange, offset, companyId, includeUnposted) {
|
||||
return (
|
||||
this.dataSources &&
|
||||
this.dataSources
|
||||
.get(DATA_SOURCE_ID)
|
||||
.getDebit(codes, dateRange, offset, companyId, includeUnposted)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Date} date Date included in the fiscal year
|
||||
* @param {number | null} companyId specific company to target
|
||||
* @returns {string | undefined}
|
||||
*/
|
||||
getFiscalStartDate(date, companyId) {
|
||||
return (
|
||||
this.dataSources &&
|
||||
this.dataSources.get(DATA_SOURCE_ID).getFiscalStartDate(date, companyId)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Date} date Date included in the fiscal year
|
||||
* @param {number | undefined} companyId specific company to target
|
||||
* @returns {string | undefined}
|
||||
*/
|
||||
getFiscalEndDate(date, companyId) {
|
||||
return (
|
||||
this.dataSources &&
|
||||
this.dataSources.get(DATA_SOURCE_ID).getFiscalEndDate(date, companyId)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} accountType
|
||||
* @returns {string[]}
|
||||
*/
|
||||
getAccountGroupCodes(accountType) {
|
||||
return (
|
||||
this.dataSources &&
|
||||
this.dataSources.get(DATA_SOURCE_ID).getAccountGroupCodes(accountType)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AccountingPlugin.getters = [
|
||||
"getAccountPrefixCredit",
|
||||
"getAccountPrefixDebit",
|
||||
"getAccountGroupCodes",
|
||||
"getFiscalStartDate",
|
||||
"getFiscalEndDate",
|
||||
];
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/** @odoo-module **/
|
||||
import { getOdooFunctions } from "@spreadsheet/helpers/odoo_functions_helpers";
|
||||
|
||||
/** @typedef {import("@spreadsheet/helpers/odoo_functions_helpers").OdooFunctionDescription} OdooFunctionDescription*/
|
||||
|
||||
/**
|
||||
* @param {string} formula
|
||||
* @returns {number}
|
||||
*/
|
||||
export function getNumberOfAccountFormulas(formula) {
|
||||
return getOdooFunctions(formula, ["ODOO.BALANCE", "ODOO.CREDIT", "ODOO.DEBIT"]).filter(
|
||||
(fn) => fn.isMatched
|
||||
).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first Account function description of the given formula.
|
||||
*
|
||||
* @param {string} formula
|
||||
* @returns {OdooFunctionDescription | undefined}
|
||||
*/
|
||||
export function getFirstAccountFunction(formula) {
|
||||
return getOdooFunctions(formula, ["ODOO.BALANCE", "ODOO.CREDIT", "ODOO.DEBIT"]).find(
|
||||
(fn) => fn.isMatched
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue