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
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
class CopyMany2Many(ModelSQL): "Copy Many2Many" __name__ = 'test.copy.many2many' name = fields.Char('Name') many2many = fields.Many2Many('test.copy.many2many.rel', 'many2many', 'many2many_target', 'Many2Many')
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 []
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
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
class Many2ManySize(ModelSQL): 'Many2Many Size Relation' __name__ = 'test.many2many_size' targets = fields.Many2Many('test.many2many_size.relation', 'origin', 'target', 'Targets', size=3)
class Many2ManyReference(ModelSQL): 'Many2Many Reference' __name__ = 'test.many2many_reference' targets = fields.Many2Many('test.many2many_reference.relation', 'origin', 'target', 'Targets')
class ImportDataMany2Many(ModelSQL): "Import Data Many2Many" __name__ = 'test.import_data.many2many' many2many = fields.Many2Many('test.import_data.many2many.relation', 'many2many', 'target', 'Many2Many')
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()
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()
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)
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__()
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
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)]
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()
class Many2ManyRequired(ModelSQL): 'Many2Many Required' __name__ = 'test.many2many_required' targets = fields.Many2Many('test.many2many_required.relation', 'origin', 'target', 'Targets', required=True)
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
class Many2Many(ModelSQL): 'Many2Many' __name__ = 'test.many2many' targets = fields.Many2Many('test.many2many.relation', 'origin', 'target', 'Targets')
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" )
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
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')
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()
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.', })
class User: __name__ = 'res.user' payment_gateways = fields.Many2Many('payment_gateway.gateway-res.user', 'user', 'payment_gateway', 'Payment Gateways')
class Company: "Company" __name__ = 'company.company' sales_team = fields.Many2Many('company.company-nereid.user-sales', 'company', 'nereid_user', 'Sales Team')
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)
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:]), ]
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')
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, })