[IMP] point_of_sale: Product configurator

Configure products with create_variants = 'never' from the POS

closes odoo/odoo#54956

Taskid: 208205
Related: odoo/upgrade#1522
Signed-off-by: pimodoo <pimodoo@users.noreply.github.com>
This commit is contained in:
Antoine Prieels
2020-07-30 13:08:24 +00:00
parent d35adaa030
commit 0c3fe8736d
27 changed files with 762 additions and 125 deletions
+1
View File
@@ -111,6 +111,7 @@
'static/src/xml/Popups/NumberPopup.xml',
'static/src/xml/Popups/OfflineErrorPopup.xml',
'static/src/xml/Popups/OrderImportPopup.xml',
'static/src/xml/Popups/ProductConfiguratorPopup.xml',
'static/src/xml/Screens/ProductScreen/ControlButtons/SetPricelistButton.xml',
'static/src/xml/Screens/ProductScreen/ControlButtons/SetFiscalPositionButton.xml',
'static/src/xml/ChromeWidgets/ClientScreenButton.xml',
@@ -103,6 +103,69 @@
<field name="taxes_id" eval='[(5,)]'/> <!-- no taxes -->
</record>
<function model="ir.model.data" name="_update_xmlids">
<value model="base" eval="[{
'xml_id': 'point_of_sale.desk_organizer_product_template',
'record': obj().env.ref('point_of_sale.desk_organizer').product_tmpl_id,
'noupdate': True,
}]"/>
</function>
<record id="size_attribute" model="product.attribute">
<field name="name">Size</field>
<field name="sequence">30</field>
<field name="display_type">radio</field>
<field name="create_variant">no_variant</field>
</record>
<record id="size_attribute_s" model="product.attribute.value">
<field name="name">S</field>
<field name="sequence">1</field>
<field name="attribute_id" ref="size_attribute"/>
</record>
<record id="size_attribute_m" model="product.attribute.value">
<field name="name">M</field>
<field name="sequence">2</field>
<field name="attribute_id" ref="size_attribute"/>
</record>
<record id="size_attribute_l" model="product.attribute.value">
<field name="name">L</field>
<field name="sequence">3</field>
<field name="attribute_id" ref="size_attribute"/>
</record>
<record id="desk_organizer_size" model="product.template.attribute.line">
<field name="product_tmpl_id" ref="point_of_sale.desk_organizer_product_template"/>
<field name="attribute_id" ref="size_attribute"/>
<field name="value_ids" eval="[(6, 0, [ref('size_attribute_s'), ref('size_attribute_m'), ref('size_attribute_l')])]"/>
</record>
<record id="fabric_attribute" model="product.attribute">
<field name="name">Fabric</field>
<field name="sequence">40</field>
<field name="display_type">select</field>
<field name="create_variant">no_variant</field>
</record>
<record id="fabric_attribute_plastic" model="product.attribute.value">
<field name="name">Plastic</field>
<field name="sequence">1</field>
<field name="attribute_id" ref="fabric_attribute"/>
</record>
<record id="fabric_attribute_leather" model="product.attribute.value">
<field name="name">Leather</field>
<field name="sequence">2</field>
<field name="attribute_id" ref="fabric_attribute"/>
</record>
<record id="fabric_attribute_custom" model="product.attribute.value">
<field name="name">Custom</field>
<field name="sequence">3</field>
<field name="attribute_id" ref="fabric_attribute"/>
<field name="is_custom">True</field>
</record>
<record id="desk_organizer_fabric" model="product.template.attribute.line">
<field name="product_tmpl_id" ref="point_of_sale.desk_organizer_product_template"/>
<field name="attribute_id" ref="fabric_attribute"/>
<field name="value_ids" eval="[(6, 0, [ref('fabric_attribute_plastic'), ref('fabric_attribute_leather'), ref('fabric_attribute_custom')])]"/>
</record>
<record id="magnetic_board" model="product.product">
<field name="available_in_pos">True</field>
<field name="list_price">1.98</field>
@@ -202,6 +202,7 @@ class PosConfig(models.Model):
module_pos_loyalty = fields.Boolean("Loyalty Program")
module_pos_mercury = fields.Boolean(string="Integrated Card Payments")
manage_orders = fields.Boolean(string="Manage Orders")
product_configurator = fields.Boolean(string="Product Configurator")
is_posbox = fields.Boolean("PosBox")
is_header_or_footer = fields.Boolean("Header & Footer")
module_pos_hr = fields.Boolean(help="Show employee login screen")
+1
View File
@@ -664,6 +664,7 @@ class PosOrderLine(models.Model):
pack_lot_ids = fields.One2many('pos.pack.operation.lot', 'pos_order_line_id', string='Lot/serial Number')
product_uom_id = fields.Many2one('uom.uom', string='Product UoM', related='product_id.uom_id')
currency_id = fields.Many2one('res.currency', related='order_id.currency_id')
full_product_name = fields.Char('Full Product Name')
@api.model
def _prepare_refund_data(self, refund_order_id):
@@ -16,6 +16,12 @@ access_product_product,product.product,product.model_product_product,group_pos_u
access_product_template_pos_user,product.template pos user,product.model_product_template,group_pos_user,1,0,0,0
access_product_product_pos_manager,product.product.pos manager,product.model_product_product,group_pos_manager,1,1,1,1
access_product_template_pos_manager,product.template pos manager,product.model_product_template,group_pos_manager,1,1,1,1
access_product_attribute_pos_manager,product.attribute,product.model_product_attribute,group_pos_manager,1,1,1,1
access_product_attribute_value_pos_manager,product.attribute value,product.model_product_attribute_value,group_pos_manager,1,1,1,1
access_product_attribute_custom_value_pos_manager,product.attribute.custom value manager,product.model_product_attribute_custom_value,group_pos_manager,1,1,1,1
access_product_product_attribute_pos_manager,product.template.attribute value,product.model_product_template_attribute_value,group_pos_manager,1,1,1,1
access_product_template_attribute_exclusion_pos_manager,product.template.attribute exclusion,product.model_product_template_attribute_exclusion,group_pos_manager,1,1,1,1
access_product_template_attribute_line_pos_manager,product.template.attribute line,product.model_product_template_attribute_line,group_pos_manager,1,1,1,1
access_account_bank_statement_line_manager,account.bank.statement.line manager,account.model_account_bank_statement_line,group_pos_manager,1,1,1,1
access_product_product_supplierinfo_user,product.supplierinfo user,product.model_product_supplierinfo,group_pos_user,1,0,0,0
access_product_product_packaging_user,product.packaging user,product.model_product_packaging,group_pos_user,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
16 access_product_template_pos_user product.template pos user product.model_product_template group_pos_user 1 0 0 0
17 access_product_product_pos_manager product.product.pos manager product.model_product_product group_pos_manager 1 1 1 1
18 access_product_template_pos_manager product.template pos manager product.model_product_template group_pos_manager 1 1 1 1
19 access_product_attribute_pos_manager product.attribute product.model_product_attribute group_pos_manager 1 1 1 1
20 access_product_attribute_value_pos_manager product.attribute value product.model_product_attribute_value group_pos_manager 1 1 1 1
21 access_product_attribute_custom_value_pos_manager product.attribute.custom value manager product.model_product_attribute_custom_value group_pos_manager 1 1 1 1
22 access_product_product_attribute_pos_manager product.template.attribute value product.model_product_template_attribute_value group_pos_manager 1 1 1 1
23 access_product_template_attribute_exclusion_pos_manager product.template.attribute exclusion product.model_product_template_attribute_exclusion group_pos_manager 1 1 1 1
24 access_product_template_attribute_line_pos_manager product.template.attribute line product.model_product_template_attribute_line group_pos_manager 1 1 1 1
25 access_account_bank_statement_line_manager account.bank.statement.line manager account.model_account_bank_statement_line group_pos_manager 1 1 1 1
26 access_product_product_supplierinfo_user product.supplierinfo user product.model_product_supplierinfo group_pos_user 1 0 0 0
27 access_product_product_packaging_user product.packaging user product.model_product_packaging group_pos_user 1 0 0 0
+123 -3
View File
@@ -2504,6 +2504,13 @@ td {
left: 50%;
transform: translate(-50%, -50%);
}
.pos .modal-dialog .popup-lg{
max-width: 80%;
max-height: 600px;
height: auto;
}
.pos .modal-dialog .popup-confirm{
height: 250px;
}
@@ -2524,6 +2531,12 @@ td {
margin: 16px;
margin-top: 20px;
}
.pos .popup-lg .body {
max-height: 400px;
overflow-y: auto;
}
.pos .popup .body.traceback {
height: 238px;
overflow: auto;
@@ -2536,9 +2549,6 @@ td {
user-select: text;
}
.pos .popup .footer{
position:absolute;
bottom:0;
left:0;
width:100%;
height:60px;
border-top: solid 1px rgba(60,60,60,0.1);
@@ -3024,3 +3034,113 @@ td {
.order-receipt .title {
font-size: large;
}
/* ********* Product Configurator Popup ********* */
.pos .product_configurator_attributes {
text-align: left;
margin: 1em;
}
.pos .product_configurator_attributes .attribute {
margin-bottom: 1em;
display: inline-block;
width: 45%;
padding-left: 0.5em;
vertical-align: top;
}
@media screen and (max-width: 768px) {
.pos .product_configurator_attributes .attribute {
width: 95%;
}
}
.pos .product_configurator_attributes .attribute_name {
margin-bottom: 0.5em;
font-weight: bold;
}
.pos .product_configurator_attributes input {
min-height: 0;
width: auto;
}
/** Radio attribute **/
.pos .product_configurator_attributes .configurator_radio {
line-height: 1.5;
}
.pos .product_configurator_attributes .configurator_radio input[type='radio'] {
box-shadow: none;
margin-right: 0.5em;
}
.pos .product_configurator_attributes .configurator_radio .radio_attribute_label {
font-weight: normal;
display: inline-block;
width: 80%;
}
.pos .product_configurator_attributes .configurator_radio .price_extra {
margin-left: 0.5em;
padding: 0.2em 0.4em;
border-radius: 10rem;
color: #FFFFFF;
background-color: #6c757d;
}
.pos .product_configurator_attributes .configurator_radio .custom_value {
margin: 0.3em 1.3em;
}
/** Selector attribute **/
.pos .product_configurator_attributes .configurator_select {
cursor: pointer;
background-color: transparent;
width: 90%;
padding: 0.5em;
color: #666666;
font-size: 18px;
margin-bottom: 0.5em;
}
/** Color attribute **/
.pos .product_configurator_attributes ul.color_attribute_list {
margin-left: 0;
}
.pos .product_configurator_attributes li.color_attribute_list_item:before {
content: '';
}
.pos .product_configurator_attributes li.color_attribute_list_item {
margin-bottom: 0.5em;
text-indent: 0;
display: inline-block;
}
.pos .product_configurator_attributes .color_attribute_list_item:not(:last-child) {
margin-right: 1rem;
}
.pos .product_configurator_attributes .configurator_color {
display: inline-block;
border: 1px solid #999999;
}
.pos .product_configurator_attributes .configurator_color.active {
border: 3px ridge #66ee66;
}
.pos .product_configurator_attributes .configurator_color input {
margin: 20px;
opacity: 0;
}
.pos .product_configurator_attributes .configurator_color.active input {
margin: 18px;
}
@@ -0,0 +1,89 @@
odoo.define('point_of_sale.ProductConfiguratorPopup', function(require) {
'use strict';
const { useState, useSubEnv } = owl.hooks;
const PosComponent = require('point_of_sale.PosComponent');
const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup');
const Registries = require('point_of_sale.Registries');
class ProductConfiguratorPopup extends AbstractAwaitablePopup {
constructor() {
super(...arguments);
useSubEnv({ attribute_components: [] });
}
getPayload() {
var selected_attributes = [];
var price_extra = 0.0;
this.env.attribute_components.forEach((attribute_component) => {
let { value, extra } = attribute_component.getValue();
selected_attributes.push(value);
price_extra += extra;
});
return {
selected_attributes,
price_extra,
};
}
}
ProductConfiguratorPopup.template = 'ProductConfiguratorPopup';
Registries.Component.add(ProductConfiguratorPopup);
class BaseProductAttribute extends PosComponent {
constructor() {
super(...arguments);
this.env.attribute_components.push(this);
this.attribute = this.props.attribute;
this.values = this.attribute.values;
this.state = useState({
selected_value: parseFloat(this.values[0].id),
custom_value: '',
});
}
getValue() {
let selected_value = this.values.find((val) => val.id === parseFloat(this.state.selected_value));
let value = selected_value.name;
if (selected_value.is_custom && this.state.custom_value) {
value += `: ${this.state.custom_value}`;
}
return {
value,
extra: selected_value.price_extra
};
}
}
class RadioProductAttribute extends BaseProductAttribute {
mounted() {
// With radio buttons `t-model` selects the default input by searching for inputs with
// a matching `value` attribute. In our case, we use `t-att-value` so `value` is
// not found yet and no radio is selected by default.
// We then manually select the first input of each radio attribute.
$(this.el).find('input[type="radio"]:first').prop('checked', true);
}
}
RadioProductAttribute.template = 'RadioProductAttribute';
Registries.Component.add(RadioProductAttribute);
class SelectProductAttribute extends BaseProductAttribute { }
SelectProductAttribute.template = 'SelectProductAttribute';
Registries.Component.add(SelectProductAttribute);
class ColorProductAttribute extends BaseProductAttribute {}
ColorProductAttribute.template = 'ColorProductAttribute';
Registries.Component.add(ColorProductAttribute);
return {
ProductConfiguratorPopup,
BaseProductAttribute,
RadioProductAttribute,
SelectProductAttribute,
ColorProductAttribute,
};
});
@@ -21,7 +21,7 @@ odoo.define('point_of_sale.OrderlineDetails', function (require) {
return format.float(roundedQuantity, { digits: [69, decimals] });
};
return {
productName: line.get_product().display_name,
productName: line.get_full_product_name(),
totalPrice: line.get_price_with_tax(),
quantity: formatQty(line),
unit: line.get_unit().name,
@@ -59,7 +59,24 @@ odoo.define('point_of_sale.ProductScreen', function(require) {
this.env.pos.add_new_order();
}
const product = event.detail;
let draftPackLotLines, weight, packLotLinesToEdit;
let price_extra = 0.0;
let draftPackLotLines, weight, description, packLotLinesToEdit;
if (this.env.pos.config.product_configurator && _.some(product.attribute_line_ids, (id) => id in this.env.pos.attributes_by_ptal_id)) {
let attributes = _.map(product.attribute_line_ids, (id) => this.env.pos.attributes_by_ptal_id[id])
.filter((attr) => attr !== undefined);
let { confirmed, payload } = await this.showPopup('ProductConfiguratorPopup', {
product: product,
attributes: attributes,
});
if (confirmed) {
description = payload.selected_attributes.join(', ');
price_extra += payload.price_extra;
} else {
return;
}
}
// Gather lot information if required.
if (['serial', 'lot'].includes(product.tracking)) {
@@ -119,6 +136,8 @@ odoo.define('point_of_sale.ProductScreen', function(require) {
// Add the product after having the extra information.
this.currentOrder.add_product(product, {
draftPackLotLines,
description: description,
price_extra: price_extra,
quantity: weight,
});
+87 -5
View File
@@ -401,7 +401,7 @@ exports.PosModel = Backbone.Model.extend({
model: 'product.product',
fields: ['display_name', 'lst_price', 'standard_price', 'categ_id', 'pos_categ_id', 'taxes_id',
'barcode', 'default_code', 'to_weight', 'uom_id', 'description_sale', 'description',
'product_tmpl_id','tracking', 'write_date', 'available_in_pos'],
'product_tmpl_id','tracking', 'write_date', 'available_in_pos', 'attribute_line_ids'],
order: _.map(['sequence','default_code','name'], function (name) { return {name: name}; }),
domain: function(self){
var domain = ['&', '&', ['sale_ok','=',true],['available_in_pos','=',true],'|',['company_id','=',self.config.company_id[0]],['company_id','=',false]];
@@ -428,6 +428,53 @@ exports.PosModel = Backbone.Model.extend({
return new exports.Product({}, product);
}));
},
},{
model: 'product.attribute',
fields: ['name', 'display_type'],
condition: function (self) { return self.config.product_configurator; },
domain: function(){ return [['create_variant', '=', 'no_variant']]; },
loaded: function(self, product_attributes, tmp) {
tmp.product_attributes_by_id = {};
_.map(product_attributes, function (product_attribute) {
tmp.product_attributes_by_id[product_attribute.id] = product_attribute;
});
}
},{
model: 'product.attribute.value',
fields: ['name', 'attribute_id', 'is_custom', 'html_color'],
condition: function (self) { return self.config.product_configurator; },
domain: function(self, tmp){ return [['attribute_id', 'in', _.keys(tmp.product_attributes_by_id).map(parseFloat)]]; },
loaded: function(self, pavs, tmp) {
tmp.pav_by_id = {};
_.map(pavs, function (pav) {
tmp.pav_by_id[pav.id] = pav;
});
}
}, {
model: 'product.template.attribute.value',
fields: ['product_attribute_value_id', 'attribute_id', 'attribute_line_id', 'price_extra'],
condition: function (self) { return self.config.product_configurator; },
domain: function(self, tmp){ return [['attribute_id', 'in', _.keys(tmp.product_attributes_by_id).map(parseFloat)]]; },
loaded: function(self, ptavs, tmp) {
self.attributes_by_ptal_id = {};
_.map(ptavs, function (ptav) {
if (!self.attributes_by_ptal_id[ptav.attribute_line_id[0]]){
self.attributes_by_ptal_id[ptav.attribute_line_id[0]] = {
id: ptav.attribute_line_id[0],
name: tmp.product_attributes_by_id[ptav.attribute_id[0]].name,
display_type: tmp.product_attributes_by_id[ptav.attribute_id[0]].display_type,
values: [],
};
}
self.attributes_by_ptal_id[ptav.attribute_line_id[0]].values.push({
id: ptav.product_attribute_value_id[0],
name: tmp.pav_by_id[ptav.product_attribute_value_id[0]].name,
is_custom: tmp.pav_by_id[ptav.product_attribute_value_id[0]].is_custom,
html_color: tmp.pav_by_id[ptav.product_attribute_value_id[0]].html_color,
price_extra: ptav.price_extra,
});
});
}
},{
model: 'account.cash.rounding',
fields: ['name', 'rounding', 'rounding_method'],
@@ -1562,6 +1609,8 @@ exports.Orderline = Backbone.Model.extend({
this.discount = 0;
this.discountStr = '0';
this.selected = false;
this.description = '';
this.price_extra = 0;
this.id = orderline_id++;
this.price_manually_set = false;
@@ -1577,6 +1626,8 @@ exports.Orderline = Backbone.Model.extend({
this.price = json.price_unit;
this.set_discount(json.discount);
this.set_quantity(json.qty, 'do not recompute unit price');
this.set_description(json.description);
this.set_price_extra(json.price_extra);
this.id = json.id ? json.id : orderline_id++;
orderline_id = Math.max(this.id+1,orderline_id);
var pack_lot_lines = json.pack_lot_ids;
@@ -1671,6 +1722,15 @@ exports.Orderline = Backbone.Model.extend({
get_discount_str: function(){
return this.discountStr;
},
set_description: function(description){
this.description = description || '';
},
set_price_extra: function(price_extra){
this.price_extra = parseFloat(price_extra) || 0.0;
},
get_price_extra: function () {
return this.price_extra;
},
// sets the quantity of the product. The quantity will be rounded according to the
// product's unity of measure properties. Quantities greater than zero will not get
// rounded to zero
@@ -1700,7 +1760,7 @@ exports.Orderline = Backbone.Model.extend({
// just like in sale.order changing the quantity will recompute the unit price
if(! keep_price && ! this.price_manually_set){
this.set_unit_price(this.product.get_price(this.order.pricelist, this.get_quantity()));
this.set_unit_price(this.product.get_price(this.order.pricelist, this.get_quantity()) + this.get_price_extra());
this.order.fix_tax_included_price(this);
}
this.trigger('change', this);
@@ -1747,6 +1807,13 @@ exports.Orderline = Backbone.Model.extend({
get_product: function(){
return this.product;
},
get_full_product_name: function () {
var full_name = this.product.display_name;
if (this.description) {
full_name += ` (${this.description})`;
}
return full_name;
},
// selects or deselects this orderline
set_selected: function(selected){
this.selected = selected;
@@ -1768,11 +1835,13 @@ exports.Orderline = Backbone.Model.extend({
return false;
}else if(this.get_discount() > 0){ // we don't merge discounted orderlines
return false;
}else if(!utils.float_is_zero(price - orderline.get_product().get_price(orderline.order.pricelist, this.get_quantity()),
}else if(!utils.float_is_zero(price - orderline.get_product().get_price(orderline.order.pricelist, this.get_quantity()) - orderline.get_price_extra(),
this.pos.currency.decimals)){
return false;
}else if(this.product.tracking == 'lot') {
return false;
}else if (this.description !== orderline.description) {
return false;
}else{
return true;
}
@@ -1797,7 +1866,10 @@ exports.Orderline = Backbone.Model.extend({
product_id: this.get_product().id,
tax_ids: [[6, false, _.map(this.get_applicable_taxes(), function(tax){ return tax.id; })]],
id: this.id,
pack_lot_ids: pack_lot_ids
pack_lot_ids: pack_lot_ids,
description: this.description,
full_product_name: this.get_full_product_name(),
price_extra: this.get_price_extra(),
};
},
//used to create a json of the ticket, to be sent to the printer
@@ -1825,7 +1897,7 @@ exports.Orderline = Backbone.Model.extend({
generate_wrapped_product_name: function() {
var MAX_LENGTH = 24; // 40 * line ratio of .6
var wrapped = [];
var name = this.get_product().display_name;
var name = this.get_full_product_name();
var current_line = "";
while (name.length > 0) {
@@ -2820,6 +2892,12 @@ exports.Order = Backbone.Model.extend({
this.fix_tax_included_price(line);
}
if (options.price_extra !== undefined){
line.price_extra = options.price_extra;
line.set_unit_price(line.get_unit_price() + options.price_extra);
this.fix_tax_included_price(line);
}
if(options.lst_price !== undefined){
line.set_lst_price(options.lst_price);
}
@@ -2828,6 +2906,10 @@ exports.Order = Backbone.Model.extend({
line.set_discount(options.discount);
}
if (options.description !== undefined){
line.description += options.description;
}
if(options.extras !== undefined){
for (var prop in options.extras) {
line[prop] = options.extras[prop];
@@ -93,7 +93,7 @@
<div t-attf-style="background-image:url(#{orderline.product.image_base64})" />
</div>
<div>
<t t-esc="orderline.product.display_name" />
<t t-esc="orderline.get_full_product_name()" />
</div>
<div>
<t t-esc="orderline.get_quantity_str()" />
@@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="ProductConfiguratorPopup" owl="1">
<div role="dialog" class="modal-dialog">
<div class="popup popup-text popup-lg product-configurator-popup">
<header class="title">
<t t-esc="props.product.display_name" />
</header>
<main class="body product_configurator_attributes col-lg-4 col-md-6 col-sm-12">
<div t-foreach="props.attributes" t-as="attribute" class="attribute">
<div class="attribute_name" t-esc="attribute.name"/>
<RadioProductAttribute t-if="attribute.display_type === 'radio'" attribute="attribute"/>
<SelectProductAttribute t-elif="attribute.display_type === 'select'" attribute="attribute"/>
<ColorProductAttribute t-elif="attribute.display_type === 'color'" attribute="attribute"/>
</div>
</main>
<footer class="footer">
<div class="button highlight confirm" t-on-click="confirm">
Add
</div>
<div class="button cancel" t-on-click="cancel">
Cancel
</div>
</footer>
</div>
</div>
</t>
<t t-name="RadioProductAttribute" owl="1">
<div class="configurator_radio">
<div t-foreach="values" t-as="value">
<input type="radio" t-model="state.selected_value" t-att-name="attribute.id"
t-attf-id="{{ attribute.id }}_{{ value.id }}" t-att-value="value.id"/>
<label t-attf-for="{{ attribute.id }}_{{ value.id }}">
<div class="radio_attribute_label">
<t t-esc="value.name"/>
<span t-if="value.price_extra" class="price_extra">
+ <t t-esc="env.pos.format_currency(value.price_extra)"/>
</span>
</div>
<t t-if="value.id == state.selected_value &amp;&amp; value.is_custom">
<input class="custom_value" type="text" t-model="state.custom_value"/>
</t>
</label>
</div>
</div>
</t>
<t t-name="SelectProductAttribute" owl="1">
<div>
<t t-set="is_custom" t-value="false"/>
<select class="configurator_select" t-model="state.selected_value">
<option t-foreach="values" t-as="value" t-att-value="value.id">
<t t-set="is_custom" t-value="is_custom || (value.is_custom &amp;&amp; value.id == state.selected_value)"/>
<t t-esc="value.name"/>
<t t-if="value.price_extra">
+ <t t-esc="env.pos.format_currency(value.price_extra)"/>
</t>
</option>
</select>
<input class="custom_value" t-if="is_custom" type="text" t-model="state.custom_value"/>
</div>
</t>
<t t-name="ColorProductAttribute" owl="1">
<div>
<t t-set="is_custom" t-value="false"/>
<ul class="color_attribute_list">
<li t-foreach="values" t-as="value" class="color_attribute_list_item">
<t t-set="is_custom" t-value="is_custom || (value.is_custom &amp;&amp; value.id == state.selected_value)"/>
<label t-attf-class="configurator_color {{ value.id == state.selected_value ? 'active' : '' }}"
t-attf-style="background-color: {{ value.html_color }};" t-att-data-color="value.name">
<input type="radio" t-model="state.selected_value" t-att-value="value.id" t-att-name="attribute.id"/>
</label>
</li>
</ul>
<input class="custom_value" t-if="is_custom" type="text" t-model="state.custom_value"/>
</div>
</t>
</templates>
@@ -4,7 +4,7 @@
<t t-name="Orderline" owl="1">
<li t-on-click="selectLine" class="orderline" t-att-class="addedClasses">
<span class="product-name">
<t t-esc="props.line.get_product().display_name"/>
<t t-esc="props.line.get_full_product_name()"/>
<span> </span>
<t t-if="props.line.get_product().tracking!=='none'">
<t t-if="props.line.has_valid_product_lot()">
@@ -0,0 +1,66 @@
odoo.define('point_of_sale.tour.ProductConfigurator', function (require) {
'use strict';
const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods');
const { ProductConfigurator } = require('point_of_sale.tour.ProductConfiguratorTourMethods');
const { getSteps, startSteps } = require('point_of_sale.tour.utils');
var Tour = require('web_tour.tour');
// signal to start generating steps
// when finished, steps can be taken from getSteps
startSteps();
// Go by default to home category
ProductScreen.do.clickHomeCategory();
// Click on Configurable Chair product
ProductScreen.do.clickDisplayedProduct('Configurable Chair');
ProductConfigurator.check.isShown();
// Cancel configuration, not product should be in order
ProductConfigurator.do.cancelAttributes();
ProductScreen.check.orderIsEmpty();
// Click on Configurable Chair product
ProductScreen.do.clickDisplayedProduct('Configurable Chair');
ProductConfigurator.check.isShown();
// Pick Color
ProductConfigurator.do.pickColor('Red');
// Pick Radio
ProductConfigurator.do.pickSelect('Metal');
// Pick Select
ProductConfigurator.do.pickRadio('Other');
// Fill in custom attribute
ProductConfigurator.do.fillCustomAttribute('Custom Fabric');
// Confirm configuration
ProductConfigurator.do.confirmAttributes();
// Check that the product has been added to the order with correct attributes and price
ProductScreen.check.selectedOrderlineHas('Configurable Chair (Red, Metal, Other: Custom Fabric)', '1.0', '11.0');
// Orderlines with the same attributes should be merged
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickDisplayedProduct('Configurable Chair');
ProductConfigurator.do.pickColor('Red');
ProductConfigurator.do.pickSelect('Metal');
ProductConfigurator.do.pickRadio('Other');
ProductConfigurator.do.fillCustomAttribute('Custom Fabric');
ProductConfigurator.do.confirmAttributes();
ProductScreen.check.selectedOrderlineHas('Configurable Chair (Red, Metal, Other: Custom Fabric)', '2.0', '22.0');
// Orderlines with different attributes shouldn't be merged
ProductScreen.do.clickHomeCategory();
ProductScreen.do.clickDisplayedProduct('Configurable Chair');
ProductConfigurator.do.pickColor('Blue');
ProductConfigurator.do.pickSelect('Metal');
ProductConfigurator.do.pickRadio('Leather');
ProductConfigurator.do.confirmAttributes();
ProductScreen.check.selectedOrderlineHas('Configurable Chair (Blue, Metal, Leather)', '1.0', '10.0');
Tour.register('ProductConfiguratorTour', { test: true, url: '/pos/web' }, getSteps());
});
@@ -0,0 +1,77 @@
odoo.define('point_of_sale.tour.ProductConfiguratorTourMethods', function (require) {
'use strict';
const { createTourMethods } = require('point_of_sale.tour.utils');
class Do {
pickRadio(name) {
return [
{
content: `picking radio attribute with name ${name}`,
trigger: `.product-configurator-popup .radio_attribute_label:contains('${name}')`,
},
];
}
pickSelect(name) {
return [
{
content: `picking select attribute with name ${name}`,
trigger: `.product-configurator-popup .configurator_select:has(option:contains('${name}'))`,
run: `text ${name}`,
},
];
}
pickColor(name) {
return [
{
content: `picking color attribute with name ${name}`,
trigger: `.product-configurator-popup .configurator_color[data-color='${name}']`,
},
];
}
fillCustomAttribute(value) {
return [
{
content: `filling custom attribute with value ${value}`,
trigger: `.product-configurator-popup .custom_value`,
run: `text ${value}`,
},
];
}
confirmAttributes() {
return [
{
content: `confirming product configuration`,
trigger: `.product-configurator-popup .button.confirm`,
},
];
}
cancelAttributes() {
return [
{
content: `canceling product configuration`,
trigger: `.product-configurator-popup .button.cancel`,
},
];
}
}
class Check {
isShown() {
return [
{
content: 'product configurator is shown',
trigger: '.product-configurator-popup:not(:has(.oe_hidden))',
run: () => {},
},
];
}
}
return createTourMethods('ProductConfigurator', Do, Check);
});
@@ -109,6 +109,12 @@ class TestPointOfSaleHttpCommon(odoo.tests.HttpCase):
'list_price': 5.10,
'taxes_id': False,
})
configurable_chair = env['product.product'].create({
'name': 'Configurable Chair',
'available_in_pos': True,
'list_price': 10,
'taxes_id': False,
})
attribute = env['product.attribute'].create({
'name': 'add 2',
@@ -124,6 +130,68 @@ class TestPointOfSaleHttpCommon(odoo.tests.HttpCase):
})
line.product_template_value_ids[0].price_extra = 2
chair_color_attribute = env['product.attribute'].create({
'name': 'Color',
'display_type': 'color',
'create_variant': 'no_variant',
})
chair_color_red = env['product.attribute.value'].create({
'name': 'Red',
'attribute_id': chair_color_attribute.id,
'html_color': '#ff0000',
})
chair_color_blue = env['product.attribute.value'].create({
'name': 'Blue',
'attribute_id': chair_color_attribute.id,
'html_color': '#0000ff',
})
chair_color_line = env['product.template.attribute.line'].create({
'product_tmpl_id': configurable_chair.product_tmpl_id.id,
'attribute_id': chair_color_attribute.id,
'value_ids': [(6, 0, [chair_color_red.id, chair_color_blue.id])]
})
chair_color_line.product_template_value_ids[0].price_extra = 1
chair_legs_attribute = env['product.attribute'].create({
'name': 'Chair Legs',
'display_type': 'select',
'create_variant': 'no_variant',
})
chair_legs_metal = env['product.attribute.value'].create({
'name': 'Metal',
'attribute_id': chair_legs_attribute.id,
})
chair_legs_wood = env['product.attribute.value'].create({
'name': 'Wood',
'attribute_id': chair_legs_attribute.id,
})
chair_legs_line = env['product.template.attribute.line'].create({
'product_tmpl_id': configurable_chair.product_tmpl_id.id,
'attribute_id': chair_legs_attribute.id,
'value_ids': [(6, 0, [chair_legs_metal.id, chair_legs_wood.id])]
})
chair_fabrics_attribute = env['product.attribute'].create({
'name': 'Fabrics',
'display_type': 'radio',
'create_variant': 'no_variant',
})
chair_fabrics_leather = env['product.attribute.value'].create({
'name': 'Leather',
'attribute_id': chair_fabrics_attribute.id,
})
chair_fabrics_other = env['product.attribute.value'].create({
'name': 'Other',
'attribute_id': chair_fabrics_attribute.id,
'is_custom': True,
})
chair_fabrics_line = env['product.template.attribute.line'].create({
'product_tmpl_id': configurable_chair.product_tmpl_id.id,
'attribute_id': chair_fabrics_attribute.id,
'value_ids': [(6, 0, [chair_fabrics_leather.id, chair_fabrics_other.id])]
})
chair_color_line.product_template_value_ids[1].is_custom = True
fixed_pricelist = env['product.pricelist'].create({
'name': 'Fixed',
'item_ids': [(0, 0, {
@@ -437,3 +505,8 @@ class TestUi(TestPointOfSaleHttpCommon):
self.main_pos_config.write({ 'manage_orders': True, 'module_account': True })
self.main_pos_config.open_session_cb(check_coa=False)
self.start_tour("/pos/web?config_id=%d" % self.main_pos_config.id, 'OrderManagementScreenTour', login="admin", step_delay=50)
def test_04_product_configurator(self):
self.main_pos_config.write({ 'product_configurator': True })
self.main_pos_config.open_session_cb(check_coa=False)
self.start_tour("/pos/web?config_id=%d" % self.main_pos_config, 'ProductConfiguratorTour', login="admin", step_delay=50)
@@ -93,6 +93,7 @@
<script type="text/javascript" src="/point_of_sale/static/src/js/Popups/NumberPopup.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/Popups/OfflineErrorPopup.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/Popups/OrderImportPopup.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/Popups/ProductConfiguratorPopup.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/SetPricelistButton.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/Screens/ProductScreen/ControlButtons/SetFiscalPositionButton.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/ChromeWidgets/ClientScreenButton.js"></script>
@@ -7,6 +7,7 @@
<script type="text/javascript" src="/point_of_sale/static/tests/tours/helpers/utils.js"></script>
<script type="text/javascript" src="/point_of_sale/static/tests/tours/helpers/ProductScreenTourMethods.js"></script>
<script type="text/javascript" src="/point_of_sale/static/tests/tours/helpers/PaymentScreenTourMethods.js"></script>
<script type="text/javascript" src="/point_of_sale/static/tests/tours/helpers/ProductConfiguratorTourMethods.js"></script>
<script type="text/javascript" src="/point_of_sale/static/tests/tours/helpers/OrderManagementScreenTourMethods.js"></script>
<script type="text/javascript" src="/point_of_sale/static/tests/tours/helpers/ClientListScreenTourMethods.js"></script>
<script type="text/javascript" src="/point_of_sale/static/tests/tours/helpers/ReceiptScreenTourMethods.js"></script>
@@ -18,6 +19,7 @@
<script type="text/javascript" src="/point_of_sale/static/tests/tours/point_of_sale.js"></script>
<script type="text/javascript" src="/point_of_sale/static/tests/tours/ProductScreen.tour.js"></script>
<script type="text/javascript" src="/point_of_sale/static/tests/tours/PaymentScreen.tour.js"></script>
<script type="text/javascript" src="/point_of_sale/static/tests/tours/ProductConfigurator.tour.js"></script>
<script type="text/javascript" src="/point_of_sale/static/tests/tours/OrderManagementScreen.tour.js"></script>
<script type="text/javascript" src="/point_of_sale/static/tests/tours/ReceiptScreen.tour.js"></script>
<script type="text/javascript" src="/point_of_sale/static/tests/tours/Chrome.tour.js"></script>
@@ -96,6 +96,17 @@
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box" groups="product.group_product_variant" >
<div class="o_setting_left_pane">
<field name="product_configurator" />
</div>
<div class="o_setting_right_pane">
<label for="product_configurator" />
<div class="text-muted">
Select product attributes
</div>
</div>
</div>
<div id="category_reference" class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="iface_display_categ_images"/>
@@ -45,7 +45,7 @@
<page string="Products" name="products">
<field name="lines" colspan="4" nolabel="1">
<tree string="Order lines" editable="bottom">
<field name="product_id"/>
<field name="full_product_name"/>
<field name="pack_lot_ids" widget="many2many_tags" groups="stock.group_production_lot"/>
<field name="qty"/>
<field name="product_uom_id" string="UoM" groups="uom.group_uom"/>
@@ -58,7 +58,7 @@
</tree>
<form string="Order lines">
<group col="4">
<field name="product_id"/>
<field name="full_product_name"/>
<field name="qty"/>
<field name="discount" widget="monetary"/>
<field name="price_unit" widget="monetary"/>
@@ -30,6 +30,10 @@ class ProductAttribute(models.Model):
required=True)
is_used_on_products = fields.Boolean('Used on Products', compute='_compute_is_used_on_products')
product_tmpl_ids = fields.Many2many('product.template', string="Related Products", compute='_compute_products', store=True)
display_type = fields.Selection([
('radio', 'Radio'),
('select', 'Select'),
('color', 'Color')], default='radio', required=True, help="The display type used in the Product Configurator.")
@api.depends('product_tmpl_ids')
def _compute_is_used_on_products(self):
@@ -94,6 +98,12 @@ class ProductAttributeValue(models.Model):
relation='product_attribute_value_product_template_attribute_line_rel', copy=False)
is_used_on_products = fields.Boolean('Used on Products', compute='_compute_is_used_on_products')
is_custom = fields.Boolean('Is custom value', help="Allow users to input custom values for this attribute value")
html_color = fields.Char(
string='Color',
help="Here you can set a specific HTML color index (e.g. #ff0000) to display the color if the attribute type is 'Color'.")
display_type = fields.Selection(related='attribute_id.display_type', readonly=True)
_sql_constraints = [
('value_company_uniq', 'unique (name, attribute_id)', "You cannot create two values with the same name for the same attribute.")
]
@@ -408,6 +418,10 @@ class ProductTemplateAttributeValue(models.Model):
attribute_id = fields.Many2one('product.attribute', string="Attribute", related='attribute_line_id.attribute_id', store=True, index=True)
ptav_product_variant_ids = fields.Many2many('product.product', relation='product_variant_combination', string="Related Variants", readonly=True)
html_color = fields.Char('HTML Color Index', related="product_attribute_value_id.html_color")
is_custom = fields.Boolean('Is custom value', related="product_attribute_value_id.is_custom")
display_type = fields.Selection(related='product_attribute_value_id.display_type', readonly=True)
_sql_constraints = [
('attribute_value_unique', 'unique(attribute_line_id, product_attribute_value_id)', "Each value should be defined only once per attribute per product."),
]
@@ -540,3 +554,21 @@ class ProductTemplateAttributeExclusion(models.Model):
value_ids = fields.Many2many(
'product.template.attribute.value', relation="product_attr_exclusion_value_ids_rel",
string='Attribute Values', domain="[('product_tmpl_id', '=', product_tmpl_id), ('ptav_active', '=', True)]")
class ProductAttributeCustomValue(models.Model):
_name = "product.attribute.custom.value"
_description = 'Product Attribute Custom Value'
_order = 'custom_product_template_attribute_value_id, id'
name = fields.Char("Name", compute='_compute_name')
custom_product_template_attribute_value_id = fields.Many2one('product.template.attribute.value', string="Attribute Value", required=True, ondelete='restrict')
custom_value = fields.Char("Custom Value")
@api.depends('custom_product_template_attribute_value_id.name', 'custom_value')
def _compute_name(self):
for record in self:
name = (record.custom_value or '').strip()
if record.custom_product_template_attribute_value_id.display_name:
name = "%s: %s" % (record.custom_product_template_attribute_value_id.display_name, name)
record.name = name
@@ -9,6 +9,7 @@ access_product_pricelist_partner_manager,product.pricelist partner manager,model
access_product_product_employee,product.product employee,model_product_product,base.group_user,1,0,0,0
access_product_attribute,product.attribute,model_product_attribute,base.group_user,1,0,0,0
access_product_attribute_value,product.attribute value,model_product_attribute_value,base.group_user,1,0,0,0
access_product_attribute_custom_value_user,product.attribute.custom value manager,model_product_attribute_custom_value,base.group_user,1,0,0,0
access_product_product_attribute,product.template.attribute value,model_product_template_attribute_value,base.group_user,1,0,0,0
access_product_template_attribute_exclusion,product..template.attribute exclusion,model_product_template_attribute_exclusion,base.group_user,1,0,0,0
access_product_template_attribute_line,product.template.attribute line,model_product_template_attribute_line,base.group_user,1,0,0,0
@@ -21,6 +22,7 @@ access_product_pricelist_item_manager,product.pricelist.item.manager,model_produ
access_product_product_manager,product.product manager,model_product_product,base.group_system,1,1,1,1
access_product_attribute_manager,product.attribute manager,model_product_attribute,base.group_system,1,1,1,1
access_product_attribute_value_manager,product.attribute value manager,model_product_attribute_value,base.group_system,1,1,1,1
access_product_attribute_custom_value_manager,product.attribute.custom value manager,model_product_attribute_custom_value,base.group_system,1,1,1,1
access_product_product_attribute_manager,product.template.attribute value manager,model_product_template_attribute_value,base.group_system,1,1,1,1
access_product_template_attribute_exclusion_manager,product.template.attribute exclusion manager,model_product_template_attribute_exclusion,base.group_system,1,1,1,1
access_product_template_attribute_line_manager,product.template.attribute line manager,model_product_template_attribute_line,base.group_system,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
9 access_product_product_employee product.product employee model_product_product base.group_user 1 0 0 0
10 access_product_attribute product.attribute model_product_attribute base.group_user 1 0 0 0
11 access_product_attribute_value product.attribute value model_product_attribute_value base.group_user 1 0 0 0
12 access_product_attribute_custom_value_user product.attribute.custom value manager model_product_attribute_custom_value base.group_user 1 0 0 0
13 access_product_product_attribute product.template.attribute value model_product_template_attribute_value base.group_user 1 0 0 0
14 access_product_template_attribute_exclusion product..template.attribute exclusion model_product_template_attribute_exclusion base.group_user 1 0 0 0
15 access_product_template_attribute_line product.template.attribute line model_product_template_attribute_line base.group_user 1 0 0 0
22 access_product_product_manager product.product manager model_product_product base.group_system 1 1 1 1
23 access_product_attribute_manager product.attribute manager model_product_attribute base.group_system 1 1 1 1
24 access_product_attribute_value_manager product.attribute value manager model_product_attribute_value base.group_system 1 1 1 1
25 access_product_attribute_custom_value_manager product.attribute.custom value manager model_product_attribute_custom_value base.group_system 1 1 1 1
26 access_product_product_attribute_manager product.template.attribute value manager model_product_template_attribute_value base.group_system 1 1 1 1
27 access_product_template_attribute_exclusion_manager product.template.attribute exclusion manager model_product_template_attribute_exclusion base.group_system 1 1 1 1
28 access_product_template_attribute_line_manager product.template.attribute line manager model_product_template_attribute_line base.group_system 1 1 1 1
@@ -7,6 +7,7 @@
<tree string="Variant Values">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="display_type"/>
<field name="create_variant"/>
</tree>
</field>
@@ -22,6 +23,7 @@
<group name="main_fields" class="o_label_nowrap">
<label for="name" string="Attribute Name"/>
<field name="name" nolabel="1"/>
<field name="display_type" widget="radio"/>
<field name="create_variant" widget="radio" attrs="{'readonly': [('is_used_on_products', '=', True)]}"/>
</group>
<notebook>
@@ -30,6 +32,9 @@
<tree string="Values" editable="bottom">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="display_type" invisible="1"/>
<field name="is_custom" groups="product.group_product_variant"/>
<field name="html_color" attrs="{'column_invisible': [('parent.display_type', '!=', 'color')]}" widget="color"/>
</tree>
</field>
</page>
@@ -66,6 +71,7 @@
<field name="value_ids" widget="one2many">
<tree string="Values">
<field name="name"/>
<field name="html_color"/>
</tree>
<form string="Values">
<field name="name"/>
@@ -84,6 +90,8 @@
<tree string="Attributes" create="0" delete="0">
<field name="attribute_id"/>
<field name="name"/>
<field name="display_type"/>
<field name="html_color" attrs="{'invisible': [('display_type', '!=', 'color')]}" widget="color"/>
<field name="ptav_active" optional="hide"/>
<field name="price_extra" widget="monetary"/>
<field name="currency_id" invisible="1"/>
@@ -101,6 +109,8 @@
<group>
<field name="ptav_active" readonly="1" attrs="{'invisible': [('ptav_active', '=', True)]}"/>
<field name="name"/>
<field name="display_type" invisible="1"/>
<field name="html_color" attrs="{'invisible': [('display_type', '!=', 'color')]}"/>
<field name="price_extra" widget="monetary"/>
<field name="currency_id" invisible="1"/>
<field name="exclude_for" widget="one2many" mode="tree">
-1
View File
@@ -32,7 +32,6 @@ This module contains all the common features of Sales Management and eCommerce.
'views/sale_onboarding_views.xml',
'views/res_config_settings_views.xml',
'views/payment_views.xml',
'views/product_attribute_views.xml',
'views/product_views.xml',
'views/utm_campaign_views.xml',
'wizard/sale_order_cancel_views.xml',
+1 -41
View File
@@ -57,50 +57,10 @@ class ProductProduct(models.Model):
return self.product_tmpl_id._get_combination_info(self.product_template_attribute_value_ids, self.id, add_qty, pricelist, parent_combination)
class ProductAttribute(models.Model):
_inherit = "product.attribute"
display_type = fields.Selection([
('radio', 'Radio'),
('select', 'Select'),
('color', 'Color')], default='radio', required=True, help="The display type used in the Product Configurator.")
class ProductAttributeValue(models.Model):
_inherit = "product.attribute.value"
is_custom = fields.Boolean('Is custom value', help="Allow users to input custom values for this attribute value")
html_color = fields.Char(
string='Color',
help="Here you can set a specific HTML color index (e.g. #ff0000) to display the color if the attribute type is 'Color'.")
display_type = fields.Selection(related='attribute_id.display_type', readonly=True)
class ProductTemplateAttributeValue(models.Model):
_inherit = "product.template.attribute.value"
html_color = fields.Char('HTML Color Index', related="product_attribute_value_id.html_color")
is_custom = fields.Boolean('Is custom value', related="product_attribute_value_id.is_custom")
display_type = fields.Selection(related='product_attribute_value_id.display_type', readonly=True)
class ProductAttributeCustomValue(models.Model):
_name = "product.attribute.custom.value"
_description = 'Product Attribute Custom Value'
_order = 'custom_product_template_attribute_value_id, id'
_inherit = "product.attribute.custom.value"
name = fields.Char("Name", compute='_compute_name')
custom_product_template_attribute_value_id = fields.Many2one('product.template.attribute.value', string="Attribute Value", required=True, ondelete='restrict')
sale_order_line_id = fields.Many2one('sale.order.line', string="Sales Order Line", required=True, ondelete='cascade')
custom_value = fields.Char("Custom Value")
@api.depends('custom_product_template_attribute_value_id.name', 'custom_value')
def _compute_name(self):
for record in self:
name = (record.custom_value or '').strip()
if record.custom_product_template_attribute_value_id.display_name:
name = "%s: %s" % (record.custom_product_template_attribute_value_id.display_name, name)
record.name = name
_sql_constraints = [
('sol_custom_value_unique', 'unique(custom_product_template_attribute_value_id, sale_order_line_id)', "Only one Custom Value is allowed per Attribute Value per Sales Order Line.")
+1 -1
View File
@@ -43,7 +43,7 @@ access_product_product_sale_manager,product.product salemanager,model_product_pr
access_product_attribute_sale_manager,product.attribute manager,product.model_product_attribute,sales_team.group_sale_manager,1,1,1,1
access_product_attribute_value_sale_manager,product.attribute manager value,product.model_product_attribute_value,sales_team.group_sale_manager,1,1,1,1
access_product_product_attribute_sale_manager,product.template.attribute manager value,product.model_product_template_attribute_value,sales_team.group_sale_manager,1,1,1,1
access_product_product_attribute_custom_value,product.attribute.custom value manager,model_product_attribute_custom_value,sales_team.group_sale_salesman,1,1,1,1
access_product_product_attribute_custom_value_sale_manager,product.attribute.custom value manager,product.model_product_attribute_custom_value,sales_team.group_sale_salesman,1,1,1,1
access_product_template_attribute_exclusion_sale_manager,product.attribute manager filter line,product.model_product_template_attribute_exclusion,sales_team.group_sale_manager,1,1,1,1
access_product_template_attribute_line_sale_manager,product.attribute manager line,product.model_product_template_attribute_line,sales_team.group_sale_manager,1,1,1,1
access_account_tax_sale_manager,account.tax sale manager,account.model_account_tax,sales_team.group_sale_salesman,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
43 access_product_attribute_sale_manager product.attribute manager product.model_product_attribute sales_team.group_sale_manager 1 1 1 1
44 access_product_attribute_value_sale_manager product.attribute manager value product.model_product_attribute_value sales_team.group_sale_manager 1 1 1 1
45 access_product_product_attribute_sale_manager product.template.attribute manager value product.model_product_template_attribute_value sales_team.group_sale_manager 1 1 1 1
46 access_product_product_attribute_custom_value access_product_product_attribute_custom_value_sale_manager product.attribute.custom value manager model_product_attribute_custom_value product.model_product_attribute_custom_value sales_team.group_sale_salesman 1 1 1 1
47 access_product_template_attribute_exclusion_sale_manager product.attribute manager filter line product.model_product_template_attribute_exclusion sales_team.group_sale_manager 1 1 1 1
48 access_product_template_attribute_line_sale_manager product.attribute manager line product.model_product_template_attribute_line sales_team.group_sale_manager 1 1 1 1
49 access_account_tax_sale_manager account.tax sale manager account.model_account_tax sales_team.group_sale_salesman 1 0 0 0
@@ -1,68 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Product attribute -->
<record id="attribute_tree_view" model="ir.ui.view">
<field name="name">product.attribute.tree.type</field>
<field name="model">product.attribute</field>
<field name="inherit_id" ref="product.attribute_tree_view"></field>
<field name="arch" type="xml">
<field name="name" position="after">
<field name="display_type"/>
</field>
</field>
</record>
<record id="product_template_attribute_line_form" model="ir.ui.view">
<field name="name">product.template.attribute.line.form.inherit</field>
<field name="model">product.template.attribute.line</field>
<field name="inherit_id" ref="product.product_template_attribute_line_form"/>
<field name="priority" eval="8"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='value_ids']/tree/field[@name='name']" position="after">
<field name="html_color"/>
</xpath>
</field>
</record>
<record id="product_attribute_view_form" model="ir.ui.view">
<field name="name">product.attribute.form.inherit</field>
<field name="model">product.attribute</field>
<field name="inherit_id" ref="product.product_attribute_view_form"/>
<field name="priority" eval="8"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='create_variant']" position="before">
<field name="display_type" widget="radio"/>
</xpath>
<xpath expr="//field[@name='value_ids']/tree/field[@name='name']" position="after">
<field name="display_type" invisible="1"/>
<field name="is_custom" groups="product.group_product_variant"/>
<field name="html_color" attrs="{'column_invisible': [('parent.display_type', '!=', 'color')]}" widget="color"/>
</xpath>
</field>
</record>
<record id="product_template_attribute_value_view_tree_inherit" model="ir.ui.view">
<field name="name">product.template.attribute.value.view.tree.inherit</field>
<field name="model">product.template.attribute.value</field>
<field name="inherit_id" ref="product.product_template_attribute_value_view_tree"/>
<field name="arch" type="xml">
<field name="name" position="after">
<field name="display_type"/>
<field name="html_color" attrs="{'invisible': [('display_type', '!=', 'color')]}" widget="color"/>
</field>
</field>
</record>
<record id="product_template_attribute_value_view_form_inherit" model="ir.ui.view">
<field name="name">product.template.attribute.value.view.form.inherit</field>
<field name="model">product.template.attribute.value</field>
<field name="inherit_id" ref="product.product_template_attribute_value_view_form"/>
<field name="arch" type="xml">
<field name="name" position="after">
<field name="display_type" invisible="1"/>
<field name="html_color" attrs="{'invisible': [('display_type', '!=', 'color')]}"/>
</field>
</field>
</record>
</odoo>