changeset 985fe4c47073 in modules/account_payment:default
details:
https://hg.tryton.org/modules/account_payment?cmd=changeset;node=985fe4c47073
description:
Add aggregated information on Payment groups
issue7890
review305581002
diffstat:
CHANGELOG | 2 +
payment.py | 121 +++++++++++++++++++++++++++++++++++++
tests/scenario_account_payment.rst | 34 ++++++++++
view/payment_group_form.xml | 9 ++
view/payment_group_list.xml | 4 +
5 files changed, 170 insertions(+), 0 deletions(-)
diffs (240 lines):
diff -r 5efe109ffa3f -r 985fe4c47073 CHANGELOG
--- a/CHANGELOG Mon Jun 01 10:08:02 2020 +0100
+++ b/CHANGELOG Mon Jun 15 09:01:47 2020 +0200
@@ -1,3 +1,5 @@
+* Add aggregated information on Payment groups
+
Version 5.6.0 - 2020-05-04
* Bug fixes (see mercurial logs for details)
* Allow scheduling payments on their line maturity dates
diff -r 5efe109ffa3f -r 985fe4c47073 payment.py
--- a/payment.py Mon Jun 01 10:08:02 2020 +0100
+++ b/payment.py Mon Jun 15 09:01:47 2020 +0200
@@ -1,12 +1,17 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
+from collections import defaultdict
+from decimal import Decimal
from itertools import groupby
+from sql.aggregate import Sum, Count
+
from trytond.i18n import gettext
from trytond.model import Workflow, ModelView, ModelSQL, fields
from trytond.model.exceptions import AccessError
from trytond.pyson import Eval, If
from trytond.rpc import RPC
+from trytond.tools import reduce_ids, grouped_slice, cursor_dict
from trytond.transaction import Transaction
from trytond.wizard import Wizard, StateView, StateAction, Button
from trytond.pool import Pool
@@ -65,6 +70,28 @@
kind = fields.Selection(KINDS, 'Kind', required=True, readonly=True)
payments = fields.One2Many('account.payment', 'group', 'Payments',
readonly=True)
+ currency_digits = fields.Function(fields.Integer("Currency Digits"),
+ 'on_change_with_currency_digits')
+ payment_count = fields.Function(fields.Integer(
+ "Payment Count",
+ help="The number of payments in the group."),
+ 'get_payment_aggregated')
+ payment_amount = fields.Function(fields.Numeric(
+ "Payment Total Amount",
+ digits=(16, Eval('currency_digits', 2)),
+ depends=['currency_digits'],
+ help="The sum of all payment amounts."),
+ 'get_payment_aggregated')
+ payment_amount_succeeded = fields.Function(fields.Numeric(
+ "Payment Succeeded",
+ digits=(16, Eval('currency_digits', 2)),
+ depends=['currency_digits'],
+ help="The sum of the amounts of the successful payments."),
+ 'get_payment_aggregated')
+ payment_complete = fields.Function(fields.Boolean(
+ "Payment Complete",
+ help="All the payments in the group are complete."),
+ 'get_payment_aggregated', searcher='search_complete')
@classmethod
def __register__(cls, module_name):
@@ -103,6 +130,100 @@
default.setdefault('payments', None)
return super(Group, cls).copy(groups, default=default)
+ @classmethod
+ def _get_complete_states(cls):
+ return ['succeeded', 'failed']
+
+ @classmethod
+ def get_payment_aggregated(cls, groups, names):
+ pool = Pool()
+ Payment = pool.get('account.payment')
+ cursor = Transaction().connection.cursor()
+
+ payment = Payment.__table__()
+
+ # initialize result and columns
+ result = defaultdict(defaultdict)
+ columns = [
+ payment.group.as_('group_id'),
+ Count(payment.group).as_('payment_count'),
+ Sum(payment.amount).as_('payment_amount'),
+ Sum(payment.amount,
+ filter_=(payment.state == 'succeeded'),
+ ).as_('payment_amount_succeeded'),
+ Count(payment.group,
+ filter_=(~payment.state.in_(cls._get_complete_states())),
+ ).as_('payment_not_complete'),
+ ]
+
+ for sub_ids in grouped_slice(groups):
+ cursor.execute(*payment.select(*columns,
+ where=reduce_ids(payment.group, sub_ids),
+ group_by=payment.group),
+ )
+
+ for row in cursor_dict(cursor):
+ group_id = row['group_id']
+
+ result['payment_count'][group_id] = row['payment_count']
+ result['payment_complete'][group_id] = \
+ not row['payment_not_complete']
+
+ amount = row['payment_amount']
+ succeeded = row['payment_amount_succeeded']
+
+ if amount is not None:
+ # SQLite uses float for SUM
+ if not isinstance(amount, Decimal):
+ amount = Decimal(str(amount))
+ amount = cls(group_id).company.currency.round(amount)
+ result['payment_amount'][group_id] = amount
+
+ if succeeded is not None:
+ # SQLite uses float for SUM
+ if not isinstance(succeeded, Decimal):
+ succeeded = Decimal(str(succeeded))
+ succeeded = cls(group_id).company.currency.round(succeeded)
+ result['payment_amount_succeeded'][group_id] = succeeded
+
+ for key in list(result.keys()):
+ if key not in names:
+ del result[key]
+
+ return result
+
+ @classmethod
+ def search_complete(cls, name, clause):
+ pool = Pool()
+ Payment = pool.get('account.payment')
+ payment = Payment.__table__()
+
+ query_not_completed = payment.select(payment.group,
+ where=~payment.state.in_(cls._get_complete_states()),
+ group_by=payment.group)
+
+ operators = {
+ '=': 'not in',
+ '!=': 'in',
+ }
+ reverse = {
+ '=': 'in',
+ '!=': 'not in',
+ }
+
+ if clause[1] in operators:
+ if clause[2]:
+ return [('id', operators[clause[1]], query_not_completed)]
+ else:
+ return [('id', reverse[clause[1]], query_not_completed)]
+ else:
+ return []
+
+ @fields.depends('journal')
+ def on_change_with_currency_digits(self, name=None):
+ if self.journal:
+ return self.journal.currency.digits
+
_STATES = {
'readonly': Eval('state') != 'draft',
diff -r 5efe109ffa3f -r 985fe4c47073 tests/scenario_account_payment.rst
--- a/tests/scenario_account_payment.rst Mon Jun 01 10:08:02 2020 +0100
+++ b/tests/scenario_account_payment.rst Mon Jun 15 09:01:47 2020 +0200
@@ -91,6 +91,37 @@
>>> line.payment_amount
Decimal('30.00')
+Check the properties of the payment group::
+
+ >>> group = payment.group
+ >>> group.payment_count
+ 1
+ >>> group.payment_amount
+ Decimal('20.00')
+ >>> group.payment_amount_succeeded
+ >>> group.payment_complete
+ False
+
+Success the payment and recheck the payment group::
+
+ >>> payment.click('succeed')
+ >>> payment.state
+ 'succeeded'
+ >>> group.reload()
+ >>> group.payment_amount_succeeded
+ Decimal('20.00')
+ >>> group.payment_complete
+ True
+
+Search for the completed payment::
+
+ >>> PaymentGroup = Model.get('account.payment.group')
+ >>> group, = PaymentGroup.find([('payment_complete', '=', 'True')])
+ >>> group.payment_complete
+ True
+ >>> group.id == payment.group.id
+ True
+
Partially fail to pay the remaining::
>>> pay_line = Wizard('account.move.line.pay', [line])
@@ -109,6 +140,9 @@
>>> payment.click('fail')
>>> payment.state
'failed'
+ >>> payment.group.payment_complete
+ True
+ >>> payment.group.payment_amount_succeeded
>>> line.reload()
>>> line.payment_amount
Decimal('30.00')
diff -r 5efe109ffa3f -r 985fe4c47073 view/payment_group_form.xml
--- a/view/payment_group_form.xml Mon Jun 01 10:08:02 2020 +0100
+++ b/view/payment_group_form.xml Mon Jun 15 09:01:47 2020 +0200
@@ -10,4 +10,13 @@
<field name="journal"/>
<label name="kind"/>
<field name="kind"/>
+ <separator id="payments" string="Payments Info" colspan="4"/>
+ <label name="payment_count" str+ <label name="payment_count"
string="Count"/>
+ <field name="payment_count"/>
+ <label name="payment_amount" string="Total Amount"/>
+ <field name="payment_amount"/>
+ <label name="payment_amount_succeeded" string="Succeeded"/>
+ <field name="payment_amount_succeeded"/>
+ <label name="payment_complete" string="Complete"/>
+ <field name="payment_complete"/>
</form>
diff -r 5efe109ffa3f -r 985fe4c47073 view/payment_group_list.xml
--- a/view/payment_group_list.xml Mon Jun 01 10:08:02 2020 +0100
+++ b/view/payment_group_list.xml Mon Jun 15 09:01:47 2020 +0200
@@ -5,4 +5,8 @@
<field name="number"/>
<field name="journal"/>
<field name="kind"/>
+ <field name="payment_count"/>
+ <field name="payment_amount"/>
+ <field name="payment_amount_succeeded"/>
+ <field name="payment_complete"/>
</tree>