class DeactivableMixin(Model): "Mixin to allow to soft deletion of records" __slots__ = () active = fields.Boolean(lazy_gettext('ir.msg_active'), help=lazy_gettext('ir.msg_active_help')) @classmethod def default_active(cls): return True @classmethod def __post_setup__(cls): super().__post_setup__() inactive = ~Eval('active', cls.default_active()) for name, field in cls._fields.items(): if name == 'active': continue if 'readonly' in field.states: field.states['readonly'] |= inactive else: field.states['readonly'] = inactive if 'active' not in field.depends: field.depends.append('active') if issubclass(cls, ModelView): for states in cls._buttons.values(): if 'readonly' in states: states['readonly'] |= inactive else: states['readonly'] = inactive if 'active' not in states.setdefault('depends', []): states['depends'].append('active')
class DeactivableMixin(object): "Mixin to allow to soft deletion of records" __slots__ = () active = fields.Boolean(lazy_gettext('ir.msg_active'), help=lazy_gettext('ir.msg_active_help')) @classmethod def default_active(cls): return True
class ShopVSFIdentifierMixin: __slots__ = () vsf_identifier = fields.Many2One( 'web.shop.vsf_identifier', lazy_gettext('web_shop_vue_storefront.msg_vsf_identifier'), ondelete='RESTRICT', readonly=True) @classmethod def set_vsf_identifier(cls, records): pool = Pool() Identifier = pool.get('web.shop.vsf_identifier') for record in records: if not record.vsf_identifier: record.vsf_identifier = Identifier(record=record) cls.save(records)
def sequence_ordered(field_name='sequence', field_label=lazy_gettext('ir.msg_sequence'), order='ASC NULLS FIRST'): "Returns a mixin to order the model by order fields" class SequenceOrderedMixin(object): "Mixin to order model by a sequence field" __slots__ = () @classmethod def __setup__(cls): super(SequenceOrderedMixin, cls).__setup__() cls._order = [(field_name, order)] + cls._order setattr(SequenceOrderedMixin, field_name, fields.Integer(field_label)) return SequenceOrderedMixin
def resource_copy(resource, name, string): class _ResourceCopyMixin(ResourceCopyMixin): @classmethod def copy(cls, records, default=None): if default is None: default = {} else: default = default.copy() default.setdefault(name, None) return super().copy(records, default=default) def copy_resources_to(self, target): pool = Pool() Resource = pool.get(resource) try: super().copy_resources_to(target) except AttributeError: pass to_copy = [] for record in getattr(self, name): if (record.copy_to_resources and target.__name__ in record.copy_to_resources): to_copy.append(record) if to_copy: return Resource.copy(to_copy, default={ 'resource': str(target), 'copy_to_resources': None, }) setattr( _ResourceCopyMixin, name, fields.One2Many(resource, 'resource', string, help=lazy_gettext('ir.msg_resource_copy_help'))) return _ResourceCopyMixin
class AttachmentCopyMixin( resource_copy( 'ir.attachment', 'attachments', lazy_gettext('ir.msg_attachments'))): pass
class AvatarMixin: avatars = fields.One2Many('ir.avatar', 'resource', lazy_gettext('ir.msg_avatars'), size=1) avatar = fields.Function(fields.Binary(lazy_gettext('ir.msg_avatar')), '_get_avatar', setter='_set_avatar') avatar_url = fields.Function( fields.Char(lazy_gettext('ir.msg_avatar_url')), '_get_avatar_url') @property def has_avatar(self): if self.avatars: avatar, = self.avatars return bool(avatar.image_id or avatar.image) return False def _get_avatar(self, name): if self.avatars: avatar, = self.avatars return avatar.get(size=size) return None @classmethod def _set_avatar(cls, records, name, value): pool = Pool() Avatar = pool.get('ir.avatar') avatars = [] image = Avatar.convert(value) for record in records: if record.avatars: avatar, = record.avatars else: avatar = Avatar(resource=record) avatars.append(avatar) Avatar.save(avatars) # Use write the image to store only once in filestore Avatar.write(avatars, { 'image': image, }) def _get_avatar_url(self, name): if self.avatars: avatar, = self.avatars return avatar.url @classmethod def generate_avatar(cls, records, field='rec_name'): from trytond.ir.avatar import generate, PIL if not PIL: return records = [r for r in records if not r.has_avatar] if not records: return for record in records: record.avatar = generate(size, getattr(record, field)) cls.save(records) if default: @classmethod def create(cls, vlist): records = super().create(vlist) cls.generate_avatar(records, field=default) return records @classmethod def write(cls, *args): records = sum(args[0:None:2], []) super().write(*args) cls.generate_avatar(records, field=default)
class Model(URLMixin, PoolBase, metaclass=ModelMeta): """ Define a model in Tryton. """ __slots__ = ('_id', '_values', '_init_values', '_removed', '_deleted') _rec_name = 'name' id = fields.Integer(lazy_gettext('ir.msg_ID'), readonly=True) @classmethod def __setup__(cls): super(Model, cls).__setup__() cls.__rpc__ = { 'default_get': RPC(cache=dict(seconds=5 * 60)), 'fields_get': RPC(cache=dict(days=1)), 'pre_validate': RPC(instantiate=0), } cls.__access__ = set() # Copy fields and update depends for attr in dir(cls): if attr.startswith('_'): continue if not isinstance(getattr(cls, attr), fields.Field): continue field_name = attr field = getattr(cls, field_name) # Copy the original field definition to prevent side-effect with # the mutable attributes for parent_cls in cls.__mro__: parent_field = getattr(parent_cls, field_name, None) if isinstance(parent_field, fields.Field): field = parent_field field = copy.deepcopy(field) setattr(cls, field_name, field) @classmethod def __post_setup__(cls): super(Model, cls).__post_setup__() # Set _fields cls._fields = {} for attr in dir(cls): if attr.startswith('_'): continue if isinstance(getattr(cls, attr), fields.Field): cls._fields[attr] = getattr(cls, attr) cls._record = record(cls.__name__ + '._record', cls._fields.keys()) # Set _defaults cls._defaults = {} fields_names = list(cls._fields.keys()) for field_name in fields_names: default_method = getattr(cls, 'default_%s' % field_name, False) if callable(default_method): cls._defaults[field_name] = default_method for k in cls._defaults: assert k in cls._fields, \ 'Default function defined in %s but field %s does not exist!' \ % (cls.__name__, k,) # Set name to fields for name, field in cls._fields.items(): if field.name is None: field.name = name else: assert field.name == name, ('Duplicate fields on %s: %s, %s' % (cls, field.name, name)) @classmethod def _get_name(cls): ''' Returns the first non-empty line of the model docstring. ''' assert cls.__doc__, '%s has no docstring' % cls lines = cls.__doc__.splitlines() for line in lines: line = line.strip() if line: return line @classmethod def __register__(cls, module_name): """ Add model in ir.model and ir.model.field. """ super(Model, cls).__register__(module_name) pool = Pool() Translation = pool.get('ir.translation') Model_ = pool.get('ir.model') ModelField = pool.get('ir.model.field') model_id = Model_.register(cls, module_name) ModelField.register(cls, module_name, model_id) Translation.register_model(cls, module_name) Translation.register_fields(cls, module_name) @classmethod def default_get(cls, fields_names, with_rec_name=True): ''' Return a dict with the default values for each field in fields_names. If with_rec_name is True, rec_name will be added. ''' pool = Pool() value = {} default_rec_name = Transaction().context.get('default_rec_name') if (default_rec_name and cls._rec_name in cls._fields and cls._rec_name in fields_names): value[cls._rec_name] = default_rec_name # get the default values defined in the object for field_name in fields_names: if field_name in cls._defaults: value[field_name] = cls._defaults[field_name]() field = cls._fields[field_name] if (field._type == 'boolean' and field_name not in value): value[field_name] = False if (with_rec_name and field._type in ('many2one', ) and value.get(field_name)): Target = pool.get(field.model_name) if 'rec_name' in Target._fields: value.setdefault(field_name + '.', {})['rec_name'] = Target( value[field_name]).rec_name return value @classmethod def fields_get(cls, fields_names=None, level=0): """ Return the definition of each field on the model. """ definition = {} pool = Pool() Translation = pool.get('ir.translation') FieldAccess = pool.get('ir.model.field.access') ModelAccess = pool.get('ir.model.access') # Add translation to cache language = Transaction().language trans_args = [] for fname, field in cls._fields.items(): if fields_names and fname not in fields_names: continue trans_args.extend(field.definition_translations(cls, language)) Translation.get_sources(trans_args) encoder = PYSONEncoder() decoder = PYSONDecoder(noeval=True) accesses = FieldAccess.get_access([cls.__name__])[cls.__name__] for fname, field in cls._fields.items(): if fields_names and fname not in fields_names: continue definition[fname] = field.definition(cls, language) if not accesses.get(fname, {}).get('write', True): definition[fname]['readonly'] = True states = decoder.decode(definition[fname]['states']) states.pop('readonly', None) definition[fname]['states'] = encoder.encode(states) for right in ['create', 'delete']: definition[fname][right] = accesses.get(fname, {}).get(right, True) if level > 0: relation = definition[fname].get('relation') if relation: Relation = pool.get(relation) relation_fields = Relation.fields_get(level=level - 1) definition[fname]['relation_fields'] = relation_fields for name, props in relation_fields.items(): # Convert selection into list if isinstance(props.get('selection'), str): change_with = props.get('selection_change_with') if change_with: selection = getattr(Relation(), props['selection'])() else: selection = getattr(Relation, props['selection'])() props['selection'] = selection schema = definition[fname].get('schema_model') if schema: Schema = pool.get(schema) definition[fname]['relation_fields'] = ( Schema.get_relation_fields()) for fname in list(definition.keys()): # filter out fields which aren't in the fields_names list if fields_names: if fname not in fields_names: del definition[fname] elif not ModelAccess.check_relation( cls.__name__, fname, mode='read'): del definition[fname] return definition def pre_validate(self): pass @classmethod def __names__(cls, field=None): pool = Pool() IrModel = pool.get('ir.model') IrModelField = pool.get('ir.model.field') names = { 'model': IrModel.get_name(cls.__name__), } if field: names['field'] = IrModelField.get_name(cls.__name__, field) return names def __init__(self, id=None, **kwargs): super(Model, self).__init__() if id is not None: id = int(id) self._id = id self._deleted = self._removed = None if kwargs: self._values = self._record() parent_values = defaultdict(dict) has_context = {} for name, value in kwargs.items(): if not name.startswith('_parent_'): setattr(self, name, value) else: name, field = name.split('.', 1) name = name[len('_parent_'):] parent_values[name][field] = value value = parent_values[name] if getattr(self.__class__, name).context: has_context[name] = value for name, value in parent_values.items(): setattr(self, name, value) # Set field with context a second times # to ensure it was evaluated with all the fields for name, value in has_context.items(): setattr(self, name, value) self._init_values = self._values._copy() else: self._values = None self._init_values = None def __copy__(self): copied = self.__class__(self.id) copied._values = copy.copy(self._values) copied._init_values = copy.copy(self._init_values) return copied def __getattr__(self, name): if name.startswith('__') and name.endswith('__'): raise AttributeError try: return self._values[name] except (KeyError, TypeError): raise AttributeError("'%s' Model has no attribute '%s': %s" % (self.__name__, name, self._values)) def __contains__(self, name): return name in self._fields def __int__(self): return int(self.id) def __str__(self): return '%s,%s' % (self.__name__, self.id) def __repr__(self): if self.id is None or self.id < 0: return "Pool().get('%s')(**%s)" % (self.__name__, repr(self._default_values)) else: return "Pool().get('%s')(%s)" % (self.__name__, self.id) def __eq__(self, other): if not isinstance(other, Model): return NotImplemented elif self.id is None or other.id is None: return id(self) == id(other) return (self.__name__, self.id) == (other.__name__, other.id) def __lt__(self, other): if not isinstance(other, Model) or self.__name__ != other.__name__: return NotImplemented return self.id < other.id def __ne__(self, other): return not self == other def __hash__(self): return hash((self.__name__, id(self) if self.id is None else self.id)) def __bool__(self): return True @property def _default_values(self): """Return the values not stored. By default, the value of a field is its internal representation except: - for Many2One and One2One field: the id - for Reference field: the string model,id - for Many2Many: the list of ids - for One2Many: the list of `_default_values` """ values = {} # JCA : preload rec names if in called from _changed_values add_rec_names = ServerContext().get('_default_rec_names', False) if self._values: for fname, value in self._values._items(): field = self._fields[fname] rec_name = None if field._type in ('many2one', 'one2one', 'reference'): if value: if add_rec_names: rec_name = getattr(value, 'rec_name', '') if field._type == 'reference': value = str(value) else: value = value.id elif field._type in ('one2many', 'many2many'): if field._type == 'one2many': value = [r._default_values for r in value] else: value = [r.id for r in value] values[fname] = value if rec_name is not None: values['%s.' % fname] = {'rec_name': rec_name} return values
class DictSchemaMixin(object): __slots__ = () _rec_name = 'string' name = fields.Char(lazy_gettext('ir.msg_dict_schema_name'), required=True) string = fields.Char(lazy_gettext('ir.msg_dict_schema_string'), translate=True, required=True) help = fields.Text(lazy_gettext('ir.msg_dict_schema_help'), translate=True) type_ = fields.Selection([ ('boolean', lazy_gettext('ir.msg_dict_schema_boolean')), ('integer', lazy_gettext('ir.msg_dict_schema_integer')), ('char', lazy_gettext('ir.msg_dict_schema_char')), ('float', lazy_gettext('ir.msg_dict_schema_float')), ('numeric', lazy_gettext('ir.msg_dict_schema_numeric')), ('date', lazy_gettext('ir.msg_dict_schema_date')), ('datetime', lazy_gettext('ir.msg_dict_schema_datetime')), ('selection', lazy_gettext('ir.msg_dict_schema_selection')), ('multiselection', lazy_gettext('ir.msg_dict_schema_multiselection')), ], lazy_gettext('ir.msg_dict_schema_type'), required=True) digits = fields.Integer(lazy_gettext('ir.msg_dict_schema_digits'), states={ 'invisible': ~Eval('type_').in_(['float', 'numeric']), }, depends=['type_']) domain = fields.Char(lazy_gettext('ir.msg_dict_schema_domain')) selection = fields.Text( lazy_gettext('ir.msg_dict_schema_selection'), states={ 'invisible': ~Eval('type_').in_(['selection', 'multiselection']), }, translate=True, depends=['type_'], help=lazy_gettext('ir.msg_dict_schema_selection_help')) selection_sorted = fields.Boolean( lazy_gettext('ir.msg_dict_schema_selection_sorted'), states={ 'invisible': ~Eval('type_').in_(['selection', 'multiselection']), }, depends=['type_'], help=lazy_gettext('ir.msg_dict_schema_selection_sorted_help')) help_selection = fields.Text( lazy_gettext('ir.msg_dict_schema_help_selection'), translate=True, states={ 'invisible': ~Eval('type_').in_(['selection', 'multiselection']), }, depends=['type_'], help=lazy_gettext('is.msg_dict_schema_help_selection_help')) selection_json = fields.Function( fields.Char(lazy_gettext('ir.msg_dict_schema_selection_json'), states={ 'invisible': ~Eval('type_').in_(['selection', 'multiselection']), }, depends=['type_']), 'get_selection_json') help_selection_json = fields.Function( fields.Char(lazy_gettext('ir.msg_dict_schema_help_selection_json'), states={ 'invisible': ~Eval('type_').in_(['selection', 'multiselection']), }, depends=['type_']), 'get_selection_json') _relation_fields_cache = Cache('_dict_schema_mixin.get_relation_fields') @classmethod def __setup__(cls): super(DictSchemaMixin, cls).__setup__() cls.__rpc__.update({ 'get_keys': RPC(instantiate=0), }) @staticmethod def default_digits(): return 2 @staticmethod def default_selection_sorted(): return True @fields.depends('name', 'string') def on_change_string(self): if not self.name and self.string: self.name = slugify(self.string.lower(), hyphenate='_') @classmethod def validate(cls, schemas): super(DictSchemaMixin, cls).validate(schemas) cls.check_domain(schemas) cls.check_selection(schemas) @classmethod def check_domain(cls, schemas): for schema in schemas: if not schema.domain: continue try: value = PYSONDecoder().decode(schema.domain) except Exception: raise DomainError( gettext('ir.msg_dict_schema_invalid_domain', schema=schema.rec_name)) if not isinstance(value, list): raise DomainError( gettext('ir.msg_dict_schema_invalid_domain', schema=schema.rec_name)) @classmethod def check_selection(cls, schemas): for schema in schemas: if schema.type_ not in {'selection', 'multiselection'}: continue for name in ['selection', 'help_selection']: try: dict(json.loads(schema.get_selection_json(name + '_json'))) except Exception: raise SelectionError( gettext('ir.msg_dict_schema_invalid_%s' % name, schema=schema.rec_name)) def get_selection_json(self, name): field = name[:-len('_json')] db_selection = getattr(self, field) or '' selection = [[w.strip() for w in v.split(':', 1)] for v in db_selection.splitlines() if v] return json.dumps(selection, separators=(',', ':')) @classmethod def get_keys(cls, records): pool = Pool() Config = pool.get('ir.configuration') keys = [] for record in records: new_key = { 'id': record.id, 'name': record.name, 'string': record.string, 'help': record.help, 'type': record.type_, 'domain': record.domain, 'sequence': getattr(record, 'sequence', record.name), } if record.type_ in {'selection', 'multiselection'}: with Transaction().set_context(language=Config.get_language()): english_key = cls(record.id) selection = OrderedDict( json.loads(english_key.selection_json)) selection.update(dict(json.loads(record.selection_json))) new_key['selection'] = list(selection.items()) new_key['help_selection'] = dict( json.loads(record.help_selection_json)) new_key['sort'] = record.selection_sorted elif record.type_ in ('float', 'numeric'): new_key['digits'] = (16, record.digits) keys.append(new_key) return keys @classmethod def get_relation_fields(cls): if not config.get('dict', cls.__name__, default=True): return {} fields = cls._relation_fields_cache.get(cls.__name__) if fields is not None: return fields keys = cls.get_keys(cls.search([])) fields = {k['name']: k for k in keys} cls._relation_fields_cache.set(cls.__name__, fields) return fields @classmethod def create(cls, vlist): records = super().create(vlist) cls._relation_fields_cache.clear() return records @classmethod def write(cls, *args): super().write(*args) cls._relation_fields_cache.clear() @classmethod def delete(cls, records): super().delete(records) cls._relation_fields_cache.clear()
def test_lazy_gettext(self): "lazy_gettext returns a LazyString" lazy = lazy_gettext('tests.msg_test') self.assertIsInstance(lazy, LazyString) self.assertEqual(str(lazy), "Message")
class CategoryTree(ModelSQL, ModelView): "Stock Reporting Margin per Category" __name__ = 'stock.reporting.margin.category.tree' name = fields.Function(fields.Char("Name"), 'get_name') parent = fields.Many2One('stock.reporting.margin.category.tree', "Parent") children = fields.One2Many('stock.reporting.margin.category.tree', 'parent', "Children") cost = fields.Function( Monetary(lazy_gettext('stock.msg_stock_reporting_cost'), currency='currency', digits='currency'), 'get_total') revenue = fields.Function( Monetary(lazy_gettext('stock.msg_stock_reporting_revenue'), currency='currency', digits='currency'), 'get_total') profit = fields.Function( Monetary(lazy_gettext('stock.msg_stock_reporting_profit'), currency='currency', digits='currency'), 'get_total') margin = fields.Function( Monetary(lazy_gettext('stock.msg_stock_reporting_margin'), digits=(14, 4)), 'get_margin') currency = fields.Function( fields.Many2One('currency.currency', lazy_gettext('stock.msg_stock_reporting_currency')), 'get_currency') @classmethod def __setup__(cls): super().__setup__() cls._order.insert(0, ('name', 'ASC')) @classmethod def table_query(cls): pool = Pool() Category = pool.get('product.category') return Category.__table__() @classmethod def get_name(cls, categories, name): pool = Pool() Category = pool.get('product.category') categories = Category.browse(categories) return {c.id: c.name for c in categories} @classmethod def order_name(cls, tables): pool = Pool() Category = pool.get('product.category') table, _ = tables[None] if 'category' not in tables: category = Category.__table__() tables['category'] = { None: (category, table.id == category.id), } return Category.name.convert_order('name', tables['category'], Category) def time_series_all(self): return [] @classmethod def get_total(cls, categories, names): pool = Pool() ReportingCategory = pool.get('stock.reporting.margin.category') table = cls.__table__() reporting_category = ReportingCategory.__table__() cursor = Transaction().connection.cursor() categories = cls.search([ ('parent', 'child_of', [c.id for c in categories]), ]) ids = [c.id for c in categories] parents = {} reporting_categories = [] for sub_ids in grouped_slice(ids): sub_ids = list(sub_ids) where = reduce_ids(table.id, sub_ids) cursor.execute(*table.select(table.id, table.parent, where=where)) parents.update(cursor) where = reduce_ids(reporting_category.id, sub_ids) cursor.execute( *reporting_category.select(reporting_category.id, where=where)) reporting_categories.extend(r for r, in cursor) result = {} reporting_categories = ReportingCategory.browse(reporting_categories) for name in names: values = dict.fromkeys(ids, 0) values.update( (c.id, getattr(c, name)) for c in reporting_categories) result[name] = cls._sum_tree(categories, values, parents) return result @classmethod def _sum_tree(cls, categories, values, parents): result = values.copy() categories = set((c.id for c in categories)) leafs = categories - set(parents.values()) while leafs: for category in leafs: categories.remove(category) parent = parents.get(category) if parent in result: result[parent] += result[category] next_leafs = set(categories) for category in categories: parent = parents.get(category) if not parent: continue if parent in next_leafs and parent in categories: next_leafs.remove(parent) leafs = next_leafs return result def get_margin(self, name): digits = self.__class__.margin.digits if self.profit is not None and self.revenue: return (self.profit / self.revenue).quantize( Decimal(1) / 10**digits[1]) def get_currency(self, name): pool = Pool() Company = pool.get('company.company') company = Transaction().context.get('company') if company: return Company(company).currency.id @classmethod def view_attributes(cls): return super().view_attributes() + [ ('/tree/field[@name="profit"]', 'visual', If(Eval('profit', 0) < 0, 'danger', '')), ('/tree/field[@name="margin"]', 'visual', If(Eval('margin', 0) < 0, 'danger', '')), ]
class Abstract(ModelSQL, ModelView): company = fields.Many2One( 'company.company', lazy_gettext('stock.msg_stock_reporting_company')) cost = Monetary(lazy_gettext('stock.msg_stock_reporting_cost'), currency='currency', digits='currency') revenue = Monetary(lazy_gettext('stock.msg_stock_reporting_revenue'), currency='currency', digits='currency') profit = Monetary(lazy_gettext('stock.msg_stock_reporting_profit'), currency='currency', digits='currency') margin = fields.Numeric(lazy_gettext('stock.msg_stock_reporting_margin'), digits=(14, 4), states={ 'invisible': ~Eval('margin'), }) margin_trend = fields.Function( fields.Char(lazy_gettext('stock.msg_stock_reporting_margin_trend')), 'get_trend') time_series = None currency = fields.Many2One( 'currency.currency', lazy_gettext('stock.msg_stock_reporting_currency')) @classmethod def table_query(cls): from_item, tables, withs = cls._joins() return from_item.select(*cls._columns(tables, withs), where=cls._where(tables, withs), group_by=cls._group_by(tables, withs), with_=withs.values()) @classmethod def _joins(cls): pool = Pool() Company = pool.get('company.company') Currency = pool.get('currency.currency') Move = pool.get('stock.move') Location = pool.get('stock.location') tables = {} tables['move'] = move = Move.__table__() tables['move.company'] = company = Company.__table__() tables['move.company.currency'] = currency = Currency.__table__() tables['move.from_location'] = from_location = Location.__table__() tables['move.to_location'] = to_location = Location.__table__() withs = {} withs['currency_rate'] = currency_rate = With( query=Currency.currency_rate_sql()) withs['currency_rate_company'] = currency_rate_company = With( query=Currency.currency_rate_sql()) from_item = (move.join( currency_rate, condition=(move.currency == currency_rate.currency) & (currency_rate.start_date <= move.effective_date) & ((currency_rate.end_date == Null) | (currency_rate.end_date >= move.effective_date)) ).join(company, condition=move.company == company.id).join( currency, condition=company.currency == currency.id).join( currency_rate_company, condition=(company.currency == currency_rate_company.currency) & (currency_rate_company.start_date <= move.effective_date) & ((currency_rate_company.end_date == Null) | (currency_rate_company.end_date >= move.effective_date)) ).join(from_location, condition=(move.from_location == from_location.id)).join( to_location, condition=(move.to_location == to_location.id))) return from_item, tables, withs @classmethod def _columns(cls, tables, withs): move = tables['move'] from_location = tables['move.from_location'] to_location = tables['move.to_location'] currency = tables['move.company.currency'] sign = Case((from_location.type.in_(cls._to_location_types()) & to_location.type.in_(cls._from_location_types()), -1), else_=1) cost = cls._column_cost(tables, withs, sign) revenue = cls._column_revenue(tables, withs, sign) profit = revenue - cost margin = Case((revenue != 0, profit / revenue), else_=Null) return [ cls._column_id(tables, withs).as_('id'), Literal(0).as_('create_uid'), CurrentTimestamp().as_('create_date'), cls.write_uid.sql_cast(Literal(Null)).as_('write_uid'), cls.write_date.sql_cast(Literal(Null)).as_('write_date'), move.company.as_('company'), cls.cost.sql_cast(Round(cost, currency.digits)).as_('cost'), cls.revenue.sql_cast(Round(revenue, currency.digits)).as_('revenue'), cls.profit.sql_cast(Round(profit, currency.digits)).as_('profit'), cls.margin.sql_cast(Round(margin, cls.margin.digits[1])).as_('margin'), currency.id.as_('currency'), ] @classmethod def _column_id(cls, tables, withs): move = tables['move'] return Min(move.id) @classmethod def _column_cost(cls, tables, withs, sign): move = tables['move'] return Sum(sign * cls.cost.sql_cast(move.internal_quantity) * Coalesce(move.cost_price, 0)) @classmethod def _column_revenue(cls, tables, withs, sign): move = tables['move'] currency = withs['currency_rate'] currency_company = withs['currency_rate_company'] return Sum(sign * cls.revenue.sql_cast(move.quantity) * Coalesce(move.unit_price, 0) * Coalesce(currency_company.rate / currency.rate, 0)) @classmethod def _group_by(cls, tables, withs): move = tables['move'] currency = tables['move.company.currency'] return [move.company, currency.id, currency.digits] @classmethod def _where(cls, tables, withs): context = Transaction().context move = tables['move'] from_location = tables['move.from_location'] to_location = tables['move.to_location'] where = move.company == context.get('company') where &= ((from_location.type.in_(cls._from_location_types()) & to_location.type.in_(cls._to_location_types())) | (from_location.type.in_(cls._to_location_types()) & to_location.type.in_(cls._from_location_types()))) where &= move.state == 'done' from_date = context.get('from_date') if from_date: where &= move.effective_date >= from_date to_date = context.get('to_date') if to_date: where &= move.effective_date <= to_date return where @classmethod def _from_location_types(cls): return ['storage', 'drop'] @classmethod def _to_location_types(cls): types = ['customer'] if Transaction().context.get('include_lost'): types += ['lost_found'] return types @property def time_series_all(self): delta = self._period_delta() for ts, next_ts in pairwise(self.time_series or []): yield ts if delta and next_ts: date = ts.date + delta while date < next_ts.date: yield None date += delta @classmethod def _period_delta(cls): context = Transaction().context return { 'year': relativedelta(years=1), 'month': relativedelta(months=1), 'day': relativedelta(days=1), }.get(context.get('period')) def get_trend(self, name): name = name[:-len('_trend')] if pygal: chart = pygal.Line() chart.add('', [ getattr(ts, name) or 0 if ts else 0 for ts in self.time_series_all ]) return chart.render_sparktext() @classmethod def view_attributes(cls): return super().view_attributes() + [ ('/tree/field[@name="profit"]', 'visual', If(Eval('profit', 0) < 0, 'danger', '')), ('/tree/field[@name="margin"]', 'visual', If(Eval('margin', 0) < 0, 'danger', '')), ]
class Abstract(ModelSQL): company = fields.Many2One('company.company', lazy_gettext("sale.msg_sale_reporting_company")) number = fields.Integer( lazy_gettext("sale.msg_sale_reporting_number"), help=lazy_gettext("sale.msg_sale_reporting_number_help")) revenue = fields.Numeric(lazy_gettext("sale.msg_sale_reporting_revenue"), digits=(16, Eval('currency_digits', 2)), depends=['currency_digits']) revenue_trend = fields.Function( fields.Char(lazy_gettext("sale.msg_sale_reporting_revenue_trend")), 'get_trend') time_series = None currency_digits = fields.Function( fields.Integer( lazy_gettext("sale.msg_sale_reporting_currency_digits")), 'get_currency_digits') @classmethod def table_query(cls): from_item, tables = cls._joins() return from_item.select(*cls._columns(tables), where=cls._where(tables), group_by=cls._group_by(tables)) @classmethod def _joins(cls): pool = Pool() Company = pool.get('company.company') Currency = pool.get('currency.currency') Line = pool.get('sale.line') Sale = pool.get('sale.sale') tables = {} tables['line'] = line = Line.__table__() tables['line.sale'] = sale = Sale.__table__() tables['line.sale.company'] = company = Company.__table__() currency_sale = Currency.currency_rate_sql() tables['currency_sale'] = currency_sale currency_company = Currency.currency_rate_sql() tables['currency_company'] = currency_company from_item = (line.join(sale, condition=line.sale == sale.id).join( currency_sale, condition=(sale.currency == currency_sale.currency) & (currency_sale.start_date <= sale.sale_date) & ((currency_sale.end_date == Null) | (currency_sale.end_date >= sale.sale_date))).join( company, condition=sale.company == company.id).join( currency_company, condition=(company.currency == currency_company.currency) & (currency_company.start_date <= sale.sale_date) & ((currency_company.end_date == Null) | (currency_company.end_date >= sale.sale_date)))) return from_item, tables @classmethod def _columns(cls, tables): line = tables['line'] sale = tables['line.sale'] currency_company = tables['currency_company'] currency_sale = tables['currency_sale'] revenue = cls.revenue.sql_cast( Sum(line.quantity * line.unit_price * currency_company.rate / currency_sale.rate)) return [ cls._column_id(tables).as_('id'), Literal(0).as_('create_uid'), CurrentTimestamp().as_('create_date'), cls.write_uid.sql_cast(Literal(Null)).as_('write_uid'), cls.write_date.sql_cast(Literal(Null)).as_('write_date'), sale.company.as_('company'), revenue.as_('revenue'), Count(sale.id, distinct=True).as_('number'), ] @classmethod def _column_id(cls, tables): line = tables['line'] return Min(line.id) @classmethod def _group_by(cls, tables): sale = tables['line.sale'] return [sale.company] @classmethod def _where(cls, tables): context = Transaction().context sale = tables['line.sale'] where = sale.company == context.get('company') where &= sale.state.in_(cls._sale_states()) from_date = context.get('from_date') if from_date: where &= sale.sale_date >= from_date to_date = context.get('to_date') if to_date: where &= sale.sale_date <= to_date warehouse = context.get('warehouse') if warehouse: where &= sale.warehouse == warehouse return where @classmethod def _sale_states(cls): return ['confirmed', 'processing', 'done'] @property def time_series_all(self): delta = self._period_delta() for ts, next_ts in pairwise(self.time_series or []): yield ts if delta and next_ts: date = ts.date + delta while date < next_ts.date: yield None date += delta @classmethod def _period_delta(cls): context = Transaction().context return { 'year': relativedelta(years=1), 'month': relativedelta(months=1), 'day': relativedelta(days=1), }.get(context.get('period')) def get_trend(self, name): name = name[:-len('_trend')] if pygal: chart = pygal.Line() chart.add('', [ getattr(ts, name) if ts else 0 for ts in self.time_series_all ]) return chart.render_sparktext() def get_currency_digits(self, name): return self.company.currency.digits
class NoteCopyMixin( resource_copy('ir.note', 'notes', lazy_gettext('ir.msg_notes'))): pass
class Abstract(ModelSQL): company = fields.Many2One('company.company', lazy_gettext("sale.msg_sale_reporting_company")) number = fields.Integer( lazy_gettext("sale.msg_sale_reporting_number"), help=lazy_gettext("sale.msg_sale_reporting_number_help")) revenue = Monetary(lazy_gettext("sale.msg_sale_reporting_revenue"), digits='currency', currency='currency') revenue_trend = fields.Function( fields.Char(lazy_gettext("sale.msg_sale_reporting_revenue_trend")), 'get_trend') time_series = None currency = fields.Function( fields.Many2One('currency.currency', lazy_gettext("sale.msg_sale_reporting_currency")), 'get_currency') @classmethod def table_query(cls): from_item, tables, withs = cls._joins() return from_item.select(*cls._columns(tables, withs), where=cls._where(tables, withs), group_by=cls._group_by(tables, withs), with_=withs.values()) @classmethod def _sale_line(cls, length, index, company_id=None): pool = Pool() Line = pool.get('sale.line') Sale = pool.get('sale.sale') line = Line.__table__() sale = Sale.__table__() return (line.join(sale, condition=line.sale == sale.id).select( (line.id * length + index).as_('id'), line.product.as_('product'), Coalesce(line.actual_quantity, line.quantity).as_('quantity'), line.unit_price.as_('unit_price'), Concat('sale.sale,', line.sale).as_('order'), sale.sale_date.as_('date'), sale.company.as_('company'), sale.currency.as_('currency'), sale.party.as_('customer'), sale.warehouse.as_('location'), sale.shipment_address.as_('shipment_address'), where=sale.state.in_(cls._sale_states()) & (sale.company == company_id), )) @classmethod def _lines(cls): return [cls._sale_line] @classmethod def _joins(cls): pool = Pool() Company = pool.get('company.company') Currency = pool.get('currency.currency') context = Transaction().context tables = {} company = context.get('company') lines = cls._lines() tables['line'] = line = Union(*(l(len(lines), i, company) for i, l in enumerate(lines))) tables['line.company'] = company = Company.__table__() withs = {} currency_sale = With(query=Currency.currency_rate_sql()) withs['currency_sale'] = currency_sale currency_company = With(query=Currency.currency_rate_sql()) withs['currency_company'] = currency_company from_item = (line.join( currency_sale, condition=(line.currency == currency_sale.currency) & (currency_sale.start_date <= line.date) & ((currency_sale.end_date == Null) | (currency_sale.end_date >= line.date))).join( company, condition=line.company == company.id).join( currency_company, condition=(company.currency == currency_company.currency) & (currency_company.start_date <= line.date) & ((currency_company.end_date == Null) | (currency_company.end_date >= line.date)))) return from_item, tables, withs @classmethod def _columns(cls, tables, withs): line = tables['line'] currency_company = withs['currency_company'] currency_sale = withs['currency_sale'] revenue = cls.revenue.sql_cast( Sum(line.quantity * line.unit_price * currency_company.rate / currency_sale.rate)) return [ cls._column_id(tables, withs).as_('id'), Literal(0).as_('create_uid'), CurrentTimestamp().as_('create_date'), cls.write_uid.sql_cast(Literal(Null)).as_('write_uid'), cls.write_date.sql_cast(Literal(Null)).as_('write_date'), line.company.as_('company'), revenue.as_('revenue'), Count(line.order, distinct=True).as_('number'), ] @classmethod def _column_id(cls, tables, withs): line = tables['line'] return Min(line.id) @classmethod def _group_by(cls, tables, withs): line = tables['line'] return [line.company] @classmethod def _where(cls, tables, withs): pool = Pool() Location = pool.get('stock.location') context = Transaction().context line = tables['line'] where = Literal(True) from_date = context.get('from_date') if from_date: where &= line.date >= from_date to_date = context.get('to_date') if to_date: where &= line.date <= to_date warehouse = context.get('warehouse') if warehouse: locations = Location.search([ ('parent', 'child_of', warehouse), ], query=True) where &= line.location.in_(locations) return where @classmethod def _sale_states(cls): return ['confirmed', 'processing', 'done'] @property def time_series_all(self): delta = self._period_delta() for ts, next_ts in pairwise(self.time_series or []): yield ts if delta and next_ts: date = ts.date + delta while date < next_ts.date: yield None date += delta @classmethod def _period_delta(cls): context = Transaction().context return { 'year': relativedelta(years=1), 'month': relativedelta(months=1), 'day': relativedelta(days=1), }.get(context.get('period')) def get_trend(self, name): name = name[:-len('_trend')] if pygal: chart = pygal.Line() chart.add('', [ getattr(ts, name) if ts else 0 for ts in self.time_series_all ]) return chart.render_sparktext() def get_currency(self, name): return self.company.currency.id