[IMP][REF] product,sale,p_o_s: pricelist usability refactoring
Manage pricelists through a common configuration between sale and point_of_sale Remove lots of technical fields Improved Pricelist Flow ----------------------- if pricelists enabled (product.group_product_pricelist), two modes are proposed: * basic(default): pricelists enabled, but only with basic rules * advanced: pricelists enabled, but with advanced rules (product.group_sale_pricelist) When pricelists are enabled, we can always access them through the menu OR access their product-targeted rules from the products/templates views.
This commit is contained in:
@@ -8,36 +8,3 @@ class ResConfigSettings(models.TransientModel):
|
||||
|
||||
sale_tax_id = fields.Many2one('account.tax', string="Default Sale Tax", related='company_id.account_sale_tax_id', readonly=False)
|
||||
module_pos_mercury = fields.Boolean(string="Integrated Card Payments", help="The transactions are processed by Vantiv. Set your Vantiv credentials on the related payment journal.")
|
||||
pos_sales_price = fields.Boolean("Multiple Product Prices", config_parameter='point_of_sale.pos_sales_price')
|
||||
pos_pricelist_setting = fields.Selection([
|
||||
('percentage', 'Multiple prices per product (e.g. customer segments, currencies)'),
|
||||
('formula', 'Price computed from formulas (discounts, margins, roundings)')
|
||||
], string="POS Pricelists", config_parameter='point_of_sale.pos_pricelist_setting')
|
||||
|
||||
@api.onchange('pos_sales_price')
|
||||
def _onchange_pos_sales_price(self):
|
||||
if not self.pos_sales_price:
|
||||
self.pos_pricelist_setting = False
|
||||
if self.pos_sales_price and not self.pos_pricelist_setting:
|
||||
self.pos_pricelist_setting = 'percentage'
|
||||
|
||||
@api.onchange('pos_pricelist_setting')
|
||||
def _onchange_pos_pricelist_setting(self):
|
||||
if self.pos_pricelist_setting == 'percentage':
|
||||
self.update({
|
||||
'group_product_pricelist': True,
|
||||
'group_sale_pricelist': True,
|
||||
'group_pricelist_item': False,
|
||||
})
|
||||
elif self.pos_pricelist_setting == 'formula':
|
||||
self.update({
|
||||
'group_product_pricelist': False,
|
||||
'group_sale_pricelist': True,
|
||||
'group_pricelist_item': True,
|
||||
})
|
||||
else:
|
||||
self.update({
|
||||
'group_product_pricelist': False,
|
||||
'group_sale_pricelist': False,
|
||||
'group_pricelist_item': False,
|
||||
})
|
||||
|
||||
@@ -29,24 +29,21 @@
|
||||
</div>
|
||||
<h2>Pricing</h2>
|
||||
<div class="row mt16 o_settings_container">
|
||||
<div class="col-12 col-lg-6 o_setting_box" title="There are two ways to manage pricelists: 1) Multiple prices per product (e.g. quantity, shop-specific) : must be set in the Sales tab of the product detail form. 2) Price computed from formulas (discounts, margins, rounding) : must be set in the pricelist form.">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="pos_sales_price"/>
|
||||
<field name="group_product_pricelist"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="pos_sales_price"/>
|
||||
<label for="group_product_pricelist"/>
|
||||
<div class="text-muted">
|
||||
Set multiple prices per product, automated discounts, etc.
|
||||
</div>
|
||||
<div class="content-group" attrs="{'invisible': [('pos_sales_price' ,'=', False)]}">
|
||||
<div class="content-group" attrs="{'invisible': [('group_product_pricelist' ,'=', False)]}">
|
||||
<div class="mt16">
|
||||
<field name="group_product_pricelist" invisible="1"/>
|
||||
<field name="group_sale_pricelist" invisible="1"/>
|
||||
<field name="group_pricelist_item" invisible="1"/>
|
||||
<field name="pos_pricelist_setting" widget="radio" class="o_light_label"/>
|
||||
<field name="product_pricelist_setting" widget="radio" class="o_light_label"/>
|
||||
</div>
|
||||
<button name="%(product.product_pricelist_action2)d" icon="fa-arrow-right" type="action" string="Pricelists"
|
||||
attrs="{'invisible': [('pos_pricelist_setting', '=', False)]}" class="btn-link"/>
|
||||
<button name="%(product.product_pricelist_action2)d" icon="fa-arrow-right" type="action" string="Pricelists" groups="product.group_sale_pricelist" class="btn-link"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -113,8 +113,7 @@ class ProductProduct(models.Model):
|
||||
volume = fields.Float('Volume')
|
||||
weight = fields.Float('Weight', digits='Stock Weight')
|
||||
|
||||
pricelist_item_ids = fields.Many2many(
|
||||
'product.pricelist.item', 'Pricelist Items', compute='_get_pricelist_items')
|
||||
pricelist_item_count = fields.Integer("Number of price rules", compute="_compute_variant_item_count")
|
||||
|
||||
packaging_ids = fields.One2many(
|
||||
'product.packaging', 'product_id', 'Product Packages',
|
||||
@@ -317,12 +316,12 @@ class ProductProduct(models.Model):
|
||||
else:
|
||||
product.product_template_attribute_value_ids += values_per_template[product.product_tmpl_id.id][pav.id]
|
||||
|
||||
def _get_pricelist_items(self):
|
||||
def _compute_variant_item_count(self):
|
||||
for product in self:
|
||||
product.pricelist_item_ids = self.env['product.pricelist.item'].search([
|
||||
'|',
|
||||
('product_id', '=', product.id),
|
||||
('product_tmpl_id', '=', product.product_tmpl_id.id)]).ids
|
||||
domain = ['|',
|
||||
'&', ('product_tmpl_id', '=', product.product_tmpl_id.id), ('applied_on', '=', '1_product'),
|
||||
'&', ('product_id', '=', product.id), ('applied_on', '=', '0_product_variant')]
|
||||
product.pricelist_item_count = self.env['product.pricelist.item'].search_count(domain)
|
||||
|
||||
@api.constrains('attribute_value_ids')
|
||||
def _check_attribute_value_ids(self):
|
||||
@@ -581,6 +580,25 @@ class ProductProduct(models.Model):
|
||||
return _('Products: ') + self.env['product.category'].browse(self._context['categ_id']).name
|
||||
return res
|
||||
|
||||
def open_pricelist_rules(self):
|
||||
self.ensure_one()
|
||||
domain = ['|',
|
||||
'&', ('product_tmpl_id', '=', self.product_tmpl_id.id), ('applied_on', '=', '1_product'),
|
||||
'&', ('product_id', '=', self.id), ('applied_on', '=', '0_product_variant')]
|
||||
return {
|
||||
'name': _('Price Rules'),
|
||||
'view_mode': 'tree,form',
|
||||
'views': [(self.env.ref('product.product_pricelist_item_tree_view_from_product').id, 'tree'), (False, 'form')],
|
||||
'res_model': 'product.pricelist.item',
|
||||
'type': 'ir.actions.act_window',
|
||||
'target': 'current',
|
||||
'domain': domain,
|
||||
'context': {
|
||||
'default_product_id': self.id,
|
||||
'default_applied_on': '0_product_variant',
|
||||
}
|
||||
}
|
||||
|
||||
def open_product_template(self):
|
||||
""" Utility method used to add an "Open Template" button in product views """
|
||||
self.ensure_one()
|
||||
|
||||
@@ -7,7 +7,6 @@ from odoo import api, fields, models, tools, _
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
|
||||
|
||||
|
||||
class Pricelist(models.Model):
|
||||
_name = "product.pricelist"
|
||||
_description = "Pricelist"
|
||||
@@ -46,7 +45,7 @@ class Pricelist(models.Model):
|
||||
FROM ((
|
||||
SELECT pr.id, pr.name
|
||||
FROM product_pricelist pr JOIN
|
||||
res_currency cur ON
|
||||
res_currency cur ON
|
||||
(pr.currency_id = cur.id)
|
||||
WHERE pr.name || ' (' || cur.name || ')' = %(name)s
|
||||
)
|
||||
@@ -59,7 +58,7 @@ class Pricelist(models.Model):
|
||||
tr.name = 'product.pricelist,name' AND
|
||||
tr.lang = %(lang)s
|
||||
) JOIN
|
||||
res_currency cur ON
|
||||
res_currency cur ON
|
||||
(pr.currency_id = cur.id)
|
||||
WHERE tr.value || ' (' || cur.name || ')' = %(name)s
|
||||
)
|
||||
@@ -374,20 +373,22 @@ class ResCountryGroup(models.Model):
|
||||
|
||||
class PricelistItem(models.Model):
|
||||
_name = "product.pricelist.item"
|
||||
_description = "Pricelist Item"
|
||||
_description = "Pricelist Rule"
|
||||
_order = "applied_on, min_quantity desc, categ_id desc, id desc"
|
||||
# NOTE: if you change _order on this model, make sure it matches the SQL
|
||||
# query built in _compute_price_rule() above in this file to avoid
|
||||
# inconstencies and undeterministic issues.
|
||||
|
||||
def _get_default_applied_on(self):
|
||||
return self.env.user.has_group('product.group_product_pricelist') and '1_product' or '3_global'
|
||||
def _default_pricelist_id(self):
|
||||
return self.env['product.pricelist'].search([
|
||||
'|', ('company_id', '=', False),
|
||||
('company_id', '=', self.env.company.id)], limit=1)
|
||||
|
||||
product_tmpl_id = fields.Many2one(
|
||||
'product.template', 'Product Template', ondelete='cascade',
|
||||
'product.template', 'Product', ondelete='cascade',
|
||||
help="Specify a template if this rule only applies to one product template. Keep empty otherwise.")
|
||||
product_id = fields.Many2one(
|
||||
'product.product', 'Product', ondelete='cascade',
|
||||
'product.product', 'Product Variant', ondelete='cascade',
|
||||
help="Specify a product if this rule only applies to one product. Keep empty otherwise.")
|
||||
categ_id = fields.Many2one(
|
||||
'product.category', 'Product Category', ondelete='cascade',
|
||||
@@ -398,11 +399,11 @@ class PricelistItem(models.Model):
|
||||
"than or equal to the minimum quantity specified in this field.\n"
|
||||
"Expressed in the default unit of measure of the product.")
|
||||
applied_on = fields.Selection([
|
||||
('3_global', 'Global'),
|
||||
('3_global', 'All Products'),
|
||||
('2_product_category', ' Product Category'),
|
||||
('1_product', 'Product'),
|
||||
('0_product_variant', 'Product Variant')], "Apply On",
|
||||
default=_get_default_applied_on, required=True,
|
||||
default='3_global', required=True,
|
||||
help='Pricelist Item applicable on selected option')
|
||||
base = fields.Selection([
|
||||
('list_price', 'Sales Price'),
|
||||
@@ -414,7 +415,7 @@ class PricelistItem(models.Model):
|
||||
'Cost Price : The base price will be the cost price.\n'
|
||||
'Other Pricelist : Computation of the base price based on another Pricelist.')
|
||||
base_pricelist_id = fields.Many2one('product.pricelist', 'Other Pricelist')
|
||||
pricelist_id = fields.Many2one('product.pricelist', 'Pricelist', index=True, ondelete='cascade', required=True)
|
||||
pricelist_id = fields.Many2one('product.pricelist', 'Pricelist', index=True, ondelete='cascade', required=True, default=_default_pricelist_id)
|
||||
price_surcharge = fields.Float(
|
||||
'Price Surcharge', digits='Product Price',
|
||||
help='Specify the fixed amount to add or substract(if negative) to the amount calculated with the discount.')
|
||||
@@ -436,13 +437,15 @@ class PricelistItem(models.Model):
|
||||
currency_id = fields.Many2one(
|
||||
'res.currency', 'Currency',
|
||||
readonly=True, related='pricelist_id.currency_id', store=True)
|
||||
active = fields.Boolean(
|
||||
readonly=True, related="pricelist_id.active", store=True)
|
||||
date_start = fields.Date('Start Date', help="Starting date for the pricelist item validation")
|
||||
date_end = fields.Date('End Date', help="Ending valid for the pricelist item validation")
|
||||
compute_price = fields.Selection([
|
||||
('fixed', 'Fix Price'),
|
||||
('fixed', 'Fixed Price'),
|
||||
('percentage', 'Percentage (discount)'),
|
||||
('formula', 'Formula')], index=True, default='fixed')
|
||||
fixed_price = fields.Float('Fixed Price', digits='Product Price')
|
||||
('formula', 'Formula')], index=True, default='fixed', required=True)
|
||||
fixed_price = fields.Monetary('Fixed Price')
|
||||
percent_price = fields.Float('Percentage Price')
|
||||
# functional fields used for usability purposes
|
||||
name = fields.Char(
|
||||
@@ -464,35 +467,39 @@ class PricelistItem(models.Model):
|
||||
raise ValidationError(_('The minimum margin should be lower than the maximum margin.'))
|
||||
return True
|
||||
|
||||
@api.depends('categ_id', 'product_tmpl_id', 'product_id', 'compute_price', 'fixed_price', \
|
||||
@api.constrains('product_id', 'product_tmpl_id', 'categ_id')
|
||||
def _check_product_consistency(self):
|
||||
for item in self:
|
||||
if item.applied_on == "2_product_category" and not item.categ_id:
|
||||
raise ValidationError(_("Please specify the category for which this rule should be applied"))
|
||||
elif item.applied_on == "1_product" and not item.product_tmpl_id:
|
||||
raise ValidationError(_("Please specify the product for which this rule should be applied"))
|
||||
elif item.applied_on == "0_product_variant" and not item.product_id:
|
||||
raise ValidationError(_("Please specify the product variant for which this rule should be applied"))
|
||||
|
||||
@api.depends('applied_on', 'categ_id', 'product_tmpl_id', 'product_id', 'compute_price', 'fixed_price', \
|
||||
'pricelist_id', 'percent_price', 'price_discount', 'price_surcharge')
|
||||
def _get_pricelist_item_name_price(self):
|
||||
for item in self:
|
||||
if item.categ_id:
|
||||
item.name = _("Category: %s") % (item.categ_id.name)
|
||||
elif item.product_tmpl_id:
|
||||
item.name = item.product_tmpl_id.name
|
||||
elif item.product_id:
|
||||
item.name = item.product_id.display_name.replace('[%s]' % item.product_id.code, '')
|
||||
if item.categ_id and item.applied_on == '2_product_category':
|
||||
item.name = _("Category: %s") % (item.categ_id.display_name)
|
||||
elif item.product_tmpl_id and item.applied_on == '1_product':
|
||||
item.name = _("Product: %s") % (item.product_tmpl_id.display_name)
|
||||
elif item.product_id and item.applied_on == '0_product_variant':
|
||||
item.name = _("Variant: %s") % (item.product_id.with_context(display_default_code=False).display_name)
|
||||
else:
|
||||
item.name = _("All Products")
|
||||
|
||||
if item.compute_price == 'fixed':
|
||||
item.price = ("%s %s") % (item.fixed_price, item.pricelist_id.currency_id.name)
|
||||
if item.currency_id.position == 'after':
|
||||
item.price = "%s %s" % (item.fixed_price, item.currency_id.symbol)
|
||||
else:
|
||||
item.price = "%s %s" % (item.currency_id.symbol, item.fixed_price)
|
||||
elif item.compute_price == 'percentage':
|
||||
item.price = _("%s %% discount") % (item.percent_price)
|
||||
else:
|
||||
item.price = _("%s %% discount and %s surcharge") % (item.price_discount, item.price_surcharge)
|
||||
|
||||
@api.onchange('applied_on')
|
||||
def _onchange_applied_on(self):
|
||||
if self.applied_on != '0_product_variant':
|
||||
self.product_id = False
|
||||
if self.applied_on != '1_product':
|
||||
self.product_tmpl_id = False
|
||||
if self.applied_on != '2_product_category':
|
||||
self.categ_id = False
|
||||
|
||||
@api.onchange('compute_price')
|
||||
def _onchange_compute_price(self):
|
||||
if self.compute_price != 'fixed':
|
||||
@@ -508,14 +515,65 @@ class PricelistItem(models.Model):
|
||||
'price_max_margin': 0.0,
|
||||
})
|
||||
|
||||
@api.onchange('product_id')
|
||||
def _onchange_product_id(self):
|
||||
has_product_id = self.filtered('product_id')
|
||||
for item in has_product_id:
|
||||
item.product_tmpl_id = item.product_id.product_tmpl_id
|
||||
if self.env.context.get('default_applied_on', False) == '1_product':
|
||||
# If a product variant is specified, apply on variants instead
|
||||
# Reset if product variant is removed
|
||||
has_product_id.update({'applied_on': '0_product_variant'})
|
||||
(self - has_product_id).update({'applied_on': '1_product'})
|
||||
|
||||
@api.onchange('product_tmpl_id')
|
||||
def _onchange_product_tmpl_id(self):
|
||||
has_tmpl_id = self.filtered('product_tmpl_id')
|
||||
for item in has_tmpl_id:
|
||||
if item.product_id and item.product_id.product_tmpl_id != item.product_tmpl_id:
|
||||
item.product_id = None
|
||||
|
||||
@api.onchange('product_id', 'product_tmpl_id', 'categ_id')
|
||||
def _onchane_rule_content(self):
|
||||
if not self.user_has_groups('product.group_sale_pricelist') and not self.env.context.get('default_applied_on', False):
|
||||
# If advanced pricelists are disabled (applied_on field is not visible)
|
||||
# AND we aren't coming from a specific product template/variant.
|
||||
variants_rules = self.filtered('product_id')
|
||||
template_rules = (self-variants_rules).filtered('product_tmpl_id')
|
||||
variants_rules.update({'applied_on': '0_product_variant'})
|
||||
template_rules.update({'applied_on': '1_product'})
|
||||
(self-variants_rules-template_rules).update({'applied_on': '3_global'})
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for values in vals_list:
|
||||
if values.get('applied_on', False):
|
||||
# Ensure item consistency for later searches.
|
||||
applied_on = values['applied_on']
|
||||
if applied_on == '3_global':
|
||||
values.update(dict(product_id=None, product_tmpl_id=None, categ_id=None))
|
||||
elif applied_on == '2_product_category':
|
||||
values.update(dict(product_id=None, product_tmpl_id=None))
|
||||
elif applied_on == '1_product':
|
||||
values.update(dict(product_id=None, categ_id=None))
|
||||
elif applied_on == '0_product_variant':
|
||||
values.update(dict(categ_id=None))
|
||||
return super(PricelistItem, self).create(vals_list)
|
||||
|
||||
def write(self, values):
|
||||
if values.get('applied_on', False):
|
||||
# Ensure item consistency for later searches.
|
||||
applied_on = values['applied_on']
|
||||
if applied_on == '3_global':
|
||||
values.update(dict(product_id=None, product_tmpl_id=None, categ_id=None))
|
||||
elif applied_on == '2_product_category':
|
||||
values.update(dict(product_id=None, product_tmpl_id=None))
|
||||
elif applied_on == '1_product':
|
||||
values.update(dict(product_id=None, categ_id=None))
|
||||
elif applied_on == '0_product_variant':
|
||||
values.update(dict(categ_id=None))
|
||||
res = super(PricelistItem, self).write(values)
|
||||
# When the pricelist changes we need the product.template price
|
||||
# to be invalided and recomputed.
|
||||
self.invalidate_cache()
|
||||
return res
|
||||
|
||||
@api.onchange('product_tmpl_id')
|
||||
def _onchange_product_tmpl_id(self):
|
||||
if self.user_has_groups('product.group_product_variant'):
|
||||
self.product_id = False
|
||||
|
||||
@@ -148,11 +148,17 @@ class ProductTemplate(models.Model):
|
||||
'Internal Reference', compute='_compute_default_code',
|
||||
inverse='_set_default_code', store=True)
|
||||
|
||||
item_ids = fields.One2many('product.pricelist.item', 'product_tmpl_id', 'Pricelist Items')
|
||||
pricelist_item_count = fields.Integer("Number of price rules", compute="_compute_item_count")
|
||||
|
||||
can_image_1024_be_zoomed = fields.Boolean("Can Image 1024 be zoomed", compute='_compute_can_image_1024_be_zoomed', store=True)
|
||||
has_configurable_attributes = fields.Boolean("Is a configurable product", compute='_compute_has_configurable_attributes', store=True)
|
||||
|
||||
def _compute_item_count(self):
|
||||
for template in self:
|
||||
# Pricelist item count counts the rules applicable on current template or on its variants.
|
||||
template.pricelist_item_count = template.env['product.pricelist.item'].search_count([
|
||||
'|', ('product_tmpl_id', '=', template.id), ('product_id', 'in', template.product_variant_ids.ids)])
|
||||
|
||||
@api.depends('image_1920', 'image_1024')
|
||||
def _compute_can_image_1024_be_zoomed(self):
|
||||
for template in self:
|
||||
@@ -454,6 +460,26 @@ class ProductTemplate(models.Model):
|
||||
'', args=[('id', 'in', list(searched_ids))],
|
||||
operator='ilike', limit=limit, name_get_uid=name_get_uid)
|
||||
|
||||
def open_pricelist_rules(self):
|
||||
self.ensure_one()
|
||||
domain = ['|',
|
||||
('product_tmpl_id', '=', self.id),
|
||||
('product_id', 'in', self.product_variant_ids.ids)]
|
||||
return {
|
||||
'name': _('Price Rules'),
|
||||
'view_mode': 'tree,form',
|
||||
'views': [(self.env.ref('product.product_pricelist_item_tree_view_from_product').id, 'tree'), (False, 'form')],
|
||||
'res_model': 'product.pricelist.item',
|
||||
'type': 'ir.actions.act_window',
|
||||
'target': 'current',
|
||||
'domain': domain,
|
||||
'context': {
|
||||
'default_product_tmpl_id': self.id,
|
||||
'default_applied_on': '1_product',
|
||||
'product_without_variants': self.product_variant_count == 1,
|
||||
},
|
||||
}
|
||||
|
||||
def price_compute(self, price_type, uom=False, currency=False, company=False):
|
||||
# TDE FIXME: delegate to template or not ? fields are reencoded here ...
|
||||
# compatibility about context keys used a bit everywhere in the code
|
||||
|
||||
@@ -14,14 +14,17 @@ class ResConfigSettings(models.TransientModel):
|
||||
module_sale_product_matrix = fields.Boolean("Product Grid Configurator")
|
||||
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",
|
||||
group_product_pricelist = fields.Boolean("Pricelists",
|
||||
implied_group='product.group_product_pricelist')
|
||||
group_sale_pricelist = fields.Boolean("Advanced Pricelists",
|
||||
implied_group='product.group_sale_pricelist',
|
||||
help="""Allows to manage different prices based on rules per category of customers.
|
||||
Example: 10% for retailers, promotion of 5 EUR on this product, etc.""")
|
||||
group_product_pricelist = fields.Boolean("Show pricelists On Products",
|
||||
implied_group='product.group_product_pricelist')
|
||||
group_pricelist_item = fields.Boolean("Show pricelists to customers",
|
||||
implied_group='product.group_pricelist_item')
|
||||
product_pricelist_setting = fields.Selection([
|
||||
('basic', 'Multiple prices per product'),
|
||||
('advanced', 'Advanced price rules (discounts, formulas)')
|
||||
], default='basic', string="Pricelists Method", config_parameter='product.product_pricelist_setting',
|
||||
help="Multiple prices: Pricelists with fixed price rules by product,\nAdvanced rules: enables advanced price rules for pricelists.")
|
||||
product_weight_in_lbs = fields.Selection([
|
||||
('0', 'Kilogram'),
|
||||
('1', 'Pound'),
|
||||
@@ -47,6 +50,23 @@ class ResConfigSettings(models.TransientModel):
|
||||
if self.module_sale_product_configurator and not self.group_product_variant:
|
||||
self.group_product_variant = True
|
||||
|
||||
@api.onchange('group_multi_currency')
|
||||
def _onchange_group_multi_currency(self):
|
||||
if self.group_product_pricelist and not self.group_sale_pricelist and self.group_multi_currency:
|
||||
self.group_sale_pricelist = True
|
||||
|
||||
@api.onchange('group_product_pricelist')
|
||||
def _onchange_group_sale_pricelist(self):
|
||||
if not self.group_product_pricelist and self.group_sale_pricelist:
|
||||
self.group_sale_pricelist = False
|
||||
|
||||
@api.onchange('product_pricelist_setting')
|
||||
def _onchange_product_pricelist_setting(self):
|
||||
if self.product_pricelist_setting == 'basic':
|
||||
self.group_sale_pricelist = False
|
||||
else:
|
||||
self.group_sale_pricelist = True
|
||||
|
||||
def set_values(self):
|
||||
super(ResConfigSettings, self).set_values()
|
||||
if not self.group_discount_per_so_line:
|
||||
|
||||
@@ -7,11 +7,6 @@
|
||||
<field name="category_id" ref="base.module_category_hidden"/>
|
||||
</record>
|
||||
|
||||
<record id="group_pricelist_item" model="res.groups">
|
||||
<field name="name">Manage Pricelist Items</field>
|
||||
<field name="category_id" ref="base.module_category_hidden"/>
|
||||
</record>
|
||||
|
||||
<record id="group_product_pricelist" model="res.groups">
|
||||
<field name="name">Pricelists On Product</field>
|
||||
<field name="category_id" ref="base.module_category_hidden"/>
|
||||
|
||||
@@ -33,14 +33,14 @@ class TestProductPricelist(TransactionCase):
|
||||
}), (0, 0, {
|
||||
'name': '10% Discount on Assemble Computer',
|
||||
'applied_on': '1_product',
|
||||
'product_id': self.ipad_retina_display.id,
|
||||
'product_tmpl_id': self.ipad_retina_display.product_tmpl_id.id,
|
||||
'compute_price': 'formula',
|
||||
'base': 'list_price',
|
||||
'price_discount': 10
|
||||
}), (0, 0, {
|
||||
'name': '1 surchange on Laptop',
|
||||
'applied_on': '1_product',
|
||||
'product_id': self.laptop_E5023.id,
|
||||
'product_tmpl_id': self.laptop_E5023.product_tmpl_id.id,
|
||||
'compute_price': 'formula',
|
||||
'base': 'list_price',
|
||||
'price_surcharge': 1
|
||||
@@ -54,7 +54,7 @@ class TestProductPricelist(TransactionCase):
|
||||
'price_discount': 5
|
||||
}), (0, 0, {
|
||||
'name': '30% Discount on all products',
|
||||
'applied_on': '0_product_variant',
|
||||
'applied_on': '3_global',
|
||||
'date_start': '2011-12-27',
|
||||
'date_end': '2011-12-31',
|
||||
'compute_price': 'formula',
|
||||
|
||||
@@ -2,18 +2,87 @@
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<record id="product_pricelist_item_tree_view" model="ir.ui.view">
|
||||
<field name="name">product.pricelist.item.tree</field>
|
||||
<record model="ir.ui.view" id="product_pricelist_item_view_search">
|
||||
<field name="name">product.pricelist.item.search</field>
|
||||
<field name="model">product.pricelist.item</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Pricelist Items">
|
||||
<field colspan="4" name="name"/>
|
||||
<field name="product_id" groups="product.group_product_variant"/>
|
||||
<field name="product_tmpl_id"/>
|
||||
<field name="categ_id"/>
|
||||
<field name="min_quantity"/>
|
||||
<field name="date_start"/>
|
||||
<field name="date_end"/>
|
||||
<search string="Products Price Rules Search">
|
||||
<filter name="Product Rule" domain="[('applied_on', '=', '1_product')]"/>
|
||||
<filter name="Variant Rule" domain="[('applied_on', '=', '0_product_variant')]" groups="product.group_product_variant"/>
|
||||
<separator/>
|
||||
<field name="pricelist_id"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
<field name="currency_id" groups="base.group_multi_currency"/>
|
||||
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Product" name="groupby_product" domain="[]" context="{'group_by': 'product_tmpl_id'}"/>
|
||||
<filter string="Variant"
|
||||
name="groupby_product_variant"
|
||||
domain="[('applied_on', '=', '0_product_variant')]"
|
||||
context="{'group_by': 'product_tmpl_id'}"
|
||||
groups="product.group_product_variant"/>
|
||||
<filter string="Pricelist"
|
||||
name="groupby_vendor"
|
||||
domain="[]"
|
||||
context="{'group_by': 'pricelist_id'}"
|
||||
groups="product.group_sale_pricelist"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="product_pricelist_item_tree_view" model="ir.ui.view">
|
||||
<field name="name">product.pricelist.item.tree</field>
|
||||
<field name="model">product.pricelist.item</field>
|
||||
<field name="priority">10</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Price Rules">
|
||||
<field name="pricelist_id"/>
|
||||
<field name="name" string="Applied On"/>
|
||||
<field name="price"/>
|
||||
<field name="min_quantity" colspan="4"/>
|
||||
<field name="date_start" optional="hide"/>
|
||||
<field name="date_end" optional="hide"/>
|
||||
<field name="company_id" groups="base.group_multi_company" optional="show"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="product_pricelist_item_tree_view_from_product" model="ir.ui.view">
|
||||
<!-- Access and edit price rules from a given product/product variant -->
|
||||
<field name="name">product.pricelist.item.tree</field>
|
||||
<field name="model">product.pricelist.item</field>
|
||||
<field name="priority">100</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Pricelist Items" editable="bottom">
|
||||
<!-- Scope = coming from a product/product template -->
|
||||
<field name="pricelist_id" string="Pricelist" options="{'no_create_edit':1, 'no_open': 1}"/>
|
||||
<field name="name" string="Applied On"/>
|
||||
<field name="categ_id" invisible="1"/>
|
||||
<field name="product_tmpl_id"
|
||||
invisible="context.get('active_model')!='product.category'"
|
||||
attrs="{'required': [('applied_on', '=', '1_product')]}"
|
||||
domain="[('categ_id', '=', context.get('default_categ_id', True))]"
|
||||
options="{'no_create_edit':1, 'no_open': 1}"/>
|
||||
<field name="product_id"
|
||||
groups="product.group_product_variant"
|
||||
invisible="context.get('product_without_variants', False)"
|
||||
readonly="context.get('active_model')=='product.product'"
|
||||
attrs="{'required': [('applied_on', '=', '0_product_variant')]}"
|
||||
domain="['|', '|',
|
||||
('id', '=', context.get('default_product_id', 0)),
|
||||
('product_tmpl_id', '=', context.get('default_product_tmpl_id', 0)),
|
||||
('categ_id', '=', context.get('default_categ_id', 0)),
|
||||
]"
|
||||
options="{'no_create_edit':1, 'no_open': 1}"
|
||||
/>
|
||||
<field name="min_quantity" colspan="4"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="fixed_price" string="Price" required='1'/>
|
||||
<field name="date_start" optional="show"/>
|
||||
<field name="date_end" optional="show"/>
|
||||
<field name="applied_on" invisible="1"/>
|
||||
<field name="company_id" groups="base.group_multi_company" optional="show" options="{'no_create_edit':1, 'no_open': 1}"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
@@ -23,61 +92,79 @@
|
||||
<field name="model">product.pricelist.item</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Pricelist Items">
|
||||
<h1><field name="name"/></h1>
|
||||
<group>
|
||||
<group>
|
||||
<field name="applied_on" widget="radio"/>
|
||||
<field name="categ_id" attrs="{'invisible':[('applied_on', '!=', '2_product_category')], 'required':[('applied_on', '=', '2_product_category')]}"/>
|
||||
<field name="product_tmpl_id" attrs="{'invisible':[('applied_on', '!=', '1_product')],'required':[('applied_on', '=', '1_product')]}" string="Product"/>
|
||||
<field name="product_id" attrs="{'invisible':[('applied_on', '!=', '0_product_variant')],'required':[('applied_on', '=', '0_product_variant')]}" string="Product Variant"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="min_quantity"/>
|
||||
<field name="date_start"/>
|
||||
<field name="date_end"/>
|
||||
</group>
|
||||
</group>
|
||||
<separator string="Price Computation"/>
|
||||
<group>
|
||||
<group>
|
||||
<label for="compute_price" string="Compute Price"/>
|
||||
<div>
|
||||
<field name="compute_price" widget="radio"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<div attrs="{'invisible':[('compute_price', '!=', 'fixed')]}">
|
||||
<field name="fixed_price" nolabel= "1" widget='monetary' options="{'currency_field': 'currency_id'}"/></div>
|
||||
<div attrs="{'invisible':[('compute_price', '!=', 'percentage')]}">
|
||||
<field name="percent_price" nolabel="1" class="oe_inline"/>%%
|
||||
</div>
|
||||
</div>
|
||||
</group>
|
||||
</group>
|
||||
<div class="oe_grey" groups="uom.group_uom">
|
||||
<p>The computed price is expressed in the default Unit of Measure of the product.</p>
|
||||
</div>
|
||||
<group col="6" attrs="{'invisible':[('compute_price', '!=', 'formula')]}">
|
||||
<field name="base" colspan="6"/>
|
||||
<sheet>
|
||||
<h1><field name="name"/></h1>
|
||||
<group>
|
||||
<group name="pricelist_rule_target">
|
||||
<field name="applied_on" widget="radio"/>
|
||||
<field name="categ_id" attrs="{
|
||||
'invisible':[('applied_on', '!=', '2_product_category')],
|
||||
'required':[('applied_on', '=', '2_product_category')]}"
|
||||
options="{'no_create':1}"/>
|
||||
<field name="product_tmpl_id" attrs="{
|
||||
'invisible':[('applied_on', '!=', '1_product')],
|
||||
'required':[('applied_on', '=', '1_product')]}"
|
||||
options="{'no_create':1}"/>
|
||||
<field name="product_id" attrs="{
|
||||
'invisible':[('applied_on', '!=', '0_product_variant')],
|
||||
'required':[('applied_on', '=', '0_product_variant')]}"
|
||||
options="{'no_create':1}"/>
|
||||
</group>
|
||||
<group name="pricelist_rule_limits">
|
||||
<field name="min_quantity"/>
|
||||
<field name="date_start"/>
|
||||
<field name="date_end"/>
|
||||
</group>
|
||||
<group name="pricelist_rule_related" groups="base.group_no_one">
|
||||
<!-- Infos from the pricelist for UI rendering (monetary fields, ...) -->
|
||||
<field name="pricelist_id" invisible="1"/>
|
||||
<field name="currency_id" groups="base.group_multi_currency"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Price Computation" name="pricelist_rule_computation" groups="product.group_sale_pricelist">
|
||||
<group name="pricelist_rule_method">
|
||||
<field name="compute_price" string="Compute Price" widget="radio"/>
|
||||
</group>
|
||||
<group name="pricelist_rule_base">
|
||||
<field name="fixed_price" attrs="{'invisible':[('compute_price', '!=', 'fixed')]}"/>
|
||||
<label for="percent_price" attrs="{'invisible':[('compute_price', '!=', 'percentage')]}"/>
|
||||
<div attrs="{'invisible':[('compute_price', '!=', 'percentage')]}">
|
||||
<field name="percent_price"
|
||||
class="oe_inline"
|
||||
attrs="{'invisible':[('compute_price', '!=', 'percentage')]}"/>
|
||||
%%
|
||||
</div>
|
||||
<field name="base" attrs="{'invisible':[('compute_price', '!=', 'formula')]}"/>
|
||||
<field name="base_pricelist_id" attrs="{
|
||||
'invisible': ['|', ('compute_price', '!=', 'formula'), ('base', '!=', 'pricelist')],
|
||||
'required': [('compute_price', '=', 'formula'), ('base', '=', 'pricelist')],
|
||||
'readonly': [('base', '!=', 'pricelist')]}"/>
|
||||
</group>
|
||||
</group>
|
||||
<div class="oe_grey" groups="uom.group_uom">
|
||||
<p>The computed price is expressed in the default Unit of Measure of the product.</p>
|
||||
</div>
|
||||
<group name="pricelist_rule_advanced" col="6" attrs="{'invisible':[('compute_price', '!=', 'formula')]}" groups="product.group_sale_pricelist">
|
||||
<label for="" string="New Price ="/>
|
||||
<div>
|
||||
<span attrs="{'invisible':[('base', '!=', 'list_price')]}">Sales Price - </span>
|
||||
<span attrs="{'invisible':[('base', '!=', 'standard_price')]}">Cost - </span>
|
||||
<span attrs="{'invisible':[('base', '!=', 'pricelist')]}">Other Pricelist - </span>
|
||||
</div>
|
||||
<label for="price_discount"/>
|
||||
<div class="o_row">
|
||||
<field name="price_discount"/>
|
||||
<span>%%</span>
|
||||
</div>
|
||||
<label string=" + " for="price_surcharge"/>
|
||||
<field name="price_surcharge" nolabel="1"/>
|
||||
|
||||
<label for="" string="New Price ="/>
|
||||
<div>
|
||||
<span attrs="{'invisible':[('base', '!=', 'list_price')]}">Sales Price - </span>
|
||||
<span attrs="{'invisible':[('base', '!=', 'standard_price')]}">Cost - </span>
|
||||
<span attrs="{'invisible':[('base', '!=', 'pricelist')]}">Other Pricelist - </span>
|
||||
</div>
|
||||
<label for="price_discount"/>
|
||||
<div class="o_row">
|
||||
<field name="price_discount"/>
|
||||
<span>%%</span>
|
||||
</div>
|
||||
<label string=" + " for="price_surcharge"/>
|
||||
<field name="price_surcharge" nolabel="1"/>
|
||||
|
||||
<field name="price_round" string="Rounding Method"/>
|
||||
<field name="price_min_margin" string="Min. Margin"/>
|
||||
<field name="price_max_margin" string="Max. Margin"/>
|
||||
|
||||
<field name="base_pricelist_id" attrs="{'invisible':[('base', '!=', 'pricelist')],'required': [('base','=', 'pricelist')], 'readonly': [('base','!=', 'pricelist')]}"/>
|
||||
</group>
|
||||
<field name="price_round" string="Rounding Method"/>
|
||||
<field name="price_min_margin" string="Min. Margin"/>
|
||||
<field name="price_max_margin" string="Max. Margin"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
@@ -142,28 +229,44 @@
|
||||
<h1><field name="name" placeholder="e.g. USD Retailers"/></h1>
|
||||
</div>
|
||||
<group>
|
||||
<field name="discount_policy" groups="product.group_discount_per_so_line" widget="radio"/>
|
||||
<field name="currency_id" groups="base.group_multi_currency"/>
|
||||
<field name="active" invisible="1"/>
|
||||
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
|
||||
<field name="country_group_ids"/>
|
||||
<group name="pricelist_settings">
|
||||
<field name="currency_id" groups="base.group_multi_currency"/>
|
||||
<field name="active" invisible="1"/>
|
||||
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
|
||||
</group>
|
||||
</group>
|
||||
<div>
|
||||
<separator string="Pricelist Items"/>
|
||||
<field name="item_ids" nolabel="1" context="{'default_base':'list_price'}" groups="product.group_pricelist_item">
|
||||
<tree string="Pricelist Items">
|
||||
<field name="name" string="Applicable On"/>
|
||||
<field name="min_quantity"/>
|
||||
<field name="date_start"/>
|
||||
<field name="date_end"/>
|
||||
<field name="price" string="Price"/>
|
||||
<field name="base" invisible="1"/>
|
||||
<field name="price_discount" invisible="1"/>
|
||||
<field name="applied_on" invisible="1"/>
|
||||
<field name="compute_price" invisible="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
</div>
|
||||
<notebook>
|
||||
<page name="pricelist_rules" string="Price Rules">
|
||||
<field name="item_ids" nolabel="1" context="{'default_base':'list_price'}">
|
||||
<tree string="Pricelist Items" editable="bottom">
|
||||
<field name="product_tmpl_id" string="Products" required="1"/>
|
||||
<field name="product_id" string="Variants"
|
||||
groups="product.group_product_variant"
|
||||
domain="[('product_tmpl_id', '=', product_tmpl_id)]"
|
||||
options="{'no_create':1}"/>
|
||||
<field name="min_quantity"/>
|
||||
<field name="fixed_price" string="Price"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="pricelist_id" invisible="1"/>
|
||||
<!-- Pricelist ID is here only for related fields to be correctly computed -->
|
||||
<field name="date_start"/>
|
||||
<field name="date_end"/>
|
||||
<field name="base" invisible="1"/>
|
||||
<field name="applied_on" invisible="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
<page name="pricelist_config" string="Configuration">
|
||||
<group>
|
||||
<group name="pricelist_availability" string="Availability">
|
||||
<field name="country_group_ids" widget="many2many_tags"/>
|
||||
</group>
|
||||
<group name="pricelist_discounts" groups="product.group_discount_per_so_line" string="Discounts">
|
||||
<field name="discount_policy" widget="radio"/>
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
@@ -173,21 +276,25 @@
|
||||
<field name="name">product.pricelist.form.inherit</field>
|
||||
<field name="model">product.pricelist</field>
|
||||
<field name="inherit_id" ref="product.product_pricelist_view"/>
|
||||
<field name="groups_id" eval="[(4, ref('product.group_product_pricelist'))]"/>
|
||||
<field name="groups_id" eval="[(4, ref('product.group_sale_pricelist'))]"/>
|
||||
<field name="arch" type="xml">
|
||||
<!-- When in advanced pricelist mode : pricelist rules
|
||||
Should open in a form view and not be editable inline anymore.
|
||||
-->
|
||||
<field name="item_ids" position="replace">
|
||||
<field name="item_ids" nolabel="1" context="{'default_base':'list_price'}">
|
||||
<tree string="Pricelist Items" editable="bottom">
|
||||
<field name="product_tmpl_id" string="Products" required="1"/>
|
||||
<field name="product_id" string="Variants" groups="product.group_product_variant" domain="[('product_tmpl_id', '=', product_tmpl_id)]"/>
|
||||
<field name="fixed_price" string="Price" widget="monetary"/>
|
||||
<field name="min_quantity"/>
|
||||
<field name="date_start"/>
|
||||
<field name="date_end"/>
|
||||
<field name="base" invisible="1"/>
|
||||
<field name="applied_on" invisible="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
<field name="item_ids" nolabel="1" context="{'default_base':'list_price'}" groups="product.group_product_pricelist">
|
||||
<tree string="Pricelist Items">
|
||||
<field name="name" string="Applicable On"/>
|
||||
<field name="min_quantity"/>
|
||||
<field name="price" string="Price"/>
|
||||
<field name="date_start"/>
|
||||
<field name="date_end"/>
|
||||
<field name="base" invisible="1"/>
|
||||
<field name="price_discount" invisible="1"/>
|
||||
<field name="applied_on" invisible="1"/>
|
||||
<field name="compute_price" invisible="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
@@ -221,5 +328,11 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="product_pricelist_item_action" model="ir.actions.act_window">
|
||||
<field name="name">Price Rules</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">product.pricelist.item</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -45,7 +45,20 @@
|
||||
<field name="categ_id" string="Product Category"/>
|
||||
</group>
|
||||
<group name="group_standard_price">
|
||||
<field name="list_price" string="Sales Price" widget='monetary' options="{'currency_field': 'currency_id', 'field_digits': True}"/>
|
||||
<label for="list_price"/>
|
||||
<div name="pricing">
|
||||
<field name="list_price" string="Sales Price" widget='monetary' options="{'currency_field': 'currency_id', 'field_digits': True}"/>
|
||||
<button name="open_pricelist_rules" icon="fa-arrow-right" type="object"
|
||||
groups="product.group_product_pricelist" class="oe_inline">
|
||||
<field name="pricelist_item_count" attrs="{'invisible': [('pricelist_item_count', '=', 0)]}"/>
|
||||
<span attrs="{'invisible': [('pricelist_item_count', '=', 1)]}">
|
||||
Extra Prices
|
||||
</span>
|
||||
<span attrs="{'invisible': [('pricelist_item_count', '!=', 1)]}">
|
||||
Extra Price
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<label for="standard_price" groups="base.group_user" attrs="{'invisible': [('product_variant_count', '>', 1), ('is_product_variant', '=', False)]}"/>
|
||||
<div name="standard_price_uom" groups="base.group_user" attrs="{'invisible': [('product_variant_count', '>', 1), ('is_product_variant', '=', False)]}" class="o_row">
|
||||
<field name="standard_price" widget='monetary' options="{'currency_field': 'cost_currency_id'}"/>
|
||||
@@ -68,71 +81,6 @@
|
||||
</page>
|
||||
<page string="Sales" attrs="{'invisible':[('sale_ok','=',False)]}" name="sales" invisible="1">
|
||||
<group name="sale">
|
||||
<group groups="product.group_product_pricelist">
|
||||
<div name="pricelist_item" groups="product.group_product_pricelist">
|
||||
<separator string="Pricing"/>
|
||||
<field name="item_ids" nolabel="1" mode="tree,kanban" context="{'default_base':'list_price', 'default_applied_on' :'1_product'}">
|
||||
<tree string="Pricelist Items" editable="bottom">
|
||||
<field name="pricelist_id" string="Pricelist" required='1'/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="fixed_price" string="Price" required='1' widget='monetary' options="{'currency_field': 'currency_id'}"/>
|
||||
<field name="min_quantity"/>
|
||||
<field name="date_start"/>
|
||||
<field name="date_end"/>
|
||||
<field name="applied_on" invisible="1"/>
|
||||
</tree>
|
||||
<kanban string="Pricelist Items" class="o_kanban_mobile">
|
||||
<field name="pricelist_id"/>
|
||||
<field name="currency_id"/>
|
||||
<field name="fixed_price"/>
|
||||
<field name="min_quantity"/>
|
||||
<field name="date_start"/>
|
||||
<field name="date_end"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div t-attf-class="oe_kanban_global_click">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<strong>
|
||||
<field name='pricelist_id'/>: <field name="fixed_price" widget='monetary' options="{'currency_field': 'currency_id'}"/>
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<span><t t-esc='record.min_quantity.string'/>: <t t-esc='record.min_quantity.value'/></span>
|
||||
</div>
|
||||
</div>
|
||||
<span t-if='record.date_start.value || record.date_end.value'>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
Valid <span t-if='record.date_start.value'> from <t t-esc='record.date_start.value'/></span><span t-if='record.date_end.value'> until <t t-esc='record.date_end.value'/></span>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
<form string="Pricelist Items">
|
||||
<h1><field name="name"/></h1>
|
||||
<group>
|
||||
<group>
|
||||
<field name="applied_on" invisible="1"/>
|
||||
<field name="pricelist_id" string="Pricelist" required='1'/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="fixed_price" string="Price" required='1' widget='monetary' options="{'currency_field': 'currency_id'}"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="min_quantity"/>
|
||||
<field name="date_start"/>
|
||||
<field name="date_end"/>
|
||||
</group>
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
</div>
|
||||
</group>
|
||||
<group name="email_template_and_project" invisible="1"/>
|
||||
</group>
|
||||
<group string="Sales Description" name="description">
|
||||
@@ -383,29 +331,15 @@
|
||||
<attribute name="name">lst_price</attribute>
|
||||
<attribute name="attrs">{'readonly': [('product_variant_count', '>', 1)]}</attribute>
|
||||
</field>
|
||||
<xpath expr="//label[@for='list_price']" position="replace">
|
||||
<label for="lst_price"/>
|
||||
</xpath>
|
||||
<group name="packaging" position="attributes">
|
||||
<attribute name="attrs">{'invisible': 0}</attribute>
|
||||
</group>
|
||||
<field name="name" position="after">
|
||||
<field name="product_tmpl_id" class="oe_inline" readonly="1" invisible="1" attrs="{'required': [('id', '!=', False)]}"/>
|
||||
</field>
|
||||
<field name="item_ids" position="inside">
|
||||
<field name="pricelist_item_ids" widget="many2many">
|
||||
<tree string="Pricelist Items">
|
||||
<field name="pricelist_id"/>
|
||||
<field name="name" string="Applicable On"/>
|
||||
<field name="min_quantity"/>
|
||||
<field name="date_start"/>
|
||||
<field name="date_end"/>
|
||||
<field name="price" string="Price"/>
|
||||
<field name="base" invisible="1"/>
|
||||
<field name="sequence" invisible="1"/>
|
||||
<field name="price_discount" invisible="1"/>
|
||||
<field name="applied_on" invisible="1"/>
|
||||
<field name="compute_price" invisible="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
</field>
|
||||
<xpath expr="//div[hasclass('oe_title')]" position="inside">
|
||||
<field name="attribute_value_ids" widget="many2many_tags" readonly="1"
|
||||
groups="product.group_product_variant"/>
|
||||
|
||||
@@ -109,8 +109,7 @@ class ProductTemplate(models.Model):
|
||||
def get_import_templates(self):
|
||||
res = super(ProductTemplate, self).get_import_templates()
|
||||
if self.env.context.get('sale_multi_pricelist_product_template'):
|
||||
sale_pricelist_setting = self.env['ir.config_parameter'].sudo().get_param('sale.sale_pricelist_setting')
|
||||
if sale_pricelist_setting == 'percentage':
|
||||
if self.user_has_groups('product.group_sale_pricelist'):
|
||||
return [{
|
||||
'label': _('Import Template for Products'),
|
||||
'template': '/product/static/xls/product_template.xls'
|
||||
|
||||
@@ -15,16 +15,6 @@ class ResConfigSettings(models.TransientModel):
|
||||
portal_confirmation_sign = fields.Boolean(related='company_id.portal_confirmation_sign', string='Online Signature', readonly=False)
|
||||
portal_confirmation_pay = fields.Boolean(related='company_id.portal_confirmation_pay', string='Online Payment', readonly=False)
|
||||
group_sale_delivery_address = fields.Boolean("Customer Addresses", implied_group='sale.group_delivery_invoice_address')
|
||||
multi_sales_price = fields.Boolean("Multiple Sales Prices per Product")
|
||||
multi_sales_price_method = fields.Selection([
|
||||
('percentage', 'Multiple prices per product (e.g. customer segments, currencies)'),
|
||||
('formula', 'Prices computed from formulas (discounts, margins, roundings)')
|
||||
], default='percentage', string="Pricelists Method")
|
||||
sale_pricelist_setting = fields.Selection([
|
||||
('fixed', 'A single sales price per product'),
|
||||
('percentage', 'Multiple prices per product (e.g. customer segments, currencies)'),
|
||||
('formula', 'Price computed from formulas (discounts, margins, roundings)')
|
||||
], string="Pricelists", config_parameter='sale.sale_pricelist_setting')
|
||||
group_proforma_sales = fields.Boolean(string="Pro-Forma Invoice", implied_group='sale.group_proforma_sales',
|
||||
help="Allows you to send pro-forma invoice.")
|
||||
group_sale_order_dates = fields.Boolean("Delivery Date", implied_group='sale.group_sale_order_dates')
|
||||
@@ -80,35 +70,6 @@ class ResConfigSettings(models.TransientModel):
|
||||
if self.default_invoice_policy != 'order':
|
||||
self.env['ir.config_parameter'].set_param('sale.automatic_invoice', False)
|
||||
|
||||
@api.onchange('multi_sales_price', 'multi_sales_price_method')
|
||||
def _onchange_sale_price(self):
|
||||
if self.multi_sales_price and not self.multi_sales_price_method:
|
||||
self.update({
|
||||
'multi_sales_price_method': 'percentage',
|
||||
})
|
||||
self.sale_pricelist_setting = self.multi_sales_price and self.multi_sales_price_method or 'fixed'
|
||||
|
||||
@api.onchange('sale_pricelist_setting')
|
||||
def _onchange_sale_pricelist_setting(self):
|
||||
if self.sale_pricelist_setting == 'percentage':
|
||||
self.update({
|
||||
'group_product_pricelist': True,
|
||||
'group_sale_pricelist': True,
|
||||
'group_pricelist_item': False,
|
||||
})
|
||||
elif self.sale_pricelist_setting == 'formula':
|
||||
self.update({
|
||||
'group_product_pricelist': False,
|
||||
'group_sale_pricelist': True,
|
||||
'group_pricelist_item': True,
|
||||
})
|
||||
else:
|
||||
self.update({
|
||||
'group_product_pricelist': False,
|
||||
'group_sale_pricelist': False,
|
||||
'group_pricelist_item': False,
|
||||
})
|
||||
|
||||
@api.onchange('use_quotation_validity_days')
|
||||
def _onchange_use_quotation_validity_days(self):
|
||||
if self.quotation_validity_days <= 0:
|
||||
@@ -121,15 +82,3 @@ class ResConfigSettings(models.TransientModel):
|
||||
return {
|
||||
'warning': {'title': "Warning", 'message': "Quotation Validity is required and must be greater than 0."},
|
||||
}
|
||||
|
||||
@api.model
|
||||
def get_values(self):
|
||||
res = super(ResConfigSettings, self).get_values()
|
||||
ICPSudo = self.env['ir.config_parameter'].sudo()
|
||||
sale_pricelist_setting = ICPSudo.get_param('sale.sale_pricelist_setting')
|
||||
res.update(
|
||||
multi_sales_price=sale_pricelist_setting in ['percentage', 'formula'],
|
||||
multi_sales_price_method=sale_pricelist_setting in ['percentage', 'formula'] and sale_pricelist_setting or False,
|
||||
sale_pricelist_setting=sale_pricelist_setting,
|
||||
)
|
||||
return res
|
||||
|
||||
@@ -137,15 +137,12 @@ class TestSaleOrder(TestCommonSaleNoChart):
|
||||
line._onchange_discount()
|
||||
# Check that pricelist of the SO has been applied on the sale order lines or not
|
||||
for line in self.sale_order.order_line:
|
||||
if self.sale_order.pricelist_id in line.product_id.item_ids.mapped('pricelist_id'): # concerned by pricelist (all except product_deliver)
|
||||
if line.product_id == self.product_order:
|
||||
self.assertEquals(line.price_unit, self.pricelist_discount_incl_item3.fixed_price, 'Price of product_order should be %s applied on the order line' % (self.pricelist_discount_incl_item3.fixed_price,))
|
||||
else: # only services (service_order and service_deliver)
|
||||
for item in self.sale_order.pricelist_id.item_ids.filtered(lambda l: l.product_tmpl_id == line.product_id.product_tmpl_id):
|
||||
price = item.percent_price
|
||||
self.assertEquals(price, (line.product_id.list_price - line.price_unit) / line.product_id.list_price * 100, 'Pricelist of the SO should be applied on an order line %s' % (line.product_id.name,))
|
||||
else:
|
||||
self.assertEquals(line.price_unit, line.product_id.list_price, 'Pricelist of the SO should not be applied on an order line')
|
||||
if line.product_id == self.product_order:
|
||||
self.assertEquals(line.price_unit, self.pricelist_discount_incl_item3.fixed_price, 'Price of product_order should be %s applied on the order line' % (self.pricelist_discount_incl_item3.fixed_price,))
|
||||
else: # only services (service_order and service_deliver)
|
||||
for item in self.sale_order.pricelist_id.item_ids.filtered(lambda l: l.product_tmpl_id == line.product_id.product_tmpl_id):
|
||||
price = item.percent_price
|
||||
self.assertEquals(price, (line.product_id.list_price - line.price_unit) / line.product_id.list_price * 100, 'Pricelist of the SO should be applied on an order line %s' % (line.product_id.name,))
|
||||
|
||||
def test_sale_with_pricelist_discount_excluded(self):
|
||||
""" Test SO with the pricelist 'discount displayed' and check discount and unit price appeared on its lines """
|
||||
|
||||
@@ -112,25 +112,21 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box" title="There are two ways to manage pricelists: 1) Multiple prices per product: must be set in the Sales tab of the product detail form. 2) Price computed from formulas: must be set in the pricelist form.">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="multi_sales_price"/>
|
||||
<field name="group_product_pricelist"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="multi_sales_price"/>
|
||||
<label for="group_product_pricelist"/>
|
||||
<div class="text-muted">
|
||||
Set multiple prices per product, automated discounts, etc.
|
||||
</div>
|
||||
<div class="content-group" attrs="{'invisible': [('multi_sales_price' ,'=', False)]}">
|
||||
<div class="content-group" attrs="{'invisible': [('group_product_pricelist' ,'=', False)]}">
|
||||
<div class="mt16">
|
||||
<field name="group_product_pricelist" invisible="1"/>
|
||||
<field name="group_sale_pricelist" invisible="1"/>
|
||||
<field name="group_pricelist_item" invisible="1"/>
|
||||
<field name="multi_sales_price_method" widget="radio" class="o_light_label"/>
|
||||
<field name="sale_pricelist_setting" invisible="1" widget="radio"/>
|
||||
<field name="product_pricelist_setting" widget="radio" class="o_light_label"/>
|
||||
</div>
|
||||
<button name="%(product.product_pricelist_action2)d" icon="fa-arrow-right" type="action" string="Pricelists"
|
||||
attrs="{'invisible': [('sale_pricelist_setting', '=', False)]}" class="btn-link"/>
|
||||
<button name="%(product.product_pricelist_action2)d" icon="fa-arrow-right" type="action" string="Pricelists" groups="product.group_product_pricelist" class="btn-link"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
<menuitem id="next_id_16" name="Units of Measure" parent="sale.prod_config_main" sequence="6" groups="uom.group_uom" active="False"/>
|
||||
<menuitem action="uom.product_uom_form_action" id="menu_product_uom_form_action" parent="sale.prod_config_main" sequence="7" groups="uom.group_uom" active="False"/>
|
||||
<menuitem action="uom.product_uom_categ_form_action" id="menu_product_uom_categ_form_action" parent="sale.prod_config_main" sequence="8" groups="uom.group_uom" active="False"/>
|
||||
<menuitem id="menu_product_pricelist_main" name="Pricelists" parent="product_menu_catalog" action="product.product_pricelist_action2" groups="product.group_sale_pricelist" sequence="3" active="False"/>
|
||||
<menuitem id="menu_product_pricelist_main" name="Pricelists" parent="product_menu_catalog" action="product.product_pricelist_action2" groups="product.group_product_pricelist" sequence="3" active="False"/>
|
||||
|
||||
<record id="sale_order_view_activity" model="ir.ui.view">
|
||||
<field name="name">sale.order.activity</field>
|
||||
@@ -308,7 +308,7 @@
|
||||
<label for="date_order" string="Order Date"/>
|
||||
</div>
|
||||
<field name="date_order" attrs="{'required': [('state', 'in', ['sale', 'done'])], 'invisible': [('state', 'in', ['draft', 'sent'])]}" nolabel="1"/>
|
||||
<field name="pricelist_id" groups="product.group_sale_pricelist"/>
|
||||
<field name="pricelist_id" groups="product.group_product_pricelist"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="payment_term_id" options="{'no_open':True,'no_create': True}"/>
|
||||
</group>
|
||||
|
||||
@@ -19,7 +19,7 @@ class TestUi(odoo.tests.HttpCase):
|
||||
self.env.ref('base.user_admin').write({
|
||||
'groups_id': [
|
||||
(4, self.env.ref('product.group_product_variant').id),
|
||||
(4, self.env.ref('product.group_sale_pricelist').id),
|
||||
(4, self.env.ref('product.group_product_pricelist').id),
|
||||
],
|
||||
})
|
||||
|
||||
@@ -88,7 +88,7 @@ class TestUi(odoo.tests.HttpCase):
|
||||
self.env.ref('base.user_admin').write({
|
||||
'groups_id': [
|
||||
(4, self.env.ref('product.group_product_variant').id),
|
||||
(4, self.env.ref('product.group_sale_pricelist').id),
|
||||
(4, self.env.ref('product.group_product_pricelist').id),
|
||||
],
|
||||
})
|
||||
|
||||
@@ -131,7 +131,7 @@ class TestUi(odoo.tests.HttpCase):
|
||||
self.env.ref('account.group_show_line_subtotals_tax_included').users |= admin
|
||||
|
||||
# Active pricelist on SO
|
||||
self.env.ref('product.group_sale_pricelist').users |= admin
|
||||
self.env.ref('product.group_product_pricelist').users |= admin
|
||||
|
||||
# Add a 15% tax on desk
|
||||
tax = self.env['account.tax'].create({'name': "Test tax", 'amount': 15})
|
||||
@@ -145,7 +145,7 @@ class TestUi(odoo.tests.HttpCase):
|
||||
product_template = self.env.ref('product.product_product_4_product_template')
|
||||
pricelist = self.env.ref('product.list0')
|
||||
|
||||
if not pricelist.item_ids.filtered(lambda i: i.product_tmpl_id == product_template and i.price_discount == 20):
|
||||
if not pricelist.item_ids.filtered(lambda i: i.product_tmpl_id == product_template and i.applied_on == '1_product' and i.price_discount == 20):
|
||||
self.env['product.pricelist.item'].create({
|
||||
'base': 'list_price',
|
||||
'applied_on': '1_product',
|
||||
|
||||
@@ -76,5 +76,5 @@ class ResConfigSettings(models.TransientModel):
|
||||
def _onchange_group_discount_per_so_line(self):
|
||||
if self.group_discount_per_so_line:
|
||||
self.update({
|
||||
'multi_sales_price': True,
|
||||
'group_product_pricelist': True,
|
||||
})
|
||||
|
||||
@@ -12,17 +12,6 @@
|
||||
</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>
|
||||
<field name="inherit_id" ref="product.product_pricelist_view"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="currency_id" position="before">
|
||||
<field name="code"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="product_product_website_tree_view">
|
||||
<field name="name">product.product.website.tree</field>
|
||||
<field name="model">product.product</field>
|
||||
@@ -188,11 +177,14 @@
|
||||
<field name="inherit_id" ref="product.product_pricelist_view" />
|
||||
<field name="model">product.pricelist</field>
|
||||
<field name="arch" type="xml">
|
||||
<field name='currency_id' position='after'>
|
||||
<field name="selectable"/>
|
||||
<field name="website_id" options="{'no_create': True}" />
|
||||
</field>
|
||||
</field>
|
||||
<xpath expr="//group[@name='pricelist_availability']" position="after">
|
||||
<group name="pricelist_website" string="Website">
|
||||
<field name="website_id" options="{'no_create': True}"/>
|
||||
<field name="selectable"/>
|
||||
<field name="code"/>
|
||||
</group>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="website_sale_pricelist_tree_view" model="ir.ui.view">
|
||||
|
||||
@@ -102,20 +102,19 @@
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box" id="pricelists_setting" title="With the first mode you can set several prices in the product config form (from Sales tab). With the second one, you set prices and computation rules from Pricelists.">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="multi_sales_price"/>
|
||||
<field name="group_product_pricelist"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="multi_sales_price"/>
|
||||
<label for="group_product_pricelist"/>
|
||||
<div class="text-muted">
|
||||
Apply specific prices per country, discounts, etc.
|
||||
</div>
|
||||
<div class="content-group mt16" attrs="{'invisible': [('multi_sales_price', '=', False)]}">
|
||||
<div class="content-group mt16" attrs="{'invisible': [('group_product_pricelist', '=', False)]}">
|
||||
<field name="group_sale_pricelist" invisible="1"/>
|
||||
<field name="group_pricelist_item" invisible="1"/>
|
||||
<field name="group_product_pricelist" invisible="1"/>
|
||||
<field name="multi_sales_price_method" class="o_light_label" widget="radio"/>
|
||||
<field name="product_pricelist_setting" class="o_light_label" widget="radio"/>
|
||||
</div>
|
||||
<div attrs="{'invisible': [('multi_sales_price', '=', False)]}">
|
||||
<div attrs="{'invisible': [('group_product_pricelist', '=', False)]}">
|
||||
<button type="action" name="%(product.product_pricelist_action2)d" string="Pricelists" class="btn-link" icon="fa-arrow-right"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user