django-orchestra/orchestra/apps/payments/methods/sepadirectdebit.py

267 lines
12 KiB
Python

import os
import lxml.builder
from lxml import etree
from lxml.builder import E
from StringIO import StringIO
from django import forms
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from django_iban.forms import IBANFormField
from django_iban.validators import IBANValidator, IBAN_COUNTRY_CODE_LENGTH
from rest_framework import serializers
from .. import settings
from .options import PaymentSourceDataForm, PaymentMethod
class SEPADirectDebitForm(PaymentSourceDataForm):
iban = IBANFormField(label='IBAN',
widget=forms.TextInput(attrs={'size': '50'}))
name = forms.CharField(max_length=128, label=_("Name"),
widget=forms.TextInput(attrs={'size': '50'}))
class SEPADirectDebitSerializer(serializers.Serializer):
iban = serializers.CharField(label='IBAN', validators=[IBANValidator()],
min_length=min(IBAN_COUNTRY_CODE_LENGTH.values()), max_length=34)
name = serializers.CharField(label=_("Name"), max_length=128)
class SEPADirectDebit(PaymentMethod):
verbose_name = _("Direct Debit")
label_field = 'name'
number_field = 'iban'
process_credit = True
form = SEPADirectDebitForm
serializer = SEPADirectDebitSerializer
def process(self, transactions):
debts = []
credits = []
for transaction in transactions:
if transaction.amount < 0:
credits.append(transaction)
else:
debts.append(transaction)
if debts:
self._process_debts(debts)
if credits:
self._process_credits(credits)
def _process_credits(self, transactions):
from ..models import PaymentProcess
self.object = PaymentProcess.objects.create()
context = self.get_context(transactions)
sepa = lxml.builder.ElementMaker(
nsmap = {
'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
None: 'urn:iso:std:iso:20022:tech:xsd:pain.001.001.03',
}
)
sepa = sepa.Document(
E.CstmrCdtTrfInitn(
self._get_header(context),
E.PmtInf( # Payment Info
E.PmtInfId(str(self.object.id)), # Payment Id
E.PmtMtd("TRF"), # Payment Method
E.NbOfTxs(context['num_transactions']), # Number of Transactions
E.CtrlSum(context['total']), # Control Sum
E.ReqdExctnDt ( # Requested Execution Date
context['now'].strftime("%Y-%m-%d")
),
E.Dbtr( # Debtor
E.Nm(context['name'])
),
E.DbtrAcct( # Debtor Account
E.Id(
E.IBAN(context['iban'])
)
),
E.DbtrAgt( # Debtor Agent
E.FinInstnId( # Financial Institution Id
E.BIC(context['bic'])
)
),
*list(self._get_credit_transactions(transactions)) # Transactions
)
)
)
file_name = 'credit-transfer-%i.xml' % self.object.id
self._process_xml(sepa, 'pain.001.001.03.xsd', file_name)
def _process_debts(self, transactions):
from ..models import PaymentProcess
self.object = PaymentProcess.objects.create()
context = self.get_context(transactions)
sepa = lxml.builder.ElementMaker(
nsmap = {
'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
None: 'urn:iso:std:iso:20022:tech:xsd:pain.008.001.02',
}
)
sepa = sepa.Document(
E.CstmrDrctDbtInitn(
self._get_header(context),
E.PmtInf( # Payment Info
E.PmtInfId(str(self.object.id)), # Payment Id
E.PmtMtd("DD"), # Payment Method
E.NbOfTxs(context['num_transactions']), # Number of Transactions
E.CtrlSum(context['total']), # Control Sum
E.PmtTpInf( # Payment Type Info
E.SvcLvl( # Service Level
E.Cd("SEPA") # Code
),
E.LclInstrm( # Local Instrument
E.Cd("CORE") # Code
),
E.SeqTp("RCUR") # Sequence Type
),
E.ReqdColltnDt( # Requested Collection Date
context['now'].strftime("%Y-%m-%d")
),
E.Cdtr( # Creditor
E.Nm(context['name'])
),
E.CdtrAcct( # Creditor Account
E.Id(
E.IBAN(context['iban'])
)
),
E.CdtrAgt( # Creditor Agent
E.FinInstnId( # Financial Institution Id
E.BIC(context['bic'])
)
),
*list(self._get_debt_transactions(transactions)) # Transactions
)
)
)
file_name = 'direct-debit-%i.xml' % self.object.id
self._process_xml(sepa, 'pain.008.001.02.xsd', file_name)
def get_context(self, transactions):
return {
'name': settings.PAYMENTS_DD_CREDITOR_NAME,
'iban': settings.PAYMENTS_DD_CREDITOR_IBAN,
'bic': settings.PAYMENTS_DD_CREDITOR_BIC,
'at02_id': settings.PAYMENTS_DD_CREDITOR_AT02_ID,
'now': timezone.now(),
'total': str(sum([abs(transaction.amount) for transaction in transactions])),
'num_transactions': str(len(transactions)),
}
def _get_debt_transactions(self, transactions):
for transaction in transactions:
self.object.transactions.add(transaction)
# TODO transaction.account
account = transaction.bill.account
# FIXME
data = account.payment_sources.first().data
transaction.state = transaction.WAITTING_CONFIRMATION
transaction.save()
yield E.DrctDbtTxInf( # Direct Debit Transaction Info
E.PmtId( # Payment Id
E.EndToEndId(str(transaction.id)) # Payment Id/End to End
),
E.InstdAmt( # Instructed Amount
str(abs(transaction.amount)),
Ccy=transaction.currency.upper()
),
E.DrctDbtTx( # Direct Debit Transaction
E.MndtRltdInf( # Mandate Related Info
E.MndtId(str(account.id)), # Mandate Id
E.DtOfSgntr( # Date of Signature
account.register_date.strftime("%Y-%m-%d")
)
)
),
E.DbtrAgt( # Debtor Agent
E.FinInstnId( # Financial Institution Id
E.Othr(
E.Id('NOTPROVIDED')
)
)
),
E.Dbtr( # Debtor
E.Nm(account.name), # Name
),
E.DbtrAcct( # Debtor Account
E.Id(
E.IBAN(data['iban'])
),
),
)
def _get_credit_transactions(self, transactions):
for transaction in transactions:
self.object.transactions.add(transaction)
# TODO transaction.account
account = transaction.bill.account
# FIXME
data = account.payment_sources.first().data
transaction.state = transaction.WAITTING_CONFIRMATION
transaction.save()
yield E.CdtTrfTxInf( # Credit Transfer Transaction Info
E.PmtId( # Payment Id
E.EndToEndId(str(transaction.id)) # Payment Id/End to End
),
E.Amt( # Amount
E.InstdAmt( # Instructed Amount
str(abs(transaction.amount)),
Ccy=transaction.currency.upper()
)
),
E.CdtrAgt( # Creditor Agent
E.FinInstnId( # Financial Institution Id
E.Othr(
E.Id('NOTPROVIDED')
)
)
),
E.Cdtr( # Debtor
E.Nm(account.name), # Name
),
E.CdtrAcct( # Creditor Account
E.Id(
E.IBAN(data['iban'])
),
),
)
def _get_header(self, context):
return E.GrpHdr( # Group Header
E.MsgId(str(self.object.id)), # Message Id
E.CreDtTm( # Creation Date Time
context['now'].strftime("%Y-%m-%dT%H:%M:%S")
),
E.NbOfTxs(context['num_transactions']), # Number of Transactions
E.CtrlSum(context['total']), # Control Sum
E.InitgPty( # Initiating Party
E.Nm(context['name']), # Name
E.Id( # Identification
E.OrgId( # Organisation Id
E.Othr(
E.Id(context['at02_id'])
)
)
)
)
)
def _process_xml(self, sepa, xsd, file_name):
# http://www.iso20022.org/documents/messages/1_0_version/pain/schemas/pain.008.001.02.zip
path = os.path.dirname(os.path.realpath(__file__))
xsd_path = os.path.join(path, xsd)
schema_doc = etree.parse(xsd_path)
schema = etree.XMLSchema(schema_doc)
sepa = etree.parse(StringIO(etree.tostring(sepa)))
schema.assertValid(sepa)
self.object.file = file_name
self.object.save()
sepa.write(self.object.file.path,
pretty_print=True,
xml_declaration=True,
encoding='UTF-8')