示例#1
0
class User:
    __metaclass__ = PoolMeta
    __name__ = "res.user"
    subdivisions = fields.Many2Many('company.subdivision-res.user',
            'user', 'subdivision', 'Subdivisions')
    subdivision = fields.Many2One('company.subdivision', 'Subdivision', domain=[
            ('id', 'in', Eval('subdivisions', [])),
            ], depends=['subdivisions'])

    @classmethod
    def __setup__(cls):
        super(User, cls).__setup__()
        cls._preferences_fields.extend([
                'subdivision',
                'subdivisions',
                ])
        cls._context_fields.insert(0, 'subdivision')
        cls._context_fields.insert(0, 'subdivisions')

    def get_status_bar(self, name):
        status = super(User, self).get_status_bar(name)
        if self.subdivision:
            status += ' - %s' % (self.subdivision.rec_name)
        return status
示例#2
0
class Psicologo(Persona):
    'Psicologo'
    _name = 'cefiro.psicologo'
    _description = __doc__

    login = fields.Char('Nombre de usuario interno', required=True)
    password = fields.Function(fields.Char(u'Contraseña', required=True),
                               'mensaje', 'crear')
    telefono = fields.Char(u'Teléfono')
    mail = fields.Char(u'Correo electrónico')
    pacientes = fields.One2Many('cefiro.paciente',
                                'psicologo',
                                'Pacientes',
                                readonly=True)
    consultas = fields.Many2Many('cefiro.encuentropsi', 'persona', 'evento',
                                 'Consultas')

    #Esto es para crear el usuario interno
    #	confirmacionInterno = fields.Function(fields.Char(u'Confirmación (dejar en blanco)'),'mensaje','crear')

    def mensaje(self, ids, name):
        res = {}
        for elem in self.browse(ids):
            #			res[elem.id] = "Usuario interno creado"
            res[elem.id] = "xxxxxxxxxxxxxxxxxxxx"
        return res

    def crear(self, ids, name, value):
        user_obj = Pool().get('res.user')
        for elem in self.browse(ids):
            user_obj.create({
                'name': elem.name,
                'login': elem.login,
                'password': value
            })
        return
示例#3
0
class CopyMany2Many(ModelSQL):
    "Copy Many2Many"
    __name__ = 'test.copy.many2many'
    name = fields.Char('Name')
    many2many = fields.Many2Many('test.copy.many2many.rel', 'many2many',
                                 'many2many_target', 'Many2Many')
示例#4
0
class PaymentGateway(ModelSQL, ModelView):
    """
    Payment Gateway

    Payment gateway record is a specific configuration for a `provider`
    """
    __name__ = 'payment_gateway.gateway'

    active = fields.Boolean('Active', select=True)
    name = fields.Char('Name',
                       required=True,
                       select=True,
                       states=STATES,
                       depends=DEPENDS)
    journal = fields.Many2One('account.journal',
                              'Journal',
                              required=True,
                              states=STATES,
                              depends=DEPENDS)
    provider = fields.Selection('get_providers',
                                'Provider',
                                required=True,
                                states=STATES,
                                depends=DEPENDS)
    method = fields.Selection('get_methods',
                              'Method',
                              required=True,
                              states=STATES,
                              depends=DEPENDS)
    test = fields.Boolean('Test Account', states=STATES, depends=DEPENDS)

    users = fields.Many2Many('payment_gateway.gateway-res.user',
                             'payment_gateway', 'user', 'Users')
    configured = fields.Boolean('Configured ?', readonly=True)

    @classmethod
    def __setup__(cls):
        super(PaymentGateway, cls).__setup__()
        cls._buttons.update({
            'test_gateway_configuration': {
                'readonly': ~Bool(Eval('active')),
            },
        })

    @classmethod
    @ModelView.button
    def test_gateway_configuration(cls, gateways):
        for gateway in gateways:
            journal = gateway.journal
            configured = bool(journal.debit_account
                              and not journal.debit_account.party_required)
            gateway.configured = configured
            gateway.save()

    @staticmethod
    def default_active():
        return True

    @staticmethod
    def default_provider():
        return 'self'

    @classmethod
    def get_providers(cls):
        """
        Downstream modules can add to the list
        """
        return []

    @fields.depends('provider')
    def get_methods(self):
        """
        Downstream modules can override the method and add entries to this
        """
        return []
示例#5
0
class Product:
    "Product extension for Nereid"
    __metaclass__ = PoolMeta
    __name__ = "product.product"

    #: Decides the number of products that would be remebered.
    recent_list_size = 5

    #: The list of fields allowed to be sent back on a JSON response from the
    #: application. This is validated before any product info is built
    #:
    #: The `name`, `sale_price`, `id` and `uri` are sent by default
    #:
    #: .. versionadded:: 0.3
    json_allowed_fields = set(['rec_name', 'sale_price', 'id', 'uri'])

    uri = fields.Char(
        'URI', select=True, states=DEFAULT_STATE2
    )

    displayed_on_eshop = fields.Boolean('Displayed on E-Shop?', select=True)
    long_description = fields.Text('Long Description')
    media = fields.One2Many("product.media", "product", "Media")
    images = fields.Function(
        fields.One2Many('nereid.static.file', None, 'Images'),
        getter='get_product_images'
    )
    up_sells = fields.Many2Many(
        'product.product-product.product',
        'product', 'up_sell', 'Up-Sells', states=DEFAULT_STATE
    )
    cross_sells = fields.Many2Many(
        'product.product-product.product',
        'product', 'cross_sell', 'Cross-Sells', states=DEFAULT_STATE
    )
    default_image = fields.Function(
        fields.Many2One('nereid.static.file', 'Image'), 'get_default_image',
    )
    use_template_description = fields.Boolean("Use template's description")

    @classmethod
    def view_attributes(cls):
        return super(Product, cls).view_attributes() + [
            ('//page[@id="desc"]', 'states', {
                'invisible': Bool(Eval('use_template_description'))
            }), ('//page[@id="ecomm_det"]', 'states', {
                'invisible': Not(Bool(Eval('displayed_on_eshop')))
            }), ('//page[@id="related_products"]', 'states', {
                'invisible': Not(Bool(Eval('displayed_on_eshop')))
            })]

    @classmethod
    def copy(cls, products, default=None):
        """Duplicate products
        """
        if default is None:
            default = {}
        default = default.copy()
        default['displayed_on_eshop'] = False

        duplicate_products = []
        for index, product in enumerate(products, start=1):
            if product.uri:
                default['uri'] = "%s-copy-%d" % (product.uri, index)

            duplicate_products.extend(
                super(Product, cls).copy([product], default)
            )

        return duplicate_products

    @classmethod
    def validate(cls, products):
        super(Product, cls).validate(products)
        cls.check_uri_uniqueness(products)

    @classmethod
    def get_default_image(cls, products, name):
        """
        Returns default product image if any.
        """
        res = {}
        for product in products:
            images = product.images or product.template.images
            res[product.id] = images[0].id if images else None
        return res

    @classmethod
    def __setup__(cls):
        super(Product, cls).__setup__()
        cls.description.states['invisible'] = Bool(
            Eval('use_template_description')
        )
        cls._error_messages.update({
            'unique_uri': ('URI of Product must be Unique'),
        })
        cls.per_page = 12

    @staticmethod
    def default_displayed_on_eshop():
        return False

    @fields.depends('template', 'uri')
    def on_change_with_uri(self):
        """
        If the URI is empty, slugify template name into URI
        """
        if not self.uri and self.template:
            return slugify(self.template.name)
        return self.uri

    @staticmethod
    def default_use_template_description():
        return True

    @classmethod
    def check_uri_uniqueness(cls, products):
        """
        Ensure uniqueness of products uri.
        """
        query = ['OR']
        for product in products:
            # Do not check for unique uri if product is marked as
            # not displayed on eshop
            if not product.displayed_on_eshop:
                continue

            arg = [
                'AND', [
                    ('id', '!=', product.id)
                ], [
                    ('uri', 'ilike', product.uri)
                ]
            ]
            query.append(arg)
        if query != ['OR'] and cls.search(query):
            cls.raise_user_error('unique_uri')

    @classmethod
    @route('/product/<uri>')
    @route('/product/<path:path>/<uri>')
    def render(cls, uri, path=None):
        """Renders the template for a single product.

        :param uri: URI of the product
        :param path: Ignored parameter. This is used in
                     cases where SEO friendly URL like
                     product/category/sub-cat/sub-sub-cat/product-uri
                     are generated
        """
        products = cls.search([
            ('displayed_on_eshop', '=', True),
            ('uri', '=', uri),
            ('template.active', '=', True),
        ], limit=1)
        if not products:
            return NotFound('Product Not Found')

        cls._add_to_recent_list(int(products[0]))
        return render_template('product.jinja', product=products[0])

    @classmethod
    @route('/products/+recent', methods=['GET', 'POST'])
    def recent_products(cls):
        """
        GET
        ---

        Return a list of recently visited products in JSON

        POST
        ----

        Add the product to the recent list manually. This method is required
        if the product page is cached, or is served by a Caching Middleware
        like Varnish which may clear the session before sending the request to
        Nereid.

        Just as with GET the response is the AJAX of recent products
        """
        if request.method == 'POST':
            cls._add_to_recent_list(request.form.get('product_id', type=int))

        fields = set(request.args.getlist('fields')) or cls.json_allowed_fields
        fields = fields & cls.json_allowed_fields

        if 'sale_price' in fields:
            fields.remove('sale_price')

        response = []
        if hasattr(session, 'sid'):
            products = cls.browse(session.get('recent-products', []))
            for product in products:
                product_val = {}
                for field in fields:
                    product_val[field] = getattr(product, field)
                product_val['sale_price'] = format_currency(
                    product.sale_price(),
                    current_locale.currency.code
                )
                response.append(product_val)

        return jsonify(products=response)

    @classmethod
    def _add_to_recent_list(cls, product_id):
        """Adds the given product ID to the list of recently viewed products
        By default the list size is 5. To change this you can inherit
        product.product and set :attr:`recent_list_size` attribute to a
        non negative integer value

        For faster and easier access the products are stored with the ids alone
        this behaviour can be modified by subclassing.

        The deque object cannot be saved directly in the cache as its not
        serialisable. Hence a conversion to list is made on the fly

        .. versionchanged:: 0.3
            If there is no session for the user this function returns an empty
            list. This ensures that the code is consistent with iterators that
            may use the returned value

        :param product_id: the product id to prepend to the list
        """
        if not hasattr(session, 'sid'):
            current_app.logger.warning(
                "No session. Not saving to browsing history"
            )
            return []

        recent_products = deque(
            session.setdefault('recent-products', []), cls.recent_list_size
        )
        # XXX: If a product is already in the recently viewed list, but it
        # would be nice to remember the recent_products list in the order of
        # visits.
        if product_id not in recent_products:
            recent_products.appendleft(product_id)
            session['recent-products'] = list(recent_products)
        return recent_products

    @classmethod
    @route('/products')
    @route('/products/<int:page>')
    def render_list(cls, page=1):
        """
        Renders the list of all products which are displayed_on_shop=True

        .. tip::

            The implementation uses offset for pagination and could be
            extremely resource intensive on databases. Hence you might want to
            either have an alternate cache/search server based pagination or
            limit the pagination to a maximum page number.

            The base implementation does NOT limit this and could hence result
            in poor performance

        :param page: The page in pagination to be displayed
        """

        products = Pagination(cls, [
            ('displayed_on_eshop', '=', True),
            ('template.active', '=', True),
        ], page, cls.per_page)
        return render_template('product-list.jinja', products=products)

    def sale_price(self, quantity=0):
        """Return the Sales Price.
        A wrapper designed to work as a context variable in templating

        The price is calculated from the pricelist associated with the current
        user. The user in the case of guest user is logged in user. In the
        event that the logged in user does not have a pricelist set against
        the user, the guest user's pricelist is chosen.

        Finally if neither the guest user, nor the regsitered user has a
        pricelist set against them then the list price is displayed as the
        price of the product

        :param quantity: Quantity
        """
        return self.list_price

    @classmethod
    @route('/sitemaps/product-index.xml')
    def sitemap_index(cls):
        """
        Returns a Sitemap Index Page
        """
        index = SitemapIndex(cls, [
            ('displayed_on_eshop', '=', True),
            ('template.active', '=', True),
        ])
        return index.render()

    @classmethod
    @route('/sitemaps/product-<int:page>.xml')
    def sitemap(cls, page):
        sitemap_section = SitemapSection(
            cls, [
                ('displayed_on_eshop', '=', True),
                ('template.active', '=', True),
            ], page
        )
        sitemap_section.changefreq = 'daily'
        return sitemap_section.render()

    def get_absolute_url(self, **kwargs):
        """
        Return the URL of the current product.

        This method works only under a nereid request context
        """
        return url_for('product.product.render', uri=self.uri, **kwargs)

    def _json(self):
        """
        Return a JSON serializable dictionary of the product
        """
        response = {
            'template': {
                'name': self.template.rec_name,
                'id': self.template.id,
                'list_price': self.list_price,
            },
            'code': self.code,
            'description': self.description,
        }
        return response

    def get_long_description(self):
        """
        Get long description of product.

        If the product is set to use the template's long description, then
        the template long description is sent back.

        The returned value is a `~jinja2.Markup` object which makes it
        HTML safe and can be used directly in templates. It is recommended
        to use this method instead of trying to wrap this logic in the
        templates.
        """
        if self.use_template_description:
            description = self.template.long_description
        else:
            description = self.long_description

        return Markup(description or '')

    def get_description(self):
        """
        Get description of product.

        If the product is set to use the template's description, then
        the template description is sent back.

        The returned value is a `~jinja2.Markup` object which makes it
        HTML safe and can be used directly in templates. It is recommended
        to use this method instead of trying to wrap this logic in the
        templates.
        """
        if self.use_template_description:
            description = self.template.description
        else:
            description = self.description
        return Markup(description or '')

    @classmethod
    def get_product_images(cls, products, name=None):
        """
        Getter for `images` function field
        """
        res = {}
        for product in products:
            product_images = []
            for media in product.media:
                if not media.static_file.mimetype:
                    continue
                if 'image' in media.static_file.mimetype:
                    product_images.append(media.static_file.id)
            res[product.id] = product_images
        return res

    def get_images(self):
        """
        Get images of product variant.
        Fallback to template's images if there are no images
        for product.
        """
        if self.images:
            return self.images
        return self.template.images
示例#6
0
class Email(ModelSQL, ModelView):
    "Email Notification"
    __name__ = 'notification.email'

    from_ = fields.Char(
        "From",
        translate=True,
        help="Leave empty for the value defined in the configuration file.")
    subject = fields.Char("Subject",
                          translate=True,
                          help="The Genshi syntax can be used "
                          "with 'record' in the evaluation context.\n"
                          "If empty the report name will be used.")
    recipients = fields.Many2One(
        'ir.model.field',
        "Recipients",
        domain=[
            ('model.model', '=', Eval('model')),
        ],
        depends=['model'],
        help="The field that contains the recipient(s).")
    fallback_recipients = fields.Many2One(
        'res.user',
        "Recipients Fallback User",
        domain=[
            ('email', '!=', None),
        ],
        states={
            'invisible': ~Eval('recipients'),
        },
        depends=['recipients'],
        help="User notified when no recipients e-mail is found")
    recipients_secondary = fields.Many2One(
        'ir.model.field',
        "Secondary Recipients",
        domain=[
            ('model.model', '=', Eval('model')),
        ],
        depends=['model'],
        help="The field that contains the secondary recipient(s).")
    fallback_recipients_secondary = fields.Many2One(
        'res.user',
        "Secondary Recipients Fallback User",
        domain=[
            ('email', '!=', None),
        ],
        states={
            'invisible': ~Eval('recipients_secondary'),
        },
        depends=['recipients'],
        help="User notified when no secondary recipients e-mail is found")
    recipients_hidden = fields.Many2One(
        'ir.model.field',
        "Hidden Recipients",
        domain=[
            ('model.model', '=', Eval('model')),
        ],
        depends=['model'],
        help="The field that contains the hidden recipient(s).")
    fallback_recipients_hidden = fields.Many2One(
        'res.user',
        "Hidden Recipients Fallback User",
        domain=[
            ('email', '!=', None),
        ],
        states={
            'invisible': ~Eval('recipients_hidden'),
        },
        depends=['recipients_hidden'],
        help="User notified when no hidden recipients e-mail is found")

    contact_mechanism = fields.Selection(
        'get_contact_mechanisms',
        "Contact Mechanism",
        help="Define which email to use from the party's contact mechanisms")

    content = fields.Many2One('ir.action.report',
                              "Content",
                              required=True,
                              domain=[('template_extension', 'in',
                                       ['txt', 'html', 'xhtml'])],
                              help="The report used as email template.")
    attachments = fields.Many2Many('notification.email.attachment',
                                   'notification',
                                   'report',
                                   "Attachments",
                                   domain=[
                                       ('model', '=', Eval('model')),
                                   ],
                                   depends=['model'],
                                   help="The reports used as attachments.")

    triggers = fields.One2Many('ir.trigger',
                               'notification_email',
                               "Triggers",
                               domain=[('model.model', '=', Eval('model'))],
                               depends=['model'],
                               help="Add a trigger for the notification.")
    send_after = fields.TimeDelta(
        "Send After",
        help="The delay after which the email must be sent.\n"
        "Applied if a worker queue is activated.")

    model = fields.Function(fields.Char("Model"),
                            'on_change_with_model',
                            searcher='search_model')

    @classmethod
    def __setup__(cls):
        pool = Pool()
        EmailTemplate = pool.get('ir.email.template')
        super().__setup__()
        for field in [
                'recipients',
                'recipients_secondary',
                'recipients_hidden',
        ]:
            field = getattr(cls, field)
            field.domain.append([
                'OR',
                ('relation', 'in', EmailTemplate.email_models()),
                [
                    ('model.model', 'in', EmailTemplate.email_models()),
                    ('name', '=', 'id'),
                ],
            ])

    def get_rec_name(self, name):
        return self.content.rec_name

    @classmethod
    def search_rec_name(cls, name, clause):
        return [('content', ) + tuple(clause[1:])]

    @classmethod
    def get_contact_mechanisms(cls):
        pool = Pool()
        try:
            ContactMechanism = pool.get('party.contact_mechanism')
        except KeyError:
            return [(None, "")]
        return ContactMechanism.usages()

    @fields.depends('content')
    def on_change_with_model(self, name=None):
        if self.content:
            return self.content.model

    @classmethod
    def search_model(cls, name, clause):
        return [('content.model', ) + tuple(clause[1:])]

    def _get_addresses(self, value):
        pool = Pool()
        EmailTemplate = pool.get('ir.email.template')
        with Transaction().set_context(usage=self.contact_mechanism):
            return EmailTemplate.get_addresses(value)

    def _get_languages(self, value):
        pool = Pool()
        EmailTemplate = pool.get('ir.email.template')
        with Transaction().set_context(usage=self.contact_mechanism):
            return EmailTemplate.get_languages(value)

    def get_email(self, record, sender, to, cc, bcc, languages):
        pool = Pool()
        Attachment = pool.get('notification.email.attachment')

        # TODO order languages to get default as last one for title
        content, title = get_email(self.content, record, languages)
        language = list(languages)[-1]
        from_ = sender
        with Transaction().set_context(language=language.code):
            notification = self.__class__(self.id)
            if notification.from_:
                from_ = notification.from_
            if self.subject:
                title = (TextTemplate(
                    notification.subject).generate(record=record).render())

        if self.attachments:
            msg = MIMEMultipart('mixed')
            msg.attach(content)
            for report in self.attachments:
                msg.attach(Attachment.get_mime(report, record, language.code))
        else:
            msg = content

        set_from_header(msg, sender, from_)
        msg['To'] = ', '.join(to)
        msg['Cc'] = ', '.join(cc)
        msg['Subject'] = Header(title, 'utf-8')
        msg['Auto-Submitted'] = 'auto-generated'
        return msg

    def get_log(self, record, trigger, msg, bcc=None):
        return {
            'recipients': msg['To'],
            'recipients_secondary': msg['Cc'],
            'recipients_hidden': bcc,
            'resource': str(record),
            'notification': trigger.notification_email.id,
            'trigger': trigger.id,
        }

    @classmethod
    def trigger(cls, records, trigger):
        "Action function for the triggers"
        notification_email = trigger.notification_email
        if not notification_email:
            raise ValueError(
                'Trigger "%s" is not related to any email notification' %
                trigger.rec_name)
        if notification_email.send_after:
            with Transaction().set_context(
                    queue_name='notification_email',
                    queue_scheduled_at=trigger.notification_email.send_after):
                notification_email.__class__.__queue__._send_email_queued(
                    notification_email, [r.id for r in records], trigger.id)
        else:
            notification_email.send_email(records, trigger)

    def _send_email_queued(self, ids, trigger_id):
        pool = Pool()
        Model = pool.get(self.model)
        Trigger = pool.get('ir.trigger')
        records = Model.browse(ids)
        trigger = Trigger(trigger_id)
        self.send_email(records, trigger)

    def send_email(self, records, trigger):
        pool = Pool()
        Log = pool.get('notification.email.log')
        datamanager = SMTPDataManager()
        Transaction().join(datamanager)
        from_ = (config.get('notification_email', 'from')
                 or config.get('email', 'from'))
        logs = []
        for record in records:
            to, to_languages = self._get_to(record)
            cc, cc_languages = self._get_cc(record)
            bcc, bcc_languages = self._get_bcc(record)
            languagues = to_languages | cc_languages | bcc_languages
            to_addrs = [e for _, e in getaddresses(to + cc + bcc)]
            if to_addrs:
                msg = self.get_email(record, from_, to, cc, bcc, languagues)
                sendmail_transactional(from_,
                                       to_addrs,
                                       msg,
                                       datamanager=datamanager)
                logs.append(
                    self.get_log(record, trigger, msg, bcc=', '.join(bcc)))
        if logs:
            Log.create(logs)

    def _get_to(self, record):
        to = []
        languagues = set()
        if self.recipients:
            recipients = getattr(record, self.recipients.name, None)
            if recipients:
                languagues.update(self._get_languages(recipients))
                to = self._get_addresses(recipients)
        if not to and self.fallback_recipients:
            languagues.update(self._get_languages(self.fallback_recipients))
            to = self._get_addresses(self.fallback_recipients)
        return to, languagues

    def _get_cc(self, record):
        cc = []
        languagues = set()
        if self.recipients_secondary:
            recipients_secondary = getattr(record,
                                           self.recipients_secondary.name,
                                           None)
            if recipients_secondary:
                languagues.update(self._get_languages(recipients_secondary))
                cc = self._get_addresses(recipients_secondary)
        if not cc and self.fallback_recipients_secondary:
            languagues.update(
                self._get_languages(self.fallback_recipients_secondary))
            cc = self._get_addresses(self.fallback_recipients_secondary)
        return cc, languagues

    def _get_bcc(self, record):
        bcc = []
        languagues = set()
        if self.recipients_hidden:
            recipients_hidden = getattr(record, self.recipients_hidden.name,
                                        None)
            if recipients_hidden:
                languagues.update(self._get_languages(recipients_hidden))
                bcc = self._get_addresses(recipients_hidden)
        if not bcc and self.fallback_recipients_hidden:
            languagues.update(
                self._get_languages(self.fallback_recipients_hidden))
            bcc = self._get_addresses(self.fallback_recipients_hidden)
        return bcc, languagues

    @classmethod
    def validate(cls, notifications):
        super().validate(notifications)
        for notification in notifications:
            notification.check_subject()

    def check_subject(self):
        if not self.subject:
            return
        try:
            TextTemplate(self.subject)
        except Exception as exception:
            raise TemplateError(
                gettext(
                    'notification_email.'
                    'msg_notification_invalid_subject',
                    notification=self.rec_name,
                    exception=exception)) from exception
示例#7
0
class Many2ManySize(ModelSQL):
    'Many2Many Size Relation'
    __name__ = 'test.many2many_size'
    targets = fields.Many2Many('test.many2many_size.relation', 'origin',
        'target', 'Targets', size=3)
示例#8
0
class Many2ManyReference(ModelSQL):
    'Many2Many Reference'
    __name__ = 'test.many2many_reference'
    targets = fields.Many2Many('test.many2many_reference.relation', 'origin',
        'target', 'Targets')
示例#9
0
class ImportDataMany2Many(ModelSQL):
    "Import Data Many2Many"
    __name__ = 'test.import_data.many2many'
    many2many = fields.Many2Many('test.import_data.many2many.relation',
                                 'many2many', 'target', 'Many2Many')
示例#10
0
class Article(Workflow, ModelSQL, ModelView, CMSMenuItemMixin):
    "CMS Articles"
    __name__ = 'nereid.cms.article'
    _rec_name = 'uri'

    uri = fields.Char('URI', required=True, select=True, translate=True)
    title = fields.Char('Title', required=True, select=True, translate=True)
    content = fields.Text('Content', required=True, translate=True)
    template = fields.Char('Template', required=True)
    active = fields.Boolean('Active', select=True)
    image = fields.Many2One('nereid.static.file', 'Image')
    employee = fields.Many2One('company.employee', 'Employee')
    author = fields.Many2One('nereid.user', 'Author')
    published_on = fields.Date('Published On')
    publish_date = fields.Function(
        fields.Char('Publish Date'), 'get_publish_date'
    )
    sequence = fields.Integer('Sequence', required=True, select=True)
    reference = fields.Reference('Reference', selection='allowed_models')
    description = fields.Text('Short Description')
    attributes = fields.One2Many(
        'nereid.cms.article.attribute', 'article', 'Attributes'
    )
    categories = fields.Many2Many(
        'nereid.cms.category-article', 'article', 'category', 'Categories',
    )
    content_type = fields.Selection(
        'content_type_selection', 'Content Type',
        required=True
    )
    # Article can have a banner
    banner = fields.Many2One('nereid.cms.banner', 'Banner')
    state = fields.Selection([
        ('draft', 'Draft'),
        ('published', 'Published'),
        ('archived', 'Archived')
    ], 'State', required=True, select=True, readonly=True)

    @classmethod
    def __register__(cls, module_name):
        TableHandler = backend.get('TableHandler')
        cursor = Transaction().cursor

        table = TableHandler(cursor, cls, module_name)

        if not table.column_exist('employee'):
            table.column_rename('author', 'employee')

        super(Article, cls).__register__(module_name)

    @classmethod
    def __setup__(cls):
        super(Article, cls).__setup__()
        cls._order.insert(0, ('sequence', 'ASC'))
        cls._transitions |= set((
                ('draft', 'published'),
                ('published', 'draft'),
                ('published', 'archived'),
                ('archived', 'draft'),
        ))
        cls._buttons.update({
            'archive': {
                'invisible': Eval('state') != 'published',
            },
            'publish': {
                'invisible': Eval('state').in_(['published', 'archived']),
            },
            'draft': {
                'invisible': Eval('state') == 'draft',
            }
        })

    @classmethod
    def content_type_selection(cls):
        """
        Returns a selection for content_type.
        """
        default_types = [
            ('html', 'HTML'),
            ('plain', 'Plain Text')
        ]

        if markdown:
            default_types.append(('markdown', 'Markdown'))
        if publish_parts:
            default_types.append(('rst', 'reStructured TeXT'))

        return default_types

    @classmethod
    def default_content_type(cls):
        """
        Default content_type.
        """
        return 'plain'

    def __html__(self):
        """
        Uses content_type field to generate html content.
        Concept from Jinja2's Markup class.
        """
        if self.content_type == 'rst':
            if publish_parts:
                res = publish_parts(self.content, writer_name='html')
                return res['html_body']
            self.raise_user_error(
                "`docutils` not installed, to render rst articles."
            )
        if self.content_type == 'markdown':
            if markdown:
                return markdown(self.content)
            self.raise_user_error(
                "`markdown` not installed, to render markdown article."
            )
        return self.content

    @classmethod
    @ModelView.button
    @Workflow.transition('archived')
    def archive(cls, articles):
        pass

    @classmethod
    @ModelView.button
    @Workflow.transition('published')
    def publish(cls, articles):
        pass

    @classmethod
    @ModelView.button
    @Workflow.transition('draft')
    def draft(cls, articles):
        pass

    @classmethod
    def allowed_models(cls):
        MenuItem = Pool().get('nereid.cms.menuitem')

        return MenuItem.allowed_models()

    @staticmethod
    def default_active():
        return True

    @fields.depends('title', 'uri')
    def on_change_title(self):
        res = {}
        if self.title and not self.uri:
            res['uri'] = slugify(self.title)
        return res

    @staticmethod
    def default_template():
        return 'article.jinja'

    @staticmethod
    def default_employee():
        User = Pool().get('res.user')

        if 'employee' in Transaction().context:
            return Transaction().context['employee']

        user = User(Transaction().user)
        if user.employee:
            return user.employee.id

        if has_request_context() and current_user.employee:
            return current_user.employee.id

    @staticmethod
    def default_author():
        if has_request_context():
            return current_user.id

    @staticmethod
    def default_published_on():
        Date = Pool().get('ir.date')
        return Date.today()

    @classmethod
    @route('/article/<uri>')
    def render(cls, uri):
        """
        Renders the template
        """
        try:
            article, = cls.search([
                ('uri', '=', uri),
                ('state', '=', 'published'),
            ])
        except ValueError:
            abort(404)
        return render_template(article.template, article=article)

    @classmethod
    @route('/sitemaps/article-index.xml')
    def sitemap_index(cls):
        index = SitemapIndex(cls, [])
        return index.render()

    @classmethod
    @route('/sitemaps/article-<int:page>.xml')
    def sitemap(cls, page):
        sitemap_section = SitemapSection(cls, [], page)
        sitemap_section.changefreq = 'daily'
        return sitemap_section.render()

    @classmethod
    def get_publish_date(cls, records, name):
        """
        Return publish date to render on view
        """
        res = {}
        for record in records:
            res[record.id] = str(record.published_on)
        return res

    def get_absolute_url(self, **kwargs):
        return url_for(
            'nereid.cms.article.render', uri=self.uri, **kwargs
        )

    @staticmethod
    def default_state():
        if 'published' in Transaction().context:
            return 'published'
        return 'draft'

    def get_menu_item(self, max_depth):
        """
        Return huge dictionary with serialized article category for menu item

        {
            title: <display name>,
            link: <url>,
            record: <instance of record>  # if type_ is `record`
        }
        """
        return {
            'record': self,
            'title': self.title,
            'link': self.get_absolute_url(),
        }

    def atom_id(self):
        """
        Returns an atom ID for the article
        """
        return (
            'tag:' + current_website.name + ',' +
            self.publish_date + ':Article/' + str(self.id)
        )

    def atom_publish_date(self):
        """
        Returns the article's publish date with timezone set as UTC
        """
        return pytz.utc.localize(
            datetime.combine(self.published_on, datetime.min.time())
        )

    def serialize(self, purpose=None):
        """
        Serialize Article records
        """
        if purpose == 'atom':
            # The keys in the dictionary returned are used by Werkzeug's
            # AtomFeed class.
            return {
                'id': self.atom_id(),
                'title': self.title,
                'author': (
                    self.author.serialize(purpose=purpose) if self.author
                    else None
                ),
                'content': self.content,
                'content_type': (
                    'text' if self.content_type == 'plain' else 'html'
                ),
                'link': {
                    'rel': 'alternate',
                    'type': 'text/html',
                    'href': self.get_absolute_url(external=True),
                },
                'category': [
                    category.serialize(purpose=purpose)
                    for category in self.categories
                ],
                'published': self.atom_publish_date(),
                'updated': self.write_date or self.atom_publish_date(),
            }
        elif hasattr(super(Article, self), 'serialize'):
            return super(Article, self).serialize(purpose=purpose)

    @classmethod
    @route('/article/all.atom')
    def atom_feed(cls):
        """
        Renders the atom feed for all articles.
        """
        feed = AtomFeed(
            "All Articles", feed_url=request.url, url=request.host_url
        )
        for article in cls.search([
            ('state', '=', 'published')
        ]):
            feed.add(**article.serialize(purpose='atom'))

        return feed.get_response()
示例#11
0
class ArticleCategory(ModelSQL, ModelView, CMSMenuItemMixin):
    "Article Categories"
    __name__ = 'nereid.cms.article.category'
    _rec_name = 'title'

    title = fields.Char(
        'Title', size=100, translate=True, required=True, select=True
    )
    unique_name = fields.Char(
        'Unique Name', required=True, select=True,
        help='Unique Name is used as the uri.'
    )
    active = fields.Boolean('Active', select=True)
    description = fields.Text('Description', translate=True)
    template = fields.Char('Template', required=True)
    articles = fields.Many2Many(
        'nereid.cms.category-article', 'category', 'article', 'Article',
        context={'published': True}
    )

    # Article Category can have a banner
    banner = fields.Many2One('nereid.cms.banner', 'Banner')
    sort_order = fields.Selection([
        ('sequence', 'Sequence'),
        ('older_first', 'Older First'),
        ('recent_first', 'Recent First'),
    ], 'Sort Order')
    published_articles = fields.Function(
        fields.One2Many(
            'nereid.cms.article', 'category', 'Published Articles'
        ), 'get_published_articles'
    )
    articles_per_page = fields.Integer('Articles per Page', required=True)

    @staticmethod
    def default_sort_order():
        return 'recent_first'

    @staticmethod
    def default_active():
        'Return True'
        return True

    @staticmethod
    def default_template():
        return 'article-category.jinja'

    @classmethod
    def __setup__(cls):
        super(ArticleCategory, cls).__setup__()
        cls._sql_constraints += [
            ('unique_name', 'UNIQUE(unique_name)',
                'The Unique Name of the Category must be unique.'),
        ]

    @fields.depends('title', 'unique_name')
    def on_change_title(self):
        if self.title and not self.unique_name:
            self.unique_name = slugify(self.title)

    @staticmethod
    def default_articles_per_page():
        return 10

    @classmethod
    @route('/article-category/<uri>/')
    @route('/article-category/<uri>/<int:page>')
    def render(cls, uri, page=1):
        """
        Renders the category
        """
        Article = Pool().get('nereid.cms.article')

        # Find in cache or load from DB
        try:
            category, = cls.search([('unique_name', '=', uri)])
        except ValueError:
            abort(404)

        order = []
        if category.sort_order == 'recent_first':
            order.append(('write_date', 'DESC'))
        elif category.sort_order == 'older_first':
            order.append(('write_date', 'ASC'))
        elif category.sort_order == 'sequence':
            order.append(('sequence', 'ASC'))

        articles = Pagination(
            Article, [
                ('categories', '=', category.id),
                ('state', '=', 'published')
            ], page, category.articles_per_page, order=order
        )
        return render_template(
            category.template, category=category, articles=articles)

    @classmethod
    @context_processor('get_article_category')
    def get_article_category(cls, uri, silent=True):
        """Returns the browse record of the article category given by uri
        """
        category = cls.search([('unique_name', '=', uri)], limit=1)
        if not category and not silent:
            raise RuntimeError("Article category %s not found" % uri)
        return category[0] if category else None

    @classmethod
    @route('/sitemaps/article-category-index.xml')
    def sitemap_index(cls):
        index = SitemapIndex(cls, [])
        return index.render()

    @classmethod
    @route('/sitemaps/article-category-<int:page>.xml')
    def sitemap(cls, page):
        sitemap_section = SitemapSection(cls, [], page)
        sitemap_section.changefreq = 'daily'
        return sitemap_section.render()

    def get_absolute_url(self, **kwargs):
        return url_for(
            'nereid.cms.article.category.render',
            uri=self.unique_name, **kwargs
        )

    def get_published_articles(self, name):
        """
        Get the published articles.
        """
        NereidArticle = Pool().get('nereid.cms.article')

        articles = NereidArticle.search([
            ('state', '=', 'published'),
            ('categories', '=', self.id)
        ])
        return map(int, articles)

    def get_children(self, max_depth):
        """
        Return serialized menu_item for current menu_item children
        """
        NereidArticle = Pool().get('nereid.cms.article')

        articles = NereidArticle.search([
            ('state', '=', 'published'),
            ('categories', '=', self.id)
        ])
        return [
            article.get_menu_item(max_depth=max_depth - 1)
            for article in articles
        ]

    def serialize(self, purpose=None):
        """
        Article category serialize method
        """
        if purpose == 'atom':
            return {
                'term': self.unique_name,
            }
        elif hasattr(super(ArticleCategory, self), 'serialize'):
            return super(ArticleCategory, self).serialize(purpose=purpose)

    @classmethod
    @route('/article-category/<uri>.atom')
    def atom_feed(cls, uri):
        """
        Returns atom feed for articles published under a particular category.
        """
        try:
            category, = cls.search([
                ('unique_name', '=', uri),
            ], limit=1)
        except ValueError:
            abort(404)

        feed = AtomFeed(
            "Articles by Category %s" % category.unique_name,
            feed_url=request.url, url=request.host_url
        )
        for article in category.published_articles:
            feed.add(**article.serialize(purpose='atom'))

        return feed.get_response()
示例#12
0
class Email(ModelSQL, ModelView):
    "Email Notitification"
    __name__ = 'notification.email'

    from_ = fields.Char(
        "From",
        help="Leave empty for the value defined in the configuration file.")
    recipients = fields.Many2One(
        'ir.model.field',
        "Recipients",
        domain=[
            ('model.model', '=', Eval('model')),
            ('relation', 'in', ['res.user', 'party.party', 'web.user']),
        ],
        depends=['model'],
        help="The field that contains the recipient(s).")
    recipients_secondary = fields.Many2One(
        'ir.model.field',
        "Secondary Recipients",
        domain=[
            ('model.model', '=', Eval('model')),
            ('relation', 'in', ['res.user', 'party.party', 'web.user']),
        ],
        depends=['model'],
        help="The field that contains the secondary recipient(s).")
    recipients_hidden = fields.Many2One(
        'ir.model.field',
        "Hidden Recipients",
        domain=[
            ('model.model', '=', Eval('model')),
            ('relation', 'in', ['res.user', 'party.party', 'web.user']),
        ],
        depends=['model'],
        help="The field that contains the hidden recipient(s).")

    content = fields.Many2One('ir.action.report',
                              "Content",
                              required=True,
                              domain=[('template_extension', 'in',
                                       ['txt', 'html', 'xhtml'])],
                              help="The report used as email template.")
    attachments = fields.Many2Many('notification.email.attachment',
                                   'notification',
                                   'report',
                                   "Attachments",
                                   domain=[
                                       ('model', '=', Eval('model')),
                                   ],
                                   depends=['model'],
                                   help="The reports used as attachments.")

    triggers = fields.One2Many('ir.trigger',
                               'notification_email',
                               "Triggers",
                               domain=[('model.model', '=', Eval('model'))],
                               depends=['model'],
                               help="Add a trigger for the notification.")

    model = fields.Function(fields.Char("Model"),
                            'on_change_with_model',
                            searcher='search_model')

    def get_rec_name(self, name):
        return self.content.rec_name

    @classmethod
    def search_rec_name(cls, name, clause):
        return [('content', ) + tuple(clause[1:])]

    @fields.depends('content')
    def on_change_with_model(self, name=None):
        if self.content:
            return self.content.model

    @classmethod
    def search_model(cls, name, clause):
        return [('content.model', ) + tuple(clause[1:])]

    def _get_address(self, record):
        pool = Pool()
        User = pool.get('res.user')
        try:
            Party = pool.get('party.party')
        except KeyError:
            Party = None
        try:
            WebUser = pool.get('web.user')
        except KeyError:
            WebUser = None
        if isinstance(record, User) and record.email:
            return _formataddr(record.rec_name, record.email)
        elif Party and isinstance(record, Party) and record.email:
            # TODO similar to address_get for contact mechanism
            return _formataddr(record.rec_name, record.email)
        elif WebUser and isinstance(record, WebUser):
            name = None
            if record.party:
                name = record.party.rec_name
            return _formataddr(name, record.email)

    def _get_addresses(self, value):
        if isinstance(value, (list, tuple)):
            addresses = [(self._get_address(v) for v in value)]
        else:
            addresses = [self._get_address(value)]
        return filter(None, addresses)

    def _get_language(self, record):
        pool = Pool()
        Configuration = pool.get('ir.configuration')
        User = pool.get('res.user')
        Lang = pool.get('ir.lang')
        try:
            Party = pool.get('party.party')
        except KeyError:
            Party = None
        try:
            WebUser = pool.get('web.user')
        except KeyError:
            WebUser = None
        if isinstance(record, User):
            if record.language:
                return record.language
        elif Party and isinstance(record, Party):
            if record.lang:
                return record.lang
        elif WebUser and isinstance(record, WebUser):
            if record.party and record.party.lang:
                return record.party.lang
        lang, = Lang.search([
            ('code', '=', Configuration.get_language()),
        ],
                            limit=1)
        return lang

    def _get_languages(self, value):
        if isinstance(value, (list, tuple)):
            return {self._get_language(v) for v in value}
        else:
            return {self._get_language(value)}

    def get_email(self, record, from_, to, cc, bcc, languages):
        pool = Pool()
        Attachment = pool.get('notification.email.attachment')

        # TODO order languages to get default as last one for title
        content, title = get_email(self.content, record, languages)

        if self.attachments:
            msg = MIMEMultipart('mixed')
            msg.attach(content)
            language = list(languages)[-1]
            for report in self.attachments:
                msg.attach(Attachment.get_mime(report, record, language.code))
        else:
            msg = content

        msg['From'] = from_
        msg['To'] = ', '.join(to)
        msg['Cc'] = ', '.join(cc)
        msg['Bcc'] = ', '.join(bcc)
        msg['Subject'] = Header(title, 'utf-8')
        msg['Auto-Submitted'] = 'auto-generated'
        return msg

    def get_log(self, record, trigger, msg):
        return {
            'recipients': msg['To'],
            'recipients_secondary': msg['Cc'],
            'recipients_hidden': msg['Bcc'],
            'trigger': trigger.id,
        }

    @classmethod
    def trigger(cls, records, trigger):
        "Action function for the triggers"
        if not trigger.notification_email:
            raise ValueError(
                'Trigger "%s" is not related to any email notification' %
                trigger.rec_name)
        trigger.notification_email.send_email(records, trigger)

    def send_email(self, records, trigger):
        pool = Pool()
        Log = pool.get('notification.email.log')
        datamanager = SMTPDataManager()
        Transaction().join(datamanager)
        from_ = self.from_ or config.get('email', 'from')
        logs = []
        for record in records:
            languagues = set()
            to = []
            if self.recipients:
                recipients = getattr(record, self.recipients.name, None)
                if recipients:
                    languagues.update(self._get_languages(recipients))
                    to = self._get_addresses(recipients)
            cc = []
            if self.recipients_secondary:
                recipients_secondary = getattr(record,
                                               self.recipients_secondary.name,
                                               None)
                if recipients_secondary:
                    languagues.update(
                        self._get_languages(recipients_secondary))
                    cc = self._get_addresses(recipients_secondary)
            bcc = []
            if self.recipients_hidden:
                recipients_hidden = getattr(record,
                                            self.recipients_hidden.name, None)
                if recipients_hidden:
                    languagues.update(self._get_languages(recipients_hidden))
                    bcc = self._get_addresses(recipients_hidden)

            msg = self.get_email(record, from_, to, cc, bcc, languagues)
            to_addrs = [e for _, e in getaddresses(to + cc + bcc)]
            if to_addrs:
                sendmail_transactional(from_,
                                       to_addrs,
                                       msg,
                                       datamanager=datamanager)
                logs.append(self.get_log(record, trigger, msg))
        if logs:
            Log.create(logs)
示例#13
0
class CaseAbstract(ModelSQL, ModelView):
    'Medical Record Case Abstract'
    __name__ = 'mrca.mrca'

    icd10 = fields.Many2One('gnuhealth.pathology', 'ICD 10',
        domain=[
            'OR',
            ('classifier', '=', 'ICD10'),
            ('classifier', '=', None)
        ] ,select=True)
    
    patient = fields.Many2One('gnuhealth.inpatient.registration', 'Patient', domain=[('state', '=', 'done')], required=True, select=True)

    icd11 = fields.Char('Main Condition Code')
    icd11_description = fields.Function(
        fields.Text('Main Condition Interpretation'), 'get_icd11_information')

    icd11_other = fields.Text('Other Conditions')
    icd11_other_description = fields.Function(
        fields.Text('Other Conditions - Interpretation'), 'get_icd11_other_conditions')

    coding_tool = fields.Function(
        fields.Char('Coding Tool'), 'get_coding_tool_url')

    icd10_other = fields.Many2Many('mrca.mrca_patient.abstract', 'code', 'code','Other Conditions (ICD 10)')
    icd10_procedures = fields.Many2Many('mrca.mrca_patient.abstract', 'description', 'description','Procedures (ICD 10)')

    new_diag = fields.Boolean('Newly Diagnosed', select=True)
    re_admiss = fields.Boolean('Re-Admission', select=True)

    def get_coding_tool_url(self, ids=None, name=None):
        return get_coding_tool_url()

    @classmethod
    def default_coding_tool(cls):
        return get_coding_tool_url()

    def get_icd_url(self, name=None):
        return get_icd_url()

    def get_icd11_information(self, name):
        if not self.icd11:
            return ''
        return self.build_condition_str(self.icd11)

    def get_icd11_other_conditions(self, name):
        if not self.icd11_other:
            return ''
        codes = self.icd11_other.split('\n')
        descriptions = []
        for code in codes:
            desc = self.build_condition_str(code)
            if not desc or desc in descriptions:
                continue
            title = 'Interpretation for {}'.format(code)
            sep = '-' * (len(title) * 2)
            descriptions.append(
                '{}\n{}\n{}'.format(title, sep, desc)
            )
        return '\n\n'.join(descriptions)
    
    def build_condition_str(self, code):
        host = self.get_icd_url()
        try:
            output = query_icd11(code, host=host)
        except requests.exceptions.ConnectionError as e:
            print("Except as e: {}".format(e))
            return 'Error: Cannot connect to the disease database container.'
        except Exception as e:
            print("Except as e: {}".format(e))
            return ''
        descriptions = []
        for item in output:
            descriptions.append('- {} - {}'.format(item['code'], item['description']))
        return '\n'.join(descriptions)


    @classmethod
    def __setup__(cls):
	    super(CaseAbstract, cls).__setup__()
示例#14
0
class Sheet(TaggedMixin, Workflow, ModelSQL, ModelView):
    'Shine Sheet'
    __name__ = 'shine.sheet'
    name = fields.Char('Name', required=True)
    revision = fields.Integer('Revision', required=True, readonly=True)
    alias = fields.Char('Alias')
    type = fields.Selection([
        ('sheet', 'Sheet'),
        ('singleton', 'Singleton'),
    ],
                            'Type',
                            states={
                                'readonly': Eval('state') != 'draft',
                            },
                            required=True)
    state = fields.Selection(SHEET_STATES,
                             'State',
                             readonly=True,
                             required=True)
    formulas = fields.One2Many('shine.formula',
                               'sheet',
                               'Formulas',
                               states={
                                   'readonly': Eval('state') != 'draft',
                               },
                               depends=['state'])
    dataset = fields.Many2One('shine.dataset',
                              'Data Set',
                              states={
                                  'readonly': Eval('state') != 'draft',
                              },
                              depends=['state'])
    timeout = fields.Integer(
        'Timeout (s)',
        states={
            'invisible': ~Bool(Eval('dataset')),
            'required': Bool(Eval('dataset')),
        },
        help='Maximum amount of time allowed for computing sheet data.')
    views = fields.One2Many('shine.view', 'sheet', 'Views')
    tags = fields.Many2Many('shine.sheet.tag',
                            'sheet',
                            'tag',
                            'Tags',
                            domain=[
                                ('view', '=', False),
                            ])
    tags_char = fields.Function(fields.Char('Tags'),
                                'on_change_with_tags_char',
                                searcher='search_tags_char')
    current_table = fields.Many2One('shine.table', 'Table', readonly=True)
    python_code = fields.Function(fields.Text('Python Code'),
                                  'get_python_code')
    quick_edition = fields.Selection(
        SELECTION_EDITABLE,
        'Quick Edition',
        required=True,
        sort=False,
        states={
            'readonly': Eval('state') != 'draft',
        },
        help='"Bottom" adds new records at the bottom of the list.\n'
        '"Top" adds new records at the top of the list.')

    @staticmethod
    def default_quick_edition():
        return 'bottom'

    @staticmethod
    def default_type():
        return 'sheet'

    @staticmethod
    def default_state():
        return 'draft'

    @staticmethod
    def default_revision():
        return 1

    @staticmethod
    def default_timeout():
        Config = Pool().get('shine.configuration')
        return Config(0).default_timeout

    @classmethod
    def __setup__(cls):
        super(Sheet, cls).__setup__()
        cls._transitions |= set((
            ('draft', 'active'),
            ('draft', 'canceled'),
            ('active', 'draft'),
            ('active', 'canceled'),
            ('canceled', 'draft'),
        ))
        cls._buttons.update({
            'activate': {
                'icon': 'tryton-ok',
                'invisible': Eval('state') != 'draft',
            },
            'draft': {
                'icon': 'tryton-undo',
                'invisible': Eval('state') != 'active',
            },
            'open': {
                'icon': 'tryton-forward',
                'invisible': Eval('state') != 'active',
                'depends': ['current_table']
            },
            'compute': {
                'icon':
                'tryton-refresh',
                'invisible':
                ((Eval('state') != 'active') | ~Bool(Eval('dataset'))),
            },
            'update_formulas': {
                'icon': 'tryton-refresh',
                'invisible':
                ((Eval('state') != 'draft') | ~Bool(Eval('dataset'))),
            },
        })

    @fields.depends('name', 'alias')
    def on_change_name(self):
        if self.alias:
            return
        self.alias = convert_to_symbol(self.name)

    @fields.depends('tags')
    def on_change_with_tags_char(self, name=None):
        return ', '.join(sorted([x.name for x in self.tags]))

    @classmethod
    def search_tags_char(cls, name, clause):
        return [('tags.name', ) + tuple(clause[1:])]

    @classmethod
    @ModelView.button
    @Workflow.transition('active')
    def activate(cls, sheets):
        pool = Pool()
        Table = pool.get('shine.table')
        Field = pool.get('shine.table.field')
        Data = pool.get('shine.data')

        for sheet in sheets:
            sheet.check_formulas()
            sheet.check_icons()

            sheet.revision += 1
            table = Table()
            table.name = sheet.data_table_name
            table.singleton = (sheet.type == 'singleton')
            fields = []
            for formula in sheet.formulas:
                if not formula.type:
                    continue
                if not formula.store:
                    continue
                fields.append(
                    Field(
                        name=formula.alias,
                        string=formula.name,
                        type=formula.type,
                        help=formula.expression,
                        related_model=formula.related_model,
                        formula=(formula.expression if formula.expression
                                 and formula.expression.startswith('=') else
                                 None),
                    ))
            table.fields = fields
            table.create_table()
            table.save()

            if (not sheet.dataset and sheet.current_table
                    and sheet.current_table.count()):
                table.copy_from(sheet.current_table)
                with Transaction().set_context({'shine_table': table.id}):
                    Data.update_formulas()

            sheet.current_table = table

        cls.save(sheets)
        cls.reset_views(sheets)

    @classmethod
    def reset_views(cls, sheets):
        pool = Pool()
        View = pool.get('shine.view')

        to_delete = []
        for sheet in sheets:
            to_delete += [x for x in sheet.views if x.system]
        View.delete(to_delete)

        sheets = cls.browse([x.id for x in sheets])

        to_save = []
        for sheet in sheets:
            if sheet.type == 'sheet':
                to_save.append(sheet.get_default_list_view())
            to_save.append(sheet.get_default_form_view())
        View.save(to_save)

    def get_default_list_view(self):
        pool = Pool()
        View = pool.get('shine.view')
        ViewTableFormula = pool.get('shine.view.table.formula')

        view = View()
        view.sheet = self
        view.name = 'Default List View'
        view.system = True
        view.type = 'table'
        view.table_editable = self.quick_edition
        table_formulas = []
        for formula in self.formulas:
            table_formulas.append(ViewTableFormula(formula=formula))
        view.table_formulas = tuple(table_formulas)
        return view

    def get_default_form_view(self):
        View = Pool().get('shine.view')

        view = View()
        view.sheet = self
        view.name = 'Default Form View'
        view.system = True
        view.type = 'custom'
        view.custom_type = 'form'

        fields = []
        for formula in self.formulas:
            fields.append('<label name="%s"/>' % formula.alias)
            if formula.type in ('datetime', 'timestamp'):
                fields.append('<group col="2">'
                              '<field name="%s" widget="date"/>'
                              '<field name="%s" widget="time"/>'
                              '</group>' % (formula.alias, formula.alias))
                continue
            if formula.type == 'icon':
                fields.append('<image name="%s"/>\n' % (formula.alias))
                continue

            attributes = []
            if formula.type == 'image':
                attributes.append('widget="image"')

            fields.append('<field name="%s" %s/>\n' %
                          (formula.alias, ' '.join(attributes)))

        view.custom_arch = ('<?xml version="1.0"?>\n'
                            '<form>\n'
                            '%s'
                            '</form>') % '\n'.join(fields)
        return view

    def check_formulas(self):
        any_formula = False
        for formula in self.formulas:
            if formula.store:
                any_formula = True
            icon = formula.expression_icon
            if icon and icon != 'green':
                raise UserError(
                    gettext('shine.invalid_formula',
                            sheet=self.rec_name,
                            formula=formula.rec_name))
        if not any_formula:
            raise UserError(gettext('shine.no_formulas', sheet=self.rec_name))

    def check_icons(self):
        was_icon = False
        for formula in self.formulas:
            if formula.type == 'icon':
                if was_icon:
                    raise UserError(
                        gettext('shine.consecutive_icons',
                                sheet=self.rec_name))
                was_icon = True
            else:
                was_icon = False
        if was_icon:
            raise UserError(gettext('shine.last_icon', sheet=self.rec_name))

    @classmethod
    @ModelView.button
    @Workflow.transition('draft')
    def draft(cls, sheets):
        pass

    @classmethod
    @ModelView.button_action('shine.act_open_sheet_form')
    def open(cls, sheets):
        pass

    @classmethod
    def copy(cls, sheets, default=None):
        if default is None:
            default = {}
        else:
            default = default.copy()
        default.setdefault('current_table', None)
        # TODO: views should be copied and their formulas should point to the
        # formulas of the new sheet
        default.setdefault('views', None)
        return super(Sheet, cls).copy(sheets, default)

    @classmethod
    @ModelView.button
    def compute(cls, sheets):
        for sheet in sheets:
            sheet.compute_sheet()

    @classmethod
    @ModelView.button
    def update_formulas(cls, sheets):
        pool = Pool()
        Formula = pool.get('shine.formula')
        Model = pool.get('ir.model')
        formulas = []
        for sheet in sheets:
            if not sheet.dataset:
                return
            current_formulas = [x.alias for x in sheet.formulas]
            for field in sheet.dataset.get_fields():
                if field['alias'] in current_formulas:
                    continue
                formula = Formula()
                formula.sheet = sheet
                formula.name = field['name']
                formula.alias = field['alias']
                formula.type = field['type']
                if field.get('related_model'):
                    related_model, = Model.search([
                        ('model', '=', field['related_model']),
                    ])
                    formula.related_model = related_model
                formula.store = True
                formulas.append(formula)
        if formulas:
            Formula.save(formulas)

    @property
    def data_table_name(self):
        return ('shine.sheet.%d.%d' % (self.id or 0, self.revision)).replace(
            '.', '_')

    def compute_sheet(self):
        cursor = Transaction().connection.cursor()

        table = sql.Table(self.data_table_name)
        cursor.execute(*table.delete())

        #fields = dict([(x.alias, x) for x in self.formulas])
        direct_fields = [x.alias for x in self.formulas if not x.expression]
        formula_fields = [x.alias for x in self.formulas if x.expression]
        sql_fields = [
            sql.Column(table, x) for x in direct_fields + formula_fields
        ]

        parser = formulas.Parser()
        formula_fields = [(x, parser.ast(x.expression)[1].compile()
                           if x.expression.startswith('=') else '')
                          for x in self.formulas if x.expression]

        insert_values = []
        checker = TimeoutChecker(self.timeout, self.timeout_exception)
        if not formula_fields:
            # If there are no formula_fields we can make the loop faster as
            # we don't use OrderedDict and don't evaluate formulas
            for records in self.dataset.get_data():
                checker.check()
                for record in records:
                    insert_values.append(
                        [getattr(record, x) for x in direct_fields])
        else:
            for records in self.dataset.get_data():
                checker.check()
                for record in records:
                    values = OrderedDict()
                    if direct_fields:
                        values.update(
                            OrderedDict([(x, getattr(record, x))
                                         for x in direct_fields]))
                    if formula_fields:
                        for field, ast in formula_fields:
                            if field.expression.startswith('='):
                                inputs = []
                                for input_ in ast.inputs.keys():
                                    # TODO: Check if input_ exists and raise
                                    # proper user error Indeed, we should check
                                    # de formulas when we move to active state
                                    inputs.append(values[input_.lower()])
                                value = ast(inputs)[0]
                            else:
                                value = field.expression
                            ftype = FIELD_TYPE_PYTHON[field.type]
                            values[field] = ftype(value)
                    insert_values.append(list(values.values()))
        if insert_values:
            cursor.execute(*table.insert(sql_fields, insert_values))

    def get_python_code(self, name):
        models = []
        if self.type == 'singleton':
            models.append('ModelSingleton')
        models += ['ModelSQL', 'ModelView']

        class_name = ''.join([x.capitalize() for x in self.alias.split('_')])
        code = []
        code.append('class %s(%s):' % (class_name, ', '.join(models)))
        code.append('    "%s"' % self.name)
        code.append('    __name__ = "%s"' % self.alias.replace('_', '.'))
        for formula in self.formulas:
            if not formula.type:
                continue
            code.append(
                '    %s = fields.%s("%s")' %
                (formula.alias, FIELD_TYPE_CLASS[formula.type], formula.name))
        return '\n'.join(code)

    def timeout_exception(self):
        raise TimeoutException
示例#15
0
class Lot(metaclass=PoolMeta):
    __name__ = 'stock.lot'

    subscription_services = fields.Many2Many(
        'sale.subscription.service-stock.lot.asset', 'lot', 'service',
        "Services")
    subscription_lines = fields.One2Many('sale.subscription.line', 'asset_lot',
                                         "Subscription Lines")
    subscribed = fields.Function(fields.Many2One('sale.subscription.line',
                                                 "Subscribed"),
                                 'get_subscribed',
                                 searcher='search_subscribed')

    @classmethod
    def get_subscribed(cls, lots, name):
        pool = Pool()
        Date = pool.get('ir.date')
        SubscriptionLine = pool.get('sale.subscription.line')

        subscribed_lines = {l.id: None for l in lots}
        date = Transaction().context.get('date', Date.today())
        for sub_lots in grouped_slice(lots):
            lines = SubscriptionLine.search([('asset_lot', 'in',
                                              [l.id for l in sub_lots]),
                                             [
                                                 ('start_date', '<=', date),
                                                 [
                                                     'OR',
                                                     ('end_date', '=', None),
                                                     ('end_date', '>', date),
                                                 ],
                                             ]])
            subscribed_lines.update((s.asset_lot.id, s.id) for s in lines)
        return subscribed_lines

    @classmethod
    def search_subscribed(cls, name, clause):
        pool = Pool()
        Date = pool.get('ir.date')

        name, operator, value = clause[:3]
        date = Transaction().context.get('date', Date.today())
        domain = [
            ('asset_lot', '!=', None),
            ('start_date', '<=', date),
            [
                'OR',
                ('end_date', '=', None),
                ('end_date', '>', date),
            ],
        ]
        if '.' in name:
            _, target_name = name.split('.', 1)
            domain.append((target_name, ) + tuple(clause[1:]))
            return [('subscription_lines', 'where', domain)]
        else:
            if (operator, value) == ('=', None):
                return [('subscription_lines', 'not where', domain)]
            elif (operator, value) == ('!=', None):
                return [('subscription_lines', 'where', domain)]
            else:
                if isinstance(value, str):
                    target_name = 'rec_name'
                else:
                    target_name = 'id'
                domain.append((target_name, ) + tuple(clause[1:]))
                return [('subscription_lines', 'where', domain)]
示例#16
0
文件: work.py 项目: tinavas/FSERP
class Work:
    __name__ = 'project.work'
    predecessors = fields.Many2Many('project.predecessor_successor',
                                    'successor',
                                    'predecessor',
                                    'Predecessors',
                                    domain=[
                                        ('parent', '=', Eval('parent')),
                                        ('id', '!=', Eval('id')),
                                    ],
                                    depends=['parent', 'id'])
    successors = fields.Many2Many('project.predecessor_successor',
                                  'predecessor',
                                  'successor',
                                  'Successors',
                                  domain=[
                                      ('parent', '=', Eval('parent')),
                                      ('id', '!=', Eval('id')),
                                  ],
                                  depends=['parent', 'id'])
    leveling_delay = fields.Float("Leveling Delay", required=True)
    back_leveling_delay = fields.Float("Back Leveling Delay", required=True)
    allocations = fields.One2Many('project.allocation',
                                  'work',
                                  'Allocations',
                                  states={
                                      'invisible': Eval('type') != 'task',
                                  },
                                  depends=['type'])
    duration = fields.Function(
        fields.TimeDelta('Duration', 'company_work_time'),
        'get_function_fields')
    early_start_time = fields.DateTime("Early Start Time", readonly=True)
    late_start_time = fields.DateTime("Late Start Time", readonly=True)
    early_finish_time = fields.DateTime("Early Finish Time", readonly=True)
    late_finish_time = fields.DateTime("Late Finish Time", readonly=True)
    actual_start_time = fields.DateTime("Actual Start Time")
    actual_finish_time = fields.DateTime("Actual Finish Time")
    constraint_start_time = fields.DateTime("Constraint Start Time")
    constraint_finish_time = fields.DateTime("Constraint  Finish Time")
    early_start_date = fields.Function(fields.Date('Early Start'),
                                       'get_function_fields')
    late_start_date = fields.Function(fields.Date('Late Start'),
                                      'get_function_fields')
    early_finish_date = fields.Function(fields.Date('Early Finish'),
                                        'get_function_fields')
    late_finish_date = fields.Function(fields.Date('Late Finish'),
                                       'get_function_fields')
    actual_start_date = fields.Function(fields.Date('Actual Start'),
                                        'get_function_fields',
                                        setter='set_function_fields')
    actual_finish_date = fields.Function(fields.Date('Actual Finish'),
                                         'get_function_fields',
                                         setter='set_function_fields')
    constraint_start_date = fields.Function(fields.Date('Constraint Start',
                                                        depends=['type']),
                                            'get_function_fields',
                                            setter='set_function_fields')
    constraint_finish_date = fields.Function(fields.Date('Constraint Finish',
                                                         depends=['type']),
                                             'get_function_fields',
                                             setter='set_function_fields')

    @classmethod
    def __setup__(cls):
        super(Work, cls).__setup__()

    @classmethod
    def validate(cls, works):
        super(Work, cls).validate(works)
        cls.check_recursion(works, parent='successors')

    @staticmethod
    def default_leveling_delay():
        return 0.0

    @staticmethod
    def default_back_leveling_delay():
        return 0.0

    @classmethod
    def get_function_fields(cls, works, names):
        '''
        Function to compute function fields
        '''
        res = {}

        ids = [w.id for w in works]
        if 'duration' in names:
            all_works = cls.search([('parent', 'child_of', ids),
                                    ('active', '=', True)]) + works
            all_works = set(all_works)

            durations = {}
            id2work = {}
            leafs = set()
            for work in all_works:
                id2work[work.id] = work
                if not work.children:
                    leafs.add(work.id)

                total_allocation = 0
                if not work.allocations or not work.effort_duration:
                    durations[work.id] = work.effort_duration
                    continue
                for allocation in work.allocations:
                    total_allocation += allocation.percentage
                durations[work.id] = datetime.timedelta(
                    seconds=work.effort_duration.total_seconds() /
                    (total_allocation / 100.0))

            while leafs:
                for work_id in leafs:
                    work = id2work[work_id]
                    all_works.remove(work)
                    if not work.active:
                        continue
                    if work.parent and work.parent.id in durations:
                        durations[work.parent.id] += durations[work_id]
                next_leafs = set(w.id for w in all_works)
                for work in all_works:
                    if not work.parent:
                        continue
                    if work.parent.id in next_leafs and work.parent in works:
                        next_leafs.remove(work.parent.id)
                leafs = next_leafs
            res['duration'] = durations

        fun_fields = ('early_start_date', 'early_finish_date',
                      'late_start_date', 'late_finish_date',
                      'actual_start_date', 'actual_finish_date',
                      'constraint_start_date', 'constraint_finish_date')
        db_fields = ('early_start_time', 'early_finish_time',
                     'late_start_time', 'late_finish_time',
                     'actual_start_time', 'actual_finish_time',
                     'constraint_start_time', 'constraint_finish_time')

        for fun_field, db_field in zip(fun_fields, db_fields):
            if fun_field in names:
                values = {}
                for work in works:
                    values[work.id] = getattr(work, db_field) \
                        and getattr(work, db_field).date() or None
                res[fun_field] = values

        return res

    @classmethod
    def set_function_fields(cls, works, name, value):
        fun_fields = ('actual_start_date', 'actual_finish_date',
                      'constraint_start_date', 'constraint_finish_date')
        db_fields = ('actual_start_time', 'actual_finish_time',
                     'constraint_start_time', 'constraint_finish_time')
        for fun_field, db_field in zip(fun_fields, db_fields):
            if fun_field == name:
                cls.write(
                    works, {
                        db_field:
                        (value
                         and datetime.datetime.combine(value, datetime.time())
                         or None),
                    })
                break

    @property
    def hours(self):
        if not self.duration:
            return 0
        return self.duration.total_seconds() / 60 / 60

    @classmethod
    def add_minutes(cls, company, date, minutes):
        minutes = int(round(minutes))
        minutes = date.minute + minutes

        hours = minutes // 60
        if hours:
            date = cls.add_hours(company, date, hours)

        minutes = minutes % 60

        date = datetime.datetime(date.year, date.month, date.day, date.hour,
                                 minutes, date.second)

        return date

    @classmethod
    def add_hours(cls, company, date, hours):
        while hours:
            if hours != intfloor(hours):
                minutes = (hours - intfloor(hours)) * 60
                date = cls.add_minutes(company, date, minutes)
            hours = intfloor(hours)

            hours = date.hour + hours
            days = hours // company.hours_per_work_day
            if days:
                date = cls.add_days(company, date, days)

            hours = hours % company.hours_per_work_day

            date = datetime.datetime(date.year, date.month, date.day,
                                     intfloor(hours), date.minute, date.second)

            hours = hours - intfloor(hours)

        return date

    @classmethod
    def add_days(cls, company, date, days):
        day_per_week = company.hours_per_work_week / company.hours_per_work_day

        while days:
            if days != intfloor(days):
                hours = (days - intfloor(days)) * company.hours_per_work_day
                date = cls.add_hours(company, date, hours)
            days = intfloor(days)

            days = date.weekday() + days

            weeks = days // day_per_week
            days = days % day_per_week

            if weeks:
                date = cls.add_weeks(company, date, weeks)

            date += datetime.timedelta(days=-date.weekday() + intfloor(days))

            days = days - intfloor(days)

        return date

    @classmethod
    def add_weeks(cls, company, date, weeks):
        day_per_week = company.hours_per_work_week / company.hours_per_work_day

        if weeks != intfloor(weeks):
            days = (weeks - intfloor(weeks)) * day_per_week
            if days:
                date = cls.add_days(company, date, days)

        date += datetime.timedelta(days=7 * intfloor(weeks))

        return date

    def compute_dates(self):
        values = {}
        get_early_finish = lambda work: values.get(work, {}).get(
            'early_finish_time', work.early_finish_time)
        get_late_start = lambda work: values.get(work, {}).get(
            'late_start_time', work.late_start_time)
        maxdate = lambda x, y: x and y and max(x, y) or x or y
        mindate = lambda x, y: x and y and min(x, y) or x or y

        # propagate constraint_start_time
        constraint_start = reduce(maxdate, (pred.early_finish_time
                                            for pred in self.predecessors),
                                  None)

        if constraint_start is None and self.parent:
            constraint_start = self.parent.early_start_time

        constraint_start = maxdate(constraint_start,
                                   self.constraint_start_time)

        works = deque([(self, constraint_start)])
        work2children = {}
        parent = None

        while works or parent:
            if parent:
                work = parent
                parent = None

                # Compute early_finish
                if work.children:
                    early_finish_time = reduce(
                        maxdate, map(get_early_finish, work.children), None)
                else:
                    early_finish_time = None
                    if values[work]['early_start_time']:
                        early_finish_time = self.add_hours(
                            work.company, values[work]['early_start_time'],
                            work.hours)
                values[work]['early_finish_time'] = early_finish_time

                # Propagate constraint_start on successors
                for w in work.successors:
                    works.append((w, early_finish_time))

                if not work.parent:
                    continue

                # housecleaning work2children
                if work.parent not in work2children:
                    work2children[work.parent] = set()
                work2children[work.parent].update(work.successors)

                if work in work2children[work.parent]:
                    work2children[work.parent].remove(work)

                # if no sibling continue to walk up the tree
                if not work2children.get(work.parent):
                    if work.parent not in values:
                        values[work.parent] = {}
                    parent = work.parent

                continue

            work, constraint_start = works.popleft()
            # take constraint define on the work into account
            constraint_start = maxdate(constraint_start,
                                       work.constraint_start_time)

            if constraint_start:
                early_start = self.add_hours(work.company, constraint_start,
                                             work.leveling_delay)
            else:
                early_start = None

            # update values
            if work not in values:
                values[work] = {}
            values[work]['early_start_time'] = early_start

            # Loop on children if they exist
            if work.children and work not in work2children:
                work2children[work] = set(work.children)
                # Propagate constraint_start on children
                for w in work.children:
                    if w.predecessors:
                        continue
                    works.append((w, early_start))
            else:
                parent = work

        # propagate constraint_finish_time
        constraint_finish = reduce(mindate, (succ.late_start_time
                                             for succ in self.successors),
                                   None)

        if constraint_finish is None and self.parent:
            constraint_finish = self.parent.late_finish_time

        constraint_finish = mindate(constraint_finish,
                                    self.constraint_finish_time)

        works = deque([(self, constraint_finish)])
        work2children = {}
        parent = None

        while works or parent:
            if parent:
                work = parent
                parent = None

                # Compute late_start
                if work.children:
                    reduce(mindate, map(get_late_start, work.children), None)
                else:
                    late_start_time = None
                    if values[work]['late_finish_time']:
                        late_start_time = self.add_hours(
                            work.company, values[work]['late_finish_time'],
                            -work.hours)
                values[work]['late_start_time'] = late_start_time

                # Propagate constraint_finish on predecessors
                for w in work.predecessors:
                    works.append((w, late_start_time))

                if not work.parent:
                    continue

                # housecleaning work2children
                if work.parent not in work2children:
                    work2children[work.parent] = set()
                work2children[work.parent].update(work.predecessors)

                if work in work2children[work.parent]:
                    work2children[work.parent].remove(work)

                # if no sibling continue to walk up the tree
                if not work2children.get(work.parent):
                    if work.parent not in values:
                        values[work.parent] = {}
                    parent = work.parent

                continue

            work, constraint_finish = works.popleft()
            # take constraint define on the work into account
            constraint_finish = mindate(constraint_finish,
                                        work.constraint_finish_time)

            if constraint_finish:
                late_finish = self.add_hours(work.company, constraint_finish,
                                             -work.back_leveling_delay)
            else:
                late_finish = None

            # update values
            if work not in values:
                values[work] = {}
            values[work]['late_finish_time'] = late_finish

            # Loop on children if they exist
            if work.children and work not in work2children:
                work2children[work] = set(work.children)
                # Propagate constraint_start on children
                for w in work.children:
                    if w.successors:
                        continue
                    works.append((w, late_finish))
            else:
                parent = work

        # write values
        write_fields = ('early_start_time', 'early_finish_time',
                        'late_start_time', 'late_finish_time')
        for work, val in values.iteritems():
            write_cond = False
            for field in write_fields:
                if field in val and getattr(work, field) != val[field]:
                    write_cond = True
                    break

            if write_cond:
                self.write([work], val)

    def reset_leveling(self):
        get_key = lambda w: (set(p.id for p in w.predecessors),
                             set(s.id for s in w.successors))

        parent_id = self.parent and self.parent.id or None
        siblings = self.search([('parent', '=', parent_id)])
        to_clean = []

        ref_key = get_key(self)
        for sibling in siblings:
            if sibling.leveling_delay == sibling.back_leveling_delay == 0:
                continue
            if get_key(sibling) == ref_key:
                to_clean.append(sibling)

        if to_clean:
            self.write(to_clean, {
                'leveling_delay': 0,
                'back_leveling_delay': 0,
            })

    def create_leveling(self):
        # define some helper functions
        get_key = lambda w: (set(p.id for p in w.predecessors),
                             set(s.id for s in w.successors))
        over_alloc = lambda current_alloc, work: (reduce(
            lambda res, alloc:
            (res or (current_alloc[alloc.employee.id] + alloc.percentage) > 100
             ), work.allocations, False))

        def sum_allocs(current_alloc, work):
            res = defaultdict(float)
            for alloc in work.allocations:
                empl = alloc.employee.id
                res[empl] = current_alloc[empl] + alloc.percentage
            return res

        def compute_delays(siblings):
            # time_line is a list [[end_delay, allocations], ...], this
            # mean that allocations is valid between the preceding end_delay
            # (or 0 if it doesn't exist) and the current end_delay.
            timeline = []
            for sibling in siblings:
                delay = 0
                ignored = []
                overloaded = []
                item = None

                while timeline:
                    # item is [end_delay, allocations]
                    item = heappop(timeline)
                    if over_alloc(item[1], sibling):
                        ignored.extend(overloaded)
                        ignored.append(item)
                        delay = item[0]
                        continue
                    elif item[1] >= delay + sibling.duration:
                        overloaded.append(item)
                    else:
                        # Succes!
                        break

                heappush(timeline, [
                    delay + sibling.duration,
                    sum_allocs(defaultdict(float), sibling), sibling.id
                ])

                for i in ignored:
                    heappush(timeline, i)
                for i in overloaded:
                    i[1] = sum_allocs(i[1], sibling)
                    heappush(timeline, i)

                yield sibling, delay

        siblings = self.search([('parent', '=',
                                 self.parent.id if self.parent else None)])

        refkey = get_key(self)
        siblings = [s for s in siblings if get_key(s) == refkey]

        for sibling, delay in compute_delays(siblings):
            self.write([sibling], {
                'leveling_delay': delay,
            })

        siblings.reverse()
        for sibling, delay in compute_delays(siblings):
            self.write([sibling], {
                'back_leveling_delay': delay,
            })

        if self.parent:
            self.parent.compute_dates()

    @classmethod
    def write(cls, *args):
        super(Work, cls).write(*args)

        actions = iter(args)
        for works, values in zip(actions, actions):
            if 'effort' in values:
                for work in works:
                    work.reset_leveling()
            fields = ('constraint_start_time', 'constraint_finish_time',
                      'effort')
            if reduce(lambda x, y: x or y in values, fields, False):
                for work in works:
                    work.compute_dates()

    @classmethod
    def create(cls, vlist):
        works = super(Work, cls).create(vlist)
        for work in works:
            work.reset_leveling()
            work.compute_dates()
        return works

    @classmethod
    def delete(cls, works):
        to_update = set()
        for work in works:
            if work.parent and work.parent not in works:
                to_update.add(work.parent)
                to_update.update(c for c in work.parent.children
                                 if c not in works)
        super(Work, cls).delete(works)

        for work in to_update:
            work.reset_leveling()
            work.compute_dates()
示例#17
0
class Many2ManyRequired(ModelSQL):
    'Many2Many Required'
    __name__ = 'test.many2many_required'
    targets = fields.Many2Many('test.many2many_required.relation', 'origin',
        'target', 'Targets', required=True)
示例#18
0
class UIMenu(sequence_ordered(), ModelSQL, ModelView):
    "UI menu"
    __name__ = 'ir.ui.menu'

    name = fields.Char('Menu', required=True, translate=True)
    childs = fields.One2Many('ir.ui.menu', 'parent', 'Children')
    parent = fields.Many2One('ir.ui.menu',
                             'Parent Menu',
                             select=True,
                             ondelete='CASCADE')
    groups = fields.Many2Many('ir.ui.menu-res.group', 'menu', 'group',
                              'Groups')
    complete_name = fields.Function(fields.Char('Complete Name'),
                                    'get_rec_name',
                                    searcher='search_rec_name')
    icon = fields.Selection('list_icons', 'Icon', translate=False)
    action = fields.Function(fields.Reference(
        'Action',
        selection=[
            ('', ''),
            ('ir.action.report', 'ir.action.report'),
            ('ir.action.act_window', 'ir.action.act_window'),
            ('ir.action.wizard', 'ir.action.wizard'),
            ('ir.action.url', 'ir.action.url'),
        ]),
                             'get_action',
                             setter='set_action')
    action_keywords = fields.One2Many('ir.action.keyword', 'model',
                                      'Action Keywords')
    active = fields.Boolean('Active')
    favorite = fields.Function(fields.Boolean('Favorite'), 'get_favorite')

    @classmethod
    def __setup__(cls):
        super(UIMenu, cls).__setup__()
        cls._error_messages.update({
            'wrong_name': ('"%%s" is not a valid menu name because it is '
                           'not allowed to contain "%s".' % SEPARATOR),
        })

    @classmethod
    def order_complete_name(cls, tables):
        return cls.name.convert_order('name', tables, cls)

    @staticmethod
    def default_icon():
        return 'tryton-open'

    @staticmethod
    def default_sequence():
        return 10

    @staticmethod
    def default_active():
        return True

    @staticmethod
    def list_icons():
        pool = Pool()
        Icon = pool.get('ir.ui.icon')
        return sorted(CLIENT_ICONS + [(name, name)
                                      for _, name in Icon.list_icons()])

    @classmethod
    def validate(cls, menus):
        super(UIMenu, cls).validate(menus)
        cls.check_recursion(menus)
        for menu in menus:
            menu.check_name()

    def check_name(self):
        if SEPARATOR in self.name:
            self.raise_user_error('wrong_name', (self.name, ))

    def get_rec_name(self, name):
        parent = self.parent
        name = self.name
        while parent:
            name = parent.name + SEPARATOR + name
            parent = parent.parent
        return name

    @classmethod
    def search_rec_name(cls, name, clause):
        if isinstance(clause[2], basestring):
            values = clause[2].split(SEPARATOR.strip())
            values.reverse()
            domain = []
            field = 'name'
            for name in values:
                domain.append((field, clause[1], name.strip()))
                field = 'parent.' + field
        else:
            domain = [('name', ) + tuple(clause[1:])]
        ids = [m.id for m in cls.search(domain, order=[])]
        return [('parent', 'child_of', ids)]

    @classmethod
    def search_global(cls, text):
        # TODO improve search clause
        for record in cls.search([
            ('rec_name', 'ilike', '%%%s%%' % text),
        ]):
            if record.action:
                yield record, record.rec_name, record.icon

    @classmethod
    def search(cls,
               domain,
               offset=0,
               limit=None,
               order=None,
               count=False,
               query=False):
        menus = super(UIMenu, cls).search(domain,
                                          offset=offset,
                                          limit=limit,
                                          order=order,
                                          count=False,
                                          query=query)
        if query:
            return menus

        if menus:
            parent_ids = [x.parent.id for x in menus if x.parent]
            parents = cls.search([
                ('id', 'in', parent_ids),
            ])
            menus = [
                x for x in menus
                if (x.parent and x.parent in parents) or not x.parent
            ]

        if count:
            return len(menus)
        return menus

    @classmethod
    def get_action(cls, menus, name):
        pool = Pool()
        actions = dict((m.id, None) for m in menus)
        with Transaction().set_context(active_test=False):
            menus = cls.browse(menus)
        action_keywords = sum((list(m.action_keywords) for m in menus), [])
        key = lambda k: k.action.type
        action_keywords.sort(key=key)
        for type, action_keywords in groupby(action_keywords, key=key):
            action_keywords = list(action_keywords)
            for action_keyword in action_keywords:
                model = action_keyword.model
                actions[model.id] = '%s,-1' % type

            Action = pool.get(type)
            action2keyword = {k.action.id: k for k in action_keywords}
            with Transaction().set_context(active_test=False):
                factions = Action.search([
                    ('action', 'in', action2keyword.keys()),
                ])
            for action in factions:
                model = action2keyword[action.id].model
                actions[model.id] = str(action)
        return actions

    @classmethod
    def set_action(cls, menus, name, value):
        pool = Pool()
        ActionKeyword = pool.get('ir.action.keyword')
        action_keywords = []
        transaction = Transaction()
        for i in range(0, len(menus), transaction.database.IN_MAX):
            sub_menus = menus[i:i + transaction.database.IN_MAX]
            action_keywords += ActionKeyword.search([
                ('keyword', '=', 'tree_open'),
                ('model', 'in', [str(menu) for menu in sub_menus]),
            ])
        if action_keywords:
            with Transaction().set_context(_timestamp=False):
                ActionKeyword.delete(action_keywords)
        if not value:
            return
        if isinstance(value, basestring):
            action_type, action_id = value.split(',')
        else:
            action_type, action_id = value
        if int(action_id) <= 0:
            return
        Action = pool.get(action_type)
        action = Action(int(action_id))
        to_create = []
        for menu in menus:
            with Transaction().set_context(_timestamp=False):
                to_create.append({
                    'keyword': 'tree_open',
                    'model': str(menu),
                    'action': action.action.id,
                })
        if to_create:
            ActionKeyword.create(to_create)

    @classmethod
    def get_favorite(cls, menus, name):
        pool = Pool()
        Favorite = pool.get('ir.ui.menu.favorite')
        user = Transaction().user
        favorites = Favorite.search([
            ('menu', 'in', [m.id for m in menus]),
            ('user', '=', user),
        ])
        menu2favorite = dict(
            (m.id, False if m.action else None) for m in menus)
        menu2favorite.update(dict((f.menu.id, True) for f in favorites))
        return menu2favorite
示例#19
0
class Many2Many(ModelSQL):
    'Many2Many'
    __name__ = 'test.many2many'
    targets = fields.Many2Many('test.many2many.relation', 'origin', 'target',
        'Targets')
示例#20
0
class NereidUser(ModelSQL, ModelView):
    """
    Nereid Users
    """
    __name__ = "nereid.user"
    _rec_name = 'display_name'

    party = fields.Many2One(
        'party.party', 'Party', required=True,
        ondelete='CASCADE', select=1
    )

    display_name = fields.Char('Display Name', required=True)

    #: The email of the user is also the login name/username of the user
    email = fields.Char("e-Mail", select=1)

    #: The password is the user password + the salt, which is
    #: then hashed together
    password = fields.Sha('Password')

    #: The salt which was used to make the hash is separately
    #: stored. Needed for
    salt = fields.Char('Salt', size=8)

    # The company of the website(s) to which the user is affiliated. This
    # allows websites of the same company to share authentication/users. It
    # does not make business or technical sense to have website of multiple
    # companies share the authentication.
    #
    # .. versionchanged:: 0.3
    #     Company is mandatory
    company = fields.Many2One('company.company', 'Company', required=True)

    timezone = fields.Selection(
        [(x, x) for x in pytz.common_timezones], 'Timezone', translate=False
    )

    permissions = fields.Many2Many(
        'nereid.permission-nereid.user',
        'nereid_user', 'permission', 'Permissions'
    )

    email_verified = fields.Boolean("Email Verified")
    active = fields.Boolean('Active')

    @staticmethod
    def default_email_verified():
        return False

    @staticmethod
    def default_active():
        """
        If the user gets created from the web the activation should happen
        through the activation link. However, users created from tryton
        interface are activated by default
        """
        if has_request_context():
            return False
        return True

    @classmethod
    def __register__(cls, module_name):
        TableHandler = backend.get("TableHandler")
        table = TableHandler(Transaction().cursor, cls, module_name)
        user = cls.__table__()

        super(NereidUser, cls).__register__(module_name)

        # Migrations
        if table.column_exist('activation_code'):
            # Migration for activation_code field
            # Set the email_verification and active based on activation code
            user.update(
                columns=[user.active, user.email_verified],
                values=[True, True],
                where=(user.activation_code == None)
            )
            # Finally drop the column
            table.drop_column('activation_code', exception=True)

    def serialize(self, purpose=None):
        """
        Return a JSON serializable object that represents this record
        """
        return {
            'id': self.id,
            'email': self.email,
            'display_name': self.display_name,
            'permissions': list(self.get_permissions()),
        }

    def get_permissions(self):
        """
        Returns all the permissions as a list of names
        """
        # TODO: Cache this value for each user to avoid hitting the database
        # everytime.
        return frozenset([p.value for p in self.permissions])

    def has_permissions(self, perm_all=None, perm_any=None):
        """Check if the user has all required permissions in perm_all and
        has any permission from perm_any for access

        :param perm_all: A set/frozenset of all permission values/keywords.
        :param perm_any: A set/frozenset of any permission values/keywords.

        :return: True/False
        """
        if not perm_all and not perm_any:
            # Access allowed if no permission is required
            return True
        if not isinstance(perm_all, (set, frozenset)):
            perm_all = frozenset(perm_all if perm_all else [])
        if not isinstance(perm_any, (set, frozenset)):
            perm_any = frozenset(perm_any if perm_any else [])
        current_user_permissions = self.get_permissions()

        if perm_all and not perm_all.issubset(current_user_permissions):
            return False
        if perm_any and not perm_any.intersection(current_user_permissions):
            return False
        return True

    @staticmethod
    def default_timezone():
        return "UTC"

    @staticmethod
    def default_company():
        return Transaction().context.get('company') or False

    @classmethod
    def __setup__(cls):
        super(NereidUser, cls).__setup__()
        cls._sql_constraints += [
            ('unique_email_company', 'UNIQUE(email, company)',
                'Email must be unique in a company'),
        ]

    @property
    def _signer(self):
        return TimestampSigner(current_app.secret_key)

    @property
    def _serializer(self):
        return URLSafeSerializer(current_app.secret_key)

    def _get_sign(self, salt):
        """
        Returns a timestampsigned, url_serialized sign  with a salt
        'verification'.
        """
        return self._signer.sign(self._serializer.dumps(self.id, salt=salt))

    def get_email_verification_link(self, **options):
        """
        Returns an email verification link for the user
        """
        return url_for(
            'nereid.user.verify_email',
            sign=self._get_sign('verification'),
            active_id=self.id,
            **options
        )

    def get_activation_link(self, **options):
        """
        Returns an activation link for the user
        """
        return url_for(
            'nereid.user.activate',
            sign=self._get_sign('activation'),
            active_id=self.id,
            **options
        )

    def get_reset_password_link(self, **options):
        """
        Returns a password reset link for the user
        """
        return url_for(
            'nereid.user.new_password',
            sign=self._get_sign('reset-password'),
            active_id=self.id,
            **options
        )

    @classmethod
    def build_response(cls, message, response, xhr_status_code):
        """
        Method to handle response for jinja and XHR requests.

        message: Message to show as flash and send as json response.
        response: redirect or render_template method.
        xhr_status_code: Status code to be sent with json response.
        """
        if request.is_xhr or request.is_json:
            return jsonify(message=message), xhr_status_code
        flash(_(message))
        return response

    @route(
        "/verify-email/<int:active_id>/<sign>", methods=["GET"],
        readonly=False
    )
    def verify_email(self, sign, max_age=24 * 60 * 60):
        """
        Verifies the email and redirects to home page. This is a method in
        addition to the activate method which activates the account in addition
        to verifying the email.
        """
        try:
            unsigned = self._serializer.loads(
                self._signer.unsign(sign, max_age=max_age),
                salt='verification'
            )
        except SignatureExpired:
            return self.build_response(
                'The verification link has expired',
                redirect(url_for('nereid.website.home')), 400
            )
        except BadSignature:
            return self.build_response(
                'The verification token is invalid!',
                redirect(url_for('nereid.website.home')), 400
            )
        else:
            if self.id == unsigned:
                self.email_verified = True
                self.save()
                return self.build_response(
                    'Your email has been verified!',
                    redirect(url_for('nereid.website.home')), 200
                )
            else:
                return self.build_response(
                    'The verification token is invalid!',
                    redirect(url_for('nereid.website.home')), 400
                )

    @staticmethod
    def get_registration_form():
        """
        Returns a registration form for use in the site

        .. tip::

            Configuration of re_captcha

            Remember to forward X-Real-IP in the case of Proxy servers

        """
        # Add re_captcha if the configuration has such an option
        if config.has_option('nereid', 're_captcha_public_key'):
            registration_form = RegistrationForm(
                captcha={'ip_address': request.remote_addr}
            )
        else:
            registration_form = RegistrationForm()

        return registration_form

    @classmethod
    @route("/registration", methods=["GET", "POST"])
    def registration(cls):
        """
        Invokes registration of an user
        """
        Party = Pool().get('party.party')
        ContactMechanism = Pool().get('party.contact_mechanism')

        registration_form = cls.get_registration_form()

        if registration_form.validate_on_submit():
            with Transaction().set_context(active_test=False):
                existing = cls.search([
                    ('email', '=', registration_form.email.data),
                    ('company', '=', request.nereid_website.company.id),
                ])
            if existing:
                message = _(
                    'A registration already exists with this email. '
                    'Please contact customer care'
                )
                if request.is_xhr or request.is_json:
                    return jsonify(message=unicode(message)), 400
                else:
                    flash(message)
            else:
                party = Party(name=registration_form.name.data)
                party.addresses = []
                party.contact_mechanisms = [
                    ContactMechanism(
                        type="email",
                        value=registration_form.email.data
                    )
                ]
                party.save()
                nereid_user = cls(**{
                    'party': party.id,
                    'display_name': registration_form.name.data,
                    'email': registration_form.email.data,
                    'password': registration_form.password.data,
                    'company': request.nereid_website.company.id,
                }
                )
                nereid_user.save()
                registration.send(nereid_user)
                nereid_user.send_activation_email()
                message = _(
                    'Registration Complete. Check your email for activation'
                )
                if request.is_xhr or request.is_json:
                    return jsonify(message=unicode(message)), 201
                else:
                    flash(message)
                return redirect(
                    request.args.get('next', url_for('nereid.website.home'))
                )

        if registration_form.errors and (request.is_xhr or request.is_json):
            return jsonify({
                'message': unicode(_('Form has errors')),
                'errors': registration_form.errors,
            }), 400

        return render_template('registration.jinja', form=registration_form)

    def send_activation_email(self):
        """
        Send an activation email to the user

        :param nereid_user: The browse record of the user
        """
        EmailQueue = Pool().get('email.queue')

        email_message = render_email(
            config.get('email', 'from'),
            self.email, _('Account Activation'),
            text_template='emails/activation-text.jinja',
            html_template='emails/activation-html.jinja',
            nereid_user=self
        )
        EmailQueue.queue_mail(
            config.get('email', 'from'), self.email, email_message.as_string()
        )

    @classmethod
    @route("/change-password", methods=["GET", "POST"])
    @login_required
    def change_password(cls):
        """
        Changes the password

        .. tip::
            On changing the password, the user is logged out and the login page
            is thrown at the user
        """
        form = ChangePasswordForm(request.form)

        if request.method == 'POST' and form.validate():
            if request.nereid_user.match_password(form.old_password.data):
                cls.write(
                    [request.nereid_user],
                    {'password': form.password.data}
                )
                flash(
                    _('Your password has been successfully changed! '
                        'Please login again')
                )
                logout_user()
                return redirect(url_for('nereid.website.login'))
            else:
                flash(_("The current password you entered is invalid"))

        return render_template(
            'change-password.jinja', change_password_form=form
        )

    @route("/new-password/<int:active_id>/<sign>", methods=["GET", "POST"])
    def new_password(self, sign, max_age=24 * 60 * 60):
        """Create a new password

        This is intended to be used when a user requests for a password reset.
        The link sent out to reset the password will be a timestamped sign
        which is validated for max_age before allowing the user to set the
        new password.
        """
        form = NewPasswordForm()
        if form.validate_on_submit():
            try:
                unsigned = self._serializer.loads(
                    self._signer.unsign(sign, max_age=max_age),
                    salt='reset-password'
                )
            except SignatureExpired:
                return self.build_response(
                    'The password reset link has expired',
                    redirect(url_for('nereid.website.login')), 400
                )
            except BadSignature:
                return self.build_response(
                    'Invalid reset password code',
                    redirect(url_for('nereid.website.login')), 400
                )
            else:
                if not self.id == unsigned:
                    current_app.logger.debug('Invalid reset password code')
                    abort(403)

                self.write([self], {'password': form.password.data})
                return self.build_response(
                    'Your password has been successfully changed! '
                    'Please login again',
                    redirect(url_for('nereid.website.login')), 200
                )
        elif form.errors:
            if request.is_xhr or request.is_json:
                return jsonify(error=form.errors), 400
            flash(_('Passwords must match'))

        return render_template(
            'new-password.jinja', password_form=form, sign=sign, user=self
        )

    @route(
        "/activate-account/<int:active_id>/<sign>", methods=["GET"],
        readonly=False
    )
    def activate(self, sign, max_age=24 * 60 * 60):
        """A web request handler for activation of the user account. This
        method verifies the email and if it succeeds, activates the account.

        If your workflow requires a manual approval of every account, override
        this to not activate an account, or make a no op out of this method.

        If all what you require is verification of email, `verify_email` method
        could be used.
        """
        try:
            unsigned = self._serializer.loads(
                self._signer.unsign(sign, max_age=max_age),
                salt='activation'
            )
        except SignatureExpired:
            flash(_("The activation link has expired"))
        except BadSignature:
            flash(_("The activation token is invalid!"))
        else:
            if self.id == unsigned:
                self.active = True
                self.email_verified = True
                self.save()
                flash(_('Your account has been activated. Please login now.'))
            else:
                flash(_('Invalid Activation Code'))

        return redirect(url_for('nereid.website.login'))

    @classmethod
    @route("/reset-account", methods=["GET", "POST"])
    def reset_account(cls):
        """
        Reset the password for the user.

        .. tip::
            This does NOT reset the password, but just creates an activation
            code and sends the link to the email of the user. If the user uses
            the link, he can change his password.
        """
        form = ResetAccountForm()
        if form.validate_on_submit():
            try:
                nereid_user, = cls.search([
                    ('email', '=', form.email.data),
                    ('company', '=', request.nereid_website.company.id),
                ])
            except ValueError:
                return cls.build_response(
                    'Invalid email address',
                    render_template('reset-password.jinja'),
                    400
                )
            nereid_user.send_reset_email()
            return cls.build_response(
                'An email has been sent to your account for resetting'
                ' your credentials',
                redirect(url_for('nereid.website.login')), 200
            )
        elif form.errors:
            if request.is_xhr or request.is_json:
                return jsonify(error=form.errors), 400
            flash(_('Invalid email address.'))

        return render_template('reset-password.jinja')

    def send_reset_email(self):
        """
        Send an account reset email to the user

        :param nereid_user: The browse record of the user
        """
        EmailQueue = Pool().get('email.queue')

        email_message = render_email(
            config.get('email', 'from'),
            self.email, _('Account Password Reset'),
            text_template='emails/reset-text.jinja',
            html_template='emails/reset-html.jinja',
            nereid_user=self
        )
        EmailQueue.queue_mail(
            config.get('email', 'from'), self.email, email_message.as_string()
        )

    def match_password(self, password):
        """
        Checks if 'password' is the same as the current users password.

        :param password: The password of the user (string or unicode)
        :return: True or False
        """
        password += self.salt or ''
        if isinstance(password, unicode):
            password = password.encode('utf-8')
        if hashlib:
            digest = hashlib.sha1(password).hexdigest()
        else:
            digest = sha.new(password).hexdigest()
        return (digest == self.password)

    @classmethod
    def authenticate(cls, email, password):
        """Assert credentials and if correct return the
        browse record of the user.

        .. versionchanged:: 3.0.4.0

            Does not check if the user account is active or not as that
            is not in the scope of 'authentication'.

        :param email: email of the user
        :param password: password of the user
        :return:
            Browse Record: Successful Login
            None: User cannot be found or wrong password
        """
        if not (email and password):
            return None
        with Transaction().set_context(active_test=False):
            users = cls.search([
                ('email', '=', email),
                ('company', '=', request.nereid_website.company.id),
            ])

        if not users:
            current_app.logger.debug("No user with email %s" % email)
            return None

        if len(users) > 1:
            current_app.logger.debug('%s has too many accounts' % email)
            return None

        user, = users
        if user.match_password(password):
            return user

        return None

    @classmethod
    def load_user(cls, user_id):
        """
        Implements the load_user method for Flask-Login

        :param user_id: Unicode ID of the user
        """
        try:
            with Transaction().set_context(active_test=False):
                user, = cls.search([('id', '=', int(user_id))])
        except ValueError:
            return None

        # Instead of returning the active record returned in the above search
        # we are creating a new record here. This is because the returned
        # active record seems to carry around the context setting of
        # active_test and any nested lookup from the record will result in
        # records being fetched which are inactive.
        return cls(int(user_id))

    @classmethod
    def load_user_from_header(cls, header_val):
        """
        Implements the header_loader method for Flask-Login

        :param header_val: Value of the header
        """
        # Basic authentication
        if header_val.startswith('Basic '):
            header_val = header_val.replace('Basic ', '', 1)
            try:
                header_val = base64.b64decode(header_val)
            except TypeError:
                pass
            else:
                return cls.authenticate(*header_val.split(':'))

        # TODO: Digest authentication

        # Token in Authorization header
        if header_val.startswith(('token ', 'Token ')):
            token = header_val \
                            .replace('token ', '', 1) \
                            .replace('Token ', '', 1)
            return cls.load_user_from_token(token)

    @classmethod
    def load_user_from_token(cls, token):
        """
        Implements the token_loader method for Flask-Login

        :param token: The token sent in the user's request
        """
        serializer = TimedJSONWebSignatureSerializer(
            current_app.secret_key,
            expires_in=current_app.token_validity_duration
        )

        try:
            data = serializer.loads(token)
        except SignatureExpired:
            return None     # valid token, but expired
        except BadSignature:
            return None     # invalid token

        user = cls(data['id'])
        if user.password != data['password']:
            # The password has been changed by the user. So the token
            # should also be invalid.
            return None

        return user

    def get_auth_token(self):
        """
        Return an authentication token for the user. The auth token uniquely
        identifies the user and includes the salted hash of the password, then
        encrypted with a Timed serializer.

        The token_validity_duration can be set in application configuration
        using TOKEN_VALIDITY_DURATION
        """
        serializer = TimedJSONWebSignatureSerializer(
            current_app.secret_key,
            expires_in=current_app.token_validity_duration
        )
        local_txn = None
        if Transaction().cursor is None:
            # Flask-Login can call get_auth_token outside the context
            # of a nereid transaction. If that is the case, launch a
            # new transaction here.
            local_txn = Transaction().start(
                current_app.database_name, 0, readonly=True
            )
            self = self.__class__(self.id)
        try:
            return serializer.dumps({'id': self.id, 'password': self.password})
        finally:
            if local_txn is not None:
                Transaction().stop()

    @classmethod
    def unauthorized_handler(cls):
        """
        This is called when the user is required to log in.

        If the request is XHR, then a JSON message with the status code 401
        is sent as response, else a redirect to the login page is returned.
        """
        if request.is_xhr:
            rv = jsonify(message="Bad credentials")
            rv.status_code = 401
            return rv
        return redirect(
            login_url(current_app.login_manager.login_view, request.url)
        )

    def is_authenticated(self):
        """
        Returns True if the user is authenticated, i.e. they have provided
        valid credentials. (Only authenticated users will fulfill the criteria
        of login_required.)
        """
        return bool(self.id)

    def is_active(self):
        return self.active

    def is_anonymous(self):
        return not self.id

    def get_id(self):
        return unicode(self.id)

    @staticmethod
    def _convert_values(values):
        """
        A helper method which looks if the password is specified in the values.
        If it is, then the salt is also made and added

        :param values: A dictionary of field: value pairs
        """
        if 'password' in values and values['password']:
            values['salt'] = ''.join(random.sample(
                string.ascii_letters + string.digits, 8))
            values['password'] += values['salt']

        return values

    @classmethod
    def create(cls, vlist):
        """
        Create, but add salt before saving

        :param vlist: List of dictionary of Values
        """
        vlist = [cls._convert_values(vals.copy()) for vals in vlist]
        return super(NereidUser, cls).create(vlist)

    @classmethod
    def write(cls, nereid_users, values, *args):
        """
        Update salt before saving
        """
        return super(NereidUser, cls).write(
            nereid_users, cls._convert_values(values), *args
        )

    @staticmethod
    def get_gravatar_url(email, **kwargs):
        """
        Return a gravatar url for the given email

        :param email: e-mail of the user
        :param https: To get a secure URL
        :param default: The default image to return if there is no profile pic
                        For example a unisex avatar
        :param size: The size for the image
        """
        if kwargs.get('https', request.scheme == 'https'):
            url = 'https://secure.gravatar.com/avatar/%s?'
        else:
            url = 'http://www.gravatar.com/avatar/%s?'
        url = url % hashlib.md5(email.lower()).hexdigest()

        params = []
        default = kwargs.get('default', None)
        if default:
            params.append(('d', default))

        size = kwargs.get('size', None)
        if size:
            params.append(('s', str(size)))

        return url + urllib.urlencode(params)

    def get_profile_picture(self, **kwargs):
        """
        Return the url to the profile picture of the user.

        The default implementation fetches the profile image of the user from
        gravatar using :meth:`get_gravatar_url`
        """
        return self.get_gravatar_url(self.email, **kwargs)

    @staticmethod
    def aslocaltime(naive_date, local_tz_name=None):
        """
        Returns a localized time using `pytz.astimezone` method.

        :param naive_date: a naive datetime (datetime with no timezone
                           information), which is assumed to be the UTC time.
        :param local_tz_name: The timezone in which the date has to be returned
        :type local_tz_name: string

        :return: A datetime object with local time
        """

        utc_date = pytz.utc.localize(naive_date)

        if not local_tz_name:
            return utc_date

        local_tz = pytz.timezone(local_tz_name)
        if local_tz == pytz.utc:
            return utc_date

        return utc_date.astimezone(local_tz)

    def as_user_local_time(self, naive_date):
        """
        Returns a date localized in the user's timezone.

        :param naive_date: a naive datetime (datetime with no timezone
                           information), which is assumed to be the UTC time.
        """
        return self.aslocaltime(naive_date, self.timezone)

    @classmethod
    @route("/me", methods=["GET", "POST"])
    @login_required
    def profile(cls):
        """
        User profile
        """
        user_form = ProfileForm(obj=request.nereid_user)
        if user_form.validate_on_submit():
            cls.write(
                [request.nereid_user], {
                    'display_name': user_form.display_name.data,
                    'timezone': user_form.timezone.data,
                }
            )
            flash('Your profile has been updated.')

        if request.is_xhr or request.is_json:
            return jsonify(request.nereid_user.serialize())

        return render_template(
            'profile.jinja', user_form=user_form, active_type_name="general"
        )
示例#21
0
class InvoiceLine(metaclass=PoolMeta):
    __name__ = 'account.invoice.line'

    lims_service_party = fields.Function(fields.Many2One(
        'party.party',
        'Party',
        depends=['invoice_type'],
        states={
            'invisible':
            Or(
                Eval('_parent_invoice', {}).get('type') == 'in',
                Eval('invoice_type') == 'in'),
        }),
                                         'get_fraction_field',
                                         searcher='search_fraction_field')
    lims_service_entry = fields.Function(fields.Many2One(
        'lims.entry',
        'Entry',
        depends=['invoice_type'],
        states={
            'invisible':
            Or(
                Eval('_parent_invoice', {}).get('type') == 'in',
                Eval('invoice_type') == 'in'),
        }),
                                         'get_fraction_field',
                                         searcher='search_fraction_field')
    lims_service_sample = fields.Function(fields.Many2One(
        'lims.sample',
        'Sample',
        depends=['invoice_type'],
        states={
            'invisible':
            Or(
                Eval('_parent_invoice', {}).get('type') == 'in',
                Eval('invoice_type') == 'in'),
        }),
                                          'get_fraction_field',
                                          searcher='search_fraction_field')
    lims_service_results_reports = fields.Function(
        fields.Char('Results Reports',
                    depends=['invoice_type'],
                    states={
                        'invisible':
                        Or(
                            Eval('_parent_invoice', {}).get('type') == 'in',
                            Eval('invoice_type') == 'in'),
                    }),
        'get_results_reports',
        searcher='search_results_reports')
    party_domain = fields.Function(
        fields.Many2Many('party.party', None, None, 'Party domain'),
        'get_party_domain')

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls.origin.states['readonly'] = True
        cls.party.domain = [
            'OR', ('id', '=', Eval('party')),
            If(Bool(Eval('party_domain')), ('id', 'in', Eval('party_domain')),
               ('id', '!=', -1))
        ]
        if 'party_domain' not in cls.party.depends:
            cls.party.depends.append('party_domain')
        cls.product.states['readonly'] = Or(
            Eval('invoice_state') != 'draft',
            Bool(Eval('lims_service_sample')))
        cls.product.depends.append('lims_service_sample')

    @classmethod
    def delete(cls, lines):
        if not Transaction().context.get('delete_service', False):
            cls.check_service_invoice(lines)
        super().delete(lines)

    @classmethod
    def check_service_invoice(cls, lines):
        for line in lines:
            if (line.origin and line.origin.__name__ == 'lims.service'
                    and not line.economic_offer):
                raise UserError(
                    gettext('lims_account_invoice.msg_delete_service_invoice',
                            service=line.origin.rec_name))

    @classmethod
    def get_fraction_field(cls, lines, names):
        result = {}
        for name in names:
            result[name] = {}
            for l in lines:
                if l.origin and l.origin.__name__ == 'lims.service':
                    # name[13:]: remove 'lims_service_' from field name
                    field = getattr(l.origin.fraction, name[13:], None)
                    result[name][l.id] = field.id if field else None
                else:
                    result[name][l.id] = None
        return result

    @classmethod
    def search_fraction_field(cls, name, clause):
        return [('origin.fraction.' + name[13:], ) + tuple(clause[1:]) +
                ('lims.service', )]

    def _order_service_field(name):
        def order_field(tables):
            Service = Pool().get('lims.service')
            field = Service._fields[name]
            table, _ = tables[None]
            service_tables = tables.get('service')
            if service_tables is None:
                service = Service.__table__()
                service_tables = {
                    None:
                    (service,
                     (table.origin.like('lims.service,%') &
                      (Service.id.sql_cast(
                          Substring(table.origin,
                                    Position(',', table.origin) + Literal(1)))
                       == service.id))),
                }
                tables['service'] = service_tables
            return field.convert_order(name, service_tables, Service)

        return staticmethod(order_field)

    order_lims_service_entry = _order_service_field('entry')

    @classmethod
    def get_results_reports(cls, lines, name):
        pool = Pool()
        NotebookLine = pool.get('lims.notebook.line')
        result = {}
        for l in lines:
            reports = []
            if l.origin and l.origin.__name__ == 'lims.service':
                notebook_lines = NotebookLine.search([
                    ('service', '=', l.origin.id),
                    ('results_report', '!=', None),
                ],
                                                     limit=1)
                if notebook_lines:
                    reports = [
                        nl.results_report.rec_name for nl in notebook_lines
                    ]
            if reports:
                result[l.id] = ', '.join([r for r in reports])
            else:
                result[l.id] = None
        return result

    @classmethod
    def search_results_reports(cls, name, clause):
        cursor = Transaction().connection.cursor()
        pool = Pool()
        ResultsReport = pool.get('lims.results_report')
        NotebookLine = pool.get('lims.notebook.line')

        value = clause[2]
        cursor.execute(
            'SELECT DISTINCT(nl.service) '
            'FROM "' + ResultsReport._table + '" r '
            'INNER JOIN "' + NotebookLine._table + '" nl '
            'ON nl.results_report = r.id '
            'WHERE r.number ILIKE %s', (value, ))
        services = [x[0] for x in cursor.fetchall()]
        if not services:
            return [('id', '=', -1)]

        services_ids = ['lims.service,' + str(s) for s in services]
        return [('origin', 'in', services_ids)]

    @classmethod
    def _get_origin(cls):
        models = super()._get_origin()
        models.append('lims.service')
        return models

    @fields.depends('origin')
    def get_party_domain(self, name=None):
        pool = Pool()
        Config = pool.get('lims.configuration')

        config_ = Config(1)

        parties = []
        if self.origin and self.origin.__name__ == 'lims.service':
            party = self.origin.party
            parties.append(party.id)
            if config_.invoice_party_relation_type:
                parties.extend([
                    r.to.id for r in party.relations
                    if r.type == config_.invoice_party_relation_type
                ])
        return parties
示例#22
0
class Many2ManyReference(ModelSQL):
    'Many2Many Reference'
    __name__ = 'test.many2many_reference'
    name = fields.Char('Name', required=True)
    targets = fields.Many2Many('test.many2many_reference.relation', 'origin',
                               'target', 'Targets')
示例#23
0
class Expense(Workflow, ModelView, ModelSQL):
    'Account Expense'
    __name__ = 'account.iesa.expense'
    _order_name = 'number'

    company = fields.Many2One(
        'company.company',
        'Company',
        required=True,
        states=_STATES,
        select=True,
        domain=[
            ('id', If(Eval('context', {}).contains('company'), '=',
                      '!='), Eval('context', {}).get('company', -1)),
        ],
        depends=_DEPENDS)
    number = fields.Char('Number', size=None, select=True, required=False)
    reference = fields.Char('Reference',
                            size=None,
                            states=_STATES,
                            depends=_DEPENDS)
    description = fields.Char('Description',
                              size=None,
                              states=_STATES,
                              depends=_DEPENDS,
                              required=True)
    state = fields.Selection(STATES, 'State', readonly=True)
    date = fields.Date('Expense Date',
                       states={
                           'readonly':
                           Eval('state').in_(['posted', 'canceled']),
                           'required':
                           Eval('state').in_(['draft', 'posted'], ),
                       },
                       depends=['state'])
    accounting_date = fields.Date('Accounting Date',
                                  states=_STATES,
                                  depends=_DEPENDS)
    currency = fields.Many2One('currency.currency',
                               'Currency',
                               required=True,
                               states=_STATES,
                               depends=_DEPENDS)
    currency_digits = fields.Function(fields.Integer('Currency Digits'),
                                      'on_change_with_currency_digits')
    currency_date = fields.Function(fields.Date('Currency Date'),
                                    'on_change_with_currency_date')
    journal = fields.Many2One('account.journal',
                              'Journal',
                              required=True,
                              states=_STATES,
                              depends=_DEPENDS,
                              domain=[('type', '=', 'cash')])
    account = fields.Many2One('account.account',
                              'Account',
                              required=False,
                              states=_STATES,
                              depends=_DEPENDS + ['company'],
                              domain=[
                                  ('company', '=', Eval('company', -1)),
                              ])
    move = fields.Many2One('account.move',
                           'Move',
                           readonly=True,
                           domain=[
                               ('company', '=', Eval('company', -1)),
                           ],
                           depends=['company'])
    lines = fields.One2Many('account.iesa.expense.line',
                            'expense',
                            'Expense Lines',
                            required=True,
                            states=_STATES,
                            depends=_DEPENDS + ['company'],
                            domain=[
                                ('company', '=', Eval('company', -1)),
                            ])
    existing_move_lines = fields.Function(
        fields.Many2Many(
            'account.move.line',
            None,
            None,
            'Expense Moves',
            domain=[('company', '=', Eval('company', -1))],
            states=_STATES,
            depends=['state', 'company'],
        ), 'get_moves')
    amount = fields.Numeric(
        'Amount',
        digits=(16, Eval('currency_digits', 2)),
        depends=['currency_digits'],
        required=True,
        states=_STATES,
    )
    comment = fields.Text('Comment', states=_STATES, depends=_DEPENDS)
    ticket = fields.Char('Ticket', states=_STATES, required=True)
    receipt = fields.Char('Receipt')
    party = fields.Many2One(
        'party.party',
        'Party',
        states=_STATES,
        domain=[('company', '=', Eval('context', {}).get('company', -1))],
    )

    @classmethod
    def __setup__(cls):
        super(Expense, cls).__setup__()
        cls._order = [
            ('number', 'DESC'),
            ('id', 'DESC'),
        ]
        cls._error_messages.update({
            'missing_account_receivable': ('Missing Account Revenue.'),
            'missing_account_credit': ('Missing Account Credit.'),
            'amount_can_not_be_zero': ('Amount to Pay can not be zero.'),
            'post_unbalanced_expense':
            ('You can not post expense "%s" because '
             'it is an unbalanced.'),
        })
        cls._transitions |= set((
            ('draft', 'canceled'),
            ('draft', 'quotation'),
            ('quotation', 'posted'),
            ('quotation', 'draft'),
            ('quotation', 'canceled'),
            ('canceled', 'draft'),
        ))
        cls._buttons.update({
            'cancel': {
                'invisible': ~Eval('state').in_(['draft', 'quotation']),
                'icon': 'tryton-cancel',
                'depends': ['state'],
            },
            'draft': {
                'invisible':
                Eval('state').in_(['draft', 'posted', 'canceled']),
                'icon':
                If(
                    Eval('state') == 'canceled', 'tryton-clear',
                    'tryton-go-previous'),
                'depends': ['state'],
            },
            'quote': {
                'invisible': Eval('state') != 'draft',
                'icon': 'tryton-go-next',
                'depends': ['state'],
            },
            'post': {
                'invisible': Eval('state') != 'quotation',
                'icon': 'tryton-ok',
                'depends': ['state'],
            },
        })

    @classmethod
    def search_rec_name(cls, name, clause):
        if clause[1].startswith('!') or clause[1].startswith('not '):
            bool_op = 'AND'
        else:
            bool_op = 'OR'
        return [
            bool_op,
            ('number', ) + tuple(clause[1:]),
            ('description', ) + tuple(clause[1:]),
            ('party', ) + tuple(clause[1:]),
        ]

    def get_rec_name(self, name):
        if self.number:
            return self.number
        elif self.description:
            return '[%s]' % self.description
        return '(%s)' % self.id

    @staticmethod
    def default_state():
        return 'draft'

    @staticmethod
    def default_currency():
        Company = Pool().get('company.company')
        if Transaction().context.get('company'):
            company = Company(Transaction().context['company'])
            return company.currency.id

    @staticmethod
    def default_currency_digits():
        Company = Pool().get('company.company')
        if Transaction().context.get('company'):
            company = Company(Transaction().context['company'])
            return company.currency.digits
        return 2

    @staticmethod
    def default_company():
        return Transaction().context.get('company')

    @classmethod
    def default_date(cls):
        pool = Pool()
        Date = pool.get('ir.date')
        return Date.today()

    @fields.depends('party', 'lines', 'existing_move_lines', 'company',
                    'description')
    def on_change_party(self, name=None):
        pool = Pool()
        Line = pool.get('account.iesa.expense.line')
        MoveLine = pool.get('account.move.line')

        self.lines = []
        self.existing_move_lines = []
        self.invoices = []

        if self.party is not None:
            party = self.party.id
            description = self.description
            lines = []
            line = Line()
            line.party = party
            line.description = description
            line.expense_state = 'draft'
            line.company = self.company.id
            line.amount = 0
            lines.append(line)
            self.lines = lines

    def get_moves(self, name=None):
        moves = []
        return moves

    @fields.depends('lines', 'existing_move_lines')
    def on_change_lines(self, name=None):
        found_invoices = []
        MoveLine = Pool().get('account.move.line')

        parties = []
        if self.lines:
            for line in self.lines:
                if line.party:
                    parties.append(line.party.id)
        if parties is not []:
            found_moves = MoveLine.search([('party', 'in', parties)])
            if found_moves is not None:
                self.existing_move_lines = found_moves

    @fields.depends('currency')
    def on_change_with_currency_digits(self, name=None):
        if self.currency:
            return self.currency.digits
        return 2

    @fields.depends('date')
    def on_change_with_currency_date(self, name=None):
        Date = Pool().get('ir.date')
        return self.date or Date.today()

    @classmethod
    def set_number(cls, expenses):
        '''
        Fill the number field with the expense sequence
        '''
        pool = Pool()
        Sequence = pool.get('ir.sequence')
        Config = pool.get('sale.configuration')

        config = Config(1)
        for expense in expenses:
            if expense.number:
                continue
            expense.number = Sequence.get_id(config.iesa_expense_sequence.id)
        cls.save(expenses)

    @fields.depends('date')
    def on_change_with_currency_date(self, name=None):
        Date = Pool().get('ir.date')
        return self.date or Date.today()

    def get_move(self):

        pool = Pool()
        Move = pool.get('account.move')
        Period = pool.get('account.period')
        MoveLine = pool.get('account.move.line')

        journal = self.journal
        date = self.date
        amount = self.amount
        description = self.number
        origin = self
        lines = []

        credit_line = MoveLine(description=self.description, )
        credit_line.debit, credit_line.credit = 0, self.amount
        credit_line.account = self.account

        if not credit_line.account:
            self.raise_user_error('missing_account_credit')

        lines.append(credit_line)

        for line in self.lines:
            if line.account.party_required:
                new_line = MoveLine(description=line.description,
                                    account=line.account,
                                    party=line.party)
            else:
                new_line = MoveLine(description=line.description,
                                    account=line.account)
            new_line.debit, new_line.credit = line.amount, 0
            lines.append(new_line)

        period_id = Period.find(self.company.id, date=date)

        move = Move(journal=journal,
                    period=period_id,
                    date=date,
                    company=self.company,
                    lines=lines,
                    origin=self,
                    description=description)
        move.save()
        Move.post([move])

        return move

    @classmethod
    @ModelView.button
    @Workflow.transition('canceled')
    def cancel(cls, expenses):
        pass

    @classmethod
    @ModelView.button
    @Workflow.transition('draft')
    def draft(cls, expenses):
        pass

    @classmethod
    @ModelView.button
    @Workflow.transition('quotation')
    def quote(cls, expenses):
        for expense in expenses:
            company = expense.company
            total_amount = expense.amount
            current_amount = 0

            for line in expense.lines:
                current_amount += line.amount
            balance = total_amount - current_amount

            if not company.currency.is_zero(balance):
                cls.raise_user_error('post_unbalanced_expense',
                                     (expense.rec_name, ))
        cls.set_number(expenses)

    @classmethod
    @ModelView.button_action('account_expense.report_iesa_expense')
    @Workflow.transition('posted')
    def post(cls, expenses):
        '''
        Post de expense
        '''

        pool = Pool()
        Move = pool.get('account.move')
        Line = pool.get('account.move.line')
        Period = pool.get('account.period')
        Invoice = pool.get('account.invoice')
        Currency = pool.get('currency.currency')
        Date = pool.get('ir.date')

        for expense in expenses:
            move = None
            move = expense.get_move()
            expense.accounting_date = Date.today()
            expense.move = move
            expense.state = 'posted'
            expense.save()
示例#24
0
class Partner(ModelSQL, ModelView):
    "cooperative_ar"
    __name__ = "cooperative.partner"
    _rec_name = 'party'
    status = fields.Selection([
        ('active', 'Active'),
        ('give_up', 'Give Up'),
    ],
                              'Status',
                              required=True)
    file = fields.Integer('File', required=True)
    party = fields.Many2One('party.party', 'Party', required=True)
    company = fields.Many2One('company.company', 'Company', required=True)
    first_name = fields.Char('First Name', required=True)
    last_name = fields.Char('Last Name', required=True)
    gender = fields.Selection([
        ('male', 'Male'),
        ('female', 'Female'),
        ('other', 'other'),
    ],
                              'Gender',
                              required=True)
    dni = fields.Char('DNI', required=True)
    nationality = fields.Many2One('country.country',
                                  'Nationality',
                                  required=True)
    marital_status = fields.Selection([
        ('soltero/a', 'Soltero/a'),
        ('casado/a', 'Casado/a'),
        ('divorciado/a', 'Divorciado/a'),
        ('viudo/a', 'Viudo/a'),
        ('otra', 'Otra'),
    ],
                                      'Marital Status',
                                      required=True)
    incorporation_date = fields.Date('Incorporation Date', required=True)
    leaving_date = fields.Date(
        'Leaving Date',
        states={
            'readonly': ~Equal(Eval('status'), 'give_up'),
            'required': Equal(Eval('status'), 'give_up'),
        },
    )

    payed_quotes = fields.Numeric('Payed Quotes')
    vacation_days = fields.Integer('Vacation Days')
    vacation = fields.One2Many('cooperative.partner.vacation', 'partner',
                               'Vacation')
    meeting = fields.Many2Many('cooperative.partner-meeting', 'partner',
                               'meeting', 'Meeting')
    sanction = fields.One2Many('cooperative.partner.sanction', 'partner',
                               'Sanction')
    recibo = fields.One2Many('cooperative.partner.recibo', 'partner', 'Recibo')

    marital_status = fields.Selection([
        ('', ''),
        ('soltero/a', 'Soltero/a'),
        ('casado/a', 'Casado/a'),
        ('divorciado/a', 'Divorciado/a'),
        ('viudo/a', 'Viudo/a'),
        ('otra', 'Otra'),
    ], 'Marital Status')

    proposal_letter = fields.Binary('Proposal Letter')
    proof_tax = fields.Binary('Proof of tax registation')

    meeting_date_of_incoroporation = fields.Date(
        'Meeting date of incorporation', required=True)

    categoria_profesional = fields.Char('Categoria Profesional')
    lugar_de_trabajo = fields.Char('Lugar de Trabajo')
    contratista = fields.Boolean('Es Contratista')

    def get_rec_name(self, name):
        """Return Record name"""
        return "%d - %s" % (self.file, self.party.rec_name)

    @classmethod
    def search_rec_name(cls, name, clause):
        if cls.search([('dni', ) + tuple(clause[1:])], limit=1):
            return [('dni', ) + tuple(clause[1:])]
        return [(cls._rec_name, ) + tuple(clause[1:])]

    @staticmethod
    def default_status():
        return 'active'

    @staticmethod
    def default_nationality():
        return Id('country', 'ar').pyson()

    @staticmethod
    def default_company():
        return Transaction().context.get('company')

    @classmethod
    def write(cls, partners, vals):
        if 'file' in vals:
            data = cls.search([('file', '=', vals['file'])])
            if data and data != partners:
                cls.raise_user_error('unique_file')

        return super(Partner, cls).write(partners, vals)

    @classmethod
    def create(cls, vlist):
        for vals in vlist:
            if 'file' in vals:
                data = cls.search([('file', '=', vals['file'])])
                if data:
                    cls.raise_user_error('unique_file')

        return super(Partner, cls).create(vlist)

    @classmethod
    def __setup__(cls):
        super(Partner, cls).__setup__()
        cls._error_messages.update({
            'unique_file': 'The file must be unique.',
        })
示例#25
0
class User:
    __name__ = 'res.user'

    payment_gateways = fields.Many2Many('payment_gateway.gateway-res.user',
                                        'user', 'payment_gateway',
                                        'Payment Gateways')
示例#26
0
class Company:
    "Company"
    __name__ = 'company.company'

    sales_team = fields.Many2Many('company.company-nereid.user-sales',
                                  'company', 'nereid_user', 'Sales Team')
示例#27
0
class Expense(Workflow, ModelView, ModelSQL):
    'Account Expense'
    __name__ = 'account.iesa.expense'
    _order_name = 'number'

    company = fields.Many2One(
        'company.company',
        'Company',
        required=True,
        states=_STATES,
        select=True,
        domain=[
            ('id', If(Eval('context', {}).contains('company'), '=',
                      '!='), Eval('context', {}).get('company', -1)),
        ],
        depends=_DEPENDS)
    number = fields.Char('Number', size=None, select=True, required=False)
    reference = fields.Char('Reference',
                            size=None,
                            states=_STATES,
                            depends=_DEPENDS)
    description = fields.Char('Description',
                              size=None,
                              states=_STATES,
                              depends=_DEPENDS,
                              required=True)
    state = fields.Selection(STATES, 'State', readonly=True)
    date = fields.Date(
        'Expense Date',
        states=_STATES,
        depends=_DEPENDS,
    )
    accounting_date = fields.Date('Accounting Date',
                                  states=_STATES,
                                  depends=_DEPENDS)
    currency = fields.Many2One('currency.currency',
                               'Currency',
                               required=True,
                               states=_STATES,
                               depends=_DEPENDS)
    currency_digits = fields.Function(fields.Integer('Currency Digits'),
                                      'on_change_with_currency_digits')
    currency_date = fields.Function(fields.Date('Currency Date'),
                                    'on_change_with_currency_date')
    journal = fields.Many2One('account.journal',
                              'Journal',
                              required=False,
                              states=_STATES,
                              depends=_DEPENDS,
                              domain=[('type', 'in', ['cash', 'statement'])])
    payment_method = fields.Many2One(
        'account.invoice.payment.method',
        "Payment Method",
        required=True,
        domain=[
            ('company', '=', Eval('company')),
            #('debit_account', '!=', Eval('account')),
            #('credit_account', '!=', Eval('account')),
        ],
        states=_STATES,
        depends=_DEPENDS + ['company', 'account'],
    )
    account = fields.Many2One('account.account',
                              'Account',
                              required=False,
                              states=_STATES,
                              depends=_DEPENDS + ['company'],
                              domain=[
                                  ('company', '=', Eval('company', -1)),
                              ])
    move = fields.Many2One('account.move',
                           'Move',
                           readonly=True,
                           domain=[
                               ('company', '=', Eval('company', -1)),
                           ],
                           depends=['company'])
    cancel_move = fields.Many2One('account.move',
                                  'Asiento Cancelado',
                                  readonly=True,
                                  domain=[
                                      ('company', '=', Eval('company', -1)),
                                  ],
                                  depends=['company'])
    lines = fields.One2Many('account.iesa.expense.line',
                            'expense',
                            'Expense Lines',
                            required=True,
                            states=_STATES,
                            depends=_DEPENDS + ['company'],
                            context={'description': Eval('description')},
                            domain=[
                                ('company', '=', Eval('company', -1)),
                            ])
    existing_move_lines = fields.Function(
        fields.Many2Many(
            'account.move.line',
            None,
            None,
            'Expense Moves',
            domain=[('company', '=', Eval('company', -1))],
            states=_STATES,
            depends=['state', 'company'],
        ), 'get_moves')
    amount = fields.Numeric(
        'Amount',
        digits=(16, Eval('currency_digits', 2)),
        depends=['currency_digits'],
        required=True,
        states=_STATES,
    )
    comment = fields.Text('Comment', states=_STATES, depends=_DEPENDS)
    ticket = fields.Char('Ticket',
                         states=_STATES,
                         depends=_DEPENDS,
                         required=True)
    receipt = fields.Char('Receipt')
    party = fields.Many2One(
        'party.party',
        'Party',
        required=True,
        states=_STATES,
        domain=[
            'AND',
            [('company', '=', Eval('context', {}).get('company', -1))],
            [('is_provider', '=', True)],
        ],
        help='The party that generate the expense',
    )

    @classmethod
    def __setup__(cls):
        super(Expense, cls).__setup__()
        cls._order = [
            ('number', 'DESC'),
            ('id', 'DESC'),
        ]
        cls._error_messages.update({
            'missing_account_receivable': ('Missing Account Revenue.'),
            'missing_account_credit': ('Missing Account Credit.'),
            'amount_can_not_be_zero': ('Amount to Pay can not be zero.'),
            'post_unbalanced_expense':
            ('You can not post expense "%s" because '
             'it is an unbalanced.'),
            'modify_expense': ('You can not modify expense "%s" because '
                               'it is posted or cancelled.'),
            'delete_cancel': ('Expense "%s" must be cancelled before '
                              'deletion.'),
            'delete_numbered': ('The numbered expense "%s" can not be '
                                'deleted.'),
        })
        cls._transitions |= set((
            ('draft', 'canceled'),
            ('draft', 'quotation'),
            ('quotation', 'posted'),
            ('quotation', 'draft'),
            ('quotation', 'canceled'),
            ('canceled', 'draft'),
        ))
        cls._buttons.update({
            'cancel': {
                'invisible': ~Eval('state').in_(['draft', 'quotation']),
                'icon': 'tryton-cancel',
                'depends': ['state'],
            },
            'draft': {
                'invisible':
                Eval('state').in_(['draft', 'posted', 'canceled']),
                'icon':
                If(
                    Eval('state') == 'canceled', 'tryton-clear',
                    'tryton-go-previous'),
                'depends': ['state'],
            },
            'quote': {
                'invisible': Eval('state') != 'draft',
                'icon': 'tryton-go-next',
                'depends': ['state'],
            },
            'post': {
                'invisible': Eval('state') != 'quotation',
                'icon': 'tryton-ok',
                'depends': ['state'],
            },
        })

    @classmethod
    def search(cls,
               domain,
               offset=0,
               limit=None,
               order=None,
               count=False,
               query=False):
        transaction = Transaction().context

        party = transaction.get('party')
        date = transaction.get('date')

        domain = domain[:]
        if party is not None:
            domain = [domain, ('party', '=', party)]
        if date is not None:
            domain = [domain, ('date', '=', date)]

        records = super(Expense, cls).search(domain,
                                             offset=offset,
                                             limit=limit,
                                             order=order,
                                             count=count,
                                             query=query)

        if Transaction().user:
            # Clear the cache as it was not cleaned for confidential
            cache = Transaction().get_cache()
            cache.pop(cls.__name__, None)
        return records

    @classmethod
    def search_rec_name(cls, name, clause):
        if clause[1].startswith('!') or clause[1].startswith('not '):
            bool_op = 'AND'
        else:
            bool_op = 'OR'
        return [
            bool_op,
            ('number', ) + tuple(clause[1:]),
            ('description', ) + tuple(clause[1:]),
            ('party', ) + tuple(clause[1:]),
            ('ticket', ) + tuple(clause[1:]),
        ]

    def get_rec_name(self, name):
        if self.number:
            return self.number
        elif self.description:
            return '[%s]' % self.description
        return '(%s)' % self.id

    @staticmethod
    def default_state():
        return 'draft'

    @staticmethod
    def default_currency():
        Company = Pool().get('company.company')
        if Transaction().context.get('company'):
            company = Company(Transaction().context['company'])
            return company.currency.id

    @staticmethod
    def default_currency_digits():
        Company = Pool().get('company.company')
        if Transaction().context.get('company'):
            company = Company(Transaction().context['company'])
            return company.currency.digits
        return 2

    @staticmethod
    def default_company():
        return Transaction().context.get('company')

    @classmethod
    def default_date(cls):
        pool = Pool()
        Date = pool.get('ir.date')
        return Date.today()

    @staticmethod
    def default_payment_method():
        pool = Pool()
        company_id = Transaction().context.get('company')
        PaymentMethod = pool.get('account.invoice.payment.method')
        payments = PaymentMethod.search([('name', '=', 'Caja'),
                                         ('company', '=', company_id)])
        if len(payments) == 1:
            return payments[0].id

    @fields.depends('party', 'lines', 'existing_move_lines', 'company',
                    'description')
    def on_change_party(self, name=None):
        pool = Pool()
        Line = pool.get('account.iesa.expense.line')
        MoveLine = pool.get('account.move.line')

        self.lines = []
        self.existing_move_lines = []
        self.invoices = []

        if self.party is not None:
            party = self.party.id
            description = self.description
            lines = []
            line = Line()
            line.party = party
            line.description = description
            line.expense_state = 'draft'
            line.company = self.company.id
            line.amount = 0
            lines.append(line)
            self.lines = lines

    def get_moves(self, name=None):
        moves = []
        return moves

    @fields.depends('payment_method', 'account', 'ticket')
    def on_change_payment_method(self, name=None):
        self.account = None
        if self.payment_method:
            self.account = self.payment_method.debit_account.id

    @fields.depends('lines', 'existing_move_lines')
    def on_change_lines(self, name=None):
        found_invoices = []
        MoveLine = Pool().get('account.move.line')

        parties = []
        if self.lines:
            for line in self.lines:
                if line.party:
                    parties.append(line.party.id)
        if parties is not []:
            found_moves = MoveLine.search([('party', 'in', parties)])
            if found_moves is not None:
                self.existing_move_lines = found_moves

    @fields.depends('currency')
    def on_change_with_currency_digits(self, name=None):
        if self.currency:
            return self.currency.digits
        return 2

    @fields.depends('date')
    def on_change_with_currency_date(self, name=None):
        Date = Pool().get('ir.date')
        return self.date or Date.today()

    @classmethod
    def set_number(cls, expenses):
        '''
        Fill the number field with the expense sequence
        '''
        pool = Pool()
        Sequence = pool.get('ir.sequence')
        Config = pool.get('sale.configuration')

        config = Config(1)
        for expense in expenses:
            if expense.number:
                continue
            else:
                expense.number = Sequence.get_id(
                    config.iesa_expense_sequence.id)
            if not expense.ticket:
                expense.ticket = Sequence.get_id(
                    expense.payment_method.sequence.id)
        cls.save(expenses)

    @fields.depends('date')
    def on_change_with_currency_date(self, name=None):
        Date = Pool().get('ir.date')
        return self.date or Date.today()

    def get_move(self):

        pool = Pool()
        Move = pool.get('account.move')
        Period = pool.get('account.period')
        MoveLine = pool.get('account.move.line')

        journal = self.payment_method.journal
        date = self.date
        amount = self.amount
        description = self.number + ' - ' + self.description + ' - ' + self.reference
        ticket = self.ticket
        party = self.party
        origin = self
        lines = []

        credit_line = MoveLine(description=self.description)
        credit_line.debit, credit_line.credit = 0, self.amount
        #credit_line.account = self.account
        credit_line.account = self.payment_method.credit_account

        if not credit_line.account:
            self.raise_user_error('missing_account_credit')

        lines.append(credit_line)

        for line in self.lines:
            if line.account.party_required:
                new_line = MoveLine(description=line.description,
                                    account=line.account,
                                    party=line.party)
            else:
                new_line = MoveLine(description=line.description,
                                    account=line.account)
            new_line.debit, new_line.credit = line.amount, 0
            lines.append(new_line)

        period_id = Period.find(self.company.id, date=date)

        move = Move(journal=journal,
                    period=period_id,
                    date=date,
                    company=self.company,
                    lines=lines,
                    origin=self,
                    description=description,
                    ticket=ticket,
                    party=party)
        move.save()
        Move.post([move])
        return move

    @classmethod
    @ModelView.button
    @Workflow.transition('canceled')
    def cancel(cls, expenses):
        pool = Pool()
        Move = pool.get('account.move')
        Line = pool.get('account.move.line')

        cancel_moves = []
        delete_moves = []
        to_save = []
        for expense in expenses:
            if expense.move:
                if expense.move.state == 'draft':
                    delete_moves.append(expense.move)
                elif not expense.cancel_move:
                    expense.cancel_move = expense.move.cancel()
                    to_save.append(expense)
                    cancel_moves.append(expense.cancel_move)
        if cancel_moves:
            Move.save(cancel_moves)
        cls.save(to_save)
        if delete_moves:
            Move.delete(delete_moves)
        if cancel_moves:
            Move.post(cancel_moves)
        # Write state before reconcile to prevent expense to go to paid state
        cls.write(expenses, {
            'state': 'canceled',
        })
        # Reconcile lines to pay with the cancellation ones if possible
        for expense in expenses:
            if not expense.move or not expense.cancel_move:
                continue
            to_reconcile = []
            for line in expense.move.lines + expense.cancel_move.lines:
                if line.account == expense.account:
                    if line.reconciliation:
                        break
                    to_reconcile.append(line)
            else:
                if to_reconcile:
                    Line.reconcile(to_reconcile)

    @classmethod
    @ModelView.button
    @Workflow.transition('draft')
    def draft(cls, expenses):
        pass

    @classmethod
    @ModelView.button
    @Workflow.transition('quotation')
    def quote(cls, expenses):
        for expense in expenses:
            company = expense.company
            total_amount = expense.amount
            current_amount = 0

            for line in expense.lines:
                current_amount += line.amount
            balance = total_amount - current_amount

            if not company.currency.is_zero(balance):
                cls.raise_user_error('post_unbalanced_expense',
                                     (expense.rec_name, ))
        cls.set_number(expenses)

    @classmethod
    #@ModelView.button_action(
    #    'account_expense.check_iesa_expense')
    @Workflow.transition('posted')
    def post(cls, expenses):
        '''
        Post de expense
        '''

        pool = Pool()
        Move = pool.get('account.move')
        Line = pool.get('account.move.line')
        Period = pool.get('account.period')

        Currency = pool.get('currency.currency')
        Date = pool.get('ir.date')

        expenses_ids = cls.browse([e for e in expenses])
        for expense in expenses_ids:
            move = expense.get_move()
            expense.accounting_date = Date.today()
            moves = []
            if move != expense.move:
                expense.move = move
                moves.append(move)
            if moves:
                Move.save(moves)
            expense.state = 'posted'
        cls.save(expenses_ids)

    @classmethod
    def copy(cls, expenses, default=None):
        if default is None:
            default = {}
        else:
            default = default.copy()
        default.setdefault('state', cls.default_state())
        default.setdefault('number', None)
        #default.setdefault('ticket', None)
        #default.setdefault('reference', None)
        default.setdefault('accounting_date', None)
        default.setdefault('move', None)
        default.setdefault('cancel_move', None)
        return super(Expense, cls).copy(expenses, default=default)

    @classmethod
    def check_modify(cls, expenses):
        '''
        Check if the expenses can be modified
        '''
        for expense in expenses:
            if (expense.state in ('posted', 'cancel')):
                cls.raise_user_error('modify_expense', (expense.rec_name, ))

    @classmethod
    def delete(cls, expenses):
        cls.check_modify(expenses)
        # Cancel before delete
        cls.cancel(expenses)
        ExpenseLine = Pool().get('account.iesa.expense.line')
        for expense in expenses:
            if expense.state != 'canceled':
                cls.raise_user_error('delete_cancel', (expense.rec_name, ))
            if expense.number or expense.ticket:
                cls.raise_user_error('delete_numbered', (expense.rec_name, ))
        ExpenseLine.delete([l for e in expenses for l in e.lines])
        super(Expense, cls).delete(expenses)
示例#28
0
    class OrderLineComponentMixin(ModelStorage):

        line = fields.Many2One(prefix + '.line',
                               "Line",
                               required=True,
                               ondelete='CASCADE',
                               domain=[
                                   ('product.type', '=', 'kit'),
                               ])
        moves = fields.One2Many('stock.move', 'origin', 'Moves', readonly=True)
        moves_ignored = fields.Many2Many(prefix +
                                         '.line.component-ignored-stock.move',
                                         'component',
                                         'move',
                                         "Ignored Moves",
                                         readonly=True)
        moves_recreated = fields.Many2Many(
            prefix + '.line.component-recreated-stock.move',
            'component',
            'move',
            "Recreated Moves",
            readonly=True)
        move_done = fields.Function(fields.Boolean('Moves Done'),
                                    'get_move_done')
        move_exception = fields.Function(fields.Boolean('Moves Exception'),
                                         'get_move_exception')
        quantity_ratio = fields.Float("Quantity Ratio",
                                      readonly=True,
                                      states={
                                          'required': ~Eval('fixed', False),
                                      })
        price_ratio = fields.Numeric("Price Ratio",
                                     readonly=True,
                                     required=True)

        @classmethod
        def __setup__(cls):
            super().__setup__()
            cls.__access__.add('line')

        @fields.depends('line', '_parent_line.product')
        def on_change_with_parent_type(self, name=None):
            if self.line and self.line.product:
                return self.line.product.type

        @classmethod
        def set_price_ratio(cls, components):
            "Set price ratio between components"
            pool = Pool()
            Uom = pool.get('product.uom')
            list_prices = defaultdict(Decimal)
            sum_ = 0
            for component in components:
                product = component.product
                list_price = Uom.compute_price(
                    product.default_uom, product.list_price,
                    component.unit) * Decimal(str(component.quantity))
                list_prices[component] = list_price
                sum_ += list_price

            for component in components:
                if sum_:
                    ratio = list_prices[component] / sum_
                else:
                    ratio = 1 / len(components)
                component.price_ratio = ratio

        def get_move(self, type_):
            raise NotImplementedError

        def _get_shipped_quantity(self, shipment_type):
            'Return the quantity already shipped'
            pool = Pool()
            Uom = pool.get('product.uom')

            quantity = 0
            skips = set(self.moves_recreated)
            for move in self.moves:
                if move not in skips:
                    quantity += Uom.compute_qty(move.uom, move.quantity,
                                                self.unit)
            return quantity

        @property
        def _move_remaining_quantity(self):
            "Compute the remaining quantity to ship"
            pool = Pool()
            Uom = pool.get('product.uom')
            ignored = set(self.moves_ignored)
            quantity = abs(self.quantity)
            for move in self.moves:
                if move.state == 'done' or move in ignored:
                    quantity -= Uom.compute_qty(move.uom, move.quantity,
                                                self.unit)
            return quantity

        def get_move_done(self, name):
            quantity = self._move_remaining_quantity
            if quantity is None:
                return True
            else:
                return self.unit.round(quantity) <= 0

        def get_move_exception(self, name):
            skips = set(self.moves_ignored)
            skips.update(self.moves_recreated)
            for move in self.moves:
                if move.state == 'cancelled' and move not in skips:
                    return True
            return False

        def get_moved_ratio(self):
            pool = Pool()
            Uom = pool.get('product.uom')

            quantity = 0
            for move in self.moves:
                if move.state != 'done':
                    continue
                qty = Uom.compute_qty(move.uom, move.quantity, self.unit)
                dest_type = self.line.to_location.type
                if (move.to_location.type == dest_type
                        and move.from_location.type != dest_type):
                    quantity += qty
                elif (move.from_location.type == dest_type
                      and move.to_location.type != dest_type):
                    quantity -= qty
            if self.quantity < 0:
                quantity *= -1
            return quantity / self.quantity

        def get_rec_name(self, name):
            return super().get_rec_name(name) + (' @ %s' % self.line.rec_name)

        @classmethod
        def search_rec_name(cls, name, clause):
            return super().search_rec_name(name, clause) + [
                ('line.rec_name', ) + tuple(clause[1:]),
            ]
示例#29
0
class CopyMany2ManyReference(ModelSQL):
    "Copy Many2ManyReference"
    __name__ = 'test.copy.many2many_reference'
    name = fields.Char('Name')
    many2many = fields.Many2Many('test.copy.many2many_reference.rel',
                                 'many2many', 'many2many_target', 'Many2Many')
示例#30
0
class Party(ModelSQL, ModelView):
    "Party"
    __name__ = 'party.party'

    name = fields.Char('Name',
                       required=True,
                       select=True,
                       states=STATES,
                       depends=DEPENDS)
    pan = fields.Char('Pan')
    code = fields.Char('Code',
                       required=True,
                       select=True,
                       states={
                           'readonly': Eval('code_readonly', True),
                       },
                       depends=['code_readonly'])
    code_readonly = fields.Function(fields.Boolean('Code Readonly'),
                                    'get_code_readonly')
    lang = fields.Many2One("ir.lang",
                           'Language',
                           states=STATES,
                           depends=DEPENDS)
    vat_number = fields.Char('VAT Number',
                             help="Value Added Tax number",
                             states={
                                 'readonly': ~Eval('active', True),
                                 'required': Bool(Eval('vat_country')),
                             },
                             depends=['active', 'vat_country'])
    vat_country = fields.Selection(
        VAT_COUNTRIES,
        'VAT Country',
        states=STATES,
        depends=DEPENDS,
        help="Setting VAT country will enable validation of the VAT number.",
        translate=False)
    vat_code = fields.Function(fields.Char('VAT Code'),
                               'on_change_with_vat_code',
                               searcher='search_vat_code')
    addresses = fields.One2Many('party.address',
                                'party',
                                'Addresses',
                                states=STATES,
                                depends=DEPENDS)
    contact_mechanisms = fields.One2Many('party.contact_mechanism',
                                         'party',
                                         'Contact Mechanisms',
                                         states=STATES,
                                         depends=DEPENDS)
    categories = fields.Many2Many('party.party-party.category',
                                  'party',
                                  'category',
                                  'Categories',
                                  states=STATES,
                                  depends=DEPENDS)
    active = fields.Boolean('Active', select=True)
    full_name = fields.Function(fields.Char('Full Name'), 'get_full_name')
    phone = fields.Function(fields.Char('Phone'), 'get_mechanism')
    mobile = fields.Function(fields.Char('Mobile'), 'get_mechanism')
    fax = fields.Function(fields.Char('Fax'), 'get_mechanism')
    email = fields.Function(fields.Char('E-Mail'), 'get_mechanism')
    website = fields.Function(fields.Char('Website'), 'get_mechanism')

    @classmethod
    def __setup__(cls):
        super(Party, cls).__setup__()
        cls._sql_constraints = [('code_uniq', 'UNIQUE(code)',
                                 'The code of the party must be unique.')]
        cls._error_messages.update({
            'invalid_vat': ('Invalid VAT number "%(vat)s" on party '
                            '"%(party)s".'),
        })
        cls._order.insert(0, ('name', 'ASC'))

    @staticmethod
    def order_code(tables):
        table, _ = tables[None]
        return [CharLength(table.code), table.code]

    @staticmethod
    def default_active():
        return True

    @staticmethod
    def default_categories():
        return Transaction().context.get('categories', [])

    @staticmethod
    def default_addresses():
        if Transaction().user == 0:
            return []
        Address = Pool().get('party.address')
        fields_names = list(x for x in Address._fields.keys()
                            if x not in ('id', 'create_uid', 'create_date',
                                         'write_uid', 'write_date'))
        return [Address.default_get(fields_names)]

    @staticmethod
    def default_lang():
        Configuration = Pool().get('party.configuration')
        config = Configuration(1)
        if config.party_lang:
            return config.party_lang.id

    @staticmethod
    def default_code_readonly():
        Configuration = Pool().get('party.configuration')
        config = Configuration(1)
        return bool(config.party_sequence)

    def get_code_readonly(self, name):
        return True

    @fields.depends('vat_country', 'vat_number')
    def on_change_with_vat_code(self, name=None):
        return (self.vat_country or '') + (self.vat_number or '')

    @fields.depends('vat_country', 'vat_number')
    def on_change_with_vat_number(self):
        if not self.vat_country:
            return self.vat_number
        code = self.vat_country.lower()
        vat_module = None
        try:
            module = import_module('stdnum.%s' % code)
            vat_module = getattr(module, 'vat', None)
            if not vat_module:
                vat_module = import_module('stdnum.%s.vat' % code)
        except ImportError:
            pass
        if vat_module:
            return vat_module.compact(self.vat_number)
        return self.vat_number

    @classmethod
    def search_vat_code(cls, name, clause):
        res = []
        value = clause[2]
        for country, _ in VAT_COUNTRIES:
            if isinstance(value, basestring) \
                    and country \
                    and value.upper().startswith(country):
                res.append(('vat_country', '=', country))
                value = value[len(country):]
                break
        res.append(('vat_number', clause[1], value))
        return res

    def get_full_name(self, name):
        return self.name

    def get_mechanism(self, name):
        for mechanism in self.contact_mechanisms:
            if mechanism.type == name:
                return mechanism.value
        return ''

    @classmethod
    def create(cls, vlist):
        Sequence = Pool().get('ir.sequence')
        Configuration = Pool().get('party.configuration')

        vlist = [x.copy() for x in vlist]
        for values in vlist:
            if not values.get('code'):
                config = Configuration(1)
                values['code'] = Sequence.get_id(config.party_sequence.id)
            values.setdefault('addresses', None)
        return super(Party, cls).create(vlist)

    @classmethod
    def copy(cls, parties, default=None):
        if default is None:
            default = {}
        default = default.copy()
        default['code'] = None
        return super(Party, cls).copy(parties, default=default)

    @classmethod
    def search_global(cls, text):
        for record, rec_name, icon in super(Party, cls).search_global(text):
            icon = icon or 'tryton-party'
            yield record, rec_name, icon

    @classmethod
    def search_rec_name(cls, name, clause):
        if clause[1].startswith('!') or clause[1].startswith('not '):
            bool_op = 'AND'
        else:
            bool_op = 'OR'
        return [
            bool_op,
            ('code', ) + tuple(clause[1:]),
            ('name', ) + tuple(clause[1:]),
        ]

    def address_get(self, type=None):
        """
        Try to find an address for the given type, if no type matches
        the first address is returned.
        """
        Address = Pool().get("party.address")
        addresses = Address.search([("party", "=", self.id),
                                    ("active", "=", True)],
                                   order=[('sequence', 'ASC'), ('id', 'ASC')])
        if not addresses:
            return None
        default_address = addresses[0]
        if not type:
            return default_address
        for address in addresses:
            if getattr(address, type):
                return address
        return default_address

    @classmethod
    def validate(cls, parties):
        super(Party, cls).validate(parties)
        for party in parties:
            party.check_vat()

    def check_vat(self):
        '''
        Check the VAT number depending of the country.
        http://sima-pc.com/nif.php
        '''
        if not HAS_VATNUMBER:
            return

        if not self.vat_country:
            return

        vat_number = self.on_change_with_vat_number()
        if vat_number != self.vat_number:
            self.vat_number = vat_number
            self.save()

        if not getattr(vatnumber,
                       'check_vat_' + self.vat_country.lower())(vat_number):
            self.raise_user_error('invalid_vat', {
                'vat': vat_number,
                'party': self.rec_name,
            })