570 lines
20 KiB
Python
570 lines
20 KiB
Python
# This file is part product_esale module for Tryton.
|
|
# The COPYRIGHT file at the top level of this repository contains
|
|
# the full copyright notices and license terms.
|
|
from trytond.model import ModelSQL, fields
|
|
from trytond.pool import Pool, PoolMeta
|
|
from trytond.tools import cursor_dict
|
|
from trytond.transaction import Transaction
|
|
from trytond.cache import Cache
|
|
from trytond.pyson import Eval, Bool, Or
|
|
from .tools import slugify
|
|
|
|
__all__ = ['Template', 'Product', 'ProductMenu', 'ProductRelated',
|
|
'ProductUpSell', 'ProductCrossSell',]
|
|
|
|
IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/gif']
|
|
STATES = {
|
|
'readonly': ~Eval('active', True),
|
|
'invisible': (~Eval('unique_variant', False) & Eval(
|
|
'_parent_template', {}).get('unique_variant', False)),
|
|
}
|
|
DEPENDS = ['active', 'unique_variant']
|
|
|
|
def attribute2dict(s):
|
|
d = {}
|
|
for v in s.split('|'):
|
|
k, v = v.split(':')
|
|
d[k] = v
|
|
return d
|
|
|
|
|
|
class Template:
|
|
__metaclass__ = PoolMeta
|
|
__name__ = 'product.template'
|
|
esale_visibility = fields.Selection([
|
|
('all','All'),
|
|
('search','Search'),
|
|
('catalog','Catalog'),
|
|
('none','None'),
|
|
], 'Visibility')
|
|
esale_slug = fields.Char('Slug', translate=True,
|
|
states={
|
|
'required': Eval('esale_available', True),
|
|
},
|
|
depends=['esale_available'])
|
|
esale_slug_langs = fields.Function(fields.Dict(None, 'Slug Langs'), 'get_esale_slug_langs')
|
|
esale_shortdescription = fields.Text('Short Description', translate=True,
|
|
help='You could write wiki markup to create html content. Formats text following '
|
|
'the Creole syntax https://en.wikipedia.org/wiki/Creole_(markup)')
|
|
esale_description = fields.Text('Sale Description', translate=True,
|
|
help='You could write wiki markup to create html content. Formats text following '
|
|
'the Creole syntax https://en.wikipedia.org/wiki/Creole_(markup).')
|
|
esale_metadescription = fields.Char('Meta Description', translate=True,
|
|
help='Almost all search engines recommend it to be shorter ' \
|
|
'than 155 characters of plain text')
|
|
esale_metakeyword = fields.Char('Meta Keyword', translate=True)
|
|
esale_metatitle = fields.Char('Meta Title', translate=True)
|
|
esale_menus = fields.Many2Many('product.template-esale.catalog.menu',
|
|
'template', 'menu', 'Menus')
|
|
esale_relateds = fields.Many2Many('product.template-product.related',
|
|
'template', 'related', 'Relateds',
|
|
domain=[
|
|
('id', '!=', Eval('id')),
|
|
('esale_available', '=', True),
|
|
('salable', '=', True),
|
|
], depends=['id'])
|
|
esale_upsells = fields.Many2Many('product.template-product.upsell',
|
|
'template', 'upsell', 'Up Sells',
|
|
domain=[
|
|
('id', '!=', Eval('id')),
|
|
('esale_available', '=', True),
|
|
('salable', '=', True),
|
|
], depends=['id'])
|
|
esale_crosssells = fields.Many2Many('product.template-product.crosssell',
|
|
'template', 'crosssell', 'Cross Sells',
|
|
domain=[
|
|
('id', '!=', Eval('id')),
|
|
('esale_available', '=', True),
|
|
('salable', '=', True),
|
|
], depends=['id'])
|
|
esale_sequence = fields.Integer('Sequence',
|
|
help='Gives the sequence order when displaying category list.')
|
|
esale_images = fields.Function(fields.Char('eSale Images'), 'get_esale_images')
|
|
esale_default_images = fields.Function(fields.Char('eSale Default Images'), 'get_esale_default_images')
|
|
esale_all_images = fields.Function(fields.Char('eSale All Images'), 'get_esale_all_images')
|
|
_esale_slug_langs_cache = Cache('product_template.esale_slug_langs')
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super(Template, cls).__setup__()
|
|
cls._error_messages.update({
|
|
'slug_exists': 'Slug %s exists. Get another slug!',
|
|
'delete_esale_template': 'Product %s is esale active. '
|
|
'Descheck active field to dissable esale products',
|
|
})
|
|
|
|
@staticmethod
|
|
def default_esale_visibility():
|
|
return 'all'
|
|
|
|
@staticmethod
|
|
def default_esale_sequence():
|
|
return 1
|
|
|
|
@staticmethod
|
|
def default_template_attribute_set():
|
|
'''Product Template Attribute'''
|
|
Config = Pool().get('product.configuration')
|
|
pconfig = Config(1)
|
|
if pconfig.template_attribute_set:
|
|
return pconfig.template_attribute_set.id
|
|
|
|
@staticmethod
|
|
def default_template_attributes():
|
|
'''Product Template Attribute Options'''
|
|
Config = Pool().get('product.configuration')
|
|
pconfig = Config(1)
|
|
if pconfig.template_attribute_set_options:
|
|
return attribute2dict(pconfig.template_attribute_set_options)
|
|
|
|
@staticmethod
|
|
def default_attribute_set():
|
|
'''Product Attribute'''
|
|
Config = Pool().get('product.configuration')
|
|
pconfig = Config(1)
|
|
if pconfig.product_attribute_set:
|
|
return pconfig.product_attribute_set.id
|
|
|
|
@staticmethod
|
|
def default_default_uom():
|
|
'''Default UOM'''
|
|
Config = Pool().get('product.configuration')
|
|
config = Config(1)
|
|
if config.default_uom:
|
|
return config.default_uom.id
|
|
|
|
@fields.depends('name', 'esale_slug')
|
|
def on_change_esale_available(self):
|
|
try:
|
|
super(Template, self).on_change_esale_available()
|
|
except AttributeError:
|
|
pass
|
|
if self.name and not self.esale_slug:
|
|
self.esale_slug = slugify(self.name)
|
|
|
|
@fields.depends('name', 'esale_slug')
|
|
def on_change_name(self):
|
|
try:
|
|
super(Template, self).on_change_name()
|
|
except AttributeError:
|
|
pass
|
|
if self.name and not self.esale_slug:
|
|
self.esale_slug = slugify(self.name)
|
|
|
|
@fields.depends('esale_slug')
|
|
def on_change_esale_slug(self):
|
|
if self.esale_slug:
|
|
self.esale_slug = slugify(self.esale_slug)
|
|
|
|
@classmethod
|
|
def view_attributes(cls):
|
|
return super(Template, cls).view_attributes() + [
|
|
('//page[@id="attachments"]', 'states', {
|
|
'invisible': Bool(Eval('unique_variant', True)),
|
|
})]
|
|
|
|
@classmethod
|
|
def get_slug(cls, id, slug):
|
|
"""Get another product is same slug
|
|
Slug is identificator unique
|
|
:param id: int
|
|
:param slug: str
|
|
:return True or False
|
|
"""
|
|
Config = Pool().get('product.configuration')
|
|
config = Config(1)
|
|
if not config.check_slug:
|
|
return True
|
|
|
|
records = [t.id for t in cls.search([('esale_available','=', True)])]
|
|
if id and id in records:
|
|
records.remove(id)
|
|
products = cls.search([('esale_slug','=',slug),('id','in',records)])
|
|
if products:
|
|
cls.raise_user_error('slug_exists', slug)
|
|
return True
|
|
|
|
def get_esale_images(self, name):
|
|
'''Return dict product images: base, small and thumb'''
|
|
images = {}
|
|
base = None
|
|
small = None
|
|
thumb = None
|
|
for attachment in self.attachments:
|
|
if not attachment.esale_available or attachment.esale_exclude:
|
|
continue
|
|
if attachment.esale_base_image and not base:
|
|
base = attachment.name
|
|
if attachment.esale_small_image and not small:
|
|
small = attachment.name
|
|
if attachment.esale_thumbnail and not thumb:
|
|
thumb = attachment.name
|
|
|
|
images['base'] = base
|
|
images['small'] = small
|
|
images['thumbnail'] = thumb
|
|
|
|
return images
|
|
|
|
def get_esale_all_images(self, name):
|
|
'''Return list product images'''
|
|
images = []
|
|
for attachment in self.attachments:
|
|
if not attachment.esale_available or attachment.esale_exclude:
|
|
continue
|
|
images.append({
|
|
'name': attachment.name,
|
|
'digest': attachment.file_id,
|
|
})
|
|
|
|
return images
|
|
|
|
def get_esale_default_images(self, name):
|
|
'''Return dict product digest images: base, small and thumb'''
|
|
images = {}
|
|
base = None
|
|
small = None
|
|
thumb = None
|
|
for attachment in self.attachments:
|
|
if not attachment.esale_available or attachment.esale_exclude:
|
|
continue
|
|
if attachment.esale_base_image and not base:
|
|
base = {
|
|
'name': attachment.name,
|
|
'digest': attachment.file_id,
|
|
}
|
|
if attachment.esale_small_image and not small:
|
|
small = {
|
|
'name': attachment.name,
|
|
'digest': attachment.file_id,
|
|
}
|
|
if attachment.esale_thumbnail and not thumb:
|
|
thumb = {
|
|
'name': attachment.name,
|
|
'digest': attachment.file_id,
|
|
}
|
|
images['base'] = base
|
|
images['small'] = small
|
|
images['thumbnail'] = thumb
|
|
|
|
return images
|
|
|
|
def get_esale_slug_langs(self, name):
|
|
'''Return dict slugs by all languaes actives'''
|
|
pool = Pool()
|
|
Lang = pool.get('ir.lang')
|
|
Template = pool.get('product.template')
|
|
|
|
template_id = self.id
|
|
langs = Lang.search([
|
|
('active', '=', True),
|
|
('translatable', '=', True),
|
|
])
|
|
|
|
slugs = {}
|
|
for lang in langs:
|
|
with Transaction().set_context(language=lang.code):
|
|
template, = Template.read([template_id], ['esale_slug'])
|
|
slugs[lang.code] = template['esale_slug']
|
|
|
|
return slugs
|
|
|
|
@classmethod
|
|
def create(cls, vlist):
|
|
for values in vlist:
|
|
values = values.copy()
|
|
if values.get('esale_available'):
|
|
name = values.get('name')
|
|
slug = slugify(values.get('esale_slug', name))
|
|
cls.get_slug(None, slug)
|
|
values['esale_slug'] = slug
|
|
return super(Template, cls).create(vlist)
|
|
|
|
@classmethod
|
|
def write(cls, *args):
|
|
"""Get another product slug same shop"""
|
|
actions = iter(args)
|
|
args = []
|
|
for templates, values in zip(actions, actions):
|
|
if values.get('esale_slug'):
|
|
slug = slugify(values.get('esale_slug'))
|
|
for template in templates:
|
|
cls.get_slug(template.id, slug)
|
|
values['esale_slug'] = slug
|
|
salable = values.get('salable')
|
|
if salable == False:
|
|
values['esale_active'] = False
|
|
args.extend((templates, values))
|
|
return super(Template, cls).write(*args)
|
|
|
|
@classmethod
|
|
def copy(cls, templates, default=None):
|
|
new_templates = []
|
|
for template in templates:
|
|
if template.esale_slug:
|
|
default['esale_slug'] = '%s-copy' % template.esale_slug
|
|
new_template, = super(Template, cls).copy([template], default=default)
|
|
new_templates.append(new_template)
|
|
return new_templates
|
|
|
|
@classmethod
|
|
def delete(cls, templates):
|
|
for template in templates:
|
|
if template.esale_available:
|
|
cls.raise_user_error('delete_esale_template', (template.rec_name,))
|
|
super(Template, cls).delete(templates)
|
|
|
|
@staticmethod
|
|
def attribute_options(codes):
|
|
'''Return attribute options convert to dict by code
|
|
@param: names: list
|
|
return dict {'attrname': {options}}
|
|
'''
|
|
options = {}
|
|
cursor = Transaction().connection.cursor()
|
|
names = ["'"+c+"'" for c in codes]
|
|
query = "SELECT name, selection from product_attribute " \
|
|
"where name in (%s) and type_ = 'selection'" % ','.join(names)
|
|
cursor.execute(query)
|
|
vals = cursor_dict(cursor)
|
|
|
|
for val in vals:
|
|
opts = {}
|
|
for o in val['selection'].split('\n'):
|
|
opt = o.split(':')
|
|
opts[opt[0]] = opt[1]
|
|
options[val['name']] = opts
|
|
return options
|
|
|
|
|
|
class Product:
|
|
__metaclass__ = PoolMeta
|
|
__name__ = 'product.product'
|
|
esale_available = fields.Function(fields.Boolean('eSale'),
|
|
'get_esale_available', searcher='search_esale_available')
|
|
esale_active = fields.Function(fields.Boolean('Active eSale'),
|
|
'get_esale_active', searcher='search_esale_active')
|
|
esale_slug = fields.Char('Slug', translate=True, states=STATES,
|
|
depends=DEPENDS)
|
|
esale_sequence = fields.Integer('Sequence',
|
|
help='Gives the sequence order when displaying variants list.')
|
|
unique_variant = fields.Function(fields.Boolean('Unique Variant'),
|
|
'on_change_with_unique_variant')
|
|
|
|
# def __getattr__(self, name):
|
|
# result = super(Product, self).__getattr__(name)
|
|
# if not result and name == 'esale_slug':
|
|
# return getattr(self.template, name)
|
|
# return result
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super(Product, cls).__setup__()
|
|
cls._order.insert(0, ('esale_sequence', 'ASC'))
|
|
cls._order.insert(1, ('id', 'ASC'))
|
|
# Add code require attribute
|
|
for fname in ('code',):
|
|
fstates = getattr(cls, fname).states
|
|
if fstates.get('required'):
|
|
fstates['required'] = Or(fstates['required'],
|
|
Bool(Eval('esale_available', False)))
|
|
else:
|
|
fstates['required'] = Bool(Eval('esale_available', False))
|
|
getattr(cls, fname).depends.append('esale_available')
|
|
|
|
@staticmethod
|
|
def default_esale_sequence():
|
|
return 1
|
|
|
|
@staticmethod
|
|
def default_attributes():
|
|
'''Product Attribute Options'''
|
|
Config = Pool().get('product.configuration')
|
|
pconfig = Config(1)
|
|
if pconfig.product_attribute_set_options:
|
|
return attribute2dict(pconfig.product_attribute_set_options)
|
|
|
|
@classmethod
|
|
def search(cls, domain, offset=0, limit=None, order=None, count=False,
|
|
query=False):
|
|
for d in domain:
|
|
if d and d[0] == 'esale_slug':
|
|
domain = ['OR', domain[:], ('template.esale_slug', 'ilike', d[2])]
|
|
break
|
|
return super(Product, cls).search(domain, offset=offset, limit=limit,
|
|
order=order, count=count, query=query)
|
|
|
|
@fields.depends('template', 'sale')
|
|
def on_change_with_unique_variant(self, name=None):
|
|
if self.template:
|
|
return self.template.unique_variant
|
|
return False
|
|
|
|
def get_esale_available(self, name):
|
|
return self.template.esale_available if self.template else False
|
|
|
|
@classmethod
|
|
def search_esale_available(cls, name, clause):
|
|
return [('template.esale_available',) + tuple(clause[1:])]
|
|
|
|
def get_esale_active(self, name):
|
|
return self.template.esale_active if self.template else False
|
|
|
|
@classmethod
|
|
def search_esale_active(cls, name, clause):
|
|
return [('template.esale_active',) + tuple(clause[1:])]
|
|
|
|
@classmethod
|
|
def get_product_relateds(cls, products, exclude=False):
|
|
'''
|
|
Products Relateds.
|
|
Exclude option: not return related product if are in products
|
|
:param products: object list
|
|
:param exclude: bool
|
|
Return list dict product, price
|
|
'''
|
|
prods = []
|
|
templates = []
|
|
relateds = []
|
|
|
|
if not products:
|
|
return None
|
|
|
|
for product in products:
|
|
templates.append(product.template)
|
|
if product.esale_relateds:
|
|
for template in product.esale_relateds:
|
|
relateds.append(template)
|
|
|
|
if not relateds:
|
|
return None
|
|
|
|
relateds = list(set(relateds))
|
|
if exclude:
|
|
relateds = list(set(relateds) - set(templates))
|
|
prices = cls.get_sale_price(relateds, 1)
|
|
for template in relateds:
|
|
product, = template.products
|
|
prods.append({
|
|
'product': product,
|
|
'unit_price': prices[product.id],
|
|
})
|
|
return prods
|
|
|
|
@classmethod
|
|
def get_product_upsells(cls, products, exclude=False):
|
|
'''
|
|
Products Up Sells
|
|
Exclude option: not return upsell product if are in products
|
|
:param products: object list
|
|
:param exclude: bool
|
|
Return list dict product, price
|
|
'''
|
|
prods = []
|
|
templates = []
|
|
upsells = []
|
|
|
|
if not products:
|
|
return None
|
|
|
|
for product in products:
|
|
templates.append(product.template)
|
|
if product.esale_upsells:
|
|
for template in product.esale_upsells:
|
|
upsells.append(template)
|
|
|
|
if not upsells:
|
|
return None
|
|
|
|
upsells = list(set(upsells))
|
|
if exclude:
|
|
upsells = list(set(upsells) - set(templates))
|
|
prices = cls.get_sale_price(upsells, 1)
|
|
for template in upsells:
|
|
product, = template.products
|
|
prods.append({
|
|
'product': product,
|
|
'unit_price': prices[product.id],
|
|
})
|
|
return prods
|
|
|
|
@classmethod
|
|
def get_product_crosssells(cls, products, exclude=False):
|
|
'''
|
|
Products Crosssells
|
|
Exclude option: not return upsell product if are in products
|
|
:param products: object list
|
|
:param exclude: bool
|
|
Return list dict product, price
|
|
'''
|
|
prods = []
|
|
templates = []
|
|
crosssells = []
|
|
|
|
if not products:
|
|
return None
|
|
|
|
for product in products:
|
|
templates.append(product.template)
|
|
if product.esale_crosssells:
|
|
for template in product.esale_crosssells:
|
|
crosssells.append(template)
|
|
|
|
if not crosssells:
|
|
return None
|
|
|
|
crosssells = list(set(crosssells))
|
|
if exclude:
|
|
crosssells = list(set(crosssells) - set(templates))
|
|
prices = cls.get_sale_price(crosssells, 1)
|
|
for template in crosssells:
|
|
product, = template.products
|
|
prods.append({
|
|
'product': product,
|
|
'unit_price': prices[product.id],
|
|
})
|
|
return prods
|
|
|
|
|
|
class ProductMenu(ModelSQL):
|
|
'Product - Menu'
|
|
__name__ = 'product.template-esale.catalog.menu'
|
|
_table = 'product_template_esale_catalog_menu'
|
|
|
|
template = fields.Many2One('product.template', 'Template', ondelete='CASCADE',
|
|
select=True, required=True)
|
|
menu = fields.Many2One('esale.catalog.menu', 'Menu', ondelete='CASCADE',
|
|
select=True, required=True)
|
|
|
|
|
|
class ProductRelated(ModelSQL):
|
|
'Product - Related'
|
|
__name__ = 'product.template-product.related'
|
|
_table = 'product_template_product_related'
|
|
|
|
template = fields.Many2One('product.template', 'Template', ondelete='CASCADE',
|
|
select=True, required=True)
|
|
related = fields.Many2One('product.template', 'Related', ondelete='CASCADE',
|
|
select=True, required=True)
|
|
|
|
|
|
class ProductUpSell(ModelSQL):
|
|
'Product - Upsell'
|
|
__name__ = 'product.template-product.upsell'
|
|
_table = 'product_template_product_upsell'
|
|
|
|
template = fields.Many2One('product.template', 'Template', ondelete='CASCADE',
|
|
select=True, required=True)
|
|
upsell = fields.Many2One('product.template', 'Upsell', ondelete='CASCADE',
|
|
select=True, required=True)
|
|
|
|
|
|
class ProductCrossSell(ModelSQL):
|
|
'Product - Cross Sell'
|
|
__name__ = 'product.template-product.crosssell'
|
|
_table = 'product_template_product_crosssell'
|
|
template = fields.Many2One('product.template', 'Template', ondelete='CASCADE',
|
|
select=True, required=True)
|
|
crosssell = fields.Many2One('product.template', 'Cross Sell', ondelete='CASCADE',
|
|
select=True, required=True)
|