From bb398854c0ab25ffb6c0a2a70fc26e19bdeb650a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Mon, 11 Apr 2022 20:51:41 +0200 Subject: [PATCH] Prevent to select reconciled payment or group on draft statement line issue11175 review381781002 --- CHANGELOG | 1 + __init__.py | 1 + account.py | 47 ++++++++++++++- payment.py | 66 +++++++++++++++++++++ payment.xml | 6 ++ statement.py | 13 +++- tests/scenario_account_payment_clearing.rst | 22 +++++++ view/payment_group_list.xml | 8 +++ view/payment_list.xml | 3 + 9 files changed, 164 insertions(+), 3 deletions(-) create mode 100644 view/payment_group_list.xml diff --git a/CHANGELOG b/CHANGELOG index 65821708c2..2e8fc0aea5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Prevent to select reconciled payment or group on draft statement line * Reconcile payment clearing line with all statement lines * Use reference field for statement line relation * Add support for Python 3.10 diff --git a/__init__.py b/__init__.py index bd58aa721b..43236590a0 100644 --- a/__init__.py +++ b/__init__.py @@ -12,6 +12,7 @@ def register(): payment.Group, payment.SucceedStart, account.Move, + account.MoveReconciliation, ir.Cron, module='account_payment_clearing', type_='model') Pool.register( diff --git a/account.py b/account.py index 25cec74c1f..503eb6f85c 100644 --- a/account.py +++ b/account.py @@ -1,6 +1,7 @@ # 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 trytond.pool import PoolMeta +from trytond.pool import Pool, PoolMeta +from trytond.tools import grouped_slice class Move(metaclass=PoolMeta): @@ -10,3 +11,47 @@ class Move(metaclass=PoolMeta): def _get_origin(cls): models = super(Move, cls)._get_origin() return models + ['account.payment'] + + +def _payments_to_update(reconciliations): + pool = Pool() + Payment = pool.get('account.payment') + Reconciliation = pool.get('account.move.reconciliation') + + moves = set() + others = set() + for reconciliation in reconciliations: + for line in reconciliation.lines: + moves.add(line.move) + others.update(line.reconciliations_delegated) + + payments = set() + for sub_moves in grouped_slice(moves): + payments.update(Payment.search([ + ('clearing_move', 'in', [m.id for m in sub_moves]), + ], order=[])) + if others: + payments.update(_payments_to_update(Reconciliation.browse(others))) + + return payments + + +class MoveReconciliation(metaclass=PoolMeta): + __name__ = 'account.move.reconciliation' + + @classmethod + def create(cls, vlist): + pool = Pool() + Payment = pool.get('account.payment') + reconciliations = super().create(vlist) + Payment.__queue__.update_reconciled( + list(_payments_to_update(reconciliations))) + return reconciliations + + @classmethod + def delete(cls, reconciliations): + pool = Pool() + Payment = pool.get('account.payment') + payments = _payments_to_update(reconciliations) + super().delete(reconciliations) + Payment.__queue__.update_reconciled(list(payments)) diff --git a/payment.py b/payment.py index 4ca3ad0c3c..f3ef02e380 100644 --- a/payment.py +++ b/payment.py @@ -3,9 +3,13 @@ from collections import defaultdict from functools import wraps +from sql.aggregate import BoolAnd, Min + +from trytond import backend from trytond.model import ModelView, Workflow, fields from trytond.pool import Pool, PoolMeta from trytond.pyson import Bool, Eval +from trytond.tools import grouped_slice, reduce_ids from trytond.transaction import Transaction from trytond.wizard import Button, StateTransition, StateView, Wizard @@ -94,6 +98,7 @@ def cancel_clearing_move(func): for party in to_reconcile: for lines in to_reconcile[party].values(): Line.reconcile(lines) + cls.update_reconciled(payments) return wrapper @@ -120,6 +125,12 @@ class Payment(metaclass=PoolMeta): help="Define the account to use for clearing move.") clearing_move = fields.Many2One('account.move', 'Clearing Move', readonly=True) + clearing_reconciled = fields.Boolean( + "Clearing Reconciled", select=True, + states={ + 'invisible': ~Eval('clearing_move'), + }, + help="Checked if clearing line is reconciled.") @property def amount_line_paid(self): @@ -190,6 +201,7 @@ class Payment(metaclass=PoolMeta): to_reconcile.append(lines) Line.reconcile(*to_reconcile) cls.reconcile_clearing(payments) + cls.update_reconciled(payments) @property def clearing_account(self): @@ -276,6 +288,16 @@ class Payment(metaclass=PoolMeta): def fail(cls, payments): super(Payment, cls).fail(payments) + @classmethod + def update_reconciled(cls, payments): + for payment in payments: + if payment.clearing_move: + payment.clearing_reconciled = all( + l.reconciliation for l in payment.clearing_lines) + else: + payment.clearing_reconciled = False + cls.save(payments) + @classmethod def reconcile_clearing(cls, payments): pool = Pool() @@ -317,6 +339,50 @@ class Payment(metaclass=PoolMeta): class Group(metaclass=PoolMeta): __name__ = 'account.payment.group' + clearing_reconciled = fields.Function(fields.Boolean( + "Clearing Reconciled", + help="All payments in the group are reconciled."), + 'get_reconciled', searcher='search_reconciled') + + @classmethod + def get_reconciled(cls, groups, name): + pool = Pool() + Payment = pool.get('account.payment') + payment = Payment.__table__() + cursor = Transaction().connection.cursor() + result = defaultdict() + if backend.name == 'sqlite': + column = Min(payment.clearing_reconciled) + else: + column = BoolAnd(payment.clearing_reconciled) + for sub_groups in grouped_slice(groups): + cursor.execute(*payment.select( + payment.group, column, + where=reduce_ids(payment.group, sub_groups), + group_by=payment.group)) + result.update(cursor) + return result + + @classmethod + def search_reconciled(cls, name, clause): + pool = Pool() + Payment = pool.get('account.payment') + payment = Payment.__table__() + + _, operator, value = clause + Operator = fields.SQL_OPERATORS[operator] + + if backend.name == 'sqlite': + column = Min(payment.clearing_reconciled) + else: + column = BoolAnd(payment.clearing_reconciled) + + query = payment.select( + payment.group, + having=Operator(column, value), + group_by=payment.group) + return [('id', 'in', query)] + @classmethod def reconcile_clearing(cls, groups): pool = Pool() diff --git a/payment.xml b/payment.xml index ba58a46ca8..d7f0e30110 100644 --- a/payment.xml +++ b/payment.xml @@ -22,6 +22,12 @@ this repository contains the full copyright notices and license terms. --> payment_list + + account.payment.group + + payment_group_list + + Succeed Payments account.payment.succeed diff --git a/statement.py b/statement.py index f57ceeceed..07105934d0 100644 --- a/statement.py +++ b/statement.py @@ -4,7 +4,7 @@ from collections import defaultdict from trytond.model import fields from trytond.pool import Pool, PoolMeta -from trytond.pyson import Eval +from trytond.pyson import Eval, If from trytond.transaction import Transaction @@ -98,9 +98,18 @@ class StatementLine(metaclass=PoolMeta): @classmethod def __setup__(cls): super(StatementLine, cls).__setup__() + cls.related_to.domain['account.payment'] = [ + cls.related_to.domain.get('account.payment', []), + If(Eval('statement_state') == 'draft', + ('clearing_reconciled', '!=', True), + ()), + ] cls.related_to.domain['account.payment.group'] = [ ('company', '=', Eval('company', -1)), - ('currency', '=', Eval('currency', -1)) + ('currency', '=', Eval('currency', -1)), + If(Eval('statement_state') == 'draft', + ('clearing_reconciled', '!=', True), + ()), ] @classmethod diff --git a/tests/scenario_account_payment_clearing.rst b/tests/scenario_account_payment_clearing.rst index 30855c80e6..9805aa7b01 100644 --- a/tests/scenario_account_payment_clearing.rst +++ b/tests/scenario_account_payment_clearing.rst @@ -119,6 +119,8 @@ Succeed payment:: True >>> payment.clearing_move.state 'draft' + >>> bool(payment.clearing_reconciled) + False >>> payable.reload() >>> payable.balance Decimal('-20.00') @@ -133,6 +135,8 @@ Fail payment:: >>> payment.state 'failed' >>> payment.clearing_move + >>> bool(payment.clearing_reconciled) + False >>> payment.line.reconciliation >>> payable.reload() >>> payable.balance @@ -214,6 +218,8 @@ Fail payment with posted clearing:: >>> payment.state 'failed' >>> payment.clearing_move + >>> bool(payment.clearing_reconciled) + False >>> payment.line.reconciliation >>> clearing_move.reload() >>> line, = [l for l in clearing_move.lines @@ -284,6 +290,20 @@ Validate statement:: >>> bank_clearing.reload() >>> bank_clearing.balance Decimal('0.00') + >>> payment.reload() + >>> bool(payment.clearing_reconciled) + True + >>> bool(payment.group.clearing_reconciled) + True + +Unreconcile payment clearing to allow reimbursement:: + + >>> move_line.reconciliation.delete() + >>> payment.reload() + >>> bool(payment.clearing_reconciled) + False + >>> bool(payment.group.clearing_reconciled) + False Create a statement that reimburse the payment group:: @@ -460,6 +480,8 @@ Validate statement and check the payment is confirmed:: >>> payment.reload() >>> payment.state 'succeeded' + >>> bool(payment.clearing_reconciled) + True >>> debit_line, = [l for l in payment.clearing_move.lines if l.debit > 0] >>> debit_line.debit Decimal('50.00') diff --git a/view/payment_group_list.xml b/view/payment_group_list.xml new file mode 100644 index 0000000000..ad6956a837 --- /dev/null +++ b/view/payment_group_list.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/view/payment_list.xml b/view/payment_list.xml index 16c22272e5..8ff63fa768 100644 --- a/view/payment_list.xml +++ b/view/payment_list.xml @@ -2,6 +2,9 @@ + + +