Initial commit: Odoo 18.0-20251222 extra-addons
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
import {Component} from "@odoo/owl";
|
||||
import {X2Many2DMatrixRenderer} from "@web_widget_x2many_2d_matrix/components/x2many_2d_matrix_renderer/x2many_2d_matrix_renderer.esm";
|
||||
import {exprToBoolean} from "@web/core/utils/strings";
|
||||
import {registry} from "@web/core/registry";
|
||||
import {standardFieldProps} from "@web/views/fields/standard_field_props";
|
||||
|
||||
export class X2Many2DMatrixField extends Component {
|
||||
setup() {
|
||||
this.activeField = this.props.record.activeFields[this.props.name];
|
||||
}
|
||||
|
||||
getList() {
|
||||
return this.props.record.data[this.props.name];
|
||||
}
|
||||
|
||||
get list() {
|
||||
return this.getList();
|
||||
}
|
||||
|
||||
getListView() {
|
||||
return this.props.list_view;
|
||||
}
|
||||
|
||||
get list_view() {
|
||||
return this.getListView();
|
||||
}
|
||||
}
|
||||
|
||||
X2Many2DMatrixField.template = "web_widget_x2many_2d_matrix.X2Many2DMatrixField";
|
||||
X2Many2DMatrixField.props = {
|
||||
...standardFieldProps,
|
||||
list_view: {type: Object, optional: false},
|
||||
matrixFields: {type: Object, optional: false},
|
||||
isXClickable: {type: Boolean, optional: true},
|
||||
isYClickable: {type: Boolean, optional: true},
|
||||
showRowTotals: {type: Boolean, optional: true},
|
||||
showColumnTotals: {type: Boolean, optional: true},
|
||||
};
|
||||
|
||||
X2Many2DMatrixField.components = {X2Many2DMatrixRenderer};
|
||||
export const x2Many2DMatrixField = {
|
||||
component: X2Many2DMatrixField,
|
||||
extractProps({attrs, views}) {
|
||||
return {
|
||||
list_view: views.list,
|
||||
matrixFields: {
|
||||
value: attrs.field_value,
|
||||
x: attrs.field_x_axis,
|
||||
y: attrs.field_y_axis,
|
||||
},
|
||||
isXClickable: exprToBoolean(attrs.x_axis_clickable),
|
||||
isYClickable: exprToBoolean(attrs.y_axis_clickable),
|
||||
showRowTotals:
|
||||
"show_row_totals" in attrs
|
||||
? exprToBoolean(attrs.show_row_totals)
|
||||
: true,
|
||||
showColumnTotals:
|
||||
"show_column_totals" in attrs
|
||||
? exprToBoolean(attrs.show_column_totals)
|
||||
: true,
|
||||
};
|
||||
},
|
||||
};
|
||||
registry.category("fields").add("x2many_2d_matrix", x2Many2DMatrixField);
|
||||
@@ -0,0 +1,71 @@
|
||||
$x2many_2d_matrix_max_height: 450px;
|
||||
|
||||
.o_form_view .o_field_x2many_2d_matrix {
|
||||
.table-responsive {
|
||||
max-height: $x2many_2d_matrix_max_height;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.o_input {
|
||||
padding: 1px 0px;
|
||||
}
|
||||
|
||||
.table {
|
||||
> thead > tr > th {
|
||||
white-space: pre-line;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
|
||||
&.total {
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
> tbody {
|
||||
> tr {
|
||||
&:nth-of-type(2n + 1) td.row-total,
|
||||
&:nth-of-type(2n) td.row-total,
|
||||
> td {
|
||||
text-align: right;
|
||||
|
||||
.o_field_many2one_selection {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
text-align: left;
|
||||
border-right-width: 1px;
|
||||
border-right-color: $gray-300;
|
||||
border-right-style: solid;
|
||||
}
|
||||
&.row-total {
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-weight: bold;
|
||||
position: sticky;
|
||||
right: 0;
|
||||
border-left-width: 1px;
|
||||
border-left-color: $gray-300;
|
||||
border-left-style: solid;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> tfoot > tr > th {
|
||||
text-align: right;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
|
||||
&.col-total {
|
||||
padding: 0.75rem;
|
||||
right: 0;
|
||||
border-left-width: 1px;
|
||||
border-left-color: $gray-300;
|
||||
border-left-style: solid;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates>
|
||||
<t t-name="web_widget_x2many_2d_matrix.X2Many2DMatrixField">
|
||||
<div class="table-responsive">
|
||||
<X2Many2DMatrixRenderer
|
||||
list="list"
|
||||
list_view="list_view"
|
||||
matrixFields="props.matrixFields"
|
||||
showRowTotals="props.showRowTotals"
|
||||
showColumnTotals="props.showColumnTotals"
|
||||
readonly="props.readonly"
|
||||
isXClickable="props.isXClickable"
|
||||
isYClickable="props.isYClickable"
|
||||
/>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,221 @@
|
||||
import {Component, onWillUpdateProps} from "@odoo/owl";
|
||||
import {evaluateBooleanExpr, evaluateExpr} from "@web/core/py_js/py";
|
||||
import {Domain} from "@web/core/domain";
|
||||
import {Record} from "@web/model/relational_model/record";
|
||||
import {getFieldContext} from "@web/model/relational_model/utils";
|
||||
|
||||
export class X2Many2DMatrixRenderer extends Component {
|
||||
setup() {
|
||||
this.columns = this._getColumns();
|
||||
this.rows = this._getRows();
|
||||
this.matrix = this._getMatrix();
|
||||
|
||||
onWillUpdateProps((newProps) => {
|
||||
this.columns = this._getColumns(newProps.list.records);
|
||||
this.rows = this._getRows(newProps.list.records);
|
||||
this.matrix = this._getMatrix(newProps.list.records);
|
||||
});
|
||||
}
|
||||
|
||||
_getColumns(records = this.list.records) {
|
||||
const columns = [];
|
||||
records.forEach((record) => {
|
||||
const column = {
|
||||
value: record.data[this.matrixFields.x],
|
||||
text: record.data[this.matrixFields.x],
|
||||
rawValue: record.data[this.matrixFields.x],
|
||||
};
|
||||
if (record.fields[this.matrixFields.x].type === "many2one") {
|
||||
column.text = column.value[1];
|
||||
column.value = column.value[0];
|
||||
}
|
||||
if (columns.findIndex((c) => c.value === column.value) !== -1) return;
|
||||
columns.push(column);
|
||||
});
|
||||
return columns;
|
||||
}
|
||||
|
||||
_getRows(records = this.list.records) {
|
||||
const rows = [];
|
||||
records.forEach((record) => {
|
||||
const row = {
|
||||
value: record.data[this.matrixFields.y],
|
||||
text: record.data[this.matrixFields.y],
|
||||
rawValue: record.data[this.matrixFields.y],
|
||||
};
|
||||
if (record.fields[this.matrixFields.y].type === "many2one") {
|
||||
row.text = row.value[1];
|
||||
row.value = row.value[0];
|
||||
}
|
||||
if (rows.findIndex((r) => r.value === row.value) !== -1) return;
|
||||
rows.push(row);
|
||||
});
|
||||
return rows;
|
||||
}
|
||||
|
||||
_getPointOfRecord(record) {
|
||||
let xValue = record.data[this.matrixFields.x];
|
||||
if (record.fields[this.matrixFields.x].type === "many2one") {
|
||||
xValue = xValue[0];
|
||||
}
|
||||
let yValue = record.data[this.matrixFields.y];
|
||||
if (record.fields[this.matrixFields.y].type === "many2one") {
|
||||
yValue = yValue[0];
|
||||
}
|
||||
|
||||
const x = this.columns.findIndex((c) => c.value === xValue);
|
||||
const y = this.rows.findIndex((r) => r.value === yValue);
|
||||
return {x, y};
|
||||
}
|
||||
|
||||
_getMatrix(records = this.list.records) {
|
||||
const matrix = this.rows.map(() =>
|
||||
new Array(this.columns.length).fill(null).map(() => {
|
||||
return {value: 0, records: []};
|
||||
})
|
||||
);
|
||||
records.forEach((record) => {
|
||||
const value = record.data[this.matrixFields.value];
|
||||
const {x, y} = this._getPointOfRecord(record);
|
||||
matrix[y][x].value += value;
|
||||
matrix[y][x].records.push(record);
|
||||
});
|
||||
return matrix;
|
||||
}
|
||||
|
||||
get list() {
|
||||
return this.props.list;
|
||||
}
|
||||
|
||||
get matrixFields() {
|
||||
return this.props.matrixFields;
|
||||
}
|
||||
|
||||
get valueFieldComponent() {
|
||||
return this.props.list_view.fieldNodes[this.matrixFields.value + "_0"].field
|
||||
.component;
|
||||
}
|
||||
|
||||
get xFieldComponent() {
|
||||
return this.props.list_view.fieldNodes[this.matrixFields.x + "_0"].field
|
||||
.component;
|
||||
}
|
||||
|
||||
get yFieldComponent() {
|
||||
return this.props.list_view.fieldNodes[this.matrixFields.y + "_0"].field
|
||||
.component;
|
||||
}
|
||||
|
||||
_aggregateRow(row) {
|
||||
const y = this.rows.findIndex((r) => r.value === row);
|
||||
const total = this.matrix[y].map((r) => r.value).reduce((aggr, x) => aggr + x);
|
||||
return total;
|
||||
}
|
||||
|
||||
_aggregateColumn(column) {
|
||||
const x = this.columns.findIndex((c) => c.value === column);
|
||||
const total = this.matrix
|
||||
.map((r) => r[x])
|
||||
.map((r) => r.value)
|
||||
.reduce((aggr, y) => aggr + y);
|
||||
return total;
|
||||
}
|
||||
|
||||
_aggregateAll() {
|
||||
const total = this.matrix
|
||||
.map((r) => r.map((x) => x.value).reduce((aggr, x) => aggr + x))
|
||||
.reduce((aggr, y) => aggr + y);
|
||||
return total;
|
||||
}
|
||||
|
||||
_canAggregate() {
|
||||
return ["integer", "float", "monetary"].includes(
|
||||
this.list.fields[this.matrixFields.value].type
|
||||
);
|
||||
}
|
||||
|
||||
_getValueFieldProps(column, row) {
|
||||
const x = this.columns.findIndex((c) => c.value === column);
|
||||
const y = this.rows.findIndex((r) => r.value === row);
|
||||
const record = this.matrix[y][x].records[0];
|
||||
|
||||
if (!record) {
|
||||
return null;
|
||||
}
|
||||
return this._getMatrixFieldProps(record, "value");
|
||||
}
|
||||
|
||||
_getAxisFieldProps(value, axis) {
|
||||
const fieldName = this.matrixFields[axis];
|
||||
const record = new Record(this.list.model, this.list._config, {
|
||||
[fieldName]: value,
|
||||
});
|
||||
const props = this._getMatrixFieldProps(record, axis);
|
||||
if (this.list.fields[fieldName].type === "many2one") {
|
||||
props.canOpen =
|
||||
axis === "x" ? this.props.isXClickable : this.props.isYClickable;
|
||||
}
|
||||
props.readonly = true;
|
||||
return props;
|
||||
}
|
||||
|
||||
_getAggregateProps(value) {
|
||||
const record = new Record(this.list.model, this.list._config, {
|
||||
[this.matrixFields.value]: value,
|
||||
});
|
||||
const props = this._getMatrixFieldProps(record, "value");
|
||||
props.readonly = true;
|
||||
return props;
|
||||
}
|
||||
|
||||
_getMatrixFieldProps(record, fieldName) {
|
||||
const fieldInfo =
|
||||
this.props.list_view.fieldNodes[this.matrixFields[fieldName] + "_0"];
|
||||
const dynamicInfo = {
|
||||
get context() {
|
||||
return getFieldContext(record, fieldInfo.name, fieldInfo.context);
|
||||
},
|
||||
domain() {
|
||||
const evalContext = record.evalContext;
|
||||
if (fieldInfo.domain) {
|
||||
return new Domain(
|
||||
evaluateExpr(fieldInfo.domain, evalContext)
|
||||
).toList();
|
||||
}
|
||||
},
|
||||
required: evaluateBooleanExpr(
|
||||
fieldInfo.required,
|
||||
record.evalContextWithVirtualIds
|
||||
),
|
||||
readonly:
|
||||
this.props.readonly ||
|
||||
evaluateBooleanExpr(
|
||||
fieldInfo.readonly,
|
||||
record.evalContextWithVirtualIds
|
||||
),
|
||||
};
|
||||
const result = {
|
||||
readonly: dynamicInfo.readonly,
|
||||
record: record,
|
||||
name: this.matrixFields[fieldName],
|
||||
...(fieldInfo.field.extractProps || (() => ({}))).apply(fieldInfo.field, [
|
||||
fieldInfo,
|
||||
dynamicInfo,
|
||||
]),
|
||||
};
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
X2Many2DMatrixRenderer.template = "web_widget_x2many_2d_matrix.X2Many2DMatrixRenderer";
|
||||
X2Many2DMatrixRenderer.props = {
|
||||
list: {type: Object, optional: false},
|
||||
list_view: {type: Object, optional: false},
|
||||
matrixFields: {type: Object, optional: false},
|
||||
readonly: {type: Boolean, optional: true},
|
||||
domain: {type: [Array, Function], optional: true},
|
||||
showRowTotals: {type: Boolean, optional: true},
|
||||
showColumnTotals: {type: Boolean, optional: true},
|
||||
isXClickable: {type: Boolean, optional: true},
|
||||
isYClickable: {type: Boolean, optional: true},
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates>
|
||||
<t t-name="web_widget_x2many_2d_matrix.X2Many2DMatrixRenderer">
|
||||
<table
|
||||
class="o_list_table table table-responsive table-sm table-hover position-relative mb-0 o_list_table_ungrouped table-striped"
|
||||
t-if="rows.length > 0"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th />
|
||||
<th
|
||||
t-foreach="columns"
|
||||
t-as="column"
|
||||
t-key="column.value"
|
||||
class="text-center"
|
||||
>
|
||||
<t
|
||||
t-component="xFieldComponent"
|
||||
t-props="_getAxisFieldProps(column.rawValue, 'x')"
|
||||
/>
|
||||
</th>
|
||||
<th t-if="props.showRowTotals" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="rows" t-as="row" t-key="row.value">
|
||||
<td>
|
||||
<t
|
||||
t-component="yFieldComponent"
|
||||
t-props="_getAxisFieldProps(row.rawValue, 'y')"
|
||||
/>
|
||||
</td>
|
||||
<td t-foreach="columns" t-as="column" t-key="column.value">
|
||||
<t
|
||||
t-set="value_field_props"
|
||||
t-value="_getValueFieldProps(column.value, row.value)"
|
||||
/>
|
||||
<t
|
||||
t-if="value_field_props"
|
||||
t-component="valueFieldComponent"
|
||||
t-props="value_field_props"
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
t-if="props.showRowTotals and _canAggregate()"
|
||||
class="row-total"
|
||||
>
|
||||
<t
|
||||
t-component="valueFieldComponent"
|
||||
t-props="_getAggregateProps(_aggregateRow(row.value))"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr t-if="props.showColumnTotals and _canAggregate()">
|
||||
<th />
|
||||
<th t-foreach="columns" t-as="column" t-key="column.value">
|
||||
<t
|
||||
t-component="valueFieldComponent"
|
||||
t-props="_getAggregateProps(_aggregateColumn(column.value))"
|
||||
/>
|
||||
</th>
|
||||
<th t-if="props.showRowTotals" class="col-total">
|
||||
<t
|
||||
t-component="valueFieldComponent"
|
||||
t-props="_getAggregateProps(_aggregateAll())"
|
||||
/>
|
||||
</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
<div t-else="" class="alert alert-info">
|
||||
Nothing to display.
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
Reference in New Issue
Block a user