Initial commit: Web packages

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

View file

@ -0,0 +1,100 @@
/** @odoo-module **/
import { _t } from "@web/core/l10n/translation";
import { loadJS } from "@web/core/assets";
import { registry } from "@web/core/registry";
import { formatFloat } from "@web/views/fields/formatters";
import { standardFieldProps } from "@web/views/fields/standard_field_props";
import { Component, onWillStart, useEffect, useRef } from "@odoo/owl";
export class GaugeField extends Component {
setup() {
this.chart = null;
this.canvasRef = useRef("canvas");
onWillStart(() => loadJS("/web/static/lib/Chart/Chart.js"));
useEffect(() => {
this.renderChart();
return () => {
if (this.chart) {
this.chart.destroy();
}
};
});
}
get formattedValue() {
return formatFloat(this.props.value, { humanReadable: true, decimals: 1 });
}
renderChart() {
const gaugeValue = this.props.value;
let maxValue = Math.max(gaugeValue, this.props.record.data[this.props.maxValueField] || this.props.maxValue);
let maxLabel = maxValue;
if (gaugeValue === 0 && maxValue === 0) {
maxValue = 1;
maxLabel = 0;
}
const config = {
type: "doughnut",
data: {
datasets: [
{
data: [gaugeValue, maxValue - gaugeValue],
backgroundColor: ["#1f77b4", "#dddddd"],
label: this.props.title,
},
],
},
options: {
circumference: Math.PI,
rotation: -Math.PI,
responsive: true,
tooltips: {
displayColors: false,
callbacks: {
label: function (tooltipItems) {
if (tooltipItems.index === 0) {
return _t("Value: ") + gaugeValue;
}
return _t("Max: ") + maxLabel;
},
},
},
title: {
display: true,
text: this.props.title,
padding: 4,
},
layout: {
padding: {
bottom: 5,
},
},
maintainAspectRatio: false,
cutoutPercentage: 70,
},
};
this.chart = new Chart(this.canvasRef.el, config);
}
}
GaugeField.template = "web.GaugeField";
GaugeField.props = {
...standardFieldProps,
maxValueField: { type: String },
title: { type: String },
maxValue: { type: Number },
};
GaugeField.extractProps = ({ attrs, field }) => {
return {
maxValueField: attrs.options.max_field || "",
title: attrs.options.title || field.string,
maxValue: attrs.options.max_value || 100,
};
};
registry.category("fields").add("gauge", GaugeField);

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="web.GaugeField" owl="1">
<div class="oe_gauge position-relative">
<canvas t-ref="canvas"/>
<span class="o_gauge_value position-absolute start-0 end-0 bottom-0 text-center" t-esc="props.value"/>
</div>
</t>
</templates>

View file

@ -0,0 +1,130 @@
odoo.define('web_kanban_gauge.widget', function (require) {
"use strict";
var AbstractField = require('web.AbstractField');
var core = require('web.core');
var field_registry = require('web.field_registry');
var utils = require('web.utils');
var _t = core._t;
/**
* options
*
* - max_value: maximum value of the gauge [default: 100]
* - max_field: get the max_value from the field that must be present in the
* view; takes over max_value
* - gauge_value_field: if set, the value displayed below the gauge is taken
* from this field instead of the base field used for
* the gauge. This allows to display a number different
* from the gauge.
* - label: lable of the gauge, displayed below the gauge value
* - label_field: get the label from the field that must be present in the
* view; takes over label
* - title: title of the gauge, displayed on top of the gauge
* - style: custom style
*/
var GaugeWidget = AbstractField.extend({
className: "oe_gauge",
jsLibs: [
'/web/static/lib/Chart/Chart.js',
],
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @override
* @private
*/
_render: function () {
// current value
var val = this.value;
if (_.isArray(JSON.parse(val))) {
val = JSON.parse(val);
}
var gauge_value = _.isArray(val) && val.length ? val[val.length-1].value : val;
if (this.nodeOptions.gauge_value_field) {
gauge_value = this.recordData[this.nodeOptions.gauge_value_field];
}
// max_value
var max_value = this.nodeOptions.max_value || 100;
if (this.nodeOptions.max_field) {
max_value = this.recordData[this.nodeOptions.max_field];
}
max_value = Math.max(gauge_value, max_value);
// title
var title = this.nodeOptions.title || this.field.string;
var maxLabel = max_value;
if (gauge_value === 0 && max_value === 0) {
max_value = 1;
maxLabel = 0;
}
var config = {
type: 'doughnut',
data: {
datasets: [{
data: [
gauge_value,
max_value - gauge_value
],
backgroundColor: [
"#1f77b4", "#dddddd"
],
label: title
}],
},
options: {
circumference: Math.PI,
rotation: -Math.PI,
responsive: true,
tooltips: {
displayColors: false,
callbacks: {
label: function(tooltipItems) {
if (tooltipItems.index === 0) {
return _t('Value: ') + gauge_value;
}
return _t('Max: ') + maxLabel;
},
},
},
title: {
display: true,
text: title,
padding: 4,
},
layout: {
padding: {
bottom: 5
}
},
maintainAspectRatio: false,
cutoutPercentage: 70,
}
};
this.$canvas = $('<canvas/>');
this.$el.empty();
this.$el.append(this.$canvas);
this.$el.attr('style', this.nodeOptions.style);
this.$el.css({position: 'relative'});
var context = this.$canvas[0].getContext('2d');
this.chart = new Chart(context, config);
var humanValue = utils.human_number(gauge_value, 1);
var $value = $('<span class="o_gauge_value">').text(humanValue);
$value.css({'text-align': 'center', position: 'absolute', left: 0, right: 0, bottom: '6px', 'font-weight': 'bold'});
this.$el.append($value);
},
});
field_registry.add("gauge", GaugeWidget);
return GaugeWidget;
});

View file

@ -0,0 +1,63 @@
/** @odoo-module **/
import { getFixture, getNodesTextContent } from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
let serverData;
let target;
QUnit.module("Fields", (hooks) => {
hooks.beforeEach(() => {
target = getFixture();
serverData = {
models: {
partner: {
fields: {
int_field: {
string: "int_field",
type: "integer",
},
another_int_field: {
string: "another_int_field",
type: "integer",
},
},
records: [
{ id: 1, int_field: 10, another_int_field: 45 },
{ id: 2, int_field: 4, another_int_field: 10 },
],
},
},
};
setupViewRegistries();
});
QUnit.module("GaugeField");
QUnit.test("GaugeField in kanban view", async function (assert) {
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: `
<kanban>
<field name="another_int_field"/>
<templates>
<t t-name="kanban-box">
<div>
<field name="int_field" widget="gauge" options="{'max_field': 'another_int_field'}"/>
</div>
</t>
</templates>
</kanban>`,
});
assert.containsN(target, ".o_kanban_record:not(.o_kanban_ghost)", 2);
assert.containsN(target, ".o_field_widget[name=int_field] .oe_gauge canvas", 2);
assert.deepEqual(getNodesTextContent(target.querySelectorAll(".o_gauge_value")), [
"10",
"4",
]);
});
});

View file

@ -0,0 +1,49 @@
odoo.define('web_kanban_gauge.gauge_tests', function (require) {
"use strict";
var KanbanView = require('web.KanbanView');
var testUtils = require('web.test_utils');
var createView = testUtils.createView;
QUnit.module('fields', {}, function () {
QUnit.module('basic_fields', {
beforeEach: function () {
this.data = {
partner: {
fields: {
int_field: {string: "int_field", type: "integer", sortable: true},
},
records: [
{id: 1, int_field: 10},
{id: 2, int_field: 4},
]
},
};
}
}, function () {
QUnit.module('gauge widget');
QUnit.test('basic rendering', async function (assert) {
assert.expect(1);
var kanban = await createView({
View: KanbanView,
model: 'partner',
data: this.data,
arch: '<kanban><templates><t t-name="kanban-box">' +
'<div><field name="int_field" widget="gauge"/></div>' +
'</t></templates></kanban>',
});
assert.containsOnce(kanban, '.o_kanban_record:first .oe_gauge canvas',
"should render the gauge widget");
kanban.destroy();
});
});
});
});

View file

@ -0,0 +1,57 @@
/** @odoo-module **/
import { getFixture, getNodesTextContent } from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
let serverData;
let target;
QUnit.module("Fields", (hooks) => {
hooks.beforeEach(() => {
target = getFixture();
serverData = {
models: {
partner: {
fields: {
int_field: {
string: "int_field",
type: "integer",
},
},
records: [
{ id: 1, int_field: 10 },
],
},
},
};
setupViewRegistries();
});
QUnit.module("GaugeValue");
QUnit.test("GaugeValue in kanban view", async function (assert) {
await makeView({
type: "kanban",
resModel: "partner",
serverData,
arch: `
<kanban>
<templates>
<t t-name="kanban-box">
<div>
<field name="int_field" widget="gauge" options="{'max_value': 120}"/>
<field name="int_field" widget="gauge"/>
</div>
</t>
</templates>
</kanban>`,
});
assert.containsN(target, ".o_field_widget[name=int_field] .oe_gauge canvas", 2);
assert.deepEqual(getNodesTextContent(target.querySelectorAll(".o_gauge_value")), [
"10",
"10",
]);
});
});