Improvements on bills and payment management
This commit is contained in:
parent
13df742284
commit
4c603bf584
5
TODO.md
5
TODO.md
|
@ -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?
|
* Rename pack to plan ? one can have multiple plans?
|
||||||
|
|
||||||
* transaction.process FK?
|
* transaction.process FK?
|
||||||
|
|
||||||
|
* translations
|
||||||
|
from django.utils import translation
|
||||||
|
with translation.override('en'):
|
||||||
|
* Plurals!
|
||||||
|
|
|
@ -14,10 +14,10 @@ def admin_field(method):
|
||||||
kwargs['field'] = args[0] if args else ''
|
kwargs['field'] = args[0] if args else ''
|
||||||
kwargs['order'] = kwargs.get('order', kwargs['field'])
|
kwargs['order'] = kwargs.get('order', kwargs['field'])
|
||||||
kwargs['popup'] = kwargs.get('popup', False)
|
kwargs['popup'] = kwargs.get('popup', False)
|
||||||
kwargs['description'] = kwargs.get('description',
|
kwargs['short_description'] = kwargs.get('short_description',
|
||||||
kwargs['field'].split('__')[-1].replace('_', ' ').capitalize())
|
kwargs['field'].split('__')[-1].replace('_', ' ').capitalize())
|
||||||
admin_method = partial(method, **kwargs)
|
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.allow_tags = True
|
||||||
admin_method.admin_order_field = kwargs['order']
|
admin_method.admin_order_field = kwargs['order']
|
||||||
return admin_method
|
return admin_method
|
||||||
|
|
|
@ -62,8 +62,8 @@ def get_account_items():
|
||||||
if isinstalled('orchestra.apps.payments'):
|
if isinstalled('orchestra.apps.payments'):
|
||||||
url = reverse('admin:payments_transaction_changelist')
|
url = reverse('admin:payments_transaction_changelist')
|
||||||
childrens.append(items.MenuItem(_("Transactions"), url))
|
childrens.append(items.MenuItem(_("Transactions"), url))
|
||||||
url = reverse('admin:payments_paymentprocess_changelist')
|
url = reverse('admin:payments_transactionprocess_changelist')
|
||||||
childrens.append(items.MenuItem(_("Payment processes"), url))
|
childrens.append(items.MenuItem(_("Transaction processes"), url))
|
||||||
url = reverse('admin:payments_paymentsource_changelist')
|
url = reverse('admin:payments_paymentsource_changelist')
|
||||||
childrens.append(items.MenuItem(_("Payment sources"), url))
|
childrens.append(items.MenuItem(_("Payment sources"), url))
|
||||||
if isinstalled('orchestra.apps.issues'):
|
if isinstalled('orchestra.apps.issues'):
|
||||||
|
|
|
@ -93,7 +93,10 @@ def action_to_view(action, modeladmin):
|
||||||
@admin_field
|
@admin_field
|
||||||
def admin_link(*args, **kwargs):
|
def admin_link(*args, **kwargs):
|
||||||
instance = args[-1]
|
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):
|
if not getattr(obj, 'pk', None):
|
||||||
return '---'
|
return '---'
|
||||||
opts = obj._meta
|
opts = obj._meta
|
||||||
|
|
|
@ -104,6 +104,7 @@ class AccountListAdmin(AccountAdmin):
|
||||||
ordering = ('user__username',)
|
ordering = ('user__username',)
|
||||||
|
|
||||||
def select_account(self, instance):
|
def select_account(self, instance):
|
||||||
|
# TODO get query string from request.META['QUERY_STRING'] to preserve filters
|
||||||
context = {
|
context = {
|
||||||
'url': '../?account=' + str(instance.pk),
|
'url': '../?account=' + str(instance.pk),
|
||||||
'name': instance.name
|
'name': instance.name
|
||||||
|
@ -262,7 +263,7 @@ class SelectAccountAdminMixin(AccountAdminMixin):
|
||||||
context.update(extra_context or {})
|
context.update(extra_context or {})
|
||||||
return super(AccountAdminMixin, self).add_view(request,
|
return super(AccountAdminMixin, self).add_view(request,
|
||||||
form_url=form_url, extra_context=context)
|
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):
|
def save_model(self, request, obj, form, change):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import StringIO
|
import StringIO
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
|
from django.contrib import messages
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
@ -34,10 +35,29 @@ view_bill.verbose_name = _("View")
|
||||||
view_bill.url_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):
|
def close_bills(modeladmin, request, queryset):
|
||||||
# TODO confirmation with payment source selection
|
queryset = queryset.filter(status=queryset.model.OPEN)
|
||||||
for bill in queryset:
|
if not queryset:
|
||||||
bill.close()
|
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.verbose_name = _("Close")
|
||||||
close_bills.url_name = 'close'
|
close_bills.url_name = 'close'
|
||||||
|
|
||||||
|
|
34
orchestra/apps/bills/forms.py
Normal file
34
orchestra/apps/bills/forms.py
Normal 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
|
|
@ -17,7 +17,7 @@ class Migration(migrations.Migration):
|
||||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
('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)),
|
('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')])),
|
('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')),
|
('created_on', models.DateTimeField(auto_now_add=True, verbose_name='created on')),
|
||||||
('due_on', models.DateField(null=True, verbose_name='due on', blank=True)),
|
('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')),
|
('last_modified_on', models.DateTimeField(auto_now=True, verbose_name='last modified on')),
|
||||||
|
|
|
@ -7,15 +7,14 @@ from django.db import models, migrations
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('payments', '__first__'),
|
|
||||||
('bills', '0001_initial'),
|
('bills', '0001_initial'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='bill',
|
model_name='bill',
|
||||||
name='payment_source',
|
name='closed_on',
|
||||||
field=models.ForeignKey(blank=True, to='payments.PaymentSource', help_text='Optionally specify a payment source for this bill', null=True, verbose_name='payment source'),
|
field=models.DateTimeField(null=True, verbose_name='closed on', blank=True),
|
||||||
preserve_default=True,
|
preserve_default=True,
|
||||||
),
|
),
|
||||||
]
|
]
|
|
@ -1,4 +1,5 @@
|
||||||
import inspect
|
import inspect
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.template import loader, Context
|
from django.template import loader, Context
|
||||||
|
@ -28,14 +29,12 @@ class Bill(models.Model):
|
||||||
CLOSED = 'CLOSED'
|
CLOSED = 'CLOSED'
|
||||||
SENT = 'SENT'
|
SENT = 'SENT'
|
||||||
PAID = 'PAID'
|
PAID = 'PAID'
|
||||||
RETURNED = 'RETURNED'
|
|
||||||
BAD_DEBT = 'BAD_DEBT'
|
BAD_DEBT = 'BAD_DEBT'
|
||||||
STATUSES = (
|
STATUSES = (
|
||||||
(OPEN, _("Open")),
|
(OPEN, _("Open")),
|
||||||
(CLOSED, _("Closed")),
|
(CLOSED, _("Closed")),
|
||||||
(SENT, _("Sent")),
|
(SENT, _("Sent")),
|
||||||
(PAID, _("Paid")),
|
(PAID, _("Paid")),
|
||||||
(RETURNED, _("Returned")),
|
|
||||||
(BAD_DEBT, _("Bad debt")),
|
(BAD_DEBT, _("Bad debt")),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -51,13 +50,11 @@ class Bill(models.Model):
|
||||||
blank=True)
|
blank=True)
|
||||||
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
|
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
|
||||||
related_name='%(class)s')
|
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)
|
type = models.CharField(_("type"), max_length=16, choices=TYPES)
|
||||||
status = models.CharField(_("status"), max_length=16, choices=STATUSES,
|
status = models.CharField(_("status"), max_length=16, choices=STATUSES,
|
||||||
default=OPEN)
|
default=OPEN)
|
||||||
created_on = models.DateTimeField(_("created on"), auto_now_add=True)
|
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)
|
due_on = models.DateField(_("due on"), null=True, blank=True)
|
||||||
last_modified_on = models.DateTimeField(_("last modified on"), auto_now=True)
|
last_modified_on = models.DateTimeField(_("last modified on"), auto_now=True)
|
||||||
#base = models.DecimalField(max_digits=12, decimal_places=2)
|
#base = models.DecimalField(max_digits=12, decimal_places=2)
|
||||||
|
@ -95,27 +92,40 @@ class Bill(models.Model):
|
||||||
bill_type = self.get_type()
|
bill_type = self.get_type()
|
||||||
if bill_type == 'BILL':
|
if bill_type == 'BILL':
|
||||||
raise TypeError("get_new_number() can not be used on a Bill class")
|
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)
|
prefix = getattr(settings, 'BILLS_%s_NUMBER_PREFIX' % bill_type)
|
||||||
if self.status == self.OPEN:
|
if self.status == self.OPEN:
|
||||||
prefix = 'O{}'.format(prefix)
|
prefix = 'O{}'.format(prefix)
|
||||||
bills = bills.filter(status=self.OPEN)
|
bills = cls.objects.filter(number__regex=r'^%s[1-9]+' % prefix)
|
||||||
num_bills = bills.order_by('-number').first() or 0
|
last_number = bills.order_by('-number').values_list('number', flat=True).first()
|
||||||
if num_bills is not 0:
|
if last_number is None:
|
||||||
num_bills = int(num_bills.number[-number_length:])
|
last_number = 0
|
||||||
else:
|
else:
|
||||||
bills = bills.exclude(status=self.OPEN)
|
last_number = int(last_number[len(prefix)+4:])
|
||||||
num_bills = bills.count()
|
number = last_number + 1
|
||||||
zeros = (number_length - len(str(num_bills))) * '0'
|
year = timezone.now().strftime("%Y")
|
||||||
number = zeros + str(num_bills + 1)
|
number_length = settings.BILLS_NUMBER_LENGTH
|
||||||
|
zeros = (number_length - len(str(number))) * '0'
|
||||||
|
number = zeros + str(number)
|
||||||
self.number = '{prefix}{year}{number}'.format(
|
self.number = '{prefix}{year}{number}'.format(
|
||||||
prefix=prefix, year=year, number=number)
|
prefix=prefix, year=year, number=number)
|
||||||
|
|
||||||
def close(self):
|
def get_due_date(self, payment=None):
|
||||||
self.html = self.render()
|
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.status = self.CLOSED
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
@ -131,13 +141,12 @@ class Bill(models.Model):
|
||||||
('%s.pdf' % self.number, html_to_pdf(self.html), 'application/pdf')
|
('%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.status = self.SENT
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def render(self):
|
def render(self, payment=False):
|
||||||
|
if payment is False:
|
||||||
|
payment = self.account.paymentsources.get_default()
|
||||||
context = Context({
|
context = Context({
|
||||||
'bill': self,
|
'bill': self,
|
||||||
'lines': self.lines.all().prefetch_related('sublines'),
|
'lines': self.lines.all().prefetch_related('sublines'),
|
||||||
|
@ -147,8 +156,12 @@ class Bill(models.Model):
|
||||||
'phone': settings.BILLS_SELLER_PHONE,
|
'phone': settings.BILLS_SELLER_PHONE,
|
||||||
'website': settings.BILLS_SELLER_WEBSITE,
|
'website': settings.BILLS_SELLER_WEBSITE,
|
||||||
'email': settings.BILLS_SELLER_EMAIL,
|
'email': settings.BILLS_SELLER_EMAIL,
|
||||||
|
'bank_account': settings.BILLS_SELLER_BANK_ACCOUNT,
|
||||||
},
|
},
|
||||||
'currency': settings.BILLS_CURRENCY,
|
'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(),
|
template = getattr(settings, 'BILLS_%s_TEMPLATE' % self.get_type(),
|
||||||
settings.BILLS_DEFAULT_TEMPLATE)
|
settings.BILLS_DEFAULT_TEMPLATE)
|
||||||
|
|
|
@ -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_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',
|
BILLS_EMAIL_NOTIFICATION_TEMPLATE = getattr(settings, 'BILLS_EMAIL_NOTIFICATION_TEMPLATE',
|
||||||
|
|
|
@ -8,11 +8,9 @@
|
||||||
|
|
||||||
|
|
||||||
{% block breadcrumbs %}
|
{% block breadcrumbs %}
|
||||||
TODO
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Are you sure you want to close selected bills</h1>
|
<h1>Are you sure you want to close selected bills</h1>
|
||||||
<p>Once a bill is closed it can not be further modified.</p>
|
<p>Once a bill is closed it can not be further modified.</p>
|
||||||
|
@ -20,7 +18,7 @@ TODO
|
||||||
<form action="" method="post">{% csrf_token %}
|
<form action="" method="post">{% csrf_token %}
|
||||||
<div>
|
<div>
|
||||||
<div style="margin:20px;">
|
<div style="margin:20px;">
|
||||||
{{ form.as_admin }}
|
{{ formset }}
|
||||||
</div>
|
</div>
|
||||||
{% for obj in queryset %}
|
{% for obj in queryset %}
|
||||||
<input type="hidden" name="{{ action_checkbox_name }}" value="{{ obj.pk|unlocalize }}" />
|
<input type="hidden" name="{{ action_checkbox_name }}" value="{{ obj.pk|unlocalize }}" />
|
||||||
|
|
|
@ -51,13 +51,13 @@
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#date {
|
#date {
|
||||||
clear: left;
|
clear: left;
|
||||||
clear: right;
|
clear: right;
|
||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
padding-top: 0px;
|
padding-top: 0px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
#text {
|
#text {
|
||||||
|
@ -97,13 +97,13 @@ hr {
|
||||||
<div id="number" class="column-1">
|
<div id="number" class="column-1">
|
||||||
<span id="number-title">Membership Fee</span><br>
|
<span id="number-title">Membership Fee</span><br>
|
||||||
<span id="number-value">{{ bill.number }}</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>
|
||||||
|
|
||||||
<div id="amount" class="column-2">
|
<div id="amount" class="column-2">
|
||||||
<span id="amount-value">{{ bill.get_total }} €</span><br>
|
<span id="amount-value">{{ bill.get_total }} €</span><br>
|
||||||
<span id="amount-note">To pay before {{ bill.due_date }}<br>
|
<span id="amount-note">Due date {{ payment.due_date | default:default_due_date | date }}<br>
|
||||||
on 213.232.322.232.332<br>
|
{% if not payment.message %}On {{ seller_info.bank_account }}{% endif %}<br>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,7 @@
|
||||||
<hr>
|
<hr>
|
||||||
<div id="due-date">
|
<div id="due-date">
|
||||||
<span class="title">DUE DATE</span><br>
|
<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>
|
||||||
<div id="total">
|
<div id="total">
|
||||||
<span class="title">TOTAL</span><br>
|
<span class="title">TOTAL</span><br>
|
||||||
|
@ -54,7 +54,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div id="bill-date">
|
<div id="bill-date">
|
||||||
<span class="title">{{ bill.get_type_display.upper }} DATE</span><br>
|
<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>
|
</div>
|
||||||
<div id="buyer-details">
|
<div id="buyer-details">
|
||||||
|
@ -122,13 +122,13 @@
|
||||||
<div id="footer-column-2">
|
<div id="footer-column-2">
|
||||||
<div id="payment">
|
<div id="payment">
|
||||||
<span class="title">PAYMENT</span>
|
<span class="title">PAYMENT</span>
|
||||||
{% if bill.payment.message %}
|
{% if payment.message %}
|
||||||
{{ bill.payment.message }}
|
{{ payment.message | safe }}
|
||||||
{% else %}
|
{% else %}
|
||||||
You can pay our invoice by bank transfer. <br>
|
You can pay our invoice by bank transfer. <br>
|
||||||
Please make sure to state your name and the invoice number.
|
Please make sure to state your name and the invoice number.
|
||||||
Our bank account number is <br>
|
Our bank account number is <br>
|
||||||
<strong>000-000-000-000 (Orchestra)</strong>
|
<strong>{{ seller_info.bank_account }}</strong>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div id="questions">
|
<div id="questions">
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.apps.bills.models import Invoice, Fee, BillLine, BillSubline
|
from orchestra.apps.bills.models import Invoice, Fee, BillLine, BillSubline
|
||||||
|
|
||||||
|
|
||||||
|
@ -15,9 +17,7 @@ class BillsBackend(object):
|
||||||
rate=service.nominal_price,
|
rate=service.nominal_price,
|
||||||
amount=size,
|
amount=size,
|
||||||
total=nominal_price, tax=0,
|
total=nominal_price, tax=0,
|
||||||
description="{ini} to {end}".format(
|
description=self.format_period(ini, end),
|
||||||
ini=ini.strftime("%b, %Y"),
|
|
||||||
end=(end-datetime.timedelta(seconds=1)).strftime("%b, %Y")),
|
|
||||||
)
|
)
|
||||||
self.create_sublines(line, discounts)
|
self.create_sublines(line, discounts)
|
||||||
bills.append(fee)
|
bills.append(fee)
|
||||||
|
@ -28,9 +28,7 @@ class BillsBackend(object):
|
||||||
bills.append(invoice)
|
bills.append(invoice)
|
||||||
description = order.description
|
description = order.description
|
||||||
if service.billing_period != service.NEVER:
|
if service.billing_period != service.NEVER:
|
||||||
description += " {ini} to {end}".format(
|
description += " %s" % self.format_period(ini, end)
|
||||||
ini=ini.strftime("%b, %Y"),
|
|
||||||
end=(end-datetime.timedelta(seconds=1)).strftime("%b, %Y"))
|
|
||||||
line = invoice.lines.create(
|
line = invoice.lines.create(
|
||||||
description=description,
|
description=description,
|
||||||
rate=service.nominal_price,
|
rate=service.nominal_price,
|
||||||
|
@ -41,6 +39,14 @@ class BillsBackend(object):
|
||||||
self.create_sublines(line, discounts)
|
self.create_sublines(line, discounts)
|
||||||
return bills
|
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):
|
def create_sublines(self, line, discounts):
|
||||||
for name, value in discounts:
|
for name, value in discounts:
|
||||||
line.sublines.create(
|
line.sublines.create(
|
||||||
|
|
|
@ -1,36 +1,61 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.conf.urls import patterns, url
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.shortcuts import render, redirect
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.admin.utils import admin_colored, admin_link
|
from orchestra.admin.utils import admin_colored, admin_link
|
||||||
from orchestra.apps.accounts.admin import AccountAdminMixin
|
from orchestra.apps.accounts.admin import AccountAdminMixin
|
||||||
|
|
||||||
from .actions import process_transactions
|
from .actions import process_transactions
|
||||||
from .methods import SEPADirectDebit
|
from .methods import PaymentMethod
|
||||||
from .models import PaymentSource, Transaction, PaymentProcess
|
from .models import PaymentSource, Transaction, TransactionProcess
|
||||||
|
|
||||||
|
|
||||||
STATE_COLORS = {
|
STATE_COLORS = {
|
||||||
Transaction.WAITTING_PROCESSING: 'darkorange',
|
Transaction.WAITTING_PROCESSING: 'darkorange',
|
||||||
Transaction.WAITTING_CONFIRMATION: 'purple',
|
Transaction.WAITTING_CONFIRMATION: 'magenta',
|
||||||
Transaction.CONFIRMED: 'green',
|
Transaction.CONFIRMED: 'olive',
|
||||||
|
Transaction.SECURED: 'green',
|
||||||
Transaction.REJECTED: 'red',
|
Transaction.REJECTED: 'red',
|
||||||
Transaction.LOCKED: 'magenta',
|
|
||||||
Transaction.DISCARTED: 'blue',
|
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):
|
class TransactionAdmin(AccountAdminMixin, admin.ModelAdmin):
|
||||||
list_display = (
|
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')
|
list_filter = ('source__method', 'state')
|
||||||
actions = (process_transactions,)
|
actions = (process_transactions,)
|
||||||
filter_by_account_fields = ['source']
|
filter_by_account_fields = ['source']
|
||||||
|
readonly_fields = ('process_link', 'account_link')
|
||||||
|
|
||||||
bill_link = admin_link('bill')
|
bill_link = admin_link('bill')
|
||||||
source_link = admin_link('source')
|
source_link = admin_link('source')
|
||||||
|
process_link = admin_link('process', short_description=_("proc"))
|
||||||
account_link = admin_link('bill__account')
|
account_link = admin_link('bill__account')
|
||||||
display_state = admin_colored('state', colors=STATE_COLORS)
|
display_state = admin_colored('state', colors=STATE_COLORS)
|
||||||
|
|
||||||
|
@ -47,14 +72,51 @@ class PaymentSourceAdmin(AccountAdminMixin, admin.ModelAdmin):
|
||||||
if obj:
|
if obj:
|
||||||
self.form = obj.method_class().get_form()
|
self.form = obj.method_class().get_form()
|
||||||
else:
|
else:
|
||||||
self.form = forms.ModelForm
|
self.form = PaymentMethod.get_plugin(self.method)().get_form()
|
||||||
return super(PaymentSourceAdmin, self).get_form(request, obj=obj, **kwargs)
|
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')
|
list_display = ('id', 'file_url', 'display_transactions', 'created_at')
|
||||||
fields = ('data', 'file_url', 'display_transactions', 'created_at')
|
fields = ('data', 'file_url', 'display_transactions', 'created_at')
|
||||||
readonly_fields = ('file_url', 'display_transactions', 'created_at')
|
readonly_fields = ('file_url', 'display_transactions', 'created_at')
|
||||||
|
inlines = [TransactionInline]
|
||||||
|
|
||||||
def file_url(self, process):
|
def file_url(self, process):
|
||||||
if process.file:
|
if process.file:
|
||||||
|
@ -85,4 +147,4 @@ class PaymentProcessAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
admin.site.register(PaymentSource, PaymentSourceAdmin)
|
admin.site.register(PaymentSource, PaymentSourceAdmin)
|
||||||
admin.site.register(Transaction, TransactionAdmin)
|
admin.site.register(Transaction, TransactionAdmin)
|
||||||
admin.site.register(PaymentProcess, PaymentProcessAdmin)
|
admin.site.register(TransactionProcess, TransactionProcessAdmin)
|
||||||
|
|
|
@ -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
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from dateutil import relativedelta
|
||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
from orchestra.utils import plugins
|
from orchestra.utils import plugins
|
||||||
|
@ -9,6 +10,7 @@ class PaymentMethod(plugins.Plugin):
|
||||||
process_credit = False
|
process_credit = False
|
||||||
form = None
|
form = None
|
||||||
serializer = None
|
serializer = None
|
||||||
|
due_delta = relativedelta.relativedelta(months=1)
|
||||||
|
|
||||||
__metaclass__ = plugins.PluginMount
|
__metaclass__ = plugins.PluginMount
|
||||||
|
|
||||||
|
@ -25,6 +27,9 @@ class PaymentMethod(plugins.Plugin):
|
||||||
|
|
||||||
def get_number(self, data):
|
def get_number(self, data):
|
||||||
return data[self.number_field]
|
return data[self.number_field]
|
||||||
|
|
||||||
|
def get_bill_message(self, source):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class PaymentSourceDataForm(forms.ModelForm):
|
class PaymentSourceDataForm(forms.ModelForm):
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import os
|
import datetime
|
||||||
import lxml.builder
|
import lxml.builder
|
||||||
|
import os
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
from lxml.builder import E
|
from lxml.builder import E
|
||||||
from StringIO import StringIO
|
from StringIO import StringIO
|
||||||
|
@ -35,6 +36,11 @@ class SEPADirectDebit(PaymentMethod):
|
||||||
process_credit = True
|
process_credit = True
|
||||||
form = SEPADirectDebitForm
|
form = SEPADirectDebitForm
|
||||||
serializer = SEPADirectDebitSerializer
|
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):
|
def process(self, transactions):
|
||||||
debts = []
|
debts = []
|
||||||
|
@ -50,8 +56,8 @@ class SEPADirectDebit(PaymentMethod):
|
||||||
self._process_credits(credits)
|
self._process_credits(credits)
|
||||||
|
|
||||||
def _process_credits(self, transactions):
|
def _process_credits(self, transactions):
|
||||||
from ..models import PaymentProcess
|
from ..models import TransactionProcess
|
||||||
self.object = PaymentProcess.objects.create()
|
self.process = TransactionProcess.objects.create()
|
||||||
context = self.get_context(transactions)
|
context = self.get_context(transactions)
|
||||||
sepa = lxml.builder.ElementMaker(
|
sepa = lxml.builder.ElementMaker(
|
||||||
nsmap = {
|
nsmap = {
|
||||||
|
@ -63,7 +69,7 @@ class SEPADirectDebit(PaymentMethod):
|
||||||
E.CstmrCdtTrfInitn(
|
E.CstmrCdtTrfInitn(
|
||||||
self._get_header(context),
|
self._get_header(context),
|
||||||
E.PmtInf( # Payment Info
|
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.PmtMtd("TRF"), # Payment Method
|
||||||
E.NbOfTxs(context['num_transactions']), # Number of Transactions
|
E.NbOfTxs(context['num_transactions']), # Number of Transactions
|
||||||
E.CtrlSum(context['total']), # Control Sum
|
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)
|
self._process_xml(sepa, 'pain.001.001.03.xsd', file_name)
|
||||||
|
|
||||||
def _process_debts(self, transactions):
|
def _process_debts(self, transactions):
|
||||||
from ..models import PaymentProcess
|
from ..models import TransactionProcess
|
||||||
self.object = PaymentProcess.objects.create()
|
self.process = TransactionProcess.objects.create()
|
||||||
context = self.get_context(transactions)
|
context = self.get_context(transactions)
|
||||||
sepa = lxml.builder.ElementMaker(
|
sepa = lxml.builder.ElementMaker(
|
||||||
nsmap = {
|
nsmap = {
|
||||||
|
@ -104,7 +110,7 @@ class SEPADirectDebit(PaymentMethod):
|
||||||
E.CstmrDrctDbtInitn(
|
E.CstmrDrctDbtInitn(
|
||||||
self._get_header(context),
|
self._get_header(context),
|
||||||
E.PmtInf( # Payment Info
|
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.PmtMtd("DD"), # Payment Method
|
||||||
E.NbOfTxs(context['num_transactions']), # Number of Transactions
|
E.NbOfTxs(context['num_transactions']), # Number of Transactions
|
||||||
E.CtrlSum(context['total']), # Control Sum
|
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)
|
self._process_xml(sepa, 'pain.008.001.02.xsd', file_name)
|
||||||
|
|
||||||
def get_context(self, transactions):
|
def get_context(self, transactions):
|
||||||
|
@ -153,10 +159,9 @@ class SEPADirectDebit(PaymentMethod):
|
||||||
|
|
||||||
def _get_debt_transactions(self, transactions):
|
def _get_debt_transactions(self, transactions):
|
||||||
for transaction in transactions:
|
for transaction in transactions:
|
||||||
self.object.transactions.add(transaction)
|
transaction.process = self.process
|
||||||
account = transaction.account
|
account = transaction.account
|
||||||
# TODO
|
data = transaction.source.data
|
||||||
data = account.paymentsources.first().data
|
|
||||||
transaction.state = transaction.WAITTING_CONFIRMATION
|
transaction.state = transaction.WAITTING_CONFIRMATION
|
||||||
transaction.save()
|
transaction.save()
|
||||||
yield E.DrctDbtTxInf( # Direct Debit Transaction Info
|
yield E.DrctDbtTxInf( # Direct Debit Transaction Info
|
||||||
|
@ -194,10 +199,9 @@ class SEPADirectDebit(PaymentMethod):
|
||||||
|
|
||||||
def _get_credit_transactions(self, transactions):
|
def _get_credit_transactions(self, transactions):
|
||||||
for transaction in transactions:
|
for transaction in transactions:
|
||||||
self.object.transactions.add(transaction)
|
transaction.process = self.process
|
||||||
account = transaction.account
|
account = transaction.account
|
||||||
# FIXME
|
data = transaction.source.data
|
||||||
data = account.payment_sources.first().data
|
|
||||||
transaction.state = transaction.WAITTING_CONFIRMATION
|
transaction.state = transaction.WAITTING_CONFIRMATION
|
||||||
transaction.save()
|
transaction.save()
|
||||||
yield E.CdtTrfTxInf( # Credit Transfer Transaction Info
|
yield E.CdtTrfTxInf( # Credit Transfer Transaction Info
|
||||||
|
@ -229,7 +233,7 @@ class SEPADirectDebit(PaymentMethod):
|
||||||
|
|
||||||
def _get_header(self, context):
|
def _get_header(self, context):
|
||||||
return E.GrpHdr( # Group Header
|
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
|
E.CreDtTm( # Creation Date Time
|
||||||
context['now'].strftime("%Y-%m-%dT%H:%M:%S")
|
context['now'].strftime("%Y-%m-%dT%H:%M:%S")
|
||||||
),
|
),
|
||||||
|
@ -255,9 +259,9 @@ class SEPADirectDebit(PaymentMethod):
|
||||||
schema = etree.XMLSchema(schema_doc)
|
schema = etree.XMLSchema(schema_doc)
|
||||||
sepa = etree.parse(StringIO(etree.tostring(sepa)))
|
sepa = etree.parse(StringIO(etree.tostring(sepa)))
|
||||||
schema.assertValid(sepa)
|
schema.assertValid(sepa)
|
||||||
self.object.file = file_name
|
self.process.file = file_name
|
||||||
self.object.save()
|
self.process.save()
|
||||||
sepa.write(self.object.file.path,
|
sepa.write(self.process.file.path,
|
||||||
pretty_print=True,
|
pretty_print=True,
|
||||||
xml_declaration=True,
|
xml_declaration=True,
|
||||||
encoding='UTF-8')
|
encoding='UTF-8')
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.utils import timezone
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from jsonfield import JSONField
|
from jsonfield import JSONField
|
||||||
|
@ -12,8 +13,7 @@ from .methods import PaymentMethod
|
||||||
|
|
||||||
|
|
||||||
class PaymentSourcesQueryset(models.QuerySet):
|
class PaymentSourcesQueryset(models.QuerySet):
|
||||||
def get_source(self):
|
def get_default(self):
|
||||||
# TODO
|
|
||||||
return self.filter(is_active=True).first()
|
return self.filter(is_active=True).first()
|
||||||
|
|
||||||
|
|
||||||
|
@ -41,6 +41,15 @@ class PaymentSource(models.Model):
|
||||||
@cached_property
|
@cached_property
|
||||||
def number(self):
|
def number(self):
|
||||||
return self.method_class().get_number(self.data)
|
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):
|
class TransactionQuerySet(models.QuerySet):
|
||||||
|
@ -53,14 +62,14 @@ class Transaction(models.Model):
|
||||||
WAITTING_CONFIRMATION = 'WAITTING_CONFIRMATION'
|
WAITTING_CONFIRMATION = 'WAITTING_CONFIRMATION'
|
||||||
CONFIRMED = 'CONFIRMED'
|
CONFIRMED = 'CONFIRMED'
|
||||||
REJECTED = 'REJECTED'
|
REJECTED = 'REJECTED'
|
||||||
LOCKED = 'LOCKED'
|
|
||||||
DISCARTED = 'DISCARTED'
|
DISCARTED = 'DISCARTED'
|
||||||
|
SECURED = 'SECURED'
|
||||||
STATES = (
|
STATES = (
|
||||||
(WAITTING_PROCESSING, _("Waitting processing")),
|
(WAITTING_PROCESSING, _("Waitting processing")),
|
||||||
(WAITTING_CONFIRMATION, _("Waitting confirmation")),
|
(WAITTING_CONFIRMATION, _("Waitting confirmation")),
|
||||||
(CONFIRMED, _("Confirmed")),
|
(CONFIRMED, _("Confirmed")),
|
||||||
(REJECTED, _("Rejected")),
|
(REJECTED, _("Rejected")),
|
||||||
(LOCKED, _("Locked")),
|
(SECURED, _("Secured")),
|
||||||
(DISCARTED, _("Discarted")),
|
(DISCARTED, _("Discarted")),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -70,6 +79,8 @@ class Transaction(models.Model):
|
||||||
related_name='transactions')
|
related_name='transactions')
|
||||||
source = models.ForeignKey(PaymentSource, null=True, blank=True,
|
source = models.ForeignKey(PaymentSource, null=True, blank=True,
|
||||||
verbose_name=_("source"), related_name='transactions')
|
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,
|
state = models.CharField(_("state"), max_length=32, choices=STATES,
|
||||||
default=WAITTING_PROCESSING)
|
default=WAITTING_PROCESSING)
|
||||||
amount = models.DecimalField(_("amount"), max_digits=12, decimal_places=2)
|
amount = models.DecimalField(_("amount"), max_digits=12, decimal_places=2)
|
||||||
|
@ -85,18 +96,16 @@ class Transaction(models.Model):
|
||||||
return self.bill.account
|
return self.bill.account
|
||||||
|
|
||||||
|
|
||||||
# TODO rename to TransactionProcess or PaymentRequest TransactionRequest
|
class TransactionProcess(models.Model):
|
||||||
class PaymentProcess(models.Model):
|
|
||||||
"""
|
"""
|
||||||
Stores arbitrary data generated by payment methods while processing transactions
|
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)
|
data = JSONField(_("data"), blank=True)
|
||||||
file = models.FileField(_("file"), blank=True)
|
file = models.FileField(_("file"), blank=True)
|
||||||
created_at = models.DateTimeField(_("created at"), auto_now_add=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):
|
def __unicode__(self):
|
||||||
return str(self.id)
|
return str(self.id)
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue