Browse Source

Merge pull request #67 from LasLabs/release/10.0/LABS-411-contract_auto

[10.0] [ADD] contract_auto: Automatic Payment For Contracts
pull/104/head
Dave Lasley 7 years ago
committed by GitHub
parent
commit
d8ad62bc02
  1. 14
      .travis.yml
  2. 89
      contract_payment_auto/README.rst
  3. 5
      contract_payment_auto/__init__.py
  4. 27
      contract_payment_auto/__manifest__.py
  5. 19
      contract_payment_auto/data/ir_cron_data.xml
  6. 98
      contract_payment_auto/data/mail_template_data.xml
  7. 8
      contract_payment_auto/models/__init__.py
  8. 174
      contract_payment_auto/models/account_analytic_account.py
  9. 108
      contract_payment_auto/models/account_analytic_contract.py
  10. 12
      contract_payment_auto/models/account_invoice.py
  11. 18
      contract_payment_auto/models/res_partner.py
  12. 6
      contract_payment_auto/tests/__init__.py
  13. 303
      contract_payment_auto/tests/test_account_analytic_account.py
  14. 51
      contract_payment_auto/tests/test_account_analytic_contract.py
  15. 44
      contract_payment_auto/views/account_analytic_account_view.xml
  16. 36
      contract_payment_auto/views/account_analytic_contract_view.xml
  17. 22
      contract_payment_auto/views/res_partner_view.xml

14
.travis.yml

@ -7,26 +7,26 @@ python:
addons:
apt:
# only add the two lines below if you need wkhtmltopdf for your tests
# sources:
# - pov-wkhtmltopdf
sources:
- pov-wkhtmltopdf
packages:
- expect-dev # provides unbuffer utility
- python-lxml # because pip installation is slow
- python-simplejson
- python-serial
- python-yaml
# - wkhtmltopdf # only add if needed and check the before_install section below
- wkhtmltopdf
# set up an X server to run wkhtmltopdf.
#before_install:
# - "export DISPLAY=:910.0"
# - "sh -e /etc/init.d/xvfb start"
before_install:
- "export DISPLAY=:910.0"
- "sh -e /etc/init.d/xvfb start"
env:
global:
- VERSION="10.0" TESTS="0" LINT_CHECK="0" TRANSIFEX="0"
- TRANSIFEX_USER='transbot@odoo-community.org'
- WKHTMLTOPDF_VERSION="0.12.4"
- secure: ArnbVaF5+ry6zVysZ7HA9xcnQrodo8FsXXcon9yINTYRfDC9QEr+/mTLAPRl+lVLdtWV2GuGuX0vPYzBxFWpY3LR0BpKqXzx0G51s94zR2WWEmizYFzFhpAuIxoU4CYNckHFSUaPAQJhwB/pYx9+H/W6bMjG/VnZBq+AmBJ2Kh0=
matrix:

89
contract_payment_auto/README.rst

@ -0,0 +1,89 @@
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
=====================
Contract Auto Payment
=====================
This module allows for the configuration of automatic payments on invoices that are created by a contract.
Usage
=====
Enable Automatic Payment
------------------------
* Navigate to a customer contract
* Check the `Auto Pay?` box to enable automatic payment
* Configure the options as desired
* Set the `Payment Token` to the payment token that should be used for automatic payment
Automatic Payment Settings
--------------------------
The following settings are available at both the contract and contract template level:
| Name | Description |
|------|-------------|
| Invoice Message | Message template that is used to send invoices to customers upon creation. |
| Payment Retry Message | Message template that is used to alert a customer that their automatic payment failed for some reason and will be retried. |
| Payment Fail Message | Message template that is used to alert a customer that their automatic payment failed and will no longer be retried. |
| Auto Pay Retries | Amount of times to attempt an automatic payment before discontinuing and removing the payment token from the contract/account payment method. |
| Auto Pay Retry Hours | Amount of hours that should lapse until retrying failed payments. |
Payment Token
-------------
A valid payment token is required to use this module. These tokens are typically created during the `website_sale` checkout process, but they can also be created manually at the acquirer.
A payment token can be defined in one of two areas:
* Contract - Defining a payment token in the contract will allow for the use of this token for automatic payments on this contract only.
* Partner - Defining a payment token in the partner will allow for the use of this token for automatic payments on all contracts for this partner that do not have a payment token defined.
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
:alt: Try me on Runbot
:target: https://runbot.odoo-community.org/runbot/110/10.0
Known issues / Roadmap
======================
* None
Bug Tracker
===========
Bugs are tracked on `GitHub Issues
<https://github.com/OCA/contract/issues>`_. In case of trouble, please
check there if your issue has already been reported. If you spotted it first,
help us smash it by providing detailed and welcomed feedback.
Credits
=======
Images
------
* Odoo Community Association: `Icon <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_.
Contributors
------------
* Dave Lasley <dave@laslabs.com>
Maintainer
----------
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
This module is maintained by the OCA.
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
To contribute to this module, please visit https://odoo-community.org.

5
contract_payment_auto/__init__.py

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2017 LasLabs Inc.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from . import models

27
contract_payment_auto/__manifest__.py

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# Copyright 2017 LasLabs Inc.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
{
"name": "Contract - Auto Payment",
"summary": "Adds automatic payments to contracts.",
"version": "10.0.1.0.0",
"category": "Contract Management",
"license": "AGPL-3",
"author": "LasLabs, "
"Odoo Community Association (OCA)",
"website": "https://laslabs.com",
"depends": [
"contract",
"payment",
],
"data": [
"data/mail_template_data.xml",
"data/ir_cron_data.xml",
"views/account_analytic_account_view.xml",
"views/account_analytic_contract_view.xml",
"views/res_partner_view.xml",
],
"installable": True,
"application": False,
}

19
contract_payment_auto/data/ir_cron_data.xml

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2017 LasLabs Inc.
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
-->
<odoo noupdate="1">
<record id="ir_cron_auto_pay" model="ir.cron">
<field name="name">Contract Automatic Payments</field>
<field name="interval_type">hours</field>
<field name="interval_number">1</field>
<field name="model">account.analytic.account</field>
<field name="function">cron_retry_auto_pay</field>
<field name="args">()</field>
</record>
</odoo>

98
contract_payment_auto/data/mail_template_data.xml

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2017 LasLabs Inc.
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
-->
<odoo noupdate="1">
<record id="mail_template_auto_pay_retry" model="mail.template">
<field name="name">Invoice - AutoPay To Retry</field>
<field name="email_from">${(object.user_id.email and '%s &lt;%s&gt;' % (object.user_id.name, object.user_id.email) or '')|safe}</field>
<field name="subject">Automatic Payment Failure (Ref ${object.number or 'n/a'})</field>
<field name="partner_to">${object.partner_id.id}</field>
<field name="model_id" ref="account.model_account_invoice"/>
<field name="auto_delete" eval="True"/>
<field name="report_template" ref="account.account_invoices"/>
<field name="report_name">Invoice_${(object.number or '').replace('/','_')}_${object.state == 'draft' and 'draft' or ''}</field>
<field name="lang">${object.partner_id.lang}</field>
<field name="body_html"><![CDATA[
<p>
Hello ${object.partner_id.name}
% set access_action = object.get_access_action()
% set access_url = access_action['type'] == 'ir.actions.act_url' and access_action['url'] or '/report/pdf/account.report_invoice/' + str(object.id)
% if object.partner_id.parent_id:
(<i>${object.partner_id.parent_id.name}</i>)
% endif
,
</p>
<p>
The automatic payment for your invoice
<a href="${access_url}">
<strong>
${object.number}
</strong>
% if object.origin:
(with reference: ${object.origin} )
% endif
</a>
failed.
</p>
<p>
Please verify that your payment information is correct, and that funds are
available in the account.
</p>
]]>
</field>
</record>
<record id="mail_template_auto_pay_fail" model="mail.template">
<field name="name">Invoice - AutoPay Failed</field>
<field name="email_from">${(object.user_id.email and '%s &lt;%s&gt;' % (object.user_id.name, object.user_id.email) or '')|safe}</field>
<field name="subject">Automatic Payment Failure (Ref ${object.number or 'n/a'})</field>
<field name="partner_to">${object.partner_id.id}</field>
<field name="model_id" ref="account.model_account_invoice"/>
<field name="auto_delete" eval="True"/>
<field name="report_template" ref="account.account_invoices"/>
<field name="report_name">Invoice_${(object.number or '').replace('/','_')}_${object.state == 'draft' and 'draft' or ''}</field>
<field name="lang">${object.partner_id.lang}</field>
<field name="body_html"><![CDATA[
<p>
Hello ${object.partner_id.name}
% set access_action = object.get_access_action()
% set access_url = access_action['type'] == 'ir.actions.act_url' and access_action['url'] or '/report/pdf/account.report_invoice/' + str(object.id)
% if object.partner_id.parent_id:
(<i>${object.partner_id.parent_id.name}</i>)
% endif
,
</p>
<p>
The automatic payment for your invoice
<a href="${access_url}">
<strong>
${object.number}
</strong>
% if object.origin:
(with reference: ${object.origin} )
% endif
</a>
failed.
</p>
<p>
Please verify that your payment information is correct, and that funds are
available in the account.
</p>
]]>
</field>
</record>
</odoo>

8
contract_payment_auto/models/__init__.py

@ -0,0 +1,8 @@
# -*- coding: utf-8 -*-
# Copyright 2017 LasLabs Inc.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from . import account_analytic_account
from . import account_analytic_contract
from . import account_invoice
from . import res_partner

174
contract_payment_auto/models/account_analytic_account.py

@ -0,0 +1,174 @@
# -*- 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),
)
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):
""" Return values for create of payment.transaction for invoice."""
amount_due = invoice.residual
token = self.payment_token_id
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)

108
contract_payment_auto/models/account_analytic_contract.py

@ -0,0 +1,108 @@
# -*- coding: utf-8 -*-
# Copyright 2017 LasLabs Inc.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import api, fields, models
def _context_mail_templates(env):
return env['account.analytic.contract']._context_mail_templates()
class AccountAnalyticContract(models.Model):
_inherit = 'account.analytic.contract'
invoice_mail_template_id = fields.Many2one(
string='Invoice Message',
comodel_name='mail.template',
default=lambda s: s._default_invoice_mail_template_id(),
domain="[('model', '=', 'account.invoice')]",
context=_context_mail_templates,
help="During the automatic payment process, an invoice will be "
"created and validated. If this template is selected, it will "
"automatically be sent to the customer during this process "
"using the defined template.",
)
pay_retry_mail_template_id = fields.Many2one(
string='Payment Retry Message',
comodel_name='mail.template',
default=lambda s: s._default_pay_retry_mail_template_id(),
domain="[('model', '=', 'account.invoice')]",
context=_context_mail_templates,
help="If automatic payment fails for some reason, but will be "
"re-attempted later, this message will be sent to the billed "
"partner.",
)
pay_fail_mail_template_id = fields.Many2one(
string='Payment Failed Message',
comodel_name='mail.template',
default=lambda s: s._default_pay_fail_mail_template_id(),
domain="[('model', '=', 'account.invoice')]",
context=_context_mail_templates,
help="If automatic payment fails for some reason, this message "
"will be sent to the billed partner.",
)
is_auto_pay = fields.Boolean(
string='Auto Pay?',
default=True,
help="Check this to enable automatic payment for invoices that are "
"created for this contract.",
)
auto_pay_retries = fields.Integer(
default=lambda s: s._default_auto_pay_retries(),
help="Amount times to retry failed/declined automatic payment "
"before giving up."
)
auto_pay_retry_hours = fields.Integer(
default=lambda s: s._default_auto_pay_retry_hours(),
help="Amount of hours that should lapse until a failed automatic "
"is retried.",
)
@api.model
def _default_invoice_mail_template_id(self):
return self.env.ref(
'account.email_template_edi_invoice',
raise_if_not_found=False,
)
@api.model
def _default_pay_retry_mail_template_id(self):
return self.env.ref(
'contract_payment_auto.mail_template_auto_pay_retry',
raise_if_not_found=False,
)
@api.model
def _default_pay_fail_mail_template_id(self):
return self.env.ref(
'contract_payment_auto.mail_template_auto_pay_fail',
raise_if_not_found=False,
)
@api.model
def _default_auto_pay_retries(self):
return 3
@api.model
def _default_auto_pay_retry_hours(self):
return 24
@api.model
def _context_mail_templates(self):
""" Return a context for use in mail templates. """
default_model = self.env.ref('account.model_account_invoice')
report_template = self.env.ref('account.account_invoices')
return {
'default_model_id': default_model.id,
'default_email_from': "${(object.user_id.email and '%s <%s>' % "
"(object.user_id.name, object.user_id.email)"
" or '')|safe}",
'default_partner_to': '${object.partner_id.id}',
'default_lang': '${object.partner_id.lang}',
'default_auto_delete': True,
'report_template': report_template.id,
'report_name': "Invoice_${(object.number or '').replace('/','_')}"
"_${object.state == 'draft' and 'draft' or ''}",
}

12
contract_payment_auto/models/account_invoice.py

@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
# Copyright 2017 LasLabs Inc.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import fields, models
class AccountInvoice(models.Model):
_inherit = 'account.invoice'
auto_pay_attempts = fields.Integer()
auto_pay_failed = fields.Datetime()

18
contract_payment_auto/models/res_partner.py

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
# Copyright 2017 LasLabs Inc.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import fields, models
class ResPartner(models.Model):
_inherit = 'res.partner'
payment_token_id = fields.Many2one(
string='Payment Token',
comodel_name='payment.token',
domain="[('id', 'in', payment_token_ids)]",
help='This is the payment token that will be used to automatically '
'reconcile debts for this partner, if there is not one already '
'set on the analytic account.',
)

6
contract_payment_auto/tests/__init__.py

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright 2017 LasLabs Inc.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from . import test_account_analytic_account
from . import test_account_analytic_contract

303
contract_payment_auto/tests/test_account_analytic_account.py

@ -0,0 +1,303 @@
# -*- coding: utf-8 -*-
# Copyright 2017 LasLabs Inc.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import mock
from contextlib import contextmanager
from odoo import fields
from odoo.tools import mute_logger
from odoo.tests.common import TransactionCase
from ..models import account_analytic_account
class TestAccountAnalyticAccount(TransactionCase):
def setUp(self):
super(TestAccountAnalyticAccount, self).setUp()
self.Model = self.env['account.analytic.account']
self.partner = self.env.ref('base.res_partner_2')
self.product = self.env.ref('product.product_product_2')
self.product.taxes_id += self.env['account.tax'].search(
[('type_tax_use', '=', 'sale')], limit=1)
self.product.description_sale = 'Test description sale'
self.template_vals = {
'recurring_rule_type': 'yearly',
'recurring_interval': 12345,
'name': 'Test Contract Template',
'is_auto_pay': True,
}
self.template = self.env['account.analytic.contract'].create(
self.template_vals,
)
self.acquirer = self.env['payment.acquirer'].create({
'name': 'Test Acquirer',
'provider': 'manual',
'view_template_id': self.env['ir.ui.view'].search([], limit=1).id,
})
self.payment_token = self.env['payment.token'].create({
'name': 'Test Token',
'partner_id': self.partner.id,
'active': True,
'acquirer_id': self.acquirer.id,
'acquirer_ref': 'Test',
})
self.contract = self.Model.create({
'name': 'Test Contract',
'partner_id': self.partner.id,
'pricelist_id': self.partner.property_product_pricelist.id,
'recurring_invoices': True,
'date_start': '2016-02-15',
'recurring_next_date': fields.Datetime.now(),
'payment_token_id': self.payment_token.id,
})
self.contract_line = self.env['account.analytic.invoice.line'].create({
'analytic_account_id': self.contract.id,
'product_id': self.product.id,
'name': 'Services from #START# to #END#',
'quantity': 1,
'uom_id': self.product.uom_id.id,
'price_unit': 100,
'discount': 50,
})
def _validate_invoice(self, invoice):
self.assertEqual(len(invoice), 1)
self.assertEqual(invoice._name, 'account.invoice')
def _create_invoice(self, open=False, sent=False):
self.contract.is_auto_pay = False
invoice = self.contract._create_invoice()
if open or sent:
invoice.action_invoice_open()
if sent:
invoice.sent = True
self.contract.is_auto_pay = True
return invoice
@contextmanager
def _mock_transaction(self, state='authorized', s2s_side_effect=None):
Transactions = self.contract.env['payment.transaction']
TransactionsCreate = Transactions.create
if not callable(s2s_side_effect):
s2s_side_effect = [s2s_side_effect]
s2s = mock.MagicMock()
s2s.side_effect = s2s_side_effect
def create(vals):
record = TransactionsCreate(vals)
record.state = state
return record
model_create = mock.MagicMock()
model_create.side_effect = create
Transactions._patch_method('create', model_create)
Transactions._patch_method('s2s_do_transaction', s2s)
try:
yield
finally:
Transactions._revert_method('create')
Transactions._revert_method('s2s_do_transaction')
def test_onchange_partner_id_payment_token(self):
""" It should clear the payment token. """
self.assertTrue(self.contract.payment_token_id)
self.contract._onchange_partner_id_payment_token()
self.assertFalse(self.contract.payment_token_id)
def test_create_invoice_no_autopay(self):
""" It should return the new invoice without calling autopay. """
self.contract.is_auto_pay = False
with mock.patch.object(self.contract, '_do_auto_pay') as method:
invoice = self.contract._create_invoice()
self._validate_invoice(invoice)
method.assert_not_called()
def test_create_invoice_autopay(self):
""" It should return the new invoice after calling autopay. """
with mock.patch.object(self.contract, '_do_auto_pay') as method:
invoice = self.contract._create_invoice()
self._validate_invoice(invoice)
method.assert_called_once_with(invoice)
def test_do_auto_pay_ensure_one(self):
""" It should ensure_one on self. """
with self.assertRaises(ValueError):
self.env['account.analytic.account']._do_auto_pay(
self._create_invoice(),
)
def test_do_auto_pay_invoice_ensure_one(self):
""" It should ensure_one on the invoice. """
with self.assertRaises(ValueError):
self.contract._do_auto_pay(
self.env['account.invoice'],
)
def test_do_auto_pay_open_invoice(self):
""" It should open the invoice. """
invoice = self._create_invoice()
self.contract._do_auto_pay(invoice)
self.assertEqual(invoice.state, 'open')
def test_do_auto_pay_sends_message(self):
""" It should call the send message method with the invoice. """
with mock.patch.object(self.contract, '_send_invoice_message') as m:
invoice = self._create_invoice()
self.contract._do_auto_pay(invoice)
m.assert_called_once_with(invoice)
def test_do_auto_pay_does_pay(self):
""" It should try to pay the invoice. """
with mock.patch.object(self.contract, '_pay_invoice') as m:
invoice = self._create_invoice()
self.contract._do_auto_pay(invoice)
m.assert_called_once_with(invoice)
def test_pay_invoice_not_open(self):
""" It should return None if the invoice isn't open. """
invoice = self._create_invoice()
res = self.contract._pay_invoice(invoice)
self.assertIs(res, None)
def test_pay_invoice_no_residual(self):
""" It should return None if no residual on the invoice. """
invoice = self._create_invoice()
invoice.state = 'open'
res = self.contract._pay_invoice(invoice)
self.assertIs(res, None)
def test_pay_invoice_no_token(self):
""" It should return None if no payment token. """
self.contract.payment_token_id = False
invoice = self._create_invoice(True)
res = self.contract._pay_invoice(invoice)
self.assertIs(res, None)
def test_pay_invoice_success(self):
""" It should return True on success. """
with self._mock_transaction(s2s_side_effect=True):
invoice = self._create_invoice(True)
res = self.contract._pay_invoice(invoice)
self.assertTrue(res)
@mute_logger(account_analytic_account.__name__)
def test_pay_invoice_exception(self):
""" It should catch exceptions. """
with self._mock_transaction(s2s_side_effect=Exception):
invoice = self._create_invoice(True)
res = self.contract._pay_invoice(invoice)
self.assertIs(res, None)
def test_pay_invoice_invalid_state(self):
""" It should return None on invalid state. """
with self._mock_transaction(s2s_side_effect=True):
invoice = self._create_invoice(True)
invoice.state = 'draft'
res = self.contract._pay_invoice(invoice)
self.assertIs(res, None)
@mute_logger(account_analytic_account.__name__)
def test_pay_invoice_increments_retries(self):
""" It should increment invoice retries on failure. """
with self._mock_transaction(s2s_side_effect=False):
invoice = self._create_invoice(True)
self.assertFalse(invoice.auto_pay_attempts)
self.contract._pay_invoice(invoice)
self.assertTrue(invoice.auto_pay_attempts)
def test_pay_invoice_updates_fail_date(self):
""" It should update the invoice auto pay fail date on failure. """
with self._mock_transaction(s2s_side_effect=False):
invoice = self._create_invoice(True)
self.assertFalse(invoice.auto_pay_failed)
self.contract._pay_invoice(invoice)
self.assertTrue(invoice.auto_pay_failed)
def test_pay_invoice_too_many_attempts(self):
""" It should clear autopay after too many attempts. """
with self._mock_transaction(s2s_side_effect=False):
invoice = self._create_invoice(True)
invoice.auto_pay_attempts = self.contract.auto_pay_retries - 1
self.contract._pay_invoice(invoice)
self.assertFalse(self.contract.is_auto_pay)
self.assertFalse(self.contract.payment_token_id)
def test_pay_invoice_too_many_attempts_partner_token(self):
""" It should clear the partner token when attempts were on it. """
self.partner.payment_token_id = self.contract.payment_token_id
with self._mock_transaction(s2s_side_effect=False):
invoice = self._create_invoice(True)
invoice.auto_pay_attempts = self.contract.auto_pay_retries
self.contract._pay_invoice(invoice)
self.assertFalse(self.partner.payment_token_id)
def test_get_tx_vals(self):
""" It should return a dict. """
self.assertIsInstance(
self.contract._get_tx_vals(self._create_invoice()),
dict,
)
def test_send_invoice_message_sent(self):
""" It should return None if the invoice has already been sent. """
invoice = self._create_invoice(sent=True)
res = self.contract._send_invoice_message(invoice)
self.assertIs(res, None)
def test_send_invoice_message_no_template(self):
""" It should return None if the invoice isn't sent. """
invoice = self._create_invoice(True)
self.contract.invoice_mail_template_id = False
res = self.contract._send_invoice_message(invoice)
self.assertIs(res, None)
def test_send_invoice_message_sets_invoice_state(self):
""" It should set the invoice to sent. """
invoice = self._create_invoice(True)
self.assertFalse(invoice.sent)
self.contract._send_invoice_message(invoice)
self.assertTrue(invoice.sent)
def test_send_invoice_message_returns_mail(self):
""" It should create and return the message. """
invoice = self._create_invoice(True)
res = self.contract._send_invoice_message(invoice)
self.assertEqual(res._name, 'mail.mail')
def test_cron_retry_auto_pay_needed(self):
""" It should auto-pay the correct invoice if needed. """
invoice = self._create_invoice(True)
invoice.write({
'auto_pay_attempts': 1,
'auto_pay_failed': '2015-01-01 00:00:00',
})
meth = mock.MagicMock()
self.contract._patch_method('_do_auto_pay', meth)
try:
self.contract.cron_retry_auto_pay()
finally:
self.contract._revert_method('_do_auto_pay')
meth.assert_called_once_with(invoice)
def test_cron_retry_auto_pay_skip(self):
""" It should skip invoices that don't need to be paid. """
invoice = self._create_invoice(True)
invoice.write({
'auto_pay_attempts': 1,
'auto_pay_failed': fields.Datetime.now(),
})
meth = mock.MagicMock()
self.contract._patch_method('_do_auto_pay', meth)
try:
self.contract.cron_retry_auto_pay()
finally:
self.contract._revert_method('_do_auto_pay')
meth.assert_not_called()

51
contract_payment_auto/tests/test_account_analytic_contract.py

@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
# Copyright 2017 LasLabs Inc.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo.tests.common import TransactionCase
class TestAccountAnalyticContract(TransactionCase):
def setUp(self):
super(TestAccountAnalyticContract, self).setUp()
self.Model = self.env['account.analytic.contract']
def test_default_invoice_mail_template_id(self):
""" It should return a mail template associated with invoice. """
res = self.Model._default_invoice_mail_template_id()
self.assertEqual(
res.model, 'account.invoice',
)
def test_default_pay_retry_mail_template_id(self):
""" It should return a mail template associated with invoice. """
res = self.Model._default_pay_retry_mail_template_id()
self.assertEqual(
res.model, 'account.invoice',
)
def test_default_pay_fail_mail_template_id(self):
""" It should return a mail template associated with invoice. """
res = self.Model._default_pay_fail_mail_template_id()
self.assertEqual(
res.model, 'account.invoice',
)
def test_default_auto_pay_retries(self):
""" It should return an int. """
self.assertIsInstance(
self.Model._default_auto_pay_retries(), int,
)
def test_default_auto_pay_retry_hours(self):
""" It should return an int. """
self.assertIsInstance(
self.Model._default_auto_pay_retry_hours(), int,
)
def test_context_mail_templates(self):
""" It should return a dict. """
self.assertIsInstance(
self.Model._context_mail_templates(), dict,
)

44
contract_payment_auto/views/account_analytic_account_view.xml

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2017 LasLabs Inc.
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
-->
<odoo>
<record id="account_analytic_account_recurring_form_form" model="ir.ui.view">
<field name="name">Contract Auto Pay</field>
<field name="model">account.analytic.account</field>
<field name="inherit_id" ref="contract.account_analytic_account_recurring_form_form"/>
<field name="arch" type="xml">
<xpath expr="//button[@name='contract.act_recurring_invoices']" position="after">
<br attrs="{'invisible': [('recurring_invoices','!=',True)]}" />
<field name="is_auto_pay"
class="oe_inline"
attrs="{'invisible': [('recurring_invoices','!=',True)]}"
/>
<label for="is_auto_pay"
attrs="{'invisible': [('recurring_invoices','!=',True)]}"
/>
</xpath>
<xpath expr="//label[@for='recurring_invoice_line_ids']" position="before">
<group name="group_auto_pay"
attrs="{'invisible': [('is_auto_pay', '=', False)]}"
>
<group>
<field name="payment_token_id" />
<field name="invoice_mail_template_id" />
<field name="pay_retry_mail_template_id" />
</group>
<group>
<field name="pay_fail_mail_template_id" />
<field name="auto_pay_retries" />
<field name="auto_pay_retry_hours" />
</group>
</group>
</xpath>
</field>
</record>
</odoo>

36
contract_payment_auto/views/account_analytic_contract_view.xml

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2017 LasLabs Inc.
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
-->
<odoo>
<record id="account_analytic_contract_view_form" model="ir.ui.view">
<field name="name">Contract Template Auto Pay</field>
<field name="model">account.analytic.contract</field>
<field name="inherit_id" ref="contract.account_analytic_contract_view_form"/>
<field name="arch" type="xml">
<xpath expr="//group[@name='group_main_right']" position="inside">
<field name="is_auto_pay" />
</xpath>
<xpath expr="//group[@name='group_main']" position="after">
<group name="group_auto_pay"
attrs="{'invisible': [('is_auto_pay', '=', False)]}"
>
<group>
<field name="invoice_mail_template_id" />
<field name="pay_retry_mail_template_id" />
<field name="auto_pay_retry_hours" />
</group>
<group>
<field name="pay_fail_mail_template_id" />
<field name="auto_pay_retries" />
</group>
</group>
</xpath>
</field>
</record>
</odoo>

22
contract_payment_auto/views/res_partner_view.xml

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2017 LasLabs Inc.
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
-->
<odoo>
<record id="view_partner_form" model="ir.ui.view">
<field name="name">Res Partner Auto Pay</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="arch" type="xml">
<xpath expr="//group[@name='sale']" position="inside">
<field name="payment_token_ids" invisible="1" />
<field name="payment_token_id" />
</xpath>
</field>
</record>
</odoo>
Loading…
Cancel
Save