class account_invoice(models.Model):
    _inherit = 'account.invoice'

    number_days = fields.Integer('Delay Days')
    total_interest = fields.Float(string="Total interest",
                                  digits=dp.get_precision('Account'))
    annual_interest = fields.Float(string="Interest %",
                                   related="payment_term_id.annual_interest",
                                   help="Annual Interest Percent")
    second_interest = fields.Float(string="Second interest %",
                                   related="payment_term_id.second_interest")
    amount_limit_second_interest = fields.Float(
        string="Lump Amount",
        related="payment_term_id.amount_limit_second_interest")

    # envoie email du rappel automatically
    @api.model
    def process_send_reminder(self):
        self.send_reminders()

    @api.multi
    def send_reminders(self):
        account_invoices = self.search([('type', '=', 'out_invoice'),
                                        ('state', '=', 'open')])
        template_res = self.env['mail.template']
        imd_res = self.env['ir.model.data']
        for account in account_invoices:
            d = datetime.today().strftime('%Y-%m-%d')
            date = dateutil.parser.parse(d).date()
            dd = date.strftime("%Y-%m-%d")
            date1 = datetime.strptime(dd, '%Y-%m-%d').date()
            date2 = datetime.strptime(account.date_due, '%Y-%m-%d').date()
            diff = (date1 - date2).days
            print('diff--', diff)

            if (diff == 10 or diff == 20 or diff == 30 or diff == 40
                    or diff == 50 or (diff >= 70 and
                                      (diff + 10) % 20 == 0)) and diff != 0:
                print('email envoyé--', account.id)
                _, template_id = imd_res.get_object_reference(
                    'DrAnytime_CRM', 'email_invoice_reminder')
                template = template_res.browse(template_id)
                template.send_mail(account.id)

    # This function is called when the scheduler goes on
    @api.model
    def process_scheduler_interest(self):
        self.calcul_interest()

    @api.multi
    def calcul_interest(self):
        account_invoices = self.search([('type', '=', 'out_invoice'),
                                        ('state', '=', 'open')])
        template_res = self.env['mail.template']
        imd_res = self.env['ir.model.data']
        for account in account_invoices:
            d = datetime.today().strftime('%Y-%m-%d')
            date = dateutil.parser.parse(d).date()
            dd = date.strftime("%Y-%m-%d")
            date1 = datetime.strptime(dd, '%Y-%m-%d').date()
            date2 = datetime.strptime(account.date_due, '%Y-%m-%d').date()
            print('ddddda--', date2)
            diff = (date1 - date2).days

            print('diff------', diff)
            total_interest = 0
            if diff == 20 or diff % 30 == 0:
                total_interest = (account.amount_untaxed *
                                  (account.annual_interest / 365) * diff) / 100
                print('int1-----', total_interest)
            if (diff >= 60 and diff % 30 == 0) and diff != 0:
                second_interest = (account.amount_untaxed *
                                   (account.second_interest / 365) *
                                   diff) / 100
                if second_interest < account.amount_limit_second_interest:
                    total_interest = total_interest + account.amount_limit_second_interest
                    print('int2-----', total_interest)

                else:
                    total_interest = total_interest + second_interest
                    print('int1-----', total_interest)

            print('ss-----', self.number)
            print('value total interest---',
                  account.currency_id.round(total_interest))
            print('account interest ----',
                  account.currency_id.round(account.total_interest))
            if account.currency_id.round(
                    total_interest) > account.currency_id.round(
                        account.total_interest):
                print('okkkk')
                vals = {
                    'number_days':
                    diff,
                    'total_interest':
                    total_interest,
                    'amount_total':
                    account.amount_untaxed + total_interest +
                    account.amount_tax,
                    'residual':
                    account.amount_untaxed + total_interest +
                    account.amount_tax,
                    'residual_signed':
                    account.amount_untaxed + total_interest +
                    account.amount_tax,
                    'amount_total_company_signed':
                    account.currency_id.compute(
                        account.amount_untaxed + total_interest +
                        account.amount_tax, account.company_id.currency_id),
                    'amount_total_signed':
                    account.amount_untaxed + total_interest +
                    account.amount_tax,
                    'amount_residual':
                    account.amount_untaxed + total_interest +
                    account.amount_tax,
                }

            else:
                vals = ({
                    'number_days': diff,
                })

            account.write(vals)
            account.interest_move_create(total_interest)

    @api.multi
    def process_manual_interest(self):
        context = dict(self._context or {})
        active_ids = context.get('active_ids', []) or []

        for record in self.env['account.invoice'].browse(active_ids):
            record.manual_calcul_interest()

    @api.multi
    def manual_calcul_interest(self):
        account = self
        if self.state == 'open' and self.type == 'out_invoice':
            d = datetime.today().strftime('%Y-%m-%d')
            date = dateutil.parser.parse(d).date()
            dd = date.strftime("%Y-%m-%d")
            date1 = datetime.strptime(dd, '%Y-%m-%d').date()
            date2 = datetime.strptime(self.date_due, '%Y-%m-%d').date()
            diff = (date1 - date2).days
            print('ddddda--', date2)
            print('ddddda--', date1)

            total_interest = 0
            if diff == 20 or diff // 30 > 0:
                total_interest = (self.amount_untaxed *
                                  (self.annual_interest / 365) * diff) / 100

            if diff >= 60:
                second_interest = (self.amount_untaxed *
                                   (self.second_interest / 365) * diff) / 100
                print('---ss----', (self.second_interest / 365) * 60)
                if second_interest < self.amount_limit_second_interest:
                    total_interest = total_interest + (
                        self.amount_limit_second_interest * ((diff // 30) - 1))
                else:
                    total_interest = total_interest + second_interest
            print('1---', total_interest)
            print('2-----', self.total_interest)
            print('bool----', total_interest > self.total_interest)
            if total_interest > self.total_interest:
                print('22222222222')
                vals = {
                    'number_days':
                    diff,
                    'total_interest':
                    total_interest,
                    'amount_total':
                    self.amount_untaxed + total_interest + self.amount_tax,
                    'residual':
                    self.amount_untaxed + total_interest + self.amount_tax,
                    'residual_signed':
                    self.amount_untaxed + total_interest + self.amount_tax,
                    'amount_total_company_signed':
                    self.currency_id.compute(
                        self.amount_untaxed + total_interest + self.amount_tax,
                        self.company_id.currency_id),
                    'amount_total_signed':
                    self.amount_untaxed + total_interest + self.amount_tax,
                    'amount_residual':
                    account.amount_untaxed + total_interest +
                    account.amount_tax,
                }
            else:
                print('000000000000')
                vals = ({
                    'number_days': diff,
                })
            print('aa---',
                  account.amount_untaxed + total_interest + account.amount_tax)
            self.write(vals)
            self.interest_move_create(
                account.currency_id.round(total_interest))
            print('finish')
        return True

    ####
    # @api.multi
    def interest_move_create(self, interest):
        print('enter')
        self.move_id.state = 'draft'
        account = self.env.ref('DrAnytime_CRM.a491124')
        product = self.env.ref('DrAnytime_CRM.product_interest_01')

        print('aaa----', account)
        print('bbb----', product)

        l_interest = [l for l in self.move_id.line_ids if l.name == 'interest']
        if not l_interest:
            self.env['account.move.line'].with_context(
                check_move_validity=False
            ).create({
                'name': 'interest',
                'debit': 0.0,
                'credit': interest,
                'account_id': account.id,
                # 'tax_line_id': line.tax_line_id.id,
                # 'tax_exigible': True,
                'amount_currency': 0.0,
                # 'amount_currency': -interest or 0.0,
                'currency_id': self.currency_id.id,
                'move_id': self.move_id.id,
                'partner_id': self.partner_id.id,
                'amount_residual': 0.0,
                'amount_residual_currency': 0.0,
                'invoice_id': self.id,
                'balance': -interest or 0.0,
                'quantity': 1,
                'product_uom_id': 1,
                'product_id': product.id,
                'journal_type': 'sale'
            })
            print('ok')
        else:
            l_interest_obj = l_interest[0]
            l_interest_obj.credit = interest
        t_move = [l for l in self.move_id.line_ids if l.name == '/'][0]
        credit = 0.0
        for l in self.move_id.line_ids:
            credit = credit + l.credit

        print('cred----', credit)
        t_move.debit = credit
        self.move_id.state = 'posted'

        return True
示例#2
0
class Currency(models.Model):
    _name = "res.currency"
    _description = "Currency"
    _order = "name"

    # Note: 'code' column was removed as of v6.0, the 'name' should now hold the ISO code.
    name = fields.Char(string='Currency',
                       size=3,
                       required=True,
                       help="Currency Code (ISO 4217)")
    symbol = fields.Char(
        help="Currency sign, to be used when printing amounts.", required=True)
    rate = fields.Float(
        compute='_compute_current_rate',
        string='Current Rate',
        digits=(12, 6),
        help='The rate of the currency to the currency of rate 1.')
    rate_ids = fields.One2many('res.currency.rate',
                               'currency_id',
                               string='Rates')
    rounding = fields.Float(string='Rounding Factor',
                            digits=(12, 6),
                            default=0.01)  #货币精度以及有效位,总位数12,小数位精度6
    decimal_places = fields.Integer(
        compute='_compute_decimal_places'
    )  #根据货币的精度(rouding字段),计算decimal_places(即小数位占位),rounding如果为默认值0.01,则decimal_places为2
    active = fields.Boolean(default=True)
    position = fields.Selection(
        [('after', 'After Amount'), ('before', 'Before Amount')],
        default='after',
        string='Symbol Position',
        help=
        "Determines where the currency symbol should be placed after or before the amount."
    )
    date = fields.Date(compute='_compute_date')
    currency_unit_label = fields.Char(string="Currency Unit",
                                      help="Currency Unit Name")
    currency_subunit_label = fields.Char(string="Currency Subunit",
                                         help="Currency Subunit Name")

    _sql_constraints = [('unique_name', 'unique (name)',
                         'The currency code must be unique!'),
                        ('rounding_gt_zero', 'CHECK (rounding>0)',
                         'The rounding factor must be greater than 0!')]

    @api.multi
    @api.depends('rate_ids.rate')
    def _compute_current_rate(self):
        date = self._context.get('date') or fields.Date.today()
        company_id = self._context.get(
            'company_id') or self.env['res.users']._get_company().id
        # the subquery selects the last rate before 'date' for the given currency/company
        query = """SELECT c.id, (SELECT r.rate FROM res_currency_rate r
                                  WHERE r.currency_id = c.id AND r.name <= %s
                                    AND (r.company_id IS NULL OR r.company_id = %s)
                               ORDER BY r.company_id, r.name DESC
                                  LIMIT 1) AS rate
                   FROM res_currency c
                   WHERE c.id IN %s"""
        self._cr.execute(query, (date, company_id, tuple(self.ids)))
        currency_rates = dict(self._cr.fetchall())
        for currency in self:
            currency.rate = currency_rates.get(currency.id) or 1.0

    @api.multi
    @api.depends('rounding')
    def _compute_decimal_places(self):
        for currency in self:
            if 0 < currency.rounding < 1:  #如果货币的精度小于1大于0,例如0.01,则decimal_places为2,即表示小数位占2位
                currency.decimal_places = int(
                    math.ceil(math.log10(1 / currency.rounding)))
            else:
                currency.decimal_places = 0

    @api.multi
    @api.depends('rate_ids.name')
    def _compute_date(self):
        for currency in self:
            currency.date = currency.rate_ids[:1].name

    @api.model
    def name_search(self, name='', args=None, operator='ilike', limit=100):
        results = super(Currency, self).name_search(name,
                                                    args,
                                                    operator=operator,
                                                    limit=limit)
        if not results:
            name_match = CURRENCY_DISPLAY_PATTERN.match(name)
            if name_match:
                results = super(Currency,
                                self).name_search(name_match.group(1),
                                                  args,
                                                  operator=operator,
                                                  limit=limit)
        return results

    @api.multi
    def name_get(self):
        return [(currency.id, tools.ustr(currency.name)) for currency in self]

    @api.multi
    def amount_to_text(self, amount):
        self.ensure_one()

        def _num2words(number, lang):
            try:
                return num2words(number, lang=lang).title()
            except NotImplementedError:
                return num2words(number, lang='en').title()

        if num2words is None:
            logging.getLogger(__name__).warning(
                "The library 'num2words' is missing, cannot render textual amounts."
            )
            return ""

        formatted = "%.{0}f".format(self.decimal_places) % amount
        parts = formatted.partition('.')
        integer_value = int(parts[0])
        fractional_value = int(parts[2] or 0)

        lang_code = self.env.context.get('lang') or self.env.user.lang
        lang = self.env['res.lang'].search([('code', '=', lang_code)])
        amount_words = tools.ustr('{amt_value} {amt_word}').format(
            amt_value=_num2words(integer_value, lang=lang.iso_code),
            amt_word=self.currency_unit_label,
        )
        if not self.is_zero(amount - integer_value):
            amount_words += ' ' + _('and') + tools.ustr(
                ' {amt_value} {amt_word}').format(
                    amt_value=_num2words(fractional_value, lang=lang.iso_code),
                    amt_word=self.currency_subunit_label,
                )
        return amount_words

    @api.multi
    def round(self,
              amount):  #根据货币的精度进行四舍五入,销售订单的_amount_all()方法调用该方法,用于舍入订单的未税金额和税费
        """Return ``amount`` rounded  according to ``self``'s rounding rules.

           :param float amount: the amount to round
           :return: rounded float
        """
        # TODO: Need to check why it calls round() from sale.py, _amount_all() with *No* ID after below commits,
        # https://github.com/odoo/odoo/commit/36ee1ad813204dcb91e9f5f20d746dff6f080ac2
        # https://github.com/odoo/odoo/commit/0b6058c585d7d9a57bd7581b8211f20fca3ec3f7
        # Removing self.ensure_one() will make few test cases to break of modules event_sale, sale_mrp and stock_dropshipping.
        #self.ensure_one()
        return tools.float_round(amount, precision_rounding=self.rounding)

    @api.multi
    def compare_amounts(self, amount1, amount2):
        """Compare ``amount1`` and ``amount2`` after rounding them according to the
           given currency's precision..
           An amount is considered lower/greater than another amount if their rounded
           value is different. This is not the same as having a non-zero difference!

           For example 1.432 and 1.431 are equal at 2 digits precision,
           so this method would return 0.
           However 0.006 and 0.002 are considered different (returns 1) because
           they respectively round to 0.01 and 0.0, even though
           0.006-0.002 = 0.004 which would be considered zero at 2 digits precision.

           :param float amount1: first amount to compare
           :param float amount2: second amount to compare
           :return: (resp.) -1, 0 or 1, if ``amount1`` is (resp.) lower than,
                    equal to, or greater than ``amount2``, according to
                    ``currency``'s rounding.

           With the new API, call it like: ``currency.compare_amounts(amount1, amount2)``.
        """
        return tools.float_compare(amount1,
                                   amount2,
                                   precision_rounding=self.rounding)

    @api.multi
    def is_zero(self, amount):
        """Returns true if ``amount`` is small enough to be treated as
           zero according to current currency's rounding rules.
           Warning: ``is_zero(amount1-amount2)`` is not always equivalent to
           ``compare_amounts(amount1,amount2) == 0``, as the former will round after
           computing the difference, while the latter will round before, giving
           different results for e.g. 0.006 and 0.002 at 2 digits precision.

           :param float amount: amount to compare with currency's zero

           With the new API, call it like: ``currency.is_zero(amount)``.
        """
        return tools.float_is_zero(amount, precision_rounding=self.rounding)

    @api.model
    def _get_conversion_rate(self, from_currency, to_currency):
        from_currency = from_currency.with_env(self.env)
        to_currency = to_currency.with_env(self.env)
        return to_currency.rate / from_currency.rate

    @api.model
    def _compute(self, from_currency, to_currency, from_amount, round=True):
        if (to_currency == from_currency):
            amount = to_currency.round(from_amount) if round else from_amount
        else:
            rate = self._get_conversion_rate(from_currency, to_currency)
            amount = to_currency.round(from_amount *
                                       rate) if round else from_amount * rate
        return amount

    @api.multi
    def compute(self, from_amount, to_currency, round=True):
        """ Convert `from_amount` from currency `self` to `to_currency`. """
        self, to_currency = self or to_currency, to_currency or self
        assert self, "compute from unknown currency"
        assert to_currency, "compute to unknown currency"
        # apply conversion rate
        if self == to_currency:
            to_amount = from_amount
        else:
            to_amount = from_amount * self._get_conversion_rate(
                self, to_currency)
        # apply rounding
        return to_currency.round(to_amount) if round else to_amount

    def _select_companies_rates(self):
        return """
示例#3
0
class MassMailingCampaign(models.Model):
    """Model of mass mailing campaigns. """
    _name = "mail.mass_mailing.campaign"
    _description = 'Mass Mailing Campaign'
    _rec_name = "campaign_id"
    _inherits = {'utm.campaign': 'campaign_id'}

    stage_id = fields.Many2one('mail.mass_mailing.stage', string='Stage', ondelete='restrict', required=True, 
        default=lambda self: self.env['mail.mass_mailing.stage'].search([], limit=1),
        group_expand='_group_expand_stage_ids')
    user_id = fields.Many2one(
        'res.users', string='Responsible',
        required=True, default=lambda self: self.env.uid)
    campaign_id = fields.Many2one('utm.campaign', 'campaign_id',
        required=True, ondelete='cascade',  help="This name helps you tracking your different campaign efforts, e.g. Fall_Drive, Christmas_Special")
    source_id = fields.Many2one('utm.source', string='Source',
            help="This is the link source, e.g. Search Engine, another domain,or name of email list", default=lambda self: self.env.ref('utm.utm_source_newsletter'))
    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'))
    tag_ids = fields.Many2many(
        'mail.mass_mailing.tag', 'mail_mass_mailing_tag_rel',
        'tag_id', 'campaign_id', string='Tags')
    mass_mailing_ids = fields.One2many(
        'mail.mass_mailing', 'mass_mailing_campaign_id',
        string='Mass Mailings')
    unique_ab_testing = fields.Boolean(string='Allow A/B Testing', default=False,
        help='If checked, recipients will be mailed only once for the whole campaign. '
             'This lets you send different mailings to randomly selected recipients and test '
             'the effectiveness of the mailings, without causing duplicate messages.')
    color = fields.Integer(string='Color Index')
    clicks_ratio = fields.Integer(compute="_compute_clicks_ratio", string="Number of clicks")
    # stat fields
    total = fields.Integer(compute="_compute_statistics")
    scheduled = fields.Integer(compute="_compute_statistics")
    failed = fields.Integer(compute="_compute_statistics")
    ignored = fields.Integer(compute="_compute_statistics")
    sent = fields.Integer(compute="_compute_statistics", string="Sent Emails")
    delivered = fields.Integer(compute="_compute_statistics")
    opened = fields.Integer(compute="_compute_statistics")
    replied = fields.Integer(compute="_compute_statistics")
    bounced = 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')
    total_mailings = fields.Integer(compute="_compute_total_mailings", string='Mailings')

    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_campaign_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_campaign_id IN %s
            GROUP BY stats.mass_mailing_campaign_id
        """, (tuple(self.ids), ))

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

    def _compute_statistics(self):
        """ Compute statistics of the mass mailing campaign """
        self.env.cr.execute("""
            SELECT
                c.id as campaign_id,
                COUNT(s.id) AS total,
                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.id 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.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
            FROM
                mail_mail_statistics s
            RIGHT JOIN
                mail_mass_mailing_campaign c
                ON (c.id = s.mass_mailing_campaign_id)
            WHERE
                c.id IN %s
            GROUP BY
                c.id
        """, (tuple(self.ids), ))

        for row in self.env.cr.dictfetchall():
            total = (row['total'] - row['ignored']) or 1
            row['delivered'] = row['sent'] - row['bounced']
            row['received_ratio'] = 100.0 * row['delivered'] / total
            row['opened_ratio'] = 100.0 * row['opened'] / total
            row['replied_ratio'] = 100.0 * row['replied'] / total
            row['bounced_ratio'] = 100.0 * row['bounced'] / total
            self.browse(row.pop('campaign_id')).update(row)

    def _compute_total_mailings(self):
        campaign_data = self.env['mail.mass_mailing'].read_group(
            [('mass_mailing_campaign_id', 'in', self.ids)],
            ['mass_mailing_campaign_id'], ['mass_mailing_campaign_id'])
        mapped_data = dict([(c['mass_mailing_campaign_id'][0], c['mass_mailing_campaign_id_count']) for c in campaign_data])
        for campaign in self:
            campaign.total_mailings = mapped_data.get(campaign.id, 0)

    def get_recipients(self, model=None):
        """Return the recipients of a mailing campaign. This is based on the statistics
        build for each mailing. """
        res = dict.fromkeys(self.ids, {})
        for campaign in self:
            domain = [('mass_mailing_campaign_id', '=', campaign.id)]
            if model:
                domain += [('model', '=', model)]
            res[campaign.id] = set(self.env['mail.mail.statistics'].search(domain).mapped('res_id'))
        return res

    @api.model
    def _group_expand_stage_ids(self, stages, domain, order):
        """ Read group customization in order to display all the stages in the
            kanban view, even if they are empty
        """
        stage_ids = stages._search([], order=order, access_rights_uid=SUPERUSER_ID)
        return stages.browse(stage_ids)
示例#4
0
class PaymentTransaction(models.Model):
    _name = 'payment.transaction'
    _description = 'Payment Transaction'
    _order = 'id desc'
    _rec_name = 'reference'

    @api.model
    def _lang_get(self):
        return self.env['res.lang'].get_installed()

    acquirer_id = fields.Many2one(
        string="Acquirer", comodel_name='payment.acquirer', readonly=True, required=True)
    provider = fields.Selection(related='acquirer_id.provider')
    company_id = fields.Many2one(  # Indexed to speed-up ORM searches (from ir_rule or others)
        related='acquirer_id.company_id', store=True, index=True)
    reference = fields.Char(
        string="Reference", help="The internal reference of the transaction", readonly=True,
        required=True)  # Already has an index from the UNIQUE SQL constraint
    acquirer_reference = fields.Char(
        string="Acquirer Reference", help="The acquirer reference of the transaction",
        readonly=True)  # This is not the same thing as the acquirer reference of the token
    amount = fields.Monetary(
        string="Amount", currency_field='currency_id', readonly=True, required=True)
    currency_id = fields.Many2one(
        string="Currency", comodel_name='res.currency', readonly=True, required=True)
    fees = fields.Monetary(
        string="Fees", currency_field='currency_id',
        help="The fees amount; set by the system as it depends on the acquirer", readonly=True)
    token_id = fields.Many2one(
        string="Payment Token", comodel_name='payment.token', readonly=True,
        domain='[("acquirer_id", "=", "acquirer_id")]', ondelete='restrict')
    state = fields.Selection(
        string="Status",
        selection=[('draft', "Draft"), ('pending', "Pending"), ('authorized', "Authorized"),
                   ('done', "Confirmed"), ('cancel', "Canceled"), ('error', "Error")],
        default='draft', readonly=True, required=True, copy=False)
    state_message = fields.Text(
        string="Message", help="The complementary information message about the state",
        readonly=True)
    last_state_change = fields.Datetime(
        string="Last State Change Date", readonly=True, default=fields.Datetime.now)

    # Fields used for traceability
    operation = fields.Selection(  # This should not be trusted if the state is 'draft' or 'pending'
        string="Operation",
        selection=[('online_redirect', "Online payment with redirection"),
                   ('online_direct', "Online direct payment"),
                   ('online_token', "Online payment by token"),
                   ('validation', "Validation of the payment method"),
                   ('offline', "Offline payment by token")],
        readonly=True)
    payment_id = fields.Many2one(string="Payment", comodel_name='account.payment', readonly=True)
    invoice_ids = fields.Many2many(
        string="Invoices", comodel_name='account.move', relation='account_invoice_transaction_rel',
        column1='transaction_id', column2='invoice_id', readonly=True, copy=False,
        domain=[('move_type', 'in', ('out_invoice', 'out_refund', 'in_invoice', 'in_refund'))])
    invoices_count = fields.Integer(string="Invoices Count", compute='_compute_invoices_count')

    # Fields used for user redirection & payment post-processing
    is_post_processed = fields.Boolean(
        string="Is Post-processed", help="Has the payment been post-processed")
    tokenize = fields.Boolean(
        string="Create Token",
        help="Whether a payment token should be created when post-processing the transaction")
    validation_route = fields.Char(
        string="Validation Route",
        help="The route the user is redirected to in order to refund a validation transaction")
    landing_route = fields.Char(
        string="Landing Route",
        help="The route the user is redirected to after the transaction")
    callback_model_id = fields.Many2one(
        string="Callback Document Model", comodel_name='ir.model', groups='base.group_system')
    callback_res_id = fields.Integer(string="Callback Record ID", groups='base.group_system')
    callback_method = fields.Char(string="Callback Method", groups='base.group_system')
    # Hash for additional security on top of the callback fields' group in case a bug exposes a sudo
    callback_hash = fields.Char(string="Callback Hash", groups='base.group_system')
    callback_is_done = fields.Boolean(
        string="Callback Done", help="Whether the callback has already been executed",
        groups="base.group_system", readonly=True)

    # Duplicated partner values allowing to keep a record of them, should they be later updated
    partner_id = fields.Many2one(
        string="Customer", comodel_name='res.partner', readonly=True, required=True,
        ondelete='restrict')
    partner_name = fields.Char(string="Partner Name")
    partner_lang = fields.Selection(string="Language", selection=_lang_get)
    partner_email = fields.Char(string="Email")
    partner_address = fields.Char(string="Address")
    partner_zip = fields.Char(string="Zip")
    partner_city = fields.Char(string="City")
    partner_state_id = fields.Many2one(string="State", comodel_name='res.country.state')
    partner_country_id = fields.Many2one(string="Country", comodel_name='res.country')
    partner_phone = fields.Char(string="Phone")

    _sql_constraints = [
        ('reference_uniq', 'unique(reference)', "Reference must be unique!"),
    ]

    #=== COMPUTE METHODS ===#

    @api.depends('invoice_ids')
    def _compute_invoices_count(self):
        self.env.cr.execute(
            '''
            SELECT transaction_id, count(invoice_id)
            FROM account_invoice_transaction_rel
            WHERE transaction_id IN %s
            GROUP BY transaction_id
            ''',
            [tuple(self.ids)]
        )
        tx_data = dict(self.env.cr.fetchall())  # {id: count}
        for tx in self:
            tx.invoices_count = tx_data.get(tx.id, 0)

    #=== CONSTRAINT METHODS ===#

    @api.constrains('state')
    def _check_state_authorized_supported(self):
        """ Check that authorization is supported for a transaction in the 'authorized' state. """
        illegal_authorize_state_txs = self.filtered(
            lambda tx: tx.state == 'authorized' and not tx.acquirer_id.support_authorization
        )
        if illegal_authorize_state_txs:
            raise ValidationError(_(
                "Transaction authorization is not supported by the following payment acquirers: %s",
                ', '.join(set(illegal_authorize_state_txs.mapped('acquirer_id.name')))
            ))

    #=== CRUD METHODS ===#

    @api.model_create_multi
    def create(self, values_list):
        for values in values_list:
            acquirer = self.env['payment.acquirer'].browse(values['acquirer_id'])

            if not values.get('reference'):
                values['reference'] = self._compute_reference(acquirer.provider, **values)

            # Duplicate partner values
            partner = self.env['res.partner'].browse(values['partner_id'])
            values.update({
                'partner_name': partner.name,
                'partner_lang': partner.lang,
                'partner_email': partner.email,
                'partner_address': payment_utils.format_partner_address(
                    partner.street, partner.street2
                ),
                'partner_zip': partner.zip,
                'partner_city': partner.city,
                'partner_state_id': partner.state_id.id,
                'partner_country_id': partner.country_id.id,
                'partner_phone': partner.phone,
            })

            # Compute fees
            currency = self.env['res.currency'].browse(values.get('currency_id')).exists()
            values['fees'] = acquirer._compute_fees(
                values.get('amount', 0), currency, partner.country_id
            )

            # Include acquirer-specific create values
            values.update(self._get_specific_create_values(acquirer.provider, values))

            # Generate the hash for the callback if one has be configured on the tx
            values['callback_hash'] = self._generate_callback_hash(
                values.get('callback_model_id'),
                values.get('callback_res_id'),
                values.get('callback_method'),
            )

        txs = super().create(values_list)

        # Monetary fields are rounded with the currency at creation time by the ORM. Sometimes, this
        # can lead to inconsistent string representation of the amounts sent to the providers.
        # E.g., tx.create(amount=1111.11) -> tx.amount == 1111.1100000000001
        # To ensure a proper string representation, we invalidate this request's cache values of the
        # `amount` and `fees` fields for the created transactions. This forces the ORM to read the
        # values from the DB where there were stored using `float_repr`, which produces a result
        # consistent with the format expected by providers.
        # E.g., tx.create(amount=1111.11) ; tx.invalidate_cache() -> tx.amount == 1111.11
        txs.invalidate_cache(['amount', 'fees'])

        return txs

    @api.model
    def _get_specific_create_values(self, provider, values):
        """ Complete the values of the `create` method with acquirer-specific values.

        For an acquirer to add its own create values, it must overwrite this method and return a
        dict of values. Acquirer-specific values take precedence over those of the dict of generic
        create values.

        :param str provider: The provider of the acquirer that handled the transaction
        :param dict values: The original create values
        :return: The dict of acquirer-specific create values
        :rtype: dict
        """
        return dict()

    #=== ACTION METHODS ===#

    def action_view_invoices(self):
        """ Return the action for the views of the invoices linked to the transaction.

        Note: self.ensure_one()

        :return: The action
        :rtype: dict
        """
        self.ensure_one()

        action = {
            'name': _("Invoices"),
            'type': 'ir.actions.act_window',
            'res_model': 'account.move',
            'target': 'current',
        }
        invoice_ids = self.invoice_ids.ids
        if len(invoice_ids) == 1:
            invoice = invoice_ids[0]
            action['res_id'] = invoice
            action['view_mode'] = 'form'
            action['views'] = [(self.env.ref('account.view_move_form').id, 'form')]
        else:
            action['view_mode'] = 'tree,form'
            action['domain'] = [('id', 'in', invoice_ids)]
        return action

    def action_capture(self):
        """ Check the state of the transactions and request their capture. """
        if any(tx.state != 'authorized' for tx in self):
            raise ValidationError(_("Only authorized transactions can be captured."))

        for tx in self:
            tx._send_capture_request()

    def action_void(self):
        """ Check the state of the transaction and request to have them voided. """
        if any(tx.state != 'authorized' for tx in self):
            raise ValidationError(_("Only authorized transactions can be voided."))

        for tx in self:
            tx._send_void_request()

    #=== BUSINESS METHODS - PAYMENT FLOW ===#

    @api.model
    def _compute_reference(self, provider, prefix=None, separator='-', **kwargs):
        """ Compute a unique reference for the transaction.

        The reference either corresponds to the prefix if no other transaction with that prefix
        already exists, or follows the pattern `{computed_prefix}{separator}{sequence_number}` where
          - {computed_prefix} is:
            - The provided custom prefix, if any.
            - The computation result of `_compute_reference_prefix` if the custom prefix is not
              filled but the kwargs are.
            - 'tx-{datetime}', if neither the custom prefix nor the kwargs are filled.
          - {separator} is a custom string also used in `_compute_reference_prefix`.
          - {sequence_number} is the next integer in the sequence of references sharing the exact
            same prefix, '1' if there is only one matching reference (hence without sequence number)

        Examples:
          - Given the custom prefix 'example' which has no match with an existing reference, the
            full reference will be 'example'.
          - Given the custom prefix 'example' which matches the existing reference 'example', and
            the custom separator '-', the full reference will be 'example-1'.
          - Given the kwargs {'invoice_ids': [1, 2]}, the custom separator '-' and no custom prefix,
            the full reference will be 'INV1-INV2' (or similar) if no existing reference has the
            same prefix, or 'INV1-INV2-n' if n existing references have the same prefix.

        :param str provider: The provider of the acquirer handling the transaction
        :param str prefix: The custom prefix used to compute the full reference
        :param str separator: The custom separator used to separate the prefix from the suffix, and
                              passed to `_compute_reference_prefix` if it is called
        :param dict kwargs: Optional values passed to `_compute_reference_prefix` if no custom
                            prefix is provided
        :return: The unique reference for the transaction
        :rtype: str
        """
        # Compute the prefix
        if prefix:
            # Replace special characters by their ASCII alternative (é -> e ; ä -> a ; ...)
            prefix = unicodedata.normalize('NFKD', prefix).encode('ascii', 'ignore').decode('utf-8')
        if not prefix:  # Prefix not provided or voided above, compute it based on the kwargs
            prefix = self.sudo()._compute_reference_prefix(provider, separator, **kwargs)
        if not prefix:  # Prefix not computed from the kwargs, fallback on time-based value
            prefix = payment_utils.singularize_reference_prefix()

        # Compute the sequence number
        reference = prefix  # The first reference of a sequence has no sequence number
        if self.sudo().search([('reference', '=', prefix)]):  # The reference already has a match
            # We now execute a second search on `payment.transaction` to fetch all the references
            # starting with the given prefix. The load of these two searches is mitigated by the
            # index on `reference`. Although not ideal, this solution allows for quickly knowing
            # whether the sequence for a given prefix is already started or not, usually not. An SQL
            # query wouldn't help either as the selector is arbitrary and doing that would be an
            # open-door to SQL injections.
            same_prefix_references = self.sudo().search(
                [('reference', 'like', f'{prefix}{separator}%')]
            ).with_context(prefetch_fields=False).mapped('reference')

            # A final regex search is necessary to figure out the next sequence number. The previous
            # search could not rely on alphabetically sorting the reference to infer the largest
            # sequence number because both the prefix and the separator are arbitrary. A given
            # prefix could happen to be a substring of the reference from a different sequence.
            # For instance, the prefix 'example' is a valid match for the existing references
            # 'example', 'example-1' and 'example-ref', in that order. Trusting the order to infer
            # the sequence number would lead to a collision with 'example-1'.
            search_pattern = re.compile(rf'^{prefix}{separator}(\d+)$')
            max_sequence_number = 0  # If no match is found, start the sequence with this reference
            for existing_reference in same_prefix_references:
                search_result = re.search(search_pattern, existing_reference)
                if search_result:  # The reference has the same prefix and is from the same sequence
                    # Find the largest sequence number, if any
                    current_sequence = int(search_result.group(1))
                    if current_sequence > max_sequence_number:
                        max_sequence_number = current_sequence

            # Compute the full reference
            reference = f'{prefix}{separator}{max_sequence_number + 1}'
        return reference

    @api.model
    def _compute_reference_prefix(self, provider, separator, **values):
        """ Compute the reference prefix from the transaction values.

        If the `values` parameter has an entry with 'invoice_ids' as key and a list of (4, id, O) or
        (6, 0, ids) X2M command as value, the prefix is computed based on the invoice name(s).
        Otherwise, an empty string is returned.

        Note: This method should be called in sudo mode to give access to documents (INV, SO, ...).

        :param str provider: The provider of the acquirer handling the transaction
        :param str separator: The custom separator used to separate data references
        :param dict values: The transaction values used to compute the reference prefix. It should
                            have the structure {'invoice_ids': [(X2M command), ...], ...}.
        :return: The computed reference prefix if invoice ids are found, an empty string otherwise
        :rtype: str
        """
        command_list = values.get('invoice_ids')
        if command_list:
            # Extract invoice id(s) from the X2M commands
            invoice_ids = self._fields['invoice_ids'].convert_to_cache(command_list, self)
            invoices = self.env['account.move'].browse(invoice_ids).exists()
            if len(invoices) == len(invoice_ids):  # All ids are valid
                return separator.join(invoices.mapped('name'))
        return ''

    @api.model
    def _generate_callback_hash(self, callback_model_id, callback_res_id, callback_method):
        """ Return the hash for the callback on the transaction.

        :param int callback_model_id: The model on which the callback method is defined, as a
                                      `res.model` id
        :param int callback_res_id: The record on which the callback method must be called, as an id
                                    of the callback model
        :param str callback_method: The name of the callback method
        :return: The callback hash
        :retype: str
        """
        if callback_model_id and callback_res_id and callback_method:
            model_name = self.env['ir.model'].sudo().browse(callback_model_id).model
            token = f'{model_name}|{callback_res_id}|{callback_method}'
            callback_hash = hmac_tool(self.env(su=True), 'generate_callback_hash', token)
            return callback_hash
        return None

    def _get_processing_values(self):
        """ Return a dict of values used to process the transaction.

        The returned dict contains the following entries:
            - tx_id: The transaction, as a `payment.transaction` id
            - acquirer_id: The acquirer handling the transaction, as a `payment.acquirer` id
            - provider: The provider of the acquirer
            - reference: The reference of the transaction
            - amount: The rounded amount of the transaction
            - currency_id: The currency of the transaction, as a res.currency id
            - partner_id: The partner making the transaction, as a res.partner id
            - Additional acquirer-specific entries

        Note: self.ensure_one()

        :return: The dict of processing values
        :rtype: dict
        """
        self.ensure_one()

        processing_values = {
            'acquirer_id': self.acquirer_id.id,
            'provider': self.provider,
            'reference': self.reference,
            'amount': self.amount,
            'currency_id': self.currency_id.id,
            'partner_id': self.partner_id.id,
        }

        # Complete generic processing values with acquirer-specific values
        processing_values.update(self._get_specific_processing_values(processing_values))
        _logger.info(
            "generic and acquirer-specific processing values for transaction with id %s:\n%s",
            self.id, pprint.pformat(processing_values)
        )

        # Render the html form for the redirect flow if available
        if self.operation in ('online_redirect', 'validation') \
                and self.acquirer_id.redirect_form_view_id:
            rendering_values = self._get_specific_rendering_values(processing_values)
            _logger.info(
                "acquirer-specific rendering values for transaction with id %s:\n%s",
                self.id, pprint.pformat(rendering_values)
            )
            redirect_form_html = self.acquirer_id.redirect_form_view_id._render(
                rendering_values, engine='ir.qweb'
            )
            processing_values.update(redirect_form_html=redirect_form_html)

        return processing_values

    def _get_specific_processing_values(self, processing_values):
        """ Return a dict of acquirer-specific values used to process the transaction.

        For an acquirer to add its own processing values, it must overwrite this method and return a
        dict of acquirer-specific values based on the generic values returned by this method.
        Acquirer-specific values take precedence over those of the dict of generic processing
        values.

        :param dict processing_values: The generic processing values of the transaction
        :return: The dict of acquirer-specific processing values
        :rtype: dict
        """
        return dict()

    def _get_specific_rendering_values(self, processing_values):
        """ Return a dict of acquirer-specific values used to render the redirect form.

        For an acquirer to add its own rendering values, it must overwrite this method and return a
        dict of acquirer-specific values based on the processing values (acquirer-specific
        processing values included).

        :param dict processing_values: The processing values of the transaction
        :return: The dict of acquirer-specific rendering values
        :rtype: dict
        """
        return dict()

    def _send_payment_request(self):
        """ Request the provider of the acquirer handling the transaction to execute the payment.

        For an acquirer to support tokenization, it must override this method and call it to log the
        'sent' message, then request a money transfer to its provider.

        Note: self.ensure_one()

        :return: None
        """
        self.ensure_one()
        self._log_sent_message()

    @api.model
    def _handle_feedback_data(self, provider, data):
        """ Match the transaction with the feedback data, update its state and return it.

        :param str provider: The provider of the acquirer that handled the transaction
        :param dict data: The feedback data sent by the provider
        :return: The transaction
        :rtype: recordset of `payment.transaction`
        """
        tx = self._get_tx_from_feedback_data(provider, data)
        tx._process_feedback_data(data)
        tx._execute_callback()
        return tx

    @api.model
    def _get_tx_from_feedback_data(self, provider, data):
        """ Find the transaction based on the feedback data.

        For an acquirer to handle transaction post-processing, it must overwrite this method and
        return the transaction matching the data.

        :param str provider: The provider of the acquirer that handled the transaction
        :param dict data: The feedback data sent by the acquirer
        :return: The transaction if found
        :rtype: recordset of `payment.transaction`
        """
        return self

    def _process_feedback_data(self, data):
        """ Update the transaction state and the acquirer reference based on the feedback data.

        For an acquirer to handle transaction post-processing, it must overwrite this method and
        process the feedback data.

        Note: self.ensure_one()

        :param dict data: The feedback data sent by the acquirer
        :return: None
        """
        self.ensure_one()

    def _set_pending(self, state_message=None):
        """ Update the transactions' state to 'pending'.

        :param str state_message: The reason for which the transaction is set in 'pending' state
        :return: None
        """
        allowed_states = ('draft',)
        target_state = 'pending'
        txs_to_process = self._update_state(allowed_states, target_state, state_message)
        txs_to_process._log_received_message()

    def _set_authorized(self, state_message=None):
        """ Update the transactions' state to 'authorized'.

        :param str state_message: The reason for which the transaction is set in 'authorized' state
        :return: None
        """
        allowed_states = ('draft', 'pending')
        target_state = 'authorized'
        txs_to_process = self._update_state(allowed_states, target_state, state_message)
        txs_to_process._log_received_message()

    def _set_done(self, state_message=None):
        """ Update the transactions' state to 'done'.

        :return: None
        """
        allowed_states = ('draft', 'pending', 'authorized', 'error')
        target_state = 'done'
        txs_to_process = self._update_state(allowed_states, target_state, state_message)
        txs_to_process._log_received_message()

    def _set_canceled(self, state_message=None):
        """ Update the transactions' state to 'cancel'.

        :param str state_message: The reason for which the transaction is set in 'cancel' state
        :return: None
        """
        allowed_states = ('draft', 'pending', 'authorized')
        target_state = 'cancel'
        txs_to_process = self._update_state(allowed_states, target_state, state_message)
        # Cancel the existing payments
        txs_to_process.mapped('payment_id').action_cancel()
        txs_to_process._log_received_message()

    def _set_error(self, state_message):
        """ Update the transactions' state to 'error'.

        :param str state_message: The reason for which the transaction is set in 'error' state
        :return: None
        """
        allowed_states = ('draft', 'pending', 'authorized')
        target_state = 'error'
        txs_to_process = self._update_state(allowed_states, target_state, state_message)
        txs_to_process._log_received_message()

    def _update_state(self, allowed_states, target_state, state_message):
        """ Update the transactions' state to the target state if the current state allows it.

        If the current state is the same as the target state, the transaction is skipped.

        :param tuple[str] allowed_states: The allowed source states for the target state
        :param str target_state: The target state
        :param str state_message: The message to set as `state_message`
        :return: The recordset of transactions whose state was correctly updated
        :rtype: recordset of `payment.transaction`
        """

        def _classify_by_state(_transactions):
            """Classify the transactions according to their current state.

            For each transaction of the current recordset, if:
                - The state is an allowed state: the transaction is flagged as 'to process'.
                - The state is equal to the target state: the transaction is flagged as 'processed'.
                - The state matches none of above: the transaction is flagged as 'in wrong state'.

            :param recordset _transactions: The transactions to classify, as a `payment.transaction`
                                            recordset
            :return: A 3-items tuple of recordsets of classified transactions, in this order:
                     transactions 'to process', 'processed', and 'in wrong state'
            :rtype: tuple(recordset)
            """
            _txs_to_process = _transactions.filtered(lambda _tx: _tx.state in allowed_states)
            _txs_already_processed = _transactions.filtered(lambda _tx: _tx.state == target_state)
            _txs_wrong_state = _transactions - _txs_to_process - _txs_already_processed

            return _txs_to_process, _txs_already_processed, _txs_wrong_state

        txs_to_process, txs_already_processed, txs_wrong_state = _classify_by_state(self)
        for tx in txs_already_processed:
            _logger.info(
                "tried to write tx state with same value (ref: %s, state: %s)",
                tx.reference, tx.state
            )
        for tx in txs_wrong_state:
            logging_values = {
                'reference': tx.reference,
                'tx_state': tx.state,
                'target_state': target_state,
                'allowed_states': allowed_states,
            }
            _logger.warning(
                "tried to write tx state with illegal value (ref: %(reference)s, previous state "
                "%(tx_state)s, target state: %(target_state)s, expected previous state to be in: "
                "%(allowed_states)s)", logging_values
            )
        txs_to_process.write({
            'state': target_state,
            'state_message': state_message,
            'last_state_change': fields.Datetime.now(),
        })
        return txs_to_process

    def _execute_callback(self):
        """ Execute the callbacks defined on the transactions.

        Callbacks that have already been executed are silently ignored. This case can happen when a
        transaction is first authorized before being confirmed, for instance. In this case, both
        status updates try to execute the callback.

        Only successful callbacks are marked as done. This allows callbacks to reschedule themselves
        should the conditions not be met in the present call.

        :return: None
        """
        for tx in self.filtered(lambda t: not t.sudo().callback_is_done):
            # Only use sudo to check, not to execute
            model_sudo = tx.sudo().callback_model_id
            res_id = tx.sudo().callback_res_id
            method = tx.sudo().callback_method
            if not (model_sudo and res_id and method):
                continue  # Skip transactions with unset (or not properly defined) callbacks

            valid_callback_hash = self._generate_callback_hash(model_sudo.id, res_id, method)
            if not consteq(ustr(valid_callback_hash), tx.callback_hash):
                _logger.warning("invalid callback signature for transaction with id %s", tx.id)
                continue  # Ignore tampered callbacks

            record = self.env[model_sudo.model].browse(res_id).exists()
            if not record:
                logging_values = {
                    'model': model_sudo.model,
                    'record_id': res_id,
                    'tx_id': tx.id,
                }
                _logger.warning(
                    "invalid callback record %(model)s.%(record_id)s for transaction with id "
                    "%(tx_id)s", logging_values
                )
                continue  # Ignore invalidated callbacks

            success = getattr(record, method)(tx)  # Execute the callback
            tx.callback_is_done = success or success is None  # Missing returns are successful

    def _send_refund_request(self):
        """ Request the provider of the acquirer handling the transaction to refund it.

        For an acquirer to support tokenization, it must override this method and request a refund
        to its provider *if the validation amount is not null*.

        Note: self.ensure_one()

        :return: None
        """
        self.ensure_one()

    def _send_capture_request(self):
        """ Request the provider of the acquirer handling the transaction to capture it.

        For an acquirer to support authorization, it must override this method and request a capture
        to its provider.

        Note: self.ensure_one()

        :return: None
        """
        self.ensure_one()

    def _send_void_request(self):
        """ Request the provider of the acquirer handling the transaction to void it.

        For an acquirer to support authorization, it must override this method and request the
        transaction to be voided to its provider.

        Note: self.ensure_one()

        :return: None
        """
        self.ensure_one()

    #=== BUSINESS METHODS - POST-PROCESSING ===#

    def _get_post_processing_values(self):
        """ Return a dict of values used to display the status of the transaction.

        For an acquirer to handle transaction status display, it must override this method and
        return a dict of values. Acquirer-specific values take precedence over those of the dict of
        generic post-processing values.

        The returned dict contains the following entries:
            - provider: The provider of the acquirer
            - reference: The reference of the transaction
            - amount: The rounded amount of the transaction
            - currency_id: The currency of the transaction, as a res.currency id
            - state: The transaction state: draft, pending, authorized, done, cancel or error
            - state_message: The information message about the state
            - is_post_processed: Whether the transaction has already been post-processed
            - landing_route: The route the user is redirected to after the transaction
            - Additional acquirer-specific entries

        Note: self.ensure_one()

        :return: The dict of processing values
        :rtype: dict
        """
        self.ensure_one()

        post_processing_values = {
            'provider': self.provider,
            'reference': self.reference,
            'amount': self.amount,
            'currency_code': self.currency_id.name,
            'state': self.state,
            'state_message': self.state_message,
            'is_validation': self.operation == 'validation',
            'is_post_processed': self.is_post_processed,
            'validation_route': self.validation_route,
            'landing_route': self.landing_route,
        }
        _logger.debug(
            "post-processing values for acquirer with id %s:\n%s",
            self.acquirer_id.id, pprint.pformat(post_processing_values)
        )  # DEBUG level because this can get spammy with transactions in non-final states
        return post_processing_values

    def _finalize_post_processing(self):
        """ Trigger the final post-processing tasks and mark the transactions as post-processed.

        :return: None
        """
        self._reconcile_after_done()
        self._log_received_message()  # 2nd call to link the created account.payment in the chatter
        self.is_post_processed = True

    def _cron_finalize_post_processing(self):
        """ Finalize the post-processing of recently done transactions not handled by the client.

        :return: None
        """
        txs_to_post_process = self
        if not txs_to_post_process:
            # Let the client post-process transactions so that they remain available in the portal
            client_handling_limit_date = datetime.now() - relativedelta.relativedelta(minutes=10)
            # Don't try forever to post-process a transaction that doesn't go through
            retry_limit_date = datetime.now() - relativedelta.relativedelta(days=2)
            # Retrieve all transactions matching the criteria for post-processing
            txs_to_post_process = self.search([
                ('state', '=', 'done'),
                ('is_post_processed', '=', False),
                ('last_state_change', '<=', client_handling_limit_date),
                ('last_state_change', '>=', retry_limit_date),
            ])
        for tx in txs_to_post_process:
            try:
                tx._finalize_post_processing()
                self.env.cr.commit()
            except psycopg2.OperationalError:  # A collision of accounting sequences occurred
                self.env.cr.rollback()  # Rollback and try later
            except Exception as e:
                _logger.exception(
                    "encountered an error while post-processing transaction with id %s:\n%s",
                    tx.id, e
                )
                self.env.cr.rollback()

    def _reconcile_after_done(self):
        """ Post relevant fiscal documents and create missing payments.

        As there is nothing to reconcile for validation transactions, no payment is created for
        them. This is also true for validations with a validity check (transfer of a small amount
        with immediate refund) because validation amounts are not included in payouts.

        :return: None
        """
        # Validate invoices automatically once the transaction is confirmed
        self.invoice_ids.filtered(lambda inv: inv.state == 'draft').action_post()

        # Create and post missing payments for transactions requiring reconciliation
        for tx in self.filtered(lambda t: t.operation != 'validation' and not t.payment_id):
            tx._create_payment()

    def _create_payment(self, **extra_create_values):
        """Create an `account.payment` record for the current transaction.

        If the transaction is linked to some invoices, their reconciliation is done automatically.

        Note: self.ensure_one()

        :param dict extra_create_values: Optional extra create values
        :return: The created payment
        :rtype: recordset of `account.payment`
        """
        self.ensure_one()

        payment_method = self.env['account.payment.method'].search([('code', '=', self.acquirer_id.provider)], limit=1)
        payment_values = {
            'amount': self.amount,
            'payment_type': 'inbound' if self.amount > 0 else 'outbound',
            'currency_id': self.currency_id.id,
            'partner_id': self.partner_id.commercial_partner_id.id,
            'partner_type': 'customer',
            'journal_id': self.acquirer_id.journal_id.id,
            'company_id': self.acquirer_id.company_id.id,
            'payment_method_id': payment_method.id,
            'payment_token_id': self.token_id.id,
            'payment_transaction_id': self.id,
            'ref': self.reference,
            **extra_create_values,
        }
        payment = self.env['account.payment'].create(payment_values)
        payment.action_post()

        # Track the payment to make a one2one.
        self.payment_id = payment

        if self.invoice_ids:
            self.invoice_ids.filtered(lambda inv: inv.state == 'draft').action_post()

            (payment.line_ids + self.invoice_ids.line_ids).filtered(
                lambda line: line.account_id == payment.destination_account_id
                and not line.reconciled
            ).reconcile()

        return payment

    #=== BUSINESS METHODS - LOGGING ===#

    def _log_sent_message(self):
        """ Log in the chatter of relevant documents that the transactions have been initiated.

        :return: None
        """
        for tx in self:
            message = tx._get_sent_message()
            tx._log_message_on_linked_documents(message)

    def _log_received_message(self):
        """ Log in the chatter of relevant documents that the transactions have been received.

        A transaction is 'received' when a response is received from the provider of the acquirer
        handling the transaction.

        :return: None
        """
        for tx in self:
            message = tx._get_received_message()
            tx._log_message_on_linked_documents(message)

    def _log_message_on_linked_documents(self, message):
        """ Log a message on the invoices linked to the transaction.

        For a module to implement payments and link documents to a transaction, it must override
        this method and call super, then log the message on documents linked to the transaction.

        Note: self.ensure_one()

        :param str message: The message to be logged
        :return: None
        """
        self.ensure_one()

        for invoice in self.invoice_ids:
            invoice.message_post(body=message)

    #=== BUSINESS METHODS - GETTERS ===#

    def _get_sent_message(self):
        """ Return the message stating that the transaction has been requested.

        Note: self.ensure_one()

        :return: The 'transaction sent' message
        :rtype: str
        """
        self.ensure_one()

        # Choose the message based on the payment flow
        if self.operation in ('online_redirect', 'online_direct'):
            message = _(
                "A transaction with reference %(ref)s has been initiated (%(acq_name)s).",
                ref=self.reference, acq_name=self.acquirer_id.name
            )
        else:  # 'online_token'
            message = _(
                "A transaction with reference %(ref)s has been initiated using the payment method "
                "%(token_name)s (%(acq_name)s).",
                ref=self.reference, token_name=self.token_id.name, acq_name=self.acquirer_id.name
            )
        return message

    def _get_received_message(self):
        """ Return the message stating that the transaction has been received by the provider.

        Note: self.ensure_one()
        """
        self.ensure_one()

        formatted_amount = formatLang(self.env, self.amount, currency_obj=self.currency_id)
        if self.state == 'pending':
            message = _(
                "The transaction with reference %(ref)s for %(amount)s is pending (%(acq_name)s).",
                ref=self.reference, amount=formatted_amount, acq_name=self.acquirer_id.name
            )
        elif self.state == 'authorized':
            message = _(
                "The transaction with reference %(ref)s for %(amount)s has been authorized "
                "(%(acq_name)s).", ref=self.reference, amount=formatted_amount,
                acq_name=self.acquirer_id.name
            )
        elif self.state == 'done':
            message = _(
                "The transaction with reference %(ref)s for %(amount)s has been confirmed "
                "(%(acq_name)s).", ref=self.reference, amount=formatted_amount,
                acq_name=self.acquirer_id.name
            )
            if self.payment_id:
                message += _(
                    "\nThe related payment is posted: %s",
                    self.payment_id._get_payment_chatter_link()
                )
        elif self.state == 'error':
            message = _(
                "The transaction with reference %(ref)s for %(amount)s encountered an error"
                " (%(acq_name)s).",
                ref=self.reference, amount=formatted_amount, acq_name=self.acquirer_id.name
            )
            if self.state_message:
                message += _("\nError: %s", self.state_message)
        else:
            message = _(
                "The transaction with reference %(ref)s for %(amount)s is canceled (%(acq_name)s).",
                ref=self.reference, amount=formatted_amount, acq_name=self.acquirer_id.name
            )
            if self.state_message:
                message += _("\nReason: %s", self.state_message)
        return message

    def _get_last(self):
        """ Return the last transaction of the recordset.

        :return: The last transaction of the recordset, sorted by id
        :rtype: recordset of `payment.transaction`
        """
        return self.filtered(lambda t: t.state != 'draft').sorted()[:1]
示例#5
0
class Approvalslist(models.Model):
    _name = "approvals.list"
    _inherit = ['mail.thread', 'mail.activity.mixin']
    _rec_name = 'resource_ref'
    _description = "Approval"
    _order = 'create_date desc'

    @api.model
    def create(self, vals):
        if vals:
            vals.update(
                {'name': self.env['ir.sequence'].get('approvals.list')})
        result = super(Approvalslist, self).create(vals)
        return result

    name = fields.Char(string="Name")
    model_id = fields.Many2one('ir.model', string='Approval Record')
    model_name = fields.Char(related="model_id.name", string='Model')
    date = fields.Date(string='Requesting Date')
    user_id = fields.Many2one('res.users', string='Requesting User')
    rule_id = fields.Many2one('exception.rule', string='Approval Matrix')
    group_id = fields.Many2one('res.groups', string='Approving Groups')
    test_int = fields.Integer()
    resource_ref = fields.Reference(
        string='Record',
        selection='_selection_target_model',
    )
    state = fields.Selection([('pending_approval', 'Pending Approval'),
                              ('approved', 'Approved'),
                              ('rejected', 'Rejected')],
                             string='State',
                             default="pending_approval")
    group_approval_id = fields.Many2one('group.and.approval')
    approvals_done = fields.Integer(string="Approval Done")
    rejections_done = fields.Integer(string="Rejections Done")
    approvals_required = fields.Integer(
        related='group_approval_id.minimum_approval',
        string="Approvals Required")
    rejections_required = fields.Integer(
        related='group_approval_id.minimum_rejection',
        string="Rejections Required")
    approval_user_matrix_id = fields.One2many('approval.user.matrix',
                                              'approval_id')

    day_approval = fields.Integer(string="Days To Approve")
    approval_deadline = fields.Date(string="Approval DeadLine Date")
    color = fields.Integer('Color Index', default=0)

    @api.model
    def _selection_target_model(self):
        models = self.env['ir.model'].search([])
        s = [(model.model, model.name) for model in models]
        return s

    @api.multi
    def approve(self):
        if self.env.user.id in self.group_id.users.ids:
            matrix_check = self.env['approval.user.matrix'].search(
                [('approval_id', '=', self.id),
                 ('user', '=', self.env.user.id)],
                limit=1)

            if matrix_check.user_response == False:
                self.approvals_done += 1

                matrix_check.accepted = True
                self.user_took_action_chatter('Approved')

                sumry = "Approved " + str(datetime.now())
                self.activity_feedback(
                    ['base_exception_and_approval.mail_act_approval'],
                    user_id=self.env.user.id,
                    feedback=sumry)

                if self.approvals_done == self.approvals_required:

                    self.all_activity_unlinks()
                    self.state = 'approved'
                    k = self.env['approvals.list'].search([
                        ('resource_ref', '=', self.resource_ref._name + ',' +
                         str(self.resource_ref.id))
                    ])
                    list_of_approvals = [
                        approval.state == 'approved' for approval in k
                    ]
                    if all(list_of_approvals):
                        self.resource_ref.ignore_exception = True
                        return True
            else:
                raise UserError('You already have a response on this record')
        else:
            raise UserError('You are not authorized to take an action')

    def user_took_action_chatter(self, s):
        _body = (_(("<ul><b>{0}</b> {1} </ul> ").format(self.env.user.name,
                                                        s)))
        self.resource_ref.message_post(body=_body)
        self.message_post(body=_body)

    @api.multi
    def reject(self):

        if self.env.user.id in self.group_id.users.ids:
            matrix_check = self.env['approval.user.matrix'].search(
                [('approval_id', '=', self.id),
                 ('user', '=', self.env.user.id)],
                limit=1)

            if matrix_check.user_response == False:
                self.rejections_done += 1

                matrix_check.rejected = True
                self.user_took_action_chatter('Rejected')

                sumry = "Rejected " + str(datetime.now())
                self.activity_feedback(
                    ['base_exception_and_approval.mail_act_approval'],
                    user_id=self.env.user.id,
                    feedback=sumry)

                if self.rejections_done == self.rejections_required:
                    self.state = 'rejected'
                    self.all_activity_unlinks()
                    return True

            else:
                raise UserError('You already have a response on this record')
        else:
            raise UserError('You are not authorized to take an action')

    @api.multi
    def reopen_request(self):
        self.state = 'pending_approval'

    #--------------------------------------- Activity done

    def activity_feedback(self, act_type_xmlids, user_id=None, feedback=None):
        """ Set activities as done, limiting to some activity types and
        optionally to a given user. """
        if self.env.context.get('mail_activity_automation_skip'):
            return False

        # print("--------------------------act_type_xmlids",act_type_xmlids)
        Data = self.env['ir.model.data'].sudo()
        activity_types_ids = [
            Data.xmlid_to_res_id(xmlid) for xmlid in act_type_xmlids
        ]

        # print("----------------------activity_types_ids ",activity_types_ids )

        domain = [
            '&', '&', '&', ('res_model', '=', self._name),
            ('res_id', 'in', self.ids), ('automated', '=', True),
            ('activity_type_id', 'in', [4])
        ]
        if user_id:
            domain = ['&'] + domain + [('user_id', '=', user_id)]
        activities = self.env['mail.activity'].search(domain)
        if activities:
            activities.action_feedback(feedback=feedback)
        return True

    #-----------------------------------------

    def all_activity_unlinks(self):
        if self:
            # print("------------------all_activity_unlinks")
            domain = [
                '&', '&', '&', ('res_model', '=', self._name),
                ('res_id', 'in', self.ids), ('automated', '=', True),
                ('activity_type_id', '=', 4)
            ]
            activities = self.env['mail.activity'].search(domain)
            for activity in activities:
                activity.unlink()
class AccountAnalyticContract(models.Model):
    _name = 'account.analytic.contract'

    # These fields will not be synced to the contract
    NO_SYNC = [
        'name',
        'partner_id',
    ]

    name = fields.Char(
        required=True,
    )
    # Needed for avoiding errors on several inherited behaviors
    partner_id = fields.Many2one(
        comodel_name="res.partner",
        string="Partner (always False)",
    )

    pricelist_id = fields.Many2one(
        comodel_name='product.pricelist',
        string='Pricelist',
    )
    recurring_invoice_line_ids = fields.One2many(
        comodel_name='account.analytic.contract.line',
        inverse_name='analytic_account_id',
        copy=True,
        string='Invoice Lines',
    )
    recurring_rule_type = fields.Selection(
        [('daily', 'Day(s)'),
         ('weekly', 'Week(s)'),
         ('monthly', 'Month(s)'),
         ('monthlylastday', 'Month(s) last day'),
         ('yearly', 'Year(s)'),
         ],
        default='monthly',
        string='Recurrence',
        help="Specify Interval for automatic invoice generation.",
    )
    recurring_invoicing_type = fields.Selection(
        [('pre-paid', 'Pre-paid'),
         ('post-paid', 'Post-paid'),
         ],
        default='pre-paid',
        string='Invoicing type',
        help="Specify if process date is 'from' or 'to' invoicing date",
    )
    recurring_interval = fields.Integer(
        default=1,
        string='Repeat Every',
        help="Repeat every (Days/Week/Month/Year)",
    )
    journal_id = fields.Many2one(
        'account.journal',
        string='Journal',
        default=lambda s: s._default_journal(),
        domain="[('type', '=', 'sale'),('company_id', '=', company_id)]",
    )
    company_id = fields.Many2one(
        'res.company',
        string='Company',
        required=True,
        default=lambda self: self.env.user.company_id,
    )

    @api.model
    def _default_journal(self):
        company_id = self.env.context.get(
            'company_id', self.env.user.company_id.id)
        domain = [
            ('type', '=', 'sale'),
            ('company_id', '=', company_id)]
        return self.env['account.journal'].search(domain, limit=1)
示例#7
0
class WorkManagement(models.Model):
    _name = 'benart.work_management'
    _rec_name = 'work_definiton_summary'
    _inherit = ['mail.thread', 'mail.activity.mixin', 'utm.mixin']

    @api.model
    def _get_default_stage(self):
        stage_ids = self.env['benart.par.work_management_stage'].search(
            [('active', '=', True)], order="sequence asc")

        if stage_ids:
            return stage_ids[0]

    state = fields.Selection([('open', 'Open'), ('completed', 'Completed'),
                              ('cancelled', 'Cancelled')],
                             string='State',
                             default='open',
                             translate=True,
                             track_visibility="onchange",
                             required=True)

    res_partner_id = fields.Many2one('res.partner',
                                     required=True,
                                     string="Firm",
                                     translate=True,
                                     track_visibility="onchange")
    res_partner_id_name = fields.Char(related='res_partner_id.name')
    mobile = fields.Char(related='res_partner_id.mobile')
    email = fields.Char(related='res_partner_id.email')

    assingnee_id = fields.Many2one('hr.employee',
                                   string="Assingnee",
                                   translate=True,
                                   track_visibility="onchange")

    work_definiton_summary = fields.Char(
        'Work Definition Summary',
        track_visibility="onchange",
        translate=True,
        required=True,
    )
    work_definiton = fields.Text('Work Definition',
                                 track_visibility="onchange",
                                 translate=True)

    work_management_stage_id = fields.Many2one(
        'benart.par.work_management_stage',
        'Stage',
        translate=True,
        track_visibility="onchange",
        required=True,
        index=True,
        default=_get_default_stage)

    certificate_name = fields.Char(
        related='certificate_id.certification_number')

    certificate_id = fields.Many2one('benart.certificate',
                                     'Certificate',
                                     translate=True,
                                     track_visibility="onchange")

    deadline_date = fields.Date("Deadline",
                                translate=True,
                                track_visibility="onchange",
                                default=fields.Date.context_today)

    active = fields.Boolean('Active',
                            default=True,
                            track_visibility="onchange",
                            translate=True)

    color = fields.Integer("Color Index", default=0)
    priority = fields.Selection(AVAILABLE_PRIORITIES,
                                "Appreciation",
                                default='0')

    categ_ids = fields.Many2many('benart.work_management_category',
                                 string="Tags")

    hide_deadline_date = fields.Boolean('Hide Deadline',
                                        compute='_compute_hide_deadline_date',
                                        track_visibility="onchange",
                                        translate=True)

    kanban_state = fields.Selection([('normal', 'Grey'), ('done', 'Green'),
                                     ('blocked', 'Red')],
                                    string='Kanban State',
                                    translate=True,
                                    copy=False,
                                    default='normal',
                                    required=True)

    legend_blocked = fields.Char(
        related='work_management_stage_id.legend_blocked',
        string='Kanban Blocked',
        readonly=False)
    legend_done = fields.Char(related='work_management_stage_id.legend_done',
                              string='Kanban Valid',
                              readonly=False)
    legend_normal = fields.Char(
        related='work_management_stage_id.legend_normal',
        string='Kanban Ongoing',
        readonly=False)

    @api.model
    def get_email_to_only_admin(self):
        user_group = self.env.ref(
            "odoo_benart_modified.group_certification_admin")
        email_list = [usr.login for usr in user_group.users if usr.login]
        return ",".join(email_list)

    @api.multi
    def work_management_batch(self):
        template = self.env.ref(
            'benart_work_management.work_management_not_updated_work')
        work_management_id = self.env['benart.work_management'].search(
            [('active', '=', True)], limit=1)
        if template:
            template.send_mail(work_management_id.id, force_send=True)

    @api.multi
    def get_not_updated_works(self):
        not_updated_work_management_ids = []
        work_management_ids = self.env['benart.work_management'].search([
            ('active', '=', True), ('state', '=', 'open')
        ])
        for i in work_management_ids:
            message_id = self.env['mail.message'].search(
                [('message_type', '=', 'comment'),
                 ('model', '=', 'benart.work_management'),
                 ('res_id', '=', i.id)],
                order="date desc",
                limit=1)
            if not message_id and i.create_date < (datetime.today() -
                                                   relativedelta(weeks=1)):
                not_updated_work_management_ids.append(i)
            elif message_id and message_id.date < (datetime.today() -
                                                   relativedelta(weeks=1)):
                not_updated_work_management_ids.append(i)
        return not_updated_work_management_ids

    @api.multi
    def _compute_hide_deadline_date(self):
        for i in self:
            if self.env.user.has_group(
                    'benart_work_management.group_work_management_admin'):
                i.hide_deadline_date = True
            else:
                i.hide_deadline_date = False

    @api.multi
    @api.constrains('work_management_stage_id')
    def _compute_assingnee_id(self):
        template = self.env.ref(
            'benart_work_management.work_management_assign_mail')
        for i in self:
            if i.work_management_stage_id and i.work_management_stage_id.default_assignee_id:
                i.assingnee_id = i.work_management_stage_id.default_assignee_id
                if template:
                    template.send_mail(i.id, force_send=True)

    @api.model
    def get_email_to(self):
        user_group = self.env.ref(
            "benart_work_management.group_work_management_admin")
        email_list = [usr.login for usr in user_group.users if usr.login]
        if self.assingnee_id:
            email_list.append(self.assingnee_id.user_id.login)
        return ",".join(email_list)

    def complete_work(self):
        template = self.env.ref(
            'benart_work_management.work_management_completed_maill')
        if template:
            for i in self:
                i.active = False
                i.state = "completed"
                template.send_mail(i.id, force_send=True)

    def cancel_work(self):
        template = self.env.ref(
            'benart_work_management.work_management_cancelled_maill')
        if template:
            for i in self:
                i.active = False
                i.state = "cancelled"
                template.send_mail(i.id, force_send=True)

    def reopen_work(self):
        template = self.env.ref(
            'benart_work_management.work_management_reopen_maill')
        if template:
            for i in self:
                i.active = True
                i.state = "open"
                template.send_mail(i.id, force_send=True)

    @api.multi
    def action_get_attachment_tree_view(self):
        attachment_action = self.env.ref('base.action_attachment')
        action = attachment_action.read()[0]
        action['context'] = {
            'default_res_model': self._name,
            'default_res_id': self.ids[0]
        }
        action['domain'] = str(
            ['&', ('res_model', '=', self._name), ('res_id', 'in', self.ids)])
        action['search_view_id'] = (self.env.ref(
            'benart_work_management.ir_attachment_view_search_inherit_work_management'
        ).id, )
        return action
示例#8
0
class AsteriskUser(models.Model):
    _name = 'asterisk.user'
    _order = 'user'
    _description = _('Asterisk User')
    _rec_name = 'user'

    server = fields.Many2one(comodel_name='asterisk.server', required=True)
    user = fields.Many2one('res.users',
                           required=True,
                           ondelete='cascade',
                           domain=[('share', '=', False)])
    peer = fields.Many2one('asterisk.sip_peer',
                           required=True,
                           ondelete='restrict')
    extension = fields.Char(required=True)
    extension_id = fields.One2many('asterisk.extension', inverse_name='user')
    partner = fields.Many2one(related='user.partner_id',
                              readonly=True,
                              string=_('Contact'))
    phone = fields.Char(related='partner.phone')
    mobile = fields.Char(related='partner.mobile')
    voicemail = fields.Char(related='partner.email', string="Voicemail")
    voicemail_enabled = fields.Boolean()
    voicemail_password = fields.Char()
    ring_timeout = fields.Integer(default=30, required=True)
    ring_timeout_estimation = fields.Char(
        compute='_get_ring_timeout_estimation', string=_('Ring timeout'))
    forward_on_timeout = fields.Boolean()
    forward_on_busy = fields.Boolean()
    route_group = fields.Many2one('asterisk.outgoing_route_group',
                                  ondelete='restrict')
    alert_info = fields.Char()
    # TODO
    """
    forward_on_busy_type = fields.Selection([('mobile', 'Mobile phone'),
                                             ('custom', 'Custom phone number')],
                                            default='mobile',
                                            string=_('Forward to'))
    """
    forward_on_unavailable = fields.Boolean()
    timeout_number = fields.Char(string=_('Number to forward'))
    on_busy_number = fields.Char(string=_('Number to forward'))
    on_unavailable_number = fields.Char(string=_('Number to forward'))
    forward_unconditional = fields.Boolean()
    unconditional_number = fields.Char(string=_('Number to forward'))
    dialplan = fields.Text(compute='_get_dialplan')

    _sql_constraints = [
        ('extension_uniq', 'unique(server,extension)',
         _('This extension is already defined on this server!')),
        ('user_uniq', 'unique(server,"user")',
         _('This user is already defined on this server!')),
        ('peer_uniq', 'unique(server,peer)',
         _('This peer is already defined on this server!')),
    ]

    @api.model
    def create(self, vals):
        res = super(AsteriskUser, self).create(vals)
        if res:
            if res.peer.extension_id:
                res.peer.extension_id.unlink()
            res.extension_id = self.env['asterisk.extension'].create({
                'extension_type':
                'user',
                'server':
                res.server.id,
                'user':
                res.id
            })
            self.build_conf()
            self.build_voicemail()
            self.env['asterisk.extension'].sudo().build_conf()
        return res

    @api.multi
    def write(self, vals):
        super(AsteriskUser, self).write(vals)
        self.build_conf()
        self.build_voicemail()
        self.env['asterisk.extension'].sudo().build_conf()
        return True

    @api.multi
    def unlink(self):
        for rec in self:
            # Copy routing group on peer level
            group_id = rec.route_group.id
            extension = rec.extension
            peer = rec.peer
            rec.extension_id.unlink()
            peer.write({
                'route_group': group_id,
                'extension':
                extension  # Extension will be created on peer create()
            })
            super(AsteriskUser, rec).unlink()
        self.build_conf()
        self.build_voicemail()
        self.env['asterisk.extension'].sudo().build_conf()
        return True

    @api.model
    def build_conf(self):
        conf_dict = {}
        users = self.env['asterisk.user'].search([])
        for rec in users:
            # Init server config data
            if not conf_dict.get(rec.server.id):
                conf_dict[rec.server.id] = '[odoo-users]\n'
            conf_dict[rec.server.id] += \
                'exten => {},1,Gosub(odoo-user-{},${{EXTEN}},1)\n'.format(
                                                            rec.extension,
                                                            rec.extension)
        # Now render common dialplan
        for server_id in conf_dict.keys():
            conf_dict[rec.server.id] += self.env['ir.qweb'].render(
                'asterisk_base.asterisk_users_extensions',
                {}).decode('latin-1')
        # Now add personal users contexts
        for rec in users:
            conf_dict[rec.server.id] += '{}\n'.format(
                self.build_user_context(rec))
        # Create asterisk conf
        for server_id in conf_dict.keys():
            conf = self.env['asterisk.conf'].get_or_create(
                server_id, 'extensions_odoo_users.conf')
            # Set conf content
            conf.content = u'{}'.format(
                remove_empty_lines(conf_dict[server_id]))
            conf.include_from('extensions.conf')

    @api.model
    def build_user_context(self, rec):
        rec.ensure_one()
        res = self.env['ir.qweb'].render(
            'asterisk_base.asterisk_user_context', {
                'extension': rec.extension,
                'user_name': rec.partner.name,
                'voicemail_enabled': rec.voicemail_enabled,
                'peer_name': rec.peer.name,
                'ring_timeout': rec.ring_timeout,
                'timeout_number': rec.timeout_number,
                'on_busy_number': rec.on_busy_number,
                'on_unavailable_number': rec.on_unavailable_number,
                'unconditional_number': rec.unconditional_number,
            }).decode('latin-1')
        return remove_empty_lines(res)

    # LITNIALEX TODO: Set peer' mailbox??'
    def build_voicemail(self):
        conf_dict = {}
        for rec in self.env['asterisk.user'].search([]):
            if not conf_dict.get(rec.server.id):
                conf_dict[rec.server.id] = '[odoo-default]\n'
            if rec.extension and rec.voicemail_enabled and rec.voicemail_password:
                conf_dict[rec.server.id] += '{} => {},{},{}\n'.format(
                    rec.extension, rec.voicemail_password, rec.partner.name,
                    rec.voicemail)
                # Update peer's mailbox'
                # PJSIP: check
                if rec.peer:
                    rec.peer.mailbox = '{}@odoo-default'.format(rec.extension)
        # Build conf files
        # Create conf files
        for server_id in conf_dict.keys():
            # First try to get existing conf
            conf = self.env['asterisk.conf'].get_or_create(
                server_id, 'voicemail_odoo.conf')
            conf.content = u'{}'.format(conf_dict[server_id])
            conf.is_updated = True
            conf.include_from('voicemail.conf')

    @api.multi
    def _get_dialplan(self):
        for rec in self:
            rec.dialplan = rec.build_user_context(rec)

    @api.multi
    def _get_ring_timeout_estimation(self):
        for rec in self:
            rec.ring_timeout_estimation = _('{} seconds (~ {} rings)').format(
                rec.ring_timeout, rec.ring_timeout // 5)

    @api.onchange('server')
    def _set_peers(self):
        if self.peer:
            self.peer = False

    @api.onchange('peer')
    def _set_peer_extension(self):
        if self.peer.extension:
            self.extension = self.peer.extension

    @api.multi
    def call_user(self):
        # Used from tree view button
        self.ensure_one()
        self.server.originate_call(self.extension)
class Picking(models.Model):
    """Stock Picking Model."""

    _inherit = "stock.picking"

    @api.depends('state')
    @api.one
    def _get_invoiced(self):
        """Method to get the total invoiced count."""
        for order in self:
            invoice_ids = self.env['account.invoice'].search(
                [('picking_id', '=', order.id)])
            order.invoice_count = len(invoice_ids)

    invoice_count = fields.Integer(
        string='# of Invoices', compute='_get_invoiced',)

    @api.multi
    def button_view_invoice(self):
        """Method to view invoice with filters."""
        mod_obj = self.env['ir.model.data']
        act_obj = self.env['ir.actions.act_window']
        work_order_id = self.env['account.invoice'].search(
            [('picking_id', '=', self.id)])
        inv_ids = []
        for inv_id in work_order_id:
            inv_ids.append(inv_id.id)
            result = mod_obj.get_object_reference(
                'account', 'action_invoice_tree1')
            id = result and result[1] or False
            result = act_obj.browse(id).read()[0]
            res = mod_obj.get_object_reference('account', 'invoice_form')
            result['views'] = [(res and res[1] or False, 'form')]
            result['res_id'] = work_order_id[0].id or False
        return result

    @api.multi
    def action_done(self):
        """Overridden method to generate the invoice based on shipment."""
        super(Picking, self).action_done()
        if self.state == 'done':
            if self.picking_type_id.code == 'incoming':
                purchase = self.purchase_id
                vals = {
                    'type': 'in_invoice',
                    'origin': self.origin,
                    'pur_id': purchase and purchase.id or False,
                    'purchase_id': purchase and purchase.id or False,
                    'partner_id': self.partner_id and
                    self.partner_id.id or False,
                    'picking_id': self.id
                }
                res = self.env['account.invoice'].create(vals)
                res.purchase_order_change()
                res.compute_taxes()
                res._onchange_partner_id()
                for inv_line in res.invoice_line_ids:
                    if inv_line.quantity <= 0:
                        inv_line.unlink()

            if self.picking_type_id.code == 'outgoing':
                inv_obj = self.env['account.invoice']
                sale_order_line_obj = self.env['account.invoice.line']
                sale_order = self.env['sale.order'].search([
                    ('name', '=', self.origin)], limit=1)
                delivery_partner = self.partner_id
                if sale_order:
                    bank_acc = inv_obj._get_default_bank_id(
                        'out_invoice',
                        self.company_id and self.company_id.id)
                    invoice = inv_obj.create({
                        'origin': self.origin,
                        'picking_id': self.id,
                        'type': 'out_invoice',
                        'reference': False,
                        'sale_id': sale_order.id,
                        'account_id': delivery_partner and
                        delivery_partner.property_account_receivable_id and
                        delivery_partner.property_account_receivable_id.id or
                        False,
                        # 'partner_id': sale_order and
                        # sale_order.partner_invoice_id and
                        # sale_order.partner_invoice_id.id or
                        # delivery_partner and delivery_partner.id or False,

                        # We set SO customer as Invoice
                        # Customer As per Client need.
                        'partner_id': sale_order.partner_id and
                        sale_order.partner_id.id or
                        sale_order.partner_invoice_id and
                        sale_order.partner_invoice_id.id or False,
                        'partner_shipping_id':
                        sale_order.partner_shipping_id and
                        sale_order.partner_shipping_id.id or
                        delivery_partner and delivery_partner.id or False,
                        'currency_id': sale_order.pricelist_id.currency_id.id,
                        'payment_term_id': sale_order.payment_term_id.id,
                        'fiscal_position_id': sale_order.fiscal_position_id and
                        sale_order.fiscal_position_id.id or
                        sale_order.partner_id and
                        sale_order.partner_id.property_account_position_id.id,
                        'team_id': sale_order.team_id.id,
                        'comment': sale_order.note,
                        'partner_bank_id': bank_acc and bank_acc.id or False,
                        'date_invoice': fields.Datetime.now().date()
                    })
                    # invoice.date_invoice = fields.Datetime.now().date()
                    for sale_line in self.move_lines:
                        if sale_line.product_id.property_account_income_id:
                            account = sale_line.product_id and \
                                sale_line.product_id.property_account_income_id
                        elif sale_line.product_id.categ_id.\
                                property_account_income_categ_id:
                            account = sale_line.product_id and \
                                sale_line.product_id.categ_id.\
                                property_account_income_categ_id
                        else:
                            account_search = \
                                self.env['ir.property'].search(
                                    [('name', '=',
                                        'property_account_income_categ_id')])
                            account = account_search.value_reference
                            account = account.split(",")[1]
                            account = self.env['account.account'].\
                                browse(account)
                        inv_line = sale_order_line_obj.create({
                            'name': sale_line.name,
                            'account_id': account.id,
                            'invoice_id': invoice.id,
                            'price_unit': sale_line.price_unit * -1,
                            'quantity': sale_line.product_uom_qty,
                            'uom_id': sale_line.product_id.uom_id.id,
                            'product_id': sale_line.product_id.id,
                        })
                        order_line = self.env['sale.order.line'].search(
                            [('order_id', '=', sale_order.id),
                             ('product_id', '=', sale_line.product_id.id)])
                        for order_line in order_line:
                            order_line.write({
                                'qty_to_invoice': sale_line.product_uom_qty,
                                'invoice_lines': [(4, inv_line.id)]
                            })

                        tax_ids = []
                        if order_line and order_line[0]:
                            for tax in order_line[0].tax_id:
                                tax_ids.append(tax.id)
                                inv_line.write({
                                    'price_unit': order_line[0].price_unit,
                                    'discount': order_line[0].discount,
                                    'invoice_line_tax_ids': [(6, 0, tax_ids)]
                                })
                    invoice.compute_taxes()
        return True
示例#10
0
class Company(models.Model):
    _inherit = "res.company"
    _check_company_auto = True

    def _default_confirmation_mail_template(self):
        try:
            return self.env.ref(
                'stock.mail_template_data_delivery_confirmation').id
        except ValueError:
            return False

    internal_transit_location_id = fields.Many2one(
        'stock.location',
        'Internal Transit Location',
        ondelete="restrict",
        check_company=True,
        help=
        "Technical field used for resupply routes between warehouses that belong to this company"
    )
    stock_move_email_validation = fields.Boolean("Email Confirmation picking",
                                                 default=False)
    stock_mail_confirmation_template_id = fields.Many2one(
        'mail.template',
        string="Email Template confirmation picking",
        domain="[('model', '=', 'stock.picking')]",
        default=_default_confirmation_mail_template,
        help="Email sent to the customer once the order is done.")
    annual_inventory_month = fields.Selection(
        [
            ('1', 'January'),
            ('2', 'February'),
            ('3', 'March'),
            ('4', 'April'),
            ('5', 'May'),
            ('6', 'June'),
            ('7', 'July'),
            ('8', 'August'),
            ('9', 'September'),
            ('10', 'October'),
            ('11', 'November'),
            ('12', 'December'),
        ],
        string='Annual Inventory Month',
        default='12',
        help=
        "Annual inventory month for products not in a location with a cyclic inventory date. Set to no month if no automatic annual inventory."
    )
    annual_inventory_day = fields.Integer(
        string='Day of the month',
        default=31,
        help=
        """Day of the month when the annual inventory should occur. If zero or negative, then the first day of the month will be selected instead.
        If greater than the last day of a month, then the last day of the month will be selected instead."""
    )

    def _create_transit_location(self):
        '''Create a transit location with company_id being the given company_id. This is needed
           in case of resuply routes between warehouses belonging to the same company, because
           we don't want to create accounting entries at that time.
        '''
        parent_location = self.env.ref('stock.stock_location_locations',
                                       raise_if_not_found=False)
        for company in self:
            location = self.env['stock.location'].create({
                'name':
                _('Inter-warehouse transit'),
                'usage':
                'transit',
                'location_id':
                parent_location and parent_location.id or False,
                'company_id':
                company.id,
                'active':
                False
            })

            company.write({'internal_transit_location_id': location.id})

            company.partner_id.with_company(company).write({
                'property_stock_customer':
                location.id,
                'property_stock_supplier':
                location.id,
            })

    def _create_inventory_loss_location(self):
        parent_location = self.env.ref(
            'stock.stock_location_locations_virtual', raise_if_not_found=False)
        for company in self:
            inventory_loss_location = self.env['stock.location'].create({
                'name':
                'Inventory adjustment',
                'usage':
                'inventory',
                'location_id':
                parent_location.id,
                'company_id':
                company.id,
            })
            self.env['ir.property']._set_default(
                "property_stock_inventory",
                "product.template",
                inventory_loss_location,
                company.id,
            )

    def _create_production_location(self):
        parent_location = self.env.ref(
            'stock.stock_location_locations_virtual', raise_if_not_found=False)
        for company in self:
            production_location = self.env['stock.location'].create({
                'name':
                'Production',
                'usage':
                'production',
                'location_id':
                parent_location.id,
                'company_id':
                company.id,
            })
            self.env['ir.property']._set_default(
                "property_stock_production",
                "product.template",
                production_location,
                company.id,
            )

    def _create_scrap_location(self):
        parent_location = self.env.ref(
            'stock.stock_location_locations_virtual', raise_if_not_found=False)
        for company in self:
            scrap_location = self.env['stock.location'].create({
                'name':
                'Scrap',
                'usage':
                'inventory',
                'location_id':
                parent_location.id,
                'company_id':
                company.id,
                'scrap_location':
                True,
            })

    def _create_scrap_sequence(self):
        scrap_vals = []
        for company in self:
            scrap_vals.append({
                'name': '%s Sequence scrap' % company.name,
                'code': 'stock.scrap',
                'company_id': company.id,
                'prefix': 'SP/',
                'padding': 5,
                'number_next': 1,
                'number_increment': 1
            })
        if scrap_vals:
            self.env['ir.sequence'].create(scrap_vals)

    @api.model
    def create_missing_warehouse(self):
        """ This hook is used to add a warehouse on existing companies
        when module stock is installed.
        """
        company_ids = self.env['res.company'].search([])
        company_with_warehouse = self.env['stock.warehouse'].with_context(
            active_test=False).search([]).mapped('company_id')
        company_without_warehouse = company_ids - company_with_warehouse
        for company in company_without_warehouse:
            self.env['stock.warehouse'].create({
                'name':
                company.name,
                'code':
                company.name[:5],
                'company_id':
                company.id,
                'partner_id':
                company.partner_id.id,
            })

    @api.model
    def create_missing_transit_location(self):
        company_without_transit = self.env['res.company'].search([
            ('internal_transit_location_id', '=', False)
        ])
        company_without_transit._create_transit_location()

    @api.model
    def create_missing_inventory_loss_location(self):
        company_ids = self.env['res.company'].search([])
        inventory_loss_product_template_field = self.env[
            'ir.model.fields']._get('product.template',
                                    'property_stock_inventory')
        companies_having_property = self.env['ir.property'].sudo().search([
            ('fields_id', '=', inventory_loss_product_template_field.id),
            ('res_id', '=', False)
        ]).mapped('company_id')
        company_without_property = company_ids - companies_having_property
        company_without_property._create_inventory_loss_location()

    @api.model
    def create_missing_production_location(self):
        company_ids = self.env['res.company'].search([])
        production_product_template_field = self.env['ir.model.fields']._get(
            'product.template', 'property_stock_production')
        companies_having_property = self.env['ir.property'].sudo().search([
            ('fields_id', '=', production_product_template_field.id),
            ('res_id', '=', False)
        ]).mapped('company_id')
        company_without_property = company_ids - companies_having_property
        company_without_property._create_production_location()

    @api.model
    def create_missing_scrap_location(self):
        company_ids = self.env['res.company'].search([])
        companies_having_scrap_loc = self.env['stock.location'].search([
            ('scrap_location', '=', True)
        ]).mapped('company_id')
        company_without_property = company_ids - companies_having_scrap_loc
        company_without_property._create_scrap_location()

    @api.model
    def create_missing_scrap_sequence(self):
        company_ids = self.env['res.company'].search([])
        company_has_scrap_seq = self.env['ir.sequence'].search([
            ('code', '=', 'stock.scrap')
        ]).mapped('company_id')
        company_todo_sequence = company_ids - company_has_scrap_seq
        company_todo_sequence._create_scrap_sequence()

    def _create_per_company_locations(self):
        self.ensure_one()
        self._create_transit_location()
        self._create_inventory_loss_location()
        self._create_production_location()
        self._create_scrap_location()

    def _create_per_company_sequences(self):
        self.ensure_one()
        self._create_scrap_sequence()

    def _create_per_company_picking_types(self):
        self.ensure_one()

    def _create_per_company_rules(self):
        self.ensure_one()

    @api.model_create_multi
    def create(self, vals_list):
        companies = super().create(vals_list)
        for company in companies:
            company.sudo()._create_per_company_locations()
            company.sudo()._create_per_company_sequences()
            company.sudo()._create_per_company_picking_types()
            company.sudo()._create_per_company_rules()
        self.env['stock.warehouse'].sudo().create([{
            'name':
            company.name,
            'code':
            self.env.context.get('default_code') or company.name[:5],
            'company_id':
            company.id,
            'partner_id':
            company.partner_id.id
        } for company in companies])
        return companies
class CustomInfoValue(models.Model):
    _description = "Custom information value"
    _name = "custom.info.value"
    _rec_name = 'value'
    _order = ("model, res_id, category_sequence, category_id, "
              "property_sequence, property_id")
    _sql_constraints = [
        ("property_owner",
         "UNIQUE (property_id, model, res_id)",
         "Another property with that name exists for that resource."),
    ]

    model = fields.Char(
        related="property_id.model", index=True, readonly=True,
        auto_join=True, store=True, compute_sudo=True,
    )
    owner_id = fields.Reference(
        selection="_selection_owner_id", string="Owner",
        compute="_compute_owner_id", inverse="_inverse_owner_id",
        help="Record that owns this custom value.",
        compute_sudo=True,
    )
    res_id = fields.Integer(
        string="Resource ID", required=True, index=True, store=True,
        ondelete="cascade",
    )
    property_id = fields.Many2one(
        comodel_name='custom.info.property', required=True, string='Property',
        readonly=True,
    )
    property_sequence = fields.Integer(
        related="property_id.sequence", store=True, index=True, readonly=True,
    )
    category_sequence = fields.Integer(
        string="Category Sequence",
        related="property_id.category_id.sequence", store=True, readonly=True,
    )
    category_id = fields.Many2one(
        related="property_id.category_id", store=True, readonly=True,
    )
    name = fields.Char(related='property_id.name', readonly=True)
    field_type = fields.Selection(
        related="property_id.field_type", readonly=True,
    )
    field_name = fields.Char(
        compute="_compute_field_name",
        help="Technical name of the field where the value is stored.",
    )
    required = fields.Boolean(related="property_id.required", readonly=True)
    value = fields.Char(
        compute="_compute_value", inverse="_inverse_value",
        search="_search_value",
        help="Value, always converted to/from the typed field.",
    )
    value_str = fields.Char(string="Text value", translate=True, index=True)
    value_int = fields.Integer(string="Whole number value", index=True)
    value_float = fields.Float(string="Decimal number value", index=True)
    value_bool = fields.Boolean(string="Yes/No value", index=True)
    value_id = fields.Many2one(
        comodel_name="custom.info.option", string="Selection value",
        ondelete="cascade", domain="[('property_ids', '=', property_id)]",
    )

    @api.multi
    def check_access_rule(self, operation):
        """You access a value if you access its owner record."""
        if self.env.uid != SUPERUSER_ID:
            for record in self.filtered('owner_id'):
                record.owner_id.check_access_rights(operation)
                record.owner_id.check_access_rule(operation)
        return super().check_access_rule(operation)

    @api.model
    def _selection_owner_id(self):
        """You can choose among models linked to a template."""
        models = self.env["ir.model.fields"].search([
            ("ttype", "=", "many2one"),
            ("relation", "=", "custom.info.template"),
            ("model_id.transient", "=", False),
            "!", ("model", "=like", "custom.info.%"),
        ]).mapped("model_id")
        models = models.search([("id", "in", models.ids)], order="name")
        return [(m.model, m.name) for m in models
                if m.model in self.env and self.env[m.model]._auto]

    @api.multi
    @api.depends("property_id.field_type")
    def _compute_field_name(self):
        """Get the technical name where the real typed value is stored."""
        for s in self:
            s.field_name = "value_{!s}".format(s.property_id.field_type)

    @api.multi
    @api.depends("res_id", "model")
    def _compute_owner_id(self):
        """Get the id from the linked record."""
        for record in self:
            record.owner_id = "{},{}".format(record.model, record.res_id)

    @api.multi
    def _inverse_owner_id(self):
        """Store the owner according to the model and ID."""
        for record in self.filtered('owner_id'):
            record.model = record.owner_id._name
            record.res_id = record.owner_id.id

    @api.multi
    @api.depends("property_id.field_type", "field_name", "value_str",
                 "value_int", "value_float", "value_bool", "value_id")
    def _compute_value(self):
        """Get the value as a string, from the original field."""
        for s in self:
            if s.field_type == "id":
                s.value = s.value_id.display_name
            elif s.field_type == "bool":
                s.value = _("Yes") if s.value_bool else _("No")
            else:
                s.value = getattr(s, s.field_name, False)

    @api.multi
    def _inverse_value(self):
        """Write the value correctly converted in the typed field."""
        for record in self:
            if (record.field_type == "id"
                    and record.value == record.value_id.display_name):
                # Avoid another search that can return a different value
                continue
            record[record.field_name] = self._transform_value(
                record.value, record.field_type, record.property_id,
            )

    @api.one
    @api.constrains("property_id", "value_str", "value_int", "value_float")
    def _check_min_max_limits(self):
        """Ensure value falls inside the property's stablished limits."""
        minimum, maximum = self.property_id.minimum, self.property_id.maximum
        if minimum <= maximum:
            value = self[self.field_name]
            if not value:
                # This is a job for :meth:`.~_check_required`
                return
            if self.field_type == "str":
                number = len(self.value_str)
                message = _(
                    "Length for %(prop)s is %(val)s, but it should be "
                    "between %(min)d and %(max)d.")
            elif self.field_type in {"int", "float"}:
                number = value
                if self.field_type == "int":
                    message = _(
                        "Value for %(prop)s is %(val)s, but it should be "
                        "between %(min)d and %(max)d.")
                else:
                    message = _(
                        "Value for %(prop)s is %(val)s, but it should be "
                        "between %(min)f and %(max)f.")
            else:
                return
            if not minimum <= number <= maximum:
                raise ValidationError(message % {
                    "prop": self.property_id.display_name,
                    "val": number,
                    "min": minimum,
                    "max": maximum,
                })

    @api.multi
    @api.onchange("property_id")
    def _onchange_property_set_default_value(self):
        """Load default value for this property."""
        for record in self:
            if not record.value and record.property_id.default_value:
                record.value = record.property_id.default_value
            if not record.field_type and record.property_id.field_type:
                record.field_type = record.property_id.field_type

    @api.onchange('value')
    def _onchange_value(self):
        """Inverse function is not launched after writing, so we need to
        trigger it right now."""
        self._inverse_value()

    @api.model
    def _transform_value(self, value, format_, properties=None):
        """Transforms a text value to the expected format.

        :param str/bool value:
            Custom value in raw string.

        :param str format_:
            Target conversion format for the value. Must be available among
            ``custom.info.property`` options.

        :param recordset properties:
            Useful when :param:`format_` is ``id``, as it helps to ensure the
            option is available in these properties. If :param:`format_` is
            ``id`` and :param:`properties` is ``None``, no transformation will
            be made for :param:`value`.
        """
        if not value:
            value = False
        elif format_ == "id" and properties:
            value = self.env["custom.info.option"].search([
                ("property_ids", "in", properties.ids),
                ("name", "ilike", u"%{}%".format(value)),
            ], limit=1)
        elif format_ == "bool":
            value = value.strip().lower() not in {
                "0", "false", "", "no", "off", _("No").lower()}
        elif format_ not in {"str", "id"}:
            value = safe_eval("{!s}({!r})".format(format_, value))
        return value

    @api.model
    def _search_value(self, operator, value):
        """Search from the stored field directly."""
        options = (
            o[0] for o in
            self.property_id._fields["field_type"]
            .get_description(self.env)["selection"])
        domain = []
        for fmt in options:
            try:
                _value = (self._transform_value(value, fmt)
                          if not isinstance(value, list) else
                          [self._transform_value(v, fmt) for v in value])
            except ValueError:
                # If you are searching something that cannot be casted, then
                # your property is probably from another type
                continue
            domain += [
                "&",
                ("field_type", "=", fmt),
                ("value_" + fmt, operator, _value),
            ]
        return ["|"] * int(len(domain) / 3 - 1) + domain
示例#12
0
class CompanyElectronic(models.Model):
    _name = 'res.company'
    _inherit = [
        'res.company',
        'mail.thread',
    ]

    commercial_name = fields.Char(
        string="Nombre comercial",
        required=False,
    )

    activity_id = fields.Many2one(
        "economic.activity",
        string="Actividad Económica por defecto",
        required=False,
    )

    economic_activities_ids = fields.Many2many(
        'economic.activity',
        string=u'Actividades Económicas',
    )

    signature = fields.Binary(string="Llave Criptográfica", )
    identification_id = fields.Many2one("identification.type",
                                        string="Tipo de identificacion",
                                        required=False)
    district_id = fields.Many2one("res.country.district",
                                  string="Distrito",
                                  required=False)
    county_id = fields.Many2one("res.country.county",
                                string="Cantón",
                                required=False)
    neighborhood_id = fields.Many2one("res.country.neighborhood",
                                      string="Barrios",
                                      required=False)
    frm_ws_identificador = fields.Char(string="Usuario de Factura Electrónica",
                                       required=False)
    frm_ws_password = fields.Char(string="Password de Factura Electrónica",
                                  required=False)

    frm_ws_ambiente = fields.Selection(
        selection=[('disabled', 'Deshabilitado'), ('api-stag', 'Pruebas'),
                   ('api-prod', 'Producción')],
        string="Ambiente",
        required=True,
        default='disabled',
        help=
        'Es el ambiente en al cual se le está actualizando el certificado. Para el ambiente '
        'de calidad (stag), para el ambiente de producción (prod). Requerido.')

    frm_pin = fields.Char(
        string="Pin",
        required=False,
        help='Es el pin correspondiente al certificado. Requerido')

    sucursal_MR = fields.Integer(string="Sucursal para secuencias de MRs",
                                 required=False,
                                 default="1")
    terminal_MR = fields.Integer(string="Terminal para secuencias de MRs",
                                 required=False,
                                 default="1")

    CCE_sequence_id = fields.Many2one(
        'ir.sequence',
        string='Secuencia Aceptación',
        help=
        'Secuencia de confirmacion de aceptación de comprobante electrónico. Dejar en blanco '
        'y el sistema automaticamente se lo creará.',
        readonly=False,
        copy=False,
    )
    CPCE_sequence_id = fields.Many2one(
        'ir.sequence',
        string='Secuencia Parcial',
        help=
        'Secuencia de confirmación de aceptación parcial de comprobante electrónico. Dejar '
        'en blanco y el sistema automáticamente se lo creará.',
        readonly=False,
        copy=False,
    )
    RCE_sequence_id = fields.Many2one(
        'ir.sequence',
        string='Secuencia Rechazo',
        help=
        'Secuencia de confirmación de rechazo de comprobante electrónico. Dejar '
        'en blanco y el sistema automáticamente se lo creará.',
        readonly=False,
        copy=False,
    )
    FEC_sequence_id = fields.Many2one(
        'ir.sequence',
        string='Secuencia de Facturas Electrónicas de Compra',
        readonly=False,
        copy=False,
    )

    @api.onchange('mobile')
    def _onchange_mobile(self):
        if self.mobile:
            mobile = phonenumbers.parse(self.mobile, self.country_id.code)
            valid = phonenumbers.is_valid_number(mobile)
            if not valid:
                alert = {
                    'title': 'Atención',
                    'message': 'Número de teléfono inválido'
                }
                return {'value': {'mobile': ''}, 'warning': alert}

    @api.onchange('phone')
    def _onchange_phone(self):
        if self.phone:
            phone = phonenumbers.parse(self.phone, self.country_id.code)
            valid = phonenumbers.is_valid_number(phone)
            if not valid:
                alert = {
                    'title': 'Atención',
                    'message': _('Número de teléfono inválido')
                }
                return {'value': {'phone': ''}, 'warning': alert}

    @api.model
    def create(self, vals):
        """ Try to automatically add the Comprobante Confirmation sequence to the company.
            It will attempt to create and assign before storing. The sequence that is
            created will be coded with the following syntax:
                account.invoice.supplier.<tipo>.<company_name>
            where tipo is: accept, partial or reject, and company_name is either the first word
            of the name or commercial name.
        """
        new_comp_id = super(CompanyElectronic, self).create(vals)
        #new_comp = self.browse(new_comp_id)
        new_comp_id.try_create_configuration_sequences()
        return new_comp_id  #new_comp.id

    def try_create_configuration_sequences(self):
        """ Try to automatically add the Comprobante Confirmation sequence to the company.
            It will first check if sequence already exists before attempt to create. The s
            equence is coded with the following syntax:
                account.invoice.supplier.<tipo>.<company_name>
            where tipo is: accept, partial or reject, and company_name is either the first word
            of the name or commercial name.
        """
        company_subname = self.commercial_name
        if not company_subname:
            company_subname = getattr(self, 'name')
        company_subname = company_subname.split(' ')[0].lower()
        ir_sequence = self.env['ir.sequence']
        to_write = {}
        for field, seq_code, seq_name in _TIPOS_CONFIRMACION:
            if not getattr(self, field, None):
                seq_code += company_subname
                seq = self.env.ref(
                    seq_code, raise_if_not_found=False) or ir_sequence.create(
                        {
                            'name': seq_name,
                            'code': seq_code,
                            'implementation': 'standard',
                            'padding': 10,
                            'use_date_range': False,
                            'company_id': getattr(self, 'id'),
                        })
                to_write[field] = seq.id

        if to_write:
            self.write(to_write)

    @api.multi
    def test_get_token(self):
        token_m_h = api_facturae.get_token_hacienda(self.env.user,
                                                    self.frm_ws_ambiente)
        if token_m_h:
            _logger.info('E-INV CR - I got the token')
        return

    @api.multi
    def action_get_economic_activities(self):
        if self.vat:
            json_response = api_facturae.get_economic_activities(self)
            _logger.error('E-INV CR  - Economic Activities: %s', json_response)
            if json_response["status"] == 200:
                activities = json_response["activities"]
                activities_codes = list()
                for activity in activities:
                    if activity["estado"] == "A":
                        activities_codes.append(activity["codigo"])
                economic_activities = self.env['economic.activity'].search([
                    ('code', 'in', activities_codes)
                ])

                self.economic_activities_ids = economic_activities
                self.name = json_response["name"]
            else:
                alert = {
                    'title': json_response["status"],
                    'message': json_response["text"]
                }
                return {'value': {'vat': ''}, 'warning': alert}
        else:
            alert = {
                'title': 'Atención',
                'message': _('Company VAT is invalid')
            }
            return {'value': {'vat': ''}, 'warning': alert}
示例#13
0
class IrMailServer(models.Model):
    """Represents an SMTP server, able to send outgoing emails, with SSL and TLS capabilities."""
    _name = "ir.mail_server"
    _description = 'Mail Server'
    _order = 'sequence'

    NO_VALID_RECIPIENT = ("At least one valid recipient address should be "
                          "specified for outgoing emails (To/Cc/Bcc)")

    name = fields.Char(string='Description', required=True, index=True)
    smtp_host = fields.Char(string='SMTP Server',
                            required=True,
                            help="Hostname or IP of SMTP server")
    smtp_port = fields.Integer(
        string='SMTP Port',
        required=True,
        default=25,
        help="SMTP Port. Usually 465 for SSL, and 25 or 587 for other cases.")
    smtp_user = fields.Char(string='Username',
                            help="Optional username for SMTP authentication",
                            groups='base.group_system')
    smtp_pass = fields.Char(string='Password',
                            help="Optional password for SMTP authentication",
                            groups='base.group_system')
    smtp_encryption = fields.Selection(
        [('none', 'None'), ('starttls', 'TLS (STARTTLS)'), ('ssl', 'SSL/TLS')],
        string='Connection Security',
        required=True,
        default='none',
        help="Choose the connection encryption scheme:\n"
        "- None: SMTP sessions are done in cleartext.\n"
        "- TLS (STARTTLS): TLS encryption is requested at start of SMTP session (Recommended)\n"
        "- SSL/TLS: SMTP sessions are encrypted with SSL/TLS through a dedicated port (default: 465)"
    )
    smtp_debug = fields.Boolean(
        string='Debugging',
        help="If enabled, the full output of SMTP sessions will "
        "be written to the server log at DEBUG level "
        "(this is very verbose and may include confidential info!)")
    sequence = fields.Integer(
        string='Priority',
        default=10,
        help=
        "When no specific mail server is requested for a mail, the highest priority one "
        "is used. Default priority is 10 (smaller number = higher priority)")
    active = fields.Boolean(default=True)

    def test_smtp_connection(self):
        for server in self:
            smtp = False
            try:
                smtp = self.connect(mail_server_id=server.id)
                # simulate sending an email from current user's address - without sending it!
                email_from, email_to = self.env.user.email, '*****@*****.**'
                if not email_from:
                    raise UserError(
                        _('Please configure an email on the current user to simulate '
                          'sending an email message via this outgoing server'))
                # Testing the MAIL FROM step should detect sender filter problems
                (code, repl) = smtp.mail(email_from)
                if code != 250:
                    raise UserError(
                        _('The server refused the sender address (%(email_from)s) '
                          'with error %(repl)s') % locals())
                # Testing the RCPT TO step should detect most relaying problems
                (code, repl) = smtp.rcpt(email_to)
                if code not in (250, 251):
                    raise UserError(
                        _('The server refused the test recipient (%(email_to)s) '
                          'with error %(repl)s') % locals())
                # Beginning the DATA step should detect some deferred rejections
                # Can't use self.data() as it would actually send the mail!
                smtp.putcmd("data")
                (code, repl) = smtp.getreply()
                if code != 354:
                    raise UserError(
                        _('The server refused the test connection '
                          'with error %(repl)s') % locals())
            except UserError as e:
                # let UserErrors (messages) bubble up
                raise e
            except Exception as e:
                raise UserError(
                    _("Connection Test Failed! Here is what we got instead:\n %s"
                      ) % ustr(e))
            finally:
                try:
                    if smtp:
                        smtp.close()
                except Exception:
                    # ignored, just a consequence of the previous exception
                    pass

        title = _("Connection Test Succeeded!")
        message = _("Everything seems properly set up!")
        return {
            'type': 'ir.actions.client',
            'tag': 'display_notification',
            'params': {
                'title': title,
                'message': message,
                'sticky': False,
            }
        }

    def connect(self,
                host=None,
                port=None,
                user=None,
                password=None,
                encryption=None,
                smtp_debug=False,
                mail_server_id=None):
        """Returns a new SMTP connection to the given SMTP server.
           When running in test mode, this method does nothing and returns `None`.

           :param host: host or IP of SMTP server to connect to, if mail_server_id not passed
           :param int port: SMTP port to connect to
           :param user: optional username to authenticate with
           :param password: optional password to authenticate with
           :param string encryption: optional, ``'ssl'`` | ``'starttls'``
           :param bool smtp_debug: toggle debugging of SMTP sessions (all i/o
                              will be output in logs)
           :param mail_server_id: ID of specific mail server to use (overrides other parameters)
        """
        # Do not actually connect while running in test mode
        if getattr(threading.currentThread(), 'testing', False):
            return None

        mail_server = smtp_encryption = None
        if mail_server_id:
            mail_server = self.sudo().browse(mail_server_id)
        elif not host:
            mail_server = self.sudo().search([], order='sequence', limit=1)

        if mail_server:
            smtp_server = mail_server.smtp_host
            smtp_port = mail_server.smtp_port
            smtp_user = mail_server.smtp_user
            smtp_password = mail_server.smtp_pass
            smtp_encryption = mail_server.smtp_encryption
            smtp_debug = smtp_debug or mail_server.smtp_debug
        else:
            # we were passed individual smtp parameters or nothing and there is no default server
            smtp_server = host or tools.config.get('smtp_server')
            smtp_port = tools.config.get('smtp_port',
                                         25) if port is None else port
            smtp_user = user or tools.config.get('smtp_user')
            smtp_password = password or tools.config.get('smtp_password')
            smtp_encryption = encryption
            if smtp_encryption is None and tools.config.get('smtp_ssl'):
                smtp_encryption = 'starttls'  # smtp_ssl => STARTTLS as of v7

        if not smtp_server:
            raise UserError((_("Missing SMTP Server") + "\n" +
                             _("Please define at least one SMTP server, "
                               "or provide the SMTP parameters explicitly.")))

        if smtp_encryption == 'ssl':
            if 'SMTP_SSL' not in smtplib.__all__:
                raise UserError(
                    _("Your Odoo Server does not support SMTP-over-SSL. "
                      "You could use STARTTLS instead. "
                      "If SSL is needed, an upgrade to Python 2.6 on the server-side "
                      "should do the trick."))
            connection = smtplib.SMTP_SSL(smtp_server,
                                          smtp_port,
                                          timeout=SMTP_TIMEOUT)
        else:
            connection = smtplib.SMTP(smtp_server,
                                      smtp_port,
                                      timeout=SMTP_TIMEOUT)
        connection.set_debuglevel(smtp_debug)
        if smtp_encryption == 'starttls':
            # starttls() will perform ehlo() if needed first
            # and will discard the previous list of services
            # after successfully performing STARTTLS command,
            # (as per RFC 3207) so for example any AUTH
            # capability that appears only on encrypted channels
            # will be correctly detected for next step
            connection.starttls()

        if smtp_user:
            # Attempt authentication - will raise if AUTH service not supported
            # The user/password must be converted to bytestrings in order to be usable for
            # certain hashing schemes, like HMAC.
            # See also bug #597143 and python issue #5285
            smtp_user = pycompat.to_text(ustr(smtp_user))
            smtp_password = pycompat.to_text(ustr(smtp_password))
            connection.login(smtp_user, smtp_password)

        # Some methods of SMTP don't check whether EHLO/HELO was sent.
        # Anyway, as it may have been sent by login(), all subsequent usages should consider this command as sent.
        connection.ehlo_or_helo_if_needed()

        return connection

    def build_email(self,
                    email_from,
                    email_to,
                    subject,
                    body,
                    email_cc=None,
                    email_bcc=None,
                    reply_to=False,
                    attachments=None,
                    message_id=None,
                    references=None,
                    object_id=False,
                    subtype='plain',
                    headers=None,
                    body_alternative=None,
                    subtype_alternative='plain'):
        """Constructs an RFC2822 email.message.Message object based on the keyword arguments passed, and returns it.

           :param string email_from: sender email address
           :param list email_to: list of recipient addresses (to be joined with commas)
           :param string subject: email subject (no pre-encoding/quoting necessary)
           :param string body: email body, of the type ``subtype`` (by default, plaintext).
                               If html subtype is used, the message will be automatically converted
                               to plaintext and wrapped in multipart/alternative, unless an explicit
                               ``body_alternative`` version is passed.
           :param string body_alternative: optional alternative body, of the type specified in ``subtype_alternative``
           :param string reply_to: optional value of Reply-To header
           :param string object_id: optional tracking identifier, to be included in the message-id for
                                    recognizing replies. Suggested format for object-id is "res_id-model",
                                    e.g. "12345-crm.lead".
           :param string subtype: optional mime subtype for the text body (usually 'plain' or 'html'),
                                  must match the format of the ``body`` parameter. Default is 'plain',
                                  making the content part of the mail "text/plain".
           :param string subtype_alternative: optional mime subtype of ``body_alternative`` (usually 'plain'
                                              or 'html'). Default is 'plain'.
           :param list attachments: list of (filename, filecontents) pairs, where filecontents is a string
                                    containing the bytes of the attachment
           :param list email_cc: optional list of string values for CC header (to be joined with commas)
           :param list email_bcc: optional list of string values for BCC header (to be joined with commas)
           :param dict headers: optional map of headers to set on the outgoing mail (may override the
                                other headers, including Subject, Reply-To, Message-Id, etc.)
           :rtype: email.message.EmailMessage
           :return: the new RFC2822 email message
        """
        email_from = email_from or tools.config.get('email_from')
        assert email_from, "You must either provide a sender address explicitly or configure "\
                           "a global sender address in the server configuration or with the "\
                           "--email-from startup parameter."

        headers = headers or {}  # need valid dict later
        email_cc = email_cc or []
        email_bcc = email_bcc or []
        body = body or u''

        msg = EmailMessage(policy=email.policy.SMTP)
        if not message_id:
            if object_id:
                message_id = tools.generate_tracking_message_id(object_id)
            else:
                message_id = make_msgid()
        msg['Message-Id'] = message_id
        if references:
            msg['references'] = references
        msg['Subject'] = subject
        msg['From'] = email_from
        del msg['Reply-To']
        msg['Reply-To'] = reply_to or email_from
        msg['To'] = email_to
        if email_cc:
            msg['Cc'] = email_cc
        if email_bcc:
            msg['Bcc'] = email_bcc
        msg['Date'] = datetime.datetime.utcnow()
        for key, value in headers.items():
            msg[pycompat.to_text(ustr(key))] = value

        email_body = ustr(body)
        if subtype == 'html' and not body_alternative:
            msg.add_alternative(email_body, subtype=subtype, charset='utf-8')
            msg.add_alternative(html2text.html2text(email_body),
                                subtype='plain',
                                charset='utf-8')
        elif body_alternative:
            msg.add_alternative(email_body, subtype=subtype, charset='utf-8')
            msg.add_alternative(ustr(body_alternative),
                                subtype=subtype_alternative,
                                charset='utf-8')
        else:
            msg.set_content(email_body, subtype=subtype, charset='utf-8')

        if attachments:
            for (fname, fcontent, mime) in attachments:
                maintype, subtype = mime.split(
                    '/') if mime and '/' in mime else ('application',
                                                       'octet-stream')
                msg.add_attachment(fcontent, maintype, subtype, filename=fname)
        return msg

    @api.model
    def _get_default_bounce_address(self):
        '''Compute the default bounce address.

        The default bounce address is used to set the envelop address if no
        envelop address is provided in the message.  It is formed by properly
        joining the parameters "mail.bounce.alias" and
        "mail.catchall.domain".

        If "mail.bounce.alias" is not set it defaults to "postmaster-odoo".

        If "mail.catchall.domain" is not set, return None.

        '''
        get_param = self.env['ir.config_parameter'].sudo().get_param
        postmaster = get_param('mail.bounce.alias', default='postmaster-odoo')
        domain = get_param('mail.catchall.domain')
        if postmaster and domain:
            return '%s@%s' % (postmaster, domain)

    @api.model
    def send_email(self,
                   message,
                   mail_server_id=None,
                   smtp_server=None,
                   smtp_port=None,
                   smtp_user=None,
                   smtp_password=None,
                   smtp_encryption=None,
                   smtp_debug=False,
                   smtp_session=None):
        """Sends an email directly (no queuing).

        No retries are done, the caller should handle MailDeliveryException in order to ensure that
        the mail is never lost.

        If the mail_server_id is provided, sends using this mail server, ignoring other smtp_* arguments.
        If mail_server_id is None and smtp_server is None, use the default mail server (highest priority).
        If mail_server_id is None and smtp_server is not None, use the provided smtp_* arguments.
        If both mail_server_id and smtp_server are None, look for an 'smtp_server' value in server config,
        and fails if not found.

        :param message: the email.message.Message to send. The envelope sender will be extracted from the
                        ``Return-Path`` (if present), or will be set to the default bounce address.
                        The envelope recipients will be extracted from the combined list of ``To``,
                        ``CC`` and ``BCC`` headers.
        :param smtp_session: optional pre-established SMTP session. When provided,
                             overrides `mail_server_id` and all the `smtp_*` parameters.
                             Passing the matching `mail_server_id` may yield better debugging/log
                             messages. The caller is in charge of disconnecting the session.
        :param mail_server_id: optional id of ir.mail_server to use for sending. overrides other smtp_* arguments.
        :param smtp_server: optional hostname of SMTP server to use
        :param smtp_encryption: optional TLS mode, one of 'none', 'starttls' or 'ssl' (see ir.mail_server fields for explanation)
        :param smtp_port: optional SMTP port, if mail_server_id is not passed
        :param smtp_user: optional SMTP user, if mail_server_id is not passed
        :param smtp_password: optional SMTP password to use, if mail_server_id is not passed
        :param smtp_debug: optional SMTP debug flag, if mail_server_id is not passed
        :return: the Message-ID of the message that was just sent, if successfully sent, otherwise raises
                 MailDeliveryException and logs root cause.
        """
        # Use the default bounce address **only if** no Return-Path was
        # provided by caller.  Caller may be using Variable Envelope Return
        # Path (VERP) to detect no-longer valid email addresses.
        smtp_from = message['Return-Path'] or self._get_default_bounce_address(
        ) or message['From']
        assert smtp_from, "The Return-Path or From header is required for any outbound email"

        # The email's "Envelope From" (Return-Path), and all recipient addresses must only contain ASCII characters.
        from_rfc2822 = extract_rfc2822_addresses(smtp_from)
        assert from_rfc2822, (
            "Malformed 'Return-Path' or 'From' address: %r - "
            "It should contain one valid plain ASCII email") % smtp_from
        # use last extracted email, to support rarities like 'Support@MyComp <*****@*****.**>'
        smtp_from = from_rfc2822[-1]
        email_to = message['To']
        email_cc = message['Cc']
        email_bcc = message['Bcc']
        del message['Bcc']

        smtp_to_list = [
            address for base in [email_to, email_cc, email_bcc]
            for address in extract_rfc2822_addresses(base) if address
        ]
        assert smtp_to_list, self.NO_VALID_RECIPIENT

        x_forge_to = message['X-Forge-To']
        if x_forge_to:
            # `To:` header forged, e.g. for posting on mail.channels, to avoid confusion
            del message['X-Forge-To']
            del message['To']  # avoid multiple To: headers!
            message['To'] = x_forge_to

        # Do not actually send emails in testing mode!
        if getattr(threading.currentThread(), 'testing',
                   False) or self.env.registry.in_test_mode():
            _test_logger.info("skip sending email in test mode")
            return message['Message-Id']

        try:
            message_id = message['Message-Id']
            smtp = smtp_session
            smtp = smtp or self.connect(smtp_server,
                                        smtp_port,
                                        smtp_user,
                                        smtp_password,
                                        smtp_encryption,
                                        smtp_debug,
                                        mail_server_id=mail_server_id)
            smtp.sendmail(smtp_from, smtp_to_list, message.as_string())
            # do not quit() a pre-established smtp_session
            if not smtp_session:
                smtp.quit()
        except smtplib.SMTPServerDisconnected:
            raise
        except Exception as e:
            params = (ustr(smtp_server), e.__class__.__name__, ustr(e))
            msg = _(
                "Mail delivery failed via SMTP server '%s'.\n%s: %s") % params
            _logger.info(msg)
            raise MailDeliveryException(_("Mail Delivery Failed"), msg)
        return message_id

    @api.onchange('smtp_encryption')
    def _onchange_encryption(self):
        result = {}
        if self.smtp_encryption == 'ssl':
            self.smtp_port = 465
            if not 'SMTP_SSL' in smtplib.__all__:
                result['warning'] = {
                    'title':
                    _('Warning'),
                    'message':
                    _('Your server does not seem to support SSL, you may want to try STARTTLS instead'
                      ),
                }
        else:
            self.smtp_port = 25
        return result
class TmsExpenseLine(models.Model):
    _name = 'tms.expense.line'
    _description = 'Expense Line'

    loan_id = fields.Many2one('tms.expense.loan', string='Loan')
    travel_id = fields.Many2one('tms.travel', string='Travel')
    expense_id = fields.Many2one(
        'tms.expense',
        string='Expense',
    )
    product_qty = fields.Float(string='Qty', default=1.0)
    unit_price = fields.Float()
    price_subtotal = fields.Float(
        compute='_compute_price_subtotal',
        string='Subtotal',
    )
    product_uom_id = fields.Many2one('product.uom', string='Unit of Measure')
    line_type = fields.Selection([('real_expense', 'Real Expense'),
                                  ('made_up_expense', 'Made-up Expense'),
                                  ('salary', 'Salary'), ('fuel', 'Fuel'),
                                  ('fuel_cash', 'Fuel in Cash'),
                                  ('refund', 'Refund'),
                                  ('salary_retention', 'Salary Retention'),
                                  ('salary_discount', 'Salary Discount'),
                                  ('other_income', 'Other Income'),
                                  ('tollstations', 'Toll Stations'),
                                  ('loan', 'Loan')],
                                 compute='_compute_line_type',
                                 store=True,
                                 readonly=True)
    name = fields.Char('Description', required=True)
    sequence = fields.Integer(
        help="Gives the sequence order when displaying a list of "
        "sales order lines.",
        default=10)
    price_total = fields.Float(
        string='Total',
        compute='_compute_price_total',
    )
    tax_amount = fields.Float(compute='_compute_tax_amount', )
    special_tax_amount = fields.Float(string='Special Tax')
    tax_ids = fields.Many2many('account.tax',
                               string='Taxes',
                               domain=[('type_tax_use', '=', 'purchase')])
    notes = fields.Text()
    employee_id = fields.Many2one('hr.employee', string='Driver')
    date = fields.Date()
    state = fields.Char(readonly=True)
    control = fields.Boolean()
    automatic = fields.Boolean(
        help="Check this if you want to create Advances and/or "
        "Fuel Vouchers for this line automatically")
    is_invoice = fields.Boolean(string='Is Invoice?')
    partner_id = fields.Many2one(
        'res.partner',
        string='Supplier',
    )
    invoice_date = fields.Date('Date')
    invoice_number = fields.Char()
    invoice_id = fields.Many2one('account.invoice', string='Supplier Invoice')
    product_id = fields.Many2one(
        'product.product',
        string='Product',
        required=True,
    )
    route_id = fields.Many2one('tms.route',
                               related='travel_id.route_id',
                               string='Route',
                               readonly=True)
    expense_fuel_log = fields.Boolean(readonly=True)

    @api.onchange('product_id')
    def _onchange_product_id(self):
        if self.line_type not in [
                'salary', 'salary_retention', 'salary_discount'
        ]:
            self.tax_ids = self.product_id.supplier_taxes_id
        self.line_type = self.product_id.tms_product_category
        self.product_uom_id = self.product_id.uom_id.id
        self.name = self.product_id.name

    @api.depends('product_id')
    def _compute_line_type(self):
        for rec in self:
            rec.line_type = rec.product_id.tms_product_category

    @api.depends('tax_ids', 'product_qty', 'unit_price')
    def _compute_tax_amount(self):
        for rec in self:
            taxes = rec.tax_ids.compute_all(
                rec.unit_price, rec.expense_id.currency_id, rec.product_qty,
                rec.expense_id.employee_id.address_home_id)
            if taxes['taxes']:
                for tax in taxes['taxes']:
                    rec.tax_amount += tax['amount']
            else:
                rec.tax_amount = 0.0

    @api.depends('product_qty', 'unit_price', 'line_type')
    def _compute_price_subtotal(self):
        for rec in self:
            if rec.line_type in [
                    'salary_retention', 'salary_discount', 'loan'
            ]:
                rec.price_subtotal = rec.product_qty * rec.unit_price * -1
            elif rec.line_type == 'fuel':
                rec.price_subtotal = rec.unit_price
            else:
                rec.price_subtotal = rec.product_qty * rec.unit_price

    @api.depends('price_subtotal', 'tax_ids')
    def _compute_price_total(self):
        for rec in self:
            if rec.line_type == 'fuel':
                rec.price_total = rec.unit_price
            elif rec.line_type in [
                    'salary_retention', 'salary_discount', 'loan'
            ]:
                rec.price_total = rec.price_subtotal - rec.tax_amount
            else:
                rec.price_total = rec.price_subtotal + rec.tax_amount

    @api.model
    def create(self, values):
        expense_line = super(TmsExpenseLine, self).create(values)
        if expense_line.line_type in ('salary_discount', 'salary_retention',
                                      'loan'):
            if expense_line.price_total > 0:
                raise ValidationError(
                    _('This line type needs a '
                      'negative value to continue!'))
        return expense_line
示例#15
0
class ExamResult(models.Model):
    _name = 'exam.result'
    _inherit = ["mail.thread", "ir.needaction_mixin"]
    _rec_name = 'roll_no_id'
    _description = 'exam result Information'

    @api.multi
    @api.depends('result_ids', 'result_ids.obtain_marks',
                 'result_ids.marks_reeval')
    def _compute_total(self):
        '''Method to compute total'''
        for rec in self:
            total = 0.0
            if rec.result_ids:
                for line in rec.result_ids:
                    obtain_marks = line.obtain_marks
                    if line.state == "re-evaluation":
                        obtain_marks = line.marks_reeval
                    total += obtain_marks
            rec.total = total

    @api.multi
    @api.depends('total')
    def _compute_per(self):
        '''Method to compute percentage'''
        total = 0.0
        obtained_total = 0.0
        obtain_marks = 0.0
        per = 0.0
        for result in self:
            for sub_line in result.result_ids:
                if sub_line.state == "re-evaluation":
                    obtain_marks = sub_line.marks_reeval
                else:
                    obtain_marks = sub_line.obtain_marks
                total += sub_line.maximum_marks or 0
                obtained_total += obtain_marks
            if total > 1.0:
                per = (obtained_total / total) * 100
                if result.grade_system:
                    for grade_id in result.grade_system.grade_ids:
                        if per >= grade_id.from_mark and\
                                per <= grade_id.to_mark:
                            result.grade = grade_id.grade or ''
            result.percentage = per
        return True

    @api.multi
    @api.depends('percentage')
    def _compute_result(self):
        '''Method to compute result'''
        for rec in self:
            flag = False
            if rec.result_ids:
                for grade in rec.result_ids:
                    if not grade.grade_line_id.fail:
                        rec.result = 'Pass'
                    else:
                        flag = True
            if flag:
                rec.result = 'Fail'

    @api.model
    def create(self, vals):
        if vals.get('student_id'):
            student = self.env['student.student'].browse(
                vals.get('student_id'))
            vals.update({
                'roll_no_id': student.roll_no,
                'standard_id': student.standard_id.id
            })
        return super(ExamResult, self).create(vals)

    @api.multi
    def write(self, vals):
        if vals.get('student_id'):
            student = self.env['student.student'].browse(
                vals.get('student_id'))
            vals.update({
                'roll_no_id': student.roll_no,
                'standard_id': student.standard_id.id
            })
        return super(ExamResult, self).write(vals)

    @api.multi
    def unlink(self):
        for rec in self:
            if rec.state != 'draft':
                raise ValidationError(
                    _('''You can delete record in unconfirm
                state only!.'''))
        return super(ExamResult, self).unlink()

    @api.onchange('student_id')
    def onchange_student(self):
        '''Method to get standard and roll no of student selected'''
        if self.student_id:
            self.standard_id = self.student_id.standard_id.id
            self.roll_no_id = self.student_id.roll_no

    s_exam_ids = fields.Many2one("exam.exam",
                                 "Examination",
                                 required=True,
                                 help="Select Exam")
    student_id = fields.Many2one("student.student",
                                 "Student Name",
                                 required=True,
                                 help="Select Student")
    roll_no_id = fields.Integer(string="Roll No", readonly=True)
    pid = fields.Char(related='student_id.pid',
                      string="Student ID",
                      readonly=True)
    standard_id = fields.Many2one("school.standard",
                                  "Standard",
                                  help="Select Standard")
    result_ids = fields.One2many("exam.subject", "exam_id", "Exam Subjects")
    total = fields.Float(compute='_compute_total',
                         string='Obtain Total',
                         store=True,
                         help="Total of marks")
    percentage = fields.Float("Percentage",
                              compute="_compute_per",
                              store=True,
                              help="Percentage Obtained")
    result = fields.Char(compute='_compute_result',
                         string='Result',
                         store=True,
                         help="Result Obtained")
    grade = fields.Char("Grade",
                        compute="_compute_per",
                        store=True,
                        help="Grade Obtained")
    state = fields.Selection(
        [('draft', 'Draft'), ('confirm', 'Confirm'),
         ('re-evaluation', 'Re-Evaluation'),
         ('re-evaluation_confirm', 'Re-Evaluation Confirm'), ('done', 'Done')],
        'State',
        readonly=True,
        track_visibility='onchange',
        default='draft')
    color = fields.Integer('Color')
    grade_system = fields.Many2one('grade.master',
                                   "Grade System",
                                   help="Grade System selected")
    message_ids = fields.One2many(
        'mail.message',
        'res_id',
        'Messages',
        domain=lambda self: [('model', '=', self._name)],
        auto_join=True)
    message_follower_ids = fields.One2many(
        'mail.followers',
        'res_id',
        'Followers',
        domain=lambda self: [('res_model', '=', self._name)])

    @api.multi
    def result_confirm(self):
        '''Method to confirm result'''
        for rec in self:
            for line in rec.result_ids:
                if line.maximum_marks == 0:
                    # Check subject marks not greater than maximum marks
                    raise ValidationError(
                        _('Kindly add maximum\
                            marks of subject "%s".') % (line.subject_id.name))
                elif line.minimum_marks == 0:
                    raise ValidationError(
                        _('Kindly add minimum\
                        marks of subject "%s".') % (line.subject_id.name))
                elif ((line.maximum_marks == 0 or line.minimum_marks == 0)
                      and line.obtain_marks):
                    raise ValidationError(
                        _('Kindly add marks\
                        details of subject "%s"!') % (line.subject_id.name))
            vals = {
                'grade': rec.grade,
                'percentage': rec.percentage,
                'state': 'confirm'
            }
            rec.write(vals)
        return True

    @api.multi
    def re_evaluation_confirm(self):
        '''Method to change state to re_evaluation_confirm'''
        self.write({'state': 're-evaluation_confirm'})

    @api.multi
    def result_re_evaluation(self):
        '''Method to set state to re-evaluation'''
        for rec in self:
            for line in rec.result_ids:
                line.marks_reeval = line.obtain_marks
            rec.state = 're-evaluation'
        return True

    @api.multi
    def set_done(self):
        '''Method to obtain history of student'''
        history_obj = self.env['student.history']
        for rec in self:
            vals = {
                'student_id': rec.student_id.id,
                'academice_year_id': rec.student_id.year.id,
                'standard_id': rec.standard_id.id,
                'percentage': rec.percentage,
                'result': rec.result
            }
            history = history_obj.search([
                ('student_id', '=', rec.student_id.id),
                ('academice_year_id', '=', rec.student_id.year.id),
                ('standard_id', '=', rec.standard_id.id)
            ])
            if history:
                history_obj.write(vals)
            elif not history:
                history_obj.create(vals)
            rec.write({'state': 'done'})
        return True
示例#16
0
class WebsiteVisitor(models.Model):
    _name = 'website.visitor'
    _description = 'Website Visitor'
    _order = 'last_connection_datetime DESC'

    name = fields.Char('Name')
    access_token = fields.Char(required=True, default=lambda x: uuid.uuid4().hex, index=True, copy=False, groups='base.group_website_publisher')
    active = fields.Boolean('Active', default=True)
    website_id = fields.Many2one('website', "Website", readonly=True)
    partner_id = fields.Many2one('res.partner', string="Linked Partner", help="Partner of the last logged in user.")
    partner_image = fields.Binary(related='partner_id.image_1920')

    # localisation and info
    country_id = fields.Many2one('res.country', 'Country', readonly=True)
    country_flag = fields.Binary(related="country_id.image", string="Country Flag")
    lang_id = fields.Many2one('res.lang', string='Language', help="Language from the website when visitor has been created")
    timezone = fields.Selection(_tz_get, string='Timezone')
    email = fields.Char(string='Email', compute='_compute_email_phone')
    mobile = fields.Char(string='Mobile Phone', compute='_compute_email_phone')

    # Visit fields
    visit_count = fields.Integer('Number of visits', default=1, readonly=True, help="A new visit is considered if last connection was more than 8 hours ago.")
    website_track_ids = fields.One2many('website.track', 'visitor_id', string='Visited Pages History', readonly=True)
    visitor_page_count = fields.Integer('Page Views', compute="_compute_page_statistics", help="Total number of visits on tracked pages")
    page_ids = fields.Many2many('website.page', string="Visited Pages", compute="_compute_page_statistics")
    page_count = fields.Integer('# Visited Pages', compute="_compute_page_statistics", help="Total number of tracked page visited")
    last_visited_page_id = fields.Many2one('website.page', string="Last Visited Page", compute="_compute_last_visited_page_id")

    # Time fields
    create_date = fields.Datetime('First connection date', readonly=True)
    last_connection_datetime = fields.Datetime('Last Connection', default=fields.Datetime.now, help="Last page view date", readonly=True)
    time_since_last_action = fields.Char('Last action', compute="_compute_time_statistics", help='Time since last page view. E.g.: 2 minutes ago')
    is_connected = fields.Boolean('Is connected ?', compute='_compute_time_statistics', help='A visitor is considered as connected if his last page view was within the last 5 minutes.')

    _sql_constraints = [
        ('access_token_unique', 'unique(access_token)', 'Access token should be unique.'),
        ('partner_uniq', 'unique(partner_id)', 'A partner is linked to only one visitor.'),
    ]

    @api.depends('name')
    def name_get(self):
        return [(
            record.id,
            (record.name or _('Website Visitor #%s') % record.id)
        ) for record in self]

    @api.depends('partner_id.email_normalized', 'partner_id.mobile')
    def _compute_email_phone(self):
        results = self.env['res.partner'].search_read(
            [('id', 'in', self.partner_id.ids)],
            ['id', 'email_normalized', 'mobile'],
        )
        mapped_data = {
            result['id']: {
                'email_normalized': result['email_normalized'],
                'mobile': result['mobile']
            } for result in results
        }

        for visitor in self:
            visitor.email = mapped_data.get(visitor.partner_id.id, {}).get('email_normalized')
            visitor.mobile = mapped_data.get(visitor.partner_id.id, {}).get('mobile')

    @api.depends('website_track_ids')
    def _compute_page_statistics(self):
        results = self.env['website.track'].read_group(
            [('visitor_id', 'in', self.ids), ('url', '!=', False)], ['visitor_id', 'page_id', 'url'], ['visitor_id', 'page_id', 'url'], lazy=False)
        mapped_data = {}
        for result in results:
            visitor_info = mapped_data.get(result['visitor_id'][0], {'page_count': 0, 'visitor_page_count': 0, 'page_ids': set()})
            visitor_info['visitor_page_count'] += result['__count']
            visitor_info['page_count'] += 1
            if result['page_id']:
                visitor_info['page_ids'].add(result['page_id'][0])
            mapped_data[result['visitor_id'][0]] = visitor_info

        for visitor in self:
            visitor_info = mapped_data.get(visitor.id, {'page_count': 0, 'visitor_page_count': 0, 'page_ids': set()})
            visitor.page_ids = [(6, 0, visitor_info['page_ids'])]
            visitor.visitor_page_count = visitor_info['visitor_page_count']
            visitor.page_count = visitor_info['page_count']

    @api.depends('website_track_ids.page_id')
    def _compute_last_visited_page_id(self):
        results = self.env['website.track'].read_group([('visitor_id', 'in', self.ids)],
                                                       ['visitor_id', 'page_id', 'visit_datetime:max'],
                                                       ['visitor_id', 'page_id'], lazy=False)
        mapped_data = {result['visitor_id'][0]: result['page_id'][0] for result in results if result['page_id']}
        for visitor in self:
            visitor.last_visited_page_id = mapped_data.get(visitor.id, False)

    @api.depends('last_connection_datetime')
    def _compute_time_statistics(self):
        results = self.env['website.visitor'].with_context(active_test=False).search_read([('id', 'in', self.ids)], ['id', 'last_connection_datetime'])
        mapped_data = {result['id']: result['last_connection_datetime'] for result in results}

        for visitor in self:
            last_connection_date = mapped_data[visitor.id]
            visitor.time_since_last_action = _format_time_ago(self.env, (datetime.now() - last_connection_date))
            visitor.is_connected = (datetime.now() - last_connection_date) < timedelta(minutes=5)

    def _prepare_visitor_send_mail_values(self):
        if self.partner_id.email:
            return {
                'res_model': 'res.partner',
                'res_id': self.partner_id.id,
                'partner_ids': [self.partner_id.id],
            }
        return {}

    def action_send_mail(self):
        self.ensure_one()
        visitor_mail_values = self._prepare_visitor_send_mail_values()
        if not visitor_mail_values:
            raise UserError(_("There is no email linked this visitor."))
        compose_form = self.env.ref('mail.email_compose_message_wizard_form', False)
        ctx = dict(
            default_model=visitor_mail_values.get('res_model'),
            default_res_id=visitor_mail_values.get('res_id'),
            default_use_template=False,
            default_partner_ids=[(6, 0, visitor_mail_values.get('partner_ids'))],
            default_composition_mode='comment',
            default_reply_to=self.env.user.partner_id.email,
        )
        return {
            'name': _('Compose Email'),
            'type': 'ir.actions.act_window',
            'view_mode': 'form',
            'res_model': 'mail.compose.message',
            'views': [(compose_form.id, 'form')],
            'view_id': compose_form.id,
            'target': 'new',
            'context': ctx,
        }

    def _get_visitor_from_request(self, force_create=False):
        """ Return the visitor as sudo from the request if there is a visitor_uuid cookie.
            It is possible that the partner has changed or has disconnected.
            In that case the cookie is still referencing the old visitor and need to be replaced
            with the one of the visitor returned !!!. """

        # This function can be called in json with mobile app.
        # In case of mobile app, no uid is set on the jsonRequest env.
        # In case of multi db, _env is None on request, and request.env unbound.
        if not request:
            return None
        Visitor = self.env['website.visitor'].sudo()
        visitor = Visitor
        access_token = request.httprequest.cookies.get('visitor_uuid')
        if access_token:
            visitor = Visitor.with_context(active_test=False).search([('access_token', '=', access_token)])

        if not self.env.user._is_public():
            partner_id = self.env.user.partner_id
            if not visitor or visitor.partner_id and visitor.partner_id != partner_id:
                # Partner and no cookie or wrong cookie
                visitor = Visitor.with_context(active_test=False).search([('partner_id', '=', partner_id.id)])
        elif visitor and visitor.partner_id:
            # Cookie associated to a Partner
            visitor = Visitor

        if force_create and not visitor:
            visitor = self._create_visitor()

        return visitor

    def _handle_webpage_dispatch(self, response, website_page):
        # get visitor. Done here to avoid having to do it multiple times in case of override.
        visitor_sudo = self._get_visitor_from_request(force_create=True)
        if request.httprequest.cookies.get('visitor_uuid', '') != visitor_sudo.access_token:
            expiration_date = datetime.now() + timedelta(days=365)
            response.set_cookie('visitor_uuid', visitor_sudo.access_token, expires=expiration_date)
        self._handle_website_page_visit(response, website_page, visitor_sudo)

    def _handle_website_page_visit(self, response, website_page, visitor_sudo):
        """ Called on dispatch. This will create a website.visitor if the http request object
        is a tracked website page or a tracked view. Only on tracked elements to avoid having
        too much operations done on every page or other http requests.
        Note: The side effect is that the last_connection_datetime is updated ONLY on tracked elements."""
        url = request.httprequest.url
        website_track_values = {
            'url': url,
            'visit_datetime': datetime.now(),
        }
        if website_page:
            website_track_values['page_id'] = website_page.id
            domain = [('page_id', '=', website_page.id)]
        else:
            domain = [('url', '=', url)]
        visitor_sudo._add_tracking(domain, website_track_values)
        if visitor_sudo.lang_id.id != request.lang.id:
            visitor_sudo.write({'lang_id': request.lang.id})

    def _add_tracking(self, domain, website_track_values):
        """ Add the track and update the visitor"""
        domain = expression.AND([domain, [('visitor_id', '=', self.id)]])
        last_view = self.env['website.track'].sudo().search(domain, limit=1)
        if not last_view or last_view.visit_datetime < datetime.now() - timedelta(minutes=30):
            website_track_values['visitor_id'] = self.id
            self.env['website.track'].create(website_track_values)
        self._update_visitor_last_visit()

    def _create_visitor(self, website_track_values=None):
        """ Create a visitor and add a track to it if website_track_values is set."""
        country_code = request.session.get('geoip', {}).get('country_code', False)
        country_id = request.env['res.country'].sudo().search([('code', '=', country_code)], limit=1).id if country_code else False
        vals = {
            'lang_id': request.lang.id,
            'country_id': country_id,
            'website_id': request.website.id,
        }
        if not self.env.user._is_public():
            vals['partner_id'] = self.env.user.partner_id.id
            vals['name'] = self.env.user.partner_id.name
        if website_track_values:
            vals['website_track_ids'] = [(0, 0, website_track_values)]
        return self.sudo().create(vals)

    def _cron_archive_visitors(self):
        one_week_ago = datetime.now() - timedelta(days=7)
        visitors_to_archive = self.env['website.visitor'].sudo().search([('last_connection_datetime', '<', one_week_ago)])
        visitors_to_archive.write({'active': False})

    def _update_visitor_last_visit(self):
        """ We need to do this part here to avoid concurrent updates error. """
        try:
            with self.env.cr.savepoint():
                query_lock = "SELECT * FROM website_visitor where id = %s FOR UPDATE NOWAIT"
                self.env.cr.execute(query_lock, (self.id,), log_exceptions=False)

                date_now = datetime.now()
                query = "UPDATE website_visitor SET "
                if self.last_connection_datetime < (date_now - timedelta(hours=8)):
                    query += "visit_count = visit_count + 1,"
                query += """
                    active = True,
                    last_connection_datetime = %s
                    WHERE id = %s
                """
                self.env.cr.execute(query, (date_now, self.id), log_exceptions=False)
        except Exception:
            pass
示例#17
0
class AdditionalExamResult(models.Model):
    _name = 'additional.exam.result'
    _description = 'subject result Information'
    _rec_name = 'roll_no_id'

    @api.multi
    @api.depends('a_exam_id', 'obtain_marks')
    def _compute_student_result(self):
        '''Method to compute result of student'''
        for rec in self:
            if rec.a_exam_id and rec.a_exam_id:
                if rec.a_exam_id.minimum_marks < \
                        rec.obtain_marks:
                    rec.result = 'Pass'
                else:
                    rec.result = 'Fail'

    @api.model
    def create(self, vals):
        '''Override create method to get roll no and standard'''
        student = self.env['student.student'].browse(vals.get('student_id'))
        vals.update({
            'roll_no_id': student.roll_no,
            'standard_id': student.standard_id.id
        })
        return super(AdditionalExamResult, self).create(vals)

    @api.multi
    def write(self, vals):
        '''Override write method to get roll no and standard'''
        student = self.env['student.student'].browse(vals.get('student_id'))
        vals.update({
            'roll_no_id': student.roll_no,
            'standard_id': student.standard_id.id
        })
        return super(AdditionalExamResult, self).write(vals)

    @api.onchange('student_id')
    def onchange_student(self):
        ''' Method to get student roll no and standard by selecting student'''
        self.standard_id = self.student_id.standard_id.id
        self.roll_no_id = self.student_id.roll_no

    @api.constrains('obtain_marks')
    def _validate_obtain_marks(self):
        if self.obtain_marks > self.a_exam_id.subject_id.maximum_marks:
            raise ValidationError(
                _('''The obtained marks should not extend
                                    maximum marks!.'''))
        return True

    a_exam_id = fields.Many2one('additional.exam',
                                'Additional Examination',
                                required=True,
                                help="Select Additional Exam")
    student_id = fields.Many2one('student.student',
                                 'Student Name',
                                 required=True,
                                 help="Select Student")
    roll_no_id = fields.Integer("Roll No", readonly=True)
    standard_id = fields.Many2one('school.standard', "Standard", readonly=True)
    obtain_marks = fields.Float('Obtain Marks', help="Marks obtain in exam")
    result = fields.Char(compute='_compute_student_result',
                         string='Result',
                         help="Result Obtained",
                         store=True)
示例#18
0
class StockLocationRack(models.Model):

    _name = "stock.location.rack"
    _order = 'sequence, name'

    name = fields.Char('Name', required=1)
    warehouse_id = fields.Many2one('stock.warehouse', string='Warehouse')
    code = fields.Char('Code', required=1)
    sequence = fields.Integer('Sequence')
    x_number = fields.Integer('Pos X', required=1)
    y_number = fields.Integer('Pos Y count', required=1)
    z_number = fields.Integer('Pos Z count', required=1)
    index = fields.Selection(INDEX_SELECTION, 'Index pos')
    parent_id = fields.Many2one('stock.location',
                                string="Parent location",
                                required=1)
    rotation = fields.Selection(INDEX_ROTATION, 'Rotation', default='medium')
    in_pack = fields.Boolean(
        'Must be in pack',
        default=False,
        help=
        "If checked, al quants in this location be in pack, so  ops and moves must have result_package_id"
    )
    need_check = fields.Boolean(
        "Need check",
        default=False,
        help="Need check in PDA (p.e. when not tags in location)")

    @api.multi
    def update_rack_loc_ids(self):

        for rack in self:
            usage = self.parent_id.usage
            loc_order = str(rack.sequence).zfill(3)
            for y in range(1, rack.y_number + 1):
                for z in range(1, rack.z_number + 1):
                    for x in range(1, rack.x_number + 1):
                        pos_x = str(rack.x_number)
                        pos_y = str(y).zfill(2)
                        pos_z = str(z).zfill(2)
                        order_z = rack.z_number - z
                        if rack.index == 'inc':
                            order_y = y - 1
                            order_y2 = False
                        elif rack.index == 'dec':
                            order_y = rack.y_number - y
                            order_y2 = False
                        else:
                            order_y = y
                            order_y2 = rack.y_number - y
                        picking_order_dec = ''
                        picking_order = int('{}{}{}'.format(
                            loc_order,
                            str(order_y).zfill(2),
                            str(order_z).zfill(2)))
                        if order_y2:
                            picking_order_dec = int('{}{}{}'.format(
                                loc_order,
                                str(order_y2).zfill(2),
                                str(order_z).zfill(2)))

                        name = "{} / {}.{}.{}".format(rack.name, pos_x, pos_y,
                                                      pos_z)
                        barcode = "{}.{}.{}.{}".format(rack.code, pos_x, pos_y,
                                                       pos_z)

                        vals = {
                            'name': name,
                            'warehouse_id': rack.warehouse_id.id,
                            'location_id': rack.parent_id.id,
                            'usage': usage,
                            'posx': x,
                            'posy': y,
                            'posz': z,
                            'barcode': barcode,
                            'picking_order': picking_order,
                            'picking_order_dec': picking_order_dec,
                            'rotation': rack.rotation,
                            'rack_id': rack.id,
                            'in_pack': rack.in_pack,
                            'need_check': rack.need_check,
                            'company_id': False
                        }
                        domain_location = [('barcode', '=', barcode),
                                           ('warehouse_id', '=',
                                            rack.warehouse_id.id)]
                        location = self.env['stock.location'].search(
                            domain_location)
                        if location:
                            location.write(vals)
                        else:
                            self.env['stock.location'].create(vals)
示例#19
0
class LineaDeContrato(models.Model):
    _name = 'erp.operaciones.linea_de_contrato'
    _order = 'tipo_de_producto, orden_marca, producto'

    @api.model
    def _default_currency(self):
        return 2

    @api.onchange('producto')
    def _onchange_producto(self):
        self.tipo_de_producto = self.producto.categ_id.name
        self.orden_marca = self.producto.marca.orden
        self.orden_repuesto = 0

        if len(self.producto.repuestos) > 0:
            self.orden_repuesto = self.producto.repuestos[0]

        if self.producto.cantidad_minima_de_orden is not False:
            self.cantidad_producto_total_contrato = self.producto.cantidad_minima_de_orden

        # self.contrato.chequear_si_el_producto_esta_insertado(self.producto)

        self.price_unit = 0
        self.price_subtotal = 0

        if self.contrato.pricelist_id.id is not False:
            for linea in self.contrato.pricelist_id.item_ids:
                if linea.product_id.id == self.producto.id:
                    self.price_unit = linea.importe_mon_cliente
                    self.price_subtotal = self.cantidad_producto_total_contrato * self.price_unit
        self.cantidad_por_caja_master = self.producto.cantidad_por_caja_master
        self.volumen_caja_master = self.producto.volumen_caja_master

        self.moneda = self.contrato.moneda_mon.id

    @api.onchange('cantidad_por_caja_master')
    def _onchange_cantidad_caja_master(self):
        self.price_subtotal = self.cantidad_producto_total_contrato * self.price_unit

        if self.cantidad_por_caja_master != 0:
            if self.cantidad_producto_total_contrato % self.cantidad_por_caja_master != 0:
                res = {}
                warning = False

                warning = {
                    'title': _('Warning!'),
                    'message': "Acaba de introducir una cantidad de producto que no es divisible entre la cantidad de caja máster.",
                }
                self.discount = 0
                res = {'warning': warning}

                return res

    @api.onchange('price_unit')
    def _onchange_importe_unitario(self):
        self.price_subtotal = self.cantidad_producto_total_contrato * self.price_unit



    @api.onchange('cantidad_producto_total_contrato')
    def _onchange_cantidad_producto_total_contrato(self):
        self.price_subtotal = self.cantidad_producto_total_contrato * self.price_unit

        if self.cantidad_por_caja_master != 0:
            self.volumen_total_de_linea_producto = self.cantidad_producto_actual_contrato * self.volumen_caja_master / self.cantidad_por_caja_master

            if self.cantidad_producto_actual_contrato % self.cantidad_por_caja_master != 0:
                res = {}
                warning = False

                warning = {
                    'title': _('Warning!'),
                    'message': "Acaba de introducir una cantidad de producto que no es divisible entre la cantidad de caja máster.",
                }
                self.discount = 0
                res = {'warning': warning}

                return res


    @api.onchange('volumen_caja_master')
    def _onchange_volumen_caja_master(self):
        if self.cantidad_por_caja_master != 0:
            self.volumen_total_de_linea_producto = self.cantidad_producto_actual_contrato * self.volumen_caja_master / self.cantidad_por_caja_master

    producto = fields.Many2one('product.product', required=True)

    #foto = fields.Binary("Variant Image", attachment=True, related='producto.image_variant_128')
    foto = fields.Image("Variant Image", related="producto.product_tmpl_id.image_128", max_width=128, max_height=128)

    modelo = fields.Char(string="Modelo", related='producto.name')
    codigo_proveedor = fields.Char(string="Código proveedor", related='producto.codigo_proveedor')
    cantidad_minima_de_orden = fields.Integer(string="MOQ", related='producto.cantidad_minima_de_orden')

    tipo_de_producto = fields.Char(string="Línea de producto")
    orden_marca = fields.Char(string="Orden marca")

    cantidad_por_caja_master = fields.Integer()
    volumen_caja_master = fields.Float(string='Volumen caja máster', digits=dp.get_precision('seisDecimales'))

    currency_id = fields.Many2one('res.currency', string='Moneda', default=_default_currency)
    price_unit = fields.Monetary(string='Precio unitario', currency_field='currency_id')

    volumen_total_de_linea_producto = fields.Float(string='Volumen total producto',
                                                   digits=dp.get_precision('seisDecimales'))
    price_subtotal = fields.Monetary(string='Importe total producto', currency_field='currency_id')

    cantidad_producto_actual_contrato = fields.Integer()
    cantidad_producto_total_contrato = fields.Integer()

    sequence = fields.Integer(string='Orden', default=10)

    contrato = fields.Many2one('erp.operaciones.contrato', ondelete='cascade')
示例#20
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'}
示例#21
0
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'})
示例#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
示例#23
0
class ExceptionRule(models.Model):
    _name = 'exception.rule'
    _inherit = ['mail.thread', 'mail.activity.mixin']
    _description = "Exception Rules"
    _order = 'active desc, sequence asc'

    name = fields.Char('Name', translate=True)
    description = fields.Text('Description', translate=True)
    sequence = fields.Integer(
        string='Sequence',
        help="Gives the sequence order when applying the test")
    rule_group = fields.Selection(
        selection=[],
        help="Rule group is used to group the rules that must validated "
        "at same time for a target object. Ex: "
        "validate sale.order.line rules with sale order rules.",
    )
    model = fields.Selection(selection=[], string='Apply on')
    active = fields.Boolean('Active')
    filter_domain = fields.Char()
    action_type = fields.Selection([('domain', 'Filter'),
                                    ('code', 'Python Code')],
                                   default='domain')
    group_approval_ids = fields.One2many('group.and.approval', 'rule_id')

    # check_len_group=fields.Integer("In Hand Value", compute="check_group_lines_exist",store=True)

    day_approval = fields.Integer("Days To Approve")

    # @api.depends('group_approval_ids')
    # def check_group_lines_exist(self):
    #     # print("-----------------compute method---------")
    #     for s in self:
    #         s.check_len_group = len(s.group_approval_ids)
    #
    # @api.constrains('check_len_group')
    # def check_no_of_group_validation(self):
    #     # print("--------------constaint method-----------")
    #     for i in self:
    #         if i.check_len_group <= 0:
    #             raise UserError(
    #             _('There should be atleast one group in Approval Matrix'))

    # ************* create and write method method***************
    @api.model
    def create(self, vals):
        res = super(ExceptionRule, self).create(vals)
        if not res.group_approval_ids:
            raise UserError(
                _('There should be atleast one group in Approval Matrix'))
        return res

    @api.multi
    def _write(self, vals):
        res = super(ExceptionRule, self)._write(vals)
        for s in self:
            if not s.group_approval_ids:
                raise UserError(
                    _('There should be atleast one group in Approval Matrix'))
        return res

    @api.constrains('filter_domain')
    def change_chatter_on_filter(self):
        _body = (_((
            "<ul>A new condition  <b style='color:green'>{0}</b> has been added in Rule</ul> "
        ).format(self.filter_domain)))
        self.message_post(body=_body)

    @api.constrains('action_type')
    def change_chatter_on_action_type(self):

        if self.action_type == 'domain':
            previous = 'code'
        else:
            previous = 'domain'

        _body = (_((
            "<ul>Exception Mode Changed <b style='color:red'>{1}</b> ------> <b style='color:green'>{0}</b></ul> "
        ).format(previous, self.action_type)))
        self.message_post(body=_body)

    @api.constrains('code')
    def chatter_on_code(self):
        _body = (_(("<ul>Code was changed<ul>")))
        self.message_post(body=_body)

    code = fields.Text(
        'Python Code',
        help="Python code executed to check if the exception apply or "
        "not. The code must apply block = True to apply the "
        "exception.",
        default="""
        # Python code. Use failed = True to block the base.exception.
        # You can use the following variables :
        #  - self: ORM model of the record which is checked
        #  - "rule_group" or "rule_group_"line:
        #       browse_record of the base.exception or
        #       base.exception line (ex rule_group = sale for sale order)
        #  - object: same as order or line, browse_record of the base.exception or
        #    base.exception line
        #  - pool: ORM model pool (i.e. self.pool)
        #  - time: Python time module
        #  - cr: database cursor
        #  - uid: current user id
        #  - context: current context
    """)

    @api.multi
    def refactor_code(self):
        search_string = '''k = env['{0}'].search({1}) '''.format(
            self.model, self.filter_domain)
        remaining = [search_string, 'if sale.id in k.ids:', '  failed = True']
        code_string = '\n'.join(remaining)
        self.code = code_string

    @api.multi
    def toggle_active(self):
        """ Inverse the value of the field ``active`` on the records in ``self``. """
        for record in self:
            record.active = not record.active
示例#24
0
class Project(models.Model):
    _name = "project.project"
    _description = "Project"
    _inherit = ['mail.alias.mixin', 'mail.thread', 'ir.needaction_mixin']
    _inherits = {'account.analytic.account': "analytic_account_id"}
    _order = "sequence, name, id"
    _period_number = 5

    def get_alias_model_name(self, vals):
        return vals.get('alias_model', 'project.task')

    def get_alias_values(self):
        values = super(Project, self).get_alias_values()
        values['alias_defaults'] = {'project_id': self.id}
        return values

    @api.multi
    def unlink(self):
        analytic_accounts_to_delete = self.env['account.analytic.account']
        for project in self:
            if project.tasks:
                raise UserError(
                    _('You cannot delete a project containing tasks. You can either delete all the project\'s tasks and then delete the project or simply deactivate the project.'
                      ))
            if project.analytic_account_id and not project.analytic_account_id.line_ids:
                analytic_accounts_to_delete |= project.analytic_account_id
        res = super(Project, self).unlink()
        analytic_accounts_to_delete.unlink()
        return res

    def _compute_attached_docs_count(self):
        Attachment = self.env['ir.attachment']
        for project in self:
            project.doc_count = Attachment.search_count([
                '|', '&', ('res_model', '=', 'project.project'),
                ('res_id', '=', project.id), '&',
                ('res_model', '=', 'project.task'),
                ('res_id', 'in', project.task_ids.ids)
            ])

    def _compute_task_count(self):
        for project in self:
            project.task_count = len(project.task_ids)

    def _compute_task_needaction_count(self):
        projects_data = self.env['project.task'].read_group(
            [('project_id', 'in', self.ids),
             ('message_needaction', '=', True)], ['project_id'],
            ['project_id'])
        mapped_data = {
            project_data['project_id'][0]:
            int(project_data['project_id_count'])
            for project_data in projects_data
        }
        for project in self:
            project.task_needaction_count = mapped_data.get(project.id, 0)

    @api.model
    def _get_alias_models(self):
        """ Overriden in project_issue to offer more options """
        return [('project.task', "Tasks")]

    @api.multi
    def attachment_tree_view(self):
        self.ensure_one()
        domain = [
            '|', '&', ('res_model', '=', 'project.project'),
            ('res_id', 'in', self.ids), '&',
            ('res_model', '=', 'project.task'),
            ('res_id', 'in', self.task_ids.ids)
        ]
        return {
            'name':
            _('Attachments'),
            'domain':
            domain,
            'res_model':
            'ir.attachment',
            'type':
            'ir.actions.act_window',
            'view_id':
            False,
            'view_mode':
            'kanban,tree,form',
            'view_type':
            'form',
            'help':
            _('''<p class="oe_view_nocontent_create">
                        Documents are attached to the tasks and issues of your project.</p><p>
                        Send messages or log internal notes with attachments to link
                        documents to your project.
                    </p>'''),
            'limit':
            80,
            'context':
            "{'default_res_model': '%s','default_res_id': %d}" %
            (self._name, self.id)
        }

    @api.model
    def activate_sample_project(self):
        """ Unarchives the sample project 'project.project_project_data' and
            reloads the project dashboard """
        # Unarchive sample project
        project = self.env.ref('project.project_project_data', False)
        if project:
            project.write({'active': True})

        cover_image = self.env.ref('project.msg_task_data_14_attach', False)
        cover_task = self.env.ref('project.project_task_data_14', False)
        if cover_image and cover_task:
            cover_task.write({'displayed_image_id': cover_image.id})

        # Change the help message on the action (no more activate project)
        action = self.env.ref('project.open_view_project_all', False)
        action_data = None
        if action:
            action.sudo().write({
                "help":
                _('''<p class="oe_view_nocontent_create">Click to create a new project.</p>'''
                  )
            })
            action_data = action.read()[0]
        # Reload the dashboard
        return action_data

    def _compute_is_favorite(self):
        for project in self:
            project.is_favorite = self.env.user in project.favorite_user_ids

    def _get_default_favorite_user_ids(self):
        return [(6, 0, [self.env.uid])]

    @api.model
    def default_get(self, flds):
        result = super(Project, self).default_get(flds)
        result['use_tasks'] = True
        return result

    # Lambda indirection method to avoid passing a copy of the overridable method when declaring the field
    _alias_models = lambda self: self._get_alias_models()

    active = fields.Boolean(
        default=True,
        help=
        "If the active field is set to False, it will allow you to hide the project without removing it."
    )
    sequence = fields.Integer(
        default=10,
        help="Gives the sequence order when displaying a list of Projects.")
    analytic_account_id = fields.Many2one(
        'account.analytic.account',
        string='Contract/Analytic',
        help=
        "Link this project to an analytic account if you need financial management on projects. "
        "It enables you to connect projects with budgets, planning, cost and revenue analysis, timesheets on projects, etc.",
        ondelete="cascade",
        required=True,
        auto_join=True)
    favorite_user_ids = fields.Many2many(
        'res.users',
        'project_favorite_user_rel',
        'project_id',
        'user_id',
        default=_get_default_favorite_user_ids,
        string='Members')
    is_favorite = fields.Boolean(
        compute='_compute_is_favorite',
        string='Show Project on dashboard',
        help="Whether this project should be displayed on the dashboard or not"
    )
    label_tasks = fields.Char(
        string='Use Tasks as',
        default='Tasks',
        help="Gives label to tasks on project's kanban view.")
    tasks = fields.One2many('project.task',
                            'project_id',
                            string="Task Activities")
    resource_calendar_id = fields.Many2one(
        'resource.calendar',
        string='Working Time',
        help="Timetable working hours to adjust the gantt diagram report")
    type_ids = fields.Many2many('project.task.type',
                                'project_task_type_rel',
                                'project_id',
                                'type_id',
                                string='Tasks Stages')
    task_count = fields.Integer(compute='_compute_task_count', string="Tasks")
    task_needaction_count = fields.Integer(
        compute='_compute_task_needaction_count', string="Tasks")
    task_ids = fields.One2many(
        'project.task',
        'project_id',
        string='Tasks',
        domain=['|', ('stage_id.fold', '=', False), ('stage_id', '=', False)])
    color = fields.Integer(string='Color Index')
    user_id = fields.Many2one('res.users',
                              string='Project Manager',
                              default=lambda self: self.env.user)
    alias_id = fields.Many2one(
        'mail.alias',
        string='Alias',
        ondelete="restrict",
        required=True,
        help=
        "Internal email associated with this project. Incoming emails are automatically synchronized "
        "with Tasks (or optionally Issues if the Issue Tracker module is installed)."
    )
    alias_model = fields.Selection(
        _alias_models,
        string="Alias Model",
        index=True,
        required=True,
        default='project.task',
        help=
        "The kind of document created when an email is received on this project's email alias"
    )
    privacy_visibility = fields.Selection(
        [
            ('followers', _('On invitation only')),
            ('employees', _('Visible by all employees')),
            ('portal', _('Visible by following customers')),
        ],
        string='Privacy',
        required=True,
        default='employees',
        help=
        "Holds visibility of the tasks or issues that belong to the current project:\n"
        "- On invitation only: Employees may only see the followed project, tasks or issues\n"
        "- Visible by all employees: Employees may see all project, tasks or issues\n"
        "- Visible by following customers: employees see everything;\n"
        "   if website is activated, portal users may see project, tasks or issues followed by\n"
        "   them or by someone of their company\n")
    doc_count = fields.Integer(compute='_compute_attached_docs_count',
                               string="Number of documents attached")
    date_start = fields.Date(string='Start Date')
    date = fields.Date(string='Expiration Date',
                       index=True,
                       track_visibility='onchange')

    _sql_constraints = [
        ('project_date_greater', 'check(date >= date_start)',
         'Error! project start-date must be lower than project end-date.')
    ]

    @api.multi
    def map_tasks(self, new_project_id):
        """ copy and map tasks from old to new project """
        tasks = self.env['project.task']
        for task in self.tasks:
            # preserve task name and stage, normally altered during copy
            defaults = {'stage_id': task.stage_id.id, 'name': task.name}
            tasks += task.copy(defaults)
        return self.browse(new_project_id).write(
            {'tasks': [(6, 0, tasks.ids)]})

    @api.multi
    def copy(self, default=None):
        if default is None:
            default = {}
        self = self.with_context(active_test=False)
        if not default.get('name'):
            default['name'] = _("%s (copy)") % (self.name)
        project = super(Project, self).copy(default)
        for follower in self.message_follower_ids:
            project.message_subscribe(partner_ids=follower.partner_id.ids,
                                      subtype_ids=follower.subtype_ids.ids)
        self.map_tasks(project.id)
        return project

    @api.model
    def create(self, vals):
        ir_values = self.env['ir.values'].get_default(
            'project.config.settings', 'generate_project_alias')
        if ir_values:
            vals['alias_name'] = vals.get('alias_name') or vals.get('name')
        # Prevent double project creation when 'use_tasks' is checked
        self = self.with_context(project_creation_in_progress=True,
                                 mail_create_nosubscribe=True)
        return super(Project, self).create(vals)

    @api.multi
    def write(self, vals):
        # if alias_model has been changed, update alias_model_id accordingly
        if vals.get('alias_model'):
            vals['alias_model_id'] = self.env['ir.model'].search(
                [('model', '=', vals.get('alias_model', 'project.task'))],
                limit=1).id
        res = super(Project, self).write(vals)
        if 'active' in vals:
            # archiving/unarchiving a project does it on its tasks, too
            self.with_context(active_test=False).mapped('tasks').write(
                {'active': vals['active']})
        return res

    @api.multi
    def toggle_favorite(self):
        favorite_projects = not_fav_projects = self.env[
            'project.project'].sudo()
        for project in self:
            if self.env.user in project.favorite_user_ids:
                favorite_projects |= project
            else:
                not_fav_projects |= project

        # Project User has no write access for project.
        not_fav_projects.write({'favorite_user_ids': [(4, self.env.uid)]})
        favorite_projects.write({'favorite_user_ids': [(3, self.env.uid)]})

    @api.multi
    def close_dialog(self):
        return {'type': 'ir.actions.act_window_close'}
示例#25
0
文件: repo.py 项目: Mark952700/runbot
class runbot_repo(models.Model):

    _name = "runbot.repo"

    name = fields.Char('Repository', required=True)
    short_name = fields.Char('Repository',
                             compute='_compute_short_name',
                             store=False,
                             readonly=True)
    sequence = fields.Integer('Sequence')
    path = fields.Char(compute='_get_path', string='Directory', readonly=True)
    base = fields.Char(
        compute='_get_base_url', string='Base URL', readonly=True
    )  # Could be renamed to a more explicit name like base_url
    nginx = fields.Boolean('Nginx')
    mode = fields.Selection(
        [('disabled', 'Disabled'), ('poll', 'Poll'), ('hook', 'Hook')],
        default='poll',
        string="Mode",
        required=True,
        help=
        "hook: Wait for webhook on /runbot/hook/<id> i.e. github push event")
    hook_time = fields.Datetime('Last hook time')
    duplicate_id = fields.Many2one(
        'runbot.repo',
        'Duplicate repo',
        help='Repository for finding duplicate builds')
    modules = fields.Char(
        "Modules to install",
        help="Comma-separated list of modules to install and test.")
    modules_auto = fields.Selection(
        [('none', 'None (only explicit modules list)'),
         ('repo', 'Repository modules (excluding dependencies)'),
         ('all', 'All modules (including dependencies)')],
        default='repo',
        string="Other modules to install automatically")

    dependency_ids = fields.Many2many(
        'runbot.repo',
        'runbot_repo_dep_rel',
        column1='dependant_id',
        column2='dependency_id',
        string='Extra dependencies',
        help="Community addon repos which need to be present to run tests.")
    token = fields.Char("Github token", groups="runbot.group_runbot_admin")
    group_ids = fields.Many2many('res.groups', string='Limited to groups')

    def _root(self):
        """Return root directory of repository"""
        default = os.path.join(os.path.dirname(__file__), '../static')
        return os.path.abspath(default)

    @api.depends('name')
    def _get_path(self):
        """compute the server path of repo from the name"""
        root = self._root()
        for repo in self:
            name = repo.name
            for i in '@:/':
                name = name.replace(i, '_')
            repo.path = os.path.join(root, 'repo', name)

    @api.depends('name')
    def _get_base_url(self):
        for repo in self:
            name = re.sub('.+@', '', repo.name)
            name = re.sub('^https://', '', name)  # support https repo style
            name = re.sub('.git$', '', name)
            name = name.replace(':', '/')
            repo.base = name

    @api.depends('name', 'base')
    def _compute_short_name(self):
        for repo in self:
            repo.short_name = '/'.join(repo.base.split('/')[-2:])

    def _git(self, cmd):
        """Execute a git command 'cmd'"""
        for repo in self:
            cmd = ['git', '--git-dir=%s' % repo.path] + cmd
            _logger.info("git command: %s", ' '.join(cmd))
            return subprocess.check_output(cmd).decode('utf-8')

    def _git_rev_parse(self, branch_name):
        return self._git(['rev-parse', branch_name]).strip()

    def _git_export(self, treeish, dest):
        """Export a git repo to dest"""
        self.ensure_one()
        _logger.debug('checkout %s %s %s', self.name, treeish, dest)
        p1 = subprocess.Popen(
            ['git', '--git-dir=%s' % self.path, 'archive', treeish],
            stdout=subprocess.PIPE)
        p2 = subprocess.Popen(['tar', '-xmC', dest],
                              stdin=p1.stdout,
                              stdout=subprocess.PIPE)
        p1.stdout.close()  # Allow p1 to receive a SIGPIPE if p2 exits.
        p2.communicate()[0]

    def _hash_exists(self, commit_hash):
        """ Verify that a commit hash exists in the repo """
        self.ensure_one()
        try:
            self._git(['cat-file', '-e', commit_hash])
        except subprocess.CalledProcessError:
            return False
        return True

    def _github(self, url, payload=None, ignore_errors=False):
        """Return a http request to be sent to github"""
        for repo in self:
            if not repo.token:
                return
            try:
                match_object = re.search('([^/]+)/([^/]+)/([^/.]+(.git)?)',
                                         repo.base)
                if match_object:
                    url = url.replace(':owner', match_object.group(2))
                    url = url.replace(':repo', match_object.group(3))
                    url = 'https://api.%s%s' % (match_object.group(1), url)
                    session = requests.Session()
                    session.auth = (repo.token, 'x-oauth-basic')
                    session.headers.update({
                        'Accept':
                        'application/vnd.github.she-hulk-preview+json'
                    })
                    if payload:
                        response = session.post(url, data=json.dumps(payload))
                    else:
                        response = session.get(url)
                    response.raise_for_status()
                    return response.json()
            except Exception:
                if ignore_errors:
                    _logger.exception('Ignored github error %s %r', url,
                                      payload)
                else:
                    raise

    def _get_refs(self):
        """Find new refs
        :return: list of tuples with following refs informations:
        name, sha, date, author, author_email, subject, committer, committer_email
        """
        self.ensure_one()
        fields = [
            'refname', 'objectname', 'committerdate:iso8601', 'authorname',
            'authoremail', 'subject', 'committername', 'committeremail'
        ]
        fmt = "%00".join(["%(" + field + ")" for field in fields])
        git_refs = self._git([
            'for-each-ref', '--format', fmt, '--sort=-committerdate',
            'refs/heads', 'refs/pull'
        ])
        git_refs = git_refs.strip()
        return [
            tuple(field for field in line.split('\x00'))
            for line in git_refs.split('\n')
        ]

    def _find_or_create_branches(self, refs):
        """Parse refs and create branches that does not exists yet
        :param refs: list of tuples returned by _get_refs()
        :return: dict {branch.name: branch.id}
        The returned structure contains all the branches from refs newly created
        or older ones.
        """
        Branch = self.env['runbot.branch']
        self.env.cr.execute(
            """
            WITH t (branch) AS (SELECT unnest(%s))
          SELECT t.branch, b.id
            FROM t LEFT JOIN runbot_branch b ON (b.name = t.branch)
           WHERE b.repo_id = %s;
        """, ([r[0] for r in refs], self.id))
        ref_branches = {r[0]: r[1] for r in self.env.cr.fetchall()}

        for name, sha, date, author, author_email, subject, committer, committer_email in refs:
            if not ref_branches.get(name):
                _logger.debug('repo %s found new branch %s', self.name, name)
                new_branch = Branch.create({'repo_id': self.id, 'name': name})
                ref_branches[name] = new_branch.id
        return ref_branches

    def _find_new_commits(self, refs, ref_branches):
        """Find new commits in bare repo
        :param refs: list of tuples returned by _get_refs()
        :param ref_branches: dict structure {branch.name: branch.id}
                             described in _find_or_create_branches
        """
        self.ensure_one()
        Branch = self.env['runbot.branch']
        Build = self.env['runbot.build']
        icp = self.env['ir.config_parameter']
        max_age = int(icp.get_param('runbot.runbot_max_age', default=30))

        self.env.cr.execute(
            """
            WITH t (build, branch_id) AS (SELECT unnest(%s), unnest(%s))
          SELECT b.name, b.branch_id
            FROM t LEFT JOIN runbot_build b ON (b.name = t.build) AND (b.branch_id = t.branch_id)
        """, ([r[1] for r in refs], [ref_branches[r[0]] for r in refs]))
        # generate a set of tuples (branch_id, sha)
        builds_candidates = {(r[1], r[0]) for r in self.env.cr.fetchall()}

        for name, sha, date, author, author_email, subject, committer, committer_email in refs:
            branch = Branch.browse(ref_branches[name])

            # skip the build for old branches (Could be checked before creating the branch in DB ?)
            if dateutil.parser.parse(date[:19]) + datetime.timedelta(
                    days=max_age) < datetime.datetime.now():
                continue

            # create build (and mark previous builds as skipped) if not found
            if not (branch.id, sha) in builds_candidates:
                _logger.debug('repo %s branch %s new build found revno %s',
                              self.name, branch.name, sha)
                build_info = {
                    'branch_id': branch.id,
                    'name': sha,
                    'author': author,
                    'author_email': author_email,
                    'committer': committer,
                    'committer_email': committer_email,
                    'subject': subject,
                    'date': dateutil.parser.parse(date[:19]),
                    'coverage': branch.coverage,
                }
                if not branch.sticky:
                    # pending builds are skipped as we have a new ref
                    builds_to_skip = Build.search(
                        [('branch_id', '=', branch.id),
                         ('state', '=', 'pending')],
                        order='sequence asc')
                    builds_to_skip._skip(reason='New ref found')
                    if builds_to_skip:
                        build_info['sequence'] = builds_to_skip[0].sequence
                    # testing builds are killed
                    builds_to_kill = Build.search([('branch_id', '=',
                                                    branch.id),
                                                   ('state', '=', 'testing'),
                                                   ('committer', '=',
                                                    committer)])
                    builds_to_kill.write({'state': 'deathrow'})
                    for btk in builds_to_kill:
                        btk._log(
                            'repo._update_git',
                            'Build automatically killed, newer build found.')

                new_build = Build.create(build_info)
                # create a reverse dependency build if needed
                if branch.sticky:
                    for rev_repo in self.search([('dependency_ids', 'in',
                                                  self.id)]):
                        # find the latest build with the same branch name
                        latest_rev_build = Build.search([
                            ('repo_id.id', '=', rev_repo.id),
                            ('branch_id.branch_name', '=', branch.branch_name)
                        ],
                                                        order='id desc',
                                                        limit=1)
                        if latest_rev_build:
                            _logger.debug(
                                'Reverse dependency build %s forced in repo %s by commit %s',
                                latest_rev_build.dest, rev_repo.name, sha[:6])
                            latest_rev_build.build_type = 'indirect'
                            new_build.revdep_build_ids += latest_rev_build._force(
                                message='Rebuild from dependency %s commit %s'
                                % (self.name, sha[:6]))

        # skip old builds (if their sequence number is too low, they will not ever be built)
        skippable_domain = [('repo_id', '=', self.id),
                            ('state', '=', 'pending')]
        icp = self.env['ir.config_parameter']
        running_max = int(
            icp.get_param('runbot.runbot_running_max', default=75))
        builds_to_be_skipped = Build.search(skippable_domain,
                                            order='sequence desc',
                                            offset=running_max)
        builds_to_be_skipped._skip()

    @api.multi
    def _create_pending_builds(self):
        """ Find new commits in physical repos"""
        refs = {}
        ref_branches = {}
        for repo in self:
            try:
                refs[repo] = repo._get_refs()
            except Exception:
                _logger.exception('Fail to get refs for repo %s', repo.name)
            if repo in refs:
                ref_branches[repo] = repo._find_or_create_branches(refs[repo])

        # keep _find_or_create_branches separated from build creation to ease
        # closest branch detection
        for repo in self:
            if repo in refs:
                repo._find_new_commits(refs[repo], ref_branches[repo])

    def _clone(self):
        """ Clone the remote repo if needed """
        self.ensure_one()
        repo = self
        if not os.path.isdir(os.path.join(repo.path, 'refs')):
            _logger.info("Cloning repository '%s' in '%s'" %
                         (repo.name, repo.path))
            subprocess.call(['git', 'clone', '--bare', repo.name, repo.path])

    def _update_git(self, force):
        """ Update the git repo on FS """
        self.ensure_one()
        repo = self
        _logger.debug('repo %s updating branches', repo.name)

        if not os.path.isdir(os.path.join(repo.path)):
            os.makedirs(repo.path)
        self._clone()

        # check for mode == hook
        fname_fetch_head = os.path.join(repo.path, 'FETCH_HEAD')
        if not force and os.path.isfile(fname_fetch_head):
            fetch_time = os.path.getmtime(fname_fetch_head)
            if repo.mode == 'hook' and repo.hook_time and dt2time(
                    repo.hook_time) < fetch_time:
                t0 = time.time()
                _logger.debug(
                    'repo %s skip hook fetch fetch_time: %ss ago hook_time: %ss ago',
                    repo.name, int(t0 - fetch_time),
                    int(t0 - dt2time(repo.hook_time)))
                return
        self._update_fetch_cmd()

    def _update_fetch_cmd(self):
        # Extracted from update_git to be easily overriden in external module
        self.ensure_one()
        repo = self
        repo._git([
            'fetch', '-p', 'origin', '+refs/heads/*:refs/heads/*',
            '+refs/pull/*/head:refs/pull/*'
        ])

    @api.multi
    def _update(self, force=True):
        """ Update the physical git reposotories on FS"""
        for repo in self:
            try:
                repo._update_git(force)
            except Exception:
                _logger.exception('Fail to update repo %s', repo.name)

    @api.multi
    def _scheduler(self):
        """Schedule builds for the repository"""
        ids = self.ids
        if not ids:
            return
        icp = self.env['ir.config_parameter']
        workers = int(icp.get_param('runbot.runbot_workers', default=6))
        running_max = int(
            icp.get_param('runbot.runbot_running_max', default=75))
        host = fqdn()

        Build = self.env['runbot.build']
        domain = [('repo_id', 'in', ids), ('branch_id.job_type', '!=', 'none')]
        domain_host = domain + [('host', '=', host)]

        # schedule jobs (transitions testing -> running, kill jobs, ...)
        build_ids = Build.search(domain_host +
                                 [('state', 'in',
                                   ['testing', 'running', 'deathrow'])])
        build_ids._schedule()

        # launch new tests
        nb_testing = Build.search_count(domain_host +
                                        [('state', '=', 'testing')])
        available_slots = workers - nb_testing
        if available_slots > 0:
            # commit transaction to reduce the critical section duration
            self.env.cr.commit()
            # self-assign to be sure that another runbot instance cannot self assign the same builds
            query = """UPDATE
                            runbot_build
                        SET
                            host = %(host)s
                        WHERE
                            runbot_build.id IN (
                                SELECT
                                    runbot_build.id
                                FROM
                                    runbot_build
                                LEFT JOIN runbot_branch ON runbot_branch.id = runbot_build.branch_id
                            WHERE
                                runbot_build.repo_id IN %(repo_ids)s
                                AND runbot_build.state = 'pending'
                                AND runbot_branch.job_type != 'none'
                                AND runbot_build.host IS NULL
                            ORDER BY
                                runbot_branch.sticky DESC,
                                runbot_branch.priority DESC,
                                array_position(array['normal','rebuild','indirect','scheduled']::varchar[], runbot_build.build_type) ASC,
                                runbot_build.sequence ASC
                            FOR UPDATE OF runbot_build SKIP LOCKED
                        LIMIT %(available_slots)s)"""

            self.env.cr.execute(
                query, {
                    'repo_ids': tuple(ids),
                    'host': fqdn(),
                    'available_slots': available_slots
                })
            pending_build = Build.search(domain + domain_host +
                                         [('state', '=', 'pending')])
            if pending_build:
                pending_build._schedule()
                self.env.cr.commit()

        # terminate and reap doomed build
        build_ids = Build.search(domain_host + [('state', '=', 'running')]).ids
        # sort builds: the last build of each sticky branch then the rest
        sticky = {}
        non_sticky = []
        for build in Build.browse(build_ids):
            if build.branch_id.sticky and build.branch_id.id not in sticky:
                sticky[build.branch_id.id] = build.id
            else:
                non_sticky.append(build.id)
        build_ids = list(sticky.values())
        build_ids += non_sticky
        # terminate extra running builds
        Build.browse(build_ids)[running_max:]._kill()
        Build.browse(build_ids)._reap()

    def _domain(self):
        return self.env.get('ir.config_parameter').get_param(
            'runbot.runbot_domain', fqdn())

    def _reload_nginx(self):
        settings = {}
        settings['port'] = config.get('http_port')
        settings['runbot_static'] = os.path.join(
            get_module_resource('runbot', 'static'), '')
        nginx_dir = os.path.join(self._root(), 'nginx')
        settings['nginx_dir'] = nginx_dir
        settings['re_escape'] = re.escape
        settings['fqdn'] = fqdn()
        nginx_repos = self.search([('nginx', '=', True)], order='id')
        if nginx_repos:
            settings['builds'] = self.env['runbot.build'].search([
                ('repo_id', 'in', nginx_repos.ids), ('state', '=', 'running'),
                ('host', '=', fqdn())
            ])

            nginx_config = self.env['ir.ui.view'].render_template(
                "runbot.nginx_config", settings)
            os.makedirs(nginx_dir, exist_ok=True)
            open(os.path.join(nginx_dir, 'nginx.conf'),
                 'wb').write(nginx_config)
            try:
                _logger.debug('reload nginx')
                pid = int(
                    open(os.path.join(nginx_dir,
                                      'nginx.pid')).read().strip(' \n'))
                os.kill(pid, signal.SIGHUP)
            except Exception:
                _logger.debug('start nginx')
                if subprocess.call(
                    ['/usr/sbin/nginx', '-p', nginx_dir, '-c', 'nginx.conf']):
                    # obscure nginx bug leaving orphan worker listening on nginx port
                    if not subprocess.call(
                        ['pkill', '-f', '-P1', 'nginx: worker']):
                        _logger.debug(
                            'failed to start nginx - orphan worker killed, retrying'
                        )
                        subprocess.call([
                            '/usr/sbin/nginx', '-p', nginx_dir, '-c',
                            'nginx.conf'
                        ])
                    else:
                        _logger.debug(
                            'failed to start nginx - failed to kill orphan worker - oh well'
                        )

    def _get_cron_period(self, min_margin=120):
        """ Compute a randomized cron period with a 2 min margin below
        real cron timeout from config.
        """
        cron_limit = config.get('limit_time_real_cron')
        req_limit = config.get('limit_time_real')
        cron_timeout = cron_limit if cron_limit > -1 else req_limit
        return cron_timeout - (min_margin + random.randint(1, 60))

    def _cron_fetch_and_schedule(self, hostname):
        """This method have to be called from a dedicated cron on a runbot
        in charge of orchestration.
        """
        if hostname != fqdn():
            return 'Not for me'
        start_time = time.time()
        timeout = self._get_cron_period()
        icp = self.env['ir.config_parameter']
        update_frequency = int(
            icp.get_param('runbot.runbot_update_frequency', default=10))
        while time.time() - start_time < timeout:
            repos = self.search([('mode', '!=', 'disabled')])
            repos._update(force=False)
            repos._create_pending_builds()

            self.env.cr.commit()
            self.invalidate_cache()
            time.sleep(update_frequency)

    def _cron_fetch_and_build(self, hostname):
        """ This method have to be called from a dedicated cron
        created on each runbot instance.
        """
        if hostname != fqdn():
            return 'Not for me'
        start_time = time.time()
        timeout = self._get_cron_period()
        icp = self.env['ir.config_parameter']
        update_frequency = int(
            icp.get_param('runbot.runbot_update_frequency', default=10))
        while time.time() - start_time < timeout:
            repos = self.search([('mode', '!=', 'disabled')])
            repos._scheduler()
            self.env.cr.commit()
            self.env.reset()
            self = self.env()[self._name]
            self._reload_nginx()
            time.sleep(update_frequency)
示例#26
0
class AccountAnalyticAccount(models.Model):
    _inherit = 'account.analytic.account'
    _description = 'Analytic Account'

    use_tasks = fields.Boolean(
        string='Use Tasks',
        help="Check this box to manage internal activities through this project"
    )
    company_uom_id = fields.Many2one('product.uom',
                                     related='company_id.project_time_mode_id',
                                     string="Company UOM")
    project_ids = fields.One2many('project.project',
                                  'analytic_account_id',
                                  string='Projects')
    project_count = fields.Integer(compute='_compute_project_count',
                                   string='Project Count')

    def _compute_project_count(self):
        for account in self:
            account.project_count = len(
                account.with_context(active_test=False).project_ids)

    @api.model
    def _trigger_project_creation(self, vals):
        '''
        This function is used to decide if a project needs to be automatically created or not when an analytic account is created. It returns True if it needs to be so, False otherwise.
        '''
        return vals.get(
            'use_tasks'
        ) and 'project_creation_in_progress' not in self.env.context

    @api.multi
    def project_create(self, vals):
        '''
        This function is called at the time of analytic account creation and is used to create a project automatically linked to it if the conditions are meet.
        '''
        self.ensure_one()
        Project = self.env['project.project']
        project = Project.with_context(active_test=False).search([
            ('analytic_account_id', '=', self.id)
        ])
        if not project and self._trigger_project_creation(vals):
            project_values = {
                'name': vals.get('name'),
                'analytic_account_id': self.id,
                'use_tasks': True,
            }
            return Project.create(project_values).id
        return False

    @api.model
    def create(self, vals):
        analytic_account = super(AccountAnalyticAccount, self).create(vals)
        analytic_account.project_create(vals)
        return analytic_account

    @api.multi
    def write(self, vals):
        vals_for_project = vals.copy()
        for account in self:
            if not vals.get('name'):
                vals_for_project['name'] = account.name
            account.project_create(vals_for_project)
        return super(AccountAnalyticAccount, self).write(vals)

    @api.multi
    def unlink(self):
        projects = self.env['project.project'].search([('analytic_account_id',
                                                        'in', self.ids)])
        has_tasks = self.env['project.task'].search_count([('project_id', 'in',
                                                            projects.ids)])
        if has_tasks:
            raise UserError(
                _('Please remove existing tasks in the project linked to the accounts you want to delete.'
                  ))
        return super(AccountAnalyticAccount, self).unlink()

    @api.model
    def name_search(self, name, args=None, operator='ilike', limit=100):
        if args is None:
            args = []
        if self.env.context.get('current_model') == 'project.project':
            return self.search(args + [('name', operator, name)],
                               limit=limit).name_get()

        return super(AccountAnalyticAccount,
                     self).name_search(name,
                                       args=args,
                                       operator=operator,
                                       limit=limit)

    @api.multi
    def projects_action(self):
        projects = self.with_context(active_test=False).mapped('project_ids')
        result = {
            "type": "ir.actions.act_window",
            "res_model": "project.project",
            "views": [[False, "tree"], [False, "form"]],
            "domain": [["id", "in", projects.ids]],
            "context": {
                "create": False
            },
            "name": "Projects",
        }
        if len(projects) == 1:
            result['views'] = [(False, "form")]
            result['res_id'] = projects.id
        else:
            result = {'type': 'ir.actions.act_window_close'}
        return result
示例#27
0
class MassMailingContact(models.Model):
    """Model of a contact. This model is different from the partner model
    because it holds only some basic information: name, email. The purpose is to
    be able to deal with large contact list to email without bloating the partner
    base."""
    _name = 'mail.mass_mailing.contact'
    _inherit = ['mail.thread', 'mail.blacklist.mixin']
    _description = 'Mass Mailing Contact'
    _order = 'email'
    _rec_name = 'email'

    name = fields.Char()
    company_name = fields.Char(string='Company Name')
    title_id = fields.Many2one('res.partner.title', string='Title')
    email = fields.Char(required=True)
    is_email_valid = fields.Boolean(compute='_compute_is_email_valid', store=True)
    list_ids = fields.Many2many(
        'mail.mass_mailing.list', 'mail_mass_mailing_contact_list_rel',
        'contact_id', 'list_id', string='Mailing Lists')
    subscription_list_ids = fields.One2many('mail.mass_mailing.list_contact_rel',
        'contact_id', string='Subscription Information')
    message_bounce = fields.Integer(string='Bounced', help='Counter of the number of bounced emails for this contact.', default=0)
    country_id = fields.Many2one('res.country', string='Country')
    tag_ids = fields.Many2many('res.partner.category', string='Tags')
    opt_out = fields.Boolean('Opt Out', compute='_compute_opt_out', search='_search_opt_out',
                             help='Opt out flag for a specific mailing list.'
                                  'This field should not be used in a view without a unique and active mailing list context.')

    @api.depends('email')
    def _compute_is_email_valid(self):
        for record in self:
            normalized = tools.email_normalize(record.email)
            record.is_email_valid = normalized if not normalized else True

    @api.model
    def _search_opt_out(self, operator, value):
        # Assumes operator is '=' or '!=' and value is True or False
        if operator != '=':
            if operator == '!=' and isinstance(value, bool):
                value = not value
            else:
                raise NotImplementedError()

        if 'default_list_ids' in self._context and isinstance(self._context['default_list_ids'], (list, tuple)) and len(self._context['default_list_ids']) == 1:
            [active_list_id] = self._context['default_list_ids']
            contacts = self.env['mail.mass_mailing.list_contact_rel'].search([('list_id', '=', active_list_id)])
            return [('id', 'in', [record.contact_id.id for record in contacts if record.opt_out == value])]
        else:
            raise UserError('Search opt out cannot be executed without a unique and valid active mailing list context.')

    @api.depends('subscription_list_ids')
    def _compute_opt_out(self):
        if 'default_list_ids' in self._context and isinstance(self._context['default_list_ids'], (list, tuple)) and len(self._context['default_list_ids']) == 1:
            [active_list_id] = self._context['default_list_ids']
            for record in self:
                active_subscription_list = record.subscription_list_ids.filtered(lambda l: l.list_id.id == active_list_id)
                record.opt_out = active_subscription_list.opt_out
        else:
            for record in self:
                record.opt_out = False

    def get_name_email(self, name):
        name, email = self.env['res.partner']._parse_partner_name(name)
        if name and not email:
            email = name
        if email and not name:
            name = email
        return name, email

    @api.model
    def name_create(self, name):
        name, email = self.get_name_email(name)
        contact = self.create({'name': name, 'email': email})
        return contact.name_get()[0]

    @api.model
    def add_to_list(self, name, list_id):
        name, email = self.get_name_email(name)
        contact = self.create({'name': name, 'email': email, 'list_ids': [(4, list_id)]})
        return contact.name_get()[0]

    @api.multi
    def message_get_default_recipients(self):
        return dict((record.id, {'partner_ids': [], 'email_to': record.email_normalized, 'email_cc': False}) for record in self)
class CRMLeadMiningRequest(models.Model):
    _name = 'crm.iap.lead.mining.request'
    _description = 'CRM Lead Mining Request'

    def _default_lead_type(self):
        if self.env.user.has_group('crm.group_use_lead'):
            return 'lead'
        else:
            return 'opportunity'

    name = fields.Char(string='Request Number', required=True, readonly=True, default=lambda self: _('New'), copy=False)
    state = fields.Selection([('draft', 'Draft'), ('done', 'Done'), ('error', 'Error')], string='Status', required=True, default='draft')

    # Request Data
    lead_number = fields.Integer(string='Number of Leads', required=True, default=10)
    search_type = fields.Selection([('companies', 'Companies'), ('people', 'Companies and their Contacts')], string='Target', required=True, default='companies')
    error = fields.Text(string='Error', readonly=True)

    # Lead / Opportunity Data
    lead_type = fields.Selection([('lead', 'Lead'), ('opportunity', 'Opportunity')], string='Type', required=True, default=_default_lead_type)
    team_id = fields.Many2one('crm.team', string='Sales Team', domain="[('use_opportunities', '=', True)]")
    user_id = fields.Many2one('res.users', string='Salesperson')
    tag_ids = fields.Many2many('crm.tag', string='Tags')
    lead_ids = fields.One2many('crm.lead', 'lead_mining_request_id', string='Generated Lead / Opportunity')
    leads_count = fields.Integer(compute='_compute_leads_count', string='Number of Generated Leads')

    # Company Criteria Filter
    filter_on_size = fields.Boolean(string='Filter on Size', default=False)
    company_size_min = fields.Integer(string='Size', default=1)
    company_size_max = fields.Integer(default=1000)
    country_ids = fields.Many2many('res.country', string='Countries')
    state_ids = fields.Many2many('res.country.state', string='States')
    industry_ids = fields.Many2many('crm.iap.lead.industry', string='Industries')

    # Contact Generation Filter
    contact_number = fields.Integer(string='Number of Contacts', default=1)
    contact_filter_type = fields.Selection([('role', 'Role'), ('seniority', 'Seniority')], string='Filter on', default='role')
    preferred_role_id = fields.Many2one('crm.iap.lead.role', string='Preferred Role')
    role_ids = fields.Many2many('crm.iap.lead.role', string='Other Roles')
    seniority_id = fields.Many2one('crm.iap.lead.seniority', string='Seniority')

    # Fields for the blue tooltip
    lead_credits = fields.Char(compute='_compute_tooltip', readonly=True)
    lead_contacts_credits = fields.Char(compute='_compute_tooltip', readonly=True)
    lead_total_credits = fields.Char(compute='_compute_tooltip', readonly=True)

    @api.onchange('lead_number', 'contact_number')
    def _compute_tooltip(self):
        for record in self:
            company_credits = CREDIT_PER_COMPANY * record.lead_number
            contact_credits = CREDIT_PER_CONTACT * record.contact_number
            total_contact_credits = contact_credits * record.lead_number
            record.lead_contacts_credits = _("Up to %d additional credits will be consumed to identify %d contacts per company.") % (contact_credits*company_credits, record.contact_number)
            record.lead_credits = _('%d credits will be consumed to find %d companies.') % (company_credits, record.lead_number)
            record.lead_total_credits = _("This makes a total of %d credits for this request.") % (total_contact_credits + company_credits)

    @api.depends('lead_ids')
    def _compute_leads_count(self):
        for req in self:
            req.leads_count = len(req.lead_ids)

    @api.onchange('lead_number')
    def _onchange_lead_number(self):
        if self.lead_number <= 0:
            self.lead_number = 1
        elif self.lead_number > MAX_LEAD:
            self.lead_number = MAX_LEAD

    @api.onchange('contact_number')
    def _onchange_contact_number(self):
        if self.contact_number <= 0:
            self.contact_number = 1
        elif self.contact_number > MAX_CONTACT:
            self.contact_number = MAX_CONTACT

    @api.onchange('country_ids')
    def _onchange_country_ids(self):
        self.state_ids = []

    @api.onchange('company_size_min')
    def _onchange_company_size_min(self):
        if self.company_size_min <= 0:
            self.company_size_min = 1
        elif self.company_size_min > self.company_size_max:
            self.company_size_min = self.company_size_max

    @api.onchange('company_size_max')
    def _onchange_company_size_max(self):
        if self.company_size_max < self.company_size_min:
            self.company_size_max = self.company_size_min
    
    def _prepare_iap_payload(self):
        """
        This will prepare the data to send to the server
        """
        self.ensure_one()
        payload = {'lead_number': self.lead_number,
                   'search_type': self.search_type,
                   'countries': self.country_ids.mapped('code')}
        if self.state_ids:
            payload['states'] = self.state_ids.mapped('code')
        if self.filter_on_size:
            payload.update({'company_size_min': self.company_size_min,
                            'company_size_max': self.company_size_max})
        if self.industry_ids:
            payload['industry_ids'] = self.industry_ids.mapped('reveal_id')
        if self.search_type == 'people':
            payload.update({'contact_number': self.contact_number,
                            'contact_filter_type': self.contact_filter_type})
            if self.contact_filter_type == 'role':
                payload.update({'preferred_role': self.preferred_role_id.reveal_id,
                                'other_roles': self.role_ids.mapped('reveal_id')})
            elif self.contact_filter_type == 'seniority':
                payload['seniority'] = self.seniority_id.reveal_id
        return payload

    def _perform_request(self):
        """
        This will perform the request and create the corresponding leads.
        The user will be notified if he hasn't enough credits.
        """
        server_payload = self._prepare_iap_payload()
        reveal_account = self.env['iap.account'].get('reveal')
        dbuuid = self.env['ir.config_parameter'].sudo().get_param('database.uuid')
        endpoint = self.env['ir.config_parameter'].sudo().get_param('reveal.endpoint', DEFAULT_ENDPOINT) + '/iap/clearbit/1/lead_mining_request'
        params = {
            'account_token': reveal_account.account_token,
            'dbuuid': dbuuid,
            'data': server_payload
        }
        try:
            response = jsonrpc(endpoint, params=params, timeout=300)
            return response['data']
        except InsufficientCreditError as e:
            self.error = 'Insufficient credits. Recharge your account and retry.'
            self.state = 'error'
            self._cr.commit()
            raise e

    def _create_leads_from_response(self, result):
        """ This method will get the response from the service and create the leads accordingly """
        self.ensure_one()
        lead_vals = []
        messages_to_post = {}
        for data in result:
            lead_vals.append(self._lead_vals_from_response(data))
            messages_to_post[data['company_data']['clearbit_id']] = self.env['crm.iap.lead.helpers'].format_data_for_message_post(data['company_data'], data.get('people_data'))
        leads = self.env['crm.lead'].create(lead_vals)
        for lead in leads:
            if messages_to_post.get(lead.reveal_id):
                lead.message_post_with_view('crm_iap_lead.lead_message_template', values=messages_to_post[lead.reveal_id], subtype_id=self.env.ref('mail.mt_note').id)

    # Methods responsible for format response data into valid odoo lead data
    @api.model
    def _lead_vals_from_response(self, data):
        self.ensure_one()
        company_data = data.get('company_data')
        people_data = data.get('people_data')
        lead_vals = self.env['crm.iap.lead.helpers'].lead_vals_from_response(self.lead_type, self.team_id.id, self.tag_ids.ids, self.user_id.id, company_data, people_data)
        lead_vals['lead_mining_request_id'] = self.id
        return lead_vals

    @api.model
    def get_empty_list_help(self, help):
        help_title = _('Create a Lead Mining Request')
        sub_title = _('Generate new leads based on their country, industry, size, etc.')
        return '<p class="o_view_nocontent_smiling_face">%s</p><p class="oe_view_nocontent_alias">%s</p>' % (help_title, sub_title)

    def action_draft(self):
        self.ensure_one()
        self.name = _('New')
        self.state = 'draft'

    def action_submit(self):
        self.ensure_one()
        if self.name == _('New'):
            self.name = self.env['ir.sequence'].next_by_code('crm.iap.lead.mining.request') or _('New')
        results = self._perform_request()
        if results:
            self._create_leads_from_response(results)
            self.state = 'done'
        if self.lead_type == 'lead':
            return self.action_get_lead_action()
        elif self.lead_type == 'opportunity':
            return self.action_get_opportunity_action()

    def action_get_lead_action(self):
        self.ensure_one()
        action = self.env.ref('crm.crm_lead_all_leads').read()[0]
        action['domain'] = [('id', 'in', self.lead_ids.ids), ('type', '=', 'lead')]
        action['help'] = _("""<p class="o_view_nocontent_empty_folder">
            No leads found
        </p><p>
            No leads could be generated according to your search criteria
        </p>""")
        return action

    def action_get_opportunity_action(self):
        self.ensure_one()
        action = self.env.ref('crm.crm_lead_opportunities').read()[0]
        action['domain'] = [('id', 'in', self.lead_ids.ids), ('type', '=', 'opportunity')]
        action['help'] = _("""<p class="o_view_nocontent_empty_folder">
            No opportunities found
        </p><p>
            No opportunities could be generated according to your search criteria
        </p>""")
        return action
示例#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()})
示例#30
0
class Session(models.Model):
    _name = "openacademy.session"

    name = fields.Char(required=True)
    start_date = fields.Date(default=fields.Date.today)
    duration = fields.Float(digits=(6, 2), help="Duration in Days.")
    seats = fields.Integer(string="Number of Seats")
    instructor_id = fields.Many2one('res.partner',
                                    string="Instructor",
                                    domain=[
                                        '|', ('instructor', '=', True),
                                        ('category_id.name', 'ilike',
                                         "Teacher")
                                    ])
    course_id = fields.Many2one('openacademy.course',
                                ondelete='cascade',
                                string="Course",
                                required=True)
    attendee_ids = fields.Many2many('res.partner', string="Attendees")
    taken_seats = fields.Float(string='Taken Seats', compute='_taken_seats')
    active = fields.Boolean(default=True)
    end_date = fields.Date(string="End Date",
                           store=True,
                           compute='_get_end_date',
                           inverse='_set_end_date')
    hours = fields.Float(string='Duration in hours',
                         compute='_get_hours',
                         inverse='_set_hours')
    attendees_count = fields.Integer(string="Attendees Count",
                                     compute="_get_attendees_count",
                                     store=True)
    color = fields.Integer()

    @api.depends('attendee_ids')
    def _get_attendees_count(self):
        for record in self:
            record.attendees_count = len(record.attendee_ids)

    @api.depends('duration')
    def _get_hours(self):
        for record in self:
            record.hours = record.duration * 24

    def _set_hours(self):
        for record in self:
            record.duration = record.hours / 24

    @api.depends('seats', 'attendee_ids')
    def _taken_seats(self):
        for a in self:
            if not a.seats:
                a.taken_seats = 0.00
            else:
                a.taken_seats = (len(a.attendee_ids) / a.seats) * 100

    #Caledar View
    @api.depends('start_date', 'duration')
    def _get_end_date(self):
        for r in self:
            if not (r.start_date and r.duration):
                r.end_date = r.start_date
                continue
            start = fields.Datetime.from_string(r.start_date)
            duration = timedelta(days=r.duration, seconds=-1)
            r.end_date = start + duration

    def _set_end_date(self):
        for r in self:
            if not (r.start_date and r.end_date):
                continue
            start_date = fields.Datetime.from_string(r.start_date)
            end_date = fields.Datetime.from_string(r.end_date)
            r.duration = (end_date - start_date).days + 1

    #onchange function is giving erorr while adding -ve seats number and increasing the number of attendee's than the seats
    #but it is not raising the warning
    # except warning it is working fine!
    @api.onchange('seats', 'attendee_ids')
    def _validate_verify_seats(self):
        if self.seats < 0:
            return {
                'warning': {
                    'title': _('Invalid value of Seats'),
                    'msg': _('Seats cannot be Negative'),
                },
            }
        if self.seats < len(self.attendee_ids):
            return {
                'warning': {
                    'title': _('Too many Attendees.'),
                    'msg': _('Ether Increase Seats or reduce Attendees.'),
                },
            }

    @api.constrains('instructor_id', 'attendee_ids')
    def _check_constraints(self):
        for record in self:
            if record.instructor_id:
                if record.instructor_id in record.attendee_ids:
                    raise exceptions.ValidationError(
                        _('An instructor cannot be  an attendee in his/her own Session.'
                          ))


# class open_academy(models.Model):
#     _name = 'open_academy.open_academy'

#     name = fields.Char()
#     value = fields.Integer()
#     value2 = fields.Float(compute="_value_pc", store=True)
#     description = fields.Text()
#
#     @api.depends('value')
#     def _value_pc(self):
#         self.value2 = float(self.value) / 100