changeset 1f9dc29c1458 in modules/account_payment:default
details:
https://hg.tryton.org/modules/account_payment?cmd=changeset&node=1f9dc29c1458
description:
Add wizard to create direct debit for configured parties
issue10772
review379071002
diffstat:
CHANGELOG | 1 +
__init__.py | 3 +
account.py | 74 ++++++++++++++++
account.xml | 21 ++++
doc/index.rst | 3 +
party.py | 97 +++++++++++++++++++++-
party.xml | 40 +++++++++
tests/scenario_account_payment_direct_debit.rst | 106 ++++++++++++++++++++++++
tests/test_account_payment.py | 5 +
view/move_line_create_direct_debit_form.xml | 10 ++
view/party_form.xml | 9 +-
view/party_reception_direct_debit_form.xml | 14 +++
view/party_reception_direct_debit_list.xml | 9 ++
13 files changed, 386 insertions(+), 6 deletions(-)
diffs (531 lines):
diff -r 69b733325048 -r 1f9dc29c1458 CHANGELOG
--- a/CHANGELOG Mon Oct 11 18:22:19 2021 +0200
+++ b/CHANGELOG Mon Oct 11 18:35:27 2021 +0200
@@ -1,3 +1,4 @@
+* Add wizard to create direct debit for configured parties
* Add succeed button for payment groups
Version 6.0.0 - 2021-05-03
diff -r 69b733325048 -r 1f9dc29c1458 __init__.py
--- a/__init__.py Mon Oct 11 18:22:19 2021 +0200
+++ b/__init__.py Mon Oct 11 18:35:27 2021 +0200
@@ -13,18 +13,21 @@
payment.Payment,
account.MoveLine,
payment.ProcessPaymentStart,
+ account.CreateDirectDebitStart,
account.PayLineStart,
account.PayLineAskJournal,
account.Configuration,
account.ConfigurationPaymentGroupSequence,
party.Party,
party.PartyPaymentDirectDebit,
+ party.PartyReceptionDirectDebit,
module='account_payment', type_='model')
Pool.register(
account.Invoice,
module='account_payment', type_='model', depends=['account_invoice'])
Pool.register(
payment.ProcessPayment,
+ account.CreateDirectDebit,
account.PayLine,
party.Replace,
party.Erase,
diff -r 69b733325048 -r 1f9dc29c1458 account.py
--- a/account.py Mon Oct 11 18:22:19 2021 +0200
+++ b/account.py Mon Oct 11 18:35:27 2021 +0200
@@ -185,6 +185,80 @@
'payment_blocked': False,
})
+ @classmethod
+ def _pay_direct_debit_domain(cls, date):
+ return [
+ ['OR',
+ ('account.type.receivable', '=', True),
+ ('account.type.payable', '=', True),
+ ],
+ ('party', '!=', None),
+ ('reconciliation', '=', None),
+ ('payment_amount', '!=', 0),
+ ('move_state', '=', 'posted'),
+ ['OR',
+ ('debit', '>', 0),
+ ('credit', '<', 0),
+ ],
+ ('maturity_date', '<=', date),
+ ]
+
+ @classmethod
+ def pay_direct_debit(cls, date=None):
+ pool = Pool()
+ Date = pool.get('ir.date')
+ Payment = pool.get('account.payment')
+ Reception = pool.get('party.party.reception_direct_debit')
+ if date is None:
+ date = Date.today()
+ with Transaction().set_context(_check_access=True):
+ lines = cls.search(cls._pay_direct_debit_domain(date))
+
+ payments = []
+ for line in lines:
+ if not line.payment_amount:
+ # SQLite fails to search for payment_amount != 0
+ continue
+ pattern = Reception.get_pattern(line)
+ for reception in line.party.reception_direct_debits:
+ if reception.match(pattern):
+ payments.extend(reception.get_payments(line))
+ Payment.save(payments)
+ return payments
+
+
+class CreateDirectDebit(Wizard):
+ "Create Direct Debit"
+ __name__ = 'account.move.line.create_direct_debit'
+
+ start = StateView('account.move.line.create_direct_debit.start',
+ 'account_payment.move_line_create_direct_debit_view_form', [
+ Button("Cancel", 'end', 'tryton-cancel'),
+ Button("Create", 'create_', 'tryton-ok', default=True),
+ ])
+ create_ = StateAction('account_payment.act_payment_form')
+
+ def do_create_(self, action):
+ pool = Pool()
+ Line = pool.get('account.move.line')
+ payments = Line.pay_direct_debit(date=self.start.date)
+ return action, {
+ 'res_id': [p.id for p in payments],
+ }
+
+
+class CreateDirectDebitStart(ModelView):
+ "Create Direct Debit"
+ __name__ = 'account.move.line.create_direct_debit.start'
+
+ date = fields.Date(
+ "Date", required=True,
+ help="Create direct debit for lines due up to this date.")
+
+ @classmethod
+ def default_date(cls):
+ return Pool().get('ir.date').today()
+
class PayLineStart(ModelView):
"Pay Line"
diff -r 69b733325048 -r 1f9dc29c1458 account.xml
--- a/account.xml Mon Oct 11 18:22:19 2021 +0200
+++ b/account.xml Mon Oct 11 18:35:27 2021 +0200
@@ -73,6 +73,27 @@
<field name="perm_delete" eval="False"/>
</record>
+ <record model="ir.action.wizard" id="wizard_create_direct_debit">
+ <field name="name">Create Direct Debit</field>
+ <field
name="wiz_name">account.move.line.create_direct_debit</field>
+ </record>
+ <menuitem
+ parent="menu_payments"
+ action="wizard_create_direct_debit"
+ sequence="40"
+ id="menu_create_direct_debit"/>
+
+ <record model="ir.action-res.group"
id="wizard_create_direct_debit-group_payment">
+ <field name="action" ref="wizard_create_direct_debit"/>
+ <field name="group" ref="group_payment"/>
+ </record>
+
+ <record model="ir.ui.view"
id="move_line_create_direct_debit_view_form">
+ <field
name="model">account.move.line.create_direct_debit.start</field>
+ <field name="type">form</field>
+ <field name="name">move_line_create_direct_debit_form</field>
+ </record>
+
<record model="ir.ui.view" id="move_line_pay_start_view_form">
<field name="model">account.move.line.pay.start</field>
<field name="type">form</field>
diff -r 69b733325048 -r 1f9dc29c1458 doc/index.rst
--- a/doc/index.rst Mon Oct 11 18:22:19 2021 +0200
+++ b/doc/index.rst Mon Oct 11 18:35:27 2021 +0200
@@ -24,6 +24,9 @@
using the `Pay Lines` action. The amount is computed from the debit/credit
deducing existing payments for this line.
+Direct debit payments can be created from the wizard `Create Direct Debit` for
+all the parties that are configured with at least one *Direct Debits* record.
+
The payment can be in one of this states:
* Draft
diff -r 69b733325048 -r 1f9dc29c1458 party.py
--- a/party.py Mon Oct 11 18:22:19 2021 +0200
+++ b/party.py Mon Oct 11 18:35:27 2021 +0200
@@ -2,7 +2,8 @@
# this repository contains the full copyright notices and license terms.
from trytond.i18n import gettext
from trytond.pool import PoolMeta, Pool
-from trytond.model import ModelSQL, fields
+from trytond.model import (
+ ModelSQL, ModelView, sequence_ordered, MatchMixin, fields)
from trytond.modules.company.model import CompanyValueMixin
from trytond.modules.party.exceptions import EraseError
from trytond.pyson import Eval
@@ -17,11 +18,23 @@
payment_direct_debit = fields.MultiValue(payment_direct_debit)
payment_direct_debits = fields.One2Many(
'party.party.payment_direct_debit', 'party', "Direct Debits")
+ reception_direct_debits = fields.One2Many(
+ 'party.party.reception_direct_debit', 'party', "Direct Debits",
+ help="Fill to debit automatically the customer.")
@classmethod
def default_payment_direct_debit(cls, **pattern):
return False
+ @classmethod
+ def copy(cls, parties, default=None):
+ if default is None:
+ default = {}
+ else:
+ default = default.copy()
+ default.setdefault('reception_direct_debits')
+ return super().copy(parties, default=default)
+
class PartyPaymentDirectDebit(ModelSQL, CompanyValueMixin):
"Party Payment Direct Debit"
@@ -35,6 +48,87 @@
payment_direct_debit = payment_direct_debit
+class PartyReceptionDirectDebit(
+ sequence_ordered(), MatchMixin, ModelView, ModelSQL):
+ "Party Reception Direct Debit"
+ __name__ = 'party.party.reception_direct_debit'
+
+ party = fields.Many2One(
+ 'party.party', "Party", ondelete='CASCADE', select=True,
+ context={
+ 'company': Eval('company', -1),
+ },
+ depends=['company'])
+ journal = fields.Many2One(
+ 'account.payment.journal', "Journal", required=True)
+ company = fields.Function(
+ fields.Many2One('company.company', "Company"),
+ 'on_change_with_company', searcher='search_company')
+ currency = fields.Function(
+ fields.Many2One('currency.currency', "Currency"),
+ 'on_change_with_currency')
+ process_method = fields.Function(
+ fields.Selection('get_process_methods', "Process Method"),
+ 'on_change_with_process_method')
+
+ @fields.depends('journal')
+ def on_change_with_company(self, name=None):
+ if self.journal:
+ return self.journal.company.id
+
+ @classmethod
+ def search_company(cls, name, clause):
+ nested = clause[0].lstrip(name)
+ return [('journal.' + name + nested,) + tuple(clause[1:])]
+
+ @fields.depends('journal')
+ def on_change_with_currency(self, name=None):
+ if self.journal:
+ return self.journal.currency.id
+
+ @classmethod
+ def get_process_methods(cls):
+ pool = Pool()
+ Journal = pool.get('account.payment.journal')
+ name = 'process_method'
+ return Journal.fields_get([name])[name]['selection']
+
+ @fields.depends('journal')
+ def on_change_with_process_method(self, name=None):
+ if self.journal:
+ return self.journal.process_method
+
+ @classmethod
+ def get_pattern(cls, line):
+ return {
+ 'company': line.company.id,
+ 'currency': line.payment_currency.id,
+ }
+
+ def get_payments(self, line):
+ pool = Pool()
+ Date = pool.get('ir.date')
+ today = Date.today()
+ for date, amount in self._compute(
+ line.maturity_date or today, line.payment_amount):
+ yield self._get_payment(line, date, amount)
+
+ def _compute(self, date, amount):
+ yield date, amount
+
+ def _get_payment(self, line, date, amount):
+ pool = Pool()
+ Payment = pool.get('account.payment')
+ return Payment(
+ company=line.company,
+ journal=self.journal,
+ kind='receivable',
+ party=line.party,
+ date=date,
+ amount=amount,
+ line=line)
+
+
class Replace(metaclass=PoolMeta):
__name__ = 'party.replace'
@@ -42,6 +136,7 @@
def fields_to_replace(cls):
return super().fields_to_replace() + [
('account.payment', 'party'),
+ ('party.party.reception_direct_debit', 'party'),
]
diff -r 69b733325048 -r 1f9dc29c1458 party.xml
--- a/party.xml Mon Oct 11 18:22:19 2021 +0200
+++ b/party.xml Mon Oct 11 18:35:27 2021 +0200
@@ -8,5 +8,45 @@
<field name="inherit" ref="party.party_view_form"/>
<field name="name">party_form</field>
</record>
+
+ <record model="ir.ui.view" id="party_reception_direct_debit_view_form">
+ <field name="model">party.party.reception_direct_debit</field>
+ <field name="type">form</field>
+ <field name="name">party_reception_direct_debit_form</field>
+ </record>
+
+ <record model="ir.ui.view" id="party_reception_direct_debit_view_list">
+ <field name="model">party.party.reception_direct_debit</field>
+ <field name="type">tree</field>
+ <field name="name">party_reception_direct_debit_list</field>
+ </record>
+
+ <record model="ir.rule.group"
id="rule_group_party_reception_direct_debit_companies">
+ <field name="name">User in companies</field>
+ <field name="model" search="[('model', '=',
'party.party.reception_direct_debit')]"/>
+ <field name="global_p" eval="True"/>
+ </record>
+ <record model="ir.rule"
id="rule_party_reception_direct_debit_companies">
+ <field name="domain"
+ eval="[('company', 'in', Eval('companies', []))]"
+ pyson="1"/>
+ <field name="rule_group"
ref="rule_group_party_reception_direct_debit_companies"/>
+ </record>
+
+ <record model="ir.model.access"
id="access_party_reception_direct_debit">
+ <field name="model" search="[('model', '=',
'party.party.reception_direct_debit')]"/>
+ <field name="perm_read" eval="False"/>
+ <field name="perm_write" eval="False"/>
+ <field name="perm_create" eval="False"/>
+ <field name="perm_delete" eval="False"/>
+ </record>
+ <record model="ir.model.access"
id="access_party_reception_direct_debit_payment">
+ <field name="model" search="[('model', '=',
'party.party.reception_direct_debit')]"/>
+ <field name="group" ref="group_payment"/>
+ <field name="perm_read" eval="True"/>
+ <field name="perm_write" eval="True"/>
+ <field name="perm_create" eval="True"/>
+ <field name="perm_delete" eval="True"/>
+ </record>
</data>
</tryton>
diff -r 69b733325048 -r 1f9dc29c1458
tests/scenario_account_payment_direct_debit.rst
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/scenario_account_payment_direct_debit.rst Mon Oct 11 18:35:27
2021 +0200
@@ -0,0 +1,106 @@
+=============================
+Payment Direct Debit Scenario
+=============================
+
+Imports::
+
+ >>> import datetime as dt
+ >>> from decimal import Decimal
+
+ >>> from proteus import Model, Wizard
+ >>> from trytond.tests.tools import activate_modules
+ >>> from trytond.modules.company.tests.tools import (
+ ... create_company, get_company)
+ >>> from trytond.modules.account.tests.tools import (
+ ... create_fiscalyear, create_chart, get_accounts)
+
+ >>> today = dt.date.today()
+ >>> tomorrow = today + dt.timedelta(days=1)
+
+Activate modules::
+
+ >>> config = activate_modules('account_payment')
+
+ >>> Journal = Model.get('account.journal')
+ >>> Move = Model.get('account.move')
+ >>> Party = Model.get('party.party')
+ >>> Payment = Model.get('account.payment')
+ >>> PaymentJournal = Model.get('account.payment.journal')
+
+Create company::
+
+ >>> _ = create_company()
+ >>> company = get_company()
+
+Create fiscal year::
+
+ >>> fiscalyear = create_fiscalyear(company)
+ >>> fiscalyear.click('create_period')
+
+Create chart of accounts::
+
+ >>> _ = create_chart(company)
+ >>> accounts = get_accounts(company)
+
+ >>> revnue_journal, = Journal.find([('code', '=', 'REV')])
+
+Create payment journal::
+
+ >>> payment_journal = PaymentJournal(
+ ... name="Manual", process_method='manual')
+ >>> payment_journal.save()
+
+Create parties::
+
+ >>> customer1 = Party(name="Customer 1")
+ >>> _ = customer1.reception_direct_debits.new(journal=payment_journal)
+ >>> customer1.save()
+ >>> customer2 = Party(name="Customer 2")
+ >>> customer2.save()
+
+Create receivable moves::
+
+ >>> move = Move()
+ >>> move.journal = revnue_journal
+ >>> line = move.lines.new(
+ ... account=accounts['receivable'], party=customer1,
+ ... debit=Decimal('100.00'), maturity_date=tomorrow)
+ >>> line = move.lines.new(
+ ... account=accounts['revenue'],
+ ... credit=Decimal('100.00'))
+ >>> move.click('post')
+
+ >>> move = Move()
+ >>> move.journal = revnue_journal
+ >>> line = move.lines.new(
+ ... account=accounts['receivable'], party=customer2,
+ ... debit=Decimal('200.00'), maturity_date=tomorrow)
+ >>> line = move.lines.new(
+ ... account=accounts['revenue'],
+ ... credit=Decimal('200.00'))
+ >>> move.click('post')
+
+Create direct debit::
+
+ >>> create_direct_debit = Wizard('account.move.line.create_direct_debit')
+ >>> create_direct_debit.form.date = tomorrow + dt.timedelta(days=1)
+ >>> create_direct_debit.execute('create_')
+
+ >>> payment, = Payment.find([])
+ >>> payment.amount
+ Decimal('100.00')
+ >>> payment.party == customer1
+ True
+ >>> payment.date == tomorrow
+ True
+ >>> payment.journal == payment_journal
+ True
+
+Re-run create direct debit does nothing::
+
+ >>> create_direct_debit = Wizard('account.move.line.create_direct_debit')
+ >>> create_direct_debit.form.date = tomorrow + dt.timedelta(days=1)
+ >>> create_direct_debit.execute('create_')
+
+ >>> Payment.find([]) == [payment]
+ True
diff -r 69b733325048 -r 1f9dc29c1458 tests/test_account_payment.py
--- a/tests/test_account_payment.py Mon Oct 11 18:22:19 2021 +0200
+++ b/tests/test_account_payment.py Mon Oct 11 18:35:27 2021 +0200
@@ -31,6 +31,11 @@
checker=doctest_checker,
optionflags=doctest.REPORT_ONLY_FIRST_FAILURE))
suite.addTests(doctest.DocFileSuite(
+ 'scenario_account_payment_direct_debit.rst',
+ tearDown=doctest_teardown, encoding='utf-8',
+ checker=doctest_checker,
+ optionflags=doctest.REPORT_ONLY_FIRST_FAILURE))
+ suite.addTests(doctest.DocFileSuite(
'scenario_account_payment_planning.rst',
tearDown=doctest_teardown, encoding='utf-8',
checker=doctest_checker,
diff -r 69b733325048 -r 1f9dc29c1458 view/move_line_create_direct_debit_form.xml
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/view/move_line_create_direct_debit_form.xml Mon Oct 11 18:35:27
2021 +0200
@@ -0,0 +1,10 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<form col="2">
+ <image name="tryton-question" xexpand="0" xfill="0"/>
+ <group col="2" xexpand="1" id="create_date">
+ <label string="Create Direct Debit for date" id="create"/>
+ <field name="date"/>
+ </group>
+</form>
diff -r 69b733325048 -r 1f9dc29c1458 view/party_form.xml
--- a/view/party_form.xml Mon Oct 11 18:22:19 2021 +0200
+++ b/view/party_form.xml Mon Oct 11 18:35:27 2021 +0200
@@ -2,10 +2,9 @@
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<data>
- <xpath expr="//page[@id='accounting']/field[@name='account_payable']"
- position="after">
- <label id="empty" colspan="2"/>
- <label name="payment_direct_debit"/>
- <field name="payment_direct_debit"/>
+ <xpath expr="//page[@id='accounting']/field[@name='account_payable']"
position="after">
+ <field name="reception_direct_debits" colspan="2" height="200"
yexpand="0"/>
+ <label name="payment_direct_debit" yalign="0"/>
+ <field name="payment_direct_debit" yalign="0"/>
</xpath>
</data>
diff -r 69b733325048 -r 1f9dc29c1458 view/party_reception_direct_debit_form.xml
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/view/party_reception_direct_debit_form.xml Mon Oct 11 18:35:27
2021 +0200
@@ -0,0 +1,14 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<form>
+ <label name="party"/>
+ <field name="party"/>
+ <label name="sequence"/>
+ <field name="sequence"/>
+
+ <label name="journal"/>
+ <field name="journal"/>
+ <label name="currency"/>
+ <field name="currency"/>
+</form>
diff -r 69b733325048 -r 1f9dc29c1458 view/party_reception_direct_debit_list.xml
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/view/party_reception_direct_debit_list.xml Mon Oct 11 18:35:27
2021 +0200
@@ -0,0 +1,9 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<tree sequence="sequence">
+ <field name="party" expand="2"/>
+ <field name="company" expand="1"/>
+ <field name="journal" expand="1"/>
+ <field name="currency"/>
+</tree>