Initial commit: Report packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:51 +02:00
commit bc5e1e9efa
604 changed files with 474102 additions and 0 deletions

12
README.md Normal file
View file

@ -0,0 +1,12 @@
# Report
This repository contains OCA OCB packages for report.
## Packages Included
- odoo-bringout-oca-ocb-spreadsheet
- odoo-bringout-oca-ocb-spreadsheet_dashboard
- odoo-bringout-oca-ocb-spreadsheet_dashboard_hr_expense
- odoo-bringout-oca-ocb-spreadsheet_dashboard_hr_timesheet
- odoo-bringout-oca-ocb-spreadsheet_dashboard_pos_hr
- odoo-bringout-oca-ocb-spreadsheet_dashboard_purchase

View file

@ -0,0 +1,47 @@
# Spreadsheet
Spreadsheet
## Installation
```bash
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
## Source
Based on [OCA/OCB](https://github.com/OCA/OCB) branch 16.0, addon `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

View file

@ -0,0 +1,32 @@
# Architecture
```mermaid
flowchart TD
U[Users] -->|HTTP| V[Views and QWeb Templates]
V --> C[Controllers]
V --> W[Wizards Transient Models]
C --> M[Models and ORM]
W --> M
M --> R[Reports]
DX[Data XML] --> M
S[Security ACLs and Groups] -. enforces .-> M
subgraph Spreadsheet Module - spreadsheet
direction LR
M:::layer
W:::layer
C:::layer
V:::layer
R:::layer
S:::layer
DX:::layer
end
classDef layer fill:#eef8ff,stroke:#6ea8fe,stroke-width:1px
```
Notes
- Views include tree/form/kanban templates and report templates.
- Controllers provide website/portal routes when present.
- Wizards are UI flows implemented with `models.TransientModel`.
- Data XML loads data/demo records; Security defines groups and access.

View file

@ -0,0 +1,3 @@
# Configuration
Refer to Odoo settings for spreadsheet. Configure related models, access rights, and options as needed.

View file

@ -0,0 +1,3 @@
# Controllers
This module does not define custom HTTP controllers.

View file

@ -0,0 +1,6 @@
# Dependencies
This addon depends on:
- [bus](../../odoo-bringout-oca-ocb-bus)
- [web](../../odoo-bringout-oca-ocb-web)

View file

@ -0,0 +1,4 @@
# FAQ
- Q: Which Odoo version? A: 16.0 (OCA/OCB packaged).
- Q: How to enable? A: Start server with --addon spreadsheet or install in UI.

View file

@ -0,0 +1,7 @@
# Install
```bash
pip install odoo-bringout-oca-ocb-spreadsheet"
# or
uv pip install odoo-bringout-oca-ocb-spreadsheet"
```

View file

@ -0,0 +1,13 @@
# Models
Detected core models and extensions in spreadsheet.
```mermaid
classDiagram
class res_currency
class res_currency_rate
```
Notes
- Classes show model technical names; fields omitted for brevity.
- Items listed under _inherit are extensions of existing models.

View file

@ -0,0 +1,6 @@
# Overview
Packaged Odoo addon: spreadsheet. Provides features documented in upstream Odoo 16 under this addon.
- Source: OCA/OCB 16.0, addon spreadsheet
- License: LGPL-3

View file

@ -0,0 +1,3 @@
# Reports
This module does not define custom reports.

View file

@ -0,0 +1,8 @@
# Security
This module does not define custom security rules or access controls beyond Odoo defaults.
Default Odoo security applies:
- Base user access through standard groups
- Model access inherited from dependencies
- No custom row-level security rules

View file

@ -0,0 +1,5 @@
# Troubleshooting
- Ensure Python and Odoo environment matches repo guidance.
- Check database connectivity and logs if startup fails.
- Validate that dependent addons listed in DEPENDENCIES.md are installed.

View file

@ -0,0 +1,7 @@
# Usage
Start Odoo including this addon (from repo root):
```bash
python3 scripts/nix_odoo_web_server.py --db-name mydb --addon spreadsheet
```

View file

@ -0,0 +1,3 @@
# Wizards
This module does not include UI wizards.

View file

@ -0,0 +1,43 @@
[project]
name = "odoo-bringout-oca-ocb-spreadsheet"
version = "16.0.0"
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",
"requests>=2.25.1"
]
readme = "README.md"
requires-python = ">= 3.11"
classifiers = [
"Development Status :: 5 - Production/Stable",
"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.12",
"Topic :: Office/Business",
]
[project.urls]
homepage = "https://github.com/bringout/0"
repository = "https://github.com/bringout/0"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.metadata]
allow-direct-references = true
[tool.hatch.build.targets.wheel]
packages = ["spreadsheet"]
[tool.rye]
managed = true
dev-dependencies = [
"pytest>=8.4.1",
]

View file

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

View file

@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': "Spreadsheet",
'version': '1.0',
'category': 'Hidden',
'summary': 'Spreadsheet',
'description': 'Spreadsheet',
'depends': ['bus', 'web'],
'data': [],
'demo': [],
'installable': True,
'auto_install': False,
'license': 'LGPL-3',
'assets': {
'spreadsheet.o_spreadsheet': [
'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/**/*')
],
'web.assets_backend': [
'spreadsheet/static/src/**/*.scss',
'spreadsheet/static/src/assets_backend/**/*',
('remove', 'spreadsheet/static/src/**/*.dark.scss'),
],
"web.dark_mode_assets_backend": [
'spreadsheet/static/src/**/*.dark.scss',
],
'web.qunit_suite_tests': [
'spreadsheet/static/tests/**/*',
('include', 'spreadsheet.o_spreadsheet')
]
}
}

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

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

View file

@ -0,0 +1,54 @@
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.model
def get_company_currency_for_spreadsheet(self, company_id=None):
"""
Returns the currency structure for the currency of the company.
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
: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
if not company.exists():
return False
currency = company.currency_id
return {
"code": currency.name,
"symbol": currency.symbol,
"decimalPlaces": currency.decimal_places,
"position": currency.position,
}

View file

@ -0,0 +1,29 @@
from odoo import api, fields, models
class ResCurrencyRate(models.Model):
_inherit = "res.currency.rate"
@api.model
def _get_rate_for_spreadsheet(self, currency_from_code, currency_to_code, date=None):
if not currency_from_code or not currency_to_code:
return False
Currency = self.env["res.currency"].with_context({"active_test": False})
currency_from = Currency.search([("name", "=", currency_from_code)])
currency_to = Currency.search([("name", "=", currency_to_code)])
if not currency_from or not currency_to:
return False
company = 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.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")),
})
result.append(record)
return result

View file

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 38 38"><defs><style>.cls-1{fill:#429646;}.cls-1,.cls-2{fill-rule:evenodd;}.cls-2{fill:#fff;fill-opacity:0.93;}.cls-3,.cls-4{fill:none;stroke:#429646;}.cls-3{stroke-width:2px;}.cls-4{stroke-miterlimit:10;stroke-width:3px;}</style></defs><title>E1</title><polygon class="cls-1" points="34 2 34 34 4 34 34 2"/><path id="pdf-a" class="cls-2" d="M6,1H32a3,3,0,0,1,3,3V33a3,3,0,0,1-3,3H6a3,3,0,0,1-3-3V4A3,3,0,0,1,6,1Z"/><path class="cls-3" d="M7,2H31a3,3,0,0,1,3,3V32a3,3,0,0,1-3,3H7a3,3,0,0,1-3-3V5A3,3,0,0,1,7,2Z"/><line class="cls-4" x1="9.26" y1="26.6" x2="14.22" y2="26.6"/><line class="cls-4" x1="9.26" y1="21.28" x2="14.22" y2="21.28"/><line class="cls-4" x1="9.26" y1="15.95" x2="14.22" y2="15.95"/><line class="cls-4" x1="9.26" y1="10.62" x2="14.22" y2="10.62"/><line class="cls-4" x1="16.52" y1="26.6" x2="21.48" y2="26.6"/><line class="cls-4" x1="16.52" y1="21.28" x2="21.48" y2="21.28"/><line class="cls-4" x1="16.52" y1="15.95" x2="21.48" y2="15.95"/><line class="cls-4" x1="16.52" y1="10.62" x2="21.48" y2="10.62"/><line class="cls-4" x1="23.77" y1="26.6" x2="28.73" y2="26.6"/><line class="cls-4" x1="23.77" y1="21.28" x2="28.73" y2="21.28"/><line class="cls-4" x1="23.77" y1="15.95" x2="28.73" y2="15.95"/><line class="cls-4" x1="23.77" y1="10.62" x2="28.73" y2="10.62"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,363 @@
/*!
* 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,66 @@
/** @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 { _t } from "@web/core/l10n/translation";
const { Model } = spreadsheet;
async function downloadSpreadsheet(env, action) {
let { orm, name, data, stateUpdateMessages, xlsxData } = action.params;
if (!xlsxData) {
const dataSources = new DataSources(orm);
const model = new Model(migrate(data), { dataSources }, stateUpdateMessages);
await waitForDataLoaded(model);
xlsxData = model.exportXLSX();
}
await download({
url: "/spreadsheet/xlsx",
data: {
zip_name: `${name}.xlsx`,
files: new Blob([JSON.stringify(xlsxData.files)], { 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

@ -0,0 +1,25 @@
/** @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

@ -0,0 +1,48 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { getBundle, loadBundle } from "@web/core/assets";
import { sprintf } from "@web/core/utils/strings";
const actionRegistry = registry.category("actions");
/**
*
* @param {object} env
* @param {string} actionName
* @param {function} actionLazyLoader
*/
export async function loadSpreadsheetAction(env, actionName, actionLazyLoader) {
const desc = await getBundle("spreadsheet.o_spreadsheet");
await loadBundle(desc);
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 }
);
}
}
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);

View file

@ -0,0 +1,47 @@
/** @odoo-module */
import { OdooViewsDataSource } from "@spreadsheet/data_sources/odoo_views_data_source";
import { _t } from "@web/core/l10n/translation";
import { GraphModel as ChartModel} from "@web/views/graph/graph_model";
export default class ChartDataSource extends OdooViewsDataSource {
/**
* @override
* @param {Object} services Services (see DataSource)
*/
constructor(services, params) {
super(services, params);
}
/**
* @protected
*/
async _load() {
await super._load();
const metaData = {
fieldAttrs: {},
...this._metaData,
};
this._model = new ChartModel(
{
_t,
},
metaData,
{
orm: this._orm,
}
);
await this._model.load(this._searchParams);
}
getData() {
if (!this.isReady()) {
this.load();
return { datasets: [], labels: [] };
}
if (!this._isValid) {
return { datasets: [], labels: [] };
}
return this._model.data;
}
}

View file

@ -0,0 +1,16 @@
/** @odoo-module */
import spreadsheet from "@spreadsheet/o_spreadsheet/o_spreadsheet_extended";
const { chartComponentRegistry } = spreadsheet.registries;
const { ChartJsComponent } = spreadsheet.components;
chartComponentRegistry.add("odoo_bar", ChartJsComponent);
chartComponentRegistry.add("odoo_line", ChartJsComponent);
chartComponentRegistry.add("odoo_pie", ChartJsComponent);
import OdooChartCorePlugin from "./plugins/odoo_chart_core_plugin";
import ChartOdooMenuPlugin from "./plugins/chart_odoo_menu_plugin";
import OdooChartUIPlugin from "./plugins/odoo_chart_ui_plugin";
export { OdooChartCorePlugin, ChartOdooMenuPlugin, OdooChartUIPlugin };

View file

@ -0,0 +1,100 @@
/** @odoo-module */
import spreadsheet from "@spreadsheet/o_spreadsheet/o_spreadsheet_extended";
import { _t } from "@web/core/l10n/translation";
import { OdooChart } from "./odoo_chart";
const { chartRegistry } = spreadsheet.registries;
const { getDefaultChartJsRuntime, chartFontColor, ChartColors } = spreadsheet.helpers;
export class OdooBarChart extends OdooChart {
constructor(definition, sheetId, getters) {
super(definition, sheetId, getters);
this.verticalAxisPosition = definition.verticalAxisPosition;
this.stacked = definition.stacked;
}
getDefinition() {
return {
...super.getDefinition(),
verticalAxisPosition: this.verticalAxisPosition,
stacked: this.stacked,
};
}
}
chartRegistry.add("odoo_bar", {
match: (type) => type === "odoo_bar",
createChart: (definition, sheetId, getters) => new OdooBarChart(definition, sheetId, getters),
getChartRuntime: createOdooChartRuntime,
validateChartDefinition: (validator, definition) =>
OdooBarChart.validateChartDefinition(validator, definition),
transformDefinition: (definition) => OdooBarChart.transformDefinition(definition),
getChartDefinitionFromContextCreation: () => OdooBarChart.getDefinitionFromContextCreation(),
name: _t("Bar"),
});
function createOdooChartRuntime(chart, getters) {
const background = chart.background || "#FFFFFF";
const { datasets, labels } = chart.dataSource.getData();
const chartJsConfig = getBarConfiguration(chart, labels);
const colors = new ChartColors();
for (const { label, data } of datasets) {
const color = colors.next();
const dataset = {
label,
data,
borderColor: color,
backgroundColor: color,
};
chartJsConfig.data.datasets.push(dataset);
}
return { background, chartJsConfig };
}
function getBarConfiguration(chart, labels) {
const fontColor = chartFontColor(chart.background);
const config = getDefaultChartJsRuntime(chart, labels, fontColor);
config.type = chart.type.replace("odoo_", "");
const legend = {
...config.options.legend,
display: chart.legendPosition !== "none",
labels: { fontColor },
};
legend.position = chart.legendPosition;
config.options.legend = legend;
config.options.layout = {
padding: { left: 20, right: 20, top: chart.title ? 10 : 25, bottom: 10 },
};
config.options.scales = {
xAxes: [
{
ticks: {
// x axis configuration
maxRotation: 60,
minRotation: 15,
padding: 5,
labelOffset: 2,
fontColor,
},
},
],
yAxes: [
{
position: chart.verticalAxisPosition,
ticks: {
fontColor,
// y axis configuration
beginAtZero: true, // the origin of the y axis is always zero
},
},
],
};
if (chart.stacked) {
config.options.scales.xAxes[0].stacked = true;
config.options.scales.yAxes[0].stacked = true;
}
return config;
}

View file

@ -0,0 +1,133 @@
/** @odoo-module */
import spreadsheet from "@spreadsheet/o_spreadsheet/o_spreadsheet_extended";
import ChartDataSource from "../data_source/chart_data_source";
const { AbstractChart, CommandResult } = spreadsheet;
/**
* @typedef {import("@web/search/search_model").SearchParams} SearchParams
*
* @typedef MetaData
* @property {Array<Object>} domains
* @property {Array<string>} groupBy
* @property {string} measure
* @property {string} mode
* @property {string} [order]
* @property {string} resModel
* @property {boolean} stacked
*
* @typedef OdooChartDefinition
* @property {string} type
* @property {MetaData} metaData
* @property {SearchParams} searchParams
* @property {string} title
* @property {string} background
* @property {string} legendPosition
*
* @typedef OdooChartDefinitionDataSource
* @property {MetaData} metaData
* @property {SearchParams} searchParams
*
*/
export class OdooChart extends AbstractChart {
/**
* @param {OdooChartDefinition} definition
* @param {string} sheetId
* @param {Object} getters
*/
constructor(definition, sheetId, getters) {
super(definition, sheetId, getters);
this.type = definition.type;
this.metaData = definition.metaData;
this.searchParams = definition.searchParams;
this.legendPosition = definition.legendPosition;
this.background = definition.background;
this.dataSource = undefined;
}
static transformDefinition(definition) {
return definition;
}
static validateChartDefinition(validator, definition) {
return CommandResult.Success;
}
static getDefinitionFromContextCreation() {
throw new Error("It's not possible to convert an Odoo chart to a native chart");
}
/**
* @returns {OdooChartDefinitionDataSource}
*/
getDefinitionForDataSource() {
return {
metaData: {
...this.metaData,
mode: this.type.replace("odoo_", ""),
},
searchParams: this.searchParams,
};
}
/**
* @returns {OdooChartDefinition}
*/
getDefinition() {
return {
//@ts-ignore Defined in the parent class
title: this.title,
background: this.background,
legendPosition: this.legendPosition,
metaData: this.metaData,
searchParams: this.searchParams,
type: this.type,
};
}
getDefinitionForExcel() {
// Export not supported
return undefined;
}
/**
* @returns {OdooChart}
*/
updateRanges() {
// No range on this graph
return this;
}
/**
* @returns {OdooChart}
*/
copyForSheetId() {
return this;
}
/**
* @returns {OdooChart}
*/
copyInSheetId() {
return this;
}
getContextCreation() {
return {};
}
getSheetIdsUsedInChartRanges() {
return [];
}
setDataSource(dataSource) {
if (dataSource instanceof ChartDataSource) {
this.dataSource = dataSource;
}
else {
throw new Error("Only ChartDataSources can be added.");
}
}
}

View file

@ -0,0 +1,135 @@
/** @odoo-module */
import spreadsheet from "@spreadsheet/o_spreadsheet/o_spreadsheet_extended";
import { _t } from "@web/core/l10n/translation";
import { OdooChart } from "./odoo_chart";
import { LINE_FILL_TRANSPARENCY } from "@web/views/graph/graph_renderer";
const { chartRegistry } = spreadsheet.registries;
const {
getDefaultChartJsRuntime,
chartFontColor,
ChartColors,
getFillingMode,
colorToRGBA,
rgbaToHex,
} = spreadsheet.helpers;
export class OdooLineChart extends OdooChart {
constructor(definition, sheetId, getters) {
super(definition, sheetId, getters);
this.verticalAxisPosition = definition.verticalAxisPosition;
this.stacked = definition.stacked;
this.cumulative = definition.cumulative;
}
getDefinition() {
return {
...super.getDefinition(),
verticalAxisPosition: this.verticalAxisPosition,
stacked: this.stacked,
cumulative: this.cumulative,
};
}
}
chartRegistry.add("odoo_line", {
match: (type) => type === "odoo_line",
createChart: (definition, sheetId, getters) => new OdooLineChart(definition, sheetId, getters),
getChartRuntime: createOdooChartRuntime,
validateChartDefinition: (validator, definition) =>
OdooLineChart.validateChartDefinition(validator, definition),
transformDefinition: (definition) => OdooLineChart.transformDefinition(definition),
getChartDefinitionFromContextCreation: () => OdooLineChart.getDefinitionFromContextCreation(),
name: _t("Line"),
});
function createOdooChartRuntime(chart, getters) {
const background = chart.background || "#FFFFFF";
const { datasets, labels } = chart.dataSource.getData();
const chartJsConfig = getLineConfiguration(chart, labels);
const colors = new ChartColors();
for (let [index, { label, data }] of datasets.entries()) {
const color = colors.next();
const backgroundRGBA = colorToRGBA(color);
if (chart.stacked) {
// use the transparency of Odoo to keep consistency
backgroundRGBA.a = LINE_FILL_TRANSPARENCY;
}
if (chart.cumulative) {
let accumulator = 0;
data = data.map((value) => {
accumulator += value;
return accumulator;
});
}
const backgroundColor = rgbaToHex(backgroundRGBA);
const dataset = {
label,
data,
lineTension: 0,
borderColor: color,
backgroundColor,
pointBackgroundColor: color,
fill: chart.stacked ? getFillingMode(index) : false,
};
chartJsConfig.data.datasets.push(dataset);
}
return { background, chartJsConfig };
}
function getLineConfiguration(chart, labels) {
const fontColor = chartFontColor(chart.background);
const config = getDefaultChartJsRuntime(chart, labels, fontColor);
config.type = chart.type.replace("odoo_", "");
const legend = {
...config.options.legend,
display: chart.legendPosition !== "none",
labels: {
fontColor,
generateLabels(chart) {
const { data } = chart;
const labels = window.Chart.defaults.global.legend.labels.generateLabels(chart);
for (const [index, label] of labels.entries()) {
label.fillStyle = data.datasets[index].borderColor;
}
return labels;
},
},
};
legend.position = chart.legendPosition;
config.options.legend = legend;
config.options.layout = {
padding: { left: 20, right: 20, top: chart.title ? 10 : 25, bottom: 10 },
};
config.options.scales = {
xAxes: [
{
ticks: {
// x axis configuration
maxRotation: 60,
minRotation: 15,
padding: 5,
labelOffset: 2,
fontColor,
},
},
],
yAxes: [
{
position: chart.verticalAxisPosition,
ticks: {
fontColor,
// y axis configuration
beginAtZero: true, // the origin of the y axis is always zero
},
},
],
};
if (chart.stacked) {
config.options.scales.yAxes[0].stacked = true;
}
return config;
}

View file

@ -0,0 +1,72 @@
/** @odoo-module */
import spreadsheet from "@spreadsheet/o_spreadsheet/o_spreadsheet_extended";
import { _t } from "@web/core/l10n/translation";
import { OdooChart } from "./odoo_chart";
const { chartRegistry } = spreadsheet.registries;
const { getDefaultChartJsRuntime, chartFontColor, ChartColors } = spreadsheet.helpers;
chartRegistry.add("odoo_pie", {
match: (type) => type === "odoo_pie",
createChart: (definition, sheetId, getters) => new OdooChart(definition, sheetId, getters),
getChartRuntime: createOdooChartRuntime,
validateChartDefinition: (validator, definition) =>
OdooChart.validateChartDefinition(validator, definition),
transformDefinition: (definition) => OdooChart.transformDefinition(definition),
getChartDefinitionFromContextCreation: () => OdooChart.getDefinitionFromContextCreation(),
name: _t("Pie"),
});
function createOdooChartRuntime(chart, getters) {
const background = chart.background || "#FFFFFF";
const { datasets, labels } = chart.dataSource.getData();
const chartJsConfig = getPieConfiguration(chart, labels);
const colors = new ChartColors();
for (const { label, data } of datasets) {
const backgroundColor = getPieColors(colors, datasets);
const dataset = {
label,
data,
borderColor: "#FFFFFF",
backgroundColor,
};
chartJsConfig.data.datasets.push(dataset);
}
return { background, chartJsConfig };
}
function getPieConfiguration(chart, labels) {
const fontColor = chartFontColor(chart.background);
const config = getDefaultChartJsRuntime(chart, labels, fontColor);
config.type = chart.type.replace("odoo_", "");
const legend = {
...config.options.legend,
display: chart.legendPosition !== "none",
labels: { fontColor },
};
legend.position = chart.legendPosition;
config.options.legend = legend;
config.options.layout = {
padding: { left: 20, right: 20, top: chart.title ? 10 : 25, bottom: 10 },
};
config.options.tooltips = {
callbacks: {
title: function (tooltipItems, data) {
return data.datasets[tooltipItems[0].datasetIndex].label;
},
},
};
return config;
}
function getPieColors(colors, dataSetsValues) {
const pieColors = [];
const maxLength = Math.max(...dataSetsValues.map((ds) => ds.data.length));
for (let i = 0; i <= maxLength; i++) {
pieColors.push(colors.next());
}
return pieColors;
}

View file

@ -0,0 +1,28 @@
/** @odoo-module **/
import { patch } from "@web/core/utils/patch";
import spreadsheet from "@spreadsheet/o_spreadsheet/o_spreadsheet_extended";
import { useService } from "@web/core/utils/hooks";
patch(spreadsheet.components.ChartFigure.prototype, "spreadsheet.ChartFigure", {
setup() {
this._super();
this.menuService = useService("menu");
this.actionService = useService("action");
},
async navigateToOdooMenu() {
const menu = this.env.model.getters.getChartOdooMenu(this.props.figure.id);
if (!menu) {
throw new Error(`Cannot find any menu associated with the chart`);
}
await this.actionService.doAction(menu.actionID);
},
get hasOdooMenu() {
return this.env.model.getters.getChartOdooMenu(this.props.figure.id) !== undefined;
},
async onClick() {
if (this.env.isDashboard() && this.hasOdooMenu) {
this.navigateToOdooMenu();
}
},
});

View file

@ -0,0 +1,9 @@
.o-chart-menu {
.o-chart-menu-item {
padding-left: 7px;
}
.o-chart-external-link {
font-size: 15px;
}
}

View file

@ -0,0 +1,16 @@
<odoo>
<div t-name="spreadsheet.ChartFigure" t-inherit="o-spreadsheet-ChartFigure" t-inherit-mode="extension" owl="1">
<xpath expr="//div[hasclass('o-chart-menu-item')]" position="before">
<div
t-if="hasOdooMenu and !env.isDashboard()"
class="o-chart-menu-item o-chart-external-link"
t-on-click="navigateToOdooMenu">
<span class="fa fa-external-link" />
</div>
</xpath>
<xpath expr="//div[hasclass('o-chart-container')]" position="attributes">
<attribute name="t-on-click">() => this.onClick()</attribute>
<attribute name="t-att-role">env.isDashboard() and hasOdooMenu ? "button" : ""</attribute>
</xpath>
</div>
</odoo>

View file

@ -0,0 +1,83 @@
/** @odoo-module */
import spreadsheet from "@spreadsheet/o_spreadsheet/o_spreadsheet_extended";
import { omit } from "@web/core/utils/objects";
const { coreTypes, helpers } = spreadsheet;
const { deepEquals } = helpers;
/** Plugin that link charts with Odoo menus. It can contain either the Id of the odoo menu, or its xml id. */
export default class ChartOdooMenuPlugin extends spreadsheet.CorePlugin {
constructor() {
super(...arguments);
this.odooMenuReference = {};
}
/**
* Handle a spreadsheet command
* @param {Object} cmd Command
*/
handle(cmd) {
switch (cmd.type) {
case "LINK_ODOO_MENU_TO_CHART":
this.history.update("odooMenuReference", cmd.chartId, cmd.odooMenuId);
break;
case "DELETE_FIGURE":
this.history.update("odooMenuReference", cmd.id, undefined);
break;
case "DUPLICATE_SHEET":
this.updateOnDuplicateSheet(cmd.sheetId, cmd.sheetIdTo);
break;
}
}
updateOnDuplicateSheet(sheetIdFrom, sheetIdTo) {
for (const oldChartId of this.getters.getChartIds(sheetIdFrom)) {
if (!this.odooMenuReference[oldChartId]) {
continue;
}
const oldChartDefinition = this.getters.getChartDefinition(oldChartId);
const oldFigure = this.getters.getFigure(sheetIdFrom, oldChartId);
const newChartId = this.getters.getChartIds(sheetIdTo).find((newChartId) => {
const newChartDefinition = this.getters.getChartDefinition(newChartId);
const newFigure = this.getters.getFigure(sheetIdTo, newChartId);
return (
deepEquals(oldChartDefinition, newChartDefinition) &&
deepEquals(omit(newFigure, "id"), omit(oldFigure, "id")) // compare size and position
);
});
if (newChartId) {
this.history.update(
"odooMenuReference",
newChartId,
this.odooMenuReference[oldChartId]
);
}
}
}
/**
* Get odoo menu linked to the chart
*
* @param {string} chartId
* @returns {object | undefined}
*/
getChartOdooMenu(chartId) {
const menuId = this.odooMenuReference[chartId];
return menuId ? this.getters.getIrMenu(menuId) : undefined;
}
import(data) {
if (data.chartOdooMenusReferences) {
this.odooMenuReference = data.chartOdooMenusReferences;
}
}
export(data) {
data.chartOdooMenusReferences = this.odooMenuReference;
}
}
ChartOdooMenuPlugin.modes = ["normal", "headless"];
ChartOdooMenuPlugin.getters = ["getChartOdooMenu"];
coreTypes.add("LINK_ODOO_MENU_TO_CHART");

View file

@ -0,0 +1,258 @@
/** @odoo-module */
import spreadsheet from "../../o_spreadsheet/o_spreadsheet_extended";
import ChartDataSource from "../data_source/chart_data_source";
import { globalFiltersFieldMatchers } from "@spreadsheet/global_filters/plugins/global_filters_core_plugin";
import { sprintf } from "@web/core/utils/strings";
import { _t } from "@web/core/l10n/translation";
import { checkFilterFieldMatching } from "@spreadsheet/global_filters/helpers";
import CommandResult from "../../o_spreadsheet/cancelled_reason";
const { CorePlugin } = spreadsheet;
/**
* @typedef {Object} Chart
* @property {string} dataSourceId
* @property {Object} fieldMatching
*
* @typedef {import("@spreadsheet/global_filters/plugins/global_filters_core_plugin").FieldMatching} FieldMatching
*/
export default class OdooChartCorePlugin extends CorePlugin {
constructor(getters, history, range, dispatch, config, uuidGenerator) {
super(getters, history, range, dispatch, config, uuidGenerator);
this.dataSources = config.dataSources;
/** @type {Object.<string, Chart>} */
this.charts = {};
globalFiltersFieldMatchers["chart"] = {
geIds: () => this.getters.getOdooChartIds(),
getDisplayName: (chartId) => this.getters.getOdooChartDisplayName(chartId),
getTag: async (chartId) => {
const model = await this.getChartDataSource(chartId).getModelLabel();
return sprintf(_t("Chart - %s"), model);
},
getFieldMatching: (chartId, filterId) =>
this.getOdooChartFieldMatching(chartId, filterId),
waitForReady: () => this.getOdooChartsWaitForReady(),
getModel: (chartId) =>
this.getters.getChart(chartId).getDefinitionForDataSource().metaData.resModel,
getFields: (chartId) => this.getChartDataSource(chartId).getFields(),
};
}
allowDispatch(cmd) {
switch (cmd.type) {
case "ADD_GLOBAL_FILTER":
case "EDIT_GLOBAL_FILTER":
if (cmd.chart) {
return checkFilterFieldMatching(cmd.chart);
}
}
return CommandResult.Success;
}
/**
* Handle a spreadsheet command
*
* @param {Object} cmd Command
*/
handle(cmd) {
switch (cmd.type) {
case "CREATE_CHART": {
switch (cmd.definition.type) {
case "odoo_pie":
case "odoo_bar":
case "odoo_line":
this._addOdooChart(cmd.id);
break;
}
break;
}
case "UPDATE_CHART": {
switch (cmd.definition.type) {
case "odoo_pie":
case "odoo_bar":
case "odoo_line":
this._setChartDataSource(cmd.id);
break;
}
break;
}
case "DELETE_FIGURE": {
const charts = { ...this.charts };
delete charts[cmd.id];
this.history.update("charts", charts);
break;
}
case "REMOVE_GLOBAL_FILTER":
this._onFilterDeletion(cmd.id);
break;
case "ADD_GLOBAL_FILTER":
case "EDIT_GLOBAL_FILTER":
if (cmd.chart) {
this._setOdooChartFieldMatching(cmd.filter.id, cmd.chart);
}
break;
}
}
// -------------------------------------------------------------------------
// Getters
// -------------------------------------------------------------------------
/**
* Get all the odoo chart ids
* @returns {Array<string>}
*/
getOdooChartIds() {
const ids = [];
for (const sheetId of this.getters.getSheetIds()) {
ids.push(
...this.getters
.getChartIds(sheetId)
.filter((id) => this.getters.getChartType(id).startsWith("odoo_"))
);
}
return ids;
}
/**
* @param {string} chartId
* @returns {string}
*/
getChartFieldMatch(chartId) {
return this.charts[chartId].fieldMatching;
}
/**
* @param {string} id
* @returns {ChartDataSource|undefined}
*/
getChartDataSource(id) {
const dataSourceId = this.charts[id].dataSourceId;
return this.dataSources.get(dataSourceId);
}
/**
*
* @param {string} chartId
* @returns {string}
*/
getOdooChartDisplayName(chartId) {
return this.getters.getChart(chartId).title;
}
/**
* Import the pivots
*
* @param {Object} data
*/
import(data) {
for (const sheet of data.sheets) {
if (sheet.figures) {
for (const figure of sheet.figures) {
if (figure.tag === "chart" && figure.data.type.startsWith("odoo_")) {
this._addOdooChart(figure.id, figure.data.fieldMatching);
}
}
}
}
}
/**
* Export the pivots
*
* @param {Object} data
*/
export(data) {
for (const sheet of data.sheets) {
if (sheet.figures) {
for (const figure of sheet.figures) {
if (figure.tag === "chart" && figure.data.type.startsWith("odoo_")) {
figure.data.fieldMatching = this.getChartFieldMatch(figure.id);
}
}
}
}
}
// -------------------------------------------------------------------------
// Private
// -------------------------------------------------------------------------
/**
*
* @return {Promise[]}
*/
getOdooChartsWaitForReady() {
return this.getOdooChartIds().map((chartId) =>
this.getChartDataSource(chartId).loadMetadata()
);
}
/**
* Get the current pivotFieldMatching of a chart
*
* @param {string} chartId
* @param {string} filterId
*/
getOdooChartFieldMatching(chartId, filterId) {
return this.charts[chartId].fieldMatching[filterId];
}
/**
* Sets the current pivotFieldMatching of a chart
*
* @param {string} filterId
* @param {Record<string,FieldMatching>} chartFieldMatches
*/
_setOdooChartFieldMatching(filterId, chartFieldMatches) {
const charts = { ...this.charts };
for (const [chartId, fieldMatch] of Object.entries(chartFieldMatches)) {
charts[chartId].fieldMatching[filterId] = fieldMatch;
}
this.history.update("charts", charts);
}
_onFilterDeletion(filterId) {
const charts = { ...this.charts };
for (const chartId in charts) {
this.history.update("charts", chartId, "fieldMatching", filterId, undefined);
}
}
/**
* @param {string} chartId
* @param {string} dataSourceId
*/
_addOdooChart(chartId, fieldMatching = {}) {
const dataSourceId = this.uuidGenerator.uuidv4();
const charts = { ...this.charts };
charts[chartId] = {
dataSourceId,
fieldMatching,
};
const definition = this.getters.getChart(chartId).getDefinitionForDataSource();
if (!this.dataSources.contains(dataSourceId)) {
this.dataSources.add(dataSourceId, ChartDataSource, definition);
}
this.history.update("charts", charts);
this._setChartDataSource(chartId);
}
/**
* Sets the catasource on the corresponding chart
* @param {string} chartId
*/
_setChartDataSource(chartId) {
const chart = this.getters.getChart(chartId);
chart.setDataSource(this.getters.getChartDataSource(chartId));
}
}
OdooChartCorePlugin.getters = [
"getChartDataSource",
"getOdooChartIds",
"getChartFieldMatch",
"getOdooChartDisplayName",
"getOdooChartFieldMatching",
];

View file

@ -0,0 +1,85 @@
/** @odoo-module */
import spreadsheet from "../../o_spreadsheet/o_spreadsheet_extended";
import { Domain } from "@web/core/domain";
const { UIPlugin } = spreadsheet;
export default class OdooChartUIPlugin extends UIPlugin {
beforeHandle(cmd) {
switch (cmd.type) {
case "START":
// make sure the domains are correctly set before
// any evaluation
this._addDomains();
break;
}
}
/**
* Handle a spreadsheet command
*
* @param {Object} cmd Command
*/
handle(cmd) {
switch (cmd.type) {
case "ADD_GLOBAL_FILTER":
case "EDIT_GLOBAL_FILTER":
case "REMOVE_GLOBAL_FILTER":
case "SET_GLOBAL_FILTER_VALUE":
case "CLEAR_GLOBAL_FILTER_VALUE":
this._addDomains();
break;
case "UNDO":
case "REDO":
if (
cmd.commands.find((command) =>
[
"ADD_GLOBAL_FILTER",
"EDIT_GLOBAL_FILTER",
"REMOVE_GLOBAL_FILTER",
].includes(command.type)
)
) {
this._addDomains();
}
break;
}
}
// -------------------------------------------------------------------------
// Private
// -------------------------------------------------------------------------
/**
* Add an additional domain to a chart
*
* @private
*
* @param {string} chartId chart id
*/
_addDomain(chartId) {
const domainList = [];
for (const [filterId, fieldMatch] of Object.entries(
this.getters.getChartFieldMatch(chartId)
)) {
domainList.push(this.getters.getGlobalFilterDomain(filterId, fieldMatch));
}
const domain = Domain.combine(domainList, "AND").toString();
this.getters.getChartDataSource(chartId).addDomain(domain);
}
/**
* Add an additional domain to all chart
*
* @private
*
*/
_addDomains() {
for (const chartId of this.getters.getOdooChartIds()) {
this._addDomain(chartId);
}
}
}
OdooChartUIPlugin.getters = [];

View file

@ -0,0 +1,21 @@
/** @odoo-module */
import spreadsheet from "@spreadsheet/o_spreadsheet/o_spreadsheet_extended";
const { inverseCommandRegistry, otRegistry } = spreadsheet.registries;
function identity(cmd) {
return [cmd];
}
otRegistry.addTransformation(
"DELETE_FIGURE",
["LINK_ODOO_MENU_TO_CHART"],
(toTransform, executed) => {
if (executed.id === toTransform.chartId) {
return undefined;
}
return toTransform;
}
);
inverseCommandRegistry.add("LINK_ODOO_MENU_TO_CHART", identity);

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