class MixedModel(models.Model): _name = 'test_new_api.mixed' number = fields.Float(digits=(10, 2), default=3.14) date = fields.Date() now = fields.Datetime(compute='_compute_now') lang = fields.Selection(string='Language', selection='_get_lang') reference = fields.Reference(string='Related Document', selection='_reference_models') comment1 = fields.Html(sanitize=False) comment2 = fields.Html(sanitize_attributes=True, strip_classes=False) comment3 = fields.Html(sanitize_attributes=True, strip_classes=True) comment4 = fields.Html(sanitize_attributes=True, strip_style=True) currency_id = fields.Many2one( 'res.currency', default=lambda self: self.env.ref('base.EUR')) amount = fields.Monetary() @api.one def _compute_now(self): # this is a non-stored computed field without dependencies self.now = fields.Datetime.now() @api.model def _get_lang(self): return self.env['res.lang'].get_installed() @api.model def _reference_models(self): models = self.env['ir.model'].sudo().search([('state', '!=', 'manual') ]) return [(model.model, model.name) for model in models if not model.model.startswith('ir.')]
class ProductTemplate(models.Model): _inherit = "product.template" website_description = fields.Html( 'Description for the website', sanitize_attributes=False) # hack, if website_sale is not installed quote_description = fields.Html('Description for the quote', sanitize_attributes=False, translate=html_translate)
class MrpProductionMessage(models.Model): _name = "mrp.message" _description = "Production Message" @api.model def _default_valid_until(self): return datetime.today() + relativedelta(days=7) name = fields.Text(compute='_get_note_first_line', store=True) message = fields.Html(required=True) product_tmpl_id = fields.Many2one('product.template', 'Product Template') product_id = fields.Many2one('product.product', string="Product") bom_id = fields.Many2one( 'mrp.bom', 'Bill of Material', domain= "['|', ('product_id', '=', product_id), ('product_tmpl_id.product_variant_ids','=', product_id)]" ) workcenter_id = fields.Many2one('mrp.workcenter', string='Work Center') valid_until = fields.Date('Validity Date', default=_default_valid_until, required=True) routing_id = fields.Many2one('mrp.routing', string='Routing') @api.depends('message') def _get_note_first_line(self): for message in self: message.name = (message.message and html2plaintext(message.message) or "").strip().replace('*', '').split("\n")[0] @api.multi def save(self): """ Used in a wizard-like form view, manual save button when in edit mode """ return True
class ConverterTest(models.Model): _name = 'web_editor.converter.test' # disable translation export for those brilliant field labels and values _translate = False char = fields.Char() integer = fields.Integer() float = fields.Float() numeric = fields.Float(digits=(16, 2)) many2one = fields.Many2one('web_editor.converter.test.sub') binary = fields.Binary() date = fields.Date() datetime = fields.Datetime() selection = fields.Selection([ (1, "réponse A"), (2, "réponse B"), (3, "réponse C"), (4, "réponse <D>"), ]) selection_str = fields.Selection( [ ('A', "Qu'il n'est pas arrivé à Toronto"), ('B', "Qu'il était supposé arriver à Toronto"), ('C', "Qu'est-ce qu'il fout ce maudit pancake, tabernacle ?"), ('D', "La réponse D"), ], string=u"Lorsqu'un pancake prend l'avion à destination de Toronto et " u"qu'il fait une escale technique à St Claude, on dit:") html = fields.Html() text = fields.Text()
class Job(models.Model): _name = 'hr.job' _inherit = ['hr.job', 'website.seo.metadata', 'website.published.mixin'] def _get_default_website_description(self): default_description = self.env["ir.model.data"].xmlid_to_object( "website_hr_recruitment.default_website_description") return (default_description.render() if default_description else "") website_description = fields.Html('Website description', translate=html_translate, sanitize_attributes=False, default=_get_default_website_description) @api.multi def _compute_website_url(self): super(Job, self)._compute_website_url() for job in self: job.website_url = "/jobs/detail/%s" % job.id @api.multi def set_open(self): self.write({'website_published': False}) return super(Job, self).set_open()
class Invite(models.TransientModel): """ Wizard to invite partners (or channels) and make them followers. """ _name = 'mail.wizard.invite' _description = 'Invite wizard' @api.model def default_get(self, fields): result = super(Invite, self).default_get(fields) user_name = self.env.user.name_get()[0][1] model = result.get('res_model') res_id = result.get('res_id') if self._context.get('mail_invite_follower_channel_only'): result['send_mail'] = False if 'message' in fields and model and res_id: model_name = self.env['ir.model']._get(model).display_name document_name = self.env[model].browse(res_id).name_get()[0][1] message = _('<div><p>Hello,</p><p>%s invited you to follow %s document: %s.</p></div>') % (user_name, model_name, document_name) result['message'] = message elif 'message' in fields: result['message'] = _('<div><p>Hello,</p><p>%s invited you to follow a new document.</p></div>') % user_name return result res_model = fields.Char('Related Document Model', required=True, index=True, help='Model of the followed resource') res_id = fields.Integer('Related Document ID', index=True, help='Id of the followed resource') partner_ids = fields.Many2many('res.partner', string='Recipients', help="List of partners that will be added as follower of the current document.") channel_ids = fields.Many2many('mail.channel', string='Channels', help='List of channels that will be added as listeners of the current document.', domain=[('channel_type', '=', 'channel')]) message = fields.Html('Message') send_mail = fields.Boolean('Send Email', default=True, help="If checked, the partners will receive an email warning they have been added in the document's followers.") @api.multi def add_followers(self): email_from = self.env['mail.message']._get_default_from() for wizard in self: Model = self.env[wizard.res_model] document = Model.browse(wizard.res_id) # filter partner_ids to get the new followers, to avoid sending email to already following partners new_partners = wizard.partner_ids - document.message_partner_ids new_channels = wizard.channel_ids - document.message_channel_ids document.message_subscribe(new_partners.ids, new_channels.ids) model_name = self.env['ir.model']._get(wizard.res_model).display_name # send an email if option checked and if a message exists (do not send void emails) if wizard.send_mail and wizard.message and not wizard.message == '<br>': # when deleting the message, cleditor keeps a <br> message = self.env['mail.message'].create({ 'subject': _('Invitation to follow %s: %s') % (model_name, document.name_get()[0][1]), 'body': wizard.message, 'record_name': document.name_get()[0][1], 'email_from': email_from, 'reply_to': email_from, 'model': wizard.res_model, 'res_id': wizard.res_id, 'no_auto_thread': True, }) new_partners.with_context(auto_delete=True)._notify(message, force_send=True, send_after_commit=False, user_signature=True) message.unlink() return {'type': 'ir.actions.act_window_close'}
class SaleQuoteOption(models.Model): _name = "sale.quote.option" _description = "Quotation Option" template_id = fields.Many2one('sale.quote.template', 'Quotation Template Reference', ondelete='cascade', index=True, required=True) name = fields.Text('Description', required=True, translate=True) product_id = fields.Many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], required=True) layout_category_id = fields.Many2one('sale.layout_category', string='Section') website_description = fields.Html('Option Description', translate=html_translate, sanitize_attributes=False) price_unit = fields.Float('Unit Price', required=True, digits=dp.get_precision('Product Price')) discount = fields.Float('Discount (%)', digits=dp.get_precision('Discount')) uom_id = fields.Many2one('product.uom', 'Unit of Measure ', required=True) quantity = fields.Float('Quantity', required=True, digits=dp.get_precision('Product UoS'), default=1) @api.onchange('product_id') def _onchange_product_id(self): if not self.product_id: return product = self.product_id self.price_unit = product.list_price self.website_description = product.product_tmpl_id.quote_description self.name = product.name self.uom_id = product.uom_id domain = { 'uom_id': [('category_id', '=', self.product_id.uom_id.category_id.id)] } return {'domain': domain} @api.onchange('uom_id') def _onchange_product_uom(self): if not self.product_id: return if not self.uom_id: self.price_unit = 0.0 return if self.uom_id.id != self.product_id.uom_id.id: self.price_unit = self.product_id.uom_id._compute_price( self.price_unit, self.uom_id)
class WebsiteResPartner(models.Model): _name = 'res.partner' _inherit = ['res.partner', 'website.seo.metadata', 'website.published.mixin'] website_description = fields.Html('Website Partner Full Description', strip_style=True) website_short_description = fields.Text('Website Partner Short Description') @api.multi def _compute_website_url(self): super(WebsiteResPartner, self)._compute_website_url() for partner in self: partner.website_url = "/partners/%s" % slug(partner)
class SaleQuoteTemplate(models.Model): _name = "sale.quote.template" _description = "Sale Quotation Template" name = fields.Char('Quotation Template', required=True) website_description = fields.Html('Description', translate=html_translate, sanitize_attributes=False) quote_line = fields.One2many('sale.quote.line', 'quote_id', 'Quotation Template Lines', copy=True) note = fields.Text('Terms and conditions') options = fields.One2many('sale.quote.option', 'template_id', 'Optional Products Lines', copy=True) number_of_days = fields.Integer( 'Quotation Duration', help='Number of days for the validity date computation of the quotation' ) require_payment = fields.Selection( [(0, 'Online Signature'), (1, 'Online Payment')], default=0, string='Confirmation Mode', help= "Choose how you want to confirm an order to launch the delivery process. You can either " "request a digital signature or an upfront payment. With a digital signature, you can " "request the payment when issuing the invoice.") mail_template_id = fields.Many2one( 'mail.template', 'Confirmation Mail', domain=[('model', '=', 'sale.order')], help= "This e-mail template will be sent on confirmation. Leave empty to send nothing." ) active = fields.Boolean( default=True, help= "If unchecked, it will allow you to hide the quotation template without removing it." ) @api.multi def open_template(self): self.ensure_one() return { 'type': 'ir.actions.act_url', 'target': 'self', 'url': '/quote/template/%d' % self.id }
class MassMailingList(models.Model): _inherit = 'mail.mass_mailing.list' def _default_popup_content(self): return """<div class="modal-header text-center"> <h3 class="modal-title mt8">izi Presents</h3> </div> <div class="o_popup_message"> <font>7</font> <strong>Business Hacks</strong> <span> to<br/>boost your marketing</span> </div> <p class="o_message_paragraph">Join our Marketing newsletter and get <strong>this white paper instantly</strong></p>""" popup_content = fields.Html(string="Website Popup Content", translate=True, sanitize_attributes=False, default=_default_popup_content) popup_redirect_url = fields.Char(string="Website Popup Redirect URL", default='/')
class ImLivechatChannel(models.Model): _name = 'im_livechat.channel' _inherit = ['im_livechat.channel', 'website.published.mixin'] @api.multi def _compute_website_url(self): super(ImLivechatChannel, self)._compute_website_url() for channel in self: channel.website_url = "/livechat/channel/%s" % (slug(channel), ) website_description = fields.Html( "Website description", default=False, help="Description of the channel displayed on the website page", sanitize_attributes=False, translate=html_translate)
class Documentation(models.Model): _name = 'forum.documentation.toc' _description = 'Documentation ToC' _inherit = ['website.seo.metadata'] _order = "parent_left" _parent_order = "sequence, name" _parent_store = True sequence = fields.Integer('Sequence') name = fields.Char('Name', required=True, translate=True) introduction = fields.Html('Introduction', translate=True) parent_id = fields.Many2one('forum.documentation.toc', string='Parent Table Of Content', ondelete='cascade') child_ids = fields.One2many('forum.documentation.toc', 'parent_id', string='Children Table Of Content') parent_left = fields.Integer(string='Left Parent', index=True) parent_right = fields.Integer(string='Right Parent', index=True) post_ids = fields.One2many('forum.post', 'documentation_toc_id', string='Posts') forum_id = fields.Many2one('forum.forum', string='Forum', required=True) @api.multi def name_get(self): res = [] for record in self: name = record.name if record.parent_id: name = record.parent_id.name + ' / ' + name res.append((record.id, name)) return res @api.constrains('parent_id') def _check_parent_id(self): if not self._check_recursion(): raise ValidationError( _('Error ! You cannot create recursive categories.'))
class SaleOrderLine(models.Model): _inherit = "sale.order.line" _description = "Sales Order Line" website_description = fields.Html('Line Description', sanitize=False, translate=html_translate) option_line_id = fields.One2many('sale.order.option', 'line_id', 'Optional Products Lines') # Take the description on the order template if the product is present in it @api.onchange('product_id') def product_id_change(self): domain = super(SaleOrderLine, self).product_id_change() if self.order_id.template_id: self.name = next( (quote_line.name for quote_line in self.order_id.template_id.quote_line if quote_line.product_id.id == self.product_id.id), self.name) return domain @api.model def create(self, values): values = self._inject_quote_description(values) return super(SaleOrderLine, self).create(values) @api.multi def write(self, values): values = self._inject_quote_description(values) return super(SaleOrderLine, self).write(values) def _inject_quote_description(self, values): values = dict(values or {}) if not values.get('website_description') and values.get('product_id'): product = self.env['product.product'].browse(values['product_id']) values[ 'website_description'] = product.quote_description or product.website_description return values
class test_model(models.Model): _name = 'test_converter.test_model' char = fields.Char() integer = fields.Integer() float = fields.Float() numeric = fields.Float(digits=(16, 2)) many2one = fields.Many2one('test_converter.test_model.sub', group_expand='_gbf_m2o') binary = fields.Binary() date = fields.Date() datetime = fields.Datetime() selection = fields.Selection([ (1, "réponse A"), (2, "réponse B"), (3, "réponse C"), (4, "réponse <D>"), ]) selection_str = fields.Selection( [ ('A', u"Qu'il n'est pas arrivé à Toronto"), ('B', u"Qu'il était supposé arriver à Toronto"), ('C', u"Qu'est-ce qu'il fout ce maudit pancake, tabernacle ?"), ('D', u"La réponse D"), ], string=u"Lorsqu'un pancake prend l'avion à destination de Toronto et " u"qu'il fait une escale technique à St Claude, on dit:") html = fields.Html() text = fields.Text() # `base` module does not contains any model that implement the functionality # `group_expand`; test this feature here... @api.model def _gbf_m2o(self, subs, domain, order): sub_ids = subs._search([], order=order, access_rights_uid=SUPERUSER_ID) return subs.browse(sub_ids)
class MailActivity(models.Model): """ An actual activity to perform. Activities are linked to documents using res_id and res_model_id fields. Activities have a deadline that can be used in kanban view to display a status. Once done activities are unlinked and a message is posted. This message has a new activity_type_id field that indicates the activity linked to the message. """ _name = 'mail.activity' _description = 'Activity' _order = 'date_deadline ASC' _rec_name = 'summary' @api.model def default_get(self, fields): res = super(MailActivity, self).default_get(fields) if not fields or 'res_model_id' in fields and res.get('res_model'): res['res_model_id'] = self.env['ir.model']._get( res['res_model']).id return res # owner res_id = fields.Integer('Related Document ID', index=True, required=True) res_model_id = fields.Many2one('ir.model', 'Related Document Model', index=True, ondelete='cascade', required=True) res_model = fields.Char('Related Document Model', index=True, related='res_model_id.model', store=True, readonly=True) res_name = fields.Char('Document Name', compute='_compute_res_name', store=True, help="Display name of the related document.", readonly=True) # activity activity_type_id = fields.Many2one( 'mail.activity.type', 'Activity', domain= "['|', ('res_model_id', '=', False), ('res_model_id', '=', res_model_id)]" ) activity_category = fields.Selection(related='activity_type_id.category') icon = fields.Char('Icon', related='activity_type_id.icon') summary = fields.Char('Summary') note = fields.Html('Note') feedback = fields.Html('Feedback') date_deadline = fields.Date('Due Date', index=True, required=True, default=fields.Date.context_today) # description user_id = fields.Many2one('res.users', 'Assigned to', default=lambda self: self.env.user, index=True, required=True) state = fields.Selection([('overdue', 'Overdue'), ('today', 'Today'), ('planned', 'Planned')], 'State', compute='_compute_state') recommended_activity_type_id = fields.Many2one( 'mail.activity.type', string="Recommended Activity Type") previous_activity_type_id = fields.Many2one( 'mail.activity.type', string='Previous Activity Type') has_recommended_activities = fields.Boolean( 'Next activities available', compute='_compute_has_recommended_activities', help='Technical field for UX purpose') @api.multi @api.onchange('previous_activity_type_id') def _compute_has_recommended_activities(self): for record in self: record.has_recommended_activities = bool( record.previous_activity_type_id.next_type_ids) @api.depends('res_model', 'res_id') def _compute_res_name(self): for activity in self: activity.res_name = self.env[activity.res_model].browse( activity.res_id).name_get()[0][1] @api.depends('date_deadline') def _compute_state(self): today_default = date.today() for record in self.filtered(lambda activity: activity.date_deadline): today = today_default if record.user_id.tz: today_utc = pytz.UTC.localize(datetime.utcnow()) today_tz = today_utc.astimezone( pytz.timezone(record.user_id.tz)) today = date(year=today_tz.year, month=today_tz.month, day=today_tz.day) date_deadline = fields.Date.from_string(record.date_deadline) diff = (date_deadline - today) if diff.days == 0: record.state = 'today' elif diff.days < 0: record.state = 'overdue' else: record.state = 'planned' @api.onchange('activity_type_id') def _onchange_activity_type_id(self): if self.activity_type_id: self.summary = self.activity_type_id.summary self.date_deadline = (datetime.now() + timedelta(days=self.activity_type_id.days)) @api.onchange('previous_activity_type_id') def _onchange_previous_activity_type_id(self): if self.previous_activity_type_id.next_type_ids: self.recommended_activity_type_id = self.previous_activity_type_id.next_type_ids[ 0] @api.onchange('recommended_activity_type_id') def _onchange_recommended_activity_type_id(self): if self.recommended_activity_type_id: self.activity_type_id = self.recommended_activity_type_id @api.multi def _check_access(self, operation): """ Rule to access activities * create: check write rights on related document; * write: rule OR write rights on document; * unlink: rule OR write rights on document; """ self.check_access_rights( operation, raise_exception=True) # will raise an AccessError if operation in ('write', 'unlink'): try: self.check_access_rule(operation) except exceptions.AccessError: pass else: return doc_operation = 'read' if operation == 'read' else 'write' activity_to_documents = dict() for activity in self.sudo(): activity_to_documents.setdefault(activity.res_model, list()).append(activity.res_id) for model, res_ids in activity_to_documents.items(): self.env[model].check_access_rights(doc_operation, raise_exception=True) try: self.env[model].browse(res_ids).check_access_rule( doc_operation) except exceptions.AccessError: raise exceptions.AccessError( _('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)' ) % (self._description, operation)) @api.model def create(self, values): # already compute default values to be sure those are computed using the current user values_w_defaults = self.default_get(self._fields.keys()) values_w_defaults.update(values) # continue as sudo because activities are somewhat protected activity = super(MailActivity, self.sudo()).create(values_w_defaults) activity_user = activity.sudo(self.env.user) activity_user._check_access('create') self.env[activity_user.res_model].browse( activity_user.res_id).message_subscribe( partner_ids=[activity_user.user_id.partner_id.id]) if activity.date_deadline <= fields.Date.today(): self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', activity.user_id.partner_id.id), { 'type': 'activity_updated', 'activity_created': True }) return activity_user @api.multi def write(self, values): self._check_access('write') if values.get('user_id'): pre_responsibles = self.mapped('user_id.partner_id') res = super(MailActivity, self.sudo()).write(values) if values.get('user_id'): for activity in self: self.env[activity.res_model].browse( activity.res_id).message_subscribe( partner_ids=[activity.user_id.partner_id.id]) if activity.date_deadline <= fields.Date.today(): self.env['bus.bus'].sendone( (self._cr.dbname, 'res.partner', activity.user_id.partner_id.id), { 'type': 'activity_updated', 'activity_created': True }) for activity in self: if activity.date_deadline <= fields.Date.today(): for partner in pre_responsibles: self.env['bus.bus'].sendone( (self._cr.dbname, 'res.partner', partner.id), { 'type': 'activity_updated', 'activity_deleted': True }) return res @api.multi def unlink(self): self._check_access('unlink') for activity in self: if activity.date_deadline <= fields.Date.today(): self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', activity.user_id.partner_id.id), { 'type': 'activity_updated', 'activity_deleted': True }) return super(MailActivity, self.sudo()).unlink() @api.multi def action_done(self): """ Wrapper without feedback because web button add context as parameter, therefore setting context to feedback """ return self.action_feedback() def action_feedback(self, feedback=False): message = self.env['mail.message'] if feedback: self.write(dict(feedback=feedback)) for activity in self: record = self.env[activity.res_model].browse(activity.res_id) record.message_post_with_view( 'mail.message_activity_done', values={'activity': activity}, subtype_id=self.env.ref('mail.mt_activities').id, mail_activity_type_id=activity.activity_type_id.id, ) message |= record.message_ids[0] self.unlink() return message.ids and message.ids[0] or False @api.multi def action_close_dialog(self): return {'type': 'ir.actions.act_window_close'}
class Message(models.Model): """ Messages model: system notification (replacing res.log notifications), comments (OpenChatter discussion) and incoming emails. """ _name = 'mail.message' _description = 'Message' _order = 'id desc' _rec_name = 'record_name' _message_read_limit = 30 @api.model def _get_default_from(self): if self.env.user.email: return formataddr((self.env.user.name, self.env.user.email)) raise UserError(_("Unable to send email, please configure the sender's email address.")) @api.model def _get_default_author(self): return self.env.user.partner_id # content subject = fields.Char('Subject') date = fields.Datetime('Date', default=fields.Datetime.now) body = fields.Html('Contents', default='', sanitize_style=True, strip_classes=True) attachment_ids = fields.Many2many( 'ir.attachment', 'message_attachment_rel', 'message_id', 'attachment_id', string='Attachments', help='Attachments are linked to a document through model / res_id and to the message ' 'through this field.') parent_id = fields.Many2one( 'mail.message', 'Parent Message', index=True, ondelete='set null', help="Initial thread message.") child_ids = fields.One2many('mail.message', 'parent_id', 'Child Messages') # related document model = fields.Char('Related Document Model', index=True) res_id = fields.Integer('Related Document ID', index=True) record_name = fields.Char('Message Record Name', help="Name get of the related document.") # characteristics message_type = fields.Selection([ ('email', 'Email'), ('comment', 'Comment'), ('notification', 'System notification')], 'Type', required=True, default='email', help="Message type: email for email message, notification for system " "message, comment for other messages such as user replies", oldname='type') subtype_id = fields.Many2one('mail.message.subtype', 'Subtype', ondelete='set null', index=True) mail_activity_type_id = fields.Many2one( 'mail.activity.type', 'Mail Activity Type', index=True, ondelete='set null') # origin email_from = fields.Char( 'From', default=_get_default_from, help="Email address of the sender. This field is set when no matching partner is found and replaces the author_id field in the chatter.") author_id = fields.Many2one( 'res.partner', 'Author', index=True, ondelete='set null', default=_get_default_author, help="Author of the message. If not set, email_from may hold an email address that did not match any partner.") author_avatar = fields.Binary("Author's avatar", related='author_id.image_small') # recipients partner_ids = fields.Many2many('res.partner', string='Recipients') needaction_partner_ids = fields.Many2many( 'res.partner', 'mail_message_res_partner_needaction_rel', string='Partners with Need Action') needaction = fields.Boolean( 'Need Action', compute='_get_needaction', search='_search_needaction', help='Need Action') channel_ids = fields.Many2many( 'mail.channel', 'mail_message_mail_channel_rel', string='Channels') # notifications notification_ids = fields.One2many( 'mail.notification', 'mail_message_id', 'Notifications', auto_join=True, copy=False) # user interface starred_partner_ids = fields.Many2many( 'res.partner', 'mail_message_res_partner_starred_rel', string='Favorited By') starred = fields.Boolean( 'Starred', compute='_get_starred', search='_search_starred', help='Current user has a starred notification linked to this message') # tracking tracking_value_ids = fields.One2many( 'mail.tracking.value', 'mail_message_id', string='Tracking values', groups="base.group_no_one", help='Tracked values are stored in a separate model. This field allow to reconstruct ' 'the tracking and to generate statistics on the model.') # mail gateway no_auto_thread = fields.Boolean( 'No threading for answers', help='Answers do not go in the original document discussion thread. This has an impact on the generated message-id.') message_id = fields.Char('Message-Id', help='Message unique identifier', index=True, readonly=1, copy=False) reply_to = fields.Char('Reply-To', help='Reply email address. Setting the reply_to bypasses the automatic thread creation.') mail_server_id = fields.Many2one('ir.mail_server', 'Outgoing mail server') @api.multi def _get_needaction(self): """ Need action on a mail.message = notified on my channel """ my_messages = self.env['mail.notification'].sudo().search([ ('mail_message_id', 'in', self.ids), ('res_partner_id', '=', self.env.user.partner_id.id), ('is_read', '=', False)]).mapped('mail_message_id') for message in self: message.needaction = message in my_messages @api.model def _search_needaction(self, operator, operand): if operator == '=' and operand: return ['&', ('notification_ids.res_partner_id', '=', self.env.user.partner_id.id), ('notification_ids.is_read', '=', False)] return ['&', ('notification_ids.res_partner_id', '=', self.env.user.partner_id.id), ('notification_ids.is_read', '=', True)] @api.depends('starred_partner_ids') def _get_starred(self): """ Compute if the message is starred by the current user. """ # TDE FIXME: use SQL starred = self.sudo().filtered(lambda msg: self.env.user.partner_id in msg.starred_partner_ids) for message in self: message.starred = message in starred @api.model def _search_starred(self, operator, operand): if operator == '=' and operand: return [('starred_partner_ids', 'in', [self.env.user.partner_id.id])] return [('starred_partner_ids', 'not in', [self.env.user.partner_id.id])] #------------------------------------------------------ # Notification API #------------------------------------------------------ @api.model def mark_all_as_read(self, channel_ids=None, domain=None): """ Remove all needactions of the current partner. If channel_ids is given, restrict to messages written in one of those channels. """ partner_id = self.env.user.partner_id.id delete_mode = not self.env.user.share # delete employee notifs, keep customer ones if not domain and delete_mode: query = "DELETE FROM mail_message_res_partner_needaction_rel WHERE res_partner_id IN %s" args = [(partner_id,)] if channel_ids: query += """ AND mail_message_id in (SELECT mail_message_id FROM mail_message_mail_channel_rel WHERE mail_channel_id in %s)""" args += [tuple(channel_ids)] query += " RETURNING mail_message_id as id" self._cr.execute(query, args) self.invalidate_cache() ids = [m['id'] for m in self._cr.dictfetchall()] else: # not really efficient method: it does one db request for the # search, and one for each message in the result set to remove the # current user from the relation. msg_domain = [('needaction_partner_ids', 'in', partner_id)] if channel_ids: msg_domain += [('channel_ids', 'in', channel_ids)] unread_messages = self.search(expression.AND([msg_domain, domain])) notifications = self.env['mail.notification'].sudo().search([ ('mail_message_id', 'in', unread_messages.ids), ('res_partner_id', '=', self.env.user.partner_id.id), ('is_read', '=', False)]) if delete_mode: notifications.unlink() else: notifications.write({'is_read': True}) ids = unread_messages.mapped('id') notification = {'type': 'mark_as_read', 'message_ids': ids, 'channel_ids': channel_ids} self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), notification) return ids @api.multi def mark_as_unread(self, channel_ids=None): """ Add needactions to messages for the current partner. """ partner_id = self.env.user.partner_id.id for message in self: message.write({'needaction_partner_ids': [(4, partner_id)]}) ids = [m.id for m in self] notification = {'type': 'mark_as_unread', 'message_ids': ids, 'channel_ids': channel_ids} self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), notification) @api.multi def set_message_done(self): """ Remove the needaction from messages for the current partner. """ partner_id = self.env.user.partner_id delete_mode = not self.env.user.share # delete employee notifs, keep customer ones notifications = self.env['mail.notification'].sudo().search([ ('mail_message_id', 'in', self.ids), ('res_partner_id', '=', partner_id.id), ('is_read', '=', False)]) if not notifications: return # notifies changes in messages through the bus. To minimize the number of # notifications, we need to group the messages depending on their channel_ids groups = [] messages = notifications.mapped('mail_message_id') current_channel_ids = messages[0].channel_ids current_group = [] for record in messages: if record.channel_ids == current_channel_ids: current_group.append(record.id) else: groups.append((current_group, current_channel_ids)) current_group = [record.id] current_channel_ids = record.channel_ids groups.append((current_group, current_channel_ids)) current_group = [record.id] current_channel_ids = record.channel_ids if delete_mode: notifications.unlink() else: notifications.write({'is_read': True}) for (msg_ids, channel_ids) in groups: notification = {'type': 'mark_as_read', 'message_ids': msg_ids, 'channel_ids': [c.id for c in channel_ids]} self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', partner_id.id), notification) @api.model def unstar_all(self): """ Unstar messages for the current partner. """ partner_id = self.env.user.partner_id.id starred_messages = self.search([('starred_partner_ids', 'in', partner_id)]) starred_messages.write({'starred_partner_ids': [(3, partner_id)]}) ids = [m.id for m in starred_messages] notification = {'type': 'toggle_star', 'message_ids': ids, 'starred': False} self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), notification) @api.multi def toggle_message_starred(self): """ Toggle messages as (un)starred. Technically, the notifications related to uid are set to (un)starred. """ # a user should always be able to star a message he can read self.check_access_rule('read') starred = not self.starred if starred: self.sudo().write({'starred_partner_ids': [(4, self.env.user.partner_id.id)]}) else: self.sudo().write({'starred_partner_ids': [(3, self.env.user.partner_id.id)]}) notification = {'type': 'toggle_star', 'message_ids': [self.id], 'starred': starred} self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), notification) #------------------------------------------------------ # Message loading for web interface #------------------------------------------------------ @api.model def _message_read_dict_postprocess(self, messages, message_tree): """ Post-processing on values given by message_read. This method will handle partners in batch to avoid doing numerous queries. :param list messages: list of message, as get_dict result :param dict message_tree: {[msg.id]: msg browse record as super user} """ # 1. Aggregate partners (author_id and partner_ids), attachments and tracking values partners = self.env['res.partner'].sudo() attachments = self.env['ir.attachment'] message_ids = list(message_tree.keys()) for message in message_tree.values(): if message.author_id: partners |= message.author_id if message.subtype_id and message.partner_ids: # take notified people of message with a subtype partners |= message.partner_ids elif not message.subtype_id and message.partner_ids: # take specified people of message without a subtype (log) partners |= message.partner_ids if message.needaction_partner_ids: # notified partners |= message.needaction_partner_ids if message.attachment_ids: attachments |= message.attachment_ids # Read partners as SUPERUSER -> message being browsed as SUPERUSER it is already the case partners_names = partners.name_get() partner_tree = dict((partner[0], partner) for partner in partners_names) # 2. Attachments as SUPERUSER, because could receive msg and attachments for doc uid cannot see attachments_data = attachments.sudo().read(['id', 'datas_fname', 'name', 'mimetype']) attachments_tree = dict((attachment['id'], { 'id': attachment['id'], 'filename': attachment['datas_fname'], 'name': attachment['name'], 'mimetype': attachment['mimetype'], }) for attachment in attachments_data) # 3. Tracking values tracking_values = self.env['mail.tracking.value'].sudo().search([('mail_message_id', 'in', message_ids)]) message_to_tracking = dict() tracking_tree = dict.fromkeys(tracking_values.ids, False) for tracking in tracking_values: message_to_tracking.setdefault(tracking.mail_message_id.id, list()).append(tracking.id) tracking_tree[tracking.id] = { 'id': tracking.id, 'changed_field': tracking.field_desc, 'old_value': tracking.get_old_display_value()[0], 'new_value': tracking.get_new_display_value()[0], 'field_type': tracking.field_type, } # 4. Update message dictionaries for message_dict in messages: message_id = message_dict.get('id') message = message_tree[message_id] if message.author_id: author = partner_tree[message.author_id.id] else: author = (0, message.email_from) partner_ids = [] if message.subtype_id: partner_ids = [partner_tree[partner.id] for partner in message.partner_ids if partner.id in partner_tree] else: partner_ids = [partner_tree[partner.id] for partner in message.partner_ids if partner.id in partner_tree] customer_email_data = [] for notification in message.notification_ids.filtered(lambda notif: notif.res_partner_id.partner_share and notif.res_partner_id.active): customer_email_data.append((partner_tree[notification.res_partner_id.id][0], partner_tree[notification.res_partner_id.id][1], notification.email_status)) attachment_ids = [] for attachment in message.attachment_ids: if attachment.id in attachments_tree: attachment_ids.append(attachments_tree[attachment.id]) tracking_value_ids = [] for tracking_value_id in message_to_tracking.get(message_id, list()): if tracking_value_id in tracking_tree: tracking_value_ids.append(tracking_tree[tracking_value_id]) message_dict.update({ 'author_id': author, 'partner_ids': partner_ids, 'customer_email_status': (all(d[2] == 'sent' for d in customer_email_data) and 'sent') or (any(d[2] == 'exception' for d in customer_email_data) and 'exception') or (any(d[2] == 'bounce' for d in customer_email_data) and 'bounce') or 'ready', 'customer_email_data': customer_email_data, 'attachment_ids': attachment_ids, 'tracking_value_ids': tracking_value_ids, }) return True @api.model def message_fetch(self, domain, limit=20): return self.search(domain, limit=limit).message_format() @api.multi def message_format(self): """ Get the message values in the format for web client. Since message values can be broadcasted, computed fields MUST NOT BE READ and broadcasted. :returns list(dict). Example : { 'body': HTML content of the message 'model': u'res.partner', 'record_name': u'Agrolait', 'attachment_ids': [ { 'file_type_icon': u'webimage', 'id': 45, 'name': u'sample.png', 'filename': u'sample.png' } ], 'needaction_partner_ids': [], # list of partner ids 'res_id': 7, 'tracking_value_ids': [ { 'old_value': "", 'changed_field': "Customer", 'id': 2965, 'new_value': "Axelor" } ], 'author_id': (3, u'Administrator'), 'email_from': '*****@*****.**' # email address or False 'subtype_id': (1, u'Discussions'), 'channel_ids': [], # list of channel ids 'date': '2015-06-30 08:22:33', 'partner_ids': [[7, "Sacha Du Bourg-Palette"]], # list of partner name_get 'message_type': u'comment', 'id': 59, 'subject': False 'is_note': True # only if the subtype is internal } """ message_values = self.read([ 'id', 'body', 'date', 'author_id', 'email_from', # base message fields 'message_type', 'subtype_id', 'subject', # message specific 'model', 'res_id', 'record_name', # document related 'channel_ids', 'partner_ids', # recipients 'starred_partner_ids', # list of partner ids for whom the message is starred ]) message_tree = dict((m.id, m) for m in self.sudo()) self._message_read_dict_postprocess(message_values, message_tree) # add subtype data (is_note flag, subtype_description). Do it as sudo # because portal / public may have to look for internal subtypes subtype_ids = [msg['subtype_id'][0] for msg in message_values if msg['subtype_id']] subtypes = self.env['mail.message.subtype'].sudo().browse(subtype_ids).read(['internal', 'description']) subtypes_dict = dict((subtype['id'], subtype) for subtype in subtypes) # fetch notification status notif_dict = {} notifs = self.env['mail.notification'].sudo().search([('mail_message_id', 'in', list(mid for mid in message_tree)), ('is_read', '=', False)]) for notif in notifs: mid = notif.mail_message_id.id if not notif_dict.get(mid): notif_dict[mid] = {'partner_id': list()} notif_dict[mid]['partner_id'].append(notif.res_partner_id.id) for message in message_values: message['needaction_partner_ids'] = notif_dict.get(message['id'], dict()).get('partner_id', []) message['is_note'] = message['subtype_id'] and subtypes_dict[message['subtype_id'][0]]['internal'] message['subtype_description'] = message['subtype_id'] and subtypes_dict[message['subtype_id'][0]]['description'] if message['model'] and self.env[message['model']]._original_module: message['module_icon'] = modules.module.get_module_icon(self.env[message['model']]._original_module) return message_values #------------------------------------------------------ # mail_message internals #------------------------------------------------------ @api.model_cr def init(self): self._cr.execute("""SELECT indexname FROM pg_indexes WHERE indexname = 'mail_message_model_res_id_idx'""") if not self._cr.fetchone(): self._cr.execute("""CREATE INDEX mail_message_model_res_id_idx ON mail_message (model, res_id)""") @api.model def _find_allowed_model_wise(self, doc_model, doc_dict): doc_ids = list(doc_dict) allowed_doc_ids = self.env[doc_model].with_context(active_test=False).search([('id', 'in', doc_ids)]).ids return set([message_id for allowed_doc_id in allowed_doc_ids for message_id in doc_dict[allowed_doc_id]]) @api.model def _find_allowed_doc_ids(self, model_ids): IrModelAccess = self.env['ir.model.access'] allowed_ids = set() for doc_model, doc_dict in model_ids.items(): if not IrModelAccess.check(doc_model, 'read', False): continue allowed_ids |= self._find_allowed_model_wise(doc_model, doc_dict) return allowed_ids @api.model def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None): """ Override that adds specific access rights of mail.message, to remove ids uid could not see according to our custom rules. Please refer to check_access_rule for more details about those rules. Non employees users see only message with subtype (aka do not see internal logs). After having received ids of a classic search, keep only: - if author_id == pid, uid is the author, OR - uid belongs to a notified channel, OR - uid is in the specified recipients, OR - uid has a notification on the message, OR - uid have read access to the related document is model, res_id - otherwise: remove the id """ # Rules do not apply to administrator if self._uid == SUPERUSER_ID: return super(Message, self)._search( args, offset=offset, limit=limit, order=order, count=count, access_rights_uid=access_rights_uid) # Non-employee see only messages with a subtype (aka, no internal logs) if not self.env['res.users'].has_group('base.group_user'): args = ['&', '&', ('subtype_id', '!=', False), ('subtype_id.internal', '=', False)] + list(args) # Perform a super with count as False, to have the ids, not a counter ids = super(Message, self)._search( args, offset=offset, limit=limit, order=order, count=False, access_rights_uid=access_rights_uid) if not ids and count: return 0 elif not ids: return ids pid = self.env.user.partner_id.id author_ids, partner_ids, channel_ids, allowed_ids = set([]), set([]), set([]), set([]) model_ids = {} # check read access rights before checking the actual rules on the given ids super(Message, self.sudo(access_rights_uid or self._uid)).check_access_rights('read') self._cr.execute(""" SELECT DISTINCT m.id, m.model, m.res_id, m.author_id, COALESCE(partner_rel.res_partner_id, needaction_rel.res_partner_id), channel_partner.channel_id as channel_id FROM "%s" m LEFT JOIN "mail_message_res_partner_rel" partner_rel ON partner_rel.mail_message_id = m.id AND partner_rel.res_partner_id = %%(pid)s LEFT JOIN "mail_message_res_partner_needaction_rel" needaction_rel ON needaction_rel.mail_message_id = m.id AND needaction_rel.res_partner_id = %%(pid)s LEFT JOIN "mail_message_mail_channel_rel" channel_rel ON channel_rel.mail_message_id = m.id LEFT JOIN "mail_channel" channel ON channel.id = channel_rel.mail_channel_id LEFT JOIN "mail_channel_partner" channel_partner ON channel_partner.channel_id = channel.id AND channel_partner.partner_id = %%(pid)s WHERE m.id = ANY (%%(ids)s)""" % self._table, dict(pid=pid, ids=ids)) for id, rmod, rid, author_id, partner_id, channel_id in self._cr.fetchall(): if author_id == pid: author_ids.add(id) elif partner_id == pid: partner_ids.add(id) elif channel_id: channel_ids.add(id) elif rmod and rid: model_ids.setdefault(rmod, {}).setdefault(rid, set()).add(id) allowed_ids = self._find_allowed_doc_ids(model_ids) final_ids = author_ids | partner_ids | channel_ids | allowed_ids if count: return len(final_ids) else: # re-construct a list based on ids, because set did not keep the original order id_list = [id for id in ids if id in final_ids] return id_list @api.multi def check_access_rule(self, operation): """ Access rules of mail.message: - read: if - author_id == pid, uid is the author OR - uid is in the recipients (partner_ids) OR - uid has been notified (needaction) OR - uid is member of a listern channel (channel_ids.partner_ids) OR - uid have read access to the related document if model, res_id - otherwise: raise - create: if - no model, no res_id (private message) OR - pid in message_follower_ids if model, res_id OR - uid can read the parent OR - uid have write or create access on the related document if model, res_id, OR - otherwise: raise - write: if - author_id == pid, uid is the author, OR - uid is in the recipients (partner_ids) OR - uid has write or create access on the related document if model, res_id - otherwise: raise - unlink: if - uid has write or create access on the related document if model, res_id - otherwise: raise Specific case: non employee users see only messages with subtype (aka do not see internal logs). """ def _generate_model_record_ids(msg_val, msg_ids): """ :param model_record_ids: {'model': {'res_id': (msg_id, msg_id)}, ... } :param message_values: {'msg_id': {'model': .., 'res_id': .., 'author_id': ..}} """ model_record_ids = {} for id in msg_ids: vals = msg_val.get(id, {}) if vals.get('model') and vals.get('res_id'): model_record_ids.setdefault(vals['model'], set()).add(vals['res_id']) return model_record_ids if self._uid == SUPERUSER_ID: return # Non employees see only messages with a subtype (aka, not internal logs) if not self.env['res.users'].has_group('base.group_user'): self._cr.execute('''SELECT DISTINCT message.id, message.subtype_id, subtype.internal FROM "%s" AS message LEFT JOIN "mail_message_subtype" as subtype ON message.subtype_id = subtype.id WHERE message.message_type = %%s AND (message.subtype_id IS NULL OR subtype.internal IS TRUE) AND message.id = ANY (%%s)''' % (self._table), ('comment', self.ids,)) if self._cr.fetchall(): raise AccessError( _('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') % (self._description, operation)) # Read mail_message.ids to have their values message_values = dict((res_id, {}) for res_id in self.ids) if operation in ['read', 'write']: self._cr.execute(""" SELECT DISTINCT m.id, m.model, m.res_id, m.author_id, m.parent_id, COALESCE(partner_rel.res_partner_id, needaction_rel.res_partner_id), channel_partner.channel_id as channel_id FROM "%s" m LEFT JOIN "mail_message_res_partner_rel" partner_rel ON partner_rel.mail_message_id = m.id AND partner_rel.res_partner_id = %%(pid)s LEFT JOIN "mail_message_res_partner_needaction_rel" needaction_rel ON needaction_rel.mail_message_id = m.id AND needaction_rel.res_partner_id = %%(pid)s LEFT JOIN "mail_message_mail_channel_rel" channel_rel ON channel_rel.mail_message_id = m.id LEFT JOIN "mail_channel" channel ON channel.id = channel_rel.mail_channel_id LEFT JOIN "mail_channel_partner" channel_partner ON channel_partner.channel_id = channel.id AND channel_partner.partner_id = %%(pid)s WHERE m.id = ANY (%%(ids)s)""" % self._table, dict(pid=self.env.user.partner_id.id, ids=self.ids)) for mid, rmod, rid, author_id, parent_id, partner_id, channel_id in self._cr.fetchall(): message_values[mid] = { 'model': rmod, 'res_id': rid, 'author_id': author_id, 'parent_id': parent_id, 'notified': any((message_values[mid].get('notified'), partner_id, channel_id)) } else: self._cr.execute("""SELECT DISTINCT id, model, res_id, author_id, parent_id FROM "%s" WHERE id = ANY (%%s)""" % self._table, (self.ids,)) for mid, rmod, rid, author_id, parent_id in self._cr.fetchall(): message_values[mid] = {'model': rmod, 'res_id': rid, 'author_id': author_id, 'parent_id': parent_id} # Author condition (READ, WRITE, CREATE (private)) author_ids = [] if operation == 'read' or operation == 'write': author_ids = [mid for mid, message in message_values.items() if message.get('author_id') and message.get('author_id') == self.env.user.partner_id.id] elif operation == 'create': author_ids = [mid for mid, message in message_values.items() if not message.get('model') and not message.get('res_id')] # Parent condition, for create (check for received notifications for the created message parent) notified_ids = [] if operation == 'create': # TDE: probably clean me parent_ids = [message.get('parent_id') for message in message_values.values() if message.get('parent_id')] self._cr.execute("""SELECT DISTINCT m.id, partner_rel.res_partner_id, channel_partner.partner_id FROM "%s" m LEFT JOIN "mail_message_res_partner_rel" partner_rel ON partner_rel.mail_message_id = m.id AND partner_rel.res_partner_id = (%%s) LEFT JOIN "mail_message_mail_channel_rel" channel_rel ON channel_rel.mail_message_id = m.id LEFT JOIN "mail_channel" channel ON channel.id = channel_rel.mail_channel_id LEFT JOIN "mail_channel_partner" channel_partner ON channel_partner.channel_id = channel.id AND channel_partner.partner_id = (%%s) WHERE m.id = ANY (%%s)""" % self._table, (self.env.user.partner_id.id, self.env.user.partner_id.id, parent_ids,)) not_parent_ids = [mid[0] for mid in self._cr.fetchall() if any([mid[1], mid[2]])] notified_ids += [mid for mid, message in message_values.items() if message.get('parent_id') in not_parent_ids] # Recipients condition, for read and write (partner_ids) and create (message_follower_ids) other_ids = set(self.ids).difference(set(author_ids), set(notified_ids)) model_record_ids = _generate_model_record_ids(message_values, other_ids) if operation in ['read', 'write']: notified_ids = [mid for mid, message in message_values.items() if message.get('notified')] elif operation == 'create': for doc_model, doc_ids in model_record_ids.items(): followers = self.env['mail.followers'].sudo().search([ ('res_model', '=', doc_model), ('res_id', 'in', list(doc_ids)), ('partner_id', '=', self.env.user.partner_id.id), ]) fol_mids = [follower.res_id for follower in followers] notified_ids += [mid for mid, message in message_values.items() if message.get('model') == doc_model and message.get('res_id') in fol_mids] # CRUD: Access rights related to the document other_ids = other_ids.difference(set(notified_ids)) model_record_ids = _generate_model_record_ids(message_values, other_ids) document_related_ids = [] for model, doc_ids in model_record_ids.items(): DocumentModel = self.env[model] mids = DocumentModel.browse(doc_ids).exists() if hasattr(DocumentModel, 'check_mail_message_access'): DocumentModel.check_mail_message_access(mids.ids, operation) # ?? mids ? else: self.env['mail.thread'].check_mail_message_access(mids.ids, operation, model_name=model) document_related_ids += [mid for mid, message in message_values.items() if message.get('model') == model and message.get('res_id') in mids.ids] # Calculate remaining ids: if not void, raise an error other_ids = other_ids.difference(set(document_related_ids)) if not (other_ids and self.browse(other_ids).exists()): return raise AccessError( _('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') % (self._description, operation)) @api.model def _get_record_name(self, values): """ Return the related document name, using name_get. It is done using SUPERUSER_ID, to be sure to have the record name correctly stored. """ model = values.get('model', self.env.context.get('default_model')) res_id = values.get('res_id', self.env.context.get('default_res_id')) if not model or not res_id or model not in self.env: return False return self.env[model].sudo().browse(res_id).name_get()[0][1] @api.model def _get_reply_to(self, values): """ Return a specific reply_to: alias of the document through message_get_reply_to or take the email_from """ model, res_id, email_from = values.get('model', self._context.get('default_model')), values.get('res_id', self._context.get('default_res_id')), values.get('email_from') # ctx values / defualt_get res ? if model and hasattr(self.env[model], 'message_get_reply_to'): # return self.env[model].browse(res_id).message_get_reply_to([res_id], default=email_from)[res_id] return self.env[model].message_get_reply_to([res_id], default=email_from)[res_id] else: # return self.env['mail.thread'].message_get_reply_to(default=email_from)[None] return self.env['mail.thread'].message_get_reply_to([None], default=email_from)[None] @api.model def _get_message_id(self, values): if values.get('no_auto_thread', False) is True: message_id = tools.generate_tracking_message_id('reply_to') elif values.get('res_id') and values.get('model'): message_id = tools.generate_tracking_message_id('%(res_id)s-%(model)s' % values) else: message_id = tools.generate_tracking_message_id('private') return message_id @api.multi def _invalidate_documents(self): """ Invalidate the cache of the documents followed by ``self``. """ for record in self: if record.model and record.res_id: self.env[record.model].invalidate_cache(ids=[record.res_id]) @api.model def create(self, values): # coming from mail.js that does not have pid in its values if self.env.context.get('default_starred'): self = self.with_context({'default_starred_partner_ids': [(4, self.env.user.partner_id.id)]}) if 'email_from' not in values: # needed to compute reply_to values['email_from'] = self._get_default_from() if not values.get('message_id'): values['message_id'] = self._get_message_id(values) if 'reply_to' not in values: values['reply_to'] = self._get_reply_to(values) if 'record_name' not in values and 'default_record_name' not in self.env.context: values['record_name'] = self._get_record_name(values) if 'attachment_ids' not in values: values.setdefault('attachment_ids', []) # extract base64 images if 'body' in values: Attachments = self.env['ir.attachment'] data_to_url = {} def base64_to_boundary(match): key = match.group(2) if not data_to_url.get(key): name = 'image%s' % len(data_to_url) attachment = Attachments.create({ 'name': name, 'datas': match.group(2), 'datas_fname': name, 'res_model': 'mail.message', }) attachment.generate_access_token() values['attachment_ids'].append((4, attachment.id)) data_to_url[key] = ['/web/image/%s?access_token=%s' % (attachment.id, attachment.access_token), name] return '%s%s alt="%s"' % (data_to_url[key][0], match.group(3), data_to_url[key][1]) values['body'] = _image_dataurl.sub(base64_to_boundary, tools.ustr(values['body'])) # delegate creation of tracking after the create as sudo to avoid access rights issues tracking_values_cmd = values.pop('tracking_value_ids', False) message = super(Message, self).create(values) if tracking_values_cmd: message.sudo().write({'tracking_value_ids': tracking_values_cmd}) message._invalidate_documents() if not self.env.context.get('message_create_from_mail_mail'): message._notify(force_send=self.env.context.get('mail_notify_force_send', True), user_signature=self.env.context.get('mail_notify_user_signature', True)) return message @api.multi def read(self, fields=None, load='_classic_read'): """ Override to explicitely call check_access_rule, that is not called by the ORM. It instead directly fetches ir.rules and apply them. """ self.check_access_rule('read') return super(Message, self).read(fields=fields, load=load) @api.multi def write(self, vals): if 'model' in vals or 'res_id' in vals: self._invalidate_documents() res = super(Message, self).write(vals) self._invalidate_documents() return res @api.multi def unlink(self): # cascade-delete attachments that are directly attached to the message (should only happen # for mail.messages that act as parent for a standalone mail.mail record). self.check_access_rule('unlink') self.mapped('attachment_ids').filtered( lambda attach: attach.res_model == self._name and (attach.res_id in self.ids or attach.res_id == 0) ).unlink() self._invalidate_documents() return super(Message, self).unlink() #------------------------------------------------------ # Messaging API #------------------------------------------------------ @api.multi def _notify(self, force_send=False, send_after_commit=True, user_signature=True): """ Compute recipients to notify based on specified recipients and document followers. Delegate notification to partners to send emails and bus notifications and to channels to broadcast messages on channels """ group_user = self.env.ref('base.group_user') # have a sudoed copy to manipulate partners (public can go here with website modules like forum / blog / ... ) self_sudo = self.sudo() self.ensure_one() partners_sudo = self_sudo.partner_ids channels_sudo = self_sudo.channel_ids # all followers of the mail.message document have to be added as partners and notified # and filter to employees only if the subtype is internal if self_sudo.subtype_id and self.model and self.res_id: followers = self_sudo.env['mail.followers'].search([ ('res_model', '=', self.model), ('res_id', '=', self.res_id), ('subtype_ids', 'in', self_sudo.subtype_id.id), ]) if self_sudo.subtype_id.internal: followers = followers.filtered(lambda fol: fol.channel_id or (fol.partner_id.user_ids and group_user in fol.partner_id.user_ids[0].mapped('groups_id'))) channels_sudo |= followers.mapped('channel_id') partners_sudo |= followers.mapped('partner_id') # remove author from notified partners if not self._context.get('mail_notify_author', False) and self_sudo.author_id: partners_sudo = partners_sudo - self_sudo.author_id # update message, with maybe custom values message_values = {} if channels_sudo: message_values['channel_ids'] = [(6, 0, channels_sudo.ids)] if partners_sudo: message_values['needaction_partner_ids'] = [(6, 0, partners_sudo.ids)] if self.model and self.res_id and hasattr(self.env[self.model], 'message_get_message_notify_values'): message_values.update(self.env[self.model].browse(self.res_id).message_get_message_notify_values(self, message_values)) if message_values: self.write(message_values) # notify partners and channels # those methods are called as SUPERUSER because portal users posting messages # have no access to partner model. Maybe propagating a real uid could be necessary. email_channels = channels_sudo.filtered(lambda channel: channel.email_send) notif_partners = partners_sudo.filtered(lambda partner: 'inbox' in partner.mapped('user_ids.notification_type')) if email_channels or partners_sudo - notif_partners: partners_sudo.search([ '|', ('id', 'in', (partners_sudo - notif_partners).ids), ('channel_ids', 'in', email_channels.ids), ('email', '!=', self_sudo.author_id.email or self_sudo.email_from), ])._notify(self, force_send=force_send, send_after_commit=send_after_commit, user_signature=user_signature) notif_partners._notify_by_chat(self) channels_sudo._notify(self) # Discard cache, because child / parent allow reading and therefore # change access rights. if self.parent_id: self.parent_id.invalidate_cache() return True
class Note(models.Model): _name = 'note.note' _inherit = ['mail.thread', 'mail.activity.mixin'] _description = "Note" _order = 'sequence' def _get_default_stage_id(self): return self.env['note.stage'].search([('user_id', '=', self.env.uid)], limit=1) name = fields.Text(compute='_compute_name', string='Note Summary', store=True) user_id = fields.Many2one('res.users', string='Owner', default=lambda self: self.env.uid) memo = fields.Html('Note Content') sequence = fields.Integer('Sequence') stage_id = fields.Many2one('note.stage', compute='_compute_stage_id', inverse='_inverse_stage_id', string='Stage') stage_ids = fields.Many2many('note.stage', 'note_stage_rel', 'note_id', 'stage_id', string='Stages of Users', default=_get_default_stage_id) open = fields.Boolean(string='Active', default=True) date_done = fields.Date('Date done') color = fields.Integer(string='Color Index') tag_ids = fields.Many2many('note.tag', 'note_tags_rel', 'note_id', 'tag_id', string='Tags') @api.depends('memo') def _compute_name(self): """ Read the first line of the memo to determine the note name """ for note in self: text = html2plaintext(note.memo) if note.memo else '' note.name = text.strip().replace('*', '').split("\n")[0] @api.multi def _compute_stage_id(self): for note in self: for stage in note.stage_ids.filtered( lambda stage: stage.user_id == self.env.user): note.stage_id = stage @api.multi def _inverse_stage_id(self): for note in self.filtered('stage_id'): note.stage_ids = note.stage_id + note.stage_ids.filtered( lambda stage: stage.user_id != self.env.user) @api.model def name_create(self, name): return self.create({'memo': name}).name_get()[0] @api.model def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True): if groupby and groupby[0] == "stage_id": stages = self.env['note.stage'].search([('user_id', '=', self.env.uid)]) if stages: # if the user has some stages result = [{ # notes by stage for stages user '__context': {'group_by': groupby[1:]}, '__domain': domain + [('stage_ids.id', '=', stage.id)], 'stage_id': (stage.id, stage.name), 'stage_id_count': self.search_count(domain + [('stage_ids', '=', stage.id)]), '__fold': stage.fold, } for stage in stages] # note without user's stage nb_notes_ws = self.search_count(domain + [('stage_ids', 'not in', stages.ids)]) if nb_notes_ws: # add note to the first column if it's the first stage dom_not_in = ('stage_ids', 'not in', stages.ids) if result and result[0]['stage_id'][0] == stages[0].id: dom_in = result[0]['__domain'].pop() result[0]['__domain'] = domain + [ '|', dom_in, dom_not_in ] result[0]['stage_id_count'] += nb_notes_ws else: # add the first stage column result = [{ '__context': { 'group_by': groupby[1:] }, '__domain': domain + [dom_not_in], 'stage_id': (stages[0].id, stages[0].name), 'stage_id_count': nb_notes_ws, '__fold': stages[0].name, }] + result else: # if stage_ids is empty, get note without user's stage nb_notes_ws = self.search_count(domain) if nb_notes_ws: result = [{ # notes for unknown stage '__context': { 'group_by': groupby[1:] }, '__domain': domain, 'stage_id': False, 'stage_id_count': nb_notes_ws }] else: result = [] return result return super(Note, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy) @api.multi def action_close(self): return self.write({'open': False, 'date_done': fields.date.today()}) @api.multi def action_open(self): return self.write({'open': True})
class SaleOrder(models.Model): _inherit = 'sale.order' def _get_default_template(self): template = self.env.ref('website_quote.website_quote_template_default', raise_if_not_found=False) return template and template.active and template or False def _get_default_online_payment(self): default_template = self._get_default_template() if self.template_id: return self.template_id.require_payment elif default_template: return default_template.require_payment elif self.env['ir.config_parameter'].sudo().get_param( 'sale.sale_portal_confirmation_options', default='none') == 'pay': return 1 else: return 0 template_id = fields.Many2one('sale.quote.template', 'Quotation Template', readonly=True, states={ 'draft': [('readonly', False)], 'sent': [('readonly', False)] }, default=_get_default_template) website_description = fields.Html('Description', sanitize_attributes=False, translate=html_translate) options = fields.One2many('sale.order.option', 'order_id', 'Optional Products Lines', copy=True, readonly=True, states={ 'draft': [('readonly', False)], 'sent': [('readonly', False)] }) amount_undiscounted = fields.Float('Amount Before Discount', compute='_compute_amount_undiscounted', digits=0) quote_viewed = fields.Boolean('Quotation Viewed') require_payment = fields.Selection( [(0, 'Online Signature'), (1, 'Online Payment')], default=_get_default_online_payment, string='Confirmation Mode', help= "Choose how you want to confirm an order to launch the delivery process. You can either " "request a digital signature or an upfront payment. With a digital signature, you can " "request the payment when issuing the invoice.") @api.multi def copy(self, default=None): if self.template_id and self.template_id.number_of_days > 0: default = dict(default or {}) default['validity_date'] = fields.Date.to_string( datetime.now() + timedelta(self.template_id.number_of_days)) return super(SaleOrder, self).copy(default=default) @api.one def _compute_amount_undiscounted(self): total = 0.0 for line in self.order_line: total += line.price_subtotal + line.price_unit * ( (line.discount or 0.0) / 100.0 ) * line.product_uom_qty # why is there a discount in a field named amount_undiscounted ?? self.amount_undiscounted = total @api.onchange('partner_id') def onchange_partner_id(self): super(SaleOrder, self).onchange_partner_id() self.note = self.template_id.note or self.note @api.onchange('partner_id') def onchange_update_description_lang(self): if not self.template_id: return else: template = self.template_id.with_context(lang=self.partner_id.lang) self.website_description = template.website_description @api.onchange('template_id') def onchange_template_id(self): if not self.template_id: return template = self.template_id.with_context(lang=self.partner_id.lang) order_lines = [(5, 0, 0)] for line in template.quote_line: discount = 0 if self.pricelist_id: price = self.pricelist_id.with_context( uom=line.product_uom_id.id).get_product_price( line.product_id, 1, False) if self.pricelist_id.discount_policy == 'without_discount' and line.price_unit: discount = (line.price_unit - price) / line.price_unit * 100 price = line.price_unit else: price = line.price_unit data = { 'name': line.name, 'price_unit': price, 'discount': 100 - ((100 - discount) * (100 - line.discount) / 100), 'product_uom_qty': line.product_uom_qty, 'product_id': line.product_id.id, 'layout_category_id': line.layout_category_id, 'product_uom': line.product_uom_id.id, 'website_description': line.website_description, 'state': 'draft', 'customer_lead': self._get_customer_lead(line.product_id.product_tmpl_id), } if self.pricelist_id: data.update(self.env['sale.order.line']._get_purchase_price( self.pricelist_id, line.product_id, line.product_uom_id, fields.Date.context_today(self))) order_lines.append((0, 0, data)) self.order_line = order_lines self.order_line._compute_tax_id() option_lines = [] for option in template.options: if self.pricelist_id: price = self.pricelist_id.with_context( uom=option.uom_id.id).get_product_price( option.product_id, 1, False) else: price = option.price_unit data = { 'product_id': option.product_id.id, 'layout_category_id': option.layout_category_id, 'name': option.name, 'quantity': option.quantity, 'uom_id': option.uom_id.id, 'price_unit': price, 'discount': option.discount, 'website_description': option.website_description, } option_lines.append((0, 0, data)) self.options = option_lines if template.number_of_days > 0: self.validity_date = fields.Date.to_string( datetime.now() + timedelta(template.number_of_days)) self.website_description = template.website_description self.require_payment = template.require_payment if template.note: self.note = template.note @api.multi def open_quotation(self): self.ensure_one() self.write({'quote_viewed': True}) return { 'type': 'ir.actions.act_url', 'target': 'self', 'url': '/quote/%s/%s' % (self.id, self.access_token) } @api.multi def get_access_action(self, access_uid=None): """ Instead of the classic form view, redirect to the online quote if it exists. """ self.ensure_one() user = access_uid and self.env['res.users'].sudo().browse( access_uid) or self.env.user if not self.template_id or (not user.share and not self.env.context.get('force_website')): return super(SaleOrder, self).get_access_action(access_uid) return { 'type': 'ir.actions.act_url', 'url': '/quote/%s/%s' % (self.id, self.access_token), 'target': 'self', 'res_id': self.id, } def get_mail_url(self): self.ensure_one() if self.state not in ['sale', 'done']: auth_param = url_encode( self.partner_id.signup_get_auth_param()[self.partner_id.id]) return '/quote/%s/%s?' % (self.id, self.access_token) + auth_param return super(SaleOrder, self).get_mail_url() def get_portal_confirmation_action(self): """ Template override default behavior of pay / sign chosen in sales settings """ if self.require_payment is not None or self.require_payment is not False: return 'pay' if self.require_payment == 1 else 'sign' return super(SaleOrder, self).get_portal_confirmation_action() @api.multi def action_confirm(self): res = super(SaleOrder, self).action_confirm() for order in self: if order.template_id and order.template_id.mail_template_id: self.template_id.mail_template_id.send_mail(order.id) return res @api.multi def _get_payment_type(self): self.ensure_one() return 'form_save' if self.require_payment else 'form'
class SaleOrderOption(models.Model): _name = "sale.order.option" _description = "Sale Options" _order = 'sequence, id' order_id = fields.Many2one('sale.order', 'Sales Order Reference', ondelete='cascade', index=True) line_id = fields.Many2one('sale.order.line', on_delete="set null") name = fields.Text('Description', required=True) product_id = fields.Many2one('product.product', 'Product', domain=[('sale_ok', '=', True)]) layout_category_id = fields.Many2one('sale.layout_category', string='Section') website_description = fields.Html('Line Description', sanitize_attributes=False, translate=html_translate) price_unit = fields.Float('Unit Price', required=True, digits=dp.get_precision('Product Price')) discount = fields.Float('Discount (%)', digits=dp.get_precision('Discount')) uom_id = fields.Many2one('product.uom', 'Unit of Measure ', required=True) quantity = fields.Float('Quantity', required=True, digits=dp.get_precision('Product UoS'), default=1) sequence = fields.Integer( 'Sequence', help= "Gives the sequence order when displaying a list of suggested product." ) @api.onchange('product_id', 'uom_id') def _onchange_product_id(self): if not self.product_id: return product = self.product_id.with_context( lang=self.order_id.partner_id.lang) self.price_unit = product.list_price self.website_description = product.quote_description or product.website_description self.name = product.name if product.description_sale: self.name += '\n' + product.description_sale self.uom_id = self.uom_id or product.uom_id pricelist = self.order_id.pricelist_id if pricelist and product: partner_id = self.order_id.partner_id.id self.price_unit = pricelist.with_context( uom=self.uom_id.id).get_product_price(product, self.quantity, partner_id) domain = { 'uom_id': [('category_id', '=', self.product_id.uom_id.category_id.id)] } return {'domain': domain} @api.multi def button_add_to_order(self): self.ensure_one() order = self.order_id if order.state not in ['draft', 'sent']: return False order_line = order.order_line.filtered( lambda line: line.product_id == self.product_id) if order_line: order_line = order_line[0] order_line.product_uom_qty += 1 else: vals = { 'price_unit': self.price_unit, 'website_description': self.website_description, 'name': self.name, 'order_id': order.id, 'product_id': self.product_id.id, 'layout_category_id': self.layout_category_id.id, 'product_uom_qty': self.quantity, 'product_uom': self.uom_id.id, 'discount': self.discount, } order_line = self.env['sale.order.line'].create(vals) order_line._compute_tax_id() self.write({'line_id': order_line.id}) return {'type': 'ir.actions.client', 'tag': 'reload'}
class MailTemplate(models.Model): "Templates for sending email" _name = "mail.template" _description = 'Email Templates' _order = 'name' @api.model def default_get(self, fields): res = super(MailTemplate, self).default_get(fields) if res.get('model'): res['model_id'] = self.env['ir.model']._get(res.pop('model')).id return res name = fields.Char('Name') model_id = fields.Many2one('ir.model', 'Applies to', help="The type of document this template can be used with") model = fields.Char('Related Document Model', related='model_id.model', index=True, store=True, readonly=True) lang = fields.Char('Language', help="Optional translation language (ISO code) to select when sending out an email. " "If not set, the english version will be used. " "This should usually be a placeholder expression " "that provides the appropriate language, e.g. " "${object.partner_id.lang}.", placeholder="${object.partner_id.lang}") user_signature = fields.Boolean('Add Signature', help="If checked, the user's signature will be appended to the text version " "of the message") subject = fields.Char('Subject', translate=True, help="Subject (placeholders may be used here)") email_from = fields.Char('From', help="Sender address (placeholders may be used here). If not set, the default " "value will be the author's email alias if configured, or email address.") use_default_to = fields.Boolean( 'Default recipients', help="Default recipients of the record:\n" "- partner (using id on a partner or the partner_id field) OR\n" "- email (using email_from or email field)") email_to = fields.Char('To (Emails)', help="Comma-separated recipient addresses (placeholders may be used here)") partner_to = fields.Char('To (Partners)', oldname='email_recipients', help="Comma-separated ids of recipient partners (placeholders may be used here)") email_cc = fields.Char('Cc', help="Carbon copy recipients (placeholders may be used here)") reply_to = fields.Char('Reply-To', help="Preferred response address (placeholders may be used here)") mail_server_id = fields.Many2one('ir.mail_server', 'Outgoing Mail Server', readonly=False, help="Optional preferred server for outgoing mails. If not set, the highest " "priority one will be used.") body_html = fields.Html('Body', translate=True, sanitize=False) report_name = fields.Char('Report Filename', translate=True, help="Name to use for the generated report file (may contain placeholders)\n" "The extension can be omitted and will then come from the report type.") report_template = fields.Many2one('ir.actions.report', 'Optional report to print and attach') ref_ir_act_window = fields.Many2one('ir.actions.act_window', 'Sidebar action', readonly=True, copy=False, help="Sidebar action to make this template available on records " "of the related document model") attachment_ids = fields.Many2many('ir.attachment', 'email_template_attachment_rel', 'email_template_id', 'attachment_id', 'Attachments', help="You may attach files to this template, to be added to all " "emails created from this template") auto_delete = fields.Boolean('Auto Delete', default=True, help="Permanently delete this email after sending it, to save space") # Fake fields used to implement the placeholder assistant model_object_field = fields.Many2one('ir.model.fields', string="Field", help="Select target field from the related document model.\n" "If it is a relationship field you will be able to select " "a target field at the destination of the relationship.") sub_object = fields.Many2one('ir.model', 'Sub-model', readonly=True, help="When a relationship field is selected as first field, " "this field shows the document model the relationship goes to.") sub_model_object_field = fields.Many2one('ir.model.fields', 'Sub-field', help="When a relationship field is selected as first field, " "this field lets you select the target field within the " "destination document model (sub-model).") null_value = fields.Char('Default Value', help="Optional value to use if the target field is empty") copyvalue = fields.Char('Placeholder Expression', help="Final placeholder expression, to be copy-pasted in the desired template field.") scheduled_date = fields.Char('Scheduled Date', help="If set, the queue manager will send the email after the date. If not set, the email will be send as soon as possible. Jinja2 placeholders may be used.") @api.onchange('model_id') def onchange_model_id(self): # TDE CLEANME: should'nt it be a stored related ? if self.model_id: self.model = self.model_id.model else: self.model = False def build_expression(self, field_name, sub_field_name, null_value): """Returns a placeholder expression for use in a template field, based on the values provided in the placeholder assistant. :param field_name: main field name :param sub_field_name: sub field name (M2O) :param null_value: default value if the target value is empty :return: final placeholder expression """ expression = '' if field_name: expression = "${object." + field_name if sub_field_name: expression += "." + sub_field_name if null_value: expression += " or '''%s'''" % null_value expression += "}" return expression @api.onchange('model_object_field', 'sub_model_object_field', 'null_value') def onchange_sub_model_object_value_field(self): if self.model_object_field: if self.model_object_field.ttype in ['many2one', 'one2many', 'many2many']: model = self.env['ir.model']._get(self.model_object_field.relation) if model: self.sub_object = model.id self.copyvalue = self.build_expression(self.model_object_field.name, self.sub_model_object_field and self.sub_model_object_field.name or False, self.null_value or False) else: self.sub_object = False self.sub_model_object_field = False self.copyvalue = self.build_expression(self.model_object_field.name, False, self.null_value or False) else: self.sub_object = False self.copyvalue = False self.sub_model_object_field = False self.null_value = False @api.multi def unlink(self): self.unlink_action() return super(MailTemplate, self).unlink() @api.multi def copy(self, default=None): default = dict(default or {}, name=_("%s (copy)") % self.name) return super(MailTemplate, self).copy(default=default) @api.multi def unlink_action(self): for template in self: if template.ref_ir_act_window: template.ref_ir_act_window.sudo().unlink() return True @api.multi def create_action(self): ActWindowSudo = self.env['ir.actions.act_window'].sudo() view = self.env.ref('mail.email_compose_message_wizard_form') for template in self: button_name = _('Send Mail (%s)') % template.name action = ActWindowSudo.create({ 'name': button_name, 'type': 'ir.actions.act_window', 'res_model': 'mail.compose.message', 'src_model': template.model_id.model, 'view_type': 'form', 'context': "{'default_composition_mode': 'mass_mail', 'default_template_id' : %d, 'default_use_template': True}" % (template.id), 'view_mode': 'form,tree', 'view_id': view.id, 'target': 'new', 'binding_model_id': template.model_id.id, }) template.write({'ref_ir_act_window': action.id}) return True # ---------------------------------------- # RENDERING # ---------------------------------------- @api.model def _replace_local_links(self, html): """ Post-processing of html content to replace local links to absolute links, using web.base.url as base url. """ if not html: return html # form a tree root = lxml.html.fromstring(html) if not len(root) and root.text is None and root.tail is None: html = u'<div>%s</div>' % html root = lxml.html.fromstring(html, encoding='unicode') base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') base = urls.url_parse(base_url) def _process_link(url): new_url = urls.url_parse(url) if new_url.scheme and (new_url.netloc or new_url.scheme == 'mailto'): return url return new_url.replace(scheme=base.scheme, netloc=base.netloc).to_url() # check all nodes, replace : # - img src -> check URL # - a href -> check URL for node in root.iter(): if node.tag == 'a' and node.get('href'): node.set('href', _process_link(node.get('href'))) elif node.tag == 'img' and not node.get('src', 'data').startswith(u'data'): node.set('src', _process_link(node.get('src'))) html = lxml.html.tostring(root, pretty_print=False, method='html', encoding='unicode') # this is ugly, but lxml/etree tostring want to put everything in a 'div' that breaks the editor -> remove that if html.startswith(u'<div>') and html.endswith(u'</div>'): html = html[5:-6] return html @api.model def render_post_process(self, html): html = self._replace_local_links(html) return html @api.model def render_template(self, template_txt, model, res_ids, post_process=False): """ Render the given template text, replace mako expressions ``${expr}`` with the result of evaluating these expressions with an evaluation context containing: - ``user``: Model of the current user - ``object``: record of the document record this mail is related to - ``context``: the context passed to the mail composition wizard :param str template_txt: the template text to render :param str model: model name of the document record this mail is related to. :param int res_ids: list of ids of document records those mails are related to. """ multi_mode = True if isinstance(res_ids, pycompat.integer_types): multi_mode = False res_ids = [res_ids] results = dict.fromkeys(res_ids, u"") # try to load the template try: mako_env = mako_safe_template_env if self.env.context.get('safe') else mako_template_env template = mako_env.from_string(tools.ustr(template_txt)) except Exception: _logger.info("Failed to load template %r", template_txt, exc_info=True) return multi_mode and results or results[res_ids[0]] # prepare template variables records = self.env[model].browse(it for it in res_ids if it) # filter to avoid browsing [None] res_to_rec = dict.fromkeys(res_ids, None) for record in records: res_to_rec[record.id] = record variables = { 'format_date': lambda date, format=False, context=self._context: format_date(self.env, date, format), 'format_tz': lambda dt, tz=False, format=False, context=self._context: format_tz(self.env, dt, tz, format), 'format_amount': lambda amount, currency, context=self._context: format_amount(self.env, amount, currency), 'user': self.env.user, 'ctx': self._context, # context kw would clash with mako internals } for res_id, record in res_to_rec.items(): variables['object'] = record try: render_result = template.render(variables) except Exception: _logger.info("Failed to render template %r using values %r" % (template, variables), exc_info=True) raise UserError(_("Failed to render template %r using values %r")% (template, variables)) if render_result == u"False": render_result = u"" results[res_id] = render_result if post_process: for res_id, result in results.items(): results[res_id] = self.render_post_process(result) return multi_mode and results or results[res_ids[0]] @api.multi def get_email_template(self, res_ids): multi_mode = True if isinstance(res_ids, pycompat.integer_types): res_ids = [res_ids] multi_mode = False if res_ids is None: res_ids = [None] results = dict.fromkeys(res_ids, False) if not self.ids: return results self.ensure_one() langs = self.render_template(self.lang, self.model, res_ids) for res_id, lang in langs.items(): if lang: template = self.with_context(lang=lang) else: template = self results[res_id] = template return multi_mode and results or results[res_ids[0]] @api.multi def generate_recipients(self, results, res_ids): """Generates the recipients of the template. Default values can ben generated instead of the template values if requested by template or context. Emails (email_to, email_cc) can be transformed into partners if requested in the context. """ self.ensure_one() if self.use_default_to or self._context.get('tpl_force_default_to'): default_recipients = self.env['mail.thread'].message_get_default_recipients(res_model=self.model, res_ids=res_ids) for res_id, recipients in default_recipients.items(): results[res_id].pop('partner_to', None) results[res_id].update(recipients) for res_id, values in results.items(): partner_ids = values.get('partner_ids', list()) if self._context.get('tpl_partners_only'): mails = tools.email_split(values.pop('email_to', '')) + tools.email_split(values.pop('email_cc', '')) for mail in mails: partner_id = self.env['res.partner'].find_or_create(mail) partner_ids.append(partner_id) partner_to = values.pop('partner_to', '') if partner_to: # placeholders could generate '', 3, 2 due to some empty field values tpl_partner_ids = [int(pid) for pid in partner_to.split(',') if pid] partner_ids += self.env['res.partner'].sudo().browse(tpl_partner_ids).exists().ids results[res_id]['partner_ids'] = partner_ids return results @api.multi def generate_email(self, res_ids, fields=None): """Generates an email from the template for given the given model based on records given by res_ids. :param template_id: id of the template to render. :param res_id: id of the record to use for rendering the template (model is taken from template definition) :returns: a dict containing all relevant fields for creating a new mail.mail entry, with one extra key ``attachments``, in the format [(report_name, data)] where data is base64 encoded. """ self.ensure_one() multi_mode = True if isinstance(res_ids, pycompat.integer_types): res_ids = [res_ids] multi_mode = False if fields is None: fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'scheduled_date'] res_ids_to_templates = self.get_email_template(res_ids) # templates: res_id -> template; template -> res_ids templates_to_res_ids = {} for res_id, template in res_ids_to_templates.items(): templates_to_res_ids.setdefault(template, []).append(res_id) results = dict() for template, template_res_ids in templates_to_res_ids.items(): Template = self.env['mail.template'] # generate fields value for all res_ids linked to the current template if template.lang: Template = Template.with_context(lang=template._context.get('lang')) for field in fields: Template = Template.with_context(safe=field in {'subject'}) generated_field_values = Template.render_template( getattr(template, field), template.model, template_res_ids, post_process=(field == 'body_html')) for res_id, field_value in generated_field_values.items(): results.setdefault(res_id, dict())[field] = field_value # compute recipients if any(field in fields for field in ['email_to', 'partner_to', 'email_cc']): results = template.generate_recipients(results, template_res_ids) # update values for all res_ids for res_id in template_res_ids: values = results[res_id] # body: add user signature, sanitize if 'body_html' in fields and template.user_signature: signature = self.env.user.signature if signature: values['body_html'] = tools.append_content_to_html(values['body_html'], signature, plaintext=False) if values.get('body_html'): values['body'] = tools.html_sanitize(values['body_html']) # technical settings values.update( mail_server_id=template.mail_server_id.id or False, auto_delete=template.auto_delete, model=template.model, res_id=res_id or False, attachment_ids=[attach.id for attach in template.attachment_ids], ) # Add report in attachments: generate once for all template_res_ids if template.report_template: for res_id in template_res_ids: attachments = [] report_name = self.render_template(template.report_name, template.model, res_id) report = template.report_template report_service = report.report_name if report.report_type not in ['qweb-html', 'qweb-pdf']: raise UserError(_('Unsupported report type %s found.') % report.report_type) result, format = report.render_qweb_pdf([res_id]) # TODO in trunk, change return format to binary to match message_post expected format result = base64.b64encode(result) if not report_name: report_name = 'report.' + report_service ext = "." + format if not report_name.endswith(ext): report_name += ext attachments.append((report_name, result)) results[res_id]['attachments'] = attachments return multi_mode and results or results[res_ids[0]] @api.multi def send_mail(self, res_id, force_send=False, raise_exception=False, email_values=None): """Generates a new mail message for the given template and record, and schedules it for delivery through the ``mail`` module's scheduler. :param int res_id: id of the record to render the template with (model is taken from the template) :param bool force_send: if True, the generated mail.message is immediately sent after being created, as if the scheduler was executed for this message only. :param dict email_values: if set, the generated mail.message is updated with given values dict :returns: id of the mail.message that was created """ self.ensure_one() Mail = self.env['mail.mail'] Attachment = self.env['ir.attachment'] # TDE FIXME: should remove dfeault_type from context # create a mail_mail based on values, without attachments values = self.generate_email(res_id) values['recipient_ids'] = [(4, pid) for pid in values.get('partner_ids', list())] values.update(email_values or {}) attachment_ids = values.pop('attachment_ids', []) attachments = values.pop('attachments', []) # add a protection against void email_from if 'email_from' in values and not values.get('email_from'): values.pop('email_from') mail = Mail.create(values) # manage attachments for attachment in attachments: attachment_data = { 'name': attachment[0], 'datas_fname': attachment[0], 'datas': attachment[1], 'type': 'binary', 'res_model': 'mail.message', 'res_id': mail.mail_message_id.id, } attachment_ids.append(Attachment.create(attachment_data).id) if attachment_ids: values['attachment_ids'] = [(6, 0, attachment_ids)] mail.write({'attachment_ids': [(6, 0, attachment_ids)]}) if force_send: mail.send(raise_exception=raise_exception) return mail.id # TDE CLEANME: return mail + api.returns ?
class Planner(models.Model): """Planner Model. Each Planner has link to an ir.ui.view record that is a template used to display the planner pages. Each Planner has link to ir.ui.menu record that is a top menu used to display the planner launcher(progressbar) Method _prepare_<planner_application>_data(self) (model method) that generates the values used to display in specific planner pages """ _name = 'web.planner' _description = 'Planner' @api.model def _get_planner_application(self): return [] name = fields.Char(string='Name', required=True) menu_id = fields.Many2one('ir.ui.menu', string='Menu', required=True) view_id = fields.Many2one('ir.ui.view', string='Template', required=True) progress = fields.Integer(string="Progress Percentage", company_dependent=True) # data field is used to store the data filled by user in planner(JSON Data) data = fields.Text(string="Data", company_dependent=True) tooltip_planner = fields.Html(string='Planner Tooltips', translate=True) planner_application = fields.Selection('_get_planner_application', string='Planner Application', required=True) active = fields.Boolean( string="Active", default=True, help= "If the active field is set to False, it will allow you to hide the planner. This change requires a refresh of your page." ) @api.model def render(self, template_id, planner_app): # prepare the planner data as per the planner application values = { 'prepare_backend_url': self.prepare_backend_url, 'is_module_installed': self.is_module_installed, } planner_find_method_name = '_prepare_%s_data' % planner_app if hasattr(self, planner_find_method_name): values.update(getattr( self, planner_find_method_name)()) # update the default value return self.env['ir.ui.view'].browse(template_id).render(values=values) @api.model def prepare_backend_url(self, action_xml_id, view_type='list', module_name=None): """ prepare the backend url to the given action, or to the given module view. :param action_xml_id : the xml id of the action to redirect to :param view_type : the view type to display when redirecting (form, kanban, list, ...) :param module_name : the name of the module to display (if action_xml_id is 'open_module_tree'), or to redirect to if the action is not found. :returns url : the url to the correct page """ params = dict(view_type=view_type) # setting the action action = self.env.ref(action_xml_id, False) if action: params['action'] = action.id params['view_type'] = action.view_type or view_type else: params['model'] = 'ir.module.module' # setting the module if module_name: module = self.env['ir.module.module'].sudo().search( [('name', '=', module_name)], limit=1) if module: params['id'] = module.id else: return "#show_enterprise" return "/web#%s" % (urls.url_encode(params), ) @api.model def is_module_installed(self, module_name=None): return module_name in self.env['ir.module.module']._installed() @api.model def get_planner_progress(self, planner_application): return self.search([('planner_application', '=', planner_application) ]).progress
class Users(models.Model): """ User class. A res.users record models an OpenERP user and is different from an employee. res.users class now inherits from res.partner. The partner model is used to store the data related to the partner: lang, name, address, avatar, ... The user model is now dedicated to technical data. """ _name = "res.users" _description = 'Users' _inherits = {'res.partner': 'partner_id'} _order = 'name, login' __uid_cache = defaultdict(dict) # {dbname: {uid: password}} # User can write on a few of his own fields (but not his groups for example) SELF_WRITEABLE_FIELDS = [ 'signature', 'action_id', 'company_id', 'email', 'name', 'image', 'image_medium', 'image_small', 'lang', 'tz' ] # User can read a few of his own fields SELF_READABLE_FIELDS = [ 'signature', 'company_id', 'login', 'email', 'name', 'image', 'image_medium', 'image_small', 'lang', 'tz', 'tz_offset', 'groups_id', 'partner_id', '__last_update', 'action_id' ] def _default_groups(self): default_user = self.env.ref('base.default_user', raise_if_not_found=False) return (default_user or self.env['res.users']).sudo().groups_id def _companies_count(self): return self.env['res.company'].sudo().search_count([]) partner_id = fields.Many2one('res.partner', required=True, ondelete='restrict', auto_join=True, string='Related Partner', help='Partner-related data of the user') login = fields.Char(required=True, help="Used to log into the system") password = fields.Char( default='', invisible=True, copy=False, help= "Keep empty if you don't want the user to be able to connect on the system." ) new_password = fields.Char(string='Set Password', compute='_compute_password', inverse='_inverse_password', help="Specify a value only when creating a user or if you're "\ "changing the user's password, otherwise leave empty. After "\ "a change of password, the user has to login again.") signature = fields.Html() active = fields.Boolean(default=True) action_id = fields.Many2one( 'ir.actions.actions', string='Home Action', help= "If specified, this action will be opened at log on for this user, in addition to the standard menu." ) groups_id = fields.Many2many('res.groups', 'res_groups_users_rel', 'uid', 'gid', string='Groups', default=_default_groups) log_ids = fields.One2many('res.users.log', 'create_uid', string='User log entries') login_date = fields.Datetime(related='log_ids.create_date', string='Latest connection') share = fields.Boolean( compute='_compute_share', compute_sudo=True, string='Share User', store=True, help= "External user with limited access, created only for the purpose of sharing data." ) companies_count = fields.Integer(compute='_compute_companies_count', string="Number of Companies", default=_companies_count) tz_offset = fields.Char(compute='_compute_tz_offset', string='Timezone offset', invisible=True) @api.model def _get_company(self): return self.env.user.company_id # Special behavior for this field: res.company.search() will only return the companies # available to the current user (should be the user's companies?), when the user_preference # context is set. company_id = fields.Many2one( 'res.company', string='Company', required=True, default=_get_company, help='The company this user is currently working for.', context={'user_preference': True}) company_ids = fields.Many2many('res.company', 'res_company_users_rel', 'user_id', 'cid', string='Companies', default=_get_company) # overridden inherited fields to bypass access rights, in case you have # access to the user but not its corresponding partner name = fields.Char(related='partner_id.name', inherited=True) email = fields.Char(related='partner_id.email', inherited=True) _sql_constraints = [('login_key', 'UNIQUE (login)', 'You can not have two users with the same login !')] def _compute_password(self): for user in self: user.password = '' def _inverse_password(self): for user in self: if not user.new_password: # Do not update the password if no value is provided, ignore silently. # For example web client submits False values for all empty fields. continue if user == self.env.user: # To change their own password, users must use the client-specific change password wizard, # so that the new password is immediately used for further RPC requests, otherwise the user # will face unexpected 'Access Denied' exceptions. raise UserError( _('Please use the change password wizard (in User Preferences or User menu) to change your own password.' )) else: user.password = user.new_password @api.depends('groups_id') def _compute_share(self): for user in self: user.share = not user.has_group('base.group_user') @api.multi def _compute_companies_count(self): companies_count = self._companies_count() for user in self: user.companies_count = companies_count @api.depends('tz') def _compute_tz_offset(self): for user in self: user.tz_offset = datetime.datetime.now( pytz.timezone(user.tz or 'GMT')).strftime('%z') @api.onchange('login') def on_change_login(self): if self.login and tools.single_email_re.match(self.login): self.email = self.login @api.onchange('parent_id') def onchange_parent_id(self): return self.mapped('partner_id').onchange_parent_id() @api.multi @api.constrains('company_id', 'company_ids') def _check_company(self): if any(user.company_ids and user.company_id not in user.company_ids for user in self): raise ValidationError( _('The chosen company is not in the allowed companies for this user' )) @api.multi @api.constrains('action_id') def _check_action_id(self): action_open_website = self.env.ref('base.action_open_website', raise_if_not_found=False) if action_open_website and any( user.action_id.id == action_open_website.id for user in self): raise ValidationError( _('The "App Switcher" action cannot be selected as home action.' )) @api.multi def read(self, fields=None, load='_classic_read'): if fields and self == self.env.user: for key in fields: if not (key in self.SELF_READABLE_FIELDS or key.startswith('context_')): break else: # safe fields only, so we read as super-user to bypass access rights self = self.sudo() result = super(Users, self).read(fields=fields, load=load) canwrite = self.env['ir.model.access'].check('res.users', 'write', False) if not canwrite: for vals in result: if vals['id'] != self._uid: for key in USER_PRIVATE_FIELDS: if key in vals: vals[key] = '********' return result @api.model def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True): groupby_fields = set([groupby] if isinstance( groupby, pycompat.string_types) else groupby) if groupby_fields.intersection(USER_PRIVATE_FIELDS): raise AccessError(_("Invalid 'group by' parameter")) return super(Users, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy) @api.model def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None): if self._uid != SUPERUSER_ID and args: domain_fields = { term[0] for term in args if isinstance(term, (tuple, list)) } if domain_fields.intersection(USER_PRIVATE_FIELDS): raise AccessError(_('Invalid search criterion')) return super(Users, self)._search(args, offset=offset, limit=limit, order=order, count=count, access_rights_uid=access_rights_uid) @api.model def create(self, vals): user = super(Users, self).create(vals) user.partner_id.active = user.active if user.partner_id.company_id: user.partner_id.write({'company_id': user.company_id.id}) return user @api.multi def write(self, values): if values.get('active') == False: for user in self: if user.id == SUPERUSER_ID: raise UserError(_("You cannot deactivate the admin user.")) elif user.id == self._uid: raise UserError( _("You cannot deactivate the user you're currently logged in as." )) if self == self.env.user: for key in list(values): if not (key in self.SELF_WRITEABLE_FIELDS or key.startswith('context_')): break else: if 'company_id' in values: if values[ 'company_id'] not in self.env.user.company_ids.ids: del values['company_id'] # safe fields only, so we write as super-user to bypass access rights self = self.sudo() res = super(Users, self).write(values) if 'company_id' in values: for user in self: # if partner is global we keep it that way if user.partner_id.company_id and user.partner_id.company_id.id != values[ 'company_id']: user.partner_id.write({'company_id': user.company_id.id}) # clear default ir values when company changes self.env['ir.default'].clear_caches() # clear caches linked to the users if 'groups_id' in values: self.env['ir.model.access'].call_cache_clearing_methods() self.env['ir.rule'].clear_caches() self.has_group.clear_cache(self) if any( key.startswith('context_') or key in ('lang', 'tz') for key in values): self.context_get.clear_cache(self) if any(key in values for key in ['active'] + USER_PRIVATE_FIELDS): db = self._cr.dbname for id in self.ids: self.__uid_cache[db].pop(id, None) if any(key in values for key in self._get_session_token_fields()): self._invalidate_session_cache() return res @api.multi def unlink(self): if SUPERUSER_ID in self.ids: raise UserError( _('You can not remove the admin user as it is used internally for resources created by izi (updates, module installation, ...)' )) db = self._cr.dbname for id in self.ids: self.__uid_cache[db].pop(id, None) self._invalidate_session_cache() return super(Users, self).unlink() @api.model def name_search(self, name='', args=None, operator='ilike', limit=100): if args is None: args = [] users = self.browse() if name and operator in ['=', 'ilike']: users = self.search([('login', '=', name)] + args, limit=limit) if not users: users = self.search([('name', operator, name)] + args, limit=limit) return users.name_get() @api.multi def copy(self, default=None): self.ensure_one() default = dict(default or {}) if ('name' not in default) and ('partner_id' not in default): default['name'] = _("%s (copy)") % self.name if 'login' not in default: default['login'] = _("%s (copy)") % self.login return super(Users, self).copy(default) @api.model @tools.ormcache('self._uid') def context_get(self): user = self.env.user result = {} for k in self._fields: if k.startswith('context_'): context_key = k[8:] elif k in ['lang', 'tz']: context_key = k else: context_key = False if context_key: res = getattr(user, k) or False if isinstance(res, models.BaseModel): res = res.id result[context_key] = res or False return result @api.model @api.returns('ir.actions.act_window', lambda record: record.id) def action_get(self): return self.sudo().env.ref('base.action_res_users_my') def check_super(self, passwd): return check_super(passwd) @api.model def check_credentials(self, password): """ Override this method to plug additional authentication methods""" user = self.sudo().search([('id', '=', self._uid), ('password', '=', password)]) if not user: raise AccessDenied() @api.model def _update_last_login(self): # only create new records to avoid any side-effect on concurrent transactions # extra records will be deleted by the periodical garbage collection self.env['res.users.log'].create({}) # populated by defaults @classmethod def _login(cls, db, login, password): if not password: return False user_id = False try: with cls.pool.cursor() as cr: self = api.Environment(cr, SUPERUSER_ID, {})[cls._name] user = self.search([('login', '=', login)]) if user: user_id = user.id user.sudo(user_id).check_credentials(password) user.sudo(user_id)._update_last_login() except AccessDenied: user_id = False status = "successful" if user_id else "failed" ip = request.httprequest.environ['REMOTE_ADDR'] if request else 'n/a' _logger.info("Login %s for db:%s login:%s from %s", status, db, login, ip) return user_id @classmethod def authenticate(cls, db, login, password, user_agent_env): """Verifies and returns the user ID corresponding to the given ``login`` and ``password`` combination, or False if there was no matching user. :param str db: the database on which user is trying to authenticate :param str login: username :param str password: user password :param dict user_agent_env: environment dictionary describing any relevant environment attributes """ uid = cls._login(db, login, password) if uid == SUPERUSER_ID: # Successfully logged in as admin! # Attempt to guess the web base url... if user_agent_env and user_agent_env.get('base_location'): try: with cls.pool.cursor() as cr: base = user_agent_env['base_location'] ICP = api.Environment(cr, uid, {})['ir.config_parameter'] if not ICP.get_param('web.base.url.freeze'): ICP.set_param('web.base.url', base) except Exception: _logger.exception( "Failed to update web.base.url configuration parameter" ) return uid @classmethod def check(cls, db, uid, passwd): """Verifies that the given (uid, password) is authorized for the database ``db`` and raise an exception if it is not.""" if not passwd: # empty passwords disallowed for obvious security reasons raise AccessDenied() db = cls.pool.db_name if cls.__uid_cache[db].get(uid) == passwd: return cr = cls.pool.cursor() try: self = api.Environment(cr, uid, {})[cls._name] self.check_credentials(passwd) cls.__uid_cache[db][uid] = passwd finally: cr.close() def _get_session_token_fields(self): return {'id', 'login', 'password', 'active'} @tools.ormcache('sid') def _compute_session_token(self, sid): """ Compute a session token given a session id and a user id """ # retrieve the fields used to generate the session token session_fields = ', '.join(sorted(self._get_session_token_fields())) self.env.cr.execute( """SELECT %s, (SELECT value FROM ir_config_parameter WHERE key='database.secret') FROM res_users WHERE id=%%s""" % (session_fields), (self.id, )) if self.env.cr.rowcount != 1: self._invalidate_session_cache() return False data_fields = self.env.cr.fetchone() # generate hmac key key = (u'%s' % (data_fields, )).encode('utf-8') # hmac the session id data = sid.encode('utf-8') h = hmac.new(key, data, sha256) # keep in the cache the token return h.hexdigest() @api.multi def _invalidate_session_cache(self): """ Clear the sessions cache """ self._compute_session_token.clear_cache(self) @api.model def change_password(self, old_passwd, new_passwd): """Change current user password. Old password must be provided explicitly to prevent hijacking an existing user session, or for cases where the cleartext password is not used to authenticate requests. :return: True :raise: izi.exceptions.AccessDenied when old password is wrong :raise: izi.exceptions.UserError when new password is not set or empty """ self.check(self._cr.dbname, self._uid, old_passwd) if new_passwd: # use self.env.user here, because it has uid=SUPERUSER_ID return self.env.user.write({'password': new_passwd}) raise UserError( _("Setting empty passwords is not allowed for security reasons!")) @api.multi def preference_save(self): return { 'type': 'ir.actions.client', 'tag': 'reload_context', } @api.multi def preference_change_password(self): return { 'type': 'ir.actions.client', 'tag': 'change_password', 'target': 'new', } @api.model def has_group(self, group_ext_id): # use singleton's id if called on a non-empty recordset, otherwise # context uid uid = self.id or self._uid return self.sudo(user=uid)._has_group(group_ext_id) @api.model @tools.ormcache('self._uid', 'group_ext_id') def _has_group(self, group_ext_id): """Checks whether user belongs to given group. :param str group_ext_id: external ID (XML ID) of the group. Must be provided in fully-qualified form (``module.ext_id``), as there is no implicit module to use.. :return: True if the current user is a member of the group with the given external ID (XML ID), else False. """ assert group_ext_id and '.' in group_ext_id, "External ID must be fully qualified" module, ext_id = group_ext_id.split('.') self._cr.execute( """SELECT 1 FROM res_groups_users_rel WHERE uid=%s AND gid IN (SELECT res_id FROM ir_model_data WHERE module=%s AND name=%s)""", (self._uid, module, ext_id)) return bool(self._cr.fetchone()) # for a few places explicitly clearing the has_group cache has_group.clear_cache = _has_group.clear_cache @api.multi def _is_public(self): self.ensure_one() return self.has_group('base.group_public') @api.multi def _is_system(self): self.ensure_one() return self.has_group('base.group_system') @api.multi def _is_admin(self): self.ensure_one() return self._is_superuser() or self.has_group('base.group_erp_manager') @api.multi def _is_superuser(self): self.ensure_one() return self.id == SUPERUSER_ID @api.model def get_company_currency_id(self): return self.env.user.company_id.currency_id.id
class Task(models.Model): _name = "project.task" _description = "Task" _date_name = "date_start" _inherit = ['mail.thread', 'mail.activity.mixin', 'portal.mixin'] _mail_post_access = 'read' _order = "priority desc, sequence, id desc" def _get_default_partner(self): if 'default_project_id' in self.env.context: default_project_id = self.env['project.project'].browse(self.env.context['default_project_id']) return default_project_id.exists().partner_id def _get_default_stage_id(self): """ Gives default stage_id """ project_id = self.env.context.get('default_project_id') if not project_id: return False return self.stage_find(project_id, [('fold', '=', False)]) @api.model def _read_group_stage_ids(self, stages, domain, order): search_domain = [('id', 'in', stages.ids)] if 'default_project_id' in self.env.context: search_domain = ['|', ('project_ids', '=', self.env.context['default_project_id'])] + search_domain stage_ids = stages._search(search_domain, order=order, access_rights_uid=SUPERUSER_ID) return stages.browse(stage_ids) active = fields.Boolean(default=True) name = fields.Char(string='Task Title', track_visibility='always', required=True, index=True) description = fields.Html(string='Description') priority = fields.Selection([ ('0', 'Low'), ('1', 'Normal'), ], default='0', index=True, string="Priority") sequence = fields.Integer(string='Sequence', index=True, default=10, help="Gives the sequence order when displaying a list of tasks.") stage_id = fields.Many2one('project.task.type', string='Stage', track_visibility='onchange', index=True, default=_get_default_stage_id, group_expand='_read_group_stage_ids', domain="[('project_ids', '=', project_id)]", copy=False) tag_ids = fields.Many2many('project.tags', string='Tags', oldname='categ_ids') kanban_state = fields.Selection([ ('normal', 'Grey'), ('done', 'Green'), ('blocked', 'Red')], string='Kanban State', copy=False, default='normal', required=True, help="A task's kanban state indicates special situations affecting it:\n" " * Grey is the default situation\n" " * Red indicates something is preventing the progress of this task\n" " * Green indicates the task is ready to be pulled to the next stage") kanban_state_label = fields.Char(compute='_compute_kanban_state_label', string='Kanban State', track_visibility='onchange') create_date = fields.Datetime(index=True) write_date = fields.Datetime(index=True) #not displayed in the view but it might be useful with base_automation module (and it needs to be defined first for that) date_start = fields.Datetime(string='Starting Date', default=fields.Datetime.now, index=True, copy=False) date_end = fields.Datetime(string='Ending Date', index=True, copy=False) date_assign = fields.Datetime(string='Assigning Date', index=True, copy=False, readonly=True) date_deadline = fields.Date(string='Deadline', index=True, copy=False) date_last_stage_update = fields.Datetime(string='Last Stage Update', default=fields.Datetime.now, index=True, copy=False, readonly=True) project_id = fields.Many2one('project.project', string='Project', default=lambda self: self.env.context.get('default_project_id'), index=True, track_visibility='onchange', change_default=True) notes = fields.Text(string='Notes') planned_hours = fields.Float(string='Initially Planned Hours', help='Estimated time to do the task, usually set by the project manager when the task is in draft state.') remaining_hours = fields.Float(string='Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task.") user_id = fields.Many2one('res.users', string='Assigned to', default=lambda self: self.env.uid, index=True, track_visibility='always') partner_id = fields.Many2one('res.partner', string='Customer', default=_get_default_partner) manager_id = fields.Many2one('res.users', string='Project Manager', related='project_id.user_id', readonly=True, related_sudo=False) company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env['res.company']._company_default_get()) color = fields.Integer(string='Color Index') user_email = fields.Char(related='user_id.email', string='User Email', readonly=True, related_sudo=False) attachment_ids = fields.One2many('ir.attachment', compute='_compute_attachment_ids', string="Main Attachments", help="Attachment that don't come from message.") # In the domain of displayed_image_id, we couln't use attachment_ids because a one2many is represented as a list of commands so we used res_model & res_id displayed_image_id = fields.Many2one('ir.attachment', domain="[('res_model', '=', 'project.task'), ('res_id', '=', id), ('mimetype', 'ilike', 'image')]", string='Cover Image') legend_blocked = fields.Char(related='stage_id.legend_blocked', string='Kanban Blocked Explanation', readonly=True, related_sudo=False) legend_done = fields.Char(related='stage_id.legend_done', string='Kanban Valid Explanation', readonly=True, related_sudo=False) legend_normal = fields.Char(related='stage_id.legend_normal', string='Kanban Ongoing Explanation', readonly=True, related_sudo=False) parent_id = fields.Many2one('project.task', string='Parent Task', index=True) child_ids = fields.One2many('project.task', 'parent_id', string="Sub-tasks", context={'active_test': False}) subtask_project_id = fields.Many2one('project.project', related="project_id.subtask_project_id", string='Sub-task Project', readonly=True) subtask_count = fields.Integer(compute='_compute_subtask_count', type='integer', string="Sub-task count") email_from = fields.Char(string='Email', help="These people will receive email.", index=True) email_cc = fields.Char(string='Watchers Emails', help="""These email addresses will be added to the CC field of all inbound and outbound emails for this record before being sent. Separate multiple email addresses with a comma""") # Computed field about working time elapsed between record creation and assignation/closing. working_hours_open = fields.Float(compute='_compute_elapsed', string='Working hours to assign', store=True, group_operator="avg") working_hours_close = fields.Float(compute='_compute_elapsed', string='Working hours to close', store=True, group_operator="avg") working_days_open = fields.Float(compute='_compute_elapsed', string='Working days to assign', store=True, group_operator="avg") working_days_close = fields.Float(compute='_compute_elapsed', string='Working days to close', store=True, group_operator="avg") # customer portal: include comment and incoming emails in communication history website_message_ids = fields.One2many(domain=lambda self: [('model', '=', self._name), ('message_type', 'in', ['email', 'comment'])]) def _compute_attachment_ids(self): for task in self: attachment_ids = self.env['ir.attachment'].search([('res_id', '=', task.id), ('res_model', '=', 'project.task')]).ids message_attachment_ids = self.mapped('message_ids.attachment_ids').ids # from mail_thread task.attachment_ids = list(set(attachment_ids) - set(message_attachment_ids)) @api.multi @api.depends('create_date', 'date_end', 'date_assign') def _compute_elapsed(self): task_linked_to_calendar = self.filtered( lambda task: task.project_id.resource_calendar_id and task.create_date ) for task in task_linked_to_calendar: dt_create_date = fields.Datetime.from_string(task.create_date) if task.date_assign: dt_date_assign = fields.Datetime.from_string(task.date_assign) task.working_hours_open = task.project_id.resource_calendar_id.get_work_hours_count( dt_create_date, dt_date_assign, False, compute_leaves=True) task.working_days_open = task.working_hours_open / 24.0 if task.date_end: dt_date_end = fields.Datetime.from_string(task.date_end) task.working_hours_close = task.project_id.resource_calendar_id.get_work_hours_count( dt_create_date, dt_date_end, False, compute_leaves=True) task.working_days_close = task.working_hours_close / 24.0 (self - task_linked_to_calendar).update(dict.fromkeys( ['working_hours_open', 'working_hours_close', 'working_days_open', 'working_days_close'], 0.0)) @api.depends('stage_id', 'kanban_state') def _compute_kanban_state_label(self): for task in self: if task.kanban_state == 'normal': task.kanban_state_label = task.legend_normal elif task.kanban_state == 'blocked': task.kanban_state_label = task.legend_blocked else: task.kanban_state_label = task.legend_done def _compute_portal_url(self): super(Task, self)._compute_portal_url() for task in self: task.portal_url = '/my/task/%s' % task.id @api.onchange('partner_id') def _onchange_partner_id(self): self.email_from = self.partner_id.email @api.onchange('project_id') def _onchange_project(self): if self.project_id: if self.project_id.partner_id: self.partner_id = self.project_id.partner_id if self.project_id not in self.stage_id.project_ids: self.stage_id = self.stage_find(self.project_id.id, [('fold', '=', False)]) else: self.stage_id = False @api.onchange('user_id') def _onchange_user(self): if self.user_id: self.date_start = fields.Datetime.now() @api.multi def copy(self, default=None): if default is None: default = {} if not default.get('name'): default['name'] = _("%s (copy)") % self.name if 'remaining_hours' not in default: default['remaining_hours'] = self.planned_hours return super(Task, self).copy(default) @api.multi def _compute_subtask_count(self): for task in self: task.subtask_count = self.search_count([('id', 'child_of', task.id), ('id', '!=', task.id)]) @api.constrains('parent_id') def _check_parent_id(self): for task in self: if not task._check_recursion(): raise ValidationError(_('Error! You cannot create recursive hierarchy of task(s).')) @api.constrains('parent_id') def _check_subtask_project(self): for task in self: if task.parent_id.project_id and task.project_id != task.parent_id.project_id.subtask_project_id: raise UserError(_("You can't define a parent task if its project is not correctly configured. The sub-task's project of the parent task's project should be this task's project")) # Override view according to the company definition @api.model def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): # read uom as admin to avoid access rights issues, e.g. for portal/share users, # this should be safe (no context passed to avoid side-effects) obj_tm = self.env.user.company_id.project_time_mode_id tm = obj_tm and obj_tm.name or 'Hours' res = super(Task, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu) # read uom as admin to avoid access rights issues, e.g. for portal/share users, # this should be safe (no context passed to avoid side-effects) obj_tm = self.env.user.company_id.project_time_mode_id # using get_object to get translation value uom_hour = self.env.ref('product.product_uom_hour', False) if not obj_tm or not uom_hour or obj_tm.id == uom_hour.id: return res eview = etree.fromstring(res['arch']) # if the project_time_mode_id is not in hours (so in days), display it as a float field def _check_rec(eview): if eview.attrib.get('widget', '') == 'float_time': eview.set('widget', 'float') for child in eview: _check_rec(child) return True _check_rec(eview) res['arch'] = etree.tostring(eview, encoding='unicode') # replace reference of 'Hours' to 'Day(s)' for f in res['fields']: # TODO this NOT work in different language than english # the field 'Initially Planned Hours' should be replaced by 'Initially Planned Days' # but string 'Initially Planned Days' is not available in translation if 'Hours' in res['fields'][f]['string']: res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours', obj_tm.name) return res @api.model def get_empty_list_help(self, help): self = self.with_context( empty_list_help_id=self.env.context.get('default_project_id'), empty_list_help_model='project.project', empty_list_help_document_name=_("tasks") ) return super(Task, self).get_empty_list_help(help) # ---------------------------------------- # Case management # ---------------------------------------- def stage_find(self, section_id, domain=[], order='sequence'): """ Override of the base.stage method Parameter of the stage search taken from the lead: - section_id: if set, stages must belong to this section or be a default stage; if not set, stages must be default stages """ # collect all section_ids section_ids = [] if section_id: section_ids.append(section_id) section_ids.extend(self.mapped('project_id').ids) search_domain = [] if section_ids: search_domain = [('|')] * (len(section_ids) - 1) for section_id in section_ids: search_domain.append(('project_ids', '=', section_id)) search_domain += list(domain) # perform search, return the first found return self.env['project.task.type'].search(search_domain, order=order, limit=1).id # ------------------------------------------------ # CRUD overrides # ------------------------------------------------ @api.model def create(self, vals): # context: no_log, because subtype already handle this context = dict(self.env.context, mail_create_nolog=True) # for default stage if vals.get('project_id') and not context.get('default_project_id'): context['default_project_id'] = vals.get('project_id') # user_id change: update date_assign if vals.get('user_id'): vals['date_assign'] = fields.Datetime.now() # Stage change: Update date_end if folded stage if vals.get('stage_id'): vals.update(self.update_date_end(vals['stage_id'])) task = super(Task, self.with_context(context)).create(vals) return task @api.multi def write(self, vals): now = fields.Datetime.now() # stage change: update date_last_stage_update if 'stage_id' in vals: vals.update(self.update_date_end(vals['stage_id'])) vals['date_last_stage_update'] = now # reset kanban state when changing stage if 'kanban_state' not in vals: vals['kanban_state'] = 'normal' # user_id change: update date_assign if vals.get('user_id') and 'date_assign' not in vals: vals['date_assign'] = now result = super(Task, self).write(vals) return result def update_date_end(self, stage_id): project_task_type = self.env['project.task.type'].browse(stage_id) if project_task_type.fold: return {'date_end': fields.Datetime.now()} return {'date_end': False} @api.multi def get_access_action(self, access_uid=None): """ Instead of the classic form view, redirect to website for portal users that can read the task. """ self.ensure_one() user, record = self.env.user, self if access_uid: user = self.env['res.users'].sudo().browse(access_uid) record = self.sudo(user) if user.share: try: record.check_access_rule('read') except AccessError: pass else: return { 'type': 'ir.actions.act_url', 'url': '/my/task/%s' % self.id, 'target': 'self', 'res_id': self.id, } return super(Task, self).get_access_action(access_uid) # --------------------------------------------------- # Mail gateway # --------------------------------------------------- @api.multi def _track_template(self, tracking): res = super(Task, self)._track_template(tracking) test_task = self[0] changes, tracking_value_ids = tracking[test_task.id] if 'stage_id' in changes and test_task.stage_id.mail_template_id: res['stage_id'] = (test_task.stage_id.mail_template_id, {'composition_mode': 'mass_mail'}) return res @api.multi def _track_subtype(self, init_values): self.ensure_one() if 'kanban_state_label' in init_values and self.kanban_state == 'blocked': return 'project.mt_task_blocked' elif 'kanban_state_label' in init_values and self.kanban_state == 'done': return 'project.mt_task_ready' elif 'user_id' in init_values and self.user_id: # assigned -> new return 'project.mt_task_new' elif 'stage_id' in init_values and self.stage_id and self.stage_id.sequence <= 1: # start stage -> new return 'project.mt_task_new' elif 'stage_id' in init_values: return 'project.mt_task_stage' return super(Task, self)._track_subtype(init_values) @api.multi def _notification_recipients(self, message, groups): """ Handle project users and managers recipients that can convert assign tasks and create new one directly from notification emails. """ groups = super(Task, self)._notification_recipients(message, groups) self.ensure_one() if not self.user_id: take_action = self._notification_link_helper('assign') project_actions = [{'url': take_action, 'title': _('I take it')}] else: project_actions = [] new_group = ( 'group_project_user', lambda partner: bool(partner.user_ids) and any(user.has_group('project.group_project_user') for user in partner.user_ids), { 'actions': project_actions, }) groups = [new_group] + groups for group_name, group_method, group_data in groups: if group_name in ['customer', 'portal']: continue group_data['has_button_access'] = True return groups @api.model def message_get_reply_to(self, res_ids, default=None): """ Override to get the reply_to of the parent project. """ tasks = self.sudo().browse(res_ids) project_ids = tasks.mapped('project_id').ids aliases = self.env['project.project'].message_get_reply_to(project_ids, default=default) return {task.id: aliases.get(task.project_id.id, False) for task in tasks} @api.multi def email_split(self, msg): email_list = tools.email_split((msg.get('to') or '') + ',' + (msg.get('cc') or '')) # check left-part is not already an alias aliases = self.mapped('project_id.alias_name') return [x for x in email_list if x.split('@')[0] not in aliases] @api.model def message_new(self, msg, custom_values=None): """ Overrides mail_thread message_new that is called by the mailgateway through message_process. This override updates the document according to the email. """ # remove default author when going through the mail gateway. Indeed we # do not want to explicitly set user_id to False; however we do not # want the gateway user to be responsible if no other responsible is # found. create_context = dict(self.env.context or {}) create_context['default_user_id'] = False if custom_values is None: custom_values = {} defaults = { 'name': msg.get('subject') or _("No Subject"), 'email_from': msg.get('from'), 'email_cc': msg.get('cc'), 'planned_hours': 0.0, 'partner_id': msg.get('author_id') } defaults.update(custom_values) task = super(Task, self.with_context(create_context)).message_new(msg, custom_values=defaults) email_list = task.email_split(msg) partner_ids = [p for p in task._find_partner_from_emails(email_list, force_create=False) if p] task.message_subscribe(partner_ids) return task @api.multi def message_update(self, msg, update_vals=None): """ Override to update the task according to the email. """ if update_vals is None: update_vals = {} maps = { 'cost': 'planned_hours', } for line in msg['body'].split('\n'): line = line.strip() res = tools.command_re.match(line) if res: match = res.group(1).lower() field = maps.get(match) if field: try: update_vals[field] = float(res.group(2).lower()) except (ValueError, TypeError): pass email_list = self.email_split(msg) partner_ids = [p for p in self._find_partner_from_emails(email_list, force_create=False) if p] self.message_subscribe(partner_ids) return super(Task, self).message_update(msg, update_vals=update_vals) @api.multi def message_get_suggested_recipients(self): recipients = super(Task, self).message_get_suggested_recipients() for task in self.filtered('partner_id'): reason = _('Customer Email') if task.partner_id.email else _('Customer') if task.partner_id: task._message_add_suggested_recipient(recipients, partner=task.partner_id, reason=reason) elif task.email_from: task._message_add_suggested_recipient(recipients, partner=task.email_from, reason=reason) return recipients @api.multi def message_get_email_values(self, notif_mail=None): res = super(Task, self).message_get_email_values(notif_mail=notif_mail) headers = {} if res.get('headers'): try: headers.update(safe_eval(res['headers'])) except Exception: pass if self.project_id: current_objects = [h for h in headers.get('X-izi-Objects', '').split(',') if h] current_objects.insert(0, 'project.project-%s, ' % self.project_id.id) headers['X-izi-Objects'] = ','.join(current_objects) if self.tag_ids: headers['X-izi-Tags'] = ','.join(self.tag_ids.mapped('name')) res['headers'] = repr(headers) return res def _message_post_after_hook(self, message): if self.email_from and not self.partner_id: # we consider that posting a message with a specified recipient (not a follower, a specific one) # on a document without customer means that it was created through the chatter using # suggested recipients. This heuristic allows to avoid ugly hacks in JS. new_partner = message.partner_ids.filtered(lambda partner: partner.email == self.email_from) if new_partner: self.search([ ('partner_id', '=', False), ('email_from', '=', new_partner.email), ('stage_id.fold', '=', False)]).write({'partner_id': new_partner.id}) return super(Task, self)._message_post_after_hook(message) def action_assign_to_me(self): self.write({'user_id': self.env.user.id}) def action_open_parent_task(self): return { 'name': _('Parent Task'), 'view_type': 'form', 'view_mode': 'form', 'res_model': 'project.task', 'res_id': self.parent_id.id, 'type': 'ir.actions.act_window' } def action_subtask(self): action = self.env.ref('project.project_task_action_sub_task').read()[0] ctx = self.env.context.copy() ctx.update({ 'default_parent_id' : self.id, 'default_project_id' : self.env.context.get('project_id', self.subtask_project_id.id), 'default_name' : self.env.context.get('name', self.name) + ':', 'default_partner_id' : self.env.context.get('partner_id', self.partner_id.id), 'search_default_project_id': self.env.context.get('project_id', self.subtask_project_id.id), }) action['context'] = ctx action['domain'] = [('id', 'child_of', self.id), ('id', '!=', self.id)] return action
class MrpWorkorder(models.Model): _name = 'mrp.workorder' _description = 'Work Order' _inherit = ['mail.thread'] name = fields.Char('Work Order', required=True, states={ 'done': [('readonly', True)], 'cancel': [('readonly', True)] }) workcenter_id = fields.Many2one('mrp.workcenter', 'Work Center', required=True, states={ 'done': [('readonly', True)], 'cancel': [('readonly', True)] }) working_state = fields.Selection('Workcenter Status', related='workcenter_id.working_state', help='Technical: used in views only') production_id = fields.Many2one('mrp.production', 'Manufacturing Order', index=True, ondelete='cascade', required=True, track_visibility='onchange', states={ 'done': [('readonly', True)], 'cancel': [('readonly', True)] }) product_id = fields.Many2one('product.product', 'Product', related='production_id.product_id', readonly=True, help='Technical: used in views only.', store=True) product_uom_id = fields.Many2one('product.uom', 'Unit of Measure', related='production_id.product_uom_id', readonly=True, help='Technical: used in views only.') production_availability = fields.Selection( 'Stock Availability', readonly=True, related='production_id.availability', store=True, help='Technical: used in views and domains only.') production_state = fields.Selection('Production State', readonly=True, related='production_id.state', help='Technical: used in views only.') product_tracking = fields.Selection( 'Product Tracking', related='production_id.product_id.tracking', help='Technical: used in views only.') qty_production = fields.Float('Original Production Quantity', readonly=True, related='production_id.product_qty') qty_remaining = fields.Float( 'Quantity To Be Produced', compute='_compute_qty_remaining', digits=dp.get_precision('Product Unit of Measure')) qty_produced = fields.Float( 'Quantity', default=0.0, readonly=True, digits=dp.get_precision('Product Unit of Measure'), help="The number of products already handled by this work order") qty_producing = fields.Float( 'Currently Produced Quantity', default=1.0, digits=dp.get_precision('Product Unit of Measure'), states={ 'done': [('readonly', True)], 'cancel': [('readonly', True)] }) is_produced = fields.Boolean(string="Has Been Produced", compute='_compute_is_produced') state = fields.Selection([('pending', 'Pending'), ('ready', 'Ready'), ('progress', 'In Progress'), ('done', 'Finished'), ('cancel', 'Cancelled')], string='Status', default='pending') date_planned_start = fields.Datetime('Scheduled Date Start', states={ 'done': [('readonly', True)], 'cancel': [('readonly', True)] }) date_planned_finished = fields.Datetime('Scheduled Date Finished', states={ 'done': [('readonly', True)], 'cancel': [('readonly', True)] }) date_start = fields.Datetime('Effective Start Date', states={ 'done': [('readonly', True)], 'cancel': [('readonly', True)] }) date_finished = fields.Datetime('Effective End Date', states={ 'done': [('readonly', True)], 'cancel': [('readonly', True)] }) duration_expected = fields.Float('Expected Duration', digits=(16, 2), states={ 'done': [('readonly', True)], 'cancel': [('readonly', True)] }, help="Expected duration (in minutes)") duration = fields.Float('Real Duration', compute='_compute_duration', readonly=True, store=True) duration_unit = fields.Float('Duration Per Unit', compute='_compute_duration', readonly=True, store=True) duration_percent = fields.Integer('Duration Deviation (%)', compute='_compute_duration', group_operator="avg", readonly=True, store=True) operation_id = fields.Many2one( 'mrp.routing.workcenter', 'Operation' ) # Should be used differently as BoM can change in the meantime worksheet = fields.Binary('Worksheet', related='operation_id.worksheet', readonly=True) move_raw_ids = fields.One2many('stock.move', 'workorder_id', 'Moves') move_line_ids = fields.One2many( 'stock.move.line', 'workorder_id', 'Moves to Track', domain=[('done_wo', '=', True)], help= "Inventory moves for which you must scan a lot number at this work order" ) active_move_line_ids = fields.One2many('stock.move.line', 'workorder_id', domain=[('done_wo', '=', False)]) final_lot_id = fields.Many2one('stock.production.lot', 'Lot/Serial Number', domain="[('product_id', '=', product_id)]", states={ 'done': [('readonly', True)], 'cancel': [('readonly', True)] }) tracking = fields.Selection(related='production_id.product_id.tracking') time_ids = fields.One2many('mrp.workcenter.productivity', 'workorder_id') is_user_working = fields.Boolean( 'Is the Current User Working', compute='_compute_is_user_working', help="Technical field indicating whether the current user is working. " ) production_messages = fields.Html('Workorder Message', compute='_compute_production_messages') next_work_order_id = fields.Many2one('mrp.workorder', "Next Work Order") scrap_ids = fields.One2many('stock.scrap', 'workorder_id') scrap_count = fields.Integer(compute='_compute_scrap_move_count', string='Scrap Move') production_date = fields.Datetime( 'Production Date', related='production_id.date_planned_start', store=True) color = fields.Integer('Color', compute='_compute_color') capacity = fields.Float( 'Capacity', default=1.0, help="Number of pieces that can be produced in parallel.") @api.multi def name_get(self): return [(wo.id, "%s - %s - %s" % (wo.production_id.name, wo.product_id.name, wo.name)) for wo in self] @api.one @api.depends('production_id.product_qty', 'qty_produced') def _compute_is_produced(self): rounding = self.production_id.product_uom_id.rounding self.is_produced = float_compare(self.qty_produced, self.production_id.product_qty, precision_rounding=rounding) >= 0 @api.one @api.depends('time_ids.duration', 'qty_produced') def _compute_duration(self): self.duration = sum(self.time_ids.mapped('duration')) self.duration_unit = round(self.duration / max(self.qty_produced, 1), 2) # rounding 2 because it is a time if self.duration_expected: self.duration_percent = 100 * ( self.duration_expected - self.duration) / self.duration_expected else: self.duration_percent = 0 def _compute_is_user_working(self): """ Checks whether the current user is working """ for order in self: if order.time_ids.filtered( lambda x: (x.user_id.id == self.env.user.id) and (not x.date_end) and (x.loss_type in ('productive', 'performance'))): order.is_user_working = True else: order.is_user_working = False @api.depends('production_id', 'workcenter_id', 'production_id.bom_id') def _compute_production_messages(self): ProductionMessage = self.env['mrp.message'] for workorder in self: domain = [('valid_until', '>=', fields.Date.today()), '|', ('workcenter_id', '=', False), ('workcenter_id', '=', workorder.workcenter_id.id), '|', '|', '|', ('product_id', '=', workorder.product_id.id), '&', ('product_id', '=', False), ('product_tmpl_id', '=', workorder.product_id.product_tmpl_id.id), ('bom_id', '=', workorder.production_id.bom_id.id), ('routing_id', '=', workorder.operation_id.routing_id.id)] messages = ProductionMessage.search(domain).mapped('message') workorder.production_messages = "<br/>".join(messages) or False @api.multi def _compute_scrap_move_count(self): data = self.env['stock.scrap'].read_group( [('workorder_id', 'in', self.ids)], ['workorder_id'], ['workorder_id']) count_data = dict((item['workorder_id'][0], item['workorder_id_count']) for item in data) for workorder in self: workorder.scrap_count = count_data.get(workorder.id, 0) @api.multi @api.depends('date_planned_finished', 'production_id.date_planned_finished') def _compute_color(self): late_orders = self.filtered( lambda x: x.production_id.date_planned_finished and x. date_planned_finished > x.production_id.date_planned_finished) for order in late_orders: order.color = 4 for order in (self - late_orders): order.color = 2 @api.onchange('qty_producing') def _onchange_qty_producing(self): """ Update stock.move.lot records, according to the new qty currently produced. """ moves = self.move_raw_ids.filtered( lambda move: move.state not in ('done', 'cancel') and move.product_id.tracking != 'none' and move. product_id.id != self.production_id.product_id.id) for move in moves: move_lots = self.active_move_line_ids.filtered( lambda move_lot: move_lot.move_id == move) if not move_lots: continue rounding = move.product_uom.rounding new_qty = float_round(move.unit_factor * self.qty_producing, precision_rounding=rounding) if move.product_id.tracking == 'lot': move_lots[0].product_qty = new_qty move_lots[0].qty_done = new_qty elif move.product_id.tracking == 'serial': # Create extra pseudo record qty_todo = float_round(new_qty - sum(move_lots.mapped('qty_done')), precision_rounding=rounding) if float_compare(qty_todo, 0.0, precision_rounding=rounding) > 0: while float_compare( qty_todo, 0.0, precision_rounding=rounding) > 0: self.active_move_line_ids += self.env[ 'stock.move.line'].new({ 'move_id': move.id, 'product_id': move.product_id.id, 'lot_id': False, 'product_uom_qty': 0.0, 'product_uom_id': move.product_uom.id, 'qty_done': min(1.0, qty_todo), 'workorder_id': self.id, 'done_wo': False, 'location_id': move.location_id.id, 'location_dest_id': move.location_dest_id.id, 'date': move.date, }) qty_todo -= 1 elif float_compare(qty_todo, 0.0, precision_rounding=rounding) < 0: qty_todo = abs(qty_todo) for move_lot in move_lots: if float_compare( qty_todo, 0, precision_rounding=rounding) <= 0: break if not move_lot.lot_id and float_compare( qty_todo, move_lot.qty_done, precision_rounding=rounding) >= 0: qty_todo = float_round(qty_todo - move_lot.qty_done, precision_rounding=rounding) self.active_move_line_ids -= move_lot # Difference operator else: #move_lot.product_qty = move_lot.product_qty - qty_todo if float_compare(move_lot.qty_done - qty_todo, 0, precision_rounding=rounding) == 1: move_lot.qty_done = move_lot.qty_done - qty_todo else: move_lot.qty_done = 0 qty_todo = 0 @api.multi def write(self, values): if ('date_planned_start' in values or 'date_planned_finished' in values) and any(workorder.state == 'done' for workorder in self): raise UserError(_('You can not change the finished work order.')) return super(MrpWorkorder, self).write(values) def _generate_lot_ids(self): """ Generate stock move lines """ self.ensure_one() MoveLine = self.env['stock.move.line'] tracked_moves = self.move_raw_ids.filtered( lambda move: move.state not in ('done', 'cancel') and move.product_id.tracking != 'none' and move. product_id != self.production_id.product_id and move.bom_line_id) for move in tracked_moves: qty = move.unit_factor * self.qty_producing if move.product_id.tracking == 'serial': while float_compare( qty, 0.0, precision_rounding=move.product_uom.rounding) > 0: MoveLine.create({ 'move_id': move.id, 'product_uom_qty': 0, 'product_uom_id': move.product_uom.id, 'qty_done': min(1, qty), 'production_id': self.production_id.id, 'workorder_id': self.id, 'product_id': move.product_id.id, 'done_wo': False, 'location_id': move.location_id.id, 'location_dest_id': move.location_dest_id.id, }) qty -= 1 else: MoveLine.create({ 'move_id': move.id, 'product_uom_qty': 0, 'product_uom_id': move.product_uom.id, 'qty_done': qty, 'product_id': move.product_id.id, 'production_id': self.production_id.id, 'workorder_id': self.id, 'done_wo': False, 'location_id': move.location_id.id, 'location_dest_id': move.location_dest_id.id, }) def _assign_default_final_lot_id(self): self.final_lot_id = self.env['stock.production.lot'].search( [('use_next_on_work_order_id', '=', self.id)], order='create_date, id', limit=1) def _get_byproduct_move_line(self, by_product_move, quantity): return { 'move_id': by_product_move.id, 'product_id': by_product_move.product_id.id, 'product_uom_qty': quantity, 'product_uom_id': by_product_move.product_uom.id, 'qty_done': quantity, 'workorder_id': self.id, 'location_id': by_product_move.location_id.id, 'location_dest_id': by_product_move.location_dest_id.id, } @api.multi def record_production(self): self.ensure_one() if self.qty_producing <= 0: raise UserError( _('Please set the quantity you are currently producing. It should be different from zero.' )) if (self.production_id.product_id.tracking != 'none') and not self.final_lot_id and self.move_raw_ids: raise UserError( _('You should provide a lot/serial number for the final product' )) # Update quantities done on each raw material line # For each untracked component without any 'temporary' move lines, # (the new workorder tablet view allows registering consumed quantities for untracked components) # we assume that only the theoretical quantity was used for move in self.move_raw_ids: if move.has_tracking == 'none' and (move.state not in ('done', 'cancel')) and move.bom_line_id\ and move.unit_factor and not move.move_line_ids.filtered(lambda ml: not ml.done_wo): rounding = move.product_uom.rounding if self.product_id.tracking != 'none': qty_to_add = float_round(self.qty_producing * move.unit_factor, precision_rounding=rounding) move._generate_consumed_move_line(qty_to_add, self.final_lot_id) else: move.quantity_done += float_round( self.qty_producing * move.unit_factor, precision_rounding=rounding) # Transfer quantities from temporary to final move lots or make them final for move_line in self.active_move_line_ids: # Check if move_line already exists if move_line.qty_done <= 0: # rounding... move_line.sudo().unlink() continue if move_line.product_id.tracking != 'none' and not move_line.lot_id: raise UserError( _('You should provide a lot/serial number for a component') ) # Search other move_line where it could be added: lots = self.move_line_ids.filtered( lambda x: (x.lot_id.id == move_line.lot_id.id) and (not x.lot_produced_id) and (not x.done_move) and (x.product_id == move_line.product_id)) if lots: lots[0].qty_done += move_line.qty_done lots[0].lot_produced_id = self.final_lot_id.id move_line.sudo().unlink() else: move_line.lot_produced_id = self.final_lot_id.id move_line.done_wo = True # One a piece is produced, you can launch the next work order if self.next_work_order_id.state == 'pending': self.next_work_order_id.state = 'ready' self.move_line_ids.filtered( lambda move_line: not move_line.done_move and not move_line. lot_produced_id and move_line.qty_done > 0).write({ 'lot_produced_id': self.final_lot_id.id, 'lot_produced_qty': self.qty_producing }) # If last work order, then post lots used # TODO: should be same as checking if for every workorder something has been done? if not self.next_work_order_id: production_move = self.production_id.move_finished_ids.filtered( lambda x: (x.product_id.id == self.production_id.product_id.id ) and (x.state not in ('done', 'cancel'))) if production_move.product_id.tracking != 'none': move_line = production_move.move_line_ids.filtered( lambda x: x.lot_id.id == self.final_lot_id.id) if move_line: move_line.product_uom_qty += self.qty_producing else: move_line.create({ 'move_id': production_move.id, 'product_id': production_move.product_id.id, 'lot_id': self.final_lot_id.id, 'product_uom_qty': self.qty_producing, 'product_uom_id': production_move.product_uom.id, 'qty_done': self.qty_producing, 'workorder_id': self.id, 'location_id': production_move.location_id.id, 'location_dest_id': production_move.location_dest_id.id, }) else: production_move.quantity_done += self.qty_producing if not self.next_work_order_id: for by_product_move in self.production_id.move_finished_ids.filtered( lambda x: (x.product_id.id != self.production_id.product_id .id) and (x.state not in ('done', 'cancel'))): if by_product_move.has_tracking != 'serial': values = self._get_byproduct_move_line( by_product_move, self.qty_producing * by_product_move.unit_factor) self.env['stock.move.line'].create(values) elif by_product_move.has_tracking == 'serial': qty_todo = by_product_move.product_uom._compute_quantity( self.qty_producing * by_product_move.unit_factor, by_product_move.product_id.uom_id) for i in range( 0, int(float_round(qty_todo, precision_digits=0))): values = self._get_byproduct_move_line( by_product_move, 1) self.env['stock.move.line'].create(values) # Update workorder quantity produced self.qty_produced += self.qty_producing if self.final_lot_id: self.final_lot_id.use_next_on_work_order_id = self.next_work_order_id self.final_lot_id = False # Set a qty producing rounding = self.production_id.product_uom_id.rounding if float_compare(self.qty_produced, self.production_id.product_qty, precision_rounding=rounding) >= 0: self.qty_producing = 0 elif self.production_id.product_id.tracking == 'serial': self._assign_default_final_lot_id() self.qty_producing = 1.0 self._generate_lot_ids() else: self.qty_producing = float_round(self.production_id.product_qty - self.qty_produced, precision_rounding=rounding) self._generate_lot_ids() if self.next_work_order_id and self.production_id.product_id.tracking != 'none': self.next_work_order_id._assign_default_final_lot_id() if float_compare(self.qty_produced, self.production_id.product_qty, precision_rounding=rounding) >= 0: self.button_finish() return True @api.multi def button_start(self): self.ensure_one() # As button_start is automatically called in the new view if self.state in ('done', 'cancel'): return True # Need a loss in case of the real time exceeding the expected timeline = self.env['mrp.workcenter.productivity'] if self.duration < self.duration_expected: loss_id = self.env['mrp.workcenter.productivity.loss'].search( [('loss_type', '=', 'productive')], limit=1) if not len(loss_id): raise UserError( _("You need to define at least one productivity loss in the category 'Productivity'. Create one from the Manufacturing app, menu: Configuration / Productivity Losses." )) else: loss_id = self.env['mrp.workcenter.productivity.loss'].search( [('loss_type', '=', 'performance')], limit=1) if not len(loss_id): raise UserError( _("You need to define at least one productivity loss in the category 'Performance'. Create one from the Manufacturing app, menu: Configuration / Productivity Losses." )) for workorder in self: if workorder.production_id.state != 'progress': workorder.production_id.write({ 'state': 'progress', 'date_start': datetime.now(), }) timeline.create({ 'workorder_id': workorder.id, 'workcenter_id': workorder.workcenter_id.id, 'description': _('Time Tracking: ') + self.env.user.name, 'loss_id': loss_id[0].id, 'date_start': datetime.now(), 'user_id': self.env.user.id }) return self.write({ 'state': 'progress', 'date_start': datetime.now(), }) @api.multi def button_finish(self): self.ensure_one() self.end_all() return self.write({ 'state': 'done', 'date_finished': fields.Datetime.now() }) @api.multi def end_previous(self, doall=False): """ @param: doall: This will close all open time lines on the open work orders when doall = True, otherwise only the one of the current user """ # TDE CLEANME timeline_obj = self.env['mrp.workcenter.productivity'] domain = [('workorder_id', 'in', self.ids), ('date_end', '=', False)] if not doall: domain.append(('user_id', '=', self.env.user.id)) not_productive_timelines = timeline_obj.browse() for timeline in timeline_obj.search(domain, limit=None if doall else 1): wo = timeline.workorder_id if wo.duration_expected <= wo.duration: if timeline.loss_type == 'productive': not_productive_timelines += timeline timeline.write({'date_end': fields.Datetime.now()}) else: maxdate = fields.Datetime.from_string( timeline.date_start) + relativedelta( minutes=wo.duration_expected - wo.duration) enddate = datetime.now() if maxdate > enddate: timeline.write({'date_end': enddate}) else: timeline.write({'date_end': maxdate}) not_productive_timelines += timeline.copy({ 'date_start': maxdate, 'date_end': enddate }) if not_productive_timelines: loss_id = self.env['mrp.workcenter.productivity.loss'].search( [('loss_type', '=', 'performance')], limit=1) if not len(loss_id): raise UserError( _("You need to define at least one unactive productivity loss in the category 'Performance'. Create one from the Manufacturing app, menu: Configuration / Productivity Losses." )) not_productive_timelines.write({'loss_id': loss_id.id}) return True @api.multi def end_all(self): return self.end_previous(doall=True) @api.multi def button_pending(self): self.end_previous() return True @api.multi def button_unblock(self): for order in self: order.workcenter_id.unblock() return True @api.multi def action_cancel(self): return self.write({'state': 'cancel'}) @api.multi def button_done(self): if any([x.state in ('done', 'cancel') for x in self]): raise UserError( _('A Manufacturing Order is already done or cancelled!')) self.end_all() return self.write({'state': 'done', 'date_finished': datetime.now()}) @api.multi def button_scrap(self): self.ensure_one() return { 'name': _('Scrap'), 'view_type': 'form', 'view_mode': 'form', 'res_model': 'stock.scrap', 'view_id': self.env.ref('stock.stock_scrap_form_view2').id, 'type': 'ir.actions.act_window', 'context': { 'default_workorder_id': self.id, 'default_production_id': self.production_id.id, 'product_ids': (self.production_id.move_raw_ids.filtered( lambda x: x.state not in ('done', 'cancel')) | self.production_id.move_finished_ids.filtered( lambda x: x.state == 'done')).mapped('product_id').ids }, # 'context': {'product_ids': self.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel')).mapped('product_id').ids + [self.production_id.product_id.id]}, 'target': 'new', } @api.multi def action_see_move_scrap(self): self.ensure_one() action = self.env.ref('stock.action_stock_scrap').read()[0] action['domain'] = [('workorder_id', '=', self.id)] return action @api.multi @api.depends('qty_production', 'qty_produced') def _compute_qty_remaining(self): for wo in self: wo.qty_remaining = float_round( wo.qty_production - wo.qty_produced, precision_rounding=wo.production_id.product_uom_id.rounding)
class Track(models.Model): _name = "event.track" _description = 'Event Track' _order = 'priority, date' _inherit = ['mail.thread', 'mail.activity.mixin', 'website.seo.metadata', 'website.published.mixin'] @api.model def _get_default_stage_id(self): return self.env['event.track.stage'].search([], limit=1).id name = fields.Char('Title', required=True, translate=True) active = fields.Boolean(default=True) user_id = fields.Many2one('res.users', 'Responsible', track_visibility='onchange', default=lambda self: self.env.user) partner_id = fields.Many2one('res.partner', 'Speaker') partner_name = fields.Char('Speaker Name') partner_email = fields.Char('Speaker Email') partner_phone = fields.Char('Speaker Phone') partner_biography = fields.Html('Speaker Biography') tag_ids = fields.Many2many('event.track.tag', string='Tags') stage_id = fields.Many2one( 'event.track.stage', string='Stage', index=True, copy=False, default=_get_default_stage_id, group_expand='_read_group_stage_ids', required=True, track_visibility='onchange') kanban_state = fields.Selection([ ('normal', 'Grey'), ('done', 'Green'), ('blocked', 'Red')], string='Kanban State', copy=False, default='normal', required=True, track_visibility='onchange', help="A track's kanban state indicates special situations affecting it:\n" " * Grey is the default situation\n" " * Red indicates something is preventing the progress of this track\n" " * Green indicates the track is ready to be pulled to the next stage") description = fields.Html('Track Description', translate=html_translate, sanitize_attributes=False) date = fields.Datetime('Track Date') duration = fields.Float('Duration', default=1.5) location_id = fields.Many2one('event.track.location', 'Room') event_id = fields.Many2one('event.event', 'Event', required=True) color = fields.Integer('Color Index') priority = fields.Selection([ ('0', 'Low'), ('1', 'Medium'), ('2', 'High'), ('3', 'Highest')], 'Priority', required=True, default='1') image = fields.Binary('Image', related='partner_id.image_medium', store=True, attachment=True) @api.multi @api.depends('name') def _compute_website_url(self): super(Track, self)._compute_website_url() for track in self: if not isinstance(track.id, models.NewId): track.website_url = '/event/%s/track/%s' % (slug(track.event_id), slug(track)) @api.onchange('partner_id') def _onchange_partner_id(self): if self.partner_id: self.partner_name = self.partner_id.name self.partner_email = self.partner_id.email self.partner_phone = self.partner_id.phone self.partner_biography = self.partner_id.website_description @api.model def create(self, vals): track = super(Track, self).create(vals) track.event_id.message_post_with_view( 'website_event_track.event_track_template_new', values={'track': track}, subject=track.name, subtype_id=self.env.ref('website_event_track.mt_event_track').id, ) return track @api.multi def write(self, vals): if 'stage_id' in vals and 'kanban_state' not in vals: vals['kanban_state'] = 'normal' res = super(Track, self).write(vals) if vals.get('partner_id'): self.message_subscribe([vals['partner_id']]) return res @api.model def _read_group_stage_ids(self, stages, domain, order): """ Always display all stages """ return stages.search([], order=order) @api.multi def _track_template(self, tracking): res = super(Track, self)._track_template(tracking) track = self[0] changes, tracking_value_ids = tracking[track.id] if 'stage_id' in changes and track.stage_id.mail_template_id: res['stage_id'] = (track.stage_id.mail_template_id, {'composition_mode': 'mass_mail'}) return res @api.multi def _track_subtype(self, init_values): self.ensure_one() if 'kanban_state' in init_values and self.kanban_state == 'blocked': return 'website_event_track.mt_track_blocked' elif 'kanban_state' in init_values and self.kanban_state == 'done': return 'website_event_track.mt_track_ready' return super(Track, self)._track_subtype(init_values) @api.multi def message_get_suggested_recipients(self): recipients = super(Track, self).message_get_suggested_recipients() for track in self: if track.partner_email != track.partner_id.email: track._message_add_suggested_recipient(recipients, email=track.partner_email, reason=_('Speaker Email')) return recipients def _message_post_after_hook(self, message): if self.partner_email and not self.partner_id: # we consider that posting a message with a specified recipient (not a follower, a specific one) # on a document without customer means that it was created through the chatter using # suggested recipients. This heuristic allows to avoid ugly hacks in JS. new_partner = message.partner_ids.filtered(lambda partner: partner.email == self.partner_email) if new_partner: self.search([ ('partner_id', '=', False), ('partner_email', '=', new_partner.email), ('stage_id.is_cancel', '=', False), ]).write({'partner_id': new_partner.id}) return super(Track, self)._message_post_after_hook(message) @api.multi def open_track_speakers_list(self): return { 'name': _('Speakers'), 'domain': [('id', 'in', self.mapped('partner_id').ids)], 'view_type': 'form', 'view_mode': 'kanban,form', 'res_model': 'res.partner', 'view_id': False, 'type': 'ir.actions.act_window', }
class BlogPost(models.Model): _name = "blog.post" _description = "Blog Post" _inherit = [ 'mail.thread', 'website.seo.metadata', 'website.published.mixin' ] _order = 'id DESC' _mail_post_access = 'read' @api.multi def _compute_website_url(self): super(BlogPost, self)._compute_website_url() for blog_post in self: blog_post.website_url = "/blog/%s/post/%s" % (slug( blog_post.blog_id), slug(blog_post)) @api.multi @api.depends('post_date', 'visits') def _compute_ranking(self): res = {} for blog_post in self: if blog_post.id: # avoid to rank one post not yet saved and so withtout post_date in case of an onchange. age = datetime.now() - fields.Datetime.from_string( blog_post.post_date) res[blog_post.id] = blog_post.visits * ( 0.5 + random.random()) / max(3, age.days) return res def _default_content(self): return ''' <section class="s_text_block"> <div class="container"> <div class="row"> <div class="col-md-12 mb16 mt16"> <p class="o_default_snippet_text">''' + _( "Start writing here...") + '''</p> </div> </div> </div> </section> ''' name = fields.Char('Title', required=True, translate=True, default='') subtitle = fields.Char('Sub Title', translate=True) author_id = fields.Many2one('res.partner', 'Author', default=lambda self: self.env.user.partner_id) active = fields.Boolean('Active', default=True) cover_properties = fields.Text( 'Cover Properties', default= '{"background-image": "none", "background-color": "oe_black", "opacity": "0.2", "resize_class": ""}' ) blog_id = fields.Many2one('blog.blog', 'Blog', required=True, ondelete='cascade') tag_ids = fields.Many2many('blog.tag', string='Tags') content = fields.Html('Content', default=_default_content, translate=html_translate, sanitize=False) teaser = fields.Text('Teaser', compute='_compute_teaser', inverse='_set_teaser') teaser_manual = fields.Text(string='Teaser Content') website_message_ids = fields.One2many( domain=lambda self: [('model', '=', self._name), ('message_type', '=', 'comment'), ('path', '=', False)]) # creation / update stuff create_date = fields.Datetime('Created on', index=True, readonly=True) published_date = fields.Datetime('Published Date') post_date = fields.Datetime( 'Publishing date', compute='_compute_post_date', inverse='_set_post_date', store=True, help= "The blog post will be visible for your visitors as of this date on the website if it is set as published." ) create_uid = fields.Many2one('res.users', 'Created by', index=True, readonly=True) write_date = fields.Datetime('Last Modified on', index=True, readonly=True) write_uid = fields.Many2one('res.users', 'Last Contributor', index=True, readonly=True) author_avatar = fields.Binary(related='author_id.image_small', string="Avatar") visits = fields.Integer('No of Views', copy=False) ranking = fields.Float(compute='_compute_ranking', string='Ranking') @api.multi @api.depends('content', 'teaser_manual') def _compute_teaser(self): for blog_post in self: if blog_post.teaser_manual: blog_post.teaser = blog_post.teaser_manual else: content = html2plaintext(blog_post.content).replace('\n', ' ') blog_post.teaser = content[:150] + '...' @api.multi def _set_teaser(self): for blog_post in self: blog_post.teaser_manual = blog_post.teaser @api.multi @api.depends('create_date', 'published_date') def _compute_post_date(self): for blog_post in self: if blog_post.published_date: blog_post.post_date = blog_post.published_date else: blog_post.post_date = blog_post.create_date @api.multi def _set_post_date(self): for blog_post in self: blog_post.published_date = blog_post.post_date if not blog_post.published_date: blog_post._write(dict(post_date=blog_post.create_date) ) # dont trigger inverse function def _check_for_publication(self, vals): if vals.get('website_published'): for post in self: post.blog_id.message_post_with_view( 'website_blog.blog_post_template_new_post', subject=post.name, values={'post': post}, subtype_id=self.env['ir.model.data'].xmlid_to_res_id( 'website_blog.mt_blog_blog_published')) return True return False @api.model def create(self, vals): post_id = super(BlogPost, self.with_context(mail_create_nolog=True)).create(vals) post_id._check_for_publication(vals) return post_id @api.multi def write(self, vals): result = True for post in self: copy_vals = dict(vals) if 'website_published' in vals and 'published_date' not in vals and ( post.published_date or '') <= fields.Datetime.now(): copy_vals['published_date'] = vals[ 'website_published'] and fields.Datetime.now() or False result &= super(BlogPost, self).write(copy_vals) self._check_for_publication(vals) return result @api.multi def get_access_action(self, access_uid=None): """ Instead of the classic form view, redirect to the post on website directly if user is an employee or if the post is published. """ self.ensure_one() user = access_uid and self.env['res.users'].sudo().browse( access_uid) or self.env.user if user.share and not self.sudo().website_published: return super(BlogPost, self).get_access_action(access_uid) return { 'type': 'ir.actions.act_url', 'url': self.url, 'target': 'self', 'target_type': 'public', 'res_id': self.id, } @api.multi def _notification_recipients(self, message, groups): groups = super(BlogPost, self)._notification_recipients(message, groups) for group_name, group_method, group_data in groups: group_data['has_button_access'] = True return groups @api.multi def message_get_message_notify_values(self, message, message_values): """ Override to avoid keeping all notified recipients of a comment. We avoid tracking needaction on post comments. Only emails should be sufficient. """ if message.message_type == 'comment': return { 'needaction_partner_ids': [], } return {}
class PosConfig(models.Model): _name = 'pos.config' def _default_sale_journal(self): journal = self.env.ref('point_of_sale.pos_sale_journal', raise_if_not_found=False) if journal and journal.sudo().company_id == self.env.user.company_id: return journal return self._default_invoice_journal() def _default_invoice_journal(self): return self.env['account.journal'].search([('type', '=', 'sale'), ('company_id', '=', self.env.user.company_id.id)], limit=1) def _default_pricelist(self): return self.env['product.pricelist'].search([('currency_id', '=', self.env.user.company_id.currency_id.id)], limit=1) def _get_default_location(self): return self.env['stock.warehouse'].search([('company_id', '=', self.env.user.company_id.id)], limit=1).lot_stock_id def _get_group_pos_manager(self): return self.env.ref('point_of_sale.group_pos_manager') def _get_group_pos_user(self): return self.env.ref('point_of_sale.group_pos_user') def _compute_default_customer_html(self): return self.env['ir.qweb'].render('point_of_sale.customer_facing_display_html') name = fields.Char(string='Point of Sale Name', index=True, required=True, help="An internal identification of the point of sale.") is_installed_account_accountant = fields.Boolean(compute="_compute_is_installed_account_accountant") journal_ids = fields.Many2many( 'account.journal', 'pos_config_journal_rel', 'pos_config_id', 'journal_id', string='Available Payment Methods', domain="[('journal_user', '=', True ), ('type', 'in', ['bank', 'cash'])]",) picking_type_id = fields.Many2one('stock.picking.type', string='Operation Type') use_existing_lots = fields.Boolean(related='picking_type_id.use_existing_lots') stock_location_id = fields.Many2one( 'stock.location', string='Stock Location', domain=[('usage', '=', 'internal')], required=True, default=_get_default_location) journal_id = fields.Many2one( 'account.journal', string='Sales Journal', domain=[('type', '=', 'sale')], help="Accounting journal used to post sales entries.", default=_default_sale_journal) invoice_journal_id = fields.Many2one( 'account.journal', string='Invoice Journal', domain=[('type', '=', 'sale')], help="Accounting journal used to create invoices.", default=_default_invoice_journal) currency_id = fields.Many2one('res.currency', compute='_compute_currency', string="Currency") iface_cashdrawer = fields.Boolean(string='Cashdrawer', help="Automatically open the cashdrawer.") iface_payment_terminal = fields.Boolean(string='Payment Terminal', help="Enables Payment Terminal integration.") iface_electronic_scale = fields.Boolean(string='Electronic Scale', help="Enables Electronic Scale integration.") iface_vkeyboard = fields.Boolean(string='Virtual KeyBoard', help=u"Don’t turn this option on if you take orders on smartphones or tablets. \n Such devices already benefit from a native keyboard.") iface_customer_facing_display = fields.Boolean(string='Customer Facing Display', help="Show checkout to customers with a remotely-connected screen.") iface_print_via_proxy = fields.Boolean(string='Print via Proxy', help="Bypass browser printing and prints via the hardware proxy.") iface_scan_via_proxy = fields.Boolean(string='Scan via Proxy', help="Enable barcode scanning with a remotely connected barcode scanner.") iface_invoicing = fields.Boolean(string='Invoicing', help='Enables invoice generation from the Point of Sale.') iface_big_scrollbars = fields.Boolean('Large Scrollbars', help='For imprecise industrial touchscreens.') iface_print_auto = fields.Boolean(string='Automatic Receipt Printing', default=False, help='The receipt will automatically be printed at the end of each order.') iface_print_skip_screen = fields.Boolean(string='Skip Preview Screen', default=True, help='The receipt screen will be skipped if the receipt can be printed automatically.') iface_precompute_cash = fields.Boolean(string='Prefill Cash Payment', help='The payment input will behave similarily to bank payment input, and will be prefilled with the exact due amount.') iface_tax_included = fields.Selection([('subtotal', 'Tax-Excluded Prices'), ('total', 'Tax-Included Prices')], "Tax Display", default='subtotal', required=True) iface_start_categ_id = fields.Many2one('pos.category', string='Initial Category', help='The point of sale will display this product category by default. If no category is specified, all available products will be shown.') iface_display_categ_images = fields.Boolean(string='Display Category Pictures', help="The product categories will be displayed with pictures.") restrict_price_control = fields.Boolean(string='Restrict Price Modifications to Managers', help="Only users with Manager access rights for PoS app can modify the product prices on orders.") cash_control = fields.Boolean(string='Cash Control', help="Check the amount of the cashbox at opening and closing.") receipt_header = fields.Text(string='Receipt Header', help="A short text that will be inserted as a header in the printed receipt.") receipt_footer = fields.Text(string='Receipt Footer', help="A short text that will be inserted as a footer in the printed receipt.") proxy_ip = fields.Char(string='IP Address', size=45, help='The hostname or ip address of the hardware proxy, Will be autodetected if left empty.') active = fields.Boolean(default=True) uuid = fields.Char(readonly=True, default=lambda self: str(uuid.uuid4()), help='A globally unique identifier for this pos configuration, used to prevent conflicts in client-generated data.') sequence_id = fields.Many2one('ir.sequence', string='Order IDs Sequence', readonly=True, help="This sequence is automatically created by izi but you can change it " "to customize the reference numbers of your orders.", copy=False) sequence_line_id = fields.Many2one('ir.sequence', string='Order Line IDs Sequence', readonly=True, help="This sequence is automatically created by izi but you can change it " "to customize the reference numbers of your orders lines.", copy=False) session_ids = fields.One2many('pos.session', 'config_id', string='Sessions') current_session_id = fields.Many2one('pos.session', compute='_compute_current_session', string="Current Session") current_session_state = fields.Char(compute='_compute_current_session') last_session_closing_cash = fields.Float(compute='_compute_last_session') last_session_closing_date = fields.Date(compute='_compute_last_session') pos_session_username = fields.Char(compute='_compute_current_session_user') pos_session_state = fields.Char(compute='_compute_current_session_user') group_by = fields.Boolean(string='Group Journal Items', default=True, help="Check this if you want to group the Journal Items by Product while closing a Session.") pricelist_id = fields.Many2one('product.pricelist', string='Default Pricelist', required=True, default=_default_pricelist, help="The pricelist used if no customer is selected or if the customer has no Sale Pricelist configured.") available_pricelist_ids = fields.Many2many('product.pricelist', string='Available Pricelists', default=_default_pricelist, help="Make several pricelists available in the Point of Sale. You can also apply a pricelist to specific customers from their contact form (in Sales tab). To be valid, this pricelist must be listed here as an available pricelist. Otherwise the default pricelist will apply.") company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.user.company_id) barcode_nomenclature_id = fields.Many2one('barcode.nomenclature', string='Barcode Nomenclature', help='Defines what kind of barcodes are available and how they are assigned to products, customers and cashiers.') group_pos_manager_id = fields.Many2one('res.groups', string='Point of Sale Manager Group', default=_get_group_pos_manager, help='This field is there to pass the id of the pos manager group to the point of sale client.') group_pos_user_id = fields.Many2one('res.groups', string='Point of Sale User Group', default=_get_group_pos_user, help='This field is there to pass the id of the pos user group to the point of sale client.') iface_tipproduct = fields.Boolean(string="Product tips") tip_product_id = fields.Many2one('product.product', string='Tip Product', help="This product is used as reference on customer receipts.") fiscal_position_ids = fields.Many2many('account.fiscal.position', string='Fiscal Positions', help='This is useful for restaurants with onsite and take-away services that imply specific tax rates.') default_fiscal_position_id = fields.Many2one('account.fiscal.position', string='Default Fiscal Position') default_cashbox_lines_ids = fields.One2many('account.cashbox.line', 'default_pos_id', string='Default Balance') customer_facing_display_html = fields.Html(string='Customer facing display content', translate=True, default=_compute_default_customer_html) use_pricelist = fields.Boolean("Use a pricelist.") group_sale_pricelist = fields.Boolean("Use pricelists to adapt your price per customers", implied_group='product.group_sale_pricelist', help="""Allows to manage different prices based on rules per category of customers. Example: 10% for retailers, promotion of 5 EUR on this product, etc.""") group_pricelist_item = fields.Boolean("Show pricelists to customers", implied_group='product.group_pricelist_item') tax_regime = fields.Boolean("Tax Regime") tax_regime_selection = fields.Boolean("Tax Regime Selection value") barcode_scanner = fields.Boolean("Barcode Scanner") start_category = fields.Boolean("Set Start Category") module_pos_restaurant = fields.Boolean("Is a Bar/Restaurant") module_pos_discount = fields.Boolean("Global Discounts") module_pos_loyalty = fields.Boolean("Loyalty Program") module_pos_mercury = fields.Boolean(string="Integrated Card Payments") module_pos_reprint = fields.Boolean(string="Reprint Receipt") is_posbox = fields.Boolean("PosBox") is_header_or_footer = fields.Boolean("Header & Footer") def _compute_is_installed_account_accountant(self): account_accountant = self.env['ir.module.module'].sudo().search([('name', '=', 'account_accountant'), ('state', '=', 'installed')]) for pos_config in self: pos_config.is_installed_account_accountant = account_accountant and account_accountant.id @api.depends('journal_id.currency_id', 'journal_id.company_id.currency_id') def _compute_currency(self): for pos_config in self: if pos_config.journal_id: pos_config.currency_id = pos_config.journal_id.currency_id.id or pos_config.journal_id.company_id.currency_id.id else: pos_config.currency_id = self.env.user.company_id.currency_id.id @api.depends('session_ids') def _compute_current_session(self): for pos_config in self: session = pos_config.session_ids.filtered(lambda r: r.user_id.id == self.env.uid and \ not r.state == 'closed' and \ not r.rescue) # sessions ordered by id desc pos_config.current_session_id = session and session[0].id or False pos_config.current_session_state = session and session[0].state or False @api.depends('session_ids') def _compute_last_session(self): PosSession = self.env['pos.session'] for pos_config in self: session = PosSession.search_read( [('config_id', '=', pos_config.id), ('state', '=', 'closed')], ['cash_register_balance_end_real', 'stop_at'], order="stop_at desc", limit=1) if session: pos_config.last_session_closing_cash = session[0]['cash_register_balance_end_real'] pos_config.last_session_closing_date = session[0]['stop_at'] else: pos_config.last_session_closing_cash = 0 pos_config.last_session_closing_date = False @api.depends('session_ids') def _compute_current_session_user(self): for pos_config in self: session = pos_config.session_ids.filtered(lambda s: s.state in ['opening_control', 'opened', 'closing_control'] and not s.rescue) pos_config.pos_session_username = session and session[0].user_id.name or False pos_config.pos_session_state = session and session[0].state or False @api.constrains('company_id', 'stock_location_id') def _check_company_location(self): if self.stock_location_id.company_id and self.stock_location_id.company_id.id != self.company_id.id: raise ValidationError(_("The company of the stock location is different than the one of point of sale")) @api.constrains('company_id', 'journal_id') def _check_company_journal(self): if self.journal_id and self.journal_id.company_id.id != self.company_id.id: raise ValidationError(_("The company of the sales journal is different than the one of point of sale")) @api.constrains('company_id', 'invoice_journal_id') def _check_company_invoice_journal(self): if self.invoice_journal_id and self.invoice_journal_id.company_id.id != self.company_id.id: raise ValidationError(_("The invoice journal and the point of sale must belong to the same company")) @api.constrains('company_id', 'journal_ids') def _check_company_payment(self): if self.env['account.journal'].search_count([('id', 'in', self.journal_ids.ids), ('company_id', '!=', self.company_id.id)]): raise ValidationError(_("The company of a payment method is different than the one of point of sale")) @api.constrains('pricelist_id', 'available_pricelist_ids', 'journal_id', 'invoice_journal_id', 'journal_ids') def _check_currencies(self): if self.pricelist_id not in self.available_pricelist_ids: raise ValidationError(_("The default pricelist must be included in the available pricelists.")) if any(self.available_pricelist_ids.mapped(lambda pricelist: pricelist.currency_id != self.currency_id)): raise ValidationError(_("All available pricelists must be in the same currency as the company or" " as the Sales Journal set on this point of sale if you use" " the Accounting application.")) if self.invoice_journal_id.currency_id and self.invoice_journal_id.currency_id != self.currency_id: raise ValidationError(_("The invoice journal must be in the same currency as the Sales Journal or the company currency if that is not set.")) if any(self.journal_ids.mapped(lambda journal: journal.currency_id and journal.currency_id != self.currency_id)): raise ValidationError(_("All payment methods must be in the same currency as the Sales Journal or the company currency if that is not set.")) @api.onchange('iface_print_via_proxy') def _onchange_iface_print_via_proxy(self): self.iface_print_auto = self.iface_print_via_proxy @api.onchange('picking_type_id') def _onchange_picking_type_id(self): if self.picking_type_id.default_location_src_id.usage == 'internal' and self.picking_type_id.default_location_dest_id.usage == 'customer': self.stock_location_id = self.picking_type_id.default_location_src_id.id @api.onchange('use_pricelist') def _onchange_use_pricelist(self): """ If the 'pricelist' box is unchecked, we reset the pricelist_id to stop using a pricelist for this posbox. """ if not self.use_pricelist: self.pricelist_id = self._default_pricelist() else: self.update({ 'group_sale_pricelist': True, 'group_pricelist_item': True, }) @api.onchange('available_pricelist_ids') def _onchange_available_pricelist_ids(self): if self.pricelist_id not in self.available_pricelist_ids: self.pricelist_id = False @api.onchange('iface_scan_via_proxy') def _onchange_iface_scan_via_proxy(self): if self.iface_scan_via_proxy: self.barcode_scanner = True else: self.barcode_scanner = False @api.onchange('barcode_scanner') def _onchange_barcode_scanner(self): if self.barcode_scanner: self.barcode_nomenclature_id = self.env['barcode.nomenclature'].search([], limit=1) else: self.barcode_nomenclature_id = False @api.onchange('is_posbox') def _onchange_is_posbox(self): if not self.is_posbox: self.proxy_ip = False self.iface_scan_via_proxy = False self.iface_electronic_scale = False self.iface_cashdrawer = False self.iface_print_via_proxy = False self.iface_customer_facing_display = False @api.onchange('tax_regime') def _onchange_tax_regime(self): if not self.tax_regime: self.default_fiscal_position_id = False @api.onchange('tax_regime_selection') def _onchange_tax_regime_selection(self): if not self.tax_regime_selection: self.fiscal_position_ids = [(5, 0, 0)] @api.onchange('start_category') def _onchange_start_category(self): if not self.start_category: self.iface_start_categ_id = False @api.onchange('is_header_or_footer') def _onchange_header_footer(self): if not self.is_header_or_footer: self.receipt_header = False self.receipt_footer = False @api.multi def name_get(self): result = [] for config in self: if (not config.session_ids) or (config.session_ids[0].state == 'closed'): result.append((config.id, config.name + ' (' + _('not used') + ')')) continue result.append((config.id, config.name + ' (' + config.session_ids[0].user_id.name + ')')) return result @api.model def create(self, values): if values.get('is_posbox') and values.get('iface_customer_facing_display'): if values.get('customer_facing_display_html') and not values['customer_facing_display_html'].strip(): values['customer_facing_display_html'] = self._compute_default_customer_html() IrSequence = self.env['ir.sequence'].sudo() val = { 'name': _('POS Order %s') % values['name'], 'padding': 4, 'prefix': "%s/" % values['name'], 'code': "pos.order", 'company_id': values.get('company_id', False), } # force sequence_id field to new pos.order sequence values['sequence_id'] = IrSequence.create(val).id val.update(name=_('POS order line %s') % values['name'], code='pos.order.line') values['sequence_line_id'] = IrSequence.create(val).id pos_config = super(PosConfig, self).create(values) pos_config.sudo()._check_modules_to_install() pos_config.sudo()._check_groups_implied() # If you plan to add something after this, use a new environment. The one above is no longer valid after the modules install. return pos_config @api.multi def write(self, vals): if (self.is_posbox or vals.get('is_posbox')) and (self.iface_customer_facing_display or vals.get('iface_customer_facing_display')): facing_display = (self.customer_facing_display_html or vals.get('customer_facing_display_html') or '').strip() if not facing_display: vals['customer_facing_display_html'] = self._compute_default_customer_html() result = super(PosConfig, self).write(vals) self.sudo()._set_fiscal_position() self.sudo()._check_modules_to_install() self.sudo()._check_groups_implied() return result @api.multi def unlink(self): for pos_config in self.filtered(lambda pos_config: pos_config.sequence_id or pos_config.sequence_line_id): pos_config.sequence_id.unlink() pos_config.sequence_line_id.unlink() return super(PosConfig, self).unlink() def _set_fiscal_position(self): for config in self: if config.tax_regime and config.default_fiscal_position_id.id not in config.fiscal_position_ids.ids: config.fiscal_position_ids = [(4, config.default_fiscal_position_id.id)] elif not config.tax_regime_selection and not config.tax_regime and config.fiscal_position_ids.ids: config.fiscal_position_ids = [(5, 0, 0)] def _check_modules_to_install(self): module_installed = False for pos_config in self: for field_name in [f for f in pos_config.fields_get_keys() if f.startswith('module_')]: module_name = field_name.split('module_')[1] module_to_install = self.env['ir.module.module'].sudo().search([('name', '=', module_name)]) if getattr(pos_config, field_name) and module_to_install.state not in ('installed', 'to install', 'to upgrade'): module_to_install.button_immediate_install() module_installed = True # just in case we want to do something if we install a module. (like a refresh ...) return module_installed def _check_groups_implied(self): for pos_config in self: for field_name in [f for f in pos_config.fields_get_keys() if f.startswith('group_')]: field = pos_config._fields[field_name] if field.type in ('boolean', 'selection') and hasattr(field, 'implied_group'): field_group_xmlids = getattr(field, 'group', 'base.group_user').split(',') field_groups = self.env['res.groups'].concat(*(self.env.ref(it) for it in field_group_xmlids)) field_groups.write({'implied_ids': [(4, self.env.ref(field.implied_group).id)]}) def execute(self): return { 'type': 'ir.actions.client', 'tag': 'reload', 'params': {'wait': True} } # Methods to open the POS @api.multi def open_ui(self): """ open the pos interface """ self.ensure_one() return { 'type': 'ir.actions.act_url', 'url': '/pos/web/', 'target': 'self', } @api.multi def open_session_cb(self): """ new session button create one if none exist access cash control interface if enabled or start a session """ self.ensure_one() if not self.current_session_id: self.current_session_id = self.env['pos.session'].create({ 'user_id': self.env.uid, 'config_id': self.id }) if self.current_session_id.state == 'opened': return self.open_ui() return self._open_session(self.current_session_id.id) return self._open_session(self.current_session_id.id) @api.multi def open_existing_session_cb(self): """ close session button access session form to validate entries """ self.ensure_one() return self._open_session(self.current_session_id.id) def _open_session(self, session_id): return { 'name': _('Session'), 'view_type': 'form', 'view_mode': 'form,tree', 'res_model': 'pos.session', 'res_id': session_id, 'view_id': False, 'type': 'ir.actions.act_window', }
class IrActions(models.Model): _name = 'ir.actions.actions' _table = 'ir_actions' _order = 'name' name = fields.Char(required=True) type = fields.Char(string='Action Type', required=True) xml_id = fields.Char(compute='_compute_xml_id', string="External ID") help = fields.Html( string='Action Description', help= 'Optional help text for the users with a description of the target view, such as its usage and purpose.', translate=True) binding_model_id = fields.Many2one( 'ir.model', ondelete='cascade', help= "Setting a value makes this action available in the sidebar for the given model." ) binding_type = fields.Selection([('action', 'Action'), ('action_form_only', "Form-only"), ('report', 'Report')], required=True, default='action') def _compute_xml_id(self): res = self.get_external_id() for record in self: record.xml_id = res.get(record.id) @api.model def create(self, vals): res = super(IrActions, self).create(vals) # self.get_bindings() depends on action records self.clear_caches() return res @api.multi def write(self, vals): res = super(IrActions, self).write(vals) # self.get_bindings() depends on action records self.clear_caches() return res @api.multi def unlink(self): """unlink ir.action.todo which are related to actions which will be deleted. NOTE: ondelete cascade will not work on ir.actions.actions so we will need to do it manually.""" todos = self.env['ir.actions.todo'].search([('action_id', 'in', self.ids)]) todos.unlink() res = super(IrActions, self).unlink() # self.get_bindings() depends on action records self.clear_caches() return res @api.model def _get_eval_context(self, action=None): """ evaluation context to pass to safe_eval """ return { 'uid': self._uid, 'user': self.env.user, 'time': time, 'datetime': datetime, 'dateutil': dateutil, 'timezone': timezone, 'b64encode': base64.b64encode, 'b64decode': base64.b64decode, } @api.model @tools.ormcache('frozenset(self.env.user.groups_id.ids)', 'model_name') def get_bindings(self, model_name): """ Retrieve the list of actions bound to the given model. :return: a dict mapping binding types to a list of dict describing actions, where the latter is given by calling the method ``read`` on the action record. """ cr = self.env.cr query = """ SELECT a.id, a.type, a.binding_type FROM ir_actions a, ir_model m WHERE a.binding_model_id=m.id AND m.model=%s ORDER BY a.id """ cr.execute(query, [model_name]) # discard unauthorized actions, and read action definitions result = defaultdict(list) user_groups = self.env.user.groups_id for action_id, action_model, binding_type in cr.fetchall(): try: action = self.env[action_model].browse(action_id) action_groups = getattr(action, 'groups_id', ()) if action_groups and not action_groups & user_groups: # the user may not perform this action continue result[binding_type].append(action.read()[0]) except (AccessError, MissingError): continue return result
class EventEvent(models.Model): """Event""" _name = 'event.event' _description = 'Event' _inherit = ['mail.thread'] _order = 'date_begin' name = fields.Char( string='Event Name', translate=True, required=True, readonly=False, states={'done': [('readonly', True)]}) active = fields.Boolean(default=True) user_id = fields.Many2one( 'res.users', string='Responsible', default=lambda self: self.env.user, track_visibility="onchange", readonly=False, states={'done': [('readonly', True)]}) company_id = fields.Many2one( 'res.company', string='Company', change_default=True, default=lambda self: self.env['res.company']._company_default_get('event.event'), required=False, readonly=False, states={'done': [('readonly', True)]}) organizer_id = fields.Many2one( 'res.partner', string='Organizer', track_visibility="onchange", default=lambda self: self.env.user.company_id.partner_id) event_type_id = fields.Many2one( 'event.type', string='Category', readonly=False, states={'done': [('readonly', True)]}, oldname='type') color = fields.Integer('Kanban Color Index') event_mail_ids = fields.One2many('event.mail', 'event_id', string='Mail Schedule', copy=True) # Seats and computation seats_max = fields.Integer( string='Maximum Attendees Number', oldname='register_max', readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, help="For each event you can define a maximum registration of seats(number of attendees), above this numbers the registrations are not accepted.") seats_availability = fields.Selection( [('limited', 'Limited'), ('unlimited', 'Unlimited')], 'Maximum Attendees', required=True, default='unlimited') seats_min = fields.Integer( string='Minimum Attendees', oldname='register_min', help="For each event you can define a minimum reserved seats (number of attendees), if it does not reach the mentioned registrations the event can not be confirmed (keep 0 to ignore this rule)") seats_reserved = fields.Integer( oldname='register_current', string='Reserved Seats', store=True, readonly=True, compute='_compute_seats') seats_available = fields.Integer( oldname='register_avail', string='Available Seats', store=True, readonly=True, compute='_compute_seats') seats_unconfirmed = fields.Integer( oldname='register_prospect', string='Unconfirmed Seat Reservations', store=True, readonly=True, compute='_compute_seats') seats_used = fields.Integer( oldname='register_attended', string='Number of Participants', store=True, readonly=True, compute='_compute_seats') seats_expected = fields.Integer( string='Number of Expected Attendees', readonly=True, compute='_compute_seats') # Registration fields registration_ids = fields.One2many( 'event.registration', 'event_id', string='Attendees', readonly=False, states={'done': [('readonly', True)]}) # Date fields date_tz = fields.Selection('_tz_get', string='Timezone', required=True, default=lambda self: self.env.user.tz or 'UTC') date_begin = fields.Datetime( string='Start Date', required=True, track_visibility='onchange', states={'done': [('readonly', True)]}) date_end = fields.Datetime( string='End Date', required=True, track_visibility='onchange', states={'done': [('readonly', True)]}) date_begin_located = fields.Char(string='Start Date Located', compute='_compute_date_begin_tz') date_end_located = fields.Char(string='End Date Located', compute='_compute_date_end_tz') state = fields.Selection([ ('draft', 'Unconfirmed'), ('cancel', 'Cancelled'), ('confirm', 'Confirmed'), ('done', 'Done')], string='Status', default='draft', readonly=True, required=True, copy=False, help="If event is created, the status is 'Draft'. If event is confirmed for the particular dates the status is set to 'Confirmed'. If the event is over, the status is set to 'Done'. If event is cancelled the status is set to 'Cancelled'.") auto_confirm = fields.Boolean(string='Autoconfirm Registrations') is_online = fields.Boolean('Online Event') address_id = fields.Many2one( 'res.partner', string='Location', default=lambda self: self.env.user.company_id.partner_id, readonly=False, states={'done': [('readonly', True)]}, track_visibility="onchange") country_id = fields.Many2one('res.country', 'Country', related='address_id.country_id', store=True) twitter_hashtag = fields.Char('Twitter Hashtag') description = fields.Html( string='Description', oldname='note', translate=html_translate, sanitize_attributes=False, readonly=False, states={'done': [('readonly', True)]}) # badge fields badge_front = fields.Html(string='Badge Front') badge_back = fields.Html(string='Badge Back') badge_innerleft = fields.Html(string='Badge Inner Left') badge_innerright = fields.Html(string='Badge Inner Right') event_logo = fields.Html(string='Event Logo') @api.multi @api.depends('seats_max', 'registration_ids.state') def _compute_seats(self): """ Determine reserved, available, reserved but unconfirmed and used seats. """ # initialize fields to 0 for event in self: event.seats_unconfirmed = event.seats_reserved = event.seats_used = event.seats_available = 0 # aggregate registrations by event and by state if self.ids: state_field = { 'draft': 'seats_unconfirmed', 'open': 'seats_reserved', 'done': 'seats_used', } query = """ SELECT event_id, state, count(event_id) FROM event_registration WHERE event_id IN %s AND state IN ('draft', 'open', 'done') GROUP BY event_id, state """ self._cr.execute(query, (tuple(self.ids),)) for event_id, state, num in self._cr.fetchall(): event = self.browse(event_id) event[state_field[state]] += num # compute seats_available for event in self: if event.seats_max > 0: event.seats_available = event.seats_max - (event.seats_reserved + event.seats_used) event.seats_expected = event.seats_unconfirmed + event.seats_reserved + event.seats_used @api.model def _tz_get(self): return [(x, x) for x in pytz.all_timezones] @api.one @api.depends('date_tz', 'date_begin') def _compute_date_begin_tz(self): if self.date_begin: self.date_begin_located = format_tz(self.with_context(use_babel=True).env, self.date_begin, tz=self.date_tz) else: self.date_begin_located = False @api.one @api.depends('date_tz', 'date_end') def _compute_date_end_tz(self): if self.date_end: self.date_end_located = format_tz(self.with_context(use_babel=True).env, self.date_end, tz=self.date_tz) else: self.date_end_located = False @api.onchange('event_type_id') def _onchange_type(self): if self.event_type_id: self.seats_min = self.event_type_id.default_registration_min self.seats_max = self.event_type_id.default_registration_max if self.event_type_id.default_registration_max: self.seats_availability = 'limited' if self.event_type_id.auto_confirm: self.auto_confirm = self.event_type_id.auto_confirm if self.event_type_id.use_hashtag: self.twitter_hashtag = self.event_type_id.default_hashtag if self.event_type_id.use_timezone: self.date_tz = self.event_type_id.default_timezone self.is_online = self.event_type_id.is_online if self.event_type_id.event_type_mail_ids: self.event_mail_ids = [(5, 0, 0)] + [{ 'template_id': line.template_id, 'interval_nbr': line.interval_nbr, 'interval_unit': line.interval_unit, 'interval_type': line.interval_type} for line in self.event_type_id.event_type_mail_ids] @api.constrains('seats_min', 'seats_max', 'seats_availability') def _check_seats_min_max(self): if any(event.seats_availability == 'limited' and event.seats_min > event.seats_max for event in self): raise ValidationError(_('Maximum attendees number should be greater than minimum attendees number.')) @api.constrains('seats_max', 'seats_available') def _check_seats_limit(self): if any(event.seats_availability == 'limited' and event.seats_max and event.seats_available < 0 for event in self): raise ValidationError(_('No more available seats.')) @api.one @api.constrains('date_begin', 'date_end') def _check_closing_date(self): if self.date_end < self.date_begin: raise ValidationError(_('Closing Date cannot be set before Beginning Date.')) @api.multi @api.depends('name', 'date_begin', 'date_end') def name_get(self): result = [] for event in self: date_begin = fields.Datetime.from_string(event.date_begin) date_end = fields.Datetime.from_string(event.date_end) dates = [fields.Date.to_string(fields.Datetime.context_timestamp(event, dt)) for dt in [date_begin, date_end] if dt] dates = sorted(set(dates)) result.append((event.id, '%s (%s)' % (event.name, ' - '.join(dates)))) return result @api.model def create(self, vals): res = super(EventEvent, self).create(vals) if res.organizer_id: res.message_subscribe([res.organizer_id.id]) if res.auto_confirm: res.button_confirm() return res @api.multi def write(self, vals): res = super(EventEvent, self).write(vals) if vals.get('organizer_id'): self.message_subscribe([vals['organizer_id']]) return res @api.multi def copy(self, default=None): self.ensure_one() default = dict(default or {}, name=_("%s (copy)") % (self.name)) return super(EventEvent, self).copy(default) @api.one def button_draft(self): self.state = 'draft' @api.multi def button_cancel(self): if any('done' in event.mapped('registration_ids.state') for event in self): raise UserError(_("There are already attendees who attended this event. Please reset it to draft if you want to cancel this event.")) self.registration_ids.write({'state': 'cancel'}) self.state = 'cancel' @api.one def button_done(self): self.state = 'done' @api.one def button_confirm(self): self.state = 'confirm' @api.one def mail_attendees(self, template_id, force_send=False, filter_func=lambda self: self.state != 'cancel'): for attendee in self.registration_ids.filtered(filter_func): self.env['mail.template'].browse(template_id).send_mail(attendee.id, force_send=force_send) @api.multi def _is_event_registrable(self): return True
class CrmLeadForwardToPartner(models.TransientModel): """ Forward info history to partners. """ _name = 'crm.lead.forward.to.partner' @api.model def _convert_to_assignation_line(self, lead, partner): lead_location = [] partner_location = [] if lead.country_id: lead_location.append(lead.country_id.name) if lead.city: lead_location.append(lead.city) if partner: if partner.country_id: partner_location.append(partner.country_id.name) if partner.city: partner_location.append(partner.city) return { 'lead_id': lead.id, 'lead_location': ", ".join(lead_location), 'partner_assigned_id': partner and partner.id or False, 'partner_location': ", ".join(partner_location), 'lead_link': self.get_lead_portal_url(lead.id, lead.type), } @api.model def default_get(self, fields): template = self.env.ref( 'website_crm_partner_assign.email_template_lead_forward_mail', False) res = super(CrmLeadForwardToPartner, self).default_get(fields) active_ids = self.env.context.get('active_ids') default_composition_mode = self.env.context.get( 'default_composition_mode') res['assignation_lines'] = [] if template: res['body'] = template.body_html if active_ids: leads = self.env['crm.lead'].browse(active_ids) if default_composition_mode == 'mass_mail': partner_assigned_dict = leads.search_geo_partner() else: partner_assigned_dict = { lead.id: lead.partner_assigned_id.id for lead in leads } res['partner_id'] = leads[0].partner_assigned_id.id for lead in leads: partner_id = partner_assigned_dict.get(lead.id) or False partner = self.env['res.partner'].browse(partner_id) res['assignation_lines'].append( (0, 0, self._convert_to_assignation_line(lead, partner))) return res @api.multi def action_forward(self): self.ensure_one() template = self.env.ref( 'website_crm_partner_assign.email_template_lead_forward_mail', False) if not template: raise UserError( _('The Forward Email Template is not in the database')) portal_group = self.env.ref('base.group_portal') local_context = self.env.context.copy() if not (self.forward_type == 'single'): no_email = set() for lead in self.assignation_lines: if lead.partner_assigned_id and not lead.partner_assigned_id.email: no_email.add(lead.partner_assigned_id.name) if no_email: raise UserError( _('Set an email address for the partner(s): %s') % ", ".join(no_email)) if self.forward_type == 'single' and not self.partner_id.email: raise UserError( _('Set an email address for the partner %s') % self.partner_id.name) partners_leads = {} for lead in self.assignation_lines: partner = self.forward_type == 'single' and self.partner_id or lead.partner_assigned_id lead_details = { 'lead_link': lead.lead_link, 'lead_id': lead.lead_id, } if partner: partner_leads = partners_leads.get(partner.id) if partner_leads: partner_leads['leads'].append(lead_details) else: partners_leads[partner.id] = { 'partner': partner, 'leads': [lead_details] } for partner_id, partner_leads in partners_leads.items(): in_portal = False if portal_group: for contact in ( partner.child_ids or partner).filtered(lambda contact: contact.user_ids): in_portal = portal_group.id in [ g.id for g in contact.user_ids[0].groups_id ] local_context['partner_id'] = partner_leads['partner'] local_context['partner_leads'] = partner_leads['leads'] local_context['partner_in_portal'] = in_portal template.with_context(local_context).send_mail(self.id) leads = self.env['crm.lead'] for lead_data in partner_leads['leads']: leads |= lead_data['lead_id'] values = { 'partner_assigned_id': partner_id, 'user_id': partner_leads['partner'].user_id.id } leads.with_context(mail_auto_subscribe_no_notify=1).write(values) self.env['crm.lead'].message_subscribe([partner_id]) return True def get_lead_portal_url(self, lead_id, type): action = type == 'opportunity' and 'action_portal_opportunities' or 'action_portal_leads' action_ref = self.env.ref('website_crm_partner_assign.%s' % (action, ), False) portal_link = "%s/?db=%s#id=%s&action=%s&view_type=form" % ( self.env['ir.config_parameter'].sudo().get_param('web.base.url'), self.env.cr.dbname, lead_id, action_ref and action_ref.id or False) return portal_link def get_portal_url(self): portal_link = "%s/?db=%s" % (self.env['ir.config_parameter'].sudo( ).get_param('web.base.url'), self.env.cr.dbname) return portal_link forward_type = fields.Selection([ ('single', 'a single partner: manual selection of partner'), ('assigned', "several partners: automatic assignation, using GPS coordinates and partner's grades" ) ], 'Forward selected leads to', default=lambda self: self.env.context.get( 'forward_type') or 'single') partner_id = fields.Many2one('res.partner', 'Forward Leads To') assignation_lines = fields.One2many('crm.lead.assignation', 'forward_id', 'Partner Assignation') body = fields.Html('Contents', help='Automatically sanitized HTML contents')