From d1e7ed8c48aa899cde93654ac40b92130fe9d095 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=A9rome=20Perrin?= <jerome@nexedi.com>
Date: Thu, 20 May 2021 08:26:39 +0200
Subject: [PATCH] payment_mean_sepa: add constraints for credit transfer
 payment lines

Checking that for all lines:
 - bank account is set, for both sides
 - bank accounts have IBAN and BIC
 - bank accounts are validated
 - bank accounts belong to the section (to prevent problem when cloning
and not changing the bank account)

These constraints are present in a property sheet that have to be associated to
accounting transaction movement portal type in a configurator step, and also as
assertions in the file generation.
---
 ...actionLineSEPACreditTransferConstraint.xml |  64 ++++++++++
 .../payment_belong_to_section_constraint.xml  | 105 +++++++++++++++++
 .../payment_iban_bic_constraint.xml           | 105 +++++++++++++++++
 .../payment_validated_constraint.xml          | 105 +++++++++++++++++
 ...sactionLine_checkPaymentBelongToSection.py |  14 +++
 ...actionLine_checkPaymentBelongToSection.xml |  62 ++++++++++
 ...tionGroup_getSEPACreditTransferDataDict.py |   5 +
 ...st.erp5.testPaymentTransactionGroupSEPA.py | 109 +++++++++++++++++-
 .../bt/template_property_sheet_id_list        |   3 +-
 9 files changed, 570 insertions(+), 2 deletions(-)
 create mode 100644 bt5/erp5_payment_mean_sepa/PropertySheetTemplateItem/portal_property_sheets/AccountingTransactionLineSEPACreditTransferConstraint.xml
 create mode 100644 bt5/erp5_payment_mean_sepa/PropertySheetTemplateItem/portal_property_sheets/AccountingTransactionLineSEPACreditTransferConstraint/payment_belong_to_section_constraint.xml
 create mode 100644 bt5/erp5_payment_mean_sepa/PropertySheetTemplateItem/portal_property_sheets/AccountingTransactionLineSEPACreditTransferConstraint/payment_iban_bic_constraint.xml
 create mode 100644 bt5/erp5_payment_mean_sepa/PropertySheetTemplateItem/portal_property_sheets/AccountingTransactionLineSEPACreditTransferConstraint/payment_validated_constraint.xml
 create mode 100644 bt5/erp5_payment_mean_sepa/SkinTemplateItem/portal_skins/erp5_payment_mean_sepa/AccountingTransactionLine_checkPaymentBelongToSection.py
 create mode 100644 bt5/erp5_payment_mean_sepa/SkinTemplateItem/portal_skins/erp5_payment_mean_sepa/AccountingTransactionLine_checkPaymentBelongToSection.xml

diff --git a/bt5/erp5_payment_mean_sepa/PropertySheetTemplateItem/portal_property_sheets/AccountingTransactionLineSEPACreditTransferConstraint.xml b/bt5/erp5_payment_mean_sepa/PropertySheetTemplateItem/portal_property_sheets/AccountingTransactionLineSEPACreditTransferConstraint.xml
new file mode 100644
index 00000000000..64b35fbae0f
--- /dev/null
+++ b/bt5/erp5_payment_mean_sepa/PropertySheetTemplateItem/portal_property_sheets/AccountingTransactionLineSEPACreditTransferConstraint.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0"?>
+<ZopeData>
+  <record id="1" aka="AAAAAAAAAAE=">
+    <pickle>
+      <global name="Property Sheet" module="erp5.portal_type"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>_count</string> </key>
+            <value>
+              <persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
+            </value>
+        </item>
+        <item>
+            <key> <string>_mt_index</string> </key>
+            <value>
+              <persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
+            </value>
+        </item>
+        <item>
+            <key> <string>_tree</string> </key>
+            <value>
+              <persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent>
+            </value>
+        </item>
+        <item>
+            <key> <string>description</string> </key>
+            <value> <string>Constraints checking that lines of payment transactions satisfy all the requirements to be used in SEPA Credit Transfer</string> </value>
+        </item>
+        <item>
+            <key> <string>id</string> </key>
+            <value> <string>AccountingTransactionLineSEPACreditTransferConstraint</string> </value>
+        </item>
+        <item>
+            <key> <string>portal_type</string> </key>
+            <value> <string>Property Sheet</string> </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+  <record id="2" aka="AAAAAAAAAAI=">
+    <pickle>
+      <global name="Length" module="BTrees.Length"/>
+    </pickle>
+    <pickle> <int>0</int> </pickle>
+  </record>
+  <record id="3" aka="AAAAAAAAAAM=">
+    <pickle>
+      <global name="OOBTree" module="BTrees.OOBTree"/>
+    </pickle>
+    <pickle>
+      <none/>
+    </pickle>
+  </record>
+  <record id="4" aka="AAAAAAAAAAQ=">
+    <pickle>
+      <global name="OOBTree" module="BTrees.OOBTree"/>
+    </pickle>
+    <pickle>
+      <none/>
+    </pickle>
+  </record>
+</ZopeData>
diff --git a/bt5/erp5_payment_mean_sepa/PropertySheetTemplateItem/portal_property_sheets/AccountingTransactionLineSEPACreditTransferConstraint/payment_belong_to_section_constraint.xml b/bt5/erp5_payment_mean_sepa/PropertySheetTemplateItem/portal_property_sheets/AccountingTransactionLineSEPACreditTransferConstraint/payment_belong_to_section_constraint.xml
new file mode 100644
index 00000000000..bf602bb16b3
--- /dev/null
+++ b/bt5/erp5_payment_mean_sepa/PropertySheetTemplateItem/portal_property_sheets/AccountingTransactionLineSEPACreditTransferConstraint/payment_belong_to_section_constraint.xml
@@ -0,0 +1,105 @@
+<?xml version="1.0"?>
+<ZopeData>
+  <record id="1" aka="AAAAAAAAAAE=">
+    <pickle>
+      <global name="TALES Constraint" module="erp5.portal_type"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>_identity_criterion</string> </key>
+            <value>
+              <persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
+            </value>
+        </item>
+        <item>
+            <key> <string>_range_criterion</string> </key>
+            <value>
+              <persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
+            </value>
+        </item>
+        <item>
+            <key> <string>description</string> </key>
+            <value>
+              <none/>
+            </value>
+        </item>
+        <item>
+            <key> <string>expression</string> </key>
+            <value> <string>python: context.AccountingTransactionLine_checkPaymentBelongToSection(source=True) and context.AccountingTransactionLine_checkPaymentBelongToSection(source=False)</string> </value>
+        </item>
+        <item>
+            <key> <string>id</string> </key>
+            <value> <string>payment_belong_to_section_constraint</string> </value>
+        </item>
+        <item>
+            <key> <string>int_index</string> </key>
+            <value>
+              <none/>
+            </value>
+        </item>
+        <item>
+            <key> <string>membership_criterion_category</string> </key>
+            <value>
+              <tuple/>
+            </value>
+        </item>
+        <item>
+            <key> <string>message_expression_false</string> </key>
+            <value> <string>Bank Account must belong to Section</string> </value>
+        </item>
+        <item>
+            <key> <string>portal_type</string> </key>
+            <value> <string>TALES Constraint</string> </value>
+        </item>
+        <item>
+            <key> <string>string_index</string> </key>
+            <value>
+              <none/>
+            </value>
+        </item>
+        <item>
+            <key> <string>test_method_id</string> </key>
+            <value>
+              <tuple/>
+            </value>
+        </item>
+        <item>
+            <key> <string>test_tales_expression</string> </key>
+            <value> <string>python: context.isMemberOf(\'payment_mode/%s\' % context.getPortalObject().portal_preferences.getPreferredSepaCreditTransferPaymentMode()) and ((context.getSource(portal_type=\'Account\') and context.getSourceValue(portal_type=\'Account\').isMemberOf(\'account_type/asset/cash/bank\')) or (context.getDestination(portal_type=\'Account\') and context.getDestinationValue(portal_type=\'Account\').isMemberOf(\'account_type/asset/cash/bank\')))\n
+</string> </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+  <record id="2" aka="AAAAAAAAAAI=">
+    <pickle>
+      <global name="PersistentMapping" module="Persistence.mapping"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>data</string> </key>
+            <value>
+              <dictionary/>
+            </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+  <record id="3" aka="AAAAAAAAAAM=">
+    <pickle>
+      <global name="PersistentMapping" module="Persistence.mapping"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>data</string> </key>
+            <value>
+              <dictionary/>
+            </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+</ZopeData>
diff --git a/bt5/erp5_payment_mean_sepa/PropertySheetTemplateItem/portal_property_sheets/AccountingTransactionLineSEPACreditTransferConstraint/payment_iban_bic_constraint.xml b/bt5/erp5_payment_mean_sepa/PropertySheetTemplateItem/portal_property_sheets/AccountingTransactionLineSEPACreditTransferConstraint/payment_iban_bic_constraint.xml
new file mode 100644
index 00000000000..560ab1b04df
--- /dev/null
+++ b/bt5/erp5_payment_mean_sepa/PropertySheetTemplateItem/portal_property_sheets/AccountingTransactionLineSEPACreditTransferConstraint/payment_iban_bic_constraint.xml
@@ -0,0 +1,105 @@
+<?xml version="1.0"?>
+<ZopeData>
+  <record id="1" aka="AAAAAAAAAAE=">
+    <pickle>
+      <global name="TALES Constraint" module="erp5.portal_type"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>_identity_criterion</string> </key>
+            <value>
+              <persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
+            </value>
+        </item>
+        <item>
+            <key> <string>_range_criterion</string> </key>
+            <value>
+              <persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
+            </value>
+        </item>
+        <item>
+            <key> <string>description</string> </key>
+            <value>
+              <none/>
+            </value>
+        </item>
+        <item>
+            <key> <string>expression</string> </key>
+            <value> <string>python: context.getDestinationPayment(portal_type=\'Bank Account\') and context.getDestinationPaymentValue(portal_type=\'Bank Account\').getIban() and context.getDestinationPaymentValue(portal_type=\'Bank Account\').getBicCode() and context.getSourcePayment(portal_type=\'Bank Account\') and context.getSourcePaymentValue(portal_type=\'Bank Account\').getIban() and context.getSourcePaymentValue(portal_type=\'Bank Account\').getBicCode()</string> </value>
+        </item>
+        <item>
+            <key> <string>id</string> </key>
+            <value> <string>payment_iban_bic_constraint</string> </value>
+        </item>
+        <item>
+            <key> <string>int_index</string> </key>
+            <value>
+              <none/>
+            </value>
+        </item>
+        <item>
+            <key> <string>membership_criterion_category</string> </key>
+            <value>
+              <tuple/>
+            </value>
+        </item>
+        <item>
+            <key> <string>message_expression_false</string> </key>
+            <value> <string>Bank Accounts must have IBAN and BIC</string> </value>
+        </item>
+        <item>
+            <key> <string>portal_type</string> </key>
+            <value> <string>TALES Constraint</string> </value>
+        </item>
+        <item>
+            <key> <string>string_index</string> </key>
+            <value>
+              <none/>
+            </value>
+        </item>
+        <item>
+            <key> <string>test_method_id</string> </key>
+            <value>
+              <tuple/>
+            </value>
+        </item>
+        <item>
+            <key> <string>test_tales_expression</string> </key>
+            <value> <string>python: context.isMemberOf(\'payment_mode/%s\' % context.getPortalObject().portal_preferences.getPreferredSepaCreditTransferPaymentMode()) and ((context.getSource(portal_type=\'Account\') and context.getSourceValue(portal_type=\'Account\').isMemberOf(\'account_type/asset/cash/bank\')) or (context.getDestination(portal_type=\'Account\') and context.getDestinationValue(portal_type=\'Account\').isMemberOf(\'account_type/asset/cash/bank\')))\n
+</string> </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+  <record id="2" aka="AAAAAAAAAAI=">
+    <pickle>
+      <global name="PersistentMapping" module="Persistence.mapping"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>data</string> </key>
+            <value>
+              <dictionary/>
+            </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+  <record id="3" aka="AAAAAAAAAAM=">
+    <pickle>
+      <global name="PersistentMapping" module="Persistence.mapping"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>data</string> </key>
+            <value>
+              <dictionary/>
+            </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+</ZopeData>
diff --git a/bt5/erp5_payment_mean_sepa/PropertySheetTemplateItem/portal_property_sheets/AccountingTransactionLineSEPACreditTransferConstraint/payment_validated_constraint.xml b/bt5/erp5_payment_mean_sepa/PropertySheetTemplateItem/portal_property_sheets/AccountingTransactionLineSEPACreditTransferConstraint/payment_validated_constraint.xml
new file mode 100644
index 00000000000..a101c728857
--- /dev/null
+++ b/bt5/erp5_payment_mean_sepa/PropertySheetTemplateItem/portal_property_sheets/AccountingTransactionLineSEPACreditTransferConstraint/payment_validated_constraint.xml
@@ -0,0 +1,105 @@
+<?xml version="1.0"?>
+<ZopeData>
+  <record id="1" aka="AAAAAAAAAAE=">
+    <pickle>
+      <global name="TALES Constraint" module="erp5.portal_type"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>_identity_criterion</string> </key>
+            <value>
+              <persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
+            </value>
+        </item>
+        <item>
+            <key> <string>_range_criterion</string> </key>
+            <value>
+              <persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
+            </value>
+        </item>
+        <item>
+            <key> <string>description</string> </key>
+            <value>
+              <none/>
+            </value>
+        </item>
+        <item>
+            <key> <string>expression</string> </key>
+            <value> <string>python: context.getDestinationPayment(portal_type=\'Bank Account\') and context.getDestinationPaymentValue(portal_type=\'Bank Account\').getValidationState() == \'validated\' and context.getSourcePayment(portal_type=\'Bank Account\') and context.getSourcePaymentValue(portal_type=\'Bank Account\').getValidationState() == \'validated\'</string> </value>
+        </item>
+        <item>
+            <key> <string>id</string> </key>
+            <value> <string>payment_validated_constraint</string> </value>
+        </item>
+        <item>
+            <key> <string>int_index</string> </key>
+            <value>
+              <none/>
+            </value>
+        </item>
+        <item>
+            <key> <string>membership_criterion_category</string> </key>
+            <value>
+              <tuple/>
+            </value>
+        </item>
+        <item>
+            <key> <string>message_expression_false</string> </key>
+            <value> <string>Bank Accounts must be validated</string> </value>
+        </item>
+        <item>
+            <key> <string>portal_type</string> </key>
+            <value> <string>TALES Constraint</string> </value>
+        </item>
+        <item>
+            <key> <string>string_index</string> </key>
+            <value>
+              <none/>
+            </value>
+        </item>
+        <item>
+            <key> <string>test_method_id</string> </key>
+            <value>
+              <tuple/>
+            </value>
+        </item>
+        <item>
+            <key> <string>test_tales_expression</string> </key>
+            <value> <string>python: context.isMemberOf(\'payment_mode/%s\' % context.getPortalObject().portal_preferences.getPreferredSepaCreditTransferPaymentMode()) and ((context.getSource(portal_type=\'Account\') and context.getSourceValue(portal_type=\'Account\').isMemberOf(\'account_type/asset/cash/bank\')) or (context.getDestination(portal_type=\'Account\') and context.getDestinationValue(portal_type=\'Account\').isMemberOf(\'account_type/asset/cash/bank\')))\n
+</string> </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+  <record id="2" aka="AAAAAAAAAAI=">
+    <pickle>
+      <global name="PersistentMapping" module="Persistence.mapping"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>data</string> </key>
+            <value>
+              <dictionary/>
+            </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+  <record id="3" aka="AAAAAAAAAAM=">
+    <pickle>
+      <global name="PersistentMapping" module="Persistence.mapping"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>data</string> </key>
+            <value>
+              <dictionary/>
+            </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+</ZopeData>
diff --git a/bt5/erp5_payment_mean_sepa/SkinTemplateItem/portal_skins/erp5_payment_mean_sepa/AccountingTransactionLine_checkPaymentBelongToSection.py b/bt5/erp5_payment_mean_sepa/SkinTemplateItem/portal_skins/erp5_payment_mean_sepa/AccountingTransactionLine_checkPaymentBelongToSection.py
new file mode 100644
index 00000000000..01c1b7a1dd1
--- /dev/null
+++ b/bt5/erp5_payment_mean_sepa/SkinTemplateItem/portal_skins/erp5_payment_mean_sepa/AccountingTransactionLine_checkPaymentBelongToSection.py
@@ -0,0 +1,14 @@
+"""Check that the bank account "belong" to the destination section.
+"""
+portal = context.getPortalObject()
+if source:
+  section = context.getSourceSectionValue(portal_type=portal.getPortalEntityTypeList())
+  bank_account = context.getSourcePaymentValue(portal_type=portal.getPortalPaymentNodeTypeList())
+else:
+  section = context.getDestinationSectionValue(portal_type=portal.getPortalEntityTypeList())
+  bank_account = context.getDestinationPaymentValue(portal_type=portal.getPortalPaymentNodeTypeList())
+
+if section is None or bank_account is None:
+  return True
+section = section.Organisation_getMappingRelatedOrganisation()
+return bank_account.getParentValue() == section
diff --git a/bt5/erp5_payment_mean_sepa/SkinTemplateItem/portal_skins/erp5_payment_mean_sepa/AccountingTransactionLine_checkPaymentBelongToSection.xml b/bt5/erp5_payment_mean_sepa/SkinTemplateItem/portal_skins/erp5_payment_mean_sepa/AccountingTransactionLine_checkPaymentBelongToSection.xml
new file mode 100644
index 00000000000..fa01cbfde98
--- /dev/null
+++ b/bt5/erp5_payment_mean_sepa/SkinTemplateItem/portal_skins/erp5_payment_mean_sepa/AccountingTransactionLine_checkPaymentBelongToSection.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0"?>
+<ZopeData>
+  <record id="1" aka="AAAAAAAAAAE=">
+    <pickle>
+      <global name="PythonScript" module="Products.PythonScripts.PythonScript"/>
+    </pickle>
+    <pickle>
+      <dictionary>
+        <item>
+            <key> <string>Script_magic</string> </key>
+            <value> <int>3</int> </value>
+        </item>
+        <item>
+            <key> <string>_bind_names</string> </key>
+            <value>
+              <object>
+                <klass>
+                  <global name="NameAssignments" module="Shared.DC.Scripts.Bindings"/>
+                </klass>
+                <tuple/>
+                <state>
+                  <dictionary>
+                    <item>
+                        <key> <string>_asgns</string> </key>
+                        <value>
+                          <dictionary>
+                            <item>
+                                <key> <string>name_container</string> </key>
+                                <value> <string>container</string> </value>
+                            </item>
+                            <item>
+                                <key> <string>name_context</string> </key>
+                                <value> <string>context</string> </value>
+                            </item>
+                            <item>
+                                <key> <string>name_m_self</string> </key>
+                                <value> <string>script</string> </value>
+                            </item>
+                            <item>
+                                <key> <string>name_subpath</string> </key>
+                                <value> <string>traverse_subpath</string> </value>
+                            </item>
+                          </dictionary>
+                        </value>
+                    </item>
+                  </dictionary>
+                </state>
+              </object>
+            </value>
+        </item>
+        <item>
+            <key> <string>_params</string> </key>
+            <value> <string>source</string> </value>
+        </item>
+        <item>
+            <key> <string>id</string> </key>
+            <value> <string>AccountingTransactionLine_checkPaymentBelongToSection</string> </value>
+        </item>
+      </dictionary>
+    </pickle>
+  </record>
+</ZopeData>
diff --git a/bt5/erp5_payment_mean_sepa/SkinTemplateItem/portal_skins/erp5_payment_mean_sepa/PaymentTransactionGroup_getSEPACreditTransferDataDict.py b/bt5/erp5_payment_mean_sepa/SkinTemplateItem/portal_skins/erp5_payment_mean_sepa/PaymentTransactionGroup_getSEPACreditTransferDataDict.py
index 518950992cd..59c6c77f4ae 100644
--- a/bt5/erp5_payment_mean_sepa/SkinTemplateItem/portal_skins/erp5_payment_mean_sepa/PaymentTransactionGroup_getSEPACreditTransferDataDict.py
+++ b/bt5/erp5_payment_mean_sepa/SkinTemplateItem/portal_skins/erp5_payment_mean_sepa/PaymentTransactionGroup_getSEPACreditTransferDataDict.py
@@ -59,6 +59,11 @@ for brain in context.PaymentTransactionGroup_getAccountingTransactionLineList():
   assert creditor_bank_account.getValidationState() == 'validated', \
     '%s is not validated' % creditor_bank_account.getRelativeUrl() 
 
+  assert transaction_line.AccountingTransactionLine_checkPaymentBelongToSection(source=True), \
+    'source bank account on %s does not belong to section' % transaction_line.getRelativeUrl()
+  assert transaction_line.AccountingTransactionLine_checkPaymentBelongToSection(source=False), \
+    'destination bank account on %s does not belong to section' % transaction_line.getRelativeUrl()
+
   end_to_end_id = transaction.getReference()
   assert end_to_end_id
   assert end_to_end_id not in end_to_end_id_set
diff --git a/bt5/erp5_payment_mean_sepa/TestTemplateItem/portal_components/test.erp5.testPaymentTransactionGroupSEPA.py b/bt5/erp5_payment_mean_sepa/TestTemplateItem/portal_components/test.erp5.testPaymentTransactionGroupSEPA.py
index e6faa69b949..9cdf98176bf 100644
--- a/bt5/erp5_payment_mean_sepa/TestTemplateItem/portal_components/test.erp5.testPaymentTransactionGroupSEPA.py
+++ b/bt5/erp5_payment_mean_sepa/TestTemplateItem/portal_components/test.erp5.testPaymentTransactionGroupSEPA.py
@@ -50,7 +50,6 @@ class TestPaymentTransactionGroupPaymentSEPA(AccountingTestCase):
         title='France',
         reference='FR',
       )
-    self.tic()
 
   def _createPTG(self):
     ptg = self.portal.payment_transaction_group_module.newContent(
@@ -224,3 +223,111 @@ class TestPaymentTransactionGroupPaymentSEPA(AccountingTestCase):
         [node.text for node in pain.findall('.//{*}GrpHdr/{*}CtrlSum')],
         ['300.00'],
     )
+
+
+class TestSEPAConstraints(AccountingTestCase):
+  def afterSetUp(self):
+    AccountingTestCase.afterSetUp(self)
+    ti = self.portal.portal_types['Accounting Transaction Line']
+    ti.setTypePropertySheetList(
+        ti.getTypePropertySheetList() + ['AccountingTransactionLineSEPACreditTransferConstraint'])
+    if 'wire_transfer' not in self.portal.portal_categories.payment_mode.objectIds():
+      self.portal.portal_categories.payment_mode.newContent(
+          portal_type='Category',
+          id='wire_transfer',
+          title='Wire Transfer',
+      )
+    self.portal.portal_preferences.getActiveSystemPreference().setPreferredSepaCreditTransferPaymentMode('wire_transfer')
+    self.tic()
+
+  def beforeTearDown(self):
+    self.abort()
+    ti = self.portal.portal_types['Accounting Transaction Line']
+    ti.setTypePropertySheetList(
+        [ps for ps in ti.getTypePropertySheetList() if ps != 'AccountingTransactionLineSEPACreditTransferConstraint'])
+    self.commit()
+
+  def test_payment_transaction_constraint(self):
+    section_bank_account = self.section.newContent(
+        portal_type='Bank Account',
+        bic_code='X',
+        iban='FR76...',
+    )
+    section_bank_account.validate()
+    supplier = self.portal.organisation_module.newContent(
+        portal_type='Organisation',
+    )
+    supplier_bank_account = supplier.newContent(
+        portal_type='Bank Account',
+    )
+
+    payment_transaction = self._makeOne(
+        portal_type='Payment Transaction',
+        resource_value=self.currency_module.euro,
+        source_section_value=self.section,
+        source_payment_value=section_bank_account,
+        destination_section_value=supplier,
+        start_date=DateTime(2021, 1, 1),
+        lines=(
+            dict(
+                source_value=self.portal.account_module.payable,
+                source_debit=100),
+            dict(
+                source_value=self.portal.account_module.bank,
+                source_credit=100)))
+
+    self.assertEqual(
+        sorted([str(m.getMessage()) for m in payment_transaction.checkConsistency()]),
+        [],
+    )
+    payment_transaction.setPaymentModeValue(self.portal.portal_categories.payment_mode.wire_transfer)
+    self.assertEqual(
+        sorted([str(m.getMessage()) for m in payment_transaction.checkConsistency()]),
+        ['Bank Accounts must be validated', 'Bank Accounts must have IBAN and BIC', ],
+    )
+
+    payment_transaction.setDestinationPaymentValue(supplier_bank_account)
+    supplier_bank_account.setBicCode('X')
+    supplier_bank_account.setIban('FR76...')
+    self.assertEqual(
+        sorted([str(m.getMessage()) for m in payment_transaction.checkConsistency()]),
+        ['Bank Accounts must be validated', ],
+    )
+    supplier_bank_account.validate()
+    self.assertEqual(
+        sorted([str(m.getMessage()) for m in payment_transaction.checkConsistency()]),
+        [],
+    )
+
+    payment_transaction.setSourcePaymentValue(supplier_bank_account)
+    self.assertEqual(
+        sorted([str(m.getMessage()) for m in payment_transaction.checkConsistency()]),
+        ['Bank Account must belong to Section', ],
+    )
+    payment_transaction.setSourcePaymentValue(section_bank_account)
+    self.assertEqual(
+        sorted([str(m.getMessage()) for m in payment_transaction.checkConsistency()]),
+        [],
+    )
+    payment_transaction.setDestinationPaymentValue(section_bank_account)
+    self.assertEqual(
+        sorted([str(m.getMessage()) for m in payment_transaction.checkConsistency()]),
+        ['Bank Account must belong to Section', ],
+    )
+    payment_transaction.setDestinationPaymentValue(supplier_bank_account)
+    self.assertEqual(
+        sorted([str(m.getMessage()) for m in payment_transaction.checkConsistency()]),
+        [],
+    )
+
+    section_bank_account.setIban('')
+    self.assertEqual(
+        sorted([str(m.getMessage()) for m in payment_transaction.checkConsistency()]),
+        ['Bank Accounts must have IBAN and BIC', ],
+    )
+
+    section_bank_account.invalidate()
+    self.assertEqual(
+        sorted([str(m.getMessage()) for m in payment_transaction.checkConsistency()]),
+        ['Bank Accounts must be validated', 'Bank Accounts must have IBAN and BIC', ],
+    )
diff --git a/bt5/erp5_payment_mean_sepa/bt/template_property_sheet_id_list b/bt5/erp5_payment_mean_sepa/bt/template_property_sheet_id_list
index e1b5e90c12f..d38ec370c58 100644
--- a/bt5/erp5_payment_mean_sepa/bt/template_property_sheet_id_list
+++ b/bt5/erp5_payment_mean_sepa/bt/template_property_sheet_id_list
@@ -1 +1,2 @@
-PaymentMeanSEPAPreference
\ No newline at end of file
+PaymentMeanSEPAPreference
+AccountingTransactionLineSEPACreditTransferConstraint
\ No newline at end of file
-- 
2.30.9