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 @@
+
+
+