From f78691a6b85aa4c9afbaaa458099ce4d124587bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Lef=C3=A8vre?= Date: Tue, 6 Sep 2022 14:13:54 +0000 Subject: [PATCH] [MOV] spreadsheet_account: move module to community The spreadsheet_account module is moved to the community version for the upcoming spreadsheet dashboards Task 2799682 Part-of: odoo/odoo#97606 --- addons/spreadsheet_account/__init__.py | 1 + addons/spreadsheet_account/__manifest__.py | 26 + addons/spreadsheet_account/models/__init__.py | 2 + addons/spreadsheet_account/models/account.py | 147 +++ .../spreadsheet_account/models/res_company.py | 29 + .../static/src/accounting_datasource.js | 135 +++ .../static/src/accounting_functions.js | 288 ++++++ .../spreadsheet_account/static/src/index.js | 45 + .../static/src/plugins/accounting_plugin.js | 102 +++ .../spreadsheet_account/static/src/utils.js | 26 + .../static/tests/accounting_test_data.js | 42 + .../static/tests/mock_server.js | 20 + .../tests/model/account_groups_tests.js | 36 + .../static/tests/model/accounting_tests.js | 385 ++++++++ .../static/tests/model/fiscal_year_tests.js | 103 +++ .../tests/ui/accounting_drilldown_tests.js | 76 ++ addons/spreadsheet_account/tests/__init__.py | 6 + .../tests/test_account_group.py | 87 ++ .../tests/test_company_fiscal_year.py | 98 ++ .../tests/test_debit_credit.py | 866 ++++++++++++++++++ addons/web/tooling/_eslintignore | 4 +- addons/web/tooling/_prettierignore | 8 +- 22 files changed, 2528 insertions(+), 4 deletions(-) create mode 100644 addons/spreadsheet_account/__init__.py create mode 100644 addons/spreadsheet_account/__manifest__.py create mode 100644 addons/spreadsheet_account/models/__init__.py create mode 100644 addons/spreadsheet_account/models/account.py create mode 100644 addons/spreadsheet_account/models/res_company.py create mode 100644 addons/spreadsheet_account/static/src/accounting_datasource.js create mode 100644 addons/spreadsheet_account/static/src/accounting_functions.js create mode 100644 addons/spreadsheet_account/static/src/index.js create mode 100644 addons/spreadsheet_account/static/src/plugins/accounting_plugin.js create mode 100644 addons/spreadsheet_account/static/src/utils.js create mode 100644 addons/spreadsheet_account/static/tests/accounting_test_data.js create mode 100644 addons/spreadsheet_account/static/tests/mock_server.js create mode 100644 addons/spreadsheet_account/static/tests/model/account_groups_tests.js create mode 100644 addons/spreadsheet_account/static/tests/model/accounting_tests.js create mode 100644 addons/spreadsheet_account/static/tests/model/fiscal_year_tests.js create mode 100644 addons/spreadsheet_account/static/tests/ui/accounting_drilldown_tests.js create mode 100644 addons/spreadsheet_account/tests/__init__.py create mode 100644 addons/spreadsheet_account/tests/test_account_group.py create mode 100644 addons/spreadsheet_account/tests/test_company_fiscal_year.py create mode 100644 addons/spreadsheet_account/tests/test_debit_credit.py diff --git a/addons/spreadsheet_account/__init__.py b/addons/spreadsheet_account/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/addons/spreadsheet_account/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/addons/spreadsheet_account/__manifest__.py b/addons/spreadsheet_account/__manifest__.py new file mode 100644 index 00000000000..8abbeb69ded --- /dev/null +++ b/addons/spreadsheet_account/__manifest__.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +{ + 'name': "Spreadsheet Accounting Formulas", + 'version': '1.0', + 'category': 'Accounting', + 'summary': 'Spreadsheet Accounting formulas', + 'description': 'Spreadsheet Accounting formulas', + 'depends': ['spreadsheet', 'account'], + 'data': [], + 'installable': True, + 'auto_install': True, + 'license': 'LGPL-3', + 'assets': { + 'spreadsheet.o_spreadsheet': [ + ( + 'after', + 'spreadsheet/static/src/o_spreadsheet/o_spreadsheet.js', + 'spreadsheet_account/static/src/**/*.js' + ), + ], + 'web.qunit_suite_tests': [ + 'spreadsheet_account/static/tests/**/*.js', + ], + } +} diff --git a/addons/spreadsheet_account/models/__init__.py b/addons/spreadsheet_account/models/__init__.py new file mode 100644 index 00000000000..3b54bd6a98d --- /dev/null +++ b/addons/spreadsheet_account/models/__init__.py @@ -0,0 +1,2 @@ +from . import account +from . import res_company diff --git a/addons/spreadsheet_account/models/account.py b/addons/spreadsheet_account/models/account.py new file mode 100644 index 00000000000..68d975235a1 --- /dev/null +++ b/addons/spreadsheet_account/models/account.py @@ -0,0 +1,147 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import date +from dateutil.relativedelta import relativedelta + +from odoo import models, api, _ +from odoo.osv import expression +from odoo.tools import date_utils + + +class AccountMove(models.Model): + _inherit = "account.account" + + @api.model + def _get_date_period_boundaries(self, date_period, company): + period_type = date_period["range_type"] + year = date_period.get("year") + month = date_period.get("month") + quarter = date_period.get("quarter") + day = date_period.get("day") + if period_type == "year": + fiscal_day = company.fiscalyear_last_day + fiscal_month = int(company.fiscalyear_last_month) + if not (fiscal_day == 31 and fiscal_month == 12): + year += 1 + current = date(year, fiscal_month, fiscal_day) + start, end = date_utils.get_fiscal_year(current, fiscal_day, fiscal_month) + elif period_type == "month": + start = date(year, month, 1) + end = start + relativedelta(months=1, days=-1) + elif period_type == "quarter": + first_month = quarter * 3 - 2 + start = date(year, first_month, 1) + end = start + relativedelta(months=3, days=-1) + elif period_type == "day": + fiscal_day = company.fiscalyear_last_day + fiscal_month = int(company.fiscalyear_last_month) + end = date(year, month, day) + start, _ = date_utils.get_fiscal_year(end, fiscal_day, fiscal_month) + return start, end + + def _build_spreadsheet_formula_domain(self, formula_params): + code = formula_params["code"] + if not code: + return expression.FALSE_DOMAIN + company_id = formula_params["company_id"] or self.env.company.id + company = self.env["res.company"].browse(company_id) + start, end = self._get_date_period_boundaries( + formula_params["date_range"], company + ) + + base_domain = [ + ("account_id.code", "=like", f"{code}%"), + ("company_id", "=", company_id), + ] + if formula_params["include_unposted"]: + base_domain = expression.AND( + [base_domain, [("move_id.state", "!=", "cancel")]] + ) + else: + base_domain = expression.AND( + [base_domain, [("move_id.state", "=", "posted")]] + ) + + balance_domain = expression.AND( + [ + base_domain, + [ + ("account_id.include_initial_balance", "=", True), + ("date", "<=", end), + ], + ] + ) + result_domain = expression.AND( + [ + base_domain, + [ + ("account_id.include_initial_balance", "=", False), + ("date", ">=", start), + ("date", "<=", end), + ], + ] + ) + return expression.OR([balance_domain, result_domain]) + + @api.model + def spreadsheet_move_line_action(self, args): + domain = self._build_spreadsheet_formula_domain(args) + return { + "type": "ir.actions.act_window", + "res_model": "account.move.line", + "view_mode": "list", + "views": [[False, "list"]], + "target": "current", + "domain": domain, + "name": _("Journal items for account prefix %s", args["code"]), + } + + @api.model + def spreadsheet_fetch_debit_credit(self, args_list): + """Fetch data for ODOO.CREDIT, ODOO.DEBIT and ODOO.BALANCE formulas + The input list looks like this: + [{ + date_range: { + range_type: "year" + year: int + }, + company_id: int + code: str + include_unposted: bool + }] + """ + results = [] + for args in args_list: + company_id = args["company_id"] or self.env.company.id + domain = self._build_spreadsheet_formula_domain(args) + # remove this when _search always returns a Query object + if domain == expression.FALSE_DOMAIN: + results.append({"credit": 0, "debit": 0}) + continue + MoveLines = self.env["account.move.line"].with_company(company_id) + query = MoveLines._search(domain) + query.order = None + query_str, params = query.select( + "SUM(debit) AS debit", "SUM(credit) AS credit" + ) + self.env.cr.execute(query_str, params) + line_values = self.env.cr.dictfetchone() + results.append( + { + "credit": line_values["credit"] or 0, + "debit": line_values["debit"] or 0, + } + ) + + return results + + @api.model + def get_account_group(self, account_types): + data = self._read_group( + [("account_type", "in", account_types), ("company_id", "=", self.env.company.id)], + ["code:array_agg"], + ["account_type"], + ) + mapped = {group["account_type"]: group["code"] for group in data} + return [mapped.get(account_type, []) for account_type in account_types] diff --git a/addons/spreadsheet_account/models/res_company.py b/addons/spreadsheet_account/models/res_company.py new file mode 100644 index 00000000000..cd7d049208e --- /dev/null +++ b/addons/spreadsheet_account/models/res_company.py @@ -0,0 +1,29 @@ +from odoo import models, api, fields + +from odoo.tools import date_utils + + +class ResCompany(models.Model): + _inherit = "res.company" + + @api.model + def get_fiscal_dates(self, payload): + companies = self.env["res.company"].browse( + data["company_id"] or self.env.company.id for data in payload + ) + existing_companies = companies.exists() + # prefetch both fields + existing_companies.read(["fiscalyear_last_day", "fiscalyear_last_month"]) + results = [] + + for data, company in zip(payload, companies): + if company not in existing_companies: + results.append(False) + continue + start, end = date_utils.get_fiscal_year( + fields.Date.to_date(data["date"]), + day=company.fiscalyear_last_day, + month=int(company.fiscalyear_last_month), + ) + results.append({"start": start, "end": end}) + return results diff --git a/addons/spreadsheet_account/static/src/accounting_datasource.js b/addons/spreadsheet_account/static/src/accounting_datasource.js new file mode 100644 index 00000000000..4f3200d6534 --- /dev/null +++ b/addons/spreadsheet_account/static/src/accounting_datasource.js @@ -0,0 +1,135 @@ +/** @odoo-module */ +import { camelToSnakeObject, sum, toServerDateString } from "@spreadsheet/helpers/helpers"; +import { _t } from "@web/core/l10n/translation"; +import { sprintf } from "@web/core/utils/strings"; + +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.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)); + } + const results = []; + let error = undefined; + // If some payload were to raise an error, we still need to process the others + // to make sure they are part of the next batch call. + for (const code of codes) { + try { + const result = this.serverData.batch.get( + "account.account", + "spreadsheet_fetch_debit_credit", + camelToSnakeObject({ dateRange, code, companyId, includeUnposted }) + ); + results.push(result); + } catch (err) { + error = error || err; + } + } + if (error) { + throw error; + } + return { + debit: sum(results.map((values) => values.debit)), + credit: sum(results.map((values) => values.credit)), + }; + } + + /** + * 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; + } +} diff --git a/addons/spreadsheet_account/static/src/accounting_functions.js b/addons/spreadsheet_account/static/src/accounting_functions.js new file mode 100644 index 00000000000..6bac2e25176 --- /dev/null +++ b/addons/spreadsheet_account/static/src/accounting_functions.js @@ -0,0 +1,288 @@ +/** @odoo-module **/ + +import { _t } from "@web/core/l10n/translation"; +import { sprintf } from "@web/core/utils/strings"; + +import spreadsheet from "@spreadsheet/o_spreadsheet/o_spreadsheet_extended"; +const { functionRegistry } = spreadsheet.registries; +const { args, toBoolean, toString, toNumber, toJsDate } = spreadsheet.helpers; + +const QuarterRegexp = /^q([1-4])\/(\d{4})$/i; +const MonthRegexp = /^0?([1-9]|1[0-2])\/(\d{4})$/i; + +/** + * @typedef {Object} YearDateRange + * @property {"year"} rangeType + * @property {number} year + */ + +/** + * @typedef {Object} QuarterDateRange + * @property {"quarter"} rangeType + * @property {number} year + * @property {number} quarter + */ + +/** + * @typedef {Object} MonthDateRange + * @property {"month"} rangeType + * @property {number} year + * @property {number} month + */ + +/** + * @typedef {Object} DayDateRange + * @property {"day"} rangeType + * @property {number} year + * @property {number} month + * @property {number} day + */ + +/** + * @typedef {YearDateRange | QuarterDateRange | MonthDateRange | DayDateRange} DateRange + */ + +/** + * @param {string} dateRange + * @returns {QuarterDateRange | undefined} + */ +function parseAccountingQuarter(dateRange) { + const found = dateRange.match(QuarterRegexp); + return found + ? { + rangeType: "quarter", + year: toNumber(found[2]), + quarter: toNumber(found[1]), + } + : undefined; +} + +/** + * @param {string} dateRange + * @returns {MonthDateRange | undefined} + */ +function parseAccountingMonth(dateRange) { + const found = dateRange.match(MonthRegexp); + return found + ? { + rangeType: "month", + year: toNumber(found[2]), + month: toNumber(found[1]), + } + : undefined; +} + +/** + * @param {string} dateRange + * @returns {YearDateRange | undefined} + */ +function parseAccountingYear(dateRange) { + const dateNumber = toNumber(dateRange); + // This allows a bit of flexibility for the user if they were to input a + // numeric value instead of a year. + // Users won't need to fetch accounting info for year 3000 before a long time + // And the numeric value 3000 corresponds to 18th march 1908, so it's not an + //issue to prevent them from fetching accounting data prior to that date. + if (dateNumber < 3000) { + return { rangeType: "year", year: dateNumber }; + } + return undefined; +} + +/** + * @param {string} dateRange + * @returns {DayDateRange} + */ +function parseAccountingDay(dateRange) { + const dateNumber = toNumber(dateRange); + return { + rangeType: "day", + year: functionRegistry.get("YEAR").compute(dateNumber), + month: functionRegistry.get("MONTH").compute(dateNumber), + day: functionRegistry.get("DAY").compute(dateNumber), + }; +} + +/** + * @param {string | number} dateRange + * @returns {DateRange} + */ +export function parseAccountingDate(dateRange) { + try { + dateRange = toString(dateRange).trim(); + return ( + parseAccountingQuarter(dateRange) || + parseAccountingMonth(dateRange) || + parseAccountingYear(dateRange) || + parseAccountingDay(dateRange) + ); + } catch (_) { + throw new Error(sprintf(_t(`'%s' is not a valid period. Supported formats are "21/12/2022", "Q1/2022", "12/2022", and "2022".`), dateRange)); + } +} + +const ODOO_FIN_ARGS = ` + account_codes (string) ${_t("The prefix of the accounts.")} + date_range (string, date) ${_t(`The date range. Supported formats are "21/12/2022", "Q1/2022", "12/2022", and "2022".`)} + offset (number, default=0) ${_t("Year offset applied to date_range.")} + company_id (number, optional) ${_t("The company to target (Advanced).")} + include_unposted (boolean, default=TRUE) ${_t("Set to TRUE to include unposted entries.")} +` + +functionRegistry.add("ODOO.CREDIT", { + description: _t("Get the total credit for the specified account(s) and period."), + args: args(ODOO_FIN_ARGS), + returns: ["NUMBER"], + compute: function ( + accountCodes, + dateRange, + offset = 0, + companyId = null, + includeUnposted = true + ) { + accountCodes = toString(accountCodes).split(",").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 = true + ) { + return this.getters.getCompanyCurrencyFormat(companyId && companyId.value) || "#,##0.00"; + }, +}); + +functionRegistry.add("ODOO.DEBIT", { + description: _t("Get the total debit for the specified account(s) and period."), + args: args(ODOO_FIN_ARGS), + returns: ["NUMBER"], + compute: function ( + accountCodes, + dateRange, + offset = 0, + companyId = null, + includeUnposted = true + ) { + accountCodes = toString(accountCodes).split(",").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 = true + ) { + return this.getters.getCompanyCurrencyFormat(companyId && companyId.value) || "#,##0.00"; + }, +}); + +functionRegistry.add("ODOO.BALANCE", { + description: _t("Get the total balance for the specified account(s) and period."), + args: args(ODOO_FIN_ARGS), + returns: ["NUMBER"], + compute: function ( + accountCodes, + dateRange, + offset = 0, + companyId = null, + includeUnposted = true + ) { + accountCodes = toString(accountCodes).split(",").sort(); + offset = toNumber(offset); + dateRange = parseAccountingDate(dateRange); + includeUnposted = toBoolean(includeUnposted); + return ( + this.getters.getAccountPrefixDebit( + accountCodes, + dateRange, + offset, + companyId, + includeUnposted + ) - + this.getters.getAccountPrefixCredit( + accountCodes, + dateRange, + offset, + companyId, + includeUnposted + ) + ); + }, + computeFormat: function ( + accountCodes, + dateRange, + offset = 0, + companyId = null, + includeUnposted = true + ) { + return this.getters.getCompanyCurrencyFormat(companyId && companyId.value) || "#,##0.00"; + }, +}); + +functionRegistry.add("ODOO.FISCALYEAR.START", { + description: _t("Returns the starting date of the fiscal year encompassing the provided date."), + args: args(` + date (date) ${_t("Reference date.")} + company_id (number, optional) ${_t("The company.")} + `), + returns: ["NUMBER"], + computeFormat: () => "m/d/yyyy", + compute: function (date, companyId = null) { + const startDate = this.getters.getFiscalStartDate( + toJsDate(date), + companyId === null ? null : toNumber(companyId) + ); + return toNumber(startDate); + }, +}); + +functionRegistry.add("ODOO.FISCALYEAR.END", { + description: _t("Returns the ending date of the fiscal year encompassing the provided date."), + args: args(` + date (date) ${_t("Reference date.")} + company_id (number, optional) ${_t("The company.")} + `), + returns: ["NUMBER"], + computeFormat: () => "m/d/yyyy", + compute: function (date, companyId = null) { + const endDate = this.getters.getFiscalEndDate( + toJsDate(date), + companyId === null ? null : toNumber(companyId) + ); + return toNumber(endDate); + }, +}); + +functionRegistry.add("ODOO.ACCOUNT.GROUP", { + description: _t("Returns the account ids of a given group."), + args: args(` + type (string) ${_t("Account type.")} + `), + returns: ["NUMBER"], + computeFormat: () => "m/d/yyyy", + compute: function (accountType) { + const accountTypes = this.getters.getAccountGroupCodes(toString(accountType)); + return accountTypes.join(","); + }, +}); diff --git a/addons/spreadsheet_account/static/src/index.js b/addons/spreadsheet_account/static/src/index.js new file mode 100644 index 00000000000..307e69aef85 --- /dev/null +++ b/addons/spreadsheet_account/static/src/index.js @@ -0,0 +1,45 @@ +/** @odoo-module */ + +import { _lt } from "@web/core/l10n/translation"; +import spreadsheet from "@spreadsheet/o_spreadsheet/o_spreadsheet_extended"; +import AccountingPlugin from "./plugins/accounting_plugin"; +import { getFirstAccountFunction, getNumberOfAccountFormulas } from "./utils"; +import { parseAccountingDate } from "./accounting_functions"; +import { camelToSnakeObject } from "@spreadsheet/helpers/helpers"; + +const { cellMenuRegistry, uiPluginRegistry } = spreadsheet.registries; +const { astToFormula } = spreadsheet; +const { toString, toBoolean } = spreadsheet.helpers; + +uiPluginRegistry.add("odooAccountingAggregates", AccountingPlugin); + +cellMenuRegistry.add("move_lines_see_records", { + name: _lt("See records"), + sequence: 176, + async action(env) { + const cell = env.model.getters.getActiveCell(); + const { args } = getFirstAccountFunction(cell.content); + let [code, date_range, offset, companyId, includeUnposted] = args + .map(astToFormula) + .map((arg) => env.model.getters.evaluateFormula(arg)); + code = toString(code); + const dateRange = parseAccountingDate(date_range); + dateRange.year += offset || 0; + companyId = companyId || null; + includeUnposted = toBoolean(includeUnposted); + + const action = await env.services.orm.call( + "account.account", + "spreadsheet_move_line_action", + [camelToSnakeObject({ dateRange, companyId, code, includeUnposted })] + ); + await env.services.action.doAction(action); + }, + isVisible: (env) => { + const cell = env.model.getters.getActiveCell(); + return ( + cell && !cell.evaluated.error && + cell.evaluated.value !== "" && getNumberOfAccountFormulas(cell.content) === 1 + ); + }, +}); diff --git a/addons/spreadsheet_account/static/src/plugins/accounting_plugin.js b/addons/spreadsheet_account/static/src/plugins/accounting_plugin.js new file mode 100644 index 00000000000..aa689a0a3c1 --- /dev/null +++ b/addons/spreadsheet_account/static/src/plugins/accounting_plugin.js @@ -0,0 +1,102 @@ +/** @odoo-module */ + +import spreadsheet from "@spreadsheet/o_spreadsheet/o_spreadsheet_extended"; +import { AccountingDataSource } from "../accounting_datasource"; +const DATA_SOURCE_ID = "ACCOUNTING_AGGREGATES"; + +/** + * @typedef {import("../accounting_functions").DateRange} DateRange + */ + +export default class AccountingPlugin extends spreadsheet.UIPlugin { + constructor(getters, history, dispatch, config) { + super(getters, history, dispatch, config); + this.dataSources = config.dataSources; + if (this.dataSources) { + this.dataSources.add(DATA_SOURCE_ID, AccountingDataSource); + } + } + + // ------------------------------------------------------------------------- + // Getters + // ------------------------------------------------------------------------- + + /** + * Gets the total balance for given account code prefix + * @param {string[]} codes prefixes of the accounts' codes + * @param {DateRange} dateRange start date of the period to look + * @param {number} offset end date of the period to look + * @param {number | null} companyId specific company to target + * @param {boolean} includeUnposted wether or not select unposted entries + * @returns {number} + */ + getAccountPrefixCredit(codes, dateRange, offset, companyId, includeUnposted) { + return ( + this.dataSources && + this.dataSources + .get(DATA_SOURCE_ID) + .getCredit(codes, dateRange, offset, companyId, includeUnposted) + ); + } + + /** + * Gets the total balance for a given account code prefix + * @param {string[]} codes prefixes of the accounts codes + * @param {DateRange} dateRange start date of the period to look + * @param {number} offset end date of the period to look + * @param {number | null} companyId specific company to target + * @param {boolean} includeUnposted wether or not select unposted entries + * @returns {number} + */ + getAccountPrefixDebit(codes, dateRange, offset, companyId, includeUnposted) { + return ( + this.dataSources && + this.dataSources + .get(DATA_SOURCE_ID) + .getDebit(codes, dateRange, offset, companyId, includeUnposted) + ); + } + + /** + * @param {Date} date Date included in the fiscal year + * @param {number | null} companyId specific company to target + * @returns {string | undefined} + */ + getFiscalStartDate(date, companyId) { + return ( + this.dataSources && + this.dataSources.get(DATA_SOURCE_ID).getFiscalStartDate(date, companyId) + ); + } + + /** + * @param {Date} date Date included in the fiscal year + * @param {number | undefined} companyId specific company to target + * @returns {string | undefined} + */ + getFiscalEndDate(date, companyId) { + return ( + this.dataSources && + this.dataSources.get(DATA_SOURCE_ID).getFiscalEndDate(date, companyId) + ); + } + + /** + * @param {string} accountType + * @returns {string[]} + */ + getAccountGroupCodes(accountType) { + return ( + this.dataSources && + this.dataSources.get(DATA_SOURCE_ID).getAccountGroupCodes(accountType) + ); + } +} + +AccountingPlugin.getters = [ + "getAccountPrefixCredit", + "getAccountPrefixDebit", + "getAccountGroupCodes", + "getFiscalStartDate", + "getFiscalEndDate", +]; diff --git a/addons/spreadsheet_account/static/src/utils.js b/addons/spreadsheet_account/static/src/utils.js new file mode 100644 index 00000000000..52cf00c34df --- /dev/null +++ b/addons/spreadsheet_account/static/src/utils.js @@ -0,0 +1,26 @@ +/** @odoo-module **/ +import { getOdooFunctions } from "@spreadsheet/helpers/odoo_functions_helpers"; + +/** @typedef {import("@spreadsheet/helpers/odoo_functions_helpers").OdooFunctionDescription} OdooFunctionDescription*/ + +/** + * @param {string} formula + * @returns {number} + */ +export function getNumberOfAccountFormulas(formula) { + return getOdooFunctions(formula, (functionName) => + ["ODOO.BALANCE", "ODOO.CREDIT", "ODOO.DEBIT"].includes(functionName) + ).filter((fn) => fn.isMatched).length; +} + +/** + * Get the first Account function description of the given formula. + * + * @param {string} formula + * @returns {OdooFunctionDescription | undefined} + */ +export function getFirstAccountFunction(formula) { + return getOdooFunctions(formula, (functionName) => + ["ODOO.BALANCE", "ODOO.CREDIT", "ODOO.DEBIT"].includes(functionName) + ).find((fn) => fn.isMatched); +} diff --git a/addons/spreadsheet_account/static/tests/accounting_test_data.js b/addons/spreadsheet_account/static/tests/accounting_test_data.js new file mode 100644 index 00000000000..d1a6d2b6c51 --- /dev/null +++ b/addons/spreadsheet_account/static/tests/accounting_test_data.js @@ -0,0 +1,42 @@ +/** @odoo-module */ + +import { getBasicData } from "@spreadsheet/../tests/utils/data"; + +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" }, + ], + }, + }, + views: { + "account.move.line,false,list": /* xml */ ` + + + + + + `, + "account.move.line,false,search": /* xml */ ``, + }, + }; +} diff --git a/addons/spreadsheet_account/static/tests/mock_server.js b/addons/spreadsheet_account/static/tests/mock_server.js new file mode 100644 index 00000000000..5f96d9fd362 --- /dev/null +++ b/addons/spreadsheet_account/static/tests/mock_server.js @@ -0,0 +1,20 @@ +/** @odoo-module */ + +import { registry } from "@web/core/registry"; + +registry + .category("mock_server") + .add("account.account/spreadsheet_fetch_debit_credit", function (route, args) { + return new Array(args.args[0].length).fill({ credit: 0, debit: 0 }); + }) + .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"], + ], {}); + return records.map((record) => record.code); + }); + return data; + }); diff --git a/addons/spreadsheet_account/static/tests/model/account_groups_tests.js b/addons/spreadsheet_account/static/tests/model/account_groups_tests.js new file mode 100644 index 00000000000..fff24e5a220 --- /dev/null +++ b/addons/spreadsheet_account/static/tests/model/account_groups_tests.js @@ -0,0 +1,36 @@ +/** @odoo-module */ + +import { nextTick } from "@web/../tests/helpers/utils"; +import { setCellContent } from "@spreadsheet/../tests/utils/commands"; +import { createModelWithDataSource } 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 nextTick(); + 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 nextTick(); + 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 nextTick(); + assert.equal(getCellValue(model, "A1"), "100104,200104"); + }); +}); diff --git a/addons/spreadsheet_account/static/tests/model/accounting_tests.js b/addons/spreadsheet_account/static/tests/model/accounting_tests.js new file mode 100644 index 00000000000..efa96bf2da4 --- /dev/null +++ b/addons/spreadsheet_account/static/tests/model/accounting_tests.js @@ -0,0 +1,385 @@ +/** @odoo-module */ + +import { nextTick } from "@web/../tests/helpers/utils"; +import { setCellContent } from "@spreadsheet/../tests/utils/commands"; +import { createModelWithDataSource } 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 nextTick(); + 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 nextTick(); + 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 nextTick(); + 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 nextTick(); + 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: 42, credit: 16 }, + { debit: 100, credit: 10 }, + ]; + } + }, + }); + setCellContent(model, "A1", `=ODOO.CREDIT("100,200", "2022")`); + setCellContent(model, "A2", `=ODOO.DEBIT("100,200", "2022")`); + setCellContent(model, "A3", `=ODOO.BALANCE("100,200", "2022")`); + await nextTick(); + assert.equal(getCellValue(model, "A1"), 26); + assert.equal(getCellValue(model, "A2"), 142); + assert.equal(getCellValue(model, "A3"), 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 nextTick(); + 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 (let blob of blobs) { + assert.step(JSON.stringify(blob)); + } + } + return []; + }, + }); + 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)`); + await nextTick(); + + assert.verifySteps([ + JSON.stringify( + camelToSnakeObject({ + dateRange: parseAccountingDate("2022"), + code: "100", + companyId: null, + includeUnposted: true, + }) + ), + JSON.stringify( + camelToSnakeObject({ + dateRange: parseAccountingDate("01/2022"), + code: "100", + companyId: null, + includeUnposted: true, + }) + ), + JSON.stringify( + camelToSnakeObject({ + dateRange: parseAccountingDate("Q2/2022"), + code: "100", + companyId: null, + includeUnposted: true, + }) + ), + JSON.stringify( + camelToSnakeObject({ + dateRange: parseAccountingDate("2021"), + code: "10", + companyId: null, + includeUnposted: true, + }) + ), + JSON.stringify( + camelToSnakeObject({ + dateRange: parseAccountingDate("2021"), + code: "5", + companyId: 2, + includeUnposted: true, + }) + ), + JSON.stringify( + camelToSnakeObject({ + dateRange: parseAccountingDate("05/04/2022"), + code: "5", + companyId: null, + includeUnposted: true, + }) + ), + JSON.stringify( + camelToSnakeObject({ + dateRange: parseAccountingDate("2022"), + code: "5", + companyId: null, + includeUnposted: false, + }) + ), + JSON.stringify( + camelToSnakeObject({ + dateRange: parseAccountingDate("05/05/2022"), + code: "100", + companyId: null, + includeUnposted: true, + }) + ), + ]); + }); + + 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 (let 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 nextTick(); + + assert.verifySteps([ + "spreadsheet_fetch_debit_credit", + JSON.stringify( + camelToSnakeObject({ + dateRange: parseAccountingDate("2022"), + code: "100", + companyId: null, + includeUnposted: true, + }) + ), + JSON.stringify( + camelToSnakeObject({ + dateRange: parseAccountingDate("2022"), + code: "200", + companyId: null, + includeUnposted: true, + }) + ), + ]); + }); + + 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 (let 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 nextTick(); + 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"), + code: "100104", + companyId: null, + includeUnposted: true, + }) + ), + JSON.stringify( + camelToSnakeObject({ + dateRange: parseAccountingDate("2022"), + code: "200104", + companyId: null, + includeUnposted: true, + }) + ), + ]); + }); + + 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 (let 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 nextTick(); + 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"), + code: "100", + companyId: null, + includeUnposted: true, + }) + ), + "spreadsheet_fetch_debit_credit", + JSON.stringify( + camelToSnakeObject({ + dateRange: parseAccountingDate("2022"), + code: "100104", + companyId: null, + includeUnposted: true, + }) + ), + JSON.stringify( + camelToSnakeObject({ + dateRange: parseAccountingDate("2022"), + code: "200104", + companyId: null, + includeUnposted: true, + }) + ), + ]); + }); + + 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, + }); + }); +}); diff --git a/addons/spreadsheet_account/static/tests/model/fiscal_year_tests.js b/addons/spreadsheet_account/static/tests/model/fiscal_year_tests.js new file mode 100644 index 00000000000..6ed8d9c3c98 --- /dev/null +++ b/addons/spreadsheet_account/static/tests/model/fiscal_year_tests.js @@ -0,0 +1,103 @@ +/** @odoo-module */ +import { nextTick } from "@web/../tests/helpers/utils"; +import { setCellContent } from "@spreadsheet/../tests/utils/commands"; +import { createModelWithDataSource } 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 nextTick(); + 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 nextTick(); + 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 nextTick(); + 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." + ); + }); +}); diff --git a/addons/spreadsheet_account/static/tests/ui/accounting_drilldown_tests.js b/addons/spreadsheet_account/static/tests/ui/accounting_drilldown_tests.js new file mode 100644 index 00000000000..d9d9d0f513e --- /dev/null +++ b/addons/spreadsheet_account/static/tests/ui/accounting_drilldown_tests.js @@ -0,0 +1,76 @@ +/** @odoo-module */ + +import { nextTick } from "@web/../tests/helpers/utils"; +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 } 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, [ + { + code: "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)`); + await nextTick(); + 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); + }); +}); diff --git a/addons/spreadsheet_account/tests/__init__.py b/addons/spreadsheet_account/tests/__init__.py new file mode 100644 index 00000000000..64396d3ac64 --- /dev/null +++ b/addons/spreadsheet_account/tests/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import test_account_group +from . import test_debit_credit +from . import test_company_fiscal_year diff --git a/addons/spreadsheet_account/tests/test_account_group.py b/addons/spreadsheet_account/tests/test_account_group.py new file mode 100644 index 00000000000..098bbcaf37a --- /dev/null +++ b/addons/spreadsheet_account/tests/test_account_group.py @@ -0,0 +1,87 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.tests.common import TransactionCase + + +class SpreadsheetAccountGroupTest(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.env["account.account"].create( + { + "company_id": cls.env.user.company_id.id, + "name": "spreadsheet revenue Company 1", + "account_type": "income", + "code": "123", + } + ) + + cls.env["account.account"].create( + { + "company_id": cls.env.user.company_id.id, + "name": "spreadsheet expense Company 1", + "account_type": "income_other", + "code": "456", + } + ) + + cls.env["account.account"].create( + { + "company_id": cls.env.user.company_id.id, + "name": "spreadsheet revenue Company 2", + "account_type": "income", + "code": "789", + } + ) + + def _unlink_default_accounts(self, account_ids): + used_account_id = self.env["account.account"].search([("code", '=', '123')]) + aml_ids = self.env["account.move.line"].search([("account_id", "in", account_ids.ids), ("company_id", "=", self.env.company.id)]) + journal_ids = self.env["account.journal"].search([("default_account_id", "in", account_ids.ids), ("company_id", "=", self.env.company.id)]) + for aml_id in aml_ids: + aml_id.account_id = used_account_id + for journal_id in journal_ids: + journal_id.default_account_id = used_account_id + for account_id in account_ids: + self.env["ir.property"]._set_default("property_account_income_categ_id", "product.category", used_account_id, self.env.company) + account_id.unlink() + + def test_fetch_account_no_group(self): + self.assertEqual(self.env["account.account"].get_account_group([]), []) + + def test_fetch_account_one_group(self): + account_ids = self.env["account.account"].search([("account_type", "=", "income_other"), ("company_id", "=", self.env.company.id), ("code", "!=", "456")]) + self._unlink_default_accounts(account_ids) + self.assertEqual( + self.env["account.account"].get_account_group(["income_other"]), + [["456"]], + ) + + def test_group_with_no_account(self): + account_ids = self.env["account.account"].search([("account_type", "=", "income_other"), ("company_id", "=", self.env.company.id)]) + self._unlink_default_accounts(account_ids) + self.assertEqual( + self.env["account.account"].get_account_group(["income_other"]), [[]] + ) + + def test_with_wrong_account_type_id(self): + self.assertEqual(self.env["account.account"].get_account_group([999999]), [[]]) + + def test_group_with_multiple_accounts(self): + account_ids = self.env["account.account"].search([("account_type", "=", "income"), ("company_id", "=", self.env.company.id), ("code", "not in", ["123", "789"])]) + self._unlink_default_accounts(account_ids) + self.assertEqual( + self.env["account.account"].get_account_group(["income"]), + [["123", "789"]], + ) + + def test_response_is_ordered(self): + o1_codes_1, o1_codes_2 = self.env["account.account"].get_account_group( + ["income", "income_other"] + ) + o2_codes_2, o2_codes_1 = self.env["account.account"].get_account_group( + ["income_other", "income"] + ) + self.assertEqual(o1_codes_1, o2_codes_1) + self.assertEqual(o1_codes_2, o2_codes_2) diff --git a/addons/spreadsheet_account/tests/test_company_fiscal_year.py b/addons/spreadsheet_account/tests/test_company_fiscal_year.py new file mode 100644 index 00000000000..04d373ec44b --- /dev/null +++ b/addons/spreadsheet_account/tests/test_company_fiscal_year.py @@ -0,0 +1,98 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.tests.common import TransactionCase + +from datetime import date + + +class SpreadsheetFiscalYearTest(TransactionCase): + def test_fiscal_year_reference(self): + self.env.company.fiscalyear_last_day = 3 + self.env.company.fiscalyear_last_month = "2" + + self.assertEqual( + self.env["res.company"].get_fiscal_dates( + [{"company_id": None, "date": "2020-03-05"}] + ), + [ + {"start": date(2020, 2, 4), "end": date(2021, 2, 3)}, + ], + ) + + def test_fiscal_year_reference_last_day(self): + self.env.company.fiscalyear_last_day = 3 + self.env.company.fiscalyear_last_month = "2" + + self.assertEqual( + self.env["res.company"].get_fiscal_dates( + [{"company_id": None, "date": "2020-02-03"}] + ), + [ + {"start": date(2019, 2, 4), "end": date(2020, 2, 3)}, + ], + ) + + def test_fiscal_year_reference_first_day(self): + self.env.company.fiscalyear_last_day = 3 + self.env.company.fiscalyear_last_month = "2" + + self.assertEqual( + self.env["res.company"].get_fiscal_dates( + [{"company_id": None, "date": "2020-02-04"}] + ), + [{"start": date(2020, 2, 4), "end": date(2021, 2, 3)}], + ) + + def test_fiscal_year_with_company_id(self): + company = self.env["res.company"].create( + { + "name": "test company", + "fiscalyear_last_day": 3, + "fiscalyear_last_month": "2", + } + ) + self.assertEqual( + self.env["res.company"].get_fiscal_dates( + [ + {"company_id": company.id, "date": "2020-02-04"}, + {"company_id": None, "date": "2020-02-04"}, + ] + ), + [ + {"start": date(2020, 2, 4), "end": date(2021, 2, 3)}, + {"start": date(2020, 1, 1), "end": date(2020, 12, 31)}, + ], + ) + + def test_result_order(self): + company = self.env["res.company"].create( + { + "name": "test company", + "fiscalyear_last_day": 3, + "fiscalyear_last_month": "2", + } + ) + request1 = {"company_id": company.id, "date": "2020-02-04"} + request2 = {"company_id": None, "date": "2020-02-04"} + [o1_request1, o1_request2] = self.env["res.company"].get_fiscal_dates( + [request1, request2] + ) + [o2_request2, o2_request1] = self.env["res.company"].get_fiscal_dates( + [request2, request1] + ) + self.assertEqual(o1_request1, o2_request1) + self.assertEqual(o1_request2, o2_request2) + + def test_fiscal_year_with_wrong_company_id(self): + self.assertEqual( + self.env["res.company"].get_fiscal_dates( + [ + {"company_id": 999, "date": "2020-02-04"}, + {"company_id": None, "date": "2020-02-04"}, + ] + ), + [ + False, + {"start": date(2020, 1, 1), "end": date(2020, 12, 31)}, + ], + ) diff --git a/addons/spreadsheet_account/tests/test_debit_credit.py b/addons/spreadsheet_account/tests/test_debit_credit.py new file mode 100644 index 00000000000..72e01ab78b3 --- /dev/null +++ b/addons/spreadsheet_account/tests/test_debit_credit.py @@ -0,0 +1,866 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import date + +from odoo import Command +from odoo.tests import tagged +from odoo.addons.account.tests.common import AccountTestInvoicingCommon + + +@tagged("post_install", "-at_install") +class SpreadsheetAccountingFunctionsTest(AccountTestInvoicingCommon): + @classmethod + def setUpClass(cls, chart_template_ref=None): + super().setUpClass(chart_template_ref=chart_template_ref) + + cls.account_revenue_c1 = cls.env["account.account"].create( + { + "company_id": cls.company_data["company"].id, + "name": "spreadsheet revenue Company 1", + "account_type": "income", + "code": "sp1234566", + } + ) + + cls.account_expense_c1 = cls.env["account.account"].create( + { + "company_id": cls.company_data["company"].id, + "name": "spreadsheet expense Company 1", + "account_type": "expense", + "code": "sp1234577", + } + ) + + cls.account_revenue_c2 = cls.env["account.account"].create( + { + "company_id": cls.company_data_2["company"].id, + "name": "spreadsheet revenue Company 2", + "account_type": "income", + "code": "sp99887755", + } + ) + + cls.account_expense_c2 = cls.env["account.account"].create( + { + "company_id": cls.company_data_2["company"].id, + "name": "spreadsheet expense Company 2", + "account_type": "expense", + "code": "sp99887766", + } + ) + + cls.env["account.move"].create( + { + "company_id": cls.company_data["company"].id, + "move_type": "entry", + "date": "2022-04-02", + "line_ids": [ + Command.create( + { + "name": "line_debit_c1", + "account_id": cls.account_revenue_c1.id, + "debit": 500, + } + ), + Command.create( + { + "name": "line_credit_c1", + "account_id": cls.account_expense_c1.id, + "credit": 500, + }, + ), + ], + } + ) + + cls.env["account.move"].with_company(cls.company_data_2["company"]).create( + { + "company_id": cls.company_data_2["company"].id, + "move_type": "entry", + "date": "2022-02-02", + "line_ids": [ + Command.create( + { + "name": "line_debit_c2", + "account_id": cls.account_revenue_c2.id, + "debit": 1500, + "company_id": cls.company_data_2["company"].id, + }, + ), + Command.create( + { + "name": "line_credit_c2", + "account_id": cls.account_expense_c2.id, + "credit": 1500, + "company_id": cls.company_data_2["company"].id, + }, + ), + ], + } + ) + + def test_empty_payload(self): + self.assertEqual( + self.env["account.account"].spreadsheet_fetch_debit_credit([]), [] + ) + + def test_exact_code(self): + self.assertEqual( + self.env["account.account"].spreadsheet_fetch_debit_credit( + [ + { + "date_range": { + "range_type": "year", + "year": 2022, + }, + "code": self.account_revenue_c1.code, + "company_id": None, + "include_unposted": True, + } + ] + ), + [{"credit": 0.0, "debit": 500.0}], + ) + + def test_response_order(self): + request_1 = { + "date_range": { + "range_type": "year", + "year": 2022, + }, + "code": self.account_revenue_c1.code, + "company_id": None, + "include_unposted": True, + } + request_2 = { + "date_range": { + "range_type": "year", + "year": 2020, + }, + "code": self.account_revenue_c1.code, + "company_id": None, + "include_unposted": True, + } + [o1_res1, o1_res2] = self.env["account.account"].spreadsheet_fetch_debit_credit( + [request_1, request_2] + ) + [o2_res2, o2_res1] = self.env["account.account"].spreadsheet_fetch_debit_credit( + [request_2, request_1] + ) + self.assertEqual(o1_res1, o2_res1) + self.assertEqual(o1_res2, o2_res2) + + def test_prefix_code(self): + code = "sp1234" + self.assertEqual( + self.env["account.account"].spreadsheet_fetch_debit_credit( + [ + { + "date_range": { + "range_type": "year", + "year": 2022, + }, + "code": code, + "company_id": None, + "include_unposted": True, + } + ] + ), + [ + {"credit": 500.0, "debit": 500.0}, + ], + ) + + def test_do_not_count_future_years(self): + self.env["account.move"].create( + { + "company_id": self.company_data["company"].id, + "move_type": "entry", + "date": "2022-04-02", + "line_ids": [ + Command.create( + { + "name": "line_debit_c1", + "account_id": self.account_revenue_c1.id, + "debit": 1000, + }, + ), + Command.create( + { + "name": "line_credit_c1", + "account_id": self.account_expense_c1.id, + "credit": 1000, + }, + ), + ], + } + ) + self.assertEqual( + self.env["account.account"].spreadsheet_fetch_debit_credit( + [ + { + "date_range": { + "range_type": "year", + "year": 2021, + }, + "code": self.account_revenue_c1.code, + "company_id": None, + "include_unposted": True, + } + ] + ), + [ + {"credit": 0, "debit": 0.0}, + ], + ) + + def test_year_date_period(self): + self.env["account.move"].create( + { + "company_id": self.company_data["company"].id, + "move_type": "entry", + "date": "2020-04-02", + "line_ids": [ + Command.create( + { + "name": "line_debit_c1", + "account_id": self.account_revenue_c1.id, + "debit": 1000, + }, + ), + Command.create( + { + "name": "line_credit_c1", + "account_id": self.account_expense_c1.id, + "credit": 1000, + }, + ), + ], + } + ) + self.assertEqual( + self.env["account.account"].spreadsheet_fetch_debit_credit( + [ + { + "date_range": { + "range_type": "year", + "year": 2021, + }, + "code": self.account_revenue_c1.code, + "company_id": None, + "include_unposted": True, + } + ] + ), + [ + {"credit": 0, "debit": 0.0}, + ], + ) + + def test_shifted_fiscal_year_date_period(self): + self.company_data["company"].fiscalyear_last_day = 3 + self.company_data["company"].fiscalyear_last_month = "2" + self.env["account.move"].create( + { + "company_id": self.company_data["company"].id, + "move_type": "entry", + "date": "2023-01-02", + "line_ids": [ + Command.create( + { + "name": "line_debit_c1", + "account_id": self.account_revenue_c1.id, + "debit": 1000, + }, + ), + Command.create( + { + "name": "line_credit_c1", + "account_id": self.account_expense_c1.id, + "credit": 1000, + }, + ), + ], + } + ) + self.assertEqual( + self.env["account.account"].spreadsheet_fetch_debit_credit( + [ + { + "date_range": { + "range_type": "year", + "year": 2022, + }, + "code": self.account_revenue_c1.code, + "company_id": None, + "include_unposted": True, + } + ] + ), + [{"credit": 0, "debit": 1500.0}], + ) + + def test_quarter_date_period(self): + self.env["account.move"].create( + { + "company_id": self.company_data["company"].id, + "move_type": "entry", + "date": "2022-07-02", + "line_ids": [ + Command.create( + { + "name": "line_debit_c1", + "account_id": self.account_revenue_c1.id, + "debit": 777, + }, + ), + Command.create( + { + "name": "line_credit_c1", + "account_id": self.account_revenue_c1.id, + "credit": 777, + }, + ), + ], + } + ) + self.assertEqual( + self.env["account.account"].spreadsheet_fetch_debit_credit( + [ + { + "date_range": { + "range_type": "quarter", + "year": 2022, + "quarter": 3, + }, + "code": self.account_revenue_c1.code, + "company_id": None, + "include_unposted": True, + } + ] + ), + [{"credit": 777, "debit": 777}], + ) + + def test_month_date_period(self): + self.env["account.move"].create( + { + "company_id": self.company_data["company"].id, + "move_type": "entry", + "date": "2022-07-02", + "line_ids": [ + Command.create( + { + "name": "line_debit_c1", + "account_id": self.account_revenue_c1.id, + "debit": 666, + }, + ), + Command.create( + { + "name": "line_credit_c1", + "account_id": self.account_revenue_c1.id, + "credit": 666, + }, + ), + ], + } + ) + self.assertEqual( + self.env["account.account"].spreadsheet_fetch_debit_credit( + [ + { + "date_range": {"range_type": "month", "year": 2022, "month": 7}, + "code": self.account_revenue_c1.code, + "company_id": None, + "include_unposted": True, + } + ] + ), + [{"credit": 666, "debit": 666}], + ) + + def test_day_date_period(self): + self.env["account.move"].create( + { + "company_id": self.company_data["company"].id, + "move_type": "entry", + "date": "2022-07-02", + "line_ids": [ + Command.create( + { + "name": "line_debit_c1", + "account_id": self.account_revenue_c1.id, + "debit": 555, + }, + ), + Command.create( + { + "name": "line_credit_c1", + "account_id": self.account_revenue_c1.id, + "credit": 555, + }, + ), + ], + } + ) + self.assertEqual( + self.env["account.account"].spreadsheet_fetch_debit_credit( + [ + { + "date_range": { + "range_type": "day", + "year": 2022, + "month": 7, + "day": 2, + }, + "code": self.account_revenue_c1.code, + "company_id": None, + "include_unposted": True, + } + ] + ), + [{"credit": 555, "debit": 1055}], + ) + + def test_first_fiscal_day_date_period(self): + self.company_data["company"].fiscalyear_last_day = 3 + self.company_data["company"].fiscalyear_last_month = "2" + self.env["account.move"].create( + { + "company_id": self.company_data["company"].id, + "move_type": "entry", + "date": "2022-02-03", + "line_ids": [ + Command.create( + { + "name": "line_debit_c1", + "account_id": self.account_revenue_c1.id, + "debit": 111, + }, + ), + Command.create( + { + "name": "line_credit_c1", + "account_id": self.account_revenue_c1.id, + "credit": 111, + }, + ), + ], + } + ) + self.env["account.move"].create( + { + "company_id": self.company_data["company"].id, + "move_type": "entry", + "date": "2022-02-04", + "line_ids": [ + Command.create( + { + "name": "line_debit_c1", + "account_id": self.account_revenue_c1.id, + "debit": 423, + }, + ), + Command.create( + { + "name": "line_credit_c1", + "account_id": self.account_revenue_c1.id, + "credit": 423, + }, + ), + ], + } + ) + self.assertEqual( + self.env["account.account"].spreadsheet_fetch_debit_credit( + [ + { + "date_range": { + "range_type": "day", + "year": 2022, + "month": 2, + "day": 4, + }, + "code": self.account_revenue_c1.code, + "company_id": None, + "include_unposted": True, + } + ] + ), + [ + {"credit": 423, "debit": 423}, + ], + ) + + def test_balance_account_by_year(self): + # On balance accounts, we sum the lines from the creation up to the last dat of date_range + self.account_revenue_c1.sudo().include_initial_balance = True + self.assertEqual( + self.env["account.account"].spreadsheet_fetch_debit_credit( + [ + { + "date_range": { + "range_type": "year", + "year": 2025, + }, + "code": self.account_revenue_c1.code, + "company_id": None, + "include_unposted": True, + } + ] + ), + [ + {"credit": 0, "debit": 500}, + ], + ) + + def test_balance_quarter_date_period(self): + self.account_revenue_c1.sudo().include_initial_balance = True + self.env["account.move"].create( + { + "company_id": self.company_data["company"].id, + "move_type": "entry", + "date": "2022-07-02", + "line_ids": [ + Command.create( + { + "name": "line_debit_c1", + "account_id": self.account_revenue_c1.id, + "debit": 777, + }, + ), + Command.create( + { + "name": "line_credit_c1", + "account_id": self.account_revenue_c1.id, + "credit": 777, + }, + ), + ], + } + ) + self.assertEqual( + self.env["account.account"].spreadsheet_fetch_debit_credit( + [ + { + "date_range": { + "range_type": "quarter", + "year": 2022, + "quarter": 3, + }, + "code": self.account_revenue_c1.code, + "company_id": None, + "include_unposted": True, + } + ] + ), + [ + {"credit": 777, "debit": 1277}, + ], + ) + + def test_balance_month_date_period(self): + self.account_revenue_c1.sudo().include_initial_balance = True + self.env["account.move"].create( + { + "company_id": self.company_data["company"].id, + "move_type": "entry", + "date": "2022-07-02", + "line_ids": [ + Command.create( + { + "name": "line_debit_c1", + "account_id": self.account_revenue_c1.id, + "debit": 777, + }, + ), + Command.create( + { + "name": "line_credit_c1", + "account_id": self.account_revenue_c1.id, + "credit": 777, + }, + ), + ], + } + ) + self.assertEqual( + self.env["account.account"].spreadsheet_fetch_debit_credit( + [ + { + "date_range": {"range_type": "month", "year": 2022, "month": 7}, + "code": self.account_revenue_c1.code, + "company_id": None, + "include_unposted": True, + } + ] + ), + [ + {"credit": 777, "debit": 1277}, + ], + ) + + def test_balance_day_date_period(self): + self.account_revenue_c1.sudo().include_initial_balance = True + self.env["account.move"].create( + { + "company_id": self.company_data["company"].id, + "move_type": "entry", + "date": "2022-07-02", + "line_ids": [ + Command.create( + { + "name": "line_debit_c1", + "account_id": self.account_revenue_c1.id, + "debit": 777, + }, + ), + Command.create( + { + "name": "line_credit_c1", + "account_id": self.account_revenue_c1.id, + "credit": 777, + }, + ), + ], + } + ) + self.assertEqual( + self.env["account.account"].spreadsheet_fetch_debit_credit( + [ + { + "date_range": { + "range_type": "day", + "year": 2022, + "month": 7, + "day": 2, + }, + "code": self.account_revenue_c1.code, + "company_id": None, + "include_unposted": True, + } + ] + ), + [ + {"credit": 777, "debit": 1277}, + ], + ) + + def test_move_state_ignore_cancel(self): + self.account_revenue_c1.sudo().include_initial_balance = True + self.env["account.move"].create( + { + "company_id": self.company_data["company"].id, + "move_type": "entry", + "date": "2022-04-02", + "state": "cancel", + "line_ids": [ + Command.create( + { + "name": "line_debit_c1", + "account_id": self.account_revenue_c1.id, + "debit": 10000000, + }, + ), + Command.create( + { + "name": "line_credit_c1", + "account_id": self.account_revenue_c1.id, + "credit": 10000000, + }, + ), + ], + } + ) + self.assertEqual( + self.env["account.account"].spreadsheet_fetch_debit_credit( + [ + { + "date_range": { + "range_type": "year", + "year": 2022, + }, + "code": self.account_revenue_c1.code, + "company_id": None, + "include_unposted": False, + } + ] + ), + [ + {"credit": 0, "debit": 0}, + ], + ) + self.assertEqual( + self.env["account.account"].spreadsheet_fetch_debit_credit( + [ + { + "date_range": { + "range_type": "year", + "year": 2022, + }, + "code": self.account_revenue_c1.code, + "company_id": None, + "include_unposted": True, + } + ] + ), + [ + {"credit": 0, "debit": 500}, + ], + ) + + def test_move_state_unposted(self): + self.account_revenue_c1.sudo().include_initial_balance = True + move = self.env["account.move"].create( + { + "company_id": self.company_data["company"].id, + "move_type": "entry", + "date": "2022-04-02", + "line_ids": [ + Command.create( + { + "name": "line_debit_c1", + "account_id": self.account_revenue_c1.id, + "debit": 888, + }, + ), + Command.create( + { + "name": "line_credit_c1", + "account_id": self.account_revenue_c1.id, + "credit": 888, + }, + ), + ], + } + ) + move._post() + self.assertEqual( + self.env["account.account"].spreadsheet_fetch_debit_credit( + [ + { + "date_range": { + "range_type": "year", + "year": 2022, + }, + "code": self.account_revenue_c1.code, + "company_id": None, + "include_unposted": False, + } + ] + ), + [ + {"credit": 888, "debit": 888}, + ], + ) + self.assertEqual( + self.env["account.account"].spreadsheet_fetch_debit_credit( + [ + { + "date_range": { + "range_type": "year", + "year": 2022, + }, + "code": self.account_revenue_c1.code, + "company_id": None, + "include_unposted": True, + } + ] + ), + [ + {"credit": 888, "debit": 1388}, + ], + ) + + def test_no_code(self): + self.assertEqual( + self.env["account.account"].spreadsheet_fetch_debit_credit( + [ + { + "date_range": { + "range_type": "year", + "year": 2022, + }, + "code": "", + "company_id": None, + "include_unposted": False, + } + ] + ), + [ + {"credit": 0, "debit": 0}, + ], + ) + + def test_see_records_action(self): + action = self.env["account.account"].spreadsheet_move_line_action( + { + "date_range": { + "range_type": "year", + "year": 2022, + }, + "code": self.account_revenue_c1.code, + "company_id": self.account_revenue_c1.company_id.id, + "include_unposted": True, + } + ) + self.assertEqual( + action, + { + "type": "ir.actions.act_window", + "res_model": "account.move.line", + "view_mode": "list", + "views": [[False, "list"]], + "target": "current", + "domain": [ + "|", + "&", + "&", + "&", + ("account_id.code", "=like", "sp1234566%"), + ("company_id", "=", self.account_revenue_c1.company_id.id), + ("move_id.state", "!=", "cancel"), + "&", + ("account_id.include_initial_balance", "=", True), + ("date", "<=", date(2022, 12, 31)), + "&", + "&", + "&", + ("account_id.code", "=like", "sp1234566%"), + ("company_id", "=", self.account_revenue_c1.company_id.id), + ("move_id.state", "!=", "cancel"), + "&", + "&", + ("account_id.include_initial_balance", "=", False), + ("date", ">=", date(2022, 1, 1)), + ("date", "<=", date(2022, 12, 31)), + ], + "name": "Journal items for account prefix sp1234566", + }, + ) + + def test_see_records_action_no_code(self): + action = self.env["account.account"].spreadsheet_move_line_action( + { + "date_range": { + "range_type": "year", + "year": 2022, + }, + "code": "", + "company_id": None, + "include_unposted": True, + } + ) + self.assertEqual( + action, + { + "type": "ir.actions.act_window", + "res_model": "account.move.line", + "view_mode": "list", + "views": [[False, "list"]], + "target": "current", + "domain": [(0, "=", 1)], + "name": "Journal items for account prefix ", + }, + ) diff --git a/addons/web/tooling/_eslintignore b/addons/web/tooling/_eslintignore index 38620c5f287..e0e746f5a5a 100644 --- a/addons/web/tooling/_eslintignore +++ b/addons/web/tooling/_eslintignore @@ -91,8 +91,8 @@ addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.js !spreadsheet_edition/**/* # Whitelist spreadsheet_account -!spreadsheet_account -!spreadsheet_account/**/* +!addons/spreadsheet_account +!addons/spreadsheet_account/**/* !addons/auth_password_policy !addons/auth_password_policy/**/* diff --git a/addons/web/tooling/_prettierignore b/addons/web/tooling/_prettierignore index 3d0447e2b5e..67f0b466d5f 100644 --- a/addons/web/tooling/_prettierignore +++ b/addons/web/tooling/_prettierignore @@ -90,9 +90,13 @@ addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.js !spreadsheet_edition !spreadsheet_edition/**/* +# Whitelist spreadsheet_edition +!spreadsheet_edition +!spreadsheet_edition/**/* + # Whitelist spreadsheet_account -!spreadsheet_account -!spreadsheet_account/**/* +!addons/spreadsheet_account +!addons/spreadsheet_account/**/* !addons/auth_password_policy !addons/auth_password_policy/**/*