Example #1
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)
    active = fields.Boolean(default=True)
    description = fields.Html('Description',
                              translate=html_translate,
                              sanitize_attributes=False)
    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=html_translate,
        sanitize_attributes=False)
    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',
                             search='_search_can_see')
    can_see_full = fields.Boolean('Full Access', compute='_compute_access')
    can_upload = fields.Boolean('Can Upload', compute='_compute_access')

    def _search_can_see(self, operator, value):
        if operator not in ('=', '!=', '<>'):
            raise ValueError('Invalid operator: %s' % (operator, ))

        if not value:
            operator = operator == "=" and '!=' or '='

        if self._uid == SUPERUSER_ID:
            return [(1, '=', 1)]

        # Better perfs to split request and use inner join that left join
        req = """
            SELECT id FROM slide_channel WHERE visibility='public'
                UNION
            SELECT c.id
                FROM slide_channel c
                    INNER JOIN rel_channel_groups rg on c.id = rg.channel_id
                    INNER JOIN res_groups g on g.id = rg.group_id
                    INNER JOIN res_groups_users_rel u on g.id = u.gid and uid = %s
        """
        op = operator == "=" and "inselect" or "not inselect"
        # don't use param named because orm will add other param (test_active, ...)
        return [('id', op, (req, (self._uid)))]

    @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 _compute_website_url(self):
        super(Channel, self)._compute_website_url()
        base_url = self.env['ir.config_parameter'].get_param('web.base.url')
        for channel in self:
            if channel.id:  # avoid to perform a slug on a not yet saved record in case of an onchange.
                channel.website_url = '%s/slides/%s' % (base_url,
                                                        slug(channel))

    @api.onchange('visibility')
    def change_visibility(self):
        if self.visibility == 'public':
            self.group_ids = False

    @api.multi
    def write(self, vals):
        res = super(Channel, self).write(vals)
        if 'active' in vals:
            # archiving/unarchiving a channel does it on its slides, too
            self.with_context(active_test=False).mapped('slide_ids').write(
                {'active': vals['active']})
        return res

    @api.multi
    @api.returns('self', lambda value: value.id)
    def message_post(self, parent_id=False, subtype=None, **kwargs):
        """ Temporary workaround to avoid spam. If someone replies on a channel
        through the 'Presentation Published' email, it should be considered as a
        note as we don't want all channel followers to be notified of this answer. """
        self.ensure_one()
        if parent_id:
            parent_message = self.env['mail.message'].sudo().browse(parent_id)
            if parent_message.subtype_id and parent_message.subtype_id == self.env.ref(
                    'website_slides.mt_channel_slide_published'):
                if kwargs.get('subtype_id'):
                    kwargs['subtype_id'] = False
                subtype = 'mail.mt_note'
        return super(Channel, self).message_post(parent_id=parent_id,
                                                 subtype=subtype,
                                                 **kwargs)
Example #2
0
class CreateChildAccountWizard(models.TransientModel, Glob_tag_Model):
    '''新增下级科目的向导'''
    _name = 'accountcore.create_child_account'
    _description = '新增下级科目向导'
    fatherAccountId = fields.Many2one('accountcore.account',
                                      string='上级科目',
                                      help='新增科目的直接上级科目')
    fatherAccountNumber = fields.Char(related='fatherAccountId.number',
                                      string='上级科目编码')

    org = fields.Many2one('accountcore.org',
                          string='所属机构',
                          help="科目所属机构",
                          index=True,
                          ondelete='restrict')

    accountsArch = fields.Many2one('accountcore.accounts_arch',
                                   string='所属科目体系',
                                   help="科目所属体系",
                                   index=True,
                                   ondelete='restrict')

    accountClass = fields.Many2one('accountcore.accountclass',
                                   string='科目类别',
                                   index=True,
                                   ondelete='restrict')
    number = fields.Char(string='科目编码', required=True)
    name = fields.Char(string='科目名称', required=True)
    direction = fields.Selection([('1', '借'), ('-1', '贷')],
                                 string='余额方向',
                                 required=True)
    cashFlowControl = fields.Boolean(string='分配现金流量')
    itemClasses = fields.Many2many('accountcore.itemclass',
                                   string='包含的核算项目类别',
                                   help="录入凭证时,提示选择该类别下的核算项目",
                                   ondelete='restrict')
    accountItemClass = fields.Many2one(
        'accountcore.itemclass',
        string='作为明细科目的类别',
        help="录入凭证分录时必须输入的该类别下的一个核算项目,作用相当于明细科目",
        ondelete='restrict')
    explain = fields.Html(string='科目说明')

    @api.model
    def default_get(self, field_names):
        default = super().default_get(field_names)
        fatherAccountId = self.env.context.get('active_id')
        fatherAccount = self.env['accountcore.account'].sudo().search(
            [['id', '=', fatherAccountId]])
        default['accountsArch'] = fatherAccount.accountsArch.id
        default['fatherAccountId'] = fatherAccountId
        default['org'] = fatherAccount.org.id
        default['accountClass'] = fatherAccount.accountClass.id
        default['direction'] = fatherAccount.direction
        default['cashFlowControl'] = fatherAccount.cashFlowControl
        default['number'] = fatherAccount.number + \
            '.' + str(fatherAccount.currentChildNumber)
        return default

    @api.model
    def create(self, values):
        fatherAccountId = self.env.context.get('active_id')
        accountTable = self.env['accountcore.account'].sudo()
        fatherAccount = accountTable.search([['id', '=', fatherAccountId]])
        newAccount = {
            'fatherAccountId':
            fatherAccountId,
            'org':
            fatherAccount.org.id,
            'accountClass':
            fatherAccount.accountClass.id,
            'cashFlowControl':
            values['cashFlowControl'],
            'name':
            fatherAccount.name + '---' + values['name'],
            'number':
            fatherAccount.number + '.' + str(fatherAccount.currentChildNumber)
        }
        fatherAccount.currentChildNumber = fatherAccount.currentChildNumber + 1
        values.update(newAccount)
        rl = super(CreateChildAccountWizard, self).create(values)
        a = accountTable.create(values)
        # 添加到上级科目的直接下级
        fatherAccount.write({'childs_ids': [(4, a.id)]})
        return rl
Example #3
0
class PaymentAcquirer(models.Model):
    """ Acquirer Model. Each specific acquirer can extend the model by adding
    its own fields, using the acquirer_name as a prefix for the new fields.
    Using the required_if_provider='<name>' attribute on fields it is possible
    to have required fields that depend on a specific acquirer.

    Each acquirer has a link to an ir.ui.view record that is a template of
    a button used to display the payment form. See examples in ``payment_ogone``
    and ``payment_paypal`` modules.

    Methods that should be added in an acquirer-specific implementation:

     - ``<name>_form_generate_values(self, reference, amount, currency,
       partner_id=False, partner_values=None, tx_custom_values=None)``:
       method that generates the values used to render the form button template.
     - ``<name>_get_form_action_url(self):``: method that returns the url of
       the button form. It is used for example in ecommerce application if you
       want to post some data to the acquirer.
     - ``<name>_compute_fees(self, amount, currency_id, country_id)``: computes
       the fees of the acquirer, using generic fields defined on the acquirer
       model (see fields definition).

    Each acquirer should also define controllers to handle communication between
    OpenERP and the acquirer. It generally consists in return urls given to the
    button form and that the acquirer uses to send the customer back after the
    transaction, with transaction details given as a POST request.
    """
    _name = 'payment.acquirer'
    _description = 'Payment Acquirer'
    _order = 'website_published desc, sequence, name'

    name = fields.Char('Name', required=True, translate=True)
    description = fields.Html('Description')
    sequence = fields.Integer('Sequence',
                              default=10,
                              help="Determine the display order")
    provider = fields.Selection(selection=[('manual', 'Manual Configuration')],
                                string='Provider',
                                default='manual',
                                required=True)
    company_id = fields.Many2one(
        'res.company',
        'Company',
        default=lambda self: self.env.user.company_id.id,
        required=True)
    view_template_id = fields.Many2one('ir.ui.view',
                                       'Form Button Template',
                                       required=True)
    registration_view_template_id = fields.Many2one(
        'ir.ui.view',
        'S2S Form Template',
        domain=[('type', '=', 'qweb')],
        help="Template for method registration")
    environment = fields.Selection([('test', 'Test'), ('prod', 'Production')],
                                   string='Environment',
                                   default='test',
                                   oldname='env',
                                   required=True)
    website_published = fields.Boolean(
        'Visible in Portal / Website',
        copy=False,
        help="Make this payment acquirer available (Customer invoices, etc.)")
    # Formerly associated to `authorize` option from auto_confirm
    capture_manually = fields.Boolean(
        string="Capture Amount Manually",
        help="Capture the amount from Odoo, when the delivery is completed.")
    # Formerly associated to `generate_and_pay_invoice` option from auto_confirm
    journal_id = fields.Many2one(
        'account.journal',
        'Payment Journal',
        domain=[('type', '=', 'bank')],
        default=lambda self: self.env['account.journal'].search(
            [('type', '=', 'bank')], limit=1),
        help=
        """Payments will be registered into this journal. If you get paid straight on your bank account,
                select your bank account. If you get paid in batch for several transactions, create a specific
                payment journal for this payment acquirer to easily manage the bank reconciliation. You hold
                the amount in a temporary transfer account of your books (created automatically when you create
                the payment journal). Then when you get paid on your bank account by the payment acquirer, you
                reconcile the bank statement line with this temporary transfer account. Use reconciliation
                templates to do it in one-click.""")
    specific_countries = fields.Boolean(
        string="Specific Countries",
        help=
        "If you leave it empty, the payment acquirer will be available for all the countries."
    )
    country_ids = fields.Many2many(
        'res.country',
        'payment_country_rel',
        'payment_id',
        'country_id',
        'Countries',
        help=
        "This payment gateway is available for selected countries. If none is selected it is available for all countries."
    )

    pre_msg = fields.Html(
        'Help Message',
        translate=True,
        help='Message displayed to explain and help the payment process.')
    post_msg = fields.Html(
        'Thanks Message',
        translate=True,
        help='Message displayed after having done the payment process.')
    pending_msg = fields.Html(
        'Pending Message',
        translate=True,
        default=
        '<i>Pending,</i> Your online payment has been successfully processed. But your order is not validated yet.',
        help=
        'Message displayed, if order is in pending state after having done the payment process.'
    )
    done_msg = fields.Html(
        'Done Message',
        translate=True,
        default=
        '<i>Done,</i> Your online payment has been successfully processed. Thank you for your order.',
        help=
        'Message displayed, if order is done successfully after having done the payment process.'
    )
    cancel_msg = fields.Html(
        'Cancel Message',
        translate=True,
        default='<i>Cancel,</i> Your payment has been cancelled.',
        help='Message displayed, if order is cancel during the payment process.'
    )
    error_msg = fields.Html(
        'Error Message',
        translate=True,
        default=
        '<i>Error,</i> Please be aware that an error occurred during the transaction. The order has been confirmed but will not be paid. Do not hesitate to contact us if you have any questions on the status of your order.',
        help='Message displayed, if error is occur during the payment process.'
    )
    save_token = fields.Selection(
        [('none', 'Never'),
         ('ask', 'Let the customer decide (recommended for eCommerce)'),
         ('always', 'Always (recommended for Subscriptions)')],
        string='Save Cards',
        default='none',
        help=
        "This option allows customers to save their credit card as a payment token and to reuse it for a later purchase."
        "If you manage subscriptions (recurring invoicing), you need it to automatically charge the customer when you "
        "issue an invoice.")
    token_implemented = fields.Boolean('Saving Card Data supported',
                                       compute='_compute_feature_support')
    authorize_implemented = fields.Boolean('Authorize Mechanism Supported',
                                           compute='_compute_feature_support')
    fees_implemented = fields.Boolean('Fees Computation Supported',
                                      compute='_compute_feature_support')
    fees_active = fields.Boolean('Add Extra Fees')
    fees_dom_fixed = fields.Float('Fixed domestic fees')
    fees_dom_var = fields.Float('Variable domestic fees (in percents)')
    fees_int_fixed = fields.Float('Fixed international fees')
    fees_int_var = fields.Float('Variable international fees (in percents)')

    # TDE FIXME: remove that brol
    module_id = fields.Many2one('ir.module.module',
                                string='Corresponding Module')
    module_state = fields.Selection(selection=module.STATES,
                                    string='Installation State',
                                    related='module_id.state')

    image = fields.Binary(
        "Image",
        attachment=True,
        help=
        "This field holds the image used for this provider, limited to 1024x1024px"
    )
    image_medium = fields.Binary(
        "Medium-sized image",
        attachment=True,
        help="Medium-sized image of this provider. 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 image",
        attachment=True,
        help="Small-sized image of this provider. It is automatically "
        "resized as a 64x64px image, with aspect ratio preserved. "
        "Use this field anywhere a small image is required.")

    def _compute_feature_support(self):
        feature_support = self._get_feature_support()
        for acquirer in self:
            acquirer.fees_implemented = acquirer.provider in feature_support[
                'fees']
            acquirer.authorize_implemented = acquirer.provider in feature_support[
                'authorize']
            acquirer.token_implemented = acquirer.provider in feature_support[
                'tokenize']

    @api.multi
    def _check_required_if_provider(self):
        """ If the field has 'required_if_provider="<provider>"' attribute, then it
        required if record.provider is <provider>. """
        for acquirer in self:
            if any(
                    getattr(f, 'required_if_provider', None) ==
                    acquirer.provider and not acquirer[k]
                    for k, f in pycompat.items(self._fields)):
                return False
        return True

    _constraints = [
        (_check_required_if_provider, 'Required fields not filled', []),
    ]

    def _get_feature_support(self):
        """Get advanced feature support by provider.

        Each provider should add its technical in the corresponding
        key for the following features:
            * fees: support payment fees computations
            * authorize: support authorizing payment (separates
                         authorization and capture)
            * tokenize: support saving payment data in a payment.tokenize
                        object
        """
        return dict(authorize=[], tokenize=[], fees=[])

    @api.model
    def create(self, vals):
        image_resize_images(vals)
        vals = self._check_journal_id(vals)
        return super(PaymentAcquirer, self).create(vals)

    @api.multi
    def write(self, vals):
        image_resize_images(vals)
        vals = self._check_journal_id(vals)
        return super(PaymentAcquirer, self).write(vals)

    def _check_journal_id(self, vals):
        if not vals.get('journal_id', False):
            default_journal = self.env['account.journal'].search(
                [('type', '=', 'bank')], limit=1)
            if default_journal:
                vals.update({'journal_id': default_journal.id})
        return vals

    @api.multi
    def toggle_website_published(self):
        self.write({'website_published': not self.website_published})
        return True

    @api.multi
    def get_form_action_url(self):
        """ Returns the form action URL, for form-based acquirer implementations. """
        if hasattr(self, '%s_get_form_action_url' % self.provider):
            return getattr(self, '%s_get_form_action_url' % self.provider)()
        return False

    @api.multi
    def render(self,
               reference,
               amount,
               currency_id,
               partner_id=False,
               values=None):
        """ Renders the form template of the given acquirer as a qWeb template.
        :param string reference: the transaction reference
        :param float amount: the amount the buyer has to pay
        :param currency_id: currency id
        :param dict partner_id: optional partner_id to fill values
        :param dict values: a dictionary of values for the transction that is
        given to the acquirer-specific method generating the form values

        All templates will receive:

         - acquirer: the payment.acquirer browse record
         - user: the current user browse record
         - currency_id: id of the transaction currency
         - amount: amount of the transaction
         - reference: reference of the transaction
         - partner_*: partner-related values
         - partner: optional partner browse record
         - 'feedback_url': feedback URL, controler that manage answer of the acquirer (without base url) -> FIXME
         - 'return_url': URL for coming back after payment validation (wihout base url) -> FIXME
         - 'cancel_url': URL if the client cancels the payment -> FIXME
         - 'error_url': URL if there is an issue with the payment -> FIXME
         - context: Odoo context

        """
        if values is None:
            values = {}

        # reference and amount
        values.setdefault('reference', reference)
        amount = float_round(amount, 2)
        values.setdefault('amount', amount)

        # currency id
        currency_id = values.setdefault('currency_id', currency_id)
        if currency_id:
            currency = self.env['res.currency'].browse(currency_id)
        else:
            currency = self.env.user.company_id.currency_id
        values['currency'] = currency

        # Fill partner_* using values['partner_id'] or partner_id argument
        partner_id = values.get('partner_id', partner_id)
        billing_partner_id = values.get('billing_partner_id', partner_id)
        if partner_id:
            partner = self.env['res.partner'].browse(partner_id)
            if partner_id != billing_partner_id:
                billing_partner = self.env['res.partner'].browse(
                    billing_partner_id)
            else:
                billing_partner = partner
            values.update({
                'partner':
                partner,
                'partner_id':
                partner_id,
                'partner_name':
                partner.name,
                'partner_lang':
                partner.lang,
                'partner_email':
                partner.email,
                'partner_zip':
                partner.zip,
                'partner_city':
                partner.city,
                'partner_address':
                _partner_format_address(partner.street, partner.street2),
                'partner_country_id':
                partner.country_id.id,
                'partner_country':
                partner.country_id,
                'partner_phone':
                partner.phone,
                'partner_state':
                partner.state_id,
                'billing_partner':
                billing_partner,
                'billing_partner_id':
                billing_partner_id,
                'billing_partner_name':
                billing_partner.name,
                'billing_partner_lang':
                billing_partner.lang,
                'billing_partner_email':
                billing_partner.email,
                'billing_partner_zip':
                billing_partner.zip,
                'billing_partner_city':
                billing_partner.city,
                'billing_partner_address':
                _partner_format_address(billing_partner.street,
                                        billing_partner.street2),
                'billing_partner_country_id':
                billing_partner.country_id.id,
                'billing_partner_country':
                billing_partner.country_id,
                'billing_partner_phone':
                billing_partner.phone,
                'billing_partner_state':
                billing_partner.state_id,
            })
        if values.get('partner_name'):
            values.update({
                'partner_first_name':
                _partner_split_name(values.get('partner_name'))[0],
                'partner_last_name':
                _partner_split_name(values.get('partner_name'))[1],
            })
        if values.get('billing_partner_name'):
            values.update({
                'billing_partner_first_name':
                _partner_split_name(values.get('billing_partner_name'))[0],
                'billing_partner_last_name':
                _partner_split_name(values.get('billing_partner_name'))[1],
            })

        # Fix address, country fields
        if not values.get('partner_address'):
            values['address'] = _partner_format_address(
                values.get('partner_street', ''),
                values.get('partner_street2', ''))
        if not values.get('partner_country') and values.get(
                'partner_country_id'):
            values['country'] = self.env['res.country'].browse(
                values.get('partner_country_id'))
        if not values.get('billing_partner_address'):
            values['billing_address'] = _partner_format_address(
                values.get('billing_partner_street', ''),
                values.get('billing_partner_street2', ''))
        if not values.get('billing_partner_country') and values.get(
                'billing_partner_country_id'):
            values['billing_country'] = self.env['res.country'].browse(
                values.get('billing_partner_country_id'))

        # compute fees
        fees_method_name = '%s_compute_fees' % self.provider
        if hasattr(self, fees_method_name):
            fees = getattr(self,
                           fees_method_name)(values['amount'],
                                             values['currency_id'],
                                             values.get('partner_country_id'))
            values['fees'] = float_round(fees, 2)

        # call <name>_form_generate_values to update the tx dict with acqurier specific values
        cust_method_name = '%s_form_generate_values' % (self.provider)
        if hasattr(self, cust_method_name):
            method = getattr(self, cust_method_name)
            values = method(values)

        values.update({
            'tx_url':
            self._context.get('tx_url', self.get_form_action_url()),
            'submit_class':
            self._context.get('submit_class', 'btn btn-link'),
            'submit_txt':
            self._context.get('submit_txt'),
            'acquirer':
            self,
            'user':
            self.env.user,
            'context':
            self._context,
            'type':
            values.get('type') or 'form',
        })
        values.setdefault('return_url', False)

        return self.view_template_id.render(values, engine='ir.qweb')

    @api.multi
    def _registration_render(self, partner_id, qweb_context=None):
        if qweb_context is None:
            qweb_context = {}
        qweb_context.update(id=self.ids[0], partner_id=partner_id)
        method_name = '_%s_registration_form_generate_values' % (
            self.provider, )
        if hasattr(self, method_name):
            method = getattr(self, method_name)
            qweb_context.update(method(qweb_context))
        return self.registration_view_template_id.render(qweb_context,
                                                         engine='ir.qweb')

    @api.multi
    def s2s_process(self, data):
        cust_method_name = '%s_s2s_form_process' % (self.provider)
        if not self.s2s_validate(data):
            return False
        if hasattr(self, cust_method_name):
            method = getattr(self, cust_method_name)
            return method(data)
        return True

    @api.multi
    def s2s_validate(self, data):
        cust_method_name = '%s_s2s_form_validate' % (self.provider)
        if hasattr(self, cust_method_name):
            method = getattr(self, cust_method_name)
            return method(data)
        return True

    @api.multi
    def toggle_environment_value(self):
        prod = self.filtered(lambda acquirer: acquirer.environment == 'prod')
        prod.write({'environment': 'test'})
        (self - prod).write({'environment': 'prod'})

    @api.multi
    def button_immediate_install(self):
        # TDE FIXME: remove that brol
        if self.module_id and self.module_state != 'installed':
            self.module_id.button_immediate_install()
            context = dict(self._context, active_id=self.ids[0])
            return {
                'view_type': 'form',
                'view_mode': 'form',
                'res_model': 'payment.acquirer',
                'type': 'ir.actions.act_window',
                'res_id': self.ids[0],
                'context': context,
            }
Example #4
0
class Movimiento(models.Model):
    _name = "sa.movimiento"
    _description = "Movimiento"
    _inherit = "mail.thread"

    name = fields.Char(string="Nombre", required=True)
    type_move = fields.Selection(selection=[("ingreso", "Ingreso"),
                                            ("gasto", "Gasto")],
                                 string="Tipo de movimiento",
                                 default="ingreso",
                                 required=True,
                                 track_visibility="onchange")
    date = fields.Datetime(string="Fecha")
    amount = fields.Float("Monto", track_visibility="onchange")
    receipt_image = fields.Binary("Foto del recibo")
    notas = fields.Html("Notas")

    currency_id = fields.Many2one("res.currency", default=44)

    user_id = fields.Many2one("res.users",
                              string="Usuario",
                              default=lambda self: self.env.user.id)
    category_id = fields.Many2one("sa.category", "Categoria")
    tag_id = fields.Many2many("sa.tag", "sa_mov_sa_tag_rel", "move_id",
                              "tag_id")

    email = fields.Char(related="user_id.email", string="Correo electrònico")

    @api.constrains("amount")
    def _check_amount(self):
        if not (self.amount >= 0 and self.amount <= 100000):
            raise ValidationError("El monto debe encontrarse entre 0 y 100000")

    @api.onchange("type_move")
    def onchange_type_move(self):
        if (self.type_move == "ingreso"):
            self.name = "Ingreso: "
        elif self.type_move == "gasto":
            self.name = "Egreso"

    @api.model
    def create(self, vals):
        name = vals.get("name", "-")
        amount = vals.get("amount", "0")
        type_move = vals.get("type_move", "")
        date = vals.get("date", "")

        user = self.env.user
        count_mov = user.count_movimientos

        if count_mov >= 5 and user.has_group("saldo_app.res_groups_user_free"):
            raise ValidationError("Solo puedes crear 5 movimientos por mes")

        notas = """ 
            <p>Tipo de movimiento: {}</p>
            <p>Nombre: {}</p>
            <p>Monto: {}</p>
            <p>Fecha: {}</p>
        """
        vals["notas"] = notas.format(type_move, name, amount, date)
        return super(Movimiento, self).create(vals)

    def unlink(self):
        for record in self:
            if record.amount >= 50:
                raise ValidationError(
                    "Movimientos con montos mayores a 50 no pueden ser eliminados"
                )
        return super(Movimiento, self).unlink()
Example #5
0
class ResCompany(models.Model):
    _inherit = 'res.company'

    invoice_terms = fields.Html(string='Default Terms and Conditions',
                                translate=True)
class DisciplinaryAction(models.Model):

    _name = 'disciplinary.action'
    _inherit = ['mail.thread', 'mail.activity.mixin']
    _description = "Disciplinary Action"

    state = fields.Selection([
        ('draft', 'Draft'),
        ('explain', 'Waiting Explanation'),
        ('submitted', 'Waiting Action'),
        ('action', 'Action Validated'),
        ('cancel', 'Cancelled'),
    ],
                             default='draft',
                             track_visibility='onchange')

    name = fields.Char(string='Reference',
                       required=True,
                       copy=False,
                       readonly=True,
                       default=lambda self: _('New'))

    employee_name = fields.Many2one('hr.employee',
                                    string='Employee',
                                    required=True)
    department_name = fields.Many2one('hr.department',
                                      string='Department',
                                      required=True)
    discipline_reason = fields.Many2one('discipline.category',
                                        string='Reason',
                                        required=True)
    explanation = fields.Text(string="Explanation by Employee",
                              help='Employee have to give Explanation'
                              'to manager about the violation of discipline')
    action = fields.Many2one('action.category', string="Action")
    read_only = fields.Boolean(compute="get_user", default=True)
    warning_letter = fields.Html(string="Warning Letter")
    suspension_letter = fields.Html(string="Suspension Letter")
    termination_letter = fields.Html(string="Termination Letter")
    warning = fields.Integer(default=False)
    action_details = fields.Text(string="Action Details")
    attachment_ids = fields.Many2many(
        'ir.attachment',
        string="Attachments",
        help=
        "Employee can submit any documents which supports their explanation")
    note = fields.Text(string="Internal Note")
    joined_date = fields.Date(string="Joined Date")

    # assigning the sequence for the record
    @api.model
    def create(self, vals):
        vals['name'] = self.env['ir.sequence'].next_by_code(
            'disciplinary.action')
        return super(DisciplinaryAction, self).create(vals)

    # Check the user is a manager or employee
    @api.depends('read_only')
    def get_user(self):

        res_user = self.env['res.users'].search([('id', '=', self._uid)])
        if res_user.has_group('hr.group_hr_manager'):
            self.read_only = True
        else:
            self.read_only = False
        print(self.read_only)

    # Check the Action Selected
    @api.onchange('action')
    def onchange_action(self):
        if self.action.name == 'Written Warning':
            self.warning = 1
        elif self.action.name == 'Suspend the Employee for one Week':
            self.warning = 2
        elif self.action.name == 'Terminate the Employee':
            self.warning = 3
        elif self.action.name == 'No Action':
            self.warning = 4
        else:
            self.warning = 5

    @api.onchange('employee_name')
    @api.depends('employee_name')
    def onchange_employee_name(self):

        department = self.env['hr.employee'].search([
            ('name', '=', self.employee_name.name)
        ])
        self.department_name = department.department_id.id

        if self.state == 'action':
            raise ValidationError(_('You Can not edit a Validated Action !!'))

    @api.onchange('discipline_reason')
    @api.depends('discipline_reason')
    def onchange_reason(self):
        if self.state == 'action':
            raise ValidationError(_('You Can not edit a Validated Action !!'))

    @api.multi
    def assign_function(self):

        for rec in self:
            rec.state = 'explain'

    @api.multi
    def cancel_function(self):
        for rec in self:
            rec.state = 'cancel'

    @api.multi
    def set_to_function(self):
        for rec in self:
            rec.state = 'draft'

    @api.multi
    def action_function(self):
        for rec in self:
            if not rec.action:
                raise ValidationError(_('You have to select an Action !!'))

            if self.warning == 1:
                if not rec.warning_letter or rec.warning_letter == '<p><br></p>':
                    raise ValidationError(
                        _('You have to fill up the Warning Letter in Action Information !!'
                          ))

            elif self.warning == 2:
                if not rec.suspension_letter or rec.suspension_letter == '<p><br></p>':
                    raise ValidationError(
                        _('You have to fill up the Suspension Letter in Action Information !!'
                          ))

            elif self.warning == 3:
                if not rec.termination_letter or rec.termination_letter == '<p><br></p>':
                    raise ValidationError(
                        _('You have to fill up the Termination Letter in  Action Information !!'
                          ))

            elif self.warning == 4:
                self.action_details = "No Action Proceed"

            elif self.warning == 5:
                if not rec.action_details:
                    raise ValidationError(
                        _('You have to fill up the  Action Information !!'))
            rec.state = 'action'

    @api.multi
    def explanation_function(self):
        for rec in self:

            if not rec.explanation:
                raise ValidationError(_('You must give an explanation !!'))
        if len(self.explanation.split()) < 5:
            raise ValidationError(
                _('Your explanation must contain at least 5 words   !!'))

        self.write({'state': 'submitted'})
class ms_pendaftaran(models.Model):
    _name = "ms.pendaftaran"
    _description = "Pendaftaran"
    _order = "name desc"

    name = fields.Char(string='Pendaftaran', default='/')
    state = fields.Selection([('draft', 'Draft'), ('confirm', 'Confirmed'),
                              ('cancel', 'Cancelled')],
                             string='State',
                             default='draft')
    pasien_id = fields.Many2one('res.partner',
                                domain=[('pasien', '=', True)],
                                string='Pasien')
    poli_id = fields.Many2one('ms.poli', string='Poli yang Dituju')
    tanggal = fields.Datetime(string='Tanggal', default=fields.Datetime.now())
    note = fields.Html(string='Note')

    @api.model
    def create(self, vals):
        vals['name'] = self.env['ir.sequence'].get_sequence(
            'Pendaftaran', 'ms.pendaftaran', 'DFT/%(y)s/', 5)
        return super(ms_pendaftaran, self).create(vals)

    @api.multi
    def name_get(self):
        result = []
        for me_id in self:
            result.append(
                (me_id.id, "%s - %s" % (me_id.name, me_id.pasien_id.name)))
        return result

    @api.model
    def name_search(self, name, args=None, operator='ilike', limit=100):
        args = args or []
        if name:
            recs = self.search([
                '|',
                ('pasien_id.name', operator, name),
                ('name', operator, name),
            ] + args,
                               limit=limit)
        else:
            recs = self.search([] + args, limit=limit)
        return recs.name_get()

    @api.multi
    def action_confirm(self):
        for me_id in self:
            if me_id.state == 'draft':
                self.env['ms.pemeriksaan'].create({'pendaftaran_id': me_id.id})
                me_id.write({'state': 'confirm'})

    @api.multi
    def action_cancel(self):
        for me_id in self:
            pemeriksaan_ids = self.env['ms.pemeriksaan'].search([
                ('pendaftaran_id', '=', me_id.id), ('state', '!=', 'cancel')
            ])
            if pemeriksaan_ids:
                pemeriksaan_names = [
                    unicodedata.normalize('NFKD', pemeriksaan.name).encode(
                        'ascii', 'ignore') for pemeriksaan in pemeriksaan_ids
                ]
                raise Warning(
                    "Silahkan cancel pemeriksaan %s terlebih dahulu !" %
                    pemeriksaan_names)
            me_id.write({'state': 'cancel'})

    @api.multi
    def unlink(self):
        for me_id in self:
            if me_id.state != 'draft':
                raise Warning(
                    "Tidak bisa menghapus data pendaftaran yang bukan draft !")
        return super(ms_pendaftaran, self).unlink()
Example #8
0
class Slide(models.Model):
    _name = 'slide.slide'
    _inherit = [
        'mail.thread', 'rating.mixin', 'image.mixin', 'website.seo.metadata',
        'website.published.mixin'
    ]
    _description = 'Slides'
    _mail_post_access = 'read'
    _order_by_strategy = {
        'sequence': 'category_sequence asc, sequence asc',
        'most_viewed': 'total_views desc',
        'most_voted': 'likes desc',
        'latest': 'date_published desc',
    }
    _order = 'category_sequence asc, sequence asc'

    def _default_access_token(self):
        return str(uuid.uuid4())

    # description
    name = fields.Char('Title', required=True, translate=True)
    active = fields.Boolean(default=True)
    sequence = fields.Integer('Sequence', default=10)
    category_sequence = fields.Integer('Category sequence',
                                       related="category_id.sequence",
                                       store=True)
    user_id = fields.Many2one('res.users',
                              string='Uploaded by',
                              default=lambda self: self.env.uid)
    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')
    access_token = fields.Char("Security Token",
                               copy=False,
                               default=_default_access_token)
    is_preview = fields.Boolean(
        'Is Preview',
        default=False,
        help=
        "The course is accessible by anyone : the users don't need to join the channel to access the content of the course."
    )
    completion_time = fields.Float('# Hours', default=1, digits=(10, 4))
    # subscribers
    partner_ids = fields.Many2many('res.partner',
                                   'slide_slide_partner',
                                   'slide_id',
                                   'partner_id',
                                   string='Subscribers',
                                   groups='website.group_website_publisher')
    slide_partner_ids = fields.One2many(
        'slide.slide.partner',
        'slide_id',
        string='Subscribers information',
        groups='website.group_website_publisher')
    user_membership_id = fields.Many2one(
        'slide.slide.partner',
        string="Subscriber information",
        compute='_compute_user_membership_id',
        help="Subscriber information for the current logged in user")
    # Quiz related fields
    question_ids = fields.One2many("slide.question",
                                   "slide_id",
                                   string="Questions")
    quiz_first_attempt_reward = fields.Integer("First attempt reward",
                                               default=10)
    quiz_second_attempt_reward = fields.Integer("Second attempt reward",
                                                default=7)
    quiz_third_attempt_reward = fields.Integer(
        "Third attempt reward",
        default=5,
    )
    quiz_fourth_attempt_reward = fields.Integer(
        "Reward for every attempt after the third try", default=2)

    # content
    slide_type = fields.Selection(
        [('infographic', 'Infographic'), ('webpage', 'Web Page'),
         ('presentation', 'Presentation'), ('document', 'Document'),
         ('video', 'Video'), ('quiz', "Quiz")],
        string='Type',
        required=True,
        default='document',
        help=
        "The document type will be set automatically based on the document URL and properties (e.g. height and width for presentation and document)."
    )
    index_content = fields.Text('Transcript')
    datas = fields.Binary('Content', attachment=True)
    url = fields.Char('Document URL', help="Youtube or Google Document URL")
    document_id = fields.Char('Document ID',
                              help="Youtube or Google Document ID")
    link_ids = fields.One2many('slide.slide.link',
                               'slide_id',
                               string="External URL for this slide")
    mime_type = fields.Char('Mime-type')
    html_content = fields.Html(
        "HTML Content",
        help="Custom HTML content for slides of type 'Web Page'.",
        translate=True)
    # website
    website_id = fields.Many2one(related='channel_id.website_id',
                                 readonly=True)
    date_published = fields.Datetime('Publish Date')
    likes = fields.Integer('Likes', compute='_compute_user_info', store=True)
    dislikes = fields.Integer('Dislikes',
                              compute='_compute_user_info',
                              store=True)
    user_vote = fields.Integer('User vote', compute='_compute_user_info')
    embed_code = fields.Text('Embed Code',
                             readonly=True,
                             compute='_compute_embed_code')
    # views
    embedcount_ids = fields.One2many('slide.embed',
                                     'slide_id',
                                     string="Embed Count")
    slide_views = fields.Integer('# of Website Views',
                                 store=True,
                                 compute="_compute_slide_views")
    public_views = fields.Integer('# of Public Views')
    total_views = fields.Integer("Total # Views",
                                 default="0",
                                 compute='_compute_total',
                                 store=True)

    _sql_constraints = [(
        'exclusion_html_content_and_url',
        "CHECK(html_content IS NULL OR url IS NULL)",
        "A slide is either filled with a document url or HTML content. Not both."
    )]

    @api.depends('slide_views', 'public_views')
    def _compute_total(self):
        for record in self:
            record.total_views = record.slide_views + record.public_views

    @api.depends('slide_partner_ids.vote')
    def _compute_user_info(self):
        slide_data = dict.fromkeys(
            self.ids, dict({
                'likes': 0,
                'dislikes': 0,
                'user_vote': False
            }))
        slide_partners = self.env['slide.slide.partner'].sudo().search([
            ('slide_id', 'in', self.ids)
        ])
        for slide_partner in slide_partners:
            if slide_partner.vote == 1:
                slide_data[slide_partner.slide_id.id]['likes'] += 1
                if slide_partner.partner_id == self.env.user.partner_id:
                    slide_data[slide_partner.slide_id.id]['user_vote'] = 1
            elif slide_partner.vote == -1:
                slide_data[slide_partner.slide_id.id]['dislikes'] += 1
                if slide_partner.partner_id == self.env.user.partner_id:
                    slide_data[slide_partner.slide_id.id]['user_vote'] = -1
        for slide in self:
            slide.update(slide_data[slide.id])

    @api.depends('slide_partner_ids.slide_id')
    def _compute_slide_views(self):
        # TODO awa: tried compute_sudo, for some reason it doesn't work in here...
        read_group_res = self.env['slide.slide.partner'].sudo().read_group(
            [('slide_id', 'in', self.ids)], ['slide_id'], groupby=['slide_id'])
        mapped_data = dict((res['slide_id'][0], res['slide_id_count'])
                           for res in read_group_res)
        for slide in self:
            slide.slide_views = mapped_data.get(slide.id, 0)

    @api.depends('slide_partner_ids.partner_id')
    def _compute_user_membership_id(self):
        slide_partners = self.env['slide.slide.partner'].sudo().search([
            ('slide_id', 'in', self.ids),
            ('partner_id', '=', self.env.user.partner_id.id),
        ])

        for record in self:
            record.user_membership_id = next(
                (slide_partner for slide_partner in slide_partners
                 if slide_partner.slide_id == record),
                self.env['slide.slide.partner'])

    @api.depends('document_id', 'slide_type', 'mime_type')
    def _compute_embed_code(self):
        base_url = request and request.httprequest.url_root or self.env[
            'ir.config_parameter'].sudo().get_param('web.base.url')
        if base_url[-1] == '/':
            base_url = base_url[:-1]
        for record in self:
            if record.datas and (not record.document_id or record.slide_type
                                 in ['document', 'presentation']):
                slide_url = base_url + url_for(
                    '/slides/embed/%s?page=1' % record.id)
                record.embed_code = '<iframe src="%s" class="o_wslides_iframe_viewer" allowFullScreen="true" height="%s" width="%s" frameborder="0"></iframe>' % (
                    slide_url, 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 = '<iframe src="//drive.google.com/file/d/%s/preview" allowFullScreen="true" frameborder="0"></iframe>' % (
                        record.document_id)
            else:
                record.embed_code = False

    @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.items():
                self[key] = value

    @api.depends('name', 'channel_id.website_id.domain')
    def _compute_website_url(self):
        # TDE FIXME: clena this link.tracker strange stuff
        super(Slide, self)._compute_website_url()
        for slide in self:
            if slide.id:  # avoid to perform a slug on a not yet saved record in case of an onchange.
                base_url = slide.channel_id.get_base_url()
                # link_tracker is not in dependencies, so use it to shorten url only if installed.
                if self.env.registry.get('link.tracker'):
                    url = self.env['link.tracker'].sudo().create({
                        'url':
                        '%s/slides/slide/%s' % (base_url, slug(slide)),
                        'title':
                        slide.name,
                    }).short_url
                else:
                    url = '%s/slides/slide/%s' % (base_url, slug(slide))
                slide.website_url = url

    @api.depends('channel_id.can_publish')
    def _compute_can_publish(self):
        for record in self:
            record.can_publish = record.channel_id.can_publish

    @api.model
    def _get_can_publish_error_message(self):
        return _(
            "Publishing is restricted to the responsible of training courses or members of the publisher group for documentation courses"
        )

    # ---------------------------------------------------------
    # ORM Overrides
    # ---------------------------------------------------------

    @api.model
    def create(self, values):
        # Do not publish slide if user has not publisher rights
        channel = self.env['slide.channel'].browse(values['channel_id'])
        if not channel.can_publish:
            # 'website_published' is handled by mixin
            values['date_published'] = False

        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') and not values.get('document_id'):
            doc_data = self._parse_document_url(values['url']).get(
                'values', dict())
            for key, value in doc_data.items():
                values.setdefault(key, value)

        slide = super(Slide, self).create(values)

        if slide.website_published:
            slide._post_publication()
        return slide

    def write(self, values):
        if values.get('url') and values['url'] != self.url:
            doc_data = self._parse_document_url(values['url']).get(
                'values', dict())
            for key, value in doc_data.items():
                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

    # ---------------------------------------------------------
    # Mail/Rating
    # ---------------------------------------------------------

    @api.returns('mail.message', lambda value: value.id)
    def message_post(self, message_type='notification', **kwargs):
        self.ensure_one()
        if message_type == 'comment' and not self.channel_id.can_comment:  # user comments have a restriction on karma
            raise KarmaError(_('Not enough karma to comment'))
        return super(Slide, self).message_post(message_type=message_type,
                                               **kwargs)

    def get_access_action(self, access_uid=None):
        """ Instead of the classic form view, redirect to website if it is published. """
        self.ensure_one()
        if self.website_published:
            return {
                'type': 'ir.actions.act_url',
                'url': '%s' % self.website_url,
                'target': 'self',
                'target_type': 'public',
                'res_id': self.id,
            }
        return super(Slide, self).get_access_action(access_uid)

    def _notify_get_groups(self):
        """ Add access button to everyone if the document is active. """
        groups = super(Slide, self)._notify_get_groups()

        if self.website_published:
            for group_name, group_method, group_data in groups:
                group_data['has_button_access'] = True

        return groups

    # ---------------------------------------------------------
    # Business Methods
    # ---------------------------------------------------------

    def _post_publication(self):
        base_url = self.env['ir.config_parameter'].sudo().get_param(
            'web.base.url')
        for slide in self.filtered(lambda slide: slide.website_published and
                                   slide.channel_id.publish_template_id):
            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)
            subject = publish_template._render_template(
                publish_template.subject, 'slide.slide', slide.id)
            slide.channel_id.with_context(
                mail_create_nosubscribe=True).message_post(
                    subject=subject,
                    body=html_body,
                    subtype='website_slides.mt_channel_slide_published',
                    email_layout_xmlid='mail.mail_notification_light',
                )
        return True

    def _generate_signed_token(self, partner_id):
        """ Lazy generate the acces_token and return it signed by the given partner_id
            :rtype tuple (string, int)
            :return (signed_token, partner_id)
        """
        if not self.access_token:
            self.write({'access_token': self._default_access_token()})
        return self._sign_token(partner_id)

    def _send_share_email(self, email):
        # TDE FIXME: template to check
        mail_ids = []
        base_url = self.env['ir.config_parameter'].sudo().get_param(
            'web.base.url')
        for record in self:
            if self.env.user.has_group('base.group_portal'):
                mail_ids.append(
                    self.channel_id.share_template_id.with_context(
                        user=self.env.user, email=email,
                        base_url=base_url).sudo().send_mail(
                            record.id,
                            notif_layout='mail.mail_notification_light',
                            email_values={
                                'email_from':
                                self.env['res.company'].catchall
                                or self.env['res.company'].email
                            }))
            else:
                mail_ids.append(
                    self.channel_id.share_template_id.with_context(
                        user=self.env.user, email=email,
                        base_url=base_url).send_mail(
                            record.id,
                            notif_layout='mail.mail_notification_light'))
        return mail_ids

    def action_like(self):
        self.check_access_rights('read')
        self.check_access_rule('read')
        return self._action_vote(upvote=True)

    def action_dislike(self):
        self.check_access_rights('read')
        self.check_access_rule('read')
        return self._action_vote(upvote=False)

    def _action_vote(self, upvote=True):
        """ Private implementation of voting. It does not check for any real access
        rights; public methods should grant access before calling this method.

          :param upvote: if True, is a like; if False, is a dislike
        """
        self_sudo = self.sudo()
        SlidePartnerSudo = self.env['slide.slide.partner'].sudo()
        slide_partners = SlidePartnerSudo.search([
            ('slide_id', 'in', self.ids),
            ('partner_id', '=', self.env.user.partner_id.id)
        ])
        slide_id = slide_partners.mapped('slide_id')
        new_slides = self_sudo - slide_id
        channel = slide_id.channel_id
        karma_to_add = 0

        for slide_partner in slide_partners:
            if upvote:
                new_vote = 0 if slide_partner.vote == -1 else 1
                if slide_partner.vote != 1:
                    karma_to_add += channel.karma_gen_slide_vote
            else:
                new_vote = 0 if slide_partner.vote == 1 else -1
                if slide_partner.vote != -1:
                    karma_to_add -= channel.karma_gen_slide_vote
            slide_partner.vote = new_vote

        for new_slide in new_slides:
            new_vote = 1 if upvote else -1
            new_slide.write({
                'slide_partner_ids': [(0, 0, {
                    'vote':
                    new_vote,
                    'partner_id':
                    self.env.user.partner_id.id
                })]
            })
            karma_to_add += new_slide.channel_id.karma_gen_slide_vote * (
                1 if upvote else -1)

        if karma_to_add:
            self.env.user.add_karma(karma_to_add)

    def action_set_viewed(self, quiz_attempts_inc=False):
        if not all(slide.channel_id.is_member for slide in self):
            raise UserError(
                _('You cannot mark a slide as viewed if you are not among its members.'
                  ))

        return bool(
            self._action_set_viewed(self.env.user.partner_id,
                                    quiz_attempts_inc=quiz_attempts_inc))

    def _action_set_viewed(self, target_partner, quiz_attempts_inc=False):
        self_sudo = self.sudo()
        SlidePartnerSudo = self.env['slide.slide.partner'].sudo()
        existing_sudo = SlidePartnerSudo.search([('slide_id', 'in', self.ids),
                                                 ('partner_id', '=',
                                                  target_partner.id)])
        if quiz_attempts_inc:
            for exsting_slide in existing_sudo:
                exsting_slide.write({
                    'quiz_attempts_count':
                    exsting_slide.quiz_attempts_count + 1
                })

        new_slides = self_sudo - existing_sudo.mapped('slide_id')
        return SlidePartnerSudo.create([{
            'slide_id':
            new_slide.id,
            'channel_id':
            new_slide.channel_id.id,
            'partner_id':
            target_partner.id,
            'quiz_attempts_count':
            1 if quiz_attempts_inc else 0,
            'vote':
            0
        } for new_slide in new_slides])

    def action_set_completed(self):
        if not all(slide.channel_id.is_member for slide in self):
            raise UserError(
                _('You cannot mark a slide as completed if you are not among its members.'
                  ))

        return self._action_set_completed(self.env.user.partner_id)

    def _action_set_completed(self, target_partner):
        self_sudo = self.sudo()
        SlidePartnerSudo = self.env['slide.slide.partner'].sudo()
        existing_sudo = SlidePartnerSudo.search([('slide_id', 'in', self.ids),
                                                 ('partner_id', '=',
                                                  target_partner.id)])
        existing_sudo.write({'completed': True})

        new_slides = self_sudo - existing_sudo.mapped('slide_id')
        SlidePartnerSudo.create([{
            'slide_id': new_slide.id,
            'channel_id': new_slide.channel_id.id,
            'partner_id': target_partner.id,
            'vote': 0,
            'completed': True
        } for new_slide in new_slides])

        return True

    def _action_set_quiz_done(self):
        if not all(slide.channel_id.is_member for slide in self):
            raise UserError(
                _('You cannot mark a slide quiz as completed if you are not among its members.'
                  ))

        points = 0
        for slide in self:
            user_membership_sudo = slide.user_membership_id.sudo()
            if not user_membership_sudo or user_membership_sudo.completed or not user_membership_sudo.quiz_attempts_count:
                continue

            gains = [
                slide.quiz_first_attempt_reward,
                slide.quiz_second_attempt_reward,
                slide.quiz_third_attempt_reward,
                slide.quiz_fourth_attempt_reward
            ]
            points += gains[
                user_membership_sudo.quiz_attempts_count -
                1] if user_membership_sudo.quiz_attempts_count <= len(
                    gains) else gains[-1]

        return self.env.user.sudo().add_karma(points)

    def _compute_quiz_info(self, target_partner, quiz_done=False):
        result = dict.fromkeys(self.ids, False)
        slide_partners = self.env['slide.slide.partner'].sudo().search([
            ('slide_id', 'in', self.ids),
            ('partner_id', '=', target_partner.id)
        ])
        slide_partners_map = dict(
            (sp.slide_id.id, sp) for sp in slide_partners)
        for slide in self:
            if not slide.question_ids:
                gains = [0]
            else:
                gains = [
                    slide.quiz_first_attempt_reward,
                    slide.quiz_second_attempt_reward,
                    slide.quiz_third_attempt_reward,
                    slide.quiz_fourth_attempt_reward
                ]
            result[slide.id] = {
                'quiz_karma_max':
                gains[0],  # what could be gained if succeed at first try
                'quiz_karma_gain':
                gains[0],  # what would be gained at next test
                'quiz_karma_won': 0,  # what has been gained
                'quiz_attempts_count': 0,  # number of attempts
            }
            slide_partner = slide_partners_map.get(slide.id)
            if slide.question_ids and slide_partner:
                if slide_partner.quiz_attempts_count:
                    result[slide.id]['quiz_karma_gain'] = gains[
                        slide_partner.
                        quiz_attempts_count] if slide_partner.quiz_attempts_count < len(
                            gains) else gains[-1]
                    result[slide.id][
                        'quiz_attempts_count'] = slide_partner.quiz_attempts_count
                if quiz_done or slide_partner.completed:
                    result[slide.id]['quiz_karma_won'] = gains[
                        slide_partner.quiz_attempts_count -
                        1] if slide_partner.quiz_attempts_count < len(
                            gains) else gains[-1]
        return result

    # --------------------------------------------------
    # Parsing methods
    # --------------------------------------------------

    @api.model
    def _fetch_data(self,
                    base_url,
                    data,
                    content_type=False,
                    extra_params=False):
        result = {'values': dict()}
        try:
            response = requests.get(base_url, params=data)
            response.raise_for_status()
            if content_type == 'json':
                result['values'] = response.json()
            elif content_type in ('image', 'pdf'):
                result['values'] = base64.b64encode(response.content)
            else:
                result['values'] = response.content
        except requests.exceptions.HTTPError as e:
            result['error'] = e.response.content
        except requests.exceptions.ConnectionError as e:
            result['error'] = str(e)
        return result

    def _find_document_data_from_url(self, url):
        url_obj = urls.url_parse(url)
        if url_obj.ascii_host == 'youtu.be':
            return ('youtube', url_obj.path[1:] if url_obj.path else False)
        elif url_obj.ascii_host in ('youtube.com', 'www.youtube.com',
                                    'm.youtube.com'):
            v_query_value = url_obj.decode_query().get('v')
            if v_query_value:
                return ('youtube', v_query_value)
            split_path = url_obj.path.split('/')
            if len(split_path) >= 3 and split_path[1] in ('v', 'embed'):
                return ('youtube', split_path[2])

        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['website'].get_current_website(
        ).website_slide_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}
        items = fetch_res['values'].get('items')
        if not items:
            return {'error': _('Please enter valid Youtube or Google Doc URL')}
        youtube_values = items[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'],
                'mime_type':
                False,
            })
        return {'values': values}

    @api.model
    def _parse_google_document(self, document_id, only_preview_fields):
        def get_slide_type(vals):
            # TDE FIXME: WTF ??
            slide_type = 'presentation'
            if vals.get('image'):
                image = Image.open(io.BytesIO(base64.b64decode(vals['image'])))
                width, height = image.size
                if height > width:
                    return 'document'
            return slide_type

        # Google drive doesn't use a simple API key to access the data, but requires an access
        # token. However, this token is generated in module google_drive, which is not in the
        # dependencies of website_slides. We still keep the 'key' parameter just in case, but that
        # is probably useless.
        params = {}
        params['projection'] = 'BASIC'
        if 'google.drive.config' in self.env:
            access_token = self.env['google.drive.config'].get_access_token()
            if access_token:
                params['access_token'] = access_token
        if not params.get('access_token'):
            params['key'] = self.env['website'].get_current_website(
            ).website_slide_google_app_key

        fetch_res = self._fetch_data(
            'https://www.googleapis.com/drive/v2/files/%s' % document_id,
            params, "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['slide_type'] = get_slide_type(values)
            if 'exportLinks' in google_values:
                values['datas'] = self._fetch_data(
                    google_values['exportLinks']['application/pdf'],
                    params,
                    'pdf',
                    extra_params=True)['values']
                # Content indexing
                if google_values['exportLinks'].get('text/plain'):
                    values['index_content'] = self._fetch_data(
                        google_values['exportLinks']['text/plain'],
                        params,
                        extra_params=True)['values']
                elif google_values['exportLinks'].get('text/csv'):
                    values['index_content'] = self._fetch_data(
                        google_values['exportLinks']['text/csv'],
                        params,
                        extra_params=True)['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}

    def _default_website_meta(self):
        res = super(Slide, self)._default_website_meta()
        res['default_opengraph']['og:title'] = res['default_twitter'][
            'twitter:title'] = self.name
        res['default_opengraph']['og:description'] = res['default_twitter'][
            'twitter:description'] = self.description
        res['default_opengraph']['og:image'] = res['default_twitter'][
            'twitter:image'] = "/web/image/slide.slide/%s/image" % (self.id)
        res['default_meta_description'] = self.description
        return res
class ProductPublicCategory(models.Model):
    _inherit = "product.public.category"

    website_description = fields.Html(
        sanitize_attributes=False, translate=html_translate
    )
class LastMovementProductReport(models.Model):
	_name = 'last.movement.product.report'
	_description = 'Last Movement Product Report'


	# uncomment if using tracking modules
	# _inherit = ['mail.thread', 'mail.activity.mixin']

	_auto = False

	product_id = fields.Many2one('product.product', string="Product", readonly=True)
	last_purchase_id = fields.Many2one('purchase.order', string="Last Purchase", readonly=True,compute="_compute_last")
	last_sale_id = fields.Many2one('sale.order', string="Last Sale", readonly=True,compute="_compute_last")
	days_diff_po = fields.Integer("Days Diff PO", readonly=True)
	days_diff_so = fields.Integer("Days Diff SO", readonly=True)
	active = fields.Boolean("Active", readonly=True)


	product_tmpl_id = fields.Many2one('product.template', string="Product Templat", readonly=True)

	immediately_usable_qty = fields.Float(related="product_id.immediately_usable_qty")
	uom_id = fields.Many2one(related="product_id.uom_id")
	availability = fields.Html(string="Availability", compute="_compute_availability")


	def _item_availability(self, locs):
		self.ensure_one()
		statuses = []
		Quant = self.env['stock.quant']
		for loc in locs:
			product_context = self.with_context(location=loc.id).product_id
			# _logger.critical((self.product_id.name,product_context.qty_available, product_context.outgoing_qty, product_context.immediately_usable_qty))
			
			prodquant = Quant.search([('location_id','=',loc.id), ('product_id','=',product_context.id)])
			# _logger.warning(prodquant.mapped('reserved_quantity'))
			reserved = sum(prodquant.mapped('reserved_quantity'))
			stock = product_context.immediately_usable_qty - reserved
			if stock>0:
				statuses.append("%s = %s" % (loc.display_name, stock))
		return "<br/>".join(statuses)


	def _compute_availability(self):
		locs = self.env['stock.location'].search([('usage','=','internal'),('scrap_location','=',False)])
		for rec in self:
			item_availability = rec._item_availability(locs)
			rec.availability = item_availability


	# so_line_ids = fields.One2many('sale.order.line', 'product_id', string="Sale Item", domain=[('state','in',['done'])], readonly=True)


	def _compute_last(self):
		for rec in self:
			self.update({
				'last_sale_id':False,
				'last_purchase_id':False
				})
	
	def _select(self):
		query = """
			WITH so AS (
				select 
					sol.id
					,sol.order_id
					,so.state
					,so.date_order
					,sol.product_id
					,DATE_PART('day', now() - so.date_order) as days_diff
				FROM sale_order_line sol
				JOIN sale_order so on so.id = sol.order_id
				WHERE so.state in ('done')
			), po AS (
				select
					po.id
					,po.date_order
					,pol.product_id
					,DATE_PART('day', now() - po.date_order) as days_diff
				FROM purchase_order_line AS pol
				JOIN purchase_order AS po ON po.id = pol.order_id
				WHERE po.state in ('done')
			)

			SELECT 
				p2.id
				,p2.product_id
				,p2.active
				,p2.product_tmpl_id
				,(CASE WHEN p2.days_diff_so IS NULL THEN 0 ELSE p2.days_diff_so END) AS days_diff_so
				,(CASE WHEN p2.days_diff_po IS NULL THEN 0 ELSE p2.days_diff_po END) AS days_diff_po
			FROM (
				SELECT 
					pp.id
					,pp.id AS product_id	
					,pp.product_tmpl_id as product_tmpl_id
					,pp.active
					,MIN(so.days_diff) as days_diff_so
					,MIN(po.days_diff) as days_diff_po
				FROM product_product AS pp
				LEFT JOIN  so ON so.product_id = pp.id
				LEFT JOIN po ON po.product_id = pp.id
				GROUP BY
					pp.id
					,pp.product_tmpl_id
					,pp.active
			) AS p2
		"""
		return query
	
	
	@api.model_cr
	def init(self):
		tools.drop_view_if_exists(self.env.cr, self._table)
		self.env.cr.execute("""CREATE or REPLACE VIEW %s as (
			%s
			)""" % (self._table, self._select()))
Example #11
0
class HouseTask(models.Model):
    _name = "house.task"
    _description = "House Task"

    DEFAULT_PYTHON_CODE = """# Available variables:
    #  - env: Odoo Environment on which the action is triggered
    #  - model: Odoo Model of the record on which the action is triggered; is a void recordset
    #  - record: record on which the action is triggered; may be be void
    #  - records: recordset of all records on which the action is triggered in multi-mode; may be void
    #  - time, datetime, dateutil, timezone: useful Python libraries
    #  - log: log(message, level='info'): logging function to record debug information in ir.logging table
    #  - Warning: Warning Exception to use with raise
    # To return the next assigned user, assign: next = res.user record.\n\n\n\n"""

    name = fields.Char(required=True)
    description = fields.Html()
    active = fields.Boolean(default=True)
    user_ids = fields.Many2many(
        comodel_name="res.users",
        string="Users",
        required=True,
    )
    period_min = fields.Integer(
        string="Days to Next",
        required=True,
        help="The minimum days until next turn is required.",
    )
    period_max = fields.Integer(
        string="Days to Due",
        required=True,
        help="The number of days until next turn is considered late.",
    )
    python_code = fields.Text(
        string='Python Code',
        groups='base.group_system',
        default=DEFAULT_PYTHON_CODE,
        help="Write Python code that will recide the next user assigned to"
        "the task.",
    )
    code_prefix = fields.Char(string="Prefix for Task Turns", )
    sequence_id = fields.Many2one(
        comodel_name="ir.sequence",
        string="Task Sequence",
        help="This field contains the information related to the numbering "
        "of the turns of this task.",
        copy=False,
        readonly=True,
    )
    _sql_constraints = [
        ('name_uniq', 'unique (name)', "Name must be unique"),
    ]

    @api.model
    def _prepare_ir_sequence(self, prefix):
        """Prepare the vals for creating the sequence
        :param prefix: a string with the prefix of the sequence.
        :return: a dict with the values.
        """
        vals = {
            "name": "Housekeeping Task " + prefix,
            "code": "house.task.turn - " + prefix,
            "padding": 5,
            "prefix": prefix,
            "company_id": False,
        }
        return vals

    def write(self, vals):
        prefix = vals.get("code_prefix")
        if prefix:
            for rec in self:
                if rec.sequence_id:
                    rec.sudo().sequence_id.prefix = prefix
                else:
                    seq_vals = self._prepare_ir_sequence(prefix)
                    rec.sequence_id = self.env["ir.sequence"].create(seq_vals)
        return super().write(vals)

    @api.model
    def create(self, vals):
        prefix = vals.get("code_prefix")
        if prefix:
            seq_vals = self._prepare_ir_sequence(prefix)
            sequence = self.env["ir.sequence"].create(seq_vals)
            vals["sequence_id"] = sequence.id
        return super().create(vals)

    def _evaluate_python_code(self):
        eval_ctx = {'rec': self, 'env': self.env}
        try:
            safe_eval(self.python_code,
                      mode="exec",
                      nocopy=True,
                      globals_dict=eval_ctx)
        except Exception as error:
            raise UserError(_("Error evaluating python code.\n %s") % error)
        return eval_ctx.get('next')

    def generate(self):
        turn_model = self.env['house.task.turn']
        for rec in self:
            pending = turn_model.search([('house_task_id', '=', rec.id),
                                         ('state', '=', 'pending')])
            if pending:
                continue

            last = turn_model.search([('house_task_id', '=', rec.id),
                                      ('state', '=', 'done')],
                                     order='date_done desc',
                                     limit=1)
            if last and fields.Date.from_string(last.date_done) + timedelta(
                    days=rec.period_min) > date.today():
                continue

            assigned_to = rec._evaluate_python_code()
            if not assigned_to:
                continue

            turn_model.create({
                'house_task_id':
                rec.id,
                'date_due':
                fields.Date.from_string(last.date_done) +
                timedelta(days=rec.period_max),
                'user_id':
                assigned_to.id,
            })
Example #12
0
class FleetVehicle(models.Model):
    _inherit = ['mail.thread', 'mail.activity.mixin']
    _name = 'fleet.vehicle'
    _description = 'Vehicle'
    _order = 'license_plate asc, acquisition_date asc'

    def _get_default_state(self):
        state = self.env.ref('fleet.fleet_vehicle_state_registered', raise_if_not_found=False)
        return state if state and state.id else False

    name = fields.Char(compute="_compute_vehicle_name", store=True)
    description = fields.Html("Vehicle Description", help="Add a note about this vehicle")
    active = fields.Boolean('Active', default=True, tracking=True)
    manager_id = fields.Many2one(
        'res.users', 'Fleet Manager',
        compute='_compute_manager_id', store=True, readonly=False,
        domain=lambda self: [('groups_id', 'in', self.env.ref('fleet.fleet_group_manager').id)],
    )
    company_id = fields.Many2one(
        'res.company', 'Company',
        default=lambda self: self.env.company,
    )
    currency_id = fields.Many2one('res.currency', related='company_id.currency_id')
    country_id = fields.Many2one('res.country', related='company_id.country_id')
    country_code = fields.Char(related='country_id.code')
    license_plate = fields.Char(tracking=True,
        help='License plate number of the vehicle (i = plate number for a car)')
    vin_sn = fields.Char('Chassis Number', help='Unique number written on the vehicle motor (VIN/SN number)', copy=False)
    trailer_hook = fields.Boolean(default=False, string='Trailer Hitch', compute='_compute_model_fields', store=True, readonly=False)
    driver_id = fields.Many2one('res.partner', 'Driver', tracking=True, help='Driver address of the vehicle', copy=False)
    future_driver_id = fields.Many2one('res.partner', 'Future Driver', tracking=True, help='Next Driver Address of the vehicle', copy=False, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
    model_id = fields.Many2one('fleet.vehicle.model', 'Model',
        tracking=True, required=True, help='Model of the vehicle')

    brand_id = fields.Many2one('fleet.vehicle.model.brand', 'Brand', related="model_id.brand_id", store=True, readonly=False)
    log_drivers = fields.One2many('fleet.vehicle.assignation.log', 'vehicle_id', string='Assignment Logs')
    log_services = fields.One2many('fleet.vehicle.log.services', 'vehicle_id', 'Services Logs')
    log_contracts = fields.One2many('fleet.vehicle.log.contract', 'vehicle_id', 'Contracts')
    contract_count = fields.Integer(compute="_compute_count_all", string='Contract Count')
    service_count = fields.Integer(compute="_compute_count_all", string='Services')
    odometer_count = fields.Integer(compute="_compute_count_all", string='Odometer')
    history_count = fields.Integer(compute="_compute_count_all", string="Drivers History Count")
    next_assignation_date = fields.Date('Assignment Date', help='This is the date at which the car will be available, if not set it means available instantly')
    acquisition_date = fields.Date('Immatriculation Date', required=False,
        default=fields.Date.today, help='Date when the vehicle has been immatriculated')
    write_off_date = fields.Date('Cancellation Date', tracking=True, help="Date when the vehicle's license plate has been cancelled/removed.")
    first_contract_date = fields.Date(string="First Contract Date", default=fields.Date.today)
    color = fields.Char(help='Color of the vehicle', compute='_compute_model_fields', store=True, readonly=False)
    state_id = fields.Many2one('fleet.vehicle.state', 'State',
        default=_get_default_state, group_expand='_read_group_stage_ids',
        tracking=True,
        help='Current state of the vehicle', ondelete="set null")
    location = fields.Char(help='Location of the vehicle (garage, ...)')
    seats = fields.Integer('Seats Number', help='Number of seats of the vehicle', compute='_compute_model_fields', store=True, readonly=False)
    model_year = fields.Char('Model Year', help='Year of the model', compute='_compute_model_fields', store=True, readonly=False)
    doors = fields.Integer('Doors Number', help='Number of doors of the vehicle', compute='_compute_model_fields', store=True, readonly=False)
    tag_ids = fields.Many2many('fleet.vehicle.tag', 'fleet_vehicle_vehicle_tag_rel', 'vehicle_tag_id', 'tag_id', 'Tags', copy=False)
    odometer = fields.Float(compute='_get_odometer', inverse='_set_odometer', string='Last Odometer',
        help='Odometer measure of the vehicle at the moment of this log')
    odometer_unit = fields.Selection([
        ('kilometers', 'km'),
        ('miles', 'mi')
        ], 'Odometer Unit', default='kilometers', help='Unit of the odometer ', required=True)
    transmission = fields.Selection(
        [('manual', 'Manual'), ('automatic', 'Automatic')], 'Transmission', help='Transmission Used by the vehicle',
        compute='_compute_model_fields', store=True, readonly=False)
    fuel_type = fields.Selection(FUEL_TYPES, 'Fuel Type', help='Fuel Used by the vehicle', compute='_compute_model_fields', store=True, readonly=False)
    horsepower = fields.Integer(compute='_compute_model_fields', store=True, readonly=False)
    horsepower_tax = fields.Float('Horsepower Taxation', compute='_compute_model_fields', store=True, readonly=False)
    power = fields.Integer('Power', help='Power in kW of the vehicle', compute='_compute_model_fields', store=True, readonly=False)
    co2 = fields.Float('CO2 Emissions', help='CO2 emissions of the vehicle', compute='_compute_model_fields', store=True, readonly=False)
    co2_standard = fields.Char(compute='_compute_model_fields', store=True, readonly=False)
    image_128 = fields.Image(related='model_id.image_128', readonly=True)
    contract_renewal_due_soon = fields.Boolean(compute='_compute_contract_reminder', search='_search_contract_renewal_due_soon',
        string='Has Contracts to renew')
    contract_renewal_overdue = fields.Boolean(compute='_compute_contract_reminder', search='_search_get_overdue_contract_reminder',
        string='Has Contracts Overdue')
    contract_renewal_name = fields.Text(compute='_compute_contract_reminder', string='Name of contract to renew soon')
    contract_renewal_total = fields.Text(compute='_compute_contract_reminder', string='Total of contracts due or overdue minus one')
    contract_state = fields.Selection(
        [('futur', 'Incoming'),
         ('open', 'In Progress'),
         ('expired', 'Expired'),
         ('closed', 'Closed')
        ], string='Last Contract State', compute='_compute_contract_reminder', required=False)
    car_value = fields.Float(string="Catalog Value (VAT Incl.)", help='Value of the bought vehicle')
    net_car_value = fields.Float(string="Purchase Value", help="Purchase value of the vehicle")
    residual_value = fields.Float()
    plan_to_change_car = fields.Boolean(related='driver_id.plan_to_change_car', store=True, readonly=False)
    plan_to_change_bike = fields.Boolean(related='driver_id.plan_to_change_bike', store=True, readonly=False)
    vehicle_type = fields.Selection(related='model_id.vehicle_type')
    frame_type = fields.Selection([('diamant', 'Diamant'), ('trapez', 'Trapez'), ('wave', 'Wave')], help="Frame type of the bike")
    electric_assistance = fields.Boolean(compute='_compute_model_fields', store=True, readonly=False)
    frame_size = fields.Float()

    @api.depends('model_id')
    def _compute_model_fields(self):
        '''
        Copies all the related fields from the model to the vehicle
        '''
        model_values = dict()
        for vehicle in self.filtered('model_id'):
            if vehicle.model_id.id in model_values:
                write_vals = model_values[vehicle.model_id.id]
            else:
                # copy if value is truthy
                write_vals = {MODEL_FIELDS_TO_VEHICLE[key]: vehicle.model_id[key] for key in MODEL_FIELDS_TO_VEHICLE\
                    if vehicle.model_id[key]}
                model_values[vehicle.model_id.id] = write_vals
            vehicle.write(write_vals)

    @api.depends('model_id.brand_id.name', 'model_id.name', 'license_plate')
    def _compute_vehicle_name(self):
        for record in self:
            record.name = (record.model_id.brand_id.name or '') + '/' + (record.model_id.name or '') + '/' + (record.license_plate or _('No Plate'))

    def _get_odometer(self):
        FleetVehicalOdometer = self.env['fleet.vehicle.odometer']
        for record in self:
            vehicle_odometer = FleetVehicalOdometer.search([('vehicle_id', '=', record.id)], limit=1, order='value desc')
            if vehicle_odometer:
                record.odometer = vehicle_odometer.value
            else:
                record.odometer = 0

    def _set_odometer(self):
        for record in self:
            if record.odometer:
                date = fields.Date.context_today(record)
                data = {'value': record.odometer, 'date': date, 'vehicle_id': record.id}
                self.env['fleet.vehicle.odometer'].create(data)

    def _compute_count_all(self):
        Odometer = self.env['fleet.vehicle.odometer']
        LogService = self.env['fleet.vehicle.log.services']
        LogContract = self.env['fleet.vehicle.log.contract']
        for record in self:
            record.odometer_count = Odometer.search_count([('vehicle_id', '=', record.id)])
            record.service_count = LogService.search_count([('vehicle_id', '=', record.id), ('active', '=', record.active)])
            record.contract_count = LogContract.search_count([('vehicle_id', '=', record.id), ('state', '!=', 'closed'), ('active', '=', record.active)])
            record.history_count = self.env['fleet.vehicle.assignation.log'].search_count([('vehicle_id', '=', record.id)])

    @api.depends('log_contracts')
    def _compute_contract_reminder(self):
        params = self.env['ir.config_parameter'].sudo()
        delay_alert_contract = int(params.get_param('hr_fleet.delay_alert_contract', default=30))
        for record in self:
            overdue = False
            due_soon = False
            total = 0
            name = ''
            state = ''
            for element in record.log_contracts:
                if element.state in ('open', 'expired') and element.expiration_date:
                    current_date_str = fields.Date.context_today(record)
                    due_time_str = element.expiration_date
                    current_date = fields.Date.from_string(current_date_str)
                    due_time = fields.Date.from_string(due_time_str)
                    diff_time = (due_time - current_date).days
                    if diff_time < 0:
                        overdue = True
                        total += 1
                    if diff_time < delay_alert_contract:
                        due_soon = True
                        total += 1
                    if overdue or due_soon:
                        log_contract = self.env['fleet.vehicle.log.contract'].search([
                            ('vehicle_id', '=', record.id),
                            ('state', 'in', ('open', 'expired'))
                            ], limit=1, order='expiration_date asc')
                        if log_contract:
                            # we display only the name of the oldest overdue/due soon contract
                            name = log_contract.name
                            state = log_contract.state

            record.contract_renewal_overdue = overdue
            record.contract_renewal_due_soon = due_soon
            record.contract_renewal_total = total - 1  # we remove 1 from the real total for display purposes
            record.contract_renewal_name = name
            record.contract_state = state

    def _get_analytic_name(self):
        # This function is used in fleet_account and is overrided in l10n_be_hr_payroll_fleet
        return self.license_plate or _('No plate')

    def _search_contract_renewal_due_soon(self, operator, value):
        params = self.env['ir.config_parameter'].sudo()
        delay_alert_contract = int(params.get_param('hr_fleet.delay_alert_contract', default=30))
        res = []
        assert operator in ('=', '!=', '<>') and value in (True, False), 'Operation not supported'
        if (operator == '=' and value is True) or (operator in ('<>', '!=') and value is False):
            search_operator = 'in'
        else:
            search_operator = 'not in'
        today = fields.Date.context_today(self)
        datetime_today = fields.Datetime.from_string(today)
        limit_date = fields.Datetime.to_string(datetime_today + relativedelta(days=+delay_alert_contract))
        res_ids = self.env['fleet.vehicle.log.contract'].search([
            ('expiration_date', '>', today),
            ('expiration_date', '<', limit_date),
            ('state', 'in', ['open', 'expired'])
        ]).mapped('id')
        res.append(('id', search_operator, res_ids))
        return res

    def _search_get_overdue_contract_reminder(self, operator, value):
        res = []
        assert operator in ('=', '!=', '<>') and value in (True, False), 'Operation not supported'
        if (operator == '=' and value is True) or (operator in ('<>', '!=') and value is False):
            search_operator = 'in'
        else:
            search_operator = 'not in'
        today = fields.Date.context_today(self)
        res_ids = self.env['fleet.vehicle.log.contract'].search([
            ('expiration_date', '!=', False),
            ('expiration_date', '<', today),
            ('state', 'in', ['open', 'expired'])
        ]).mapped('id')
        res.append(('id', search_operator, res_ids))
        return res

    @api.model
    def create(self, vals):
        # Fleet administrator may not have rights to create the plan_to_change_car value when the driver_id is a res.user
        # This trick is used to prevent access right error.
        ptc_value = 'plan_to_change_car' in vals.keys() and {'plan_to_change_car': vals.pop('plan_to_change_car')}
        res = super(FleetVehicle, self).create(vals)
        if ptc_value:
            res.sudo().write(ptc_value)
        if 'driver_id' in vals and vals['driver_id']:
            res.create_driver_history(vals)
        if 'future_driver_id' in vals and vals['future_driver_id']:
            state_waiting_list = self.env.ref('fleet.fleet_vehicle_state_waiting_list', raise_if_not_found=False)
            states = res.mapped('state_id').ids
            if not state_waiting_list or state_waiting_list.id not in states:
                future_driver = self.env['res.partner'].browse(vals['future_driver_id'])
                if self.vehicle_type == 'bike':
                    future_driver.sudo().write({'plan_to_change_bike': True})
                if self.vehicle_type == 'car':
                    future_driver.sudo().write({'plan_to_change_car': True})
        return res

    def write(self, vals):
        if 'driver_id' in vals and vals['driver_id']:
            driver_id = vals['driver_id']
            for vehicle in self.filtered(lambda v: v.driver_id.id != driver_id):
                vehicle.create_driver_history(vals)
                if vehicle.driver_id:
                    vehicle.activity_schedule(
                        'mail.mail_activity_data_todo',
                        user_id=vehicle.manager_id.id or self.env.user.id,
                        note=_('Specify the End date of %s') % vehicle.driver_id.name)

        if 'future_driver_id' in vals and vals['future_driver_id']:
            state_waiting_list = self.env.ref('fleet.fleet_vehicle_state_waiting_list', raise_if_not_found=False)
            states = self.mapped('state_id').ids if 'state_id' not in vals else [vals['state_id']]
            if not state_waiting_list or state_waiting_list.id not in states:
                future_driver = self.env['res.partner'].browse(vals['future_driver_id'])
                if self.vehicle_type == 'bike':
                    future_driver.sudo().write({'plan_to_change_bike': True})
                if self.vehicle_type == 'car':
                    future_driver.sudo().write({'plan_to_change_car': True})

        res = super(FleetVehicle, self).write(vals)
        return res

    def _get_driver_history_data(self, vals):
        self.ensure_one()
        return {
            'vehicle_id': self.id,
            'driver_id': vals['driver_id'],
            'date_start': fields.Date.today(),
        }

    def create_driver_history(self, vals):
        for vehicle in self:
            self.env['fleet.vehicle.assignation.log'].create(
                vehicle._get_driver_history_data(vals),
            )

    def action_accept_driver_change(self):
        # Find all the vehicles for which the driver is the future_driver_id
        # remove their driver_id and close their history using current date
        vehicles = self.search([('driver_id', 'in', self.mapped('future_driver_id').ids)])
        vehicles.write({'driver_id': False})

        for vehicle in self:
            if vehicle.vehicle_type == 'bike':
                vehicle.future_driver_id.sudo().write({'plan_to_change_bike': False})
            if vehicle.vehicle_type == 'car':
                vehicle.future_driver_id.sudo().write({'plan_to_change_car': False})
            vehicle.driver_id = vehicle.future_driver_id
            vehicle.future_driver_id = False

    def toggle_active(self):
        self.env['fleet.vehicle.log.contract'].with_context(active_test=False).search([('vehicle_id', 'in', self.ids)]).toggle_active()
        self.env['fleet.vehicle.log.services'].with_context(active_test=False).search([('vehicle_id', 'in', self.ids)]).toggle_active()
        super(FleetVehicle, self).toggle_active()

    @api.model
    def _read_group_stage_ids(self, stages, domain, order):
        return self.env['fleet.vehicle.state'].search([], order=order)

    @api.model
    def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
        if 'co2' in fields:
            fields.remove('co2')
        return super(FleetVehicle, self).read_group(domain, fields, groupby, offset, limit, orderby, lazy)

    @api.model
    def _name_search(self, name, args=None, operator='ilike', limit=100, name_get_uid=None):
        args = args or []
        if operator == 'ilike' and not (name or '').strip():
            domain = []
        else:
            domain = ['|', ('name', operator, name), ('driver_id.name', operator, name)]
        return self._search(expression.AND([domain, args]), limit=limit, access_rights_uid=name_get_uid)

    def return_action_to_open(self):
        """ This opens the xml view specified in xml_id for the current vehicle """
        self.ensure_one()
        xml_id = self.env.context.get('xml_id')
        if xml_id:

            res = self.env['ir.actions.act_window']._for_xml_id('fleet.%s' % xml_id)
            res.update(
                context=dict(self.env.context, default_vehicle_id=self.id, group_by=False),
                domain=[('vehicle_id', '=', self.id)]
            )
            return res
        return False

    def act_show_log_cost(self):
        """ This opens log view to view and add new log for this vehicle, groupby default to only show effective costs
            @return: the costs log view
        """
        self.ensure_one()
        copy_context = dict(self.env.context)
        copy_context.pop('group_by', None)
        res = self.env['ir.actions.act_window']._for_xml_id('fleet.fleet_vehicle_costs_action')
        res.update(
            context=dict(copy_context, default_vehicle_id=self.id, search_default_parent_false=True),
            domain=[('vehicle_id', '=', self.id)]
        )
        return res

    def _track_subtype(self, init_values):
        self.ensure_one()
        if 'driver_id' in init_values:
            return self.env.ref('fleet.mt_fleet_driver_updated')
        return super(FleetVehicle, self)._track_subtype(init_values)

    def open_assignation_logs(self):
        self.ensure_one()
        return {
            'type': 'ir.actions.act_window',
            'name': 'Assignment Logs',
            'view_mode': 'tree',
            'res_model': 'fleet.vehicle.assignation.log',
            'domain': [('vehicle_id', '=', self.id)],
            'context': {'default_driver_id': self.driver_id.id, 'default_vehicle_id': self.id}
        }
Example #13
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']._get(res.pop('model')).id
        return res

    name = fields.Char('Name')
    model_id = fields.Many2one(
        'ir.model',
        'Applies to',
        help="The type of document this template can be used with")
    model = fields.Char('Related Document Model',
                        related='model_id.model',
                        index=True,
                        store=True,
                        readonly=True)
    lang = fields.Char(
        'Language',
        help=
        "Optional translation language (ISO code) to select when sending out an email. "
        "If not set, the english version will be used. "
        "This should usually be a placeholder expression "
        "that provides the appropriate language, e.g. "
        "${object.partner_id.lang}.",
        placeholder="${object.partner_id.lang}")
    user_signature = fields.Boolean(
        'Add Signature',
        help=
        "If checked, the user's signature will be appended to the text version "
        "of the message")
    subject = fields.Char('Subject',
                          translate=True,
                          help="Subject (placeholders may be used here)")
    email_from = fields.Char(
        'From',
        help=
        "Sender address (placeholders may be used here). If not set, the default "
        "value will be the author's email alias if configured, or email address."
    )
    use_default_to = fields.Boolean(
        'Default recipients',
        help="Default recipients of the record:\n"
        "- partner (using id on a partner or the partner_id field) OR\n"
        "- email (using email_from or email field)")
    email_to = fields.Char(
        'To (Emails)',
        help=
        "Comma-separated recipient addresses (placeholders may be used here)")
    partner_to = fields.Char(
        'To (Partners)',
        oldname='email_recipients',
        help=
        "Comma-separated ids of recipient partners (placeholders may be used here)"
    )
    email_cc = fields.Char(
        'Cc', help="Carbon copy recipients (placeholders may be used here)")
    reply_to = fields.Char(
        'Reply-To',
        help="Preferred response address (placeholders may be used here)")
    mail_server_id = fields.Many2one(
        'ir.mail_server',
        'Outgoing Mail Server',
        readonly=False,
        help=
        "Optional preferred server for outgoing mails. If not set, the highest "
        "priority one will be used.")
    body_html = fields.Html('Body', translate=True, sanitize=False)
    report_name = fields.Char(
        'Report Filename',
        translate=True,
        help=
        "Name to use for the generated report file (may contain placeholders)\n"
        "The extension can be omitted and will then come from the report type."
    )
    report_template = fields.Many2one('ir.actions.report',
                                      'Optional report to print and attach')
    ref_ir_act_window = fields.Many2one(
        'ir.actions.act_window',
        'Sidebar action',
        readonly=True,
        copy=False,
        help="Sidebar action to make this template available on records "
        "of the related document model")
    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."
    )
    scheduled_date = fields.Char(
        'Scheduled Date',
        help=
        "If set, the queue manager will send the email after the date. If not set, the email will be send as soon as possible. Jinja2 placeholders may be used."
    )

    @api.onchange('model_id')
    def onchange_model_id(self):
        # TDE CLEANME: should'nt it be a stored related ?
        if self.model_id:
            self.model = self.model_id.model
        else:
            self.model = False

    def build_expression(self, field_name, sub_field_name, null_value):
        """Returns a placeholder expression for use in a template field,
        based on the values provided in the placeholder assistant.

        :param field_name: main field name
        :param sub_field_name: sub field name (M2O)
        :param null_value: default value if the target value is empty
        :return: final placeholder expression """
        expression = ''
        if field_name:
            expression = "${object." + field_name
            if sub_field_name:
                expression += "." + sub_field_name
            if null_value:
                expression += " or '''%s'''" % null_value
            expression += "}"
        return expression

    @api.onchange('model_object_field', 'sub_model_object_field', 'null_value')
    def onchange_sub_model_object_value_field(self):
        if self.model_object_field:
            if self.model_object_field.ttype in [
                    'many2one', 'one2many', 'many2many'
            ]:
                model = self.env['ir.model']._get(
                    self.model_object_field.relation)
                if model:
                    self.sub_object = model.id
                    self.copyvalue = self.build_expression(
                        self.model_object_field.name,
                        self.sub_model_object_field
                        and self.sub_model_object_field.name or False,
                        self.null_value or False)
            else:
                self.sub_object = False
                self.sub_model_object_field = False
                self.copyvalue = self.build_expression(
                    self.model_object_field.name, False, self.null_value
                    or False)
        else:
            self.sub_object = False
            self.copyvalue = False
            self.sub_model_object_field = False
            self.null_value = False

    @api.multi
    def unlink(self):
        self.unlink_action()
        return super(MailTemplate, self).unlink()

    @api.multi
    def copy(self, default=None):
        default = dict(default or {}, name=_("%s (copy)") % self.name)
        return super(MailTemplate, self).copy(default=default)

    @api.multi
    def unlink_action(self):
        for template in self:
            if template.ref_ir_act_window:
                template.ref_ir_act_window.sudo().unlink()
            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',
            })
            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'].sudo().get_param(
            'web.base.url')
        base = urls.url_parse(base_url)

        def _process_link(url):
            new_url = urls.url_parse(url)
            if new_url.scheme and new_url.netloc:
                return url
            return new_url.replace(scheme=base.scheme,
                                   netloc=base.netloc).to_url()

        # check all nodes, replace :
        # - img src -> check URL
        # - a href -> check URL
        for node in root.iter():
            if node.tag == 'a' and node.get('href'):
                node.set('href', _process_link(node.get('href')))
            elif node.tag == 'img' and not node.get('src',
                                                    'data').startswith('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``: Model of the current user
         - ``object``: record of the document record this mail is related to
         - ``context``: the context passed to the mail composition wizard

        :param str template_txt: the template text to render
        :param str model: model name of the document record this mail is related to.
        :param int res_ids: list of ids of document records those mails are related to.
        """
        multi_mode = True
        if isinstance(res_ids, pycompat.integer_types):
            multi_mode = False
            res_ids = [res_ids]

        results = dict.fromkeys(res_ids, u"")

        # try to load the template
        try:
            mako_env = mako_safe_template_env if self.env.context.get(
                'safe') else mako_template_env
            template = mako_env.from_string(tools.ustr(template_txt))
        except Exception:
            _logger.info("Failed to load template %r",
                         template_txt,
                         exc_info=True)
            return multi_mode and results or results[res_ids[0]]

        # prepare template variables
        records = self.env[model].browse(
            it for it in res_ids if it)  # filter to avoid browsing [None]
        res_to_rec = dict.fromkeys(res_ids, None)
        for record in records:
            res_to_rec[record.id] = record
        variables = {
            'format_date':
            lambda date, format=False, context=self._context: format_date(
                self.env, date, format),
            'format_tz':
            lambda dt, tz=False, format=False, context=self._context:
            format_tz(self.env, dt, tz, format),
            'user':
            self.env.user,
            'ctx':
            self._context,  # context kw would clash with mako internals
        }
        for res_id, record in pycompat.items(res_to_rec):
            variables['object'] = record
            try:
                render_result = template.render(variables)
            except Exception:
                _logger.info("Failed to render template %r using values %r" %
                             (template, variables),
                             exc_info=True)
                raise UserError(
                    _("Failed to render template %r using values %r") %
                    (template, variables))
            if render_result == u"False":
                render_result = u""
            results[res_id] = render_result

        if post_process:
            for res_id, result in pycompat.items(results):
                results[res_id] = self.render_post_process(result)

        return multi_mode and results or results[res_ids[0]]

    @api.multi
    def get_email_template(self, res_ids):
        multi_mode = True
        if isinstance(res_ids, pycompat.integer_types):
            res_ids = [res_ids]
            multi_mode = False

        if res_ids is None:
            res_ids = [None]
        results = dict.fromkeys(res_ids, False)

        if not self.ids:
            return results
        self.ensure_one()

        langs = self.render_template(self.lang, self.model, res_ids)
        for res_id, lang in pycompat.items(langs):
            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 pycompat.items(default_recipients):
                results[res_id].pop('partner_to', None)
                results[res_id].update(recipients)

        for res_id, values in pycompat.items(results):
            partner_ids = values.get('partner_ids', list())
            if self._context.get('tpl_partners_only'):
                mails = tools.email_split(values.pop(
                    'email_to', '')) + tools.email_split(
                        values.pop('email_cc', ''))
                for mail in mails:
                    partner_id = self.env['res.partner'].find_or_create(mail)
                    partner_ids.append(partner_id)
            partner_to = values.pop('partner_to', '')
            if partner_to:
                # placeholders could generate '', 3, 2 due to some empty field values
                tpl_partner_ids = [
                    int(pid) for pid in partner_to.split(',') if pid
                ]
                partner_ids += self.env['res.partner'].sudo().browse(
                    tpl_partner_ids).exists().ids
            results[res_id]['partner_ids'] = partner_ids
        return results

    @api.multi
    def generate_email(self, res_ids, fields=None):
        """Generates an email from the template for given the given model based on
        records given by res_ids.

        :param template_id: id of the template to render.
        :param res_id: id of the record to use for rendering the template (model
                       is taken from template definition)
        :returns: a dict containing all relevant fields for creating a new
                  mail.mail entry, with one extra key ``attachments``, in the
                  format [(report_name, data)] where data is base64 encoded.
        """
        self.ensure_one()
        multi_mode = True
        if isinstance(res_ids, pycompat.integer_types):
            res_ids = [res_ids]
            multi_mode = False
        if fields is None:
            fields = [
                'subject', 'body_html', 'email_from', 'email_to', 'partner_to',
                'email_cc', 'reply_to', 'scheduled_date'
            ]

        res_ids_to_templates = self.get_email_template(res_ids)

        # templates: res_id -> template; template -> res_ids
        templates_to_res_ids = {}
        for res_id, template in pycompat.items(res_ids_to_templates):
            templates_to_res_ids.setdefault(template, []).append(res_id)

        results = dict()
        for template, template_res_ids in pycompat.items(templates_to_res_ids):
            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 pycompat.items(
                        generated_field_values):
                    results.setdefault(res_id, dict())[field] = field_value
            # compute recipients
            if any(field in fields
                   for field in ['email_to', 'partner_to', 'email_cc']):
                results = template.generate_recipients(results,
                                                       template_res_ids)
            # update values for all res_ids
            for res_id in template_res_ids:
                values = results[res_id]
                # body: add user signature, sanitize
                if 'body_html' in fields and template.user_signature:
                    signature = self.env.user.signature
                    if signature:
                        values['body_html'] = tools.append_content_to_html(
                            values['body_html'], signature, plaintext=False)
                if values.get('body_html'):
                    values['body'] = tools.html_sanitize(values['body_html'])
                # technical settings
                values.update(
                    mail_server_id=template.mail_server_id.id or False,
                    auto_delete=template.auto_delete,
                    model=template.model,
                    res_id=res_id or False,
                    attachment_ids=[
                        attach.id for attach in template.attachment_ids
                    ],
                )

            # Add report in attachments: generate once for all template_res_ids
            if template.report_template:
                for res_id in template_res_ids:
                    attachments = []
                    report_name = self.render_template(template.report_name,
                                                       template.model, res_id)
                    report = template.report_template
                    report_service = report.report_name

                    if report.report_type not in ['qweb-html', 'qweb-pdf']:
                        raise UserError(
                            _('Unsupported report type %s found.') %
                            report.report_type)
                    result, format = report.render_qweb_pdf([res_id])

                    # TODO in trunk, change return format to binary to match message_post expected format
                    result = base64.b64encode(result)
                    if not report_name:
                        report_name = 'report.' + report_service
                    ext = "." + format
                    if not report_name.endswith(ext):
                        report_name += ext
                    attachments.append((report_name, result))
                    results[res_id]['attachments'] = attachments

        return multi_mode and results or results[res_ids[0]]

    @api.multi
    def send_mail(self,
                  res_id,
                  force_send=False,
                  raise_exception=False,
                  email_values=None):
        """Generates a new mail message for the given template and record,
           and schedules it for delivery through the ``mail`` module's scheduler.

           :param int res_id: id of the record to render the template with
                              (model is taken from the template)
           :param bool force_send: if True, the generated mail.message is
                immediately sent after being created, as if the scheduler
                was executed for this message only.
           :param dict email_values: if set, the generated mail.message is
                updated with given values dict
           :returns: id of the mail.message that was created
        """
        self.ensure_one()
        Mail = self.env['mail.mail']
        Attachment = self.env[
            'ir.attachment']  # TDE FIXME: should remove dfeault_type from context

        # create a mail_mail based on values, without attachments
        values = self.generate_email(res_id)
        values['recipient_ids'] = [
            (4, pid) for pid in values.get('partner_ids', list())
        ]
        values.update(email_values or {})
        attachment_ids = values.pop('attachment_ids', [])
        attachments = values.pop('attachments', [])
        # add a protection against void email_from
        if 'email_from' in values and not values.get('email_from'):
            values.pop('email_from')
        mail = Mail.create(values)

        # manage attachments
        for attachment in attachments:
            attachment_data = {
                'name': attachment[0],
                'datas_fname': attachment[0],
                'datas': attachment[1],
                '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 ?
Example #14
0
class HrPlanActivityType(models.Model):
    _name = 'hr.plan.activity.type'
    _description = 'Plan activity type'
    _rec_name = 'summary'

    activity_type_id = fields.Many2one(
        'mail.activity.type',
        'Activity Type',
        default=lambda self: self.env.ref('mail.mail_activity_data_todo'),
        domain=lambda self: [
            '|', ('res_model_id', '=', False),
            ('res_model_id', '=', self.env['ir.model']._get('hr.employee').id)
        ])
    summary = fields.Char('Summary')
    responsible = fields.Selection([('coach', 'Coach'), ('manager', 'Manager'),
                                    ('employee', 'Employee'),
                                    ('other', 'Other')],
                                   default='employee',
                                   string='Responsible',
                                   required=True)
    responsible_id = fields.Many2one(
        'res.users',
        'Responsible Person',
        help='Specific responsible of activity if not linked to the employee.')
    note = fields.Html('Note')

    @api.onchange('activity_type_id')
    def _onchange_activity_type_id(self):
        if self.activity_type_id and self.activity_type_id.summary and not self.summary:
            self.summary = self.activity_type_id.summary

    def get_responsible_id(self, employee):
        if self.responsible == 'coach':
            if not employee.coach_id:
                raise UserError(
                    _('Coach of employee %s is not set.') % employee.name)
            responsible = employee.coach_id.user_id
            if not responsible:
                raise UserError(
                    _('User of coach of employee %s is not set.') %
                    employee.name)
        elif self.responsible == 'manager':
            if not employee.parent_id:
                raise UserError(
                    _('Manager of employee %s is not set.') % employee.name)
            responsible = employee.parent_id.user_id
            if not responsible:
                raise UserError(
                    _('User of manager of employee %s is not set.') %
                    employee.name)
        elif self.responsible == 'employee':
            responsible = employee.user_id
            if not responsible:
                raise UserError(
                    _('User linked to employee %s is required.') %
                    employee.name)
        elif self.responsible == 'other':
            responsible = self.responsible_id
            if not responsible:
                raise UserError(
                    _('No specific user given on activity.') % employee.name)
        return responsible
Example #15
0
class Users(models.Model):
    """ User class. A res.users record models an OpenERP user and is different
        from an employee.

        res.users class now inherits from res.partner. The partner model is
        used to store the data related to the partner: lang, name, address,
        avatar, ... The user model is now dedicated to technical data.
    """
    _name = "res.users"
    _description = 'Users'
    _inherits = {'res.partner': 'partner_id'}
    _order = 'name, login'
    __uid_cache = defaultdict(dict)  # {dbname: {uid: password}}

    # User can write on a few of his own fields (but not his groups for example)
    SELF_WRITEABLE_FIELDS = [
        'signature', 'action_id', 'company_id', 'email', 'name', 'image',
        'image_medium', 'image_small', 'lang', 'tz'
    ]
    # User can read a few of his own fields
    SELF_READABLE_FIELDS = [
        'signature', 'company_id', 'login', 'email', 'name', 'image',
        'image_medium', 'image_small', 'lang', 'tz', 'tz_offset', 'groups_id',
        'partner_id', '__last_update', 'action_id'
    ]

    def _default_groups(self):
        default_user = self.env.ref('base.default_user',
                                    raise_if_not_found=False)
        return (default_user or self.env['res.users']).groups_id

    def _companies_count(self):
        return self.env['res.company'].sudo().search_count([])

    partner_id = fields.Many2one('res.partner',
                                 required=True,
                                 ondelete='restrict',
                                 auto_join=True,
                                 string='Related Partner',
                                 help='Partner-related data of the user')
    login = fields.Char(required=True, help="Used to log into the system")
    password = fields.Char(
        default='',
        invisible=True,
        copy=False,
        help=
        "Keep empty if you don't want the user to be able to connect on the system."
    )
    new_password = fields.Char(string='Set Password',
        compute='_compute_password', inverse='_inverse_password',
        help="Specify a value only when creating a user or if you're "\
             "changing the user's password, otherwise leave empty. After "\
             "a change of password, the user has to login again.")
    signature = fields.Html()
    active = fields.Boolean(default=True)
    action_id = fields.Many2one(
        'ir.actions.actions',
        string='Home Action',
        help=
        "If specified, this action will be opened at log on for this user, in addition to the standard menu."
    )
    groups_id = fields.Many2many('res.groups',
                                 'res_groups_users_rel',
                                 'uid',
                                 'gid',
                                 string='Groups',
                                 default=_default_groups)
    log_ids = fields.One2many('res.users.log',
                              'create_uid',
                              string='User log entries')
    login_date = fields.Datetime(related='log_ids.create_date',
                                 string='Latest connection')
    share = fields.Boolean(
        compute='_compute_share',
        compute_sudo=True,
        string='Share User',
        store=True,
        help=
        "External user with limited access, created only for the purpose of sharing data."
    )
    companies_count = fields.Integer(compute='_compute_companies_count',
                                     string="Number of Companies",
                                     default=_companies_count)
    tz_offset = fields.Char(compute='_compute_tz_offset',
                            string='Timezone offset',
                            invisible=True)

    @api.model
    def _get_company(self):
        return self.env.user.company_id

    # Special behavior for this field: res.company.search() will only return the companies
    # available to the current user (should be the user's companies?), when the user_preference
    # context is set.
    company_id = fields.Many2one(
        'res.company',
        string='Company',
        required=True,
        default=_get_company,
        help='The company this user is currently working for.',
        context={'user_preference': True})
    company_ids = fields.Many2many('res.company',
                                   'res_company_users_rel',
                                   'user_id',
                                   'cid',
                                   string='Companies',
                                   default=_get_company)

    # overridden inherited fields to bypass access rights, in case you have
    # access to the user but not its corresponding partner
    name = fields.Char(related='partner_id.name', inherited=True)
    email = fields.Char(related='partner_id.email', inherited=True)

    _sql_constraints = [('login_key', 'UNIQUE (login)',
                         'You can not have two users with the same login !')]

    def _compute_password(self):
        for user in self:
            user.password = ''

    def _inverse_password(self):
        for user in self:
            if not user.new_password:
                # Do not update the password if no value is provided, ignore silently.
                # For example web client submits False values for all empty fields.
                continue
            if user == self.env.user:
                # To change their own password, users must use the client-specific change password wizard,
                # so that the new password is immediately used for further RPC requests, otherwise the user
                # will face unexpected 'Access Denied' exceptions.
                raise UserError(
                    _('Please use the change password wizard (in User Preferences or User menu) to change your own password.'
                      ))
            else:
                user.password = user.new_password

    @api.depends('groups_id')
    def _compute_share(self):
        for user in self:
            user.share = not user.has_group('base.group_user')

    @api.multi
    def _compute_companies_count(self):
        companies_count = self._companies_count()
        for user in self:
            user.companies_count = companies_count

    @api.depends('tz')
    def _compute_tz_offset(self):
        for user in self:
            user.tz_offset = datetime.datetime.now(
                pytz.timezone(user.tz or 'GMT')).strftime('%z')

    @api.onchange('login')
    def on_change_login(self):
        if self.login and tools.single_email_re.match(self.login):
            self.email = self.login

    @api.onchange('state_id')
    def onchange_state(self):
        return self.mapped('partner_id').onchange_state()

    @api.onchange('parent_id')
    def onchange_parent_id(self):
        return self.mapped('partner_id').onchange_parent_id()

    @api.multi
    @api.constrains('company_id', 'company_ids')
    def _check_company(self):
        if any(user.company_ids and user.company_id not in user.company_ids
               for user in self):
            raise ValidationError(
                _('The chosen company is not in the allowed companies for this user'
                  ))

    @api.multi
    def read(self, fields=None, load='_classic_read'):
        if fields and self == self.env.user:
            for key in fields:
                if not (key in self.SELF_READABLE_FIELDS
                        or key.startswith('context_')):
                    break
            else:
                # safe fields only, so we read as super-user to bypass access rights
                self = self.sudo()

        result = super(Users, self).read(fields=fields, load=load)

        canwrite = self.env['ir.model.access'].check('res.users', 'write',
                                                     False)
        if not canwrite:

            def override_password(vals):
                if (vals['id'] != self._uid):
                    for key in USER_PRIVATE_FIELDS:
                        if key in vals:
                            vals[key] = '********'
                return vals

            result = map(override_password, result)

        return result

    @api.model
    def read_group(self,
                   domain,
                   fields,
                   groupby,
                   offset=0,
                   limit=None,
                   orderby=False,
                   lazy=True):
        groupby_fields = set(
            [groupby] if isinstance(groupby, basestring) else groupby)
        if groupby_fields.intersection(USER_PRIVATE_FIELDS):
            raise AccessError(_("Invalid 'group by' parameter"))
        return super(Users, self).read_group(domain,
                                             fields,
                                             groupby,
                                             offset=offset,
                                             limit=limit,
                                             orderby=orderby,
                                             lazy=lazy)

    @api.model
    def _search(self,
                args,
                offset=0,
                limit=None,
                order=None,
                count=False,
                access_rights_uid=None):
        if self._uid != SUPERUSER_ID and args:
            domain_fields = {
                term[0]
                for term in args if isinstance(term, (tuple, list))
            }
            if domain_fields.intersection(USER_PRIVATE_FIELDS):
                raise AccessError(_('Invalid search criterion'))
        return super(Users, self)._search(args,
                                          offset=offset,
                                          limit=limit,
                                          order=order,
                                          count=count,
                                          access_rights_uid=access_rights_uid)

    @api.model
    def create(self, vals):
        user = super(Users, self).create(vals)
        user.partner_id.active = user.active
        if user.partner_id.company_id:
            user.partner_id.write({'company_id': user.company_id.id})
        return user

    @api.multi
    def write(self, values):
        if values.get('active') == False:
            for user in self:
                if user.id == SUPERUSER_ID:
                    raise UserError(_("You cannot deactivate the admin user."))
                elif user.id == self._uid:
                    raise UserError(
                        _("You cannot deactivate the user you're currently logged in as."
                          ))

        if self == self.env.user:
            for key in values.keys():
                if not (key in self.SELF_WRITEABLE_FIELDS
                        or key.startswith('context_')):
                    break
            else:
                if 'company_id' in values:
                    if values[
                            'company_id'] not in self.env.user.company_ids.ids:
                        del values['company_id']
                # safe fields only, so we write as super-user to bypass access rights
                self = self.sudo()

        res = super(Users, self).write(values)
        if 'company_id' in values:
            for user in self:
                # if partner is global we keep it that way
                if user.partner_id.company_id.id != values['company_id']:
                    user.partner_id.write({'company_id': user.company_id.id})
            # clear default ir values when company changes
            self.env['ir.values'].get_defaults_dict.clear_cache(
                self.env['ir.values'])

        # clear caches linked to the users
        if 'groups_id' in values:
            self.env['ir.model.access'].call_cache_clearing_methods()
            self.env['ir.rule'].clear_caches()
            self.has_group.clear_cache(self)
        if any(
                key.startswith('context_') or key in ('lang', 'tz')
                for key in values):
            self.context_get.clear_cache(self)
        if any(key in values for key in ['active'] + USER_PRIVATE_FIELDS):
            db = self._cr.dbname
            for id in self.ids:
                self.__uid_cache[db].pop(id, None)

        return res

    @api.multi
    def unlink(self):
        if SUPERUSER_ID in self.ids:
            raise UserError(
                _('You can not remove the admin user as it is used internally for resources created by Odoo (updates, module installation, ...)'
                  ))
        db = self._cr.dbname
        for id in self.ids:
            self.__uid_cache[db].pop(id, None)
        return super(Users, self).unlink()

    @api.model
    def name_search(self, name='', args=None, operator='ilike', limit=100):
        if args is None:
            args = []
        users = self.browse()
        if name and operator in ['=', 'ilike']:
            users = self.search([('login', '=', name)] + args, limit=limit)
        if not users:
            users = self.search([('name', operator, name)] + args, limit=limit)
        return users.name_get()

    @api.multi
    def copy(self, default=None):
        self.ensure_one()
        default = dict(default or {})
        if ('name' not in default) and ('partner_id' not in default):
            default['name'] = _("%s (copy)") % self.name
        if 'login' not in default:
            default['login'] = _("%s (copy)") % self.login
        return super(Users, self).copy(default)

    @api.model
    @tools.ormcache('self._uid')
    def context_get(self):
        user = self.env.user
        result = {}
        for k in self._fields:
            if k.startswith('context_'):
                context_key = k[8:]
            elif k in ['lang', 'tz']:
                context_key = k
            else:
                context_key = False
            if context_key:
                res = getattr(user, k) or False
                if isinstance(res, models.BaseModel):
                    res = res.id
                result[context_key] = res or False
        return result

    @api.model
    @api.returns('ir.actions.act_window', lambda record: record.id)
    def action_get(self):
        return self.sudo().env.ref('base.action_res_users_my')

    def check_super(self, passwd):
        return check_super(passwd)

    @api.model
    def check_credentials(self, password):
        """ Override this method to plug additional authentication methods"""
        user = self.sudo().search([('id', '=', self._uid),
                                   ('password', '=', password)])
        if not user:
            raise AccessDenied()

    @api.model
    def _update_last_login(self):
        # only create new records to avoid any side-effect on concurrent transactions
        # extra records will be deleted by the periodical garbage collection
        self.env['res.users.log'].create({})  # populated by defaults

    @classmethod
    def _login(cls, db, login, password):
        if not password:
            return False
        user_id = False
        try:
            with cls.pool.cursor() as cr:
                self = api.Environment(cr, SUPERUSER_ID, {})[cls._name]
                user = self.search([('login', '=', login)])
                if user:
                    user_id = user.id
                    user.sudo(user_id).check_credentials(password)
                    user.sudo(user_id)._update_last_login()
        except AccessDenied:
            _logger.info("Login failed for db:%s login:%s", db, login)
            user_id = False
        return user_id

    @classmethod
    def authenticate(cls, db, login, password, user_agent_env):
        """Verifies and returns the user ID corresponding to the given
          ``login`` and ``password`` combination, or False if there was
          no matching user.
           :param str db: the database on which user is trying to authenticate
           :param str login: username
           :param str password: user password
           :param dict user_agent_env: environment dictionary describing any
               relevant environment attributes
        """
        uid = cls._login(db, login, password)
        if uid == SUPERUSER_ID:
            # Successfully logged in as admin!
            # Attempt to guess the web base url...
            if user_agent_env and user_agent_env.get('base_location'):
                try:
                    with cls.pool.cursor() as cr:
                        base = user_agent_env['base_location']
                        ICP = api.Environment(cr, uid,
                                              {})['ir.config_parameter']
                        if not ICP.get_param('web.base.url.freeze'):
                            ICP.set_param('web.base.url', base)
                except Exception:
                    _logger.exception(
                        "Failed to update web.base.url configuration parameter"
                    )
        return uid

    @classmethod
    def check(cls, db, uid, passwd):
        """Verifies that the given (uid, password) is authorized for the database ``db`` and
           raise an exception if it is not."""
        if not passwd:
            # empty passwords disallowed for obvious security reasons
            raise AccessDenied()
        db = cls.pool.db_name
        if cls.__uid_cache[db].get(uid) == passwd:
            return
        cr = cls.pool.cursor()
        try:
            self = api.Environment(cr, uid, {})[cls._name]
            self.check_credentials(passwd)
            cls.__uid_cache[db][uid] = passwd
        finally:
            cr.close()

    @api.model
    def change_password(self, old_passwd, new_passwd):
        """Change current user password. Old password must be provided explicitly
        to prevent hijacking an existing user session, or for cases where the cleartext
        password is not used to authenticate requests.

        :return: True
        :raise: odoo.exceptions.AccessDenied when old password is wrong
        :raise: odoo.exceptions.UserError when new password is not set or empty
        """
        self.check(self._cr.dbname, self._uid, old_passwd)
        if new_passwd:
            # use self.env.user here, because it has uid=SUPERUSER_ID
            return self.env.user.write({'password': new_passwd})
        raise UserError(
            _("Setting empty passwords is not allowed for security reasons!"))

    @api.multi
    def preference_save(self):
        return {
            'type': 'ir.actions.client',
            'tag': 'reload_context',
        }

    @api.multi
    def preference_change_password(self):
        return {
            'type': 'ir.actions.client',
            'tag': 'change_password',
            'target': 'new',
        }

    @api.model
    def has_group(self, group_ext_id):
        # use singleton's id if called on a non-empty recordset, otherwise
        # context uid
        uid = self.id or self._uid
        return self.sudo(user=uid)._has_group(group_ext_id)

    @api.model
    @tools.ormcache('self._uid', 'group_ext_id')
    def _has_group(self, group_ext_id):
        """Checks whether user belongs to given group.

        :param str group_ext_id: external ID (XML ID) of the group.
           Must be provided in fully-qualified form (``module.ext_id``), as there
           is no implicit module to use..
        :return: True if the current user is a member of the group with the
           given external ID (XML ID), else False.
        """
        assert group_ext_id and '.' in group_ext_id, "External ID must be fully qualified"
        module, ext_id = group_ext_id.split('.')
        self._cr.execute(
            """SELECT 1 FROM res_groups_users_rel WHERE uid=%s AND gid IN
                            (SELECT res_id FROM ir_model_data WHERE module=%s AND name=%s)""",
            (self._uid, module, ext_id))
        return bool(self._cr.fetchone())

    # for a few places explicitly clearing the has_group cache
    has_group.clear_cache = _has_group.clear_cache

    @api.multi
    def _is_admin(self):
        self.ensure_one()
        return self._is_superuser() or self.has_group('base.group_erp_manager')

    @api.multi
    def _is_superuser(self):
        self.ensure_one()
        return self.id == SUPERUSER_ID

    @api.model
    def get_company_currency_id(self):
        return self.env.user.company_id.currency_id.id
Example #16
0
class SaleOrderOption(models.Model):
    _name = "sale.order.option"
    _description = "Sale Options"
    _order = 'sequence, id'

    order_id = fields.Many2one('sale.order', 'Sales Order Reference', ondelete='cascade', index=True)
    line_id = fields.Many2one('sale.order.line', on_delete="set null")
    name = fields.Text('Description', required=True)
    product_id = fields.Many2one('product.product', 'Product', domain=[('sale_ok', '=', True)])
    website_description = fields.Html('Line Description', sanitize_attributes=False, translate=html_translate)
    price_unit = fields.Float('Unit Price', required=True, digits=dp.get_precision('Product Price'))
    discount = fields.Float('Discount (%)', digits=dp.get_precision('Discount'))
    uom_id = fields.Many2one('uom.uom', 'Unit of Measure ', required=True)
    quantity = fields.Float('Quantity', required=True, digits=dp.get_precision('Product UoS'), default=1)
    sequence = fields.Integer('Sequence', help="Gives the sequence order when displaying a list of suggested product.")

    @api.onchange('product_id', 'uom_id')
    def _onchange_product_id(self):
        if not self.product_id:
            return
        product = self.product_id.with_context(lang=self.order_id.partner_id.lang)
        self.price_unit = product.list_price
        self.website_description = product.quote_description or product.website_description
        self.name = product.name
        if product.description_sale:
            self.name += '\n' + product.description_sale
        self.uom_id = self.uom_id or product.uom_id
        pricelist = self.order_id.pricelist_id
        if pricelist and product:
            partner_id = self.order_id.partner_id.id
            self.price_unit = pricelist.with_context(uom=self.uom_id.id).get_product_price(product, self.quantity, partner_id)
        domain = {'uom_id': [('category_id', '=', self.product_id.uom_id.category_id.id)]}
        return {'domain': domain}

    @api.multi
    def button_add_to_order(self):
        self.ensure_one()
        order = self.order_id
        if order.state not in ['draft', 'sent']:
            return False

        order_line = order.order_line.filtered(lambda line: line.product_id == self.product_id)
        if order_line:
            order_line = order_line[0]
            order_line.product_uom_qty += 1
        else:
            vals = {
                'price_unit': self.price_unit,
                'website_description': self.website_description,
                'name': self.name,
                'order_id': order.id,
                'product_id': self.product_id.id,
                'product_uom_qty': self.quantity,
                'product_uom': self.uom_id.id,
                'discount': self.discount,
            }
            order_line = self.env['sale.order.line'].create(vals)
            order_line._compute_tax_id()

        self.write({'line_id': order_line.id})
        return {'type': 'ir.actions.client', 'tag': 'reload'}
Example #17
0
class TransferRequest(models.Model):
    _name = 'transfer.request'
    _inherit = ['mail.thread', 'resource.mixin']

    # old implementation
    # def _get_default_requester(self):
    #     emp = self.env['hr.employee'].search([('user_id', '=', self.env.uid)])
    #     if emp:
    #         return emp[0].id
    #     else:
    #         return False

    @api.model
    def _get_default_requested_by(self):
        return self.env['res.users'].browse(self.env.uid)

    @api.model
    def _default_dest_location(self):
        if self.env.user.dest_location_id:
            return self.env.user.dest_location_id.id
        else:
            return False

    name = fields.Char(string='Name', readonly=1)
    requested_by = fields.Many2one('res.users',
                                   'Requested by',
                                   required=True,
                                   default=_get_default_requested_by)
    # requested_by_employee_id = fields.Many2one('hr.employee', string='Requester', default=_get_default_requester)
    # requested_for_employee_id = fields.Many2one('hr.employee', string='Requested For')
    analytic_account_id = fields.Many2one(
        'account.analytic.account',
        string='Analytic Account',
    )
    cancel_reason = fields.Html(string='Cancellation Reason')
    transfer_request_line_ids = fields.One2many('transfer.request.line',
                                                'transfer_request_id',
                                                string='Transferred Products')
    source_stock_location_id = fields.Many2one('stock.location',
                                               string='Source Location',
                                               domain=[('usage', '=',
                                                        'internal')])
    destination_stock_location_id = fields.Many2one(
        'stock.location',
        string='Destination Location',
        domain=[('usage', '=', 'transit')],
        default=_default_dest_location)
    state = fields.Selection([('draft', 'Draft'), ('approve', 'Approve'),
                              ('transferring', 'Transferring'),
                              ('done', 'Done'), ('cancelled', 'Cancelled')],
                             string='State',
                             default='draft',
                             track_visibility='onchange')

    picking_count = fields.Integer(string='Transfers',
                                   compute='get_request_picking_count')
    transfer_reason = fields.Many2one('transfer.request.reason',
                                      string='Transfer Reason')
    scheduled_date = fields.Datetime(
        'Scheduled Date',
        help=
        "Scheduled time for the first part of the shipment to be processed.")

    @api.model
    def create(self, vals):
        if not self.env.user.dest_location_id:
            raise ValidationError(
                _('Please configure destination location in current user related employee.'
                  ))
        vals['name'] = self.env['ir.sequence'].next_by_code('transfer.request')
        vals.update({
            'requested_by':
            self.env.uid,
            'destination_stock_location_id':
            self.env.user.dest_location_id.id,
        })
        return super(TransferRequest, self).create(vals)

    @api.multi
    def unlink(self):
        for rec in self:
            if rec.state == 'done':
                raise ValidationError(
                    _('You cannot delete done transfer request.'))
        return super(TransferRequest, self).unlink()

    @api.multi
    def set_state_to_approve(self):
        for rec in self:
            rec.state = 'approve'

    @api.multi
    def set_state_to_cancelled(self):
        for rec in self:
            if not rec.cancel_reason or rec.cancel_reason == '<p><br></p>':
                raise ValidationError(
                    _('Please add reason for canceling request first.'))
            rec.state = 'cancelled'

    @api.multi
    def set_state_to_draft(self):
        for rec in self:
            rec.state = 'draft'

    @api.multi
    def set_state_to_transferring(self):
        for rec in self:
            rec.state = 'transferring'

    @api.multi
    def set_state_to_done(self):
        for rec in self:
            rec.state = 'done'

    @api.multi
    def get_request_picking_count(self):
        for rec in self:
            stock_picking_objects = self.env['stock.picking'].search([
                ('transfer_request_id', '=', rec.id)
            ])
            rec.picking_count = len(stock_picking_objects)

    @api.multi
    def transfer_products(self):
        for rec in self:
            rec.create_transfer_for_products()
            rec.set_state_to_transferring()

            # old implementation
            # transfer_line_ids = [line.id for line in rec.transfer_request_line_ids if
            #                      rec.transfer_request_line_ids and line.transfer_created == False]
            # return {
            #     'name': _('Transfer Products'),
            #     'view_type': 'form',
            #     'view_mode': 'form',
            #     'target': 'new',
            #     'res_model': 'transfer.products.wizard',
            #     'view_id': self.env.ref('bi_transfer_request.transfer_products_wizard_form_view').id,
            #     'type': 'ir.actions.act_window',
            #     'context': {
            #         'default_source_stock_location_id': rec.source_stock_location_id.id,
            #         'default_destination_stock_location_id': rec.destination_stock_location_id.id,
            #         'default_transfer_request_line_ids': transfer_line_ids,
            #         'default_created_from': 'transfer_request',
            #     },
            # }

    def create_transfer_for_products(self):
        picking_line_vals = []
        source_warehouse = self.source_stock_location_id.get_warehouse()
        internal_picking_type = self.env['stock.picking.type'].sudo().search(
            [('code', '=', 'internal'),
             ('warehouse_id', '=', source_warehouse.id)],
            limit=1)
        if not internal_picking_type:
            raise ValidationError(_('Please configure internal transfer.'))
        else:
            internal_picking_type_id = internal_picking_type.id

        for line in self.transfer_request_line_ids:
            if line.transfer_created == False:
                picking_line_vals.append((0, 0, {
                    'product_id':
                    line.product_id.id,
                    'name':
                    line.product_id.name,
                    'product_uom_qty':
                    line.transferred_qty or line.qty,
                    'product_uom':
                    line.product_uom_id.id,
                    'company_id':
                    self.env.user.company_id.id,
                    'location_id':
                    self.source_stock_location_id.id,
                    'location_dest_id':
                    self.destination_stock_location_id.id,
                    'transfer_request_id':
                    self.id
                }))
        if len(picking_line_vals):
            picking_vals = {
                'origin': self.name,
                'scheduled_date': self.scheduled_date or fields.Datetime.now(),
                'picking_type_id': internal_picking_type_id,
                'location_id': self.source_stock_location_id.id,
                'location_dest_id': self.destination_stock_location_id.id,
                'company_id': self.env.user.company_id.id,
                'move_type': 'direct',
                'state': 'draft',
                'move_lines': picking_line_vals,
                'transfer_request_id': self.id
            }
            created_picking = self.env['stock.picking'].sudo().create(
                picking_vals)
            if created_picking:
                created_picking.action_confirm()
                for line in self.transfer_request_line_ids:
                    line.transfer_created = True
Example #18
0
class SaleOrder(models.Model):
    _inherit = 'sale.order'

    def _get_default_template(self):
        template = self.env.ref('website_quote.website_quote_template_default', raise_if_not_found=False)
        return template and template.active and template or False

    def _get_default_require_signature(self):
        default_template = self._get_default_template()
        if default_template:
            return default_template.require_signature
        else:
            return False

    def _get_default_require_payment(self):
        default_template = self._get_default_template()
        if default_template:
            return default_template.require_payment
        else:
            return False

    template_id = fields.Many2one(
        'sale.quote.template', 'Quotation Template',
        readonly=True,
        states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
        default=_get_default_template)
    website_description = fields.Html('Description', sanitize_attributes=False, translate=html_translate)
    options = fields.One2many(
        'sale.order.option', 'order_id', 'Optional Products Lines',
        copy=True, readonly=True,
        states={'draft': [('readonly', False)], 'sent': [('readonly', False)]})
    amount_undiscounted = fields.Float(
        'Amount Before Discount', compute='_compute_amount_undiscounted', digits=0)
    quote_viewed = fields.Boolean('Quotation Viewed')
    require_signature = fields.Boolean('Digital Signature', default=_get_default_require_signature,
                                       states={'sale': [('readonly', True)], 'done': [('readonly', True)]},
                                       help='Request a digital signature to the customer in order to confirm orders automatically.')
    require_payment = fields.Boolean('Electronic Payment', default=_get_default_require_payment,
                                     states={'sale': [('readonly', True)], 'done': [('readonly', True)]},
                                     help='Request an electronic payment to the customer in order to confirm orders automatically.')

    @api.multi
    @api.returns('self', lambda value: value.id)
    def copy(self, default=None):
        if self.template_id and self.template_id.number_of_days > 0:
            default = dict(default or {})
            default['validity_date'] = fields.Date.to_string(datetime.now() + timedelta(self.template_id.number_of_days))
        return super(SaleOrder, self).copy(default=default)

    @api.one
    def _compute_amount_undiscounted(self):
        total = 0.0
        for line in self.order_line:
            total += line.price_subtotal + line.price_unit * ((line.discount or 0.0) / 100.0) * line.product_uom_qty  # why is there a discount in a field named amount_undiscounted ??
        self.amount_undiscounted = total

    @api.onchange('partner_id')
    def onchange_partner_id(self):
        super(SaleOrder, self).onchange_partner_id()
        self.note = self.template_id.note or self.note

    @api.onchange('partner_id')
    def onchange_update_description_lang(self):
        if not self.template_id:
            return
        else:
            template = self.template_id.with_context(lang=self.partner_id.lang)
            self.website_description = template.website_description

    @api.onchange('template_id')
    def onchange_template_id(self):
        if not self.template_id:
            self.require_signature = False
            self.require_payment = False
            return
        template = self.template_id.with_context(lang=self.partner_id.lang)

        order_lines = [(5, 0, 0)]
        for line in template.quote_line:
            data = {
                'display_type': line.display_type,
                'name': line.name,
                'website_description': line.website_description,
                'state': 'draft',
            }
            if line.product_id:
                discount = 0
                if self.pricelist_id:
                    price = self.pricelist_id.with_context(uom=line.product_uom_id.id).get_product_price(line.product_id, 1, False)
                    if self.pricelist_id.discount_policy == 'without_discount' and line.price_unit:
                        discount = (line.price_unit - price) / line.price_unit * 100
                        price = line.price_unit

                else:
                    price = line.price_unit

                data.update({
                    'price_unit': price,
                    'discount': 100 - ((100 - discount) * (100 - line.discount)/100),
                    'product_uom_qty': line.product_uom_qty,
                    'product_id': line.product_id.id,
                    'product_uom': line.product_uom_id.id,
                    'customer_lead': self._get_customer_lead(line.product_id.product_tmpl_id),
                })
                if self.pricelist_id:
                    data.update(self.env['sale.order.line']._get_purchase_price(self.pricelist_id, line.product_id, line.product_uom_id, fields.Date.context_today(self)))
            order_lines.append((0, 0, data))

        self.order_line = order_lines
        self.order_line._compute_tax_id()

        option_lines = []
        for option in template.options:
            if self.pricelist_id:
                price = self.pricelist_id.with_context(uom=option.uom_id.id).get_product_price(option.product_id, 1, False)
            else:
                price = option.price_unit
            data = {
                'product_id': option.product_id.id,
                'name': option.name,
                'quantity': option.quantity,
                'uom_id': option.uom_id.id,
                'price_unit': price,
                'discount': option.discount,
                'website_description': option.website_description,
            }
            option_lines.append((0, 0, data))
        self.options = option_lines

        if template.number_of_days > 0:
            self.validity_date = fields.Date.to_string(datetime.now() + timedelta(template.number_of_days))

        self.website_description = template.website_description
        self.require_signature = template.require_signature
        self.require_payment = template.require_payment

        if template.note:
            self.note = template.note

    @api.constrains('template_id', 'require_signature', 'require_payment')
    def _check_portal_confirmation(self):
        for order in self.sudo().filtered('template_id'):
            if not order.require_signature and not order.require_payment:
                raise ValidationError(_('Please select a confirmation mode in Other Information: Digital Signature, Electronic Payment or both.'))

    @api.multi
    def open_quotation(self):
        self.ensure_one()
        self.write({'quote_viewed': True})
        return {
            'type': 'ir.actions.act_url',
            'target': 'self',
            'url': '/quote/%s/%s' % (self.id, self.access_token)
        }

    @api.multi
    def get_access_action(self, access_uid=None):
        """ Instead of the classic form view, redirect to the online quote if it exists. """
        self.ensure_one()
        user = access_uid and self.env['res.users'].sudo().browse(access_uid) or self.env.user

        if not self.template_id or (not user.share and not self.env.context.get('force_website')):
            return super(SaleOrder, self).get_access_action(access_uid)
        return {
            'type': 'ir.actions.act_url',
            'url': '/quote/%s/%s' % (self.id, self.access_token),
            'target': 'self',
            'res_id': self.id,
        }

    def _get_share_url(self, redirect=False, signup_partner=False):
        self.ensure_one()
        if self.state not in ['sale', 'done']:
            auth_param = url_encode(self.partner_id.signup_get_auth_param()[self.partner_id.id])
            return '/quote/%s/%s?' % (self.id, self.access_token) + auth_param
        return super(SaleOrder, self)._get_share_url(redirect)

    def get_portal_confirmation_action(self):
        """ Template override default behavior of pay / sign chosen in sales settings """
        if self.template_id:
            if self.require_signature and not self.signature:
                return 'sign'
            elif self.require_payment:
                return 'pay'
            else:
                return 'none'
        return super(SaleOrder, self).get_portal_confirmation_action()

    def has_to_be_signed(self):
        res = super(SaleOrder, self).has_to_be_signed()
        return self.require_signature if self.template_id else res

    def has_to_be_paid(self):
        res = super(SaleOrder, self).has_to_be_paid()
        return self.require_payment if self.template_id else res

    @api.multi
    def action_confirm(self):
        res = super(SaleOrder, self).action_confirm()
        for order in self:
            if order.template_id and order.template_id.mail_template_id:
                self.template_id.mail_template_id.send_mail(order.id)
        return res

    @api.multi
    def _get_payment_type(self):
        self.ensure_one()
        return 'form_save' if self.require_payment else 'form'
Example #19
0
class AccountEdiDocument(models.Model):
    _name = 'account.edi.document'
    _description = 'Electronic Document for an account.move'

    # == Stored fields ==
    move_id = fields.Many2one('account.move',
                              required=True,
                              ondelete='cascade')
    edi_format_id = fields.Many2one('account.edi.format', required=True)
    attachment_id = fields.Many2one(
        'ir.attachment',
        help=
        'The file generated by edi_format_id when the invoice is posted (and this document is processed).'
    )
    state = fields.Selection([('to_send', 'To Send'), ('sent', 'Sent'),
                              ('to_cancel', 'To Cancel'),
                              ('cancelled', 'Cancelled')])
    error = fields.Html()

    # == Not stored fields ==
    name = fields.Char(related='attachment_id.name')
    edi_format_name = fields.Char(string='Format Name',
                                  related='edi_format_id.name')

    _sql_constraints = [
        (
            'unique_edi_document_by_move_by_format',
            'UNIQUE(edi_format_id, move_id)',
            'Only one edi document by move by format',
        ),
    ]

    def _prepare_jobs(self):
        """Creates a list of jobs to be performed by '_process_jobs' for the documents in self.
        Each document represent a job, BUT if multiple documents have the same state, edi_format_id,
        doc_type (invoice or payment) and company_id AND the edi_format_id supports batching, they are grouped
        into a single job.

        :returns:         A list of tuples (key, documents)
        * key:            A tuple (edi_format_id, state, doc_type, company_id)
        ** edi_format_id: The format to perform the operation with
        ** state:         The state of the documents of this job
        ** doc_type:      Are the moves of this job invoice or payments ?
        ** company_id:    The company the moves belong to
        * documents:      The documents related to this job. If edi_format_id does not support batch, length must be one
        """

        to_process = []
        batches = {}
        for edi_doc in self.filtered(lambda d: d.state in
                                     ('to_send', 'to_cancel')):
            move = edi_doc.move_id
            edi_format = edi_doc.edi_format_id
            if move.is_invoice(include_receipts=True):
                doc_type = 'invoice'
            elif move.payment_id or move.statement_line_id:
                doc_type = 'payment'
            else:
                continue

            key = (edi_format, edi_doc.state, doc_type, move.company_id)
            if edi_format._support_batching():
                if not batches.get(key, None):
                    batches[key] = self.env['account.edi.document']
                batches[key] |= edi_doc
            else:
                to_process.append((key, edi_doc))
        to_process.extend(batches.items())
        return to_process

    @api.model
    def _process_jobs(self, to_process):
        """Post or cancel move_id (invoice or payment) by calling the related methods on edi_format_id.
        Invoices are processed before payments.
        """
        def _postprocess_post_edi_results(documents, edi_result):
            attachments_to_unlink = self.env['ir.attachment']
            for document in documents:
                move = document.move_id
                move_result = edi_result.get(move, {})
                if move_result.get('attachment'):
                    old_attachment = document.attachment_id
                    document.write({
                        'attachment_id': move_result['attachment'].id,
                        'state': 'sent',
                        'error': False,
                    })
                    if not old_attachment.res_model or not old_attachment.res_id:
                        attachments_to_unlink |= old_attachment
                else:
                    document.error = move_result.get(
                        'error', _("Error when processing the journal entry."))

            # Attachments that are not explicitly linked to a business model could be removed because they are not
            # supposed to have any traceability from the user.
            attachments_to_unlink.unlink()

        def _postprocess_cancel_edi_results(documents, edi_result):
            invoice_ids_to_cancel = set()  # Avoid duplicates
            attachments_to_unlink = self.env['ir.attachment']
            for document in documents:
                move = document.move_id
                move_result = edi_result.get(move, {})
                if move_result.get('success'):
                    old_attachment = document.attachment_id
                    document.write({
                        'state': 'cancelled',
                        'error': False,
                        'attachment_id': False,
                    })

                    if move.is_invoice(
                            include_receipts=True) and move.state == 'posted':
                        # The user requested a cancellation of the EDI and it has been approved. Then, the invoice
                        # can be safely cancelled.
                        invoice_ids_to_cancel.add(move.id)

                    if not old_attachment.res_model or not old_attachment.res_id:
                        attachments_to_unlink |= old_attachment

                else:
                    document.error = move_result.get('error') or _(
                        "Error when cancelling the journal entry.")

            if invoice_ids_to_cancel:
                invoices = self.env['account.move'].browse(
                    list(invoice_ids_to_cancel))
                invoices.button_draft()
                invoices.button_cancel()

            # Attachments that are not explicitly linked to a business model could be removed because they are not
            # supposed to have any traceability from the user.
            attachments_to_unlink.unlink()

        test_mode = self._context.get('edi_test_mode', False)

        # ==== Process invoices ====
        payments = []
        for key, batches in to_process:
            edi_format, state, doc_type, company_id = key
            if doc_type == 'payment':
                payments.append((key, batches))
                continue  # payments are processed after invoices

            for documents in batches:
                try:
                    with self.env.cr.savepoint():
                        # Locks the documents in DB. Avoid sending an invoice twice (the documents can be processed by the CRON but also manually).
                        self._cr.execute(
                            'SELECT * FROM account_edi_document WHERE id IN %s FOR UPDATE NOWAIT',
                            [tuple(self.ids)])

                        if state == 'to_send':
                            edi_result = edi_format._post_invoice_edi(
                                documents.move_id, test_mode=test_mode)
                            _postprocess_post_edi_results(
                                documents, edi_result)
                        elif state == 'to_cancel':
                            edi_result = edi_format._cancel_invoice_edi(
                                documents.move_id, test_mode=test_mode)
                            _postprocess_cancel_edi_results(
                                documents, edi_result)

                except OperationalError as e:
                    if e.pgcode == '55P03':
                        _logger.debug(
                            'Another transaction already locked documents rows. Cannot process documents.'
                        )
                    else:
                        raise e

        # ==== Process payments ====
        for key, batches in payments:
            edi_format, state, doc_type, company_id = key

            for documents in batches:
                try:
                    with self.env.cr.savepoint():
                        self._cr.execute(
                            'SELECT * FROM account_edi_document WHERE id IN %s FOR UPDATE NOWAIT',
                            [tuple(self.ids)])

                        if state == 'to_send':
                            edi_result = edi_format._post_payment_edi(
                                documents.move_id, test_mode=test_mode)
                            _postprocess_post_edi_results(
                                documents, edi_result)
                        elif state == 'to_cancel':
                            edi_result = edi_format._cancel_payment_edi(
                                documents.move_id, test_mode=test_mode)
                            _postprocess_cancel_edi_results(
                                documents, edi_result)

                except OperationalError as e:
                    if e.pgcode == '55P03':
                        _logger.debug(
                            'Another transaction already locked documents rows. Cannot process documents.'
                        )
                    else:
                        raise e

    def _process_documents_no_web_services(self):
        """ Post and cancel all the documents that don't need a web service.
        """
        jobs = self.filtered(lambda d: not d.edi_format_id._needs_web_services(
        ))._prepare_jobs()
        self._process_jobs(jobs)

    def _process_documents_web_services(self, job_count=None):
        """ Post and cancel all the documents that need a web service. This is called by CRON.

        :param job_count: Limit to the number of jobs to process among the ones that are available for treatment.
        """
        jobs = self.filtered(
            lambda d: d.edi_format_id._needs_web_services())._prepare_jobs()
        self._process_jobs(jobs[0:job_count or len(jobs)])
class Wizard(models.TransientModel):
    _name = 'mail_move_message.wizard'
    _description = 'Mail move message wizard'

    @api.model
    def _model_selection(self):
        selection = []
        config_parameters = self.env['ir.config_parameter']
        model_names = config_parameters.sudo().get_param(
            'mail_relocation_models')
        model_names = model_names.split(',') if model_names else []
        if 'default_message_id' in self.env.context:
            message = self.env['mail.message'].browse(
                self.env.context['default_message_id'])
            if message.model and message.model not in model_names:
                model_names.append(message.model)
            if message.moved_from_model and message.moved_from_model not in model_names:
                model_names.append(message.moved_from_model)
        if model_names:
            selection = [(m.model, m.display_name)
                         for m in self.env['ir.model'].search([('model', 'in',
                                                                model_names)])]
        return selection

    @api.model
    def default_get(self, fields_list):
        res = super(Wizard, self).default_get(fields_list)

        available_models = self._model_selection()
        if len(available_models):
            record = self.env[available_models[0][0]].search([], limit=1)
            res['model_record'] = len(record) and (
                available_models[0][0] + ',' + str(record.id)) or False

        if 'message_id' in res:
            message = self.env['mail.message'].browse(res['message_id'])
            email_from = message.email_from
            parts = email_split(email_from.replace(' ', ','))
            if parts:
                email = parts[0]
                name = email_from.find(
                    email
                ) != -1 and email_from[:email_from.index(email)].replace(
                    '"', '').replace('<', '').strip() or email_from
            else:
                name, email = email_from
            res['message_name_from'] = name
            res['message_email_from'] = email

            res['partner_id'] = message.author_id.id
            if message.author_id and self.env.uid not in [
                    u.id for u in message.author_id.user_ids
            ]:
                res['filter_by_partner'] = True
            if message.author_id and res.get('model'):
                res_id = self.env[res['model']].search([],
                                                       order='id desc',
                                                       limit=1)
                if res_id:
                    res['res_id'] = res_id[0].id

        config_parameters = self.env['ir.config_parameter']
        res['move_followers'] = config_parameters.sudo().get_param(
            'mail_relocation_move_followers')

        res['uid'] = self.env.uid
        return res

    message_id = fields.Many2one('mail.message', string='Message')
    message_body = fields.Html(related='message_id.body',
                               string='Message to move',
                               readonly=True)
    message_from = fields.Char(related='message_id.email_from',
                               string='From',
                               readonly=True)
    message_subject = fields.Char(related='message_id.subject',
                                  string='Subject',
                                  readonly=True)
    message_moved_by_message_id = fields.Many2one(
        'mail.message',
        related='message_id.moved_by_message_id',
        string='Moved with',
        readonly=True)
    message_moved_by_user_id = fields.Many2one(
        'res.users',
        related='message_id.moved_by_user_id',
        string='Moved by',
        readonly=True)
    message_is_moved = fields.Boolean(string='Is Moved',
                                      related='message_id.is_moved',
                                      readonly=True)
    parent_id = fields.Many2one(
        'mail.message',
        string='Search by name',
    )
    model_record = fields.Reference(selection="_model_selection",
                                    string='Record')
    model = fields.Char(compute="_compute_model_res_id", string='Model')
    res_id = fields.Integer(compute="_compute_model_res_id",
                            string='Record ID')

    can_move = fields.Boolean('Can move', compute='_compute_get_can_move')
    move_back = fields.Boolean(
        'MOVE TO ORIGIN',
        help='Move  message and submessages to original place')
    partner_id = fields.Many2one('res.partner', string='Author')
    filter_by_partner = fields.Boolean('Filter Records by partner')
    message_email_from = fields.Char()
    message_name_from = fields.Char()
    # FIXME message_to_read should be True even if current message or any his childs are unread
    message_to_read = fields.Boolean(
        compute='_compute_is_read',
        string="Unread message",
        help="Service field shows that this message were unread when moved")
    uid = fields.Integer()
    move_followers = fields.Boolean(
        'Move Followers',
        help="Add followers of current record to a new record.\n"
        "You must use this option, if new record has restricted access.\n"
        "You can change default value for this option at Settings/System Parameters"
    )

    @api.depends('model_record')
    def _compute_model_res_id(self):
        for rec in self:
            rec.model = rec.model_record and rec.model_record._name or False
            rec.res_id = rec.model_record and rec.model_record.id or False

    @api.depends('message_id')
    def _compute_get_can_move(self):
        for r in self:
            r.get_can_move_one()

    def _compute_is_read(self):
        messages = self.env['mail.message'].sudo().browse(
            self.message_id.all_child_ids.ids + [self.message_id.id])
        self.message_to_read = True in [m.needaction for m in messages]

    def get_can_move_one(self):
        self.ensure_one()
        # message was not moved before OR message is a top message of previous move
        self.can_move = not self.message_id.moved_by_message_id or self.message_id.moved_by_message_id.id == self.message_id.id

    @api.onchange('move_back')
    def on_change_move_back(self):
        if not self.move_back:
            return
        self.parent_id = self.message_id.moved_from_parent_id
        message = self.message_id
        if message.is_moved:
            self.model_record = self.env[message.moved_from_model].browse(
                message.moved_from_res_id)

    @api.onchange('parent_id', 'model_record')
    def update_move_back(self):
        model = self.message_id.moved_from_model
        self.move_back = self.parent_id == self.message_id.moved_from_parent_id \
            and self.res_id == self.message_id.moved_from_res_id \
            and (self.model == model or (not self.model and not model))

    @api.onchange('parent_id')
    def on_change_parent_id(self):
        if self.parent_id and self.parent_id.model:
            self.model = self.parent_id.model
            self.res_id = self.parent_id.res_id
        else:
            self.model = None
            self.res_id = None

    @api.onchange('model', 'filter_by_partner', 'partner_id')
    def on_change_partner(self):
        domain = {'res_id': [('id', '!=', self.message_id.res_id)]}
        if self.model and self.filter_by_partner and self.partner_id:
            fields = self.env[self.model].fields_get(False)
            contact_field = False
            for n, f in fields.items():
                if f['type'] == 'many2one' and f['relation'] == 'res.partner':
                    contact_field = n
                    break
            if contact_field:
                domain['res_id'].append(
                    (contact_field, '=', self.partner_id.id))
        if self.model:
            res_id = self.env[self.model].search(domain['res_id'],
                                                 order='id desc',
                                                 limit=1)
            self.res_id = res_id and res_id[0].id
        else:
            self.res_id = None
        return {'domain': domain}

    def check_access(self):
        for r in self:
            r.check_access_one()

    def check_access_one(self):
        self.ensure_one()
        operation = 'write'

        if not (self.model and self.res_id):
            return True
        model_obj = self.env[self.model]
        mids = model_obj.browse(self.res_id).exists()
        if hasattr(model_obj, 'check_mail_message_access'):
            model_obj.check_mail_message_access(mids.ids, operation)
        else:
            self.env['mail.thread'].check_mail_message_access(
                mids.ids, operation, model_name=self.model)

    def open_moved_by_message_id(self):
        message_id = None
        for r in self:
            message_id = r.message_moved_by_message_id.id
        return {
            'type': 'ir.actions.act_window',
            'res_model': 'mail_move_message.wizard',
            'view_mode': 'form',
            'view_type': 'form',
            'views': [[False, 'form']],
            'target': 'new',
            'context': {
                'default_message_id': message_id
            },
        }

    def move(self):
        for r in self:
            if not r.model:
                raise exceptions.except_orm(
                    _('Record field is empty!'),
                    _('Select a record for relocation first'))
        for r in self:
            r.check_access()
            if not r.parent_id or not (r.parent_id.model == r.model
                                       and r.parent_id.res_id == r.res_id):
                # link with the first message of record
                parent = self.env['mail.message'].search(
                    [('model', '=', r.model), ('res_id', '=', r.res_id)],
                    order='id',
                    limit=1)
                r.parent_id = parent.id or None
            r.message_id.move(r.parent_id.id, r.res_id, r.model, r.move_back,
                              r.move_followers, r.message_to_read,
                              r.partner_id)

        if r.model in ['mail.message', 'mail.channel', False]:
            return {
                'name': 'Chess game page',
                'type': 'ir.actions.act_url',
                'url': '/web',
                'target': 'self',
            }
        return {
            'name': _('Record'),
            'view_type': 'form',
            'view_mode': 'form',
            'res_model': r.model,
            'res_id': r.res_id,
            'views': [(False, 'form')],
            'type': 'ir.actions.act_window',
        }

    def delete(self):
        for r in self:
            r.delete_one()

    def delete_one(self):
        self.ensure_one()
        msg_id = self.message_id.id

        # Send notification
        notification = {'id': msg_id}
        self.env['bus.bus'].sendone(
            (self._cr.dbname, 'mail_move_message.delete_message'),
            notification)

        self.message_id.unlink()
        return {}

    def read_close(self):
        for r in self:
            r.read_close_one()

    def read_close_one(self):
        self.ensure_one()
        self.message_id.set_message_done()
        self.message_id.child_ids.set_message_done()
        return {'type': 'ir.actions.act_window_close'}
Example #21
0
class EventEvent(models.Model):
    """Event"""
    _name = 'event.event'
    _description = 'Event'
    _inherit = ['mail.thread', 'mail.activity.mixin']
    _order = 'date_begin'

    name = fields.Char(string='Event',
                       translate=True,
                       required=True,
                       readonly=False,
                       states={'done': [('readonly', True)]})
    active = fields.Boolean(default=True)
    user_id = fields.Many2one('res.users',
                              string='Responsible',
                              default=lambda self: self.env.user,
                              track_visibility="onchange",
                              readonly=False,
                              states={'done': [('readonly', True)]})
    company_id = fields.Many2one('res.company',
                                 string='Company',
                                 change_default=True,
                                 default=lambda self: self.env['res.company'].
                                 _company_default_get('event.event'),
                                 required=False,
                                 readonly=False,
                                 states={'done': [('readonly', True)]})
    organizer_id = fields.Many2one(
        'res.partner',
        string='Organizer',
        track_visibility="onchange",
        default=lambda self: self.env.user.company_id.partner_id)
    event_type_id = fields.Many2one('event.type',
                                    string='Category',
                                    readonly=False,
                                    states={'done': [('readonly', True)]},
                                    oldname='type')
    color = fields.Integer('Kanban Color Index')
    event_mail_ids = fields.One2many('event.mail',
                                     'event_id',
                                     string='Mail Schedule',
                                     copy=True)

    # Seats and computation
    seats_max = fields.Integer(
        string='Maximum Attendees Number',
        oldname='register_max',
        readonly=True,
        states={
            'draft': [('readonly', False)],
            'confirm': [('readonly', False)]
        },
        help=
        "For each event you can define a maximum registration of seats(number of attendees), above this numbers the registrations are not accepted."
    )
    seats_availability = fields.Selection([('limited', 'Limited'),
                                           ('unlimited', 'Unlimited')],
                                          'Maximum Attendees',
                                          required=True,
                                          default='unlimited')
    seats_min = fields.Integer(
        string='Minimum Attendees',
        oldname='register_min',
        help=
        "For each event you can define a minimum reserved seats (number of attendees), if it does not reach the mentioned registrations the event can not be confirmed (keep 0 to ignore this rule)"
    )
    seats_reserved = fields.Integer(oldname='register_current',
                                    string='Reserved Seats',
                                    store=True,
                                    readonly=True,
                                    compute='_compute_seats')
    seats_available = fields.Integer(oldname='register_avail',
                                     string='Available Seats',
                                     store=True,
                                     readonly=True,
                                     compute='_compute_seats')
    seats_unconfirmed = fields.Integer(oldname='register_prospect',
                                       string='Unconfirmed Seat Reservations',
                                       store=True,
                                       readonly=True,
                                       compute='_compute_seats')
    seats_used = fields.Integer(oldname='register_attended',
                                string='Number of Participants',
                                store=True,
                                readonly=True,
                                compute='_compute_seats')
    seats_expected = fields.Integer(string='Number of Expected Attendees',
                                    readonly=True,
                                    compute='_compute_seats')

    # Registration fields
    registration_ids = fields.One2many('event.registration',
                                       'event_id',
                                       string='Attendees',
                                       readonly=False,
                                       states={'done': [('readonly', True)]})
    # Date fields
    date_tz = fields.Selection('_tz_get',
                               string='Timezone',
                               required=True,
                               default=lambda self: self.env.user.tz or 'UTC')
    date_begin = fields.Datetime(string='Start Date',
                                 required=True,
                                 track_visibility='onchange',
                                 states={'done': [('readonly', True)]})
    date_end = fields.Datetime(string='End Date',
                               required=True,
                               track_visibility='onchange',
                               states={'done': [('readonly', True)]})
    date_begin_located = fields.Char(string='Start Date Located',
                                     compute='_compute_date_begin_tz')
    date_end_located = fields.Char(string='End Date Located',
                                   compute='_compute_date_end_tz')

    state = fields.Selection(
        [('draft', 'Unconfirmed'), ('cancel', 'Cancelled'),
         ('confirm', 'Confirmed'), ('done', 'Done')],
        string='Status',
        default='draft',
        readonly=True,
        required=True,
        copy=False,
        help=
        "If event is created, the status is 'Draft'. If event is confirmed for the particular dates the status is set to 'Confirmed'. If the event is over, the status is set to 'Done'. If event is cancelled the status is set to 'Cancelled'."
    )
    auto_confirm = fields.Boolean(string='Autoconfirm Registrations')
    is_online = fields.Boolean('Online Event')
    address_id = fields.Many2one(
        'res.partner',
        string='Location',
        default=lambda self: self.env.user.company_id.partner_id,
        readonly=False,
        states={'done': [('readonly', True)]},
        track_visibility="onchange")
    country_id = fields.Many2one('res.country',
                                 'Country',
                                 related='address_id.country_id',
                                 store=True,
                                 readonly=False)
    twitter_hashtag = fields.Char('Twitter Hashtag')
    description = fields.Html(string='Description',
                              oldname='note',
                              translate=html_translate,
                              sanitize_attributes=False,
                              readonly=False,
                              states={'done': [('readonly', True)]})
    # badge fields
    badge_front = fields.Html(string='Badge Front')
    badge_back = fields.Html(string='Badge Back')
    badge_innerleft = fields.Html(string='Badge Inner Left')
    badge_innerright = fields.Html(string='Badge Inner Right')
    event_logo = fields.Html(string='Event Logo')

    @api.multi
    @api.depends('seats_max', 'registration_ids.state')
    def _compute_seats(self):
        """ Determine reserved, available, reserved but unconfirmed and used seats. """
        # initialize fields to 0
        for event in self:
            event.seats_unconfirmed = event.seats_reserved = event.seats_used = event.seats_available = 0
        # aggregate registrations by event and by state
        if self.ids:
            state_field = {
                'draft': 'seats_unconfirmed',
                'open': 'seats_reserved',
                'done': 'seats_used',
            }
            query = """ SELECT event_id, state, count(event_id)
                        FROM event_registration
                        WHERE event_id IN %s AND state IN ('draft', 'open', 'done')
                        GROUP BY event_id, state
                    """
            self._cr.execute(query, (tuple(self.ids), ))
            for event_id, state, num in self._cr.fetchall():
                event = self.browse(event_id)
                event[state_field[state]] += num
        # compute seats_available
        for event in self:
            if event.seats_max > 0:
                event.seats_available = event.seats_max - (
                    event.seats_reserved + event.seats_used)
            event.seats_expected = event.seats_unconfirmed + event.seats_reserved + event.seats_used

    @api.model
    def _tz_get(self):
        return [(x, x) for x in pytz.all_timezones]

    @api.one
    @api.depends('date_tz', 'date_begin')
    def _compute_date_begin_tz(self):
        if self.date_begin:
            self.date_begin_located = format_tz(
                self.with_context(use_babel=True).env,
                self.date_begin,
                tz=self.date_tz)
        else:
            self.date_begin_located = False

    @api.one
    @api.depends('date_tz', 'date_end')
    def _compute_date_end_tz(self):
        if self.date_end:
            self.date_end_located = format_tz(
                self.with_context(use_babel=True).env,
                self.date_end,
                tz=self.date_tz)
        else:
            self.date_end_located = False

    @api.onchange('is_online')
    def _onchange_is_online(self):
        if self.is_online:
            self.address_id = False

    @api.onchange('event_type_id')
    def _onchange_type(self):
        if self.event_type_id:
            self.seats_min = self.event_type_id.default_registration_min
            self.seats_max = self.event_type_id.default_registration_max
            if self.event_type_id.default_registration_max:
                self.seats_availability = 'limited'

            if self.event_type_id.auto_confirm:
                self.auto_confirm = self.event_type_id.auto_confirm

            if self.event_type_id.use_hashtag:
                self.twitter_hashtag = self.event_type_id.default_hashtag

            if self.event_type_id.use_timezone:
                self.date_tz = self.event_type_id.default_timezone

            self.is_online = self.event_type_id.is_online

            if self.event_type_id.event_type_mail_ids:
                self.event_mail_ids = [
                    (5, 0, 0)
                ] + [{
                    'template_id': line.template_id,
                    'interval_nbr': line.interval_nbr,
                    'interval_unit': line.interval_unit,
                    'interval_type': line.interval_type
                } for line in self.event_type_id.event_type_mail_ids]

    @api.constrains('seats_min', 'seats_max', 'seats_availability')
    def _check_seats_min_max(self):
        if any(event.seats_availability == 'limited'
               and event.seats_min > event.seats_max for event in self):
            raise ValidationError(
                _('Maximum attendees number should be greater than minimum attendees number.'
                  ))

    @api.constrains('seats_max', 'seats_available')
    def _check_seats_limit(self):
        if any(event.seats_availability == 'limited' and event.seats_max
               and event.seats_available < 0 for event in self):
            raise ValidationError(_('No more available seats.'))

    @api.one
    @api.constrains('date_begin', 'date_end')
    def _check_closing_date(self):
        if self.date_end < self.date_begin:
            raise ValidationError(
                _('The closing date cannot be earlier than the beginning date.'
                  ))

    @api.multi
    @api.depends('name', 'date_begin', 'date_end')
    def name_get(self):
        result = []
        for event in self:
            date_begin = fields.Datetime.from_string(event.date_begin)
            date_end = fields.Datetime.from_string(event.date_end)
            dates = [
                fields.Date.to_string(
                    fields.Datetime.context_timestamp(event, dt))
                for dt in [date_begin, date_end] if dt
            ]
            dates = sorted(set(dates))
            result.append(
                (event.id, '%s (%s)' % (event.name, ' - '.join(dates))))
        return result

    @api.model
    def create(self, vals):
        res = super(EventEvent, self).create(vals)
        if res.organizer_id:
            res.message_subscribe([res.organizer_id.id])
        if res.auto_confirm:
            res.button_confirm()
        return res

    @api.multi
    def write(self, vals):
        res = super(EventEvent, self).write(vals)
        if vals.get('organizer_id'):
            self.message_subscribe([vals['organizer_id']])
        return res

    @api.multi
    @api.returns('self', lambda value: value.id)
    def copy(self, default=None):
        self.ensure_one()
        default = dict(default or {}, name=_("%s (copy)") % (self.name))
        return super(EventEvent, self).copy(default)

    @api.one
    def button_draft(self):
        self.state = 'draft'

    @api.multi
    def button_cancel(self):
        if any('done' in event.mapped('registration_ids.state')
               for event in self):
            raise UserError(
                _("There are already attendees who attended this event. Please reset it to draft if you want to cancel this event."
                  ))
        self.registration_ids.write({'state': 'cancel'})
        self.state = 'cancel'

    @api.one
    def button_done(self):
        self.state = 'done'

    @api.one
    def button_confirm(self):
        self.state = 'confirm'

    @api.one
    def mail_attendees(self,
                       template_id,
                       force_send=False,
                       filter_func=lambda self: self.state != 'cancel'):
        for attendee in self.registration_ids.filtered(filter_func):
            self.env['mail.template'].browse(template_id).send_mail(
                attendee.id, force_send=force_send)

    @api.multi
    def _is_event_registrable(self):
        return True
Example #22
0
class Task(models.Model):
    _name = "project.task"
    _description = "Task"
    _date_name = "date_start"
    _inherit = ['mail.thread', 'ir.needaction_mixin']
    _mail_post_access = 'read'
    _order = "priority desc, sequence, date_start, name, id"

    def _get_default_partner(self):
        if 'default_project_id' in self.env.context:
            default_project_id = self.env['project.project'].browse(
                self.env.context['default_project_id'])
            return default_project_id.exists().partner_id

    def _get_default_stage_id(self):
        """ Gives default stage_id """
        project_id = self.env.context.get('default_project_id')
        if not project_id:
            return False
        return self.stage_find(project_id, [('fold', '=', False)])

    @api.multi
    def _read_group_stage_ids(self,
                              domain,
                              read_group_order=None,
                              access_rights_uid=None):
        TaskType = self.env['project.task.type']
        order = TaskType._order
        access_rights_uid = access_rights_uid or self.env.uid
        if read_group_order == 'stage_id desc':
            order = '%s desc' % order
        if 'default_project_id' in self.env.context:
            search_domain = [
                '|',
                ('project_ids', '=', self.env.context['default_project_id']),
                ('id', 'in', self.ids)
            ]
        else:
            search_domain = [('id', 'in', self.ids)]
        stage_ids = TaskType._search(search_domain,
                                     order=order,
                                     access_rights_uid=access_rights_uid)
        stages = TaskType.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])))

        return result, {stage.id: stage.fold for stage in stages}

    _group_by_full = {
        'stage_id': _read_group_stage_ids,
    }

    active = fields.Boolean(default=True)
    name = fields.Char(string='Task Title',
                       track_visibility='onchange',
                       required=True,
                       index=True)
    description = fields.Html(string='Description')
    priority = fields.Selection([('0', 'Normal'), ('1', 'High')],
                                default='0',
                                index=True)
    sequence = fields.Integer(
        string='Sequence',
        index=True,
        default=10,
        help="Gives the sequence order when displaying a list of tasks.")
    stage_id = fields.Many2one('project.task.type',
                               string='Stage',
                               track_visibility='onchange',
                               index=True,
                               default=_get_default_stage_id,
                               domain="[('project_ids', '=', project_id)]",
                               copy=False)
    tag_ids = fields.Many2many('project.tags',
                               string='Tags',
                               oldname='categ_ids')
    kanban_state = fields.Selection(
        [('normal', 'In Progress'), ('done', 'Ready for next stage'),
         ('blocked', 'Blocked')],
        string='Kanban State',
        default='normal',
        track_visibility='onchange',
        required=True,
        copy=False,
        help="A task's kanban state indicates special situations affecting it:\n"
        " * Normal is the default situation\n"
        " * Blocked indicates something is preventing the progress of this task\n"
        " * Ready for next stage indicates the task is ready to be pulled to the next stage"
    )
    create_date = fields.Datetime(index=True)
    write_date = fields.Datetime(
        index=True
    )  #not displayed in the view but it might be useful with base_action_rule module (and it needs to be defined first for that)
    date_start = fields.Datetime(string='Starting Date',
                                 default=fields.Datetime.now,
                                 index=True,
                                 copy=False)
    date_end = fields.Datetime(string='Ending Date', index=True, copy=False)
    date_assign = fields.Datetime(string='Assigning Date',
                                  index=True,
                                  copy=False,
                                  readonly=True)
    date_deadline = fields.Date(string='Deadline', index=True, copy=False)
    date_last_stage_update = fields.Datetime(string='Last Stage Update',
                                             default=fields.Datetime.now,
                                             index=True,
                                             copy=False,
                                             readonly=True)
    project_id = fields.Many2one(
        'project.project',
        string='Project',
        default=lambda self: self.env.context.get('default_project_id'),
        index=True,
        track_visibility='onchange',
        change_default=True)
    notes = fields.Text(string='Notes')
    planned_hours = fields.Float(
        string='Initially Planned Hours',
        help=
        'Estimated time to do the task, usually set by the project manager when the task is in draft state.'
    )
    remaining_hours = fields.Float(
        string='Remaining Hours',
        digits=(16, 2),
        help=
        "Total remaining time, can be re-estimated periodically by the assignee of the task."
    )
    user_id = fields.Many2one('res.users',
                              string='Assigned to',
                              default=lambda self: self.env.uid,
                              index=True,
                              track_visibility='onchange')
    partner_id = fields.Many2one('res.partner',
                                 string='Customer',
                                 default=_get_default_partner)
    manager_id = fields.Many2one('res.users',
                                 string='Project Manager',
                                 related='project_id.user_id',
                                 readonly=True)
    company_id = fields.Many2one(
        'res.company',
        string='Company',
        default=lambda self: self.env['res.company']._company_default_get())
    color = fields.Integer(string='Color Index')
    user_email = fields.Char(related='user_id.email',
                             string='User Email',
                             readonly=True)
    attachment_ids = fields.One2many(
        'ir.attachment',
        'res_id',
        domain=lambda self: [('res_model', '=', self._name)],
        auto_join=True,
        string='Attachments')
    # In the domain of displayed_image_id, we couln't use attachment_ids because a one2many is represented as a list of commands so we used res_model & res_id
    displayed_image_id = fields.Many2one(
        'ir.attachment',
        domain=
        "[('res_model', '=', 'project.task'), ('res_id', '=', id), ('mimetype', 'ilike', 'image')]",
        string='Displayed Image')
    legend_blocked = fields.Char(related='stage_id.legend_blocked',
                                 string='Kanban Blocked Explanation',
                                 readonly=True)
    legend_done = fields.Char(related='stage_id.legend_done',
                              string='Kanban Valid Explanation',
                              readonly=True)
    legend_normal = fields.Char(related='stage_id.legend_normal',
                                string='Kanban Ongoing Explanation',
                                readonly=True)

    @api.onchange('project_id')
    def _onchange_project(self):
        if self.project_id:
            self.partner_id = self.project_id.partner_id
            self.stage_id = self.stage_find(self.project_id.id,
                                            [('fold', '=', False)])
        else:
            self.partner_id = False
            self.stage_id = False

    @api.onchange('user_id')
    def _onchange_user(self):
        if self.user_id:
            self.date_start = fields.Datetime.now()

    @api.multi
    def copy(self, default=None):
        if default is None:
            default = {}
        if not default.get('name'):
            default['name'] = _("%s (copy)") % self.name
        if 'remaining_hours' not in default:
            default['remaining_hours'] = self.planned_hours
        return super(Task, self).copy(default)

    @api.constrains('date_start', 'date_end')
    def _check_dates(self):
        if any(
                self.filtered(lambda task: task.date_start and task.date_end
                              and task.date_start > task.date_end)):
            return ValidationError(
                _('Error ! Task starting date must be lower than its ending date.'
                  ))

    # Override view according to the company definition
    @api.model
    def fields_view_get(self,
                        view_id=None,
                        view_type='form',
                        toolbar=False,
                        submenu=False):
        # read uom as admin to avoid access rights issues, e.g. for portal/share users,
        # this should be safe (no context passed to avoid side-effects)
        obj_tm = self.env.user.company_id.project_time_mode_id
        tm = obj_tm and obj_tm.name or 'Hours'

        res = super(Task, self).fields_view_get(view_id=view_id,
                                                view_type=view_type,
                                                toolbar=toolbar,
                                                submenu=submenu)

        # read uom as admin to avoid access rights issues, e.g. for portal/share users,
        # this should be safe (no context passed to avoid side-effects)
        obj_tm = self.env.user.company_id.project_time_mode_id
        # using get_object to get translation value
        uom_hour = self.env.ref('product.product_uom_hour', False)
        if not obj_tm or not uom_hour or obj_tm.id == uom_hour.id:
            return res

        eview = etree.fromstring(res['arch'])

        # if the project_time_mode_id is not in hours (so in days), display it as a float field
        def _check_rec(eview):
            if eview.attrib.get('widget', '') == 'float_time':
                eview.set('widget', 'float')
            for child in eview:
                _check_rec(child)
            return True

        _check_rec(eview)

        res['arch'] = etree.tostring(eview)

        # replace reference of 'Hours' to 'Day(s)'
        for f in res['fields']:
            # TODO this NOT work in different language than english
            # the field 'Initially Planned Hours' should be replaced by 'Initially Planned Days'
            # but string 'Initially Planned Days' is not available in translation
            if 'Hours' in res['fields'][f]['string']:
                res['fields'][f]['string'] = res['fields'][f][
                    'string'].replace('Hours', obj_tm.name)
        return res

    @api.model
    def get_empty_list_help(self, help):
        self = self.with_context(
            empty_list_help_id=self.env.context.get('default_project_id'),
            empty_list_help_model='project.project',
            empty_list_help_document_name=_("tasks"))
        return super(Task, self).get_empty_list_help(help)

    # ----------------------------------------
    # Case management
    # ----------------------------------------

    def stage_find(self, section_id, domain=[], order='sequence'):
        """ Override of the base.stage method
            Parameter of the stage search taken from the lead:
            - section_id: if set, stages must belong to this section or
              be a default stage; if not set, stages must be default
              stages
        """
        # collect all section_ids
        section_ids = []
        if section_id:
            section_ids.append(section_id)
        section_ids.extend(self.mapped('project_id').ids)
        search_domain = []
        if section_ids:
            search_domain = [('|')] * (len(section_ids) - 1)
            for section_id in section_ids:
                search_domain.append(('project_ids', '=', section_id))
        search_domain += list(domain)
        # perform search, return the first found
        return self.env['project.task.type'].search(search_domain,
                                                    order=order,
                                                    limit=1).id

    # ------------------------------------------------
    # CRUD overrides
    # ------------------------------------------------

    @api.model
    def create(self, vals):
        # context: no_log, because subtype already handle this
        context = dict(self.env.context, mail_create_nolog=True)

        # for default stage
        if vals.get('project_id') and not context.get('default_project_id'):
            context['default_project_id'] = vals.get('project_id')
        # user_id change: update date_assign
        if vals.get('user_id'):
            vals['date_assign'] = fields.Datetime.now()
        task = super(Task, self.with_context(context)).create(vals)
        return task

    @api.multi
    def write(self, vals):
        now = fields.Datetime.now()
        # stage change: update date_last_stage_update
        if 'stage_id' in vals:
            vals['date_last_stage_update'] = now
            # reset kanban state when changing stage
            if 'kanban_state' not in vals:
                vals['kanban_state'] = 'normal'
        # user_id change: update date_assign
        if vals.get('user_id'):
            vals['date_assign'] = now

        result = super(Task, self).write(vals)

        return result

    # ---------------------------------------------------
    # Mail gateway
    # ---------------------------------------------------

    @api.multi
    def _track_template(self, tracking):
        res = super(Task, self)._track_template(tracking)
        test_task = self[0]
        changes, tracking_value_ids = tracking[test_task.id]
        if 'stage_id' in changes and test_task.stage_id.mail_template_id:
            res['stage_id'] = (test_task.stage_id.mail_template_id, {
                'composition_mode': 'mass_mail'
            })
        return res

    @api.multi
    def _track_subtype(self, init_values):
        self.ensure_one()
        if 'kanban_state' in init_values and self.kanban_state == 'blocked':
            return 'project.mt_task_blocked'
        elif 'kanban_state' in init_values and self.kanban_state == 'done':
            return 'project.mt_task_ready'
        elif 'user_id' in init_values and self.user_id:  # assigned -> new
            return 'project.mt_task_new'
        elif 'stage_id' in init_values and self.stage_id and self.stage_id.sequence <= 1:  # start stage -> new
            return 'project.mt_task_new'
        elif 'stage_id' in init_values:
            return 'project.mt_task_stage'
        return super(Task, self)._track_subtype(init_values)

    @api.multi
    def _notification_group_recipients(self, message, recipients, done_ids,
                                       group_data):
        """ Override the mail.thread method to handle project users and officers
        recipients. Indeed those will have specific action in their notification
        emails: creating tasks, assigning it. """
        group_project_user = self.env.ref('project.group_project_user')
        for recipient in recipients.filtered(
                lambda recipient: recipient.id not in done_ids):
            if recipient.user_ids and group_project_user in recipient.user_ids[
                    0].groups_id:
                group_data['group_project_user'] |= recipient
                done_ids.add(recipient.id)
        return super(Task, self)._notification_group_recipients(
            message, recipients, done_ids, group_data)

    @api.multi
    def _notification_get_recipient_groups(self, message, recipients):
        self.ensure_one()
        res = super(Task, self)._notification_get_recipient_groups(
            message, recipients)

        take_action = self._notification_link_helper('assign')
        new_action_id = self.env.ref('project.action_view_task').id
        new_action = self._notification_link_helper('new',
                                                    action_id=new_action_id)

        actions = []
        if not self.user_id:
            actions.append({'url': take_action, 'title': _('I take it')})
        else:
            actions.append({'url': new_action, 'title': _('New Task')})

        res['group_project_user'] = {'actions': actions}
        return res

    @api.model
    def message_get_reply_to(self, res_ids, default=None):
        """ Override to get the reply_to of the parent project. """
        tasks = self.sudo().browse(res_ids)
        project_ids = tasks.mapped('project_id').ids
        aliases = self.env['project.project'].message_get_reply_to(
            project_ids, default=default)
        return {
            task.id: aliases.get(task.project_id.id, False)
            for task in tasks
        }

    @api.multi
    def email_split(self, msg):
        email_list = tools.email_split((msg.get('to') or '') + ',' +
                                       (msg.get('cc') or ''))
        # check left-part is not already an alias
        aliases = self.mapped('project_id.alias_name')
        return filter(lambda x: x.split('@')[0] not in aliases, email_list)

    @api.model
    def message_new(self, msg, custom_values=None):
        """ Override to updates the document according to the email. """
        if custom_values is None:
            custom_values = {}
        defaults = {
            'name': msg.get('subject'),
            'planned_hours': 0.0,
            'partner_id': msg.get('author_id')
        }
        defaults.update(custom_values)

        res = super(Task, self).message_new(msg, custom_values=defaults)
        task = self.browse(res)
        email_list = task.email_split(msg)
        partner_ids = filter(
            None, task._find_partner_from_emails(email_list,
                                                 force_create=False))
        task.message_subscribe(partner_ids)
        return res

    @api.multi
    def message_update(self, msg, update_vals=None):
        """ Override to update the task according to the email. """
        if update_vals is None:
            update_vals = {}
        maps = {
            'cost': 'planned_hours',
        }
        for line in msg['body'].split('\n'):
            line = line.strip()
            res = tools.command_re.match(line)
            if res:
                match = res.group(1).lower()
                field = maps.get(match)
                if field:
                    try:
                        update_vals[field] = float(res.group(2).lower())
                    except (ValueError, TypeError):
                        pass

        email_list = self.email_split(msg)
        partner_ids = filter(
            None, self._find_partner_from_emails(email_list,
                                                 force_create=False))
        self.message_subscribe(partner_ids)
        return super(Task, self).message_update(msg, update_vals=update_vals)

    @api.multi
    def message_get_suggested_recipients(self):
        recipients = super(Task, self).message_get_suggested_recipients()
        for task in self.filtered('partner_id'):
            reason = _('Customer Email') if task.partner_id.email else _(
                'Customer')
            task._message_add_suggested_recipient(recipients,
                                                  partner=task.partner_id,
                                                  reason=reason)
        return recipients

    @api.multi
    def message_get_email_values(self, notif_mail=None):
        res = super(Task, self).message_get_email_values(notif_mail=notif_mail)
        headers = {}
        if res.get('headers'):
            try:
                headers.update(safe_eval(res['headers']))
            except Exception:
                pass
        if self.project_id:
            current_objects = filter(
                None,
                headers.get('X-Odoo-Objects', '').split(','))
            current_objects.insert(0,
                                   'project.project-%s, ' % self.project_id.id)
            headers['X-Odoo-Objects'] = ','.join(current_objects)
        if self.tag_ids:
            headers['X-Odoo-Tags'] = ','.join(self.tag_ids.mapped('name'))
        res['headers'] = repr(headers)
        return res
Example #23
0
class ResConfigSettings(models.TransientModel):
    _inherit = 'res.config.settings'

    invoice_terms = fields.Html(related='company_id.invoice_terms',
                                string="Terms & Conditions",
                                readonly=False)
Example #24
0
class ItemCodeID(models.Model):
    _name = 'item.code'
    _description = 'Item Code'
    _rec_name = 'title'

    item_id = fields.Many2one(
        'item.number',
        string='Item No',
        default=lambda self: self.env.context.get('item_id'))
    project_id = fields.Many2one(
        'project.project',
        string='Project',
        default=lambda self: self.env.context.get('project_id'))
    title = fields.Char('Item Code', required=True)

    image_medium = fields.Binary(String='image', attachment=True)
    sequence = fields.Integer('Item Code number', default=10)
    description = fields.Html('Description',
                              translate=True,
                              oldname="note",
                              help="An introductory text to your page")
    sub_ids = fields.One2many('item.sub',
                              'code_id',
                              string='Item Subs',
                              copy=True)
    Type = fields.Char('Type')
    Type_of_finish = fields.Char('Type of finish')
    Length = fields.Float('Length', required=True)
    Width = fields.Float('Width', required=True)
    Height = fields.Float('Height', required=True)
    Thick = fields.Float('Thick', required=True)
    Quantity = fields.Integer('Quantity', required=True)
    Unit = fields.Many2one('uom.uom', 'Unit Of Measure')

    @api.multi
    def _default_stage_id(self):
        """ Gives default stage_id """
        project_id = self.env.context.get('default_project_id')
        if not project_id:

            return False

        return self.stage_find(project_id, [('fold', '=', False)])

    active = fields.Boolean(
        default=True,
        help=
        "If the active field is set to False, it will allow you to hide the estimation without removing it."
    )

    stage_id = fields.Many2one(
        'project.item.type',
        string='Stage',
        ondelete='restrict',
        track_visibility='onchange',
        index=True,
        copy=False,
        domain=
        "['|', ('project_ids', '=', False), ('project_ids', '=', project_id)]",
        group_expand='_read_group_stage_ids',
        default=lambda self: self.env['project.item.type'].search([], limit=1))
    kanban_state = fields.Selection([('White', 'White'), ('Blue', 'Blue'),
                                     ('Green', 'Green'), ('Orange', 'Orange'),
                                     ('Yellow', 'Yellow')],
                                    string='Kanban State',
                                    default='Blue',
                                    compute='set_state_kanban')
    kanban_state_label = fields.Char(compute='_compute_kanban_state_label',
                                     string='Kanban State Label',
                                     track_visibility='onchange')
    legend_normal = fields.Char(related='stage_id.legend_normal',
                                string='Kanban Ongoing Explanation',
                                readonly=True,
                                related_sudo=False)
    legend_blocked = fields.Char(related='stage_id.legend_blocked',
                                 string='Kanban Blocked Explanation',
                                 readonly=True,
                                 related_sudo=False)
    legend_done = fields.Char(related='stage_id.legend_done',
                              string='Kanban Valid Explanation',
                              readonly=True,
                              related_sudo=False)
    legend_delivery = fields.Char(related='stage_id.legend_delivery',
                                  string='Kanban Delivery Explanation',
                                  readonly=True,
                                  related_sudo=False)
    legend_erection = fields.Char(related='stage_id.legend_erection',
                                  string='Kanban Erection Explanation',
                                  readonly=True,
                                  related_sudo=False)
    color = fields.Integer(string='Color Index')
    user_id = fields.Many2one('res.users',
                              string="User",
                              default=lambda self: self.env.uid)
    kanbancolor = fields.Integer('Color Index', compute="set_kanban_color")

    @api.multi
    @api.depends('stage_id')
    def set_state_kanban(self):
        j = 0
        k = 0
        y = 0
        f = 0
        for record in self:

            if record.stage_id.sequence == 2:
                record.kanban_state = 'Blue'
            if record.stage_id.sequence == 5:
                record.kanban_state = 'Green'

            if record.stage_id.sequence == 4:
                record.kanban_state = 'Orange'
            if record.stage_id.sequence == 3:
                record.kanban_state = 'Yellow'
            if record.stage_id.sequence == 1:
                record.kanban_state = 'White'
            rech_item_fin = self.env['item.code'].sudo().search([
                ('item_id', '=', record.item_id.id)
            ])
            logging.info(rech_item_fin)
            if rech_item_fin:
                for i in rech_item_fin:
                    if i.stage_id.sequence == 5:
                        j += 1
                    if i.stage_id.sequence == 4:
                        k += 1
                    if i.stage_id.sequence == 3:
                        y += 1
                    if i.stage_id.sequence == 2:
                        f += 1
            if len(rech_item_fin) == j:
                rech_item_fin[0].item_id.sudo().write({'stage_id': 5})
            elif len(rech_item_fin) == k:
                rech_item_fin[0].item_id.sudo().write({'stage_id': 4})
            elif len(rech_item_fin) == y:
                rech_item_fin[0].item_id.sudo().write({'stage_id': 3})
            elif len(rech_item_fin) == f:
                rech_item_fin[0].item_id.sudo().write({'stage_id': 2})

    def set_kanban_color(self):
        for record in self:
            kanbancolor = 0
            if record.kanban_state == 'Blue':
                kanbancolor = 4
            elif record.kanban_state == 'Green':
                kanbancolor = 10
            elif record.kanban_state == 'Orange':
                kanbancolor = 2
            elif record.kanban_state == 'Yellow':
                kanbancolor = 3
            else:
                kanbancolor = 0
            record.kanbancolor = kanbancolor

    @api.depends('stage_id', 'kanban_state')
    def _compute_kanban_state_label(self):
        for task in self:

            if task.kanban_state == 'White':
                task.kanban_state_label = task.legend_normal
            if task.kanban_state == 'Blue':
                task.kanban_state_label = task.legend_delivery
            elif task.kanban_state == 'Orange':
                task.kanban_state_label = task.legend_erection
            elif task.kanban_state == 'Yellow':
                task.kanban_state_label = task.legend_blocked
            else:
                task.kanban_state_label = task.legend_done

    @api.model
    def _read_group_stage_ids(self, stages, domain, order):
        return self.env['project.item.type'].search([])
Example #25
0
class MedicalDiagnosticReport(models.Model):

    _inherit = "medical.diagnostic.report"

    with_department = fields.Boolean(default=False)
    medical_department_header = fields.Html(readonly=True)
    signature_id = fields.Many2one("res.users.signature", readonly=True)
    occurrence_date = fields.Datetime(related="encounter_id.create_date")
    encounter_id = fields.Many2one(readonly=True)
    image_ids = fields.One2many(
        "medical.diagnostic.report.image",
        inverse_name="diagnostic_report_id",
        copy=True,
        readonly=True,
    )

    def _generate_serializer(self):
        result = super(MedicalDiagnosticReport, self)._generate_serializer()
        if self.with_department:
            result.update(
                {"medical_department_header": self.medical_department_header})
        if self.image_ids:
            result.update({
                "images":
                [image._generate_serializer() for image in self.image_ids]
            })
        if self.signature_id:
            result.update({"signature_id": self.signature_id.id})
        return result

    def registered2final_change_state(self):
        res = super().registered2final_change_state()
        if not self.medical_department_id.without_practitioner:
            res["signature_id"] = self.env.user.current_signature_id.id
        return res

    def _is_editable(self):
        department = self.medical_department_id
        return super()._is_editable() and (not department or self.env.user
                                           in department.user_ids)

    @api.depends_context("uid")
    @api.depends("medical_department_id", "medical_department_id.user_ids")
    def _compute_is_editable(self):
        super()._compute_is_editable()

    def _is_cancellable(self):
        department = self.medical_department_id
        return super()._is_cancellable() and (not department or self.env.user
                                              in department.user_ids)

    @api.depends_context("uid")
    @api.depends("medical_department_id", "medical_department_id.user_ids")
    def _compute_is_cancellable(self):
        super()._compute_is_cancellable()

    def copy_action(self):
        self.ensure_one()
        result = self.copy()
        return result.get_formview_action()

    def _add_image_attachment_vals(self, name=None, datas=None, **kwargs):
        return {
            "diagnostic_report_id": self.id,
            "data": datas,
            "name": name,
        }

    def add_image_attachment(self, name=None, datas=None, **kwargs):
        self.ensure_one()
        if self.state != "registered":
            raise ValidationError(_("State must be registered"))
        self.env["medical.diagnostic.report.image"].create(
            self._add_image_attachment_vals(name=name, datas=datas, **kwargs))
        return True

    def _get_image_grouped(self):
        self.ensure_one()
        lst = self.image_ids.ids
        n = 2
        return [
            self.env["medical.diagnostic.report.image"].browse(lst[i:i + n])
            for i in range(0, len(lst), n)
        ]
Example #26
0
class ItemNumber(models.Model):
    _name = 'item.number'
    _description = 'Items Records for Projects Lines'
    _inherit = ['portal.mixin', 'mail.thread', 'mail.activity.mixin']
    _rec_name = 'title'

    def _default_stage_id(self):
        """ Gives default stage_id """
        project_id = self.env.context.get('default_project_id')
        if not project_id:
            return False
        return self.stage_find(project_id, [('fold', '=', False)])

    title = fields.Char('Item No', required=True)
    drawing_id = fields.Many2one('construction.drawing', 'Drawing')
    project_id = fields.Many2one(related='drawing_id.project_id',
                                 store=True,
                                 string='Project',
                                 readonly=True)
    pricing_id = fields.Many2one(
        'construction.pricing',
        String='Pricing',
        default=lambda self: self.env.context.get('drawing_id'))
    Type = fields.Char('Type')
    image_medium = fields.Binary(String='image', attachment=True)
    Type_of_finish = fields.Char('Type of finish')
    Length = fields.Float('Length', required=True)
    Width = fields.Float('Width', required=True)
    Height = fields.Float('Height', required=True)
    Thick = fields.Float('Thick', required=True)
    Quantity = fields.Integer('Quantity', required=True)
    Volume = fields.Float('Volume', compute='_compute_total', required=True)
    Unit = fields.Many2one('uom.uom', 'Unit Of Measure')

    # get the unit pricing for each department
    UR_production = fields.Float(String='UR Production',
                                 compute='onchange_pricing_id')
    UR_delivery = fields.Float(String='UR Delivery',
                               compute='onchange_pricing_id')
    UR_erection = fields.Float(String='UR Erection',
                               compute='onchange_pricing_id')

    #get the amount of departments
    Amount_prod = fields.Float(String='Amount Production',
                               compute='_compute_total_production',
                               required=True)
    Amount_deli = fields.Float(String='Amount Delivery',
                               compute='_compute_total_delivery',
                               required=True)
    Amount_erec = fields.Float(String='Amount Erection',
                               compute='_compute_total_erection',
                               required=True)

    # get the Total unit sum of three units
    UR_total = fields.Float(String='Unit Rate Total',
                            compute='_compute_total_UR',
                            required=True)
    Amount_total = fields.Float(String='Amount Total',
                                compute='_compute_total_amount',
                                required=True)

    # the unit rate of project with currency
    currency_id = fields.Many2one("res.currency",
                                  compute='get_currency_id',
                                  string="Currency")
    Unit_Production = fields.Float(String='Unit Production',
                                   compute='_compute_unit_production',
                                   required=True)
    Unit_Delivery = fields.Float(String='Unit Delivery',
                                 compute='_compute_unit_delivery',
                                 required=True)
    Unit_Erection = fields.Float(String='Unit Erection',
                                 compute='_compute_unit_erection',
                                 required=True)
    active = fields.Boolean(
        default=True,
        help=
        "If the active field is set to False, it will allow you to hide the estimation without removing it."
    )

    stage_id = fields.Many2one(
        'project.item.type',
        string='Stage',
        ondelete='restrict',
        track_visibility='onchange',
        index=True,
        copy=False,
        domain=
        "['|', ('project_ids', '=', False), ('project_ids', '=', project_id)]",
        group_expand='_read_group_stage_ids',
        default=lambda self: self.env['project.item.type'].search([], limit=1))
    kanban_state = fields.Selection([('White', 'White'), ('Blue', 'Blue'),
                                     ('Green', 'Green'), ('Orange', 'Orange'),
                                     ('Yellow', 'Yellow')],
                                    string='Kanban State',
                                    default='Blue',
                                    compute='set_state_kanban')
    kanban_state_label = fields.Char(compute='_compute_kanban_state_label',
                                     string='Kanban State Label',
                                     track_visibility='onchange')
    legend_normal = fields.Char(related='stage_id.legend_normal',
                                string='Kanban Ongoing Explanation',
                                readonly=True,
                                related_sudo=False)
    legend_blocked = fields.Char(related='stage_id.legend_blocked',
                                 string='Kanban Blocked Explanation',
                                 readonly=True,
                                 related_sudo=False)
    legend_done = fields.Char(related='stage_id.legend_done',
                              string='Kanban Valid Explanation',
                              readonly=True,
                              related_sudo=False)
    legend_delivery = fields.Char(related='stage_id.legend_delivery',
                                  string='Kanban Delivery Explanation',
                                  readonly=True,
                                  related_sudo=False)
    legend_erection = fields.Char(related='stage_id.legend_erection',
                                  string='Kanban Erection Explanation',
                                  readonly=True,
                                  related_sudo=False)
    color = fields.Integer(string='Color Index')
    user_id = fields.Many2one('res.users',
                              string="User",
                              default=lambda self: self.env.uid)
    item_code_ids = fields.One2many('item.code',
                                    'item_id',
                                    string='Code Items')
    sub_item_ids = fields.One2many('item.sub', 'item_id', string="Sub Items")
    sub_items_status = fields.Html(string="SUBItems",
                                   compute='get_all_sub_items')
    kanbancolor = fields.Integer('Color Index', compute="set_kanban_color")

    @api.multi
    @api.depends('stage_id')
    def set_state_kanban(self):
        j = 0
        for record in self:
            if record.stage_id.sequence == 2:
                record.kanban_state = 'Blue'
            if record.stage_id.sequence == 5:
                record.kanban_state = 'Green'

            if record.stage_id.sequence == 4:
                record.kanban_state = 'Orange'
            if record.stage_id.sequence == 3:
                record.kanban_state = 'Yellow'
            if record.stage_id.sequence == 1:
                record.kanban_state = 'White'

            # for i in record.item_code_ids :
            #      if i.stage_id.sequence == 4:
            #              j += 1
            #         # if i.stage_id.sequence == 3:
            #         #         k += 1
            #         # if i.stage_id.sequence == 2:
            #         #         y += 1
            #         # if i.stage_id.sequence == 1:
            #         #         f += 1
            # if len(record.item_code_ids):

            #     if len(record.item_code_ids)==j:
            #         record.sudo().write({'stage_id':4})
            #         record.kanban_state='Green'

    def set_kanban_color(self):
        for record in self:
            kanbancolor = 0
            if record.kanban_state == 'Blue':
                kanbancolor = 4
            elif record.kanban_state == 'Green':
                kanbancolor = 10
            elif record.kanban_state == 'Orange':
                kanbancolor = 2
            elif record.kanban_state == 'Yellow':
                kanbancolor = 3
            else:
                kanbancolor = 0
            record.kanbancolor = kanbancolor

    @api.multi
    @api.depends('project_id', 'sub_item_ids')
    def get_all_sub_items(self):
        for rec in self:
            if rec.sub_item_ids:
                sub_items = []
                body = """<table  style='width: 100%;'>"""
                body += """<tr>"""
                body += """<td style="width: 50%;border: 1px solid grey;">""" + 'Item Name' + """</td>"""
                body += """<td style="width: 50%;border: 1px solid grey;">""" + 'Stage' + """</td>"""
                body += """</tr>"""
                for data in rec.sub_item_ids:
                    body += """<tr>"""
                    body += """<td style="width: 50%;border: 1px solid grey;">""" + data.title + """</td>"""
                    body += """<td style="width: 50%;border: 1px solid grey;">""" + data.stage_id.name + """</td>"""
                    body += """</tr>"""
                body += "</table>"
                rec.sub_items_status = body

    @api.multi
    def action_open_sub_item(self):
        self.ensure_one()
        return {
            'name': _('Sub Item'),
            'domain': [('item_id', '=', self.id)],
            'view_type': 'form',
            'res_model': 'item.sub',
            'view_id': False,
            'view_mode': 'tree,form',
            'type': 'ir.actions.act_window',
            'context': "{'default_item_id': %d}" % (self.id)
        }

    @api.depends('stage_id', 'kanban_state')
    def _compute_kanban_state_label(self):
        for task in self:

            if task.kanban_state == 'White':
                task.kanban_state_label = task.legend_normal
            if task.kanban_state == 'Blue':
                task.kanban_state_label = task.legend_delivery
            elif task.kanban_state == 'Orange':
                task.kanban_state_label = task.legend_erection
            elif task.kanban_state == 'Yellow':
                task.kanban_state_label = task.legend_blocked
            else:
                task.kanban_state_label = task.legend_done

    @api.model
    def _read_group_stage_ids(self, stages, domain, order):
        return self.env['project.item.type'].search([])

    '''
    # get the unit rate price from pricing or this project
    @api.multi
    @api.onchange('pricing_id')
    def onchange_pricing_id(self):
        res = {}
        if not self.pricing_id:
            return res
        self.UR_production = self.pricing_id.UR_production
        self.UR_delivery = self.pricing_id.UR_delivery
        self.UR_erection = self.pricing_id.UR_erection
    '''

    # get the unit rate price from pricing or this project
    @api.multi
    @api.depends('pricing_id')
    def onchange_pricing_id(self):
        res = {}
        for record in self:
            if not record.pricing_id:
                return res
            record.UR_production = record.pricing_id.UR_production
            record.UR_delivery = record.pricing_id.UR_delivery
            record.UR_erection = record.pricing_id.UR_erection

    #open the item codes
    @api.multi
    def open_bom(self):
        self.ensure_one()
        ctx = {
            'default_item_id': self.id,
            'default_project_id': self.project_id.id,
            'default_title': self.title,
        }
        return {
            'type': 'ir.actions.act_window',
            'view_type': 'form',
            'view_mode': 'form',
            'res_model': 'item.code',
            'target': 'new',
            'context': ctx,
        }

    #Compute Volume of an item
    @api.multi
    @api.depends('Length', 'Width', 'Height')
    def _compute_total(self):
        for rec in self:
            rec.Volume = rec.Length * rec.Width * rec.Height

    #Compute the total units in the pricing
    @api.multi
    @api.depends('UR_production', 'UR_delivery', 'UR_erection')
    def _compute_total_UR(self):
        for rec in self:
            rec.UR_total = rec.UR_production + rec.UR_delivery + rec.UR_erection

    #Compute the total amounts of departments
    @api.multi
    @api.depends('Amount_prod', 'Amount_deli', 'Amount_erec')
    def _compute_total_amount(self):
        for rec in self:
            rec.Amount_total = rec.Amount_prod + rec.Amount_deli + rec.Amount_erec

    @api.multi
    @api.depends('Amount_prod', 'Quantity', 'Unit_Production')
    def _compute_total_production(self):
        for rec in self:
            rec.Amount_prod = rec.Quantity * rec.Unit_Production

    @api.multi
    @api.depends('Amount_deli', 'Unit_Delivery', 'Quantity')
    def _compute_total_delivery(self):
        for rec in self:
            rec.Amount_deli = rec.Quantity * rec.Unit_Delivery

    @api.multi
    @api.depends('Amount_erec', 'Unit_Erection', 'Quantity')
    def _compute_total_erection(self):
        for rec in self:
            rec.Amount_erec = rec.Quantity * rec.Unit_Erection

    # Compute Unit rates of amount
    @api.multi
    @api.depends('Unit_Production', 'UR_production', 'Volume')
    def _compute_unit_production(self):
        for rec in self:
            rec.Unit_Production = rec.UR_production * rec.Volume

    @api.multi
    @api.depends('Unit_Delivery', 'UR_delivery', 'Volume')
    def _compute_unit_delivery(self):
        for rec in self:
            rec.Unit_Delivery = rec.UR_delivery * rec.Volume

    @api.multi
    @api.depends('Unit_Erection', 'UR_erection', 'Volume')
    def _compute_unit_erection(self):
        for rec in self:
            rec.Unit_Erection = rec.UR_erection * rec.Volume

    @api.multi
    def get_currency_id(self):
        user_id = self.env.uid
        res_user_id = self.env['res.users'].browse(user_id)
        for line in self:
            line.currency_id = res_user_id.company_id.currency_id.id
Example #27
0
class BeginBalanceCheck(models.TransientModel):
    '''启用期初试算平衡向导'''
    _name = 'accountcore.begin_balance_check'
    org_ids = fields.Many2many('accountcore.org',
                               string='待检查机构',
                               required=True,
                               default=lambda s: s.env.user.currentOrg)
    result = fields.Html(string='检查结果')

    @api.multi
    def do_check(self, *args):
        '''对选中机构执行平衡检查'''
        self.ensure_one()
        check_result = {}
        result_htmlStr = ''
        for org in self.org_ids:
            check_result[org.name] = self._check(org)
        for (key, value) in check_result.items():
            result_htmlStr = result_htmlStr+"<h6>" + \
                key+"</h6>"+"".join([v[1] for v in value])
        self.result = result_htmlStr
        return {
            'name': '启用期初平衡检查',
            'type': 'ir.actions.act_window',
            'view_type': 'form',
            'view_mode': 'form',
            'target': 'new',
            'res_model': 'accountcore.begin_balance_check',
            'res_id': self.id,
        }

    def _check(self, org):
        '''对一个机构执行平衡检查'''
        rl = []
        # 获得机构期初
        balance_records = AccountsBalance.getBeginOfOrg(org)
        # 检查月初本年累计发生额借方合计=贷方合计
        rl.append(self._checkCumulativeAmountBalance(balance_records))
        # 检查月初余额借方合计=贷方合计
        rl.append(self._checkBeginingAmountBalance(balance_records))
        # 检查月已发生额借方合计=贷方合计
        rl.append(self._checkAmountBalance(balance_records))
        # 检查资产=负债+所有者权益+收入-理论
        rl.append(self._checkBalance(balance_records))
        return rl

    def _checkCumulativeAmountBalance(self, balance_records):
        '''检查月初本年累计发生额借方合计'''
        damount = AccountsBalance._sumFieldOf('cumulativeDamount',
                                              balance_records)
        camount = AccountsBalance._sumFieldOf('cumulativeCamount',
                                              balance_records)
        imbalanceAmount = damount - camount
        if imbalanceAmount == 0:
            rl_html = "<div><span class='text-success fa fa-check'></span>月初本年借方累计发生额=月初本年贷方累计发生额[" + \
                str(damount) + "="+str(camount)+"]</div>"
            return (True, rl_html)
        else:
            rl_html = "<div><span class='text-danger fa fa-close'></span>月初本年借方累计发生额合计=月初本年贷方累计发生额合计[" + \
                str(damount)+"-" + str(camount) + \
                "="+str(imbalanceAmount)+"]</div>"
            return (False, rl_html)

    def _checkBeginingAmountBalance(self, balance_records):
        '''检查月初余额借方合计'''
        damount = AccountsBalance._sumFieldOf('beginingDamount',
                                              balance_records)
        camount = AccountsBalance._sumFieldOf('beginingCamount',
                                              balance_records)
        imbalanceAmount = damount - camount
        if imbalanceAmount == 0:
            rl_html = "<div><span class='text-success fa fa-check'></span>月初借方余额合计=月初贷方贷方余额合计[" + \
                str(damount) + "=" + str(camount) + "]</div>"
            return (True, rl_html)
        else:
            rl_html = "<div><span class='text-danger fa fa-close'></span>月初借方余额合计=月初贷方余额合计[" +  \
                str(damount) + "-" + str(camount) + \
                "="+str(imbalanceAmount)+"]</div>"
            return (False, rl_html)

    def _checkAmountBalance(self, balance_records):
        '''检查月已发生额借方合计'''
        damount = AccountsBalance._sumFieldOf('damount', balance_records)
        camount = AccountsBalance._sumFieldOf('camount', balance_records)
        imbalanceAmount = damount - camount
        if imbalanceAmount == 0:
            rl_html = "<div><span class='text-success fa fa-check'></span>月借方已发生额合计=月贷方已发生额合计[" + \
                str(damount) + "=" + str(camount) + "]</div>"
            return (True, rl_html)
        else:
            rl_html = "<div><span class='text-danger fa fa-exclamation'></span>月借方已发生额合计=月贷方已发生额合计[" + \
                str(damount) + "-" + str(camount) + \
                "="+str(imbalanceAmount)+"]</div>"
            return (False, rl_html)

    def _checkBalance(self, balance_records):
        '''检查资产=负债+所有者权益+收入-成本'''
        return (True, ".....")
Example #28
0
class ResConfigSettings(models.TransientModel):
    _inherit = 'res.config.settings'

    has_accounting_entries = fields.Boolean(
        compute='_compute_has_chart_of_accounts')
    currency_id = fields.Many2one('res.currency',
                                  related="company_id.currency_id",
                                  required=True,
                                  readonly=False,
                                  string='Currency',
                                  help="Main currency of the company.")
    currency_exchange_journal_id = fields.Many2one(
        comodel_name='account.journal',
        related='company_id.currency_exchange_journal_id',
        readonly=False,
        string="Currency Exchange Journal",
        domain="[('company_id', '=', company_id), ('type', '=', 'general')]",
        help=
        'The accounting journal where automatic exchange differences will be registered'
    )
    income_currency_exchange_account_id = fields.Many2one(
        comodel_name="account.account",
        related="company_id.income_currency_exchange_account_id",
        string="Gain Account",
        readonly=False,
        domain=lambda self:
        "[('internal_type', '=', 'other'), ('deprecated', '=', False), ('company_id', '=', company_id),\
                             ('user_type_id', 'in', %s)]" % [
            self.env.ref('account.data_account_type_revenue').id,
            self.env.ref('account.data_account_type_other_income').id
        ])
    expense_currency_exchange_account_id = fields.Many2one(
        comodel_name="account.account",
        related="company_id.expense_currency_exchange_account_id",
        string="Loss Account",
        readonly=False,
        domain=lambda self:
        "[('internal_type', '=', 'other'), ('deprecated', '=', False), ('company_id', '=', company_id),\
                             ('user_type_id', '=', %s)]" % self.env.ref(
            'account.data_account_type_expenses').id)
    has_chart_of_accounts = fields.Boolean(
        compute='_compute_has_chart_of_accounts',
        string='Company has a chart of accounts')
    chart_template_id = fields.Many2one(
        'account.chart.template',
        string='Template',
        default=lambda self: self.env.company.chart_template_id,
        domain="[('visible','=', True)]")
    sale_tax_id = fields.Many2one('account.tax',
                                  string="Default Sale Tax",
                                  related='company_id.account_sale_tax_id',
                                  readonly=False)
    purchase_tax_id = fields.Many2one(
        'account.tax',
        string="Default Purchase Tax",
        related='company_id.account_purchase_tax_id',
        readonly=False)
    tax_calculation_rounding_method = fields.Selection(
        related='company_id.tax_calculation_rounding_method',
        string='Tax calculation rounding method',
        readonly=False)
    module_account_accountant = fields.Boolean(string='Accounting')
    group_analytic_accounting = fields.Boolean(
        string='Analytic Accounting',
        implied_group='analytic.group_analytic_accounting')
    group_analytic_tags = fields.Boolean(
        string='Analytic Tags', implied_group='analytic.group_analytic_tags')
    group_warning_account = fields.Boolean(
        string="Warnings in Invoices",
        implied_group='account.group_warning_account')
    group_cash_rounding = fields.Boolean(
        string="Cash Rounding", implied_group='account.group_cash_rounding')
    # group_show_line_subtotals_tax_excluded and group_show_line_subtotals_tax_included are opposite,
    # so we can assume exactly one of them will be set, and not the other.
    # We need both of them to coexist so we can take advantage of automatic group assignation.
    group_show_line_subtotals_tax_excluded = fields.Boolean(
        "Show line subtotals without taxes (B2B)",
        implied_group='account.group_show_line_subtotals_tax_excluded',
        group='base.group_portal,base.group_user,base.group_public')
    group_show_line_subtotals_tax_included = fields.Boolean(
        "Show line subtotals with taxes (B2C)",
        implied_group='account.group_show_line_subtotals_tax_included',
        group='base.group_portal,base.group_user,base.group_public')
    group_show_sale_receipts = fields.Boolean(
        string='Sale Receipt', implied_group='account.group_sale_receipts')
    group_show_purchase_receipts = fields.Boolean(
        string='Purchase Receipt',
        implied_group='account.group_purchase_receipts')
    show_line_subtotals_tax_selection = fields.Selection(
        [('tax_excluded', 'Tax-Excluded'), ('tax_included', 'Tax-Included')],
        string="Line Subtotals Tax Display",
        required=True,
        default='tax_excluded',
        config_parameter='account.show_line_subtotals_tax_selection')
    module_account_budget = fields.Boolean(string='Budget Management')
    module_account_payment = fields.Boolean(string='Invoice Online Payment')
    module_account_reports = fields.Boolean("Dynamic Reports")
    module_account_check_printing = fields.Boolean(
        "Allow check printing and deposits")
    module_account_batch_payment = fields.Boolean(
        string='Use batch payments',
        help=
        'This allows you grouping payments into a single batch and eases the reconciliation process.\n'
        '-This installs the account_batch_payment module.')
    module_account_sepa = fields.Boolean(string='SEPA Credit Transfer (SCT)')
    module_account_sepa_direct_debit = fields.Boolean(
        string='Use SEPA Direct Debit')
    module_account_plaid = fields.Boolean(string="Plaid Connector")
    module_account_yodlee = fields.Boolean(
        "Bank Interface - Sync your bank feeds automatically")
    module_account_bank_statement_import_qif = fields.Boolean(
        "Import .qif files")
    module_account_bank_statement_import_ofx = fields.Boolean(
        "Import in .ofx format")
    module_account_bank_statement_import_csv = fields.Boolean(
        "Import in .csv format")
    module_account_bank_statement_import_camt = fields.Boolean(
        "Import in CAMT.053 format")
    module_currency_rate_live = fields.Boolean(
        string="Automatic Currency Rates")
    module_account_intrastat = fields.Boolean(string='Intrastat')
    module_product_margin = fields.Boolean(string="Allow Product Margin")
    module_l10n_eu_service = fields.Boolean(string="EU Digital Goods VAT")
    module_account_taxcloud = fields.Boolean(string="Account TaxCloud")
    module_account_invoice_extract = fields.Boolean(
        string="Bill Digitalization")
    module_snailmail_account = fields.Boolean(string="Snailmail")
    tax_exigibility = fields.Boolean(string='Cash Basis',
                                     related='company_id.tax_exigibility',
                                     readonly=False)
    tax_cash_basis_journal_id = fields.Many2one(
        'account.journal',
        related='company_id.tax_cash_basis_journal_id',
        string="Tax Cash Basis Journal",
        readonly=False)
    account_cash_basis_base_account_id = fields.Many2one(
        comodel_name='account.account',
        string="Base Tax Received Account",
        readonly=False,
        related='company_id.account_cash_basis_base_account_id',
        domain=[('deprecated', '=', False)])

    qr_code = fields.Boolean(string='Display SEPA QR-code',
                             related='company_id.qr_code',
                             readonly=False)
    invoice_is_print = fields.Boolean(string='Print',
                                      related='company_id.invoice_is_print',
                                      readonly=False)
    invoice_is_email = fields.Boolean(string='Send Email',
                                      related='company_id.invoice_is_email',
                                      readonly=False)
    incoterm_id = fields.Many2one(
        'account.incoterms',
        string='Default incoterm',
        related='company_id.incoterm_id',
        help=
        'International Commercial Terms are a series of predefined commercial terms used in international transactions.',
        readonly=False)
    invoice_terms = fields.Text(related='company_id.invoice_terms',
                                string="Terms & Conditions",
                                readonly=False)
    invoice_terms_html = fields.Html(related='company_id.invoice_terms_html',
                                     string="Terms & Conditions as a Web page",
                                     readonly=False)
    terms_type = fields.Selection(related='company_id.terms_type',
                                  readonly=False)
    preview_ready = fields.Boolean(string="Display preview button",
                                   compute='_compute_terms_preview')

    use_invoice_terms = fields.Boolean(
        string='Default Terms & Conditions',
        config_parameter='account.use_invoice_terms')

    # Technical field to hide country specific fields from accounting configuration
    country_code = fields.Char(related='company_id.country_id.code',
                               readonly=True)

    def set_values(self):
        super(ResConfigSettings, self).set_values()
        if self.group_multi_currency:
            self.env.ref('base.group_user').write({
                'implied_ids':
                [(4, self.env.ref('product.group_sale_pricelist').id)]
            })
        # install a chart of accounts for the given company (if required)
        if self.env.company == self.company_id and self.chart_template_id and self.chart_template_id != self.company_id.chart_template_id:
            self.chart_template_id._load(15.0, 15.0, self.env.company)

    @api.depends('company_id')
    def _compute_has_chart_of_accounts(self):
        self.has_chart_of_accounts = bool(self.company_id.chart_template_id)
        self.has_accounting_entries = self.env[
            'account.chart.template'].existing_accounting(self.company_id)

    @api.onchange('show_line_subtotals_tax_selection')
    def _onchange_sale_tax(self):
        if self.show_line_subtotals_tax_selection == "tax_excluded":
            self.update({
                'group_show_line_subtotals_tax_included': False,
                'group_show_line_subtotals_tax_excluded': True,
            })
        else:
            self.update({
                'group_show_line_subtotals_tax_included': True,
                'group_show_line_subtotals_tax_excluded': False,
            })

    @api.onchange('group_analytic_accounting')
    def onchange_analytic_accounting(self):
        if self.group_analytic_accounting:
            self.module_account_accountant = True

    @api.onchange('module_account_budget')
    def onchange_module_account_budget(self):
        if self.module_account_budget:
            self.group_analytic_accounting = True

    @api.onchange('module_account_yodlee')
    def onchange_account_yodlee(self):
        if self.module_account_yodlee:
            self.module_account_plaid = True

    @api.onchange('tax_exigibility')
    def _onchange_tax_exigibility(self):
        res = {}
        tax = self.env['account.tax'].search(
            [('company_id', '=', self.env.company.id),
             ('tax_exigibility', '=', 'on_payment')],
            limit=1)
        if not self.tax_exigibility and tax:
            self.tax_exigibility = True
            res['warning'] = {
                'title':
                _('Error!'),
                'message':
                _('You cannot disable this setting because some of your taxes are cash basis. '
                  'Modify your taxes first before disabling this setting.')
            }
        return res

    @api.depends('terms_type')
    def _compute_terms_preview(self):
        for setting in self:
            # We display the preview button only if the terms_type is html in the setting but also on the company
            # to avoid landing on an error page (see terms.py controller)
            setting.preview_ready = self.env.company.terms_type == 'html' and setting.terms_type == 'html'

    @api.model
    def create(self, values):
        # Optimisation purpose, saving a res_config even without changing any values will trigger the write of all
        # related values, including the currency_id field on res_company. This in turn will trigger the recomputation
        # of account_move_line related field company_currency_id which can be slow depending on the number of entries
        # in the database. Thus, if we do not explicitly change the currency_id, we should not write it on the company
        if ('company_id' in values and 'currency_id' in values):
            company = self.env['res.company'].browse(values.get('company_id'))
            if company.currency_id.id == values.get('currency_id'):
                values.pop('currency_id')
        return super(ResConfigSettings, self).create(values)
Example #29
0
class MassMailing(models.Model):
    """ MassMailing models a wave of emails for a mass mailign campaign.
    A mass mailing is an occurence of sending emails. """

    _name = 'mail.mass_mailing'
    _description = 'Mass Mailing'
    # number of periods for tracking mail_mail statistics
    _period_number = 6
    _order = 'sent_date DESC'
    _inherits = {'utm.source': 'source_id'}
    _rec_name = "source_id"

    @api.model
    def _get_default_mail_server_id(self):
        server_id = self.env['ir.config_parameter'].sudo().get_param('mass_mailing.mail_server_id')
        try:
            server_id = literal_eval(server_id) if server_id else False
            return self.env['ir.mail_server'].search([('id', '=', server_id)]).id
        except ValueError:
            return False

    @api.model
    def default_get(self, fields):
        res = super(MassMailing, self).default_get(fields)
        if 'reply_to_mode' in fields and not 'reply_to_mode' in res and res.get('mailing_model_real'):
            if res['mailing_model_real'] in ['res.partner', 'mail.mass_mailing.contact']:
                res['reply_to_mode'] = 'email'
            else:
                res['reply_to_mode'] = 'thread'
        return res

    active = fields.Boolean(default=True)
    subject = fields.Char('Subject', help='Subject of emails to send', required=True)
    email_from = fields.Char(string='From', required=True,
        default=lambda self: self.env['mail.message']._get_default_from())
    sent_date = fields.Datetime(string='Sent Date', oldname='date', copy=False)
    schedule_date = fields.Datetime(string='Schedule in the Future')
    # don't translate 'body_arch', the translations are only on 'body_html'
    body_arch = fields.Html(string='Body', translate=False)
    body_html = fields.Html(string='Body converted to be send by mail', sanitize_attributes=False)
    attachment_ids = fields.Many2many('ir.attachment', 'mass_mailing_ir_attachments_rel',
        'mass_mailing_id', 'attachment_id', string='Attachments')
    keep_archives = fields.Boolean(string='Keep Archives')
    mass_mailing_campaign_id = fields.Many2one('mail.mass_mailing.campaign', string='Mass Mailing Campaign')
    campaign_id = fields.Many2one('utm.campaign', string='Campaign',
                                  help="This name helps you tracking your different campaign efforts, e.g. Fall_Drive, Christmas_Special")
    source_id = fields.Many2one('utm.source', string='Source', required=True, ondelete='cascade',
                                help="This is the link source, e.g. Search Engine, another domain, or name of email list")
    medium_id = fields.Many2one('utm.medium', string='Medium',
                                help="This is the delivery method, e.g. Postcard, Email, or Banner Ad", default=lambda self: self.env.ref('utm.utm_medium_email'))
    clicks_ratio = fields.Integer(compute="_compute_clicks_ratio", string="Number of Clicks")
    state = fields.Selection([('draft', 'Draft'), ('in_queue', 'In Queue'), ('sending', 'Sending'), ('done', 'Sent')],
        string='Status', required=True, copy=False, default='draft', group_expand='_group_expand_states')
    color = fields.Integer(string='Color Index')
    user_id = fields.Many2one('res.users', string='Responsible', default=lambda self: self.env.user)
    # mailing options
    reply_to_mode = fields.Selection(
        [('thread', 'Recipient Followers'), ('email', 'Specified Email Address')], string='Reply-To Mode', required=True)
    reply_to = fields.Char(string='Reply To', help='Preferred Reply-To Address',
        default=lambda self: self.env['mail.message']._get_default_from())
    # recipients
    mailing_model_real = fields.Char(compute='_compute_model', string='Recipients Real Model', default='mail.mass_mailing.contact', required=True)
    mailing_model_id = fields.Many2one('ir.model', string='Recipients Model', domain=[('model', 'in', MASS_MAILING_BUSINESS_MODELS)],
        default=lambda self: self.env.ref('mass_mailing.model_mail_mass_mailing_list').id)
    mailing_model_name = fields.Char(related='mailing_model_id.model', string='Recipients Model Name', readonly=True, related_sudo=True)
    mailing_domain = fields.Char(string='Domain', oldname='domain', default=[])
    mail_server_id = fields.Many2one('ir.mail_server', string='Mail Server',
        default=_get_default_mail_server_id,
        help="Use a specific mail server in priority. Otherwise Odoo relies on the first outgoing mail server available (based on their sequencing) as it does for normal mails.")
    contact_list_ids = fields.Many2many('mail.mass_mailing.list', 'mail_mass_mailing_list_rel',
        string='Mailing Lists')
    contact_ab_pc = fields.Integer(string='A/B Testing percentage',
        help='Percentage of the contacts that will be mailed. Recipients will be taken randomly.', default=100)
    # statistics data
    statistics_ids = fields.One2many('mail.mail.statistics', 'mass_mailing_id', string='Emails Statistics')
    total = fields.Integer(compute="_compute_total")
    scheduled = fields.Integer(compute="_compute_statistics")
    expected = fields.Integer(compute="_compute_statistics")
    ignored = fields.Integer(compute="_compute_statistics")
    sent = fields.Integer(compute="_compute_statistics")
    delivered = fields.Integer(compute="_compute_statistics")
    opened = fields.Integer(compute="_compute_statistics")
    clicked = fields.Integer(compute="_compute_statistics")
    replied = fields.Integer(compute="_compute_statistics")
    bounced = fields.Integer(compute="_compute_statistics")
    failed = fields.Integer(compute="_compute_statistics")
    received_ratio = fields.Integer(compute="_compute_statistics", string='Received Ratio')
    opened_ratio = fields.Integer(compute="_compute_statistics", string='Opened Ratio')
    replied_ratio = fields.Integer(compute="_compute_statistics", string='Replied Ratio')
    bounced_ratio = fields.Integer(compute="_compute_statistics", String='Bounced Ratio')
    next_departure = fields.Datetime(compute="_compute_next_departure", string='Scheduled date')

    def _compute_total(self):
        for mass_mailing in self:
            mass_mailing.total = len(mass_mailing.sudo().get_recipients())

    def _compute_clicks_ratio(self):
        self.env.cr.execute("""
            SELECT COUNT(DISTINCT(stats.id)) AS nb_mails, COUNT(DISTINCT(clicks.mail_stat_id)) AS nb_clicks, stats.mass_mailing_id AS id
            FROM mail_mail_statistics AS stats
            LEFT OUTER JOIN link_tracker_click AS clicks ON clicks.mail_stat_id = stats.id
            WHERE stats.mass_mailing_id IN %s
            GROUP BY stats.mass_mailing_id
        """, (tuple(self.ids), ))

        mass_mailing_data = self.env.cr.dictfetchall()
        mapped_data = dict([(m['id'], 100 * m['nb_clicks'] / m['nb_mails']) for m in mass_mailing_data])
        for mass_mailing in self:
            mass_mailing.clicks_ratio = mapped_data.get(mass_mailing.id, 0)

    @api.depends('mailing_model_id')
    def _compute_model(self):
        for record in self:
            record.mailing_model_real = (record.mailing_model_name != 'mail.mass_mailing.list') and record.mailing_model_name or 'mail.mass_mailing.contact'

    def _compute_statistics(self):
        """ Compute statistics of the mass mailing """
        self.env.cr.execute("""
            SELECT
                m.id as mailing_id,
                COUNT(s.id) AS expected,
                COUNT(CASE WHEN s.sent is not null THEN 1 ELSE null END) AS sent,
                COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is null AND s.ignored is null THEN 1 ELSE null END) AS scheduled,
                COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is not null THEN 1 ELSE null END) AS failed,
                COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is null AND s.ignored is not null THEN 1 ELSE null END) AS ignored,
                COUNT(CASE WHEN s.sent is not null AND s.bounced is null THEN 1 ELSE null END) AS delivered,
                COUNT(CASE WHEN s.opened is not null THEN 1 ELSE null END) AS opened,
                COUNT(CASE WHEN s.clicked is not null THEN 1 ELSE null END) AS clicked,
                COUNT(CASE WHEN s.replied is not null THEN 1 ELSE null END) AS replied,
                COUNT(CASE WHEN s.bounced is not null THEN 1 ELSE null END) AS bounced,
                COUNT(CASE WHEN s.exception is not null THEN 1 ELSE null END) AS failed
            FROM
                mail_mail_statistics s
            RIGHT JOIN
                mail_mass_mailing m
                ON (m.id = s.mass_mailing_id)
            WHERE
                m.id IN %s
            GROUP BY
                m.id
        """, (tuple(self.ids), ))
        for row in self.env.cr.dictfetchall():
            total = row['expected'] = (row['expected'] - row['ignored']) or 1
            row['received_ratio'] = 100.0 * row['delivered'] / total
            row['opened_ratio'] = 100.0 * row['opened'] / total
            row['clicks_ratio'] = 100.0 * row['clicked'] / total
            row['replied_ratio'] = 100.0 * row['replied'] / total
            row['bounced_ratio'] = 100.0 * row['bounced'] / total
            self.browse(row.pop('mailing_id')).update(row)

    @api.multi
    def _unsubscribe_token(self, res_id, email):
        """Generate a secure hash for this mailing list and parameters.

        This is appended to the unsubscription URL and then checked at
        unsubscription time to ensure no malicious unsubscriptions are
        performed.

        :param int res_id:
            ID of the resource that will be unsubscribed.

        :param str email:
            Email of the resource that will be unsubscribed.
        """
        secret = self.env["ir.config_parameter"].sudo().get_param(
            "database.secret")
        token = (self.env.cr.dbname, self.id, int(res_id), tools.ustr(email))
        return hmac.new(secret.encode('utf-8'), repr(token).encode('utf-8'), hashlib.sha512).hexdigest()

    def _compute_next_departure(self):
        cron_next_call = self.env.ref('mass_mailing.ir_cron_mass_mailing_queue').sudo().nextcall
        str2dt = fields.Datetime.from_string
        cron_time = str2dt(cron_next_call)
        for mass_mailing in self:
            if mass_mailing.schedule_date:
                schedule_date = str2dt(mass_mailing.schedule_date)
                mass_mailing.next_departure = max(schedule_date, cron_time)
            else:
                mass_mailing.next_departure = cron_time

    @api.onchange('mass_mailing_campaign_id')
    def _onchange_mass_mailing_campaign_id(self):
        if self.mass_mailing_campaign_id:
            dic = {'campaign_id': self.mass_mailing_campaign_id.campaign_id,
                   'source_id': self.mass_mailing_campaign_id.source_id,
                   'medium_id': self.mass_mailing_campaign_id.medium_id}
            self.update(dic)

    @api.onchange('mailing_model_id', 'contact_list_ids')
    def _onchange_model_and_list(self):
        mailing_domain = []
        if self.mailing_model_name:
            if self.mailing_model_name == 'mail.mass_mailing.list':
                if self.contact_list_ids:
                    mailing_domain.append(('list_ids', 'in', self.contact_list_ids.ids))
                else:
                    mailing_domain.append((0, '=', 1))
            elif self.mailing_model_name == 'res.partner':
                mailing_domain.append(('customer', '=', True))
            elif 'opt_out' in self.env[self.mailing_model_name]._fields and not self.mailing_domain:
                mailing_domain.append(('opt_out', '=', False))
        else:
            mailing_domain.append((0, '=', 1))
        self.mailing_domain = repr(mailing_domain)

    @api.onchange('subject')
    def _onchange_subject(self):
        if self.subject and not self.name:
            self.name = self.subject

    #------------------------------------------------------
    # Technical stuff
    #------------------------------------------------------

    @api.model
    def name_create(self, name):
        """ _rec_name is source_id, creates a utm.source instead """
        mass_mailing = self.create({'name': name, 'subject': name})
        return mass_mailing.name_get()[0]

    @api.model
    def create(self, vals):
        if vals.get('name') and not vals.get('subject'):
            vals['subject'] = vals['name']
        return super(MassMailing, self).create(vals)

    @api.multi
    @api.returns('self', lambda value: value.id)
    def copy(self, default=None):
        self.ensure_one()
        default = dict(default or {},
                       name=_('%s (copy)') % self.name)
        return super(MassMailing, self).copy(default=default)

    def _group_expand_states(self, states, domain, order):
        return [key for key, val in type(self).state.selection]

    def update_opt_out(self, email, list_ids, value):
        if len(list_ids) > 0:
            model = self.env['mail.mass_mailing.contact'].with_context(active_test=False)
            records = model.search([('email_normalized', '=', tools.email_normalize(email))])
            opt_out_records = self.env['mail.mass_mailing.list_contact_rel'].search([
                ('contact_id', 'in', records.ids),
                ('list_id', 'in', list_ids),
                ('opt_out', '!=', value)
            ])

            opt_out_records.write({'opt_out': value})
            message = _('The recipient <strong>unsubscribed from %s</strong> mailing list(s)') \
                if value else _('The recipient <strong>subscribed to %s</strong> mailing list(s)')
            for record in records:
                # filter the list_id by record
                record_lists = opt_out_records.filtered(lambda rec: rec.contact_id.id == record.id)
                if len(record_lists) > 0:
                    record.sudo().message_post(body=_(message % ', '.join(str(list.name) for list in record_lists.mapped('list_id'))))

    #------------------------------------------------------
    # Views & Actions
    #------------------------------------------------------

    @api.multi
    def action_duplicate(self):
        self.ensure_one()
        mass_mailing_copy = self.copy()
        if mass_mailing_copy:
            context = dict(self.env.context)
            context['form_view_initial_mode'] = 'edit'
            return {
                'type': 'ir.actions.act_window',
                'view_type': 'form',
                'view_mode': 'form',
                'res_model': 'mail.mass_mailing',
                'res_id': mass_mailing_copy.id,
                'context': context,
            }
        return False

    @api.multi
    def action_test_mailing(self):
        self.ensure_one()
        ctx = dict(self.env.context, default_mass_mailing_id=self.id)
        return {
            'name': _('Test Mailing'),
            'type': 'ir.actions.act_window',
            'view_mode': 'form',
            'res_model': 'mail.mass_mailing.test',
            'target': 'new',
            'context': ctx,
        }

    @api.multi
    def action_schedule_date(self):
        self.ensure_one()
        action = self.env.ref('mass_mailing.mass_mailing_schedule_date_action').read()[0]
        action['context'] = dict(self.env.context, default_mass_mailing_id=self.id)
        return action

    @api.multi
    def put_in_queue(self):
        self.write({'state': 'in_queue'})

    @api.multi
    def cancel_mass_mailing(self):
        self.write({'state': 'draft', 'schedule_date': False})

    @api.multi
    def retry_failed_mail(self):
        failed_mails = self.env['mail.mail'].search([('mailing_id', 'in', self.ids), ('state', '=', 'exception')])
        failed_mails.mapped('statistics_ids').unlink()
        failed_mails.sudo().unlink()
        self.write({'state': 'in_queue'})

    def action_view_sent(self):
        return self._action_view_documents_filtered('sent')

    def action_view_opened(self):
        return self._action_view_documents_filtered('opened')

    def action_view_replied(self):
        return self._action_view_documents_filtered('replied')

    def action_view_bounced(self):
        return self._action_view_documents_filtered('bounced')

    def action_view_clicked(self):
        return self._action_view_documents_filtered('clicked')

    def action_view_delivered(self):
        return self._action_view_documents_filtered('delivered')

    def _action_view_documents_filtered(self, view_filter):
        if view_filter in ('sent', 'opened', 'replied', 'bounced', 'clicked'):
            opened_stats = self.statistics_ids.filtered(lambda stat: stat[view_filter])
        elif view_filter == ('delivered'):
            opened_stats = self.statistics_ids.filtered(lambda stat: stat.sent and not stat.bounced)
        else:
            opened_stats = self.env['mail.mail.statistics']
        res_ids = opened_stats.mapped('res_id')
        model_name = self.env['ir.model']._get(self.mailing_model_real).display_name
        return {
            'name': model_name,
            'type': 'ir.actions.act_window',
            'view_mode': 'tree',
            'res_model': self.mailing_model_real,
            'domain': [('id', 'in', res_ids)],
        }

    #------------------------------------------------------
    # Email Sending
    #------------------------------------------------------

    def _get_opt_out_list(self):
        """Returns a set of emails opted-out in target model"""
        self.ensure_one()
        opt_out = {}
        target = self.env[self.mailing_model_real]
        if self.mailing_model_real == "mail.mass_mailing.contact":
            # if user is opt_out on One list but not on another
            # or if two user with same email address, one opted in and the other one opted out, send the mail anyway
            # TODO DBE Fixme : Optimise the following to get real opt_out and opt_in
            target_list_contacts = self.env['mail.mass_mailing.list_contact_rel'].search(
                [('list_id', 'in', self.contact_list_ids.ids)])
            opt_out_contacts = target_list_contacts.filtered(lambda rel: rel.opt_out).mapped('contact_id.email_normalized')
            opt_in_contacts = target_list_contacts.filtered(lambda rel: not rel.opt_out).mapped('contact_id.email_normalized')
            opt_out = set(c for c in opt_out_contacts if c not in opt_in_contacts)

            _logger.info(
                "Mass-mailing %s targets %s, blacklist: %s emails",
                self, target._name, len(opt_out))
        else:
            _logger.info("Mass-mailing %s targets %s, no opt out list available", self, target._name)
        return opt_out

    def _get_convert_links(self):
        self.ensure_one()
        utm_mixin = self.mass_mailing_campaign_id if self.mass_mailing_campaign_id else self
        vals = {'mass_mailing_id': self.id}

        if self.mass_mailing_campaign_id:
            vals['mass_mailing_campaign_id'] = self.mass_mailing_campaign_id.id
        if utm_mixin.campaign_id:
            vals['campaign_id'] = utm_mixin.campaign_id.id
        if utm_mixin.source_id:
            vals['source_id'] = utm_mixin.source_id.id
        if utm_mixin.medium_id:
            vals['medium_id'] = utm_mixin.medium_id.id
        return vals

    def _get_seen_list(self):
        """Returns a set of emails already targeted by current mailing/campaign (no duplicates)"""
        self.ensure_one()
        target = self.env[self.mailing_model_real]
        if set(['email', 'email_from']) & set(target._fields):
            mail_field = 'email' if 'email' in target._fields else 'email_from'
            # avoid loading a large number of records in memory
            # + use a basic heuristic for extracting emails
            query = """
                SELECT lower(substring(t.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)'))
                  FROM mail_mail_statistics s
                  JOIN %(target)s t ON (s.res_id = t.id)
                 WHERE substring(t.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)') IS NOT NULL
            """
        elif 'partner_id' in target._fields:
            mail_field = 'email'
            query = """
                SELECT lower(substring(p.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)'))
                  FROM mail_mail_statistics s
                  JOIN %(target)s t ON (s.res_id = t.id)
                  JOIN res_partner p ON (t.partner_id = p.id)
                 WHERE substring(p.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)') IS NOT NULL
            """
        else:
            raise UserError(_("Unsupported mass mailing model %s") % self.mailing_model_id.name)

        if self.mass_mailing_campaign_id.unique_ab_testing:
            query +="""
               AND s.mass_mailing_campaign_id = %%(mailing_campaign_id)s;
            """
        else:
            query +="""
               AND s.mass_mailing_id = %%(mailing_id)s
               AND s.model = %%(target_model)s;
            """
        query = query % {'target': target._table, 'mail_field': mail_field}
        params = {'mailing_id': self.id, 'mailing_campaign_id': self.mass_mailing_campaign_id.id, 'target_model': self.mailing_model_real}
        self._cr.execute(query, params)
        seen_list = set(m[0] for m in self._cr.fetchall())
        _logger.info(
            "Mass-mailing %s has already reached %s %s emails", self, len(seen_list), target._name)
        return seen_list

    def _get_mass_mailing_context(self):
        """Returns extra context items with pre-filled blacklist and seen list for massmailing"""
        return {
            'mass_mailing_opt_out_list': self._get_opt_out_list(),
            'mass_mailing_seen_list': self._get_seen_list(),
            'post_convert_links': self._get_convert_links(),
        }

    def get_recipients(self):
        if self.mailing_domain:
            domain = safe_eval(self.mailing_domain)
            res_ids = self.env[self.mailing_model_real].search(domain).ids
        else:
            res_ids = []
            domain = [('id', 'in', res_ids)]

        # randomly choose a fragment
        if self.contact_ab_pc < 100:
            contact_nbr = self.env[self.mailing_model_real].search_count(domain)
            topick = int(contact_nbr / 100.0 * self.contact_ab_pc)
            if self.mass_mailing_campaign_id and self.mass_mailing_campaign_id.unique_ab_testing:
                already_mailed = self.mass_mailing_campaign_id.get_recipients()[self.mass_mailing_campaign_id.id]
            else:
                already_mailed = set([])
            remaining = set(res_ids).difference(already_mailed)
            if topick > len(remaining):
                topick = len(remaining)
            res_ids = random.sample(remaining, topick)
        return res_ids

    def get_remaining_recipients(self):
        res_ids = self.get_recipients()
        already_mailed = self.env['mail.mail.statistics'].search_read([('model', '=', self.mailing_model_real),
                                                                     ('res_id', 'in', res_ids),
                                                                     ('mass_mailing_id', '=', self.id)], ['res_id'])
        already_mailed_res_ids = [record['res_id'] for record in already_mailed]
        return list(set(res_ids) - set(already_mailed_res_ids))

    def send_mail(self, res_ids=None):
        author_id = self.env.user.partner_id.id

        for mailing in self:
            if not res_ids:
                res_ids = mailing.get_remaining_recipients()
            if not res_ids:
                raise UserError(_('There is no recipients selected.'))

            composer_values = {
                'author_id': author_id,
                'attachment_ids': [(4, attachment.id) for attachment in mailing.attachment_ids],
                'body': mailing.body_html,
                'subject': mailing.subject,
                'model': mailing.mailing_model_real,
                'email_from': mailing.email_from,
                'record_name': False,
                'composition_mode': 'mass_mail',
                'mass_mailing_id': mailing.id,
                'mailing_list_ids': [(4, l.id) for l in mailing.contact_list_ids],
                'no_auto_thread': mailing.reply_to_mode != 'thread',
                'template_id': None,
                'mail_server_id': mailing.mail_server_id.id,
            }
            if mailing.reply_to_mode == 'email':
                composer_values['reply_to'] = mailing.reply_to

            composer = self.env['mail.compose.message'].with_context(active_ids=res_ids).create(composer_values)
            extra_context = self._get_mass_mailing_context()
            composer = composer.with_context(active_ids=res_ids, **extra_context)
            # auto-commit except in testing mode
            auto_commit = not getattr(threading.currentThread(), 'testing', False)
            composer.send_mail(auto_commit=auto_commit)
            mailing.write({'state': 'done', 'sent_date': fields.Datetime.now()})
        return True

    def convert_links(self):
        res = {}
        for mass_mailing in self:
            utm_mixin = mass_mailing.mass_mailing_campaign_id if mass_mailing.mass_mailing_campaign_id else mass_mailing
            html = mass_mailing.body_html if mass_mailing.body_html else ''

            vals = {'mass_mailing_id': mass_mailing.id}

            if mass_mailing.mass_mailing_campaign_id:
                vals['mass_mailing_campaign_id'] = mass_mailing.mass_mailing_campaign_id.id
            if utm_mixin.campaign_id:
                vals['campaign_id'] = utm_mixin.campaign_id.id
            if utm_mixin.source_id:
                vals['source_id'] = utm_mixin.source_id.id
            if utm_mixin.medium_id:
                vals['medium_id'] = utm_mixin.medium_id.id

            res[mass_mailing.id] = self.env['link.tracker'].convert_links(html, vals, blacklist=['/unsubscribe_from_list'])

        return res

    @api.model
    def _process_mass_mailing_queue(self):
        mass_mailings = self.search([('state', 'in', ('in_queue', 'sending')), '|', ('schedule_date', '<', fields.Datetime.now()), ('schedule_date', '=', False)])
        for mass_mailing in mass_mailings:
            user = mass_mailing.write_uid or self.env.user
            mass_mailing = mass_mailing.with_context(**user.sudo(user=user).context_get())
            if len(mass_mailing.get_remaining_recipients()) > 0:
                mass_mailing.state = 'sending'
                mass_mailing.send_mail()
            else:
                mass_mailing.write({'state': 'done', 'sent_date': fields.Datetime.now()})
Example #30
0
class MailActivity(models.Model):
    """ An actual activity to perform. Activities are linked to
    documents using res_id and res_model_id fields. Activities have a deadline
    that can be used in kanban view to display a status. Once done activities
    are unlinked and a message is posted. This message has a new activity_type_id
    field that indicates the activity linked to the message. """
    _name = 'mail.activity'
    _description = 'Activity'
    _order = 'date_deadline ASC'
    _rec_name = 'summary'

    @api.model
    def default_get(self, fields):
        res = super(MailActivity, self).default_get(fields)
        if not fields or 'res_model_id' in fields and res.get('res_model'):
            res['res_model_id'] = self.env['ir.model']._get(
                res['res_model']).id
        return res

    # owner
    res_id = fields.Integer('Related Document ID', index=True, required=True)
    res_model_id = fields.Many2one('ir.model',
                                   'Document Model',
                                   index=True,
                                   ondelete='cascade',
                                   required=True)
    res_model = fields.Char('Related Document Model',
                            index=True,
                            related='res_model_id.model',
                            store=True,
                            readonly=True)
    res_name = fields.Char('Document Name',
                           compute='_compute_res_name',
                           store=True,
                           help="Display name of the related document.",
                           readonly=True)
    # activity
    activity_type_id = fields.Many2one(
        'mail.activity.type',
        'Activity',
        domain=
        "['|', ('res_model_id', '=', False), ('res_model_id', '=', res_model_id)]",
        ondelete='restrict')
    activity_category = fields.Selection(related='activity_type_id.category')
    icon = fields.Char('Icon', related='activity_type_id.icon')
    summary = fields.Char('Summary')
    note = fields.Html('Note')
    feedback = fields.Html('Feedback')
    date_deadline = fields.Date('Due Date',
                                index=True,
                                required=True,
                                default=fields.Date.today)
    automated = fields.Boolean(
        'Automated activity',
        readonly=True,
        help=
        'Indicates this activity has been created automatically and not by any user.'
    )
    # description
    user_id = fields.Many2one('res.users',
                              'Assigned to',
                              default=lambda self: self.env.user,
                              index=True,
                              required=True)
    state = fields.Selection([('overdue', 'Overdue'), ('today', 'Today'),
                              ('planned', 'Planned')],
                             'State',
                             compute='_compute_state')
    recommended_activity_type_id = fields.Many2one(
        'mail.activity.type', string="Recommended Activity Type")
    previous_activity_type_id = fields.Many2one(
        'mail.activity.type', string='Previous Activity Type')
    has_recommended_activities = fields.Boolean(
        'Next activities available',
        compute='_compute_has_recommended_activities',
        help='Technical field for UX purpose')

    @api.multi
    @api.onchange('previous_activity_type_id')
    def _compute_has_recommended_activities(self):
        for record in self:
            record.has_recommended_activities = bool(
                record.previous_activity_type_id.next_type_ids)

    @api.depends('res_model', 'res_id')
    def _compute_res_name(self):
        for activity in self:
            activity.res_name = self.env[activity.res_model].browse(
                activity.res_id).name_get()[0][1]

    @api.depends('date_deadline')
    def _compute_state(self):
        today_default = date.today()

        for record in self.filtered(lambda activity: activity.date_deadline):
            today = today_default
            if record.user_id.tz:
                today_utc = pytz.UTC.localize(datetime.utcnow())
                today_tz = today_utc.astimezone(
                    pytz.timezone(record.user_id.tz))
                today = date(year=today_tz.year,
                             month=today_tz.month,
                             day=today_tz.day)

            date_deadline = fields.Date.from_string(record.date_deadline)
            diff = (date_deadline - today)
            if diff.days == 0:
                record.state = 'today'
            elif diff.days < 0:
                record.state = 'overdue'
            else:
                record.state = 'planned'

    @api.onchange('activity_type_id')
    def _onchange_activity_type_id(self):
        if self.activity_type_id:
            self.summary = self.activity_type_id.summary
            self.date_deadline = (datetime.now() +
                                  timedelta(days=self.activity_type_id.days))

    @api.onchange('previous_activity_type_id')
    def _onchange_previous_activity_type_id(self):
        if self.previous_activity_type_id.next_type_ids:
            self.recommended_activity_type_id = self.previous_activity_type_id.next_type_ids[
                0]

    @api.onchange('recommended_activity_type_id')
    def _onchange_recommended_activity_type_id(self):
        self.activity_type_id = self.recommended_activity_type_id

    @api.multi
    def _check_access(self, operation):
        """ Rule to access activities

         * create: check write rights on related document;
         * write: rule OR write rights on document;
         * unlink: rule OR write rights on document;
        """
        self.check_access_rights(
            operation, raise_exception=True)  # will raise an AccessError

        if operation in ('write', 'unlink'):
            try:
                self.check_access_rule(operation)
            except exceptions.AccessError:
                pass
            else:
                return
        doc_operation = 'read' if operation == 'read' else 'write'
        activity_to_documents = dict()
        for activity in self.sudo():
            activity_to_documents.setdefault(activity.res_model,
                                             list()).append(activity.res_id)
        for model, res_ids in activity_to_documents.items():
            self.env[model].check_access_rights(doc_operation,
                                                raise_exception=True)
            try:
                self.env[model].browse(res_ids).check_access_rule(
                    doc_operation)
            except exceptions.AccessError:
                raise exceptions.AccessError(
                    _('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)'
                      ) % (self._description, operation))

    @api.model
    def create(self, values):
        # already compute default values to be sure those are computed using the current user
        values_w_defaults = self.default_get(self._fields.keys())
        values_w_defaults.update(values)

        # continue as sudo because activities are somewhat protected
        activity = super(MailActivity, self.sudo()).create(values_w_defaults)
        activity_user = activity.sudo(self.env.user)
        activity_user._check_access('create')
        self.env[activity_user.res_model].browse(
            activity_user.res_id).message_subscribe(
                partner_ids=[activity_user.user_id.partner_id.id])
        if activity.date_deadline <= fields.Date.today():
            self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner',
                                         activity.user_id.partner_id.id), {
                                             'type': 'activity_updated',
                                             'activity_created': True
                                         })
        return activity_user

    @api.multi
    def write(self, values):
        self._check_access('write')
        if values.get('user_id'):
            pre_responsibles = self.mapped('user_id.partner_id')
        res = super(MailActivity, self.sudo()).write(values)

        if values.get('user_id'):
            for activity in self:
                self.env[activity.res_model].browse(
                    activity.res_id).message_subscribe(
                        partner_ids=[activity.user_id.partner_id.id])
                if activity.date_deadline <= fields.Date.today():
                    self.env['bus.bus'].sendone(
                        (self._cr.dbname, 'res.partner',
                         activity.user_id.partner_id.id), {
                             'type': 'activity_updated',
                             'activity_created': True
                         })
            for activity in self:
                if activity.date_deadline <= fields.Date.today():
                    for partner in pre_responsibles:
                        self.env['bus.bus'].sendone(
                            (self._cr.dbname, 'res.partner', partner.id), {
                                'type': 'activity_updated',
                                'activity_deleted': True
                            })
        return res

    @api.multi
    def unlink(self):
        self._check_access('unlink')
        for activity in self:
            if activity.date_deadline <= fields.Date.today():
                self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner',
                                             activity.user_id.partner_id.id), {
                                                 'type': 'activity_updated',
                                                 'activity_deleted': True
                                             })
        return super(MailActivity, self.sudo()).unlink()

    @api.multi
    def action_done(self):
        """ Wrapper without feedback because web button add context as
        parameter, therefore setting context to feedback """
        return self.action_feedback()

    def action_feedback(self, feedback=False):
        message = self.env['mail.message']
        if feedback:
            self.write(dict(feedback=feedback))
        for activity in self:
            record = self.env[activity.res_model].browse(activity.res_id)
            record.message_post_with_view(
                'mail.message_activity_done',
                values={'activity': activity},
                subtype_id=self.env['ir.model.data'].xmlid_to_res_id(
                    'mail.mt_activities'),
                mail_activity_type_id=activity.activity_type_id.id,
            )
            message |= record.message_ids[0]

        self.unlink()
        return message.ids and message.ids[0] or False

    @api.multi
    def action_done_schedule_next(self):
        wizard_ctx = dict(
            self.env.context,
            default_previous_activity_type_id=self.activity_type_id.id,
            default_res_id=self.res_id,
            default_res_model=self.res_model,
        )
        self.action_done()
        return {
            'name': _('Schedule an Activity'),
            'context': wizard_ctx,
            'view_type': 'form',
            'view_mode': 'form',
            'res_model': 'mail.activity',
            'views': [(False, 'form')],
            'type': 'ir.actions.act_window',
            'target': 'new',
        }

    @api.multi
    def action_close_dialog(self):
        return {'type': 'ir.actions.act_window_close'}