349 lines
15 KiB
Python
349 lines
15 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||
|
|
||
|
from odoo import api, fields, models, _
|
||
|
from odoo.exceptions import UserError
|
||
|
|
||
|
|
||
|
class Pricelist(models.Model):
|
||
|
_name = "product.pricelist"
|
||
|
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||
|
_description = "Pricelist"
|
||
|
_rec_names_search = ['name', 'currency_id'] # TODO check if should be removed
|
||
|
_order = "sequence asc, id asc"
|
||
|
|
||
|
def _default_currency_id(self):
|
||
|
return self.env.company.currency_id.id
|
||
|
|
||
|
name = fields.Char(string="Pricelist Name", required=True, translate=True)
|
||
|
|
||
|
active = fields.Boolean(
|
||
|
string="Active",
|
||
|
default=True,
|
||
|
help="If unchecked, it will allow you to hide the pricelist without removing it.")
|
||
|
sequence = fields.Integer(default=16)
|
||
|
|
||
|
currency_id = fields.Many2one(
|
||
|
comodel_name='res.currency',
|
||
|
default=_default_currency_id,
|
||
|
required=True,
|
||
|
tracking=1,
|
||
|
)
|
||
|
|
||
|
company_id = fields.Many2one(
|
||
|
comodel_name='res.company',
|
||
|
tracking=5,
|
||
|
)
|
||
|
country_group_ids = fields.Many2many(
|
||
|
comodel_name='res.country.group',
|
||
|
relation='res_country_group_pricelist_rel',
|
||
|
column1='pricelist_id',
|
||
|
column2='res_country_group_id',
|
||
|
string="Country Groups",
|
||
|
tracking=10,
|
||
|
)
|
||
|
|
||
|
discount_policy = fields.Selection(
|
||
|
selection=[
|
||
|
('with_discount', "Discount included in the price"),
|
||
|
('without_discount', "Show public price & discount to the customer"),
|
||
|
],
|
||
|
default='with_discount',
|
||
|
required=True,
|
||
|
tracking=15,
|
||
|
)
|
||
|
|
||
|
item_ids = fields.One2many(
|
||
|
comodel_name='product.pricelist.item',
|
||
|
inverse_name='pricelist_id',
|
||
|
string="Pricelist Rules",
|
||
|
domain=[
|
||
|
'&',
|
||
|
'|', ('product_tmpl_id', '=', None), ('product_tmpl_id.active', '=', True),
|
||
|
'|', ('product_id', '=', None), ('product_id.active', '=', True),
|
||
|
],
|
||
|
copy=True)
|
||
|
|
||
|
@api.depends('currency_id')
|
||
|
def _compute_display_name(self):
|
||
|
for pricelist in self:
|
||
|
pricelist.display_name = f'{pricelist.name} ({pricelist.currency_id.name})'
|
||
|
|
||
|
def _get_products_price(self, products, *args, **kwargs):
|
||
|
"""Compute the pricelist prices for the specified products, quantity & uom.
|
||
|
|
||
|
Note: self and self.ensure_one()
|
||
|
|
||
|
:param products: recordset of products (product.product/product.template)
|
||
|
:param float quantity: quantity of products requested (in given uom)
|
||
|
:param currency: record of currency (res.currency) (optional)
|
||
|
:param uom: unit of measure (uom.uom record) (optional)
|
||
|
If not specified, prices returned are expressed in product uoms
|
||
|
:param date: date to use for price computation and currency conversions (optional)
|
||
|
:type date: date or datetime
|
||
|
|
||
|
:returns: {product_id: product price}, considering the current pricelist if any
|
||
|
:rtype: dict(int, float)
|
||
|
"""
|
||
|
self and self.ensure_one() # self is at most one record
|
||
|
return {
|
||
|
product_id: res_tuple[0]
|
||
|
for product_id, res_tuple in self._compute_price_rule(products, *args, **kwargs).items()
|
||
|
}
|
||
|
|
||
|
def _get_product_price(self, product, *args, **kwargs):
|
||
|
"""Compute the pricelist price for the specified product, qty & uom.
|
||
|
|
||
|
Note: self and self.ensure_one()
|
||
|
|
||
|
:param product: product record (product.product/product.template)
|
||
|
:param float quantity: quantity of products requested (in given uom)
|
||
|
:param currency: record of currency (res.currency) (optional)
|
||
|
:param uom: unit of measure (uom.uom record) (optional)
|
||
|
If not specified, prices returned are expressed in product uoms
|
||
|
:param date: date to use for price computation and currency conversions (optional)
|
||
|
:type date: date or datetime
|
||
|
|
||
|
:returns: unit price of the product, considering pricelist rules if any
|
||
|
:rtype: float
|
||
|
"""
|
||
|
self and self.ensure_one() # self is at most one record
|
||
|
return self._compute_price_rule(product, *args, **kwargs)[product.id][0]
|
||
|
|
||
|
def _get_product_price_rule(self, product, *args, **kwargs):
|
||
|
"""Compute the pricelist price & rule for the specified product, qty & uom.
|
||
|
|
||
|
Note: self and self.ensure_one()
|
||
|
|
||
|
:param product: product record (product.product/product.template)
|
||
|
:param float quantity: quantity of products requested (in given uom)
|
||
|
:param currency: record of currency (res.currency) (optional)
|
||
|
:param uom: unit of measure (uom.uom record) (optional)
|
||
|
If not specified, prices returned are expressed in product uoms
|
||
|
:param date: date to use for price computation and currency conversions (optional)
|
||
|
:type date: date or datetime
|
||
|
|
||
|
:returns: (product unit price, applied pricelist rule id)
|
||
|
:rtype: tuple(float, int)
|
||
|
"""
|
||
|
self and self.ensure_one() # self is at most one record
|
||
|
return self._compute_price_rule(product, *args, **kwargs)[product.id]
|
||
|
|
||
|
def _get_product_rule(self, product, *args, **kwargs):
|
||
|
"""Compute the pricelist price & rule for the specified product, qty & uom.
|
||
|
|
||
|
Note: self and self.ensure_one()
|
||
|
|
||
|
:param product: product record (product.product/product.template)
|
||
|
:param float quantity: quantity of products requested (in given uom)
|
||
|
:param currency: record of currency (res.currency) (optional)
|
||
|
:param uom: unit of measure (uom.uom record) (optional)
|
||
|
If not specified, prices returned are expressed in product uoms
|
||
|
:param date: date to use for price computation and currency conversions (optional)
|
||
|
:type date: date or datetime
|
||
|
|
||
|
:returns: applied pricelist rule id
|
||
|
:rtype: int or False
|
||
|
"""
|
||
|
self and self.ensure_one() # self is at most one record
|
||
|
return self._compute_price_rule(product, *args, compute_price=False, **kwargs)[product.id][1]
|
||
|
|
||
|
def _compute_price_rule(
|
||
|
self, products, quantity, currency=None, uom=None, date=False, compute_price=True,
|
||
|
**kwargs
|
||
|
):
|
||
|
""" Low-level method - Mono pricelist, multi products
|
||
|
Returns: dict{product_id: (price, suitable_rule) for the given pricelist}
|
||
|
|
||
|
Note: self and self.ensure_one()
|
||
|
|
||
|
:param products: recordset of products (product.product/product.template)
|
||
|
:param float quantity: quantity of products requested (in given uom)
|
||
|
:param currency: record of currency (res.currency)
|
||
|
note: currency.ensure_one()
|
||
|
:param uom: unit of measure (uom.uom record)
|
||
|
If not specified, prices returned are expressed in product uoms
|
||
|
:param date: date to use for price computation and currency conversions
|
||
|
:type date: date or datetime
|
||
|
:param bool compute_price: whether the price should be computed (default: True)
|
||
|
|
||
|
:returns: product_id: (price, pricelist_rule)
|
||
|
:rtype: dict
|
||
|
"""
|
||
|
self and self.ensure_one() # self is at most one record
|
||
|
|
||
|
currency = currency or self.currency_id or self.env.company.currency_id
|
||
|
currency.ensure_one()
|
||
|
|
||
|
if not products:
|
||
|
return {}
|
||
|
|
||
|
if not date:
|
||
|
# Used to fetch pricelist rules and currency rates
|
||
|
date = fields.Datetime.now()
|
||
|
|
||
|
# Fetch all rules potentially matching specified products/templates/categories and date
|
||
|
rules = self._get_applicable_rules(products, date, **kwargs)
|
||
|
|
||
|
results = {}
|
||
|
for product in products:
|
||
|
suitable_rule = self.env['product.pricelist.item']
|
||
|
|
||
|
product_uom = product.uom_id
|
||
|
target_uom = uom or product_uom # If no uom is specified, fall back on the product uom
|
||
|
|
||
|
# Compute quantity in product uom because pricelist rules are specified
|
||
|
# w.r.t product default UoM (min_quantity, price_surchage, ...)
|
||
|
if target_uom != product_uom:
|
||
|
qty_in_product_uom = target_uom._compute_quantity(
|
||
|
quantity, product_uom, raise_if_failure=False
|
||
|
)
|
||
|
else:
|
||
|
qty_in_product_uom = quantity
|
||
|
|
||
|
for rule in rules:
|
||
|
if rule._is_applicable_for(product, qty_in_product_uom):
|
||
|
suitable_rule = rule
|
||
|
break
|
||
|
|
||
|
if compute_price:
|
||
|
price = suitable_rule._compute_price(
|
||
|
product, quantity, target_uom, date=date, currency=currency)
|
||
|
else:
|
||
|
# Skip price computation when only the rule is requested.
|
||
|
price = 0.0
|
||
|
results[product.id] = (price, suitable_rule.id)
|
||
|
|
||
|
return results
|
||
|
|
||
|
# Split methods to ease (community) overrides
|
||
|
def _get_applicable_rules(self, products, date, **kwargs):
|
||
|
self and self.ensure_one() # self is at most one record
|
||
|
if not self:
|
||
|
return self.env['product.pricelist.item']
|
||
|
|
||
|
# Do not filter out archived pricelist items, since it means current pricelist is also archived
|
||
|
# We do not want the computation of prices for archived pricelist to always fallback on the Sales price
|
||
|
# because no rule was found (thanks to the automatic orm filtering on active field)
|
||
|
return self.env['product.pricelist.item'].with_context(active_test=False).search(
|
||
|
self._get_applicable_rules_domain(products=products, date=date, **kwargs)
|
||
|
).with_context(self.env.context)
|
||
|
|
||
|
def _get_applicable_rules_domain(self, products, date, **kwargs):
|
||
|
self and self.ensure_one() # self is at most one record
|
||
|
if products._name == 'product.template':
|
||
|
templates_domain = ('product_tmpl_id', 'in', products.ids)
|
||
|
products_domain = ('product_id.product_tmpl_id', 'in', products.ids)
|
||
|
else:
|
||
|
templates_domain = ('product_tmpl_id', 'in', products.product_tmpl_id.ids)
|
||
|
products_domain = ('product_id', 'in', products.ids)
|
||
|
|
||
|
return [
|
||
|
('pricelist_id', '=', self.id),
|
||
|
'|', ('categ_id', '=', False), ('categ_id', 'parent_of', products.categ_id.ids),
|
||
|
'|', ('product_tmpl_id', '=', False), templates_domain,
|
||
|
'|', ('product_id', '=', False), products_domain,
|
||
|
'|', ('date_start', '=', False), ('date_start', '<=', date),
|
||
|
'|', ('date_end', '=', False), ('date_end', '>=', date),
|
||
|
]
|
||
|
|
||
|
# Multi pricelists price|rule computation
|
||
|
def _price_get(self, product, quantity, **kwargs):
|
||
|
""" Multi pricelist, mono product - returns price per pricelist """
|
||
|
return {
|
||
|
key: price[0]
|
||
|
for key, price in self._compute_price_rule_multi(product, quantity, **kwargs)[product.id].items()}
|
||
|
|
||
|
def _compute_price_rule_multi(self, products, quantity, uom=None, date=False, **kwargs):
|
||
|
""" Low-level method - Multi pricelist, multi products
|
||
|
Returns: dict{product_id: dict{pricelist_id: (price, suitable_rule)} }"""
|
||
|
if not self.ids:
|
||
|
pricelists = self.search([])
|
||
|
else:
|
||
|
pricelists = self
|
||
|
results = {}
|
||
|
for pricelist in pricelists:
|
||
|
subres = pricelist._compute_price_rule(products, quantity, uom=uom, date=date, **kwargs)
|
||
|
for product_id, price in subres.items():
|
||
|
results.setdefault(product_id, {})
|
||
|
results[product_id][pricelist.id] = price
|
||
|
return results
|
||
|
|
||
|
# res.partner.property_product_pricelist field computation
|
||
|
@api.model
|
||
|
def _get_partner_pricelist_multi(self, partner_ids):
|
||
|
""" Retrieve the applicable pricelist for given partners in a given company.
|
||
|
|
||
|
It will return the first found pricelist in this order:
|
||
|
First, the pricelist of the specific property (res_id set), this one
|
||
|
is created when saving a pricelist on the partner form view.
|
||
|
Else, it will return the pricelist of the partner country group
|
||
|
Else, it will return the generic property (res_id not set)
|
||
|
Else, it will return the first available pricelist if any
|
||
|
|
||
|
:param int company_id: if passed, used for looking up properties,
|
||
|
instead of current user's company
|
||
|
:return: a dict {partner_id: pricelist}
|
||
|
"""
|
||
|
# `partner_ids` might be ID from inactive users. We should use active_test
|
||
|
# as we will do a search() later (real case for website public user).
|
||
|
Partner = self.env['res.partner'].with_context(active_test=False)
|
||
|
company_id = self.env.company.id
|
||
|
|
||
|
Property = self.env['ir.property'].with_company(company_id)
|
||
|
Pricelist = self.env['product.pricelist']
|
||
|
pl_domain = self._get_partner_pricelist_multi_search_domain_hook(company_id)
|
||
|
|
||
|
# if no specific property, try to find a fitting pricelist
|
||
|
result = Property._get_multi('property_product_pricelist', Partner._name, partner_ids)
|
||
|
|
||
|
remaining_partner_ids = [pid for pid, val in result.items() if not val or
|
||
|
not val._get_partner_pricelist_multi_filter_hook()]
|
||
|
if remaining_partner_ids:
|
||
|
# get fallback pricelist when no pricelist for a given country
|
||
|
pl_fallback = (
|
||
|
Pricelist.search(pl_domain + [('country_group_ids', '=', False)], limit=1) or
|
||
|
Property._get('property_product_pricelist', 'res.partner') or
|
||
|
Pricelist.search(pl_domain, limit=1)
|
||
|
)
|
||
|
# group partners by country, and find a pricelist for each country
|
||
|
remaining_partners = self.env['res.partner'].browse(remaining_partner_ids)
|
||
|
partners_by_country = remaining_partners.grouped('country_id')
|
||
|
for country, partners in partners_by_country.items():
|
||
|
pl = Pricelist.search(pl_domain + [('country_group_ids.country_ids', '=', country.id if country else False)], limit=1)
|
||
|
pl = pl or pl_fallback
|
||
|
for pid in partners.ids:
|
||
|
result[pid] = pl
|
||
|
|
||
|
return result
|
||
|
|
||
|
def _get_partner_pricelist_multi_search_domain_hook(self, company_id):
|
||
|
return [
|
||
|
('active', '=', True),
|
||
|
('company_id', 'in', [company_id, False]),
|
||
|
]
|
||
|
|
||
|
def _get_partner_pricelist_multi_filter_hook(self):
|
||
|
return self.filtered('active')
|
||
|
|
||
|
@api.model
|
||
|
def get_import_templates(self):
|
||
|
return [{
|
||
|
'label': _('Import Template for Pricelists'),
|
||
|
'template': '/product/static/xls/product_pricelist.xls'
|
||
|
}]
|
||
|
|
||
|
@api.ondelete(at_uninstall=False)
|
||
|
def _unlink_except_used_as_rule_base(self):
|
||
|
linked_items = self.env['product.pricelist.item'].sudo().with_context(active_test=False).search([
|
||
|
('base', '=', 'pricelist'),
|
||
|
('base_pricelist_id', 'in', self.ids),
|
||
|
('pricelist_id', 'not in', self.ids),
|
||
|
])
|
||
|
if linked_items:
|
||
|
raise UserError(_(
|
||
|
'You cannot delete those pricelist(s):\n(%s)\n, they are used in other pricelist(s):\n%s',
|
||
|
'\n'.join(linked_items.base_pricelist_id.mapped('display_name')),
|
||
|
'\n'.join(linked_items.pricelist_id.mapped('display_name'))
|
||
|
))
|