2014-09-05 14:27:30 +00:00
|
|
|
import datetime
|
2016-02-04 16:05:48 +00:00
|
|
|
import logging
|
2014-09-05 14:27:30 +00:00
|
|
|
import os
|
2015-04-02 16:14:55 +00:00
|
|
|
from io import StringIO
|
2014-07-30 12:55:33 +00:00
|
|
|
|
|
|
|
from django import forms
|
|
|
|
from django.utils import timezone
|
|
|
|
from django.utils.translation import ugettext_lazy as _
|
|
|
|
from django_iban.validators import IBANValidator, IBAN_COUNTRY_CODE_LENGTH
|
|
|
|
from rest_framework import serializers
|
|
|
|
|
2014-11-24 14:39:41 +00:00
|
|
|
from orchestra.plugins.forms import PluginDataForm
|
2014-09-26 21:24:23 +00:00
|
|
|
|
2014-07-30 12:55:33 +00:00
|
|
|
from .. import settings
|
2014-09-26 21:24:23 +00:00
|
|
|
from .options import PaymentMethod
|
2014-07-30 12:55:33 +00:00
|
|
|
|
|
|
|
|
2016-02-04 16:05:48 +00:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
try:
|
|
|
|
import lxml
|
|
|
|
except ImportError:
|
2016-02-09 12:17:42 +00:00
|
|
|
logger.error('Error loading lxml, module not installed.')
|
2016-02-04 16:05:48 +00:00
|
|
|
|
|
|
|
|
2014-09-26 21:24:23 +00:00
|
|
|
class SEPADirectDebitForm(PluginDataForm):
|
2014-11-09 10:16:07 +00:00
|
|
|
iban = forms.CharField(label='IBAN',
|
2015-07-15 12:13:03 +00:00
|
|
|
widget=forms.TextInput(attrs={'size': '50'}))
|
2014-07-30 12:55:33 +00:00
|
|
|
name = forms.CharField(max_length=128, label=_("Name"),
|
2015-07-15 12:13:03 +00:00
|
|
|
widget=forms.TextInput(attrs={'size': '50'}))
|
2014-07-30 12:55:33 +00:00
|
|
|
|
|
|
|
|
2014-08-29 12:45:27 +00:00
|
|
|
class SEPADirectDebitSerializer(serializers.Serializer):
|
2014-07-30 12:55:33 +00:00
|
|
|
iban = serializers.CharField(label='IBAN', validators=[IBANValidator()],
|
2015-07-15 12:13:03 +00:00
|
|
|
min_length=min(IBAN_COUNTRY_CODE_LENGTH.values()), max_length=34)
|
2014-07-30 12:55:33 +00:00
|
|
|
name = serializers.CharField(label=_("Name"), max_length=128)
|
2014-11-09 10:16:07 +00:00
|
|
|
|
|
|
|
def validate(self, data):
|
|
|
|
data['iban'] = data['iban'].strip()
|
|
|
|
data['name'] = data['name'].strip()
|
|
|
|
return data
|
2014-07-30 12:55:33 +00:00
|
|
|
|
|
|
|
|
2014-08-29 12:45:27 +00:00
|
|
|
class SEPADirectDebit(PaymentMethod):
|
2014-09-04 15:55:43 +00:00
|
|
|
verbose_name = _("SEPA Direct Debit")
|
2014-07-30 12:55:33 +00:00
|
|
|
label_field = 'name'
|
|
|
|
number_field = 'iban'
|
2016-04-15 09:56:10 +00:00
|
|
|
allow_recharge = True
|
2014-08-29 12:45:27 +00:00
|
|
|
form = SEPADirectDebitForm
|
|
|
|
serializer = SEPADirectDebitSerializer
|
2014-09-05 14:27:30 +00:00
|
|
|
due_delta = datetime.timedelta(days=5)
|
2015-09-29 12:35:22 +00:00
|
|
|
state_help = {
|
|
|
|
'WAITTING_PROCESSING': _("The transaction is created and requires the generation of "
|
|
|
|
"the SEPA direct debit XML file."),
|
|
|
|
'WAITTING_EXECUTION': _("SEPA Direct Debit XML file is generated but needs to be sent "
|
|
|
|
"to the financial institution."),
|
|
|
|
}
|
2014-09-05 14:27:30 +00:00
|
|
|
|
2015-03-11 16:32:33 +00:00
|
|
|
def get_bill_message(self):
|
2015-07-15 12:13:03 +00:00
|
|
|
context = {
|
|
|
|
'number': self.instance.number
|
|
|
|
}
|
|
|
|
return settings.PAYMENTS_DD_BILL_MESSAGE % context
|
2014-07-30 12:55:33 +00:00
|
|
|
|
2014-09-10 16:53:09 +00:00
|
|
|
@classmethod
|
|
|
|
def process(cls, transactions):
|
2014-07-30 12:55:33 +00:00
|
|
|
debts = []
|
|
|
|
credits = []
|
|
|
|
for transaction in transactions:
|
|
|
|
if transaction.amount < 0:
|
|
|
|
credits.append(transaction)
|
|
|
|
else:
|
|
|
|
debts.append(transaction)
|
2014-09-10 16:53:09 +00:00
|
|
|
processes = []
|
2014-07-30 12:55:33 +00:00
|
|
|
if debts:
|
2014-09-10 16:53:09 +00:00
|
|
|
proc = cls.process_debts(debts)
|
|
|
|
processes.append(proc)
|
2014-07-30 12:55:33 +00:00
|
|
|
if credits:
|
2014-09-10 16:53:09 +00:00
|
|
|
proc = cls.process_credits(credits)
|
|
|
|
processes.append(proc)
|
|
|
|
return processes
|
2014-07-30 12:55:33 +00:00
|
|
|
|
2014-09-10 16:53:09 +00:00
|
|
|
@classmethod
|
|
|
|
def process_credits(cls, transactions):
|
2016-02-04 16:05:48 +00:00
|
|
|
import lxml.builder
|
|
|
|
from lxml.builder import E
|
2014-09-05 14:27:30 +00:00
|
|
|
from ..models import TransactionProcess
|
2014-09-10 16:53:09 +00:00
|
|
|
process = TransactionProcess.objects.create()
|
|
|
|
context = cls.get_context(transactions)
|
2014-09-14 09:52:45 +00:00
|
|
|
# http://businessbanking.bankofireland.com/fs/doc/wysiwyg/b22440-mss130725-pain001-xml-file-structure-dec13.pdf
|
2014-07-30 12:55:33 +00:00
|
|
|
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(
|
2016-04-15 09:56:10 +00:00
|
|
|
cls.get_header(context, process),
|
2014-07-30 12:55:33 +00:00
|
|
|
E.PmtInf( # Payment Info
|
2014-09-14 09:52:45 +00:00
|
|
|
E.PmtInfId(str(process.id)), # Payment Id
|
2014-07-30 12:55:33 +00:00
|
|
|
E.PmtMtd("TRF"), # Payment Method
|
|
|
|
E.NbOfTxs(context['num_transactions']), # Number of Transactions
|
|
|
|
E.CtrlSum(context['total']), # Control Sum
|
2014-09-14 09:52:45 +00:00
|
|
|
E.ReqdExctnDt( # Requested Execution Date
|
|
|
|
(context['now']+datetime.timedelta(days=10)).strftime("%Y-%m-%d")
|
2014-07-30 12:55:33 +00:00
|
|
|
),
|
|
|
|
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'])
|
|
|
|
)
|
|
|
|
),
|
2014-09-10 16:53:09 +00:00
|
|
|
*list(cls.get_credit_transactions(transactions, process)) # Transactions
|
2014-07-30 12:55:33 +00:00
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
2014-09-10 16:53:09 +00:00
|
|
|
file_name = 'credit-transfer-%i.xml' % process.id
|
|
|
|
cls.process_xml(sepa, 'pain.001.001.03.xsd', file_name, process)
|
|
|
|
return process
|
2014-07-30 12:55:33 +00:00
|
|
|
|
2014-09-10 16:53:09 +00:00
|
|
|
@classmethod
|
|
|
|
def process_debts(cls, transactions):
|
2016-02-04 16:05:48 +00:00
|
|
|
import lxml.builder
|
|
|
|
from lxml.builder import E
|
2014-09-05 14:27:30 +00:00
|
|
|
from ..models import TransactionProcess
|
2014-09-10 16:53:09 +00:00
|
|
|
process = TransactionProcess.objects.create()
|
|
|
|
context = cls.get_context(transactions)
|
2014-09-14 09:52:45 +00:00
|
|
|
# http://businessbanking.bankofireland.com/fs/doc/wysiwyg/sepa-direct-debit-pain-008-001-02-xml-file-structure-july-2013.pdf
|
2014-07-30 12:55:33 +00:00
|
|
|
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(
|
2014-09-10 16:53:09 +00:00
|
|
|
cls.get_header(context, process),
|
2014-07-30 12:55:33 +00:00
|
|
|
E.PmtInf( # Payment Info
|
2014-09-14 09:52:45 +00:00
|
|
|
E.PmtInfId(str(process.id)), # Payment Id
|
2014-07-30 12:55:33 +00:00
|
|
|
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'])
|
|
|
|
)
|
|
|
|
),
|
2014-09-10 16:53:09 +00:00
|
|
|
*list(cls.get_debt_transactions(transactions, process)) # Transactions
|
2014-07-30 12:55:33 +00:00
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
2014-09-10 16:53:09 +00:00
|
|
|
file_name = 'direct-debit-%i.xml' % process.id
|
|
|
|
cls.process_xml(sepa, 'pain.008.001.02.xsd', file_name, process)
|
|
|
|
return process
|
2014-07-30 12:55:33 +00:00
|
|
|
|
2014-09-10 16:53:09 +00:00
|
|
|
@classmethod
|
|
|
|
def get_context(cls, transactions):
|
2014-07-30 12:55:33 +00:00
|
|
|
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)),
|
|
|
|
}
|
|
|
|
|
2014-09-10 16:53:09 +00:00
|
|
|
@classmethod
|
|
|
|
def get_debt_transactions(cls, transactions, process):
|
2016-02-04 16:05:48 +00:00
|
|
|
import lxml.builder
|
|
|
|
from lxml.builder import E
|
2014-07-30 12:55:33 +00:00
|
|
|
for transaction in transactions:
|
2014-09-10 16:53:09 +00:00
|
|
|
transaction.process = process
|
2014-10-11 16:21:51 +00:00
|
|
|
transaction.state = transaction.WAITTING_EXECUTION
|
2016-04-07 11:14:44 +00:00
|
|
|
transaction.save(update_fields=('state', 'process', 'modified_at'))
|
2014-09-04 15:55:43 +00:00
|
|
|
account = transaction.account
|
2014-09-05 14:27:30 +00:00
|
|
|
data = transaction.source.data
|
2014-07-30 12:55:33 +00:00
|
|
|
yield E.DrctDbtTxInf( # Direct Debit Transaction Info
|
|
|
|
E.PmtId( # Payment Id
|
2015-06-02 12:59:49 +00:00
|
|
|
E.EndToEndId( # Payment Id/End to End
|
|
|
|
str(transaction.bill.number)+'-'+str(transaction.id)
|
|
|
|
)
|
2014-07-30 12:55:33 +00:00
|
|
|
),
|
|
|
|
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
|
2014-10-11 16:21:51 +00:00
|
|
|
account.date_joined.strftime("%Y-%m-%d")
|
2014-07-30 12:55:33 +00:00
|
|
|
)
|
|
|
|
)
|
|
|
|
),
|
|
|
|
E.DbtrAgt( # Debtor Agent
|
|
|
|
E.FinInstnId( # Financial Institution Id
|
|
|
|
E.Othr(
|
|
|
|
E.Id('NOTPROVIDED')
|
|
|
|
)
|
|
|
|
)
|
|
|
|
),
|
|
|
|
E.Dbtr( # Debtor
|
2015-06-02 12:59:49 +00:00
|
|
|
E.Nm(account.billcontact.get_name()), # Name
|
2014-07-30 12:55:33 +00:00
|
|
|
),
|
|
|
|
E.DbtrAcct( # Debtor Account
|
|
|
|
E.Id(
|
2015-05-30 14:44:05 +00:00
|
|
|
E.IBAN(data['iban'].replace(' ', ''))
|
2014-07-30 12:55:33 +00:00
|
|
|
),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
|
2014-09-10 16:53:09 +00:00
|
|
|
@classmethod
|
2016-04-15 09:56:10 +00:00
|
|
|
def get_credit_transactions(cls, transactions, process):
|
2016-02-04 16:05:48 +00:00
|
|
|
import lxml.builder
|
|
|
|
from lxml.builder import E
|
2014-07-30 12:55:33 +00:00
|
|
|
for transaction in transactions:
|
2014-09-10 16:53:09 +00:00
|
|
|
transaction.process = process
|
2014-10-11 16:21:51 +00:00
|
|
|
transaction.state = transaction.WAITTING_EXECUTION
|
2016-04-07 11:14:44 +00:00
|
|
|
transaction.save(update_fields=('state', 'process', 'modified_at'))
|
2014-09-04 15:55:43 +00:00
|
|
|
account = transaction.account
|
2014-09-05 14:27:30 +00:00
|
|
|
data = transaction.source.data
|
2014-07-30 12:55:33 +00:00
|
|
|
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(
|
2015-05-30 14:44:05 +00:00
|
|
|
E.IBAN(data['iban'].replace(' ', ''))
|
2014-07-30 12:55:33 +00:00
|
|
|
),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
|
2014-09-10 16:53:09 +00:00
|
|
|
@classmethod
|
|
|
|
def get_header(cls, context, process):
|
2016-02-04 16:05:48 +00:00
|
|
|
import lxml.builder
|
|
|
|
from lxml.builder import E
|
2014-07-30 12:55:33 +00:00
|
|
|
return E.GrpHdr( # Group Header
|
2014-09-10 16:53:09 +00:00
|
|
|
E.MsgId(str(process.id)), # Message Id
|
2014-07-30 12:55:33 +00:00
|
|
|
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'])
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
2014-09-10 16:53:09 +00:00
|
|
|
@classmethod
|
|
|
|
def process_xml(cls, sepa, xsd, file_name, process):
|
2016-02-04 16:05:48 +00:00
|
|
|
from lxml import etree
|
2014-07-30 12:55:33 +00:00
|
|
|
# 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)
|
2015-05-30 14:44:05 +00:00
|
|
|
sepa = StringIO(etree.tostring(sepa).decode('utf8'))
|
|
|
|
sepa = etree.parse(sepa)
|
2014-07-30 12:55:33 +00:00
|
|
|
schema.assertValid(sepa)
|
2014-09-10 16:53:09 +00:00
|
|
|
process.file = file_name
|
2014-09-30 16:06:42 +00:00
|
|
|
process.save(update_fields=['file'])
|
2014-09-10 16:53:09 +00:00
|
|
|
sepa.write(process.file.path,
|
2014-07-30 12:55:33 +00:00
|
|
|
pretty_print=True,
|
|
|
|
xml_declaration=True,
|
|
|
|
encoding='UTF-8')
|