19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:32:02 +01:00
parent 62d197ac8b
commit 184bb0e321
667 changed files with 691406 additions and 239886 deletions

View file

@ -10,38 +10,16 @@ pip install odoo-bringout-oca-ocb-spreadsheet
## Dependencies
This addon depends on:
- bus
- web
## Manifest Information
- **Name**: Spreadsheet
- **Version**: 1.0
- **Category**: Hidden
- **License**: LGPL-3
- **Installable**: True
- portal
## Source
Based on [OCA/OCB](https://github.com/OCA/OCB) branch 16.0, addon `spreadsheet`.
- Repository: https://github.com/OCA/OCB
- Branch: 19.0
- Path: addons/spreadsheet
## License
This package maintains the original LGPL-3 license from the upstream Odoo project.
## Documentation
- Overview: doc/OVERVIEW.md
- Architecture: doc/ARCHITECTURE.md
- Models: doc/MODELS.md
- Controllers: doc/CONTROLLERS.md
- Wizards: doc/WIZARDS.md
- Reports: doc/REPORTS.md
- Security: doc/SECURITY.md
- Install: doc/INSTALL.md
- Usage: doc/USAGE.md
- Configuration: doc/CONFIGURATION.md
- Dependencies: doc/DEPENDENCIES.md
- Troubleshooting: doc/TROUBLESHOOTING.md
- FAQ: doc/FAQ.md
This package preserves the original LGPL-3 license.

View file

@ -1,13 +1,16 @@
[project]
name = "odoo-bringout-oca-ocb-spreadsheet"
version = "16.0.0"
description = "Spreadsheet - Spreadsheet"
description = "Spreadsheet -
Spreadsheet
"
authors = [
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
]
dependencies = [
"odoo-bringout-oca-ocb-bus>=16.0.0",
"odoo-bringout-oca-ocb-web>=16.0.0",
"odoo-bringout-oca-ocb-bus>=19.0.0",
"odoo-bringout-oca-ocb-web>=19.0.0",
"odoo-bringout-oca-ocb-portal>=19.0.0",
"requests>=2.25.1"
]
readme = "README.md"
@ -17,7 +20,7 @@ classifiers = [
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Office/Business",
]

View file

@ -1,3 +1,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
from . import controllers

View file

@ -3,35 +3,106 @@
{
'name': "Spreadsheet",
'version': '1.0',
'category': 'Hidden',
'category': 'Productivity/Dashboard',
'summary': 'Spreadsheet',
'description': 'Spreadsheet',
'depends': ['bus', 'web'],
'data': [],
'demo': [],
'depends': ['bus', 'web', 'portal'],
'installable': True,
'auto_install': False,
'author': 'Odoo S.A.',
'license': 'LGPL-3',
'data': [
'views/public_readonly_spreadsheet_templates.xml',
],
'assets': {
'web.chartjs_lib' : [
'spreadsheet/static/lib/chartjs-chart-geo/chartjs-chart-geo.js',
'spreadsheet/static/lib/chart_js_treemap.js',
],
'spreadsheet.o_spreadsheet': [
'web/static/src/views/graph/graph_model.js',
'web/static/src/views/pivot/pivot_model.js',
'web/static/src/polyfills/clipboard.js',
('include', 'web.chartjs_lib'),
'spreadsheet/static/src/o_spreadsheet/o_spreadsheet.js',
'spreadsheet/static/src/**/*.js',
# Load all o_spreadsheet templates first to allow to inherit them
'spreadsheet/static/src/o_spreadsheet/o_spreadsheet.xml',
'spreadsheet/static/src/**/*.xml',
('remove', 'spreadsheet/static/src/assets_backend/**/*')
('remove', 'spreadsheet/static/src/assets_backend/**/*'),
('remove', 'spreadsheet/static/src/public_readonly_app/**/*'),
],
'spreadsheet.assets_print': [
'spreadsheet/static/src/print_assets/**/*',
],
'spreadsheet.public_spreadsheet': [
('include', 'web.assets_frontend_minimal'),
('include', 'web._assets_helpers'), # bootstrap variables
'web/static/src/scss/bootstrap_overridden.scss',
'web/static/src/scss/pre_variables.scss',
'web/static/lib/bootstrap/scss/_variables.scss',
'web/static/lib/bootstrap/scss/_variables-dark.scss',
'web/static/lib/bootstrap/scss/_maps.scss',
('include', 'web._assets_bootstrap'),
'web/static/lib/popper/popper.js',
'web/static/lib/bootstrap/js/dist/util/index.js',
'web/static/lib/bootstrap/js/dist/dom/data.js',
'web/static/lib/bootstrap/js/dist/dom/event-handler.js',
'web/static/lib/bootstrap/js/dist/dom/manipulator.js',
'web/static/lib/bootstrap/js/dist/dom/selector-engine.js',
'web/static/lib/bootstrap/js/dist/util/config.js',
'web/static/lib/bootstrap/js/dist/base-component.js',
'web/static/lib/bootstrap/js/dist/collapse.js',
'web/static/lib/bootstrap/js/dist/dropdown.js',
'web/static/src/libs/fontawesome/css/font-awesome.css',
'web/static/lib/owl/owl.js',
'web/static/lib/luxon/luxon.js',
'web/static/lib/owl/odoo_module.js',
'web/static/src/core/utils/**/*.js',
'web/static/src/core/browser/browser.js',
'web/static/src/core/browser/feature_detection.js',
'web/static/src/core/registry.js',
'web/static/src/core/assets.js',
'web/static/src/core/templates.js',
'web/static/src/core/template_inheritance.js',
'web/static/src/session.js',
'web/static/src/env.js',
'web/static/src/core/**/*.js',
('remove', 'web/static/src/core/emoji_picker/emoji_data.js'),
('include', 'web.chartjs_lib'),
'spreadsheet/static/src/o_spreadsheet/o_spreadsheet.js',
'spreadsheet/static/src/o_spreadsheet/o_spreadsheet.xml',
'spreadsheet/static/src/o_spreadsheet/o_spreadsheet_variables.scss',
'spreadsheet/static/src/o_spreadsheet/o_spreadsheet.scss',
'spreadsheet/static/src/o_spreadsheet/icons.xml',
'spreadsheet/static/src/o_spreadsheet/o_spreadsheet_extended.scss',
'spreadsheet/static/src/o_spreadsheet/migration.js',
'spreadsheet/static/src/helpers/odoo_functions_helpers.js',
'spreadsheet/static/src/pivot/pivot_helpers.js',
'spreadsheet/static/src/o_spreadsheet/odoo_module.js',
'spreadsheet/static/src/helpers/helpers.js',
'spreadsheet/static/src/public_readonly_app/**/*.xml',
'spreadsheet/static/src/public_readonly_app/**/*.scss',
'spreadsheet/static/src/public_readonly_app/**/*',
'spreadsheet/static/src/hooks.js',
'spreadsheet/static/src/plugins.js',
],
'web.assets_backend': [
'spreadsheet/static/src/o_spreadsheet/o_spreadsheet_variables.scss',
'spreadsheet/static/src/**/*.scss',
'spreadsheet/static/src/assets_backend/**/*',
('remove', 'spreadsheet/static/src/public_readonly_app/**/*.scss'),
('remove', 'spreadsheet/static/src/**/*.dark.scss'),
('remove', 'spreadsheet/static/src/print_assets/**/*'),
],
"web.dark_mode_assets_backend": [
"web.assets_web_dark": [
'spreadsheet/static/src/**/*.dark.scss',
],
'web.qunit_suite_tests': [
'web.assets_unit_tests': [
'spreadsheet/static/tests/**/*',
('include', 'spreadsheet.o_spreadsheet')
]
('include', 'spreadsheet.o_spreadsheet'),
'spreadsheet/static/src/public_readonly_app/**/*.xml',
'spreadsheet/static/src/public_readonly_app/**/*.js',
('remove', 'spreadsheet/static/src/public_readonly_app/main.js'),
],
}
}

View file

@ -0,0 +1,3 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import main

View file

@ -0,0 +1,45 @@
import logging
from odoo import http
from odoo.http import request, Controller
logger = logging.getLogger(__name__)
class SpreadsheetController(Controller):
@http.route("/spreadsheet/log", type="jsonrpc", auth="user", methods=["POST"])
def log_action(self, action_type, datasources, **kw):
if datasources:
self._log_spreadsheet_export(action_type, request.env.uid, datasources)
def _log_spreadsheet_export(self, action_type, user_id, datasources):
if action_type not in ["download", "copy", "freeze", "print"]:
return
data = [src for datasource in datasources if (src := self._stringify_source(datasource))]
if not data:
return
logger.info(
"User %d exported (%s) spreadsheet data (%s) from %s",
user_id, action_type, "), (".join(data), request.httprequest.environ["REMOTE_ADDR"]
)
@staticmethod
def _stringify_source(source):
res_model = source.get("resModel")
if not res_model or res_model not in request.env:
return
if not (fields := source.get("fields", [])):
return
string = f"model: {res_model} with fields: [{','.join(fields)}]"
if groupby := source.get("groupby"):
string += f" grouped by [{','.join(groupby)}]"
if domain := source.get("domain"):
string += f" with domain {domain}"
return string

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,8 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import ir_model
from . import ir_http
from . import res_currency
from . import res_currency_rate
from . import res_lang
from . import spreadsheet_mixin

View file

@ -0,0 +1,16 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class IrHttp(models.AbstractModel):
_inherit = 'ir.http'
def session_info(self):
"""
Override this method to enable the 'Insert in spreadsheet' button in the
web client.
"""
res = super().session_info()
res["can_insert_in_spreadsheet"] = False
return res

View file

@ -0,0 +1,21 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, api
class IrModel(models.Model):
_inherit = "ir.model"
@api.readonly
@api.model
def has_searchable_parent_relation(self, model_names):
result = {}
for model_name in model_names:
model = self.env.get(model_name)
if model is None or not model.has_access("read"):
result[model_name] = False
else:
# we consider only stored parent relationships were meant to
# be searched
result[model_name] = model._parent_store and model._parent_name in model._fields
return result

View file

@ -4,34 +4,7 @@ from odoo import api, models
class ResCurrency(models.Model):
_inherit = "res.currency"
@api.model
def get_currencies_for_spreadsheet(self, currency_names):
"""
Returns the currency structure of provided currency names.
This function is meant to be called by the spreadsheet js lib,
hence the formatting of the result.
:currency_names list(str): list of currency names (e.g. ["EUR", "USD", "CAD"])
:return: list of dicts of the form `{ "code": str, "symbol": str, "decimalPlaces": int, "position":str }`
"""
currencies = self.with_context(active_test=False).search(
[("name", "in", currency_names)],
)
result = []
for currency_name in currency_names:
currency = next(filter(lambda curr: curr.name == currency_name, currencies), None)
if currency:
currency_data = {
"code": currency.name,
"symbol": currency.symbol,
"decimalPlaces": currency.decimal_places,
"position": currency.position,
}
else:
currency_data = None
result.append(currency_data)
return result
@api.readonly
@api.model
def get_company_currency_for_spreadsheet(self, company_id=None):
"""
@ -39,7 +12,7 @@ class ResCurrency(models.Model):
This function is meant to be called by the spreadsheet js lib,
hence the formatting of the result.
:company_id int: Id of the company
:param int company_id: Id of the company
:return: dict of the form `{ "code": str, "symbol": str, "decimalPlaces": int, "position":str }`
"""
company = self.env["res.company"].browse(company_id) if company_id else self.env.company

View file

@ -5,7 +5,7 @@ class ResCurrencyRate(models.Model):
_inherit = "res.currency.rate"
@api.model
def _get_rate_for_spreadsheet(self, currency_from_code, currency_to_code, date=None):
def _get_rate_for_spreadsheet(self, currency_from_code, currency_to_code, date=None, company_id=None):
if not currency_from_code or not currency_to_code:
return False
Currency = self.env["res.currency"].with_context({"active_test": False})
@ -13,17 +13,25 @@ class ResCurrencyRate(models.Model):
currency_to = Currency.search([("name", "=", currency_to_code)])
if not currency_from or not currency_to:
return False
company = self.env.company
company = self.env["res.company"].browse(company_id) if company_id else self.env.company
date = fields.Date.from_string(date) if date else fields.Date.context_today(self)
return Currency._get_conversion_rate(currency_from, currency_to, company, date)
@api.readonly
@api.model
def get_rates_for_spreadsheet(self, requests):
result = []
for request in requests:
record = request.copy()
record.update({
"rate": self._get_rate_for_spreadsheet(request["from"], request["to"], request.get("date")),
})
record.update(
{
"rate": self._get_rate_for_spreadsheet(
request["from"],
request["to"],
request.get("date"),
request.get("company_id"),
),
}
)
result.append(record)
return result

View file

@ -0,0 +1,40 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
from odoo.addons.spreadsheet.utils.formatting import (
strftime_format_to_spreadsheet_date_format,
strftime_format_to_spreadsheet_time_format,
)
class ResLang(models.Model):
_inherit = "res.lang"
@api.readonly
@api.model
def get_locales_for_spreadsheet(self):
"""Return the list of locales available for a spreadsheet."""
langs = self.with_context(active_test=False).search([])
spreadsheet_locales = [lang._odoo_lang_to_spreadsheet_locale() for lang in langs]
return spreadsheet_locales
@api.model
def _get_user_spreadsheet_locale(self):
"""Convert the odoo lang to a spreadsheet locale."""
lang = self._lang_get(self.env.user.lang or 'en_US')
return lang._odoo_lang_to_spreadsheet_locale()
def _odoo_lang_to_spreadsheet_locale(self):
"""Convert an odoo lang to a spreadsheet locale."""
return {
"name": self.name,
"code": self.code,
"thousandsSeparator": self.thousands_sep,
"decimalSeparator": self.decimal_point,
"dateFormat": strftime_format_to_spreadsheet_date_format(self.date_format),
"timeFormat": strftime_format_to_spreadsheet_time_format(self.time_format),
"formulaArgSeparator": ";" if self.decimal_point == "," else ",",
"weekStart": int(self.week_start),
}

View file

@ -0,0 +1,175 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import io
import zipfile
import base64
import json
import re
from collections import defaultdict
from odoo import api, fields, models, _, tools
from odoo.exceptions import ValidationError, MissingError
from odoo.addons.spreadsheet.utils.validate_data import fields_in_spreadsheet, menus_xml_ids_in_spreadsheet
class SpreadsheetMixin(models.AbstractModel):
_name = 'spreadsheet.mixin'
_description = "Spreadsheet mixin"
_auto = False
spreadsheet_binary_data = fields.Binary(
string="Spreadsheet file",
default=lambda self: self._empty_spreadsheet_data_base64(),
)
spreadsheet_data = fields.Text(compute='_compute_spreadsheet_data', inverse='_inverse_spreadsheet_data')
spreadsheet_file_name = fields.Char(compute='_compute_spreadsheet_file_name')
thumbnail = fields.Binary()
@api.constrains("spreadsheet_binary_data")
def _check_spreadsheet_data(self):
for spreadsheet in self.filtered("spreadsheet_binary_data"):
try:
data = json.loads(base64.b64decode(spreadsheet.spreadsheet_binary_data).decode())
except (json.JSONDecodeError, UnicodeDecodeError):
raise ValidationError(_("Uh-oh! Looks like the spreadsheet file contains invalid data."))
if not (tools.config['test_enable'] or tools.config['test_file']):
continue
if data.get("[Content_Types].xml"):
# this is a xlsx file
continue
display_name = spreadsheet.display_name
errors = []
for model, field_chains in fields_in_spreadsheet(data).items():
if model not in self.env:
errors.append(f"- model '{model}' used in '{display_name}' does not exist")
continue
for field_chain in field_chains:
field_model = model
for fname in field_chain.split("."): # field chain 'product_id.channel_ids'
if fname not in self.env[field_model]._fields:
errors.append(f"- field '{fname}' used in spreadsheet '{display_name}' does not exist on model '{field_model}'")
continue
field = self.env[field_model]._fields[fname]
if field.relational:
field_model = field.comodel_name
for xml_id in menus_xml_ids_in_spreadsheet(data):
record = self.env.ref(xml_id, raise_if_not_found=False)
if not record:
errors.append(f"- xml id '{xml_id}' used in spreadsheet '{display_name}' does not exist")
continue
# check that the menu has an action. Root menus always have an action.
if not record.action and record.parent_id.id:
errors.append(f"- menu with xml id '{xml_id}' used in spreadsheet '{display_name}' does not have an action")
if errors:
raise ValidationError(
_(
"Uh-oh! Looks like the spreadsheet file contains invalid data.\n\n%(errors)s",
errors="\n".join(errors),
),
)
@api.depends("spreadsheet_binary_data")
def _compute_spreadsheet_data(self):
attachments = self.env['ir.attachment'].with_context(bin_size=False).search([
('res_model', '=', self._name),
('res_field', '=', 'spreadsheet_binary_data'),
('res_id', 'in', self.ids),
])
data = {
attachment.res_id: attachment.raw
for attachment in attachments
}
for spreadsheet in self:
spreadsheet.spreadsheet_data = data.get(spreadsheet.id, False)
def _inverse_spreadsheet_data(self):
for spreadsheet in self:
if not spreadsheet.spreadsheet_data:
spreadsheet.spreadsheet_binary_data = False
else:
spreadsheet.spreadsheet_binary_data = base64.b64encode(spreadsheet.spreadsheet_data.encode())
@api.depends('display_name')
def _compute_spreadsheet_file_name(self):
for spreadsheet in self:
spreadsheet.spreadsheet_file_name = f"{spreadsheet.display_name}.osheet.json"
@api.onchange('spreadsheet_binary_data')
def _onchange_data_(self):
self._check_spreadsheet_data()
@api.readonly
@api.model
def get_display_names_for_spreadsheet(self, args):
ids_per_model = defaultdict(list)
for arg in args:
ids_per_model[arg["model"]].append(arg["id"])
display_names = defaultdict(dict)
for model, ids in ids_per_model.items():
records = self.env[model].with_context(active_test=False).search([("id", "in", ids)])
for record in records:
display_names[model][record.id] = record.display_name
# return the display names in the same order as the input
return [
display_names[arg["model"]].get(arg["id"])
for arg in args
]
def _empty_spreadsheet_data_base64(self):
"""Create an empty spreadsheet workbook.
Encoded as base64
"""
data = json.dumps(self._empty_spreadsheet_data())
return base64.b64encode(data.encode())
def _empty_spreadsheet_data(self):
"""Create an empty spreadsheet workbook.
The sheet name should be the same for all users to allow consistent references
in formulas. It is translated for the user creating the spreadsheet.
"""
lang = self.env["res.lang"]._lang_get(self.env.user.lang)
locale = lang._odoo_lang_to_spreadsheet_locale()
return {
"sheets": [
{
"id": "sheet1",
"name": _("Sheet1"),
}
],
"settings": {
"locale": locale,
},
"revisionId": "START_REVISION",
}
def _zip_xslx_files(self, files):
stream = io.BytesIO()
with zipfile.ZipFile(stream, 'w', compression=zipfile.ZIP_DEFLATED) as doc_zip:
for f in files:
# to reduce networking load, only the image path is sent.
# It's replaced by the image content here.
if 'imageSrc' in f:
try:
content = self._get_file_content(f['imageSrc'])
doc_zip.writestr(f['path'], content)
except MissingError:
pass
else:
doc_zip.writestr(f['path'], f['content'])
return stream.getvalue()
def _get_file_content(self, file_path):
if file_path.startswith('data:image/png;base64,'):
return base64.b64decode(file_path.split(',')[1])
match = re.match(r'/web/image/(\d+)', file_path)
file_record = self.env['ir.binary']._find_record(
res_model='ir.attachment',
res_id=int(match.group(1)),
)
return self.env['ir.binary']._get_stream_from(file_record).read()

Binary file not shown.

After

Width:  |  Height:  |  Size: 557 B

View file

@ -0,0 +1 @@
<svg width="50" height="50" viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M46 42C46 44.2091 44.2091 46 42 46L4 46L4 8C4 5.79086 5.79086 4 8 4L46 4L46 42Z" fill="#1A6F66"/><path d="M18 18L46 18L46 42C46 44.2091 44.2091 46 42 46L18 46L18 18Z" fill="#1AD3BB"/><path d="M18 18L4 18L4 8C4 5.79086 5.79086 4 8 4L18 4L18 18Z" fill="#1AD3BB"/></svg>

After

Width:  |  Height:  |  Size: 372 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

File diff suppressed because it is too large Load diff

View file

@ -1,363 +0,0 @@
/*!
* chartjs-gauge.js v0.3.0
* https://github.com/haiiaaa/chartjs-gauge/
* (c) 2021 chartjs-gauge.js Contributors
* Released under the MIT License
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('chart.js')) :
typeof define === 'function' && define.amd ? define(['chart.js'], factory) :
(global = global || self, global.Gauge = factory(global.Chart));
}(this, (function (Chart) { 'use strict';
Chart = Chart && Object.prototype.hasOwnProperty.call(Chart, 'default') ? Chart['default'] : Chart;
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
function ownKeys(object, enumerableOnly) {
var keys = Object.keys(object);
if (Object.getOwnPropertySymbols) {
var symbols = Object.getOwnPropertySymbols(object);
if (enumerableOnly) symbols = symbols.filter(function (sym) {
return Object.getOwnPropertyDescriptor(object, sym).enumerable;
});
keys.push.apply(keys, symbols);
}
return keys;
}
function _objectSpread2(target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i] != null ? arguments[i] : {};
if (i % 2) {
ownKeys(Object(source), true).forEach(function (key) {
_defineProperty(target, key, source[key]);
});
} else if (Object.getOwnPropertyDescriptors) {
Object.defineProperties(target, Object.getOwnPropertyDescriptors(source));
} else {
ownKeys(Object(source)).forEach(function (key) {
Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
});
}
}
return target;
}
Chart.defaults._set('gauge', {
needle: {
// Needle circle radius as the percentage of the chart area width
radiusPercentage: 2,
// Needle width as the percentage of the chart area width
widthPercentage: 3.2,
// Needle length as the percentage of the interval between inner radius (0%) and outer radius (100%) of the arc
lengthPercentage: 80,
// The color of the needle
color: 'rgba(0, 0, 0, 1)'
},
valueLabel: {
// fontSize: undefined
display: true,
formatter: null,
color: 'rgba(255, 255, 255, 1)',
backgroundColor: 'rgba(0, 0, 0, 1)',
borderRadius: 5,
padding: {
top: 5,
right: 5,
bottom: 5,
left: 5
},
bottomMarginPercentage: 5
},
animation: {
duration: 1000,
animateRotate: true,
animateScale: false
},
// The percentage of the chart that we cut out of the middle.
cutoutPercentage: 50,
// The rotation of the chart, where the first data arc begins.
rotation: -Math.PI,
// The total circumference of the chart.
circumference: Math.PI,
legend: {
display: false
},
tooltips: {
enabled: false
}
});
var GaugeController = Chart.controllers.doughnut.extend({
getValuePercent: function getValuePercent(_ref, value) {
var minValue = _ref.minValue,
data = _ref.data;
var min = minValue || 0;
var max = [undefined, null].includes(data[data.length - 1]) ? 1 : data[data.length - 1];
var length = max - min;
var percent = (value - min) / length;
return percent;
},
getWidth: function getWidth(chart) {
return chart.chartArea.right - chart.chartArea.left;
},
getTranslation: function getTranslation(chart) {
var chartArea = chart.chartArea,
offsetX = chart.offsetX,
offsetY = chart.offsetY;
var centerX = (chartArea.left + chartArea.right) / 2;
var centerY = (chartArea.top + chartArea.bottom) / 2;
var dx = centerX + offsetX;
var dy = centerY + offsetY;
return {
dx: dx,
dy: dy
};
},
getAngle: function getAngle(_ref2) {
var chart = _ref2.chart,
valuePercent = _ref2.valuePercent;
var _chart$options = chart.options,
rotation = _chart$options.rotation,
circumference = _chart$options.circumference;
return rotation + circumference * valuePercent;
},
/* TODO set min padding, not applied until chart.update() (also chartArea must have been set)
setBottomPadding(chart) {
const needleRadius = this.getNeedleRadius(chart);
const padding = this.chart.config.options.layout.padding;
if (needleRadius > padding.bottom) {
padding.bottom = needleRadius;
return true;
}
return false;
},
*/
drawNeedle: function drawNeedle(ease) {
if (!this.chart.animating) {
// triggered when hovering
ease = 1;
}
var _this$chart = this.chart,
ctx = _this$chart.ctx,
config = _this$chart.config,
innerRadius = _this$chart.innerRadius,
outerRadius = _this$chart.outerRadius;
var dataset = config.data.datasets[this.index];
var _this$getMeta = this.getMeta(),
previous = _this$getMeta.previous;
var _config$options$needl = config.options.needle,
radiusPercentage = _config$options$needl.radiusPercentage,
widthPercentage = _config$options$needl.widthPercentage,
lengthPercentage = _config$options$needl.lengthPercentage,
color = _config$options$needl.color;
var width = this.getWidth(this.chart);
var needleRadius = radiusPercentage / 100 * width;
var needleWidth = widthPercentage / 100 * width;
var needleLength = lengthPercentage / 100 * (outerRadius - innerRadius) + innerRadius; // center
var _this$getTranslation = this.getTranslation(this.chart),
dx = _this$getTranslation.dx,
dy = _this$getTranslation.dy; // interpolate
var origin = this.getAngle({
chart: this.chart,
valuePercent: previous.valuePercent
}); // TODO valuePercent is in current.valuePercent also
var target = this.getAngle({
chart: this.chart,
valuePercent: this.getValuePercent(dataset, dataset.value)
});
var angle = origin + (target - origin) * ease; // draw
ctx.save();
ctx.translate(dx, dy);
ctx.rotate(angle);
ctx.fillStyle = color; // draw circle
ctx.beginPath();
ctx.ellipse(0, 0, needleRadius, needleRadius, 0, 0, 2 * Math.PI);
ctx.fill(); // draw needle
ctx.beginPath();
ctx.moveTo(0, needleWidth / 2);
ctx.lineTo(needleLength, 0);
ctx.lineTo(0, -needleWidth / 2);
ctx.fill();
ctx.restore();
},
drawValueLabel: function drawValueLabel(ease) {
// eslint-disable-line no-unused-vars
if (!this.chart.config.options.valueLabel.display) {
return;
}
var _this$chart2 = this.chart,
ctx = _this$chart2.ctx,
config = _this$chart2.config;
var defaultFontFamily = config.options.defaultFontFamily;
var dataset = config.data.datasets[this.index];
var _config$options$value = config.options.valueLabel,
formatter = _config$options$value.formatter,
fontSize = _config$options$value.fontSize,
color = _config$options$value.color,
backgroundColor = _config$options$value.backgroundColor,
borderRadius = _config$options$value.borderRadius,
padding = _config$options$value.padding,
bottomMarginPercentage = _config$options$value.bottomMarginPercentage;
var width = this.getWidth(this.chart);
var bottomMargin = bottomMarginPercentage / 100 * width;
var fmt = formatter || function (value) {
return value;
};
var valueText = fmt(dataset.value).toString();
ctx.textBaseline = 'middle';
ctx.textAlign = 'center';
if (fontSize) {
ctx.font = "".concat(fontSize, "px ").concat(defaultFontFamily);
} // const { width: textWidth, actualBoundingBoxAscent, actualBoundingBoxDescent } = ctx.measureText(valueText);
// const textHeight = actualBoundingBoxAscent + actualBoundingBoxDescent;
var _ctx$measureText = ctx.measureText(valueText),
textWidth = _ctx$measureText.width; // approximate height until browsers support advanced TextMetrics
var textHeight = Math.max(ctx.measureText('m').width, ctx.measureText("\uFF37").width);
var x = -(padding.left + textWidth / 2);
var y = -(padding.top + textHeight / 2);
var w = padding.left + textWidth + padding.right;
var h = padding.top + textHeight + padding.bottom; // center
var _this$getTranslation2 = this.getTranslation(this.chart),
dx = _this$getTranslation2.dx,
dy = _this$getTranslation2.dy; // add rotation
var rotation = this.chart.options.rotation % (Math.PI * 2.0);
dx += bottomMargin * Math.cos(rotation + Math.PI / 2);
dy += bottomMargin * Math.sin(rotation + Math.PI / 2); // draw
ctx.save();
ctx.translate(dx, dy); // draw background
ctx.beginPath();
Chart.helpers.canvas.roundedRect(ctx, x, y, w, h, borderRadius);
ctx.fillStyle = backgroundColor;
ctx.fill(); // draw value text
ctx.fillStyle = color || config.options.defaultFontColor;
var magicNumber = 0.075; // manual testing
ctx.fillText(valueText, 0, textHeight * magicNumber);
ctx.restore();
},
// overrides
update: function update(reset) {
var dataset = this.chart.config.data.datasets[this.index];
dataset.minValue = dataset.minValue || 0;
var meta = this.getMeta();
var initialValue = {
valuePercent: 0
}; // animations on will call update(reset) before update()
if (reset) {
meta.previous = null;
meta.current = initialValue;
} else {
dataset.data.sort(function (a, b) {
return a - b;
});
meta.previous = meta.current || initialValue;
meta.current = {
valuePercent: this.getValuePercent(dataset, dataset.value)
};
}
Chart.controllers.doughnut.prototype.update.call(this, reset);
},
updateElement: function updateElement(arc, index, reset) {
// TODO handle reset and options.animation
Chart.controllers.doughnut.prototype.updateElement.call(this, arc, index, reset);
var dataset = this.getDataset();
var data = dataset.data; // const { options } = this.chart.config;
// scale data
var previousValue = index === 0 ? dataset.minValue : data[index - 1];
var value = data[index];
var startAngle = this.getAngle({
chart: this.chart,
valuePercent: this.getValuePercent(dataset, previousValue)
});
var endAngle = this.getAngle({
chart: this.chart,
valuePercent: this.getValuePercent(dataset, value)
});
var circumference = endAngle - startAngle;
arc._model = _objectSpread2({}, arc._model, {
startAngle: startAngle,
endAngle: endAngle,
circumference: circumference
});
},
draw: function draw(ease) {
Chart.controllers.doughnut.prototype.draw.call(this, ease);
this.drawNeedle(ease);
this.drawValueLabel(ease);
}
});
/* eslint-disable max-len, func-names */
var polyfill = function polyfill() {
if (CanvasRenderingContext2D.prototype.ellipse === undefined) {
CanvasRenderingContext2D.prototype.ellipse = function (x, y, radiusX, radiusY, rotation, startAngle, endAngle, antiClockwise) {
this.save();
this.translate(x, y);
this.rotate(rotation);
this.scale(radiusX, radiusY);
this.arc(0, 0, 1, startAngle, endAngle, antiClockwise);
this.restore();
};
}
};
polyfill();
Chart.controllers.gauge = GaugeController;
Chart.Gauge = function (context, config) {
config.type = 'gauge';
return new Chart(context, config);
};
var index = Chart.Gauge;
return index;
})));

View file

@ -0,0 +1,157 @@
import { FieldMatching } from "./global_filter.d";
import {
CorePlugin,
UIPlugin,
DispatchResult,
CommandResult,
AddPivotCommand,
UpdatePivotCommand,
CancelledReason,
} from "@odoo/o-spreadsheet";
import * as OdooCancelledReason from "@spreadsheet/o_spreadsheet/cancelled_reason";
type CoreDispatch = CorePlugin["dispatch"];
type UIDispatch = UIPlugin["dispatch"];
type CoreCommand = Parameters<CorePlugin["allowDispatch"]>[0];
type Command = Parameters<UIPlugin["allowDispatch"]>[0];
// TODO look for a way to remove this and use the real import * as OdooCancelledReason
type OdooCancelledReason = string;
declare module "@spreadsheet" {
interface OdooCommandDispatcher {
dispatch<T extends OdooCommandTypes, C extends Extract<OdooCommand, { type: T }>>(
type: {} extends Omit<C, "type"> ? T : never
): OdooDispatchResult;
dispatch<T extends OdooCommandTypes, C extends Extract<OdooCommand, { type: T }>>(
type: T,
r: Omit<C, "type">
): OdooDispatchResult;
}
interface OdooCoreCommandDispatcher {
dispatch<T extends OdooCoreCommandTypes, C extends Extract<OdooCoreCommand, { type: T }>>(
type: {} extends Omit<C, "type"> ? T : never
): OdooDispatchResult;
dispatch<T extends OdooCoreCommandTypes, C extends Extract<OdooCoreCommand, { type: T }>>(
type: T,
r: Omit<C, "type">
): OdooDispatchResult;
}
interface OdooDispatchResult extends DispatchResult {
readonly reasons: (CancelledReason | OdooCancelledReason)[];
isCancelledBecause(reason: CancelledReason | OdooCancelledReason): boolean;
}
type OdooCommandTypes = OdooCommand["type"];
type OdooCoreCommandTypes = OdooCoreCommand["type"];
type OdooDispatch = UIDispatch & OdooCommandDispatcher["dispatch"];
type OdooCoreDispatch = CoreDispatch & OdooCoreCommandDispatcher["dispatch"];
// CORE
export interface ExtendedAddPivotCommand extends AddPivotCommand {
pivot: ExtendedPivotCoreDefinition;
}
export interface ExtendedUpdatePivotCommand extends UpdatePivotCommand {
pivot: ExtendedPivotCoreDefinition;
}
export interface AddThreadCommand {
type: "ADD_COMMENT_THREAD";
threadId: number;
sheetId: string;
col: number;
row: number;
}
export interface EditThreadCommand {
type: "EDIT_COMMENT_THREAD";
threadId: number;
sheetId: string;
col: number;
row: number;
isResolved: boolean;
}
export interface DeleteThreadCommand {
type: "DELETE_COMMENT_THREAD";
threadId: number;
sheetId: string;
col: number;
row: number;
}
// this command is deprecated. use UPDATE_PIVOT instead
export interface UpdatePivotDomainCommand {
type: "UPDATE_ODOO_PIVOT_DOMAIN";
pivotId: string;
domain: Array;
}
export interface AddGlobalFilterCommand {
type: "ADD_GLOBAL_FILTER";
filter: CmdGlobalFilter;
[string]: any; // Fields matching
}
export interface EditGlobalFilterCommand {
type: "EDIT_GLOBAL_FILTER";
filter: CmdGlobalFilter;
[string]: any; // Fields matching
}
export interface RemoveGlobalFilterCommand {
type: "REMOVE_GLOBAL_FILTER";
id: string;
}
export interface MoveGlobalFilterCommand {
type: "MOVE_GLOBAL_FILTER";
id: string;
delta: number;
}
// UI
export interface RefreshAllDataSourcesCommand {
type: "REFRESH_ALL_DATA_SOURCES";
}
export interface SetGlobalFilterValueCommand {
type: "SET_GLOBAL_FILTER_VALUE";
id: string;
value: any;
}
export interface SetManyGlobalFilterValueCommand {
type: "SET_MANY_GLOBAL_FILTER_VALUE";
filters: { filterId: string; value: any }[];
}
type OdooCoreCommand =
| ExtendedAddPivotCommand
| ExtendedUpdatePivotCommand
| UpdatePivotDomainCommand
| AddThreadCommand
| DeleteThreadCommand
| EditThreadCommand
| AddGlobalFilterCommand
| EditGlobalFilterCommand
| RemoveGlobalFilterCommand
| MoveGlobalFilterCommand;
export type AllCoreCommand = OdooCoreCommand | CoreCommand;
type OdooLocalCommand =
| RefreshAllDataSourcesCommand
| SetGlobalFilterValueCommand
| SetManyGlobalFilterValueCommand;
type OdooCommand = OdooCoreCommand | OdooLocalCommand;
export type AllCommand = OdooCommand | Command;
}

View file

@ -0,0 +1,11 @@
import { SpreadsheetChildEnv as SSChildEnv } from "@odoo/o-spreadsheet";
import { Services } from "services";
declare module "@spreadsheet" {
import { Model } from "@odoo/o-spreadsheet";
export interface SpreadsheetChildEnv extends SSChildEnv {
model: OdooSpreadsheetModel;
services: Services;
}
}

View file

@ -0,0 +1,11 @@
declare module "@spreadsheet" {
import { AddFunctionDescription, Arg, EvalContext } from "@odoo/o-spreadsheet";
export interface CustomFunctionDescription extends AddFunctionDescription {
compute: (this: ExtendedEvalContext, ...args: Arg[]) => any;
}
interface ExtendedEvalContext extends EvalContext {
getters: OdooGetters;
}
}

View file

@ -0,0 +1,74 @@
import { CorePlugin, Model, UID } from "@odoo/o-spreadsheet";
import { ChartOdooMenuPlugin, OdooChartCorePlugin, OdooChartCoreViewPlugin } from "@spreadsheet/chart";
import { CurrencyPlugin } from "@spreadsheet/currency/plugins/currency";
import { AccountingPlugin } from "addons/spreadsheet_account/static/src/plugins/accounting_plugin";
import { GlobalFiltersCorePlugin, GlobalFiltersCoreViewPlugin } from "@spreadsheet/global_filters";
import { ListCorePlugin, ListCoreViewPlugin } from "@spreadsheet/list";
import { IrMenuPlugin } from "@spreadsheet/ir_ui_menu/ir_ui_menu_plugin";
import { PivotOdooCorePlugin } from "@spreadsheet/pivot";
import { PivotCoreGlobalFilterPlugin } from "@spreadsheet/pivot/plugins/pivot_core_global_filter_plugin";
type Getters = Model["getters"];
type CoreGetters = CorePlugin["getters"];
/**
* Union of all getter names of a plugin.
*
* e.g. With the following plugin
* @example
* class MyPlugin {
* static getters = [
* "getCell",
* "getCellValue",
* ] as const;
* getCell() { ... }
* getCellValue() { ... }
* }
* type Names = GetterNames<typeof MyPlugin>
* // is equivalent to "getCell" | "getCellValue"
*/
type GetterNames<Plugin extends { getters: readonly string[] }> = Plugin["getters"][number];
/**
* Extract getter methods from a plugin, based on its `getters` static array.
* @example
* class MyPlugin {
* static getters = [
* "getCell",
* "getCellValue",
* ] as const;
* getCell() { ... }
* getCellValue() { ... }
* }
* type MyPluginGetters = PluginGetters<typeof MyPlugin>;
* // MyPluginGetters is equivalent to:
* // {
* // getCell: () => ...,
* // getCellValue: () => ...,
* // }
*/
type PluginGetters<Plugin extends { new (...args: unknown[]): any; getters: readonly string[] }> =
Pick<InstanceType<Plugin>, GetterNames<Plugin>>;
declare module "@spreadsheet" {
/**
* Add getters from custom plugins defined in odoo
*/
interface OdooCoreGetters extends CoreGetters {}
interface OdooCoreGetters extends PluginGetters<typeof GlobalFiltersCorePlugin> {}
interface OdooCoreGetters extends PluginGetters<typeof ListCorePlugin> {}
interface OdooCoreGetters extends PluginGetters<typeof OdooChartCorePlugin> {}
interface OdooCoreGetters extends PluginGetters<typeof ChartOdooMenuPlugin> {}
interface OdooCoreGetters extends PluginGetters<typeof IrMenuPlugin> {}
interface OdooCoreGetters extends PluginGetters<typeof PivotOdooCorePlugin> {}
interface OdooCoreGetters extends PluginGetters<typeof PivotCoreGlobalFilterPlugin> {}
interface OdooGetters extends Getters {}
interface OdooGetters extends OdooCoreGetters {}
interface OdooGetters extends PluginGetters<typeof GlobalFiltersCoreViewPlugin> {}
interface OdooGetters extends PluginGetters<typeof ListCoreViewPlugin> {}
interface OdooGetters extends PluginGetters<typeof OdooChartCoreViewPlugin> {}
interface OdooGetters extends PluginGetters<typeof CurrencyPlugin> {}
interface OdooGetters extends PluginGetters<typeof AccountingPlugin> {}
}

View file

@ -0,0 +1,175 @@
import { Range, RangeData } from "@odoo/o-spreadsheet";
import { DomainListRepr } from "@web/core/domain";
declare module "@spreadsheet" {
export type DateDefaultValue =
| "today"
| "yesterday"
| "last_7_days"
| "last_30_days"
| "last_90_days"
| "month_to_date"
| "last_month"
| "this_month"
| "this_quarter"
| "last_12_months"
| "this_year"
| "year_to_date";
export interface MonthDateValue {
type: "month";
year: number;
month: number; // 1-12
}
export interface QuarterDateValue {
type: "quarter";
year: number;
quarter: number; // 1-4
}
export interface YearDateValue {
type: "year";
year: number;
}
export interface RelativeDateValue {
type: "relative";
period:
| "today"
| "yesterday"
| "last_7_days"
| "last_30_days"
| "last_90_days"
| "month_to_date"
| "last_month"
| "last_12_months"
| "year_to_date";
}
export interface DateRangeValue {
type: "range";
from?: string;
to?: string;
}
export type DateValue =
| MonthDateValue
| QuarterDateValue
| YearDateValue
| RelativeDateValue
| DateRangeValue;
interface SetValue {
operator: "set" | "not set";
}
interface RelationIdsValue {
operator: "in" | "not in" | "child_of";
ids: number[];
}
interface RelationContainsValue {
operator: "ilike" | "not ilike" | "starts with";
strings: string[];
}
interface CurrentUser {
operator: "in" | "not in";
ids: "current_user";
}
export type RelationValue = RelationIdsValue | SetValue | RelationContainsValue;
type RelationDefaultValue = RelationValue | CurrentUser;
interface NumericUnaryValue {
operator: "=" | "!=" | ">" | "<";
targetValue: number;
}
interface NumericRangeValue {
operator: "between";
minimumValue: number;
maximumValue: number;
}
export type NumericValue = NumericUnaryValue | NumericRangeValue;
interface TextInValue {
operator: "in" | "not in";
strings: string[];
}
interface TextContainsValue {
operator: "ilike" | "not ilike" | "starts with";
text: string;
}
export type TextValue = TextInValue | TextContainsValue | SetValue;
interface SelectionInValue {
operator: "in" | "not in";
selectionValues: string[];
}
export interface FieldMatching {
chain: string;
type: string;
offset?: number;
}
export interface TextGlobalFilter {
type: "text";
id: string;
label: string;
rangesOfAllowedValues?: Range[];
defaultValue?: TextValue;
}
export interface SelectionGlobalFilter {
type: "selection";
id: string;
label: string;
resModel: string;
selectionField: string;
defaultValue?: SelectionInValue;
}
export interface CmdTextGlobalFilter extends TextGlobalFilter {
rangesOfAllowedValues?: RangeData[];
}
export interface DateGlobalFilter {
type: "date";
id: string;
label: string;
defaultValue?: DateDefaultValue;
}
export interface RelationalGlobalFilter {
type: "relation";
id: string;
label: string;
modelName: string;
includeChildren: boolean;
defaultValue?: RelationDefaultValue;
domainOfAllowedValues?: DomainListRepr | string;
}
export interface NumericGlobalFilter {
type: "numeric";
id: string;
label: string;
defaultValue?: NumericValue;
}
export interface BooleanGlobalFilter {
type: "boolean";
id: string;
label: string;
defaultValue?: SetValue;
}
export type GlobalFilter = TextGlobalFilter | DateGlobalFilter | RelationalGlobalFilter | BooleanGlobalFilter | SelectionGlobalFilter | NumericGlobalFilter;
export type CmdGlobalFilter = CmdTextGlobalFilter | DateGlobalFilter | RelationalGlobalFilter | BooleanGlobalFilter | SelectionGlobalFilter | NumericGlobalFilter;
}

View file

@ -0,0 +1,10 @@
declare module "@spreadsheet" {
export interface Zone {
bottom: number;
left: number;
right: number;
top: number;
}
export interface LazyTranslatedString extends String {}
}

View file

@ -0,0 +1,16 @@
declare module "@spreadsheet" {
import { Model } from "@odoo/o-spreadsheet";
export interface OdooSpreadsheetModel extends Model {
getters: OdooGetters;
dispatch: OdooDispatch;
}
export interface OdooSpreadsheetModelConstructor {
new (
data: object,
config: Partial<Model["config"]>,
revisions: object[]
): OdooSpreadsheetModel;
}
}

View file

@ -0,0 +1,75 @@
import { OdooPivotRuntimeDefinition } from "@spreadsheet/pivot/pivot_runtime";
import { ORM } from "@web/core/orm_service";
import { PivotMeasure } from "@spreadsheet/pivot/pivot_runtime";
import { ServerData } from "@spreadsheet/data_sources/server_data";
import { Pivot, CommonPivotCoreDefinition, PivotCoreDefinition } from "@odoo/o-spreadsheet";
declare module "@spreadsheet" {
export interface OdooPivotCoreDefinition extends CommonPivotCoreDefinition {
type: "ODOO";
model: string;
domain: Array;
context: Object;
actionXmlId: string;
}
export type ExtendedPivotCoreDefinition = PivotCoreDefinition | OdooPivotCoreDefinition;
interface OdooPivot<T> extends Pivot<T> {
type: ExtendedPivotCoreDefinition["type"];
}
export interface GFLocalPivot {
id: string;
fieldMatching: Record<string, any>;
}
export interface OdooField {
name: string;
type: string;
string: string;
relation?: string;
searchable?: boolean;
aggregator?: string;
store?: boolean;
}
export type OdooFields = Record<string, Field | undefined>;
export interface PivotMetaData {
colGroupBys: string[];
rowGroupBys: string[];
activeMeasures: string[];
resModel: string;
fields?: Record<string, Field | undefined>;
modelLabel?: string;
fieldAttrs: any;
}
export interface PivotSearchParams {
groupBy: string[];
orderBy: string[];
domain: Array;
context: Object;
}
/* Params used for the odoo pivot model */
export interface WebPivotModelParams {
metaData: PivotMetaData;
searchParams: PivotSearchParams;
}
export interface OdooPivotModelParams {
fields: OdooFields;
definition: OdooPivotRuntimeDefinition;
searchParams: {
domain: Array;
context: Object;
};
}
export interface PivotModelServices {
serverData: ServerData;
orm: ORM;
getters: OdooGetters;
}
}

View file

@ -0,0 +1,29 @@
declare module "@spreadsheet" {
import { CommandResult, CorePlugin, UIPlugin } from "@odoo/o-spreadsheet";
import { CommandResult as CR } from "@spreadsheet/o_spreadsheet/cancelled_reason";
type OdooCommandResult = CommandResult | typeof CR;
export interface OdooCorePlugin extends CorePlugin {
getters: OdooCoreGetters;
dispatch: OdooCoreDispatch;
allowDispatch(command: AllCoreCommand): string | string[];
beforeHandle(command: AllCoreCommand): void;
handle(command: AllCoreCommand): void;
}
export interface OdooCorePluginConstructor {
new (config: unknown): OdooCorePlugin;
}
export interface OdooUIPlugin extends UIPlugin {
getters: OdooGetters;
dispatch: OdooDispatch;
allowDispatch(command: AllCommand): string | string[];
beforeHandle(command: AllCommand): void;
handle(command: AllCommand): void;
}
export interface OdooUIPluginConstructor {
new (config: unknown): OdooUIPlugin;
}
}

View file

@ -0,0 +1,55 @@
/**
* @typedef {import("@web/webclient/actions/action_service").ActionOptions} ActionOptions
*/
/**
* @param {*} env
* @param {string} actionXmlId
* @param {Object} actionDescription
* @param {ActionOptions} options
*/
export async function navigateTo(env, actionXmlId, actionDescription, options) {
const actionService = env.services.action;
let navigateActionDescription;
const { views, view_mode, domain, context, name, res_model, res_id } = actionDescription;
try {
navigateActionDescription = await actionService.loadAction(actionXmlId, context);
const filteredViews = views.map(
([v, viewType]) =>
navigateActionDescription.views.find(([, type]) => viewType === type) || [
v,
viewType,
]
);
navigateActionDescription = {
...navigateActionDescription,
context,
domain,
name,
res_model,
res_id,
view_mode,
target: "current",
views: filteredViews,
};
} catch {
navigateActionDescription = {
type: "ir.actions.act_window",
name,
res_model,
res_id,
views,
target: "current",
domain,
context,
view_mode,
};
} finally {
await actionService.doAction(
// clear empty keys
JSON.parse(JSON.stringify(navigateActionDescription)),
options
);
}
}

View file

@ -0,0 +1,23 @@
import { useSpreadsheetNotificationStore } from "@spreadsheet/hooks";
import { Spreadsheet, Model } from "@odoo/o-spreadsheet";
import { Component } from "@odoo/owl";
/**
* Component wrapping the <Spreadsheet> component from o-spreadsheet
* to add user interactions extensions from odoo such as notifications,
* error dialogs, etc.
*/
export class SpreadsheetComponent extends Component {
static template = "spreadsheet.SpreadsheetComponent";
static components = { Spreadsheet };
static props = {
model: Model,
};
get model() {
return this.props.model;
}
setup() {
useSpreadsheetNotificationStore();
}
}

View file

@ -0,0 +1,5 @@
.o_spreadsheet_container {
flex: 1 1 auto;
overflow: auto;
height: 100%;
}

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<templates>
<div t-name="spreadsheet.SpreadsheetComponent" class="o_spreadsheet_container o_field_highlight">
<Spreadsheet model="model"/>
</div>
</templates>

View file

@ -1,66 +1,46 @@
/** @odoo-module */
import { DataSources } from "@spreadsheet/data_sources/data_sources";
import { migrate } from "@spreadsheet/o_spreadsheet/migration";
import { download } from "@web/core/network/download";
import { registry } from "@web/core/registry";
import spreadsheet from "../o_spreadsheet/o_spreadsheet_extended";
import { createSpreadsheetModel, waitForDataLoaded } from "@spreadsheet/helpers/model";
import { user } from "@web/core/user";
import { _t } from "@web/core/l10n/translation";
const { Model } = spreadsheet;
/**
* @param {import("@web/env").OdooEnv} env
* @param {object} action
*/
async function downloadSpreadsheet(env, action) {
let { orm, name, data, stateUpdateMessages, xlsxData } = action.params;
const canExport = await user.hasGroup("base.group_allow_export");
if (!canExport) {
env.services.notification.add(
_t("You don't have the rights to export data. Please contact an Administrator."),
{
title: _t("Access Error"),
type: "danger",
}
);
return;
}
let { name, data, sources, stateUpdateMessages, xlsxData } = action.params;
if (!xlsxData) {
const dataSources = new DataSources(orm);
const model = new Model(migrate(data), { dataSources }, stateUpdateMessages);
const model = await createSpreadsheetModel({ env, data, revisions: stateUpdateMessages });
await waitForDataLoaded(model);
sources = model.getters.getLoadedDataSources();
xlsxData = model.exportXLSX();
}
await download({
url: "/spreadsheet/xlsx",
data: {
zip_name: `${name}.xlsx`,
files: new Blob([JSON.stringify(xlsxData.files)], { type: "application/json" }),
files: new Blob([JSON.stringify(xlsxData.files)], {
type: "application/json",
}),
datasources: new Blob([JSON.stringify(sources)], {
type: "application/json",
}),
},
});
}
/**
* Ensure that the spreadsheet does not contains cells that are in loading state
* @param {Model} model
* @returns {Promise<void>}
*/
export async function waitForDataLoaded(model) {
const dataSources = model.config.dataSources;
return new Promise((resolve, reject) => {
function check() {
model.dispatch("EVALUATE_CELLS");
if (isLoaded(model)) {
dataSources.removeEventListener("data-source-updated", check);
resolve();
}
}
dataSources.addEventListener("data-source-updated", check);
check();
});
}
function isLoaded(model) {
for (const sheetId of model.getters.getSheetIds()) {
for (const cell of Object.values(model.getters.getCells(sheetId))) {
if (
cell.evaluated &&
cell.evaluated.type === "error" &&
cell.evaluated.error.message === _t("Data is loading")
) {
return false;
}
}
}
return true;
}
registry
.category("actions")
.add("action_download_spreadsheet", downloadSpreadsheet, { force: true });

View file

@ -1,25 +0,0 @@
/** @odoo-module */
import { _lt } from "@web/core/l10n/translation";
export const FILTER_DATE_OPTION = {
quarter: ["first_quarter", "second_quarter", "third_quarter", "fourth_quarter"],
year: ["this_year", "last_year", "antepenultimate_year"],
};
// TODO Remove this mapping, We should only need number > description to avoid multiple conversions
// This would require a migration though
export const monthsOptions = [
{ id: "january", description: _lt("January") },
{ id: "february", description: _lt("February") },
{ id: "march", description: _lt("March") },
{ id: "april", description: _lt("April") },
{ id: "may", description: _lt("May") },
{ id: "june", description: _lt("June") },
{ id: "july", description: _lt("July") },
{ id: "august", description: _lt("August") },
{ id: "september", description: _lt("September") },
{ id: "october", description: _lt("October") },
{ id: "november", description: _lt("November") },
{ id: "december", description: _lt("December") },
];

View file

@ -1,48 +1,49 @@
/** @odoo-module **/
import { _t } from "@web/core/l10n/translation";
import { registry } from "@web/core/registry";
import { getBundle, loadBundle } from "@web/core/assets";
import { sprintf } from "@web/core/utils/strings";
import { loadBundle } from "@web/core/assets";
const actionRegistry = registry.category("actions");
/**
*
* @param {object} env
* Add a new function client action which loads the spreadsheet bundle, then
* launch the actual action.
* The action should be redefine in the bundle with `{ force: true }`
* and the actual action component or function
* @param {string} actionName
* @param {function} actionLazyLoader
* @param {string} [path]
* @param {string} [displayName]
*/
export async function loadSpreadsheetAction(env, actionName, actionLazyLoader) {
const desc = await getBundle("spreadsheet.o_spreadsheet");
await loadBundle(desc);
export function addSpreadsheetActionLazyLoader(actionName, path, displayName) {
const actionLazyLoader = async (env, action) => {
// load the bundle which should redefine the action in the registry
await loadBundle("spreadsheet.o_spreadsheet");
if (actionRegistry.get(actionName) === actionLazyLoader) {
// At this point, the real spreadsheet client action should be loaded and have
// replaced this function in the action registry. If it's not the case,
// it probably means that there was a crash in the bundle (e.g. syntax
// error). In this case, this action will remain in the registry, which
// will lead to an infinite loop. To prevent that, we push another action
// in the registry.
actionRegistry.add(
actionName,
() => {
const msg = sprintf(env._t("%s couldn't be loaded"), actionName);
env.services.notification.add(msg, { type: "danger" });
},
{ force: true }
);
if (actionRegistry.get(actionName) === actionLazyLoader) {
// At this point, the real spreadsheet client action should be loaded and have
// replaced this function in the action registry. If it's not the case,
// it probably means that there was a crash in the bundle (e.g. syntax
// error). In this case, this action will remain in the registry, which
// will lead to an infinite loop. To prevent that, we push another action
// in the registry.
actionRegistry.add(
actionName,
() => {
const msg = _t("%s couldn't be loaded", actionName);
env.services.notification.add(msg, { type: "danger" });
},
{ force: true }
);
}
// then do the action again, with the actual definition registered
return action;
};
if (path) {
actionLazyLoader.path = path;
}
if (displayName) {
actionLazyLoader.displayName = displayName;
}
actionRegistry.add(actionName, actionLazyLoader);
}
const loadSpreadsheetDownloadAction = async (env, context) => {
await loadSpreadsheetAction(env, "action_download_spreadsheet", loadSpreadsheetDownloadAction);
return {
...context,
target: "current",
tag: "action_download_spreadsheet",
type: "ir.actions.client",
};
};
actionRegistry.add("action_download_spreadsheet", loadSpreadsheetDownloadAction);
addSpreadsheetActionLazyLoader("action_download_spreadsheet");

View file

@ -0,0 +1,19 @@
import { registry } from "@web/core/registry";
import { BinaryField, binaryField } from "@web/views/fields/binary/binary_field";
export class SpreadsheetBinaryField extends BinaryField {
static template = "spreadsheet.SpreadsheetBinaryField";
setup() {
super.setup();
}
async onFileDownload() {}
}
export const spreadsheetBinaryField = {
...binaryField,
component: SpreadsheetBinaryField,
};
registry.category("fields").add("binary_spreadsheet", spreadsheetBinaryField);

Some files were not shown because too many files have changed in this diff Show more