[IMP] spreadsheet_dashboard_edition: load dashboard with user locale

Currently, spreadsheet dashboards always display numbers with the en_US
format, no matter the user's lang.

That's because the locale is hardcoded in the spreadsheet source file, in
the source code (or it falls back to the default en_US locale if it's missing).

With this commit, we dynamically change the spreadsheet locale with the user's
locale when he loads a dashboard.

We can change the locale with every user because the dashboard is readonly.
It cannot create a giant mess with dates in various locales.

Limitation
----------
Since hardcoded date formats are not changed when the locale changes, dates
will keep the en_US format. However, most (if not all) dates in dashboards
are coming from ODOO.PIVOT and ODOO.LIST functions, which compute the formats
dynamically based on the current locale.

closes odoo/odoo#135119

Task: 3484002
Related: odoo/enterprise#47284
Signed-off-by: Rémi Rahir (rar) <rar@odoo.com>
This commit is contained in:
Lucas Lefèvre (lul)
2023-10-11 17:01:57 +00:00
parent b99c147b63
commit aa118d7dc0
8 changed files with 81 additions and 41 deletions
@@ -68,7 +68,8 @@ class SpreadsheetMixin(models.AbstractModel):
],
"settings": {
"locale": locale,
}
},
"revisionId": "START_REVISION",
}
def _zip_xslx_files(self, files):
@@ -1,5 +1,8 @@
import json
from odoo import fields, models
class SpreadsheetDashboard(models.Model):
_name = 'spreadsheet.dashboard'
_description = 'Spreadsheet Dashboard'
@@ -10,3 +13,14 @@ class SpreadsheetDashboard(models.Model):
dashboard_group_id = fields.Many2one('spreadsheet.dashboard.group', required=True)
sequence = fields.Integer()
group_ids = fields.Many2many('res.groups', default=lambda self: self.env.ref('base.group_user'))
def get_readonly_dashboard(self):
self.ensure_one()
snapshot = json.loads(self.spreadsheet_data)
user_locale = self.env['res.lang']._get_user_spreadsheet_locale()
snapshot.setdefault('settings', {})['locale'] = user_locale
return {
'snapshot': snapshot,
'revisions': [],
}
@@ -25,9 +25,7 @@ export class SpreadsheetDashboardAction extends Component {
// with the breadcrumb
// TODO write a test
/** @type {DashboardLoader}*/
this.loader = useState(
new DashboardLoader(this.env, this.env.services.orm, this._fetchDashboardData)
);
this.loader = useState(new DashboardLoader(this.env, this.env.services.orm));
onWillStart(async () => {
if (this.props.state && this.props.state.dashboardLoader) {
const { groups, dashboards } = this.props.state.dashboardLoader;
@@ -117,20 +115,6 @@ export class SpreadsheetDashboardAction extends Component {
this.state.activeDashboard = this.loader.getDashboard(dashboardId);
}
/**
* @private
* @param {number} dashboardId
* @returns {Promise<{ data: string, revisions: object[] }>}
*/
async _fetchDashboardData(dashboardId) {
const [record] = await this.orm.read(
"spreadsheet.dashboard",
[dashboardId],
["spreadsheet_data"]
);
return { data: JSON.parse(record.spreadsheet_data), revisions: [] };
}
async shareSpreadsheet(data, excelExport) {
const url = await this.orm.call("spreadsheet.dashboard.share", "action_get_share_url", [
{
@@ -37,8 +37,6 @@ export const Status = {
* @property {string} name
* @property {Array<Dashboard>} dashboards
*
* @typedef {(dashboardId: number) => Promise<{ data: string, revisions: object[] }>} FetchDashboardData
*
* @typedef {import("@web/env").OdooEnv} OdooEnv
*
* @typedef {import("@web/core/orm_service").ORM} ORM
@@ -48,9 +46,8 @@ export class DashboardLoader {
/**
* @param {OdooEnv} env
* @param {ORM} orm
* @param {FetchDashboardData} fetchDashboardData
*/
constructor(env, orm, fetchDashboardData) {
constructor(env, orm) {
/** @private */
this.env = env;
/** @private */
@@ -59,8 +56,6 @@ export class DashboardLoader {
this.groups = [];
/** @private @type {Object<number, Dashboard>} */
this.dashboards = {};
/** @private */
this.fetchDashboardData = fetchDashboardData;
}
/**
@@ -166,8 +161,12 @@ export class DashboardLoader {
const dashboard = this._getDashboard(dashboardId);
dashboard.status = Status.Loading;
try {
const { data, revisions } = await this.fetchDashboardData(dashboardId);
dashboard.model = this._createSpreadsheetModel(data, revisions);
const { snapshot, revisions } = await this.orm.call(
"spreadsheet.dashboard",
"get_readonly_dashboard",
[dashboardId]
);
dashboard.model = this._createSpreadsheetModel(snapshot, revisions);
dashboard.status = Status.Loaded;
} catch (error) {
dashboard.error = error;
@@ -193,14 +192,14 @@ export class DashboardLoader {
/**
* @private
* @param {string} data
* @param {object} snapshot
* @param {object[]} revisions
* @returns {Model}
*/
_createSpreadsheetModel(data, revisions = []) {
_createSpreadsheetModel(snapshot, revisions = []) {
const dataSources = new DataSources(this.env);
const model = new Model(
migrate(data),
migrate(snapshot),
{
custom: { env: this.env, orm: this.orm, dataSources },
mode: "dashboard",
@@ -102,12 +102,8 @@ QUnit.test("display error message", async (assert) => {
mockRPC: function (route, args) {
if (
args.model === "spreadsheet.dashboard" &&
((args.method === "read" &&
args.args[0][0] === 2 &&
args.args[1][0] === "spreadsheet_data") ||
// this is not correct from a module dependency POV but it's required for the test
// to pass when `spreadsheet_dashboard_edition` module is installed
(args.method === "join_spreadsheet_session" && args.args[0] === 2))
args.method === "get_readonly_dashboard" &&
args.args[0] === 2
) {
throw new Error("Bip");
}
@@ -98,7 +98,14 @@ QUnit.test("load spreadsheet data", async (assert) => {
QUnit.test("load spreadsheet data only once", async (assert) => {
const loader = await createDashboardLoader({
mockRPC: function (route, args) {
if (args.method === "read" && args.model === "spreadsheet.dashboard") {
if (args.model === "spreadsheet.dashboard" && args.method === "read") {
// read names
assert.step(`spreadsheet ${args.args[0]} loaded`);
}
if (
args.model === "spreadsheet.dashboard" &&
args.method === "get_readonly_dashboard"
) {
assert.step(`spreadsheet ${args.args[0]} loaded`);
}
},
@@ -159,6 +166,13 @@ QUnit.test("load multiple spreadsheets", async (assert) => {
assert.step("load groups");
}
if (args.method === "read" && args.model === "spreadsheet.dashboard") {
// read names
assert.step(`spreadsheet ${args.args[0]} loaded`);
}
if (
args.model === "spreadsheet.dashboard" &&
args.method === "get_readonly_dashboard"
) {
assert.step(`spreadsheet ${args.args[0]} loaded`);
}
},
@@ -180,9 +194,8 @@ QUnit.test("load spreadsheet data with error", async (assert) => {
const loader = await createDashboardLoader({
mockRPC: function (route, args) {
if (
args.method === "read" &&
args.model === "spreadsheet.dashboard" &&
args.args[1][0] === "spreadsheet_data"
args.method === "get_readonly_dashboard" &&
args.model === "spreadsheet.dashboard"
) {
throw new Error("Bip");
}
@@ -0,0 +1,16 @@
/** @odoo-module */
import { registry } from "@web/core/registry";
registry
.category("mock_server")
.add("spreadsheet.dashboard/get_readonly_dashboard", function (route, args) {
const [id] = args.args;
const dashboard = this.models["spreadsheet.dashboard"].records.find(
(record) => record.id === id
);
return {
snapshot: JSON.parse(dashboard.spreadsheet_data),
revisions: [],
};
});
@@ -1,9 +1,11 @@
import json
from odoo.tests.common import TransactionCase
from odoo.exceptions import UserError
class TestSpreadsheetDashboard(TransactionCase):
from .common import DashboardTestCommon
class TestSpreadsheetDashboard(DashboardTestCommon):
def test_create_with_default_values(self):
group = self.env["spreadsheet.dashboard.group"].create(
{"name": "a group"}
@@ -32,3 +34,18 @@ class TestSpreadsheetDashboard(TransactionCase):
})
with self.assertRaises(UserError, msg="You cannot delete a_group as it is used in another module"):
group.unlink()
def test_load_with_user_locale(self):
dashboard = self.create_dashboard().with_user(self.user)
self.user.lang = "en_US"
data = dashboard.get_readonly_dashboard()
locale = data["snapshot"]["settings"]["locale"]
self.assertEqual(locale["code"], "en_US")
self.assertEqual(len(data["revisions"]), 0)
self.env.ref("base.lang_fr").active = True
self.user.lang = "fr_FR"
data = dashboard.get_readonly_dashboard()
locale = data["snapshot"]["settings"]["locale"]
self.assertEqual(locale["code"], "fr_FR")
self.assertEqual(len(data["revisions"]), 0)