… nein, der Titel dieses Posts soll Python nicht schlecht machen, im Gegenteil, es ist ein nettes Wortspiel in diesem Zusammenhang ;).
Um mal ein wenig in die Programmiersprache Python, die bei Mausbrand verwendet wird, reinzukommen, habe ich mir glaube ich das trockendste Thema überhaupt ausgesucht :-D… ich muss aber zu meiner Verteidigung sagen, dass ich der Meinung bin, man kann eine Programmiersprache nur erfolgreich lernen und einsetzen, wenn man damit auch Programme schreibt, die einen konkreten Nutzen haben und mit denen man auch etwas bewirken kann.
Nun, für mein erstes Python-Programm habe ich mir ausgesucht, dass ich für den Verein eine Schnittstelle benötige, um SEPA-Lastschriften automatisiert per Datei bei der Bank einzureichen. Dies wurde in der Vergangenheit immer händisch gemacht und ist ein langweiliger, undankbarer Job.
Das kleine Skript sepa_pain.py implementiert das Nachrichtenformat SEPA-PAIN Version 008-002 des EBICS (Electronic Banking Internet Communication Standard). Ich muss dazu sagen, dass ich einen Großteil der Logik aus einer kleinen PHP-Library geklaut habe, nur ich wollte mich ja erst mal ein wenig mit Python vertraut machen.
Nun hier ist das Programm! Für Augenkrebs und sonstiges übernehme ich keine Haftung. Ich denke auch, dass ich einiges hätte eleganter machen können, das Problem ist nur, das ich mich dazu noch zu wenig mit Python auskenne um solche Tricks zu kennen.
Daher: Quick&Dirty!
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 |
# Tiny Interface for SEPA-debits using the SEPA-PAIN-008-002-specification # by Jan Max Meyer / bierpilot # # based on https://github.com/michael-dev/php-sepa-lastschrift # # I'm sorry for the bad code but this is my first program I wrote in Python ;) import uuid import re from xml.etree.ElementTree import ElementTree, Element, SubElement, Comment, tostring import time from datetime import date class SEPALastschrift(object): # Konstruktor def __init__(self, datum = date.today(), msgid = str( uuid.uuid1() ).replace("-", "" )[:-5], initiator = "Horst Schlemmer", creditor_name = "Schlemmer AG", creditor_iban = "DE09121688720378475751", creditor_bic = "PBNKDEFF", creditor_id = "DE99XXX00000312547", ccy = "EUR", cdtype = "CORE" ): self.datum = datum self.msgid = msgid self.initiator = initiator self.creditor_name = creditor_name self.creditor_iban = creditor_iban self.creditor_bic = creditor_bic self.creditor_id = creditor_id self.ccy = ccy self.cdtype = cdtype self.txtypes = [ "FRST","RCUR","OOFF","FNAL" ] self.txs = {} self.sums = {} # Lastschrift def addLastschrift( self, type, iban, bic, name, mandat, mandat_sig_date = date.today(), amount = 0.0, subject = "", ultimate_deb = None, id = str( uuid.uuid1() ).replace("-", "" ) ): # Type if not any( type in t for t in self.txtypes ): print( "Type", type, "is not known" ) return False #Mandat if not re.match( "^([A-Za-z0-9]|[\+|\?|/|\-|:|\(|\)|\.|,|']){1,35}$", mandat ): print( "Invalid mandat" ) return False #Name if not re.match( "^[A-Za-z0-9\+\?/\-:\(\)\.,' ]{1,70}$", name ): print( "Invalid name" ) return False #Subject if not re.match( "^[A-Za-z0-9\+\?/\-:\(\)\.,' ]{0,140}$", subject ): print( "Invalid subject" ) return False tx = {} tx[ "id" ] = id tx[ "type" ] = type tx[ "name" ] = name tx[ "iban" ] = iban tx[ "bic" ] = bic tx[ "amount" ] = amount * 100 tx[ "mandat" ] = mandat tx[ "mandat_sig_date" ] = mandat_sig_date tx[ "subject" ] = subject tx[ "ultimate_deb" ] = ultimate_deb if not type in self.txs.keys(): self.txs[ type ] = [] self.txs[ type ].append( tx ) if not type in self.sums.keys(): self.sums[ type ] = tx[ "amount" ] else: self.sums[ type ] += tx[ "amount" ] return True def toXML( self ): painVersion = "008.002.02" painXSDFile = "pain." + painVersion + ".xsd" root = Element( "Document" ) root.set( "xmlns", "urn:iso:std:iso:20022:tech:xsd:pain." + painVersion ) root.set( "xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance" ) root.set( "xsi:schemaLocation", "urn:iso:std:iso:20022:tech:xsd:pain." + painVersion + " " + painXSDFile ) init = SubElement( root, "CstmrDrctDbtInitn" ) # Header header = SubElement( init, "GrpHdr" ) SubElement( header, "MsgId" ).text = self.msgid SubElement( header, "CreDtTm" ).text = time.strftime( '%Y-%m-%dT%H:%M:%SZ' ) cnt = 0 for i in self.sums.keys(): cnt += len( self.txs[ i ] ) SubElement( header, "NbOfTxs" ).text = str( cnt ) sum = 0 for i in self.sums.keys(): sum += self.sums[ i ] SubElement( header, "CtrlSum" ).text = "%d.%02d" % ( sum / 100, sum % 100 ) SubElement( SubElement( header, "InitgPty" ), "Nm" ).text = self.initiator # Payments for type in self.txtypes: if type in self.txs.keys(): #print( "---", type ) # Payment Information Header payment = SubElement( init, "PmtInf") SubElement( payment, "PmtInfId" ).text = self.msgid + "-" + type SubElement( payment, "PmtMtd" ).text = "DD" SubElement( payment, "NbOfTxs" ).text = str( len( self.txs[ type ] ) ) SubElement( payment, "CtrlSum" ).text = "%d.%02d" % ( self.sums[ type ] / 100, self.sums[ type ] % 100 ) info = SubElement( payment, "PmtTpInf" ) SubElement( SubElement( info, "SvcLvl" ), "Cd" ).text = "SEPA" SubElement( SubElement( info, "LclInstrm" ), "Cd" ).text = self.cdtype SubElement( info, "SeqTp" ).text = type SubElement( payment, "ReqdColltnDt" ).text = time.strftime( '%Y-%m-%d' ) SubElement( SubElement( payment, "Cdtr" ), "Nm" ).text = self.creditor_name SubElement( SubElement( SubElement( payment, "CdtrAcct" ), "Id" ), "IBAN" ).text = self.creditor_iban SubElement( SubElement( SubElement( payment, "CdtrAgt" ), "FinInstnId" ), "BIC" ).text = self.creditor_bic SubElement( payment, "ChrgBr" ).text = "SLEV" id = SubElement( SubElement( SubElement( SubElement( payment, "CdtrSchmeId" ), "Id" ), "PrvtId" ), "Othr" ) SubElement( id, "Id" ).text = self.creditor_id SubElement( SubElement( id, "SchmeNm" ), "Prtry" ).text = "SEPA" for tx in self.txs[ type ]: info = SubElement( payment, "DrctDbtTxInf" ) SubElement( SubElement( info, "PmtId" ), "EndToEndId" ).text = tx[ "id" ] SubElement( info, "InstdAmt", Ccy = self.ccy ).text = "%d.%02d" % ( tx[ "amount" ] / 100, tx[ "amount" ] % 100 ) mandat = SubElement( SubElement( info, "DrctDbtTx" ), "MndtRltdInf" ) SubElement( mandat, "MndtId" ).text = tx[ "mandat" ] SubElement( mandat, "DtOfSgntr" ).text = tx[ "mandat_sig_date" ].strftime( "%Y-%m-%d" ) SubElement( SubElement( SubElement( info, "DbtrAgt" ), "FinInstnId" ), "BIC" ).text = tx[ "bic" ] SubElement( SubElement( info, "Dbtr" ), "Nm" ).text = tx[ "name" ] SubElement( SubElement( SubElement( info, "DbtrAcct" ), "Id" ), "IBAN" ).text = tx[ "iban" ] if not tx[ "ultimate_deb" ] is None: SubElement( SubElement( info, "UltmtDbtr" ), "Nm" ).text = tx[ "ultimate_deb" ] SubElement( SubElement( info, "RmtInf" ), "Ustrd" ).text = tx[ "subject" ] return ElementTree( root ) S1 = SEPALastschrift( date.today(), initiator = "Rolph Rautenmann", creditor_name = "Rautenmann Devices", creditor_iban = "DE74110220100221331", creditor_bic = "WELADED1SWT", creditor_id = "DE33XXX000111211" ) S1.addLastschrift( "FRST", "DE17440100461122133211", "PBNKDEFF", "Yussuf Testus", "123", date.today(), 100, "Mitgliedsbeitrag" ) S1.addLastschrift( "FRST", "DE64440100440021313333", "PBNKDEFF", "Bernd Stromberg", "456", date.today(), 100, "Andere sorgen" ) xml = S1.toXML() xml.write("test.xml", encoding="utf-8") |
Das schöne: Die Dateien, die dieses Skript ausgibt, werden sogar im Online-Banking eingelesen und akzeptiert. Es scheint also weitestgehend valide zu sein.
Allerdings muss man ja schon anmerken, dass sich die Finanzbehörde mit dem namen PAIN mal wieder alle Mühe gegeben hat… es ist wirklich schmerzhaft, diese komischen XML-Tags zu entziffern wie z.B. DtOfSgntr oder DrctDbtTxInf… wunderbar! 🙁