class ProductTemplate(models.Model): _inherit = "product.template" quotation_only_description = fields.Html( 'Quotation Only Description', sanitize_attributes=False, translate=html_translate, help="The quotation description (not used on eCommerce)") quotation_description = fields.Html( 'Quotation Description', compute='_compute_quotation_description', help= "This field uses the Quotation Only Description if it is defined, otherwise it will try to read the eCommerce Description." ) @api.multi def _compute_quotation_description(self): for record in self: if record.quotation_only_description: record.quotation_description = record.quotation_only_description elif hasattr(record, 'website_description') and record.website_description: record.quotation_description = record.website_description else: record.quotation_description = ''
class KarmaRank(models.Model): _name = 'gamification.karma.rank' _description = 'Rank based on karma' _inherit = 'image.mixin' _order = 'karma_min' name = fields.Text(string='Rank Name', translate=True, required=True) description = fields.Html( string='Description', translate=html_translate, sanitize_attributes=False, ) description_motivational = fields.Html( string='Motivational', translate=html_translate, sanitize_attributes=False, help="Motivational phrase to reach this rank") karma_min = fields.Integer(string='Required Karma', help='Minimum karma needed to reach this rank') user_ids = fields.One2many('res.users', 'rank_id', string='Users', help="Users having this rank") @api.model_create_multi def create(self, values_list): res = super(KarmaRank, self).create(values_list) users = self.env['res.users'].sudo().search([('karma', '>', 0)]) users._recompute_rank() return res def write(self, vals): if 'karma_min' in vals: previous_ranks = self.env['gamification.karma.rank'].search( [], order="karma_min DESC").ids low = min(vals['karma_min'], self.karma_min) high = max(vals['karma_min'], self.karma_min) res = super(KarmaRank, self).write(vals) if 'karma_min' in vals: after_ranks = self.env['gamification.karma.rank'].search( [], order="karma_min DESC").ids if previous_ranks != after_ranks: users = self.env['res.users'].sudo().search([('karma', '>', 0) ]) else: users = self.env['res.users'].sudo().search([ ('karma', '>=', low), ('karma', '<=', high) ]) users._recompute_rank() return res
class User(models.Model): _inherit = ["res.users", "website_dependent.mixin"] _name = "res.users" signature = fields.Html(company_dependent=True, website_dependent=True) # extra field to detach email field from res.partner email = fields.Char(string="Multi Website Email", related="email_multi_website", inherited=False) email_multi_website = fields.Char(company_dependent=True, website_dependent=True) @api.model def create(self, vals): res = super(User, self).create(vals) # make value company independent res._force_default(FIELD_NAME, vals.get("email")) for f in FIELDS: res._force_default(f, vals.get(f)) return res def write(self, vals): res = super(User, self).write(vals) # TODO: will it work with OCA's partner_firstname module? if any(k in vals for k in ["name", "email"] + FIELDS): for f in ALL_FIELDS: self._update_properties_label(f) return res def _auto_init(self): for f in FIELDS: self._auto_init_website_dependent(f) return super(User, self)._auto_init()
class Job(models.Model): _name = 'hr.job' _inherit = [ 'hr.job', 'website.seo.metadata', 'website.published.multi.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, prefetch=False) def _compute_website_url(self): super(Job, self)._compute_website_url() for job in self: job.website_url = "/jobs/detail/%s" % job.id def set_open(self): self.write({'website_published': False}) return super(Job, self).set_open()
class SaleOrder(models.Model): _inherit = 'sale.order' website_description = fields.Html('Website Description', sanitize_attributes=False, translate=html_translate) @api.onchange('partner_id') def onchange_update_description_lang(self): if not self.sale_order_template_id: return else: template = self.sale_order_template_id.with_context(lang=self.partner_id.lang) self.website_description = template.website_description def _compute_line_data_for_template_change(self, line): vals = super(SaleOrder, self)._compute_line_data_for_template_change(line) vals.update(website_description=line.website_description) return vals def _compute_option_data_for_template_change(self, option): vals = super(SaleOrder, self)._compute_option_data_for_template_change(option) vals.update(website_description=option.website_description) return vals @api.onchange('sale_order_template_id') def onchange_sale_order_template_id(self): ret = super(SaleOrder, self).onchange_sale_order_template_id() if self.sale_order_template_id: template = self.sale_order_template_id.with_context(lang=self.partner_id.lang) self.website_description = template.website_description return ret
class ConverterTest(models.Model): _name = 'web_editor.converter.test' _description = '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 SaleOrderTemplateLine(models.Model): _inherit = "sale.order.template.line" website_description = fields.Html( 'Website Description', related='product_id.product_tmpl_id.quotation_only_description', translate=html_translate, readonly=False) @api.onchange('product_id') def _onchange_product_id(self): ret = super(SaleOrderTemplateLine, self)._onchange_product_id() if self.product_id: self.website_description = self.product_id.quotation_description return ret @api.model def create(self, values): values = self._inject_quotation_description(values) return super(SaleOrderTemplateLine, self).create(values) def write(self, values): values = self._inject_quotation_description(values) return super(SaleOrderTemplateLine, self).write(values) def _inject_quotation_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.quotation_description return values
class test_model(models.Model): _name = 'test_converter.test_model' _description = 'Test Converter 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(attachment=False) date = fields.Date() datetime = fields.Datetime() 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 MailingList(models.Model): _inherit = 'mailing.list' def _default_toast_content(self): return '<p>Thanks for subscribing!</p>' website_popup_ids = fields.One2many('website.mass_mailing.popup', 'mailing_list_id', string="Website Popups") toast_content = fields.Html(default=_default_toast_content, translate=True)
class MassMailingPopup(models.Model): _name = 'website.mass_mailing.popup' _description = "Mailing list popup" def _default_popup_content(self): return self.env['ir.ui.view'].render_template('website_mass_mailing.s_newsletter_block') mailing_list_id = fields.Many2one('mailing.list') website_id = fields.Many2one('website') popup_content = fields.Html(string="Website Popup Content", default=_default_popup_content, translate=True, sanitize=False)
class SaleOrderTemplateOption(models.Model): _inherit = "sale.order.template.option" website_description = fields.Html('Website Description', translate=html_translate, sanitize_attributes=False) @api.onchange('product_id') def _onchange_product_id(self): ret = super(SaleOrderTemplateOption, self)._onchange_product_id() if self.product_id: self.website_description = self.product_id.quotation_description return ret
class ProductPublicCategory(models.Model): _name = "product.public.category" _inherit = ["website.seo.metadata", "website.multi.mixin", 'image.mixin'] _description = "Website Product Category" _parent_store = True _order = "sequence, name" name = fields.Char(required=True, translate=True) parent_id = fields.Many2one('product.public.category', string='Parent Category', index=True) parent_path = fields.Char(index=True) child_id = fields.One2many('product.public.category', 'parent_id', string='Children Categories') parents_and_self = fields.Many2many('product.public.category', compute='_compute_parents_and_self') sequence = fields.Integer( help= "Gives the sequence order when displaying a list of product categories.", index=True) website_description = fields.Html('Category Description', sanitize_attributes=False, translate=html_translate) product_tmpl_ids = fields.Many2many( 'product.template', relation='product_public_category_product_template_rel') @api.constrains('parent_id') def check_parent_id(self): if not self._check_recursion(): raise ValueError( _('Error ! You cannot create recursive categories.')) def name_get(self): res = [] for category in self: res.append((category.id, " / ".join(category.parents_and_self.mapped('name')))) return res def unlink(self): self.child_id.parent_id = None return super(ProductPublicCategory, self).unlink() def _compute_parents_and_self(self): for category in self: if category.parent_path: category.parents_and_self = self.env[ 'product.public.category'].browse( [int(p) for p in category.parent_path.split('/')[:-1]]) else: category.parents_and_self = category
class Ks_NewSnippet(models.Model): _name = 'theme.ks_new_snippet' _description = 'Save the custom snippets' name = fields.Char("Name", required='true') ks_snippet_body = fields.Html('Snippet Html', sanitize=False) ks_snippet_css = fields.Text('Snippet Css') ks_snippet_thumbnail = fields.Binary("Thumbnail", attachment=True) @api.model def create(self, values): rec = super(Ks_NewSnippet, self).create(values) return rec
class SaleOrderTemplate(models.Model): _inherit = "sale.order.template" website_description = fields.Html('Website Description', translate=html_translate, sanitize_attributes=False) @api.multi def open_template(self): self.ensure_one() return { 'type': 'ir.actions.act_url', 'target': 'self', 'url': '/sale_quotation_builder/template/%d' % self.id }
class WebsiteResPartner(models.Model): _name = 'res.partner' _inherit = ['res.partner', 'website.seo.metadata'] website_description = fields.Html('Website Partner Full Description', strip_style=True, translate=html_translate) website_short_description = fields.Text( 'Website Partner Short Description') def _compute_website_url(self): super(WebsiteResPartner, self)._compute_website_url() for partner in self: partner.website_url = "/partners/%s" % slug(partner)
class MixedModel(models.Model): _name = 'test_new_api.mixed' _description = 'Test New API Mixed' number = fields.Float(digits=(10, 2), default=3.14) number2 = fields.Float(digits='New API Precision') date = fields.Date() moment = fields.Datetime() 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() def _compute_now(self): # this is a non-stored computed field without dependencies for message in self: message.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 ImLivechatChannel(models.Model): _name = 'im_livechat.channel' _inherit = ['im_livechat.channel', 'website.published.mixin'] 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 DigestTip(models.Model): _name = 'digest.tip' _description = 'Digest Tips' _order = 'sequence' sequence = fields.Integer( 'Sequence', default=1, help='Used to display digest tip in email template base on order') user_ids = fields.Many2many('res.users', string='Recipients', help='Users having already received this tip') tip_description = fields.Html('Tip description', translate=html_translate) group_id = fields.Many2one( 'res.groups', string='Authorized Group', default=lambda self: self.env.ref('base.group_user'))
class SaleOrderOption(models.Model): _inherit = "sale.order.option" website_description = fields.Html('Website Description', sanitize_attributes=False, translate=html_translate) @api.onchange('product_id', 'uom_id') def _onchange_product_id(self): ret = super(SaleOrderOption, self)._onchange_product_id() if self.product_id: product = self.product_id.with_context(lang=self.order_id.partner_id.lang) self.website_description = product.quotation_description return ret def _get_values_to_add_to_order(self): values = super(SaleOrderOption, self)._get_values_to_add_to_order() values.update(website_description=self.website_description) return values
class website_menu(models.Model): _inherit = "website.menu" html_menu = fields.Html('Menu Design Block', translate=html_translate) is_dynamic_menu = fields.Boolean("Is Dynamic Menu", default=False) is_vertical_menu = fields.Boolean("Vertical menu", default=False) # method whcih redirect to frontend for design menu html structure def action_edit_menu(self, context=None): if not len(self.ids) == 1: raise ValueError('One and only one ID allowed for this action') url = '/menu_html_builder?model=website.menu&id=%d&enable_editor=1' % ( self.id) return { 'name': ('Edit Template'), 'type': 'ir.actions.act_url', 'url': url, 'target': 'self', } @api.onchange('is_vertical_menu') def is_vertical_menu_change(self): res = self.env['website.menu'].sudo().search([ ('is_vertical_menu', '=', True), ('website_id', 'in', (False, self.website_id.id)) ]) if res: if self.is_vertical_menu: res.write({'is_vertical_menu': False}) self.is_vertical_menu = True else: self.is_vertical_menu = False @api.one def _compute_visible(self): visible = True if self.page_id and not self.page_id.sudo( ).is_visible and not self.user_has_groups('base.group_user'): visible = False if self.is_vertical_menu: visible = False self.is_visible = visible
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">Eagle 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 SaleOrderLine(models.Model): _inherit = "sale.order.line" website_description = fields.Html('Website Description', sanitize=False, translate=html_translate) @api.model def create(self, values): values = self._inject_quotation_description(values) return super(SaleOrderLine, self).create(values) def write(self, values): values = self._inject_quotation_description(values) return super(SaleOrderLine, self).write(values) def _inject_quotation_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.update(website_description=product.quotation_description) return values
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', tracking=True, default=lambda self: self.env.user) company_id = fields.Many2one('res.company', related='event_id.company_id') partner_id = fields.Many2one('res.partner', 'Speaker') partner_name = fields.Char('Name') partner_email = fields.Char('Email') partner_phone = fields.Char('Phone') partner_biography = fields.Html('Biography') tag_ids = fields.Many2many('event.track.tag', string='Tags') stage_id = fields.Many2one('event.track.stage', string='Stage', ondelete='restrict', index=True, copy=False, default=_get_default_stage_id, group_expand='_read_group_stage_ids', required=True, tracking=True) kanban_state = fields.Selection( [('normal', 'Grey'), ('done', 'Green'), ('blocked', 'Red')], string='Kanban State', copy=False, default='normal', required=True, tracking=True, 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(translate=html_translate, sanitize_attributes=False) date = fields.Datetime('Track Date') date_end = fields.Datetime('Track End Date', compute='_compute_end_date', store=True) duration = fields.Float('Duration', default=1.5, help="Track duration in hours.") 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.Image("Image", related='partner_id.image_128', store=True, readonly=False) @api.depends('name') def _compute_website_url(self): super(Track, self)._compute_website_url() for track in self: if track.id: 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.depends('date', 'duration') def _compute_end_date(self): for track in self: if track.date: delta = timedelta(minutes=60 * track.duration) track.date_end = track.date + delta else: track.date_end = False @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 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) def _track_template(self, changes): res = super(Track, self)._track_template(changes) track = self[0] if 'stage_id' in changes and track.stage_id.mail_template_id: res['stage_id'] = (track.stage_id.mail_template_id, { 'composition_mode': 'comment', 'auto_delete_message': True, 'subtype_id': self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note'), 'email_layout_xmlid': 'mail.mail_notification_light' }) return res def _track_subtype(self, init_values): self.ensure_one() if 'kanban_state' in init_values and self.kanban_state == 'blocked': return self.env.ref('website_event_track.mt_track_blocked') elif 'kanban_state' in init_values and self.kanban_state == 'done': return self.env.ref('website_event_track.mt_track_ready') return super(Track, self)._track_subtype(init_values) def _message_get_suggested_recipients(self): recipients = super(Track, self)._message_get_suggested_recipients() for track in self: if track.partner_email and 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, msg_vals): 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, msg_vals) def open_track_speakers_list(self): return { 'name': _('Speakers'), 'domain': [('id', 'in', self.mapped('partner_id').ids)], 'view_mode': 'kanban,form', 'res_model': 'res.partner', 'view_id': False, 'type': 'ir.actions.act_window', }
class EventEvent(models.Model): """Event""" _name = 'event.event' _description = 'Event' _inherit = ['mail.thread', 'mail.activity.mixin'] _order = 'date_begin' name = fields.Char(string='Event', 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, tracking=True, readonly=False, states={'done': [('readonly', True)]}) company_id = fields.Many2one('res.company', string='Company', change_default=True, default=lambda self: self.env.company, required=False, readonly=False, states={'done': [('readonly', True)]}) organizer_id = fields.Many2one( 'res.partner', string='Organizer', tracking=True, default=lambda self: self.env.company.partner_id, domain= "['|', ('company_id', '=', False), ('company_id', '=', company_id)]") event_type_id = fields.Many2one('event.type', string='Category', readonly=False, states={'done': [('readonly', True)]}) 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', 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', 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(string='Reserved Seats', store=True, readonly=True, compute='_compute_seats') seats_available = fields.Integer(string='Available Seats', store=True, readonly=True, compute='_compute_seats') seats_unconfirmed = fields.Integer(string='Unconfirmed Seat Reservations', store=True, readonly=True, compute='_compute_seats') seats_used = fields.Integer(string='Number of Participants', store=True, readonly=True, compute='_compute_seats') seats_expected = fields.Integer(string='Number of Expected Attendees', compute_sudo=True, 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, tracking=True, states={'done': [('readonly', True)]}) date_end = fields.Datetime(string='End Date', required=True, tracking=True, 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') is_one_day = fields.Boolean(compute='_compute_field_is_one_day') 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.company.partner_id, readonly=False, states={'done': [('readonly', True)]}, domain= "['|', ('company_id', '=', False), ('company_id', '=', company_id)]", tracking=True) country_id = fields.Many2one('res.country', 'Country', related='address_id.country_id', store=True, readonly=False) twitter_hashtag = fields.Char('Twitter Hashtag') description = fields.Html(string='Description', 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.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.env['event.registration'].flush(['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.depends('date_tz', 'date_begin') def _compute_date_begin_tz(self): for event in self: if event.date_begin: event.date_begin_located = format_datetime(self.env, event.date_begin, tz=event.date_tz, dt_format='medium') else: event.date_begin_located = False @api.depends('date_tz', 'date_end') def _compute_date_end_tz(self): for event in self: if event.date_end: event.date_end_located = format_datetime(self.env, event.date_end, tz=event.date_tz, dt_format='medium') else: event.date_end_located = False @api.depends('date_begin', 'date_end', 'date_tz') def _compute_field_is_one_day(self): for event in self: # Need to localize because it could begin late and finish early in # another timezone event = event.with_context(tz=event.date_tz) begin_tz = fields.Datetime.context_timestamp( event, event.date_begin) end_tz = fields.Datetime.context_timestamp(event, event.date_end) event.is_one_day = (begin_tz.date() == end_tz.date()) @api.onchange('is_online') def _onchange_is_online(self): if self.is_online: self.address_id = 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)] + [(0, 0, { attribute_name: line[attribute_name] for attribute_name in self.env['event.type.mail']. _get_event_mail_fields_whitelist() }) 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.constrains('date_begin', 'date_end') def _check_closing_date(self): for event in self: if event.date_end < event.date_begin: raise ValidationError( _('The closing date cannot be earlier than the beginning date.' )) @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 def write(self, vals): res = super(EventEvent, self).write(vals) if vals.get('organizer_id'): self.message_subscribe([vals['organizer_id']]) return res @api.returns('self', lambda value: value.id) def copy(self, default=None): self.ensure_one() default = dict(default or {}, name=_("%s (copy)") % (self.name)) return super(EventEvent, self).copy(default) def button_draft(self): self.write({'state': 'draft'}) 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' def button_done(self): self.write({'state': 'done'}) def button_confirm(self): self.write({'state': 'confirm'}) def mail_attendees(self, template_id, force_send=False, filter_func=lambda self: self.state != 'cancel'): for event in self: for attendee in event.registration_ids.filtered(filter_func): self.env['mail.template'].browse(template_id).send_mail( attendee.id, force_send=force_send) def _is_event_registrable(self): return self.date_end > fields.Datetime.now() def _get_ics_file(self): """ Returns iCalendar file for the event invitation. :returns a dict of .ics file content for each event """ result = {} if not vobject: return result for event in self: cal = vobject.iCalendar() cal_event = cal.add('vevent') cal_event.add('created').value = fields.Datetime.now().replace( tzinfo=pytz.timezone('UTC')) cal_event.add('dtstart').value = fields.Datetime.from_string( event.date_begin).replace(tzinfo=pytz.timezone('UTC')) cal_event.add('dtend').value = fields.Datetime.from_string( event.date_end).replace(tzinfo=pytz.timezone('UTC')) cal_event.add('summary').value = event.name if event.address_id: cal_event.add( 'location').value = event.sudo().address_id.contact_address result[event.id] = cal.serialize().encode('utf-8') return result
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) if self._context.get('mail_invite_follower_channel_only'): result['send_mail'] = False if 'message' not in fields: return result user_name = self.env.user.name_get()[0][1] model = result.get('res_model') res_id = result.get('res_id') if model and res_id: document = self.env['ir.model']._get(model).display_name title = self.env[model].browse(res_id).display_name msg_fmt = _( '%(user_name)s invited you to follow %(document)s document: %(title)s' ) else: msg_fmt = _('%(user_name)s invited you to follow a new document.') text = msg_fmt % locals() message = html.DIV(html.P(_('Hello,')), html.P(text)) result['message'] = etree.tostring(message) 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, 'add_sign': True, }) self.env['res.partner'].with_context(auto_delete=True)._notify( message, [{ 'id': pid, 'share': True, 'notif': 'email', 'type': 'customer', 'groups': [] } for pid in new_partners.ids], document, force_send=True, send_after_commit=False) message.unlink() return {'type': 'ir.actions.act_window_close'}
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)', 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 def unlink(self): self.unlink_action() return super(MailTemplate, self).unlink() @api.returns('self', lambda value: value.id) def copy(self, default=None): default = dict(default or {}, name=_("%s (copy)") % self.name) return super(MailTemplate, self).copy(default=default) def unlink_action(self): for template in self: if template.ref_ir_act_window: template.ref_ir_act_window.unlink() return True def create_action(self): ActWindow = self.env['ir.actions.act_window'] view = self.env.ref('mail.email_compose_message_wizard_form') for template in self: button_name = _('Send Mail (%s)') % template.name action = ActWindow.create({ 'name': button_name, 'type': 'ir.actions.act_window', 'res_model': 'mail.compose.message', '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 render_post_process(self, html): html = self.env['mail.thread']._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, int): 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, date_format=False, lang_code=False: format_date( self.env, date, date_format, lang_code), 'format_datetime': lambda dt, tz=False, dt_format=False, lang_code=False: format_datetime(self.env, dt, tz, dt_format, lang_code), 'format_amount': lambda amount, currency, lang_code=False: tools.format_amount( self.env, amount, currency, lang_code), 'format_duration': lambda value: tools.format_duration(value), '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]] def get_email_template(self, res_ids): multi_mode = True if isinstance(res_ids, int): 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() if self.env.context.get('template_preview_lang'): lang = self.env.context.get('template_preview_lang') for res_id in res_ids: results[res_id] = self.with_context(lang=lang) else: 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]] 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'): records = self.env[self.model].browse(res_ids).sudo() default_recipients = self.env[ 'mail.thread']._message_get_default_recipients_on_records( records) for res_id, recipients in default_recipients.items(): results[res_id].pop('partner_to', None) results[res_id].update(recipients) records_company = None if self._context.get( 'tpl_partners_only' ) and self.model and results and 'company_id' in self.env[ self.model]._fields: records = self.env[self.model].browse(results.keys()).read( ['company_id']) records_company = { rec['id']: (rec['company_id'][0] if rec['company_id'] else None) for rec in records } 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', '')) Partner = self.env['res.partner'] if records_company: Partner = Partner.with_context( default_company_id=records_company[res_id]) for mail in mails: partner_id = 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 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 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, int): 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 in ['qweb-html', 'qweb-pdf']: result, format = report.render_qweb_pdf([res_id]) else: res = report.render([res_id]) if not res: raise UserError( _('Unsupported report type %s found.') % report.report_type) result, format = res # 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]] # ---------------------------------------- # EMAIL # ---------------------------------------- def send_mail(self, res_id, force_send=False, raise_exception=False, email_values=None, notif_layout=False): """ Generates a new mail.mail. Template is rendered on record given by res_id and model coming from template. :param int res_id: id of the record to render the template :param bool force_send: send email immediately; otherwise use the mail queue (recommended); :param dict email_values: update generated mail with those values to further customize the mail; :param str notif_layout: optional notification layout to encapsulate the generated email; :returns: id of the mail.mail that was created """ self.ensure_one() Mail = self.env['mail.mail'] Attachment = self.env[ 'ir.attachment'] # TDE FIXME: should remove default_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['attachment_ids'] = [ (4, aid) for aid in values.get('attachment_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') # encapsulate body if notif_layout and values['body_html']: try: template = self.env.ref(notif_layout, raise_if_not_found=True) except ValueError: _logger.warning( 'QWeb template %s not found when sending template %s. Sending without layouting.' % (notif_layout, self.name)) else: record = self.env[self.model].browse(res_id) template_ctx = { 'message': self.env['mail.message'].sudo().new( dict(body=values['body_html'], record_name=record.display_name)), 'model_description': self.env['ir.model']._get(record._name).display_name, 'company': 'company_id' in record and record['company_id'] or self.env.company, 'record': record, } body = template.render(template_ctx, engine='ir.qweb', minimal_qcontext=True) values['body_html'] = self.env[ 'mail.thread']._replace_local_links(body) mail = Mail.create(values) # manage attachments for attachment in attachments: attachment_data = { 'name': attachment[0], 'datas': attachment[1], 'type': 'binary', 'res_model': 'mail.message', 'res_id': mail.mail_message_id.id, } attachment_ids.append((4, Attachment.create(attachment_data).id)) if attachment_ids: mail.write({'attachment_ids': attachment_ids}) if force_send: mail.send(raise_exception=raise_exception) return mail.id # TDE CLEANME: return mail + api.returns ?
class MailTemplate(models.Model): _inherit = ['mail.template', 'website_dependent.mixin'] _name = 'mail.template' body_html = fields.Html(company_dependent=True, website_dependent=True) mail_server_id = fields.Many2one( string='Outgoing Mail Server (Multi-Website)', company_dependent=True, website_dependent=True) report_template = fields.Many2one( string='Optional report to print and attach (Multi-Website)', company_dependent=True, website_dependent=True) @api.multi def generate_email(self, res_ids, fields=None): """Remove mail_server_id when not set to recompute in _default_mail_server_id in mail.message""" multi_mode = True if isinstance(res_ids, pycompat.integer_types): multi_mode = False res = super(MailTemplate, self).generate_email(res_ids, fields=fields) if not multi_mode: list_of_dict = {0: res} else: list_of_dict = res for _unused, data in list_of_dict.items(): if 'mail_server_id' in data and not data.get('mail_server_id'): del data['mail_server_id'] return res @api.model def _render_template(self, template_txt, model, res_ids, post_process=False): """Override to add website to context""" 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 } # [NEW] Check website and company context company = self.env['res.company'] # empty value company_id = self.env.context.get('force_company') if company_id: company = self.env['res.company'].sudo().browse(company_id) if self.env.context.get('website_id'): website = self.env['website'].browse( self.env.context.get('website_id')) else: website = self.env.user.backend_website_id for res_id, record in res_to_rec.items(): record_company = company if not record_company: if hasattr(record, 'company_id') and record.company_id: record_company = record.company_id record_website = website if hasattr(record, 'website_id') and record.website_id: record_website = record.website_id if record_company and record_website \ and record_website.company_id != company: # company and website are incompatible, so keep only company record_website = self.env['website'] # empty value record_context = dict(force_company=record_company.id, website_id=record_website.id) variables['object'] = record.with_context(**record_context) variables['website'] = record_website 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"" if post_process: render_result = self.with_context( **record_context).render_post_process(render_result) results[res_id] = render_result return multi_mode and results or results[res_ids[0]] @api.model def create(self, vals): res = super(MailTemplate, self).create(vals) # make value company independent for f in FIELDS: res._force_default(f, vals.get(f)) return res @api.multi def write(self, vals): res = super(MailTemplate, self).write(vals) # TODO: will it work with OCA's partner_firstname module? if 'name' in vals: fields_to_update = FIELDS else: fields_to_update = [f for f in FIELDS if f in vals] for f in fields_to_update: self._update_properties_label(f) return res def _auto_init(self): for f in FIELDS: self._auto_init_website_dependent(f) return super(MailTemplate, self)._auto_init()
class IrActions(models.Model): _name = 'ir.actions.actions' _description = '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_create_multi def create(self, vals_list): res = super(IrActions, self).create(vals_list) # 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 SurveyQuestion(models.Model): """ Questions that will be asked in a survey. Each question can have one of more suggested answers (eg. in case of dropdown choices, multi-answer checkboxes, radio buttons...). Technical note: survey.question is also the model used for the survey's pages (with the "is_page" field set to True). A page corresponds to a "section" in the interface, and the fact that it separates the survey in actual pages in the interface depends on the "questions_layout" parameter on the survey.survey model. Pages are also used when randomizing questions. The randomization can happen within a "page". Using the same model for questions and pages allows to put all the pages and questions together in a o2m field (see survey.survey.question_and_page_ids) on the view side and easily reorganize your survey by dragging the items around. It also removes on level of encoding by directly having 'Add a page' and 'Add a question' links on the tree view of questions, enabling a faster encoding. However, this has the downside of making the code reading a little bit more complicated. Efforts were made at the model level to create computed fields so that the use of these models still seems somewhat logical. That means: - A survey still has "page_ids" (question_and_page_ids filtered on is_page = True) - These "page_ids" still have question_ids (questions located between this page and the next) - These "question_ids" still have a "page_id" That makes the use and display of these information at view and controller levels easier to understand. """ _name = 'survey.question' _description = 'Survey Question' _rec_name = 'question' _order = 'sequence,id' @api.model def default_get(self, fields): defaults = super(SurveyQuestion, self).default_get(fields) if (not fields or 'question_type' in fields): defaults['question_type'] = False if defaults.get( 'is_page') == True else 'free_text' return defaults # Question metadata survey_id = fields.Many2one('survey.survey', string='Survey', ondelete='cascade') page_id = fields.Many2one('survey.question', string='Page', compute="_compute_page_id", store=True) question_ids = fields.One2many('survey.question', string='Questions', compute="_compute_question_ids") scoring_type = fields.Selection(related='survey_id.scoring_type', string='Scoring Type', readonly=True) sequence = fields.Integer('Sequence', default=10) # Question is_page = fields.Boolean('Is a page?') questions_selection = fields.Selection( related='survey_id.questions_selection', readonly=True, help= "If randomized is selected, add the number of random questions next to the section." ) random_questions_count = fields.Integer( 'Random questions count', default=1, help= "Used on randomized sections to take X random questions from all the questions of that section." ) title = fields.Char('Title', required=True, translate=True) question = fields.Char('Question', related="title") description = fields.Html( 'Description', help= "Use this field to add additional explanations about your question", translate=True) question_type = fields.Selection( [('free_text', 'Multiple Lines Text Box'), ('textbox', 'Single Line Text Box'), ('numerical_box', 'Numerical Value'), ('date', 'Date'), ('datetime', 'Datetime'), ('simple_choice', 'Multiple choice: only one answer'), ('multiple_choice', 'Multiple choice: multiple answers allowed'), ('matrix', 'Matrix')], string='Question Type') # simple choice / multiple choice / matrix labels_ids = fields.One2many( 'survey.label', 'question_id', string='Types of answers', copy=True, help= 'Labels used for proposed choices: simple choice, multiple choice and columns of matrix' ) # matrix matrix_subtype = fields.Selection( [('simple', 'One choice per row'), ('multiple', 'Multiple choices per row')], string='Matrix Type', default='simple') labels_ids_2 = fields.One2many( 'survey.label', 'question_id_2', string='Rows of the Matrix', copy=True, help='Labels used for proposed choices: rows of matrix') # Display options column_nb = fields.Selection( [('12', '1'), ('6', '2'), ('4', '3'), ('3', '4'), ('2', '6')], string='Number of columns', default='12', help= 'These options refer to col-xx-[12|6|4|3|2] classes in Bootstrap for dropdown-based simple and multiple choice questions.' ) display_mode = fields.Selection( [('columns', 'Radio Buttons'), ('dropdown', 'Selection Box')], string='Display Mode', default='columns', help='Display mode of simple choice questions.') # Comments comments_allowed = fields.Boolean('Show Comments Field') comments_message = fields.Char( 'Comment Message', translate=True, default=lambda self: _("If other, please specify:")) comment_count_as_answer = fields.Boolean( 'Comment Field is an Answer Choice') # Validation validation_required = fields.Boolean('Validate entry') validation_email = fields.Boolean('Input must be an email') validation_length_min = fields.Integer('Minimum Text Length') validation_length_max = fields.Integer('Maximum Text Length') validation_min_float_value = fields.Float('Minimum value') validation_max_float_value = fields.Float('Maximum value') validation_min_date = fields.Date('Minimum Date') validation_max_date = fields.Date('Maximum Date') validation_min_datetime = fields.Datetime('Minimum Datetime') validation_max_datetime = fields.Datetime('Maximum Datetime') validation_error_msg = fields.Char( 'Validation Error message', translate=True, default=lambda self: _("The answer you entered is not valid.")) # Constraints on number of answers (matrices) constr_mandatory = fields.Boolean('Mandatory Answer') constr_error_msg = fields.Char( 'Error message', translate=True, default=lambda self: _("This question requires an answer.")) # Answer user_input_line_ids = fields.One2many('survey.user_input_line', 'question_id', string='Answers', domain=[('skipped', '=', False)], groups='survey.group_survey_user') _sql_constraints = [ ('positive_len_min', 'CHECK (validation_length_min >= 0)', 'A length must be positive!'), ('positive_len_max', 'CHECK (validation_length_max >= 0)', 'A length must be positive!'), ('validation_length', 'CHECK (validation_length_min <= validation_length_max)', 'Max length cannot be smaller than min length!'), ('validation_float', 'CHECK (validation_min_float_value <= validation_max_float_value)', 'Max value cannot be smaller than min value!'), ('validation_date', 'CHECK (validation_min_date <= validation_max_date)', 'Max date cannot be smaller than min date!'), ('validation_datetime', 'CHECK (validation_min_datetime <= validation_max_datetime)', 'Max datetime cannot be smaller than min datetime!') ] @api.onchange('validation_email') def _onchange_validation_email(self): if self.validation_email: self.validation_required = False @api.onchange('is_page') def _onchange_is_page(self): if self.is_page: self.question_type = False # Validation methods def validate_question(self, post, answer_tag): """ Validate question, depending on question type and parameters """ self.ensure_one() try: checker = getattr(self, 'validate_' + self.question_type) except AttributeError: _logger.warning(self.question_type + ": This type of question has no validation method") return {} else: return checker(post, answer_tag) def validate_free_text(self, post, answer_tag): self.ensure_one() errors = {} answer = post[answer_tag].strip() # Empty answer to mandatory question if self.constr_mandatory and not answer: errors.update({answer_tag: self.constr_error_msg}) return errors def validate_textbox(self, post, answer_tag): self.ensure_one() errors = {} answer = post[answer_tag].strip() # Empty answer to mandatory question if self.constr_mandatory and not answer: errors.update({answer_tag: self.constr_error_msg}) # Email format validation # Note: this validation is very basic: # all the strings of the form # <something>@<anything>.<extension> # will be accepted if answer and self.validation_email: if not email_validator.match(answer): errors.update( {answer_tag: _('This answer must be an email address')}) # Answer validation (if properly defined) # Length of the answer must be in a range if answer and self.validation_required: if not (self.validation_length_min <= len(answer) <= self.validation_length_max): errors.update({answer_tag: self.validation_error_msg}) return errors def validate_numerical_box(self, post, answer_tag): self.ensure_one() errors = {} answer = post[answer_tag].strip() # Empty answer to mandatory question if self.constr_mandatory and not answer: errors.update({answer_tag: self.constr_error_msg}) # Checks if user input is a number if answer: try: floatanswer = float(answer) except ValueError: errors.update({answer_tag: _('This is not a number')}) # Answer validation (if properly defined) if answer and self.validation_required: # Answer is not in the right range with tools.ignore(Exception): floatanswer = float( answer) # check that it is a float has been done hereunder if not (self.validation_min_float_value <= floatanswer <= self.validation_max_float_value): errors.update({answer_tag: self.validation_error_msg}) return errors def date_validation(self, date_type, post, answer_tag, min_value, max_value): self.ensure_one() errors = {} if date_type not in ('date', 'datetime'): raise ValueError("Unexpected date type value") answer = post[answer_tag].strip() # Empty answer to mandatory question if self.constr_mandatory and not answer: errors.update({answer_tag: self.constr_error_msg}) # Checks if user input is a date if answer: try: if date_type == 'datetime': dateanswer = fields.Datetime.from_string(answer) else: dateanswer = fields.Date.from_string(answer) except ValueError: errors.update({answer_tag: _('This is not a date')}) return errors # Answer validation (if properly defined) if answer and self.validation_required: # Answer is not in the right range try: if date_type == 'datetime': date_from_string = fields.Datetime.from_string else: date_from_string = fields.Date.from_string dateanswer = date_from_string(answer) min_date = date_from_string(min_value) max_date = date_from_string(max_value) if min_date and max_date and not (min_date <= dateanswer <= max_date): # If Minimum and Maximum Date are entered errors.update({answer_tag: self.validation_error_msg}) elif min_date and not min_date <= dateanswer: # If only Minimum Date is entered and not Define Maximum Date errors.update({answer_tag: self.validation_error_msg}) elif max_date and not dateanswer <= max_date: # If only Maximum Date is entered and not Define Minimum Date errors.update({answer_tag: self.validation_error_msg}) except ValueError: # check that it is a date has been done hereunder pass return errors def validate_date(self, post, answer_tag): return self.date_validation('date', post, answer_tag, self.validation_min_date, self.validation_max_date) def validate_datetime(self, post, answer_tag): return self.date_validation('datetime', post, answer_tag, self.validation_min_datetime, self.validation_max_datetime) def validate_simple_choice(self, post, answer_tag): self.ensure_one() errors = {} if self.comments_allowed: comment_tag = "%s_%s" % (answer_tag, 'comment') # Empty answer to mandatory self if self.constr_mandatory and answer_tag not in post: errors.update({answer_tag: self.constr_error_msg}) if self.constr_mandatory and answer_tag in post and not post[ answer_tag].strip(): errors.update({answer_tag: self.constr_error_msg}) # Answer is a comment and is empty if self.constr_mandatory and answer_tag in post and post[ answer_tag] == "-1" and self.comment_count_as_answer and comment_tag in post and not post[ comment_tag].strip(): errors.update({answer_tag: self.constr_error_msg}) return errors def validate_multiple_choice(self, post, answer_tag): self.ensure_one() errors = {} if self.constr_mandatory: answer_candidates = dict_keys_startswith(post, answer_tag) comment_flag = answer_candidates.pop(("%s_%s" % (answer_tag, -1)), None) if self.comments_allowed: comment_answer = answer_candidates.pop( ("%s_%s" % (answer_tag, 'comment')), '').strip() # Preventing answers with blank value if all(not answer.strip() for answer in answer_candidates.values()) and answer_candidates: errors.update({answer_tag: self.constr_error_msg}) # There is no answer neither comments (if comments count as answer) if not answer_candidates and self.comment_count_as_answer and ( not comment_flag or not comment_answer): errors.update({answer_tag: self.constr_error_msg}) # There is no answer at all if not answer_candidates and not self.comment_count_as_answer: errors.update({answer_tag: self.constr_error_msg}) return errors def validate_matrix(self, post, answer_tag): self.ensure_one() errors = {} if self.constr_mandatory: lines_number = len(self.labels_ids_2) answer_candidates = dict_keys_startswith(post, answer_tag) answer_candidates.pop(("%s_%s" % (answer_tag, 'comment')), '').strip() # Number of lines that have been answered if self.matrix_subtype == 'simple': answer_number = len(answer_candidates) elif self.matrix_subtype == 'multiple': answer_number = len( {sk.rsplit('_', 1)[0] for sk in answer_candidates}) else: raise RuntimeError("Invalid matrix subtype") # Validate that each line has been answered if answer_number != lines_number: errors.update({answer_tag: self.constr_error_msg}) return errors @api.depends('survey_id.question_and_page_ids.is_page', 'survey_id.question_and_page_ids.sequence') def _compute_question_ids(self): """Will take all questions of the survey for which the index is higher than the index of this page and lower than the index of the next page.""" for question in self: if question.is_page: next_page_index = False for page in question.survey_id.page_ids: if page._index() > question._index(): next_page_index = page._index() break question.question_ids = question.survey_id.question_ids.filtered( lambda q: q._index() > question._index() and (not next_page_index or q._index() < next_page_index)) else: question.question_ids = self.env['survey.question'] @api.depends('survey_id.question_and_page_ids.is_page', 'survey_id.question_and_page_ids.sequence') def _compute_page_id(self): """Will find the page to which this question belongs to by looking inside the corresponding survey""" for question in self: if question.is_page: question.page_id = None else: question.page_id = next((iter( question.survey_id.question_and_page_ids.filtered( lambda q: q.is_page and q.sequence < question.sequence ).sorted(reverse=True))), None) def _index(self): """We would normally just use the 'sequence' field of questions BUT, if the pages and questions are created without ever moving records around, the sequence field can be set to 0 for all the questions. However, the order of the recordset is always correct so we can rely on the index method.""" self.ensure_one() return list(self.survey_id.question_and_page_ids).index(self) def get_correct_answer_ids(self): self.ensure_one() return self.labels_ids.filtered(lambda label: label.is_correct)
class HrPlanActivityType(models.Model): _name = 'hr.plan.activity.type' _description = 'Plan activity type' _rec_name = 'summary' activity_type_id = fields.Many2one( 'mail.activity.type', 'Activity Type', default=lambda self: self.env.ref('mail.mail_activity_data_todo'), domain=lambda self: [ '|', ('res_model_id', '=', False), ('res_model_id', '=', self.env['ir.model']._get('hr.employee').id) ]) summary = fields.Char('Summary') responsible = fields.Selection([('coach', 'Coach'), ('manager', 'Manager'), ('employee', 'Employee'), ('other', 'Other')], default='employee', string='Responsible', required=True) responsible_id = fields.Many2one( 'res.users', 'Responsible Person', help='Specific responsible of activity if not linked to the employee.') note = fields.Html('Note') @api.onchange('activity_type_id') def _onchange_activity_type_id(self): if self.activity_type_id and self.activity_type_id.summary and not self.summary: self.summary = self.activity_type_id.summary def get_responsible_id(self, employee): if self.responsible == 'coach': if not employee.coach_id: raise UserError( _('Coach of employee %s is not set.') % employee.name) responsible = employee.coach_id.user_id if not responsible: raise UserError( _('User of coach of employee %s is not set.') % employee.name) elif self.responsible == 'manager': if not employee.parent_id: raise UserError( _('Manager of employee %s is not set.') % employee.name) responsible = employee.parent_id.user_id if not responsible: raise UserError( _('User of manager of employee %s is not set.') % employee.name) elif self.responsible == 'employee': responsible = employee.user_id if not responsible: raise UserError( _('User linked to employee %s is required.') % employee.name) elif self.responsible == 'other': responsible = self.responsible_id if not responsible: raise UserError( _('No specific user given on activity.') % employee.name) return responsible