Files
contract/contract_payment_auto/models/account_analytic_account.py
fcayre caec4cf6f8 [FIX] contract_payment_auto: transaction create must always get a token (#167)
When a contrat had no payment token but the corresponding partner had
one, the transaction was created without an acquirer, leading to an
integrity error in postgres.

This change makes sure the token used to test the ability to pay an
invoice is passed along to the transaction creation call.

Tests were also added to check the ability to use the contract token if
present, but the partner's in the opposite case.

This change fixes #165.
2018-05-28 17:46:32 +02:00

174 lines
5.8 KiB
Python

# -*- coding: utf-8 -*-
# Copyright 2017 LasLabs Inc.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import logging
from datetime import datetime, timedelta
from odoo import api, fields, models, _
_logger = logging.getLogger(__name__)
class AccountAnalyticAccount(models.Model):
_inherit = 'account.analytic.account'
payment_token_id = fields.Many2one(
string='Payment Token',
comodel_name='payment.token',
domain="[('partner_id', '=', partner_id)]",
context="{'default_partner_id': partner_id}",
help='This is the payment token that will be used to automatically '
'reconcile debts against this account. If none is set, the '
'bill to partner\'s default token will be used.',
)
@api.multi
@api.onchange('partner_id')
def _onchange_partner_id_payment_token(self):
""" Clear the payment token when the partner is changed. """
self.payment_token_id = self.env['payment.token']
@api.model
def cron_retry_auto_pay(self):
""" Retry automatic payments for appropriate invoices. """
invoice_lines = self.env['account.invoice.line'].search([
('invoice_id.state', '=', 'open'),
('invoice_id.auto_pay_attempts', '>', 0),
('account_analytic_id.is_auto_pay', '=', True),
])
now = datetime.now()
for invoice_line in invoice_lines:
account = invoice_line.account_analytic_id
invoice = invoice_line.invoice_id
fail_time = fields.Datetime.from_string(invoice.auto_pay_failed)
retry_delta = timedelta(hours=account.auto_pay_retry_hours)
retry_time = fail_time + retry_delta
if retry_time < now:
account._do_auto_pay(invoice)
@api.multi
def _create_invoice(self):
""" If automatic payment is enabled, perform auto pay actions. """
invoice = super(AccountAnalyticAccount, self)._create_invoice()
if not self.is_auto_pay:
return invoice
self._do_auto_pay(invoice)
return invoice
@api.multi
def _do_auto_pay(self, invoice):
""" Perform all automatic payment operations on open invoices. """
self.ensure_one()
invoice.ensure_one()
invoice.action_invoice_open()
self._send_invoice_message(invoice)
self._pay_invoice(invoice)
@api.multi
def _pay_invoice(self, invoice):
""" Pay the invoice using the account or partner token. """
if invoice.state != 'open':
_logger.info('Cannot pay an invoice that is not in open state.')
return
if not invoice.residual:
_logger.debug('Cannot pay an invoice with no balance.')
return
token = self.payment_token_id or self.partner_id.payment_token_id
if not token:
_logger.debug(
'Cannot pay an invoice without defining a payment token',
)
return
transaction = self.env['payment.transaction'].create(
self._get_tx_vals(invoice, token),
)
valid_states = ['authorized', 'done']
try:
result = transaction.s2s_do_transaction()
if not result or transaction.state not in valid_states:
_logger.debug(
'Payment transaction failed (%s)',
transaction.state_message,
)
else:
# Success
return True
except Exception:
_logger.exception(
'Payment transaction (%s) generated a gateway error.',
transaction.id,
)
transaction.state = 'error'
invoice.write({
'auto_pay_attempts': invoice.auto_pay_attempts + 1,
'auto_pay_failed': fields.Datetime.now(),
})
if invoice.auto_pay_attempts >= self.auto_pay_retries:
template = self.pay_fail_mail_template_id
self.write({
'is_auto_pay': False,
'payment_token_id': False,
})
if token == self.partner_id.payment_token_id:
self.partner_id.payment_token_id = False
else:
template = self.pay_retry_mail_template_id
if template:
template.send_mail(invoice.id)
return
@api.multi
def _get_tx_vals(self, invoice, token):
""" Return values for create of payment.transaction for invoice."""
amount_due = invoice.residual
partner = token.partner_id
reference = self.env['payment.transaction'].get_next_reference(
invoice.number,
)
return {
'reference': '%s' % reference,
'acquirer_id': token.acquirer_id.id,
'payment_token_id': token.id,
'amount': amount_due,
'state': 'draft',
'currency_id': invoice.currency_id.id,
'partner_id': partner.id,
'partner_country_id': partner.country_id.id,
'partner_city': partner.city,
'partner_zip': partner.zip,
'partner_email': partner.email,
}
@api.multi
def _send_invoice_message(self, invoice):
""" Send the appropriate emails for the invoices if needed. """
if invoice.sent:
return
if not self.invoice_mail_template_id:
return
_logger.info('Sending invoice %s, %s (template %s)',
invoice, invoice.number, self.invoice_mail_template_id)
mail_id = self.invoice_mail_template_id.send_mail(invoice.id)
invoice.with_context(mail_post_autofollow=True)
invoice.sent = True
invoice.message_post(body=_("Invoice sent"))
return self.env['mail.mail'].browse(mail_id)