The price_list_used is set on property so it is executed under the transaction context but we expect to have the price list from the company set in the instance context. issue10664 review369491002
835 lines
29 KiB
Python
835 lines
29 KiB
Python
# 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
|
|
import logging
|
|
from decimal import Decimal
|
|
from importlib import import_module
|
|
|
|
import stdnum
|
|
import stdnum.exceptions
|
|
from sql import Null, Column, Literal
|
|
from sql.operators import Equal
|
|
|
|
from trytond.i18n import gettext
|
|
from trytond.model import (
|
|
ModelView, ModelSQL, Model, UnionMixin, DeactivableMixin, sequence_ordered,
|
|
Exclude, fields)
|
|
from trytond.pyson import Eval, If, Get
|
|
from trytond.transaction import Transaction
|
|
from trytond.pool import Pool
|
|
from trytond import backend
|
|
from trytond.tools import lstrip_wildcard
|
|
from trytond.tools.multivalue import migrate_property
|
|
from trytond.modules.company.model import (
|
|
CompanyMultiValueMixin, CompanyValueMixin)
|
|
from .exceptions import InvalidIdentifierCode
|
|
from .ir import price_decimal
|
|
|
|
|
|
__all__ = ['price_digits', 'round_price', 'TemplateFunction']
|
|
logger = logging.getLogger(__name__)
|
|
|
|
TYPES = [
|
|
('goods', 'Goods'),
|
|
('assets', 'Assets'),
|
|
('service', 'Service'),
|
|
]
|
|
COST_PRICE_METHODS = [
|
|
('fixed', 'Fixed'),
|
|
('average', 'Average'),
|
|
]
|
|
|
|
price_digits = (16, price_decimal)
|
|
|
|
|
|
def round_price(value, rounding=None):
|
|
"Round price using the price digits"
|
|
return value.quantize(
|
|
Decimal(1) / 10 ** price_digits[1], rounding=rounding)
|
|
|
|
|
|
class Template(
|
|
DeactivableMixin, ModelSQL, ModelView, CompanyMultiValueMixin):
|
|
"Product Template"
|
|
__name__ = "product.template"
|
|
_order_name = 'rec_name'
|
|
name = fields.Char(
|
|
"Name", size=None, required=True, translate=True, select=True)
|
|
code_readonly = fields.Function(
|
|
fields.Boolean("Code Readonly"), 'get_code_readonly')
|
|
code = fields.Char(
|
|
"Code", select=True,
|
|
states={
|
|
'readonly': Eval('code_readonly', False),
|
|
},
|
|
depends=['code_readonly'])
|
|
type = fields.Selection(TYPES, "Type", required=True)
|
|
consumable = fields.Boolean('Consumable',
|
|
states={
|
|
'invisible': Eval('type', 'goods') != 'goods',
|
|
},
|
|
depends=['type'],
|
|
help="Check to allow stock moves to be assigned "
|
|
"regardless of stock level.")
|
|
list_price = fields.MultiValue(fields.Numeric(
|
|
"List Price", required=True, digits=price_digits,
|
|
help="The standard price the product is sold at."))
|
|
list_prices = fields.One2Many(
|
|
'product.list_price', 'template', "List Prices")
|
|
cost_price = fields.Function(fields.Numeric(
|
|
"Cost Price", digits=price_digits,
|
|
help="The amount it costs to purchase or make the product, "
|
|
"or carry out the service."),
|
|
'get_cost_price')
|
|
cost_price_method = fields.MultiValue(fields.Selection(
|
|
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")
|
|
default_uom = fields.Many2One('product.uom', "Default UOM", required=True,
|
|
help="The standard unit of measure for the product.\n"
|
|
"Used internally when calculating the stock levels of goods "
|
|
"and assets.")
|
|
default_uom_category = fields.Function(
|
|
fields.Many2One('product.uom.category', 'Default UOM Category'),
|
|
'on_change_with_default_uom_category',
|
|
searcher='search_default_uom_category')
|
|
default_uom_digits = fields.Function(fields.Integer("Default Unit Digits"),
|
|
'on_change_with_default_uom_digits')
|
|
categories = fields.Many2Many(
|
|
'product.template-product.category', 'template', 'category',
|
|
"Categories",
|
|
help="The categories that the product is in.\n"
|
|
"Used to group similar products together.")
|
|
categories_all = fields.Many2Many(
|
|
'product.template-product.category.all',
|
|
'template', 'category', "Categories", readonly=True)
|
|
products = fields.One2Many(
|
|
'product.product', 'template', "Variants",
|
|
domain=[
|
|
If(~Eval('active'), ('active', '=', False), ()),
|
|
],
|
|
depends=['active'],
|
|
help="The different variants the product comes in.")
|
|
|
|
@classmethod
|
|
def __register__(cls, module_name):
|
|
super(Template, cls).__register__(module_name)
|
|
|
|
table = cls.__table_handler__(module_name)
|
|
|
|
# Migration from 3.8: rename category into categories
|
|
if table.column_exist('category'):
|
|
logger.warning(
|
|
'The column "category" on table "%s" must be dropped manually',
|
|
cls._table)
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
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'), []),
|
|
]
|
|
cls.cost_price_method.depends = ['type']
|
|
|
|
@classmethod
|
|
def _cost_price_method_domain_per_type(cls):
|
|
return {'service': [('cost_price_method', '=', 'fixed')]}
|
|
|
|
@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')
|
|
return super(Template, cls).multivalue_model(field)
|
|
|
|
@classmethod
|
|
def order_rec_name(cls, tables):
|
|
table, _ = tables[None]
|
|
return [table.code, table.name]
|
|
|
|
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):
|
|
if clause[1].startswith('!') or clause[1].startswith('not '):
|
|
bool_op = 'AND'
|
|
else:
|
|
bool_op = 'OR'
|
|
code_value = clause[2]
|
|
if clause[1].endswith('like'):
|
|
code_value = lstrip_wildcard(clause[2])
|
|
return [bool_op,
|
|
('name',) + tuple(clause[1:]),
|
|
('code', clause[1], code_value) + tuple(clause[3:]),
|
|
('products.code', clause[1], code_value) + tuple(clause[3:]),
|
|
('products.identifiers.code', clause[1], code_value)
|
|
+ tuple(clause[3:]),
|
|
]
|
|
|
|
@staticmethod
|
|
def default_type():
|
|
return 'goods'
|
|
|
|
@staticmethod
|
|
def default_consumable():
|
|
return False
|
|
|
|
def get_cost_price(self, name):
|
|
if len(self.products) == 1:
|
|
product, = self.products
|
|
return product.cost_price
|
|
|
|
@classmethod
|
|
def default_cost_price_method(cls, **pattern):
|
|
pool = Pool()
|
|
Configuration = pool.get('product.configuration')
|
|
return Configuration(1).get_multivalue(
|
|
'default_cost_price_method', **pattern)
|
|
|
|
@classmethod
|
|
def default_products(cls):
|
|
transaction = Transaction()
|
|
if (transaction.user == 0
|
|
or not transaction.context.get('default_products', True)):
|
|
return []
|
|
return [{}]
|
|
|
|
@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'
|
|
|
|
@fields.depends('default_uom')
|
|
def on_change_with_default_uom_category(self, name=None):
|
|
if self.default_uom:
|
|
return self.default_uom.category.id
|
|
|
|
@classmethod
|
|
def search_default_uom_category(cls, name, clause):
|
|
return [('default_uom.category' + clause[0].lstrip(name),)
|
|
+ tuple(clause[1:])]
|
|
|
|
@fields.depends('default_uom')
|
|
def on_change_with_default_uom_digits(self, name=None):
|
|
if self.default_uom:
|
|
return self.default_uom.digits
|
|
|
|
@classmethod
|
|
def _new_code(cls):
|
|
pool = Pool()
|
|
Configuration = pool.get('product.configuration')
|
|
config = Configuration(1)
|
|
sequence = config.template_sequence
|
|
if sequence:
|
|
return sequence.get()
|
|
|
|
@classmethod
|
|
def create(cls, vlist):
|
|
pool = Pool()
|
|
Product = pool.get('product.product')
|
|
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()
|
|
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)
|
|
|
|
@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)
|
|
|
|
@classmethod
|
|
def search_global(cls, text):
|
|
for record, rec_name, icon in super(Template, cls).search_global(text):
|
|
icon = icon or 'tryton-product'
|
|
yield record, rec_name, icon
|
|
|
|
|
|
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
|
|
|
|
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
|
|
|
|
|
|
class Product(
|
|
DeactivableMixin, ModelSQL, ModelView, CompanyMultiValueMixin):
|
|
"Product Variant"
|
|
__name__ = "product.product"
|
|
_order_name = 'rec_name'
|
|
template = fields.Many2One(
|
|
'product.template', "Product Template",
|
|
required=True, ondelete='CASCADE', select=True,
|
|
search_context={'default_products': False},
|
|
domain=[
|
|
If(Eval('active'), ('active', '=', True), ()),
|
|
],
|
|
depends=['active'],
|
|
help="The product that defines the common properties "
|
|
"inherited by the variant.")
|
|
code_readonly = fields.Function(fields.Boolean('Code Readonly'),
|
|
'get_code_readonly')
|
|
prefix_code = fields.Function(fields.Char(
|
|
"Prefix Code",
|
|
states={
|
|
'invisible': ~Eval('prefix_code'),
|
|
}),
|
|
'on_change_with_prefix_code')
|
|
suffix_code = fields.Char(
|
|
"Suffix Code",
|
|
states={
|
|
'readonly': Eval('code_readonly', False),
|
|
},
|
|
depends=['code_readonly'],
|
|
help="The unique identifier for the product (aka SKU).")
|
|
code = fields.Char("Code", readonly=True, select=True,
|
|
help="A unique identifier for the variant.")
|
|
identifiers = fields.One2Many(
|
|
'product.identifier', 'product', "Identifiers",
|
|
help="Other identifiers associated with the variant.")
|
|
cost_price = fields.MultiValue(fields.Numeric(
|
|
"Cost Price", required=True, digits=price_digits,
|
|
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)
|
|
list_price_uom = fields.Function(fields.Numeric('List Price',
|
|
digits=price_digits), 'get_price_uom')
|
|
cost_price_uom = fields.Function(fields.Numeric('Cost Price',
|
|
digits=price_digits), 'get_price_uom')
|
|
|
|
@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'])
|
|
|
|
super(Product, cls).__setup__()
|
|
cls.__access__.add('template')
|
|
cls._order.insert(0, ('rec_name', 'ASC'))
|
|
|
|
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'),
|
|
]
|
|
|
|
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
|
|
if 'template' not in tfield.depends:
|
|
tfield.depends.append('template')
|
|
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'
|
|
|
|
@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
|
|
|
|
@fields.depends('template', '_parent_template.id')
|
|
def on_change_template(self):
|
|
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):
|
|
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
|
|
|
|
@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)
|
|
|
|
@classmethod
|
|
def default_cost_price(cls, **pattern):
|
|
return Decimal(0)
|
|
|
|
@classmethod
|
|
def search_template(cls, name, clause):
|
|
return [('template.' + clause[0],) + tuple(clause[1:])]
|
|
|
|
@classmethod
|
|
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']
|
|
return [product.code] + Template.name.convert_order('name',
|
|
tables['template'], Template)
|
|
|
|
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):
|
|
if clause[1].startswith('!') or clause[1].startswith('not '):
|
|
bool_op = 'AND'
|
|
else:
|
|
bool_op = 'OR'
|
|
code_value = clause[2]
|
|
if clause[1].endswith('like'):
|
|
code_value = lstrip_wildcard(clause[2])
|
|
return [bool_op,
|
|
('code', clause[1], code_value) + tuple(clause[3:]),
|
|
('identifiers.code', clause[1], code_value) + tuple(clause[3:]),
|
|
('template.name',) + tuple(clause[1:]),
|
|
('template.code', clause[1], code_value) + tuple(clause[3:]),
|
|
]
|
|
|
|
@staticmethod
|
|
def get_price_uom(products, name):
|
|
Uom = Pool().get('product.uom')
|
|
res = {}
|
|
field = name[:-4]
|
|
if Transaction().context.get('uom'):
|
|
to_uom = Uom(Transaction().context['uom'])
|
|
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
|
|
return res
|
|
|
|
@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
|
|
|
|
@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()
|
|
|
|
@classmethod
|
|
def _new_suffix_code(cls):
|
|
pool = Pool()
|
|
Configuration = pool.get('product.configuration')
|
|
config = Configuration(1)
|
|
sequence = config.product_sequence
|
|
if sequence:
|
|
return sequence.get()
|
|
|
|
@classmethod
|
|
def create(cls, vlist):
|
|
vlist = [x.copy() for x in vlist]
|
|
for values in vlist:
|
|
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()
|
|
default.setdefault('suffix_code', None)
|
|
default.setdefault('code', None)
|
|
return super().copy(products, default=default)
|
|
|
|
@property
|
|
def list_price_used(self):
|
|
transaction = Transaction()
|
|
with transaction.reset_context(), \
|
|
transaction.set_context(self._context):
|
|
return self.template.get_multivalue('list_price')
|
|
|
|
@classmethod
|
|
def sync_code(cls, products):
|
|
for product in products:
|
|
code = ''.join(filter(None, [
|
|
product.prefix_code, product.suffix_code]))
|
|
if not code:
|
|
code = None
|
|
if code != product.code:
|
|
product.code = code
|
|
cls.save(products)
|
|
|
|
|
|
class ProductListPrice(ModelSQL, CompanyValueMixin):
|
|
"Product List Price"
|
|
__name__ = 'product.list_price'
|
|
template = fields.Many2One(
|
|
'product.template', "Template", ondelete='CASCADE', select=True,
|
|
context={
|
|
'company': Eval('company', -1),
|
|
},
|
|
depends=['company'])
|
|
list_price = fields.Numeric("List Price", digits=price_digits)
|
|
|
|
@classmethod
|
|
def __register__(cls, module_name):
|
|
exist = backend.TableHandler.table_exist(cls._table)
|
|
|
|
super(ProductListPrice, cls).__register__(module_name)
|
|
|
|
if not exist:
|
|
cls._migrate_property([], [], [])
|
|
|
|
@classmethod
|
|
def _migrate_property(cls, field_names, value_names, fields):
|
|
field_names.append('list_price')
|
|
value_names.append('list_price')
|
|
fields.append('company')
|
|
migrate_property(
|
|
'product.template', field_names, cls, value_names,
|
|
parent='template', fields=fields)
|
|
|
|
|
|
class ProductCostPriceMethod(ModelSQL, CompanyValueMixin):
|
|
"Product Cost Price Method"
|
|
__name__ = 'product.cost_price_method'
|
|
template = fields.Many2One(
|
|
'product.template', "Template", ondelete='CASCADE', select=True,
|
|
context={
|
|
'company': Eval('company', -1),
|
|
},
|
|
depends=['company'])
|
|
cost_price_method = fields.Selection(
|
|
'get_cost_price_methods', "Cost Price Method")
|
|
|
|
@classmethod
|
|
def __register__(cls, module_name):
|
|
pool = Pool()
|
|
ProductCostPrice = pool.get('product.cost_price')
|
|
sql_table = cls.__table__()
|
|
cost_price = ProductCostPrice.__table__()
|
|
cursor = Transaction().connection.cursor()
|
|
|
|
exist = backend.TableHandler.table_exist(cls._table)
|
|
cost_price_exist = backend.TableHandler.table_exist(
|
|
ProductCostPrice._table)
|
|
|
|
super(ProductCostPriceMethod, cls).__register__(module_name)
|
|
|
|
# Migrate from 4.4: move cost_price_method from ProductCostPrice
|
|
if not exist and not cost_price_exist:
|
|
cls._migrate_property([], [], [])
|
|
elif not exist and cost_price_exist:
|
|
cost_price_table = backend.TableHandler(
|
|
ProductCostPrice, module_name)
|
|
if cost_price_table.column_exist('template'):
|
|
columns = ['create_uid', 'create_date',
|
|
'write_uid', 'write_date',
|
|
'template', 'cost_price_method']
|
|
cursor.execute(*sql_table.insert(
|
|
columns=[Column(sql_table, c) for c in columns],
|
|
values=cost_price.select(
|
|
*[Column(cost_price, c) for c in columns])))
|
|
|
|
@classmethod
|
|
def _migrate_property(cls, field_names, value_names, fields):
|
|
field_names.append('cost_price_method')
|
|
value_names.append('cost_price_method')
|
|
fields.append('company')
|
|
migrate_property(
|
|
'product.template', field_names, cls, value_names,
|
|
parent='template', fields=fields)
|
|
|
|
@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', select=True,
|
|
context={
|
|
'company': Eval('company', -1),
|
|
},
|
|
depends=['company'])
|
|
cost_price = fields.Numeric(
|
|
"Cost Price", digits=price_digits)
|
|
|
|
@classmethod
|
|
def __register__(cls, module_name):
|
|
pool = Pool()
|
|
Product = pool.get('product.product')
|
|
sql_table = cls.__table__()
|
|
product = Product.__table__()
|
|
cursor = Transaction().connection.cursor()
|
|
|
|
exist = backend.TableHandler.table_exist(cls._table)
|
|
|
|
super(ProductCostPrice, cls).__register__(module_name)
|
|
|
|
table = cls.__table_handler__(module_name)
|
|
if not exist:
|
|
# Create template column for property migration
|
|
table.add_column('template', 'INTEGER')
|
|
cls._migrate_property([], [], [])
|
|
|
|
# Migration from 4.4: replace template by product
|
|
if table.column_exist('template'):
|
|
columns = ['create_uid', 'create_date',
|
|
'write_uid', 'write_date', 'cost_price']
|
|
cursor.execute(*sql_table.insert(
|
|
columns=[Column(sql_table, c) for c in columns]
|
|
+ [sql_table.product],
|
|
values=sql_table.join(product,
|
|
condition=sql_table.template == product.template
|
|
).select(
|
|
*[Column(sql_table, c) for c in columns]
|
|
+ [product.id],
|
|
where=(sql_table.template != Null)
|
|
& (sql_table.product == Null))))
|
|
cursor.execute(*sql_table.delete(
|
|
where=(sql_table.template != Null)
|
|
& (sql_table.product == Null)))
|
|
table.drop_column('template')
|
|
|
|
@classmethod
|
|
def _migrate_property(cls, field_names, value_names, fields):
|
|
field_names.append('cost_price')
|
|
value_names.append('cost_price')
|
|
fields.append('company')
|
|
migrate_property(
|
|
'product.template', field_names, cls, value_names,
|
|
parent='template', fields=fields)
|
|
|
|
|
|
class TemplateCategory(ModelSQL):
|
|
'Template - Category'
|
|
__name__ = 'product.template-product.category'
|
|
template = fields.Many2One('product.template', 'Template',
|
|
ondelete='CASCADE', required=True, select=True)
|
|
category = fields.Many2One('product.category', 'Category',
|
|
ondelete='CASCADE', required=True, select=True)
|
|
|
|
|
|
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']
|
|
|
|
|
|
class ProductIdentifier(sequence_ordered(), ModelSQL, ModelView):
|
|
"Product Identifier"
|
|
__name__ = 'product.identifier'
|
|
_rec_name = 'code'
|
|
product = fields.Many2One('product.product', "Product", ondelete='CASCADE',
|
|
required=True, select=True,
|
|
help="The product identified by the code.")
|
|
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)
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls.__access__.add('product')
|
|
|
|
@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:
|
|
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))
|