Files
tryton/modules/product/product.py
T

813 lines
28 KiB
Python
Raw Normal View History

2014-12-02 12:55:30 +01:00
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import copy
2016-03-19 09:05:13 +01:00
import logging
from decimal import Decimal
2019-06-06 19:20:43 +02:00
from importlib import import_module
2019-06-06 19:20:43 +02:00
import stdnum
import stdnum.exceptions
2023-01-19 11:25:25 +01:00
from sql import Literal
from sql.functions import CharLength
2020-03-19 00:33:37 +01:00
from sql.operators import Equal
2019-06-06 19:20:43 +02:00
from trytond.i18n import gettext
2018-03-26 14:10:59 +02:00
from trytond.model import (
DeactivableMixin, Exclude, Index, Model, ModelSQL, ModelView, UnionMixin,
fields, sequence_ordered)
2017-04-01 00:31:59 +02:00
from trytond.modules.company.model import (
CompanyMultiValueMixin, CompanyValueMixin)
2021-12-11 17:58:42 +01:00
from trytond.pool import Pool
from trytond.pyson import Eval, Get, If
from trytond.tools import is_full_text, lstrip_wildcard
2021-12-11 17:58:42 +01:00
from trytond.transaction import Transaction
2022-12-26 22:32:23 +01:00
try:
from trytond.tools import barcode
except ImportError:
barcode = None
2019-06-06 19:20:43 +02:00
from .exceptions import InvalidIdentifierCode
2020-05-28 18:22:45 +02:00
from .ir import price_decimal
2019-06-06 19:20:43 +02:00
2020-04-23 21:44:00 +02:00
__all__ = ['price_digits', 'round_price', 'TemplateFunction']
2016-03-19 09:05:13 +01:00
logger = logging.getLogger(__name__)
2012-09-24 22:18:00 +02:00
TYPES = [
('goods', 'Goods'),
('assets', 'Assets'),
('service', 'Service'),
]
COST_PRICE_METHODS = [
('fixed', 'Fixed'),
('average', 'Average'),
]
2008-05-28 12:59:40 +02:00
2020-05-28 18:22:45 +02:00
price_digits = (16, price_decimal)
2008-05-28 12:59:40 +02:00
2020-04-23 21:44:00 +02:00
def round_price(value, rounding=None):
"Round price using the price digits"
2021-09-13 23:07:13 +02:00
if isinstance(value, int):
return Decimal(value)
2020-04-23 21:44:00 +02:00
return value.quantize(
Decimal(1) / 10 ** price_digits[1], rounding=rounding)
2018-03-26 14:10:59 +02:00
class Template(
DeactivableMixin, ModelSQL, ModelView, CompanyMultiValueMixin):
2008-05-28 12:59:40 +02:00
"Product Template"
2012-09-24 22:18:00 +02:00
__name__ = "product.template"
2020-06-15 09:00:44 +02:00
_order_name = 'rec_name'
name = fields.Char(
"Name", size=None, required=True, translate=True)
code_readonly = fields.Function(
fields.Boolean("Code Readonly"), 'get_code_readonly')
code = fields.Char(
"Code", strip='leading',
states={
'readonly': Eval('code_readonly', False),
})
type = fields.Selection(TYPES, "Type", required=True)
consumable = fields.Boolean('Consumable',
states={
'invisible': Eval('type', 'goods') != 'goods',
},
2020-04-13 17:50:45 +02:00
help="Check to allow stock moves to be assigned "
"regardless of stock level.")
2017-04-01 00:31:59 +02:00
list_price = fields.MultiValue(fields.Numeric(
2021-09-13 23:29:56 +02:00
"List Price", digits=price_digits,
states={
'readonly': ~Eval('context', {}).get('company'),
},
2020-04-13 17:50:45 +02:00
help="The standard price the product is sold at."))
2017-04-01 00:31:59 +02:00
list_prices = fields.One2Many(
'product.list_price', 'template', "List Prices")
cost_price = fields.Function(fields.Numeric(
2020-04-13 17:50:45 +02:00
"Cost Price", digits=price_digits,
help="The amount it costs to purchase or make the product, "
"or carry out the service."),
'get_cost_price')
2017-04-01 00:31:59 +02:00
cost_price_method = fields.MultiValue(fields.Selection(
2020-04-13 17:50:45 +02:00
COST_PRICE_METHODS, "Cost Price Method", required=True,
help="The method used to calculate the cost price."))
cost_price_methods = fields.One2Many(
'product.cost_price_method', 'template', "Cost Price Methods")
2023-08-20 12:17:27 +02:00
default_uom = fields.Many2One(
'product.uom', "Default UoM", required=True,
help="The standard Unit of Measure for the product.\n"
2020-04-13 17:50:45 +02:00
"Used internally when calculating the stock levels of goods "
"and assets.")
2012-03-25 15:38:44 +02:00
default_uom_category = fields.Function(
2023-08-20 12:17:27 +02:00
fields.Many2One(
'product.uom.category', "Default UoM Category",
help="The category of the default Unit of Measure."),
2013-05-27 17:48:12 +02:00
'on_change_with_default_uom_category',
searcher='search_default_uom_category')
categories = fields.Many2Many(
'product.template-product.category', 'template', 'category',
2020-04-13 17:50:45 +02:00
"Categories",
help="The categories that the product is in.\n"
"Used to group similar products together.")
2017-10-02 16:06:36 +02:00
categories_all = fields.Many2Many(
'product.template-product.category.all',
'template', 'category', "Categories", readonly=True)
2020-04-13 17:50:45 +02:00
products = fields.One2Many(
'product.product', 'template', "Variants",
domain=[
If(~Eval('active'), ('active', '=', False), ()),
],
2020-04-13 17:50:45 +02:00
help="The different variants the product comes in.")
2008-05-28 12:59:40 +02:00
2020-06-15 09:00:44 +02:00
@classmethod
def __setup__(cls):
cls.code.search_unaccented = False
2020-06-15 09:00:44 +02:00
super().__setup__()
t = cls.__table__()
cls._sql_indexes.update({
Index(t, (t.code, Index.Similarity())),
})
2020-06-15 09:00:44 +02:00
cls._order.insert(0, ('rec_name', 'ASC'))
types_cost_method = cls._cost_price_method_domain_per_type()
cls.cost_price_method.domain = [
Get(types_cost_method, Eval('type'), []),
]
@classmethod
def _cost_price_method_domain_per_type(cls):
return {'service': [('cost_price_method', '=', 'fixed')]}
2017-04-01 00:31:59 +02:00
@classmethod
def multivalue_model(cls, field):
pool = Pool()
if field == 'list_price':
return pool.get('product.list_price')
elif field == 'cost_price_method':
return pool.get('product.cost_price_method')
2017-04-01 00:31:59 +02:00
return super(Template, cls).multivalue_model(field)
@classmethod
def order_code(cls, tables):
table, _ = tables[None]
if cls.default_code_readonly():
return [CharLength(table.code), table.code]
else:
return [table.code]
2020-06-15 09:00:44 +02:00
@classmethod
def order_rec_name(cls, tables):
table, _ = tables[None]
2022-10-27 13:53:05 +02:00
return cls.order_code(tables) + [table.name]
2020-06-15 09:00:44 +02:00
2020-03-19 00:28:43 +01:00
def get_rec_name(self, name):
if self.code:
return '[' + self.code + '] ' + self.name
2020-03-19 00:28:43 +01:00
else:
return self.name
@classmethod
def search_rec_name(cls, name, clause):
_, operator, operand, *extra = clause
if operator.startswith('!') or operator.startswith('not '):
bool_op = 'AND'
else:
bool_op = 'OR'
code_value = operand
if operator.endswith('like') and is_full_text(operand):
code_value = lstrip_wildcard(operand)
return [bool_op,
('name', operator, operand, *extra),
('code', operator, code_value, *extra),
('products.code', operator, code_value, *extra),
('products.identifiers.code', operator, code_value, *extra),
]
2012-09-24 22:18:00 +02:00
@staticmethod
def default_type():
return 'goods'
2008-05-28 12:59:40 +02:00
2012-09-24 22:18:00 +02:00
@staticmethod
def default_consumable():
2012-08-23 23:18:16 +02:00
return False
def get_cost_price(self, name):
if len(self.products) == 1:
product, = self.products
return product.cost_price
2017-04-01 00:31:59 +02:00
@classmethod
def default_cost_price_method(cls, **pattern):
2017-04-01 00:31:59 +02:00
pool = Pool()
Configuration = pool.get('product.configuration')
return Configuration(1).get_multivalue(
'default_cost_price_method', **pattern)
2017-04-01 00:31:59 +02:00
@classmethod
def default_products(cls):
transaction = Transaction()
if (transaction.user == 0
or not transaction.context.get('default_products', True)):
2013-03-20 22:50:51 +01:00
return []
2017-01-10 11:51:23 +01:00
return [{}]
2013-03-20 22:50:51 +01:00
@classmethod
def default_code_readonly(cls):
pool = Pool()
Configuration = pool.get('product.configuration')
config = Configuration(1)
return bool(config.template_sequence)
def get_code_readonly(self, name):
return self.default_code_readonly()
@fields.depends('type', 'cost_price_method')
def on_change_type(self):
if self.type == 'service':
self.cost_price_method = 'fixed'
2014-01-26 14:29:18 +01:00
@fields.depends('default_uom')
2012-09-24 22:18:00 +02:00
def on_change_with_default_uom_category(self, name=None):
2023-03-26 09:49:00 +02:00
return self.default_uom.category if self.default_uom else None
2012-03-25 15:38:44 +02:00
2013-05-27 17:48:12 +02:00
@classmethod
def search_default_uom_category(cls, name, clause):
return [('default_uom.category' + clause[0][len(name):], *clause[1:])]
2013-05-27 17:48:12 +02:00
@classmethod
def _new_code(cls):
pool = Pool()
Configuration = pool.get('product.configuration')
config = Configuration(1)
sequence = config.template_sequence
if sequence:
2021-03-11 23:00:10 +01:00
return sequence.get()
2013-10-27 19:34:34 +01:00
@classmethod
def create(cls, vlist):
2020-03-19 00:28:43 +01:00
pool = Pool()
Product = pool.get('product.product')
2013-10-27 19:34:34 +01:00
vlist = [v.copy() for v in vlist]
for values in vlist:
values.setdefault('products', None)
if not values.get('code'):
values['code'] = cls._new_code()
2020-03-19 00:28:43 +01:00
templates = super(Template, cls).create(vlist)
products = sum((t.products for t in templates), ())
Product.sync_code(products)
return templates
@classmethod
def write(cls, *args):
pool = Pool()
Product = pool.get('product.product')
super().write(*args)
templates = sum(args[0:None:2], [])
products = sum((t.products for t in templates), ())
Product.sync_code(products)
2013-10-27 19:34:34 +01:00
2020-07-24 10:03:09 +02:00
@classmethod
def copy(cls, templates, default=None):
if default is None:
default = {}
else:
default = default.copy()
default.setdefault('code', None)
return super().copy(templates, default=default)
2014-02-20 23:28:47 +01:00
@classmethod
def search_global(cls, text):
2015-02-04 13:38:19 +01:00
for record, rec_name, icon in super(Template, cls).search_global(text):
2014-02-20 23:28:47 +01:00
icon = icon or 'tryton-product'
2015-02-04 13:38:19 +01:00
yield record, rec_name, icon
2014-02-20 23:28:47 +01:00
2013-03-20 22:50:51 +01:00
class TemplateFunction(fields.Function):
def __init__(self, field):
super(TemplateFunction, self).__init__(
field, 'get_template', searcher='search_template')
# Disable on_change as it is managed by on_change_template
self.on_change = set()
self.on_change_with = set()
def __copy__(self):
return TemplateFunction(copy.copy(self._field))
def __deepcopy__(self, memo):
return TemplateFunction(copy.deepcopy(self._field, memo))
@staticmethod
def order(name):
@classmethod
def order(cls, tables):
pool = Pool()
Template = pool.get('product.template')
product, _ = tables[None]
if 'template' not in tables:
template = Template.__table__()
tables['template'] = {
None: (template, product.template == template.id),
}
return getattr(Template, name).convert_order(
name, tables['template'], Template)
return order
2021-03-24 00:02:15 +01:00
def definition(self, model, language):
pool = Pool()
Template = pool.get('product.template')
definition = super().definition(model, language)
definition['searchable'] = self._field.definition(
Template, language)['searchable']
return definition
2018-03-26 14:10:59 +02:00
class Product(
DeactivableMixin, ModelSQL, ModelView, CompanyMultiValueMixin):
2013-03-20 22:50:51 +01:00
"Product Variant"
__name__ = "product.product"
_order_name = 'rec_name'
template = fields.Many2One(
'product.template', "Product Template",
required=True, ondelete='CASCADE',
2020-04-13 17:50:45 +02:00
search_context={'default_products': False},
domain=[
If(Eval('active'), ('active', '=', True), ()),
],
2020-04-13 17:50:45 +02:00
help="The product that defines the common properties "
"inherited by the variant.")
code_readonly = fields.Function(fields.Boolean('Code Readonly'),
'get_code_readonly')
2020-03-19 00:28:43 +01:00
prefix_code = fields.Function(fields.Char(
"Prefix Code",
states={
'invisible': ~Eval('prefix_code'),
}),
'on_change_with_prefix_code')
suffix_code = fields.Char(
"Suffix Code", strip='trailing',
states={
'readonly': Eval('code_readonly', False),
},
2020-03-19 00:33:37 +01:00
help="The unique identifier for the product (aka SKU).")
code = fields.Char(
"Code", readonly=True,
2020-04-13 17:50:45 +02:00
help="A unique identifier for the variant.")
identifiers = fields.One2Many(
'product.identifier', 'product', "Identifiers",
2020-04-13 17:50:45 +02:00
help="Other identifiers associated with the variant.")
cost_price = fields.MultiValue(fields.Numeric(
"Cost Price", digits=price_digits,
states={
'readonly': ~Eval('context', {}).get('company'),
},
2020-04-13 17:50:45 +02:00
help="The amount it costs to purchase or make the variant, "
"or carry out the service."))
cost_prices = fields.One2Many(
'product.cost_price', 'product', "Cost Prices")
description = fields.Text("Description", translate=True)
2013-03-20 22:50:51 +01:00
list_price_uom = fields.Function(fields.Numeric('List Price',
digits=price_digits), 'get_price_uom')
2013-03-20 22:50:51 +01:00
cost_price_uom = fields.Function(fields.Numeric('Cost Price',
digits=price_digits), 'get_price_uom')
2013-03-20 22:50:51 +01:00
@classmethod
def __setup__(cls):
pool = Pool()
Template = pool.get('product.template')
if not hasattr(cls, '_no_template_field'):
cls._no_template_field = set()
cls._no_template_field.update(['products'])
cls.suffix_code.search_unaccented = False
cls.code.search_unaccented = False
super(Product, cls).__setup__()
2021-03-07 11:13:53 +01:00
cls.__access__.add('template')
2020-06-15 09:00:44 +02:00
cls._order.insert(0, ('rec_name', 'ASC'))
2020-03-19 00:33:37 +01:00
t = cls.__table__()
cls._sql_constraints = [
('code_exclude', Exclude(t, (t.code, Equal),
where=(t.active == Literal(True))
& (t.code != '')),
'product.msg_product_code_unique'),
]
cls._sql_indexes.add(
Index(t, (t.code, Index.Similarity())))
2020-03-19 00:33:37 +01:00
for attr in dir(Template):
tfield = getattr(Template, attr)
if not isinstance(tfield, fields.Field):
continue
if attr in cls._no_template_field:
continue
field = getattr(cls, attr, None)
if not field or isinstance(field, TemplateFunction):
tfield = copy.deepcopy(tfield)
if hasattr(tfield, 'field'):
tfield.field = None
invisible_state = ~Eval('template')
if 'invisible' in tfield.states:
tfield.states['invisible'] |= invisible_state
else:
tfield.states['invisible'] = invisible_state
setattr(cls, attr, TemplateFunction(tfield))
order_method = getattr(cls, 'order_%s' % attr, None)
if (not order_method
and not isinstance(tfield, (
fields.Function,
fields.One2Many,
fields.Many2Many))):
order_method = TemplateFunction.order(attr)
setattr(cls, 'order_%s' % attr, order_method)
if isinstance(tfield, fields.One2Many):
getattr(cls, attr).setter = '_set_template_function'
2020-03-19 00:28:43 +01:00
@classmethod
def __register__(cls, module):
table = cls.__table__()
table_h = cls.__table_handler__(module)
fill_suffix_code = (
table_h.column_exist('code')
and not table_h.column_exist('suffix_code'))
super().__register__(module)
cursor = Transaction().connection.cursor()
# Migration from 5.4: split code into prefix/suffix
if fill_suffix_code:
cursor.execute(*table.update(
[table.suffix_code],
[table.code]))
@classmethod
def _set_template_function(cls, products, name, value):
# Prevent NotImplementedError for One2Many
pass
@classmethod
def order_suffix_code(cls, tables):
table, _ = tables[None]
if cls.default_code_readonly():
return [CharLength(table.suffix_code), table.suffix_code]
else:
return [table.suffix_code]
@classmethod
def order_code(cls, tables):
pool = Pool()
Template = pool.get('product.template')
table, _ = tables[None]
if cls.default_code_readonly() or Template.default_code_readonly():
return [CharLength(table.code), table.code]
else:
return [table.code]
2019-10-16 21:19:46 +02:00
@fields.depends('template', '_parent_template.id')
def on_change_template(self):
2018-06-26 19:11:48 +02:00
for name, field in self._fields.items():
if isinstance(field, TemplateFunction):
if self.template:
value = getattr(self.template, name, None)
else:
value = None
setattr(self, name, value)
def get_template(self, name):
value = getattr(self.template, name)
if isinstance(value, Model):
2021-08-30 23:58:28 +02:00
field = getattr(self.__class__, name)
if field._type == 'reference':
return str(value)
return value.id
elif (isinstance(value, (list, tuple))
and value and isinstance(value[0], Model)):
return [r.id for r in value]
else:
return value
2020-03-19 00:28:43 +01:00
@fields.depends('template', '_parent_template.code')
def on_change_with_prefix_code(self, name=None):
if self.template:
return self.template.code
@classmethod
def multivalue_model(cls, field):
pool = Pool()
if field == 'cost_price':
return pool.get('product.cost_price')
return super(Product, cls).multivalue_model(field)
def set_multivalue(self, name, value, save=True, **pattern):
context = Transaction().context
if name in {'cost_price', 'list_price'} and not value:
if not pattern.get('company', context.get('company')):
return []
return super().set_multivalue(name, value, save=save, **pattern)
def get_multivalue(self, name, **pattern):
if isinstance(self._fields[name], TemplateFunction):
return self.template.get_multivalue(name, **pattern)
else:
return super().get_multivalue(name, **pattern)
@classmethod
def default_cost_price(cls, **pattern):
context = Transaction().context
if pattern.get('company', context.get('company')):
return Decimal(0)
@classmethod
def search_template(cls, name, clause):
2017-09-27 18:37:29 +02:00
return [('template.' + clause[0],) + tuple(clause[1:])]
@classmethod
2013-09-06 22:34:24 +02:00
def order_rec_name(cls, tables):
pool = Pool()
Template = pool.get('product.template')
product, _ = tables[None]
if 'template' not in tables:
template = Template.__table__()
tables['template'] = {
None: (template, product.template == template.id),
}
else:
template = tables['template']
2022-10-27 13:53:05 +02:00
return cls.order_code(tables) + Template.name.convert_order('name',
2013-09-06 22:34:24 +02:00
tables['template'], Template)
2013-03-20 22:50:51 +01:00
def get_rec_name(self, name):
if self.code:
return '[' + self.code + '] ' + self.name
else:
return self.name
@classmethod
def search_rec_name(cls, name, clause):
_, operator, operand, *extra = clause
if operator.startswith('!') or operator.startswith('not '):
2015-04-05 23:36:13 +02:00
bool_op = 'AND'
else:
bool_op = 'OR'
code_value = operand
if operator.endswith('like') and is_full_text(operand):
code_value = lstrip_wildcard(operand)
2015-04-05 23:36:13 +02:00
return [bool_op,
('code', operator, code_value, *extra),
('identifiers.code', operator, code_value, *extra),
('template.name', operator, operand, *extra),
('template.code', operator, code_value, *extra),
2014-01-31 09:07:04 +01:00
]
2013-03-20 22:50:51 +01:00
2012-09-24 22:18:00 +02:00
@staticmethod
def get_price_uom(products, name):
Uom = Pool().get('product.uom')
2008-05-28 12:59:40 +02:00
res = {}
field = name[:-4]
2010-08-15 00:23:46 +01:00
if Transaction().context.get('uom'):
2012-09-24 22:18:00 +02:00
to_uom = Uom(Transaction().context['uom'])
2008-05-28 12:59:40 +02:00
else:
to_uom = None
for product in products:
price = getattr(product, field)
if to_uom and product.default_uom.category == to_uom.category:
res[product.id] = Uom.compute_price(
product.default_uom, price, to_uom)
else:
res[product.id] = price
2008-05-28 12:59:40 +02:00
return res
2014-02-20 23:28:47 +01:00
@classmethod
def search_global(cls, text):
for id_, rec_name, icon in super(Product, cls).search_global(text):
icon = icon or 'tryton-product'
yield id_, rec_name, icon
2016-03-19 09:05:13 +01:00
@classmethod
def default_code_readonly(cls):
pool = Pool()
Configuration = pool.get('product.configuration')
config = Configuration(1)
return bool(config.product_sequence)
def get_code_readonly(self, name):
return self.default_code_readonly()
2022-11-05 10:06:27 +01:00
def identifier_get(self, types=None):
"Return the first identifier for the given types"
if isinstance(types, str) or types is None:
types = {types}
for identifier in self.identifiers:
if identifier.type in types:
return identifier
@classmethod
2020-03-19 00:28:43 +01:00
def _new_suffix_code(cls):
pool = Pool()
Configuration = pool.get('product.configuration')
config = Configuration(1)
sequence = config.product_sequence
if sequence:
2021-03-11 23:00:10 +01:00
return sequence.get()
@classmethod
def create(cls, vlist):
vlist = [x.copy() for x in vlist]
for values in vlist:
2020-03-19 00:28:43 +01:00
if not values.get('suffix_code'):
values['suffix_code'] = cls._new_suffix_code()
products = super().create(vlist)
cls.sync_code(products)
return products
@classmethod
def write(cls, *args):
super().write(*args)
products = sum(args[0:None:2], [])
cls.sync_code(products)
@classmethod
def copy(cls, products, default=None):
if default is None:
default = {}
else:
default = default.copy()
2020-03-19 00:28:43 +01:00
default.setdefault('suffix_code', None)
default.setdefault('code', None)
return super().copy(products, default=default)
2019-10-01 22:23:55 +02:00
@property
def list_price_used(self):
transaction = Transaction()
with transaction.reset_context(), \
transaction.set_context(self._context):
return self.template.get_multivalue('list_price')
2019-10-01 22:23:55 +02:00
2020-03-19 00:28:43 +01:00
@classmethod
def sync_code(cls, products):
for product in products:
code = ''.join(filter(None, [
product.prefix_code, product.suffix_code]))
2023-12-05 10:28:35 +01:00
if cls.code.strip:
if cls.code.strip == 'leading':
code = code.lstrip()
elif cls.code.strip == 'trailing':
code = code.rstrip()
else:
code = code.strip()
2020-03-19 00:28:43 +01:00
if not code:
code = None
if code != product.code:
product.code = code
cls.save(products)
2016-03-19 09:05:13 +01:00
2017-04-01 00:31:59 +02:00
class ProductListPrice(ModelSQL, CompanyValueMixin):
"Product List Price"
__name__ = 'product.list_price'
template = fields.Many2One(
'product.template', "Template", ondelete='CASCADE',
context={
'company': Eval('company', -1),
},
depends={'company'})
2017-04-01 00:31:59 +02:00
list_price = fields.Numeric("List Price", digits=price_digits)
@classmethod
def __setup__(cls):
super().__setup__()
cls.company.required = True
2017-04-01 00:31:59 +02:00
class ProductCostPriceMethod(ModelSQL, CompanyValueMixin):
"Product Cost Price Method"
__name__ = 'product.cost_price_method'
2017-04-01 00:31:59 +02:00
template = fields.Many2One(
'product.template', "Template", ondelete='CASCADE',
context={
'company': Eval('company', -1),
},
depends={'company'})
2017-04-01 00:31:59 +02:00
cost_price_method = fields.Selection(
'get_cost_price_methods', "Cost Price Method")
2017-04-01 00:31:59 +02:00
@classmethod
def get_cost_price_methods(cls):
pool = Pool()
Template = pool.get('product.template')
field_name = 'cost_price_method'
methods = Template.fields_get([field_name])[field_name]['selection']
methods.append((None, ''))
return methods
class ProductCostPrice(ModelSQL, CompanyValueMixin):
"Product Cost Price"
__name__ = 'product.cost_price'
product = fields.Many2One(
'product.product', "Product", ondelete='CASCADE',
context={
'company': Eval('company', -1),
},
depends={'company'})
cost_price = fields.Numeric(
2021-09-13 23:29:29 +02:00
"Cost Price", required=True, digits=price_digits)
@classmethod
def __setup__(cls):
super().__setup__()
cls.company.required = True
2016-03-19 09:05:13 +01:00
class TemplateCategory(ModelSQL):
'Template - Category'
__name__ = 'product.template-product.category'
template = fields.Many2One(
'product.template', "Template", ondelete='CASCADE', required=True)
category = fields.Many2One(
'product.category', "Category", ondelete='CASCADE', required=True)
2017-10-02 16:06:36 +02:00
class TemplateCategoryAll(UnionMixin, ModelSQL):
"Template - Category All"
__name__ = 'product.template-product.category.all'
template = fields.Many2One('product.template', "Template")
category = fields.Many2One('product.category', "Category")
@classmethod
def union_models(cls):
return ['product.template-product.category']
2019-06-06 19:20:43 +02:00
class ProductIdentifier(sequence_ordered(), ModelSQL, ModelView):
"Product Identifier"
__name__ = 'product.identifier'
_rec_name = 'code'
product = fields.Many2One(
'product.product', "Product", ondelete='CASCADE', required=True,
2020-04-13 17:50:45 +02:00
help="The product identified by the code.")
2019-06-06 19:20:43 +02:00
type = fields.Selection([
(None, ''),
('ean', "International Article Number"),
('isan', "International Standard Audiovisual Number"),
('isbn', "International Standard Book Number"),
('isil', "International Standard Identifier for Libraries"),
('isin', "International Securities Identification Number"),
('ismn', "International Standard Music Number"),
], "Type")
type_string = type.translated('type')
code = fields.Char("Code", required=True)
2021-03-07 11:13:53 +01:00
@classmethod
def __setup__(cls):
cls.code.search_unaccented = False
2021-03-07 11:13:53 +01:00
super().__setup__()
cls.__access__.add('product')
t = cls.__table__()
cls._sql_indexes.update({
Index(
t,
(t.product, Index.Equality()),
(t.code, Index.Similarity())),
})
2021-03-07 11:13:53 +01:00
2019-06-06 19:20:43 +02:00
@fields.depends('type', 'code')
def on_change_with_code(self):
if self.type and self.type != 'other':
try:
module = import_module('stdnum.%s' % self.type)
return module.compact(self.code)
except ImportError:
2019-06-06 19:20:43 +02:00
pass
except stdnum.exceptions.ValidationError:
pass
return self.code
def pre_validate(self):
super().pre_validate()
self.check_code()
@fields.depends('type', 'product', 'code')
def check_code(self):
if self.type:
try:
module = import_module('stdnum.%s' % self.type)
except ModuleNotFoundError:
return
if not module.is_valid(self.code):
if self.product and self.product.id > 0:
product = self.product.rec_name
else:
product = ''
raise InvalidIdentifierCode(
gettext('product.msg_invalid_code',
type=self.type_string,
code=self.code,
product=product))
2022-12-26 22:32:23 +01:00
def barcode(self, format='svg', **options):
if barcode and self.type in barcode.BARCODES:
generator = getattr(barcode, 'generate_%s' % format)
return generator(self.type, self.on_change_with_code(), **options)