[IMP] product,(website_)sale: Allow users to fully configure products

Purpose
=======

- Configure a product from a sales order as easily as in the ecommerce
  The backend uses the same template as the frontend to configure a products and its options

- Display optionnal products of optionnal products in the "sale options" section

- Allow adding exlusions to some combinations of the product and/or
  to some combinations of the optionnal and accessory products

- Product attributes can now have custom values.
  These values are specified by the user during the SO creation.
  They are also available on the webshop frontend.

- Product attributes can be configured to create variants "on demand".
  This means that product.templates having these attributes will only create product.product variant
  when that combination is actually selected in a sale order/in the web shop.

  Product attribute values are also improved to handle extra prices and exclusions
  in all three modes ("never", "dynamic, "always")

Specification
=============

1. Improve the "Add to cart" with optional products

- When you add an option to cart, move the product to the cart & show the options of this option
- Don't open the option wizard if no option available (in case this module becomes generic,
  it's better to skip this step when it's not needed)
- Display the options right under their related product in the cart

2. Do not allow to add the product as an optional product for itself
Why ?
- No functional sense
- Creates issues in the add to cart wizard

3. Exclude some attribute combinations within the product or with related options and accessory products

- Rename VARIANT PRICES button -> CONFIGURE VARIANTS
- When you click this button and select a value, open form view rather than inline edition
- Form view of variant values:
	- Attribute
	- Value
	- HTML Color Index
	- Attribute Price Extra
	- Not Compatible with: o2m tab with 2 fields: Product AND Attribute Value (of this product)
	  [m2m tag selection]
	- in any new line, autocomplete the current product by default
	- Tooltip: A list of product and attribute values that you want to exclude for
          this product's attribue value. Also applies on optionnal and accessory products.

4. New option to configure a product in a sales order line

- This option is only visible if the corresponding option is activated in the sales settings
- It opens a wizard to select a product template, configure related attribute values
  and select optional products (based on frontend view)
- Attributes not compatible with other selected attributes should not be selectable
- Must work like in the frontend

5. Create variants on demand
- add a new mode to generate variants as they are added to the order/cart
  (needed for big # of combinations managed in the inventory and/or produced in mrp with specific boms)
- replace the boolean by a selection radio
  tooltip for this new field:
	Always:
	  Variants are generated straight away when
	  the attribute values are combined in the product configuration.
	Only when the product is added to a sales order:
	  Variants are generated once the combination is selected in the sales order.
	  This is advised when variants are customized on demand and manufactured as such in Odoo.
	Never:
	  Advised when variants are not needed in Odoo.
	  The selected values just show up in the description of sales order lines.
- ability to define attribute extra prices & exclude combinations in all variant modes
- if at least one attribute of the product in set to "on demand" mode, generate all the variants "on demand"
This commit is contained in:
Aurélien Warnon
2018-09-25 18:42:07 +02:00
172 changed files with 4307 additions and 12995 deletions
-5
View File
@@ -942,11 +942,6 @@ file_filter = addons/website_sale_management/i18n/<lang>.po
source_file = addons/website_sale_management/i18n/website_sale_management.pot
source_lang = en
[odoo-12.website_sale_options]
file_filter = addons/website_sale_options/i18n/<lang>.po
source_file = addons/website_sale_options/i18n/website_sale_options.pot
source_lang = en
[odoo-12.website_sale_stock]
file_filter = addons/website_sale_stock/i18n/<lang>.po
source_file = addons/website_sale_stock/i18n/website_sale_stock.pot
+8
View File
@@ -234,6 +234,7 @@ class MrpBomLine(models.Model):
bom_id = fields.Many2one(
'mrp.bom', 'Parent BoM',
index=True, ondelete='cascade', required=True)
parent_product_tmpl_id = fields.Many2one('product.template', 'Parent Product Template', related='bom_id.product_tmpl_id')
attribute_value_ids = fields.Many2many(
'product.attribute.value', string='Apply on Variants',
help="BOM Product Variants needed form apply this line.")
@@ -294,6 +295,13 @@ class MrpBomLine(models.Model):
if self.product_id:
self.product_uom_id = self.product_id.uom_id.id
@api.onchange('parent_product_tmpl_id')
def onchange_parent_product(self):
return {'domain': {'attribute_value_ids': [
('id', 'in', self.parent_product_tmpl_id.mapped('attribute_line_ids.value_ids.id')),
('attribute_id.create_variant', '!=', 'never')
]}}
@api.model_create_multi
def create(self, vals_list):
for values in vals_list:
+15 -2
View File
@@ -24,6 +24,14 @@ class ReportBomStructure(models.AbstractModel):
doc['report_type'] = 'pdf'
doc['report_structure'] = data and data.get('report_type') or 'all'
docs.append(doc)
if not candidates:
if data and data.get('childs'):
doc = self._get_pdf_line(bom_id, qty=int(data.get('quantity')), child_bom_ids=json.loads(data.get('childs')))
else:
doc = self._get_pdf_line(bom_id, unfolded=True)
doc['report_type'] = 'pdf'
doc['report_structure'] = data and data.get('report_type') or 'all'
docs.append(doc)
return {
'doc_ids': docids,
'doc_model': 'mrp.bom',
@@ -95,6 +103,12 @@ class ReportBomStructure(models.AbstractModel):
product = self.env['product.product'].browse(int(product_id))
else:
product = bom.product_id or bom.product_tmpl_id.product_variant_id
if product:
attachments = self.env['mrp.document'].search(['|', '&', ('res_model', '=', 'product.product'),
('res_id', '=', product.id), '&', ('res_model', '=', 'product.template'), ('res_id', '=', product.product_tmpl_id.id)])
else:
product = bom.product_tmpl_id
attachments = self.env['mrp.document'].search([('res_model', '=', 'product.template'), ('res_id', '=', product.id)])
operations = self._get_operation_line(bom.routing_id, (bom_quantity / bom.product_qty), 0)
lines = {
'bom': bom,
@@ -108,8 +122,7 @@ class ReportBomStructure(models.AbstractModel):
'level': level or 0,
'operations': operations,
'operations_cost': sum([op['total'] for op in operations]),
'attachments': self.env['mrp.document'].search(['|', '&',
('res_model', '=', 'product.product'), ('res_id', '=', product.id), '&', ('res_model', '=', 'product.template'), ('res_id', '=', product.product_tmpl_id.id)]),
'attachments': attachments,
'operations_time': sum([op['duration_expected'] for op in operations])
}
components, total = self._get_bom_lines(bom, bom_quantity, product, line_id, level)
+3 -2
View File
@@ -57,7 +57,7 @@
</group>
<notebook>
<page string="Components">
<field name="bom_line_ids" widget="one2many_list">
<field name="bom_line_ids" widget="one2many_list" context="{'default_parent_product_tmpl_id': product_tmpl_id}">
<tree string="Components" editable="bottom">
<field name="sequence" widget="handle"/>
<field name="product_id" context="{'default_type': 'product'}"/>
@@ -65,8 +65,9 @@
<button name="action_see_attachments" type="object" icon="fa-files-o" attrs="{'invisible': [('has_attachments', '=', False)]}" aria-label="No attachment" title="No attachment"/>
<button name="action_see_attachments" type="object" icon="fa-file" attrs="{'invisible': [('has_attachments', '=', True)]}" aria-label="Attachments" title="Attachments"/>
<field name="product_qty"/>
<field name="parent_product_tmpl_id" invisible="1" />
<field name="product_uom_id" options="{'no_open':True,'no_create':True}" groups="uom.group_uom"/>
<field name="attribute_value_ids" widget="many2many_tags" options="{'no_create': True}" domain="[('product_ids.product_tmpl_id', '=', parent.product_tmpl_id)]" attrs="{'column_invisible': [('parent.product_id', '!=', False)]}" groups="product.group_product_variant"/>
<field name="attribute_value_ids" widget="many2many_tags" options="{'no_create': True}" attrs="{'column_invisible': [('parent.product_id', '!=', False)]}" groups="product.group_product_variant"/>
<field name="operation_id" groups="mrp.group_mrp_routings" domain="[('routing_id', '=', parent.routing_id)]" options="{'no_quick_create':True,'no_create_edit':True}" attrs="{'column_invisible': [('parent.type', '=', 'phantom')]}"/>
</tree>
</field>
+2 -3
View File
@@ -31,15 +31,14 @@ class TestUi(odoo.tests.HttpCase):
pear = env.ref('point_of_sale.whiteboard')
attribute_value = env['product.attribute.value'].create({
'name': 'add 2',
'product_ids': [(6, 0, [pear.id])],
'attribute_id': env['product.attribute'].create({
'name': 'add 2',
}).id,
})
env['product.attribute.price'].create({
env['product.product.attribute.value'].create({
'product_tmpl_id': pear.product_tmpl_id.id,
'price_extra': 2,
'value_id': attribute_value.id,
'product_attribute_value_id': attribute_value.id,
})
fixed_pricelist = env['product.pricelist'].create({
+24 -14
View File
@@ -212,16 +212,6 @@
<field name="attribute_line_ids" eval="[(6,0,[ref('product.product_attribute_line_1'), ref('product.product_attribute_line_2')])]"/>
</record>
<record id="product_product_4d" model="product.product">
<field name="active" eval="False"/>
</record>
<record id="product_attribute_price_1" model="product.attribute.price">
<field name="product_tmpl_id" ref="product_product_4_product_template"/>
<field name="value_id" ref="product_attribute_value_2"/>
<field name="price_extra">50.40</field>
</record>
<record id="product_product_5" model="product.product">
<field name="name">Corner Desk Right Sit</field>
<field name="categ_id" ref="product_category_5"/>
@@ -320,14 +310,34 @@
<field name="attribute_id" ref="product_attribute_1"/>
<field name="value_ids" eval="[(6,0,[ref('product.product_attribute_value_1'), ref('product.product_attribute_value_2')])]"/>
</record>
<record id="product_product_11_product_template" model="product.template">
<field name="attribute_line_ids" eval="[(6,0,[ref('product.product_attribute_line_4')])]"/>
</record>
<record id="product_attribute_price_2" model="product.attribute.price">
<field name="product_tmpl_id" ref="product_product_11_product_template"/>
<field name="value_id" ref="product_attribute_value_2"/>
<!--
Handle automatically created product.product.attribute.value.
Meaning that the combination between the "customizable desk" and the attribute value "black" will be materialized
into a "product.product.attribute.value" with the ref "product.product_product_attribute_value_1".
This will allow setting fields like "price_extra" and "exclude_for"
-->
<function model="ir.model.data" name="_update_xmlids">
<value model="base" eval="[{
'xml_id': 'product.product_product_attribute_value_1',
'record': obj().env.ref('product.product_attribute_line_1').product_value_ids[1],
'noupdate': True,
}, {
'xml_id': 'product.product_product_attribute_value_2',
'record': obj().env.ref('product.product_attribute_line_4').product_value_ids[1],
'noupdate': True,
}]"/>
</function>
<record id="product_product_attribute_value_1" model="product.product.attribute.value">
<field name="price_extra">50.40</field>
</record>
<record id="product_product_attribute_value_2" model="product.product.attribute.value">
<field name="price_extra">6.40</field>
</record>
+21 -11
View File
@@ -93,7 +93,7 @@ class ProductProduct(models.Model):
lst_price = fields.Float(
'Sale Price', compute='_compute_product_lst_price',
digits=dp.get_precision('Product Price'), inverse='_set_product_lst_price',
help="The sale price is managed from the product template. Click on the 'Variant Prices' button to set the extra attribute prices.")
help="The sale price is managed from the product template. Click on the 'Configure Variants' button to set the extra attribute prices.")
default_code = fields.Char('Internal Reference', index=True)
code = fields.Char('Reference', compute='_compute_product_code')
@@ -110,6 +110,8 @@ class ProductProduct(models.Model):
help="International Article Number used for product identification.")
attribute_value_ids = fields.Many2many(
'product.attribute.value', string='Attributes', ondelete='restrict')
product_attribute_value_ids = fields.Many2many(
'product.product.attribute.value', string='Attribute Values', compute="_compute_product_attribute_value_ids")
# image: all image fields are base64 encoded and PIL-supported
image_variant = fields.Binary(
"Variant Image", attachment=True,
@@ -193,15 +195,10 @@ class ProductProduct(models.Model):
value -= product.price_extra
product.write({'list_price': value})
@api.depends('attribute_value_ids.price_ids.price_extra', 'attribute_value_ids.price_ids.product_tmpl_id')
@api.depends('product_attribute_value_ids.price_extra')
def _compute_product_price_extra(self):
# TDE FIXME: do a real multi and optimize a bit ?
for product in self:
price_extra = 0.0
for attribute_price in product.mapped('attribute_value_ids.price_ids'):
if attribute_price.product_tmpl_id == product.product_tmpl_id:
price_extra += attribute_price.price_extra
product.price_extra = price_extra
product.price_extra = sum(product.mapped('product_attribute_value_ids.price_extra'))
@api.depends('list_price', 'price_extra')
def _compute_product_lst_price(self):
@@ -276,6 +273,12 @@ class ProductProduct(models.Model):
else:
self.product_tmpl_id.image = image
def _compute_product_attribute_value_ids(self):
for product in self:
product.product_attribute_value_ids = self.env['product.product.attribute.value']._search([
('product_tmpl_id', '=', product.product_tmpl_id.id),
('product_attribute_value_id', 'in', product.attribute_value_ids.ids)])
@api.one
def _get_pricelist_items(self):
self.pricelist_item_ids = self.env['product.pricelist.item'].search([
@@ -290,7 +293,7 @@ class ProductProduct(models.Model):
for value in product.attribute_value_ids:
if value.attribute_id in attributes:
raise ValidationError(_('Error! It is not allowed to choose more than one value for a given attribute.'))
if value.attribute_id.create_variant:
if value.attribute_id.create_variant == 'always':
attributes |= value.attribute_id
return True
@@ -324,9 +327,10 @@ class ProductProduct(models.Model):
# Check if product still exists, in case it has been unlinked by unlinking its template
if not product.exists():
continue
# Check if the product is last product of this template
# Check if the product is last product of this template...
other_products = self.search([('product_tmpl_id', '=', product.product_tmpl_id.id), ('id', '!=', product.id)])
if not other_products:
# ... and do not delete product template if it's configured to be created "on demand"
if not other_products and not product.product_tmpl_id.has_dynamic_attributes():
unlink_templates |= product.product_tmpl_id
unlink_products |= product
res = super(ProductProduct, unlink_products).unlink()
@@ -529,6 +533,11 @@ class ProductProduct(models.Model):
prices[product.id] = product[price_type] or 0.0
if price_type == 'list_price':
prices[product.id] += product.price_extra
# we need to add the price from the attributes that do not generate variants
# (see field product.attribute create_variant)
if self._context.get('no_variant_attributes_price_extra'):
# we have a list of price_extra that comes from the attribute values, we need to sum all that
prices[product.id] += sum(self._context.get('no_variant_attributes_price_extra'))
if uom:
prices[product.id] = product.uom_id._compute_price(prices[product.id], uom)
@@ -581,6 +590,7 @@ class ProductProduct(models.Model):
name = self.display_name
if self.description_sale:
name += '\n' + self.description_sale
return name
+116 -50
View File
@@ -16,78 +16,78 @@ class ProductAttribute(models.Model):
value_ids = fields.One2many('product.attribute.value', 'attribute_id', 'Values', copy=True)
sequence = fields.Integer('Sequence', help="Determine the display order")
attribute_line_ids = fields.One2many('product.attribute.line', 'attribute_id', 'Lines')
create_variant = fields.Boolean(default=True, help="Check this if you want to create multiple variants for this attribute.")
create_variant = fields.Selection([
('never', 'Never'),
('always', 'Always'),
('dynamic', 'Only when the product is added to a sales order')],
default='always',
string="Create Variants",
help="Check this if you want to create multiple variants for this attribute.", required=True)
class ProductAttributevalue(models.Model):
class ProductAttributeValue(models.Model):
_name = "product.attribute.value"
_order = 'attribute_id, sequence, id'
_description = 'Product Attribute Value'
_description = 'Attribute Value'
name = fields.Char('Attribute Value', required=True, translate=True)
sequence = fields.Integer('Sequence', help="Determine the display order")
attribute_id = fields.Many2one('product.attribute', 'Attribute', ondelete='cascade', required=True)
product_ids = fields.Many2many('product.product', string='Variants', readonly=True)
price_extra = fields.Float(
'Attribute Price Extra', compute='_compute_price_extra', inverse='_set_price_extra',
default=0.0, digits=dp.get_precision('Product Price'),
help="Price Extra: Extra price for the variant with this attribute value on sale price. eg. 200 price extra, 1000 + 200 = 1200.")
price_ids = fields.One2many('product.attribute.price', 'value_id', 'Attribute Prices', readonly=True)
name = fields.Char(string='Value', required=True, translate=True)
sequence = fields.Integer(string='Sequence', help="Determine the display order")
attribute_id = fields.Many2one('product.attribute', string='Attribute', ondelete='cascade', required=True)
_sql_constraints = [
('value_company_uniq', 'unique (name,attribute_id)', 'This attribute value already exists !')
('value_company_uniq', 'unique (name, attribute_id)', 'This attribute value already exists !')
]
@api.one
def _compute_price_extra(self):
if self._context.get('active_id'):
price = self.price_ids.filtered(lambda price: price.product_tmpl_id.id == self._context['active_id'])
self.price_extra = price.price_extra
else:
self.price_extra = 0.0
def _set_price_extra(self):
if not self._context.get('active_id'):
return
AttributePrice = self.env['product.attribute.price']
prices = AttributePrice.search([('value_id', 'in', self.ids), ('product_tmpl_id', '=', self._context['active_id'])])
updated = prices.mapped('value_id')
if prices:
prices.write({'price_extra': self.price_extra})
else:
for value in self - updated:
AttributePrice.create({
'product_tmpl_id': self._context['active_id'],
'value_id': value.id,
'price_extra': self.price_extra,
})
@api.multi
def name_get(self):
if not self._context.get('show_attribute', True): # TDE FIXME: not used
return super(ProductAttributevalue, self).name_get()
return super(ProductAttributeValue, self).name_get()
return [(value.id, "%s: %s" % (value.attribute_id.name, value.name)) for value in self]
@api.multi
def _variant_name(self, variable_attributes):
return ", ".join([v.name for v in self if v.attribute_id in variable_attributes])
@api.multi
def unlink(self):
linked_products = self.env['product.product'].with_context(active_test=False).search([('attribute_value_ids', 'in', self.ids)])
if linked_products:
raise UserError(_('The operation cannot be completed:\nYou are trying to delete an attribute value with a reference on a product variant.'))
return super(ProductAttributevalue, self).unlink()
return super(ProductAttributeValue, self).unlink()
class ProductProductAttributeValue(models.Model):
_name = "product.product.attribute.value"
_order = 'sequence, attribute_id, id'
_description = 'Product Attribute Value'
name = fields.Char('Value', related="product_attribute_value_id.name")
product_attribute_value_id = fields.Many2one('product.attribute.value', string='Attribute Value', required=True, ondelete='cascade', index=True)
product_tmpl_id = fields.Many2one('product.template', string='Product Template', required=True, ondelete='cascade', index=True)
attribute_id = fields.Many2one('product.attribute', string='Attribute', related="product_attribute_value_id.attribute_id")
sequence = fields.Integer('Sequence', related="product_attribute_value_id.sequence")
price_extra = fields.Float(
string='Attribute Price Extra', default=0.0, digits=dp.get_precision('Product Price'),
help="Price Extra: Extra price for the variant with this attribute value on sale price. eg. 200 price extra, 1000 + 200 = 1200.")
exclude_for = fields.Many2many(
'product.attribute.filter.line', string="Exclude for", relation="product_attribute_value_exclusion",
help="Make this attribute value not compatible with other values of the product or some attribute values of optional and accessory products.")
@api.multi
def _variant_name(self, variable_attributes):
return ", ".join([v.name for v in self if v.attribute_id in variable_attributes])
def name_get(self):
if not self._context.get('show_attribute', True): # TDE FIXME: not used
return super(ProductAttributeValue, self).name_get()
return [(value.id, "%s: %s" % (value.attribute_id.name, value.name)) for value in self]
class ProductAttributePrice(models.Model):
_name = "product.attribute.price"
_description = 'Product Attribute Price'
class ProductAttributeFilterLine(models.Model):
_name = "product.attribute.filter.line"
_description = 'Product Attribute Filter Line'
product_tmpl_id = fields.Many2one('product.template', 'Product Template', ondelete='cascade', required=True)
value_id = fields.Many2one('product.attribute.value', 'Product Attribute Value', ondelete='cascade', required=True)
price_extra = fields.Float('Price Extra', digits=dp.get_precision('Product Price'))
product_tmpl_id = fields.Many2one('product.template', string='Product Template', ondelete='cascade', required=True)
value_ids = fields.Many2many(
'product.product.attribute.value', relation="product_attr_filter_line_value_ids_rel",
string='Attribute Values', domain="[('product_tmpl_id', '=', product_tmpl_id)]")
class ProductAttributeLine(models.Model):
@@ -95,9 +95,15 @@ class ProductAttributeLine(models.Model):
_rec_name = 'attribute_id'
_description = 'Product Attribute Line'
product_tmpl_id = fields.Many2one('product.template', 'Product Template', ondelete='cascade', required=True)
attribute_id = fields.Many2one('product.attribute', 'Attribute', ondelete='restrict', required=True)
product_tmpl_id = fields.Many2one('product.template', string='Product Template', ondelete='cascade', required=True)
attribute_id = fields.Many2one('product.attribute', string='Attribute', ondelete='restrict', required=True)
value_ids = fields.Many2many('product.attribute.value', string='Attribute Values')
product_value_ids = fields.Many2many(
'product.product.attribute.value',
string='Product Attribute Values',
compute="_set_product_value_ids",
store=False
)
@api.constrains('value_ids', 'attribute_id')
def _check_valid_attribute(self):
@@ -105,6 +111,66 @@ class ProductAttributeLine(models.Model):
raise ValidationError(_('You cannot use this attribute with the following value.'))
return True
@api.model
def create(self, values):
res = super(ProductAttributeLine, self).create(values)
res._update_product_product_attribute_values()
return res
def write(self, values):
res = super(ProductAttributeLine, self).write(values)
self._update_product_product_attribute_values()
return res
@api.depends('value_ids')
def _set_product_value_ids(self):
for product_attribute_line in self:
product_attribute_line.product_value_ids = self.env['product.product.attribute.value'].search([
('product_tmpl_id', 'in', product_attribute_line.product_tmpl_id.ids),
('product_attribute_value_id.attribute_id', 'in', product_attribute_line.value_ids.mapped('attribute_id').ids)]
).sorted(lambda product_product_attribute: product_product_attribute.sequence)
@api.multi
def unlink(self):
for product_attribute_line in self:
self.env['product.product.attribute.value'].search([
('product_tmpl_id', 'in', product_attribute_line.product_tmpl_id.ids),
('product_attribute_value_id.attribute_id', 'in', product_attribute_line.value_ids.mapped('attribute_id').ids)]).unlink()
return super(ProductAttributeLine, self).unlink()
def _update_product_product_attribute_values(self):
"""
Create or unlink product.product.attribute.value based on the attribute lines.
If the product.attribute.value is removed, remove the corresponding product.product.attribute.value
If no product.product.attribute.value exists for the newly added product.attribute.value, create it.
"""
for attribute_line in self:
# All existing product.product.attribute.value for this template
product_product_attribute_values_to_remove = self.env['product.product.attribute.value'].search([
('product_tmpl_id', '=', attribute_line.product_tmpl_id.id),
('product_attribute_value_id.attribute_id', 'in', attribute_line.value_ids.mapped('attribute_id').ids)])
# All existing product.attribute.value shared by all products
# eg (Yellow, Red, Blue, Small, Large)
existing_product_attribute_values = product_product_attribute_values_to_remove.mapped('product_attribute_value_id')
# Loop on product.attribute.values for the line (eg: Yellow, Red, Blue)
for product_attribute_value in attribute_line.value_ids:
if product_attribute_value in existing_product_attribute_values:
# property is already existing: don't touch, remove it from list to avoid unlinking it
product_product_attribute_values_to_remove = product_product_attribute_values_to_remove.filtered(
lambda value: product_attribute_value not in value.mapped('product_attribute_value_id')
)
else:
# property does not exist: create it
self.env['product.product.attribute.value'].create({
'product_attribute_value_id': product_attribute_value.id,
'product_tmpl_id': attribute_line.product_tmpl_id.id})
# at this point, existing properties can be removed to reflect the modifications on value_ids
if product_product_attribute_values_to_remove:
product_product_attribute_values_to_remove.unlink()
@api.model
def _name_search(self, name, args=None, operator='ilike', limit=100, name_get_uid=None):
# TDE FIXME: currently overriding the domain; however as it includes a
+88 -8
View File
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import itertools
import operator
import psycopg2
from odoo.addons import decimal_precision as dp
@@ -9,6 +10,7 @@ from odoo.addons import decimal_precision as dp
from odoo import api, fields, models, tools, _
from odoo.exceptions import ValidationError, RedirectWarning, except_orm
from odoo.tools import pycompat
from odoo.exceptions import UserError
class ProductTemplate(models.Model):
@@ -159,6 +161,15 @@ class ProductTemplate(models.Model):
@api.multi
def _compute_template_price(self):
prices = self._compute_template_price_no_inverse()
for template in self:
template.price = prices.get(template.id, 0.0)
@api.multi
def _compute_template_price_no_inverse(self):
"""The _compute_template_price writes the 'list_price' field with an inverse method
This method allows computing the price without writing the 'list_price'
"""
prices = {}
pricelist_id_or_name = self._context.get('pricelist')
if pricelist_id_or_name:
@@ -179,8 +190,7 @@ class ProductTemplate(models.Model):
partners = [partner] * len(self)
prices = pricelist.get_products_price(self, quantities, partners)
for template in self:
template.price = prices.get(template.id, 0.0)
return prices
@api.multi
def _set_template_price(self):
@@ -403,6 +413,11 @@ class ProductTemplate(models.Model):
prices = dict.fromkeys(self.ids, 0.0)
for template in templates:
prices[template.id] = template[price_type] or 0.0
# yes, there can be attribute values for product template if it's not a variant YET
# (see field product.attribute create_variant)
if price_type == 'list_price' and self._context.get('current_attributes_price_extra'):
# we have a list of price_extra that comes from the attribute values, we need to sum all that
prices[template.id] += sum(self._context.get('current_attributes_price_extra'))
if uom:
prices[template.id] = template.uom_id._compute_price(prices[template.id], uom)
@@ -431,7 +446,7 @@ class ProductTemplate(models.Model):
for tmpl_id in self.with_context(active_test=False):
# adding an attribute with only one value should not recreate product
# write this attribute on every product to make sure we don't lose them
variant_alone = tmpl_id.attribute_line_ids.filtered(lambda line: line.attribute_id.create_variant and len(line.value_ids) == 1).mapped('value_ids')
variant_alone = tmpl_id.attribute_line_ids.filtered(lambda line: line.attribute_id.create_variant == 'always' and len(line.value_ids) == 1).mapped('value_ids')
for value_id in variant_alone:
updated_products = tmpl_id.product_variant_ids.filtered(lambda product: value_id.attribute_id not in product.mapped('attribute_value_ids.attribute_id'))
updated_products.write({'attribute_value_ids': [(4, value_id.id)]})
@@ -439,25 +454,31 @@ class ProductTemplate(models.Model):
# iterator of n-uple of product.attribute.value *ids*
variant_matrix = [
AttributeValues.browse(value_ids)
for value_ids in itertools.product(*(line.value_ids.ids for line in tmpl_id.attribute_line_ids if line.value_ids[:1].attribute_id.create_variant))
for value_ids in itertools.product(*(line.value_ids.ids for line in tmpl_id.attribute_line_ids if line.value_ids[:1].attribute_id.create_variant != 'never'))
]
# get the value (id) sets of existing variants
existing_variants = {frozenset(variant.attribute_value_ids.filtered(lambda r: r.attribute_id.create_variant).ids) for variant in tmpl_id.product_variant_ids}
existing_variants = {frozenset(variant.attribute_value_ids.filtered(lambda r: r.attribute_id.create_variant != 'never').ids) for variant in tmpl_id.product_variant_ids}
# -> for each value set, create a recordset of values to create a
# variant for if the value set isn't already a variant
for value_ids in variant_matrix:
if set(value_ids.ids) not in existing_variants:
if set(value_ids.ids) not in existing_variants and not any(value_id.attribute_id.create_variant == 'dynamic' for value_id in value_ids):
variants_to_create.append({
'product_tmpl_id': tmpl_id.id,
'attribute_value_ids': [(6, 0, value_ids.ids)]
})
if len(variants_to_create) > 1000:
raise UserError(_("""
The number of variants to generate is too high.
You should either not generate variants for each combination or generate them on demand from the sales order.
To do so, open the form view of attributes and change the mode of *Create Variants*."""))
# check product
for product_id in tmpl_id.product_variant_ids:
if not product_id.active and product_id.attribute_value_ids.filtered(lambda r: r.attribute_id.create_variant) in variant_matrix:
if not product_id.active and product_id.attribute_value_ids.filtered(lambda r: r.attribute_id.create_variant != 'never') in variant_matrix:
variants_to_activate.append(product_id)
elif product_id.attribute_value_ids.filtered(lambda r: r.attribute_id.create_variant) not in variant_matrix:
elif product_id.attribute_value_ids.filtered(lambda r: r.attribute_id.create_variant != 'never') not in variant_matrix:
variants_to_unlink.append(product_id)
if variants_to_activate:
@@ -479,6 +500,65 @@ class ProductTemplate(models.Model):
return True
def has_dynamic_attributes(self):
return self.attribute_line_ids and \
any(create_variant == 'dynamic'
for create_variant in self.mapped('attribute_line_ids.value_ids.attribute_id.create_variant'))
@api.multi
def get_filtered_variants(self, reference_product=None):
"""
Will filter availability (excluded) for the product
combinations (ex: color: white excludes size: large).
Will also filter availability (excluded) for the parent
product if specified (meaning that this product is a an optionnal or
accessory product of the reference_product).
Args:
reference_product (product.product): The reference product that has
the current product as an option or accessory product.
Returns:
The filtered list of product variants
"""
self.ensure_one()
product_product_attribute_values = self.env['product.product.attribute.value'].search([('product_tmpl_id', '=', self.id)])
if reference_product:
# append the reference_product if provided
product_product_attribute_values |= reference_product.product_attribute_value_ids
product_variants = self.product_variant_ids
for product_product_attribute_value in product_product_attribute_values:
# CASE 1: The whole product is excluded when no attribute values are selected in the parent product
# returns empty recordset of product.product if so. What is checked is:
# If the product_attribute value doesn't belong to self (i.e. belongs to the reference product)
# and self is the excluded product template on the exclusion lines
# and the exclusions is on the product without specified product attribute values (i.e. the whole product is excluded)
if product_product_attribute_value.product_tmpl_id != self \
and self in product_product_attribute_value.exclude_for.mapped('product_tmpl_id') \
and any(not exclude_for.value_ids
for exclude_for in product_product_attribute_value.exclude_for.filtered(
lambda excluded_product_attribute_value: excluded_product_attribute_value.product_tmpl_id == self)):
return self.env['product.product']
# CASE 2: Check if some of the product.product.attribute.value of the product are excluded
# for this prodcut. A variant could be excluded:
# - Either by itself (eg: The office chair with iron legs excludes the color white)
# - Or by the reference product (eg: The customizable desk with iron legs excludes the office chair with aluminium legs)
for excluded in product_product_attribute_value.exclude_for.filtered(
lambda excluded_product_attribute_value: excluded_product_attribute_value.product_tmpl_id == self):
product_variants -= product_variants.filtered(
lambda variant:
# 1/ Check the applicability of the exclusion
# i.e: the restriction comes from the parent
# OR the restriction is on a product_attribute_value that this variant has, eg:
# if the office chair with iron legs excludes the color white, we must check
# that this variant has iron legs to check the exclusion
(product_product_attribute_value.product_tmpl_id != self or product_product_attribute_value in variant.product_attribute_value_ids) and
# 2/ Check the variant has one of the excluded attribute values
any(attribute_value in excluded.value_ids for attribute_value in variant.product_attribute_value_ids))
return product_variants
@api.model
def get_empty_list_help(self, help):
self = self.with_context(
+1 -1
View File
@@ -13,7 +13,7 @@ class ResConfigSettings(models.TransientModel):
" * Checked : Product are visible for every company, even if a company is defined on the partner.\n"
" * Unchecked : Each company can see only its product (product where company is defined). Product not related to a company are visible for all companies.")
group_uom = fields.Boolean("Units of Measure", implied_group='uom.group_uom')
group_product_variant = fields.Boolean("Attributes and Variants", implied_group='product.group_product_variant')
group_product_variant = fields.Boolean("Variants and Options", implied_group='product.group_product_variant')
group_stock_packaging = fields.Boolean('Product Packagings',
implied_group='product.group_stock_packaging')
group_sale_pricelist = fields.Boolean("Use pricelists to adapt your price per customers",
+4 -2
View File
@@ -10,7 +10,8 @@ access_product_product_employee,product.product employee,model_product_product,b
access_product_price_history_employee,prices.history employee,model_product_price_history,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_price,product.attribute price,model_product_attribute_price,base.group_user,1,0,0,0
access_product_product_attribute,product.product.attribute value,model_product_product_attribute_value,base.group_user,1,0,0,0
access_product_attribute_filter_line,product.attribute filter line,model_product_attribute_filter_line,base.group_user,1,0,0,0
access_product_attribute_line,product.attribute line,model_product_attribute_line,base.group_user,1,0,0,0
access_product_category_manager,product.category.manager,model_product_category,base.group_system,1,1,1,1
access_product_template_manager,product.template.manager,model_product_template,base.group_system,1,1,1,1
@@ -22,5 +23,6 @@ access_product_product_manager,product.product manager,model_product_product,bas
access_product_price_history_manager,prices.history manager,model_product_price_history,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_price_manager,product.attribute price manager,model_product_attribute_price,base.group_system,1,1,1,1
access_product_product_attribute_manager,product.product.attribute value manager,model_product_product_attribute_value,base.group_system,1,1,1,1
access_product_attribute_filter_line_manager,product.attribute filter line manager,model_product_attribute_filter_line,base.group_system,1,1,1,1
access_product_attribute_line_manager,product.attribute line manager,model_product_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
10 access_product_price_history_employee prices.history employee model_product_price_history base.group_user 1 0 0 0
11 access_product_attribute product.attribute model_product_attribute base.group_user 1 0 0 0
12 access_product_attribute_value product.attribute value model_product_attribute_value base.group_user 1 0 0 0
13 access_product_attribute_price access_product_product_attribute product.attribute price product.product.attribute value model_product_attribute_price model_product_product_attribute_value base.group_user 1 0 0 0
14 access_product_attribute_filter_line product.attribute filter line model_product_attribute_filter_line base.group_user 1 0 0 0
15 access_product_attribute_line product.attribute line model_product_attribute_line base.group_user 1 0 0 0
16 access_product_category_manager product.category.manager model_product_category base.group_system 1 1 1 1
17 access_product_template_manager product.template.manager model_product_template base.group_system 1 1 1 1
23 access_product_price_history_manager prices.history manager model_product_price_history base.group_system 1 1 1 1
24 access_product_attribute_manager product.attribute manager model_product_attribute base.group_system 1 1 1 1
25 access_product_attribute_value_manager product.attribute value manager model_product_attribute_value base.group_system 1 1 1 1
26 access_product_attribute_price_manager access_product_product_attribute_manager product.attribute price manager product.product.attribute value manager model_product_attribute_price model_product_product_attribute_value base.group_system 1 1 1 1
27 access_product_attribute_filter_line_manager product.attribute filter line manager model_product_attribute_filter_line base.group_system 1 1 1 1
28 access_product_attribute_line_manager product.attribute line manager model_product_attribute_line base.group_system 1 1 1 1
+5 -1
View File
@@ -1,4 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_variants, test_pricelist, test_product_pricelist, test_seller
from . import test_variants
from . import test_pricelist
from . import test_product_pricelist
from . import test_seller
from . import test_product_attribute_value_config
@@ -0,0 +1,166 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests.common import TransactionCase
class TestProductAttributeValueConfig(TransactionCase):
def setUp(self):
super(TestProductAttributeValueConfig, self).setUp()
self.computer = self.env['product.template'].create({
'name': 'Super Computer'
})
self._add_ssd_attribute()
self._add_ram_attribute()
self._add_hdd_attribute()
self.computer.create_variant_ids()
self.computer_case = self.env['product.template'].create({
'name': 'Super Computer Case'
})
self._add_size_attribute()
self.computer_case.create_variant_ids()
def test_product_product_attribute_values_creation(self):
self.assertEqual(len(self.computer_ssd_attribute_lines.product_value_ids), 2, 'Product attribute values (ssd) were not automatically created')
self.assertEqual(len(self.computer_ram_attribute_lines.product_value_ids), 3, 'Product attribute values (ram) were not automatically created')
self.assertEqual(len(self.computer_hdd_attribute_lines.product_value_ids), 3, 'Product attribute values (hdd) were not automatically created')
self.assertEqual(len(self.computer_case_size_attribute_lines.product_value_ids), 3, 'Product attribute values (size) were not automatically created')
def test_product_filtered_exclude_for(self):
"""
Super Computer has 18 variants total (2 ssd * 3 ram * 3 hdd)
RAM 16 excudes HDD 1, that matches 2 variants:
- SSD 256 RAM 16 HDD 1
- SSD 512 RAM 16 HDD 1
=> There has to be 16 variants left when filtered
"""
self._add_ram_exclude_for()
self.assertEqual(len(self.computer.get_filtered_variants()), 16)
self.assertFalse(self._get_variant_for_attribute_values(self.computer, [self.ssd_256, self.ram_16, self.hdd_1]))
self.assertFalse(self._get_variant_for_attribute_values(self.computer, [self.ssd_512, self.ram_16, self.hdd_1]))
def test_children_product_filtered_exclude_for(self):
"""
Super Computer Case has 3 variants total (3 size)
Reference product Computer with HDD 4 excludes Size M
The following variant will be excluded:
- Size M
=> There has to be 2 variants left when filtered
"""
self._add_hdd_excludes_computer_case()
hdd_4_variant = self.computer.product_variant_ids.filtered(
lambda variant: self.hdd_4 in variant.product_attribute_value_ids.mapped('product_attribute_value_id'))[0]
self.assertEqual(len(self.computer_case.get_filtered_variants(hdd_4_variant)), 2)
self.assertFalse(self._get_variant_for_attribute_values(self.computer_case, [self.size_m], hdd_4_variant))
def _add_ssd_attribute(self):
self.ssd_attribute = self.env['product.attribute'].create({'name': 'Memory'})
self.ssd_256 = self.env['product.attribute.value'].create({
'name': '256 GB',
'attribute_id': self.ssd_attribute.id
})
self.ssd_512 = self.env['product.attribute.value'].create({
'name': '512 GB',
'attribute_id': self.ssd_attribute.id
})
self.computer_ssd_attribute_lines = self.env['product.attribute.line'].create({
'product_tmpl_id': self.computer.id,
'attribute_id': self.ssd_attribute.id,
'value_ids': [(6, 0, [self.ssd_256.id, self.ssd_512.id])]
})
def _add_ram_attribute(self):
self.ram_attribute = self.env['product.attribute'].create({'name': 'RAM'})
self.ram_8 = self.env['product.attribute.value'].create({
'name': '8 GB',
'attribute_id': self.ram_attribute.id
})
self.ram_16 = self.env['product.attribute.value'].create({
'name': '16 GB',
'attribute_id': self.ram_attribute.id
})
self.ram_32 = self.env['product.attribute.value'].create({
'name': '32 GB',
'attribute_id': self.ram_attribute.id
})
self.computer_ram_attribute_lines = self.env['product.attribute.line'].create({
'product_tmpl_id': self.computer.id,
'attribute_id': self.ram_attribute.id,
'value_ids': [(6, 0, [self.ram_8.id, self.ram_16.id, self.ram_32.id])]
})
def _add_hdd_attribute(self):
self.hdd_attribute = self.env['product.attribute'].create({'name': 'HDD'})
self.hdd_1 = self.env['product.attribute.value'].create({
'name': '1 To',
'attribute_id': self.hdd_attribute.id
})
self.hdd_2 = self.env['product.attribute.value'].create({
'name': '2 To',
'attribute_id': self.hdd_attribute.id
})
self.hdd_4 = self.env['product.attribute.value'].create({
'name': '4 To',
'attribute_id': self.hdd_attribute.id
})
self.computer_hdd_attribute_lines = self.env['product.attribute.line'].create({
'product_tmpl_id': self.computer.id,
'attribute_id': self.hdd_attribute.id,
'value_ids': [(6, 0, [self.hdd_1.id, self.hdd_2.id, self.hdd_4.id])]
})
def _add_ram_exclude_for(self):
self.ram_16_excludes_hdd_1 = self.env['product.attribute.filter.line'].create({
'product_tmpl_id': self.computer.id,
'value_ids': [(6, 0, [self._get_product_value_id(self.computer_hdd_attribute_lines, self.hdd_1).id])]
})
self._get_product_value_id(self.computer_ram_attribute_lines, self.ram_16).update({
'exclude_for': [(6, 0, [self.ram_16_excludes_hdd_1.id])]
})
def _add_size_attribute(self):
self.size_attribute = self.env['product.attribute'].create({'name': 'Size'})
self.size_m = self.env['product.attribute.value'].create({
'name': 'M',
'attribute_id': self.size_attribute.id
})
self.size_l = self.env['product.attribute.value'].create({
'name': 'L',
'attribute_id': self.size_attribute.id
})
self.size_xl = self.env['product.attribute.value'].create({
'name': 'XL',
'attribute_id': self.size_attribute.id
})
self.computer_case_size_attribute_lines = self.env['product.attribute.line'].create({
'product_tmpl_id': self.computer_case.id,
'attribute_id': self.size_attribute.id,
'value_ids': [(6, 0, [self.size_m.id, self.size_l.id, self.size_xl.id])]
})
def _add_hdd_excludes_computer_case(self):
self.hdd_4_excludes_computer_case_m = self.env['product.attribute.filter.line'].create({
'product_tmpl_id': self.computer_case.id,
'value_ids': [(6, 0, [self._get_product_value_id(self.computer_case_size_attribute_lines, self.size_m).id])]
})
self._get_product_value_id(self.computer_hdd_attribute_lines, self.hdd_4).update({
'exclude_for': [(6, 0, [self.hdd_4_excludes_computer_case_m.id])]
})
def _get_product_value_id(self, product_attribute_lines, product_attribute_value):
return product_attribute_lines.product_value_ids.filtered(
lambda product_value_id: product_value_id.product_attribute_value_id == product_attribute_value)[0]
def _get_variant_for_attribute_values(self, product, attribute_values, reference_product=None):
return product.get_filtered_variants(reference_product).filtered(
lambda variant:
all(attribute_value in variant.attribute_value_ids for attribute_value in attribute_values))
+1 -1
View File
@@ -169,7 +169,7 @@ class TestVariantsNoCreate(common.TestProductCommon):
super(TestVariantsNoCreate, self).setUp()
self.size = self.env['product.attribute'].create({
'name': 'Size',
'create_variant': False,
'create_variant': 'never',
'value_ids': [(0, 0, {'name': 'S'}), (0, 0, {'name': 'M'}), (0, 0, {'name': 'L'})],
})
self.size_S = self.size.value_ids[0]
+101 -77
View File
@@ -1,96 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="product_attribute_value_view_tree" model="ir.ui.view">
<field name="name">product.attribute.value.view.tree</field>
<field name="model">product.attribute.value</field>
<record id="product_product_attribute_value_view_tree" model="ir.ui.view">
<field name="name">product.product.attribute.value.view.tree</field>
<field name="model">product.product.attribute.value</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Attributes" editable="top">
<field name="sequence" widget="handle"/>
<tree string="Attributes" create="0" delete="0">
<field name="attribute_id"/>
<field name="name"/>
<field name="price_extra"/>
<field name="price_extra" />
</tree>
</field>
</record>
<record id="product_product_attribute_value_view_form" model="ir.ui.view">
<field name="name">product.product.attribute.value.view.form.</field>
<field name="model">product.product.attribute.value</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Product Attribute" create="0" delete="0">
<group>
<field name="name" />
<field name="price_extra" />
<field name="exclude_for" widget="one2many_list" mode="tree">
<tree editable="top">
<field name="product_tmpl_id" />
<field name="value_ids" widget="many2many_tags" options="{'no_create': true}" />
</tree>
</field>
</group>
</form>
</field>
</record>
<record id="product_attribute_value_action" model="ir.actions.act_window">
<field name="name">Variant Values</field>
<field name="name">Product Variant Values</field>
<field name="res_model">product.product.attribute.value</field>
<field name="view_mode">tree,form</field>
<field name="domain">[('product_tmpl_id', '=', active_id)]</field>
<field name="view_ids"
eval="[(5, 0, 0),
(0, 0, {'view_mode': 'tree', 'view_id': ref('product.product_product_attribute_value_view_tree')}),
(0, 0, {'view_mode': 'form', 'view_id': ref('product.product_product_attribute_value_view_form')})]" />
<field name="context">{
'default_product_tmpl_id': active_id
}</field>
</record>
<record id="attribute_tree_view" model="ir.ui.view">
<field name="name">product.attribute.tree</field>
<field name="model">product.attribute</field>
<field name="arch" type="xml">
<tree string="Variant Values" editable="top">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="create_variant"/>
</tree>
</field>
</record>
<record id="product_attribute_view_form" model="ir.ui.view">
<field name="name">product.attribute.form</field>
<field name="model">product.attribute</field>
<field name="arch" type="xml">
<form string="Product Attribute">
<group>
<group name="main_fields">
<label for="name" string="Attribute Name"/>
<field name="name" nolabel="1"/>
</group>
</group>
<group name="values_ids">
<label for="value_ids" string="Attribute Values"/>
<field name="value_ids" widget="one2many_list" nolabel="1">
<tree string="Values" editable="bottom">
<field name="name"/>
</tree>
<form string="Values">
<field name="name"/>
</form>
</field>
<field name="create_variant"/>
</group>
</form>
</field>
</record>
<record id="attribute_action" model="ir.actions.act_window">
<field name="name">Attributes</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">product.attribute</field>
<field name="view_mode">tree</field>
<field name="view_type">form</field>
</record>
<record id="variants_tree_view" model="ir.ui.view">
<field name="name">product.attribute.value.tree</field>
<field name="model">product.attribute.value</field>
<field name="arch" type="xml">
<tree string="Variant Values" editable="top">
<field name="sequence" widget="handle"/>
<field name="attribute_id"/>
<field name="name"/>
</tree>
</field>
</record>
<record id="variants_action" model="ir.actions.act_window">
<field name="name">Attribute Values</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">product.attribute.value</field>
<field name="view_mode">tree</field>
<field name="view_type">form</field>
<field name="view_id" ref="product_attribute_value_view_tree"/>
<field name="domain">[('product_ids.product_tmpl_id', '=', active_id)]</field>
<field name="context">{'default_product_tmpl_id': active_id}</field>
</record>
<record id="attribute_tree_view" model="ir.ui.view">
<field name="name">product.attribute.tree</field>
<field name="model">product.attribute</field>
<field name="arch" type="xml">
<tree string="Variant Values" editable="top">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="create_variant"/>
</tree>
</field>
</record>
<record id="product_attribute_view_form" model="ir.ui.view">
<field name="name">product.attribute.form</field>
<field name="model">product.attribute</field>
<field name="arch" type="xml">
<form string="Product Attribute">
<group>
<group name="main_fields">
<label for="name" string="Attribute Name"/>
<field name="name" nolabel="1"/>
</group>
</group>
<group name="values_ids">
<label for="value_ids" string="Attribute Values"/>
<field name="value_ids" widget="one2many_list" nolabel="1">
<tree string="Values" editable="bottom">
<field name="name"/>
</tree>
<form string="Values">
<field name="name"/>
</form>
</field>
<field name="create_variant"/>
</group>
</form>
</field>
</record>
<record id="attribute_action" model="ir.actions.act_window">
<field name="name">Attributes</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">product.attribute</field>
<field name="view_mode">tree</field>
<field name="view_type">form</field>
</record>
<record id="variants_tree_view" model="ir.ui.view">
<field name="name">product.attribute.value.tree</field>
<field name="model">product.attribute.value</field>
<field name="arch" type="xml">
<tree string="Variant Values" editable="top">
<field name="sequence" widget="handle"/>
<field name="attribute_id"/>
<field name="name"/>
</tree>
</field>
</record>
<record id="variants_action" model="ir.actions.act_window">
<field name="name">Attribute Values</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">product.attribute.value</field>
<field name="view_mode">tree</field>
<field name="view_type">form</field>
</record>
<record id="product_attribute_line_form" model="ir.ui.view">
<field name="name">product.attribute.line.form</field>
<field name="model">product.attribute.line</field>
+3 -2
View File
@@ -9,14 +9,15 @@
<field name="arch" type="xml">
<form string="Product">
<header>
<button string="Variant Prices" type="action"
<button string="Configure Variants" type="action"
name="%(product_attribute_value_action)d"
attrs="{'invisible': [('product_variant_count', '&lt;=', 1)]}"
attrs="{'invisible': [('attribute_line_ids', '=', False)]}"
groups="product.group_product_variant"/>
</header>
<sheet>
<field name='product_variant_count' invisible='1'/>
<field name='is_product_variant' invisible='1'/>
<field name='attribute_line_ids' invisible='1'/>
<field name="id" invisible="True"/>
<div class="oe_button_box" name="button_box">
<button name="toggle_active" type="object"
+4 -1
View File
@@ -28,9 +28,12 @@ This module contains all the common features of Sales Management and eCommerce.
'views/mail_activity_views.xml',
'views/assets.xml',
'views/sale_portal_templates.xml',
'views/sale_product_configurator_templates.xml',
'views/sale_onboarding_views.xml',
'views/res_config_settings_views.xml',
'views/payment_views.xml',
'views/product_attribute_views.xml',
'wizard/sale_product_configurator_views.xml',
],
'demo': [
'data/sale_demo.xml',
@@ -38,5 +41,5 @@ This module contains all the common features of Sales Management and eCommerce.
],
'uninstall_hook': "uninstall_hook",
'installable': True,
'auto_install': False,
'auto_install': False
}
+1
View File
@@ -3,3 +3,4 @@
from . import onboarding
from . import portal
from . import product_configurator
@@ -0,0 +1,192 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import http, fields
from odoo.http import request
class ProductConfiguratorController(http.Controller):
@http.route(['/product_configurator/configure'], type='json', auth="user", methods=['POST'])
def configure(self, product_id, pricelist_id, **kw):
product_template = request.env['product.template'].browse(int(product_id))
to_currency = product_template.currency_id
pricelist = self._get_pricelist(pricelist_id)
if pricelist:
product_template = product_template.with_context(pricelist=pricelist.id, partner=request.env.user.partner_id)
to_currency = pricelist.currency_id
return request.env['ir.ui.view'].render_template("sale.product_configurator_configure", {
'product': product_template,
'to_currency': to_currency,
'pricelist': pricelist,
'get_attribute_exclusions': self._get_attribute_exclusions
})
@http.route(['/product_configurator/show_optional_products'], type='json', auth="user", methods=['POST'])
def show_optional_products(self, product_id, variant_values, pricelist_id, **kw):
return self._show_optional_products(product_id, variant_values, self._get_pricelist(pricelist_id), False, **kw)
@http.route(['/product_configurator/optional_product_items'], type='json', auth="user", methods=['POST'])
def optional_product_items(self, product_id, pricelist_id, **kw):
return self._optional_product_items(product_id, self._get_pricelist(pricelist_id), **kw)
@http.route(['/product_configurator/get_combination_info'], type='json', auth="user", methods=['POST'])
def get_combination_info(self, product_template_id, product_id, combination, add_qty, pricelist_id, **kw):
return self._get_combination_info(product_template_id, product_id, combination, add_qty, self._get_pricelist(pricelist_id))
def _optional_product_items(self, product_id, pricelist, **kw):
product = request.env['product.product'].with_context(self._get_product_context(pricelist, **kw)).browse(int(product_id))
to_currency = product.currency_id
if pricelist:
to_currency = pricelist.currency_id
return request.env['ir.ui.view'].render_template("sale.optional_product_items", {
'product': product,
'reference_product': product,
'pricelist': pricelist,
'to_currency': to_currency,
'get_attribute_exclusions': self._get_attribute_exclusions,
})
def _show_optional_products(self, product_id, variant_values, pricelist, handle_stock, **kw):
product = request.env['product.product'].browse(int(product_id))
from_currency = request.env.user.company_id.currency_id
to_currency = pricelist.currency_id if pricelist else product.currency_id
company = request.env['res.company'].browse(request.env.context.get('company_id')) or request.env['res.users']._get_company()
date = request.env.context.get('date') or fields.Date.today()
compute_currency = lambda price: from_currency._convert(price, to_currency, company, date)
product = product.with_context(self._get_product_context(pricelist, **kw))
has_optional_products = False
for optional_product in product.optional_product_ids:
if optional_product.get_filtered_variants(product):
has_optional_products = True
break
if not has_optional_products:
return False
else:
return request.env['ir.ui.view'].render_template("sale.optional_products_modal", {
'product': product,
'reference_product': product,
'variant_values': variant_values,
'pricelist': pricelist,
'compute_currency': compute_currency,
'to_currency': to_currency,
'handle_stock': handle_stock,
'get_attribute_exclusions': self._get_attribute_exclusions,
})
def _get_attribute_exclusions(self, product, reference_product=None):
""" list of attribute exclusions of a product
Args:
- product (product.template): The base product template
- reference_product (product.product): The reference product from which 'product' is an optional or accessory product
:return: dict of exclusions
exclusions.exclusions: exclusions within this product
exclusions.parent_exclusions: exclusions coming from the reference_product
"""
product_attribute_values = request.env['product.product.attribute.value'].search([
('product_tmpl_id', '=', product.id),
('product_attribute_value_id', 'in', product.attribute_line_ids.mapped('value_ids').ids),
])
# array of all the excluded value_ids of all the filter lines for this product
mapped_exclusions = {
product_attribute_value.id: [
value_id
for filter_line in product_attribute_value.exclude_for.filtered(
lambda filter_line: filter_line.product_tmpl_id == product
) for value_id in filter_line.value_ids.ids
]
for product_attribute_value in product_attribute_values
}
parent_exclusions = []
if reference_product:
parent_exclusions = [
value_id
for filter_line in reference_product.mapped('product_attribute_value_ids.exclude_for').filtered(
lambda filter_line: filter_line.product_tmpl_id == product
) for value_id in filter_line.value_ids.ids]
return {
'exclusions': mapped_exclusions,
'parent_exclusions': parent_exclusions
}
def _get_product_context(self, pricelist=None, **kw):
product_context = dict(request.context)
if pricelist:
if not product_context.get('pricelist'):
product_context['pricelist'] = pricelist.id
product_context.update(kw.get('kwargs', {}).get('context', {}))
return product_context
def _get_combination_info(self, product_template_id, product_id, combination, add_qty, pricelist, **kw):
product_attribute_values = request.env['product.product.attribute.value'].browse(combination)
filtered_product_attribute_values = product_attribute_values.filtered(
lambda product_attribute_value: product_attribute_value.attribute_id.create_variant != 'never'
)
context = {
'quantity': add_qty,
'pricelist': pricelist.id if pricelist else None,
'current_attributes_price_extra': [product_attribute_value.price_extra or 0.0 for product_attribute_value in product_attribute_values]
}
product_template = request.env['product.template'].with_context(context).browse(product_template_id)
Product = request.env['product.product'].with_context(context)
if product_id and not combination:
product = Product.browse(product_id)
else:
products = Product.search([
('product_tmpl_id', '=', product_template_id)
])
product = products.filtered(
lambda product: all(product_attribute_value in product.product_attribute_value_ids
for product_attribute_value in filtered_product_attribute_values)
)
product_id = None
list_price = product_template.price_compute('list_price')[product_template.id]
price = product_template.price or list_price
if(product):
product = product.with_context(
no_variant_attributes_price_extra=[product_attribute_value.price_extra or 0.0
for product_attribute_value in product_attribute_values.filtered(
lambda product_attribute_value: product_attribute_value.attribute_id.create_variant == 'never'
)
]
)
product_id = product.id
list_price = product.price_compute('list_price')[product.id]
price = product.price or list_price
display_name = [product_template.name]
if filtered_product_attribute_values:
display_name.append(' (')
display_name.append(', '.join(filtered_product_attribute_values.mapped('name')))
display_name.append(')')
if pricelist and pricelist.currency_id != product_template.currency_id:
list_price = product_template.currency_id._convert(
list_price, pricelist.currency_id,
request.env.user.company_id, fields.Date.today()
)
return {
'product_id': product_id,
'product_template_id': product_template.id,
'display_name': ''.join(display_name),
'price': price,
'list_price': list_price
}
def _get_pricelist(self, pricelist_id):
return request.env['product.pricelist'].browse(int(pricelist_id)) if pricelist_id and pricelist_id != '0' else None
+102
View File
@@ -691,6 +691,19 @@ Thanks!</field>
<field name="price_unit">38.25</field>
</record>
<record id="product.product_attribute_2" model="product.attribute">
<field name="type">color</field>
</record>
<record id="product.product_attribute_3" model="product.attribute">
<field name="type">select</field>
</record>
<record id="product.product_attribute_value_3" model="product.attribute.value">
<field name="html_color">#FFFFFF</field>
</record>
<record id="product.product_attribute_value_4" model="product.attribute.value">
<field name="html_color">#000000</field>
</record>
<!-- Confirm some Sales Orders-->
<function model="sale.order" name="action_confirm" eval="[[ref('portal_sale_order_2')]]"/>
@@ -700,5 +713,94 @@ Thanks!</field>
<function model="crm.team" name="message_subscribe"
eval="[ref('sales_team.team_sales_department'),ref('sales_team.salesteam_website_sales')], [ref('base.partner_root')]"/>
<record id="product_product_1_product_template" model="product.template">
<field name="name">Chair floor protection</field>
<field name="categ_id" ref="product.product_category_5"/>
<field name="list_price">12.0</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
<field name="description_sale">Office chairs can harm your floor: protect it.</field>
<field name="image" type="base64" file="sale/static/img/floor_protection-image.png"/>
</record>
<record id="product.product_product_4_product_template" model="product.template">
<field name="optional_product_ids" eval="[(6,0,[ref('product.product_product_11_product_template')])]"/>
</record>
<record id="product.product_product_11_product_template" model="product.template">
<field name="optional_product_ids" eval="[(6,0,[ref('product_product_1_product_template')])]"/>
</record>
<record id="product_attribute_value_7" model="product.attribute.value">
<field name="name">Custom</field>
<field name="attribute_id" ref="product.product_attribute_1"/>
<field name="is_custom">True</field>
</record>
<record id="product.product_attribute_line_1" model="product.attribute.line">
<field name="value_ids" eval="[(4,ref('product_attribute_value_7'))]"/>
</record>
<record id="product_product_4e" model="product.product">
<field name="default_code">DESK0005</field>
<field name="product_tmpl_id" ref="product.product_product_4_product_template"/>
<field name="attribute_value_ids" eval="[(6,0,[ref('product_attribute_value_7'), ref('product.product_attribute_value_3')])]"/>
</record>
<record id="product_product_4f" model="product.product">
<field name="default_code">DESK0006</field>
<field name="product_tmpl_id" ref="product.product_product_4_product_template"/>
<field name="attribute_value_ids" eval="[(6,0,[ref('product_attribute_value_7'), ref('product.product_attribute_value_4')])]"/>
</record>
<!--
Handle automatically created product.product.attribute.value.
Check "product.product_product_attribute_value_1" for more information about this
-->
<function model="ir.model.data" name="_update_xmlids">
<value model="base" eval="[{
'xml_id': 'sale.product_product_attribute_value_3',
'record': obj().env.ref('product.product_attribute_line_1').product_value_ids[0],
'noupdate': True,
}, {
'xml_id': 'sale.product_product_attribute_value_4',
'record': obj().env.ref('product.product_attribute_line_4').product_value_ids[0],
'noupdate': True,
}, {
'xml_id': 'sale.product_product_attribute_value_5',
'record': obj().env.ref('product.product_attribute_line_2').product_value_ids[1],
'noupdate': True,
}]"/>
</function>
<record id="product_attribute_filter_line_1" model="product.attribute.filter.line">
<field name="product_tmpl_id" ref="product.product_product_4_product_template" />
<field name="value_ids" eval="[(6,0,[ref('sale.product_product_attribute_value_5')])]"/>
</record>
<record id="product_attribute_filter_line_2" model="product.attribute.filter.line">
<field name="product_tmpl_id" ref="product.product_product_11_product_template" />
<field name="value_ids" eval="[(6,0,[ref('sale.product_product_attribute_value_4')])]"/>
</record>
<!--
The "Customizable Desk's Aluminium" attribute value will excude:
- The "Customizable Desk's Black" attribute
- The "Office Chair's Steel" attribute
-->
<record id="product.product_product_attribute_value_1" model="product.product.attribute.value">
<field name="exclude_for" eval="[(6,0,[ref('sale.product_attribute_filter_line_1') ,ref('sale.product_attribute_filter_line_2')])]" />
</record>
<record id="product_attribute_filter_line_3" model="product.attribute.filter.line">
<field name="product_tmpl_id" ref="product.product_product_11_product_template" />
<field name="value_ids" eval="[(6,0,[ref('product.product_product_attribute_value_2')])]"/>
</record>
<!--
The "Customizable Desk's Steel" attribute value will excude:
- The "Office Chair's Aluminium" attribute
-->
<record id="product_product_attribute_value_3" model="product.product.attribute.value">
<field name="exclude_for" eval="[(6,0,[ref('sale.product_attribute_filter_line_3')])]" />
</record>
</data>
</odoo>
+48
View File
@@ -6,6 +6,16 @@ from odoo import api, fields, models
from odoo.tools.float_utils import float_round
class ProductTemplate(models.Model):
_inherit = "product.template"
optional_product_ids = fields.Many2many(
'product.template', 'product_optional_rel', 'src_id', 'dest_id',
string='Optional Products', help="Optional Products are suggested "
"whenever the customer hits *Add to Cart* (cross-sell strategy, "
"e.g. for computers: warranty, software, etc.).")
class ProductProduct(models.Model):
_inherit = 'product.product'
@@ -42,3 +52,41 @@ class ProductProduct(models.Model):
def _get_invoice_policy(self):
return self.invoice_policy
class ProductAttribute(models.Model):
_inherit = "product.attribute"
# YTI FIX ME: PLEASE RENAME ME INTO attribute_type
type = fields.Selection([
('radio', 'Radio'),
('select', 'Select'),
('color', 'Color')], default='radio')
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='HTML Color Index', oldname='color',
help="""Here you can set a
specific HTML color index (e.g. #ff0000) to display the color if the
attribute type is 'Color'.""")
class ProductProductAttributeValue(models.Model):
_inherit = "product.product.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")
class ProductAttributeCustomValue(models.Model):
_name = "product.attribute.custom.value"
_rec_name = 'custom_value'
_description = 'Product Attribute Custom Value'
attribute_value_id = fields.Many2one('product.attribute.value', string='Attribute')
sale_order_line_id = fields.Many2one('sale.order.line', string='Sale order line')
custom_value = fields.Char('Custom value')
+17
View File
@@ -4,6 +4,7 @@
from odoo import api, fields, models, _
from odoo.addons.base.models.res_partner import WARNING_MESSAGE, WARNING_HELP
from odoo.tools.float_utils import float_round
import json
class ProductTemplate(models.Model):
@@ -54,6 +55,22 @@ class ProductTemplate(models.Model):
}
return action
def create_product_variant(self, product_attribute_value_ids):
self.ensure_one()
attribute_value_ids = \
self.env['product.product.attribute.value'] \
.browse(json.loads(product_attribute_value_ids)) \
.mapped('product_attribute_value_id') \
.filtered(lambda attribute_value_id: attribute_value_id.attribute_id.create_variant != 'never')
product_variant = self.env['product.product'].create({
'product_tmpl_id': self.id,
'attribute_value_ids': [(6, 0, attribute_value_ids.ids)]
})
return product_variant.id
@api.onchange('type')
def _onchange_type(self):
""" Force values to stay consistent with integrity constraints """
+47 -2
View File
@@ -159,6 +159,7 @@ class SaleOrder(models.Model):
partner_shipping_id = fields.Many2one('res.partner', string='Delivery Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)], 'sale': [('readonly', False)]}, help="Delivery address for current sales order.")
pricelist_id = fields.Many2one('product.pricelist', string='Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Pricelist for current sales order.")
pricelist_id_id = fields.Integer(string="Pricelist ID", related='pricelist_id.id', readonly=True)
currency_id = fields.Many2one("res.currency", related='pricelist_id.currency_id', string="Currency", readonly=True, required=True)
analytic_account_id = fields.Many2one('account.analytic.account', 'Analytic Account', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="The analytic account related to a sales order.", copy=False, oldname='project_id')
@@ -928,6 +929,21 @@ class SaleOrderLine(models.Model):
else:
line.invoice_status = 'no'
@api.model
def default_get(self, fields):
res = super(SaleOrderLine, self).default_get(fields)
if self._context.get('product_id'):
res['product_id'] = int(self._context.get('product_id'))
if self._context.get('quantity'):
res['product_uom_qty'] = float(self._context.get('quantity'))
if self._context.get('no_variant_attribute_values'):
attributes = self._context.get('no_variant_attribute_values')
res['product_no_variant_attribute_values'] = [(6, 0, [int(attribute['value']) for attribute in attributes])]
if self._context.get('product_custom_variant_values'):
values = self._context.get('product_custom_variant_values')
res['product_custom_variant_values'] = [(0, 0, value) for value in values]
return res
@api.depends('product_uom_qty', 'discount', 'price_unit', 'tax_id')
def _compute_amount(self):
"""
@@ -1115,6 +1131,10 @@ class SaleOrderLine(models.Model):
product_updatable = fields.Boolean(compute='_compute_product_updatable', string='Can Edit Product', readonly=True, default=True)
product_uom_qty = fields.Float(string='Ordered Quantity', digits=dp.get_precision('Product Unit of Measure'), required=True, default=1.0)
product_uom = fields.Many2one('uom.uom', string='Unit of Measure')
product_custom_variant_values = fields.One2many('product.attribute.custom.value', 'sale_order_line_id', string='User entered custom product attribute values')
# M2M holding the values of product.attribute with create_variant field set to 'never'
# It allows keeping track of the extra_price associated to those attribute values and add them to the SO line description
product_no_variant_attribute_values = fields.Many2many('product.product.attribute.value', string='Product attribute values that do not create variants')
# Non-stored related field to allow portal user to see the image of the product he has ordered
product_image = fields.Binary('Product Image', related="product_id.image", store=False)
@@ -1362,9 +1382,18 @@ class SaleOrderLine(models.Model):
@api.multi
def _get_display_price(self, product):
# TO DO: move me in master/saas-16 on sale.order
# awa: don't know it it's still the case since we need the "product_no_variant_attribute_values" field now
# to be able to compute the full price
if self.product_no_variant_attribute_values:
product = product.with_context(no_variant_attributes_price_extra=[
no_variant_attribute_value.price_extra or 0
for no_variant_attribute_value in self.product_no_variant_attribute_values
])
if self.order_id.pricelist_id.discount_policy == 'with_discount':
return product.with_context(pricelist=self.order_id.pricelist_id.id).price
product_context = dict(self.env.context, partner_id=self.order_id.partner_id.id, date=self.order_id.date_order, uom=self.product_uom.id)
final_price, rule_id = self.order_id.pricelist_id.with_context(product_context).get_product_price_rule(self.product_id, self.product_uom_qty or 1.0, self.order_id.partner_id)
base_price, currency = self.with_context(product_context)._get_real_price_currency(product, rule_id, self.product_uom_qty, self.product_uom, self.order_id.pricelist_id.id)
if currency != self.order_id.pricelist_id.currency_id:
@@ -1384,7 +1413,7 @@ class SaleOrderLine(models.Model):
domain = {'product_uom': [('category_id', '=', self.product_id.uom_id.category_id.id)]}
if not self.product_uom or (self.product_id.uom_id.id != self.product_uom.id):
vals['product_uom'] = self.product_id.uom_id
vals['product_uom_qty'] = 1.0
vals['product_uom_qty'] = self.product_uom_qty or 1.0
product = self.product_id.with_context(
lang=self.order_id.partner_id.lang,
@@ -1410,7 +1439,23 @@ class SaleOrderLine(models.Model):
self.product_id = False
return result
vals.update(name=self.get_sale_order_line_multiline_description_sale(product))
name = self.get_sale_order_line_multiline_description_sale(product)
if self.product_custom_variant_values or self.product_no_variant_attribute_values:
name += '\n'
if self.product_custom_variant_values:
for product_custom_variant_value in self.product_custom_variant_values:
if product_custom_variant_value.custom_value and product_custom_variant_value.custom_value.strip():
name += '\n' + product_custom_variant_value.attribute_value_id.name + ': ' + product_custom_variant_value.custom_value.strip()
if self.product_no_variant_attribute_values:
for no_variant_attribute_value in self.product_no_variant_attribute_values.filtered(
lambda product_attribute_value: not product_attribute_value.is_custom
):
name += '\n' + no_variant_attribute_value.attribute_id.name + ': ' + no_variant_attribute_value.name
vals.update(name=name)
self._compute_tax_id()
+3 -1
View File
@@ -41,7 +41,9 @@ access_product_template_sale_manager,product.template salemanager,model_product_
access_product_product_sale_manager,product.product salemanager,model_product_product,sales_team.group_sale_manager,1,1,1,1
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_attribute_price_sale_manager,product.attribute manager price,product.model_product_attribute_price,sales_team.group_sale_manager,1,1,1,1
access_product_product_attribute_sale_manager,product.product.attribute manager value,product.model_product_product_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_manager,1,1,1,1
access_product_attribute_filter_line_sale_manager,product.attribute manager filter line,product.model_product_attribute_filter_line,sales_team.group_sale_manager,1,1,1,1
access_product_attribute_line_sale_manager,product.attribute manager line,product.model_product_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
access_account_journal_sale_manager,account.journal sale manager,account.model_account_journal,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
41 access_product_product_sale_manager product.product salemanager model_product_product sales_team.group_sale_manager 1 1 1 1
42 access_product_attribute_sale_manager product.attribute manager product.model_product_attribute sales_team.group_sale_manager 1 1 1 1
43 access_product_attribute_value_sale_manager product.attribute manager value product.model_product_attribute_value sales_team.group_sale_manager 1 1 1 1
44 access_product_attribute_price_sale_manager access_product_product_attribute_sale_manager product.attribute manager price product.product.attribute manager value product.model_product_attribute_price product.model_product_product_attribute_value sales_team.group_sale_manager 1 1 1 1
45 access_product_product_attribute_custom_value product.attribute.custom value manager model_product_attribute_custom_value sales_team.group_sale_manager 1 1 1 1
46 access_product_attribute_filter_line_sale_manager product.attribute manager filter line product.model_product_attribute_filter_line sales_team.group_sale_manager 1 1 1 1
47 access_product_attribute_line_sale_manager product.attribute manager line product.model_product_attribute_line sales_team.group_sale_manager 1 1 1 1
48 access_account_tax_sale_manager account.tax sale manager account.model_account_tax sales_team.group_sale_salesman 1 0 0 0
49 access_account_journal_sale_manager account.journal sale manager account.model_account_journal sales_team.group_sale_salesman 1 0 0 0
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

@@ -0,0 +1,71 @@
/**
* This adds the possibility to open the product configurator when the control
* "Configure a product" is clicked
*/
odoo.define('sale.ProductConfiguratorEditableListRenderer', function (require) {
"use strict";
var core = require('web.core');
var _t = core._t;
var ListRenderer = require('web.ListRenderer');
ListRenderer.include({
/**
* @override
*/
_onAddRecord: function (ev) {
// we don't want the browser to navigate to a the # url
ev.preventDefault();
// we don't want the click to cause other effects, such as unselecting
// the row that we are creating, because it counts as a click on a tr
ev.stopPropagation();
// but we do want to unselect current row
var self = this;
this.unselectRow().then(function () {
var open_product_configurator = false;
var context = ev.currentTarget.dataset.context;
var context_json = false;
if(context){
context = context.replace(/\'/g, '"');
context_json = JSON.parse(context);
open_product_configurator = context_json.open_product_configurator;
}
if(open_product_configurator){
self._rpc({
model: 'ir.model.data',
method: 'xmlid_to_res_id',
kwargs: {xmlid: 'sale.sale_product_configurator_view_form'},
}).then(function(res_id) {
self.do_action({
name: _t('Configure a product'),
type: 'ir.actions.act_window',
res_model: 'sale.product.configurator',
views: [[res_id, 'form']],
target: 'new'
}, {
on_close: function (products) {
if(products && products !== 'special'){
self.trigger_up('add_record', {
context: products,
forceEditable: "bottom" ,
allowWarning: true,
onSuccess: function(){
self.unselectRow();
}
});
}
}
});
});
} else {
self.trigger_up('add_record', {context: context && [context]}); // TODO write a test, the deferred was not considered
}
});
}
});
});
@@ -0,0 +1,200 @@
odoo.define('sale.ProductConfiguratorFormController', function (require) {
"use strict";
var core = require('web.core');
var _t = core._t;
var FormController = require('web.FormController');
var OptionalProductsModal = require('sale.OptionalProductsModal');
var ProductConfiguratorFormController = FormController.extend({
custom_events: _.extend({}, FormController.prototype.custom_events, {
field_changed: '_onFieldChanged'
}),
className: 'o_product_configurator',
/**
* @override
*/
init: function (){
this._super.apply(this, arguments);
},
/**
* We need to override the default click behavior for our "Add" button
* because there is a possibility that this product has optional products.
* If so, we need to display an extra modal to choose the options.
*
* @override
*/
_onButtonClicked: function (event) {
if (event.stopPropagation){
event.stopPropagation();
}
var attrs = event.data.attrs;
if (attrs.special === 'cancel') {
this._super.apply(this, arguments);
} else {
if (!this.$el
.parents('.modal')
.find('.o_sale_product_configurator_add')
.hasClass('disabled')){
this._handleAdd(this.$el);
}
}
},
/**
* This is overridden to allow catching the "select" event on our product template select field.
* This will not work anymore if more fields are added to the form.
* TODO awa: Find a better way to catch that event.
*
* @override
*/
_onFieldChanged: function (event) {
var self = this;
this.$el.parents('.modal').find('.o_sale_product_configurator_add').removeClass('disabled');
this._rpc({
route: '/product_configurator/configure',
params: {
product_id: event.data.changes.product_template_id.id,
pricelist_id: $('.js_sale_order_pricelist_id').html()
}
}).then(function (configurator) {
self.renderer.renderConfigurator(configurator);
});
this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* When the user adds a product that has optional products, we need to display
* a window to allow the user to choose these extra options.
*
* This will also create the product if it's in "dynamic" mode
* (see product_attribute.create_variant)
*
* @private
* @param {$.Element} $modal
*/
_handleAdd: function ($modal) {
var self = this;
var productSelector = [
'input[type="hidden"][name="product_id"]',
'input[type="radio"][name="product_id"]:checked'
];
var productId = parseInt($modal.find(productSelector.join(', ')).first().val(), 10);
var productReady = $.Deferred();
if (productId){
productReady.resolve(productId);
} else {
productReady = this._rpc({
model: 'product.template',
method: 'create_product_variant',
args: [
$modal.find('.product_template_id').val(),
JSON.stringify(self.renderer.getSelectedVariantValues($modal))
],
});
}
productReady.done(function (productId){
$modal.find(productSelector.join(', ')).val(productId);
var variantValues = self
.renderer
.getSelectedVariantValues($modal.find('.js_product'));
var productCustomVariantValues = self
.renderer
.getCustomVariantValues($modal.find('.js_product'));
var noVariantAttributeValues = self
.renderer
.getNoVariantAttributeValues($modal.find('.js_product'));
self.rootProduct = {
product_id: productId,
quantity: parseFloat($modal.find('input[name="add_qty"]').val() || 1),
variant_values: variantValues,
product_custom_variant_values: productCustomVariantValues,
no_variant_attribute_values: noVariantAttributeValues
};
self.optionalProductsModal = new OptionalProductsModal($('body'), {
rootProduct: self.rootProduct,
pricelistId: $('.js_sale_order_pricelist_id').html(),
okButtonText: _t('Confirm'),
cancelButtonText: _t('Back'),
title: _t('Configure')
}).open();
self.optionalProductsModal.on('options_empty', null,
self._onModalOptionsEmpty.bind(self));
self.optionalProductsModal.on('update_quantity', null,
self._onOptionsUpdateQuantity.bind(self));
self.optionalProductsModal.on('confirm', null,
self._onModalConfirm.bind(self));
});
},
/**
* No optional products found for this product, only add the root product
*
* @private
*/
_onModalOptionsEmpty: function () {
this._addProducts([this.rootProduct]);
},
/**
* Add all selected products
*
* @private
*/
_onModalConfirm: function () {
this._addProducts(this.optionalProductsModal.getSelectedProducts());
},
/**
* Update product configurator form
* when quantity is updated in the optional products window
*
* @private
* @param {integer} quantity
*/
_onOptionsUpdateQuantity: function (quantity) {
this.$el
.find('input[name="add_qty"]')
.val(quantity)
.trigger('change');
},
/**
* This triggers the close action for the window and
* adds the product as the "infos" parameter.
* It will allow the caller (typically the SO line form) of this window
* to handle the added products.
*
* @private
* @param {Array} products the list of added products
* {integer} products.product_id: the id of the product
* {integer} products.quantity: the added quantity for this product
* {Array} products.product_custom_variant_values:
* see product_configurator_mixin.getCustomVariantValues
* {Array} products.no_variant_attribute_values:
* see product_configurator_mixin.getNoVariantAttributeValues
*/
_addProducts: function (products) {
this.do_action({type: 'ir.actions.act_window_close', infos: products});
}
});
return ProductConfiguratorFormController;
});
@@ -0,0 +1,510 @@
odoo.define('sale.ProductConfiguratorMixin', function (require) {
'use strict';
var core = require('web.core');
var utils = require('web.utils');
var ajax = require('web.ajax');
var _t = core._t;
var ProductConfiguratorMixin = {
events: {
'click .css_attribute_color input': '_onChangeColorAttribute',
'change .main_product:not(.in_cart) input.js_quantity': 'onChangeAddQuantity',
'click button.js_add_cart_json': 'onClickAddCartJSON',
'change [data-attribute_exclusions]': 'onChangeVariant'
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* When a product is added or when the quantity is changed,
* we need to refresh the total price row
* TODO awa: add a container context to avoid global selectors ?
*
*/
computePriceTotal: function () {
if ($('.js_price_total').length){
var price = 0;
$('.js_product.in_cart').each(function (){
var quantity = parseInt($('input[name="add_qty"]').first().val());
price += parseFloat($(this).find('.js_raw_price').html()) * quantity;
});
$('.js_price_total .oe_currency_value').html(
this._priceToStr(parseFloat(price))
);
}
},
/**
* When a variant is changed, this will check:
* - If the selected combination is available or not
* - The extra price if applicable
* - The display name of the product ("Customizable desk (White, Steel)")
* - The new total price
* - The need of adding a "custom value" input
*
* @param {MouseEvent}
*/
onChangeVariant: function (ev) {
var self = this;
var $component;
if ($(ev.currentTarget).closest('form').length > 0){
$component = $(ev.currentTarget).closest('form');
} else if ($(ev.currentTarget).closest('.oe_optional_products_modal').length > 0){
$component = $(ev.currentTarget).closest('.oe_optional_products_modal');
} else {
$component = $(ev.currentTarget).closest('.o_product_configurator');
}
var qty = $component.find('input[name="add_qty"]').val();
var $parent = $(ev.target).closest('.js_product');
var combination = this.getSelectedVariantValues($parent);
self._checkExclusions($parent, combination);
ajax.jsonRpc(this._getUri('/product_configurator/get_combination_info'), 'call', {
product_template_id: parseInt($parent.find('.product_template_id').val()),
product_id: parseInt($parent.find('.product_id').val()),
combination: combination,
add_qty: parseInt(qty),
pricelist_id: this.pricelistId
}).then(function (combinationData) {
self._onChangeCombination(ev, $parent, combinationData);
});
},
/**
* Will add the "custom value" input for this attribute value if
* the attribute value is configured as "custom" (see product_attribute_value.is_custom)
*
* @private
* @param {MouseEvent} ev
*/
handleCustomValues: function ($target){
var $variantContainer;
var $customInput = false;
if ($target.is('input[type=radio]') && $target.is(':checked')) {
$variantContainer = $target.closest('ul').closest('li');
$customInput = $target;
} else if ($target.is('select')){
$variantContainer = $target.closest('li');
$customInput = $target
.find('option[value="' + $target.val() + '"]');
}
if ($variantContainer) {
if ($customInput && $customInput.data('is_custom')) {
var attributeValueId = $customInput.data('value_id');
var attributeValueName = $customInput.data('value_name');
if ($variantContainer.find('.variant_custom_value').length === 0
|| $variantContainer
.find('.variant_custom_value')
.data('attribute_value_id') !== parseInt(attributeValueId)){
$variantContainer.find('.variant_custom_value_label').remove();
$variantContainer.find('.variant_custom_value').remove();
var $label = $('<label>', {
html: attributeValueName + ': ',
class: 'variant_custom_value_label'
});
var $input = $('<input>', {
type: 'text',
'data-attribute_value_id': attributeValueId,
'data-attribute_value_name': attributeValueName,
class: 'variant_custom_value form-control'
});
$variantContainer.append($label).append($input);
}
} else {
$variantContainer.find('.variant_custom_value_label').remove();
$variantContainer.find('.variant_custom_value').remove();
}
}
},
/**
* Hack to add and remove from cart with json
*
* @param {MouseEvent} ev
*/
onClickAddCartJSON: function (ev) {
ev.preventDefault();
var $link = $(ev.currentTarget);
var $input = $link.closest('.input-group').find("input");
var min = parseFloat($input.data("min") || 0);
var max = parseFloat($input.data("max") || Infinity);
var quantity = ($link.has(".fa-minus").length ? -1 : 1) + parseFloat($input.val() || 0, 10);
var newQty = quantity > min ? (quantity < max ? quantity : max) : min;
$input.val(newQty).trigger('change');
return false;
},
/**
* When the quantity is changed, we need to query the new price of the product.
* Based on the price list, the price might change when quantity exceeds X
*
* @param {MouseEvent} ev
*/
onChangeAddQuantity: function (ev) {
var $parent;
if ($(ev.currentTarget).closest('.oe_optional_products_modal').length > 0){
$parent = $(ev.currentTarget).closest('.oe_optional_products_modal');
} else if ($(ev.currentTarget).closest('form').length > 0){
$parent = $(ev.currentTarget).closest('form');
} else {
$parent = $(ev.currentTarget).closest('.o_product_configurator');
}
this.triggerVariantChange($parent);
},
/**
* Triggers the price computation and other variant specific changes
*
* @param {$.Element} $container
*/
triggerVariantChange: function ($container) {
var self = this;
$container.find('ul[data-attribute_exclusions]').trigger('change');
$container.find('input.js_variant_change:checked, select.js_variant_change').each(function () {
self.handleCustomValues($(this));
});
},
/**
* Will look for user custom attribute values
* in the provided container
*
* @param {$.Element} $container
* @returns {Array} array of custom values with the following format
* {integer} attribute_value_id
* {string} attribute_value_name
* {string} custom_value
*/
getCustomVariantValues: function ($container) {
var variantCustomValues = [];
$container.find('.variant_custom_value').each(function (){
var $variantCustomValueInput = $(this);
if ($variantCustomValueInput.length !== 0){
variantCustomValues.push({
'attribute_value_id': $variantCustomValueInput.data('attribute_value_id'),
'attribute_value_name': $variantCustomValueInput.data('attribute_value_name'),
'custom_value': $variantCustomValueInput.val(),
});
}
});
return variantCustomValues;
},
/**
* Will look for attribute values that do not create product variant
* (see product_attribute.create_variant "dynamic")
*
* @param {$.Element} $container
* @returns {Array} array of attribute values with the following format
* {integer} attribute_value_id
* {string} attribute_value_name
* {integer} value
* {string} attribute_name
* {boolean} is_custom
*/
getNoVariantAttributeValues: function ($container) {
var noVariantAttributeValues = [];
var variantsValuesSelectors = [
'input.never.js_variant_change:checked',
'select.never.js_variant_change'
];
$container.find(variantsValuesSelectors.join(',')).each(function (){
var $variantValueInput = $(this);
if ($variantValueInput.is('select')){
$variantValueInput = $variantValueInput.find('option[value=' + $variantValueInput.val() + ']');
}
if ($variantValueInput.length !== 0){
noVariantAttributeValues.push({
'attribute_value_id': $variantValueInput.data('value_id'),
'attribute_value_name': $variantValueInput.data('value_name'),
'value': $variantValueInput.val(),
'attribute_name': $variantValueInput.data('attribute_name'),
'is_custom': $variantValueInput.data('is_custom')
});
}
});
return noVariantAttributeValues;
},
/**
* Will return the list of selected product.product.attribute.value ids
* For the modal, the "main product"'s attribute values are stored in the
* "unchanged_value_ids" data
*
* @param {$.Element} $container the container to look into
*/
getSelectedVariantValues: function ($container) {
var values = [];
var unchangedValues = $container
.find('div.oe_unchanged_value_ids')
.data('unchanged_value_ids') || [];
var variantsValuesSelectors = [
'input.js_variant_change:checked',
'select.js_variant_change'
];
_.each($container.find(variantsValuesSelectors.join(', ')), function (el) {
values.push(+$(el).val());
});
return values.concat(unchangedValues);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Will disable attribute value's inputs based on combination exclusions
* and will disable the "add" button if the selected combination
* is not available
*
* This will check both the exclusions within the product itself and
* the exclusions coming from the parent product (meaning that this product
* is an option of the parent product)
*
* @private
* @param {$.Element} $parent the parent container to apply exclusions
* @param {Array} combination the selected combination of product attribute values
*/
_checkExclusions: function ($parent, combination) {
var self = this;
var combinationData = $parent
.find('ul[data-attribute_exclusions]')
.data('attribute_exclusions');
$parent.find('option, input, label').removeClass('css_not_available');
var disable = false;
if (combinationData.exclusions) {
_.each(combination, function (combinationValue){
if (combinationData.exclusions &&
combinationData.exclusions.hasOwnProperty(combinationValue)){
// check that the selected combination is in the exclusions
_.each(combinationData.exclusions[combinationValue], function (exclusion) {
if (!disable && combination.indexOf(exclusion) > -1) {
disable = true;
}
self._disableInput($parent, exclusion);
});
}
});
}
if (combinationData.parent_exclusions){
_.each(combinationData.parent_exclusions, function (exclusion){
if (!disable && combination.indexOf(exclusion) > -1) {
disable = true;
}
self._disableInput($parent, exclusion);
});
}
$parent.toggleClass('css_not_available', disable);
$parent.find("#add_to_cart").toggleClass('disabled', disable);
$parent
.parents('.modal')
.find('.o_sale_product_configurator_add')
.toggleClass('disabled', disable);
},
/**
* Will disable the input/option that refers to the passed attributeValueId.
* This is used for showing the user that some combinations are not available.
*
* @private
* @param {$.Element} $parent
* @param {integer} attributeValueId
*/
_disableInput: function ($parent, attributeValueId) {
var $input = $parent
.find('option[value=' + attributeValueId + '], input[value=' + attributeValueId + ']');
$input.addClass('css_not_available');
$input.closest('label').addClass('css_not_available');
},
/**
* @see onChangeVariant
*
* @private
* @param {MouseEvent} ev
* @param {$.Element} $parent
* @param {Array} combination
*/
_onChangeCombination: function (ev, $parent, combination) {
var self = this;
var $price = $parent.find(".oe_price:first .oe_currency_value");
var $default_price = $parent.find(".oe_default_price:first .oe_currency_value");
var $optional_price = $parent.find(".oe_optional:first .oe_currency_value");
$price.html(self._priceToStr(combination.price));
$default_price.html(self._priceToStr(combination.list_price));
if (combination.list_price - combination.price > 0.01) {
$default_price
.closest('.oe_website_sale')
.addClass("discount");
$optional_price
.closest('.oe_optional')
.removeClass('d-none')
.css('text-decoration', 'line-through');
$default_price.parent().removeClass('d-none');
} else {
$optional_price.closest('.oe_optional').addClass('d-none');
$default_price.parent().addClass('d-none');
}
var rootComponentSelectors = [
'tr.js_product',
'.oe_website_sale',
'.o_product_configurator'
];
self._updateProductImage(
$parent.closest(rootComponentSelectors.join(', ')),
combination.product_id,
combination.product_template_id
);
$parent
.find('.product_id')
.first()
.val(combination.product_id || 0)
.trigger('change');
$parent
.find('.product_display_name')
.first()
.val(combination.display_name);
$parent
.find('.js_raw_price')
.first()
.html(combination.price);
this.computePriceTotal();
this.handleCustomValues($(ev.target));
},
/**
* returns the formatted price
*
* @private
* @param {float} price
*/
_priceToStr: function (price) {
var l10n = _t.database.parameters;
var precision = 2;
if ($('.decimal_precision').length) {
precision = parseInt($('.decimal_precision').last().data('precision'));
}
var formatted = _.str.sprintf('%.' + precision + 'f', price).split('.');
formatted[0] = utils.insert_thousand_seps(formatted[0]);
return formatted.join(l10n.decimal_point);
},
/**
* Updates the product image.
* This will use the productId if available or will fallback to the productTemplateId.
*
* @private
* @param {$.Element} $productContainer
* @param {integer} product_id
* @param {integer} productTemplateId
*/
_updateProductImage: function ($productContainer, productId, productTemplateId) {
var $img;
var model = productId ? 'product.product' : 'product.template';
var modelId = productId || productTemplateId;
var imageSrc = '/web/image?model={0}&id={1}&field=image'
.replace("{0}", model)
.replace("{1}", modelId);
if ($productContainer.find('#o-carousel-product').length) {
$img = $productContainer.find('img.js_variant_img');
$img.attr("src", imageSrc);
$img.parent().attr('data-oe-model', model).attr('data-oe-id', modelId)
.data('oe-model', model).data('oe-id', modelId);
var $thumbnail = $productContainer.find('img.js_variant_img_small');
if ($thumbnail.length !== 0) { // if only one, thumbnails are not displayed
$thumbnail.attr("src", "/web/image/{0}/{1}/image/90x90"
.replace('{0}', model)
.replace('{1}', modelId));
$('.carousel').carousel(0);
}
}
else {
var imagesSelectors = [
'span[data-oe-model^="product."][data-oe-type="image"] img:first',
'img.product_detail_img',
'span.variant_image img'
];
$img = $productContainer.find(imagesSelectors.join(', '));
$img.attr('src', imageSrc);
$img.parent()
.attr('data-oe-model', model)
.attr('data-oe-id', modelId)
.data('oe-model', model)
.data('oe-id', modelId);
}
// reset zooming constructs
$img.filter('[data-zoom-image]').attr('data-zoom-image', $img.attr('src'));
if ($img.data('zoomOdoo') !== undefined) {
$img.data('zoomOdoo').isReady = false;
}
},
/**
* Highlight selected color
*
* @private
* @param {MouseEvent} ev
*/
_onChangeColorAttribute: function (ev) {
var $parent = $(ev.target).closest('.js_product');
$parent.find('.css_attribute_color')
.removeClass("active")
.filter(':has(input:checked)')
.addClass("active");
},
/**
* Website behavior is slightly different from backend so we append
* "_website" to URLs to lead to a different route
*
* @private
* @param {string} uri The uri to adapt
*/
_getUri: function (uri) {
if (this.isWebsite){
return uri + '_website';
} else {
return uri;
}
}
};
return ProductConfiguratorMixin;
});
@@ -0,0 +1,472 @@
odoo.define('sale.OptionalProductsModal', function (require) {
"use strict";
var ajax = require('web.ajax');
var Dialog = require('web.Dialog');
var ServicesMixin = require('web.ServicesMixin');
var ProductConfiguratorMixin = require('sale.ProductConfiguratorMixin');
var weContext = require('web_editor.context');
var productNameMap = {};
var optionalProductsMap = {};
var OptionalProductsModal = Dialog.extend(ServicesMixin, ProductConfiguratorMixin, {
events: _.extend({}, Dialog.prototype.events, ProductConfiguratorMixin.events, {
'click a.js_add, a.js_remove': '_onAddOrRemoveOption',
'change .in_cart.main_product input.js_quantity': '_onChangeQuantity'
}),
/**
* Initializes the optional products modal
*
* If the "isWebsite" param is true, will also disable the following events:
* - change [data-attribute_exclusions]
* - click button.js_add_cart_json
*
* This has to be done because those events are already registered at the "website_sale"
* component level.
* This modal is part of the form that has these events registered and we
* want to avoid duplicates.
*
* @override
* @param {$.Element} parent The parent container
* @param {Object} params
* @param {integer} params.pricelistId
* @param {boolean} params.isWebsite If we're on a web shop page, we need some
* custom behavior
* @param {string} okButtonText The text to apply on the "ok" button, typically
* "Add" for the sale order and "Proceed to checkout" on the web shop
* @param {string} cancelButtonText same as "params.okButtonText" but
* for the cancel button
* @param {Object} rootProduct The root product of the optional products window
* @param {integer} rootProduct.product_id
* @param {integer} rootProduct.quantity
* @param {Array} rootProduct.variant_values
* @param {Array} rootProduct.product_custom_variant_values
* @param {Array} rootProduct.no_variant_attribute_values
*/
init: function (parent, params) {
var self = this;
this._super(parent, {
size: 'large',
buttons: [{
text: params.okButtonText,
click: this._onConfirmButtonClick,
classes: 'btn-primary'
}, {
text: params.cancelButtonText,
click: this._onCancelButtonClick
}],
title: params.title
});
this.rootProduct = params.rootProduct;
this.container = parent;
this.pricelistId = params.pricelistId;
this.isWebsite = params.isWebsite;
this.dialogClass = 'oe_optional_products_modal' + (params.isWebsite ? ' oe_website_sale' : '');
if (this.isWebsite) {
delete this.events['change [data-attribute_exclusions]'];
delete this.events['click button.js_add_cart_json'];
}
this._opened.then(function () {
self.triggerVariantChange(self.$el);
});
},
/**
* @override
*/
willStart: function () {
var self = this;
var uri = this._getUri("/product_configurator/show_optional_products");
var getModalContent = ajax.jsonRpc(uri, 'call', {
product_id: self.rootProduct.product_id,
variant_values: self.rootProduct.variant_values,
pricelist_id: self.pricelistId,
kwargs: {
context: _.extend({
'quantity': self.rootProduct.quantity
}, weContext.get()),
}
})
.then(function (modalContent) {
if (modalContent){
var $modalContent = $(modalContent);
$modalContent = self._postProcessContent($modalContent);
self.$content = $modalContent;
} else {
self.trigger('options_empty');
self.preventOpening = true;
}
});
var parentInit = self._super.apply(self, arguments);
return $.when(getModalContent, parentInit);
},
/**
* This is overridden to append the modal to the provided container (see init("parent")).
* We need this to have the modal contained in the web shop product form.
* The additional products data will then be contained in the form and sent on submit.
*
* @override
*/
open: function (options) {
$('.tooltip').remove(); // remove open tooltip if any to prevent them staying when modal is opened
var self = this;
this.appendTo($('<div/>')).then(function () {
if (!self.preventOpening){
self.$modal.find(".modal-body").replaceWith(self.$el);
self.$modal.attr('open', true);
self.$modal.removeAttr("aria-hidden");
self.$modal.modal().appendTo(self.container);
self._opened.resolve();
}
});
if (options && options.shouldFocusButtons) {
self._onFocusControlButton();
}
return self;
},
/**
* Will update quantity input to synchronize with previous window
*
* @override
*/
start: function (){
this._super.apply(this, arguments);
this.$el.find('input[name="add_qty"]').val(this.rootProduct.quantity);
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Returns the list of selected products.
* The root product is added on top of the list.
*
* @public
* @returns {Array} products
* @returns {integer} products.product_id
* @returns {integer} products.quantity
* @returns {Array} products.product_custom_variant_values
* @returns {Array} products.no_variant_attribute_values
*/
getSelectedProducts: function () {
var self = this;
var products = [this.rootProduct];
this.$modal.find('.js_product.in_cart:not(.main_product)').each(function (){
var quantity = 0;
var $item = $(this);
if ($item.find('input[name="add_qty"]').length){
quantity = $item.find('input[name="add_qty"]').val();
} else {
quantity = parseInt(
$item
.find('.optional_product_quantity span.add_qty')
.html()
.trim()
);
}
var productCustomVariantValues = self.getCustomVariantValues($(this));
var noVariantAttributeValues = self.getNoVariantAttributeValues($(this));
products.push({
product_id: parseInt($item.find('input.product_id').val()),
quantity: quantity,
product_custom_variant_values: productCustomVariantValues,
no_variant_attribute_values: noVariantAttributeValues
});
});
return products;
},
// ------------------------------------------
// Private
// ------------------------------------------
/**
* Adds the product image and updates the product description
* based on attribute values that are either "no variant" or "custom".
*
* @private
*/
_postProcessContent: function ($modalContent) {
var productId = this.rootProduct.product_id;
$modalContent
.find('img:first')
.attr("src", "/web/image/product.product/" + productId + "/image_medium");
if (this.rootProduct &&
(this.rootProduct.product_custom_variant_values ||
this.rootProduct.no_variant_attribute_values)) {
var $productDescription = $modalContent
.find('.main_product')
.find('td.td-product_name div.text-muted.small');
var description = $productDescription.html();
$.each(this.rootProduct.product_custom_variant_values, function (){
description += '<br/>' + this.attribute_value_name + ': ' + this.custom_value;
});
$.each(this.rootProduct.no_variant_attribute_values, function (){
if (this.is_custom !== 'True'){
description += '<br/>' + this.attribute_name + ': ' + this.attribute_value_name;
}
});
$productDescription.html(description);
}
return $modalContent;
},
/**
* @private
*/
_onConfirmButtonClick: function () {
this.trigger('confirm');
this.close();
},
/**
* @private
*/
_onCancelButtonClick: function () {
this.trigger('back');
this.close();
},
/**
* Will add/remove the option, that includes:
* - Moving it to the correct DOM section
* and possibly under its parent product
* - Hiding attribute values selection and showing the quantity
* - Creating the product if it's in "dynamic" mode (see product_attribute.create_variant)
* - Updating the description based on custom/no_create attribute values
* - Removing optional products if parent product is removed
* - Computing the total price
*
* @private
* @param {MouseEvent} ev
*/
_onAddOrRemoveOption: function (ev){
ev.preventDefault();
var self = this;
var $target = $(ev.currentTarget);
var $modal = $target.parents('.oe_optional_products_modal');
var $parent = $target.parents('.js_product:first');
$parent.find("a.js_add, span.js_remove").toggleClass('d-none');
$parent.find("input.js_optional_same_quantity").val($target.hasClass("js_add") ? 1 : 0 );
$parent.find(".js_remove");
var productTemplateId = $parent.find(".product_template_id").val();
if ($target.hasClass('js_add')) {
self._onAddOption($modal, $parent ,productTemplateId);
} else {
self._onRemoveOption($modal, $parent ,productTemplateId);
}
self.computePriceTotal();
},
/**
* @private
* @see _onAddOrRemoveOption
* @param {$.Element} $modal
* @param {$.Element} $parent
* @param {integer} productTemplateId
*/
_onAddOption: function ($modal, $parent, productTemplateId) {
var self = this;
var $main_product = $modal.find('.js_product:first');
var $selectOptionsText = $modal.find('.o_select_options');
// remove attribute values selection and move quantity to the correct table spot (".td-qty"))
$parent.addClass('in_cart');
$parent.find('.td-qty').addClass('text-center');
$parent.find('.td-qty').append($parent.find('.optional_product_quantity'));
// change product name to display_name (containing attribute values) when it's added to cart
// the "simple" name is kept into a map to revert to it if the product is removed from the cart
productNameMap[productTemplateId] = $parent.find(".product-name").html();
$parent.find(".product-name").html($parent.find(".product_display_name").val());
var productCustomVariantValues = self.getCustomVariantValues($parent);
var noVariantAttributeValues = self.getNoVariantAttributeValues($parent);
if (productCustomVariantValues || noVariantAttributeValues) {
var $productDescription = $parent
.find('td.td-product_name div.float-left');
var description = '';
$.each(productCustomVariantValues, function (){
description += '<br/>' + this.attribute_value_name + ': ' + this.custom_value;
});
$.each(noVariantAttributeValues, function (){
if (this.is_custom !== 'True'){
description += '<br/>' + this.attribute_name + ': ' + this.attribute_value_name;
}
});
var $customAttributeValuesDescription = $('<div>', {
class: 'custom_attribute_values_description text-muted small',
html: description
});
$productDescription.append($customAttributeValuesDescription);
}
// if it's an optional product of an optional product, place it after it's parent
var parentProductId = null;
for (var productIdKey in optionalProductsMap) {
if (optionalProductsMap[productIdKey].indexOf(productTemplateId) !== -1) {
parentProductId = productIdKey;
break;
}
$('tr:last').after($parent);
}
if (parentProductId) {
$modal.find('.product_template_id').filter(function () {
return parentProductId === $(this).val();
}).parents('.js_product:first').after($parent);
} else {
// else, place it after the main product
$main_product.after($parent);
}
var productId = $parent.find('.product_id').val();
var productReady = $.Deferred();
if (productId && productId !== '0'){
productReady.resolve(productId);
} else {
productReady = ajax.jsonRpc('/web/dataset/call', 'call', {
model: 'product.template',
method: 'create_product_variant',
args: [
productTemplateId,
JSON.stringify(self.getSelectedVariantValues($parent))
]
});
}
productReady.done(function (productId){
$parent.find('.product_id').val(productId);
ajax.jsonRpc(self._getUri("/product_configurator/optional_product_items"), 'call', {
'product_id': productId,
'pricelist_id': self.pricelistId
}).then(function (addedItem) {
var $addedItem = $(addedItem);
$modal.find('tr:last').after($addedItem);
self.$el.find('input[name="add_qty"]').trigger('change');
self.triggerVariantChange($addedItem);
if ($addedItem.find(".product_template_id").length > 0) {
// we need to map this product with it's optional products to be able to:
// - remove them from the cart if the parent product is removed
// - place the optional products under their parent in the interface when they're added to the cart
optionalProductsMap[productTemplateId] = $addedItem.find(".product_template_id").map(function () {
return $(this).val();
}).get();
}
if ($selectOptionsText.nextAll('.js_product').length === 0){
// no more optional products to select -> hide the header
$selectOptionsText.hide();
}
});
});
},
/**
* @private
* @see _onAddOrRemoveOption
* @param {$.Element} $modal
* @param {$.Element} $parent
* @param {integer} productTemplateId
*/
_onRemoveOption: function ($modal, $parent, productTemplateId){
// restore attribute values selection
$parent.removeClass('in_cart');
$parent.find('.td-qty').removeClass('text-center');
$parent.find('.js_remove.d-none').prepend($parent.find('.optional_product_quantity'));
$parent.find('.custom_attribute_values_description').remove();
var $select_options_text = $modal.find('.o_select_options');
$select_options_text.show();
// revert back to original product name and clean the entry
$parent.find(".product-name").html(productNameMap[productTemplateId]);
delete productNameMap[productTemplateId];
this._removeOptionOption($modal, productTemplateId);
$('tr:last').after($parent);
},
/**
* If the removed product had optional products, remove them as well
*
* @private
* @param {$.Element} $modal
* @param {integer} optionId The removed optional product id
*/
_removeOptionOption: function ($modal, optionId) {
var self = this;
if (optionalProductsMap[optionId]) {
optionalProductsMap[optionId].forEach(function (productId) {
$modal.find('.product_template_id').filter(function () {
return productId === $(this).val();
}).parents('.js_product:first').remove();
self._removeOptionOption($modal, productId);
});
delete optionalProductsMap[optionId];
}
},
/**
* When the quantity of the root product is updated, we need to update
* the quantity of all the selected optional products.
*
* @private
* @param {MouseEvent} ev
*/
_onChangeQuantity: function (ev){
var $quantity = $(ev.currentTarget);
var qty = parseFloat($quantity.val());
var $modal = $quantity.parents('.oe_optional_products_modal');
if (qty === 1) {
$modal.find(".js_items").addClass('d-none').removeClass('add_qty');
$modal.find(".js_item").removeClass('d-none').addClass('add_qty');
} else {
$modal.find(".js_items").removeClass('d-none').addClass('add_qty').text(qty);
$modal.find(".js_item").addClass('d-none').removeClass('add_qty');
}
this.rootProduct.quantity = qty;
if (!this.isWebsite){
this.onChangeAddQuantity(ev);
}
this.trigger('update_quantity', qty);
}
});
return OptionalProductsModal;
});
@@ -0,0 +1,52 @@
odoo.define('sale.ProductConfiguratorFormRenderer', function (require) {
"use strict";
var FormRenderer = require('web.FormRenderer');
var ProductConfiguratorMixin = require('sale.ProductConfiguratorMixin');
var ProductConfiguratorFormRenderer = FormRenderer.extend(ProductConfiguratorMixin ,{
/**
* @override
*/
init: function (){
this._super.apply(this, arguments);
this.pricelistId = $('.js_sale_order_pricelist_id').html();
},
/**
* @override
*/
start: function () {
this._super.apply(this, arguments);
this.$el.append($('<div>', {class: 'configurator_container'}));
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Renders the product configurator within the form
*
* Will also:
* - add events handling for variant changes
* - trigger variant change to compute the price and other
* variant specific changes
*
* @param {string} configuratorHtml the evaluated template of
* the product configurator
*/
renderConfigurator: function (configuratorHtml) {
var $configuratorContainer = this.$('.configurator_container');
$configuratorContainer.empty();
var $configuratorHtml = $(configuratorHtml);
$configuratorHtml.appendTo($configuratorContainer);
this.triggerVariantChange($configuratorContainer);
}
});
return ProductConfiguratorFormRenderer;
});
@@ -0,0 +1,20 @@
odoo.define('sale.ProductConfiguratorFormView', function (require) {
"use strict";
var ProductConfiguratorFormController = require('sale.ProductConfiguratorFormController');
var ProductConfiguratorFormRenderer = require('sale.ProductConfiguratorFormRenderer');
var FormView = require('web.FormView');
var viewRegistry = require('web.view_registry');
var ProductConfiguratorFormView = FormView.extend({
config: _.extend({}, FormView.prototype.config, {
Controller: ProductConfiguratorFormController,
Renderer: ProductConfiguratorFormRenderer,
}),
});
viewRegistry.add('product_configurator_form', ProductConfiguratorFormView);
return ProductConfiguratorFormView;
});
@@ -0,0 +1,194 @@
.css_attribute_color {
display: inline-block;
border: 1px solid #999999;
text-align: center;
input {
margin: 8px;
height: 13px;
opacity: 0;
}
&.active {
border: 3px ridge #66ee66;
}
&.active input {
margin: 6px;
}
&.custom_value {
background-image: linear-gradient(to bottom right, #FF0000, #FFF200, #1E9600);
}
}
.css_not_available_msg {
display: none;
}
.css_not_available.js_product {
> *:nth-child(4) > * {
display: none;
}
.css_quantity,
.product_price {
display: none;
}
.css_not_available_msg {
display: block;
}
}
.css_quantity {
max-width: 125px;
input[name="add_qty"] {
text-align: center;
}
}
option.css_not_available {
color: #ccc;
}
label.css_not_available {
opacity: 0.6;
}
label.css_attribute_color.css_not_available {
opacity: 1;
background-image: url("/website_sale/static/src/img/redcross.png");
background-size: cover;
}
.o_product_configurator {
.product_detail_img {
max-height: 240px;
}
.variant_custom_value {
margin-bottom: 10px;
}
.variant_custom_value_label {
margin-top: 10px;
}
}
.configurator_container {
ul ul {
margin-left: 1.5rem;
}
}
.oe_optional_products_modal {
.table-striped tbody tr:nth-of-type(odd) {
background-color: rgba(0, 0, 0, 0.025);
}
.o_total_row {
h5 {
padding-top: 0.4rem;
}
.js_price_total {
font-size: 1.5rem;
}
}
}
.modal.o_technical_modal .oe_optional_products_modal .btn.js_add_cart_json {
padding: 0.075rem 0.75rem;
}
.js_product {
&.in_cart {
.js_add_cart_variants {
display: none;
}
}
select {
-webkit-appearance: menulist;
-moz-appearance: menulist;
appearance: menulist;
background-image: none;
}
.remove-optionnal-item, .add-optionnal-item {
padding-left: 5px;
}
.td-product_name {
word-wrap: break-word;
}
.td-product_name {
min-width: 140px;
}
.td-img {
width: 100px;
}
.td-qty {
width: 130px;
a.input-group-addon {
background-color: transparent;
border: 0px;
}
.input-group {
display: inline-flex;
}
}
.td-action {
width: 30px;
}
.td-price,
.td-price-total {
width: 120px;
}
@include media-breakpoint-down(sm) {
.td-img,
.td-price-total {
display: none;
}
.td-qty {
width: 60px;
}
.td-price {
width: 80px;
}
}
@media (max-width: 476px) {
.td-qty {
width: 60px;
}
#modal_optional_products table thead,
.oe_cart table thead {
display: none;
}
#modal_optional_products table td.td-img,
.oe_cart table td.td-img {
display: none;
}
}
}
.o_total_row {
height: 50px;
}
.oe_striked_price {
text-decoration: line-through;
white-space: nowrap;
}
@@ -0,0 +1,154 @@
odoo.define('sale.product.configurator.tests', function (require) {
"use strict";
var FormView = require('web.FormView');
var ProductConfiguratorFormView = require('sale.ProductConfiguratorFormView');
var testUtils = require('web.test_utils');
var ajax = require('web.ajax');
var createView = testUtils.createView;
var getArch = function (){
return '<form>' +
'<sheet>' +
'<field name="sale_order_line">' +
'<tree editable="top"><control>' +
'<create string="Add a product"/>' +
'<create string="Configure a product" context="{\'open_product_configurator\': \'true\'}"/>' +
'<create string="Add a section" context="{\'default_display_type\': \'line_section\'}"/>' +
'<create string="Add a note" context="{\'default_display_type\': \'line_note\'}"/>' +
'</control>' +
'<field name="product_id"/><field name="quantity"/>' +
'</tree>' +
'</field>' +
'</sheet>' +
'</form>';
};
QUnit.module('Product Configurator', {
beforeEach: function () {
this.data = {
product_template: {
fields: {
id: {type: 'integer'}
},
records: [{
id: 42,
display_name: "Customizable Desk"
}]
},
product: {
fields: {
id: {type: 'integer'}
}
},
sale_order: {
fields: {
id: {type: 'integer'},
sale_order_line: {
string: 'lines',
type: 'one2many',
relation: 'sale_order_line'
},
}
},
sale_order_line: {
fields: {
product_id: {
string: 'product',
type: 'many2one',
relation: 'product'
},
quantity: {type: 'integer'}
}
},
sale_product_configurator: {
fields: {
product_template_id: {
string: 'product',
type: 'many2one',
relation: 'product_template'
}
},
records: [{
product_template_id: 42
}]
},
};
}
}, function (){
QUnit.test('click on "Configure a product" and check for form loading', function (assert) {
assert.expect(2);
var form = createView({
View: FormView,
model: 'sale_order',
data: this.data,
arch: getArch(),
mockRPC: function (route) {
if (route === '/web/dataset/call_kw/ir.model.data/xmlid_to_res_id') {
assert.ok(true);
return $.Deferred().then(_.constant(1));
}
return this._super.apply(this, arguments);
},
});
assert.strictEqual(form.$("a:contains('Configure a product')").length, 1);
form.$("a:contains('Configure a product')").click();
});
QUnit.test('trigger_up the "add_record" event and checks that rows are correctly added to the list', function (assert) {
assert.expect(1);
var form = createView({
View: FormView,
model: 'sale_order',
data: this.data,
arch: getArch()
});
var list = form.renderer.allFieldWidgets[form.handle][0];
list.trigger_up('add_record', {
context: [{product_id: 1, quantity: 2}, {product_id: 2, quantity: 3}],
forceEditable: "bottom" ,
allowWarning: true
});
assert.strictEqual(list.$("tr.o_data_row").length, 2);
});
QUnit.test('Select a product in the list and check for template loading', function (assert){
assert.expect(1);
var product_configurator_form = createView({
View: ProductConfiguratorFormView,
model: 'sale_product_configurator',
data: this.data,
arch:
'<form js_class="product_configurator_form">' +
'<group>' +
'<field name="product_template_id" class="oe_product_configurator_product_template_id" />' +
'</group>' +
'<footer>' +
'<button string="Add" class="btn-primary o_sale_product_configurator_add disabled"/>' +
'<button string="Cancel" class="btn-secondary" special="cancel"/>' +
'</footer>' +
'</form>',
mockRPC: function (route) {
if (route === '/product_configurator/configure') {
assert.ok(true);
return $.Deferred().then(_.constant(1));
}
return this._super.apply(this, arguments);
}
});
var div = '<div>';
var $div = $(div).addClass('js_sale_order_pricelist_id').html(43);
product_configurator_form.$('.o_input').after($div);
product_configurator_form.$('.o_input').click();
$("ul.ui-autocomplete li a:contains('Customizable Desk')").mouseenter().click();
});
});
});
+12
View File
@@ -3,10 +3,17 @@
<template id="assets_backend_inherit_sale" inherit_id="web.assets_backend" name="Sale backend assets">
<xpath expr="link[last()]" position="after">
<link rel="stylesheet" type="text/scss" href="/sale/static/src/scss/sale_onboarding.scss"/>
<link rel="stylesheet" type="text/scss" href="/sale/static/src/scss/product_configurator.scss"/>
</xpath>
<xpath expr="script[last()]" position="after">
<script type="text/javascript" src="/sale/static/src/js/sale.js"></script>
<script type="text/javascript" src="/sale/static/src/js/tour.js"></script>
<script type="text/javascript" src="/sale/static/src/js/list_editable_renderer.js"></script>
<script type="text/javascript" src="/sale/static/src/js/product_configurator_mixin.js"></script>
<script type="text/javascript" src="/sale/static/src/js/product_configurator_renderer.js"></script>
<script type="text/javascript" src="/sale/static/src/js/product_configurator_controller.js"></script>
<script type="text/javascript" src="/sale/static/src/js/product_configurator_view.js"></script>
<script type="text/javascript" src="/sale/static/src/js/product_configurator_modal.js"></script>
</xpath>
</template>
<template id="assets_frontend_inherit_sale" inherit_id="web.assets_frontend" name="Sale frontend assets">
@@ -17,4 +24,9 @@
<script type="text/javascript" src="/sale/static/src/js/sale_portal_sidebar.js"></script>
</xpath>
</template>
<template id="qunit_suite" name="product_configurator_tests" inherit_id="web.qunit_suite">
<xpath expr="//t[@t-set='head']" position="inside">
<script type="text/javascript" src="/sale/static/tests/product_configurator.test.js"></script>
</xpath>
</template>
</odoo>
@@ -0,0 +1,78 @@
<?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="type"/>
</field>
</field>
</record>
<record id="product_attribute_line_form" model="ir.ui.view">
<field name="name">product.attribute.line.form.inherit</field>
<field name="model">product.attribute.line</field>
<field name="inherit_id" ref="product.product_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="//group[@name='main_fields']" position="after">
<group>
<field name="type"/>
</group>
</xpath>
<xpath expr="//field[@name='value_ids']/tree/field[@name='name']" position="after">
<field name="html_color"/>
</xpath>
</field>
</record>
<record id="product_product_attribute_value_view_tree_inherit" model="ir.ui.view">
<field name="name">product.product.attribute.value.view.tree.inherit</field>
<field name="model">product.product.attribute.value</field>
<field name="inherit_id" ref="product.product_product_attribute_value_view_tree"/>
<field name="arch" type="xml">
<field name="name" position="after">
<field name="html_color"/>
</field>
</field>
</record>
<record id="product_product_attribute_value_view_form_inherit" model="ir.ui.view">
<field name="name">product.product.attribute.value.view.form.inherit</field>
<field name="model">product.product.attribute.value</field>
<field name="inherit_id" ref="product.product_product_attribute_value_view_form"/>
<field name="arch" type="xml">
<field name="name" position="after">
<field name="html_color" readonly="True" />
</field>
</field>
</record>
<record id="variants_tree_view" model="ir.ui.view">
<field name="name">variants.template.tree</field>
<field name="model">product.attribute.value</field>
<field name="inherit_id" ref="product.variants_tree_view"/>
<field name="arch" type="xml">
<field name="name" position="after">
<field name="is_custom" groups="product.group_product_variant" />
<field name="html_color"/>
</field>
</field>
</record>
</odoo>
@@ -18,7 +18,7 @@
<div class="o_setting_right_pane">
<label for="group_product_variant"/>
<div class="text-muted">
Set product attributes (e.g. color, size) to sell variants
Configure your products with variants and select optional products
</div>
<div class="content-group" attrs="{'invisible': [('group_product_variant','=',False)]}">
<div class="mt16">
@@ -0,0 +1,264 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="optional_products_modal" name="Optional Products">
<main class="modal-body">
<t t-call="sale.product_configurator_configure_optional_products" />
</main>
</template>
<template id="product_configurator_configure" name="Configure">
<div class="js_product main_product">
<input type="hidden" class="product_template_id" t-att-value="product.id"/>
<input type="hidden" class="product_id" t-attf-name="product_id" t-att-value="product.product_variant_id.id if len(product.product_variant_ids) == 1 else '0'"/>
<div class="col-lg-12 text-center mt-5">
<div class="col-lg-5 d-inline-block align-top text-left">
<span t-field="product.image" t-options='{"widget": "image", "class": "d-block product_detail_img"}'/>
</div>
<div class="col-lg-1 d-inline-block"></div>
<div class="col-lg-5 d-inline-block text-left">
<t t-call="sale.variants" />
<h2 class="oe_price product_id mt-3" style="white-space: nowrap;"
t-att-data-product-id="product.id"
t-esc="product.price if product.price else product.lst_price"
t-options='{
"widget": "monetary",
"display_currency": to_currency
}'/>
<div class="css_quantity input-group oe_website_spinner">
<div class="input-group-prepend">
<button t-attf-href="#" class="btn btn-primary js_add_cart_json d-none d-md-inline-block" aria-label="Remove one" title="Remove one">
<i class="fa fa-minus"></i>
</button>
</div>
<input type="text" class="js_quantity form-control quantity" data-min="1" name="add_qty" value="1"/>
<div class="input-group-append">
<button t-attf-href="#" class="btn btn-primary float_left js_add_cart_json d-none d-md-inline-block" aria-label="Add one" title="Add one">
<i class="fa fa-plus"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<template id="product_configurator_configure_optional_products">
<table class="table table-striped table-sm">
<thead>
<tr>
<th class="td-img">Product</th>
<th></th>
<th class="text-center td-qty">Quantity</th>
<th class="text-center td-price">Price</th>
</tr>
</thead>
<tbody>
<tr class="js_product in_cart main_product">
<input type="hidden" class="product_template_id" t-att-value="product.product_tmpl_id.id"/>
<input type="hidden" class="product_id" t-att-value="product.id"/>
<td class='td-img'>
<span t-field="product.image_medium" t-options='{"widget": "image" }'/>
</td>
<td class='td-product_name'>
<strong t-field="product.display_name"/>
<div class="text-muted small">
<div t-field="product.description_sale"/>
<div class="js_attributes"/>
</div>
</td>
<td class="text-center td-qty">
<div class="css_quantity input-group oe_website_spinner">
<div class="input-group-prepend">
<button t-attf-href="#" class="btn btn-primary js_add_cart_json d-none d-md-inline-block" aria-label="Remove one" title="Remove one">
<i class="fa fa-minus"></i>
</button>
</div>
<input type="text" class="js_quantity form-control quantity" data-min="1" name="add_qty" value="1"/>
<div class="input-group-append">
<button t-attf-href="#" class="btn btn-primary float_left js_add_cart_json d-none d-md-inline-block" aria-label="Add one" title="Add one">
<i class="fa fa-plus"></i>
</button>
</div>
</div>
</td>
<td class="text-center td-price" name="price">
<ul class="d-none js_add_cart_variants" t-att-data-attribute_exclusions="{'exclusions: []'}"></ul>
<div class="d-none oe_unchanged_value_ids" t-att-data-unchanged_value_ids="variant_values" ></div>
<div t-attf-class="text-danger oe_default_price oe_striked_price {{ (compute_currency(product.lst_price) - product.price) &lt; 0.01 and 'd-none' }}"
t-field="product.lst_price"
t-options='{
"widget": "monetary",
"from_currency": product.currency_id,
"display_currency": to_currency
}'/>
<span class="oe_price product_id" style="white-space: nowrap;"
t-att-data-product-id="product.id"
t-esc="product.price"
t-options='{
"widget": "monetary",
"display_currency": to_currency
}'/>
<span class="js_raw_price d-none" t-esc="product.price" />
</td>
</tr>
<tr class="o_total_row">
<td colspan="2"></td>
<td class="text-right"><h5>Total:</h5></td>
<td class="text-center">
<span class="js_price_total font-weight-bold" style="white-space: nowrap;"
t-att-data-product-id="product.id"
t-esc="product.price"
t-options='{
"widget": "monetary",
"display_currency": to_currency
}'/>
</td>
</tr>
<tr t-if="product.optional_product_ids" class="o_select_options"><td colspan="4"><h4>Available Options:</h4></td></tr>
<t t-call="sale.optional_product_items" />
</tbody>
</table>
</template>
<template id="optional_product_items">
<t t-foreach="product.optional_product_ids" t-as="product">
<t t-if="len(product.get_filtered_variants(reference_product)) > 0">
<tr class="js_product">
<td class="td-img">
<input type="hidden" class="product_template_id" t-att-value="product.id"/>
<input type="hidden" class="product_id" t-attf-name="optional-product-#{product.id}" t-att-value="product.product_variant_id.id if len(product.product_variant_ids) == 1 else '0'"/>
<input type="hidden" class="product_display_name" t-attf-name="optional-productname-#{product.id}" t-att-value="product.product_variant_id.display_name if len(product.product_variant_ids) == 1 else ''"/>
<span t-field="product.image_small" t-options='{"widget": "image"}' class="variant_image"/>
</td>
<td class='td-product_name'>
<div class="float-left">
<strong class="product-name" t-field="product.name"/>
<div class="text-muted small" t-field="product.description_sale"/>
</div>
</td>
<td class="td-qty">
<div class="float-right">
<t t-call="sale.variants"/>
</div>
</td>
<td class="text-center td-price">
<span class="text-danger oe_default_price oe_optional"
t-field="product.lst_price"
t-options='{
"widget": "monetary",
"from_currency": product.currency_id,
"display_currency": to_currency
}'/>
<span class="oe_price" style="white-space: nowrap;"
t-field="product.price"
t-options='{
"widget": "monetary",
"display_currency": to_currency
}'/>
<span class="js_raw_price d-none" t-esc="product.price" />
<p class="css_not_available_msg bg-danger" style="padding: 5px;">Option not available</p>
<input type="hidden" class="js_optional_same_quantity" t-attf-name="optional-add-#{product.id}" value="0"/>
<a role="button" href="#" class="js_add"><i class="fa fa-shopping-cart add-optionnal-item"></i></a>
<span class="js_remove d-none">
<span class="optional_product_quantity"><span class="js_item add_qty">1 </span><span class="js_items d-none">5 </span> <span t-field="product.uom_id.sudo().name"/><br/></span>
<a role="button" href="#" class="js_remove"><i class="fa fa-trash-o remove-optionnal-item"></i></a>
</span>
</td>
</tr>
</t>
</t>
</template>
<template id="variants">
<t t-set="attribute_exclusions" t-value="get_attribute_exclusions(product, reference_product)"/>
<ul t-attf-class="list-unstyled js_add_cart_variants #{ul_class}" t-att-data-attribute_exclusions="json.dumps(attribute_exclusions)">
<t t-foreach="product.attribute_line_ids.sorted(key=lambda x: x.attribute_id.sequence)" t-as="variant_id">
<li t-att-data-attribute_id="variant_id.attribute_id.id" t-att-data-attribute_name="variant_id.attribute_id.name">
<strong t-field="variant_id.attribute_id.name"/>
<t t-if="variant_id.attribute_id.type == 'select'">
<select
t-att-data-attribute_id="variant_id.attribute_id.id"
t-attf-class="form-control js_variant_change #{variant_id.attribute_id.create_variant}"
t-att-name="'attribute-%s-%s' % (product.id, variant_id.attribute_id.id)">
<t t-foreach="variant_id.product_value_ids" t-as="value_id">
<option t-att-value="value_id.id"
t-att-data-value_id="value_id.product_attribute_value_id.id"
t-att-data-value_name="value_id.name"
t-att-data-attribute_name="value_id.attribute_id.name"
t-att-data-is_custom="value_id.is_custom">
<span t-field="value_id.name"/>
<span t-if="value_id.price_extra">
<t t-esc="value_id.price_extra > 0 and '+' or ''"/><span t-field="value_id.price_extra" class="variant_price_extra" style="white-space: nowrap;" t-options='{
"widget": "monetary",
"from_currency": product.currency_id,
"display_currency": to_currency or website.currency_id
}'/>
</span>
</option>
</t>
</select>
</t>
<t t-if="variant_id.attribute_id.type == 'radio'">
<ul t-att-data-attribute_id="variant_id.attribute_id.id" class="list-unstyled">
<t t-set="inc" t-value="0"/>
<t t-foreach="variant_id.product_value_ids" t-as="value_id">
<li class="form-group js_attribute_value" style="margin: 0;">
<label class="col-form-label" style="margin: 0 20px;">
<input type="radio"
t-attf-class="js_variant_change #{variant_id.attribute_id.create_variant}"
t-att-checked="'checked' if not inc else None"
t-att-name="'attribute-%s-%s' % (product.id, variant_id.attribute_id.id)"
t-att-value="value_id.id"
t-att-data-value_id="value_id.product_attribute_value_id.id"
t-att-data-value_name="value_id.name"
t-att-data-attribute_name="value_id.attribute_id.name"
t-att-data-is_custom="value_id.is_custom"
style="vertical-align: top; margin-right: 10px;"/>
<span t-field="value_id.name"/>
<span class="badge badge-pill" t-if="value_id.price_extra">
<t t-esc="value_id.price_extra > 0 and '+' or ''"/><span t-field="value_id.price_extra" class="variant_price_extra" style="white-space: nowrap;" t-options='{
"widget": "monetary",
"from_currency": product.currency_id,
"display_currency": to_currency or website.currency_id
}'/>
</span>
</label>
</li>
<t t-set="inc" t-value="inc+1"/>
</t>
</ul>
</t>
<t t-if="variant_id.attribute_id.type == 'color'">
<ul t-att-data-attribute_id="variant_id.attribute_id.id" class="list-inline">
<t t-set="inc" t-value="0"/>
<li t-foreach="variant_id.product_value_ids" t-as="value_id" class="list-inline-item">
<label t-attf-style="background-color:#{value_id.html_color or value_id.name if not value_id.is_custom else ''}"
t-attf-class="css_attribute_color #{'active' if not inc else ''} #{'custom_value' if value_id.is_custom else ''}">
<input type="radio"
t-attf-class="js_variant_change #{variant_id.attribute_id.create_variant}"
t-att-checked="'checked' if not inc else None"
t-att-name="'attribute-%s-%s' % (product.id, variant_id.attribute_id.id)"
t-att-value="value_id.id"
t-att-title="value_id.name"
t-att-data-value_id="value_id.product_attribute_value_id.id"
t-att-data-value_name="value_id.name"
t-att-data-attribute_name="value_id.attribute_id.name"
t-att-data-is_custom="value_id.is_custom"/>
</label>
<t t-set="inc" t-value="inc+1"/>
</li>
</ul>
</t>
</li>
</t>
</ul>
</template>
</odoo>
+23 -1
View File
@@ -72,6 +72,24 @@
</field>
</record>
<record id="product_template_form_view" model="ir.ui.view">
<field name="name">product.template.product.website.form</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_form_view"/>
<field name="arch" type="xml">
<xpath expr="//page[@name='sales']" position="attributes">
<attribute name="invisible">0</attribute>
</xpath>
<xpath expr="//group[@name='sale']" position="after">
<group name="options" groups="product.group_product_variant">
<group string="Options">
<field name="optional_product_ids" widget="many2many_tags" options="{'color_field': 'color'}" domain="[('id', '!=', active_id)]" />
</group>
</group>
</xpath>
</field>
</record>
<menuitem id="product_menu_catalog" name="Products" action="product_template_action" parent="sale_menu_root" sequence="4" groups="sales_team.group_sale_salesman"/>
<menuitem id="menu_product" name="Product Variants" parent="product_menu_catalog" sequence="2" groups="base.group_no_one" active="False"/>
<menuitem action="product_template_action" id="menu_product_template_action" parent="product_menu_catalog" sequence="1" active="False"/>
@@ -272,6 +290,7 @@
<field name="validity_date" attrs="{'invisible': [('state', 'in', ['sale', 'done'])]}"/>
<field name="confirmation_date" attrs="{'invisible': [('state', 'in', ['draft', 'sent', 'cancel'])]}"/>
<field name="pricelist_id" groups="product.group_sale_pricelist"/>
<field name="pricelist_id_id" invisible="1" class="js_sale_order_pricelist_id" />
<field name="currency_id" invisible="1"/>
<field name="payment_term_id" options="{'no_create': True}"/>
</group>
@@ -377,6 +396,7 @@
>
<control>
<create string="Add a product"/>
<create string="Configure a product" context="{'open_product_configurator': 'true'}" groups="product.group_product_variant"/>
<create string="Add a section" context="{'default_display_type': 'line_section'}"/>
<create string="Add a note" context="{'default_display_type': 'line_note'}"/>
</control>
@@ -403,6 +423,8 @@
'default_description_sale': name
}"
/>
<field name="product_custom_variant_values" invisible="1" />
<field name="product_no_variant_attribute_values" invisible="1" />
<field name="name" widget="section_and_note_text"/>
<field
name="product_uom_qty"
@@ -1001,7 +1023,7 @@
<field name="inherit_id" ref="product.product_template_form_view"/>
<field name="arch" type="xml">
<xpath expr="//group[@name='email_template_and_project']" position="before">
<group string="Invoicing" name="invoicing">
<group string="Invoicing" name="invoicing" invisible="1">
<field name="invoice_policy" widget="radio"/>
<field name="service_type" widget="radio" invisible="True"/>
<field name="hide_expense_policy" invisible="1"/>
+1
View File
@@ -3,3 +3,4 @@
from . import payment_acquirer_onboarding_wizard
from . import sale_make_invoice_advance
from . import sale_product_configurator
@@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields
class SaleProductConfigurator(models.TransientModel):
_name = 'sale.product.configurator'
_description = 'Sale Product Configurator'
product_template_id = fields.Many2one(
'product.template', string="Product",
required=True, domain=['|', ('attribute_line_ids', '!=', False), ('optional_product_ids', '!=', False)])
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="sale_product_configurator_view_form" model="ir.ui.view">
<field name="name">sale.product.configurator.view.form</field>
<field name="model">sale.product.configurator</field>
<field name="arch" type="xml">
<form js_class="product_configurator_form">
<group>
<field name="product_template_id" class="oe_product_configurator_product_template_id" />
</group>
<footer>
<button string="Add" class="btn-primary o_sale_product_configurator_add disabled"/>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
</odoo>
+1
View File
@@ -47,6 +47,7 @@ The Dashboard for the Sales Manager will include
'views/sale_management_views.xml',
'views/digest_views.xml',
'views/sale_order_views.xml',
'views/assets.xml',
'report/sale_report_templates.xml',
],
'demo': [
@@ -0,0 +1,132 @@
odoo.define('sale.sale_product_configurator_advanced_tour', function (require) {
"use strict";
var tour = require('web_tour.tour');
tour.register('sale_product_configurator_advanced_tour', {
url: "/web",
test: true,
}, [tour.STEPS.SHOW_APPS_MENU_ITEM, {
trigger: '.o_app[data-menu-xmlid="sale.sale_menu_root"]',
edition: 'community'
}, {
trigger: '.o_app[data-menu-xmlid="sale.sale_menu_root"]',
edition: 'enterprise'
}, {
trigger: ".o_list_button_add",
extra_trigger: ".o_sale_order"
}, {
trigger: "a:contains('Configure a product')"
}, {
trigger: '.o_product_configurator .o_input_dropdown input',
run: 'click'
}, {
trigger: 'li a:contains("Customizable Desk")',
in_modal: false,
extra_trigger: 'ul.ui-autocomplete',
run: 'click'
}, {
trigger: 'span:contains("Custom")',
extra_trigger: '.o_product_configurator',
run: 'click'
}, {
trigger: '.o_product_configurator ul.js_add_cart_variants li[data-attribute_id]:nth-child(1) .variant_custom_value',
extra_trigger: '.o_product_configurator',
run: 'text Custom 1'
}, {
trigger: '.o_product_configurator ul.js_add_cart_variants li[data-attribute_id]:nth-child(3) span:contains("PAV9")',
extra_trigger: '.o_product_configurator',
run: 'click'
}, {
trigger: '.o_product_configurator ul.js_add_cart_variants li[data-attribute_id]:nth-child(3) .variant_custom_value',
extra_trigger: '.o_product_configurator',
run: 'text Custom 2'
}, {
trigger: '.o_product_configurator ul.js_add_cart_variants li[data-attribute_id]:nth-child(4) span:contains("PAV5")',
extra_trigger: '.o_product_configurator',
run: 'click'
}, {
trigger: '.o_product_configurator ul.js_add_cart_variants li[data-attribute_id]:nth-child(6) select ',
extra_trigger: '.o_product_configurator',
run: function (){
var inputValue = $('.o_product_configurator ul.js_add_cart_variants li[data-attribute_id]:nth-child(6) option[data-value_name="PAV9"]').val();
$('.o_product_configurator ul.js_add_cart_variants li[data-attribute_id]:nth-child(6) select').val(inputValue);
$('.o_product_configurator ul.js_add_cart_variants li[data-attribute_id]:nth-child(6) select').trigger('change');
}
}, {
trigger: '.o_product_configurator ul.js_add_cart_variants li[data-attribute_id]:nth-child(6) .variant_custom_value',
extra_trigger: '.o_product_configurator',
run: 'text Custom 3'
}, {
trigger: ".o_sale_product_configurator_add",
run: 'click'
}, {
trigger: '.main_product strong:contains("White, Custom, PAV9, PAV5, PAV1")',
extra_trigger: '.oe_optional_products_modal',
run: function () {} //check
}, {
trigger: '.main_product div:contains("Custom: Custom 1")',
extra_trigger: '.oe_optional_products_modal',
run: function () {} //check
}, {
trigger: '.main_product div:contains("PAV9: Custom 2")',
extra_trigger: '.oe_optional_products_modal',
run: function () {} //check
}, {
trigger: '.main_product div:contains("PAV9: Custom 3")',
extra_trigger: '.oe_optional_products_modal',
run: function () {} //check
}, {
trigger: '.main_product div:contains("PA5: PAV1")',
extra_trigger: '.oe_optional_products_modal',
run: function () {} //check
}, {
trigger: '.main_product div:contains("PA5: PAV1")',
extra_trigger: '.oe_optional_products_modal',
run: function () {} //check
}, {
trigger: '.main_product div:contains("PA8: PAV1")',
extra_trigger: '.oe_optional_products_modal',
run: function () {} //check
}, {
trigger: 'button span:contains(Confirm)',
extra_trigger: '.oe_optional_products_modal',
run: 'click'
}, {
trigger: 'td.o_data_cell:contains("Customizable Desk (White, Custom, PAV9, PAV5, PAV1)")',
extra_trigger: 'div[name="order_line"]',
in_modal: false,
run: function (){} //check
}, {
trigger: 'td.o_data_cell:contains("Custom: Custom 1")',
extra_trigger: 'div[name="order_line"]',
in_modal: false,
run: function (){} //check
}, {
trigger: 'td.o_data_cell:contains("PAV9: Custom 2")',
extra_trigger: 'div[name="order_line"]',
in_modal: false,
run: function (){} //check
}, {
trigger: 'td.o_data_cell:contains("PAV9: Custom 3")',
extra_trigger: 'div[name="order_line"]',
in_modal: false,
run: function (){} //check
}, {
trigger: 'td.o_data_cell:contains("PA5: PAV1")',
extra_trigger: 'div[name="order_line"]',
in_modal: false,
run: function (){} //check
}, {
trigger: 'td.o_data_cell:contains("PA5: PAV1")',
extra_trigger: 'div[name="order_line"]',
in_modal: false,
run: function (){} //check
}, {
trigger: 'td.o_data_cell:contains("PA8: PAV1")',
extra_trigger: 'div[name="order_line"]',
in_modal: false,
run: function (){} //check
}]);
});
@@ -0,0 +1,90 @@
odoo.define('sale.product_configurator_tour', function (require) {
"use strict";
var tour = require('web_tour.tour');
tour.register('sale_product_configurator_tour', {
url: "/web",
test: true,
}, [tour.STEPS.SHOW_APPS_MENU_ITEM, {
trigger: '.o_app[data-menu-xmlid="sale.sale_menu_root"]',
edition: 'community'
}, {
trigger: '.o_app[data-menu-xmlid="sale.sale_menu_root"]',
edition: 'enterprise'
}, {
trigger: ".o_list_button_add",
extra_trigger: ".o_sale_order"
}, {
trigger: "a:contains('Configure a product')"
}, {
trigger: '.o_product_configurator .o_input_dropdown input',
run: 'click'
}, {
trigger: 'li a:contains("Customizable Desk")',
in_modal: false,
extra_trigger: 'ul.ui-autocomplete',
run: 'click'
}, {
trigger: '.configurator_container span:contains("Steel")',
run: function () {
$('input.product_id').change(function () {
var request_count = 0;
if ($('.o_sale_product_configurator_add').attr('request_count')) {
request_count = parseInt($('.o_sale_product_configurator_add').attr('request_count'));
}
request_count++;
$('.o_sale_product_configurator_add').attr('request_count', request_count);
});
}
}, {
trigger: '.configurator_container span:contains("Aluminium")',
run: 'click'
}, {
trigger: 'span.oe_currency_value:contains("800")',
run: function (){} // check updated price
}, {
trigger: 'input[data-value_name="Black"]'
}, {
trigger: 'input[data-value_name="White"]'
}, {
trigger: '.o_sale_product_configurator_add[request_count="3"]',
run: function (){} // used to sync with "get_combination_info" completion
}, {
trigger: '.o_sale_product_configurator_add:not(.disabled)'
}, {
trigger: 'span:contains("Aluminium")',
extra_trigger: '.oe_optional_products_modal',
run: 'click'
}, {
trigger: '.js_product:has(strong:contains(Conference Chair)) .js_add',
extra_trigger: '.oe_optional_products_modal .js_product:has(strong:contains(Conference Chair))',
run: 'click'
}, {
trigger: '.js_product:has(strong:contains(Chair floor protection)) .js_add',
extra_trigger: '.oe_optional_products_modal .js_product:has(strong:contains(Chair floor protection))',
run: 'click'
}, {
trigger: 'button span:contains(Confirm)',
extra_trigger: '.oe_optional_products_modal',
run: 'click'
},
// check that 3 products were added to the SO
{
trigger: 'td.o_data_cell:contains("Customizable Desk (White, Aluminium)")',
extra_trigger: 'div[name="order_line"]',
in_modal: false,
run: function (){}
}, {
trigger: 'td.o_data_cell:contains("Conference Chair (Aluminium)")',
extra_trigger: 'div[name="order_line"]',
in_modal: false,
run: function (){}
}, {
trigger: 'td.o_data_cell:contains("Chair floor protection")',
extra_trigger: 'div[name="order_line"]',
in_modal: false,
run: function (){}
}]);
});
@@ -6,3 +6,65 @@ class TestUi(odoo.tests.HttpCase):
def test_01_sale_tour(self):
self.phantom_js("/web", "odoo.__DEBUG__.services['web_tour.tour'].run('sale_tour')", "odoo.__DEBUG__.services['web_tour.tour'].tours.sale_tour.ready", login="admin")
def test_02_product_configurator(self):
# To be able to test the product configurator, admin user must have access to "variants and options" feature, so we give him the right group for that
self.env.ref('base.user_admin').write({'groups_id': [(4, self.env.ref('product.group_product_variant').id)]})
self.phantom_js("/web", "odoo.__DEBUG__.services['web_tour.tour'].run('sale_product_configurator_tour')", "odoo.__DEBUG__.services['web_tour.tour'].tours.sale_product_configurator_tour.ready", login="admin")
def test_03_product_configurator_advanced(self):
# To be able to test the product configurator, admin user must have access to "variants and options" feature, so we give him the right group for that
self.env.ref('base.user_admin').write({'groups_id': [(4, self.env.ref('product.group_product_variant').id)]})
# Prepare relevant test data
# This is not included in demo data to avoid useless noise
product_attributes = self.env['product.attribute'].create([{
'name': 'PA1',
'type': 'radio',
'create_variant': 'dynamic'
}, {
'name': 'PA2',
'type': 'radio',
'create_variant': 'always'
}, {
'name': 'PA3',
'type': 'radio',
'create_variant': 'dynamic'
}, {
'name': 'PA4',
'type': 'select',
'create_variant': 'never'
}, {
'name': 'PA5',
'type': 'select',
'create_variant': 'never'
}, {
'name': 'PA7',
'type': 'color',
'create_variant': 'never'
}, {
'name': 'PA8',
'type': 'radio',
'create_variant': 'never'
}])
product_attribute_values = self.env['product.attribute.value'].create([{
'name': 'PAV' + str(i),
'is_custom': i == 9,
'attribute_id': product_attribute.id
} for i in range(1, 11) for product_attribute in product_attributes])
product_attribute_lines = self.env['product.attribute.line'].create([{
'attribute_id': product_attribute.id,
'product_tmpl_id': self.env.ref("product.product_product_4").id,
'value_ids': [(6, 0, product_attribute_values.filtered(
lambda product_attribute_value: product_attribute_value.attribute_id == product_attribute
).ids)]
} for product_attribute in product_attributes])
self.env.ref("product.product_product_4").update({
'attribute_line_ids': [(4, product_attribute_line.id) for product_attribute_line in product_attribute_lines]
})
self.phantom_js("/web", "odoo.__DEBUG__.services['web_tour.tour'].run('sale_product_configurator_advanced_tour')", "odoo.__DEBUG__.services['web_tour.tour'].tours.sale_product_configurator_advanced_tour.ready", login="admin")
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="assets_backend_inherit_sale_management" inherit_id="web.assets_backend" name="Sale management backend assets">
<xpath expr="script[last()]" position="after">
<script type="text/javascript" src="/sale_management/static/tests/product_configurator_test_ui.js"/>
<script type="text/javascript" src="/sale_management/static/tests/product_configurator_advanced_test_ui.js"/>
</xpath>
</template>
</odoo>
@@ -108,6 +108,15 @@
<xpath expr="//group[@name='description']" position="attributes">
<attribute name="invisible">0</attribute>
</xpath>
<xpath expr="//group[@name='sale']" position="attributes">
<attribute name="invisible">0</attribute>
</xpath>
<xpath expr="//group[@name='invoicing']" position="attributes">
<attribute name="invisible">0</attribute>
</xpath>
<xpath expr="//group[@name='description']" position="attributes">
<attribute name="invisible">0</attribute>
</xpath>
</field>
</record>
@@ -46,4 +46,13 @@
</p>
</field>
</record>
<menuitem id="menu_product_settings" name="Products" sequence="80"
parent="sale.menu_sale_config"/>
<menuitem id="menu_product_attribute_action"
action="product.attribute_action"
parent="menu_product_settings" groups="product.group_product_variant" sequence="1"/>
<menuitem id="menu_catalog_variants_action"
action="product.variants_action"
parent="menu_product_settings" groups="product.group_product_variant" sequence="2"/>
</odoo>
+1 -1
View File
@@ -19,7 +19,7 @@ Preferences
* Incoterms: International Commercial terms
""",
'depends': ['sale_management', 'stock_account'],
'depends': ['sale', 'stock_account'],
'data': [
'security/sale_stock_security.xml',
'security/ir.model.access.csv',
+2 -2
View File
@@ -262,8 +262,8 @@ class SaleOrderLine(models.Model):
if float_compare(product.virtual_available, product_qty, precision_digits=precision) == -1:
is_available = self._check_routing()
if not is_available:
message = _('You plan to sell %s %s but you only have %s %s available in %s warehouse.') % \
(self.product_uom_qty, self.product_uom.name, product.virtual_available, product.uom_id.name, self.order_id.warehouse_id.name)
message = _('You plan to sell %s %s of %s but you only have %s %s available in %s warehouse.') % \
(self.product_uom_qty, self.product_uom.name, self.product_id.name, product.virtual_available, product.uom_id.name, self.order_id.warehouse_id.name)
# We check if some products are available in other warehouses.
if float_compare(product.virtual_available, self.product_id.virtual_available, precision_digits=precision) == -1:
message += _('\nThere are %s %s available across all warehouses.\n\n') % \
+2 -1
View File
@@ -73,5 +73,6 @@ access_stock_forecast_manager,report.stock.forecast.manager,model_report_stock_f
access_stock_scrap_user,stock.scrap.user,model_stock_scrap,stock.group_stock_user,1,1,1,0
access_product_attribute_manager,product.attribute manager,product.model_product_attribute,stock.group_stock_manager,1,1,1,1
access_product_attribute_value_manager,product.attribute manager value,product.model_product_attribute_value,stock.group_stock_manager,1,1,1,1
access_product_attribute_price_manager,product.attribute manager price,product.model_product_attribute_price,stock.group_stock_manager,1,1,1,1
access_product_product_attribute_manager,product.product.attribute manager value,product.model_product_product_attribute_value,stock.group_stock_manager,1,1,1,1
access_product_attribute_filter_line_manager,product.attribute manager filter line,product.model_product_attribute_filter_line,stock.group_stock_manager,1,1,1,1
access_product_attribute_line_manager,product.attribute manager line,product.model_product_attribute_line,stock.group_stock_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
73 access_stock_scrap_user stock.scrap.user model_stock_scrap stock.group_stock_user 1 1 1 0
74 access_product_attribute_manager product.attribute manager product.model_product_attribute stock.group_stock_manager 1 1 1 1
75 access_product_attribute_value_manager product.attribute manager value product.model_product_attribute_value stock.group_stock_manager 1 1 1 1
76 access_product_attribute_price_manager access_product_product_attribute_manager product.attribute manager price product.product.attribute manager value product.model_product_attribute_price product.model_product_product_attribute_value stock.group_stock_manager 1 1 1 1
77 access_product_attribute_filter_line_manager product.attribute manager filter line product.model_product_attribute_filter_line stock.group_stock_manager 1 1 1 1
78 access_product_attribute_line_manager product.attribute manager line product.model_product_attribute_line stock.group_stock_manager 1 1 1 1
@@ -496,6 +496,7 @@ var AbstractField = Widget.extend({
viewType: this.viewType,
doNotSetDirty: options && options.doNotSetDirty,
notifyChange: !options || options.notifyChange !== false,
allowWarning: options && options.allowWarning,
onSuccess: def.resolve.bind(def),
onFailure: def.reject.bind(def),
});
@@ -1453,6 +1453,15 @@ var FieldOne2Many = FieldX2Many.extend({
* @param {OdooEvent|MouseEvent} ev this event comes either from the 'Add
* record' link in the list editable renderer, or from the 'Create' button
* in the kanban view
* @param {Array} ev.data.context additional context for the added records,
* if several contexts are provided, multiple records will be added
* (form dialog will only use the context at index 0 if provided)
* @param {boolean} ev.data.forceEditable this is used to bypass the dialog opening
* in case you want to add record(s) to a list
* @param {function} ev.data.onSuccess called when the records are correctly created
* (not supported by form dialog)
* @param {boolean} ev.data.allowWarning defines if the records can be added
* to the list even if warnings are triggered (e.g: stock warning for product availability)
*/
_onAddRecord: function (ev) {
var self = this;
@@ -1461,7 +1470,7 @@ var FieldOne2Many = FieldX2Many.extend({
// we don't want interference with the components upstream.
ev.stopPropagation();
if (this.editable) {
if (this.editable || data.forceEditable) {
if (!this.activeActions.create) {
if (data.onFail) {
data.onFail();
@@ -1471,15 +1480,21 @@ var FieldOne2Many = FieldX2Many.extend({
this.trigger_up('edited_list', { id: this.value.id });
this._setValue({
operation: 'CREATE',
position: this.editable,
position: this.editable || data.forceEditable,
context: data.context,
}, {
allowWarning: data.allowWarning
}).always(function () {
self.creatingRecord = false;
}).done(function(){
if(data.onSuccess){
data.onSuccess();
}
});
}
} else {
this._openFormDialog({
context: data.context,
context: data.context && data.context[0],
on_saved: function (record) {
self._setValue({ operation: 'ADD', id: record.id });
},
@@ -1328,34 +1328,54 @@ var BasicModel = AbstractModel.extend({
* @param {Object} [options]
* @param {string} [options.position=top] if the new record should be added
* on top or on bottom of the list
* @param {Object} [options.context] additional context to be merged before
* calling the default_get (eg. to set default values)
* @returns {Deferred<string>} resolves to the new record id
* @param {Object} [options.[context]] additional context to be merged before
* calling the default_get (eg. to set default values).
* If several contexts are found, multiple records are added
* @param {boolean} [params.allowWarning=false] if true, the default record
* operation can complete, even if a warning is raised
* @returns {Deferred<[string]>} resolves to the new records ids
*/
_addX2ManyDefaultRecord: function (list, options) {
var self = this;
var position = options && options.position || 'top';
var additionalContext = options && options.context;
var context = this._getContext(list, {additionalContext: additionalContext});
var params = {
context: context,
fields: list.fields,
fieldsInfo: list.fieldsInfo,
parentID: list.id,
position: position,
viewType: list.viewType,
allowWarning: options && options.allowWarning
};
return this._makeDefaultRecord(list.model, params).then(function (id) {
list._changes.push({operation: 'ADD', id: id, position: position, isNew: true});
var record = self.localData[id];
list._cache[record.res_id] = id;
if (list.orderedResIDs) {
var index = list.offset + (position !== 'top' ? list.limit : 0);
list.orderedResIDs.splice(index, 0, record.res_id);
// list could be a copy of the original one
self.localData[list.id].orderedResIDs = list.orderedResIDs;
}
return id;
var additionalContexts = options && options.context;
var makeDefaultRecords = [];
if(additionalContexts){
_.each(additionalContexts, function (context) {
params.context = self._getContext(list, {additionalContext: context});
makeDefaultRecords.push(self._makeDefaultRecord(list.model, params));
});
} else {
params.context = self._getContext(list);
makeDefaultRecords.push(self._makeDefaultRecord(list.model, params));
}
return $.when.apply($, makeDefaultRecords).then(function(){
var ids = [];
_.each(arguments, function(id){
ids.push(id);
list._changes.push({operation: 'ADD', id: id, position: position, isNew: true});
var record = self.localData[id];
list._cache[record.res_id] = id;
if (list.orderedResIDs) {
var index = list.offset + (position !== 'top' ? list.limit : 0);
list.orderedResIDs.splice(index, 0, record.res_id);
// list could be a copy of the original one
self.localData[list.id].orderedResIDs = list.orderedResIDs;
}
});
return ids;
});
},
/**
@@ -1374,6 +1394,9 @@ var BasicModel = AbstractModel.extend({
* it was changed.
* @param {string} [options.viewType] current viewType. If not set, we will assume
* main viewType from the record
* @param {boolean} [params.allowWarning=false] if true, change
* operation can complete, even if a warning is raised
* (only supported by X2ManyChange)
* @returns {Deferred} list of changed fields
*/
_applyChange: function (recordID, changes, options) {
@@ -1395,7 +1418,7 @@ var BasicModel = AbstractModel.extend({
for (var fieldName in changes) {
field = record.fields[fieldName];
if (field.type === 'one2many' || field.type === 'many2many') {
defs.push(this._applyX2ManyChange(record, fieldName, changes[fieldName], options.viewType));
defs.push(this._applyX2ManyChange(record, fieldName, changes[fieldName], options.viewType, options.allowWarning));
} else if (field.type === 'many2one' || field.type === 'reference') {
defs.push(this._applyX2OneChange(record, fieldName, changes[fieldName]));
} else {
@@ -1722,9 +1745,12 @@ var BasicModel = AbstractModel.extend({
* key. For example, it looks like {operation: ADD, id: 'partner_1'}
* @param {string} [viewType] current viewType. If not set, we will assume
* main viewType from the record
* @param {boolean} [params.allowWarning=false] if true, change
* operation can complete, even if a warning is raised
* (only supported by the 'CREATE' command.operation)
* @returns {Deferred}
*/
_applyX2ManyChange: function (record, fieldName, command, viewType) {
_applyX2ManyChange: function (record, fieldName, command, viewType, allowWarning) {
if (command.operation === 'TRIGGER_ONCHANGE') {
// the purpose of this operation is to trigger an onchange RPC, so
// there is no need to apply any change on the record (the changes
@@ -1819,16 +1845,19 @@ var BasicModel = AbstractModel.extend({
var options = {
context: command.context,
position: command.position,
allowWarning: allowWarning
};
def = this._addX2ManyDefaultRecord(list, options).then(function (id) {
if (command.position === 'bottom' && list.orderedResIDs && list.orderedResIDs.length >= list.limit) {
list.tempLimitIncrement = (list.tempLimitIncrement || 0) + 1;
list.limit += 1;
}
// FIXME: hack for lunch widget, which does useless default_get and onchange
if (command.data) {
return self._applyChange(id, command.data);
}
def = this._addX2ManyDefaultRecord(list, options).then(function (ids) {
_.each(ids, function(id){
if (command.position === 'bottom' && list.orderedResIDs && list.orderedResIDs.length >= list.limit) {
list.tempLimitIncrement = (list.tempLimitIncrement || 0) + 1;
list.limit += 1;
}
// FIXME: hack for lunch widget, which does useless default_get and onchange
if (command.data) {
return self._applyChange(id, command.data);
}
});
});
defs.push(def);
break;
@@ -46,7 +46,7 @@ var FieldManagerMixin = {
*/
_applyChanges: function (dataPointID, changes, event) {
var self = this;
var options = _.pick(event.data, 'context', 'doNotSetDirty', 'notifyChange', 'viewType');
var options = _.pick(event.data, 'context', 'doNotSetDirty', 'notifyChange', 'viewType', 'allowWarning');
return this.model.notifyChanges(dataPointID, changes, options)
.then(function (result) {
if (event.data.force_save) {
@@ -61,7 +61,7 @@ ListRenderer.include({
}
_.each(child.children, function (child) {
if (child.tag !== 'create') {
if (child.tag !== 'create' || child.attrs.invisible) {
return;
}
@@ -778,7 +778,7 @@ ListRenderer.include({
// but we do want to unselect current row
var self = this;
this.unselectRow().then(function () {
self.trigger_up('add_record', {context: ev.currentTarget.dataset.context}); // TODO write a test, the deferred was not considered
self.trigger_up('add_record', {context: ev.currentTarget.dataset.context && [ev.currentTarget.dataset.context]}); // TODO write a test, the deferred was not considered
});
},
/**
+77 -36
View File
@@ -12,6 +12,7 @@ from odoo.addons.payment.controllers.portal import PaymentProcessing
from odoo.addons.website.controllers.main import QueryURL
from odoo.exceptions import ValidationError
from odoo.addons.website.controllers.main import Website
from odoo.addons.sale.controllers.product_configurator import ProductConfiguratorController
from odoo.addons.website_form.controllers.main import WebsiteForm
from odoo.osv import expression
@@ -130,7 +131,7 @@ class Website(Website):
return views
class WebsiteSale(http.Controller):
class WebsiteSale(ProductConfiguratorController):
def _get_compute_currency_and_context(self):
pricelist_context = dict(request.env.context)
@@ -147,31 +148,6 @@ class WebsiteSale(http.Controller):
return compute_currency, pricelist_context, pricelist
def get_attribute_value_ids(self, product):
""" list of selectable attributes of a product
:return: list of product variant description
(variant id, [visible attribute ids], variant price, variant sale price)
"""
# product attributes with at least two choices
quantity = product._context.get('quantity') or 1
product = product.with_context(quantity=quantity)
visible_attrs_ids = product.attribute_line_ids.filtered(lambda l: len(l.value_ids) > 1).mapped('attribute_id').ids
to_currency = request.website.get_current_pricelist().currency_id
attribute_value_ids = []
for variant in product.product_variant_ids:
if to_currency != product.currency_id:
price = variant.currency_id._convert(
variant.website_public_price, to_currency,
request.env.user.company_id, fields.Date.today()
) / quantity
else:
price = variant.website_public_price / quantity
visible_attribute_ids = [v.id for v in variant.attribute_value_ids if v.attribute_id.id in visible_attrs_ids]
attribute_value_ids.append([variant.id, visible_attribute_ids, variant.website_price / quantity, price])
return attribute_value_ids
def _get_search_order(self, post):
# OrderBy will be parsed in orm and so no direct sql injection
# id is added to be sure that order is a unique sort key
@@ -344,7 +320,8 @@ class WebsiteSale(http.Controller):
'categories': categs,
'main_object': product,
'product': product,
'get_attribute_value_ids': self.get_attribute_value_ids,
'optional_product_ids': [p.with_context({'active_id': p.id}) for p in product.optional_product_ids],
'get_attribute_exclusions': self._get_attribute_exclusions,
}
return request.render("website_sale.product", values)
@@ -421,13 +398,10 @@ class WebsiteSale(http.Controller):
product_id=int(product_id),
add_qty=add_qty,
set_qty=set_qty,
attributes=self._filter_attributes(**kw),
product_custom_variant_values=json.loads(kw.get('product_custom_variant_values'))
)
return request.redirect("/shop/cart")
def _filter_attributes(self, **kw):
return {k: v for k, v in kw.items() if "attribute" in k}
@http.route(['/shop/cart/update_json'], type='json', auth="public", methods=['POST'], website=True, csrf=False)
def cart_update_json(self, product_id, line_id=None, add_qty=None, set_qty=None, display=True):
order = request.website.sale_get_order(force_create=1)
@@ -1005,11 +979,6 @@ class WebsiteSale(http.Controller):
ret = self.order_2_return_dict(order)
return ret
@http.route(['/shop/get_unit_price'], type='json', auth="public", methods=['POST'], website=True)
def get_unit_price(self, product_ids, add_qty, **kw):
products = request.env['product.product'].with_context({'quantity': add_qty}).browse(product_ids)
return {product.id: product.website_price / add_qty for product in products}
# ------------------------------------------------------
# Edit
# ------------------------------------------------------
@@ -1097,3 +1066,75 @@ class WebsiteSale(http.Controller):
states=[(st.id, st.name, st.code) for st in country.get_website_sale_states(mode=mode)],
phone_code=country.phone_code
)
@http.route(['/shop/cart/update_option'], type='http', auth="public", methods=['POST'], website=True, multilang=False)
def cart_options_update_json(self, product_id, add_qty=1, set_qty=0, goto_shop=None, lang=None, **kw):
if lang:
request.website = request.website.with_context(lang=lang)
order = request.website.sale_get_order(force_create=1)
optional_product_ids = []
for k, v in kw.items():
if "optional-product-" in k and int(kw.get(k.replace("product", "add"))):
optional_product_ids.append(int(v))
custom_values = json.loads(kw.get('custom_values'))
value = {}
if add_qty or set_qty:
value = order._cart_update(
product_id=int(product_id),
add_qty=add_qty,
set_qty=set_qty,
optional_product_ids=optional_product_ids,
product_custom_variant_values=self._get_product_custom_value(
int(product_id),
custom_values,
'product_custom_variant_values'
),
no_variant_attribute_values=self._get_product_custom_value(
int(product_id),
custom_values,
'no_variant_attribute_values'
)
)
# options have all time the same quantity
for option_id in optional_product_ids:
order._cart_update(
product_id=option_id,
set_qty=value.get('quantity'),
linked_line_id=value.get('line_id'),
product_custom_variant_values=self._get_product_custom_value(
option_id,
custom_values,
'product_custom_variant_values'
),
no_variant_attribute_values=self._get_product_custom_value(
option_id,
custom_values,
'no_variant_attribute_values'
)
)
return str(order.cart_quantity)
def _get_product_custom_value(self, product_id, custom_values, field):
if custom_values:
for custom_value in custom_values:
if custom_value['product_id'] == product_id:
return custom_value[field]
return None
@http.route(['/product_configurator/show_optional_products_website'], type='json', auth="public", methods=['POST'], website=True)
def show_optional_products_website(self, product_id, variant_values, **kw):
return self._show_optional_products(product_id, variant_values, request.website.get_current_pricelist(), True, **kw)
@http.route(['/product_configurator/optional_product_items_website'], type='json', auth="public", methods=['POST'], website=True)
def optional_product_items_website(self, product_id, **kw):
return self._optional_product_items(product_id, request.website.get_current_pricelist(), **kw)
@http.route(['/product_configurator/get_combination_info_website'], type='json', auth="public", methods=['POST'], website=True)
def get_combination_info_website(self, product_template_id, product_id, combination, add_qty, **kw):
return self._get_combination_info(product_template_id, product_id, combination, add_qty, request.website.get_current_pricelist())
+42 -11
View File
@@ -5,11 +5,9 @@
<field name="salesteam_id" ref="sales_team.salesteam_website_sales"/>
</record>
<record id="product.product_attribute_2" model="product.attribute">
<field name="type">color</field>
</record>
<record id="product.product_attribute_3" model="product.attribute">
<field name="type">select</field>
<record id="sale.product_product_1_product_template" model="product.template">
<field name="website_sequence">100</field>
<field name="website_published" eval="True"/>
</record>
<record id="product.product_product_24" model="product.product">
@@ -223,12 +221,6 @@
<record id="product.product_delivery_02_product_template" model="product.template">
<field name="public_categ_ids" eval="[(6,0,[ref('public_category_desks')])]"/>
</record>
<record id="product.product_attribute_value_3" model="product.attribute.value">
<field name="html_color">#FFFFFF</field>
</record>
<record id="product.product_attribute_value_4" model="product.attribute.value">
<field name="html_color">#000000</field>
</record>
<record model="product.pricelist" id="product.list0">
<field name="selectable" eval="True" />
@@ -582,4 +574,43 @@
<!-- action_confirm for confirmation date -->
<function model="sale.order" name="action_confirm" eval="[[ref('website_sale_order_14')]]"/>
<!-- Moved from deleted module "website_sale_option" -->
<record id="product_product_1" model="product.product">
<field name="name">Warranty</field>
<field name="default_code">SERV_125889</field>
<field name="list_price">20.0</field>
<field name="website_sequence">100</field>
<field name="website_published" eval="True"/>
<field name="type">service</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
<field name="description_sale">Warranty, issued to the purchaser of an article by its manufacturer, promising to repair or replace it if necessary within a specified period of time.</field>
<field name="attribute_value_ids" eval="[(6,0,[ref('product.product_attribute_value_5')])]"/>
<field name="categ_id" ref="product.product_category_3"/>
</record>
<record id="product_attribute_line_1" model="product.attribute.line">
<field name="product_tmpl_id" ref="product_product_1_product_template"/>
<field name="attribute_id" ref="product.product_attribute_3"/>
<field name="value_ids" eval="[(6,0,[ref('product.product_attribute_value_5'), ref('product.product_attribute_value_6')])]"/>
</record>
<record id="product_product_1_product_template" model="product.template">
<field name="attribute_line_ids" eval="[(6,0,[ref('product_attribute_line_1')])]"/>
<field name="public_categ_ids" eval="[(6,0,[ref('website_sale.services')])]"/>
</record>
<record id="product.product_product_4_product_template" model="product.template">
<field name="optional_product_ids" eval="[(6,0,[ref('product.product_product_11_product_template'), ref('product_product_1_product_template')])]"/>
</record>
<!-- Handle automatically created product.product.attribute.value -->
<function model="ir.model.data" name="_update_xmlids">
<value model="base" eval="[{
'xml_id': 'website_sale.product_product_attribute_value_1',
'record': obj().env.ref('website_sale.product_attribute_line_1').product_value_ids[1],
'noupdate': True,
}]"/>
</function>
<record id="product_product_attribute_value_1" model="product.product.attribute.value">
<field name="price_extra">18.00</field>
</record>
</odoo>
-14
View File
@@ -217,20 +217,6 @@ class Product(models.Model):
return self.product_tmpl_id.website_publish_button()
class ProductAttribute(models.Model):
_inherit = "product.attribute"
type = fields.Selection([('radio', 'Radio'), ('select', 'Select'), ('color', 'Color')], default='radio')
class ProductAttributeValue(models.Model):
_inherit = "product.attribute.value"
html_color = fields.Char(string='HTML Color Index', oldname='color', help="Here you can set a "
"specific HTML color index (e.g. #ff0000) to display the color on the website if the "
"attibute type is 'Color'.")
class ProductImage(models.Model):
_name = 'product.image'
_description = 'Product Image'
@@ -27,7 +27,6 @@ class ResConfigSettings(models.TransientModel):
group_delivery_invoice_address = fields.Boolean(string="Shipping Address", implied_group='sale.group_delivery_invoice_address', group='base.group_portal,base.group_user,base.group_public')
module_website_sale_options = fields.Boolean("Optional Products")
module_website_sale_digital = fields.Boolean("Digital Content")
module_website_sale_wishlist = fields.Boolean("Wishlists")
module_website_sale_comparison = fields.Boolean("Product Comparison Tool")
+69 -20
View File
@@ -66,13 +66,30 @@ class SaleOrder(models.Model):
product = self.env['product.product'].browse(product_id)
# split lines with the same product if it has untracked attributes
if product and product.mapped('attribute_line_ids').filtered(lambda r: not r.attribute_id.create_variant) and not line_id:
if product and product.mapped('attribute_line_ids').filtered(lambda r: not r.attribute_id.create_variant == 'always') and not line_id:
return self.env['sale.order.line']
domain = [('order_id', '=', self.id), ('product_id', '=', product_id)]
if line_id:
domain += [('id', '=', line_id)]
return self.env['sale.order.line'].sudo().search(domain)
else:
domain += [('product_custom_variant_values', '=', False)]
lines = self.env['sale.order.line'].sudo().search(domain)
if line_id:
return lines
linked_line_id = kwargs.get('linked_line_id', False)
optional_product_ids = set(kwargs.get('optional_product_ids', []))
lines = lines.filtered(lambda line: line.linked_line_id.id == linked_line_id)
if optional_product_ids:
# only match the lines with the same chosen optional products on the existing lines
lines = lines.filtered(lambda line: optional_product_ids == set(line.mapped('option_line_ids.product_id.id')))
else:
lines = lines.filtered(lambda line: not line.option_line_ids)
return lines
@api.multi
def _website_product_id_change(self, order_id, product_id, qty=0):
@@ -101,10 +118,7 @@ class SaleOrder(models.Model):
}
@api.multi
def _get_line_description(self, order_id, product_id, attributes=None):
if not attributes:
attributes = {}
def _get_line_description(self, order_id, product_id, no_variant_attribute_values=None, custom_values=None):
order = self.sudo().browse(order_id)
product_context = dict(self.env.context)
product_context.setdefault('lang', order.partner_id.lang)
@@ -112,23 +126,20 @@ class SaleOrder(models.Model):
name = product.display_name
# add untracked attributes in the name
untracked_attributes = []
for k, v in attributes.items():
# attribute should be like 'attribute-48-1' where 48 is the product_id, 1 is the attribute_id and v is the attribute value
attribute_value = self.env['product.attribute.value'].sudo().browse(int(v))
if attribute_value and not attribute_value.attribute_id.create_variant:
untracked_attributes.append(attribute_value.name)
if untracked_attributes:
name += '\n%s' % (', '.join(untracked_attributes))
if product.description_sale:
name += '\n%s' % (product.description_sale)
if no_variant_attribute_values:
name += ''.join(['\n%s: %s' % (attribute_value['attribute_name'], attribute_value['attribute_value_name'])
for attribute_value in no_variant_attribute_values])
if custom_values:
name += ''.join(['\n%s: %s' % (custom_value['attribute_value_name'], custom_value['custom_value']) for custom_value in custom_values])
return name
@api.multi
def _cart_update(self, product_id=None, line_id=None, add_qty=0, set_qty=0, attributes=None, **kwargs):
def _cart_update(self, product_id=None, line_id=None, add_qty=0, set_qty=0, **kwargs):
""" Add or set product quantity, add_qty can be negative """
self.ensure_one()
SaleOrderLineSudo = self.env['sale.order.line'].sudo()
@@ -155,7 +166,21 @@ class SaleOrder(models.Model):
# Create line if no line with product_id can be located
if not order_line:
values = self._website_product_id_change(self.id, product_id, qty=1)
values['name'] = self._get_line_description(self.id, product_id, attributes=attributes)
custom_values = kwargs.get('product_custom_variant_values')
if custom_values:
values['product_custom_variant_values'] = [(0, 0, {
'attribute_value_id': custom_value['attribute_value_id'],
'custom_value': custom_value['custom_value']
}) for custom_value in custom_values]
no_variant_attribute_values = kwargs.get('no_variant_attribute_values')
if no_variant_attribute_values:
values['product_no_variant_attribute_values'] = [
(6, 0, [int(attribute['value']) for attribute in no_variant_attribute_values])
]
values['name'] = self._get_line_description(self.id, product_id, no_variant_attribute_values=no_variant_attribute_values, custom_values=custom_values)
order_line = SaleOrderLineSudo.create(values)
try:
@@ -198,13 +223,34 @@ class SaleOrder(models.Model):
order_line.write(values)
return {'line_id': order_line.id, 'quantity': quantity}
# link a product to the sales order
if kwargs.get('linked_line_id'):
linked_line = SaleOrderLineSudo.browse(kwargs['linked_line_id'])
order_line.write({
'linked_line_id': linked_line.id,
'name': order_line.name + "\n" + _("Option for:") + ' ' + linked_line.product_id.display_name,
})
linked_line.write({"name": linked_line.name + "\n" + _("Option:") + ' ' + order_line.product_id.display_name})
option_lines = self.order_line.filtered(lambda l: l.linked_line_id.id == order_line.id)
for option_line_id in option_lines:
self._cart_update(option_line_id.product_id.id, option_line_id.id, add_qty, set_qty, **kwargs)
return {'line_id': order_line.id, 'quantity': quantity, 'option_ids': list(set(option_lines.ids))}
def _cart_accessories(self):
""" Suggest accessories based on 'Accessory Products' of products in cart """
for order in self:
accessory_products = order.website_order_line.mapped('product_id.accessory_product_ids').filtered(lambda product: product.website_published)
accessory_products -= order.website_order_line.mapped('product_id')
products = order.website_order_line.mapped('product_id')
accessory_products -= products
for product in products:
for product_template in accessory_products.mapped('product_tmpl_id'):
template_accessory_products = accessory_products.filtered(lambda accessory_product: accessory_product.product_tmpl_id == product_template)
# remove from the accessories the ones that are not available for the selected product
accessory_products -= template_accessory_products - product_template.get_filtered_variants(product)
return random.sample(accessory_products, len(accessory_products))
@api.multi
@@ -240,6 +286,9 @@ class SaleOrderLine(models.Model):
name_short = fields.Char(compute="_compute_name_short")
linked_line_id = fields.Many2one('sale.order.line', string='Linked Order Line', domain="[('order_id', '!=', order_id)]", ondelete='cascade')
option_line_ids = fields.One2many('sale.order.line', 'linked_line_id', string='Options Linked')
@api.multi
@api.depends('product_id.display_name')
def _compute_name_short(self):
@@ -10,7 +10,9 @@ access_product_style,product.style.public,website_sale.model_product_style,,1,0,
access_product_supplierinfo,product.supplierinfo.public,product.model_product_supplierinfo,,1,0,0,0
access_product_attribute_public,product.attribute public,product.model_product_attribute,,1,0,0,0
access_product_attribute_value_public,product.attribute value public,product.model_product_attribute_value,,1,0,0,0
access_product_attribute_price_public,product.attribute price public,product.model_product_attribute_price,,1,0,0,0
access_product_product_attribute,product.product.attribute value public,product.model_product_product_attribute_value,,1,0,0,0
access_product_product_attribute_custom_value,product.attribute.custom value,sale.model_product_attribute_custom_value,,1,0,0,0
access_product_attribute_filter_line,product.attribute filter line public,product.model_product_attribute_filter_line,,1,0,0,0
access_product_attribute_line_public,product.attribute line public,product.model_product_attribute_line,,1,0,0,0
access_fiscal_position_public,fiscal position public,account.model_account_fiscal_position,base.group_portal,1,0,0,0
access_payment_term,payment term public,account.model_account_payment_term,base.group_portal,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
10 access_product_supplierinfo product.supplierinfo.public product.model_product_supplierinfo 1 0 0 0
11 access_product_attribute_public product.attribute public product.model_product_attribute 1 0 0 0
12 access_product_attribute_value_public product.attribute value public product.model_product_attribute_value 1 0 0 0
13 access_product_attribute_price_public access_product_product_attribute product.attribute price public product.product.attribute value public product.model_product_attribute_price product.model_product_product_attribute_value 1 0 0 0
14 access_product_product_attribute_custom_value product.attribute.custom value sale.model_product_attribute_custom_value 1 0 0 0
15 access_product_attribute_filter_line product.attribute filter line public product.model_product_attribute_filter_line 1 0 0 0
16 access_product_attribute_line_public product.attribute line public product.model_product_attribute_line 1 0 0 0
17 access_fiscal_position_public fiscal position public account.model_account_fiscal_position base.group_portal 1 0 0 0
18 access_payment_term payment term public account.model_account_payment_term base.group_portal 1 0 0 0
+18 -164
View File
@@ -124,6 +124,7 @@ odoo.define('website_sale.website_sale', function (require) {
'use strict';
var utils = require('web.utils');
var ProductConfiguratorMixin = require('sale.ProductConfiguratorMixin');
var core = require('web.core');
var config = require('web.config');
var sAnimations = require('website.content.snippets.animation');
@@ -131,10 +132,10 @@ require("website.content.zoomodoo");
var _t = core._t;
sAnimations.registry.WebsiteSale = sAnimations.Class.extend({
sAnimations.registry.WebsiteSale = sAnimations.Class.extend(ProductConfiguratorMixin, {
selector: '.oe_website_sale',
read_events: {
'change input[name="add_qty"]': '_onChangeAddQuantity',
'change form .js_product:first input[name="add_qty"]': '_onChangeAddQuantity',
'mouseup .js_publish': '_onMouseupPublish',
'touchend .js_publish': '_onMouseupPublish',
'change .oe_cart input.js_quantity[data-product-id]': '_onChangeCartQuantity',
@@ -145,7 +146,6 @@ sAnimations.registry.WebsiteSale = sAnimations.Class.extend({
'mouseup form.js_add_cart_json label': '_onMouseupAddCartLabel',
'touchend form.js_add_cart_json label': '_onMouseupAddCartLabel',
'change .css_attribute_color input': '_onChangeColorAttribute',
'change input.js_variant_change, select.js_variant_change, input.js_product_change, [data-attribute_value_ids]': '_onChangeVariant',
'click .show_coupon': '_onClickShowCoupon',
'submit .o_website_sale_search': '_onSubmitSaleSearch',
'change select[name="country_id"]': '_onChangeCountry',
@@ -161,6 +161,8 @@ sAnimations.registry.WebsiteSale = sAnimations.Class.extend({
this._changeCartQuantity = _.debounce(this._changeCartQuantity.bind(this), 500);
this._changeCountry = _.debounce(this._changeCountry.bind(this), 500);
this.isWebsite = true;
},
/**
* @override
@@ -175,9 +177,7 @@ sAnimations.registry.WebsiteSale = sAnimations.Class.extend({
$('input.js_product_change', product).first().trigger('change');
});
_.each(this.$('.js_add_cart_variants'), function (add_variant) {
$('input.js_variant_change, select.js_variant_change', add_variant).first().trigger('change');
});
this.triggerVariantChange(this.$el);
this.$('select[name="country_id"]').change();
@@ -328,83 +328,24 @@ sAnimations.registry.WebsiteSale = sAnimations.Class.extend({
}
});
},
/**
* @private
*/
_priceToStr: function (price) {
var l10n = _t.database.parameters;
var precision = 2;
if ($(".decimal_precision").length) {
precision = parseInt($(".decimal_precision").last().data('precision'));
}
var formatted = _.str.sprintf('%.' + precision + 'f', price).split('.');
formatted[0] = utils.insert_thousand_seps(formatted[0]);
return formatted.join(l10n.decimal_point);
},
/**
* @private
*/
_updateProductImage: function ($productContainer, product_id) {
var $img;
if ($('#o-carousel-product').length) {
$img = $productContainer.find('img.js_variant_img');
$img.attr("src", "/web/image/product.product/" + product_id + "/image");
$img.parent().attr('data-oe-model', 'product.product').attr('data-oe-id', product_id)
.data('oe-model', 'product.product').data('oe-id', product_id);
var $thumbnail = $productContainer.find('img.js_variant_img_small');
if ($thumbnail.length !== 0) { // if only one, thumbnails are not displayed
$thumbnail.attr("src", "/web/image/product.product/" + product_id + "/image/90x90");
$('.carousel').carousel(0);
}
}
else {
$img = $productContainer.find('span[data-oe-model^="product."][data-oe-type="image"] img:first, img.product_detail_img');
$img.attr("src", "/web/image/product.product/" + product_id + "/image");
$img.parent().attr('data-oe-model', 'product.product').attr('data-oe-id', product_id)
.data('oe-model', 'product.product').data('oe-id', product_id);
}
// reset zooming constructs
$img.filter('[data-zoom-image]').attr('data-zoom-image', $img.attr('src'));
if ($img.data('zoomOdoo') !== undefined) {
$img.data('zoomOdoo').isReady = false;
}
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
* @param {MouseEvent} ev
*/
_onClickAddCartJSON: function (ev){
this.onClickAddCartJSON(ev);
},
/**
* @private
* @param {Event} ev
*/
_onChangeAddQuantity: function (ev) {
var productIDs = [];
var $addVariant = $(ev.currentTarget).closest(".js_product").find(".js_add_cart_variants");
var qty = $(ev.currentTarget).closest('form').find('input[name="add_qty"]').val();
var attributeValueIDs = $addVariant.data("attribute_value_ids");
_.each(attributeValueIDs, function (entry) {
productIDs.push(entry[0]);
});
if ($("#product_detail").length) {
// display the reduction from the pricelist in function of the quantity
this._rpc({
route: '/shop/get_unit_price',
params: {
product_ids: productIDs,
add_qty: parseInt(qty),
},
}).then(function (data) {
var current = attributeValueIDs;
for (var j = 0 ; j < current.length ; j++) {
current[j][2] = data[current[j][0]];
}
$addVariant.trigger('change');
});
}
this.onChangeAddQuantity(ev);
},
/**
* @private
@@ -444,27 +385,10 @@ sAnimations.registry.WebsiteSale = sAnimations.Class.extend({
* @private
* @param {Event} ev
*/
_onClickAddCartJSON: function (ev) { // hack to add and remove from cart with json
ev.preventDefault();
var $link = $(ev.currentTarget);
var $input = $link.closest('.input-group').find("input");
var product_id = +$input.closest('*:has(input[name="product_id"])').find('input[name="product_id"]').val();
var min = parseFloat($input.data("min") || 0);
var max = parseFloat($input.data("max") || Infinity);
var quantity = ($link.has(".fa-minus").length ? -1 : 1) + parseFloat($input.val() || 0, 10);
var new_qty = quantity > min ? (quantity < max ? quantity : max) : min;
// if they are more of one input for this product (eg: option modal)
$('input[name="'+$input.attr("name")+'"]').add($input).filter(function () {
var $prod = $(this).closest('*:has(input[name="product_id"])');
return !$prod.length || +$prod.find('input[name="product_id"]').val() === product_id;
}).val(new_qty).change();
return false;
},
/**
* @private
* @param {Event} ev
*/
_onClickSubmit: function (ev) {
_onClickSubmit: function (ev, forceSubmit) {
if ($(ev.currentTarget).is('#add_to_cart, #products_grid .a-submit') && !forceSubmit) {
return;
}
var $aSubmit = $(ev.currentTarget);
if (!ev.isDefaultPrevented() && !$aSubmit.is(".disabled")) {
ev.preventDefault();
@@ -517,76 +441,6 @@ sAnimations.registry.WebsiteSale = sAnimations.Class.extend({
.filter(':has(input:checked)')
.addClass("active");
},
/**
* @private
* @param {Event} ev
*/
_onChangeVariant: function (ev) {
var $parent = $(ev.target).closest('.js_product');
var $ul = $parent.find('.js_add_cart_variants');
var $product_id = $parent.find('.product_id').first();
var $price = $parent.find(".oe_price:first .oe_currency_value");
var $default_price = $parent.find(".oe_default_price:first .oe_currency_value");
var $optional_price = $parent.find(".oe_optional:first .oe_currency_value");
var variant_ids = $ul.data("attribute_value_ids");
var values = [];
var unchanged_values = $parent.find('div.oe_unchanged_value_ids').data('unchanged_value_ids') || [];
_.each($parent.find('input.js_variant_change:checked, select.js_variant_change'), function (el) {
values.push(+$(el).val());
});
values = values.concat(unchanged_values);
var list_variant_id = parseInt($parent.find('input.js_product_change:checked').val());
$parent.find("label").removeClass("text-muted css_not_available");
var product_id = false;
for (var k in variant_ids) {
if (_.isEmpty(_.difference(variant_ids[k][1], values)) || variant_ids[k][0] === list_variant_id) {
$price.html(this._priceToStr(variant_ids[k][2]));
$default_price.html(this._priceToStr(variant_ids[k][3]));
if (variant_ids[k][3]-variant_ids[k][2]>0.01) {
$default_price.closest('.oe_website_sale').addClass("discount");
$optional_price.closest('.oe_optional').show().css('text-decoration', 'line-through');
$default_price.parent().removeClass('d-none');
} else {
$optional_price.closest('.oe_optional').hide();
$default_price.parent().addClass('d-none');
}
product_id = variant_ids[k][0];
this._updateProductImage($(ev.currentTarget).closest('tr.js_product, .oe_website_sale'), product_id);
break;
}
}
_.each($parent.find("input.js_variant_change:radio, select.js_variant_change"), function (elem) {
var $input = $(elem);
var id = +$input.val();
var values = [id];
_.each($parent.find("ul:not(:has(input.js_variant_change[value='" + id + "'])) input.js_variant_change:checked, select.js_variant_change"), function (elem) {
values.push(+$(elem).val());
});
for (var k in variant_ids) {
if (!_.difference(values, variant_ids[k][1]).length) {
return;
}
}
$input.closest("label").addClass("css_not_available");
$input.find("option[value='" + id + "']").addClass("css_not_available");
});
if (product_id) {
$parent.removeClass("css_not_available");
$product_id.val(product_id);
$parent.find("#add_to_cart").removeClass("disabled");
} else {
$parent.addClass("css_not_available");
$product_id.val(0);
$parent.find("#add_to_cart").addClass("disabled");
}
},
/**
* @private
* @param {Event} ev
@@ -0,0 +1,199 @@
odoo.define('website_sale_options.website_sale', function (require) {
'use strict';
var core = require('web.core');
var _t = core._t;
var OptionalProductsModal = require('sale.OptionalProductsModal');
var weContext = require('web_editor.context');
var sAnimations = require('website.content.snippets.animation');
var ProductConfiguratorMixin = require('sale.ProductConfiguratorMixin');
require('website_sale.website_sale');
sAnimations.registry.WebsiteSaleOptions = sAnimations.Class.extend(ProductConfiguratorMixin, {
selector: '.oe_website_sale',
read_events: {
'click #add_to_cart, #products_grid .product_price .a-submit': '_onClickAdd',
},
/**
* @constructor
*/
init: function () {
this._super.apply(this, arguments);
this._handleAdd = _.debounce(this._handleAdd.bind(this), 200, true);
this.isWebsite = true;
delete this.events['change [data-attribute_exclusions]'];
delete this.events['change input.js_quantity'];
delete this.events['click button.js_add_cart_json'];
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Prevents the default submit to allow showing product options
* in a modal (if any)
*
* @private
* @param {MouseEvent} ev
*/
_onClickAdd: function (ev) {
ev.preventDefault();
this._handleAdd($(ev.currentTarget).closest('form'));
},
/**
* Initializes the optional products modal
* and add handlers to the modal events (confirm, back, ...)
*
* @private
* @param {$.Element} $form the related webshop form
*/
_handleAdd: function ($form) {
var self = this;
this.$form = $form;
this.isWebsite = true;
var productSelector = [
'input[type="hidden"][name="product_id"]',
'input[type="radio"][name="product_id"]:checked'
];
var productId = parseInt($form.find(productSelector.join(', ')).first().val(), 10);
var productReady = $.Deferred();
if (productId){
productReady.resolve(productId);
} else {
productReady = self._rpc({
model: 'product.template',
method: 'create_product_variant',
args: [
$form.find('.product_template_id').val(),
JSON.stringify(self.getSelectedVariantValues($form))
],
});
}
productReady.done(function (productId){
$form.find(productSelector.join(', ')).val(productId);
self.rootProduct = {
product_id: productId,
quantity: parseFloat($form.find('input[name="add_qty"]').val() || 1),
product_custom_variant_values: self.getCustomVariantValues($form.find('.js_product')),
variant_values: self.getSelectedVariantValues($form.find('.js_product')),
no_variant_attribute_values: self.getNoVariantAttributeValues($form.find('.js_product'))
};
self.optionalProductsModal = new OptionalProductsModal($form, {
rootProduct: self.rootProduct,
isWebsite: true,
okButtonText: _t('Proceed to Checkout'),
cancelButtonText: _t('Continue Shopping'),
title: _t('Add to cart')
}).open();
self.optionalProductsModal.on('options_empty', null, self._onModalOptionsEmpty.bind(self));
self.optionalProductsModal.on('update_quantity', null, self._onOptionsUpdateQuantity.bind(self));
self.optionalProductsModal.on('confirm', null, self._onModalConfirm.bind(self));
self.optionalProductsModal.on('back', null, self._onModalBack.bind(self));
});
},
/**
* No optional products found for this product
* Add custom variant values and attribute values that do not generate variants
* in the form data and trigger submit
*
* @private
*/
_onModalOptionsEmpty: function () {
var $productCustomVariantValues = $('<input>', {
name: 'product_custom_variant_values',
type: "hidden",
value: JSON.stringify(this.rootProduct.product_custom_variant_values)
});
this.$form.append($productCustomVariantValues);
var $productNoVariantAttributeValues = $('<input>', {
name: 'no_variant_attribute_values',
type: "hidden",
value: JSON.stringify(this.rootProduct.no_variant_attribute_values)
});
this.$form.append($productNoVariantAttributeValues);
this.$form.trigger('submit', [true]);
},
/**
* Update web shop base form quantity
* when quantity is updated in the optional products window
*
* @private
* @param {integer} quantity
*/
_onOptionsUpdateQuantity: function (quantity) {
this.$form
.find('input[name="add_qty"]')
.first()
.val(quantity)
.trigger('change');
},
/**
* Submit form and stay on the same page
*
* @private
*/
_onModalBack: function () {
this._onModalSubmit(false);
},
/**
* Submit form and go to shop
*
* @private
*/
_onModalConfirm: function () {
this._onModalSubmit(true);
},
/**
* Submits the form with additional parameters
* - lang
* - product_custom_variant_values: The products custom variant values
*
* @private
* @param {Boolean} goToShop Triggers a page refresh to the url "shop/cart"
*/
_onModalSubmit: function (goToShop){
var customValues = JSON.stringify(
this.optionalProductsModal.getSelectedProducts()
);
this.$form.ajaxSubmit({
url: '/shop/cart/update_option',
data: {
lang: weContext.get().lang,
custom_values: customValues
},
success: function (quantity) {
if (goToShop) {
var path = window.location.pathname.replace(/shop([\/?].*)?$/, "shop/cart");
window.location.pathname = path;
}
var $quantity = $(".my_cart_quantity");
$quantity.parent().parent().removeClass("d-none", !quantity);
$quantity.html(quantity).hide().fadeIn(600);
}
});
},
});
return sAnimations.registry.WebsiteSaleOptions;
});
@@ -38,6 +38,10 @@ tour.register('shop_buy_product', {
extra_trigger: 'label:contains(Steel) input:propChecked',
trigger: '#product_detail form[action^="/shop/cart/update"] .btn-primary',
},
{
content: "click in modal on 'Proceed to checkout' button",
trigger: 'button:contains("Proceed to Checkout")',
},
{
content: "add suggested",
extra_trigger: '#wrap:not(:has(#cart_products:contains("[A8767] Storage Box")))',
@@ -0,0 +1,63 @@
odoo.define("website_sale.tour_shop", function (require) {
"use strict";
var tour = require("web_tour.tour");
var base = require("web_editor.base");
tour.register("shop_custom_attribute_value", {
url: "/shop",
test: true,
wait_for: base.ready()
}, [{
trigger: 'img[src*="/product.template/9"]',
run: 'click'
}, {
trigger: 'li.js_attribute_value span:contains(Custom)',
extra_trigger: 'li.js_attribute_value',
run: 'click',
}, {
trigger: 'input.variant_custom_value',
run: 'text Wood',
}, {
trigger: 'a:contains(Add to Cart)',
run: 'click',
}, {
trigger: 'div:contains(Custom: Wood)',
extra_trigger: '.js_product.in_cart.main_product',
run: function (){} // checks that Yep, it's wood!
}, {
trigger: 'button.js_add_cart_json:has(i.fa-plus)',
run: 'click',
}, {
trigger: 'div.oe_striked_price span:contains(750)',
run: function (){}, // check
}, {
trigger: 'span.oe_price span:contains(600)',
run: function (){}, // check
}, {
trigger: 'li.js_attribute_value span:contains(Aluminium)',
extra_trigger: '.oe_optional_products_modal',
run: 'click'
}, {
trigger: '.oe_price span:contains(22.9)',
run: function (){}, // check
}, {
trigger: '.oe_optional_products_modal .js_product:has(strong:contains(Conference Chair)) .js_add',
extra_trigger: '.oe_optional_products_modal .js_product:has(strong:contains(Conference Chair))',
run: 'click'
}, {
trigger: '.oe_optional_products_modal .js_product:has(strong:contains(Chair floor protection)) .js_add',
extra_trigger: '.oe_optional_products_modal .js_product:has(strong:contains(Chair floor protection))',
run: 'click'
}, {
trigger: 'span:contains(1,269.80)',
run: function (){}, // check
}, {
trigger: 'button:has(span:contains(Proceed to Checkout))',
run: 'click',
}, {
trigger: 'span:contains(Custom: Wood)',
extra_trigger: '#cart_products',
run: function (){}, // check
}]);
});
@@ -0,0 +1,216 @@
odoo.define('website_sale.tour_shop_customize', function (require) {
'use strict';
var tour = require("web_tour.tour");
var base = require("web_editor.base");
tour.register('shop_customize', {
test: true,
url: '/shop',
wait_for: base.ready()
},
[
{
content: "open customize menu",
trigger: '#customize-menu > a',
},
{
content: "click on 'Product Attribute's Filters'",
trigger: "#customize-menu a:contains(Product Attribute's Filters)",
},
{
content: "select product attribute memory Steel",
extra_trigger: 'body:not(:has(#customize-menu:visible .dropdown-menu:visible))',
trigger: 'form.js_attributes label:contains(Steel) input:not(:checked)',
},
{
content: "check the selection",
trigger: 'form.js_attributes label:contains(Steel) input:checked',
run: function () {}, // it's a check
},
{
content: "select product",
extra_trigger: 'body:not(:has(.oe_website_sale .oe_product_cart:eq(2)))',
trigger: '.oe_product_cart a:contains("Customizable Desk")',
},
{
content: "open customize menu",
trigger: '#customize-menu',
extra_trigger: "#product_detail",
run: function() {
// ENABLE VARIANT GROUP
$('body').addClass('notReady');
var ajax = odoo.__DEBUG__.services['web.ajax'];
var get_group_payload = {
model: 'ir.model.data',
method: 'xmlid_to_res_id',
args: ['product.group_product_variant', false],
kwargs: {}
};
ajax.jsonpRpc('/web/dataset/call_kw', 'call', get_group_payload).then(function(group_id) {
ajax.jsonpRpc('/web/dataset/call_kw', 'call', {
model: 'res.groups',
method: 'write',
args: [group_id, {'users': [[4, 1]]}],
kwargs: {}
}).then(function() {
location.reload(true);
});
});
},
},
{
content: "open customize menu",
trigger: '#customize-menu > a',
extra_trigger: 'body:not(.notReady)',
},
{
content: "check page loaded after enable variant group",
trigger: '#customize-menu a:contains(List View of Variants)',
run: function () {}, // it's a check
},
{
content: "click on 'List View of Variants'",
trigger: "#customize-menu a:contains(List View of Variants)",
},
{
content: "check page loaded after list of variant customization enabled",
trigger: '.js_product_change',
run: function () {}, // it's a check
},
{
content: "check price is 750 and set quantity to 2",
trigger: ".js_product:first input.quantity:propValue(1)",
extra_trigger: ".product_price .oe_price .oe_currency_value:containsExact(750.00)",
run: "text 2",
},
{
content: "verify pricelist based on quantity has effect",
trigger: ".product_price .oe_price .oe_currency_value:containsExact(600.00)",
run: function () {}, // it's a check
},
{
content: "check pricelit has been applied and switch to Aluminium variant",
trigger: ".js_product label:contains('Aluminium')",
},
{
content: "verify that price has changed when changing variant",
trigger: ".product_price .oe_price .oe_currency_value:not(:containsExact(600.00))",
run: function () {}, // it's a check
},
{
content: "open customize menu",
trigger: '#customize-menu > a',
},
{
content: "remove 'List View of Variants'",
trigger: "#customize-menu a:contains(List View of Variants):has(input:checked)",
},
{
content: "check page loaded after list of variant customization disabled",
trigger: ".js_product:not(:has(.js_product_change))",
run: function () {}, // it's a check
},
{
content: "check price is 750 and set quantity to 2",
trigger: ".js_product:first input.quantity:propValue(1)",
extra_trigger: ".product_price .oe_price .oe_currency_value:containsExact(750.00)",
run: "text 2",
},
{
content: "verify pricelist based on quantity has effect",
trigger: ".product_price .oe_price .oe_currency_value:containsExact(600.00)",
run: function () {}, // it's a check
},
{
content: "switch to Aluminium variant",
trigger: ".js_product label:contains('Aluminium')",
},
{
content: "verify that price has changed when changing variant",
trigger: ".product_price .oe_price .oe_currency_value:not(:containsExact(600.00))",
run: function () {}, // it's a check
},
{
content: "switch back to Steel variant",
trigger: ".js_product label:contains('Steel')",
},
{
content: "click on 'Add to Cart' button",
trigger: "a:contains(Add to Cart)",
},
{
content: "price is lowered by pricelist and not multiplied by quantity",
trigger: "#product_confirmation .oe_price .oe_currency_value:containsExact(600.00)",
extra_trigger: "#product_confirmation input.quantity:propValue(2)",
run: function () {}, // it's a check
},
{
content: "set quantity to 1",
trigger: "#product_confirmation .js_add_cart_json .fa-minus",
},
{
content: "check that product page has been updated",
trigger: ".js_product:first input.quantity:propValue(1)",
extra_trigger: ".product_price .oe_price .oe_currency_value:containsExact(750.00)",
run: function () {}, // it's a check
},
{
content: "check that add to cart modal has been updated",
trigger: "#product_confirmation .oe_price .oe_currency_value:containsExact(750.00)",
extra_trigger: "#product_confirmation input.quantity:propValue(1)",
run: function () {}, // it's a check
},
{
content: "add an optional Warranty",
trigger: ".js_product:contains(Warranty) a:contains(Add to Cart)",
},
{
content: "click in modal on 'Proceed to checkout' button",
extra_trigger: 'body:has(.js_product:contains(Warranty) a:contains(Add to Cart):hidden)',
trigger: '.modal-footer a:contains("Proceed to Checkout")',
},
{
content: "check quantity",
trigger: '.my_cart_quantity:containsExact(2),.o_extra_menu_items .fa-plus',
run: function () {}, // it's a check
},
{
content: "check optional product",
trigger: '.optional_product',
run: function () {}, // it's a check
},
{
content: "remove large cabinet from cart",
trigger: '#cart_products a.js_add_cart_json:first',
},
{
content: "check optional product is removed",
trigger: '#wrap:not(:has(.optional_product))',
run: function () {}, // it's a check
},
{
content: "click on shop",
trigger: "a:contains(Continue Shopping)",
extra_trigger: 'body:not(:has(#products_grid_before .js_attributes))',
},
{
content: "open customize menu bis",
extra_trigger: '#products_grid_before .js_attributes',
trigger: '#customize-menu > a',
},
{
content: "remove 'Product Attribute's Filters'",
trigger: "#customize-menu a:contains(Product Attribute's Filters):has(input:checked)",
},
{
content: "finish",
extra_trigger: 'body:not(:has(#products_grid_before .js_attributes))',
trigger: '#wrap:not(:has(li:has(.my_cart_quantity):visible))',
run: function () {}, // it's a check
},
]
);
});
@@ -29,66 +29,10 @@
margin-left: 1.5em;
}
.td-product_name,
h1[itemprop="name"],
.oe_product_cart form h5 {
word-wrap: break-word;
}
.td-product_name {
min-width: 140px;
}
.td-img {
width: 100px;
}
.td-qty {
width: 130px;
a.input-group-addon {
background-color: transparent;
border: 0px;
}
}
.td-action {
width: 30px;
}
.td-price,
.td-price-total {
width: 100px;
}
@include media-breakpoint-down(sm) {
.td-img,
.td-price-total {
display: none;
}
.td-qty {
width: 60px;
}
.td-price {
width: 80px;
}
}
@media (max-width: 476px) {
.td-qty {
width: 60px;
}
#modal_optional_products table thead,
.oe_cart table thead {
display: none;
}
#modal_optional_products table td.td-img,
.oe_cart table td.td-img {
display: none;
}
}
}
/* ---- Default Styles ---- */
@@ -483,67 +427,6 @@ div#payment_method {
}
/* product detail */
.css_attribute_color {
display: inline-block;
border: 1px solid #999999;
text-align: center;
input {
margin: 8px;
height: 13px;
opacity: 0;
}
&.active {
border: 3px ridge #66ee66;
}
&.active input {
margin: 6px;
}
}
.css_not_available_msg {
display: none;
}
.css_not_available.js_product {
> *:nth-child(4) > * {
display: none;
}
.css_quantity,
.product_price {
display: none;
}
.css_not_available_msg {
display: block;
}
}
.css_quantity {
max-width: 125px;
input[name="add_qty"] {
text-align: center;
}
}
option.css_not_available {
color: #ccc;
}
label.css_not_available {
opacity: 0.6;
}
label.css_attribute_color.css_not_available {
opacity: 1;
background-image: url("/website_sale/static/src/img/redcross.png");
background-size: cover;
}
.product_detail_img {
margin-left: auto;
margin-right: auto;
@@ -551,14 +434,6 @@ label.css_attribute_color.css_not_available {
max-height: 500px;
}
.oe_default_price {
display: none;
}
.discount .oe_default_price {
display: block;
}
.oe_website_sale input.js_quantity {
min-width: 48px;
text-align: center;
@@ -16,9 +16,4 @@ div#modal_optional_products table tr td {
div#modal_optional_products .td-qty {
display: none;
}
}
.oe_striked_price {
text-decoration: line-through;
white-space: nowrap;
}
}
@@ -6,8 +6,4 @@ import odoo.tests
class TestUi(odoo.tests.HttpCase):
def test_01_admin_shop_customize_tour(self):
self.env.ref('product.group_product_variant').write({
'users': [(4, self.env.ref('base.user_admin').id)]
})
self.phantom_js("/", "odoo.__DEBUG__.services['web_tour.tour'].run('shop_customize')", "odoo.__DEBUG__.services['web_tour.tour'].tours.shop_customize.ready", login="admin")
@@ -28,35 +28,6 @@
</field>
</record>
<record id="product_attribute_line_form" model="ir.ui.view">
<field name="name">product.attribute.line.form.inherit</field>
<field name="model">product.attribute.line</field>
<field name="inherit_id" ref="product.product_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="//group[@name='main_fields']" position="after">
<group>
<field name="type"/>
</group>
</xpath>
<xpath expr="//field[@name='value_ids']/tree/field[@name='name']" position="after">
<field name="html_color"/>
</xpath>
</field>
</record>
<record model="ir.ui.view" id="product_pricelist_view">
<field name="name">product.pricelist.website.form</field>
<field name="model">product.pricelist</field>
@@ -154,39 +125,6 @@
</field>
</record>
<!-- Product attribute -->
<record id="product_attribute_value_view_tree_inherit_website_sale" model="ir.ui.view">
<field name="name">product.attribute.value.view.tree.inherit.website.sale</field>
<field name="model">product.attribute.value</field>
<field name="inherit_id" ref="product.product_attribute_value_view_tree"/>
<field name="arch" type="xml">
<field name="name" position="after">
<field name="html_color"/>
</field>
</field>
</record>
<record id="variants_tree_view" model="ir.ui.view">
<field name="name">variants.template.tree</field>
<field name="model">product.attribute.value</field>
<field name="inherit_id" ref="product.variants_tree_view"/>
<field name="arch" type="xml">
<field name="name" position="after">
<field name="html_color"/>
</field>
</field>
</record>
<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="type"/>
</field>
</field>
</record>
<!-- Product Public Categories -->
<record id="product_public_category_form_view" model="ir.ui.view">
<field name="name">product.public.category.form</field>
@@ -59,17 +59,6 @@
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box" id="sale_option_setting" title="Select optional products from the configuration form of your main products (in Sales tab).">
<div class="o_setting_left_pane">
<field name="module_website_sale_options"/>
</div>
<div class="o_setting_right_pane">
<label for="module_website_sale_options"/>
<div class="text-muted">
Suggest optional products when adding to cart
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box" id="product_availability_setting">
<div class="o_setting_left_pane">
<field name="module_website_sale_stock"/>
+25 -76
View File
@@ -14,14 +14,22 @@
<link rel="stylesheet" type="text/scss" href="/website_sale/static/src/scss/website_sale.scss" />
<link rel="stylesheet" type="text/scss" href="/website_sale/static/src/scss/website_mail.scss" />
<link rel="stylesheet" type="text/scss" href="/website_sale/static/src/scss/website_sale_frontend.scss"/>
<link rel="stylesheet" type="text/scss" href="/sale/static/src/scss/sale_portal.scss"/>
<link rel="stylesheet" type="text/scss" href="/sale/static/src/scss/product_configurator.scss"/>
<link rel="stylesheet" type="text/scss" href="/website_sale/static/src/scss/website_sale_options.scss"/>
</xpath>
<xpath expr="script[last()]" position="after">
<script type="text/javascript" src="/sale/static/src/js/product_configurator_mixin.js"></script>
<script type="text/javascript" src="/sale/static/src/js/product_configurator_modal.js"></script>
<script type="text/javascript" src="/website_sale/static/src/js/website_sale.js"></script>
<script type="text/javascript" src="/website_sale/static/src/js/website_sale_utils.js"></script>
<script type="text/javascript" src="/website_sale/static/src/js/website_sale_payment.js"></script>
<script type="text/javascript" src="/website_sale/static/src/js/website_sale_validate.js"></script>
<script type="text/javascript" src="/website_sale/static/src/js/website_sale_tour_buy.js"></script>
<script type="text/javascript" src="/website_sale/static/src/js/website_sale_tracking.js"></script>
<script type="text/javascript" src="/website_sale/static/src/js/website_sale_tour_shop_customize.js"></script>
<script type="text/javascript" src="/website_sale/static/src/js/website_sale_tour_shop_custom_attribute_value.js"></script>
<script type="text/javascript" src="/website_sale/static/src/js/website_sale_options.js"></script>
</xpath>
</template>
@@ -149,6 +157,12 @@
</a>
</t>
</xpath>
<xpath expr="//div[hasclass('product_price')]/t[1]" position="attributes">
<attribute name="t-if">len(product.product_variant_ids) == 1 and not len(product.optional_product_ids) &gt;= 1</attribute>
</xpath>
<xpath expr="//div[hasclass('product_price')]/t[2]" position="attributes">
<attribute name="t-if">len(product.product_variant_ids) &gt; 1 or len(product.optional_product_ids) &gt;= 1</attribute>
</xpath>
</template>
<template id="pricelist_list" name="Pricelists Dropdown">
@@ -470,10 +484,11 @@
<span itemprop="url" style="display:none;" t-esc="'%sshop/product/%s' % (request.httprequest.url_root, slug(product))"/>
<form action="/shop/cart/update" method="POST">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()" />
<div class="js_product" t-if="product.product_variant_ids">
<div class="js_product js_main_product" t-if="product.product_variant_ids">
<t t-placeholder="select">
<input type="hidden" class="product_id" name="product_id" t-att-value="product.product_variant_id.id if len(product.product_variant_ids) == 1 else '0'" />
<t t-call="website_sale.variants">
<input type="hidden" class="product_template_id" name="product_template_id" t-att-value="product.id" />
<t t-call="sale.variants">
<t t-set="ul_class" t-value="'flex-column'" />
</t>
</t>
@@ -602,14 +617,14 @@
"display_currency": product.currency_id,
}'/>
</h4>
<h4 class="d-none oe_not_available bg-warning">Product not available</h4>
<h4 class="d-none oe_not_available bg-warning">Option not available</h4>
</div>
</template>
<template id="product_variants" inherit_id="website_sale.product" active="False" customize_show="True" name="List View of Variants">
<xpath expr="//t[@t-placeholder='select']" position="replace">
<t t-set="attribute_value_ids" t-value="get_attribute_value_ids(product)"/>
<ul class="d-none js_add_cart_variants" t-att-data-attribute_value_ids="json.dumps(attribute_value_ids)"/>
<t t-set="attribute_exclusions" t-value="get_attribute_exclusions(product, reference_product)"/>
<ul class="d-none js_add_cart_variants" t-att-data-attribute_exclusions="json.dumps(attribute_exclusions)"/>
<input type="hidden" t-if="len(product.product_variant_ids) == 1" class="product_id" name="product_id" t-att-value="product.product_variant_id.id"/>
<t t-if="len(product.product_variant_ids) &gt; 1">
<div t-foreach="product.product_variant_ids" t-as="variant_id" class="custom-control custom-radio">
@@ -626,75 +641,6 @@
</xpath>
</template>
<template id="variants">
<t t-set="attribute_value_ids" t-value="get_attribute_value_ids(product)"/>
<ul t-attf-class="list-unstyled js_add_cart_variants #{ul_class}" t-att-data-attribute_value_ids="json.dumps(attribute_value_ids)">
<t t-foreach="product.attribute_line_ids.sorted(key=lambda x: x.attribute_id.sequence)" t-as="variant_id">
<li t-if="len(variant_id.value_ids) > 1">
<strong t-field="variant_id.attribute_id.name"/>
<t t-if="variant_id.attribute_id.type == 'select'">
<select t-attf-class="form-control #{'js_variant_change' if variant_id.attribute_id.create_variant else ''}" t-att-name="'attribute-%s-%s' % (product.id, variant_id.attribute_id.id)">
<t t-foreach="variant_id.value_ids" t-as="value_id">
<option t-att-value="value_id.id">
<span t-field="value_id.name"/>
<span t-if="value_id.price_extra">
<t t-esc="value_id.price_extra > 0 and '+' or ''"/><span t-field="value_id.price_extra" style="white-space: nowrap;" t-options='{
"widget": "monetary",
"from_currency": product.currency_id,
"display_currency": website.currency_id
}'/>
</span>
</option>
</t>
</select>
</t>
<t t-if="variant_id.attribute_id.type == 'radio'">
<ul class="list-unstyled">
<t t-set="inc" t-value="0"/>
<t t-foreach="variant_id.value_ids" t-as="value_id">
<li class="form-group js_attribute_value" style="margin: 0;">
<label class="col-form-label" style="margin: 0 20px;">
<input type="radio" t-att-class="'js_variant_change' if variant_id.attribute_id.create_variant else None" t-att-checked="'checked' if not inc else None" t-att-name="'attribute-%s-%s' % (product.id, variant_id.attribute_id.id)" t-att-value="value_id.id" style="vertical-align: top; margin-right: 10px;"/>
<span t-field="value_id.name"/>
<span class="badge badge-pill" t-if="value_id.price_extra">
<t t-esc="value_id.price_extra > 0 and '+' or ''"/><span t-field="value_id.price_extra" style="white-space: nowrap;" t-options='{
"widget": "monetary",
"from_currency": product.currency_id,
"display_currency": website.currency_id
}'/>
</span>
</label>
</li>
<t t-set="inc" t-value="inc+1"/>
</t>
</ul>
</t>
<t t-if="variant_id.attribute_id.type == 'color'">
<ul class="list-inline">
<t t-set="inc" t-value="0"/>
<li t-foreach="variant_id.value_ids" t-as="value_id" class="list-inline-item">
<label t-attf-style="background-color:#{value_id.html_color or value_id.name}"
t-attf-class="css_attribute_color #{'active' if not inc else ''}">
<input type="radio" t-att-class="'js_variant_change' if variant_id.attribute_id.create_variant else None"
t-att-checked="'checked' if not inc else None"
t-att-name="'attribute-%s-%s' % (product.id, variant_id.attribute_id.id)"
t-att-value="value_id.id"
t-att-title="value_id.name"/>
</label>
<t t-set="inc" t-value="inc+1"/>
</li>
</ul>
</t>
</li>
</t>
</ul>
</template>
<template id="wizard_checkout" name="Wizard Checkout">
<t t-set="website_sale_order" t-value="website.sale_get_order()"/>
@@ -840,7 +786,7 @@
</thead>
<tbody>
<t t-foreach="website_sale_order.website_order_line" t-as="line">
<tr>
<tr t-att-class="'optional_product info' if line.linked_line_id else None">
<td colspan="2" t-if="not line.product_id.product_tmpl_id" class='td-img'></td>
<td align="center" t-if="line.product_id.product_tmpl_id" class='td-img'>
<span t-field="line.product_id.image_small" t-options="{'widget': 'image', 'class': 'rounded'}" />
@@ -856,7 +802,7 @@
</t>
</td>
<td class="text-center td-qty">
<div class="css_quantity input-group oe_website_spinner">
<div t-if="not line.linked_line_id" class="css_quantity input-group oe_website_spinner">
<div class="input-group-prepend">
<a t-attf-href="#" class="btn btn-link js_add_cart_json d-none d-md-inline-block" aria-label="Remove one" title="Remove one">
<i class="fa fa-minus"></i>
@@ -869,6 +815,9 @@
</a>
</div>
</div>
<t t-if="line.linked_line_id">
<span class="js_quantity text-muted" t-att-data-line-id="line.id" t-att-data-product-id="line.product_id.id" t-esc="int(line.product_uom_qty)"/>
</t>
</td>
<td class="text-center td-price" name="price">
<t t-if="(compute_currency(line.product_id.lst_price) - line.price_reduce ) &gt; 0.01 and website.get_current_pricelist().discount_policy=='without_discount'">
@@ -64,4 +64,8 @@
<field name="attribute_id" ref="product_attribute_8"/>
<field name="value_ids" eval="[(6,0,[ref('product_attribute_value_8')])]"/>
</record>
<function model="product.template" name="create_variant_ids">
<value model="product.template" eval="[ref('product.product_product_6_product_template')]" />
</function>
</odoo>
@@ -38,7 +38,7 @@
<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="website_sale.product_attribute_view_form"/>
<field name="inherit_id" ref="product.product_attribute_view_form"/>
<field name="priority" eval="8"/>
<field name="arch" type="xml">
<field name='name' position='after'>
-5
View File
@@ -1,5 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import controllers
from . import models
@@ -1,23 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Optional Products',
'category': 'Website',
'summary': 'Suggest optional products when adding to cart',
'version': '1.0',
'description': """
Suggest optional products when shoppers add products to their cart (e.g. for computers: warranty, OS software, extra components).
Optional products are defined in the product form.
""",
'depends': ['website_sale'],
'data': [
'views/product_views.xml',
'views/website_sale_options_templates.xml',
],
'demo': [
'data/product_demo.xml',
],
'qweb': ['static/src/xml/*.xml'],
'installable': True,
}
@@ -1,4 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import main
@@ -1,85 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, http
from odoo.http import request
from odoo.addons.website_sale.controllers.main import WebsiteSale
class WebsiteSaleOptions(WebsiteSale):
@http.route(['/shop/product/<model("product.template"):product>'], type='http', auth="public", website=True)
def product(self, product, category='', search='', **kwargs):
r = super(WebsiteSaleOptions, self).product(product, category, search, **kwargs)
r.qcontext['optional_product_ids'] = [p.with_context({'active_id': p.id}) for p in product.optional_product_ids]
return r
@http.route(['/shop/cart/update_option'], type='http', auth="public", methods=['POST'], website=True, multilang=False)
def cart_options_update_json(self, product_id, add_qty=1, set_qty=0, goto_shop=None, lang=None, **kw):
if lang:
request.website = request.website.with_context(lang=lang)
order = request.website.sale_get_order(force_create=1)
product = request.env['product.product'].browse(int(product_id))
option_ids = product.optional_product_ids.mapped('product_variant_ids').ids
optional_product_ids = []
for k, v in kw.items():
if "optional-product-" in k and int(kw.get(k.replace("product", "add"))) and int(v) in option_ids:
optional_product_ids.append(int(v))
attributes = self._filter_attributes(**kw)
value = {}
if add_qty or set_qty:
value = order._cart_update(
product_id=int(product_id),
add_qty=add_qty,
set_qty=set_qty,
attributes=attributes,
optional_product_ids=optional_product_ids
)
# options have all time the same quantity
for option_id in optional_product_ids:
order._cart_update(
product_id=option_id,
set_qty=value.get('quantity'),
attributes=attributes,
linked_line_id=value.get('line_id')
)
return str(order.cart_quantity)
@http.route(['/shop/modal'], type='json', auth="public", methods=['POST'], website=True)
def modal(self, product_id, **kw):
pricelist = request.website.get_current_pricelist()
product_context = dict(request.context)
quantity = kw['kwargs']['context']['quantity']
if not product_context.get('pricelist'):
product_context['pricelist'] = pricelist.id
# fetch quantity from custom context
product_context.update(kw.get('kwargs', {}).get('context', {}))
from_currency = request.env.user.company_id.currency_id
to_currency = pricelist.currency_id
company = request.env['res.company'].browse(request.env.context.get('company_id')) or request.env['res.users']._get_company()
date = request.env.context.get('date') or fields.Date.today()
compute_currency = lambda price: from_currency._convert(
price, to_currency, company, date)
product = request.env['product.product'].with_context(product_context).browse(int(product_id))
main_product_attr_ids = self.get_attribute_value_ids(product)
for variant in main_product_attr_ids:
if variant[0] == product.id:
# We indeed need a list of lists (even with only 1 element)
main_product_attr_ids = [variant]
break
return request.env['ir.ui.view'].render_template("website_sale_options.modal", {
'product': product,
'quantity': quantity,
'compute_currency': compute_currency,
'get_attribute_value_ids': self.get_attribute_value_ids,
'main_product_attr_ids': main_product_attr_ids,
})
@@ -1,38 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="product_product_1" model="product.product">
<field name="name">Warranty</field>
<field name="default_code">SERV_125889</field>
<field name="list_price">20.0</field>
<field name="website_sequence">100</field>
<field name="website_published" eval="True"/>
<field name="type">service</field>
<field name="uom_id" ref="uom.product_uom_unit"/>
<field name="uom_po_id" ref="uom.product_uom_unit"/>
<field name="description_sale">Warranty, issued to the purchaser of an article by its manufacturer, promising to repair or replace it if necessary within a specified period of time.</field>
<field name="attribute_value_ids" eval="[(6,0,[ref('product.product_attribute_value_5')])]"/>
<field name="categ_id" ref="product.product_category_3"/>
</record>
<record id="product_attribute_line_1" model="product.attribute.line">
<field name="product_tmpl_id" ref="product_product_1_product_template"/>
<field name="attribute_id" ref="product.product_attribute_3"/>
<field name="value_ids" eval="[(6,0,[ref('product.product_attribute_value_5'), ref('product.product_attribute_value_6')])]"/>
</record>
<record id="product_product_1_product_template" model="product.template">
<field name="attribute_line_ids" eval="[(6,0,[ref('product_attribute_line_1')])]"/>
<field name="public_categ_ids" eval="[(6,0,[ref('website_sale.services')])]"/>
</record>
<record id="product.product_product_4_product_template" model="product.template">
<field name="optional_product_ids" eval="[(6,0,[ref('product_product_1_product_template')])]"/>
</record>
<record id="product_attribute_price_1" model="product.attribute.price">
<field name="product_tmpl_id" ref="product_product_1_product_template"/>
<field name="value_id" ref="product.product_attribute_value_6"/>
<field name="price_extra">18.00</field>
</record>
<!-- -->
</data>
</odoo>
-165
View File
@@ -1,165 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * website_sale_options
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: Odoo 9.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-08-18 14:08+0000\n"
"PO-Revision-Date: 2015-08-25 10:29+0000\n"
"Last-Translator: <>\n"
"Language-Team: Afrikaans (http://www.transifex.com/odoo/odoo-9/language/"
"af/)\n"
"Language: af\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. module: website_sale_options
#: code:addons/website_sale_options/models/sale_order.py:44
#: code:addons/website_sale_options/models/sale_order.py:56
#, python-format
msgid ""
"%s\n"
"Option for: %s"
msgstr ""
#. module: website_sale_options
#: model:product.attribute.value,name:website_sale_options.product_attribute_value_1
msgid "1 year"
msgstr ""
#. module: website_sale_options
#: model:product.attribute.value,name:website_sale_options.product_attribute_value_2
msgid "2 year"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<i class=\"fa fa-chevron-left\"/> <span class=\"hidden-xs\">Continue "
"Shopping</span><span class=\"visible-xs-inline\">Continue</span>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<i class=\"fa fa-shopping-cart fa-fw\"/> <span class=\"hidden-xs\">Proceed "
"to checkout</span><span class=\"visible-xs-inline\">Checkout</span>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "<small>Remove from cart</small>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<span class=\"js_item\">1 Item</span><span class=\"js_items hidden\">5 "
"Items</span><br/>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "<strong>Add to Cart</strong>"
msgstr ""
#. module: website_sale_options
#: model:product.attribute,name:website_sale_options.product_attribute_1
msgid "Duration"
msgstr "Duur"
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_sale_order_line_linked_line_id
msgid "Linked Order Line"
msgstr ""
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_product_template_optional_product_ids
msgid "Optional Products"
msgstr ""
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_sale_order_line_option_line_ids
msgid "Options Linked"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Price"
msgstr "Prys"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product"
msgstr "Produk"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_product_template
msgid "Product Template"
msgstr "Produk Profielvorm"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product not available"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product to add in your shopping cart"
msgstr ""
#. module: website_sale_options
#: model:ir.model.fields,help:website_sale_options.field_product_template_optional_product_ids
msgid "Products to propose when add to cart."
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Quantity"
msgstr "Hoeveelheid"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_sale_order
msgid "Sales Order"
msgstr ""
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_sale_order_line
msgid "Sales Order Line"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Select Your Options:"
msgstr ""
#. module: website_sale_options
#: model:product.product,name:website_sale_options.product_product_1
#: model:product.product,name:website_sale_options.service_cost_01
#: model:product.template,name:website_sale_options.product_product_1_product_template
#: model:product.template,name:website_sale_options.service_cost_01_product_template
msgid "Warranty"
msgstr ""
#. module: website_sale_options
#: model:product.product,description_sale:website_sale_options.product_product_1
#: model:product.product,description_sale:website_sale_options.service_cost_01
#: model:product.template,description_sale:website_sale_options.product_product_1_product_template
#: model:product.template,description_sale:website_sale_options.service_cost_01_product_template
msgid ""
"Warranty, issued to the purchaser of an article by its manufacturer, "
"promising to repair or replace it if necessary within a specified period of "
"time."
msgstr ""
#. module: website_sale_options
#. openerp-web
#: code:addons/website_sale_options/static/src/js/website_sale.test.js:13
#, python-format
msgid "click in modal on 'Proceed to checkout' button"
msgstr ""
-164
View File
@@ -1,164 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * website_sale_options
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: Odoo 9.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-08-18 14:08+0000\n"
"PO-Revision-Date: 2015-09-08 10:56+0000\n"
"Last-Translator: Martin Trigaux\n"
"Language-Team: Amharic (http://www.transifex.com/odoo/odoo-9/language/am/)\n"
"Language: am\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#. module: website_sale_options
#: code:addons/website_sale_options/models/sale_order.py:44
#: code:addons/website_sale_options/models/sale_order.py:56
#, python-format
msgid ""
"%s\n"
"Option for: %s"
msgstr ""
#. module: website_sale_options
#: model:product.attribute.value,name:website_sale_options.product_attribute_value_1
msgid "1 year"
msgstr ""
#. module: website_sale_options
#: model:product.attribute.value,name:website_sale_options.product_attribute_value_2
msgid "2 year"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<i class=\"fa fa-chevron-left\"/> <span class=\"hidden-xs\">Continue "
"Shopping</span><span class=\"visible-xs-inline\">Continue</span>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<i class=\"fa fa-shopping-cart fa-fw\"/> <span class=\"hidden-xs\">Proceed "
"to checkout</span><span class=\"visible-xs-inline\">Checkout</span>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "<small>Remove from cart</small>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<span class=\"js_item\">1 Item</span><span class=\"js_items hidden\">5 "
"Items</span><br/>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "<strong>Add to Cart</strong>"
msgstr ""
#. module: website_sale_options
#: model:product.attribute,name:website_sale_options.product_attribute_1
msgid "Duration"
msgstr "የጊዜ ገደብ"
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_sale_order_line_linked_line_id
msgid "Linked Order Line"
msgstr ""
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_product_template_optional_product_ids
msgid "Optional Products"
msgstr ""
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_sale_order_line_option_line_ids
msgid "Options Linked"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Price"
msgstr "ዋጋ"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product"
msgstr "እቃ"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_product_template
msgid "Product Template"
msgstr "የእቃው ማሳያ"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product not available"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product to add in your shopping cart"
msgstr ""
#. module: website_sale_options
#: model:ir.model.fields,help:website_sale_options.field_product_template_optional_product_ids
msgid "Products to propose when add to cart."
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Quantity"
msgstr "ብዛት"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_sale_order
msgid "Sales Order"
msgstr ""
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_sale_order_line
msgid "Sales Order Line"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Select Your Options:"
msgstr ""
#. module: website_sale_options
#: model:product.product,name:website_sale_options.product_product_1
#: model:product.product,name:website_sale_options.service_cost_01
#: model:product.template,name:website_sale_options.product_product_1_product_template
#: model:product.template,name:website_sale_options.service_cost_01_product_template
msgid "Warranty"
msgstr "ዎስትና"
#. module: website_sale_options
#: model:product.product,description_sale:website_sale_options.product_product_1
#: model:product.product,description_sale:website_sale_options.service_cost_01
#: model:product.template,description_sale:website_sale_options.product_product_1_product_template
#: model:product.template,description_sale:website_sale_options.service_cost_01_product_template
msgid ""
"Warranty, issued to the purchaser of an article by its manufacturer, "
"promising to repair or replace it if necessary within a specified period of "
"time."
msgstr ""
#. module: website_sale_options
#. openerp-web
#: code:addons/website_sale_options/static/src/js/website_sale.test.js:13
#, python-format
msgid "click in modal on 'Proceed to checkout' button"
msgstr ""
-159
View File
@@ -1,159 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * website_sale_options
#
# Translators:
# Mustafa Rawi <mustafa@cubexco.com>, 2018
# hoxhe Aits <hoxhe0@gmail.com>, 2018
# Osoul <baruni@osoul.ly>, 2018
# Mohammed Albasha <m.albasha.ma@gmail.com>, 2018
# Ghaith Gammar <g.gammar@saharaifs.net>, 2018
# Martin Trigaux, 2018
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server saas~11.5\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-21 13:18+0000\n"
"PO-Revision-Date: 2018-09-21 13:18+0000\n"
"Last-Translator: Martin Trigaux, 2018\n"
"Language-Team: Arabic (https://www.transifex.com/odoo/teams/41243/ar/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Language: ar\n"
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<i class=\"fa fa-chevron-left\"/> <span class=\"d-none d-md-"
"inline\">Continue Shopping</span><span class=\"d-inline d-md-"
"none\">Continue</span>"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<i class=\"fa fa-shopping-cart fa-fw\"/> <span class=\"d-none d-md-"
"inline\">Proceed to Checkout</span><span class=\"d-inline d-md-"
"none\">Checkout</span>"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "<small>Remove <span class=\"d-none d-md-inline\"> from Cart</span></small>"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "<span class=\"js_item\">1 </span><span class=\"js_items d-none\">5 </span>"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "<strong>Add<span class=\"d-none d-md-inline\"> to Cart</span></strong>"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Add one"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Add to Cart"
msgstr "أضف إلى السلة"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Close"
msgstr "إقفال"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Continue shopping"
msgstr ""
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_sale_order_line__linked_line_id
msgid "Linked Order Line"
msgstr "بند الأمر المقترن"
#. module: website_sale_options
#: code:addons/website_sale_options/models/sale_order.py:46
#, python-format
msgid "Option for:"
msgstr ""
#. module: website_sale_options
#: code:addons/website_sale_options/models/sale_order.py:48
#, python-format
msgid "Option:"
msgstr ""
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_product_product__optional_product_ids
#: model:ir.model.fields,field_description:website_sale_options.field_product_template__optional_product_ids
msgid "Optional Products"
msgstr "منتجات اختيارية"
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_sale_order_line__option_line_ids
msgid "Options Linked"
msgstr "الخيارات المقترنة"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Price"
msgstr "سعر"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Proceed to checkout"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product"
msgstr "المنتج"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_product_template
msgid "Product Template"
msgstr "قالب المنتج"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product not available"
msgstr "المنتج غير متوفر"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Quantity"
msgstr "الكمية"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Remove one"
msgstr ""
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_sale_order
msgid "Sale Order"
msgstr "أمر البيع"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_sale_order_line
msgid "Sales Order Line"
msgstr "سطر أمر المبيعات"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Select Your Options:"
msgstr "حدد خياراتك:"
#. module: website_sale_options
#: model:ir.model.fields,help:website_sale_options.field_product_product__optional_product_ids
#: model:ir.model.fields,help:website_sale_options.field_product_template__optional_product_ids
msgid "Suggest optional products when adding the product to a cart."
msgstr ""
-187
View File
@@ -1,187 +0,0 @@
# #-#-#-#-# bg.po (Odoo 9.0) #-#-#-#-#
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * website_sale_options
#
# Translators:
# Kaloyan Naumov <kaloyan@lumnus.net>, 2016
# #-#-#-#-# bg.po (Odoo 9.0) #-#-#-#-#
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * website_sale_options
#
# Translators:
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Odoo 9.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-08-18 14:08+0000\n"
"PO-Revision-Date: 2015-09-08 10:56+0000\n"
"Last-Translator: Martin Trigaux\n"
"Language-Team: Bulgarian (http://www.transifex.com/odoo/odoo-9/language/"
"bg/)\n"
"Language: bg\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"#-#-#-#-# bg.po (Odoo 9.0) #-#-#-#-#\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"#-#-#-#-# bg.po (Odoo 9.0) #-#-#-#-#\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. module: website_sale_options
#: code:addons/website_sale_options/models/sale_order.py:44
#: code:addons/website_sale_options/models/sale_order.py:56
#, python-format
msgid ""
"%s\n"
"Option for: %s"
msgstr ""
"%s\n"
"Опция за: %s"
#. module: website_sale_options
#: model:product.attribute.value,name:website_sale_options.product_attribute_value_1
msgid "1 year"
msgstr "1 година"
#. module: website_sale_options
#: model:product.attribute.value,name:website_sale_options.product_attribute_value_2
msgid "2 year"
msgstr "2 години"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<i class=\"fa fa-chevron-left\"/> <span class=\"hidden-xs\">Continue "
"Shopping</span><span class=\"visible-xs-inline\">Continue</span>"
msgstr ""
"<i class=\"fa fa-chevron-left\"/> <span class=\"hidden-xs\">Продължи "
"Пазаруването</span><span class=\"visible-xs-inline\">Продължи</span>"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<i class=\"fa fa-shopping-cart fa-fw\"/> <span class=\"hidden-xs\">Proceed "
"to checkout</span><span class=\"visible-xs-inline\">Checkout</span>"
msgstr ""
"<i class=\"fa fa-shopping-cart fa-fw\"/> <span class=\"hidden-xs\">Завърши "
"Поръчката</span><span class=\"visible-xs-inline\"> Поръчай</span>"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "<small>Remove from cart</small>"
msgstr "Премахни от Количката"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<span class=\"js_item\">1 Item</span><span class=\"js_items hidden\">5 "
"Items</span><br/>"
msgstr ""
"<span class=\"js_item\">1 продукт</span><span class=\"js_items hidden\">5 "
"продукта</span><br/>"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "<strong>Add to Cart</strong>"
msgstr "<strong>Добави в количката</strong>"
#. module: website_sale_options
#: model:product.attribute,name:website_sale_options.product_attribute_1
msgid "Duration"
msgstr "Продължителност"
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_sale_order_line_linked_line_id
msgid "Linked Order Line"
msgstr "Свързан ред от поръчка"
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_product_template_optional_product_ids
msgid "Optional Products"
msgstr "Опционни Продукти"
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_sale_order_line_option_line_ids
msgid "Options Linked"
msgstr "Свързани Опции"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Price"
msgstr "Цена"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product"
msgstr "Продукт"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_product_template
msgid "Product Template"
msgstr "Шаблон за продукт"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product not available"
msgstr "Продуктът не е наличен"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product to add in your shopping cart"
msgstr "Продукти, за да добавяне в Количката"
#. module: website_sale_options
#: model:ir.model.fields,help:website_sale_options.field_product_template_optional_product_ids
msgid "Products to propose when add to cart."
msgstr "Продукти, за прелаганае при добавяне в количката."
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Quantity"
msgstr "Количество"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_sale_order
msgid "Sales Order"
msgstr "Нареждане за продажба"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_sale_order_line
msgid "Sales Order Line"
msgstr "Ред от нареждане за продажба"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Select Your Options:"
msgstr "Изберете Вашите Опции:"
#. module: website_sale_options
#: model:product.product,name:website_sale_options.product_product_1
#: model:product.product,name:website_sale_options.service_cost_01
#: model:product.template,name:website_sale_options.product_product_1_product_template
#: model:product.template,name:website_sale_options.service_cost_01_product_template
msgid "Warranty"
msgstr "Гаранция"
#. module: website_sale_options
#: model:product.product,description_sale:website_sale_options.product_product_1
#: model:product.product,description_sale:website_sale_options.service_cost_01
#: model:product.template,description_sale:website_sale_options.product_product_1_product_template
#: model:product.template,description_sale:website_sale_options.service_cost_01_product_template
msgid ""
"Warranty, issued to the purchaser of an article by its manufacturer, "
"promising to repair or replace it if necessary within a specified period of "
"time."
msgstr ""
"Гаранция, издаване към купувача от производителя/дистрибутор, обещаваща "
"поправка или подмяна в рамките на определен период."
#. module: website_sale_options
#. openerp-web
#: code:addons/website_sale_options/static/src/js/website_sale.test.js:13
#, python-format
msgid "click in modal on 'Proceed to checkout' button"
msgstr "натисни бутон 'Поръчка' в попъп прозореца"
-155
View File
@@ -1,155 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * website_sale_options
#
# Translators:
# Martin Trigaux, 2018
# Boško Stojaković <bluesoft83@gmail.com>, 2018
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server saas~11.5\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-21 13:18+0000\n"
"PO-Revision-Date: 2018-09-21 13:18+0000\n"
"Last-Translator: Boško Stojaković <bluesoft83@gmail.com>, 2018\n"
"Language-Team: Bosnian (https://www.transifex.com/odoo/teams/41243/bs/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Language: bs\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<i class=\"fa fa-chevron-left\"/> <span class=\"d-none d-md-"
"inline\">Continue Shopping</span><span class=\"d-inline d-md-"
"none\">Continue</span>"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<i class=\"fa fa-shopping-cart fa-fw\"/> <span class=\"d-none d-md-"
"inline\">Proceed to Checkout</span><span class=\"d-inline d-md-"
"none\">Checkout</span>"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "<small>Remove <span class=\"d-none d-md-inline\"> from Cart</span></small>"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "<span class=\"js_item\">1 </span><span class=\"js_items d-none\">5 </span>"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "<strong>Add<span class=\"d-none d-md-inline\"> to Cart</span></strong>"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Add one"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Add to Cart"
msgstr "Dodaj u korpu"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Close"
msgstr "Zatvori"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Continue shopping"
msgstr ""
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_sale_order_line__linked_line_id
msgid "Linked Order Line"
msgstr ""
#. module: website_sale_options
#: code:addons/website_sale_options/models/sale_order.py:46
#, python-format
msgid "Option for:"
msgstr ""
#. module: website_sale_options
#: code:addons/website_sale_options/models/sale_order.py:48
#, python-format
msgid "Option:"
msgstr ""
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_product_product__optional_product_ids
#: model:ir.model.fields,field_description:website_sale_options.field_product_template__optional_product_ids
msgid "Optional Products"
msgstr "Opcionalni proizvodi"
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_sale_order_line__option_line_ids
msgid "Options Linked"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Price"
msgstr "Cijena"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Proceed to checkout"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product"
msgstr "Proizvod"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_product_template
msgid "Product Template"
msgstr "Predlog proizvoda"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product not available"
msgstr "Proizvod nije dostupan"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Quantity"
msgstr "Količina"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Remove one"
msgstr ""
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_sale_order
msgid "Sale Order"
msgstr "Prodajni nalog"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_sale_order_line
msgid "Sales Order Line"
msgstr "Stavka prodajne narudžbe"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Select Your Options:"
msgstr ""
#. module: website_sale_options
#: model:ir.model.fields,help:website_sale_options.field_product_product__optional_product_ids
#: model:ir.model.fields,help:website_sale_options.field_product_template__optional_product_ids
msgid "Suggest optional products when adding the product to a cart."
msgstr ""
-163
View File
@@ -1,163 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * website_sale_options
#
# Translators:
# Quim - eccit <quim@eccit.com>, 2017
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 10.saas~18\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-10-02 11:26+0000\n"
"PO-Revision-Date: 2017-10-02 11:26+0000\n"
"Last-Translator: Quim - eccit <quim@eccit.com>, 2017\n"
"Language-Team: Catalan (https://www.transifex.com/odoo/teams/41243/ca/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Language: ca\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. module: website_sale_options
#: model:product.attribute.value,name:website_sale_options.product_attribute_value_1
msgid "1 year"
msgstr ""
#. module: website_sale_options
#: model:product.attribute.value,name:website_sale_options.product_attribute_value_2
msgid "2 year"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<i class=\"fa fa-chevron-left\"/> <span class=\"hidden-xs\">Continue "
"Shopping</span><span class=\"visible-xs-inline\">Continue</span>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<i class=\"fa fa-shopping-cart fa-fw\"/> <span class=\"hidden-xs\">Proceed "
"to Checkout</span><span class=\"visible-xs-inline\">Checkout</span>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "<small>Remove <span class=\"hidden-xs\"> from Cart</span></small>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "<span class=\"js_item\">1 </span><span class=\"js_items hidden\">5 </span>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "<strong>Add<span class=\"hidden-xs\"> to Cart</span></strong>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Add to Cart"
msgstr ""
#. module: website_sale_options
#: model:product.attribute,name:website_sale_options.product_attribute_1
msgid "Duration"
msgstr ""
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_sale_order_line_linked_line_id
msgid "Linked Order Line"
msgstr ""
#. module: website_sale_options
#: code:addons/website_sale_options/models/sale_order.py:46
#, python-format
msgid "Option for:"
msgstr ""
#. module: website_sale_options
#: code:addons/website_sale_options/models/sale_order.py:48
#, python-format
msgid "Option:"
msgstr ""
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_product_product_optional_product_ids
#: model:ir.model.fields,field_description:website_sale_options.field_product_template_optional_product_ids
msgid "Optional Products"
msgstr ""
#. module: website_sale_options
#: model:ir.model.fields,help:website_sale_options.field_product_product_optional_product_ids
#: model:ir.model.fields,help:website_sale_options.field_product_template_optional_product_ids
msgid ""
"Optional Products are suggested whenever the customer hits *Add to Cart* "
"(cross-sell strategy, e.g. for computers: warranty, software, etc.)."
msgstr ""
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_sale_order_line_option_line_ids
msgid "Options Linked"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Price"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product"
msgstr ""
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_product_template
msgid "Product Template"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product not available"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Quantity"
msgstr ""
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_sale_order
msgid "Quotation"
msgstr "Pressupost"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_sale_order_line
msgid "Sales Order Line"
msgstr "Línia comanda de venda"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Select Your Options:"
msgstr ""
#. module: website_sale_options
#: model:product.product,name:website_sale_options.product_product_1
#: model:product.product,name:website_sale_options.service_cost_01
#: model:product.template,name:website_sale_options.product_product_1_product_template
#: model:product.template,name:website_sale_options.service_cost_01_product_template
msgid "Warranty"
msgstr ""
#. module: website_sale_options
#: model:product.product,description_sale:website_sale_options.product_product_1
#: model:product.product,description_sale:website_sale_options.service_cost_01
#: model:product.template,description_sale:website_sale_options.product_product_1_product_template
#: model:product.template,description_sale:website_sale_options.service_cost_01_product_template
msgid ""
"Warranty, issued to the purchaser of an article by its manufacturer, "
"promising to repair or replace it if necessary within a specified period of "
"time."
msgstr ""
-156
View File
@@ -1,156 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * website_sale_options
#
# Translators:
# Martin Trigaux, 2018
# Chris <krystof.reklamy13@gmail.com>, 2018
# trendspotter, 2018
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server saas~11.5\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-21 13:18+0000\n"
"PO-Revision-Date: 2018-09-21 13:18+0000\n"
"Last-Translator: trendspotter, 2018\n"
"Language-Team: Czech (https://www.transifex.com/odoo/teams/41243/cs/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Language: cs\n"
"Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n <= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;\n"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<i class=\"fa fa-chevron-left\"/> <span class=\"d-none d-md-"
"inline\">Continue Shopping</span><span class=\"d-inline d-md-"
"none\">Continue</span>"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<i class=\"fa fa-shopping-cart fa-fw\"/> <span class=\"d-none d-md-"
"inline\">Proceed to Checkout</span><span class=\"d-inline d-md-"
"none\">Checkout</span>"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "<small>Remove <span class=\"d-none d-md-inline\"> from Cart</span></small>"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "<span class=\"js_item\">1 </span><span class=\"js_items d-none\">5 </span>"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "<strong>Add<span class=\"d-none d-md-inline\"> to Cart</span></strong>"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Add one"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Add to Cart"
msgstr "Přidat do košíku"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Close"
msgstr "Zavřít"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Continue shopping"
msgstr ""
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_sale_order_line__linked_line_id
msgid "Linked Order Line"
msgstr ""
#. module: website_sale_options
#: code:addons/website_sale_options/models/sale_order.py:46
#, python-format
msgid "Option for:"
msgstr ""
#. module: website_sale_options
#: code:addons/website_sale_options/models/sale_order.py:48
#, python-format
msgid "Option:"
msgstr ""
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_product_product__optional_product_ids
#: model:ir.model.fields,field_description:website_sale_options.field_product_template__optional_product_ids
msgid "Optional Products"
msgstr "Volitelné produkty"
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_sale_order_line__option_line_ids
msgid "Options Linked"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Price"
msgstr "Cena"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Proceed to checkout"
msgstr "Pokračujte k pokladně"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product"
msgstr "Výrobek"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_product_template
msgid "Product Template"
msgstr "Šablona výrobku"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product not available"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Quantity"
msgstr "Množství"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Remove one"
msgstr ""
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_sale_order
msgid "Sale Order"
msgstr "Prodejní objednávka"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_sale_order_line
msgid "Sales Order Line"
msgstr "Řádek potvrzení objednávky"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Select Your Options:"
msgstr ""
#. module: website_sale_options
#: model:ir.model.fields,help:website_sale_options.field_product_product__optional_product_ids
#: model:ir.model.fields,help:website_sale_options.field_product_template__optional_product_ids
msgid "Suggest optional products when adding the product to a cart."
msgstr ""
-173
View File
@@ -1,173 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * website_sale_options
#
# Translators:
# Sanne Kristensen <sanne@vkdata.dk>, 2017
# Martin Trigaux <mat@odoo.com>, 2017
# jonas jensen <j.jensen@tcomp.dk>, 2017
# Laerke Mathiasen <lhm@flexerp.dk>, 2017
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 10.saas~18\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-10-02 11:26+0000\n"
"PO-Revision-Date: 2017-10-02 11:26+0000\n"
"Last-Translator: Laerke Mathiasen <lhm@flexerp.dk>, 2017\n"
"Language-Team: Danish (https://www.transifex.com/odoo/teams/41243/da/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Language: da\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. module: website_sale_options
#: model:product.attribute.value,name:website_sale_options.product_attribute_value_1
msgid "1 year"
msgstr "1 år"
#. module: website_sale_options
#: model:product.attribute.value,name:website_sale_options.product_attribute_value_2
msgid "2 year"
msgstr "2 år"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<i class=\"fa fa-chevron-left\"/> <span class=\"hidden-xs\">Continue "
"Shopping</span><span class=\"visible-xs-inline\">Continue</span>"
msgstr ""
"<i class=\"fa fa-chevron-left\"/> <span class=\"hidden-xs\">Fortsæt med at "
"handle</span><span class=\"visible-xs-inline\">Fortsæt</span>"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<i class=\"fa fa-shopping-cart fa-fw\"/> <span class=\"hidden-xs\">Proceed "
"to Checkout</span><span class=\"visible-xs-inline\">Checkout</span>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "<small>Remove <span class=\"hidden-xs\"> from Cart</span></small>"
msgstr "<small>Fjern <span class=\"hidden-xs\"> fra Indkøbskurv</span></small>"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "<span class=\"js_item\">1 </span><span class=\"js_items hidden\">5 </span>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "<strong>Add<span class=\"hidden-xs\"> to Cart</span></strong>"
msgstr ""
"<strong>Tilføj<span class=\"hidden-xs\"> til Indkøbskurven</span></strong>"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Add to Cart"
msgstr "Tilføj til kurv"
#. module: website_sale_options
#: model:product.attribute,name:website_sale_options.product_attribute_1
msgid "Duration"
msgstr "Varighed"
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_sale_order_line_linked_line_id
msgid "Linked Order Line"
msgstr "Tilknyttede ordrelinjer"
#. module: website_sale_options
#: code:addons/website_sale_options/models/sale_order.py:46
#, python-format
msgid "Option for:"
msgstr "Valgmulighed for:"
#. module: website_sale_options
#: code:addons/website_sale_options/models/sale_order.py:48
#, python-format
msgid "Option:"
msgstr "Valgmulighed:"
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_product_product_optional_product_ids
#: model:ir.model.fields,field_description:website_sale_options.field_product_template_optional_product_ids
msgid "Optional Products"
msgstr "Valgfrie produkter"
#. module: website_sale_options
#: model:ir.model.fields,help:website_sale_options.field_product_product_optional_product_ids
#: model:ir.model.fields,help:website_sale_options.field_product_template_optional_product_ids
msgid ""
"Optional Products are suggested whenever the customer hits *Add to Cart* "
"(cross-sell strategy, e.g. for computers: warranty, software, etc.)."
msgstr ""
"Valgfrie produkter foreslås, når kunden trykker på *Tilføj til kurv* "
"(krydssalgs-strategi, f.eks. til computere: garanti, software osv.)."
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_sale_order_line_option_line_ids
msgid "Options Linked"
msgstr "Valgmuligheder tilknyttet"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Price"
msgstr "Pris"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product"
msgstr "Produkt"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_product_template
msgid "Product Template"
msgstr "Produktskabelon"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product not available"
msgstr "Produkt er ikke til rådighed"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Quantity"
msgstr "Antal"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_sale_order
msgid "Quotation"
msgstr "Tilbud"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_sale_order_line
msgid "Sales Order Line"
msgstr "Salgsordrelinje"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Select Your Options:"
msgstr "Vælg dine muligheder:"
#. module: website_sale_options
#: model:product.product,name:website_sale_options.product_product_1
#: model:product.product,name:website_sale_options.service_cost_01
#: model:product.template,name:website_sale_options.product_product_1_product_template
#: model:product.template,name:website_sale_options.service_cost_01_product_template
msgid "Warranty"
msgstr "Garanti"
#. module: website_sale_options
#: model:product.product,description_sale:website_sale_options.product_product_1
#: model:product.product,description_sale:website_sale_options.service_cost_01
#: model:product.template,description_sale:website_sale_options.product_product_1_product_template
#: model:product.template,description_sale:website_sale_options.service_cost_01_product_template
msgid ""
"Warranty, issued to the purchaser of an article by its manufacturer, "
"promising to repair or replace it if necessary within a specified period of "
"time."
msgstr ""
"Garanti udstedt til køber af et produkt fra en fabrikant, med garanti på at "
"reparere eller udskifte det nødvendige inden for en bestemt tidsperiode."
-156
View File
@@ -1,156 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * website_sale_options
#
# Translators:
# e2f <projects@e2f.com>, 2018
# Martin Trigaux, 2018
# Ermin Trevisan <trevi@twanda.com>, 2018
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server saas~11.5\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-21 13:18+0000\n"
"PO-Revision-Date: 2018-09-21 13:18+0000\n"
"Last-Translator: Ermin Trevisan <trevi@twanda.com>, 2018\n"
"Language-Team: German (https://www.transifex.com/odoo/teams/41243/de/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Language: de\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<i class=\"fa fa-chevron-left\"/> <span class=\"d-none d-md-"
"inline\">Continue Shopping</span><span class=\"d-inline d-md-"
"none\">Continue</span>"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<i class=\"fa fa-shopping-cart fa-fw\"/> <span class=\"d-none d-md-"
"inline\">Proceed to Checkout</span><span class=\"d-inline d-md-"
"none\">Checkout</span>"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "<small>Remove <span class=\"d-none d-md-inline\"> from Cart</span></small>"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "<span class=\"js_item\">1 </span><span class=\"js_items d-none\">5 </span>"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "<strong>Add<span class=\"d-none d-md-inline\"> to Cart</span></strong>"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Add one"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Add to Cart"
msgstr "In den Warenkorb"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Close"
msgstr "Schließen"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Continue shopping"
msgstr ""
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_sale_order_line__linked_line_id
msgid "Linked Order Line"
msgstr "Verknüpfte Auftragsposition"
#. module: website_sale_options
#: code:addons/website_sale_options/models/sale_order.py:46
#, python-format
msgid "Option for:"
msgstr "Option für:"
#. module: website_sale_options
#: code:addons/website_sale_options/models/sale_order.py:48
#, python-format
msgid "Option:"
msgstr "Option:"
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_product_product__optional_product_ids
#: model:ir.model.fields,field_description:website_sale_options.field_product_template__optional_product_ids
msgid "Optional Products"
msgstr "Optionale Produkte"
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_sale_order_line__option_line_ids
msgid "Options Linked"
msgstr "Verlinkte Alternativen"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Price"
msgstr "Preis"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Proceed to checkout"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product"
msgstr "Produkt"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_product_template
msgid "Product Template"
msgstr "Produktvorlage"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product not available"
msgstr "Das Produkt ist nicht verfügbar."
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Quantity"
msgstr "Menge"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Remove one"
msgstr ""
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_sale_order
msgid "Sale Order"
msgstr "Verkaufsauftrag"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_sale_order_line
msgid "Sales Order Line"
msgstr "Auftragsposition"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Select Your Options:"
msgstr "Selektieren Sie Ihre Alternativen"
#. module: website_sale_options
#: model:ir.model.fields,help:website_sale_options.field_product_product__optional_product_ids
#: model:ir.model.fields,help:website_sale_options.field_product_template__optional_product_ids
msgid "Suggest optional products when adding the product to a cart."
msgstr ""
-156
View File
@@ -1,156 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * website_sale_options
#
# Translators:
# Martin Trigaux, 2018
# Kostas Goutoudis <goutoudis@gmail.com>, 2018
# George Tarasidis <george_tarasidis@yahoo.com>, 2018
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server saas~11.5\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-21 13:18+0000\n"
"PO-Revision-Date: 2018-09-21 13:18+0000\n"
"Last-Translator: George Tarasidis <george_tarasidis@yahoo.com>, 2018\n"
"Language-Team: Greek (https://www.transifex.com/odoo/teams/41243/el/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Language: el\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<i class=\"fa fa-chevron-left\"/> <span class=\"d-none d-md-"
"inline\">Continue Shopping</span><span class=\"d-inline d-md-"
"none\">Continue</span>"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<i class=\"fa fa-shopping-cart fa-fw\"/> <span class=\"d-none d-md-"
"inline\">Proceed to Checkout</span><span class=\"d-inline d-md-"
"none\">Checkout</span>"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "<small>Remove <span class=\"d-none d-md-inline\"> from Cart</span></small>"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "<span class=\"js_item\">1 </span><span class=\"js_items d-none\">5 </span>"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "<strong>Add<span class=\"d-none d-md-inline\"> to Cart</span></strong>"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Add one"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Add to Cart"
msgstr "Προσθήκη στο Καλάθι"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Close"
msgstr "Κλείσιμο"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Continue shopping"
msgstr ""
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_sale_order_line__linked_line_id
msgid "Linked Order Line"
msgstr "Συνδεδεμένη Γραμμή Εντολής"
#. module: website_sale_options
#: code:addons/website_sale_options/models/sale_order.py:46
#, python-format
msgid "Option for:"
msgstr "Επιλογή για:"
#. module: website_sale_options
#: code:addons/website_sale_options/models/sale_order.py:48
#, python-format
msgid "Option:"
msgstr "Επιλογή:"
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_product_product__optional_product_ids
#: model:ir.model.fields,field_description:website_sale_options.field_product_template__optional_product_ids
msgid "Optional Products"
msgstr "Προαιρετικά Είδη"
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_sale_order_line__option_line_ids
msgid "Options Linked"
msgstr "Συνδεδεμένες Επιλογές"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Price"
msgstr "ΤΙΜΗ"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Proceed to checkout"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product"
msgstr "Είδος"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_product_template
msgid "Product Template"
msgstr "Πρότυπο Είδους "
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product not available"
msgstr "Μη Διαθέσιμο Είδος"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Quantity"
msgstr "Ποσότητα"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Remove one"
msgstr ""
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_sale_order
msgid "Sale Order"
msgstr "Παραγγελία"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_sale_order_line
msgid "Sales Order Line"
msgstr "Γραμμή Παραγγελίας"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Select Your Options:"
msgstr "Επιλέξτε τις επιλογές σας:"
#. module: website_sale_options
#: model:ir.model.fields,help:website_sale_options.field_product_product__optional_product_ids
#: model:ir.model.fields,help:website_sale_options.field_product_template__optional_product_ids
msgid "Suggest optional products when adding the product to a cart."
msgstr ""
-165
View File
@@ -1,165 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * website_sale_options
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: Odoo 9.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-08-18 14:08+0000\n"
"PO-Revision-Date: 2015-09-08 10:56+0000\n"
"Last-Translator: Martin Trigaux\n"
"Language-Team: English (Australia) (http://www.transifex.com/odoo/odoo-9/"
"language/en_AU/)\n"
"Language: en_AU\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. module: website_sale_options
#: code:addons/website_sale_options/models/sale_order.py:44
#: code:addons/website_sale_options/models/sale_order.py:56
#, python-format
msgid ""
"%s\n"
"Option for: %s"
msgstr ""
#. module: website_sale_options
#: model:product.attribute.value,name:website_sale_options.product_attribute_value_1
msgid "1 year"
msgstr ""
#. module: website_sale_options
#: model:product.attribute.value,name:website_sale_options.product_attribute_value_2
msgid "2 year"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<i class=\"fa fa-chevron-left\"/> <span class=\"hidden-xs\">Continue "
"Shopping</span><span class=\"visible-xs-inline\">Continue</span>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<i class=\"fa fa-shopping-cart fa-fw\"/> <span class=\"hidden-xs\">Proceed "
"to checkout</span><span class=\"visible-xs-inline\">Checkout</span>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "<small>Remove from cart</small>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<span class=\"js_item\">1 Item</span><span class=\"js_items hidden\">5 "
"Items</span><br/>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "<strong>Add to Cart</strong>"
msgstr ""
#. module: website_sale_options
#: model:product.attribute,name:website_sale_options.product_attribute_1
msgid "Duration"
msgstr "Duration"
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_sale_order_line_linked_line_id
msgid "Linked Order Line"
msgstr ""
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_product_template_optional_product_ids
msgid "Optional Products"
msgstr ""
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_sale_order_line_option_line_ids
msgid "Options Linked"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Price"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product"
msgstr ""
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_product_template
msgid "Product Template"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product not available"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product to add in your shopping cart"
msgstr ""
#. module: website_sale_options
#: model:ir.model.fields,help:website_sale_options.field_product_template_optional_product_ids
msgid "Products to propose when add to cart."
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Quantity"
msgstr ""
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_sale_order
msgid "Sales Order"
msgstr "Sales Order"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_sale_order_line
msgid "Sales Order Line"
msgstr "Sales Order Line"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Select Your Options:"
msgstr ""
#. module: website_sale_options
#: model:product.product,name:website_sale_options.product_product_1
#: model:product.product,name:website_sale_options.service_cost_01
#: model:product.template,name:website_sale_options.product_product_1_product_template
#: model:product.template,name:website_sale_options.service_cost_01_product_template
msgid "Warranty"
msgstr ""
#. module: website_sale_options
#: model:product.product,description_sale:website_sale_options.product_product_1
#: model:product.product,description_sale:website_sale_options.service_cost_01
#: model:product.template,description_sale:website_sale_options.product_product_1_product_template
#: model:product.template,description_sale:website_sale_options.service_cost_01_product_template
msgid ""
"Warranty, issued to the purchaser of an article by its manufacturer, "
"promising to repair or replace it if necessary within a specified period of "
"time."
msgstr ""
#. module: website_sale_options
#. openerp-web
#: code:addons/website_sale_options/static/src/js/website_sale.test.js:13
#, python-format
msgid "click in modal on 'Proceed to checkout' button"
msgstr ""
-165
View File
@@ -1,165 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * website_sale_options
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: Odoo 9.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-08-18 14:08+0000\n"
"PO-Revision-Date: 2015-09-08 10:56+0000\n"
"Last-Translator: Martin Trigaux\n"
"Language-Team: English (United Kingdom) (http://www.transifex.com/odoo/"
"odoo-9/language/en_GB/)\n"
"Language: en_GB\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. module: website_sale_options
#: code:addons/website_sale_options/models/sale_order.py:44
#: code:addons/website_sale_options/models/sale_order.py:56
#, python-format
msgid ""
"%s\n"
"Option for: %s"
msgstr ""
#. module: website_sale_options
#: model:product.attribute.value,name:website_sale_options.product_attribute_value_1
msgid "1 year"
msgstr ""
#. module: website_sale_options
#: model:product.attribute.value,name:website_sale_options.product_attribute_value_2
msgid "2 year"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<i class=\"fa fa-chevron-left\"/> <span class=\"hidden-xs\">Continue "
"Shopping</span><span class=\"visible-xs-inline\">Continue</span>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<i class=\"fa fa-shopping-cart fa-fw\"/> <span class=\"hidden-xs\">Proceed "
"to checkout</span><span class=\"visible-xs-inline\">Checkout</span>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "<small>Remove from cart</small>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<span class=\"js_item\">1 Item</span><span class=\"js_items hidden\">5 "
"Items</span><br/>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "<strong>Add to Cart</strong>"
msgstr ""
#. module: website_sale_options
#: model:product.attribute,name:website_sale_options.product_attribute_1
msgid "Duration"
msgstr "Duration"
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_sale_order_line_linked_line_id
msgid "Linked Order Line"
msgstr ""
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_product_template_optional_product_ids
msgid "Optional Products"
msgstr ""
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_sale_order_line_option_line_ids
msgid "Options Linked"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Price"
msgstr "Price"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product"
msgstr "Product"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_product_template
msgid "Product Template"
msgstr "Product Template"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product not available"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product to add in your shopping cart"
msgstr ""
#. module: website_sale_options
#: model:ir.model.fields,help:website_sale_options.field_product_template_optional_product_ids
msgid "Products to propose when add to cart."
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Quantity"
msgstr "Quantity"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_sale_order
msgid "Sales Order"
msgstr "Sales Order"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_sale_order_line
msgid "Sales Order Line"
msgstr "Sales Order Line"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Select Your Options:"
msgstr ""
#. module: website_sale_options
#: model:product.product,name:website_sale_options.product_product_1
#: model:product.product,name:website_sale_options.service_cost_01
#: model:product.template,name:website_sale_options.product_product_1_product_template
#: model:product.template,name:website_sale_options.service_cost_01_product_template
msgid "Warranty"
msgstr ""
#. module: website_sale_options
#: model:product.product,description_sale:website_sale_options.product_product_1
#: model:product.product,description_sale:website_sale_options.service_cost_01
#: model:product.template,description_sale:website_sale_options.product_product_1_product_template
#: model:product.template,description_sale:website_sale_options.service_cost_01_product_template
msgid ""
"Warranty, issued to the purchaser of an article by its manufacturer, "
"promising to repair or replace it if necessary within a specified period of "
"time."
msgstr ""
#. module: website_sale_options
#. openerp-web
#: code:addons/website_sale_options/static/src/js/website_sale.test.js:13
#, python-format
msgid "click in modal on 'Proceed to checkout' button"
msgstr ""
-156
View File
@@ -1,156 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * website_sale_options
#
# Translators:
# Martin Trigaux, 2018
# Gelo Joga Landoo <gj@landoo.es>, 2018
# Nicolás Broggi <rnbroggi@gmail.com>, 2018
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server saas~11.5\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-09-21 13:18+0000\n"
"PO-Revision-Date: 2018-09-21 13:18+0000\n"
"Last-Translator: Nicolás Broggi <rnbroggi@gmail.com>, 2018\n"
"Language-Team: Spanish (https://www.transifex.com/odoo/teams/41243/es/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Language: es\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<i class=\"fa fa-chevron-left\"/> <span class=\"d-none d-md-"
"inline\">Continue Shopping</span><span class=\"d-inline d-md-"
"none\">Continue</span>"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<i class=\"fa fa-shopping-cart fa-fw\"/> <span class=\"d-none d-md-"
"inline\">Proceed to Checkout</span><span class=\"d-inline d-md-"
"none\">Checkout</span>"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "<small>Remove <span class=\"d-none d-md-inline\"> from Cart</span></small>"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "<span class=\"js_item\">1 </span><span class=\"js_items d-none\">5 </span>"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "<strong>Add<span class=\"d-none d-md-inline\"> to Cart</span></strong>"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Add one"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Add to Cart"
msgstr "Añadir al Carrito"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Close"
msgstr "Cerrar"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Continue shopping"
msgstr ""
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_sale_order_line__linked_line_id
msgid "Linked Order Line"
msgstr "Línea de pedido de venta relacionada"
#. module: website_sale_options
#: code:addons/website_sale_options/models/sale_order.py:46
#, python-format
msgid "Option for:"
msgstr "Opción para:"
#. module: website_sale_options
#: code:addons/website_sale_options/models/sale_order.py:48
#, python-format
msgid "Option:"
msgstr "Oprción:"
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_product_product__optional_product_ids
#: model:ir.model.fields,field_description:website_sale_options.field_product_template__optional_product_ids
msgid "Optional Products"
msgstr "Productos opcionales"
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_sale_order_line__option_line_ids
msgid "Options Linked"
msgstr "Opciones relacionadas"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Price"
msgstr "Precio"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Proceed to checkout"
msgstr ""
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product"
msgstr "Producto"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_product_template
msgid "Product Template"
msgstr "Plantilla de producto"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product not available"
msgstr "Producto no disponible"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Quantity"
msgstr "Cantidad"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Remove one"
msgstr ""
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_sale_order
msgid "Sale Order"
msgstr "Pedido de venta"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_sale_order_line
msgid "Sales Order Line"
msgstr "Línea de pedido de venta"
#. module: website_sale_options
#: model_terms:ir.ui.view,arch_db:website_sale_options.modal
msgid "Select Your Options:"
msgstr "Seleccione sus opciones:"
#. module: website_sale_options
#: model:ir.model.fields,help:website_sale_options.field_product_product__optional_product_ids
#: model:ir.model.fields,help:website_sale_options.field_product_template__optional_product_ids
msgid "Suggest optional products when adding the product to a cart."
msgstr ""
-170
View File
@@ -1,170 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * website_sale_options
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: Odoo 9.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-08-18 14:08+0000\n"
"PO-Revision-Date: 2015-09-12 00:42+0000\n"
"Last-Translator: Martin Trigaux\n"
"Language-Team: Spanish (Argentina) (http://www.transifex.com/odoo/odoo-9/"
"language/es_AR/)\n"
"Language: es_AR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. module: website_sale_options
#: code:addons/website_sale_options/models/sale_order.py:44
#: code:addons/website_sale_options/models/sale_order.py:56
#, python-format
msgid ""
"%s\n"
"Option for: %s"
msgstr ""
"%s\n"
"Opción para: %s"
#. module: website_sale_options
#: model:product.attribute.value,name:website_sale_options.product_attribute_value_1
msgid "1 year"
msgstr ""
#. module: website_sale_options
#: model:product.attribute.value,name:website_sale_options.product_attribute_value_2
msgid "2 year"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<i class=\"fa fa-chevron-left\"/> <span class=\"hidden-xs\">Continue "
"Shopping</span><span class=\"visible-xs-inline\">Continue</span>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<i class=\"fa fa-shopping-cart fa-fw\"/> <span class=\"hidden-xs\">Proceed "
"to checkout</span><span class=\"visible-xs-inline\">Checkout</span>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "<small>Remove from cart</small>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<span class=\"js_item\">1 Item</span><span class=\"js_items hidden\">5 "
"Items</span><br/>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "<strong>Add to Cart</strong>"
msgstr ""
#. module: website_sale_options
#: model:product.attribute,name:website_sale_options.product_attribute_1
msgid "Duration"
msgstr "Duración"
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_sale_order_line_linked_line_id
msgid "Linked Order Line"
msgstr "Línea de pedido de venta relacionada"
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_product_template_optional_product_ids
msgid "Optional Products"
msgstr "Productos opcionales"
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_sale_order_line_option_line_ids
msgid "Options Linked"
msgstr "Opciones relacionadas"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Price"
msgstr "Precio"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product"
msgstr "Producto"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_product_template
msgid "Product Template"
msgstr "Plantilla de Producto"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product not available"
msgstr "Producto no disponible"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product to add in your shopping cart"
msgstr "Producto a añadir en su carro"
#. module: website_sale_options
#: model:ir.model.fields,help:website_sale_options.field_product_template_optional_product_ids
msgid "Products to propose when add to cart."
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Quantity"
msgstr "Cantidad"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_sale_order
msgid "Sales Order"
msgstr "Orden de Ventas"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_sale_order_line
msgid "Sales Order Line"
msgstr "Línea de Pedido de Ventas"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Select Your Options:"
msgstr "Seleccione sus opciones:"
#. module: website_sale_options
#: model:product.product,name:website_sale_options.product_product_1
#: model:product.product,name:website_sale_options.service_cost_01
#: model:product.template,name:website_sale_options.product_product_1_product_template
#: model:product.template,name:website_sale_options.service_cost_01_product_template
msgid "Warranty"
msgstr "Garantía"
#. module: website_sale_options
#: model:product.product,description_sale:website_sale_options.product_product_1
#: model:product.product,description_sale:website_sale_options.service_cost_01
#: model:product.template,description_sale:website_sale_options.product_product_1_product_template
#: model:product.template,description_sale:website_sale_options.service_cost_01_product_template
msgid ""
"Warranty, issued to the purchaser of an article by its manufacturer, "
"promising to repair or replace it if necessary within a specified period of "
"time."
msgstr ""
"Garantía, emitida al comprador de un artículo por su fabricante, con la "
"promesa de repararlo, o sustituirlo si es necesario, en un plazo determinado "
"de tiempo."
#. module: website_sale_options
#. openerp-web
#: code:addons/website_sale_options/static/src/js/website_sale.test.js:13
#, python-format
msgid "click in modal on 'Proceed to checkout' button"
msgstr ""
-165
View File
@@ -1,165 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * website_sale_options
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: Odoo 9.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-08-18 14:08+0000\n"
"PO-Revision-Date: 2015-08-25 10:29+0000\n"
"Last-Translator: <>\n"
"Language-Team: Spanish (Bolivia) (http://www.transifex.com/odoo/odoo-9/"
"language/es_BO/)\n"
"Language: es_BO\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. module: website_sale_options
#: code:addons/website_sale_options/models/sale_order.py:44
#: code:addons/website_sale_options/models/sale_order.py:56
#, python-format
msgid ""
"%s\n"
"Option for: %s"
msgstr ""
#. module: website_sale_options
#: model:product.attribute.value,name:website_sale_options.product_attribute_value_1
msgid "1 year"
msgstr ""
#. module: website_sale_options
#: model:product.attribute.value,name:website_sale_options.product_attribute_value_2
msgid "2 year"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<i class=\"fa fa-chevron-left\"/> <span class=\"hidden-xs\">Continue "
"Shopping</span><span class=\"visible-xs-inline\">Continue</span>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<i class=\"fa fa-shopping-cart fa-fw\"/> <span class=\"hidden-xs\">Proceed "
"to checkout</span><span class=\"visible-xs-inline\">Checkout</span>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "<small>Remove from cart</small>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<span class=\"js_item\">1 Item</span><span class=\"js_items hidden\">5 "
"Items</span><br/>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "<strong>Add to Cart</strong>"
msgstr ""
#. module: website_sale_options
#: model:product.attribute,name:website_sale_options.product_attribute_1
msgid "Duration"
msgstr "Duración"
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_sale_order_line_linked_line_id
msgid "Linked Order Line"
msgstr ""
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_product_template_optional_product_ids
msgid "Optional Products"
msgstr ""
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_sale_order_line_option_line_ids
msgid "Options Linked"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Price"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product"
msgstr "Producto"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_product_template
msgid "Product Template"
msgstr "Plantilla de producto"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product not available"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product to add in your shopping cart"
msgstr ""
#. module: website_sale_options
#: model:ir.model.fields,help:website_sale_options.field_product_template_optional_product_ids
msgid "Products to propose when add to cart."
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Quantity"
msgstr "Cantidad"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_sale_order
msgid "Sales Order"
msgstr "Pedido de Venta"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_sale_order_line
msgid "Sales Order Line"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Select Your Options:"
msgstr ""
#. module: website_sale_options
#: model:product.product,name:website_sale_options.product_product_1
#: model:product.product,name:website_sale_options.service_cost_01
#: model:product.template,name:website_sale_options.product_product_1_product_template
#: model:product.template,name:website_sale_options.service_cost_01_product_template
msgid "Warranty"
msgstr ""
#. module: website_sale_options
#: model:product.product,description_sale:website_sale_options.product_product_1
#: model:product.product,description_sale:website_sale_options.service_cost_01
#: model:product.template,description_sale:website_sale_options.product_product_1_product_template
#: model:product.template,description_sale:website_sale_options.service_cost_01_product_template
msgid ""
"Warranty, issued to the purchaser of an article by its manufacturer, "
"promising to repair or replace it if necessary within a specified period of "
"time."
msgstr ""
#. module: website_sale_options
#. openerp-web
#: code:addons/website_sale_options/static/src/js/website_sale.test.js:13
#, python-format
msgid "click in modal on 'Proceed to checkout' button"
msgstr ""
-165
View File
@@ -1,165 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * website_sale_options
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: Odoo 9.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-08-18 14:08+0000\n"
"PO-Revision-Date: 2015-09-08 10:57+0000\n"
"Last-Translator: Martin Trigaux\n"
"Language-Team: Spanish (Chile) (http://www.transifex.com/odoo/odoo-9/"
"language/es_CL/)\n"
"Language: es_CL\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. module: website_sale_options
#: code:addons/website_sale_options/models/sale_order.py:44
#: code:addons/website_sale_options/models/sale_order.py:56
#, python-format
msgid ""
"%s\n"
"Option for: %s"
msgstr ""
#. module: website_sale_options
#: model:product.attribute.value,name:website_sale_options.product_attribute_value_1
msgid "1 year"
msgstr ""
#. module: website_sale_options
#: model:product.attribute.value,name:website_sale_options.product_attribute_value_2
msgid "2 year"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<i class=\"fa fa-chevron-left\"/> <span class=\"hidden-xs\">Continue "
"Shopping</span><span class=\"visible-xs-inline\">Continue</span>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<i class=\"fa fa-shopping-cart fa-fw\"/> <span class=\"hidden-xs\">Proceed "
"to checkout</span><span class=\"visible-xs-inline\">Checkout</span>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "<small>Remove from cart</small>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<span class=\"js_item\">1 Item</span><span class=\"js_items hidden\">5 "
"Items</span><br/>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "<strong>Add to Cart</strong>"
msgstr ""
#. module: website_sale_options
#: model:product.attribute,name:website_sale_options.product_attribute_1
msgid "Duration"
msgstr ""
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_sale_order_line_linked_line_id
msgid "Linked Order Line"
msgstr ""
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_product_template_optional_product_ids
msgid "Optional Products"
msgstr ""
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_sale_order_line_option_line_ids
msgid "Options Linked"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Price"
msgstr "Precio"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product"
msgstr "Producto"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_product_template
msgid "Product Template"
msgstr "Plantilla producto"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product not available"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product to add in your shopping cart"
msgstr ""
#. module: website_sale_options
#: model:ir.model.fields,help:website_sale_options.field_product_template_optional_product_ids
msgid "Products to propose when add to cart."
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Quantity"
msgstr "Cantidad"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_sale_order
msgid "Sales Order"
msgstr "Pedido de venta"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_sale_order_line
msgid "Sales Order Line"
msgstr "Línea de pedido de venta"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Select Your Options:"
msgstr ""
#. module: website_sale_options
#: model:product.product,name:website_sale_options.product_product_1
#: model:product.product,name:website_sale_options.service_cost_01
#: model:product.template,name:website_sale_options.product_product_1_product_template
#: model:product.template,name:website_sale_options.service_cost_01_product_template
msgid "Warranty"
msgstr ""
#. module: website_sale_options
#: model:product.product,description_sale:website_sale_options.product_product_1
#: model:product.product,description_sale:website_sale_options.service_cost_01
#: model:product.template,description_sale:website_sale_options.product_product_1_product_template
#: model:product.template,description_sale:website_sale_options.service_cost_01_product_template
msgid ""
"Warranty, issued to the purchaser of an article by its manufacturer, "
"promising to repair or replace it if necessary within a specified period of "
"time."
msgstr ""
#. module: website_sale_options
#. openerp-web
#: code:addons/website_sale_options/static/src/js/website_sale.test.js:13
#, python-format
msgid "click in modal on 'Proceed to checkout' button"
msgstr ""
-177
View File
@@ -1,177 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * website_sale_options
#
# Translators:
# Mateo Tibaquirá <nestormateo@gmail.com>, 2015
msgid ""
msgstr ""
"Project-Id-Version: Odoo 9.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-08-18 14:08+0000\n"
"PO-Revision-Date: 2015-11-28 00:07+0000\n"
"Last-Translator: Mateo Tibaquirá <nestormateo@gmail.com>\n"
"Language-Team: Spanish (Colombia) (http://www.transifex.com/odoo/odoo-9/"
"language/es_CO/)\n"
"Language: es_CO\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. module: website_sale_options
#: code:addons/website_sale_options/models/sale_order.py:44
#: code:addons/website_sale_options/models/sale_order.py:56
#, python-format
msgid ""
"%s\n"
"Option for: %s"
msgstr ""
"%s\n"
"Opción para: %s"
#. module: website_sale_options
#: model:product.attribute.value,name:website_sale_options.product_attribute_value_1
msgid "1 year"
msgstr "1 año"
#. module: website_sale_options
#: model:product.attribute.value,name:website_sale_options.product_attribute_value_2
msgid "2 year"
msgstr "2 años"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<i class=\"fa fa-chevron-left\"/> <span class=\"hidden-xs\">Continue "
"Shopping</span><span class=\"visible-xs-inline\">Continue</span>"
msgstr ""
"<i class=\"fa fa-chevron-left\"/> <span class=\"hidden-xs\">Continuar "
"Comprando</span><span class=\"visible-xs-inline\">Continuar</span>"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<i class=\"fa fa-shopping-cart fa-fw\"/> <span class=\"hidden-xs\">Proceed "
"to checkout</span><span class=\"visible-xs-inline\">Checkout</span>"
msgstr ""
"<i class=\"fa fa-shopping-cart fa-fw\"/> <span class=\"hidden-xs\">Proceder "
"con el pago</span><span class=\"visible-xs-inline\">Pagar</span>"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "<small>Remove from cart</small>"
msgstr "<small>Remover del carrito</small>"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<span class=\"js_item\">1 Item</span><span class=\"js_items hidden\">5 "
"Items</span><br/>"
msgstr ""
"<span class=\"js_item\">1 elemento</span><span class=\"js_items hidden\">5 "
"elementos</span><br/>"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "<strong>Add to Cart</strong>"
msgstr "<strong>Añadir al Carrito</strong>"
#. module: website_sale_options
#: model:product.attribute,name:website_sale_options.product_attribute_1
msgid "Duration"
msgstr "Duración"
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_sale_order_line_linked_line_id
msgid "Linked Order Line"
msgstr "Línea de Orden Enlazada"
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_product_template_optional_product_ids
msgid "Optional Products"
msgstr "Productos Opcionales"
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_sale_order_line_option_line_ids
msgid "Options Linked"
msgstr "Opciones Enlazadas"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Price"
msgstr "Precio"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product"
msgstr "Producto"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_product_template
msgid "Product Template"
msgstr "Plantilla de Producto"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product not available"
msgstr "Producto no disponible"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product to add in your shopping cart"
msgstr "Producto a añadir en su carrito de compra"
#. module: website_sale_options
#: model:ir.model.fields,help:website_sale_options.field_product_template_optional_product_ids
msgid "Products to propose when add to cart."
msgstr "Productos a proponer cuando añada al carrito."
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Quantity"
msgstr "Cantidad"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_sale_order
msgid "Sales Order"
msgstr "Pedido de Venta"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_sale_order_line
msgid "Sales Order Line"
msgstr "Línea Orden de Venta"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Select Your Options:"
msgstr "Seleccione Sus Opciones:"
#. module: website_sale_options
#: model:product.product,name:website_sale_options.product_product_1
#: model:product.product,name:website_sale_options.service_cost_01
#: model:product.template,name:website_sale_options.product_product_1_product_template
#: model:product.template,name:website_sale_options.service_cost_01_product_template
msgid "Warranty"
msgstr "Garantía"
#. module: website_sale_options
#: model:product.product,description_sale:website_sale_options.product_product_1
#: model:product.product,description_sale:website_sale_options.service_cost_01
#: model:product.template,description_sale:website_sale_options.product_product_1_product_template
#: model:product.template,description_sale:website_sale_options.service_cost_01_product_template
msgid ""
"Warranty, issued to the purchaser of an article by its manufacturer, "
"promising to repair or replace it if necessary within a specified period of "
"time."
msgstr ""
"Garantía, emitida al comprador de un artículo por su fabricante, con la "
"promesa de reparar o sustituir si es necesario en un plazo determinado de "
"tiempo."
#. module: website_sale_options
#. openerp-web
#: code:addons/website_sale_options/static/src/js/website_sale.test.js:13
#, python-format
msgid "click in modal on 'Proceed to checkout' button"
msgstr "haga clic en el botón 'Proceder con el pago'"
-165
View File
@@ -1,165 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * website_sale_options
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: Odoo 9.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-08-18 14:08+0000\n"
"PO-Revision-Date: 2015-09-08 10:57+0000\n"
"Last-Translator: Martin Trigaux\n"
"Language-Team: Spanish (Costa Rica) (http://www.transifex.com/odoo/odoo-9/"
"language/es_CR/)\n"
"Language: es_CR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. module: website_sale_options
#: code:addons/website_sale_options/models/sale_order.py:44
#: code:addons/website_sale_options/models/sale_order.py:56
#, python-format
msgid ""
"%s\n"
"Option for: %s"
msgstr ""
#. module: website_sale_options
#: model:product.attribute.value,name:website_sale_options.product_attribute_value_1
msgid "1 year"
msgstr ""
#. module: website_sale_options
#: model:product.attribute.value,name:website_sale_options.product_attribute_value_2
msgid "2 year"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<i class=\"fa fa-chevron-left\"/> <span class=\"hidden-xs\">Continue "
"Shopping</span><span class=\"visible-xs-inline\">Continue</span>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<i class=\"fa fa-shopping-cart fa-fw\"/> <span class=\"hidden-xs\">Proceed "
"to checkout</span><span class=\"visible-xs-inline\">Checkout</span>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "<small>Remove from cart</small>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<span class=\"js_item\">1 Item</span><span class=\"js_items hidden\">5 "
"Items</span><br/>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "<strong>Add to Cart</strong>"
msgstr ""
#. module: website_sale_options
#: model:product.attribute,name:website_sale_options.product_attribute_1
msgid "Duration"
msgstr "Duración"
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_sale_order_line_linked_line_id
msgid "Linked Order Line"
msgstr ""
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_product_template_optional_product_ids
msgid "Optional Products"
msgstr ""
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_sale_order_line_option_line_ids
msgid "Options Linked"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Price"
msgstr "Precio"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product"
msgstr "Producto"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_product_template
msgid "Product Template"
msgstr "Plantilla de producto"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product not available"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product to add in your shopping cart"
msgstr ""
#. module: website_sale_options
#: model:ir.model.fields,help:website_sale_options.field_product_template_optional_product_ids
msgid "Products to propose when add to cart."
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Quantity"
msgstr "Cantidad"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_sale_order
msgid "Sales Order"
msgstr "Pedido de venta"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_sale_order_line
msgid "Sales Order Line"
msgstr "Línea pedido de venta"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Select Your Options:"
msgstr ""
#. module: website_sale_options
#: model:product.product,name:website_sale_options.product_product_1
#: model:product.product,name:website_sale_options.service_cost_01
#: model:product.template,name:website_sale_options.product_product_1_product_template
#: model:product.template,name:website_sale_options.service_cost_01_product_template
msgid "Warranty"
msgstr ""
#. module: website_sale_options
#: model:product.product,description_sale:website_sale_options.product_product_1
#: model:product.product,description_sale:website_sale_options.service_cost_01
#: model:product.template,description_sale:website_sale_options.product_product_1_product_template
#: model:product.template,description_sale:website_sale_options.service_cost_01_product_template
msgid ""
"Warranty, issued to the purchaser of an article by its manufacturer, "
"promising to repair or replace it if necessary within a specified period of "
"time."
msgstr ""
#. module: website_sale_options
#. openerp-web
#: code:addons/website_sale_options/static/src/js/website_sale.test.js:13
#, python-format
msgid "click in modal on 'Proceed to checkout' button"
msgstr ""
-165
View File
@@ -1,165 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * website_sale_options
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: Odoo 9.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-08-18 14:08+0000\n"
"PO-Revision-Date: 2015-12-22 17:35+0000\n"
"Last-Translator: Martin Trigaux\n"
"Language-Team: Spanish (Dominican Republic) (http://www.transifex.com/odoo/"
"odoo-9/language/es_DO/)\n"
"Language: es_DO\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. module: website_sale_options
#: code:addons/website_sale_options/models/sale_order.py:44
#: code:addons/website_sale_options/models/sale_order.py:56
#, python-format
msgid ""
"%s\n"
"Option for: %s"
msgstr ""
#. module: website_sale_options
#: model:product.attribute.value,name:website_sale_options.product_attribute_value_1
msgid "1 year"
msgstr ""
#. module: website_sale_options
#: model:product.attribute.value,name:website_sale_options.product_attribute_value_2
msgid "2 year"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<i class=\"fa fa-chevron-left\"/> <span class=\"hidden-xs\">Continue "
"Shopping</span><span class=\"visible-xs-inline\">Continue</span>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<i class=\"fa fa-shopping-cart fa-fw\"/> <span class=\"hidden-xs\">Proceed "
"to checkout</span><span class=\"visible-xs-inline\">Checkout</span>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "<small>Remove from cart</small>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<span class=\"js_item\">1 Item</span><span class=\"js_items hidden\">5 "
"Items</span><br/>"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "<strong>Add to Cart</strong>"
msgstr ""
#. module: website_sale_options
#: model:product.attribute,name:website_sale_options.product_attribute_1
msgid "Duration"
msgstr "Duración"
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_sale_order_line_linked_line_id
msgid "Linked Order Line"
msgstr ""
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_product_template_optional_product_ids
msgid "Optional Products"
msgstr ""
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_sale_order_line_option_line_ids
msgid "Options Linked"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Price"
msgstr "Precio"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product"
msgstr "Producto"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_product_template
msgid "Product Template"
msgstr "Plantilla producto"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product not available"
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product to add in your shopping cart"
msgstr ""
#. module: website_sale_options
#: model:ir.model.fields,help:website_sale_options.field_product_template_optional_product_ids
msgid "Products to propose when add to cart."
msgstr ""
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Quantity"
msgstr "Cantidad"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_sale_order
msgid "Sales Order"
msgstr "Pedido de venta"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_sale_order_line
msgid "Sales Order Line"
msgstr "Línea pedido de venta"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Select Your Options:"
msgstr ""
#. module: website_sale_options
#: model:product.product,name:website_sale_options.product_product_1
#: model:product.product,name:website_sale_options.service_cost_01
#: model:product.template,name:website_sale_options.product_product_1_product_template
#: model:product.template,name:website_sale_options.service_cost_01_product_template
msgid "Warranty"
msgstr "Garantía"
#. module: website_sale_options
#: model:product.product,description_sale:website_sale_options.product_product_1
#: model:product.product,description_sale:website_sale_options.service_cost_01
#: model:product.template,description_sale:website_sale_options.product_product_1_product_template
#: model:product.template,description_sale:website_sale_options.service_cost_01_product_template
msgid ""
"Warranty, issued to the purchaser of an article by its manufacturer, "
"promising to repair or replace it if necessary within a specified period of "
"time."
msgstr ""
#. module: website_sale_options
#. openerp-web
#: code:addons/website_sale_options/static/src/js/website_sale.test.js:13
#, python-format
msgid "click in modal on 'Proceed to checkout' button"
msgstr ""
-178
View File
@@ -1,178 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * website_sale_options
#
# Translators:
# Antonio Trueba, 2016
# Rick Hunter <rick_hunter_ec@yahoo.com>, 2016
msgid ""
msgstr ""
"Project-Id-Version: Odoo 9.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-08-18 14:08+0000\n"
"PO-Revision-Date: 2016-01-23 05:08+0000\n"
"Last-Translator: Rick Hunter <rick_hunter_ec@yahoo.com>\n"
"Language-Team: Spanish (Ecuador) (http://www.transifex.com/odoo/odoo-9/"
"language/es_EC/)\n"
"Language: es_EC\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. module: website_sale_options
#: code:addons/website_sale_options/models/sale_order.py:44
#: code:addons/website_sale_options/models/sale_order.py:56
#, python-format
msgid ""
"%s\n"
"Option for: %s"
msgstr ""
"%s\n"
"Opción para: %s"
#. module: website_sale_options
#: model:product.attribute.value,name:website_sale_options.product_attribute_value_1
msgid "1 year"
msgstr "1 año"
#. module: website_sale_options
#: model:product.attribute.value,name:website_sale_options.product_attribute_value_2
msgid "2 year"
msgstr "2 años"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<i class=\"fa fa-chevron-left\"/> <span class=\"hidden-xs\">Continue "
"Shopping</span><span class=\"visible-xs-inline\">Continue</span>"
msgstr ""
"<i class=\"fa fa-chevron-left\"/> <span class=\"hidden-xs\">Continuar "
"comprando</span><span class=\"visible-xs-inline\">Continuar</span>"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<i class=\"fa fa-shopping-cart fa-fw\"/> <span class=\"hidden-xs\">Proceed "
"to checkout</span><span class=\"visible-xs-inline\">Checkout</span>"
msgstr ""
"<i class=\"fa fa-shopping-cart fa-fw\"/> <span class=\"hidden-xs\">Proceder "
"al pago</span><span class=\"visible-xs-inline\">Revisar</span>"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "<small>Remove from cart</small>"
msgstr "<small>Eliminar del carro</small>"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid ""
"<span class=\"js_item\">1 Item</span><span class=\"js_items hidden\">5 "
"Items</span><br/>"
msgstr ""
"<span class=\"js_item\">1 elemento</span><span class=\"js_items hidden\">5 "
"elementos</span><br/>"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "<strong>Add to Cart</strong>"
msgstr "<strong>Añadir al Carrito</strong>"
#. module: website_sale_options
#: model:product.attribute,name:website_sale_options.product_attribute_1
msgid "Duration"
msgstr "Duración"
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_sale_order_line_linked_line_id
msgid "Linked Order Line"
msgstr "Línea de pedido de venta relacionada"
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_product_template_optional_product_ids
msgid "Optional Products"
msgstr "Productos opcionales"
#. module: website_sale_options
#: model:ir.model.fields,field_description:website_sale_options.field_sale_order_line_option_line_ids
msgid "Options Linked"
msgstr "Opciones relacionadas"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Price"
msgstr "Precio"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product"
msgstr "Producto"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_product_template
msgid "Product Template"
msgstr "Plantilla producto"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product not available"
msgstr "Producto no disponible"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Product to add in your shopping cart"
msgstr "Producto a añadir en su carro"
#. module: website_sale_options
#: model:ir.model.fields,help:website_sale_options.field_product_template_optional_product_ids
msgid "Products to propose when add to cart."
msgstr "Productos a añadir en su carro"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Quantity"
msgstr "Cantidad"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_sale_order
msgid "Sales Order"
msgstr "Aviso para pedido de venta"
#. module: website_sale_options
#: model:ir.model,name:website_sale_options.model_sale_order_line
msgid "Sales Order Line"
msgstr "Línea pedido de venta"
#. module: website_sale_options
#: model:ir.ui.view,arch_db:website_sale_options.modal
msgid "Select Your Options:"
msgstr "Seleccione sus opciones:"
#. module: website_sale_options
#: model:product.product,name:website_sale_options.product_product_1
#: model:product.product,name:website_sale_options.service_cost_01
#: model:product.template,name:website_sale_options.product_product_1_product_template
#: model:product.template,name:website_sale_options.service_cost_01_product_template
msgid "Warranty"
msgstr "Garantía"
#. module: website_sale_options
#: model:product.product,description_sale:website_sale_options.product_product_1
#: model:product.product,description_sale:website_sale_options.service_cost_01
#: model:product.template,description_sale:website_sale_options.product_product_1_product_template
#: model:product.template,description_sale:website_sale_options.service_cost_01_product_template
msgid ""
"Warranty, issued to the purchaser of an article by its manufacturer, "
"promising to repair or replace it if necessary within a specified period of "
"time."
msgstr ""
"Garantía, emitida al comprador de un artículo por su fabricante, con la "
"promesa de repararlo, o sustituirlo si es necesario, en un plazo determinado "
"de tiempo."
#. module: website_sale_options
#. openerp-web
#: code:addons/website_sale_options/static/src/js/website_sale.test.js:13
#, python-format
msgid "click in modal on 'Proceed to checkout' button"
msgstr "haga clic en modal en el botón 'Proceder al Pago'"

Some files were not shown because too many files have changed in this diff Show More