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'].search([('model', '=', self.pool[model]._name)]).name_get()[0][1] 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, select=1, help='Model of the followed resource') res_id = fields.Integer('Related Document ID', select=1, 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_ids = self.env['ir.model'].search([('model', '=', wizard.res_model)]) model_name = model_ids.name_get()[0][1] # 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, user_signature=True) message.unlink() return {'type': 'ir.actions.act_window_close'}
class FindPaymentsWizard(models.TransientModel): _name = 'saas_portal.find_payments_wizard' invoice_lines = fields.Many2many('account.invoice.line') @api.model def default_get(self, fields): res = super(FindPaymentsWizard, self).default_get(fields) client_obj = self.env['saas_portal.client'].browse( self._context.get('active_id')) lines = self.find_partner_payments(client_obj.partner_id.id, client_obj.plan_id.id) res.update({'invoice_lines': [(6, 0, lines.ids)]}) return res @api.model def find_partner_payments(self, partner_id, plan_id): lines = self.env['account.invoice.line'].search([ ('partner_id', '=', partner_id), ('product_id.plan_id', '=', plan_id), ('period', '!=', False), ('saas_portal_client_id', '=', False) ]) return lines @api.multi def apply_invoice_lines(self): client_obj = self.env['saas_portal.client'].browse( self._context.get('active_id')) self.invoice_lines.write({'saas_portal_client_id': client_obj.id})
class AccountReportGeneralLedger(models.TransientModel): _inherit = "account.common.account.report" _name = "account.report.general.ledger" _description = "General Ledger Report" initial_balance = fields.Boolean( string='Include Initial Balances', help= 'If you selected date, this field allow you to add a row to display the amount of debit/credit/balance that precedes the filter you\'ve set.' ) sortby = fields.Selection([('sort_date', 'Date'), ('sort_journal_partner', 'Journal & Partner')], string='Sort by', required=True, default='sort_date') journal_ids = fields.Many2many('account.journal', 'account_report_general_ledger_journal_rel', 'account_id', 'journal_id', string='Journals', required=True) def _print_report(self, data): data = self.pre_print_report(data) data['form'].update(self.read(['initial_balance', 'sortby'])[0]) if data['form'].get( 'initial_balance') and not data['form'].get('date_from'): raise UserError(_("You must define a Start Date")) return self.env['report'].with_context(landscape=True).get_action( self, 'account.report_generalledger', data=data)
class SaasPortalPlan(models.Model): _inherit = 'saas_portal.plan' category_ids = fields.Many2many( 'saas.portal.category', string='Client Tags' )
class EventRegistration(models.Model): """ Store answers on attendees. """ _inherit = 'event.registration' answer_ids = fields.Many2many('event.answer', 'event_registration_answer', string='Answers')
class lunch_order_line(models.TransientModel): _name = 'lunch.order.line.lucky' def _default_supplier(self): suppliers_obj = self.env['lunch.product'].search([]).mapped("supplier") return [(4, supplier.id) for supplier in suppliers_obj] product_id = fields.Many2one('lunch.product', 'Product', store=True) supplier_ids = fields.Many2many(comodel_name='res.partner', string='Vendor', domain=lambda self: [("id", "in", self.env['lunch.product'].search([]).mapped("supplier").ids)]) is_max_budget = fields.Boolean("I'm not feeling rich", help="Enable this option to set a maximal budget for your lucky order.", store=True) max_budget = fields.Float('Max Budget', store=True) @api.multi def random_pick(self): """ To pick a random product from the selected suppliers, and create an order with this one """ self.ensure_one() if self.is_max_budget: products_obj = self.env['lunch.product'].search([('supplier', "in", self.supplier_ids.ids), ('price', '<=', self.max_budget)]) else: products_obj = self.env['lunch.product'].search([('supplier', "in", self.supplier_ids.ids)]) if len(products_obj) != 0: random_product_obj = self.env['lunch.product'].browse([random.choice(products_obj.ids)]) order_line = self.env['lunch.order.line'].create({ 'product_id': random_product_obj.id, 'order_id': self._context['active_id'] }) else: raise UserError(_('No product is matching your request. Now you will starve to death.'))
class RecruitmentStage(models.Model): _name = "hr.recruitment.stage" _description = "Stage of Recruitment" _order = 'sequence' name = fields.Char("Stage name", required=True, translate=True) sequence = fields.Integer( "Sequence", default=1, help="Gives the sequence order when displaying a list of stages.") job_ids = fields.Many2many( 'hr.job', 'job_stage_rel', 'stage_id', 'job_id', string='Job Stages', default=lambda self: [(4, self._context['default_job_id'])] if self._context.get('default_job_id') else None) requirements = fields.Text("Requirements") template_id = fields.Many2one( 'mail.template', "Use template", help= "If set, a message is posted on the applicant using the template when the applicant is set to the stage." ) fold = fields.Boolean( "Folded in Recruitment Pipe", help= "This stage is folded in the kanban view when there are no records in that stage to display." )
class Tags(models.Model): _name = "forum.tag" _description = "Forum Tag" _inherit = ['website.seo.metadata'] name = fields.Char('Name', required=True) create_uid = fields.Many2one('res.users', string='Created by', readonly=True) forum_id = fields.Many2one('forum.forum', string='Forum', required=True) post_ids = fields.Many2many('forum.post', 'forum_tag_rel', 'forum_tag_id', 'forum_id', string='Posts') posts_count = fields.Integer('Number of Posts', compute='_get_posts_count', store=True) _sql_constraints = [ ('name_uniq', 'unique (name, forum_id)', "Tag name already exists !"), ] @api.multi @api.depends("post_ids.tag_ids") def _get_posts_count(self): for tag in self: tag.posts_count = len(tag.post_ids)
class SaasPortalPlan(models.Model): _name = 'saas_portal.plan' _inherit = 'saas_portal.plan' page_url = fields.Char('Plan URL', placeholder='some-name') ecore_version = fields.Char('eCore Version', placeholder='8.0') app_store_module_ids = fields.Many2many('saas_portal.module', 'saas_portal_plan_module', 'plan_id', 'module_id', 'Modules')
class SaleOrderLine(models.Model): _inherit = 'sale.order.line' property_ids = fields.Many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties', readonly=True, states={'draft': [('readonly', False)]}) @api.multi def _get_delivered_qty(self): self.ensure_one() precision = self.env['decimal.precision'].precision_get( 'Product Unit of Measure') # In the case of a kit, we need to check if all components are shipped. We use a all or # nothing policy. A product can have several BoMs, we don't know which one was used when the # delivery was created. bom_delivered = {} for bom in self.product_id.product_tmpl_id.bom_ids: if bom.type != 'phantom': continue bom_delivered[bom.id] = False bom_exploded = self.env['mrp.bom']._bom_explode( bom, self.product_id, self.product_uom_qty)[0] for bom_line in bom_exploded: qty = 0.0 for move in self.procurement_ids.mapped('move_ids'): if move.state == 'done' and move.product_id.id == bom_line.get( 'product_id', False): qty += self.env['product.uom']._compute_qty_obj( move.product_uom, move.product_uom_qty, self.product_uom) if float_compare(qty, bom_line['product_qty'], precision_digits=precision) < 0: bom_delivered[bom.id] = False break else: bom_delivered[bom.id] = True if bom_delivered and any(bom_delivered.values()): return self.product_uom_qty elif bom_delivered: return 0.0 return super(SaleOrderLine, self)._get_delivered_qty() @api.multi def _prepare_order_line_procurement(self, group_id=False): vals = super(SaleOrderLine, self)._prepare_order_line_procurement(group_id=group_id) vals['property_ids'] = [(6, 0, self.property_ids.ids)] return vals
class event_track_tag(models.Model): _name = "event.track.tag" _description = 'Track Tag' _order = 'name' name = fields.Char('Tag') track_ids = fields.Many2many('event.track', string='Tracks') _sql_constraints = [ ('name_uniq', 'unique (name)', "Tag name already exists !"), ]
class AccountCommonReport(models.TransientModel): _name = "account.common.report" _description = "Account Common Report" company_id = fields.Many2one('res.company', string='Company', readonly=True, default=lambda self: self.env.user.company_id) journal_ids = fields.Many2many( 'account.journal', string='Journals', required=True, default=lambda self: self.env['account.journal'].search([])) date_from = fields.Date(string='Start Date') date_to = fields.Date(string='End Date') target_move = fields.Selection([ ('posted', 'All Posted Entries'), ('all', 'All Entries'), ], string='Target Moves', required=True, default='posted') def _build_contexts(self, data): result = {} result['journal_ids'] = 'journal_ids' in data['form'] and data['form'][ 'journal_ids'] or False result['state'] = 'target_move' in data['form'] and data['form'][ 'target_move'] or '' result['date_from'] = data['form']['date_from'] or False result['date_to'] = data['form']['date_to'] or False result['strict_range'] = True if result['date_from'] else False return result def _print_report(self, data): raise (_('Error!'), _('Not implemented.')) @api.multi def check_report(self): self.ensure_one() data = {} data['ids'] = self.env.context.get('active_ids', []) data['model'] = self.env.context.get('active_model', 'ir.ui.menu') data['form'] = self.read( ['date_from', 'date_to', 'journal_ids', 'target_move'])[0] used_context = self._build_contexts(data) data['form']['used_context'] = dict(used_context, lang=self.env.context.get( 'lang', 'en_US')) return self._print_report(data)
class SaleOrder(models.Model): _inherit = 'sale.order' tasks_ids = fields.Many2many('project.task', compute='_compute_tasks_ids', string='Tasks associated to this sale') tasks_count = fields.Integer(string='Tasks', compute='_compute_tasks_ids') @api.multi @api.depends('order_line.product_id.project_id') def _compute_tasks_ids(self): for order in self: order.tasks_ids = self.env['project.task'].search([ ('sale_line_id', 'in', order.order_line.ids) ]) order.tasks_count = len(order.tasks_ids) @api.multi def action_view_task(self): self.ensure_one() imd = self.env['ir.model.data'] action = imd.xmlid_to_object('project.action_view_task') list_view_id = imd.xmlid_to_res_id('project.view_task_tree2') form_view_id = imd.xmlid_to_res_id('project.view_task_form2') result = { 'name': action.name, 'help': action.help, 'type': action.type, 'views': [[list_view_id, 'tree'], [False, 'kanban'], [form_view_id, 'form'], [False, 'graph'], [False, 'calendar'], [False, 'pivot'], [False, 'graph']], 'target': action.target, 'context': action.context, 'res_model': action.res_model, } if len(self.tasks_ids) > 1: result['domain'] = "[('id','in',%s)]" % self.tasks_ids.ids elif len(self.tasks_ids) == 1: result['views'] = [(form_view_id, 'form')] result['res_id'] = self.tasks_ids.id else: result = {'type': 'ir.actions.act_window_close'} return result
class ImLivechatChannelRule(models.Model): """ Channel Rules Rules defining access to the channel (countries, and url matching). It also provide the 'auto pop' option to open automatically the conversation. """ _name = 'im_livechat.channel.rule' _description = 'Channel Rules' _order = 'sequence asc' regex_url = fields.Char('URL Regex', help="Regular expression identifying the web page on which the rules will be applied.") action = fields.Selection([('display_button', 'Display the button'), ('auto_popup', 'Auto popup'), ('hide_button', 'Hide the button')], string='Action', required=True, default='display_button', help="* Select 'Display the button' to simply display the chat button on the pages.\n"\ "* Select 'Auto popup' for to display the button, and automatically open the conversation window.\n"\ "* Select 'Hide the button' to hide the chat button on the pages.") auto_popup_timer = fields.Integer('Auto popup timer', default=0, help="Delay (in seconds) to automatically open the converssation window. Note : the selected action must be 'Auto popup', otherwise this parameter will not be take into account.") channel_id = fields.Many2one('im_livechat.channel', 'Channel', help="The channel of the rule") country_ids = fields.Many2many('res.country', 'im_livechat_channel_country_rel', 'channel_id', 'country_id', 'Country', help="The actual rule will match only for this country. So if you set select 'Belgium' and 'France' and you set the action to 'Hide Buttun', this 2 country will not be see the support button for the specified URL. This feature requires GeoIP installed on your server.") sequence = fields.Integer('Matching order', default=10, help="Given the order to find a matching rule. If 2 rules are matching for the given url/country, the one with the lowest sequence will be chosen.") def match_rule(self, channel_id, url, country_id=False): """ determine if a rule of the given channel match with the given url :param channel_id : the identifier of the channel_id :param url : the url to match with a rule :param country_id : the identifier of the country :returns the rule that match the given condition. False otherwise. :rtype : im_livechat.channel.rule """ def _match(rules): for rule in rules: if re.search(rule.regex_url, url): return rule return False # first, search the country specific rules (the first match is returned) if country_id: # don't include the country in the research if geoIP is not installed domain = [('country_ids', 'in', [country_id]), ('channel_id', '=', channel_id)] rule = _match(self.search(domain)) if rule: return rule # second, fallback on the rules without country domain = [('country_ids', '=', False), ('channel_id', '=', channel_id)] return _match(self.search(domain))
class SaasPortalClient(models.Model): _inherit = 'saas_portal.client' category_ids = fields.Many2many( 'saas.portal.category', string='Tags' ) @api.model @api.returns('self', lambda value:value.id) def create(self, vals): if vals.get('plan_id'): plan = self.env['saas_portal.plan'].browse(vals['plan_id']) vals['category_ids'] = [(6, 0, plan.category_ids.ids)] return super(SaasPortalClient, self).create(vals)
class AccountBalanceReport(models.TransientModel): _inherit = "account.common.account.report" _name = 'account.balance.report' _description = 'Trial Balance Report' journal_ids = fields.Many2many('account.journal', 'account_balance_report_journal_rel', 'account_id', 'journal_id', string='Journals', required=True, default=[]) def _print_report(self, data): data = self.pre_print_report(data) return self.env['report'].get_action(self, 'account.report_trialbalance', data=data)
class SaasTagClient(models.TransientModel): _name = 'saas_portal.tag_client' @api.model def _default_categories(self): client = self.env['saas_portal.client'].browse( self.env.context['active_id']) return client.category_ids.ids category_ids = fields.Many2many('saas.portal.category', string='Tags', default=_default_categories) @api.multi def apply(self): self.ensure_one() client = self.env['saas_portal.client'].browse( self.env.context['active_id']) client.write({'category_ids': [(6, 0, self.category_ids.ids)]}) return True
class TemplatePreview(models.TransientModel): _inherit = "mail.template" _name = "email_template.preview" _description = "Email Template Preview" @api.model def _get_records(self): """ Return Records of particular Email Template's Model """ template_id = self._context.get('template_id') default_res_id = self._context.get('default_res_id') if not template_id: return [] template = self.env['mail.template'].browse(int(template_id)) records = self.env[template.model_id.model].search([], limit=10) records |= records.browse(default_res_id) return records.name_get() @api.model def default_get(self, fields): result = super(TemplatePreview, self).default_get(fields) if 'res_id' in fields and not result.get('res_id'): records = self._get_records() result['res_id'] = records and records[0][0] or False # select first record as a Default if self._context.get('template_id') and 'model_id' in fields and not result.get('model_id'): result['model_id'] = self.env['mail.template'].browse(self._context['template_id']).model_id.id return result res_id = fields.Selection(_get_records, 'Sample Document') partner_ids = fields.Many2many('res.partner', string='Recipients') @api.onchange('res_id') @api.multi def on_change_res_id(self): mail_values = {} if self.res_id and self._context.get('template_id'): template = self.env['mail.template'].browse(self._context['template_id']) self.name = template.name mail_values = template.generate_email(self.res_id) for field in ['email_from', 'email_to', 'email_cc', 'reply_to', 'subject', 'body_html', 'partner_to', 'partner_ids', 'attachment_ids']: setattr(self, field, mail_values.get(field, False))
class AccountAgedTrialBalance(models.TransientModel): _name = 'account.aged.trial.balance' _inherit = 'account.common.partner.report' _description = 'Account Aged Trial balance Report' period_length = fields.Integer(string='Period Length (days)', required=True, default=30) journal_ids = fields.Many2many('account.journal', string='Journals', required=True) date_from = fields.Date(default=lambda *a: time.strftime('%Y-%m-%d')) def _print_report(self, data): res = {} data = self.pre_print_report(data) data['form'].update(self.read(['period_length'])[0]) period_length = data['form']['period_length'] if period_length <= 0: raise UserError(_('You must set a period length greater than 0.')) if not data['form']['date_from']: raise UserError(_('You must set a start date.')) start = datetime.strptime(data['form']['date_from'], "%Y-%m-%d") for i in range(5)[::-1]: stop = start - relativedelta(days=period_length) res[str(i)] = { 'name': (i != 0 and (str( (5 - (i + 1)) * period_length) + '-' + str( (5 - i) * period_length)) or ('+' + str(4 * period_length))), 'stop': start.strftime('%Y-%m-%d'), 'start': (i != 0 and stop.strftime('%Y-%m-%d') or False), } start = stop - relativedelta(days=1) data['form'].update(res) return self.env['report'].with_context(landscape=True).get_action( self, 'account.report_agedpartnerbalance', data=data)
class tip(models.Model): _name = 'web.tip' _description = 'Tips' @api.one @api.depends('user_ids') def _is_consumed(self): self.is_consumed = self.env.user in self.user_ids title = fields.Char('Tip title') description = fields.Html('Tip Description', required=True) action_id = fields.Many2one('ir.actions.act_window', string="Action", help="The action that will trigger the tip") model = fields.Char( "Model", help="Model name on which to trigger the tip, e.g. 'res.partner'.") type = fields.Char( "Type", help="Model type, e.g. lead or opportunity for crm.lead") mode = fields.Char("Mode", help="Mode, e.g. kanban, form") trigger_selector = fields.Char( 'Trigger selector', help= 'CSS selectors used to trigger the tip, separated by a comma (ANDed).') highlight_selector = fields.Char( 'Highlight selector', help='CSS selector for the element to highlight') end_selector = fields.Char('End selector', help='CSS selector used to end the tip') end_event = fields.Char('End event', help='Event to end the tip', default='click') placement = fields.Char( 'Placement', help='Popover placement, bottom, top, left or right', default='auto') user_ids = fields.Many2many('res.users', string='Consumed by') is_consumed = fields.Boolean(string='Tip consumed', compute='_is_consumed') @api.multi def consume(self): self.write({'user_ids': [(4, self.env.uid)]})
class account_analytic_line(models.Model): _name = 'account.analytic.line' _description = 'Analytic Line' _order = 'date desc, id desc' @api.model def _default_user(self): return self.env.user.id name = fields.Char('Description', required=True) date = fields.Date('Date', required=True, index=True, default=fields.Date.context_today) amount = fields.Monetary('Amount', required=True, default=0.0) unit_amount = fields.Float('Quantity', default=0.0) account_id = fields.Many2one('account.analytic.account', 'Analytic Account', required=True, ondelete='restrict', index=True) partner_id = fields.Many2one('res.partner', string='Partner') user_id = fields.Many2one('res.users', string='User', default=_default_user) tag_ids = fields.Many2many('account.analytic.tag', 'account_analytic_line_tag_rel', 'line_id', 'tag_id', string='Tags', copy=True) company_id = fields.Many2one(related='account_id.company_id', string='Company', store=True, readonly=True) currency_id = fields.Many2one(related="company_id.currency_id", string="Currency", readonly=True)
class Applicant(models.Model): _name = "hr.applicant" _description = "Applicant" _order = "priority desc, id desc" _inherit = ['mail.thread', 'ir.needaction_mixin', 'utm.mixin'] _mail_mass_mailing = _('Applicants') def _default_stage_id(self): if self._context.get('default_job_id'): ids = self.env['hr.recruitment.stage'].search( [('job_ids', '=', self._context['default_job_id']), ('fold', '=', False)], order='sequence asc', limit=1).ids if ids: return ids[0] return False def _default_company_id(self): company_id = False if self._context.get('default_department_id'): department = self.env['hr.department'].browse( self._context['default_department_id']) company_id = department.company_id.id if not company_id: company_id = self.env['res.company']._company_default_get( 'hr.applicant') return company_id name = fields.Char("Subject / Application Name", required=True) active = fields.Boolean( "Active", default=True, help= "If the active field is set to false, it will allow you to hide the case without removing it." ) description = fields.Text("Description") email_from = fields.Char("Email", size=128, help="These people will receive email.") email_cc = fields.Text( "Watchers Emails", size=252, 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" ) probability = fields.Float("Probability") partner_id = fields.Many2one('res.partner', "Contact") create_date = fields.Datetime("Creation Date", readonly=True, select=True) write_date = fields.Datetime("Update Date", readonly=True) stage_id = fields.Many2one('hr.recruitment.stage', 'Stage', track_visibility='onchange', domain="[('job_ids', '=', job_id)]", copy=False, select=1, default=_default_stage_id) last_stage_id = fields.Many2one( 'hr.recruitment.stage', "Last Stage", help= "Stage of the applicant before being in the current stage. Used for lost cases analysis." ) categ_ids = fields.Many2many('hr.applicant.category', string="Tags") company_id = fields.Many2one('res.company', "Company", default=_default_company_id) user_id = fields.Many2one('res.users', "Responsible", track_visibility="onchange", default=lambda self: self.env.uid) date_closed = fields.Datetime("Closed", readonly=True, select=True) date_open = fields.Datetime("Assigned", readonly=True, select=True) date_last_stage_update = fields.Datetime("Last Stage Update", select=True, default=fields.Datetime.now) date_action = fields.Date("Next Action Date") title_action = fields.Char("Next Action", size=64) priority = fields.Selection(AVAILABLE_PRIORITIES, "Appreciation", default='0') job_id = fields.Many2one('hr.job', "Applied Job") salary_proposed_extra = fields.Char( "Proposed Salary Extra", help="Salary Proposed by the Organisation, extra advantages") salary_expected_extra = fields.Char( "Expected Salary Extra", help="Salary Expected by Applicant, extra advantages") salary_proposed = fields.Float("Proposed Salary", help="Salary Proposed by the Organisation") salary_expected = fields.Float("Expected Salary", help="Salary Expected by Applicant") availability = fields.Date( "Availability", help= "The date at which the applicant will be available to start working") partner_name = fields.Char("Applicant's Name") partner_phone = fields.Char("Phone", size=32) partner_mobile = fields.Char("Mobile", size=32) type_id = fields.Many2one('hr.recruitment.degree', "Degree") department_id = fields.Many2one('hr.department', "Department") survey = fields.Many2one('survey.survey', related='job_id.survey_id', string="Survey") # TDE FIXME: rename to survey_id response_id = fields.Many2one('survey.user_input', "Response", ondelete="set null", oldname="response") reference = fields.Char("Referred By") day_open = fields.Float(compute='_compute_day', string="Days to Open") day_close = fields.Float(compute='_compute_day', string="Days to Close") color = fields.Integer("Color Index", default=0) emp_id = fields.Many2one('hr.employee', string="Employee", track_visibility="onchange", help="Employee linked to the applicant.") user_email = fields.Char(related='user_id.email', type="char", string="User Email", readonly=True) attachment_number = fields.Integer(compute='_get_attachment_number', string="Number of Attachments") employee_name = fields.Char(related='emp_id.name', string="Employee Name") attachment_ids = fields.One2many('ir.attachment', 'res_id', domain=[('res_model', '=', 'hr.applicant') ], string='Attachments') @api.depends('date_open', 'date_closed') @api.one def _compute_day(self): if self.date_open: date_create = datetime.strptime( self.create_date, tools.DEFAULT_SERVER_DATETIME_FORMAT) date_open = datetime.strptime(self.date_open, tools.DEFAULT_SERVER_DATETIME_FORMAT) self.day_open = (date_open - date_create).total_seconds() / (24.0 * 3600) if self.date_closed: date_create = datetime.strptime( self.create_date, tools.DEFAULT_SERVER_DATETIME_FORMAT) date_closed = datetime.strptime( self.date_closed, tools.DEFAULT_SERVER_DATETIME_FORMAT) self.day_close = (date_closed - date_create).total_seconds() / (24.0 * 3600) @api.multi def _get_attachment_number(self): read_group_res = self.env['ir.attachment'].read_group( [('res_model', '=', 'hr.applicant'), ('res_id', 'in', self.ids)], ['res_id'], ['res_id']) attach_data = dict( (res['res_id'], res['res_id_count']) for res in read_group_res) for record in self: record.attachment_number = attach_data.get(record.id, 0) @api.model def _read_group_stage_ids(self, ids, domain, read_group_order=None, access_rights_uid=None): access_rights_uid = access_rights_uid or self.env.uid Stage = self.env['hr.recruitment.stage'] order = Stage._order # lame hack to allow reverting search, should just work in the trivial case if read_group_order == 'stage_id desc': order = "%s desc" % order # retrieve job_id from the context and write the domain: ids + contextual columns (job or default) job_id = self._context.get('default_job_id') department_id = self._context.get('default_department_id') search_domain = [] if job_id: search_domain = [('job_ids', '=', job_id)] if department_id: if search_domain: search_domain = [ '|', ('job_ids.department_id', '=', department_id) ] + search_domain else: search_domain = [('job_ids.department_id', '=', department_id)] if self.ids: if search_domain: search_domain = ['|', ('id', 'in', self.ids)] + search_domain else: search_domain = [('id', 'in', self.ids)] stage_ids = Stage._search(search_domain, order=order, access_rights_uid=access_rights_uid) stages = Stage.sudo(access_rights_uid).browse(stage_ids) result = stages.name_get() # restore order of the search result.sort( lambda x, y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0]))) fold = {} for stage in stages: fold[stage.id] = stage.fold or False return result, fold _group_by_full = {'stage_id': _read_group_stage_ids} @api.onchange('job_id') def onchange_job_id(self): vals = self._onchange_job_id_internal(self.job_id.id) self.department_id = vals['value']['department_id'] self.user_id = vals['value']['user_id'] self.stage_id = vals['value']['stage_id'] def _onchange_job_id_internal(self, job_id): department_id = False user_id = False stage_id = self.stage_id.id if job_id: job = self.env['hr.job'].browse(job_id) department_id = job.department_id.id user_id = job.user_id.id if not self.stage_id: stage_ids = self.env['hr.recruitment.stage'].search( [('job_ids', '=', job.id), ('fold', '=', False)], order='sequence asc', limit=1).ids stage_id = stage_ids[0] if stage_ids else False return { 'value': { 'department_id': department_id, 'user_id': user_id, 'stage_id': stage_id } } @api.onchange('partner_id') def onchange_partner_id(self): self.partner_phone = self.partner_id.phone self.partner_mobile = self.partner_id.mobile self.email_from = self.partner_id.email @api.onchange('stage_id') def onchange_stage_id(self): vals = self._onchange_stage_id_internal(self.stage_id.id) if vals['value'].get('date_closed'): self.date_closed = vals['value']['date_closed'] def _onchange_stage_id_internal(self, stage_id): if not stage_id: return {'value': {}} stage = self.env['hr.recruitment.stage'].browse(stage_id) if stage.fold: return {'value': {'date_closed': fields.datetime.now()}} return {'value': {'date_closed': False}} @api.model def create(self, vals): if vals.get('department_id' ) and not self._context.get('default_department_id'): self = self.with_context( default_department_id=vals.get('department_id')) if vals.get('job_id') or self._context.get('default_job_id'): job_id = vals.get('job_id') or self._context.get('default_job_id') for key, value in self._onchange_job_id_internal( job_id)['value'].iteritems(): if key not in vals: vals[key] = value if vals.get('user_id'): vals['date_open'] = fields.Datetime.now() if 'stage_id' in vals: vals.update( self._onchange_stage_id_internal( vals.get('stage_id'))['value']) return super(Applicant, self.with_context(mail_create_nolog=True)).create(vals) @api.multi def write(self, vals): # user_id change: update date_open if vals.get('user_id'): vals['date_open'] = fields.Datetime.now() # stage_id: track last stage before update if 'stage_id' in vals: vals['date_last_stage_update'] = fields.Datetime.now() vals.update( self._onchange_stage_id_internal( vals.get('stage_id'))['value']) for applicant in self: vals['last_stage_id'] = applicant.stage_id.id res = super(Applicant, self).write(vals) else: res = super(Applicant, self).write(vals) # post processing: if stage changed, post a message in the chatter if vals.get('stage_id'): if self.stage_id.template_id: self.message_post_with_template(self.stage_id.template_id.id, notify=True, composition_mode='mass_mail') return res @api.model def get_empty_list_help(self, help): return super( Applicant, self.with_context( empty_list_help_model='hr.job', empty_list_help_id=self.env.context.get('default_job_id'), empty_list_help_document_name=_( "job applicants"))).get_empty_list_help(help) @api.multi def action_get_created_employee(self): self.ensure_one() action = self.env['ir.actions.act_window'].for_xml_id( 'hr', 'open_view_employee_list') action['res_id'] = self.mapped('emp_id').ids[0] return action @api.multi def action_makeMeeting(self): """ This opens Meeting's calendar view to schedule meeting on current applicant @return: Dictionary value for created Meeting view """ self.ensure_one() partners = self.partner_id | self.user_id.partner_id | self.department_id.manager_id.user_id.partner_id category = self.env.ref('hr_recruitment.categ_meet_interview') res = self.env['ir.actions.act_window'].for_xml_id( 'calendar', 'action_calendar_event') res['context'] = { 'search_default_partner_ids': self.partner_id.name, 'default_partner_ids': partners.ids, 'default_user_id': self.env.uid, 'default_name': self.name, 'default_categ_ids': category and [category.id] or False, } return res @api.multi def action_start_survey(self): self.ensure_one() # create a response and link it to this applicant if not self.response_id: response = self.env['survey.user_input'].create({ 'survey_id': self.survey.id, 'partner_id': self.partner_id.id }) self.response_id = response.id else: response = self.response_id # grab the token of the response and start surveying return self.survey.with_context( survey_token=response.token).action_start_survey() @api.multi def action_print_survey(self): """ If response is available then print this response otherwise print survey form (print template of the survey) """ self.ensure_one() if not self.response_id: return self.survey.action_print_survey() else: response = self.response_id return self.survey.with_context( survey_token=response.token).action_print_survey() @api.multi def action_get_attachment_tree_view(self): attachment_action = self.env.ref('base.action_attachment') action = attachment_action.read()[0] action['context'] = { 'default_res_model': self._name, 'default_res_id': self.ids[0] } action['domain'] = str( ['&', ('res_model', '=', self._name), ('res_id', 'in', self.ids)]) return action @api.multi def _track_subtype(self, init_values): record = self[0] if 'emp_id' in init_values and record.emp_id: return 'hr_recruitment.mt_applicant_hired' elif 'stage_id' in init_values and record.stage_id and record.stage_id.sequence <= 1: return 'hr_recruitment.mt_applicant_new' elif 'stage_id' in init_values and record.stage_id and record.stage_id.sequence > 1: return 'hr_recruitment.mt_applicant_stage_changed' return super(Applicant, self)._track_subtype(init_values) @api.model def message_get_reply_to(self, ids, default=None): """ Override to get the reply_to of the parent project. """ applicants = self.sudo().browse(ids) aliases = self.env['hr.job'].message_get_reply_to( applicants.mapped('job_id').ids, default=default) return dict( (applicant.id, aliases.get(applicant.job_id and applicant.job_id.id or 0, False)) for applicant in applicants) @api.multi def message_get_suggested_recipients(self): recipients = super(Applicant, self).message_get_suggested_recipients() for applicant in self: if applicant.partner_id: applicant._message_add_suggested_recipient( recipients, partner=applicant.partner_id, reason=_('Contact')) elif applicant.email_from: applicant._message_add_suggested_recipient( recipients, email=applicant.email_from, reason=_('Contact Email')) return recipients @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. """ val = msg.get('from').split('<')[0] defaults = { 'name': msg.get('subject') or _("No Subject"), 'partner_name': val, 'email_from': msg.get('from'), 'email_cc': msg.get('cc'), 'user_id': False, 'partner_id': msg.get('author_id', False), } if msg.get('priority'): defaults['priority'] = msg.get('priority') if custom_values: defaults.update(custom_values) return super(Applicant, self).message_new(msg, custom_values=defaults) @api.multi def create_employee_from_applicant(self): """ Create an hr.employee from the hr.applicants """ employee = False for applicant in self: address_id = contact_name = False if applicant.partner_id: address_id = applicant.partner_id.address_get(['contact' ])['contact'] contact_name = applicant.partner_id.name_get()[0][1] if applicant.job_id and (applicant.partner_name or contact_name): applicant.job_id.write({ 'no_of_hired_employee': applicant.job_id.no_of_hired_employee + 1 }) employee = self.env['hr.employee'].create({ 'name': applicant.partner_name or contact_name, 'job_id': applicant.job_id.id, 'address_home_id': address_id, 'department_id': applicant.department_id.id or False, 'address_id': applicant.company_id and applicant.company_id.partner_id and applicant.company_id.partner_id.id or False, 'work_email': applicant.department_id and applicant.department_id.company_id and applicant.department_id.company_id.email or False, 'work_phone': applicant.department_id and applicant.department_id.company_id and applicant.department_id.company_id.phone or False }) applicant.write({'emp_id': employee.id}) applicant.job_id.message_post( body=_('New Employee %s Hired') % applicant.partner_name if applicant.partner_name else applicant.name, subtype="hr_recruitment.mt_job_applicant_hired") employee._broadcast_welcome() else: raise UserError( _('You must define an Applied Job and a Contact Name for this applicant.' )) employee_action = self.env.ref('hr.open_view_employee_list') dict_act_window = employee_action.read([])[0] if employee: dict_act_window['res_id'] = employee.id dict_act_window['view_mode'] = 'form,tree' return dict_act_window @api.multi def archive_applicant(self): self.write({'active': False}) @api.multi def reset_applicant(self): """ Reinsert the applicant into the recruitment pipe in the first stage""" for applicant in self: first_stage_obj = self.env['hr.recruitment.stage'].search( [('job_ids', 'in', applicant.job_id.id)], order="sequence asc", limit=1) applicant.write({'active': True, 'stage_id': first_stage_obj.id})
class Slide(models.Model): """ This model represents actual presentations. Those must be one of four types: - Presentation - Document - Infographic - Video Slide has various statistics like view count, embed count, like, dislikes """ _name = 'slide.slide' _inherit = [ 'mail.thread', 'website.seo.metadata', 'website.published.mixin' ] _description = 'Slides' _PROMOTIONAL_FIELDS = [ '__last_update', 'name', 'image_thumb', 'image_medium', 'slide_type', 'total_views', 'category_id', 'channel_id', 'description', 'tag_ids', 'write_date', 'create_date', 'website_published', 'website_url', 'website_meta_title', 'website_meta_description', 'website_meta_keywords' ] _sql_constraints = [('name_uniq', 'UNIQUE(channel_id, name)', 'The slide name must be unique within a channel')] # description name = fields.Char('Title', required=True, translate=True) description = fields.Text('Description', translate=True) channel_id = fields.Many2one('slide.channel', string="Channel", required=True) category_id = fields.Many2one('slide.category', string="Category", domain="[('channel_id', '=', channel_id)]") tag_ids = fields.Many2many('slide.tag', 'rel_slide_tag', 'slide_id', 'tag_id', string='Tags') download_security = fields.Selection([('none', 'No One'), ('user', 'Authentified Users Only'), ('public', 'Everyone')], string='Download Security', required=True, default='user') image = fields.Binary('Image', attachment=True) image_medium = fields.Binary('Medium', compute="_get_image", store=True, attachment=True) image_thumb = fields.Binary('Thumbnail', compute="_get_image", store=True, attachment=True) @api.depends('image') def _get_image(self): for record in self: if record.image: record.image_medium = image.crop_image(record.image, type='top', ratio=(4, 3), thumbnail_ratio=4) record.image_thumb = image.crop_image(record.image, type='top', ratio=(4, 3), thumbnail_ratio=6) else: record.image_medium = False record.iamge_thumb = False # content slide_type = fields.Selection( [('infographic', 'Infographic'), ('presentation', 'Presentation'), ('document', 'Document'), ('video', 'Video')], string='Type', required=True, default='document', help= "Document type will be set automatically depending on file type, height and width." ) index_content = fields.Text('Transcript') datas = fields.Binary('Content') url = fields.Char('Document URL', help="Youtube or Google Document URL") document_id = fields.Char('Document ID', help="Youtube or Google Document ID") mime_type = fields.Char('Mime-type') @api.onchange('url') def on_change_url(self): self.ensure_one() if self.url: res = self._parse_document_url(self.url) if res.get('error'): raise Warning( _('Could not fetch data from url. Document or access right not available:\n%s' ) % res['error']) values = res['values'] if not values.get('document_id'): raise Warning( _('Please enter valid Youtube or Google Doc URL')) for key, value in values.iteritems(): setattr(self, key, value) # website date_published = fields.Datetime('Publish Date') website_message_ids = fields.One2many( 'mail.message', 'res_id', domain=lambda self: [('model', '=', self._name), ('message_type', '=', 'comment')], string='Website Messages', help="Website communication history") likes = fields.Integer('Likes') dislikes = fields.Integer('Dislikes') # views embedcount_ids = fields.One2many('slide.embed', 'slide_id', string="Embed Count") slide_views = fields.Integer('# of Website Views') embed_views = fields.Integer('# of Embedded Views') total_views = fields.Integer("Total # Views", default="0", compute='_compute_total', store=True) @api.depends('slide_views', 'embed_views') def _compute_total(self): for record in self: record.total_views = record.slide_views + record.embed_views embed_code = fields.Text('Embed Code', readonly=True, compute='_get_embed_code') def _get_embed_code(self): base_url = self.env['ir.config_parameter'].get_param('web.base.url') for record in self: if record.datas and not record.document_id: record.embed_code = '<iframe src="%s/slides/embed/%s?page=1" allowFullScreen="true" height="%s" width="%s" frameborder="0"></iframe>' % ( base_url, record.id, 315, 420) elif record.slide_type == 'video' and record.document_id: if not record.mime_type: # embed youtube video record.embed_code = '<iframe src="//www.youtube.com/embed/%s?theme=light" allowFullScreen="true" frameborder="0"></iframe>' % ( record.document_id) else: # embed google doc video record.embed_code = '<embed src="https://video.google.com/get_player?ps=docs&partnerid=30&docid=%s" type="application/x-shockwave-flash"></embed>' % ( record.document_id) else: record.embed_code = False @api.multi @api.depends('name') def _website_url(self, name, arg): res = super(Slide, self)._website_url(name, arg) base_url = self.env['ir.config_parameter'].get_param('web.base.url') #link_tracker is not in dependencies, so use it to shorten url only if installed. if self.env.registry.get('link.tracker'): LinkTracker = self.env['link.tracker'] res.update({(slide.id, LinkTracker.sudo().create({ 'url': '%s/slides/slide/%s' % (base_url, slug(slide)) }).short_url) for slide in self}) else: res.update({(slide.id, '%s/slides/slide/%s' % (base_url, slug(slide))) for slide in self}) return res @api.model def create(self, values): if not values.get('index_content'): values['index_content'] = values.get('description') if values.get( 'slide_type') == 'infographic' and not values.get('image'): values['image'] = values['datas'] if values.get( 'website_published') and not values.get('date_published'): values['date_published'] = datetime.datetime.now() if values.get('url'): doc_data = self._parse_document_url(values['url']).get( 'values', dict()) for key, value in doc_data.iteritems(): values.setdefault(key, value) # Do not publish slide if user has not publisher rights if not self.user_has_groups('base.group_website_publisher'): values['website_published'] = False slide = super(Slide, self).create(values) slide.channel_id.message_subscribe_users() slide._post_publication() return slide @api.multi def write(self, values): if values.get('url'): doc_data = self._parse_document_url(values['url']).get( 'values', dict()) for key, value in doc_data.iteritems(): values.setdefault(key, value) res = super(Slide, self).write(values) if values.get('website_published'): self.date_published = datetime.datetime.now() self._post_publication() return res @api.model def check_field_access_rights(self, operation, fields): """ As per channel access configuration (visibility) - public ==> no restriction on slides access - private ==> restrict all slides of channel based on access group defined on channel group_ids field - partial ==> show channel, but presentations based on groups means any user can see channel but not slide's content. For private: implement using record rule For partial: user can see channel, but channel gridview have slide detail so we have to implement partial field access mechanism for public user so he can have access of promotional field (name, view_count) of slides, but not all fields like data (actual pdf content) all fields should be accessible only for user group defined on channel group_ids """ if self.env.uid == SUPERUSER_ID: return fields or list(self._fields) fields = super(Slide, self).check_field_access_rights(operation, fields) # still read not perform so we can not access self.channel_id if self.ids: self.env.cr.execute( 'SELECT DISTINCT channel_id FROM ' + self._table + ' WHERE id IN %s', (tuple(self.ids), )) channel_ids = [x[0] for x in self.env.cr.fetchall()] channels = self.env['slide.channel'].sudo().browse(channel_ids) limited_access = all( channel.visibility == 'partial' and not len(channel.group_ids & self.env.user.groups_id) for channel in channels) if limited_access: fields = [ field for field in fields if field in self._PROMOTIONAL_FIELDS ] return fields def get_related_slides(self, limit=20): domain = [('website_published', '=', True), ('channel_id.visibility', '!=', 'private'), ('id', '!=', self.id)] if self.category_id: domain += [('category_id', '=', self.category_id.id)] for record in self.search(domain, limit=limit): yield record def get_most_viewed_slides(self, limit=20): for record in self.search([('website_published', '=', True), ('channel_id.visibility', '!=', 'private'), ('id', '!=', self.id)], limit=limit, order='total_views desc'): yield record def _post_publication(self): base_url = self.env['ir.config_parameter'].get_param('web.base.url') for slide in self.filtered(lambda slide: slide.website_published): publish_template = slide.channel_id.publish_template_id html_body = publish_template.with_context({ 'base_url': base_url }).render_template(publish_template.body_html, 'slide.slide', slide.id) slide.channel_id.message_post( body=html_body, subtype='website_slides.mt_channel_slide_published') return True @api.one def send_share_email(self, email): base_url = self.env['ir.config_parameter'].get_param('web.base.url') return self.channel_id.share_template_id.with_context({ 'email': email, 'base_url': base_url }).send_mail(self.id) # -------------------------------------------------- # Parsing methods # -------------------------------------------------- @api.model def _fetch_data(self, base_url, data, content_type=False): result = {'values': dict()} try: if data: base_url = base_url + '?%s' % urlencode(data) req = urllib2.Request(base_url) content = urllib2.urlopen(req).read() if content_type == 'json': result['values'] = json.loads(content) elif content_type in ('image', 'pdf'): result['values'] = content.encode('base64') else: result['values'] = content except urllib2.HTTPError as e: result['error'] = e.read() e.close() except urllib2.URLError as e: result['error'] = e.reason return result def _find_document_data_from_url(self, url): expr = re.compile( r'^.*((youtu.be/)|(v/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#\&\?]*).*' ) arg = expr.match(url) document_id = arg and arg.group(7) or False if document_id: return ('youtube', document_id) expr = re.compile( r'(^https:\/\/docs.google.com|^https:\/\/drive.google.com).*\/d\/([^\/]*)' ) arg = expr.match(url) document_id = arg and arg.group(2) or False if document_id: return ('google', document_id) return (None, False) def _parse_document_url(self, url, only_preview_fields=False): document_source, document_id = self._find_document_data_from_url(url) if document_source and hasattr(self, '_parse_%s_document' % document_source): return getattr(self, '_parse_%s_document' % document_source)( document_id, only_preview_fields) return {'error': _('Unknown document')} def _parse_youtube_document(self, document_id, only_preview_fields): key = self.env['ir.config_parameter'].sudo().get_param( 'website_slides.google_app_key') fetch_res = self._fetch_data( 'https://www.googleapis.com/youtube/v3/videos', { 'id': document_id, 'key': key, 'part': 'snippet', 'fields': 'items(id,snippet)' }, 'json') if fetch_res.get('error'): return fetch_res values = {'slide_type': 'video', 'document_id': document_id} youtube_values = fetch_res['values'].get('items', list(dict()))[0] if youtube_values.get('snippet'): snippet = youtube_values['snippet'] if only_preview_fields: values.update({ 'url_src': snippet['thumbnails']['high']['url'], 'title': snippet['title'], 'description': snippet['description'] }) return values values.update({ 'name': snippet['title'], 'image': self._fetch_data(snippet['thumbnails']['high']['url'], {}, 'image')['values'], 'description': snippet['description'], }) return {'values': values} @api.model def _parse_google_document(self, document_id, only_preview_fields): def get_slide_type(vals): # TDE FIXME: WTF ?? image = Image.open(io.BytesIO(vals['image'].decode('base64'))) width, height = image.size if height > width: return 'document' else: return 'presentation' key = self.env['ir.config_parameter'].sudo().get_param( 'website_slides.google_app_key') fetch_res = self._fetch_data( 'https://www.googleapis.com/drive/v2/files/%s' % document_id, { 'projection': 'BASIC', 'key': key }, "json") if fetch_res.get('error'): return fetch_res google_values = fetch_res['values'] if only_preview_fields: return { 'url_src': google_values['thumbnailLink'], 'title': google_values['title'], } values = { 'name': google_values['title'], 'image': self._fetch_data( google_values['thumbnailLink'].replace('=s220', ''), {}, 'image')['values'], 'mime_type': google_values['mimeType'], 'document_id': document_id, } if google_values['mimeType'].startswith('video/'): values['slide_type'] = 'video' elif google_values['mimeType'].startswith('image/'): values['datas'] = values['image'] values['slide_type'] = 'infographic' elif google_values['mimeType'].startswith( 'application/vnd.google-apps'): values['datas'] = self._fetch_data( google_values['exportLinks']['application/pdf'], {}, 'pdf')['values'] values['slide_type'] = get_slide_type(values) if google_values['exportLinks'].get('text/plain'): values['index_content'] = self._fetch_data( google_values['exportLinks']['text/plain'], {})['values'] if google_values['exportLinks'].get('text/csv'): values['index_content'] = self._fetch_data( google_values['exportLinks']['text/csv'], {})['values'] elif google_values['mimeType'] == 'application/pdf': # TODO: Google Drive PDF document doesn't provide plain text transcript values['datas'] = self._fetch_data(google_values['webContentLink'], {}, 'pdf')['values'] values['slide_type'] = get_slide_type(values) return {'values': values}
class Channel(models.Model): """ A channel is a container of slides. It has group-based access configuration allowing to configure slide upload and access. Slides can be promoted in channels. """ _name = 'slide.channel' _description = 'Channel for Slides' _inherit = [ 'mail.thread', 'website.seo.metadata', 'website.published.mixin' ] _order = 'sequence, id' _order_by_strategy = { 'most_viewed': 'total_views desc', 'most_voted': 'likes desc', 'latest': 'date_published desc', } name = fields.Char('Name', translate=True, required=True) description = fields.Html('Description', translate=True) sequence = fields.Integer(default=10, help='Display order') category_ids = fields.One2many('slide.category', 'channel_id', string="Categories") slide_ids = fields.One2many('slide.slide', 'channel_id', string="Slides") promote_strategy = fields.Selection([('none', 'No Featured Presentation'), ('latest', 'Latest Published'), ('most_voted', 'Most Voted'), ('most_viewed', 'Most Viewed'), ('custom', 'Featured Presentation')], string="Featuring Policy", default='most_voted', required=True) custom_slide_id = fields.Many2one('slide.slide', string='Slide to Promote') promoted_slide_id = fields.Many2one('slide.slide', string='Featured Slide', compute='_compute_promoted_slide_id', store=True) @api.depends('custom_slide_id', 'promote_strategy', 'slide_ids.likes', 'slide_ids.total_views', "slide_ids.date_published") def _compute_promoted_slide_id(self): for record in self: if record.promote_strategy == 'none': record.promoted_slide_id = False elif record.promote_strategy == 'custom': record.promoted_slide_id = record.custom_slide_id elif record.promote_strategy: slides = self.env['slide.slide'].search( [('website_published', '=', True), ('channel_id', '=', record.id)], limit=1, order=self._order_by_strategy[record.promote_strategy]) record.promoted_slide_id = slides and slides[0] or False nbr_presentations = fields.Integer('Number of Presentations', compute='_count_presentations', store=True) nbr_documents = fields.Integer('Number of Documents', compute='_count_presentations', store=True) nbr_videos = fields.Integer('Number of Videos', compute='_count_presentations', store=True) nbr_infographics = fields.Integer('Number of Infographics', compute='_count_presentations', store=True) total = fields.Integer(compute='_count_presentations', store=True) @api.depends('slide_ids.slide_type', 'slide_ids.website_published') def _count_presentations(self): result = dict.fromkeys(self.ids, dict()) res = self.env['slide.slide'].read_group( [('website_published', '=', True), ('channel_id', 'in', self.ids)], ['channel_id', 'slide_type'], ['channel_id', 'slide_type'], lazy=False) for res_group in res: result[res_group['channel_id'][0]][res_group[ 'slide_type']] = result[res_group['channel_id'][0]].get( res_group['slide_type'], 0) + res_group['__count'] for record in self: record.nbr_presentations = result[record.id].get('presentation', 0) record.nbr_documents = result[record.id].get('document', 0) record.nbr_videos = result[record.id].get('video', 0) record.nbr_infographics = result[record.id].get('infographic', 0) record.total = record.nbr_presentations + record.nbr_documents + record.nbr_videos + record.nbr_infographics publish_template_id = fields.Many2one( 'mail.template', string='Published Template', help="Email template to send slide publication through email", default=lambda self: self.env['ir.model.data'].xmlid_to_res_id( 'website_slides.slide_template_published')) share_template_id = fields.Many2one( 'mail.template', string='Shared Template', help="Email template used when sharing a slide", default=lambda self: self.env['ir.model.data'].xmlid_to_res_id( 'website_slides.slide_template_shared')) visibility = fields.Selection( [('public', 'Public'), ('private', 'Private'), ('partial', 'Show channel but restrict presentations')], default='public', required=True) group_ids = fields.Many2many( 'res.groups', 'rel_channel_groups', 'channel_id', 'group_id', string='Channel Groups', help="Groups allowed to see presentations in this channel") access_error_msg = fields.Html( 'Error Message', help="Message to display when not accessible due to access rights", default= "<p>This channel is private and its content is restricted to some users.</p>", translate=True) upload_group_ids = fields.Many2many( 'res.groups', 'rel_upload_groups', 'channel_id', 'group_id', string='Upload Groups', help= "Groups allowed to upload presentations in this channel. If void, every user can upload." ) # not stored access fields, depending on each user can_see = fields.Boolean('Can See', compute='_compute_access') can_see_full = fields.Boolean('Full Access', compute='_compute_access') can_upload = fields.Boolean('Can Upload', compute='_compute_access') @api.one @api.depends('visibility', 'group_ids', 'upload_group_ids') def _compute_access(self): self.can_see = self.visibility in [ 'public', 'private' ] or bool(self.group_ids & self.env.user.groups_id) self.can_see_full = self.visibility == 'public' or bool( self.group_ids & self.env.user.groups_id) self.can_upload = self.can_see and (not self.upload_group_ids or bool(self.upload_group_ids & self.env.user.groups_id)) @api.multi @api.depends('name') def _website_url(self, name, arg): res = super(Channel, self)._website_url(name, arg) base_url = self.env['ir.config_parameter'].get_param('web.base.url') res.update({(channel.id, '%s/slides/%s' % (base_url, slug(channel))) for channel in self}) return res @api.onchange('visibility') def change_visibility(self): if self.visibility == 'public': self.group_ids = False
class Followers(models.Model): """ mail_followers holds the data related to the follow mechanism inside eCore. Partners can choose to follow documents (records) of any kind that inherits from mail.thread. Following documents allow to receive notifications for new messages. A subscription is characterized by: :param: res_model: model of the followed objects :param: res_id: ID of resource (may be 0 for every objects) """ _name = 'mail.followers' _rec_name = 'partner_id' _log_access = False _description = 'Document Followers' res_model = fields.Char('Related Document Model', required=True, select=1, help='Model of the followed resource') res_id = fields.Integer('Related Document ID', select=1, help='Id of the followed resource') partner_id = fields.Many2one('res.partner', string='Related Partner', ondelete='cascade', select=1) channel_id = fields.Many2one('mail.channel', string='Listener', ondelete='cascade', select=1) subtype_ids = fields.Many2many( 'mail.message.subtype', string='Subtype', help= "Message subtypes followed, meaning subtypes that will be pushed onto the user's Wall." ) @api.model def _add_follower_command(self, res_model, res_ids, partner_data, channel_data, force=True): """ Please upate me :param force: if True, delete existing followers before creating new one using the subtypes given in the parameters """ force_mode = force or (all(data for data in partner_data.values()) and all(data for data in channel_data.values())) generic = [] specific = {} existing = {} # {res_id: follower_ids} p_exist = {} # {partner_id: res_ids} c_exist = {} # {channel_id: res_ids} followers = self.sudo().search([ '&', '&', ('res_model', '=', res_model), ('res_id', 'in', res_ids), '|', ('partner_id', 'in', partner_data.keys()), ('channel_id', 'in', channel_data.keys()) ]) if force_mode: followers.unlink() else: for follower in followers: existing.setdefault(follower.res_id, list()).append(follower) if follower.partner_id: p_exist.setdefault(follower.partner_id.id, list()).append(follower.res_id) if follower.channel_id: c_exist.setdefault(follower.channel_id.id, list()).append(follower.res_id) default_subtypes = self.env['mail.message.subtype'].search([ ('default', '=', True), '|', ('res_model', '=', res_model), ('res_model', '=', False) ]) if force_mode: for pid, data in partner_data.iteritems(): if not data: partner_data[pid] = default_subtypes.ids for cid, data in channel_data.iteritems(): if not data: channel_data[cid] = default_subtypes.ids # create new followers, batch ok gen_new_pids = [ pid for pid in partner_data.keys() if pid not in p_exist ] gen_new_cids = [ cid for cid in channel_data.keys() if cid not in c_exist ] for pid in gen_new_pids: generic.append([ 0, 0, { 'res_model': res_model, 'partner_id': pid, 'subtype_ids': [(6, 0, partner_data.get(pid) or default_subtypes.ids)] } ]) for cid in gen_new_cids: generic.append([ 0, 0, { 'res_model': res_model, 'channel_id': cid, 'subtype_ids': [(6, 0, channel_data.get(cid) or default_subtypes.ids)] } ]) # create new followers, each document at a time because of existing followers to avoid erasing if not force_mode: for res_id in res_ids: command = [] doc_followers = existing.get(res_id, list()) new_pids = set(partner_data.keys()) - set([ sub.partner_id.id for sub in doc_followers if sub.partner_id ]) - set(gen_new_pids) new_cids = set(channel_data.keys()) - set([ sub.channel_id.id for sub in doc_followers if sub.channel_id ]) - set(gen_new_cids) # subscribe new followers for new_pid in new_pids: command.append((0, 0, { 'res_model': res_model, 'partner_id': new_pid, 'subtype_ids': [(6, 0, partner_data.get(new_pid) or default_subtypes.ids)], })) for new_cid in new_cids: command.append((0, 0, { 'res_model': res_model, 'channel_id': new_cid, 'subtype_ids': [(6, 0, channel_data.get(new_cid) or default_subtypes.ids)], })) if command: specific[res_id] = command return generic, specific # # Modifying followers change access rights to individual documents. As the # cache may contain accessible/inaccessible data, one has to refresh it. # @api.model def create(self, vals): res = super(Followers, self).create(vals) self.invalidate_cache() return res @api.multi def write(self, vals): res = super(Followers, self).write(vals) self.invalidate_cache() return res @api.multi def unlink(self): res = super(Followers, self).unlink() self.invalidate_cache() return res _sql_constraints = [ ('mail_followers_res_partner_res_model_id_uniq', 'unique(res_model,res_id,partner_id)', 'Error, a partner cannot follow twice the same object.'), ('mail_followers_res_channel_res_model_id_uniq', 'unique(res_model,res_id,channel_id)', 'Error, a channel cannot follow twice the same object.'), ('partner_xor_channel', 'CHECK((partner_id IS NULL) != (channel_id IS NULL))', 'Error: A follower must be either a partner or a channel (but not both).' ) ]
class account_financial_report(models.Model): _name = "account.financial.report" _description = "Account Report" @api.multi @api.depends('parent_id', 'parent_id.level') def _get_level(self): '''Returns a dictionary with key=the ID of a record and value = the level of this record in the tree structure.''' for report in self: level = 0 if report.parent_id: level = report.parent_id.level + 1 report.level = level def _get_children_by_order(self): '''returns a recordset of all the children computed recursively, and sorted by sequence. Ready for the printing''' res = self children = self.search([('parent_id', 'in', self.ids)], order='sequence ASC') if children: res += children._get_children_by_order() return res name = fields.Char('Report Name', required=True, translate=True) parent_id = fields.Many2one('account.financial.report', 'Parent') children_ids = fields.One2many('account.financial.report', 'parent_id', 'Account Report') sequence = fields.Integer('Sequence') level = fields.Integer(compute='_get_level', string='Level', store=True) type = fields.Selection([ ('sum', 'View'), ('accounts', 'Accounts'), ('account_type', 'Account Type'), ('account_report', 'Report Value'), ], 'Type', default='sum') account_ids = fields.Many2many('account.account', 'account_account_financial_report', 'report_line_id', 'account_id', 'Accounts') account_report_id = fields.Many2one('account.financial.report', 'Report Value') account_type_ids = fields.Many2many( 'account.account.type', 'account_account_financial_report_type', 'report_id', 'account_type_id', 'Account Types') sign = fields.Selection( [(-1, 'Reverse balance sign'), (1, 'Preserve balance sign')], 'Sign on Reports', required=True, default=1, help= 'For accounts that are typically more debited than credited and that you would like to print as negative amounts in your reports, you should reverse the sign of the balance; e.g.: Expense account. The same applies for accounts that are typically more credited than debited and that you would like to print as positive amounts in your reports; e.g.: Income account.' ) display_detail = fields.Selection( [('no_detail', 'No detail'), ('detail_flat', 'Display children flat'), ('detail_with_hierarchy', 'Display children with hierarchy')], 'Display details', default='detail_flat') style_overwrite = fields.Selection( [ (0, 'Automatic formatting'), (1, 'Main Title 1 (bold, underlined)'), (2, 'Title 2 (bold)'), (3, 'Title 3 (bold, smaller)'), (4, 'Normal Text'), (5, 'Italic Text (smaller)'), (6, 'Smallest Text'), ], 'Financial Report Style', default=0, help= "You can set up here the format you want this record to be displayed. If you leave the automatic formatting, it will be computed based on the financial reports hierarchy (auto-computed field 'level')." )
class ModulePrototyper(models.Model): """Module Prototyper gathers different information from all over the database to build a prototype of module. We are calling it a prototype as it will most likely need to be reviewed by a developer to fix glitch that would sneak it during the generation of files but also to add not supported features. """ _name = "module_prototyper" _description = "Module Prototyper" def get_default_description(self): """ Extract the content of default description """ filepath = '{}/../data/README.rst'.format(os.path.dirname(__file__)) with open(filepath, 'r') as content_file: content = content_file.read() return content license = fields.Selection( [(licenses.GPL3, 'GPL Version 3'), (licenses.GPL3_L, 'GPL-3 or later version'), (licenses.LGPL3, 'LGPL-3'), (licenses.LGPL3_L, 'LGPL-3 or later version'), (licenses.AGPL3, 'Affero GPL-3'), (licenses.OSI, 'Other OSI Approved Licence'), ('Other proprietary', 'Other Proprietary')], string='License', default=licenses.AGPL3, ) name = fields.Char( 'Technical Name', required=True, help=('The technical name will be used to define the name of ' 'the exported module, the name of the model.')) category_id = fields.Many2one('ir.module.category', 'Category') human_name = fields.Char( 'Module Name', required=True, help=('The Module Name will be used as the displayed name of the ' 'exported module.')) summary = fields.Char('Summary', required=True, help=('Enter a summary of your module')) description = fields.Text( 'Description', required=True, help=('Enter the description of your module, what it does, how to ' 'install, configure and use it, the roadmap or known issues. ' 'The description will be exported in README.rst'), default=get_default_description) author = fields.Char('Author', required=True, help=('Enter your name')) maintainer = fields.Char( 'Maintainer', help=('Enter the name of the person or organization who will ' 'maintain this module')) website = fields.Char('Website', help=('Enter the URL of your website')) icon_image = fields.Binary( 'Icon', help=('The icon set up here will be used as the icon ' 'for the exported module also')) version = fields.Char( 'Version', size=9, default='5.0.1.0.0', help=('Enter the version of your module with 5 digits')) auto_install = fields.Boolean( 'Auto Install', default=False, help='Check if the module should be install by default.') application = fields.Boolean( 'Application', default=False, help='Check if the module is an eCore application.') # Relations dependency_ids = fields.Many2many( 'ir.module.module', 'module_prototyper_module_rel', 'module_prototyper_id', 'module_id', 'Dependencies', help=('Enter the list of required modules that need to be installed ' 'for your module to work properly')) data_ids = fields.Many2many( 'ir.filters', 'prototype_data_rel', 'module_prototyper_id', 'filter_id', 'Data filters', help="The records matching the filters will be added as data.") demo_ids = fields.Many2many( 'ir.filters', 'prototype_demo_rel', 'module_prototyper_id', 'filter_id', 'Demo filters', help="The records matching the filters will be added as demo data.") field_ids = fields.Many2many( 'ir.model.fields', 'prototype_fields_rel', 'module_prototyper_id', 'field_id', 'Fields', help=('Enter the list of fields that you have created or modified ' 'and want to export in this module. New models will be ' 'exported as long as you choose one of his fields.')) menu_ids = fields.Many2many( 'ir.ui.menu', 'prototype_menu_rel', 'module_prototyper_id', 'menu_id', 'Menu Items', help=('Enter the list of menu items that you have created and want ' 'to export in this module. Related windows actions will be ' 'exported as well.')) view_ids = fields.Many2many( 'ir.ui.view', 'prototype_view_rel', 'module_prototyper_id', 'view_id', 'Views', help=('Enter the list of views that you have created and want to ' 'export in this module.')) group_ids = fields.Many2many( 'res.groups', 'prototype_groups_rel', 'module_prototyper_id', 'group_id', 'Groups', help=('Enter the list of groups that you have created and want to ' 'export in this module.')) right_ids = fields.Many2many( 'ir.model.access', 'prototype_rights_rel', 'module_prototyper_id', 'right_id', 'Access Rights', help=('Enter the list of access rights that you have created and ' 'want to export in this module.')) rule_ids = fields.Many2many( 'ir.rule', 'prototype_rule_rel', 'module_prototyper_id', 'rule_id', 'Record Rules', help=('Enter the list of record rules that you have created and ' 'want to export in this module.')) report_ids = fields.Many2many( 'ir.actions.report.xml', 'prototype_report_rel', 'module_prototyper_id', 'report_id', 'Reports', help=('Enter the list of reports that you have created and ' 'want to export in this module.')) activity_ids = fields.Many2many( 'workflow.activity', 'prototype_wf_activity_rel', 'module_prototyper_id', 'activity_id', 'Activities', help=('Enter the list of workflow activities that you have created ' 'and want to export in this module')) transition_ids = fields.Many2many( 'workflow.transition', 'prototype_wf_transition_rel', 'module_prototyper_id', 'transition_id', 'Transitions', help=('Enter the list of workflow transitions that you have created ' 'and want to export in this module')) _env = None _data_files = () _demo_files = () _field_descriptions = None File_details = namedtuple('file_details', ['filename', 'filecontent']) template_path = '{}/../templates/'.format(os.path.dirname(__file__)) @api.model def set_jinja_env(self, api_version): """Set the Jinja2 environment. The environment will helps the system to find the templates to render. :param api_version: string, ecore api :return: jinja2.Environment instance. """ if self._env is None: self._env = Environment(lstrip_blocks=True, trim_blocks=True, loader=FileSystemLoader( os.path.join(self.template_path, api_version))) return self._env def set_field_descriptions(self): """Mock the list of fields into dictionary. It allows us to add or change attributes of the fields. :return: None """ for field in self.field_ids: field_description = {} # This will mock a field record. # the mock will allow us to add data or modify the data # of the field (like for the name) with keeping all the # attributes of the record. field_description.update({ attr_name: getattr(field, attr_name) for attr_name in dir(field) if not attr_name[0] == '_' }) field_description['name'] = self.unprefix(field.name) self._field_descriptions[field] = field_description @api.model def generate_files(self): """ Generates the files from the details of the prototype. :return: tuple """ assert self._env is not None, \ 'Run set_env(api_version) before to generate files.' # Avoid sharing these across instances self._data_files = [] self._demo_files = [] self._field_descriptions = {} self.set_field_descriptions() file_details = [] file_details.extend(self.generate_models_details()) file_details.extend(self.generate_views_details()) file_details.extend(self.generate_menus_details()) file_details.append(self.generate_module_init_file_details()) file_details.extend(self.generate_data_files()) # must be the last as the other generations might add information # to put in the __ecore__: additional dependencies, views files, etc. file_details.append(self.generate_module_ecore_file_details()) if self.icon_image: file_details.append(self.save_icon()) return file_details @api.model def save_icon(self): """Save the icon of the prototype as a image. The image is used afterwards as the icon of the exported module. :return: FileDetails instance """ # TODO: The image is not always a jpg. # 2 ways to do it: # * find a way to detect image type from the data # * add document as a dependency. # The second options seems to be better, as Document is a base module. return self.File_details( os.path.join('static', 'description', 'icon.jpg'), base64.b64decode(self.icon_image)) @api.model def generate_module_ecore_file_details(self): """Wrapper to generate the __ecore__.py file of the module.""" return self.generate_file_details( '__ecore__.py', '__ecore__.py.template', prototype=self, data_files=self._data_files, demo_fiels=self._demo_files, ) @api.model def generate_module_init_file_details(self): """Wrapper to generate the __init__.py file of the module.""" return self.generate_file_details( '__init__.py', '__init__.py.template', # no import models if no work of fields in # the prototype models=bool(self.field_ids)) @api.model def generate_models_details(self): """ Finds the models from the list of fields and generates the __init__ file and each models files (one by class). """ files = [] # TODO: doesn't work as need to find the module to import # and it is not necessary the name of the model the fields # belongs to. # ie. field.cell_phone is defined in a model inheriting from # res.partner. # How do we find the module the field was defined in? # dependencies = set([dep.id for dep in self.dependencies]) relations = {} field_descriptions = self._field_descriptions or {} for field in field_descriptions.itervalues(): model = field.get('model_id') relations.setdefault(model, []).append(field) # dependencies.add(model.id) # blind update of dependencies. # self.write({ # 'dependencies': [(6, 0, [id_ for id_ in dependencies])] # }) files.append(self.generate_models_init_details(relations.keys())) for model, custom_fields in relations.iteritems(): files.append(self.generate_model_details(model, custom_fields)) return files @api.model def generate_models_init_details(self, ir_models): """Wrapper to generate the __init__.py file in models folder.""" return self.generate_file_details( 'models/__init__.py', 'models/__init__.py.template', models=[ self.friendly_name(ir_model.model) for ir_model in ir_models ]) @api.model def generate_views_details(self): """Wrapper to generate the views files.""" relations = {} for view in self.view_ids: relations.setdefault(view.model, []).append(view) views_details = [] for model, views in relations.iteritems(): filepath = 'views/{}_view.xml'.format( self.friendly_name(self.unprefix(model))) views_details.append( self.generate_file_details(filepath, 'views/model_views.xml.template', views=views)) self._data_files.append(filepath) return views_details @api.model def generate_menus_details(self): """Wrapper to generate the menus files.""" relations = {} for menu in self.menu_ids: if menu.action and menu.action.res_model: model = self.unprefix(menu.action.res_model) else: model = 'ir_ui' relations.setdefault(model, []).append(menu) menus_details = [] for model_name, menus in relations.iteritems(): model_name = self.unprefix(model_name) filepath = 'views/{}_menus.xml'.format( self.friendly_name(model_name)) menus_details.append( self.generate_file_details( filepath, 'views/model_menus.xml.template', menus=menus, )) self._data_files.append(filepath) return menus_details @api.model def generate_model_details(self, model, field_descriptions): """Wrapper to generate the python file for the model. :param model: ir.model record. :param field_descriptions: list of ir.model.fields records. :return: FileDetails instance. """ python_friendly_name = self.friendly_name(self.unprefix(model.model)) return self.generate_file_details( 'models/{}.py'.format(python_friendly_name), 'models/model_name.py.template', name=python_friendly_name, model=model, fields=field_descriptions, ) @api.model def generate_data_files(self): """ Generate data and demo files """ data, demo = {}, {} filters = [(data, ir_filter) for ir_filter in self.data_ids ] + [(demo, ir_filter) for ir_filter in self.demo_ids] for target, ir_filter in filters: model = ir_filter.model_id model_obj = self.env[model] target.setdefault(model, model_obj.browse([])) target[model] |= model_obj.search(safe_eval(ir_filter.domain)) res = [] for prefix, model_data, file_list in [('data', data, self._data_files), ('demo', demo, self._demo_files) ]: for model_name, records in model_data.iteritems(): fname = self.friendly_name(self.unprefix(model_name)) filename = '{0}/{1}.xml'.format(prefix, fname) self._data_files.append(filename) res.append( self.generate_file_details( filename, 'data/model_name.xml.template', model=model_name, records=records, )) return res @classmethod def unprefix(cls, name): if not name: return name return re.sub('^x_', '', name) @classmethod def is_prefixed(cls, name): return bool(re.match('^x_', name)) @classmethod def friendly_name(cls, name): return name.replace('.', '_') @classmethod def fixup_domain(cls, domain): """ Fix a domain according to unprefixing of fields """ res = [] for elem in domain: if len(elem) == 3: elem = list(elem) elem[0] = cls.unprefix(elem[0]) res.append(elem) return res @classmethod def fixup_arch(cls, archstr): doc = lxml.etree.fromstring(archstr) for elem in doc.xpath("//*[@name]"): elem.attrib["name"] = cls.unprefix(elem.attrib["name"]) for elem in doc.xpath("//*[@attrs]"): try: attrs = safe_eval(elem.attrib["attrs"]) except Exception: _logger.error("Unable to eval attribute: %s, skipping", elem.attrib["attrs"]) continue if isinstance(attrs, dict): for key, val in attrs.iteritems(): if isinstance(val, (list, tuple)): attrs[key] = cls.fixup_domain(val) elem.attrib["attrs"] = repr(attrs) for elem in doc.xpath("//field"): # Make fields self-closed by removing useless whitespace if elem.text and not elem.text.strip(): elem.text = None return lxml.etree.tostring(doc) @api.model def generate_file_details(self, filename, template, **kwargs): """ generate file details from jinja2 template. :param filename: name of the file the content is related to :param template: path to the file to render the content :param kwargs: arguments of the template :return: File_details instance """ template = self._env.get_template(template) # keywords used in several templates. kwargs.update({ 'export_year': date.today().year, 'author': self.author, 'website': self.website, 'license_text': licenses.get_license_text(self.license), 'cr': self._cr, # Utility functions 'fixup_arch': self.fixup_arch, 'is_prefixed': self.is_prefixed, 'unprefix': self.unprefix, 'wrap': wrap, }) return self.File_details(filename, template.render(kwargs))
class Channel(models.Model): """ A mail.channel is a discussion group that may behave like a listener on documents. """ _description = 'Discussion channel' _name = 'mail.channel' _mail_flat_thread = False _mail_post_access = 'read' _inherit = ['mail.thread'] _inherits = {'mail.alias': 'alias_id'} def _get_default_image(self): image_path = modules.get_module_resource('mail', 'static/src/img', 'groupdefault.png') return tools.image_resize_image_big(open(image_path, 'rb').read().encode('base64')) name = fields.Char('Name', required=True, translate=True) channel_type = fields.Selection([ ('chat', 'Chat Discussion'), ('channel', 'Channel')], 'Channel Type', default='channel') description = fields.Text('Description') uuid = fields.Char('UUID', size=50, select=True, default=lambda self: '%s' % uuid.uuid4()) email_send = fields.Boolean('Send messages by email', default=False) # multi users channel channel_last_seen_partner_ids = fields.One2many('mail.channel.partner', 'channel_id', string='Last Seen') channel_partner_ids = fields.Many2many('res.partner', 'mail_channel_partner', 'channel_id', 'partner_id', string='Listeners') channel_message_ids = fields.Many2many('mail.message', 'mail_message_mail_channel_rel') message_is_follower = fields.Boolean('Is a member', compute='_compute_message_is_follower') # access public = fields.Selection([ ('public', 'Everyone'), ('private', 'Invited people only'), ('groups', 'Selected group of users')], 'Privacy', required=True, default='groups', help='This group is visible by non members. Invisible groups can add members through the invite button.') group_public_id = fields.Many2one('res.groups', string='Authorized Group', default=lambda self: self.env.ref('base.group_user')) group_ids = fields.Many2many( 'res.groups', rel='mail_channel_res_group_rel', id1='mail_channel_id', id2='groups_id', string='Auto Subscription', help="Members of those groups will automatically added as followers. " "Note that they will be able to manage their subscription manually " "if necessary.") # image: all image fields are base64 encoded and PIL-supported image = fields.Binary("Photo", default=_get_default_image, attachment=True, help="This field holds the image used as photo for the group, limited to 1024x1024px.") image_medium = fields.Binary('Medium-sized photo', compute='_get_image', inverse='_set_image_medium', store=True, attachment=True, help="Medium-sized photo of the group. It is automatically " "resized as a 128x128px image, with aspect ratio preserved. " "Use this field in form views or some kanban views.") image_small = fields.Binary('Small-sized photo', compute='_get_image', inverse='_set_image_small', store=True, attachment=True, help="Small-sized photo of the group. It is automatically " "resized as a 64x64px image, with aspect ratio preserved. " "Use this field anywhere a small image is required.") alias_id = fields.Many2one( 'mail.alias', 'Alias', ondelete="restrict", required=True, help="The email address associated with this group. New emails received will automatically create new topics.") @api.multi def _compute_message_is_follower(self): memberships = self.env['mail.channel.partner'].sudo().search([ ('channel_id', 'in', self.ids), ('partner_id', '=', self.env.user.partner_id.id), ]) membership_ids = memberships.mapped('channel_id') for record in self: record.message_is_follower = record in membership_ids @api.one @api.depends('image') def _get_image(self): self.image_medium = tools.image_resize_image_medium(self.image) self.image_small = tools.image_resize_image_small(self.image) def _set_image_medium(self): self.image = tools.image_resize_image_big(self.image_medium) def _set_image_small(self): self.image = tools.image_resize_image_big(self.image_small) @api.model def create(self, vals): # Create channel and alias channel = super(Channel, self.with_context( alias_model_name=self._name, alias_parent_model_name=self._name, mail_create_nolog=True, mail_create_nosubscribe=True) ).create(vals) channel.alias_id.write({"alias_force_thread_id": channel.id, 'alias_parent_thread_id': channel.id}) if vals.get('group_ids'): channel._subscribe_users() # make channel listen itself: posting on a channel notifies the channel if not self._context.get('mail_channel_noautofollow'): channel.message_subscribe(channel_ids=[channel.id]) return channel @api.multi def unlink(self): aliases = self.mapped('alias_id') # Delete mail.channel try: all_emp_group = self.env.ref('mail.channel_all_employees') except ValueError: all_emp_group = None if all_emp_group and all_emp_group in self: raise UserError(_('You cannot delete those groups, as the Whole Company group is required by other modules.')) res = super(Channel, self).unlink() # Cascade-delete mail aliases as well, as they should not exist without the mail.channel. aliases.sudo().unlink() return res @api.multi def write(self, vals): result = super(Channel, self).write(vals) if vals.get('group_ids'): self._subscribe_users() return result def _subscribe_users(self): for mail_channel in self: mail_channel.write({'channel_partner_ids': [(4, pid) for pid in mail_channel.mapped('group_ids').mapped('users').mapped('partner_id').ids]}) @api.multi def action_follow(self): self.ensure_one() channel_partner = self.mapped('channel_last_seen_partner_ids').filtered(lambda cp: cp.partner_id == self.env.user.partner_id) if not channel_partner: return self.write({'channel_last_seen_partner_ids': [(0, 0, {'partner_id': self.env.user.partner_id.id})]}) @api.multi def action_unfollow(self): partner_id = self.env.user.partner_id.id channel_info = self.channel_info('unsubscribe')[0] # must be computed before leaving the channel (access rights) result = self.write({'channel_partner_ids': [(3, partner_id)]}) self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', partner_id), channel_info) if not self.email_send: notification = _('<div class="o_mail_notification">left <a href="#" class="o_channel_redirect" data-oe-id="%s">#%s</a></div>') % (self.id, self.name,) # post 'channel left' message as root since the partner just unsubscribed from the channel self.sudo().message_post(body=notification, message_type="notification", subtype="mail.mt_comment", author_id=partner_id) return result @api.multi def _notification_group_recipients(self, message, recipients, done_ids, group_data): """ All recipients of a message on a channel are considered as partners. This means they will receive a minimal email, without a link to access in the backend. Mailing lists should indeed send minimal emails to avoid the noise. """ for recipient in recipients: group_data['partner'] |= recipient done_ids.add(recipient.id) return super(Channel, self)._notification_group_recipients(message, recipients, done_ids, group_data) @api.multi def message_get_email_values(self, notif_mail=None): self.ensure_one() res = super(Channel, self).message_get_email_values(notif_mail=notif_mail) headers = {} if res.get('headers'): try: headers.update(eval(res['headers'])) except Exception: pass headers['Precedence'] = 'list' # avoid out-of-office replies from MS Exchange # http://blogs.technet.com/b/exchange/archive/2006/10/06/3395024.aspx headers['X-Auto-Response-Suppress'] = 'OOF' if self.alias_domain and self.alias_name: headers['List-Id'] = '%s.%s' % (self.alias_name, self.alias_domain) headers['List-Post'] = '<mailto:%s@%s>' % (self.alias_name, self.alias_domain) # Avoid users thinking it was a personal message # X-Forge-To: will replace To: after SMTP envelope is determined by ir.mail.server list_to = '"%s" <%s@%s>' % (self.name, self.alias_name, self.alias_domain) headers['X-Forge-To'] = list_to res['headers'] = repr(headers) return res @api.multi def message_get_recipient_values(self, notif_message=None, recipient_ids=None): # real mailing list: multiple recipients (hidden by X-Forge-To) if self.alias_domain and self.alias_name: return { 'email_to': ','.join(formataddr((partner.name, partner.email)) for partner in self.env['res.partner'].sudo().browse(recipient_ids)), 'recipient_ids': [], } return super(Channel, self).message_get_recipient_values(notif_message=notif_message, recipient_ids=recipient_ids) @api.multi @api.returns('self', lambda value: value.id) def message_post(self, body='', subject=None, message_type='notification', subtype=None, parent_id=False, attachments=None, content_subtype='html', **kwargs): # auto pin 'direct_message' channel partner self.filtered(lambda channel: channel.channel_type == 'chat').mapped('channel_last_seen_partner_ids').write({'is_pinned': True}) # apply shortcode (text only) subsitution body = self.env['mail.shortcode'].apply_shortcode(body, shortcode_type='text') message = super(Channel, self.with_context(mail_create_nosubscribe=True)).message_post(body=body, subject=subject, message_type=message_type, subtype=subtype, parent_id=parent_id, attachments=attachments, content_subtype=content_subtype, **kwargs) return message #------------------------------------------------------ # Instant Messaging API #------------------------------------------------------ # A channel header should be broadcasted: # - when adding user to channel (only to the new added partners) # - when folding/minimizing a channel (only to the user making the action) # A message should be broadcasted: # - when a message is posted on a channel (to the channel, using _notify() method) # Anonymous method @api.multi def _broadcast(self, partner_ids): """ Broadcast the current channel header to the given partner ids :param partner_ids : the partner to notify """ notifications = self._channel_channel_notifications(partner_ids) self.env['bus.bus'].sendmany(notifications) @api.multi def _channel_channel_notifications(self, partner_ids): """ Generate the bus notifications of current channel for the given partner ids :param partner_ids : the partner to send the current channel header :returns list of bus notifications (tuple (bus_channe, message_content)) """ notifications = [] for partner in self.env['res.partner'].browse(partner_ids): user_id = partner.user_ids and partner.user_ids[0] or False if user_id: for channel_info in self.sudo(user_id).channel_info(): notifications.append([(self._cr.dbname, 'res.partner', partner.id), channel_info]) return notifications @api.multi def _notify(self, message): """ Broadcast the given message on the current channels. Send the message on the Bus Channel (uuid for public mail.channel, and partner private bus channel (the tuple)). A partner will receive only on message on its bus channel, even if this message belongs to multiple mail channel. Then 'channel_ids' field of the received message indicates on wich mail channel the message should be displayed. :param : mail.message to broadcast """ message.ensure_one() notifications = self._channel_message_notifications(message) self.env['bus.bus'].sendmany(notifications) @api.multi def _channel_message_notifications(self, message): """ Generate the bus notifications for the given message :param message : the mail.message to sent :returns list of bus notifications (tuple (bus_channe, message_content)) """ message_values = message.message_format()[0] notifications = [] for channel in self: notifications.append([(self._cr.dbname, 'mail.channel', channel.id), dict(message_values)]) # add uuid to allow anonymous to listen if channel.public == 'public': notifications.append([channel.uuid, dict(message_values)]) return notifications @api.multi def channel_info(self, extra_info = False): """ Get the informations header for the current channels :returns a list of channels values :rtype : list(dict) """ channel_infos = [] partner_channels = self.env['mail.channel.partner'] # find the channel partner state, if logged user if self.env.user and self.env.user.partner_id: partner_channels = self.env['mail.channel.partner'].search([('partner_id', '=', self.env.user.partner_id.id), ('channel_id', 'in', self.ids)]) # for each channel, build the information header and include the logged partner information for channel in self: info = { 'id': channel.id, 'name': channel.name, 'uuid': channel.uuid, 'state': 'open', 'is_minimized': False, 'channel_type': channel.channel_type, 'public': channel.public, 'mass_mailing': channel.email_send, } if extra_info: info['info'] = extra_info # add the partner for 'direct mesage' channel if channel.channel_type == 'chat': info['direct_partner'] = (channel.sudo() .with_context(active_test=False) .channel_partner_ids .filtered(lambda p: p.id != self.env.user.partner_id.id) .read(['id', 'name', 'im_status'])) # add user session state, if available and if user is logged if partner_channels.ids: partner_channel = partner_channels.filtered(lambda c: channel.id == c.channel_id.id) if len(partner_channel) >= 1: partner_channel = partner_channel[0] info['state'] = partner_channel.fold_state or 'open' info['is_minimized'] = partner_channel.is_minimized info['seen_message_id'] = partner_channel.seen_message_id.id # add needaction and unread counter, since the user is logged info['message_needaction_counter'] = channel.message_needaction_counter info['message_unread_counter'] = channel.message_unread_counter channel_infos.append(info) return channel_infos @api.multi def channel_fetch_message(self, last_id=False, limit=20): """ Return message values of the current channel. :param last_id : last message id to start the research :param limit : maximum number of messages to fetch :returns list of messages values :rtype : list(dict) """ self.ensure_one() domain = [("channel_ids", "in", self.ids)] if last_id: domain.append(("id", "<", last_id)) return self.env['mail.message'].message_fetch(domain=domain, limit=limit) # User methods @api.model def channel_get(self, partners_to, pin=True): """ Get the canonical private channel between some partners, create it if needed. To reuse an old channel (conversation), this one must be private, and contains only the given partners. :param partners_to : list of res.partner ids to add to the conversation :param pin : True if getting the channel should pin it for the current user :returns a channel header, or False if the users_to was False :rtype : dict """ if partners_to: partners_to.append(self.env.user.partner_id.id) # determine type according to the number of partner in the channel self.env.cr.execute(""" SELECT P.channel_id as channel_id FROM mail_channel C, mail_channel_partner P WHERE P.channel_id = C.id AND C.public LIKE 'private' AND P.partner_id IN %s AND channel_type LIKE 'chat' GROUP BY P.channel_id HAVING COUNT(P.partner_id) = %s """, (tuple(partners_to), len(partners_to),)) result = self.env.cr.dictfetchall() if result: # get the existing channel between the given partners channel = self.browse(result[0].get('channel_id')) # pin up the channel for the current partner if pin: self.env['mail.channel.partner'].search([('partner_id', '=', self.env.user.partner_id.id), ('channel_id', '=', channel.id)]).write({'is_pinned': True}) else: # create a new one channel = self.create({ 'channel_partner_ids': [(4, partner_id) for partner_id in partners_to], 'public': 'private', 'channel_type': 'chat', 'email_send': False, 'name': ', '.join(self.env['res.partner'].sudo().browse(partners_to).mapped('name')), }) # broadcast the channel header to the other partner (not me) channel._broadcast(partners_to) return channel.channel_info()[0] return False @api.model def channel_get_and_minimize(self, partners_to): channel = self.channel_get(partners_to) if channel: self.channel_minimize(channel['uuid']) return channel @api.model def channel_fold(self, uuid, state=None): """ Update the fold_state of the given session. In order to syncronize web browser tabs, the change will be broadcast to himself (the current user channel). Note: the user need to be logged :param state : the new status of the session for the current user. """ domain = [('partner_id', '=', self.env.user.partner_id.id), ('channel_id.uuid', '=', uuid)] for session_state in self.env['mail.channel.partner'].search(domain): if not state: state = session_state.fold_state if session_state.fold_state == 'open': state = 'folded' else: state = 'open' session_state.write({ 'fold_state': state, 'is_minimized': bool(state != 'closed'), }) self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), session_state.channel_id.channel_info()[0]) @api.model def channel_minimize(self, uuid, minimized=True): values = { 'fold_state': minimized and 'open' or 'closed', 'is_minimized': minimized } domain = [('partner_id', '=', self.env.user.partner_id.id), ('channel_id.uuid', '=', uuid)] channel_partners = self.env['mail.channel.partner'].search(domain) channel_partners.write(values) self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), channel_partners.channel_id.channel_info()[0]) @api.model def channel_pin(self, uuid, pinned=False): # add the person in the channel, and pin it (or unpin it) channel = self.search([('uuid', '=', uuid)]) channel_partners = self.env['mail.channel.partner'].search([('partner_id', '=', self.env.user.partner_id.id), ('channel_id', '=', channel.id)]) if not pinned: self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), channel.channel_info('unsubscribe')[0]) if channel_partners: channel_partners.write({'is_pinned': pinned}) @api.multi def channel_seen(self): self.ensure_one() if self.channel_message_ids.ids: last_message_id = self.channel_message_ids.ids[0] # zero is the index of the last message self.env['mail.channel.partner'].search([('channel_id', 'in', self.ids), ('partner_id', '=', self.env.user.partner_id.id)]).write({'seen_message_id': last_message_id}) self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), {'info': 'channel_seen', 'id': self.id, 'last_message_id': last_message_id}) return last_message_id @api.multi def channel_invite(self, partner_ids): """ Add the given partner_ids to the current channels and broadcast the channel header to them. :param partner_ids : list of partner id to add """ partners = self.env['res.partner'].browse(partner_ids) # add the partner for channel in self: partners_to_add = partners - channel.channel_partner_ids channel.write({'channel_last_seen_partner_ids': [(0, 0, {'partner_id': partner_id}) for partner_id in partners_to_add.ids]}) for partner in partners_to_add: notification = _('<div class="o_mail_notification">joined <a href="#" class="o_channel_redirect" data-oe-id="%s">#%s</a></div>') % (self.id, self.name,) self.message_post(body=notification, message_type="notification", subtype="mail.mt_comment", author_id=partner.id) # broadcast the channel header to the added partner self._broadcast(partner_ids) #------------------------------------------------------ # Instant Messaging View Specific (Slack Client Action) #------------------------------------------------------ @api.model def get_init_notifications(self): """ Get unread messages and old messages received less than AWAY_TIMER ago of minimized channel ONLY. This aims to set the minimized channel when refreshing the page. Note : the user need to be logged """ # get current user's minimzed channel minimized_channels = self.env['mail.channel.partner'].search([('is_minimized', '=', True), ('partner_id', '=', self.env.user.partner_id.id)]).mapped('channel_id') # get the message since the AWAY_TIMER threshold = datetime.datetime.now() - datetime.timedelta(seconds=AWAY_TIMER) threshold = threshold.strftime(DEFAULT_SERVER_DATETIME_FORMAT) domain = [('channel_ids', 'in', minimized_channels.ids), ('create_date', '>', threshold)] # get the message since the last poll of the user presence = self.env['bus.presence'].search([('user_id', '=', self._uid)], limit=1) if presence: domain.append(('create_date', '>', presence.last_poll)) # do the message search message_values = self.env['mail.message'].message_fetch(domain=domain) # create the notifications (channel infos first, then messages) notifications = [] for channel_info in minimized_channels.channel_info(): notifications.append([(self._cr.dbname, 'res.partner', self.env.user.partner_id.id), channel_info]) for message_value in message_values: for channel_id in message_value['channel_ids']: if channel_id in minimized_channels.ids: message_value['channel_ids'] = [channel_id] notifications.append([(self._cr.dbname, 'mail.channel', channel_id), dict(message_value)]) return notifications @api.model def channel_fetch_slot(self): """ Return the channels of the user grouped by 'slot' (channel, direct_message or private_group), and the mapping between partner_id/channel_id for direct_message channels. :returns dict : the grouped channels and the mapping """ values = {} my_partner_id = self.env.user.partner_id.id pinned_channels = self.env['mail.channel.partner'].search([('partner_id', '=', my_partner_id), ('is_pinned', '=', True)]).mapped('channel_id') # get the group/public channels values['channel_channel'] = self.search([('channel_type', '=', 'channel'), ('public', 'in', ['public', 'groups']), ('channel_partner_ids', 'in', [my_partner_id])]).channel_info() # get the pinned 'direct message' channel direct_message_channels = self.search([('channel_type', '=', 'chat'), ('id', 'in', pinned_channels.ids)]) values['channel_direct_message'] = direct_message_channels.channel_info() # get the private group values['channel_private_group'] = self.search([('channel_type', '=', 'channel'), ('public', '=', 'private'), ('channel_partner_ids', 'in', [my_partner_id])]).channel_info() return values @api.model def channel_search_to_join(self, name=None, domain=None): """ Return the channel info of the channel the current partner can join :param name : the name of the researched channels :param domain : the base domain of the research :returns dict : channel dict """ if not domain: domain = [] domain = expression.AND([ [('channel_type', '=', 'channel')], [('channel_partner_ids', 'not in', [self.env.user.partner_id.id])], [('public', '!=', 'private')], domain ]) if name: domain = expression.AND([domain, [('name', 'ilike', '%'+name+'%')]]) return self.search(domain).read(['name', 'public', 'uuid', 'channel_type']) @api.multi def channel_join_and_get_info(self): self.ensure_one() if self.channel_type == 'channel' and not self.email_send: notification = _('<div class="o_mail_notification">joined <a href="#" class="o_channel_redirect" data-oe-id="%s">#%s</a></div>') % (self.id, self.name,) self.message_post(body=notification, message_type="notification", subtype="mail.mt_comment") self.action_follow() channel_info = self.channel_info()[0] self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), channel_info) return channel_info @api.model def channel_create(self, name, privacy='public'): """ Create a channel and add the current partner, broadcast it (to make the user directly listen to it when polling) :param name : the name of the channel to create :param privacy : privacy of the channel. Should be 'public' or 'private'. :return dict : channel header """ # create the channel new_channel = self.create({ 'name': name, 'public': privacy, 'email_send': False, 'channel_partner_ids': [(4, self.env.user.partner_id.id)] }) channel_info = new_channel.channel_info('creation')[0] notification = _('<div class="o_mail_notification">created <a href="#" class="o_channel_redirect" data-oe-id="%s">#%s</a></div>') % (new_channel.id, new_channel.name,) new_channel.message_post(body=notification, message_type="notification", subtype="mail.mt_comment") self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), channel_info) return channel_info @api.model def get_mention_suggestions(self, search, limit=8): """ Return 'limit'-first channels' id, name and public fields such that the name matches a 'search' string. Exclude channels of type chat (DM), and private channels the current user isn't registered to. """ domain = expression.AND([ [('name', 'ilike', search)], [('channel_type', '=', 'channel')], expression.OR([ [('public', '!=', 'private')], [('channel_partner_ids', 'in', [self.env.user.partner_id.id])] ]) ]) return self.search_read(domain, ['id', 'name', 'public'], limit=limit) @api.model def channel_fetch_listeners(self, uuid): """ Return the id, name and email of partners listening to the given channel """ self._cr.execute(""" SELECT P.id, P.name, P.email FROM mail_channel_partner CP INNER JOIN res_partner P ON CP.partner_id = P.id INNER JOIN mail_channel C ON CP.channel_id = C.id WHERE C.uuid = %s""", (uuid,)) return self._cr.dictfetchall() @api.multi def channel_fetch_preview(self): """ Return the last message of the given channels """ self._cr.execute(""" SELECT mail_channel_id AS id, MAX(mail_message_id) AS message_id FROM mail_message_mail_channel_rel WHERE mail_channel_id IN %s GROUP BY mail_channel_id """, (tuple(self.ids),)) channels_preview = dict((r['message_id'], r) for r in self._cr.dictfetchall()) last_messages = self.env['mail.message'].browse(channels_preview.keys()).message_format() for message in last_messages: channel = channels_preview[message['id']] del(channel['message_id']) channel['last_message'] = message return channels_preview.values()
class MailComposer(models.TransientModel): """ Generic message composition wizard. You may inherit from this wizard at model and view levels to provide specific features. The behavior of the wizard depends on the composition_mode field: - 'comment': post on a record. The wizard is pre-populated via ``get_record_data`` - 'mass_mail': wizard in mass mailing mode where the mail details can contain template placeholders that will be merged with actual data before being sent to each recipient. """ _name = 'mail.compose.message' _inherit = 'mail.message' _description = 'Email composition wizard' _log_access = True _batch_size = 500 @api.model def default_get(self, fields): """ Handle composition mode. Some details about context keys: - comment: default mode, model and ID of a record the user comments - default_model or active_model - default_res_id or active_id - reply: active_id of a message the user replies to - default_parent_id or message_id or active_id: ID of the mail.message we reply to - message.res_model or default_model - message.res_id or default_res_id - mass_mail: model and IDs of records the user mass-mails - active_ids: record IDs - default_model or active_model """ result = super(MailComposer, self).default_get(fields) # v6.1 compatibility mode result['composition_mode'] = result.get( 'composition_mode', self._context.get('mail.compose.message.mode', 'comment')) result['model'] = result.get('model', self._context.get('active_model')) result['res_id'] = result.get('res_id', self._context.get('active_id')) result['parent_id'] = result.get('parent_id', self._context.get('message_id')) if not result[ 'model'] or not result['model'] in self.pool or not hasattr( self.env[result['model']], 'message_post'): result['no_auto_thread'] = True # default values according to composition mode - NOTE: reply is deprecated, fall back on comment if result['composition_mode'] == 'reply': result['composition_mode'] = 'comment' vals = {} if 'active_domain' in self._context: # not context.get() because we want to keep global [] domains vals['use_active_domain'] = True vals['active_domain'] = '%s' % self._context.get('active_domain') if result['composition_mode'] == 'comment': vals.update(self.get_record_data(result)) for field in vals: if field in fields: result[field] = vals[field] # TDE HACK: as mailboxes used default_model='res.users' and default_res_id=uid # (because of lack of an accessible pid), creating a message on its own # profile may crash (res_users does not allow writing on it) # Posting on its own profile works (res_users redirect to res_partner) # but when creating the mail.message to create the mail.compose.message # access rights issues may rise # We therefore directly change the model and res_id if result['model'] == 'res.users' and result['res_id'] == self._uid: result['model'] = 'res.partner' result['res_id'] = self.env.user.partner_id.id if fields is not None: [ result.pop(field, None) for field in result.keys() if field not in fields ] return result @api.model def _get_composition_mode_selection(self): return [('comment', 'Post on a document'), ('mass_mail', 'Email Mass Mailing'), ('mass_post', 'Post on Multiple Documents')] composition_mode = fields.Selection( selection=_get_composition_mode_selection, string='Composition mode', default='comment') partner_ids = fields.Many2many('res.partner', 'mail_compose_message_res_partner_rel', 'wizard_id', 'partner_id', 'Additional Contacts') use_active_domain = fields.Boolean('Use active domain') active_domain = fields.Text('Active domain', readonly=True) attachment_ids = fields.Many2many( 'ir.attachment', 'mail_compose_message_ir_attachments_rel', 'wizard_id', 'attachment_id', 'Attachments') is_log = fields.Boolean( 'Log an Internal Note', help='Whether the message is an internal note (comment mode only)') subject = fields.Char(default=False) # mass mode options notify = fields.Boolean( 'Notify followers', help='Notify followers of the document (mass post only)') template_id = fields.Many2one('mail.template', 'Use template', select=True, domain="[('model', '=', model)]") @api.multi def check_access_rule(self, operation): """ Access rules of mail.compose.message: - create: if - model, no res_id, I create a message in mass mail mode - then: fall back on mail.message acces rules """ # Author condition (CREATE (mass_mail)) if operation == 'create' and self._uid != SUPERUSER_ID: # read mail_compose_message.ids to have their values message_values = {} self._cr.execute( 'SELECT DISTINCT id, model, res_id FROM "%s" WHERE id = ANY (%%s) AND res_id = 0' % self._table, (self.ids, )) for mid, rmod, rid in self._cr.fetchall(): message_values[mid] = {'model': rmod, 'res_id': rid} # remove from the set to check the ids that mail_compose_message accepts author_ids = [ mid for mid, message in message_values.iteritems() if message.get('model') and not message.get('res_id') ] self = self.browse(list(set(self.ids) - set(author_ids))) # not sure slef = ... return super(MailComposer, self).check_access_rule(operation) @api.multi def _notify(self, force_send=False, user_signature=True): """ Override specific notify method of mail.message, because we do not want that feature in the wizard. """ return @api.model def get_record_data(self, values): """ Returns a defaults-like dict with initial values for the composition wizard when sending an email related a previous email (parent_id) or a document (model, res_id). This is based on previously computed default values. """ result, subject = {}, False if values.get('parent_id'): parent = self.env['mail.message'].browse(values.get('parent_id')) result['record_name'] = parent.record_name, subject = tools.ustr(parent.subject or parent.record_name or '') if not values.get('model'): result['model'] = parent.model if not values.get('res_id'): result['res_id'] = parent.res_id partner_ids = values.get('partner_ids', list()) + [ (4, id) for id in parent.partner_ids.ids ] if self._context.get( 'is_private' ) and parent.author_id: # check message is private then add author also in partner list. partner_ids += [(4, parent.author_id.id)] result['partner_ids'] = partner_ids elif values.get('model') and values.get('res_id'): doc_name_get = self.env[values.get('model')].browse( values.get('res_id')).name_get() result['record_name'] = doc_name_get and doc_name_get[0][1] or '' subject = tools.ustr(result['record_name']) re_prefix = _('Re:') if subject and not (subject.startswith('Re:') or subject.startswith(re_prefix)): subject = "%s %s" % (re_prefix, subject) result['subject'] = subject return result #------------------------------------------------------ # Wizard validation and send #------------------------------------------------------ # action buttons call with positionnal arguments only, so we need an intermediary function # to ensure the context is passed correctly @api.multi def send_mail_action(self): # TDE/ ??? return self.with_context( report_template_in_attachment=True).send_mail() @api.multi def send_mail(self, auto_commit=False): """ Process the wizard content and proceed with sending the related email(s), rendering any template patterns on the fly if needed. """ for wizard in self: # Duplicate attachments linked to the email.template. # Indeed, basic mail.compose.message wizard duplicates attachments in mass # mailing mode. But in 'single post' mode, attachments of an email template # also have to be duplicated to avoid changing their ownership. if wizard.attachment_ids and wizard.composition_mode != 'mass_mail' and wizard.template_id: new_attachment_ids = [] for attachment in wizard.attachment_ids: if attachment in wizard.template_id.attachment_ids: new_attachment_ids.append( attachment.copy({ 'res_model': 'mail.compose.message', 'res_id': wizard.id }).id) else: new_attachment_ids.append(attachment.id) wizard.write( {'attachment_ids': [(6, 0, new_attachment_ids)]}) # Mass Mailing mass_mode = wizard.composition_mode in ('mass_mail', 'mass_post') Mail = self.env['mail.mail'] ActiveModel = self.env[wizard.model if wizard. model else 'mail.thread'] if wizard.template_id: # template user_signature is added when generating body_html # mass mailing: use template auto_delete value -> note, for emails mass mailing only Mail = Mail.with_context( mail_notify_user_signature=False, mail_server_id=wizard.template_id.mail_server_id.id) ActiveModel = ActiveModel.with_context( mail_notify_user_signature=False, mail_auto_delete=wizard.template_id.auto_delete) if not hasattr(ActiveModel, 'message_post'): ActiveModel = self.env['mail.thread'].with_context( thread_model=wizard.model) if wizard.composition_mode == 'mass_post': # do not send emails directly but use the queue instead # add context key to avoid subscribing the author ActiveModel = ActiveModel.with_context( mail_notify_force_send=False, mail_create_nosubscribe=True) # wizard works in batch mode: [res_id] or active_ids or active_domain if mass_mode and wizard.use_active_domain and wizard.model: res_ids = self.env[wizard.model].search( eval(wizard.active_domain)).ids elif mass_mode and wizard.model and self._context.get( 'active_ids'): res_ids = self._context['active_ids'] else: res_ids = [wizard.res_id] batch_size = int(self.env['ir.config_parameter'].sudo().get_param( 'mail.batch_size')) or self._batch_size sliced_res_ids = [ res_ids[i:i + batch_size] for i in range(0, len(res_ids), batch_size) ] for res_ids in sliced_res_ids: batch_mails = Mail all_mail_values = wizard.get_mail_values(res_ids) for res_id, mail_values in all_mail_values.iteritems(): if wizard.composition_mode == 'mass_mail': batch_mails |= Mail.create(mail_values) else: subtype = 'mail.mt_comment' if wizard.is_log or (wizard.composition_mode == 'mass_post' and not wizard.notify ): # log a note: subtype is False subtype = False ActiveModel.browse(res_id).message_post( message_type='comment', subtype=subtype, **mail_values) if wizard.composition_mode == 'mass_mail': batch_mails.send(auto_commit=auto_commit) return {'type': 'ir.actions.act_window_close'} @api.multi def get_mail_values(self, res_ids): """Generate the values that will be used by send_mail to create mail_messages or mail_mails. """ self.ensure_one() results = dict.fromkeys(res_ids, False) rendered_values = {} mass_mail_mode = self.composition_mode == 'mass_mail' # render all template-based value at once if mass_mail_mode and self.model: rendered_values = self.render_message(res_ids) # compute alias-based reply-to in batch reply_to_value = dict.fromkeys(res_ids, None) if mass_mail_mode and not self.no_auto_thread: # reply_to_value = self.env['mail.thread'].with_context(thread_model=self.model).browse(res_ids).message_get_reply_to(default=self.email_from) reply_to_value = self.env['mail.thread'].with_context( thread_model=self.model).message_get_reply_to( res_ids, default=self.email_from) for res_id in res_ids: # static wizard (mail.message) values mail_values = { 'subject': self.subject, 'body': self.body or '', 'parent_id': self.parent_id and self.parent_id.id, 'partner_ids': [partner.id for partner in self.partner_ids], 'attachment_ids': [attach.id for attach in self.attachment_ids], 'author_id': self.author_id.id, 'email_from': self.email_from, 'record_name': self.record_name, 'no_auto_thread': self.no_auto_thread, } # mass mailing: rendering override wizard static values if mass_mail_mode and self.model: # always keep a copy, reset record name (avoid browsing records) mail_values.update(notification=True, model=self.model, res_id=res_id, record_name=False) # auto deletion of mail_mail if self.template_id and self.template_id.auto_delete: mail_values['auto_delete'] = True # rendered values using template email_dict = rendered_values[res_id] mail_values['partner_ids'] += email_dict.pop('partner_ids', []) mail_values.update(email_dict) if not self.no_auto_thread: mail_values.pop('reply_to') if reply_to_value.get(res_id): mail_values['reply_to'] = reply_to_value[res_id] if self.no_auto_thread and not mail_values.get('reply_to'): mail_values['reply_to'] = mail_values['email_from'] # mail_mail values: body -> body_html, partner_ids -> recipient_ids mail_values['body_html'] = mail_values.get('body', '') mail_values['recipient_ids'] = [ (4, id) for id in mail_values.pop('partner_ids', []) ] # process attachments: should not be encoded before being processed by message_post / mail_mail create mail_values['attachments'] = [ (name, base64.b64decode(enc_cont)) for name, enc_cont in email_dict.pop( 'attachments', list()) ] attachment_ids = [] for attach_id in mail_values.pop('attachment_ids'): new_attach_id = self.env['ir.attachment'].browse( attach_id).copy({ 'res_model': self._name, 'res_id': self.id }) attachment_ids.append(new_attach_id.id) mail_values['attachment_ids'] = self.env[ 'mail.thread']._message_preprocess_attachments( mail_values.pop('attachments', []), attachment_ids, 'mail.message', 0) results[res_id] = mail_values return results #------------------------------------------------------ # Template methods #------------------------------------------------------ @api.multi @api.onchange('template_id') def onchange_template_id_wrapper(self): self.ensure_one() values = self.onchange_template_id(self.template_id.id, self.composition_mode, self.model, self.res_id)['value'] for fname, value in values.iteritems(): setattr(self, fname, value) @api.multi def onchange_template_id(self, template_id, composition_mode, model, res_id): """ - mass_mailing: we cannot render, so return the template values - normal mode: return rendered values /!\ for x2many field, this onchange return command instead of ids """ if template_id and composition_mode == 'mass_mail': template = self.env['mail.template'].browse(template_id) fields = [ 'subject', 'body_html', 'email_from', 'reply_to', 'mail_server_id' ] values = dict((field, getattr(template, field)) for field in fields if getattr(template, field)) if template.attachment_ids: values['attachment_ids'] = [ att.id for att in template.attachment_ids ] if template.mail_server_id: values['mail_server_id'] = template.mail_server_id.id if template.user_signature and 'body_html' in values: signature = self.env.user.signature values['body_html'] = tools.append_content_to_html( values['body_html'], signature, plaintext=False) if template.report_template: attachment = self.env['ir.attachment'] attach = self.generate_attachment_from_report( template_id, res_id) for attach_fname, attach_datas in attach[res_id].pop( 'attachments', []): data_attach = { 'name': attach_fname, 'datas': attach_datas, 'datas_fname': attach_fname, 'res_model': 'mail.compose.message', 'res_id': 0, 'type': 'binary', } values.setdefault('attachment_ids', list()).append( attachment.create(data_attach).id) elif template_id: values = self.generate_email_for_composer(template_id, [res_id])[res_id] # transform attachments into attachment_ids; not attached to the document because this will # be done further in the posting process, allowing to clean database if email not send Attachment = self.env['ir.attachment'] for attach_fname, attach_datas in values.pop('attachments', []): data_attach = { 'name': attach_fname, 'datas': attach_datas, 'datas_fname': attach_fname, 'res_model': 'mail.compose.message', 'res_id': 0, 'type': 'binary', # override default_type from context, possibly meant for another model! } values.setdefault('attachment_ids', list()).append( Attachment.create(data_attach).id) else: default_values = self.with_context( default_composition_mode=composition_mode, default_model=model, default_res_id=res_id).default_get([ 'composition_mode', 'model', 'res_id', 'parent_id', 'partner_ids', 'subject', 'body', 'email_from', 'reply_to', 'attachment_ids', 'mail_server_id' ]) values = dict((key, default_values[key]) for key in [ 'subject', 'body', 'partner_ids', 'email_from', 'reply_to', 'attachment_ids', 'mail_server_id' ] if key in default_values) if values.get('body_html'): values['body'] = values.pop('body_html') # This onchange should return command instead of ids for x2many field. # ORM handle the assignation of command list on new onchange (api.v8), # this force the complete replacement of x2many field with # command and is compatible with onchange api.v7 values = self._convert_to_write(self._convert_to_cache(values)) return {'value': values} @api.multi def generate_attachment_from_report(self, template_id, res_id): fields = ['attachment_ids'] result = self.env['mail.template'].with_context( tpl_partners_only=True).browse(template_id).generate_email( [res_id], fields=fields) return result @api.multi def save_as_template(self): """ hit save as template button: current form value will be a new template attached to the current document. """ for record in self: models = self.env['ir.model'].search([('model', '=', record.model or 'mail.message')]) model_name = '' if models: model_name = models.name template_name = "%s: %s" % (model_name, tools.ustr(record.subject)) values = { 'name': template_name, 'subject': record.subject or False, 'body_html': record.body or False, 'model_id': models.id or False, 'attachment_ids': [(6, 0, [att.id for att in record.attachment_ids])], } template = self.env['mail.template'].create(values) # generate the saved template record.write({'template_id': template.id}) record.onchange_template_id_wrapper() return _reopen(self, record.id, record.model) #------------------------------------------------------ # Template rendering #------------------------------------------------------ @api.multi def render_message(self, res_ids): """Generate template-based values of wizard, for the document records given by res_ids. This method is meant to be inherited by email_template that will produce a more complete dictionary, using Jinja2 templates. Each template is generated for all res_ids, allowing to parse the template once, and render it multiple times. This is useful for mass mailing where template rendering represent a significant part of the process. Default recipients are also computed, based on mail_thread method message_get_default_recipients. This allows to ensure a mass mailing has always some recipients specified. :param browse wizard: current mail.compose.message browse record :param list res_ids: list of record ids :return dict results: for each res_id, the generated template values for subject, body, email_from and reply_to """ self.ensure_one() multi_mode = True if isinstance(res_ids, (int, long)): multi_mode = False res_ids = [res_ids] subjects = self.render_template(self.subject, self.model, res_ids) bodies = self.render_template(self.body, self.model, res_ids, post_process=True) emails_from = self.render_template(self.email_from, self.model, res_ids) replies_to = self.render_template(self.reply_to, self.model, res_ids) default_recipients = self.env[ 'mail.thread'].message_get_default_recipients(res_model=self.model, res_ids=res_ids) results = dict.fromkeys(res_ids, False) for res_id in res_ids: results[res_id] = { 'subject': subjects[res_id], 'body': bodies[res_id], 'email_from': emails_from[res_id], 'reply_to': replies_to[res_id], } results[res_id].update(default_recipients.get(res_id, dict())) # generate template-based values if self.template_id: template_values = self.generate_email_for_composer( self.template_id.id, res_ids, fields=[ 'email_to', 'partner_to', 'email_cc', 'attachment_ids', 'mail_server_id' ]) else: template_values = {} for res_id in res_ids: if template_values.get(res_id): # recipients are managed by the template results[res_id].pop('partner_ids') results[res_id].pop('email_to') results[res_id].pop('email_cc') # remove attachments from template values as they should not be rendered template_values[res_id].pop('attachment_ids', None) else: template_values[res_id] = dict() # update template values by composer values template_values[res_id].update(results[res_id]) return multi_mode and template_values or template_values[res_ids[0]] @api.model def generate_email_for_composer(self, template_id, res_ids, fields=None): """ Call email_template.generate_email(), get fields relevant for mail.compose.message, transform email_cc and email_to into partner_ids """ multi_mode = True if isinstance(res_ids, (int, long)): multi_mode = False res_ids = [res_ids] if fields is None: fields = [ 'subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'attachment_ids', 'mail_server_id' ] returned_fields = fields + ['partner_ids', 'attachments'] values = dict.fromkeys(res_ids, False) template_values = self.env['mail.template'].with_context( tpl_partners_only=True).browse(template_id).generate_email( res_ids, fields=fields) for res_id in res_ids: res_id_values = dict((field, template_values[res_id][field]) for field in returned_fields if template_values[res_id].get(field)) res_id_values['body'] = res_id_values.pop('body_html', '') values[res_id] = res_id_values return multi_mode and values or values[res_ids[0]] @api.model def render_template(self, template, model, res_ids, post_process=False): return self.env['mail.template'].render_template( template, model, res_ids, post_process=post_process)
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'].search([ ('model', '=', res.pop('model')) ]).id return res name = fields.Char('Name') model_id = fields.Many2one( 'ir.model', 'Applies to', help="The kind of document with with this template can be used") model = fields.Char('Related Document Model', related='model_id.model', select=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, help= "Rich-text/HTML version of the message (placeholders may be used here)" ) 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.xml', '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") ref_ir_value = fields.Many2one( 'ir.values', 'Sidebar Button', readonly=True, copy=False, help="Sidebar button to open the sidebar action") 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." ) @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' ]: models = self.env['ir.model'].search([ ('model', '=', self.model_object_field.relation) ]) if models: self.sub_object = models.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() if template.ref_ir_value: template.ref_ir_value.sudo().unlink() return True @api.multi def create_action(self): ActWindowSudo = self.env['ir.actions.act_window'].sudo() IrValuesSudo = self.env['ir.values'].sudo() view = self.env.ref('mail.email_compose_message_wizard_form') for template in self: src_obj = template.model_id.model 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': src_obj, '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', 'auto_refresh': 1 }) ir_value = IrValuesSudo.create({ 'name': button_name, 'model': src_obj, 'key2': 'client_action_multi', 'value': "ir.actions.act_window,%s" % action.id }) template.write({ 'ref_ir_act_window': action.id, 'ref_ir_value': ir_value.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 = '<div>%s</div>' % html root = lxml.html.fromstring(html) base_url = self.env['ir.config_parameter'].get_param('web.base.url') (base_scheme, base_netloc, bpath, bparams, bquery, bfragment) = urlparse.urlparse(base_url) def _process_link(url): new_url = url (scheme, netloc, path, params, query, fragment) = urlparse.urlparse(url) if not scheme and not netloc: new_url = urlparse.urlunparse( (base_scheme, base_netloc, path, params, query, fragment)) return new_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('data'): node.set('src', _process_link(node.get('src'))) html = lxml.html.tostring(root, pretty_print=False, method='html') # this is ugly, but lxml/etree tostring want to put everything in a 'div' that breaks the editor -> remove that if html.startswith('<div>') and html.endswith('</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``: browse_record 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, long)): 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(filter( None, res_ids)) # 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_tz': lambda dt, tz=False, format=False, context=self._context: format_tz(self.pool, self._cr, self._uid, dt, tz, format, context), 'user': self.env.user, 'ctx': self._context, # context kw would clash with mako internals } for res_id, record in res_to_rec.iteritems(): 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)) render_result = u"" if render_result == u"False": render_result = u"" results[res_id] = render_result if post_process: for res_id, result in results.iteritems(): 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, (int, long)): 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.iteritems(): 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.iteritems(): results[res_id].pop('partner_to', None) results[res_id].update(recipients) for res_id, values in results.iteritems(): 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, (int, long)): 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' ] res_ids_to_templates = self.get_email_template_batch(res_ids) # templates: res_id -> template; template -> res_ids templates_to_res_ids = {} for res_id, template in res_ids_to_templates.iteritems(): templates_to_res_ids.setdefault(template, []).append(res_id) results = dict() for template, template_res_ids in templates_to_res_ids.iteritems(): 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.iteritems(): 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 and not 'report_template_in_attachment' in self.env.context: 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 = self.pool['report'].get_pdf( self._cr, self._uid, [res_id], report_service, context=Template._context), 'pdf' else: result, format = ecore_report.render_report( self._cr, self._uid, [res_id], report_service, {'model': template.model}, Template._context) # 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): """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. :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()) ] 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], '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 ? # compatibility render_template_batch = render_template get_email_template_batch = get_email_template generate_email_batch = generate_email