From: Henryk Plötz Date: Fri, 24 Aug 2018 10:55:19 +0000 (+0200) Subject: Work towards SEPA transfers X-Git-Tag: v2.0.0~1^2~86 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=2366f6d29b68f38659e8c5de3a7d6c0c73fddec8;p=thirdparty%2Fpython-fints.git Work towards SEPA transfers --- diff --git a/fints/client.py b/fints/client.py index 53b3a0a..1ae0b3f 100644 --- a/fints/client.py +++ b/fints/client.py @@ -26,12 +26,12 @@ from .security import ( ) from .segments import HIBPA3, HIRMS2, HIUPA4 from .segments.accounts import HISPA1, HKSPA, HKSPA1 -from .segments.auth import HKTAB, HKTAN, HKTAB4, HKTAB5 +from .segments.auth import HKTAB, HKTAN, HKTAB4, HKTAB5, HKTAN5 from .segments.depot import HKWPD5, HKWPD6 from .segments.dialog import HISYN4, HKSYN3 from .segments.saldo import HKSAL5, HKSAL6, HKSAL7 from .segments.statement import HKKAZ5, HKKAZ6, HKKAZ7 -from .segments.transfer import HKCCM, HKCCS +from .segments.transfer import HKCCM1, HKCCS1 from .types import SegmentSequence from .utils import MT535_Miniparser, Password, mt940_to_array, compress_datablob, decompress_datablob from .parser import FinTS3Serializer @@ -236,8 +236,10 @@ class FinTS3Client: return responses - def _find_highest_supported_command(self, *segment_classes): + def _find_highest_supported_command(self, *segment_classes, **kwargs): """Search the BPD for the highest supported version of a segment.""" + return_parameter_segment = kwargs.get("return_parameter_segment", False) + parameter_segment_name = "{}I{}S".format(segment_classes[0].TYPE[0], segment_classes[0].TYPE[2:]) version_map = dict((clazz.VERSION, clazz) for clazz in segment_classes) max_version = self.bpd.find_segment_highest_version(parameter_segment_name, version_map.keys()) @@ -248,7 +250,10 @@ class FinTS3Client: tuple(v.header.version for v in self.bpd.find_segments(parameter_segment_name)) )) - return version_map.get(max_version.header.version) + if return_parameter_segment: + return max_version, version_map.get(max_version.header.version) + else: + return version_map.get(max_version.header.version) def get_statement(self, account: SEPAAccount, start_date: datetime.date, end_date: datetime.date): @@ -363,9 +368,9 @@ class FinTS3Client: challenge.dialog.end() - def start_simple_sepa_transfer(self, account: SEPAAccount, tan_method: TwoStepParametersCommon, iban: str, bic: str, + def start_simple_sepa_transfer(self, account: SEPAAccount, iban: str, bic: str, recipient_name: str, amount: Decimal, account_name: str, reason: str, - endtoend_id='NOTPROVIDED', tan_description=''): + endtoend_id='NOTPROVIDED'): """ Start a simple SEPA transfer. @@ -399,8 +404,8 @@ class FinTS3Client: "endtoend_id": endtoend_id, } sepa.add_payment(payment) - xml = sepa.export().decode() - return self.start_sepa_transfer(account, xml, tan_method, tan_description) + xml = sepa.export() + return self.start_sepa_transfer(account, xml) def _get_start_sepa_transfer_message(self, dialog, account: SEPAAccount, pain_message: str, tan_method, tan_description, multiple, control_sum, currency, book_as_single): @@ -416,38 +421,82 @@ class FinTS3Client: segtan ]) - def start_sepa_transfer(self, account: SEPAAccount, pain_message: str, tan_method, tan_description='', - multiple=False, control_sum=None, currency='EUR', book_as_single=False): + def _get_tan_segment(self, orig_seg, tan_process): + tan_mechanism = self.get_tan_mechanisms()[self.get_current_tan_mechanism()] + + hitans = self.bpd.find_segment_first('HITANS', tan_mechanism.VERSION) + hktan = { + 5: HKTAN5, + }.get(tan_mechanism.VERSION) + + seg = hktan(tan_process=tan_process) + + if tan_process == '1': + seg.segment_type = orig_seg.header.type + account_ = getattr(orig_seg, 'account', None) + if isinstance(account, KTI1): + seg.account = account + raise NotImplementedError("TAN-Process 1 not implemented") + + if tan_process in ('1', '3', '4') and \ + tan_mechanism.supported_media_number > 1 and \ + tan_mechanism.description_required == DescriptionRequired.MUST: + seg.tan_medium_name = self.selected_tan_medium + + return seg + + + def start_sepa_transfer(self, account: SEPAAccount, pain_message: bytes, multiple=False, + control_sum=None, currency='EUR', book_as_single=False, + pain_descriptor='urn:iso:std:iso:20022:tech:xsd:pain.001.001.03'): """ Start a custom SEPA transfer. :param account: SEPAAccount to send the transfer from. :param pain_message: SEPA PAIN message containing the transfer details. - :param tan_method: TANMethod object to use. - :param tan_description: TAN medium description (if required) :param multiple: Whether this message contains multiple transfers. :param control_sum: Sum of all transfers (required if there are multiple) :param currency: Transfer currency :param book_as_single: Kindly ask the bank to put multiple transactions as separate lines on the bank statement (defaults to ``False``) :return: Returns a TANChallenge object """ - dialog = self._get_dialog() - dialog.sync() - dialog.tan_mechs = [tan_method] - dialog.init() - with self.pin.protect(): - logger.debug('Sending: {}'.format(self._get_start_sepa_transfer_message( - dialog, account, pain_message, tan_method, tan_description, multiple, control_sum, currency, - book_as_single - ))) + with self._get_dialog() as dialog: + if multiple: + command_class = HKCCM1 + else: + command_class = HKCCS1 + + hiccxs, hkccx = self._find_highest_supported_command( + HKCCM1 if multiple else HKCCS1, + return_parameter_segment=True + ) - resp = dialog.send(self._get_start_sepa_transfer_message( - dialog, account, pain_message, tan_method, tan_description, multiple, control_sum, currency, - book_as_single - )) - logger.debug('Got response: {}'.format(resp)) - return self._tan_requiring_response(dialog, resp) + seg = hkccx( + account=hkccx._fields['account'].type.from_sepa_account(account), + sepa_descriptor=pain_descriptor, + sepa_pain_message=pain_message, + ) + + if multiple: + if hiccxs.parameter.sum_amount_required and not control_sum: + raise ValueError("Control sum required.") + if book_as_single and not hiccxs.parameter.single_booking_allowed: + # FIXME Only do a warning and fall-back to book_as_single=False? + raise ValueError("Single booking not allowed by bank.") + + if control_sum: + seg.sum_amount.amount = control_sum + seg.sum_amount.currency = currency + + if book_as_single: + seg.request_single_booking = True + + tan_seg = self._get_tan_segment(seg, '4') + response = dialog.send(seg, tan_seg) + + + #return self._tan_requiring_response(dialog, resp) def _get_start_sepa_debit_message(self, dialog, account: SEPAAccount, pain_message: str, tan_method, tan_description, multiple, control_sum, currency, book_as_single): diff --git a/fints/formals.py b/fints/formals.py index 8e3704f..fdd8e52 100644 --- a/fints/formals.py +++ b/fints/formals.py @@ -680,3 +680,11 @@ class ChallengeValidUntil(DataElementGroup): Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Sicherheitsverfahren PIN/TAN""" date = DataElementField(type='dat', _d="Datum") time = DataElementField(type='tim', _d="Uhrzeit") + +class BatchTransferParameter1(DataElementGroup): + """Parameter SEPA-Sammelüberweisung, version 1 + + Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """ + max_transfer_count = DataElementField(type='num', max_length=7, _d="Maximale Anzahl CreditTransferTransactionInformation") + sum_amount_required = DataElementField(type='jn', _d="Summenfeld benötigt") + single_booking_allowed = DataElementField(type='jn', _d="Einzelbuchung erlaubt") diff --git a/fints/segments/transfer.py b/fints/segments/transfer.py index 91e28e1..e62d3f5 100644 --- a/fints/segments/transfer.py +++ b/fints/segments/transfer.py @@ -1,4 +1,9 @@ -from . import FinTS3SegmentOLD +from fints.fields import CodeField, DataElementField, DataElementGroupField +from fints.formals import ( + KTI1, Amount1, BatchTransferParameter1 +) + +from . import FinTS3Segment, ParameterSegment, FinTS3SegmentOLD from ..models import SEPAAccount @@ -23,6 +28,30 @@ class HKCCS(FinTS3SegmentOLD): ] super().__init__(segno, data) +class HKCCS1(FinTS3Segment): + """SEPA Einzelüberweisung, version 1 + + Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """ + account = DataElementGroupField(type=KTI1, _d="Kontoverbindung international") + sepa_descriptor = DataElementField(type='an', max_length=256, _d="SEPA Descriptor") + sepa_pain_message = DataElementField(type='bin', _d="SEPA pain message") + +class HKCCM1(FinTS3Segment): + """SEPA-Sammelüberweisung, version 1 + + Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """ + account = DataElementGroupField(type=KTI1, _d="Kontoverbindung international") + sum_amount = DataElementGroupField(type=Amount1, _d="Summenfeld") + request_single_booking = DataElementField(type='jn', _d="Einzelbuchung gewünscht") + sepa_descriptor = DataElementField(type='an', max_length=256, _d="SEPA Descriptor") + sepa_pain_message = DataElementField(type='bin', _d="SEPA pain message") + +class HICCMS1(ParameterSegment): + """SEPA-Sammelüberweisung Parameter, version 1 + + Source: FinTS Financial Transaction Services, Schnittstellenspezifikation, Messages -- Multibankfähige Geschäftsvorfälle """ + parameter = DataElementGroupField(type=BatchTransferParameter1, _d="Parameter SEPA-Sammelüberweisung") + class HKCCM(FinTS3SegmentOLD): """