Commit 6e4ef313 authored by Jérome Perrin's avatar Jérome Perrin Committed by Aurel

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.
parent 130fb4ea
<?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>
<?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>
<?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>
<?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>
"""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
<?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>
...@@ -59,6 +59,11 @@ for brain in context.PaymentTransactionGroup_getAccountingTransactionLineList(): ...@@ -59,6 +59,11 @@ for brain in context.PaymentTransactionGroup_getAccountingTransactionLineList():
assert creditor_bank_account.getValidationState() == 'validated', \ assert creditor_bank_account.getValidationState() == 'validated', \
'%s is not validated' % creditor_bank_account.getRelativeUrl() '%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() end_to_end_id = transaction.getReference()
assert end_to_end_id assert end_to_end_id
assert end_to_end_id not in end_to_end_id_set assert end_to_end_id not in end_to_end_id_set
......
...@@ -50,7 +50,6 @@ class TestPaymentTransactionGroupPaymentSEPA(AccountingTestCase): ...@@ -50,7 +50,6 @@ class TestPaymentTransactionGroupPaymentSEPA(AccountingTestCase):
title='France', title='France',
reference='FR', reference='FR',
) )
self.tic()
def _createPTG(self): def _createPTG(self):
ptg = self.portal.payment_transaction_group_module.newContent( ptg = self.portal.payment_transaction_group_module.newContent(
...@@ -224,3 +223,111 @@ class TestPaymentTransactionGroupPaymentSEPA(AccountingTestCase): ...@@ -224,3 +223,111 @@ class TestPaymentTransactionGroupPaymentSEPA(AccountingTestCase):
[node.text for node in pain.findall('.//{*}GrpHdr/{*}CtrlSum')], [node.text for node in pain.findall('.//{*}GrpHdr/{*}CtrlSum')],
['300.00'], ['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', ],
)
PaymentMeanSEPAPreference PaymentMeanSEPAPreference
AccountingTransactionLineSEPACreditTransferConstraint
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment