class SurveyUserInput(models.Model):
    """
    Add some fields to the answer view for a survey. In particular a filed for
    both mobile and phone number as well as a clickable URL to the survey page.
    """
    _inherit = 'survey.user_input'
    partner_id = fields.Many2one('res.partner', string='Partner',
                                 readonly=False)
    phone = phone_fields.Phone(related='partner_id.phone', readonly=True)
    mobile = phone_fields.Phone(related='partner_id.mobile', readonly=True)
    survey_link = fields.Char("Link to complete the survey",
                              compute='_compute_survey_link')

    def _compute_survey_link(self):
        """
        Recreate the private url to access the survey from the token and public
        url available in self.
        :return: Nothing
        """
        self.survey_link = self.survey_id.public_url + '/' + self.token

    def action_view_answers(self):
        """ Print PDF report instead of redirecting to survey. """
        datas = {
            'active_ids': self.ids,
            'active_model': self._name,
        }
        return {
            'type': 'ir.actions.report.xml',
            'report_name': 'survey_phone.survey_user_input',
            'datas': datas,
            'nodestroy': True
        }
Ejemplo n.º 2
0
class tritam_sale_order(models.Model):
    _inherit = 'sale.order'

    phone = fields.Phone(string='Phone')
    mobile = fields.Phone(string='Mobile')

    @api.onchange('partner_id')
    def onchange_phone_mobile(self):
        values = {}
        if self.partner_id:
            values['phone'] = self.partner_id.phone
        if self.partner_id:
            values['mobile'] = self.partner_id.mobile
        self.update(values)

    @api.constrains('mobile')
    def _verify_mobile(self):
        for r in self:
            if r.mobile != r.partner_id.mobile:
                raise ValidationError('Không khớp mobile')

    @api.constrains('phone')
    def _verify_phone(self):
        for r in self:
            if r.phone != r.partner_id.phone:
                raise ValidationError('Không khớp phone')
Ejemplo n.º 3
0
class CrmLead(models.Model):
    _inherit = 'crm.lead'

    phone = fields.Phone(string='Phone',
                         compute='_get_phone',
                         inverse='_set_phone',
                         search='seach_phone')
    phone_hidden = fields.Phone(string='Phone hidden')

    mobile = fields.Phone(string='mobile',
                          compute='_get_mobile',
                          inverse='_set_mobile')
    mobile_hidden = fields.Phone(string='Mobile hidden')

    @api.depends('partner_id.phone', 'phone_hidden')
    def _get_phone(self):
        for record in self:
            if record.partner_id:
                record.phone = record.partner_id.phone
            else:
                record.phone = record.phone_hidden

    def _search_phone(self, phone, value):
        if self.phone:
            return [(phone, value)]

    # @api.depend('partner_id')
    def _set_phone(self):
        for record in self:
            record.phone_hidden = record.phone
            if record.partner_id:
                record.partner_id.write({'phone': record.phone_hidden})

    @api.depends('partner_id.mobile', 'mobile_hidden')
    def _get_mobile(self):
        for record in self:
            if record.partner_id:
                record.mobile = record.partner_id.mobile
            else:
                record.mobile = record.mobile_hidden

    # @api.depend('partner_id')
    def _set_mobile(self):
        for record in self:
            record.mobile_hidden = record.mobile
            if record.partner_id:
                record.partner_id.write({'mobile': record.mobile_hidden})
class CommunicationJob(models.Model):
    """ Communication Jobs are task that will either generate and send
     an e-mail or print a document when executed.

     It is useful to keep a history of the communication sent to partners
     and to send again (or print again) a particular communication.

     It is also useful to batch send communications without manually looking
     for which one to send by e-mail and which one to print.
     """
    _name = 'partner.communication.job'
    _description = 'Communication Job'
    _rec_name = 'subject'
    _order = 'date desc,sent_date desc'
    _inherit = ['partner.communication.defaults', 'ir.needaction_mixin',
                'mail.thread', 'partner.communication.orm.config.abstract']

    ##########################################################################
    #                                 FIELDS                                 #
    ##########################################################################
    config_id = fields.Many2one(
        'partner.communication.config', 'Type', required=True,
        default=lambda s: s.env.ref(
            'partner_communication.default_communication'),
    )
    model = fields.Char(related='config_id.model')
    partner_id = fields.Many2one(
        'res.partner', 'Send to', required=True, ondelete='cascade')
    partner_phone = phone_fields.Phone(related='partner_id.phone')
    partner_mobile = phone_fields.Phone(related='partner_id.mobile')
    country_id = fields.Many2one(related='partner_id.country_id')
    parent_id = fields.Many2one(related='partner_id.parent_id')
    object_ids = fields.Char('Resource ids', required=True)
    date = fields.Datetime(default=fields.Datetime.now)
    sent_date = fields.Datetime(readonly=True, copy=False)
    state = fields.Selection([
        ('call', _('Call partner')),
        ('pending', _('Pending')),
        ('done', _('Done')),
        ('cancel', _('Cancelled')),
    ], default='pending', track_visibility='onchange', copy=False)
    need_call = fields.Selection(
        [('before_sending', 'Before the communication is sent'),
         ('after_sending', 'After the communication is sent')],
        help='Indicates we should have a personal contact with the partner',
    )
    auto_send = fields.Boolean(
        help='Job is processed at creation if set to true', copy=False)
    send_mode = fields.Selection('send_mode_select')
    email_template_id = fields.Many2one(
        related='config_id.email_template_id', store=True)
    email_to = fields.Char(
        help='optional e-mail address to override recipient')
    email_id = fields.Many2one(
        'mail.mail', 'Generated e-mail', readonly=True, index=True, copy=False)
    phonecall_id = fields.Many2one('crm.phonecall', 'Phonecall log',
                                   readonly=True)
    body_html = fields.Html(sanitize=False)
    pdf_page_count = fields.Integer(string='PDF size',
                                    readonly=True)
    subject = fields.Char()
    attachment_ids = fields.One2many(
        'partner.communication.attachment', 'communication_id',
        string="Attachments")
    ir_attachment_ids = fields.Many2many(
        'ir.attachment', string='Attachments',
        compute='_compute_ir_attachments',
        inverse='_inverse_ir_attachments',
        domain=[('report_id', '!=', False)]
    )
    ir_attachment_tmp = fields.Many2many('ir.attachment', string='Attachments',
                                         compute='_compute_void',
                                         inverse='_inverse_ir_attachment_tmp')

    def _compute_ir_attachments(self):
        for job in self:
            job.ir_attachment_ids = job.mapped('attachment_ids.attachment_id')

    def count_pdf_page(self):
        skip_count = self.env.context.get(
            'skip_pdf_count',
            getattr(threading.currentThread(), 'testing', False)
        )
        if not skip_count:
            for record in self.filtered('report_id'):
                if record.send_mode == 'physical':
                    report_obj = record.env['report'].with_context(
                        lang=record.partner_id.lang,
                        must_skip_send_to_printer=True)
                    pdf_str = report_obj.get_pdf(record.ids,
                                                 record.report_id.report_name)
                    pdf = PdfFileReader(StringIO.StringIO(pdf_str))
                    record.pdf_page_count = pdf.getNumPages()

    def _inverse_ir_attachments(self):
        attach_obj = self.env['partner.communication.attachment']
        for job in self:
            for attachment in job.ir_attachment_ids:
                if attachment not in job.attachment_ids.mapped(
                        'attachment_id'):
                    if not attachment.report_id and not \
                            self.env.context.get('no_print'):
                        raise UserError(
                            _("Please select a printing configuration for the "
                              "attachments you add.")
                        )
                    attach_obj.create({
                        'name': attachment.name,
                        'communication_id': job.id,
                        'report_name': attachment.report_id.report_name or '',
                        'attachment_id': attachment.id
                    })
            # Remove deleted attachments
            job.attachment_ids.filtered(
                lambda a: a.attachment_id not in job.ir_attachment_ids
            ).unlink()

    def _compute_void(self):
        pass

    def _inverse_ir_attachment_tmp(self):
        for job in self:
            for attachment in job.ir_attachment_tmp:
                attachment.report_id = self.env.ref(
                    'partner_communication.report_a4_no_margin')
            job.ir_attachment_ids += job.ir_attachment_tmp

    @api.model
    def send_mode_select(self):
        return [
            ('digital', _('By e-mail')),
            ('physical', _('Print report')),
            ('both', _('Both'))
        ]

    ##########################################################################
    #                              ORM METHODS                               #
    ##########################################################################
    @api.model
    def create(self, vals):
        """ If a pending communication for same partner exists,
        add the object_ids to it. Otherwise, create a new communication.
        opt-out partners won't create any communication.
        """
        # Object ids accept lists, integer or string values. It should contain
        # a comma separated list of integers
        object_ids = vals.get('object_ids')
        if isinstance(object_ids, list):
            vals['object_ids'] = ','.join(map(str, object_ids))
        elif object_ids:
            vals['object_ids'] = str(object_ids)
        else:
            vals['object_ids'] = str(vals['partner_id'])

        same_job_search = [
            ('partner_id', '=', vals.get('partner_id')),
            ('config_id', '=', vals.get('config_id')),
            ('config_id', '!=',
             self.env.ref(
                          'partner_communication.default_communication'
             ).id),
            ('state', 'in', ('call', 'pending'))
        ] + self.env.context.get('same_job_search', [])
        job = self.search(same_job_search)
        if job:
            job.object_ids = job.object_ids + ',' + vals['object_ids']
            job.refresh_text()
            return job

        self._get_default_vals(vals)
        job = super(CommunicationJob, self).create(vals)

        # Determine send mode
        send_mode = job.config_id.get_inform_mode(job.partner_id)

        if 'send_mode' not in vals and 'default_send_mode' not in \
                self.env.context:
            job.send_mode = send_mode[0]
        if 'auto_send' not in vals and 'default_auto_send' not in \
                self.env.context:
            job.auto_send = send_mode[1]

        if not job.body_html or not strip_tags(job.body_html):
            job.refresh_text()
        else:
            job.set_attachments()

        # Check if phonecall is needed
        if job.need_call == 'before_sending' or \
                job.config_id.need_call == 'before_sending':
            job.state = 'call'

        if job.body_html or job.send_mode == 'physical':
            job.count_pdf_page()

        # Difference between send_mode of partner and send_mode of job
        if send_mode[0] != job.send_mode:
            if "only" in job.partner_id.global_communication_delivery_preference:
                # Send_mode chosen by the employee is not compatible with the partner
                # So we remove it and an employee must set it manually afterwards
                job.send_mode = ""

        if job.auto_send:
            job.send()

        return job

    @api.multi
    def copy(self, vals=None):
        if vals is None:
            vals = {}
        vals['auto_send'] = False
        return super(CommunicationJob, self).copy(vals)

    @api.model
    def _get_default_vals(self, vals, default_vals=None):
        """
        Used at record creation to find default values given the config of the
        communication.
        :param vals: dict: record values
        :param default_vals: list of fields to copy from config to job.
        :return: config record to use in inheritances.
                 The vals dict is updated.
        """
        if default_vals is None:
            default_vals = []
        default_vals.extend(['report_id', 'need_call', 'omr_enable_marks',
                             'omr_should_close_envelope',
                             'omr_add_attachment_tray_1',
                             'omr_add_attachment_tray_2',
                             'omr_top_mark_x', 'omr_top_mark_y',
                             'omr_single_sided',
                             ])

        config = self.config_id.browse(vals['config_id'])

        # Determine user by default : take in config or employee
        omr_config = config.omr_config_ids
        if not vals.get('user_id'):
            partner = self.env['res.partner'].browse(vals.get('partner_id'))
            if partner:
                lang_of_partner = self.env['res.lang'].search([
                    ('code', 'like', partner.lang)
                ])
                omr_config = config.get_config_for_lang(lang_of_partner)[0:]
                # responsible for the communication is user specified in the omr_config
                # or user specified in the config itself
                # or the current user
                user_id = self.env.uid
                if omr_config.user_id:
                    user_id = omr_config.user_id.id
                elif config.user_id:
                    user_id = config.user_id.id
                vals['user_id'] = user_id

        # Check all default_vals fields
        for default_val in default_vals:
            if default_val not in vals:
                if default_val.startswith('omr_'):
                    value = getattr(omr_config, default_val, False)
                else:
                    value = getattr(config, default_val)
                    if default_val.endswith('_id'):
                        value = value.id
                vals[default_val] = value

        return config

    @api.multi
    def write(self, vals):
        object_ids = vals.get('object_ids')
        if isinstance(object_ids, list):
            vals['object_ids'] = ','.join(map(str, object_ids))
        elif object_ids:
            vals['object_ids'] = str(object_ids)

        super(CommunicationJob, self).write(vals)

        if vals.get('body_html') or vals.get('send_mode') == 'physical':
            self.count_pdf_page()

        return True

    ##########################################################################
    #                             PUBLIC METHODS                             #
    ##########################################################################
    @api.multi
    def send(self):
        """ Executes the job. """
        todo = self.filtered(lambda j: j.state == 'pending')
        to_print = todo.filtered(lambda j: j.send_mode == 'physical')
        for job in todo.filtered(lambda j: j.send_mode in ('both',
                                                           'digital')):
            state = job._send_mail()
            if job.send_mode != 'both':
                job.write({
                    'state': state,
                    'sent_date': state != 'pending' and fields.Datetime.now()
                })
            else:
                # Job was sent by e-mail and must now be printed
                job.send_mode = 'physical'
                job.refresh_text()

        if to_print:
            return to_print._print_report()
        return True

    @api.multi
    def cancel(self):
        to_call = self.filtered(lambda j: j.state == 'call')
        for job in to_call:
            state = 'pending'
            if job.need_call == 'after_sending' and job.sent_date:
                state = 'done'
            to_call.write({'state': state, 'need_call': False})
        (self - to_call).write({'state': 'cancel'})
        return True

    @api.multi
    def reset(self):
        self.write({
            'state': 'pending',
            'date_sent': False,
            'email_id': False,
        })
        return True

    @api.multi
    def refresh_text(self, refresh_uid=False):
        self.mapped('attachment_ids').unlink()
        self.set_attachments()
        for job in self:
            lang = self.env.context.get('lang_preview', job.partner_id.lang)
            if job.email_template_id and job.object_ids:
                fields = self.env['mail.compose.message'].with_context(
                    lang=lang).get_generated_fields(
                    job.email_template_id, [job.id])
                job.write({
                    'body_html': fields['body_html'],
                    'subject': fields['subject'],
                })
                if refresh_uid:
                    job.user_id = self.env.user
        return True

    @api.multi
    def quick_refresh(self):
        # Only refresh text and subject, all at once
        jobs = self.filtered('email_template_id').filtered('object_ids')
        lang = self.env.context.get('lang_preview', jobs.mapped(
            'partner_id.lang'))
        template = jobs.mapped('email_template_id')
        if len(template) > 1:
            raise UserError(_(
                "This is only possible for one template at time"))
        values = self.env['mail.compose.message'].with_context(
            lang=lang).get_generated_fields(template, jobs.ids)
        if not isinstance(values, list):
            values = [values]
        for index in range(0, len(values)):
            jobs[index].write({
                'body_html': values[index]['body_html'],
                'subject': values[index]['subject']
            })
        return True

    @api.onchange('config_id', 'partner_id')
    def onchange_config_id(self):
        if self.config_id and self.partner_id:
            send_mode = self.config_id.get_inform_mode(self.partner_id)
            self.send_mode = send_mode[0]
            # set default fields
            partner_id = None
            if self.partner_id:
                partner_id = self.partner_id.id
            default_vals = {'config_id': self.config_id.id,
                            'partner_id': partner_id}
            self._get_default_vals(default_vals)
            for key, val in default_vals.iteritems():
                if key.endswith('_id'):
                    val = getattr(self, key).browse(val)
                setattr(self, key, val)

    @api.onchange('need_call')
    def onchange_need_call(self):
        if self.need_call == 'before_sending' and self.state == 'pending':
            self.state = 'call'
        if self.need_call == 'after_sending':
            if self.state == 'done':
                self.state = 'call'
            elif self.state == 'call' and not self.sent_date:
                self.state = 'pending'
        if not self.need_call and self.state == 'call':
            self.state = 'pending' if not self.sent_date else 'done'

    @api.multi
    def open_related(self):
        object_ids = map(int, self.object_ids.split(','))
        action = {
            'name': _('Related objects'),
            'type': 'ir.actions.act_window',
            'view_type': 'form',
            'view_mode': 'form,tree',
            'res_model': self.config_id.model,
            'context': self.with_context(group_by=False).env.context,
            'target': 'current',
        }
        if len(object_ids) > 1:
            action.update({
                'view_mode': 'tree,form',
                'domain': [('id', 'in', object_ids)]
            })
        else:
            action['res_id'] = object_ids[0]

        return action

    @api.multi
    def log_call(self):
        return {
            'name': _("Log your call"),
            'type': 'ir.actions.act_window',
            'view_type': 'form',
            'view_mode': 'form',
            'res_model': 'partner.communication.call.wizard',
            'context': self.with_context({
                'click2dial_id': self.partner_id.id,
                'phone_number': self.partner_phone or self.partner_mobile,
                'call_name': self.config_id.name,
                'timestamp': fields.Datetime.now(),
                'default_communication_id': self.id,
            }).env.context,
            'target': 'new',
        }

    @api.multi
    def call(self):
        """ Call partner from tree view button. """
        self.ensure_one()
        self.env['phone.common'].with_context(
            click2dial_model=self._name, click2dial_id=self.id) \
            .click2dial(self.partner_phone or self.partner_mobile)
        return self.log_call()

    @api.multi
    def get_objects(self):
        model = list(set(self.mapped('config_id.model')))
        assert len(model) == 1
        object_ids = list()
        object_id_strings = self.mapped('object_ids')
        for id_strings in object_id_strings:
            object_ids += map(int, id_strings.split(','))
        return self.env[model[0]].browse(set(object_ids))

    @api.multi
    def set_attachments(self):
        """
        Generates attachments for the communication and link them to the
        communication record.
        """
        attachment_obj = self.env['partner.communication.attachment']
        for job in self.with_context(must_skip_send_to_printer=True):
            if job.config_id.attachments_function:
                binaries = getattr(
                    job.with_context(lang=job.partner_id.lang),
                    job.config_id.attachments_function, lambda: dict())()
                for name, data in binaries.iteritems():
                    attachment_obj.create({
                        'name': name,
                        'communication_id': job.id,
                        'report_name': data[0],
                        'data': data[1],
                    })

    @api.multi
    def preview_pdf(self):
        preview_model = 'partner.communication.pdf.wizard'
        preview = self.env[preview_model].create({
            'communication_id': self.id
        })
        return {
            'name': _("Preview"),
            'type': 'ir.actions.act_window',
            'view_type': 'form',
            'view_mode': 'form',
            'res_model': preview_model,
            'res_id': preview.id,
            'context': self.env.context,
            'target': 'new',
        }

    @api.multi
    def add_omr_marks(self, pdf_data, is_latest_document):
        # Documentation
        # http://meteorite.unm.edu/site_media/pdf/reportlab-userguide.pdf
        # https://pythonhosted.org/PyPDF2/PdfFileReader.html
        # https://stackoverflow.com/a/17538003
        # https://gist.github.com/kzim44/5023021
        # https://www.blog.pythonlibrary.org/2013/07/16/
        #   pypdf-how-to-write-a-pdf-to-memory/
        self.ensure_one()

        pdf_buffer = StringIO.StringIO()
        pdf_buffer.write(pdf_data)

        existing_pdf = PdfFileReader(pdf_buffer)
        output = PdfFileWriter()
        total_pages = existing_pdf.getNumPages()

        def lastpair(a):
            b = a - 1
            if self.omr_single_sided or b % 2 == 0:
                return b
            return lastpair(b)

        # print latest omr mark on latest pair page (recto)
        latest_omr_page = lastpair(total_pages)

        for page_number in range(total_pages):
            page = existing_pdf.getPage(page_number)
            # only print omr marks on pair pages (recto)
            if self.omr_single_sided or page_number % 2 is 0:
                is_latest_page = is_latest_document and \
                    page_number == latest_omr_page
                marks = self._compute_marks(is_latest_page)
                omr_layer = self._build_omr_layer(marks)
                page.mergePage(omr_layer)
            output.addPage(page)

        out_buffer = StringIO.StringIO()
        output.write(out_buffer)

        return out_buffer.getvalue()

    def _compute_marks(self, is_latest_page):
        marks = [
            True,  # Start mark (compulsory)
            is_latest_page,
            is_latest_page and self.omr_add_attachment_tray_1,
            is_latest_page and self.omr_add_attachment_tray_2,
            is_latest_page and not self.omr_should_close_envelope
        ]
        parity_check = sum(marks) % 2 == 0
        marks.append(parity_check)
        marks.append(True)  # End mark (compulsory)
        return marks

    def _build_omr_layer(self, marks):
        self.ensure_one()
        padding_x = 4.2 * mm
        padding_y = 8.5 * mm
        top_mark_x = self.omr_top_mark_x * mm
        top_mark_y = self.omr_top_mark_y * mm
        mark_y_spacing = 4 * mm

        mark_width = 6.5 * mm
        marks_height = (len(marks) - 1) * mark_y_spacing

        logger.info('Mailer DS-75i OMR Settings: 1={} 2={}'.format(
            (297 * mm - top_mark_y) / mm,
            (top_mark_x + mark_width / 2) / mm + 0.5
        ))

        omr_buffer = StringIO.StringIO()
        omr_canvas = Canvas(omr_buffer)
        omr_canvas.setLineWidth(0.2 * mm)

        # add a white background for the omr code
        omr_canvas.setFillColor(white)
        omr_canvas.rect(
            x=top_mark_x - padding_x,
            y=top_mark_y - marks_height - padding_y,
            width=mark_width + 2 * padding_x,
            height=marks_height + 2 * padding_y,
            fill=True,
            stroke=False
        )

        for offset, mark in enumerate(marks):
            mark_y = top_mark_y - offset * mark_y_spacing
            if mark:
                omr_canvas.line(top_mark_x, mark_y,
                                top_mark_x + mark_width, mark_y)

        # Close the PDF object cleanly.
        omr_canvas.showPage()
        omr_canvas.save()

        # move to the beginning of the StringIO buffer
        omr_buffer.seek(0)
        omr_pdf = PdfFileReader(omr_buffer)

        return omr_pdf.getPage(0)

    ##########################################################################
    #                             PRIVATE METHODS                            #
    ##########################################################################
    def _send_mail(self):
        """
        Called for sending the communication by e-mail.
        :return: state of the communication depending if the e-mail was
                 successfully sent or not.
        """
        self.ensure_one()
        partner = self.partner_id
        # Send by e-mail
        email = self.email_id
        if not email:
            email_vals = {
                'recipient_ids': [(4, partner.id)],
                'communication_config_id': self.config_id.id,
                'body_html': self.body_html,
                'subject': self.subject,
                'attachment_ids': [(6, 0, self.ir_attachment_ids.ids)],
                'auto_delete': False,
                'reply_to': (self.email_template_id.reply_to or
                             self.user_id.email)
            }
            if self.email_to:
                # Replace partner e-mail by specified address
                email_vals['email_to'] = self.email_to
                del email_vals['recipient_ids']
            if 'default_email_vals' in self.env.context:
                email_vals.update(
                    self.env.context['default_email_vals'])

            email = self.env['mail.compose.message'].with_context(
                lang=partner.lang).create_emails(
                self.email_template_id, [self.id], email_vals)
            self.email_id = email
            email.send()
            # Subscribe author to thread, so that the reply
            # notifies the author.
            self.message_subscribe(self.user_id.partner_id.ids)

        final_state = 'pending'
        if email.state == 'sent':
            if self.need_call == 'after_sending':
                final_state = 'call'
            else:
                final_state = 'done'
        return final_state

    def _print_report(self):
        report_obj = self.env['report']

        for job in self:
            # Get pdf should directly send it to the printer if report
            # is correctly configured.
            to_print = report_obj.with_context(
                print_name=self.env.user.firstname[:3] + ' ' + (
                    job.subject or ''),
                must_skip_send_to_printer=True,
                lang=job.partner_id.lang
            ).get_pdf(job.ids, job.report_id.report_name)

            # Print letter
            report = job.report_id
            behaviour = report.behaviour()[report.id]
            printer = behaviour['printer'] \
                .with_context(lang=job.partner_id.lang)
            if behaviour['action'] != 'client' and printer:
                printer.print_document(
                    report.report_name, to_print, report.report_type)

            # Print attachments
            job.attachment_ids.print_attachments()
            job.write({
                'state': 'call' if job.need_call == 'after_sending'
                else 'done',
                'sent_date': fields.Datetime.now()
            })
            if not testing:
                # Commit to avoid invalid state if process fails
                self.env.cr.commit()  # pylint: disable=invalid-commit
        return True

    @api.model
    def _needaction_domain_get(self):
        """
        Used to display a count icon in the menu
        :return: domain of jobs counted
        """
        return [('state', 'in', ('call', 'pending'))]
Ejemplo n.º 5
0
class StockPicking(models.Model):
    _inherit = 'stock.picking'

    phone = fields.Phone(string='Phone', related='partner_id.phone')
    mobile = fields.Phone(string='mobile', related='partner_id.mobile')
class CommunicationJob(models.Model):
    """ Communication Jobs are task that will either generate and send
     an e-mail or print a document when executed.

     It is useful to keep a history of the communication sent to partners
     and to send again (or print again) a particular communication.

     It is also useful to batch send communications without manually looking
     for which one to send by e-mail and which one to print.
     """
    _name = 'partner.communication.job'
    _description = 'Communication Job'
    _order = 'date desc,sent_date desc'
    _inherit = ['partner.communication.defaults', 'ir.needaction_mixin',
                'mail.thread']

    ##########################################################################
    #                                 FIELDS                                 #
    ##########################################################################
    config_id = fields.Many2one(
        'partner.communication.config', 'Type', required=True,
        default=lambda s: s.env.ref(
            'partner_communication.default_communication'),
    )
    model = fields.Char(related='config_id.model')
    partner_id = fields.Many2one(
        'res.partner', 'Send to', required=True, ondelete='cascade')
    partner_phone = phone_fields.Phone(related='partner_id.phone')
    partner_mobile = phone_fields.Phone(related='partner_id.mobile')
    country_id = fields.Many2one(related='partner_id.country_id')
    parent_id = fields.Many2one(related='partner_id.parent_id')
    object_ids = fields.Char('Resource ids', required=True)
    date = fields.Datetime(default=fields.Datetime.now)
    sent_date = fields.Datetime(readonly=True)
    state = fields.Selection([
        ('call', _('Call partner')),
        ('pending', _('Pending')),
        ('done', _('Done')),
        ('cancel', _('Cancelled')),
    ], default='pending', readonly=True, track_visibility='onchange')
    need_call = fields.Boolean(
        readonly=True,
        states={'pending': [('readonly', False)]}
    )
    auto_send = fields.Boolean(
        help='Job is processed at creation if set to true')
    send_mode = fields.Selection('send_mode_select')
    email_template_id = fields.Many2one(
        related='config_id.email_template_id', store=True)
    email_to = fields.Char(
        help='optional e-mail address to override recipient')
    email_id = fields.Many2one('mail.mail', 'Generated e-mail', readonly=True)
    phonecall_id = fields.Many2one('crm.phonecall', 'Phonecall log',
                                   readonly=True)
    body_html = fields.Html(sanitize=False)
    pdf_page_count = fields.Integer(string='PDF size',
                                    readonly=True)
    subject = fields.Char()
    attachment_ids = fields.One2many(
        'partner.communication.attachment', 'communication_id',
        string="Attachments")
    ir_attachment_ids = fields.Many2many(
        'ir.attachment', string='Attachments',
        compute='_compute_ir_attachments',
        inverse='_inverse_ir_attachments',
        domain=[('report_id', '!=', False)]
    )

    def _compute_ir_attachments(self):
        for job in self:
            job.ir_attachment_ids = job.mapped('attachment_ids.attachment_id')

    def count_pdf_page(self):
        test_mode = getattr(threading.currentThread(), 'testing', False)
        if not test_mode:
            for record in self.filtered('report_id'):
                if record.send_mode == 'physical':
                    report_obj = record.env['report'].with_context(
                        lang=record.partner_id.lang,
                        must_skip_send_to_printer=True)
                    pdf_str = report_obj.get_pdf(record.ids,
                                                 record.report_id.report_name)
                    pdf = PdfFileReader(StringIO.StringIO(pdf_str))
                    record.pdf_page_count = pdf.getNumPages()

    def _inverse_ir_attachments(self):
        attach_obj = self.env['partner.communication.attachment']
        for job in self:
            for attachment in job.ir_attachment_ids:
                if attachment not in job.attachment_ids.mapped(
                        'attachment_id'):
                    if not attachment.report_id and not \
                            self.env.context.get('no_print'):
                        raise UserError(
                            _("Please select a printing configuration for the "
                              "attachments you add.")
                        )
                    attach_obj.create({
                        'name': attachment.name,
                        'communication_id': job.id,
                        'report_name': attachment.report_id.report_name or '',
                        'attachment_id': attachment.id
                    })
            # Remove deleted attachments
            job.attachment_ids.filtered(
                lambda a: a.attachment_id not in job.ir_attachment_ids
            ).unlink()

    @api.model
    def send_mode_select(self):
        return [
            ('digital', _('By e-mail')),
            ('physical', _('Print report')),
            ('both', _('Both'))
        ]

    ##########################################################################
    #                              ORM METHODS                               #
    ##########################################################################
    @api.model
    def create(self, vals):
        """ If a pending communication for same partner exists,
        add the object_ids to it. Otherwise, create a new communication.
        opt-out partners won't create any communication.
        """
        # Object ids accept lists, integer or string values. It should contain
        # a comma separated list of integers
        object_ids = vals.get('object_ids')
        if isinstance(object_ids, list):
            vals['object_ids'] = ','.join(map(str, object_ids))
        elif object_ids:
            vals['object_ids'] = str(object_ids)
        else:
            vals['object_ids'] = str(vals['partner_id'])

        same_job_search = [
            ('partner_id', '=', vals.get('partner_id')),
            ('config_id', '=', vals.get('config_id')),
            ('config_id', '!=',
             self.env.ref('partner_communication.default_communication').id),
            ('state', 'in', ('call', 'pending'))
        ] + self.env.context.get('same_job_search', [])
        job = self.search(same_job_search)
        if job:
            job.object_ids = job.object_ids + ',' + vals['object_ids']
            job.refresh_text()
            return job

        self._get_default_vals(vals)
        job = super(CommunicationJob, self).create(vals)

        # Determine send mode
        send_mode = job.config_id.get_inform_mode(job.partner_id)
        if 'send_mode' not in vals and 'default_send_mode' not in \
                self.env.context:
            job.send_mode = send_mode[0]
        if 'auto_send' not in vals and 'default_auto_send' not in \
                self.env.context:
            job.auto_send = send_mode[1]

        if not job.body_html or not strip_tags(job.body_html):
            job.refresh_text()
        else:
            job.set_attachments()

        # Check if phonecall is needed
        if job.need_call or job.config_id.need_call:
            job.state = 'call'

        if job.body_html or job.send_mode == 'physical':
            job.count_pdf_page()

        if job.auto_send:
            job.send()

        return job

    @api.model
    def _get_default_vals(self, vals, default_vals=None):
        """
        Used at record creation to find default values given the config of the
        communication.
        :param vals: dict: record values
        :param default_vals: list of fields to copy from config to job.
        :return: config record to use in inheritances.
                 The vals dict is updated.
        """
        if default_vals is None:
            default_vals = []
        default_vals.extend(['report_id', 'need_call', 'omr_enable_marks',
                             'omr_should_close_envelope',
                             'omr_add_attachment_tray_1',
                             'omr_add_attachment_tray_2'])

        config = self.config_id.browse(vals['config_id'])

        # Determine user by default : take in config or employee
        if not vals.get('user_id'):
            vals['user_id'] = config.user_id.id or self.env.uid

        # Check all default_vals fields
        for default_val in default_vals:
            if default_val not in vals:
                value = getattr(config, default_val)
                if default_val.endswith('_id'):
                    value = value.id
                vals[default_val] = value

        return config

    @api.multi
    def write(self, vals):
        object_ids = vals.get('object_ids')
        if isinstance(object_ids, list):
            vals['object_ids'] = ','.join(map(str, object_ids))
        elif object_ids:
            vals['object_ids'] = str(object_ids)
        if vals.get('need_call'):
            vals['state'] = 'call'

        super(CommunicationJob, self).write(vals)

        if vals.get('body_html') or vals.get('send_mode') == 'physical':
            self.count_pdf_page()

        return True

    ##########################################################################
    #                             PUBLIC METHODS                             #
    ##########################################################################
    @api.multi
    def send(self):
        """ Executes the job. """
        no_call = self.filtered(lambda j: not j.need_call)
        to_print = no_call.filtered(lambda j: j.send_mode == 'physical')
        for job in no_call.filtered(lambda j: j.send_mode in ('both',
                                                              'digital')):
            state = job._send_mail()
            if job.send_mode != 'both':
                job.write({
                    'state': state,
                    'sent_date': state != 'pending' and fields.Datetime.now()
                })
            else:
                # Job was sent by e-mail and must now be printed
                job.send_mode = 'physical'
                job.refresh_text()

        if to_print:
            return to_print._print_report()
        return True

    @api.multi
    def cancel(self):
        to_call = self.filtered(lambda j: j.state == 'call')
        to_call.write({'state': 'pending', 'need_call': False})
        (self - to_call).write({'state': 'cancel'})
        return True

    @api.multi
    def reset(self):
        self.write({
            'state': 'pending',
            'date_sent': False,
            'email_id': False,
        })
        return True

    @api.multi
    def refresh_text(self, refresh_uid=False):
        self.mapped('attachment_ids').unlink()
        self.set_attachments()
        for job in self:
            if job.email_template_id and job.object_ids:
                fields = self.env['mail.compose.message'].with_context(
                    lang=job.partner_id.lang).get_generated_fields(
                    job.email_template_id, [job.id])
                job.write({
                    'body_html': fields['body_html'],
                    'subject': fields['subject'],
                })
                if refresh_uid:
                    job.user_id = self.env.user
                if job.state == 'call' and not job.need_call:
                    job.state = 'pending'

        return True

    @api.multi
    def quick_refresh(self):
        # Only refresh text and subject, all at once
        jobs = self.filtered('email_template_id').filtered('object_ids')
        langs = set(jobs.mapped('partner_id.lang'))
        template = jobs.mapped('email_template_id')
        if len(langs) > 1:
            raise UserError(_("This is only possible for one lang at time"))
        if len(template) > 1:
            raise UserError(_(
                "This is only possible for one template at time"))
        values = self.env['mail.compose.message'].with_context(
            lang=langs.pop()).get_generated_fields(template, jobs.ids)
        if not isinstance(values, list):
            values = [values]
        for index in range(0, len(values)):
            jobs[index].write({
                'body_html': values[index]['body_html'],
                'subject': values[index]['subject']
            })
        return True

    @api.onchange('config_id', 'partner_id')
    def onchange_config_id(self):
        if self.config_id and self.partner_id:
            send_mode = self.config_id.get_inform_mode(self.partner_id)
            self.send_mode = send_mode[0]
            # set default fields
            default_vals = {'config_id': self.config_id.id}
            self._get_default_vals(default_vals)
            for key, val in default_vals.iteritems():
                if key.endswith('_id'):
                    val = getattr(self, key).browse(val)
                setattr(self, key, val)

    @api.multi
    def open_related(self):
        object_ids = map(int, self.object_ids.split(','))
        action = {
            'name': _('Related objects'),
            'type': 'ir.actions.act_window',
            'view_type': 'form',
            'view_mode': 'form,tree',
            'res_model': self.config_id.model,
            'context': self.with_context(group_by=False).env.context,
            'target': 'current',
        }
        if len(object_ids) > 1:
            action.update({
                'view_mode': 'tree,form',
                'domain': [('id', 'in', object_ids)]
            })
        else:
            action['res_id'] = object_ids[0]

        return action

    @api.multi
    def log_call(self):
        return {
            'name': _("Log your call"),
            'type': 'ir.actions.act_window',
            'view_type': 'form',
            'view_mode': 'form',
            'res_model': 'partner.communication.call.wizard',
            'context': self.with_context({
                'click2dial_id': self.partner_id.id,
                'phone_number': self.partner_phone or self.partner_mobile,
                'call_name': self.config_id.name,
                'timestamp': fields.Datetime.now(),
                'communication_id': self.id,
            }).env.context,
            'target': 'new',
        }

    @api.multi
    def call(self):
        """ Call partner from tree view button. """
        self.ensure_one()
        self.env['phone.common'].with_context(
            click2dial_model=self._name, click2dial_id=self.id)\
            .click2dial(self.partner_phone or self.partner_mobile)
        return self.log_call()

    @api.multi
    def get_objects(self):
        config = self.mapped('config_id')
        config.ensure_one()
        object_ids = list()
        object_id_strings = self.mapped('object_ids')
        for id_strings in object_id_strings:
            object_ids += map(int, id_strings.split(','))
        return self.env[config.model].browse(set(object_ids))

    @api.multi
    def set_attachments(self):
        """
        Generates attachments for the communication and link them to the
        communication record.
        """
        attachment_obj = self.env['partner.communication.attachment']
        for job in self.with_context(must_skip_send_to_printer=True):
            if job.config_id.attachments_function:
                binaries = getattr(
                    job.with_context(lang=job.partner_id.lang),
                    job.config_id.attachments_function, lambda: dict())()
                for name, data in binaries.iteritems():
                    attachment_obj.create({
                        'name': name,
                        'communication_id': job.id,
                        'report_name': data[0],
                        'data': data[1],
                    })

    @api.multi
    def preview_pdf(self):
        preview_model = 'partner.communication.pdf.wizard'
        preview = self.env[preview_model].create({
            'communication_id': self.id
        })
        return {
            'name': _("Preview"),
            'type': 'ir.actions.act_window',
            'view_type': 'form',
            'view_mode': 'form',
            'res_model': preview_model,
            'res_id': preview.id,
            'context': self.env.context,
            'target': 'new',
        }

    @api.multi
    def message_post(self, **kwargs):
        """
        If message is not from a user, it is probably the answer of the
        partner by e-mail. We post it on the partner thread instead of
        the communication thread
        :param kwargs: arguments
        :return: mail_message record
        """
        message = super(CommunicationJob, self).message_post(**kwargs)
        if not message.author_id.user_ids:
            message.write({
                'model': 'res.partner',
                'res_id': self.partner_id.id
            })
        return message.id

    @api.multi
    def add_omr_marks(self, pdf_data, is_latest_document):
        # Documentation
        # http://meteorite.unm.edu/site_media/pdf/reportlab-userguide.pdf
        # https://pythonhosted.org/PyPDF2/PdfFileReader.html
        # https://stackoverflow.com/a/17538003
        # https://gist.github.com/kzim44/5023021
        # https://www.blog.pythonlibrary.org/2013/07/16/
        #   pypdf-how-to-write-a-pdf-to-memory/

        self.ensure_one()

        # OMR Parameters
        number_of_alimentation = 2
        number_of_marks = 7

        orm_mark_length = 7 * mm

        # margin around the omr code which should stay white
        horizontal_margin = 4.2 * mm
        vertical_margin = 8.5 * mm

        x1 = 194 * mm
        x2 = x1 + orm_mark_length
        y1 = 180 * mm
        y_step = 4 * mm

        pdf_buffer = StringIO.StringIO()
        pdf_buffer.write(pdf_data)

        existing_pdf = PdfFileReader(pdf_buffer)
        total_pages = existing_pdf.getNumPages()

        # print latest omr mark on latest pair page (recto)
        latest_omr_page = (total_pages - 1) if total_pages % 2 is 0 \
            else total_pages

        output = PdfFileWriter()

        for page_number in range(total_pages):
            y_position = y1
            # only print omr marks on pair pages (recto)
            if page_number % 2 is 0:
                is_latest_page = True if \
                    is_latest_document and \
                    page_number == (latest_omr_page - 1) else False

                omr_buffer = StringIO.StringIO()

                # Create a canvas to write on
                p = Canvas(omr_buffer)

                # line (x1, y1, x2, y2)
                p.setLineWidth(0.2 * mm)

                # add a white background for the omr code
                p.setFillColor(white)
                p.rect(
                    x1 - horizontal_margin,
                    y1 - (number_of_marks - 1) * y_step - vertical_margin,
                    orm_mark_length + 2 * horizontal_margin,
                    (number_of_marks - 1) * y_step + 2 * vertical_margin,
                    fill=True,
                    stroke=False
                )

                # start mark (compulsory)
                p.line(x1, y_position, x2, y_position)
                y_position -= y_step

                # insert mark (only on latest page)
                if is_latest_page:
                    p.line(x1, y_position, x2, y_position)
                y_position -= y_step

                # alimentation (2 marks)
                # back 1 is the "big special" one (the lower)
                # back 2 is at the middle
                for alimentation_number in range(number_of_alimentation):
                    if is_latest_page:
                        if self.omr_add_attachment_tray_1 and \
                                alimentation_number == 0:
                            p.line(x1, y_position, x2, y_position)
                        elif self.omr_add_attachment_tray_2 and \
                                alimentation_number == 1:
                            p.line(x1, y_position, x2, y_position)
                    y_position -= y_step

                # close envelop (if display the envelop is not closed)
                if is_latest_page \
                        and not self.omr_should_close_envelope:
                    p.line(x1, y_position, x2, y_position)
                y_position -= y_step

                # # number of pages in binary (MSB first with sequence:
                # # 00, 01, 10, 11, 00, 01, ...)
                # p.line(x1, y_position, x2, y_position)
                # y_position -= y_step
                #
                # p.line(x1, y_position, x2, y_position)
                # y_position -= y_step

                # parity mark (total number of marks should be pair)
                if self._display_parity(is_latest_page):
                    p.line(x1, y_position, x2, y_position)
                y_position -= y_step

                # end mark (compulsory)
                p.line(x1, y_position, x2, y_position)

                # Close the PDF object cleanly.
                p.showPage()
                p.save()

                # move to the beginning of the StringIO buffer
                omr_buffer.seek(0)
                omr_pdf = PdfFileReader(omr_buffer)

                # add the omr marks to the page
                page = existing_pdf.getPage(page_number)
                page.mergePage(omr_pdf.getPage(0))
            else:
                page = existing_pdf.getPage(page_number)

            output.addPage(page)

        out_buffer = StringIO.StringIO()
        output.write(out_buffer)

        return out_buffer.getvalue()

    ##########################################################################
    #                             PRIVATE METHODS                            #
    ##########################################################################
    def _send_mail(self):
        """
        Called for sending the communication by e-mail.
        :return: state of the communication depending if the e-mail was
                 successfully sent or not.
        """
        self.ensure_one()
        partner = self.partner_id
        # Send by e-mail
        email = self.email_id
        if not email:
            email_vals = {
                'recipient_ids': [(4, partner.id)],
                'communication_config_id': self.config_id.id,
                'body_html': self.body_html,
                'subject': self.subject,
                'attachment_ids': [(6, 0, self.ir_attachment_ids.ids)],
                'auto_delete': False,
                'reply_to': self.email_template_id.reply_to or
                self.user_id.email
            }
            if self.email_to:
                # Replace partner e-mail by specified address
                email_vals['email_to'] = self.email_to
                del email_vals['recipient_ids']
            if 'default_email_vals' in self.env.context:
                email_vals.update(
                    self.env.context['default_email_vals'])

            email = self.env['mail.compose.message'].with_context(
                lang=partner.lang).create_emails(
                self.email_template_id, [self.id], email_vals)
            self.email_id = email
            email.send()
            # Subscribe author to thread, so that the reply
            # notifies the author.
            self.message_subscribe(self.user_id.partner_id.ids)

        return 'done' if email.state == 'sent' else 'pending'

    def _print_report(self):
        report_obj = self.env['report']

        for job in self:
            # Get pdf should directly send it to the printer if report
            # is correctly configured.
            pdf_data = report_obj.with_context(
                print_name=self.env.user.firstname[:3] + ' ' + (
                    job.subject or ''),
                must_skip_send_to_printer=True
            ).get_pdf(job.ids, job.report_id.report_name)

            # add omr to pdf if needed
            if job.omr_enable_marks:
                is_latest_document = not job.attachment_ids.filtered(
                    'attachment_id.enable_omr'
                )
                to_print = job.add_omr_marks(
                    pdf_data, is_latest_document
                )
            else:
                to_print = pdf_data

            # Print letter
            report = job.report_id
            behaviour = report.behaviour()[report.id]
            printer = behaviour['printer']
            if printer:
                printer.print_document(
                    report, to_print, report.report_type)

            # Print attachments
            job.attachment_ids.print_attachments()
            # Save info
            job.partner_id.message_post(
                job.body_html, job.subject)
            job.write({
                'state': 'done',
                'sent_date': fields.Datetime.now()
            })
            # Commit to avoid invalid state if process fails
            self.env.cr.commit()    # pylint: disable=invalid-commit
        return True

    def _display_parity(self, is_latest_page):
        # current_page_number = 0

        nb_displayed_marks = 2  # always display start and stop marks

        # insert mark is displayed only on latest page
        if is_latest_page:
            nb_displayed_marks += 1

            # a mark is added if the envelope should not be closed
            if not self.omr_should_close_envelope:
                nb_displayed_marks += 1

            # count attachment marks
            if self.omr_add_attachment_tray_1:
                nb_displayed_marks += 1
            if self.omr_add_attachment_tray_2:
                nb_displayed_marks += 1

        # # page number (2) marks
        # # no mark for page 00 (binary)
        # # one mark for page 01 and 10 (binary)
        # if current_page_number % 4 in {1, 2}:
        #     nb_displayed_marks += 1
        # # two marks for page 11 (binary)
        # elif current_page_number % 4 is 3:
        #     nb_displayed_marks += 2

        # if the nb_displayed_marks is pair, do not display the parity
        if nb_displayed_marks % 2 is 0:
            return False
        else:
            return True

    @api.model
    def _needaction_domain_get(self):
        """
        Used to display a count icon in the menu
        :return: domain of jobs counted
        """
        return [('state', 'in', ('call', 'pending'))]