Improvements on bills and payment management

This commit is contained in:
Marc 2014-09-05 14:27:30 +00:00
parent 13df742284
commit 4c603bf584
21 changed files with 279 additions and 131 deletions

View file

@ -81,3 +81,8 @@ at + clock time, midnight, noon- At 3:30 p.m., At 4:01, At noon
* Rename pack to plan ? one can have multiple plans?
* transaction.process FK?
* translations
from django.utils import translation
with translation.override('en'):
* Plurals!

View file

@ -14,10 +14,10 @@ def admin_field(method):
kwargs['field'] = args[0] if args else ''
kwargs['order'] = kwargs.get('order', kwargs['field'])
kwargs['popup'] = kwargs.get('popup', False)
kwargs['description'] = kwargs.get('description',
kwargs['short_description'] = kwargs.get('short_description',
kwargs['field'].split('__')[-1].replace('_', ' ').capitalize())
admin_method = partial(method, **kwargs)
admin_method.short_description = kwargs['description']
admin_method.short_description = kwargs['short_description']
admin_method.allow_tags = True
admin_method.admin_order_field = kwargs['order']
return admin_method

View file

@ -62,8 +62,8 @@ def get_account_items():
if isinstalled('orchestra.apps.payments'):
url = reverse('admin:payments_transaction_changelist')
childrens.append(items.MenuItem(_("Transactions"), url))
url = reverse('admin:payments_paymentprocess_changelist')
childrens.append(items.MenuItem(_("Payment processes"), url))
url = reverse('admin:payments_transactionprocess_changelist')
childrens.append(items.MenuItem(_("Transaction processes"), url))
url = reverse('admin:payments_paymentsource_changelist')
childrens.append(items.MenuItem(_("Payment sources"), url))
if isinstalled('orchestra.apps.issues'):

View file

@ -93,7 +93,10 @@ def action_to_view(action, modeladmin):
@admin_field
def admin_link(*args, **kwargs):
instance = args[-1]
obj = get_field_value(instance, kwargs['field'])
if kwargs['field'] in ['id', 'pk', '__unicode__']:
obj = instance
else:
obj = get_field_value(instance, kwargs['field'])
if not getattr(obj, 'pk', None):
return '---'
opts = obj._meta

View file

@ -104,6 +104,7 @@ class AccountListAdmin(AccountAdmin):
ordering = ('user__username',)
def select_account(self, instance):
# TODO get query string from request.META['QUERY_STRING'] to preserve filters
context = {
'url': '../?account=' + str(instance.pk),
'name': instance.name
@ -262,7 +263,7 @@ class SelectAccountAdminMixin(AccountAdminMixin):
context.update(extra_context or {})
return super(AccountAdminMixin, self).add_view(request,
form_url=form_url, extra_context=context)
return HttpResponseRedirect('./select-account/')
return HttpResponseRedirect('./select-account/?%s' % request.META['QUERY_STRING'])
def save_model(self, request, obj, form, change):
"""

View file

@ -1,6 +1,7 @@
import StringIO
import zipfile
from django.contrib import messages
from django.http import HttpResponse
from django.utils.translation import ugettext_lazy as _
@ -34,10 +35,29 @@ view_bill.verbose_name = _("View")
view_bill.url_name = 'view'
from django import forms
from django.forms.models import BaseModelFormSet
from django.forms.formsets import formset_factory
from django.forms.models import modelformset_factory
from django.shortcuts import render
from .forms import SelectPaymentSourceForm
def close_bills(modeladmin, request, queryset):
# TODO confirmation with payment source selection
for bill in queryset:
bill.close()
queryset = queryset.filter(status=queryset.model.OPEN)
if not queryset:
messages.warning(request, _("Selected bills should be in open state"))
return
SelectPaymentSourceFormSet = modelformset_factory(queryset.model, form=SelectPaymentSourceForm, extra=0)
if request.POST.get('action') == 'close_selected_bills':
formset = SelectPaymentSourceFormSet(request.POST, queryset=queryset)
if formset.is_valid():
for form in formset.forms:
form.save()
messages.success(request, _("Selected bills have been closed"))
return
formset = SelectPaymentSourceFormSet(queryset=queryset)
return render(request, 'admin/bills/close_confirmation.html', {'formset': formset})
close_bills.verbose_name = _("Close")
close_bills.url_name = 'close'

View file

@ -0,0 +1,34 @@
from django import forms
from django.utils.translation import ugettext_lazy as _
class SelectPaymentSourceForm(forms.ModelForm):
source = forms.ChoiceField(label=_("Source"), required=False)
class Meta:
fields = ('number', 'source')
def __init__(self, *args, **kwargs):
super(SelectPaymentSourceForm, self).__init__(*args, **kwargs)
bill = kwargs.get('instance')
if bill:
sources = bill.account.paymentsources.filter(is_active=True)
recharge = bool(bill.get_total() < 0)
choices = [(None, '-----------')]
for source in sources:
if not recharge or source.method_class().allow_recharge:
choices.append((source.pk, str(source)))
self.fields['source'].choices = choices
def clean_source(self):
source_id = self.cleaned_data['source']
if not source_id:
return None
source_model = self.instance.account.paymentsources.model
return source_model.objects.get(id=source_id)
def save(self, commit=True):
if commit:
source = self.cleaned_data['source']
self.instance.close(payment=source)
return self.instance

View file

@ -17,7 +17,7 @@ class Migration(migrations.Migration):
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('number', models.CharField(unique=True, max_length=16, verbose_name='number', blank=True)),
('type', models.CharField(max_length=16, verbose_name='type', choices=[(b'INVOICE', 'Invoice'), (b'AMENDMENTINVOICE', 'Amendment invoice'), (b'FEE', 'Fee'), (b'AMENDMENTFEE', 'Amendment Fee'), (b'BUDGET', 'Budget')])),
('status', models.CharField(default=b'OPEN', max_length=16, verbose_name='status', choices=[(b'OPEN', 'Open'), (b'CLOSED', 'Closed'), (b'SENT', 'Sent'), (b'PAID', 'Paid'), (b'RETURNED', 'Returned'), (b'BAD_DEBT', 'Bad debt')])),
('status', models.CharField(default=b'OPEN', max_length=16, verbose_name='status', choices=[(b'OPEN', 'Open'), (b'CLOSED', 'Closed'), (b'SENT', 'Sent'), (b'PAID', 'Paid'), (b'BAD_DEBT', 'Bad debt')])),
('created_on', models.DateTimeField(auto_now_add=True, verbose_name='created on')),
('due_on', models.DateField(null=True, verbose_name='due on', blank=True)),
('last_modified_on', models.DateTimeField(auto_now=True, verbose_name='last modified on')),

View file

@ -7,15 +7,14 @@ from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('payments', '__first__'),
('bills', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='bill',
name='payment_source',
field=models.ForeignKey(blank=True, to='payments.PaymentSource', help_text='Optionally specify a payment source for this bill', null=True, verbose_name='payment source'),
name='closed_on',
field=models.DateTimeField(null=True, verbose_name='closed on', blank=True),
preserve_default=True,
),
]

View file

@ -1,4 +1,5 @@
import inspect
from dateutil.relativedelta import relativedelta
from django.db import models
from django.template import loader, Context
@ -28,14 +29,12 @@ class Bill(models.Model):
CLOSED = 'CLOSED'
SENT = 'SENT'
PAID = 'PAID'
RETURNED = 'RETURNED'
BAD_DEBT = 'BAD_DEBT'
STATUSES = (
(OPEN, _("Open")),
(CLOSED, _("Closed")),
(SENT, _("Sent")),
(PAID, _("Paid")),
(RETURNED, _("Returned")),
(BAD_DEBT, _("Bad debt")),
)
@ -51,13 +50,11 @@ class Bill(models.Model):
blank=True)
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
related_name='%(class)s')
payment_source = models.ForeignKey('payments.PaymentSource', null=True,
verbose_name=_("payment source"),
help_text=_("Optionally specify a payment source for this bill"))
type = models.CharField(_("type"), max_length=16, choices=TYPES)
status = models.CharField(_("status"), max_length=16, choices=STATUSES,
default=OPEN)
created_on = models.DateTimeField(_("created on"), auto_now_add=True)
closed_on = models.DateTimeField(_("closed on"), blank=True, null=True)
due_on = models.DateField(_("due on"), null=True, blank=True)
last_modified_on = models.DateTimeField(_("last modified on"), auto_now=True)
#base = models.DecimalField(max_digits=12, decimal_places=2)
@ -95,27 +92,40 @@ class Bill(models.Model):
bill_type = self.get_type()
if bill_type == 'BILL':
raise TypeError("get_new_number() can not be used on a Bill class")
# Bill number resets every natural year
year = timezone.now().strftime("%Y")
bills = cls.objects.filter(created_on__year=year)
number_length = settings.BILLS_NUMBER_LENGTH
prefix = getattr(settings, 'BILLS_%s_NUMBER_PREFIX' % bill_type)
if self.status == self.OPEN:
prefix = 'O{}'.format(prefix)
bills = bills.filter(status=self.OPEN)
num_bills = bills.order_by('-number').first() or 0
if num_bills is not 0:
num_bills = int(num_bills.number[-number_length:])
bills = cls.objects.filter(number__regex=r'^%s[1-9]+' % prefix)
last_number = bills.order_by('-number').values_list('number', flat=True).first()
if last_number is None:
last_number = 0
else:
bills = bills.exclude(status=self.OPEN)
num_bills = bills.count()
zeros = (number_length - len(str(num_bills))) * '0'
number = zeros + str(num_bills + 1)
last_number = int(last_number[len(prefix)+4:])
number = last_number + 1
year = timezone.now().strftime("%Y")
number_length = settings.BILLS_NUMBER_LENGTH
zeros = (number_length - len(str(number))) * '0'
number = zeros + str(number)
self.number = '{prefix}{year}{number}'.format(
prefix=prefix, year=year, number=number)
def close(self):
self.html = self.render()
def get_due_date(self, payment=None):
now = timezone.now()
if payment:
return now + payment.get_due_delta()
return now + relativedelta(months=1)
def close(self, payment=False):
assert self.status == self.OPEN, "Bill not in Open state"
if payment is False:
payment = self.account.paymentsources.get_default()
if not self.due_on:
self.due_on = self.get_due_date(payment=payment)
self.html = self.render(payment=payment)
self.transactions.create(
bill=self, source=payment, amount=self.get_total()
)
self.closed_on = timezone.now()
self.status = self.CLOSED
self.save()
@ -131,13 +141,12 @@ class Bill(models.Model):
('%s.pdf' % self.number, html_to_pdf(self.html), 'application/pdf')
]
)
self.transactions.create(
bill=self, source=self.payment_source, amount=self.get_total()
)
self.status = self.SENT
self.save()
def render(self):
def render(self, payment=False):
if payment is False:
payment = self.account.paymentsources.get_default()
context = Context({
'bill': self,
'lines': self.lines.all().prefetch_related('sublines'),
@ -147,8 +156,12 @@ class Bill(models.Model):
'phone': settings.BILLS_SELLER_PHONE,
'website': settings.BILLS_SELLER_WEBSITE,
'email': settings.BILLS_SELLER_EMAIL,
'bank_account': settings.BILLS_SELLER_BANK_ACCOUNT,
},
'currency': settings.BILLS_CURRENCY,
'payment': payment and payment.get_bill_context(),
'default_due_date': self.get_due_date(payment=payment),
'now': timezone.now(),
})
template = getattr(settings, 'BILLS_%s_TEMPLATE' % self.get_type(),
settings.BILLS_DEFAULT_TEMPLATE)

View file

@ -30,6 +30,7 @@ BILLS_SELLER_EMAIL = getattr(settings, 'BILLS_SELLER_EMAIL', 'sales@orchestra.la
BILLS_SELLER_WEBSITE = getattr(settings, 'BILLS_SELLER_WEBSITE', 'www.orchestra.lan')
BILLS_SELLER_BANK_ACCOUNT = getattr(settings, 'BILLS_SELLER_BANK_ACCOUNT', '0000 0000 00 00000000 (Orchestra Bank)')
BILLS_EMAIL_NOTIFICATION_TEMPLATE = getattr(settings, 'BILLS_EMAIL_NOTIFICATION_TEMPLATE',

View file

@ -8,11 +8,9 @@
{% block breadcrumbs %}
TODO
{% endblock %}
{% block content %}
<h1>Are you sure you want to close selected bills</h1>
<p>Once a bill is closed it can not be further modified.</p>
@ -20,7 +18,7 @@ TODO
<form action="" method="post">{% csrf_token %}
<div>
<div style="margin:20px;">
{{ form.as_admin }}
{{ formset }}
</div>
{% for obj in queryset %}
<input type="hidden" name="{{ action_checkbox_name }}" value="{{ obj.pk|unlocalize }}" />

View file

@ -51,13 +51,13 @@
font-weight: bold;
}
#date {
clear: left;
clear: right;
margin-top: 0px;
padding-top: 0px;
font-weight: bold;
color: #666;
}
#text {
@ -97,13 +97,13 @@ hr {
<div id="number" class="column-1">
<span id="number-title">Membership Fee</span><br>
<span id="number-value">{{ bill.number }}</span><br>
<span id="number-date">{{ bill.created_on | date }}</span><br>
<span id="number-date">{{ bill.closed_on | default:now | date }}</span><br>
</div>
<div id="amount" class="column-2">
<span id="amount-value">{{ bill.get_total }} &euro;</span><br>
<span id="amount-note">To pay before {{ bill.due_date }}<br>
on 213.232.322.232.332<br>
<span id="amount-note">Due date {{ payment.due_date | default:default_due_date | date }}<br>
{% if not payment.message %}On {{ seller_info.bank_account }}{% endif %}<br>
</span>
</div>

View file

@ -46,7 +46,7 @@
<hr>
<div id="due-date">
<span class="title">DUE DATE</span><br>
<psan class="value">{{ bill.due_on|date }}</span>
<psan class="value">{{ bill.due_on | default:default_due_date | date }}</span>
</div>
<div id="total">
<span class="title">TOTAL</span><br>
@ -54,7 +54,7 @@
</div>
<div id="bill-date">
<span class="title">{{ bill.get_type_display.upper }} DATE</span><br>
<psan class="value">{{ bill.created_on|date }}</span>
<psan class="value">{{ bill.closed_on | default:now | date }}</span>
</div>
</div>
<div id="buyer-details">
@ -122,13 +122,13 @@
<div id="footer-column-2">
<div id="payment">
<span class="title">PAYMENT</span>
{% if bill.payment.message %}
{{ bill.payment.message }}
{% if payment.message %}
{{ payment.message | safe }}
{% else %}
You can pay our invoice by bank transfer. <br>
Please make sure to state your name and the invoice number.
Our bank account number is <br>
<strong>000-000-000-000 (Orchestra)</strong>
<strong>{{ seller_info.bank_account }}</strong>
{% endif %}
</div>
<div id="questions">

View file

@ -1,5 +1,7 @@
import datetime
from django.utils.translation import ugettext_lazy as _
from orchestra.apps.bills.models import Invoice, Fee, BillLine, BillSubline
@ -15,9 +17,7 @@ class BillsBackend(object):
rate=service.nominal_price,
amount=size,
total=nominal_price, tax=0,
description="{ini} to {end}".format(
ini=ini.strftime("%b, %Y"),
end=(end-datetime.timedelta(seconds=1)).strftime("%b, %Y")),
description=self.format_period(ini, end),
)
self.create_sublines(line, discounts)
bills.append(fee)
@ -28,9 +28,7 @@ class BillsBackend(object):
bills.append(invoice)
description = order.description
if service.billing_period != service.NEVER:
description += " {ini} to {end}".format(
ini=ini.strftime("%b, %Y"),
end=(end-datetime.timedelta(seconds=1)).strftime("%b, %Y"))
description += " %s" % self.format_period(ini, end)
line = invoice.lines.create(
description=description,
rate=service.nominal_price,
@ -41,6 +39,14 @@ class BillsBackend(object):
self.create_sublines(line, discounts)
return bills
def format_period(self, ini, end):
ini = ini=ini.strftime("%b, %Y")
end = (end-datetime.timedelta(seconds=1)).strftime("%b, %Y")
if ini == end:
return ini
return _("{ini} to {end}").format(ini=ini, end=end)
def create_sublines(self, line, discounts):
for name, value in discounts:
line.sublines.create(

View file

@ -1,36 +1,61 @@
from django import forms
from django.conf.urls import patterns, url
from django.contrib import admin
from django.core.urlresolvers import reverse
from django.shortcuts import render, redirect
from django.utils.translation import ugettext_lazy as _
from orchestra.admin.utils import admin_colored, admin_link
from orchestra.apps.accounts.admin import AccountAdminMixin
from .actions import process_transactions
from .methods import SEPADirectDebit
from .models import PaymentSource, Transaction, PaymentProcess
from .methods import PaymentMethod
from .models import PaymentSource, Transaction, TransactionProcess
STATE_COLORS = {
Transaction.WAITTING_PROCESSING: 'darkorange',
Transaction.WAITTING_CONFIRMATION: 'purple',
Transaction.CONFIRMED: 'green',
Transaction.WAITTING_CONFIRMATION: 'magenta',
Transaction.CONFIRMED: 'olive',
Transaction.SECURED: 'green',
Transaction.REJECTED: 'red',
Transaction.LOCKED: 'magenta',
Transaction.DISCARTED: 'blue',
}
class TransactionInline(admin.TabularInline):
model = Transaction
can_delete = False
extra = 0
fields = ('transaction_link', 'bill_link', 'source_link', 'display_state', 'amount', 'currency')
readonly_fields = fields
transaction_link = admin_link('__unicode__', short_description=_("ID"))
bill_link = admin_link('bill')
source_link = admin_link('source')
display_state = admin_colored('state', colors=STATE_COLORS)
class Media:
css = {
'all': ('orchestra/css/hide-inline-id.css',)
}
def has_add_permission(self, *args, **kwargs):
return False
class TransactionAdmin(AccountAdminMixin, admin.ModelAdmin):
list_display = (
'id', 'bill_link', 'account_link', 'source_link', 'display_state', 'amount'
'id', 'bill_link', 'account_link', 'source_link', 'display_state', 'amount', 'process_link'
)
list_filter = ('source__method', 'state')
actions = (process_transactions,)
filter_by_account_fields = ['source']
readonly_fields = ('process_link', 'account_link')
bill_link = admin_link('bill')
source_link = admin_link('source')
process_link = admin_link('process', short_description=_("proc"))
account_link = admin_link('bill__account')
display_state = admin_colored('state', colors=STATE_COLORS)
@ -47,14 +72,51 @@ class PaymentSourceAdmin(AccountAdminMixin, admin.ModelAdmin):
if obj:
self.form = obj.method_class().get_form()
else:
self.form = forms.ModelForm
self.form = PaymentMethod.get_plugin(self.method)().get_form()
return super(PaymentSourceAdmin, self).get_form(request, obj=obj, **kwargs)
def get_urls(self):
""" Hooks select account url """
urls = super(PaymentSourceAdmin, self).get_urls()
admin_site = self.admin_site
opts = self.model._meta
info = opts.app_label, opts.model_name
select_urls = patterns("",
url("/select-method/$",
self.select_method_view,
name='%s_%s_select_method' % info),
)
return select_urls + urls
def select_method_view(self, request):
context = {
'methods': PaymentMethod.get_plugin_choices(),
}
return render(request, 'admin/payments/payment_source/select_method.html', context)
def add_view(self, request, form_url='', extra_context=None):
""" Redirects to select account view if required """
if request.user.is_superuser:
method = request.GET.get('method')
if method or PaymentMethod.get_plugins() == 1:
self.method = method
if not method:
self.method = PaymentMethod.get_plugins()[0]
return super(PaymentSourceAdmin, self).add_view(request,
form_url=form_url, extra_context=extra_context)
return redirect('./select-method/?%s' % request.META['QUERY_STRING'])
def save_model(self, request, obj, form, change):
if not change:
obj.method = self.method
obj.save()
class PaymentProcessAdmin(admin.ModelAdmin):
class TransactionProcessAdmin(admin.ModelAdmin):
list_display = ('id', 'file_url', 'display_transactions', 'created_at')
fields = ('data', 'file_url', 'display_transactions', 'created_at')
readonly_fields = ('file_url', 'display_transactions', 'created_at')
inlines = [TransactionInline]
def file_url(self, process):
if process.file:
@ -85,4 +147,4 @@ class PaymentProcessAdmin(admin.ModelAdmin):
admin.site.register(PaymentSource, PaymentSourceAdmin)
admin.site.register(Transaction, TransactionAdmin)
admin.site.register(PaymentProcess, PaymentProcessAdmin)
admin.site.register(TransactionProcess, TransactionProcessAdmin)

View file

@ -1,40 +0,0 @@
from django import forms
from django.utils.translation import ugettext_lazy as _
# TODO this is for the billing phase
class TransactionCreationForm(forms.ModelForm):
# transaction_link = forms.CharField()
# account_link = forms.CharField()
# bill_link = forms.CharField()
source = forms.ChoiceField(required=False)
# exclude = forms.BooleanField(required=False)
# class Meta:
# model = Bill ?
def __init__(self, *args, **kwargs):
super(SourceSelectionForm, self).__init__(*args, **kwargs)
bill = kwargs.get('instance')
if bill:
sources = bill.account.payment_sources.filter(is_active=True)
choices = []
for source in sources:
if bill.ammount < 0:
if source.method_class().allow_recharge:
choices.append((source.method, source.method_display()))
else:
choices.append((source.method, source.method_display()))
self.fields['source'].choices = choices
# def clean(self):
# cleaned_data = super(SourceSelectionForm, self).clean()
# method = cleaned_data.get("method")
# exclude = cleaned_data.get("exclude")
# if not method and not exclude:
# raise forms.ValidationError(_("A transaction should be explicitly "
# "excluded when no payment source is available."))
class ProcessTransactionForm(forms.ModelForm):
pass

View file

@ -1,3 +1,4 @@
from dateutil import relativedelta
from django import forms
from orchestra.utils import plugins
@ -9,6 +10,7 @@ class PaymentMethod(plugins.Plugin):
process_credit = False
form = None
serializer = None
due_delta = relativedelta.relativedelta(months=1)
__metaclass__ = plugins.PluginMount
@ -25,6 +27,9 @@ class PaymentMethod(plugins.Plugin):
def get_number(self, data):
return data[self.number_field]
def get_bill_message(self, source):
raise NotImplementedError
class PaymentSourceDataForm(forms.ModelForm):

View file

@ -1,5 +1,6 @@
import os
import datetime
import lxml.builder
import os
from lxml import etree
from lxml.builder import E
from StringIO import StringIO
@ -35,6 +36,11 @@ class SEPADirectDebit(PaymentMethod):
process_credit = True
form = SEPADirectDebitForm
serializer = SEPADirectDebitSerializer
due_delta = datetime.timedelta(days=5)
def get_bill_message(self, source):
return _("This bill will been automatically charged to your bank account "
" with IBAN number<br><strong>%s</strong>.") % source.number
def process(self, transactions):
debts = []
@ -50,8 +56,8 @@ class SEPADirectDebit(PaymentMethod):
self._process_credits(credits)
def _process_credits(self, transactions):
from ..models import PaymentProcess
self.object = PaymentProcess.objects.create()
from ..models import TransactionProcess
self.process = TransactionProcess.objects.create()
context = self.get_context(transactions)
sepa = lxml.builder.ElementMaker(
nsmap = {
@ -63,7 +69,7 @@ class SEPADirectDebit(PaymentMethod):
E.CstmrCdtTrfInitn(
self._get_header(context),
E.PmtInf( # Payment Info
E.PmtInfId(str(self.object.id)), # Payment Id
E.PmtInfId(str(self.process.id)), # Payment Id
E.PmtMtd("TRF"), # Payment Method
E.NbOfTxs(context['num_transactions']), # Number of Transactions
E.CtrlSum(context['total']), # Control Sum
@ -87,12 +93,12 @@ class SEPADirectDebit(PaymentMethod):
)
)
)
file_name = 'credit-transfer-%i.xml' % self.object.id
file_name = 'credit-transfer-%i.xml' % self.process.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()
from ..models import TransactionProcess
self.process = TransactionProcess.objects.create()
context = self.get_context(transactions)
sepa = lxml.builder.ElementMaker(
nsmap = {
@ -104,7 +110,7 @@ class SEPADirectDebit(PaymentMethod):
E.CstmrDrctDbtInitn(
self._get_header(context),
E.PmtInf( # Payment Info
E.PmtInfId(str(self.object.id)), # Payment Id
E.PmtInfId(str(self.process.id)), # Payment Id
E.PmtMtd("DD"), # Payment Method
E.NbOfTxs(context['num_transactions']), # Number of Transactions
E.CtrlSum(context['total']), # Control Sum
@ -137,7 +143,7 @@ class SEPADirectDebit(PaymentMethod):
)
)
)
file_name = 'direct-debit-%i.xml' % self.object.id
file_name = 'direct-debit-%i.xml' % self.process.id
self._process_xml(sepa, 'pain.008.001.02.xsd', file_name)
def get_context(self, transactions):
@ -153,10 +159,9 @@ class SEPADirectDebit(PaymentMethod):
def _get_debt_transactions(self, transactions):
for transaction in transactions:
self.object.transactions.add(transaction)
transaction.process = self.process
account = transaction.account
# TODO
data = account.paymentsources.first().data
data = transaction.source.data
transaction.state = transaction.WAITTING_CONFIRMATION
transaction.save()
yield E.DrctDbtTxInf( # Direct Debit Transaction Info
@ -194,10 +199,9 @@ class SEPADirectDebit(PaymentMethod):
def _get_credit_transactions(self, transactions):
for transaction in transactions:
self.object.transactions.add(transaction)
transaction.process = self.process
account = transaction.account
# FIXME
data = account.payment_sources.first().data
data = transaction.source.data
transaction.state = transaction.WAITTING_CONFIRMATION
transaction.save()
yield E.CdtTrfTxInf( # Credit Transfer Transaction Info
@ -229,7 +233,7 @@ class SEPADirectDebit(PaymentMethod):
def _get_header(self, context):
return E.GrpHdr( # Group Header
E.MsgId(str(self.object.id)), # Message Id
E.MsgId(str(self.process.id)), # Message Id
E.CreDtTm( # Creation Date Time
context['now'].strftime("%Y-%m-%dT%H:%M:%S")
),
@ -255,9 +259,9 @@ class SEPADirectDebit(PaymentMethod):
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,
self.process.file = file_name
self.process.save()
sepa.write(self.process.file.path,
pretty_print=True,
xml_declaration=True,
encoding='UTF-8')

View file

@ -1,5 +1,6 @@
from django.core.exceptions import ValidationError
from django.db import models
from django.utils import timezone
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from jsonfield import JSONField
@ -12,8 +13,7 @@ from .methods import PaymentMethod
class PaymentSourcesQueryset(models.QuerySet):
def get_source(self):
# TODO
def get_default(self):
return self.filter(is_active=True).first()
@ -41,6 +41,15 @@ class PaymentSource(models.Model):
@cached_property
def number(self):
return self.method_class().get_number(self.data)
def get_bill_context(self):
method = self.method_class()
return {
'message': method.get_bill_message(self),
}
def get_due_delta(self):
return self.method_class().due_delta
class TransactionQuerySet(models.QuerySet):
@ -53,14 +62,14 @@ class Transaction(models.Model):
WAITTING_CONFIRMATION = 'WAITTING_CONFIRMATION'
CONFIRMED = 'CONFIRMED'
REJECTED = 'REJECTED'
LOCKED = 'LOCKED'
DISCARTED = 'DISCARTED'
SECURED = 'SECURED'
STATES = (
(WAITTING_PROCESSING, _("Waitting processing")),
(WAITTING_CONFIRMATION, _("Waitting confirmation")),
(CONFIRMED, _("Confirmed")),
(REJECTED, _("Rejected")),
(LOCKED, _("Locked")),
(SECURED, _("Secured")),
(DISCARTED, _("Discarted")),
)
@ -70,6 +79,8 @@ class Transaction(models.Model):
related_name='transactions')
source = models.ForeignKey(PaymentSource, null=True, blank=True,
verbose_name=_("source"), related_name='transactions')
process = models.ForeignKey('payments.TransactionProcess', null=True,
blank=True, verbose_name=_("process"), related_name='transactions')
state = models.CharField(_("state"), max_length=32, choices=STATES,
default=WAITTING_PROCESSING)
amount = models.DecimalField(_("amount"), max_digits=12, decimal_places=2)
@ -85,18 +96,16 @@ class Transaction(models.Model):
return self.bill.account
# TODO rename to TransactionProcess or PaymentRequest TransactionRequest
class PaymentProcess(models.Model):
class TransactionProcess(models.Model):
"""
Stores arbitrary data generated by payment methods while processing transactions
"""
transactions = models.ManyToManyField(Transaction, related_name='processes',
verbose_name=_("transactions"))
data = JSONField(_("data"), blank=True)
file = models.FileField(_("file"), blank=True)
created_at = models.DateTimeField(_("created at"), auto_now_add=True)
# TODO state: created, commited, secured (delayed persistence)
class Meta:
verbose_name_plural = _("Transaction processes")
def __unicode__(self):
return str(self.id)

View file

@ -0,0 +1,28 @@
{% extends "admin/base_site.html" %}
{% load i18n l10n staticfiles admin_urls %}
{% block extrastyle %}
{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static "admin/css/forms.css" %}" />
{% endblock %}
{% block breadcrumbs %}
TODO
{% endblock %}
{% block content %}
<h1>Select a method for the new payment source</h1>
<form action="" method="post">{% csrf_token %}
<div>
<div style="margin:20px;">
<ul>
{% for name, verbose in methods %}
<li><a href="../?method={{ name }}&{{ request.META.QUERY_STRING }}">{{ verbose }}</<a></li>
{% endfor %}
</ul>
</div>
{% endblock %}