diff options
| author | Victor Feyens <vfe@odoo.com> | 2019-07-08 13:06:08 +0000 |
|---|---|---|
| committer | Victor Feyens <vfe@odoo.com> | 2019-08-07 12:50:09 +0000 |
| commit | e6c2975e769b21d59d66ddaf66730f79c5fa4b82 (patch) | |
| tree | cc8fe60cb7326db92a3a6701edb3c2be2a3952f5 /addons/product_matrix | |
| parent | 82465461387a2802cc8521ef22770edb753a8c05 (diff) | |
[ADD] (sale/purchase)_product_matrix
Diffstat (limited to 'addons/product_matrix')
| -rw-r--r-- | addons/product_matrix/__init__.py | 4 | ||||
| -rw-r--r-- | addons/product_matrix/__manifest__.py | 25 | ||||
| -rw-r--r-- | addons/product_matrix/data/product_matrix_demo.xml | 135 | ||||
| -rw-r--r-- | addons/product_matrix/models/__init__.py | 1 | ||||
| -rw-r--r-- | addons/product_matrix/models/product_template.py | 92 | ||||
| -rw-r--r-- | addons/product_matrix/static/img/matrix_mycompany_tshirt.jpeg | bin | 0 -> 111742 bytes | |||
| -rw-r--r-- | addons/product_matrix/static/src/js/section_and_note_widget.js | 139 | ||||
| -rw-r--r-- | addons/product_matrix/static/src/scss/product_matrix.scss | 93 | ||||
| -rw-r--r-- | addons/product_matrix/static/src/xml/product_matrix.xml | 51 | ||||
| -rw-r--r-- | addons/product_matrix/views/assets.xml | 9 | ||||
| -rw-r--r-- | addons/product_matrix/views/matrix_templates.xml | 51 |
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 Binary files differnew file mode 100644 index 00000000000..b600989d3c2 --- /dev/null +++ b/addons/product_matrix/static/img/matrix_mycompany_tshirt.jpeg 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> |
