summaryrefslogtreecommitdiff
path: root/addons/product_matrix
diff options
context:
space:
mode:
authorVictor Feyens <vfe@odoo.com>2019-07-08 13:06:08 +0000
committerVictor Feyens <vfe@odoo.com>2019-08-07 12:50:09 +0000
commite6c2975e769b21d59d66ddaf66730f79c5fa4b82 (patch)
treecc8fe60cb7326db92a3a6701edb3c2be2a3952f5 /addons/product_matrix
parent82465461387a2802cc8521ef22770edb753a8c05 (diff)
[ADD] (sale/purchase)_product_matrix
Diffstat (limited to 'addons/product_matrix')
-rw-r--r--addons/product_matrix/__init__.py4
-rw-r--r--addons/product_matrix/__manifest__.py25
-rw-r--r--addons/product_matrix/data/product_matrix_demo.xml135
-rw-r--r--addons/product_matrix/models/__init__.py1
-rw-r--r--addons/product_matrix/models/product_template.py92
-rw-r--r--addons/product_matrix/static/img/matrix_mycompany_tshirt.jpegbin0 -> 111742 bytes
-rw-r--r--addons/product_matrix/static/src/js/section_and_note_widget.js139
-rw-r--r--addons/product_matrix/static/src/scss/product_matrix.scss93
-rw-r--r--addons/product_matrix/static/src/xml/product_matrix.xml51
-rw-r--r--addons/product_matrix/views/assets.xml9
-rw-r--r--addons/product_matrix/views/matrix_templates.xml51
11 files changed, 600 insertions, 0 deletions
diff --git a/addons/product_matrix/__init__.py b/addons/product_matrix/__init__.py
new file mode 100644
index 00000000000..dc5e6b693d1
--- /dev/null
+++ b/addons/product_matrix/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import models
diff --git a/addons/product_matrix/__manifest__.py b/addons/product_matrix/__manifest__.py
new file mode 100644
index 00000000000..801e3700250
--- /dev/null
+++ b/addons/product_matrix/__manifest__.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+{
+ 'name': "Product Matrix",
+ 'summary': """
+ Technical module: Matrix Implementation
+ """,
+ 'description': """
+ Please refer to Sale Matrix or Purchase Matrix for the uses of this module.
+ """,
+ 'category': 'Sales',
+ 'version': '1.0',
+ 'depends': ['account'],
+ # Account dependency for section_and_note widget.
+ 'qweb': [
+ "static/src/xml/product_matrix.xml",
+ ],
+ 'data': [
+ 'views/assets.xml',
+ 'views/matrix_templates.xml',
+ ],
+ 'demo': [
+ 'data/product_matrix_demo.xml',
+ ]
+}
diff --git a/addons/product_matrix/data/product_matrix_demo.xml b/addons/product_matrix/data/product_matrix_demo.xml
new file mode 100644
index 00000000000..c388edcb252
--- /dev/null
+++ b/addons/product_matrix/data/product_matrix_demo.xml
@@ -0,0 +1,135 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<odoo noupdate="1">
+ <record id="product_attribute_size" model="product.attribute">
+ <field name="name">Size</field>
+ </record>
+ <record id="product_attribute_value_size_xs" model="product.attribute.value">
+ <field name="name">XS</field>
+ <field name="attribute_id" ref="product_attribute_size"/>
+ <field name="sequence">1</field>
+ </record>
+ <record id="product_attribute_value_size_s" model="product.attribute.value">
+ <field name="name">S</field>
+ <field name="attribute_id" ref="product_attribute_size"/>
+ <field name="sequence">2</field>
+ </record>
+ <record id="product_attribute_value_size_m" model="product.attribute.value">
+ <field name="name">M</field>
+ <field name="attribute_id" ref="product_attribute_size"/>
+ <field name="sequence">3</field>
+ </record>
+ <record id="product_attribute_value_size_l" model="product.attribute.value">
+ <field name="name">L</field>
+ <field name="attribute_id" ref="product_attribute_size"/>
+ <field name="sequence">4</field>
+ </record>
+ <record id="product_attribute_value_size_xl" model="product.attribute.value">
+ <field name="name">XL</field>
+ <field name="attribute_id" ref="product_attribute_size"/>
+ <field name="sequence">5</field>
+ </record>
+ <record id="product_attribute_value_size_xxl" model="product.attribute.value">
+ <field name="name">2XL</field>
+ <field name="attribute_id" ref="product_attribute_size"/>
+ <field name="sequence">6</field>
+ </record>
+ <record id="product_attribute_value_size_xxxl" model="product.attribute.value">
+ <field name="name">3XL</field>
+ <field name="attribute_id" ref="product_attribute_size"/>
+ <field name="sequence">7</field>
+ </record>
+
+ <record id="product_attribute_gender" model="product.attribute">
+ <field name="name">Gender</field>
+ </record>
+ <record id="product_attribute_value_m" model="product.attribute.value">
+ <field name="name">Men</field>
+ <field name="attribute_id" ref="product_attribute_gender"/>
+ <field name="sequence">1</field>
+ </record>
+ <record id="product_attribute_value_w" model="product.attribute.value">
+ <field name="name">Women</field>
+ <field name="attribute_id" ref="product_attribute_gender"/>
+ <field name="sequence">2</field>
+ </record>
+
+ <record id="product_attribute_sleeves" model="product.attribute">
+ <field name="name">Sleeves</field>
+ </record>
+ <record id="product_attribute_value_ss" model="product.attribute.value">
+ <field name="name">Short Sleeves</field>
+ <field name="attribute_id" ref="product_attribute_sleeves"/>
+ <field name="sequence">1</field>
+ </record>
+ <record id="product_attribute_value_ls" model="product.attribute.value">
+ <field name="name">Long Sleeves</field>
+ <field name="attribute_id" ref="product_attribute_sleeves"/>
+ <field name="sequence">2</field>
+ </record>
+
+ <!-- Color Attribute Values -->
+ <record id="product_attribute_value_color_1" model="product.attribute.value">
+ <field name="name">Blue</field>
+ <field name="attribute_id" ref="product.product_attribute_2"/>
+ <field name="sequence">3</field>
+ </record>
+ <record id="product_attribute_value_color_2" model="product.attribute.value">
+ <field name="name">Pink</field>
+ <field name="attribute_id" ref="product.product_attribute_2"/>
+ <field name="sequence">4</field>
+ </record>
+ <record id="product_attribute_value_color_3" model="product.attribute.value">
+ <field name="name">Yellow</field>
+ <field name="attribute_id" ref="product.product_attribute_2"/>
+ <field name="sequence">5</field>
+ </record>
+ <record id="product_attribute_value_color_4" model="product.attribute.value">
+ <field name="name">Rainbow</field>
+ <field name="attribute_id" ref="product.product_attribute_2"/>
+ <field name="sequence">6</field>
+ </record>
+
+ <record id="matrix_product_template_shirt" model="product.template">
+ <field name="name">My Company Tshirt</field>
+ <field name="categ_id" ref="product.product_category_6"/>
+ <field name="default_code">GRID</field>
+ <field name="standard_price">7.0</field>
+ <field name="list_price">15.0</field>
+ <field name="type">consu</field>
+ <field name="uom_id" ref="uom.product_uom_unit"/>
+ <field name="uom_po_id" ref="uom.product_uom_unit"/>
+ <field name="description_sale">Show your company love around you =).</field>
+ <field name="image_1920" type="base64" file="product_matrix/static/img/matrix_mycompany_tshirt.jpeg"/>
+ </record>
+
+ <record id="product_template_attribute_line_size" model="product.template.attribute.line">
+ <field name="product_tmpl_id" ref="matrix_product_template_shirt"/>
+ <field name="attribute_id" ref="product_attribute_size"/>
+ <field name="value_ids" eval="[
+ (6, 0, [
+ ref('product_attribute_value_size_xs'),
+ ref('product_attribute_value_size_s'),
+ ref('product_attribute_value_size_m'),
+ ref('product_attribute_value_size_l'),
+ ref('product_attribute_value_size_xl'),
+ ref('product_attribute_value_size_xxl'),
+ ref('product_attribute_value_size_xxxl')])]"/>
+ </record>
+ <record id="product_template_attribute_line_gender" model="product.template.attribute.line">
+ <field name="product_tmpl_id" ref="matrix_product_template_shirt"/>
+ <field name="attribute_id" ref="product_attribute_gender"/>
+ <field name="value_ids" eval="[(6, 0, [ref('product_attribute_value_m'), ref('product_attribute_value_w')])]"/>
+ </record>
+ <record id="product_template_attribute_line_sleeves" model="product.template.attribute.line">
+ <field name="product_tmpl_id" ref="matrix_product_template_shirt"/>
+ <field name="attribute_id" ref="product_attribute_sleeves"/>
+ <field name="value_ids" eval="[(6, 0, [ref('product_attribute_value_ss'), ref('product_attribute_value_ls')])]"/>
+ </record>
+ <record id="product_template_attribute_line_color" model="product.template.attribute.line">
+ <field name="product_tmpl_id" ref="matrix_product_template_shirt"/>
+ <field name="attribute_id" ref="product.product_attribute_2"/>
+ <field name="value_ids" eval="[(6, 0, [ref('product_attribute_value_color_1'), ref('product_attribute_value_color_2'), ref('product_attribute_value_color_3'), ref('product_attribute_value_color_4')])]"/>
+ </record>
+
+ <function model="product.template" name="create_variant_ids" eval="([ref('product_matrix.matrix_product_template_shirt')])"/>
+</odoo>
diff --git a/addons/product_matrix/models/__init__.py b/addons/product_matrix/models/__init__.py
new file mode 100644
index 00000000000..e8fa8f6bf1e
--- /dev/null
+++ b/addons/product_matrix/models/__init__.py
@@ -0,0 +1 @@
+from . import product_template
diff --git a/addons/product_matrix/models/product_template.py b/addons/product_matrix/models/product_template.py
new file mode 100644
index 00000000000..64b164e51e6
--- /dev/null
+++ b/addons/product_matrix/models/product_template.py
@@ -0,0 +1,92 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import itertools
+
+from odoo import models
+
+
+class ProductTemplate(models.Model):
+ _inherit = 'product.template'
+
+ def _get_template_matrix(self, **kwargs):
+ self.ensure_one()
+ company_id = kwargs.get('company_id', None) or self.company_id or self.env.company
+ currency_id = kwargs.get('currency_id', None) or self.currency_id
+ display_extra = kwargs.get('display_extra_price', False)
+ attribute_lines = self.valid_product_template_attribute_line_ids
+
+ Attrib = self.env['product.template.attribute.value']
+ first_line_attributes = attribute_lines[0].product_template_value_ids
+ attribute_ids_by_line = [line.product_template_value_ids.ids for line in attribute_lines]
+
+ header = [{"name": self.display_name}] + [
+ attr._grid_header_cell(
+ fro_currency=self.currency_id,
+ to_currency=currency_id,
+ company=company_id,
+ display_extra=display_extra
+ ) for attr in first_line_attributes]
+
+ result = [[]]
+ for pool in attribute_ids_by_line:
+ result = [x + [y] for y in pool for x in result]
+ args = [iter(result)] * len(first_line_attributes)
+ rows = itertools.zip_longest(*args)
+
+ matrix = []
+ for row in rows:
+ row_attributes = Attrib.browse(row[0][1:])
+ row_header_cell = row_attributes._grid_header_cell(
+ fro_currency=self.currency_id,
+ to_currency=currency_id,
+ company=company_id,
+ display_extra=display_extra)
+ result = [row_header_cell]
+
+ for cell in row:
+ combination = Attrib.browse(cell)
+ is_possible_combination = self._is_combination_possible(combination)
+ cell.sort()
+ result.append({
+ "ptav_ids": cell,
+ "qty": 0,
+ "is_possible_combination": is_possible_combination
+ })
+ matrix.append(result)
+
+ return {
+ "header": header,
+ "matrix": matrix,
+ }
+
+
+class ProductTemplateAttributeValue(models.Model):
+ _inherit = "product.template.attribute.value"
+
+ def _grid_header_cell(self, fro_currency, to_currency, company, display_extra=True):
+ """Generate a header matrix cell for 1 or multiple attributes.
+
+ :param res.currency fro_currency:
+ :param res.currency to_currency:
+ :param res.company company:
+ :param bool display_extra: whether extra prices should be displayed in the cell
+ True by default, used to avoid showing extra prices on purchases.
+ :returns: cell with name (and price if any price_extra is defined on self)
+ :rtype: dict
+ """
+ header_cell = {
+ 'name': '-'.join([attr.name for attr in self])
+ }
+ extra_price = sum(self.mapped('price_extra')) if display_extra else 0
+ if extra_price:
+ sign = '+ ' if self.price_extra > 0 else '- '
+ header_cell.update({
+ "price": sign + self.env['ir.qweb.field.monetary'].value_to_html(
+ extra_price, {
+ 'from_currency': fro_currency,
+ 'display_currency': to_currency,
+ 'company_id': company,
+ }
+ )
+ })
+ return header_cell
diff --git a/addons/product_matrix/static/img/matrix_mycompany_tshirt.jpeg b/addons/product_matrix/static/img/matrix_mycompany_tshirt.jpeg
new file mode 100644
index 00000000000..b600989d3c2
--- /dev/null
+++ b/addons/product_matrix/static/img/matrix_mycompany_tshirt.jpeg
Binary files differ
diff --git a/addons/product_matrix/static/src/js/section_and_note_widget.js b/addons/product_matrix/static/src/js/section_and_note_widget.js
new file mode 100644
index 00000000000..98c471833d3
--- /dev/null
+++ b/addons/product_matrix/static/src/js/section_and_note_widget.js
@@ -0,0 +1,139 @@
+odoo.define('product_matrix.section_and_note_widget', function (require) {
+
+var Dialog = require('web.Dialog');
+var core = require('web.core');
+var _t = core._t;
+var qweb = core.qweb;
+var fieldRegistry = require('web.field_registry');
+require('account.section_and_note_backend');
+
+var SectionAndNoteFieldOne2Many = fieldRegistry.get('section_and_note_one2many');
+
+SectionAndNoteFieldOne2Many.include({
+ custom_events: _.extend({}, SectionAndNoteFieldOne2Many.prototype.custom_events, {
+ open_matrix: '_openMatrix',
+ }),
+
+ /**
+ * Sends the matrix modifications to the server
+ * Through a change on an invisible non stored field.
+ *
+ * @param {List} list of changes in the matrix, to be applied to the order.
+ * {integer} quantity: float
+ * {List} ptav_ids: product.template.attribute.value ids
+ *
+ * @private
+ */
+ _applyGrid: function (changes, productTemplateId) {
+ // the getParent is to trigger the event on the form Controller instead of the one2many.
+ // If not, the one2many crashes on reset because it tries to find an operation in the event
+ // even if there isn't any.
+ // the only solution would be to use a custom event catched on a new controller
+ // on the so/po form (as a js_class).
+ this.getParent().trigger_up('field_changed', {
+ dataPointID: this.dataPointID,
+ changes: {
+ grid: JSON.stringify({changes: changes, product_template_id: productTemplateId}),
+ grid_update: true // to say that the changes to grid have to be applied to the SO.
+ },
+ viewType: 'form',
+ });
+ },
+
+ /**
+ * Catches the event asking for matrix opening
+ *
+ * @param {OdooEvent} ev various values needed to open the matrix
+ * {integer} data.product_template_id product.template id
+ * {list} data.editedCellAttributes list of product.template.attribute.value ids
+ * {bool} data.edit whether the line source should be deleted or not.
+ *
+ * @private
+ */
+ _openMatrix: function (ev) {
+ ev.stopPropagation();
+ var self = this;
+ var dataPointId = ev.data.dataPointId;
+ var productTemplateId = ev.data.product_template_id;
+ var editedCellAttributes = ev.data.editedCellAttributes;
+ if (!ev.data.edit) {
+ // remove the line used to open the matrix
+ this._setValue({operation: 'DELETE', ids: [dataPointId]});
+ }
+ // the getParent is to trigger the event on the form Controller instead of the one2many.
+ // If not, the one2many crashes on reset because it tries to find an operation in the event
+ // even if there isn't any.
+ // the only solution would be to use a custom event catched on a new controller
+ // on the so/po form (as a js_class).
+ this.getParent().trigger_up('field_changed', {
+ dataPointID: this.dataPointID,
+ changes: {
+ grid_product_tmpl_id: {id: productTemplateId}
+ },
+ viewType: 'form',
+ onSuccess: function (result) {
+ // why the hell do we receive a table of 8 classes here? :o
+ self._openMatrixConfigurator(result[0].recordData.grid, productTemplateId, editedCellAttributes);
+ }
+ });
+ },
+
+ /**
+ * Triggers Matrix Dialog opening
+ *
+ * @param {String} jsonInfo matrix dialog content
+ * @param {integer} productTemplateId product.template id
+ * @param {editedCellAttributes} list of product.template.attribute.value ids
+ * used to focus on the matrix cell representing the edited line.
+ *
+ * @private
+ */
+ _openMatrixConfigurator: function (jsonInfo, productTemplateId, editedCellAttributes) {
+ var self = this;
+ var infos = JSON.parse(jsonInfo);
+ var MatrixDialog = new Dialog(this, {
+ title: _t('Choose Product Variants'),
+ size: 'extra-large', // adapt size depending on matrix size?
+ $content: $(qweb.render(
+ 'product_matrix.matrix', {
+ header: infos.header,
+ rows: infos.matrix,
+ }
+ )),
+ buttons: [
+ {text: _t('Confirm'), classes: 'btn-primary', close: true, click: function (result) {
+ var $inputs = this.$('.o_grid_input');
+ var matrixChanges = [];
+ _.each($inputs, function (matrixInput) {
+ if (matrixInput.value && matrixInput.value !== matrixInput.attributes.value.nodeValue) {
+ matrixChanges.push({
+ qty: parseFloat(matrixInput.value),
+ ptav_ids: matrixInput.attributes.ptav_ids.nodeValue.split(",").map(function (id) {
+ return parseInt(id);
+ }),
+ });
+ }
+ });
+ if (matrixChanges.length > 0) {
+ self._applyGrid(matrixChanges, productTemplateId);
+ }
+ }},
+ {text: _t('Close'), close: true},
+ ],
+ }).open();
+
+ MatrixDialog.opened(function () {
+ if (editedCellAttributes.length > 0) {
+ var str = editedCellAttributes.toString();
+ MatrixDialog.$content.find('.o_grid_input').filter((k, v) => v.attributes.ptav_ids.nodeValue === str)[0].focus();
+ } else {
+ MatrixDialog.$content.find('.o_grid_input:first()').focus();
+ }
+ });
+ },
+
+});
+
+return SectionAndNoteFieldOne2Many;
+
+});
diff --git a/addons/product_matrix/static/src/scss/product_matrix.scss b/addons/product_matrix/static/src/scss/product_matrix.scss
new file mode 100644
index 00000000000..cdd7d7cd31c
--- /dev/null
+++ b/addons/product_matrix/static/src/scss/product_matrix.scss
@@ -0,0 +1,93 @@
+.o_web_client .o_view_grid {
+ cursor: default;
+
+ table {
+ margin-bottom: 0;
+ table-layout: fixed;
+ min-width: 100%;
+ width: auto;
+ max-width: none;
+ }
+ th, td {
+ border: 0 !important;
+ vertical-align: middle;
+ width: 5em;
+ }
+ .o_grid_title_header {
+ width: 10em;
+ }
+ thead {
+ color: $o-main-text-color;
+ background-color: $o-brand-lightsecondary;
+ th {
+ text-align: center;
+ white-space: pre-line;
+ }
+ }
+ tbody {
+ background-color: $o-view-background-color;
+ text-align: right;
+ th > div {
+ display: flex;
+ > div {
+ flex: 1 1 auto;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ &:before {
+ content: "/\00a0";
+ padding: 0 2px;
+ }
+ &:first-child:before {
+ content: " ";
+ }
+ &:last-child {
+ flex: 100 1 auto;
+ }
+ }
+ }
+ tr:nth-of-type(odd) th > div:after {
+ background-color: $table-accent-bg;
+ }
+ .o_grid_cell_container {
+ display: flex;
+ align-items: center;
+
+ &:hover {
+ box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.075), 0 0 5px $o-brand-primary;
+
+ }
+ }
+ .o_grid_input {
+ cursor: text;
+ text-align: right;
+ &:focus {
+ opacity: 1 !important;
+ background-color: $o-view-background-color;
+ outline: none;
+ }
+ }
+ }
+ .o_grid_text_muted{
+ color: lighten($o-main-text-color, 15%);
+ font-style: italic;
+ }
+
+ // ensure white background completely surrounds nocontent bubble
+ .o_grid_nocontent_container {
+ overflow: auto;
+
+ .oe_view_nocontent_img_link {
+ padding:10px;
+ }
+ }
+}
+
+.o_product_variant_grid {
+ .form-control {
+ &:focus {
+ box-shadow: none;
+ border: 1px solid $gray-400;
+ }
+ }
+}
diff --git a/addons/product_matrix/static/src/xml/product_matrix.xml b/addons/product_matrix/static/src/xml/product_matrix.xml
new file mode 100644
index 00000000000..dde66ee22ee
--- /dev/null
+++ b/addons/product_matrix/static/src/xml/product_matrix.xml
@@ -0,0 +1,51 @@
+<template>
+ <div t-name="product_matrix.matrix">
+ <table class="o_view_grid o_product_variant_grid table table-sm table-striped table-bordered">
+ <thead>
+ <tr>
+ <th t-foreach="header" t-as="column_header" class="o_grid_title_header text-center">
+ <span t-esc="column_header.name"/>
+ <t t-call="product_matrix.extra_price">
+ <t t-set="price" t-value="column_header.price"/>
+ </t>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr t-foreach="rows" t-as="row">
+ <t t-foreach="row" t-as="cell">
+ <td t-if="cell.name" class="text-center">
+ <strong t-esc="cell.name"/>
+ <t t-call="product_matrix.extra_price">
+ <t t-set="price" t-value="cell.price"/>
+ </t>
+ </td>
+ <td t-else="">
+ <div t-if="cell.is_possible_combination" class="input-group o_grid_cell_container">
+ <input type="number"
+ class="o_grid_cell form-control o_grid_input"
+ t-att-ptav_ids="cell.ptav_ids"
+ t-att-value="cell.qty"/>
+ </div>
+ <span t-else="" class="o_grid_cell o_grid_text_muted o_grid_nocontent_container"> Not available </span>
+ </td>
+ </t>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <span t-name="product_matrix.extra_price" t-if="price" class="badge badge-pill badge-secondary">
+ <!--
+ price_extra is displayed as catalog price instead of
+ price after pricelist because it is impossible to
+ compute. Indeed, the pricelist rule might depend on the
+ selected variant, so the price_extra will be different
+ depending on the selected combination. The price of an
+ attribute is therefore variable and it's not very
+ accurate to display it.
+ -->
+ <span class="variant_price_extra" style="white-space: nowrap;">
+ <t t-raw="price"/>
+ </span>
+ </span>
+</template>
diff --git a/addons/product_matrix/views/assets.xml b/addons/product_matrix/views/assets.xml
new file mode 100644
index 00000000000..7f0793f8904
--- /dev/null
+++ b/addons/product_matrix/views/assets.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <template id="assets_backend_inherit_sale" inherit_id="web.assets_backend" name="Sale Grid assets">
+ <xpath expr="script[last()]" position="after">
+ <script type="text/javascript" src="/product_matrix/static/src/js/section_and_note_widget.js"/>
+ <link rel="stylesheet" type="text/scss" href="/product_matrix/static/src/scss/product_matrix.scss"/>
+ </xpath>
+ </template>
+</odoo>
diff --git a/addons/product_matrix/views/matrix_templates.xml b/addons/product_matrix/views/matrix_templates.xml
new file mode 100644
index 00000000000..e4fda48768e
--- /dev/null
+++ b/addons/product_matrix/views/matrix_templates.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <template id="matrix">
+ <t t-foreach="order.get_report_matrixes()" t-as="grid">
+ <table class="o_view_grid o_product_variant_grid table table-sm table-striped table-bordered">
+ <thead>
+ <tr>
+ <th t-foreach="grid['header']" t-as="column_header" class="o_grid_title_header text-center">
+ <span t-esc="column_header['name']"/>
+ <t t-call="product_matrix.extra_price">
+ <t t-set="price" t-value="column_header.get('price', False)"/>
+ </t>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr t-foreach="grid['matrix']" t-as="row">
+ <t t-foreach="row" t-as="cell">
+ <td t-if="cell.get('name', False)" class="text-left">
+ <strong t-esc="cell['name']"/>
+ <t t-call="product_matrix.extra_price">
+ <t t-set="price" t-value="cell.get('price', False)"/>
+ </t>
+ </td>
+ <td t-else="" class="text-right">
+ <span t-esc="cell.get('qty', 0)" class="o_grid_cell_container"/>
+ </td>
+ </t>
+ </tr>
+ </tbody>
+ </table>
+ </t>
+ </template>
+
+ <template id="extra_price">
+ <span t-if="price" class="badge badge-pill badge-secondary">
+ <!--
+ price_extra is displayed as catalog price instead of
+ price after pricelist because it is impossible to
+ compute. Indeed, the pricelist rule might depend on the
+ selected variant, so the price_extra will be different
+ depending on the selected combination. The price of an
+ attribute is therefore variable and it's not very
+ accurate to display it.
+ -->
+ <span class="variant_price_extra" style="white-space: nowrap;">
+ <t t-raw="price"/>
+ </span>
+ </span>
+ </template>
+</odoo>