Exemplo n.º 1
0
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'}
Exemplo n.º 2
0
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})
Exemplo n.º 3
0
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)
Exemplo n.º 4
0
class SaasPortalPlan(models.Model):
    _inherit = 'saas_portal.plan'
    
    category_ids = fields.Many2many(
        'saas.portal.category',
        string='Client Tags'
    )
Exemplo n.º 5
0
Arquivo: event.py Projeto: ecoreos/hz
class EventRegistration(models.Model):
    """ Store answers on attendees. """
    _inherit = 'event.registration'

    answer_ids = fields.Many2many('event.answer',
                                  'event_registration_answer',
                                  string='Answers')
Exemplo n.º 6
0
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.'))
Exemplo n.º 7
0
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."
    )
Exemplo n.º 8
0
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)
Exemplo n.º 9
0
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')
Exemplo n.º 10
0
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
Exemplo n.º 11
0
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 !"),
    ]
Exemplo n.º 12
0
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)
Exemplo n.º 13
0
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
Exemplo n.º 14
0
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))
Exemplo n.º 15
0
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)
Exemplo n.º 16
0
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)
Exemplo n.º 17
0
Arquivo: wizard.py Projeto: ecoreos/hz
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
Exemplo n.º 18
0
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)
Exemplo n.º 20
0
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)]})
Exemplo n.º 21
0
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)
Exemplo n.º 22
0
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})
Exemplo n.º 23
0
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}
Exemplo n.º 24
0
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
Exemplo n.º 25
0
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).'
         )
    ]
Exemplo n.º 26
0
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')."
    )
Exemplo n.º 27
0
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))
Exemplo n.º 28
0
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()
Exemplo n.º 29
0
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)
Exemplo n.º 30
0
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