[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:
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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,
|
||||
};
|
||||
});
|
||||
+1
-1
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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 && 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 && 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 && 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
|
||||
|
||||
|
@@ -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">
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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,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>
|
||||
Reference in New Issue
Block a user