Exemple #1
0
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')
Exemple #2
0
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
Exemple #3
0
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)
Exemple #4
0
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
Exemple #5
0
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
Exemple #6
0
class AttachmentCopyMixin(
        resource_copy(
            'ir.attachment', 'attachments',
            lazy_gettext('ir.msg_attachments'))):
    pass
Exemple #7
0
    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)
Exemple #8
0
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
Exemple #9
0
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()
Exemple #10
0
    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', '')),
        ]
Exemple #13
0
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
Exemple #14
0
class NoteCopyMixin(
        resource_copy('ir.note', 'notes', lazy_gettext('ir.msg_notes'))):
    pass
Exemple #15
0
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