mirror of
https://github.com/bringout/oca-ocb-accounting.git
synced 2026-04-21 16:22:01 +02:00
19.0 vanilla
This commit is contained in:
parent
ba20ce7443
commit
768b70e05e
2357 changed files with 1057103 additions and 712486 deletions
|
|
@ -0,0 +1,51 @@
|
|||
import { _t } from "@web/core/l10n/translation";
|
||||
|
||||
import { registries, tokenColors, helpers } from "@odoo/o-spreadsheet";
|
||||
|
||||
const { insertTokenAfterLeftParenthesis } = helpers;
|
||||
|
||||
// copy-pasted list of options from the `account_type` selection field.
|
||||
const ACCOUNT_TYPES = [
|
||||
["asset_receivable", _t("Receivable")],
|
||||
["asset_cash", _t("Bank and Cash")],
|
||||
["asset_current", _t("Current Assets")],
|
||||
["asset_non_current", _t("Non-current Assets")],
|
||||
["asset_prepayments", _t("Prepayments")],
|
||||
["asset_fixed", _t("Fixed Assets")],
|
||||
["liability_payable", _t("Payable")],
|
||||
["liability_credit_card", _t("Credit Card")],
|
||||
["liability_current", _t("Current Liabilities")],
|
||||
["liability_non_current", _t("Non-current Liabilities")],
|
||||
["equity", _t("Equity")],
|
||||
["equity_unaffected", _t("Current Year Earnings")],
|
||||
["income", _t("Income")],
|
||||
["income_other", _t("Other Income")],
|
||||
["expense", _t("Expenses")],
|
||||
["expense_depreciation", _t("Depreciation")],
|
||||
["expense_direct_cost", _t("Cost of Revenue")],
|
||||
["off_balance", _t("Off-Balance Sheet")],
|
||||
];
|
||||
|
||||
registries.autoCompleteProviders.add("account_group_types", {
|
||||
sequence: 50,
|
||||
autoSelectFirstProposal: true,
|
||||
getProposals(tokenAtCursor) {
|
||||
const functionContext = tokenAtCursor.functionContext;
|
||||
if (
|
||||
functionContext?.parent.toUpperCase() === "ODOO.ACCOUNT.GROUP" &&
|
||||
functionContext.argPosition === 0
|
||||
) {
|
||||
return ACCOUNT_TYPES.map(([technicalName, displayName]) => {
|
||||
const text = `"${technicalName}"`;
|
||||
return {
|
||||
text,
|
||||
description: displayName,
|
||||
htmlContent: [{ value: text, color: tokenColors.STRING }],
|
||||
fuzzySearchKey: technicalName + displayName,
|
||||
};
|
||||
});
|
||||
}
|
||||
return;
|
||||
},
|
||||
selectProposal: insertTokenAfterLeftParenthesis,
|
||||
});
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
/** @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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,9 @@
|
|||
/** @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";
|
||||
import * as spreadsheet from "@odoo/o-spreadsheet";
|
||||
import { EvaluationError } from "@odoo/o-spreadsheet";
|
||||
const { functionRegistry } = spreadsheet.registries;
|
||||
const { args, toBoolean, toString, toNumber, toJsDate } = spreadsheet.helpers;
|
||||
const { arg, 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;
|
||||
|
|
@ -43,41 +41,53 @@ const MonthRegexp = /^0?([1-9]|1[0-2])\/(\d{4})$/i;
|
|||
*/
|
||||
|
||||
/**
|
||||
* @param {string} dateRange
|
||||
* @param {object | undefined} dateRange
|
||||
* @returns {QuarterDateRange | undefined}
|
||||
*/
|
||||
function parseAccountingQuarter(dateRange) {
|
||||
const found = dateRange.match(QuarterRegexp);
|
||||
const found = toString(dateRange?.value).trim().match(QuarterRegexp);
|
||||
return found
|
||||
? {
|
||||
rangeType: "quarter",
|
||||
year: toNumber(found[2]),
|
||||
quarter: toNumber(found[1]),
|
||||
year: Number(found[2]),
|
||||
quarter: Number(found[1]),
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} dateRange
|
||||
* @param {object | undefined} dateRange
|
||||
* @returns {MonthDateRange | undefined}
|
||||
*/
|
||||
function parseAccountingMonth(dateRange) {
|
||||
const found = dateRange.match(MonthRegexp);
|
||||
function parseAccountingMonth(dateRange, locale) {
|
||||
if (
|
||||
typeof dateRange?.value === "number" &&
|
||||
dateRange.format?.includes("m") &&
|
||||
!dateRange.format?.includes("d")
|
||||
) {
|
||||
const date = toJsDate(dateRange.value, locale);
|
||||
return {
|
||||
rangeType: "month",
|
||||
year: date.getFullYear(),
|
||||
month: date.getMonth() + 1,
|
||||
};
|
||||
}
|
||||
const found = toString(dateRange?.value).trim().match(MonthRegexp);
|
||||
return found
|
||||
? {
|
||||
rangeType: "month",
|
||||
year: toNumber(found[2]),
|
||||
month: toNumber(found[1]),
|
||||
year: Number(found[2]),
|
||||
month: Number(found[1]),
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} dateRange
|
||||
* @param {object | undefined} dateRange
|
||||
* @returns {YearDateRange | undefined}
|
||||
*/
|
||||
function parseAccountingYear(dateRange) {
|
||||
const dateNumber = toNumber(dateRange);
|
||||
function parseAccountingYear(dateRange, locale) {
|
||||
const dateNumber = toNumber(dateRange?.value, locale);
|
||||
// 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
|
||||
|
|
@ -90,217 +100,381 @@ function parseAccountingYear(dateRange) {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {string} dateRange
|
||||
* @param {object | undefined} dateRange
|
||||
* @returns {DayDateRange}
|
||||
*/
|
||||
function parseAccountingDay(dateRange) {
|
||||
const dateNumber = toNumber(dateRange);
|
||||
function parseAccountingDay(dateRange, locale) {
|
||||
const dateNumber = toNumber(dateRange?.value, locale);
|
||||
return {
|
||||
rangeType: "day",
|
||||
year: functionRegistry.get("YEAR").compute(dateNumber),
|
||||
month: functionRegistry.get("MONTH").compute(dateNumber),
|
||||
day: functionRegistry.get("DAY").compute(dateNumber),
|
||||
year: functionRegistry.get("YEAR").compute.bind({ locale })(dateNumber),
|
||||
month: functionRegistry.get("MONTH").compute.bind({ locale })(dateNumber),
|
||||
day: functionRegistry.get("DAY").compute.bind({ locale })(dateNumber),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string | number} dateRange
|
||||
* @param {object | undefined} dateRange
|
||||
* @returns {DateRange}
|
||||
*/
|
||||
export function parseAccountingDate(dateRange) {
|
||||
export function parseAccountingDate(dateRange, locale) {
|
||||
try {
|
||||
dateRange = toString(dateRange).trim();
|
||||
return (
|
||||
parseAccountingQuarter(dateRange) ||
|
||||
parseAccountingMonth(dateRange) ||
|
||||
parseAccountingYear(dateRange) ||
|
||||
parseAccountingDay(dateRange)
|
||||
parseAccountingMonth(dateRange, locale) ||
|
||||
parseAccountingYear(dateRange, locale) ||
|
||||
parseAccountingDay(dateRange, locale)
|
||||
);
|
||||
} 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
|
||||
} catch {
|
||||
throw new EvaluationError(
|
||||
_t(
|
||||
`'%s' is not a valid period. Supported formats are "21/12/2022", "Q1/2022", "12/2022", and "2022".`,
|
||||
dateRange?.value
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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.")}
|
||||
`;
|
||||
const YEAR_OFFSET_ARG = arg("offset (number, default=0)", _t("Offset applied to the years."))
|
||||
const COMPANY_ARG = arg("company_id (number, optional)", _t("The company to target (Advanced)."))
|
||||
const POSTED_ARG = arg(
|
||||
"include_unposted (boolean, default=FALSE)",
|
||||
_t("Set to TRUE to include unposted entries.")
|
||||
)
|
||||
|
||||
const ODOO_FIN_ARGS = () => [
|
||||
arg("account_codes (string)", _t("The prefix of the accounts.")),
|
||||
arg(
|
||||
"date_range (string, date)",
|
||||
_t(`The date range. Supported formats are "21/12/2022", "Q1/2022", "12/2022", and "2022".`)
|
||||
),
|
||||
YEAR_OFFSET_ARG,
|
||||
COMPANY_ARG,
|
||||
POSTED_ARG,
|
||||
];
|
||||
|
||||
const ODOO_RESIDUAL_ARGS = () => [
|
||||
arg(
|
||||
"account_codes (string, optional)",
|
||||
_t("The prefix of the accounts. If none provided, all receivable and payable accounts will be used.")
|
||||
),
|
||||
arg(
|
||||
"date_range (string, date, optional)",
|
||||
_t(`The date range. Supported formats are "21/12/2022", "Q1/2022", "12/2022", and "2022".`)
|
||||
),
|
||||
YEAR_OFFSET_ARG,
|
||||
COMPANY_ARG,
|
||||
POSTED_ARG,
|
||||
];
|
||||
|
||||
const ODOO_PARTNER_BALANCE_ARGS = () => {
|
||||
const partner_arg = arg("partner_ids (string)", _t("The partner ids (separated by a comma)."));
|
||||
return [partner_arg, ...ODOO_RESIDUAL_ARGS()];
|
||||
}
|
||||
|
||||
functionRegistry.add("ODOO.CREDIT", {
|
||||
description: _t("Get the total credit for the specified account(s) and period."),
|
||||
args: args(ODOO_FIN_ARGS),
|
||||
args: ODOO_FIN_ARGS(),
|
||||
category: "Odoo",
|
||||
returns: ["NUMBER"],
|
||||
compute: function (
|
||||
accountCodes,
|
||||
dateRange,
|
||||
offset = 0,
|
||||
companyId = null,
|
||||
includeUnposted = false
|
||||
offset = { value: 0 },
|
||||
companyId = { value: null },
|
||||
includeUnposted = { value: false }
|
||||
) {
|
||||
accountCodes = toString(accountCodes)
|
||||
const _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";
|
||||
const _offset = toNumber(offset, this.locale);
|
||||
const _dateRange = parseAccountingDate(dateRange, this.locale);
|
||||
const _companyId = companyId?.value;
|
||||
const _includeUnposted = toBoolean(includeUnposted);
|
||||
return {
|
||||
value: this.getters.getAccountPrefixCredit(
|
||||
_accountCodes,
|
||||
_dateRange,
|
||||
_offset,
|
||||
_companyId,
|
||||
_includeUnposted
|
||||
),
|
||||
format: this.getters.getCompanyCurrencyFormat(_companyId) || "#,##0.00",
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
functionRegistry.add("ODOO.DEBIT", {
|
||||
description: _t("Get the total debit for the specified account(s) and period."),
|
||||
args: args(ODOO_FIN_ARGS),
|
||||
args: ODOO_FIN_ARGS(),
|
||||
category: "Odoo",
|
||||
returns: ["NUMBER"],
|
||||
compute: function (
|
||||
accountCodes,
|
||||
dateRange,
|
||||
offset = 0,
|
||||
companyId = null,
|
||||
includeUnposted = false
|
||||
offset = { value: 0 },
|
||||
companyId = { value: null },
|
||||
includeUnposted = { value: false }
|
||||
) {
|
||||
accountCodes = toString(accountCodes)
|
||||
const _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";
|
||||
const _offset = toNumber(offset, this.locale);
|
||||
const _dateRange = parseAccountingDate(dateRange, this.locale);
|
||||
const _companyId = companyId?.value;
|
||||
const _includeUnposted = toBoolean(includeUnposted);
|
||||
return {
|
||||
value: this.getters.getAccountPrefixDebit(
|
||||
_accountCodes,
|
||||
_dateRange,
|
||||
_offset,
|
||||
_companyId,
|
||||
_includeUnposted
|
||||
),
|
||||
format: this.getters.getCompanyCurrencyFormat(_companyId) || "#,##0.00",
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
functionRegistry.add("ODOO.BALANCE", {
|
||||
description: _t("Get the total balance for the specified account(s) and period."),
|
||||
args: args(ODOO_FIN_ARGS),
|
||||
args: ODOO_FIN_ARGS(),
|
||||
category: "Odoo",
|
||||
returns: ["NUMBER"],
|
||||
compute: function (
|
||||
accountCodes,
|
||||
dateRange,
|
||||
offset = 0,
|
||||
companyId = null,
|
||||
includeUnposted = false
|
||||
offset = { value: 0 },
|
||||
companyId = { value: null },
|
||||
includeUnposted = { value: false }
|
||||
) {
|
||||
accountCodes = toString(accountCodes)
|
||||
const _accountCodes = toString(accountCodes)
|
||||
.split(",")
|
||||
.map((code) => code.trim())
|
||||
.sort();
|
||||
offset = toNumber(offset);
|
||||
dateRange = parseAccountingDate(dateRange);
|
||||
includeUnposted = toBoolean(includeUnposted);
|
||||
return (
|
||||
const _offset = toNumber(offset, this.locale);
|
||||
const _dateRange = parseAccountingDate(dateRange, this.locale);
|
||||
const _companyId = companyId?.value;
|
||||
const _includeUnposted = toBoolean(includeUnposted);
|
||||
const value =
|
||||
this.getters.getAccountPrefixDebit(
|
||||
accountCodes,
|
||||
dateRange,
|
||||
offset,
|
||||
companyId,
|
||||
includeUnposted
|
||||
_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";
|
||||
_accountCodes,
|
||||
_dateRange,
|
||||
_offset,
|
||||
_companyId,
|
||||
_includeUnposted
|
||||
);
|
||||
return { value, format: this.getters.getCompanyCurrencyFormat(_companyId) || "#,##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.")}
|
||||
`),
|
||||
args: [
|
||||
arg("day (date)", _t("The day from which to extract the fiscal year start.")),
|
||||
arg("company_id (number, optional)", _t("The company.")),
|
||||
],
|
||||
category: "Odoo",
|
||||
returns: ["NUMBER"],
|
||||
computeFormat: () => "m/d/yyyy",
|
||||
compute: function (date, companyId = null) {
|
||||
compute: function (date, companyId = { value: null }) {
|
||||
const startDate = this.getters.getFiscalStartDate(
|
||||
toJsDate(date),
|
||||
companyId === null ? null : toNumber(companyId)
|
||||
toJsDate(date, this.locale),
|
||||
companyId.value === null ? null : toNumber(companyId, this.locale)
|
||||
);
|
||||
return toNumber(startDate);
|
||||
return {
|
||||
value: toNumber(startDate, this.locale),
|
||||
format: this.locale.dateFormat,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
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.")}
|
||||
`),
|
||||
args: [
|
||||
arg("day (date)", _t("The day from which to extract the fiscal year end.")),
|
||||
arg("company_id (number, optional)", _t("The company.")),
|
||||
],
|
||||
category: "Odoo",
|
||||
returns: ["NUMBER"],
|
||||
computeFormat: () => "m/d/yyyy",
|
||||
compute: function (date, companyId = null) {
|
||||
compute: function (date, companyId = { value: null }) {
|
||||
const endDate = this.getters.getFiscalEndDate(
|
||||
toJsDate(date),
|
||||
companyId === null ? null : toNumber(companyId)
|
||||
toJsDate(date, this.locale),
|
||||
companyId.value === null ? null : toNumber(companyId, this.locale)
|
||||
);
|
||||
return toNumber(endDate);
|
||||
return {
|
||||
value: toNumber(endDate, this.locale),
|
||||
format: this.locale.dateFormat,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const ACCOUNT_TYPES = [
|
||||
"asset_receivable",
|
||||
"asset_cash",
|
||||
"asset_current",
|
||||
"asset_non_current",
|
||||
"asset_prepayments",
|
||||
"asset_fixed",
|
||||
"liability_payable",
|
||||
"liability_credit_card",
|
||||
"liability_current",
|
||||
"liability_non_current",
|
||||
"equity",
|
||||
"equity_unaffected",
|
||||
"income",
|
||||
"income_other",
|
||||
"expense",
|
||||
"expense_depreciation",
|
||||
"expense_direct_cost",
|
||||
"off_balance",
|
||||
];
|
||||
|
||||
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,...).")}
|
||||
`),
|
||||
description: _t("Returns the account codes of a given group."),
|
||||
args: [
|
||||
arg(
|
||||
"type (string)",
|
||||
_t("The technical account type (possible values are: %s).", ACCOUNT_TYPES.join(", "))
|
||||
),
|
||||
],
|
||||
category: "Odoo",
|
||||
returns: ["NUMBER"],
|
||||
computeFormat: () => "m/d/yyyy",
|
||||
compute: function (accountType) {
|
||||
const accountTypes = this.getters.getAccountGroupCodes(toString(accountType));
|
||||
return accountTypes.join(",");
|
||||
},
|
||||
});
|
||||
|
||||
functionRegistry.add("ODOO.RESIDUAL", {
|
||||
description: _t("Return the residual amount for the specified account(s) and period"),
|
||||
args: ODOO_RESIDUAL_ARGS(),
|
||||
category: "Odoo",
|
||||
returns: ["NUMBER"],
|
||||
compute: function (
|
||||
accountCodes,
|
||||
dateRange,
|
||||
offset = { value: 0 },
|
||||
companyId = { value: null },
|
||||
includeUnposted = { value: false }
|
||||
) {
|
||||
const _accountCodes = toString(accountCodes)
|
||||
.split(",")
|
||||
.map((code) => code.trim())
|
||||
.sort();
|
||||
const _offset = toNumber(offset, this.locale);
|
||||
if ( !dateRange?.value ) {
|
||||
dateRange = { value: new Date().getFullYear() }
|
||||
}
|
||||
const _dateRange = parseAccountingDate(dateRange, this.locale);
|
||||
const _companyId = toNumber(companyId, this.locale);
|
||||
const _includeUnposted = toBoolean(includeUnposted);
|
||||
return {
|
||||
value: this.getters.getAccountResidual(
|
||||
_accountCodes,
|
||||
_dateRange,
|
||||
_offset,
|
||||
_companyId,
|
||||
_includeUnposted
|
||||
),
|
||||
format: this.getters.getCompanyCurrencyFormat(_companyId) || "#,##0.00",
|
||||
};
|
||||
},
|
||||
})
|
||||
|
||||
functionRegistry.add("ODOO.PARTNER.BALANCE", {
|
||||
description: _t("Return the partner balance for the specified account(s) and period"),
|
||||
args: ODOO_PARTNER_BALANCE_ARGS(),
|
||||
category: "Odoo",
|
||||
returns: ["NUMBER"],
|
||||
compute: function (
|
||||
partnerIds,
|
||||
accountCodes,
|
||||
dateRange,
|
||||
offset = { value: 0 },
|
||||
companyId = { value: null },
|
||||
includeUnposted = { value: false }
|
||||
) {
|
||||
const _partnerIds = toString(partnerIds)
|
||||
.split(",")
|
||||
.map((partnerId) => toNumber(partnerId, this.locale))
|
||||
.sort();
|
||||
const _accountCodes = toString(accountCodes)
|
||||
.split(",")
|
||||
.map((code) => code.trim())
|
||||
.sort();
|
||||
const _offset = toNumber(offset, this.locale);
|
||||
|
||||
if ( !dateRange?.value ) {
|
||||
dateRange = { value: new Date().getFullYear() }
|
||||
}
|
||||
const _dateRange = parseAccountingDate(dateRange, this.locale);
|
||||
const _companyId = toNumber(companyId, this.locale);
|
||||
const _includeUnposted = toBoolean(includeUnposted);
|
||||
return {
|
||||
value: this.getters.getAccountPartnerData(
|
||||
_accountCodes,
|
||||
_dateRange,
|
||||
_offset,
|
||||
_companyId,
|
||||
_includeUnposted,
|
||||
_partnerIds
|
||||
),
|
||||
format: this.getters.getCompanyCurrencyFormat(_companyId) || "#,##0.00",
|
||||
};
|
||||
},
|
||||
})
|
||||
|
||||
functionRegistry.add("ODOO.BALANCE.TAG", {
|
||||
description: _t("Return the balance of accounts for the specified tag(s) and period"),
|
||||
args: [
|
||||
arg("account_tag_ids (string)", _t("The tag ids (separated by a comma).")),
|
||||
arg(
|
||||
"date_range (string, date, optional)",
|
||||
_t(`The date range. Supported formats are "21/12/2022", "Q1/2022", "12/2022", and "2022".`)
|
||||
),
|
||||
YEAR_OFFSET_ARG,
|
||||
COMPANY_ARG,
|
||||
POSTED_ARG,
|
||||
],
|
||||
category: "Odoo",
|
||||
returns: ["NUMBER"],
|
||||
compute: function (
|
||||
accountTagIds,
|
||||
dateRange,
|
||||
offset = { value: 0 },
|
||||
companyId = { value: null },
|
||||
includeUnposted = { value: false }
|
||||
) {
|
||||
const _accountTagIds = toString(accountTagIds)
|
||||
.split(",")
|
||||
.map((accountTagId) => toNumber(accountTagId, this.locale))
|
||||
.sort();
|
||||
const _offset = toNumber(offset, this.locale);
|
||||
|
||||
if ( !dateRange?.value ) {
|
||||
dateRange = { value: new Date().getFullYear() }
|
||||
}
|
||||
const _dateRange = parseAccountingDate(dateRange, this.locale);
|
||||
const _companyId = toNumber(companyId, this.locale);
|
||||
const _includeUnposted = toBoolean(includeUnposted);
|
||||
return {
|
||||
value: this.getters.getAccountTagData(
|
||||
_accountTagIds,
|
||||
_dateRange,
|
||||
_offset,
|
||||
_companyId,
|
||||
_includeUnposted,
|
||||
),
|
||||
format: this.getters.getCompanyCurrencyFormat(_companyId) || "#,##0.00",
|
||||
};
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,52 +1,92 @@
|
|||
/** @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 { _t } from "@web/core/l10n/translation";
|
||||
import * as spreadsheet from "@odoo/o-spreadsheet";
|
||||
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 { cellMenuRegistry, featurePluginRegistry } = spreadsheet.registries;
|
||||
const { astToFormula } = spreadsheet;
|
||||
const { toString, toBoolean } = spreadsheet.helpers;
|
||||
const { isEvaluationError, toString, toBoolean } = spreadsheet.helpers;
|
||||
|
||||
uiPluginRegistry.add("odooAccountingAggregates", AccountingPlugin);
|
||||
featurePluginRegistry.add("odooAccountingAggregates", AccountingPlugin);
|
||||
|
||||
cellMenuRegistry.add("move_lines_see_records", {
|
||||
name: _lt("See records"),
|
||||
name: _t("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;
|
||||
async execute(env, newWindow) {
|
||||
const position = env.model.getters.getActivePosition();
|
||||
const sheetId = position.sheetId;
|
||||
const cell = env.model.getters.getCell(position);
|
||||
const func = getFirstAccountFunction(cell.compiledFormula.tokens);
|
||||
let codes, partner_ids, account_tag_ids = "";
|
||||
let date_range, offset, companyId, includeUnposted = false;
|
||||
const parsed_args = func.args.map(astToFormula).map(
|
||||
(arg) => env.model.getters.evaluateFormulaResult(sheetId, arg)
|
||||
);
|
||||
if ( func.functionName === "ODOO.PARTNER.BALANCE" ) {
|
||||
[partner_ids, codes, date_range, offset, companyId, includeUnposted] = parsed_args;
|
||||
} else if ( func.functionName === "ODOO.BALANCE.TAG" ) {
|
||||
[account_tag_ids, date_range, offset, companyId, includeUnposted] = parsed_args;
|
||||
} else {
|
||||
[codes, date_range, offset, companyId, includeUnposted] = parsed_args;
|
||||
}
|
||||
if ( codes?.value && !isEvaluationError(codes.value) ) {
|
||||
codes = toString(codes?.value).split(",").map((code) => code.trim());
|
||||
} else {
|
||||
codes = [];
|
||||
}
|
||||
const locale = env.model.getters.getLocale();
|
||||
let dateRange;
|
||||
if ( date_range?.value && !isEvaluationError(date_range.value) ) {
|
||||
dateRange = parseAccountingDate(date_range, locale);
|
||||
} else {
|
||||
if ( ["ODOO.PARTNER.BALANCE", "ODOO.RESIDUAL", "ODOO.BALANCE.TAG"].includes(func.functionName) ) {
|
||||
dateRange = parseAccountingDate({ value: new Date().getFullYear() }, locale);
|
||||
}
|
||||
}
|
||||
offset = parseInt(offset?.value) || 0;
|
||||
dateRange.year += offset || 0;
|
||||
companyId = parseInt(companyId) || null;
|
||||
companyId = parseInt(companyId?.value) || null;
|
||||
try {
|
||||
includeUnposted = toBoolean(includeUnposted);
|
||||
includeUnposted = toBoolean(includeUnposted.value);
|
||||
} catch {
|
||||
includeUnposted = false;
|
||||
}
|
||||
|
||||
let partnerIds, accountTagIds;
|
||||
if ( func.functionName === "ODOO.BALANCE.TAG" ) {
|
||||
accountTagIds = toString(account_tag_ids).split(",").map((tag) => tag.trim());
|
||||
} else {
|
||||
partnerIds = toString(partner_ids).split(",").map((code) => code.trim());
|
||||
}
|
||||
|
||||
let param;
|
||||
if ( func.functionName === "ODOO.BALANCE.TAG" ) {
|
||||
param = [camelToSnakeObject({ accountTagIds, dateRange, companyId, includeUnposted })]
|
||||
} else if ( func.functionName === "ODOO.PARTNER.BALANCE" ) {
|
||||
param = [camelToSnakeObject({ dateRange, companyId, codes, includeUnposted, partnerIds })]
|
||||
} else {
|
||||
param = [camelToSnakeObject({ dateRange, companyId, codes, includeUnposted })]
|
||||
}
|
||||
const action = await env.services.orm.call(
|
||||
"account.account",
|
||||
"spreadsheet_move_line_action",
|
||||
[camelToSnakeObject({ dateRange, companyId, codes, includeUnposted })]
|
||||
param
|
||||
);
|
||||
await env.services.action.doAction(action);
|
||||
await env.services.action.doAction(action, { newWindow });
|
||||
},
|
||||
isVisible: (env) => {
|
||||
const cell = env.model.getters.getActiveCell();
|
||||
const position = env.model.getters.getActivePosition();
|
||||
const evaluatedCell = env.model.getters.getEvaluatedCell(position);
|
||||
const cell = env.model.getters.getCell(position);
|
||||
return (
|
||||
!isEvaluationError(evaluatedCell.value) &&
|
||||
evaluatedCell.value !== "" &&
|
||||
cell &&
|
||||
!cell.evaluated.error &&
|
||||
cell.evaluated.value !== "" &&
|
||||
getNumberOfAccountFormulas(cell.content) === 1
|
||||
cell.isFormula &&
|
||||
getNumberOfAccountFormulas(cell.compiledFormula.tokens) === 1
|
||||
);
|
||||
},
|
||||
icon: "o-spreadsheet-Icon.SEE_RECORDS",
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,20 +1,39 @@
|
|||
/** @odoo-module */
|
||||
// @ts-check
|
||||
|
||||
import spreadsheet from "@spreadsheet/o_spreadsheet/o_spreadsheet_extended";
|
||||
import { AccountingDataSource } from "../accounting_datasource";
|
||||
const DATA_SOURCE_ID = "ACCOUNTING_AGGREGATES";
|
||||
import { EvaluationError } from "@odoo/o-spreadsheet";
|
||||
import { OdooUIPlugin } from "@spreadsheet/plugins";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { deepCopy } from "@web/core/utils/objects";
|
||||
import { camelToSnakeObject, toServerDateString } from "@spreadsheet/helpers/helpers";
|
||||
|
||||
/**
|
||||
* @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);
|
||||
export class AccountingPlugin extends OdooUIPlugin {
|
||||
static getters = /** @type {const} */ ([
|
||||
"getAccountPrefixCredit",
|
||||
"getAccountPrefixDebit",
|
||||
"getAccountGroupCodes",
|
||||
"getFiscalStartDate",
|
||||
"getFiscalEndDate",
|
||||
"getAccountResidual",
|
||||
"getAccountPartnerData",
|
||||
"getAccountTagData",
|
||||
]);
|
||||
constructor(config) {
|
||||
super(config);
|
||||
/** @type {import("@spreadsheet/data_sources/server_data").ServerData} */
|
||||
this._serverData = config.custom.odooDataProvider?.serverData;
|
||||
}
|
||||
|
||||
get serverData() {
|
||||
if (!this._serverData) {
|
||||
throw new Error(
|
||||
"'serverData' is not defined, please make sure a 'OdooDataProvider' instance is provided to the model."
|
||||
);
|
||||
}
|
||||
return this._serverData;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
|
@ -31,12 +50,8 @@ export default class AccountingPlugin extends spreadsheet.UIPlugin {
|
|||
* @returns {number}
|
||||
*/
|
||||
getAccountPrefixCredit(codes, dateRange, offset, companyId, includeUnposted) {
|
||||
return (
|
||||
this.dataSources &&
|
||||
this.dataSources
|
||||
.get(DATA_SOURCE_ID)
|
||||
.getCredit(codes, dateRange, offset, companyId, includeUnposted)
|
||||
);
|
||||
const data = this._fetchAccountData(codes, dateRange, offset, companyId, includeUnposted);
|
||||
return data.credit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -49,12 +64,8 @@ export default class AccountingPlugin extends spreadsheet.UIPlugin {
|
|||
* @returns {number}
|
||||
*/
|
||||
getAccountPrefixDebit(codes, dateRange, offset, companyId, includeUnposted) {
|
||||
return (
|
||||
this.dataSources &&
|
||||
this.dataSources
|
||||
.get(DATA_SOURCE_ID)
|
||||
.getDebit(codes, dateRange, offset, companyId, includeUnposted)
|
||||
);
|
||||
const data = this._fetchAccountData(codes, dateRange, offset, companyId, includeUnposted);
|
||||
return data.debit;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -63,10 +74,7 @@ export default class AccountingPlugin extends spreadsheet.UIPlugin {
|
|||
* @returns {string | undefined}
|
||||
*/
|
||||
getFiscalStartDate(date, companyId) {
|
||||
return (
|
||||
this.dataSources &&
|
||||
this.dataSources.get(DATA_SOURCE_ID).getFiscalStartDate(date, companyId)
|
||||
);
|
||||
return this._fetchCompanyData(date, companyId).start;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -75,10 +83,7 @@ export default class AccountingPlugin extends spreadsheet.UIPlugin {
|
|||
* @returns {string | undefined}
|
||||
*/
|
||||
getFiscalEndDate(date, companyId) {
|
||||
return (
|
||||
this.dataSources &&
|
||||
this.dataSources.get(DATA_SOURCE_ID).getFiscalEndDate(date, companyId)
|
||||
);
|
||||
return this._fetchCompanyData(date, companyId).end;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -86,17 +91,145 @@ export default class AccountingPlugin extends spreadsheet.UIPlugin {
|
|||
* @returns {string[]}
|
||||
*/
|
||||
getAccountGroupCodes(accountType) {
|
||||
return (
|
||||
this.dataSources &&
|
||||
this.dataSources.get(DATA_SOURCE_ID).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 EvaluationError(_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 })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AccountingPlugin.getters = [
|
||||
"getAccountPrefixCredit",
|
||||
"getAccountPrefixDebit",
|
||||
"getAccountGroupCodes",
|
||||
"getFiscalStartDate",
|
||||
"getFiscalEndDate",
|
||||
];
|
||||
/**
|
||||
* 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 EvaluationError(_t("The company fiscal year could not be found."));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the residual amount for given account code prefixes over a given period
|
||||
* @param {string[]} codes prefixes of the accounts codes
|
||||
* @param {DateRange} dateRange start date of the period to look
|
||||
* @param {number} offset year offset of the period to search
|
||||
* @param {number} companyId specific company to target
|
||||
* @param {boolean} includeUnposted whether or not select unposted entries
|
||||
* @returns {number | undefined}
|
||||
*/
|
||||
getAccountResidual(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 EvaluationError(_t("%s is not a valid year.", dateRange.year));
|
||||
}
|
||||
const result = this.serverData.batch.get(
|
||||
"account.account",
|
||||
"spreadsheet_fetch_residual_amount",
|
||||
camelToSnakeObject({ codes, dateRange, companyId, includeUnposted })
|
||||
);
|
||||
if (result === false) {
|
||||
throw new EvaluationError(_t("The residual amount for given accounts could not be computed."));
|
||||
}
|
||||
return result.amount_residual;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the account information for a given account code and partner
|
||||
* @private
|
||||
* @param {string[]} codes prefix of the accounts' codes
|
||||
* @param {DateRange} dateRange start date of the period to look
|
||||
* @param {number} offset year offset of the period to look
|
||||
* @param {number | null} companyId specific companyId to target
|
||||
* @param {boolean} includeUnposted wether or not select unposted entries
|
||||
* @param {number[]} partnerIds ids of the partners
|
||||
* @returns {number | undefined}
|
||||
*/
|
||||
getAccountPartnerData(codes, dateRange, offset, companyId, includeUnposted, partnerIds) {
|
||||
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 EvaluationError(_t("%s is not a valid year.", dateRange.year));
|
||||
}
|
||||
const result = this.serverData.batch.get(
|
||||
"account.account",
|
||||
"spreadsheet_fetch_partner_balance",
|
||||
camelToSnakeObject({ dateRange, codes, companyId, includeUnposted, partnerIds })
|
||||
);
|
||||
if (result === false) {
|
||||
throw new EvaluationError(_t("The balance for given partners could not be computed."));
|
||||
}
|
||||
return result.balance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the balance for a given account tag
|
||||
* @private
|
||||
* @param {number[]} accountTagIds ids of the account tags
|
||||
* @param {DateRange} dateRange start date of the period to look
|
||||
* @param {number} offset year offset of the period to look
|
||||
* @param {number | null} companyId specific companyId to target
|
||||
* @param {boolean} includeUnposted wether or not select unposted entries
|
||||
* @returns {number | undefined}
|
||||
*/
|
||||
getAccountTagData(accountTagIds, 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 too low (year <= 1) can raise an error server side.
|
||||
if (dateRange.year < 1900) {
|
||||
throw new EvaluationError(_t("%s is not a valid year.", dateRange.year));
|
||||
}
|
||||
const result = this.serverData.batch.get(
|
||||
"account.account",
|
||||
"spreadsheet_fetch_balance_tag",
|
||||
camelToSnakeObject({ accountTagIds, dateRange, companyId, includeUnposted })
|
||||
);
|
||||
if (result === false) {
|
||||
throw new EvaluationError(_t("The balance for given account tag could not be computed."));
|
||||
}
|
||||
return result.balance;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,28 @@
|
|||
/** @odoo-module **/
|
||||
import { getOdooFunctions } from "@spreadsheet/helpers/odoo_functions_helpers";
|
||||
// @ts-check
|
||||
|
||||
/** @typedef {import("@spreadsheet/helpers/odoo_functions_helpers").OdooFunctionDescription} OdooFunctionDescription*/
|
||||
import { helpers } from "@odoo/o-spreadsheet";
|
||||
|
||||
const { getFunctionsFromTokens } = helpers;
|
||||
|
||||
/**
|
||||
* @param {string} formula
|
||||
* @typedef {import("@odoo/o-spreadsheet").Token} Token
|
||||
* @typedef {import("@spreadsheet/helpers/odoo_functions_helpers").OdooFunctionDescription} OdooFunctionDescription
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Token[]} tokens
|
||||
* @returns {number}
|
||||
*/
|
||||
export function getNumberOfAccountFormulas(formula) {
|
||||
return getOdooFunctions(formula, ["ODOO.BALANCE", "ODOO.CREDIT", "ODOO.DEBIT"]).filter(
|
||||
(fn) => fn.isMatched
|
||||
).length;
|
||||
export function getNumberOfAccountFormulas(tokens) {
|
||||
return getFunctionsFromTokens(tokens, ["ODOO.BALANCE", "ODOO.CREDIT", "ODOO.DEBIT", "ODOO.RESIDUAL", "ODOO.PARTNER.BALANCE", "ODOO.BALANCE.TAG"]).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first Account function description of the given formula.
|
||||
*
|
||||
* @param {string} formula
|
||||
* @param {Token[]} tokens
|
||||
* @returns {OdooFunctionDescription | undefined}
|
||||
*/
|
||||
export function getFirstAccountFunction(formula) {
|
||||
return getOdooFunctions(formula, ["ODOO.BALANCE", "ODOO.CREDIT", "ODOO.DEBIT"]).find(
|
||||
(fn) => fn.isMatched
|
||||
);
|
||||
export function getFirstAccountFunction(tokens) {
|
||||
return getFunctionsFromTokens(tokens, ["ODOO.BALANCE", "ODOO.CREDIT", "ODOO.DEBIT", "ODOO.RESIDUAL", "ODOO.PARTNER.BALANCE", "ODOO.BALANCE.TAG"])[0];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,42 +1,68 @@
|
|||
/** @odoo-module */
|
||||
import {
|
||||
SpreadsheetModels,
|
||||
defineSpreadsheetModels,
|
||||
getBasicData,
|
||||
} from "@spreadsheet/../tests/helpers/data";
|
||||
import { fields, models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
import { getBasicData } from "@spreadsheet/../tests/utils/data";
|
||||
export class AccountMoveLine extends models.Model {
|
||||
_name = "account.move.line";
|
||||
|
||||
account_id = fields.Many2one({ relation: "account.account" });
|
||||
date = fields.Date({ string: "Date" });
|
||||
name = fields.Char({ string: "Name" });
|
||||
|
||||
_records = [
|
||||
{ id: 1, name: "line1", account_id: 1, date: "2022-06-01" },
|
||||
{ id: 2, name: "line2", account_id: 2, date: "2022-06-23" },
|
||||
];
|
||||
}
|
||||
|
||||
export class AccountAccount extends models.Model {
|
||||
_name = "account.account";
|
||||
|
||||
code = fields.Char({ string: "Code" });
|
||||
account_type = fields.Char({ string: "Account type" });
|
||||
|
||||
spreadsheet_fetch_debit_credit(args) {
|
||||
return new Array(args.length).fill({ credit: 0, debit: 0 });
|
||||
}
|
||||
|
||||
get_account_group(accountTypes) {
|
||||
const data = accountTypes.map((accountType) => {
|
||||
const records = this.env["account.account"].search_read(
|
||||
[["account_type", "=", accountType]],
|
||||
["code"]
|
||||
);
|
||||
return records.map((record) => record.code);
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
_records = [
|
||||
{ id: 1, code: "100104", account_type: "income" },
|
||||
{ id: 2, code: "100105", account_type: "income_other" },
|
||||
{ id: 3, code: "200104", account_type: "income" },
|
||||
];
|
||||
}
|
||||
|
||||
export function getAccountingData() {
|
||||
return {
|
||||
models: {
|
||||
...getBasicData(),
|
||||
"account.move.line": {
|
||||
fields: {
|
||||
account_id: { type: "many2one", relation: "account.account" },
|
||||
date: { string: "Date", type: "date" },
|
||||
},
|
||||
records: [
|
||||
{ id: 1, name: "line1", account_id: 1, date: "2022-06-01" },
|
||||
{ id: 2, name: "line2", account_id: 2, date: "2022-06-23" },
|
||||
],
|
||||
},
|
||||
"account.account": {
|
||||
fields: {
|
||||
code: { string: "Code", type: "string" },
|
||||
account_type: { string: "Account type", type: "string" },
|
||||
},
|
||||
records: [
|
||||
{ id: 1, code: "100104", account_type: "income" },
|
||||
{ id: 2, code: "100105", account_type: "income_other" },
|
||||
{ id: 3, code: "200104", account_type: "income" },
|
||||
],
|
||||
},
|
||||
},
|
||||
models: { ...getBasicData() },
|
||||
views: {
|
||||
"account.move.line,false,list": /* xml */ `
|
||||
<tree string="Move Lines">
|
||||
<list string="Move Lines">
|
||||
<field name="id"/>
|
||||
<field name="account_id"/>
|
||||
<field name="date"/>
|
||||
</tree>
|
||||
</list>
|
||||
`,
|
||||
"account.move.line,false,search": /* xml */ `<search/>`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function defineSpreadsheetAccountModels() {
|
||||
const SpreadsheetAccountModels = [AccountMoveLine, AccountAccount];
|
||||
Object.assign(SpreadsheetModels, SpreadsheetAccountModels);
|
||||
defineSpreadsheetModels();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
registry
|
||||
|
|
@ -10,10 +8,11 @@ registry
|
|||
.add("account.account/get_account_group", function (route, args, performRPC) {
|
||||
const accountTypes = args.args[0];
|
||||
const data = accountTypes.map((accountType) => {
|
||||
const records = this.mockSearchRead("account.account", [
|
||||
[["account_type", "=", accountType]],
|
||||
["code"],
|
||||
], {});
|
||||
const records = this.mockSearchRead(
|
||||
"account.account",
|
||||
[[["account_type", "=", accountType]], ["code"]],
|
||||
{}
|
||||
);
|
||||
return records.map((record) => record.code);
|
||||
});
|
||||
return data;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { setCellContent } from "@spreadsheet/../tests/helpers/commands";
|
||||
import { getCellValue } from "@spreadsheet/../tests/helpers/getters";
|
||||
import { createModelWithDataSource } from "@spreadsheet/../tests/helpers/model";
|
||||
import { waitForDataLoaded } from "@spreadsheet/helpers/model";
|
||||
import {
|
||||
defineSpreadsheetAccountModels,
|
||||
getAccountingData,
|
||||
} from "@spreadsheet_account/../tests/accounting_test_data";
|
||||
|
||||
describe.current.tags("headless");
|
||||
defineSpreadsheetAccountModels();
|
||||
|
||||
const serverData = getAccountingData();
|
||||
|
||||
test("get no account", async () => {
|
||||
const { model } = await createModelWithDataSource({ serverData });
|
||||
setCellContent(model, "A1", `=ODOO.ACCOUNT.GROUP("test")`);
|
||||
await waitForDataLoaded(model);
|
||||
expect(getCellValue(model, "A1")).toBe("");
|
||||
});
|
||||
|
||||
test("get one account", async () => {
|
||||
const { model } = await createModelWithDataSource({ serverData });
|
||||
setCellContent(model, "A1", `=ODOO.ACCOUNT.GROUP("income_other")`);
|
||||
await waitForDataLoaded(model);
|
||||
expect(getCellValue(model, "A1")).toBe("100105");
|
||||
});
|
||||
|
||||
test("get multiple accounts", async () => {
|
||||
const { model } = await createModelWithDataSource({ serverData });
|
||||
setCellContent(model, "A1", `=ODOO.ACCOUNT.GROUP("income")`);
|
||||
await waitForDataLoaded(model);
|
||||
expect(getCellValue(model, "A1")).toBe("100104,200104");
|
||||
});
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { stores } from "@odoo/o-spreadsheet";
|
||||
import { defineSpreadsheetModels } from "@spreadsheet/../tests/helpers/data";
|
||||
import { makeStore } from "@spreadsheet/../tests/helpers/stores";
|
||||
|
||||
describe.current.tags("headless");
|
||||
defineSpreadsheetModels();
|
||||
|
||||
const { CellComposerStore } = stores;
|
||||
|
||||
test("ODOO.ACCOUNT.GROUP type", async function () {
|
||||
const { store: composer } = await makeStore(CellComposerStore);
|
||||
|
||||
composer.startEdition("=ODOO.ACCOUNT.GROUP(");
|
||||
await animationFrame();
|
||||
const proposals = composer.autoCompleteProposals;
|
||||
expect(proposals.map((p) => p.text)).toEqual([
|
||||
'"asset_receivable"',
|
||||
'"asset_cash"',
|
||||
'"asset_current"',
|
||||
'"asset_non_current"',
|
||||
'"asset_prepayments"',
|
||||
'"asset_fixed"',
|
||||
'"liability_payable"',
|
||||
'"liability_credit_card"',
|
||||
'"liability_current"',
|
||||
'"liability_non_current"',
|
||||
'"equity"',
|
||||
'"equity_unaffected"',
|
||||
'"income"',
|
||||
'"income_other"',
|
||||
'"expense"',
|
||||
'"expense_depreciation"',
|
||||
'"expense_direct_cost"',
|
||||
'"off_balance"',
|
||||
]);
|
||||
composer.insertAutoCompleteValue(proposals[0].text);
|
||||
await animationFrame();
|
||||
expect(composer.currentContent).toBe('=ODOO.ACCOUNT.GROUP("asset_receivable"');
|
||||
expect(composer.isAutoCompleteDisplayed).toBe(false);
|
||||
});
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { setCellContent } from "@spreadsheet/../tests/utils/commands";
|
||||
import {
|
||||
createModelWithDataSource,
|
||||
waitForDataSourcesLoaded,
|
||||
} from "@spreadsheet/../tests/utils/model";
|
||||
import { getCellValue } from "@spreadsheet/../tests/utils/getters";
|
||||
import { getAccountingData } from "../accounting_test_data";
|
||||
|
||||
let serverData;
|
||||
|
||||
function beforeEach() {
|
||||
serverData = getAccountingData();
|
||||
}
|
||||
|
||||
QUnit.module("spreadsheet_account > account groups", { beforeEach }, () => {
|
||||
QUnit.test("get no account", async (assert) => {
|
||||
const model = await createModelWithDataSource({ serverData });
|
||||
setCellContent(model, "A1", `=ODOO.ACCOUNT.GROUP("test")`);
|
||||
await waitForDataSourcesLoaded(model);
|
||||
assert.equal(getCellValue(model, "A1"), "");
|
||||
});
|
||||
|
||||
QUnit.test("get one account", async (assert) => {
|
||||
const model = await createModelWithDataSource({ serverData });
|
||||
setCellContent(model, "A1", `=ODOO.ACCOUNT.GROUP("income_other")`);
|
||||
await waitForDataSourcesLoaded(model);
|
||||
assert.equal(getCellValue(model, "A1"), "100105");
|
||||
});
|
||||
|
||||
QUnit.test("get multiple accounts", async (assert) => {
|
||||
const model = await createModelWithDataSource({ serverData });
|
||||
setCellContent(model, "A1", `=ODOO.ACCOUNT.GROUP("income")`);
|
||||
await waitForDataSourcesLoaded(model);
|
||||
assert.equal(getCellValue(model, "A1"), "100104,200104");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,422 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { setCellContent } from "@spreadsheet/../tests/helpers/commands";
|
||||
import { getCellValue, getEvaluatedCell } from "@spreadsheet/../tests/helpers/getters";
|
||||
import { createModelWithDataSource } from "@spreadsheet/../tests/helpers/model";
|
||||
import { camelToSnakeObject } from "@spreadsheet/helpers/helpers";
|
||||
import {
|
||||
defineSpreadsheetAccountModels,
|
||||
getAccountingData,
|
||||
} from "@spreadsheet_account/../tests/accounting_test_data";
|
||||
import { parseAccountingDate } from "@spreadsheet_account/accounting_functions";
|
||||
import { makeServerError } from "@web/../tests/web_test_helpers";
|
||||
import { sprintf } from "@web/core/utils/strings";
|
||||
|
||||
import * as spreadsheet from "@odoo/o-spreadsheet";
|
||||
import { waitForDataLoaded } from "@spreadsheet/helpers/model";
|
||||
|
||||
describe.current.tags("headless");
|
||||
defineSpreadsheetAccountModels();
|
||||
|
||||
const { DEFAULT_LOCALE: locale } = spreadsheet.constants;
|
||||
|
||||
const serverData = getAccountingData();
|
||||
|
||||
test("Basic evaluation", async () => {
|
||||
const { model } = await createModelWithDataSource({
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "spreadsheet_fetch_debit_credit") {
|
||||
expect.step("spreadsheet_fetch_debit_credit");
|
||||
return [{ debit: 42, credit: 16 }];
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.CREDIT("100", "2022")`);
|
||||
setCellContent(model, "A2", `=ODOO.DEBIT("100", "2022")`);
|
||||
setCellContent(model, "A3", `=ODOO.BALANCE("100", "2022")`);
|
||||
await waitForDataLoaded(model);
|
||||
expect(getCellValue(model, "A1")).toBe(16);
|
||||
expect(getCellValue(model, "A2")).toBe(42);
|
||||
expect(getCellValue(model, "A3")).toBe(26);
|
||||
expect.verifySteps(["spreadsheet_fetch_debit_credit"]);
|
||||
});
|
||||
|
||||
test("evaluation with reference to a month period", async () => {
|
||||
const { model } = await createModelWithDataSource({
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "spreadsheet_fetch_debit_credit") {
|
||||
expect(args.args[0]).toEqual([
|
||||
{
|
||||
codes: ["100"],
|
||||
company_id: null,
|
||||
date_range: {
|
||||
month: 2,
|
||||
range_type: "month",
|
||||
year: 2022,
|
||||
},
|
||||
include_unposted: false,
|
||||
},
|
||||
]);
|
||||
expect.step("spreadsheet_fetch_debit_credit");
|
||||
return [{ debit: 42, credit: 16 }];
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "B1", "02/2022");
|
||||
setCellContent(model, "A1", `=ODOO.CREDIT("100", B1)`);
|
||||
setCellContent(model, "A2", `=ODOO.DEBIT("100", B1)`);
|
||||
setCellContent(model, "A3", `=ODOO.BALANCE("100", B1)`);
|
||||
await waitForDataLoaded(model);
|
||||
expect(getCellValue(model, "A1")).toBe(16);
|
||||
expect(getCellValue(model, "A2")).toBe(42);
|
||||
expect(getCellValue(model, "A3")).toBe(26);
|
||||
expect(getCellValue(model, "B1")).toBe(44593);
|
||||
expect.verifySteps(["spreadsheet_fetch_debit_credit"]);
|
||||
});
|
||||
|
||||
test("Functions are correctly formatted", async () => {
|
||||
const { model } = await createModelWithDataSource();
|
||||
setCellContent(model, "A1", `=ODOO.CREDIT("100", "2022")`);
|
||||
setCellContent(model, "A2", `=ODOO.DEBIT("100", "2022")`);
|
||||
setCellContent(model, "A3", `=ODOO.BALANCE("100", "2022")`);
|
||||
await waitForDataLoaded(model);
|
||||
expect(getEvaluatedCell(model, "A1").format).toBe("#,##0.00[$€]");
|
||||
expect(getEvaluatedCell(model, "A2").format).toBe("#,##0.00[$€]");
|
||||
expect(getEvaluatedCell(model, "A3").format).toBe("#,##0.00[$€]");
|
||||
});
|
||||
|
||||
test("Functions with a wrong company id is correctly in error", async () => {
|
||||
const { model } = await createModelWithDataSource({
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "get_company_currency_for_spreadsheet") {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.CREDIT("100", "2022", 0, 123456)`);
|
||||
await waitForDataLoaded(model);
|
||||
expect(getEvaluatedCell(model, "A1").message).toBe("Currency not available for this company.");
|
||||
});
|
||||
|
||||
test("formula with invalid date", async () => {
|
||||
const { model } = await createModelWithDataSource();
|
||||
setCellContent(model, "A1", `=ODOO.CREDIT("100",)`);
|
||||
setCellContent(model, "A2", `=ODOO.DEBIT("100", 0)`);
|
||||
setCellContent(model, "A3", `=ODOO.BALANCE("100", -1)`);
|
||||
setCellContent(model, "A4", `=ODOO.BALANCE("100", "not a valid period")`);
|
||||
setCellContent(model, "A5", `=ODOO.BALANCE("100", 1900)`); // this should be ok
|
||||
setCellContent(model, "A6", `=ODOO.BALANCE("100", 1900, -1)`);
|
||||
setCellContent(model, "A7", `=ODOO.DEBIT("100", 1899)`);
|
||||
await waitForDataLoaded(model);
|
||||
const errorMessage = `'%s' is not a valid period. Supported formats are "21/12/2022", "Q1/2022", "12/2022", and "2022".`;
|
||||
expect(getEvaluatedCell(model, "A1").message).toBe("0 is not a valid year.");
|
||||
expect(getEvaluatedCell(model, "A2").message).toBe("0 is not a valid year.");
|
||||
expect(getEvaluatedCell(model, "A3").message).toBe("-1 is not a valid year.");
|
||||
expect(getEvaluatedCell(model, "A4").message).toBe(sprintf(errorMessage, "not a valid period"));
|
||||
expect(getEvaluatedCell(model, "A5").value).toBe(0);
|
||||
expect(getEvaluatedCell(model, "A6").message).toBe("1899 is not a valid year.");
|
||||
expect(getEvaluatedCell(model, "A7").message).toBe("1899 is not a valid year.");
|
||||
});
|
||||
|
||||
test("Evaluation with multiple account codes", async () => {
|
||||
const { model } = await createModelWithDataSource({
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "spreadsheet_fetch_debit_credit") {
|
||||
expect.step("spreadsheet_fetch_debit_credit");
|
||||
return [{ debit: 142, credit: 26 }];
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.CREDIT("100,200", "2022")`);
|
||||
setCellContent(model, "A2", `=ODOO.DEBIT("100,200", "2022")`);
|
||||
setCellContent(model, "A3", `=ODOO.BALANCE("100,200", "2022")`);
|
||||
|
||||
// with spaces
|
||||
setCellContent(model, "B1", `=ODOO.CREDIT("100 , 200", "2022")`);
|
||||
setCellContent(model, "B2", `=ODOO.DEBIT("100 , 200", "2022")`);
|
||||
setCellContent(model, "B3", `=ODOO.BALANCE("100 , 200", "2022")`);
|
||||
|
||||
await waitForDataLoaded(model);
|
||||
expect(getCellValue(model, "A1")).toBe(26);
|
||||
expect(getCellValue(model, "A2")).toBe(142);
|
||||
expect(getCellValue(model, "A3")).toBe(116);
|
||||
|
||||
expect(getCellValue(model, "B1")).toBe(26);
|
||||
expect(getCellValue(model, "B2")).toBe(142);
|
||||
expect(getCellValue(model, "B3")).toBe(116);
|
||||
expect.verifySteps(["spreadsheet_fetch_debit_credit"]);
|
||||
});
|
||||
|
||||
test("Handle error evaluation", async () => {
|
||||
const { model } = await createModelWithDataSource({
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "spreadsheet_fetch_debit_credit") {
|
||||
throw makeServerError({ description: "a nasty error" });
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.CREDIT("100", "2022")`);
|
||||
await waitForDataLoaded(model);
|
||||
const cell = getEvaluatedCell(model, "A1");
|
||||
expect(cell.value).toBe("#ERROR");
|
||||
expect(cell.message).toBe("a nasty error");
|
||||
});
|
||||
|
||||
test("Server requests", async () => {
|
||||
const { model } = await createModelWithDataSource({
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "spreadsheet_fetch_debit_credit") {
|
||||
const blobs = args.args[0];
|
||||
for (const blob of blobs) {
|
||||
expect.step(blob);
|
||||
}
|
||||
return new Array(blobs.length).fill({ credit: 0, debit: 0 });
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.BALANCE("100", "2022")`);
|
||||
setCellContent(model, "A2", `=ODOO.CREDIT("100", "01/2022")`);
|
||||
setCellContent(model, "A3", `=ODOO.DEBIT("100","Q2/2022")`);
|
||||
setCellContent(model, "A4", `=ODOO.BALANCE("10", "2021")`);
|
||||
setCellContent(model, "A5", `=ODOO.CREDIT("10", "2022", -1)`); // same payload as A4: should only be called once
|
||||
setCellContent(model, "A6", `=ODOO.DEBIT("5", "2021", 0, 2)`);
|
||||
setCellContent(model, "A7", `=ODOO.DEBIT("5", "05/04/2021", 1)`);
|
||||
setCellContent(model, "A8", `=ODOO.BALANCE("5", "2022",,,FALSE)`);
|
||||
setCellContent(model, "A9", `=ODOO.BALANCE("100", "05/05/2022",,,TRUE)`);
|
||||
setCellContent(model, "A10", `=ODOO.BALANCE(33,2021,-2)`);
|
||||
await waitForDataLoaded(model);
|
||||
|
||||
expect.verifySteps([
|
||||
camelToSnakeObject({
|
||||
dateRange: parseAccountingDate({ value: "2022" }, locale),
|
||||
codes: ["100"],
|
||||
companyId: null,
|
||||
includeUnposted: false,
|
||||
}),
|
||||
camelToSnakeObject({
|
||||
dateRange: parseAccountingDate({ value: "01/2022" }, locale),
|
||||
codes: ["100"],
|
||||
companyId: null,
|
||||
includeUnposted: false,
|
||||
}),
|
||||
camelToSnakeObject({
|
||||
dateRange: parseAccountingDate({ value: "Q2/2022" }, locale),
|
||||
codes: ["100"],
|
||||
companyId: null,
|
||||
includeUnposted: false,
|
||||
}),
|
||||
camelToSnakeObject({
|
||||
dateRange: parseAccountingDate({ value: "2021" }, locale),
|
||||
codes: ["10"],
|
||||
companyId: null,
|
||||
includeUnposted: false,
|
||||
}),
|
||||
camelToSnakeObject({
|
||||
dateRange: parseAccountingDate({ value: "2021" }, locale),
|
||||
codes: ["5"],
|
||||
companyId: 2,
|
||||
includeUnposted: false,
|
||||
}),
|
||||
camelToSnakeObject({
|
||||
dateRange: parseAccountingDate({ value: "05/04/2022" }, locale),
|
||||
codes: ["5"],
|
||||
companyId: null,
|
||||
includeUnposted: false,
|
||||
}),
|
||||
camelToSnakeObject({
|
||||
dateRange: parseAccountingDate({ value: "2022" }, locale),
|
||||
codes: ["5"],
|
||||
companyId: null,
|
||||
includeUnposted: false,
|
||||
}),
|
||||
camelToSnakeObject({
|
||||
dateRange: parseAccountingDate({ value: "05/05/2022" }, locale),
|
||||
codes: ["100"],
|
||||
companyId: null,
|
||||
includeUnposted: true,
|
||||
}),
|
||||
camelToSnakeObject({
|
||||
dateRange: parseAccountingDate({ value: "2019" }, locale),
|
||||
codes: ["33"],
|
||||
companyId: null,
|
||||
includeUnposted: false,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
test("Server requests with multiple account codes", async () => {
|
||||
const { model } = await createModelWithDataSource({
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "spreadsheet_fetch_debit_credit") {
|
||||
expect.step("spreadsheet_fetch_debit_credit");
|
||||
const blobs = args.args[0];
|
||||
for (const blob of blobs) {
|
||||
expect.step(blob);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.BALANCE("100,200", "2022")`);
|
||||
setCellContent(model, "A2", `=ODOO.CREDIT("100,200", "2022")`);
|
||||
setCellContent(model, "A3", `=ODOO.DEBIT("100,200","2022")`);
|
||||
await waitForDataLoaded(model);
|
||||
|
||||
expect.verifySteps([
|
||||
"spreadsheet_fetch_debit_credit",
|
||||
camelToSnakeObject({
|
||||
dateRange: parseAccountingDate({ value: "2022" }, locale),
|
||||
codes: ["100", "200"],
|
||||
companyId: null,
|
||||
includeUnposted: false,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
test("account group formula as input to balance formula", async () => {
|
||||
const { model } = await createModelWithDataSource({
|
||||
serverData,
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "spreadsheet_fetch_debit_credit") {
|
||||
expect.step("spreadsheet_fetch_debit_credit");
|
||||
const blobs = args.args[0];
|
||||
for (const blob of blobs) {
|
||||
expect.step(blob);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.ACCOUNT.GROUP("income")`);
|
||||
setCellContent(model, "A2", `=ODOO.BALANCE(A1, 2022)`);
|
||||
expect(getCellValue(model, "A1")).toBe("Loading...");
|
||||
expect(getCellValue(model, "A2")).toBe("Loading...");
|
||||
await waitForDataLoaded(model);
|
||||
expect(getCellValue(model, "A1")).toBe("100104,200104");
|
||||
expect(getCellValue(model, "A2")).toBe(0);
|
||||
expect.verifySteps([
|
||||
"spreadsheet_fetch_debit_credit",
|
||||
camelToSnakeObject({
|
||||
dateRange: parseAccountingDate({ value: "2022" }, locale),
|
||||
codes: ["100104", "200104"],
|
||||
companyId: null,
|
||||
includeUnposted: false,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
test("two concurrent requests on different accounts", async () => {
|
||||
const { model } = await createModelWithDataSource({
|
||||
serverData,
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "spreadsheet_fetch_debit_credit") {
|
||||
expect.step("spreadsheet_fetch_debit_credit");
|
||||
const blobs = args.args[0];
|
||||
for (const blob of blobs) {
|
||||
expect.step(blob);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.ACCOUNT.GROUP("income")`);
|
||||
setCellContent(model, "A2", `=ODOO.BALANCE(A1, 2022)`); // batched only when A1 resolves
|
||||
setCellContent(model, "A3", `=ODOO.BALANCE("100", 2022)`); // batched directly
|
||||
expect(getCellValue(model, "A1")).toBe("Loading...");
|
||||
expect(getCellValue(model, "A2")).toBe("Loading...");
|
||||
expect(getCellValue(model, "A3")).toBe("Loading...");
|
||||
// A lot happens within the next tick.
|
||||
// Because cells are evaluated given their order in the sheet,
|
||||
// A1's request is done first, meaning it's also resolved first, which add A2 to the next batch (synchronously)
|
||||
// Only then A3 is resolved. => A2 is batched while A3 is pending
|
||||
await waitForDataLoaded(model);
|
||||
expect(getCellValue(model, "A1")).toBe("100104,200104");
|
||||
expect(getCellValue(model, "A2")).toBe(0);
|
||||
expect(getCellValue(model, "A3")).toBe(0);
|
||||
expect.verifySteps([
|
||||
"spreadsheet_fetch_debit_credit",
|
||||
camelToSnakeObject({
|
||||
dateRange: parseAccountingDate({ value: "2022" }, locale),
|
||||
codes: ["100"],
|
||||
companyId: null,
|
||||
includeUnposted: false,
|
||||
}),
|
||||
"spreadsheet_fetch_debit_credit",
|
||||
camelToSnakeObject({
|
||||
dateRange: parseAccountingDate({ value: "2022" }, locale),
|
||||
codes: ["100104", "200104"],
|
||||
companyId: null,
|
||||
includeUnposted: false,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
test("date with non-standard locale", async () => {
|
||||
const { model } = await createModelWithDataSource({
|
||||
mockRPC: async function (route, { method, args }) {
|
||||
if (method === "spreadsheet_fetch_debit_credit") {
|
||||
expect.step("spreadsheet_fetch_debit_credit");
|
||||
expect(args).toEqual([
|
||||
[
|
||||
{
|
||||
codes: ["100"],
|
||||
company_id: null,
|
||||
date_range: {
|
||||
range_type: "day",
|
||||
year: 2002,
|
||||
month: 2,
|
||||
day: 1,
|
||||
},
|
||||
include_unposted: false,
|
||||
},
|
||||
],
|
||||
]);
|
||||
return [{ debit: 142, credit: 26 }];
|
||||
}
|
||||
},
|
||||
});
|
||||
const myLocale = { ...locale, dateFormat: "d/mmm/yyyy" };
|
||||
model.dispatch("UPDATE_LOCALE", { locale: myLocale });
|
||||
setCellContent(model, "A1", "=DATE(2002, 2, 1)");
|
||||
setCellContent(model, "A2", "=ODOO.BALANCE(100, A1)");
|
||||
setCellContent(model, "A3", "=ODOO.CREDIT(100, A1)");
|
||||
setCellContent(model, "A4", "=ODOO.DEBIT(100, A1)");
|
||||
await waitForDataLoaded(model);
|
||||
expect(getEvaluatedCell(model, "A1").formattedValue).toBe("1/Feb/2002");
|
||||
expect(getCellValue(model, "A2")).toBe(116);
|
||||
expect(getCellValue(model, "A3")).toBe(26);
|
||||
expect(getCellValue(model, "A4")).toBe(142);
|
||||
expect.verifySteps(["spreadsheet_fetch_debit_credit"]);
|
||||
});
|
||||
|
||||
test("parseAccountingDate", () => {
|
||||
expect(parseAccountingDate({ value: "2022" }, locale)).toEqual({
|
||||
rangeType: "year",
|
||||
year: 2022,
|
||||
});
|
||||
expect(parseAccountingDate({ value: "11/10/2022" }, locale)).toEqual({
|
||||
rangeType: "day",
|
||||
year: 2022,
|
||||
month: 11,
|
||||
day: 10,
|
||||
});
|
||||
expect(parseAccountingDate({ value: "10/2022" }, locale)).toEqual({
|
||||
rangeType: "month",
|
||||
year: 2022,
|
||||
month: 10,
|
||||
});
|
||||
expect(parseAccountingDate({ value: "Q1/2022" }, locale)).toEqual({
|
||||
rangeType: "quarter",
|
||||
year: 2022,
|
||||
quarter: 1,
|
||||
});
|
||||
expect(parseAccountingDate({ value: "q4/2022" }, locale)).toEqual({
|
||||
rangeType: "quarter",
|
||||
year: 2022,
|
||||
quarter: 4,
|
||||
});
|
||||
// A number below 3000 is interpreted as a year.
|
||||
// It's interpreted as a regular spreadsheet date otherwise
|
||||
expect(parseAccountingDate({ value: "3005" }, locale)).toEqual({
|
||||
rangeType: "day",
|
||||
year: 1908,
|
||||
month: 3,
|
||||
day: 23,
|
||||
});
|
||||
});
|
||||
|
|
@ -1,381 +0,0 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { setCellContent } from "@spreadsheet/../tests/utils/commands";
|
||||
import {
|
||||
createModelWithDataSource,
|
||||
waitForDataSourcesLoaded,
|
||||
} from "@spreadsheet/../tests/utils/model";
|
||||
import { parseAccountingDate } from "../../src/accounting_functions";
|
||||
import { getCellValue, getCell } from "@spreadsheet/../tests/utils/getters";
|
||||
import { getAccountingData } from "../accounting_test_data";
|
||||
import { camelToSnakeObject } from "@spreadsheet/helpers/helpers";
|
||||
import { sprintf } from "@web/core/utils/strings";
|
||||
|
||||
let serverData;
|
||||
|
||||
function beforeEach() {
|
||||
serverData = getAccountingData();
|
||||
}
|
||||
|
||||
QUnit.module("spreadsheet_account > Accounting", { beforeEach }, () => {
|
||||
QUnit.module("Formulas");
|
||||
QUnit.test("Basic evaluation", async (assert) => {
|
||||
const model = await createModelWithDataSource({
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "spreadsheet_fetch_debit_credit") {
|
||||
assert.step("spreadsheet_fetch_debit_credit");
|
||||
return [{ debit: 42, credit: 16 }];
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.CREDIT("100", "2022")`);
|
||||
setCellContent(model, "A2", `=ODOO.DEBIT("100", "2022")`);
|
||||
setCellContent(model, "A3", `=ODOO.BALANCE("100", "2022")`);
|
||||
await waitForDataSourcesLoaded(model);
|
||||
assert.equal(getCellValue(model, "A1"), 16);
|
||||
assert.equal(getCellValue(model, "A2"), 42);
|
||||
assert.equal(getCellValue(model, "A3"), 26);
|
||||
assert.verifySteps(["spreadsheet_fetch_debit_credit"]);
|
||||
});
|
||||
|
||||
QUnit.test("Functions are correctly formatted", async (assert) => {
|
||||
const model = await createModelWithDataSource();
|
||||
setCellContent(model, "A1", `=ODOO.CREDIT("100", "2022")`);
|
||||
setCellContent(model, "A2", `=ODOO.DEBIT("100", "2022")`);
|
||||
setCellContent(model, "A3", `=ODOO.BALANCE("100", "2022")`);
|
||||
await waitForDataSourcesLoaded(model);
|
||||
assert.strictEqual(getCell(model, "A1").evaluated.format, "#,##0.00[$€]");
|
||||
assert.strictEqual(getCell(model, "A2").evaluated.format, "#,##0.00[$€]");
|
||||
assert.strictEqual(getCell(model, "A3").evaluated.format, "#,##0.00[$€]");
|
||||
});
|
||||
|
||||
QUnit.test("Functions with a wrong company id is correctly in error", async (assert) => {
|
||||
const model = await createModelWithDataSource({
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "get_company_currency_for_spreadsheet") {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.CREDIT("100", "2022", 0, 123456)`);
|
||||
await waitForDataSourcesLoaded(model);
|
||||
assert.strictEqual(
|
||||
getCell(model, "A1").evaluated.error.message,
|
||||
"Currency not available for this company."
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("formula with invalid date", async (assert) => {
|
||||
const model = await createModelWithDataSource();
|
||||
setCellContent(model, "A1", `=ODOO.CREDIT("100",)`);
|
||||
setCellContent(model, "A2", `=ODOO.DEBIT("100", 0)`);
|
||||
setCellContent(model, "A3", `=ODOO.BALANCE("100", -1)`);
|
||||
setCellContent(model, "A4", `=ODOO.BALANCE("100", "not a valid period")`);
|
||||
setCellContent(model, "A5", `=ODOO.BALANCE("100", 1900)`); // this should be ok
|
||||
setCellContent(model, "A6", `=ODOO.BALANCE("100", 1900, -1)`);
|
||||
setCellContent(model, "A7", `=ODOO.DEBIT("100", 1899)`);
|
||||
await waitForDataSourcesLoaded(model);
|
||||
const errorMessage = `'%s' is not a valid period. Supported formats are "21/12/2022", "Q1/2022", "12/2022", and "2022".`;
|
||||
assert.equal(getCell(model, "A1").evaluated.error.message, "0 is not a valid year.");
|
||||
assert.equal(getCell(model, "A2").evaluated.error.message, "0 is not a valid year.");
|
||||
assert.equal(getCell(model, "A3").evaluated.error.message, "-1 is not a valid year.");
|
||||
assert.equal(
|
||||
getCell(model, "A4").evaluated.error.message,
|
||||
sprintf(errorMessage, "not a valid period")
|
||||
);
|
||||
assert.equal(getCell(model, "A5").evaluated.value, 0);
|
||||
assert.equal(getCell(model, "A6").evaluated.error.message, "1899 is not a valid year.");
|
||||
assert.equal(getCell(model, "A7").evaluated.error.message, "1899 is not a valid year.");
|
||||
});
|
||||
|
||||
QUnit.test("Evaluation with multiple account codes", async (assert) => {
|
||||
const model = await createModelWithDataSource({
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "spreadsheet_fetch_debit_credit") {
|
||||
assert.step("spreadsheet_fetch_debit_credit");
|
||||
return [{ debit: 142, credit: 26 }];
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.CREDIT("100,200", "2022")`);
|
||||
setCellContent(model, "A2", `=ODOO.DEBIT("100,200", "2022")`);
|
||||
setCellContent(model, "A3", `=ODOO.BALANCE("100,200", "2022")`);
|
||||
|
||||
// with spaces
|
||||
setCellContent(model, "B1", `=ODOO.CREDIT("100 , 200", "2022")`);
|
||||
setCellContent(model, "B2", `=ODOO.DEBIT("100 , 200", "2022")`);
|
||||
setCellContent(model, "B3", `=ODOO.BALANCE("100 , 200", "2022")`);
|
||||
|
||||
await waitForDataSourcesLoaded(model);
|
||||
assert.equal(getCellValue(model, "A1"), 26);
|
||||
assert.equal(getCellValue(model, "A2"), 142);
|
||||
assert.equal(getCellValue(model, "A3"), 116);
|
||||
|
||||
assert.equal(getCellValue(model, "B1"), 26);
|
||||
assert.equal(getCellValue(model, "B2"), 142);
|
||||
assert.equal(getCellValue(model, "B3"), 116);
|
||||
assert.verifySteps(["spreadsheet_fetch_debit_credit"]);
|
||||
});
|
||||
|
||||
QUnit.test("Handle error evaluation", async (assert) => {
|
||||
const model = await createModelWithDataSource({
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "spreadsheet_fetch_debit_credit") {
|
||||
throw new Error("a nasty error");
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.CREDIT("100", "2022")`);
|
||||
await waitForDataSourcesLoaded(model);
|
||||
const cell = getCell(model, "A1");
|
||||
assert.equal(cell.evaluated.value, "#ERROR");
|
||||
assert.equal(cell.evaluated.error.message, "a nasty error");
|
||||
});
|
||||
|
||||
QUnit.test("Server requests", async (assert) => {
|
||||
const model = await createModelWithDataSource({
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "spreadsheet_fetch_debit_credit") {
|
||||
const blobs = args.args[0];
|
||||
for (const blob of blobs) {
|
||||
assert.step(JSON.stringify(blob));
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.BALANCE("100", "2022")`);
|
||||
setCellContent(model, "A2", `=ODOO.CREDIT("100", "01/2022")`);
|
||||
setCellContent(model, "A3", `=ODOO.DEBIT("100","Q2/2022")`);
|
||||
setCellContent(model, "A4", `=ODOO.BALANCE("10", "2021")`);
|
||||
setCellContent(model, "A5", `=ODOO.CREDIT("10", "2022", -1)`); // same payload as A4: should only be called once
|
||||
setCellContent(model, "A6", `=ODOO.DEBIT("5", "2021", 0, 2)`);
|
||||
setCellContent(model, "A7", `=ODOO.DEBIT("5", "05/04/2021", 1)`);
|
||||
setCellContent(model, "A8", `=ODOO.BALANCE("5", "2022",,,FALSE)`);
|
||||
setCellContent(model, "A9", `=ODOO.BALANCE("100", "05/05/2022",,,TRUE)`);
|
||||
setCellContent(model, "A10", `=ODOO.BALANCE(33,2021,-2)`);
|
||||
await waitForDataSourcesLoaded(model);
|
||||
|
||||
assert.verifySteps([
|
||||
JSON.stringify(
|
||||
camelToSnakeObject({
|
||||
dateRange: parseAccountingDate("2022"),
|
||||
codes: ["100"],
|
||||
companyId: null,
|
||||
includeUnposted: false,
|
||||
})
|
||||
),
|
||||
JSON.stringify(
|
||||
camelToSnakeObject({
|
||||
dateRange: parseAccountingDate("01/2022"),
|
||||
codes: ["100"],
|
||||
companyId: null,
|
||||
includeUnposted: false,
|
||||
})
|
||||
),
|
||||
JSON.stringify(
|
||||
camelToSnakeObject({
|
||||
dateRange: parseAccountingDate("Q2/2022"),
|
||||
codes: ["100"],
|
||||
companyId: null,
|
||||
includeUnposted: false,
|
||||
})
|
||||
),
|
||||
JSON.stringify(
|
||||
camelToSnakeObject({
|
||||
dateRange: parseAccountingDate("2021"),
|
||||
codes: ["10"],
|
||||
companyId: null,
|
||||
includeUnposted: false,
|
||||
})
|
||||
),
|
||||
JSON.stringify(
|
||||
camelToSnakeObject({
|
||||
dateRange: parseAccountingDate("2021"),
|
||||
codes: ["5"],
|
||||
companyId: 2,
|
||||
includeUnposted: false,
|
||||
})
|
||||
),
|
||||
JSON.stringify(
|
||||
camelToSnakeObject({
|
||||
dateRange: parseAccountingDate("05/04/2022"),
|
||||
codes: ["5"],
|
||||
companyId: null,
|
||||
includeUnposted: false,
|
||||
})
|
||||
),
|
||||
JSON.stringify(
|
||||
camelToSnakeObject({
|
||||
dateRange: parseAccountingDate("2022"),
|
||||
codes: ["5"],
|
||||
companyId: null,
|
||||
includeUnposted: false,
|
||||
})
|
||||
),
|
||||
JSON.stringify(
|
||||
camelToSnakeObject({
|
||||
dateRange: parseAccountingDate("05/05/2022"),
|
||||
codes: ["100"],
|
||||
companyId: null,
|
||||
includeUnposted: true,
|
||||
})
|
||||
),
|
||||
JSON.stringify(
|
||||
camelToSnakeObject({
|
||||
dateRange: parseAccountingDate("2019"),
|
||||
codes: ["33"],
|
||||
companyId: null,
|
||||
includeUnposted: false,
|
||||
})
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("Server requests with multiple account codes", async (assert) => {
|
||||
const model = await createModelWithDataSource({
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "spreadsheet_fetch_debit_credit") {
|
||||
assert.step("spreadsheet_fetch_debit_credit");
|
||||
const blobs = args.args[0];
|
||||
for (const blob of blobs) {
|
||||
assert.step(JSON.stringify(blob));
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.BALANCE("100,200", "2022")`);
|
||||
setCellContent(model, "A2", `=ODOO.CREDIT("100,200", "2022")`);
|
||||
setCellContent(model, "A3", `=ODOO.DEBIT("100,200","2022")`);
|
||||
await waitForDataSourcesLoaded(model);
|
||||
|
||||
assert.verifySteps([
|
||||
"spreadsheet_fetch_debit_credit",
|
||||
JSON.stringify(
|
||||
camelToSnakeObject({
|
||||
dateRange: parseAccountingDate("2022"),
|
||||
codes: ["100", "200"],
|
||||
companyId: null,
|
||||
includeUnposted: false,
|
||||
})
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("account group formula as input to balance formula", async (assert) => {
|
||||
const model = await createModelWithDataSource({
|
||||
serverData,
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "spreadsheet_fetch_debit_credit") {
|
||||
assert.step("spreadsheet_fetch_debit_credit");
|
||||
const blobs = args.args[0];
|
||||
for (const blob of blobs) {
|
||||
assert.step(JSON.stringify(blob));
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.ACCOUNT.GROUP("income")`);
|
||||
setCellContent(model, "A2", `=ODOO.BALANCE(A1, 2022)`);
|
||||
assert.equal(getCellValue(model, "A1"), "Loading...");
|
||||
assert.equal(getCellValue(model, "A2"), "Loading...");
|
||||
await waitForDataSourcesLoaded(model);
|
||||
assert.equal(getCellValue(model, "A1"), "100104,200104");
|
||||
assert.equal(getCellValue(model, "A2"), 0);
|
||||
assert.verifySteps([
|
||||
"spreadsheet_fetch_debit_credit",
|
||||
JSON.stringify(
|
||||
camelToSnakeObject({
|
||||
dateRange: parseAccountingDate("2022"),
|
||||
codes: ["100104", "200104"],
|
||||
companyId: null,
|
||||
includeUnposted: false,
|
||||
})
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("two concurrent requests on different accounts", async (assert) => {
|
||||
const model = await createModelWithDataSource({
|
||||
serverData,
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "spreadsheet_fetch_debit_credit") {
|
||||
assert.step("spreadsheet_fetch_debit_credit");
|
||||
const blobs = args.args[0];
|
||||
for (const blob of blobs) {
|
||||
assert.step(JSON.stringify(blob));
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.ACCOUNT.GROUP("income")`);
|
||||
setCellContent(model, "A2", `=ODOO.BALANCE(A1, 2022)`); // batched only when A1 resolves
|
||||
setCellContent(model, "A3", `=ODOO.BALANCE("100", 2022)`); // batched directly
|
||||
assert.equal(getCellValue(model, "A1"), "Loading...");
|
||||
assert.equal(getCellValue(model, "A2"), "Loading...");
|
||||
assert.equal(getCellValue(model, "A3"), "Loading...");
|
||||
// A lot happens within the next tick.
|
||||
// Because cells are evaluated given their order in the sheet,
|
||||
// A1's request is done first, meaning it's also resolved first, which add A2 to the next batch (synchronously)
|
||||
// Only then A3 is resolved. => A2 is batched while A3 is pending
|
||||
await waitForDataSourcesLoaded(model);
|
||||
assert.equal(getCellValue(model, "A1"), "100104,200104");
|
||||
assert.equal(getCellValue(model, "A2"), 0);
|
||||
assert.equal(getCellValue(model, "A3"), 0);
|
||||
assert.verifySteps([
|
||||
"spreadsheet_fetch_debit_credit",
|
||||
JSON.stringify(
|
||||
camelToSnakeObject({
|
||||
dateRange: parseAccountingDate("2022"),
|
||||
codes: ["100"],
|
||||
companyId: null,
|
||||
includeUnposted: false,
|
||||
})
|
||||
),
|
||||
"spreadsheet_fetch_debit_credit",
|
||||
JSON.stringify(
|
||||
camelToSnakeObject({
|
||||
dateRange: parseAccountingDate("2022"),
|
||||
codes: ["100104", "200104"],
|
||||
companyId: null,
|
||||
includeUnposted: false,
|
||||
})
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test("parseAccountingDate", (assert) => {
|
||||
assert.deepEqual(parseAccountingDate("2022"), {
|
||||
rangeType: "year",
|
||||
year: 2022,
|
||||
});
|
||||
assert.deepEqual(parseAccountingDate("11/10/2022"), {
|
||||
rangeType: "day",
|
||||
year: 2022,
|
||||
month: 11,
|
||||
day: 10,
|
||||
});
|
||||
assert.deepEqual(parseAccountingDate("10/2022"), {
|
||||
rangeType: "month",
|
||||
year: 2022,
|
||||
month: 10,
|
||||
});
|
||||
assert.deepEqual(parseAccountingDate("Q1/2022"), {
|
||||
rangeType: "quarter",
|
||||
year: 2022,
|
||||
quarter: 1,
|
||||
});
|
||||
assert.deepEqual(parseAccountingDate("q4/2022"), {
|
||||
rangeType: "quarter",
|
||||
year: 2022,
|
||||
quarter: 4,
|
||||
});
|
||||
// A number below 3000 is interpreted as a year.
|
||||
// It's interpreted as a regular spreadsheet date otherwise
|
||||
assert.deepEqual(parseAccountingDate("3005"), {
|
||||
rangeType: "day",
|
||||
year: 1908,
|
||||
month: 3,
|
||||
day: 23,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { setCellContent } from "@spreadsheet/../tests/helpers/commands";
|
||||
import { getCellValue, getEvaluatedCell } from "@spreadsheet/../tests/helpers/getters";
|
||||
import { createModelWithDataSource } from "@spreadsheet/../tests/helpers/model";
|
||||
import { defineSpreadsheetAccountModels } from "@spreadsheet_account/../tests/accounting_test_data";
|
||||
import { waitForDataLoaded } from "@spreadsheet/helpers/model";
|
||||
|
||||
describe.current.tags("headless");
|
||||
defineSpreadsheetAccountModels();
|
||||
|
||||
test("Basic evaluation", async () => {
|
||||
const { model } = await createModelWithDataSource({
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "spreadsheet_fetch_balance_tag") {
|
||||
expect.step("spreadsheet_fetch_balance_tag");
|
||||
expect(args.args[0]).toEqual([
|
||||
{
|
||||
account_tag_ids: [10, 14],
|
||||
date_range: {
|
||||
range_type: "year",
|
||||
year: 2025,
|
||||
},
|
||||
company_id: 0,
|
||||
include_unposted: false,
|
||||
},
|
||||
]);
|
||||
return [{ balance: 100.0 }];
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.BALANCE.TAG("10, 14", 2025)`);
|
||||
await waitForDataLoaded(model);
|
||||
expect.verifySteps(["spreadsheet_fetch_balance_tag"]);
|
||||
expect(getCellValue(model, "A1")).toBe(100.0);
|
||||
});
|
||||
|
||||
test("with wrong date format", async () => {
|
||||
const { model } = await createModelWithDataSource();
|
||||
setCellContent(model, "A1", `=ODOO.BALANCE.TAG("10, 14", "This is not a valid date")`);
|
||||
await waitForDataLoaded(model);
|
||||
expect(getEvaluatedCell(model, "A1").message).toBe(
|
||||
"'This is not a valid date' is not a valid period. Supported formats are \"21/12/2022\", \"Q1/2022\", \"12/2022\", and \"2022\"."
|
||||
);
|
||||
});
|
||||
|
||||
test("with no date", async () => {
|
||||
const { model } = await createModelWithDataSource({
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "spreadsheet_fetch_balance_tag") {
|
||||
expect.step("spreadsheet_fetch_balance_tag");
|
||||
expect(args.args[0]).toEqual([
|
||||
{
|
||||
account_tag_ids: [10, 14],
|
||||
date_range: {
|
||||
range_type: "year",
|
||||
year: new Date().getFullYear(),
|
||||
},
|
||||
company_id: 0,
|
||||
include_unposted: false,
|
||||
},
|
||||
]);
|
||||
return [{ balance: 100.0 }];
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.BALANCE.TAG("10, 14")`);
|
||||
await waitForDataLoaded(model);
|
||||
expect.verifySteps(["spreadsheet_fetch_balance_tag"]);
|
||||
expect(getCellValue(model, "A1")).toBe(100.0);
|
||||
});
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
import { defineSpreadsheetModels } from "@spreadsheet/../tests/helpers/data";
|
||||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { setCellContent } from "@spreadsheet/../tests/helpers/commands";
|
||||
import { createModelWithDataSource } from "@spreadsheet/../tests/helpers/model";
|
||||
import { getEvaluatedCell } from "@spreadsheet/../tests/helpers/getters";
|
||||
import "@spreadsheet_account/index";
|
||||
|
||||
import * as spreadsheet from "@odoo/o-spreadsheet";
|
||||
import { waitForDataLoaded } from "@spreadsheet/helpers/model";
|
||||
|
||||
describe.current.tags("headless");
|
||||
defineSpreadsheetModels();
|
||||
|
||||
const { DEFAULT_LOCALE } = spreadsheet.constants;
|
||||
|
||||
test("Basic evaluation", async () => {
|
||||
const { model } = await createModelWithDataSource({
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "get_fiscal_dates") {
|
||||
expect.step("get_fiscal_dates");
|
||||
expect(args.args).toEqual([
|
||||
[
|
||||
{
|
||||
date: "2020-11-11",
|
||||
company_id: null,
|
||||
},
|
||||
],
|
||||
]);
|
||||
return [{ start: "2020-01-01", end: "2020-12-31" }];
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.FISCALYEAR.START("11/11/2020")`);
|
||||
setCellContent(model, "A2", `=ODOO.FISCALYEAR.END("11/11/2020")`);
|
||||
await waitForDataLoaded(model);
|
||||
expect.verifySteps(["get_fiscal_dates"]);
|
||||
expect(getEvaluatedCell(model, "A1").formattedValue).toBe("1/1/2020");
|
||||
expect(getEvaluatedCell(model, "A2").formattedValue).toBe("12/31/2020");
|
||||
});
|
||||
|
||||
test("with a given company id", async () => {
|
||||
const { model } = await createModelWithDataSource({
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "get_fiscal_dates") {
|
||||
expect.step("get_fiscal_dates");
|
||||
expect(args.args).toEqual([
|
||||
[
|
||||
{
|
||||
date: "2020-11-11",
|
||||
company_id: 1,
|
||||
},
|
||||
],
|
||||
]);
|
||||
return [{ start: "2020-01-01", end: "2020-12-31" }];
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.FISCALYEAR.START("11/11/2020", 1)`);
|
||||
setCellContent(model, "A2", `=ODOO.FISCALYEAR.END("11/11/2020", 1)`);
|
||||
await waitForDataLoaded(model);
|
||||
expect.verifySteps(["get_fiscal_dates"]);
|
||||
expect(getEvaluatedCell(model, "A1").formattedValue).toBe("1/1/2020");
|
||||
expect(getEvaluatedCell(model, "A2").formattedValue).toBe("12/31/2020");
|
||||
});
|
||||
|
||||
test("with a wrong company id", async () => {
|
||||
const { model } = await createModelWithDataSource({
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "get_fiscal_dates") {
|
||||
expect.step("get_fiscal_dates");
|
||||
expect(args.args).toEqual([
|
||||
[
|
||||
{
|
||||
date: "2020-11-11",
|
||||
company_id: 100,
|
||||
},
|
||||
],
|
||||
]);
|
||||
return [false];
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.FISCALYEAR.START("11/11/2020", 100)`);
|
||||
setCellContent(model, "A2", `=ODOO.FISCALYEAR.END("11/11/2020", 100)`);
|
||||
await waitForDataLoaded(model);
|
||||
expect.verifySteps(["get_fiscal_dates"]);
|
||||
expect(getEvaluatedCell(model, "A1").message).toBe(
|
||||
"The company fiscal year could not be found."
|
||||
);
|
||||
expect(getEvaluatedCell(model, "A2").message).toBe(
|
||||
"The company fiscal year could not be found."
|
||||
);
|
||||
});
|
||||
|
||||
test("with wrong input arguments", async () => {
|
||||
const { model } = await createModelWithDataSource();
|
||||
setCellContent(model, "A1", `=ODOO.FISCALYEAR.START("not a number")`);
|
||||
setCellContent(model, "A2", `=ODOO.FISCALYEAR.END("11/11/2020", "not a number")`);
|
||||
expect(getEvaluatedCell(model, "A1").message).toBe(
|
||||
"The function ODOO.FISCALYEAR.START expects a number value, but 'not a number' is a string, and cannot be coerced to a number."
|
||||
);
|
||||
expect(getEvaluatedCell(model, "A2").message).toBe(
|
||||
"The function ODOO.FISCALYEAR.END expects a number value, but 'not a number' is a string, and cannot be coerced to a number."
|
||||
);
|
||||
});
|
||||
|
||||
test("Date format is locale dependant", async () => {
|
||||
const { model } = await createModelWithDataSource({
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "get_fiscal_dates") {
|
||||
return [{ start: "2020-01-01", end: "2020-12-31" }];
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.FISCALYEAR.START("11/11/2020", 1)`);
|
||||
setCellContent(model, "A2", `=ODOO.FISCALYEAR.END("11/11/2020", 1)`);
|
||||
await waitForDataLoaded(model);
|
||||
|
||||
expect(getEvaluatedCell(model, "A1").format).toBe("m/d/yyyy");
|
||||
expect(getEvaluatedCell(model, "A2").format).toBe("m/d/yyyy");
|
||||
|
||||
model.dispatch("UPDATE_LOCALE", { locale: { ...DEFAULT_LOCALE, dateFormat: "d/m/yyyy" } });
|
||||
|
||||
expect(getEvaluatedCell(model, "A1").format).toBe("d/m/yyyy");
|
||||
expect(getEvaluatedCell(model, "A2").format).toBe("d/m/yyyy");
|
||||
});
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
/** @odoo-module */
|
||||
import { setCellContent } from "@spreadsheet/../tests/utils/commands";
|
||||
import {
|
||||
createModelWithDataSource,
|
||||
waitForDataSourcesLoaded,
|
||||
} from "@spreadsheet/../tests/utils/model";
|
||||
import { getCell } from "@spreadsheet/../tests/utils/getters";
|
||||
import "@spreadsheet_account/index";
|
||||
|
||||
QUnit.module("spreadsheet_account > fiscal year", {}, () => {
|
||||
QUnit.test("Basic evaluation", async (assert) => {
|
||||
const model = await createModelWithDataSource({
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "get_fiscal_dates") {
|
||||
assert.step("get_fiscal_dates");
|
||||
assert.deepEqual(args.args, [
|
||||
[
|
||||
{
|
||||
date: "2020-11-11",
|
||||
company_id: null,
|
||||
},
|
||||
],
|
||||
]);
|
||||
return [{ start: "2020-01-01", end: "2020-12-31" }];
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.FISCALYEAR.START("11/11/2020")`);
|
||||
setCellContent(model, "A2", `=ODOO.FISCALYEAR.END("11/11/2020")`);
|
||||
await waitForDataSourcesLoaded(model);
|
||||
assert.verifySteps(["get_fiscal_dates"]);
|
||||
assert.equal(getCell(model, "A1").formattedValue, "1/1/2020");
|
||||
assert.equal(getCell(model, "A2").formattedValue, "12/31/2020");
|
||||
});
|
||||
|
||||
QUnit.test("with a given company id", async (assert) => {
|
||||
const model = await createModelWithDataSource({
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "get_fiscal_dates") {
|
||||
assert.step("get_fiscal_dates");
|
||||
assert.deepEqual(args.args, [
|
||||
[
|
||||
{
|
||||
date: "2020-11-11",
|
||||
company_id: 1,
|
||||
},
|
||||
],
|
||||
]);
|
||||
return [{ start: "2020-01-01", end: "2020-12-31" }];
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.FISCALYEAR.START("11/11/2020", 1)`);
|
||||
setCellContent(model, "A2", `=ODOO.FISCALYEAR.END("11/11/2020", 1)`);
|
||||
await waitForDataSourcesLoaded(model);
|
||||
assert.verifySteps(["get_fiscal_dates"]);
|
||||
assert.equal(getCell(model, "A1").formattedValue, "1/1/2020");
|
||||
assert.equal(getCell(model, "A2").formattedValue, "12/31/2020");
|
||||
});
|
||||
|
||||
QUnit.test("with a wrong company id", async (assert) => {
|
||||
const model = await createModelWithDataSource({
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "get_fiscal_dates") {
|
||||
assert.step("get_fiscal_dates");
|
||||
assert.deepEqual(args.args, [
|
||||
[
|
||||
{
|
||||
date: "2020-11-11",
|
||||
company_id: 100,
|
||||
},
|
||||
],
|
||||
]);
|
||||
return [false];
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.FISCALYEAR.START("11/11/2020", 100)`);
|
||||
setCellContent(model, "A2", `=ODOO.FISCALYEAR.END("11/11/2020", 100)`);
|
||||
await waitForDataSourcesLoaded(model);
|
||||
assert.verifySteps(["get_fiscal_dates"]);
|
||||
assert.equal(
|
||||
getCell(model, "A1").evaluated.error.message,
|
||||
"The company fiscal year could not be found."
|
||||
);
|
||||
assert.equal(
|
||||
getCell(model, "A2").evaluated.error.message,
|
||||
"The company fiscal year could not be found."
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("with wrong input arguments", async (assert) => {
|
||||
const model = await createModelWithDataSource();
|
||||
setCellContent(model, "A1", `=ODOO.FISCALYEAR.START("not a number")`);
|
||||
setCellContent(model, "A2", `=ODOO.FISCALYEAR.END("11/11/2020", "not a number")`);
|
||||
assert.equal(
|
||||
getCell(model, "A1").evaluated.error.message,
|
||||
"The function ODOO.FISCALYEAR.START expects a number value, but 'not a number' is a string, and cannot be coerced to a number."
|
||||
);
|
||||
assert.equal(
|
||||
getCell(model, "A2").evaluated.error.message,
|
||||
"The function ODOO.FISCALYEAR.END expects a number value, but 'not a number' is a string, and cannot be coerced to a number."
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { setCellContent } from "@spreadsheet/../tests/helpers/commands";
|
||||
import { getCellValue, getEvaluatedCell } from "@spreadsheet/../tests/helpers/getters";
|
||||
import { createModelWithDataSource } from "@spreadsheet/../tests/helpers/model";
|
||||
import { defineSpreadsheetAccountModels } from "@spreadsheet_account/../tests/accounting_test_data";
|
||||
import { waitForDataLoaded } from "@spreadsheet/helpers/model";
|
||||
|
||||
describe.current.tags("headless");
|
||||
defineSpreadsheetAccountModels();
|
||||
|
||||
test("Basic evaluation", async () => {
|
||||
const { model } = await createModelWithDataSource({
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "spreadsheet_fetch_partner_balance") {
|
||||
expect.step("spreadsheet_fetch_partner_balance");
|
||||
expect(args.args[0]).toEqual([
|
||||
{
|
||||
partner_ids: [14, 16],
|
||||
codes: ["112"],
|
||||
date_range: {
|
||||
range_type: "year",
|
||||
year: 2023,
|
||||
},
|
||||
company_id: 0,
|
||||
include_unposted: false,
|
||||
},
|
||||
]);
|
||||
return [{ balance: 26 }];
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.PARTNER.BALANCE("14, 16", "112", 2023)`);
|
||||
await waitForDataLoaded(model);
|
||||
expect.verifySteps(["spreadsheet_fetch_partner_balance"]);
|
||||
expect(getCellValue(model, "A1")).toBe(26);
|
||||
});
|
||||
|
||||
test("with wrong date format", async () => {
|
||||
const { model } = await createModelWithDataSource();
|
||||
setCellContent(
|
||||
model,
|
||||
"A1",
|
||||
`=ODOO.PARTNER.BALANCE("14, 16", "112", "This is not a valid date")`
|
||||
);
|
||||
await waitForDataLoaded(model);
|
||||
expect(getEvaluatedCell(model, "A1").message).toBe(
|
||||
'\'This is not a valid date\' is not a valid period. Supported formats are "21/12/2022", "Q1/2022", "12/2022", and "2022".'
|
||||
);
|
||||
});
|
||||
|
||||
test("with no date", async () => {
|
||||
const { model } = await createModelWithDataSource({
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "spreadsheet_fetch_partner_balance") {
|
||||
expect.step("spreadsheet_fetch_partner_balance");
|
||||
expect(args.args[0]).toEqual([
|
||||
{
|
||||
partner_ids: [14, 16],
|
||||
codes: ["112"],
|
||||
date_range: {
|
||||
range_type: "year",
|
||||
year: new Date().getFullYear(),
|
||||
},
|
||||
company_id: 0,
|
||||
include_unposted: false,
|
||||
},
|
||||
]);
|
||||
return [{ balance: 26 }];
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.PARTNER.BALANCE("14, 16", "112")`);
|
||||
await waitForDataLoaded(model);
|
||||
expect.verifySteps(["spreadsheet_fetch_partner_balance"]);
|
||||
expect(getCellValue(model, "A1")).toBe(26);
|
||||
});
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { setCellContent } from "@spreadsheet/../tests/helpers/commands";
|
||||
import { getCellValue, getEvaluatedCell } from "@spreadsheet/../tests/helpers/getters";
|
||||
import { createModelWithDataSource } from "@spreadsheet/../tests/helpers/model";
|
||||
import { defineSpreadsheetAccountModels } from "@spreadsheet_account/../tests/accounting_test_data";
|
||||
import { waitForDataLoaded } from "@spreadsheet/helpers/model";
|
||||
|
||||
describe.current.tags("headless");
|
||||
defineSpreadsheetAccountModels();
|
||||
|
||||
test("Basic evaluation", async () => {
|
||||
const { model } = await createModelWithDataSource({
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "spreadsheet_fetch_residual_amount") {
|
||||
expect.step("spreadsheet_fetch_residual_amount");
|
||||
expect(args.args[0]).toEqual([
|
||||
{
|
||||
codes: ["112"],
|
||||
date_range: {
|
||||
range_type: "year",
|
||||
year: 2023,
|
||||
},
|
||||
company_id: 0,
|
||||
include_unposted: false,
|
||||
},
|
||||
]);
|
||||
return [{ amount_residual: 111.11 }];
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.RESIDUAL("112", 2023)`);
|
||||
await waitForDataLoaded(model);
|
||||
expect.verifySteps(["spreadsheet_fetch_residual_amount"]);
|
||||
expect(getCellValue(model, "A1")).toBe(111.11);
|
||||
});
|
||||
|
||||
test("with wrong date format", async () => {
|
||||
const { model } = await createModelWithDataSource();
|
||||
setCellContent(model, "A1", `=ODOO.RESIDUAL("112", "This is not a valid date")`);
|
||||
await waitForDataLoaded(model);
|
||||
expect(getEvaluatedCell(model, "A1").message).toBe(
|
||||
'\'This is not a valid date\' is not a valid period. Supported formats are "21/12/2022", "Q1/2022", "12/2022", and "2022".'
|
||||
);
|
||||
});
|
||||
|
||||
test("with no date", async () => {
|
||||
const { model } = await createModelWithDataSource({
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "spreadsheet_fetch_residual_amount") {
|
||||
expect.step("spreadsheet_fetch_residual_amount");
|
||||
expect(args.args[0]).toEqual([
|
||||
{
|
||||
codes: ["112"],
|
||||
date_range: {
|
||||
range_type: "year",
|
||||
year: new Date().getFullYear(),
|
||||
},
|
||||
company_id: 0,
|
||||
include_unposted: false,
|
||||
},
|
||||
]);
|
||||
return [{ amount_residual: 111.11 }];
|
||||
}
|
||||
},
|
||||
});
|
||||
setCellContent(model, "A1", `=ODOO.RESIDUAL("112")`);
|
||||
await waitForDataLoaded(model);
|
||||
expect.verifySteps(["spreadsheet_fetch_residual_amount"]);
|
||||
expect(getCellValue(model, "A1")).toBe(111.11);
|
||||
});
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { registries, constants } from "@odoo/o-spreadsheet";
|
||||
import { selectCell, setCellContent } from "@spreadsheet/../tests/helpers/commands";
|
||||
import { createModelWithDataSource } from "@spreadsheet/../tests/helpers/model";
|
||||
import { doMenuAction } from "@spreadsheet/../tests/helpers/ui";
|
||||
import { waitForDataLoaded } from "@spreadsheet/helpers/model";
|
||||
import {
|
||||
defineSpreadsheetAccountModels,
|
||||
getAccountingData,
|
||||
} from "@spreadsheet_account/../tests/accounting_test_data";
|
||||
import { mockService } from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("headless");
|
||||
defineSpreadsheetAccountModels();
|
||||
|
||||
const { cellMenuRegistry } = registries;
|
||||
const { DEFAULT_LOCALE } = constants;
|
||||
|
||||
const serverData = getAccountingData();
|
||||
|
||||
test("Create drill down domain", async () => {
|
||||
const drillDownAction = {
|
||||
type: "ir.actions.act_window",
|
||||
res_model: "account.move.line",
|
||||
view_mode: "list",
|
||||
views: [[false, "list"]],
|
||||
target: "current",
|
||||
domain: [["account_id", "in", [1, 2]]],
|
||||
name: "my awesome action",
|
||||
};
|
||||
const fakeActionService = {
|
||||
doAction: async (action, options) => {
|
||||
expect.step("drill down action");
|
||||
expect(action).toEqual(drillDownAction);
|
||||
expect(options).toEqual({ newWindow: undefined });
|
||||
return true;
|
||||
},
|
||||
};
|
||||
mockService("action", fakeActionService);
|
||||
|
||||
const { model } = await createModelWithDataSource({
|
||||
serverData,
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "spreadsheet_move_line_action") {
|
||||
expect(args.args).toEqual([
|
||||
{
|
||||
codes: ["100"],
|
||||
company_id: null,
|
||||
include_unposted: false,
|
||||
date_range: {
|
||||
range_type: "year",
|
||||
year: 2020,
|
||||
},
|
||||
},
|
||||
]);
|
||||
return drillDownAction;
|
||||
}
|
||||
},
|
||||
});
|
||||
const env = model.config.custom.env;
|
||||
env.model = model;
|
||||
setCellContent(model, "A1", `=ODOO.BALANCE("100", 2020)`);
|
||||
setCellContent(model, "A2", `=ODOO.BALANCE("100", 0)`);
|
||||
setCellContent(model, "A3", `=ODOO.BALANCE("100", 2020, , , FALSE)`);
|
||||
setCellContent(model, "A4", `=ODOO.BALANCE("100", 2020, , , )`);
|
||||
// Does not affect non formula cells
|
||||
setCellContent(model, "A5", `5`);
|
||||
await waitForDataLoaded(model);
|
||||
selectCell(model, "A1");
|
||||
const root = cellMenuRegistry
|
||||
.getMenuItems()
|
||||
.find((item) => item.id === "move_lines_see_records");
|
||||
expect(root.isVisible(env)).toBe(true);
|
||||
await root.execute(env);
|
||||
expect.verifySteps(["drill down action"]);
|
||||
selectCell(model, "A2");
|
||||
expect(root.isVisible(env)).toBe(false);
|
||||
selectCell(model, "A3");
|
||||
expect(root.isVisible(env)).toBe(true);
|
||||
await root.execute(env);
|
||||
expect.verifySteps(["drill down action"]);
|
||||
selectCell(model, "A4");
|
||||
expect(root.isVisible(env)).toBe(true);
|
||||
await root.execute(env);
|
||||
expect.verifySteps(["drill down action"]);
|
||||
selectCell(model, "A5");
|
||||
expect(root.isVisible(env)).toBe(false);
|
||||
});
|
||||
|
||||
test("Create drill down domain when month date is a reference", async () => {
|
||||
mockService("action", { doAction: () => {} });
|
||||
const { model } = await createModelWithDataSource({
|
||||
serverData,
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "spreadsheet_move_line_action") {
|
||||
expect.step("spreadsheet_move_line_action");
|
||||
expect(args.args).toEqual([
|
||||
{
|
||||
codes: ["100"],
|
||||
company_id: null,
|
||||
include_unposted: false,
|
||||
date_range: {
|
||||
month: 2,
|
||||
range_type: "month",
|
||||
year: 2024,
|
||||
},
|
||||
},
|
||||
]);
|
||||
return {};
|
||||
}
|
||||
},
|
||||
});
|
||||
const env = model.config.custom.env;
|
||||
env.model = model;
|
||||
setCellContent(model, "A1", "02/2024");
|
||||
setCellContent(model, "A2", '=ODOO.BALANCE("100", A1)');
|
||||
await waitForDataLoaded(model);
|
||||
selectCell(model, "A2");
|
||||
await doMenuAction(cellMenuRegistry, ["move_lines_see_records"], env);
|
||||
expect.verifySteps(["spreadsheet_move_line_action"]);
|
||||
});
|
||||
|
||||
test("Create drill down domain when date uses a non-standard locale", async () => {
|
||||
mockService("action", { doAction: () => {} });
|
||||
const { model } = await createModelWithDataSource({
|
||||
serverData,
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "spreadsheet_move_line_action") {
|
||||
expect.step("spreadsheet_move_line_action");
|
||||
expect(args.args).toEqual([
|
||||
{
|
||||
codes: ["100"],
|
||||
company_id: null,
|
||||
include_unposted: false,
|
||||
date_range: {
|
||||
range_type: "day",
|
||||
year: 2002,
|
||||
month: 2,
|
||||
day: 1,
|
||||
},
|
||||
},
|
||||
]);
|
||||
return {};
|
||||
}
|
||||
},
|
||||
});
|
||||
const env = model.config.custom.env;
|
||||
env.model = model;
|
||||
const myLocale = { ...DEFAULT_LOCALE, dateFormat: "d/mmm/yyyy" };
|
||||
model.dispatch("UPDATE_LOCALE", { locale: myLocale });
|
||||
setCellContent(model, "A1", '=ODOO.BALANCE("100", DATE(2002, 2, 1))');
|
||||
await waitForDataLoaded(model);
|
||||
await doMenuAction(cellMenuRegistry, ["move_lines_see_records"], env);
|
||||
expect.verifySteps(["spreadsheet_move_line_action"]);
|
||||
});
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { selectCell, setCellContent } from "@spreadsheet/../tests/utils/commands";
|
||||
import spreadsheet from "@spreadsheet/o_spreadsheet/o_spreadsheet_extended";
|
||||
import { getAccountingData } from "../accounting_test_data";
|
||||
import {
|
||||
createModelWithDataSource,
|
||||
waitForDataSourcesLoaded,
|
||||
} from "@spreadsheet/../tests/utils/model";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
const { cellMenuRegistry } = spreadsheet.registries;
|
||||
|
||||
let serverData;
|
||||
|
||||
function beforeEach() {
|
||||
serverData = getAccountingData();
|
||||
}
|
||||
|
||||
QUnit.module("spreadsheet_account > Accounting Drill down", { beforeEach }, () => {
|
||||
QUnit.test("Create drill down domain", async (assert) => {
|
||||
const drillDownAction = {
|
||||
type: "ir.actions.act_window",
|
||||
res_model: "account.move.line",
|
||||
view_mode: "list",
|
||||
views: [[false, "list"]],
|
||||
target: "current",
|
||||
domain: [["account_id", "in", [1, 2]]],
|
||||
name: "my awesome action",
|
||||
};
|
||||
const fakeActionService = {
|
||||
name: "action",
|
||||
start() {
|
||||
return {
|
||||
async doAction(action, options) {
|
||||
assert.step("drill down action");
|
||||
assert.deepEqual(action, drillDownAction);
|
||||
assert.equal(options, undefined);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
registry.category("services").add("action", fakeActionService, { force: true });
|
||||
|
||||
const model = await createModelWithDataSource({
|
||||
serverData,
|
||||
mockRPC: async function (route, args) {
|
||||
if (args.method === "spreadsheet_move_line_action") {
|
||||
assert.deepEqual(args.args, [
|
||||
{
|
||||
codes: ["100"],
|
||||
company_id: null,
|
||||
include_unposted: false,
|
||||
date_range: {
|
||||
range_type: "year",
|
||||
year: 2020,
|
||||
},
|
||||
},
|
||||
]);
|
||||
return drillDownAction;
|
||||
}
|
||||
},
|
||||
});
|
||||
const env = model.config.evalContext.env;
|
||||
env.model = model;
|
||||
setCellContent(model, "A1", `=ODOO.BALANCE("100", 2020)`);
|
||||
setCellContent(model, "A2", `=ODOO.BALANCE("100", 0)`);
|
||||
setCellContent(model, "A3", `=ODOO.BALANCE("100", 2020, , , FALSE)`);
|
||||
setCellContent(model, "A4", `=ODOO.BALANCE("100", 2020, , , )`);
|
||||
await waitForDataSourcesLoaded(model);
|
||||
selectCell(model, "A1");
|
||||
const root = cellMenuRegistry.getAll().find((item) => item.id === "move_lines_see_records");
|
||||
assert.equal(root.isVisible(env), true);
|
||||
await root.action(env);
|
||||
assert.verifySteps(["drill down action"]);
|
||||
selectCell(model, "A2");
|
||||
assert.equal(root.isVisible(env), false);
|
||||
selectCell(model, "A3");
|
||||
assert.equal(root.isVisible(env), true);
|
||||
await root.action(env);
|
||||
assert.verifySteps(["drill down action"]);
|
||||
selectCell(model, "A4");
|
||||
assert.equal(root.isVisible(env), true);
|
||||
await root.action(env);
|
||||
assert.verifySteps(["drill down action"]);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue