Refactoring payment process
This commit is contained in:
parent
721bcd9002
commit
1456c457fc
|
@ -2,9 +2,12 @@ from functools import wraps, partial
|
|||
|
||||
from django.contrib import messages
|
||||
from django.contrib.admin import helpers
|
||||
from django.template.response import TemplateResponse
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.decorators import available_attrs
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.html import format_html
|
||||
from django.utils.text import capfirst
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
def admin_field(method):
|
||||
|
@ -24,6 +27,17 @@ def admin_field(method):
|
|||
return admin_field_wrapper
|
||||
|
||||
|
||||
def format_display_objects(modeladmin, request, queryset):
|
||||
from .utils import change_url
|
||||
opts = modeladmin.model._meta
|
||||
objects = []
|
||||
for obj in queryset:
|
||||
objects.append(format_html('{0}: <a href="{1}">{2}</a>',
|
||||
capfirst(opts.verbose_name), change_url(obj), obj)
|
||||
)
|
||||
return objects
|
||||
|
||||
|
||||
def action_with_confirmation(action_name=None, extra_context={},
|
||||
template='admin/orchestra/generic_confirmation.html'):
|
||||
"""
|
||||
|
@ -31,11 +45,12 @@ def action_with_confirmation(action_name=None, extra_context={},
|
|||
If custom template is provided the form must contain:
|
||||
<input type="hidden" name="post" value="generic_confirmation" />
|
||||
"""
|
||||
|
||||
def decorator(func, extra_context=extra_context, template=template, action_name=action_name):
|
||||
@wraps(func, assigned=available_attrs(func))
|
||||
def inner(modeladmin, request, queryset, action_name=action_name):
|
||||
def inner(modeladmin, request, queryset, action_name=action_name, extra_context=extra_context):
|
||||
# The user has already confirmed the action.
|
||||
if request.POST.get('post') == "generic_confirmation":
|
||||
if request.POST.get('post') == 'generic_confirmation':
|
||||
stay = func(modeladmin, request, queryset)
|
||||
if not stay:
|
||||
return
|
||||
|
@ -51,19 +66,23 @@ def action_with_confirmation(action_name=None, extra_context={},
|
|||
if not action_name:
|
||||
action_name = func.__name__
|
||||
context = {
|
||||
"title": "Are you sure?",
|
||||
"content_message": "Are you sure you want to %s the selected %s?" %
|
||||
(action_name, objects_name),
|
||||
"action_name": action_name.capitalize(),
|
||||
"action_value": action_value,
|
||||
"display_objects": queryset,
|
||||
'title': _("Are you sure?"),
|
||||
'content_message': _("Are you sure you want to {action} the selected {item}?").format(
|
||||
action=action_name, item=objects_name),
|
||||
'action_name': action_name.capitalize(),
|
||||
'action_value': action_value,
|
||||
'queryset': queryset,
|
||||
"opts": opts,
|
||||
"app_label": app_label,
|
||||
'opts': opts,
|
||||
'app_label': app_label,
|
||||
'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME,
|
||||
}
|
||||
|
||||
if callable(extra_context):
|
||||
extra_context = extra_context(modeladmin, request, queryset)
|
||||
context.update(extra_context)
|
||||
if 'display_objects' not in context:
|
||||
# Compute it only when necessary
|
||||
context['display_objects'] = format_display_objects(modeladmin, request, queryset)
|
||||
|
||||
# Display the confirmation page
|
||||
return TemplateResponse(request, template,
|
||||
|
|
|
@ -91,7 +91,7 @@ def action_to_view(action, modeladmin):
|
|||
return action_view
|
||||
|
||||
|
||||
def admin_change_url(obj):
|
||||
def change_url(obj):
|
||||
opts = obj._meta
|
||||
view_name = 'admin:%s_%s_change' % (opts.app_label, opts.model_name)
|
||||
return reverse(view_name, args=(obj.pk,))
|
||||
|
@ -106,7 +106,7 @@ def admin_link(*args, **kwargs):
|
|||
obj = get_field_value(instance, kwargs['field'])
|
||||
if not getattr(obj, 'pk', None):
|
||||
return '---'
|
||||
url = admin_change_url(obj)
|
||||
url = change_url(obj)
|
||||
extra = ''
|
||||
if kwargs['popup']:
|
||||
extra = 'onclick="return showAddAnotherPopup(this);"'
|
||||
|
|
|
@ -9,7 +9,8 @@ from django.utils.six.moves.urllib.parse import parse_qsl
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.admin import ExtendedModelAdmin
|
||||
from orchestra.admin.utils import wrap_admin_view, admin_link, set_url_query
|
||||
from orchestra.admin.utils import (wrap_admin_view, admin_link, set_url_query,
|
||||
change_url)
|
||||
from orchestra.core import services, accounts
|
||||
|
||||
from .filters import HasMainUserListFilter
|
||||
|
@ -136,8 +137,7 @@ class AccountAdminMixin(object):
|
|||
|
||||
def account_link(self, instance):
|
||||
account = instance.account if instance.pk else self.account
|
||||
url = reverse('admin:accounts_account_change', args=(account.pk,))
|
||||
pk = account.pk
|
||||
url = change_url(account)
|
||||
return '<a href="%s">%s</a>' % (url, str(account))
|
||||
account_link.short_description = _("account")
|
||||
account_link.allow_tags = True
|
||||
|
|
|
@ -42,7 +42,7 @@ view_bill.url_name = 'view'
|
|||
|
||||
|
||||
def close_bills(modeladmin, request, queryset):
|
||||
queryset = queryset.filter(status=queryset.model.OPEN)
|
||||
queryset = queryset.filter(is_open=True)
|
||||
if not queryset:
|
||||
messages.warning(request, _("Selected bills should be in open state"))
|
||||
return
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
from django import forms
|
||||
from django.contrib import admin
|
||||
#from django.contrib.admin.utils import unquote
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import models
|
||||
#from django.shortcuts import redirect
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.admin import ExtendedModelAdmin
|
||||
|
@ -17,23 +15,30 @@ from .models import (Bill, Invoice, AmendmentInvoice, Fee, AmendmentFee, ProForm
|
|||
BillLine)
|
||||
|
||||
|
||||
PAYMENT_STATE_COLORS = {
|
||||
Bill.PAID: 'green',
|
||||
Bill.PENDING: 'darkorange',
|
||||
Bill.BAD_DEBT: 'red',
|
||||
}
|
||||
|
||||
|
||||
class BillLineInline(admin.TabularInline):
|
||||
model = BillLine
|
||||
fields = ('description', 'rate', 'amount', 'tax', 'total', 'get_total')
|
||||
fields = ('description', 'rate', 'quantity', 'tax', 'subtotal', 'get_total')
|
||||
readonly_fields = ('get_total',)
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
if obj and obj.status != Bill.OPEN:
|
||||
if obj and not obj.is_open:
|
||||
return self.fields
|
||||
return super(BillLineInline, self).get_readonly_fields(request, obj=obj)
|
||||
|
||||
def has_add_permission(self, request):
|
||||
if request.__bill__ and request.__bill__.status != Bill.OPEN:
|
||||
if request.__bill__ and not request.__bill__.is_open:
|
||||
return False
|
||||
return super(BillLineInline, self).has_add_permission(request)
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
if obj and obj.status != Bill.OPEN:
|
||||
if obj and not obj.is_open:
|
||||
return False
|
||||
return super(BillLineInline, self).has_delete_permission(request, obj=obj)
|
||||
|
||||
|
@ -48,15 +53,15 @@ class BillLineInline(admin.TabularInline):
|
|||
|
||||
class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||
list_display = (
|
||||
'number', 'status', 'type_link', 'account_link', 'created_on_display',
|
||||
'num_lines', 'display_total'
|
||||
'number', 'is_open', 'type_link', 'account_link', 'created_on_display',
|
||||
'num_lines', 'display_total', 'display_payment_state'
|
||||
)
|
||||
list_filter = (BillTypeListFilter, 'status',)
|
||||
add_fields = ('account', 'type', 'status', 'due_on', 'comments')
|
||||
list_filter = (BillTypeListFilter, 'is_open',)
|
||||
add_fields = ('account', 'type', 'is_open', 'due_on', 'comments')
|
||||
fieldsets = (
|
||||
(None, {
|
||||
'fields': ('number', 'display_total', 'account_link', 'type',
|
||||
'status', 'due_on', 'comments'),
|
||||
'is_open', 'display_payment_state', 'is_sent', 'due_on', 'comments'),
|
||||
}),
|
||||
(_("Raw"), {
|
||||
'classes': ('collapse',),
|
||||
|
@ -65,8 +70,8 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
|||
)
|
||||
actions = [download_bills, close_bills, send_bills]
|
||||
change_view_actions = [view_bill, download_bills, send_bills, close_bills]
|
||||
change_readonly_fields = ('account_link', 'type', 'status')
|
||||
readonly_fields = ('number', 'display_total')
|
||||
change_readonly_fields = ('account_link', 'type', 'is_open')
|
||||
readonly_fields = ('number', 'display_total', 'display_payment_state')
|
||||
inlines = [BillLineInline]
|
||||
|
||||
created_on_display = admin_date('created_on')
|
||||
|
@ -90,29 +95,36 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
|||
type_link.short_description = _("type")
|
||||
type_link.admin_order_field = 'type'
|
||||
|
||||
def display_payment_state(self, bill):
|
||||
topts = bill.transactions.model._meta
|
||||
url = reverse('admin:%s_%s_changelist' % (topts.app_label, topts.module_name))
|
||||
url += '?bill=%i' % bill.pk
|
||||
state = bill.get_payment_state_display().upper()
|
||||
color = PAYMENT_STATE_COLORS.get(bill.payment_state, 'grey')
|
||||
return '<a href="{url}" style="color:{color}">{name}</a>'.format(
|
||||
url=url, color=color, name=state)
|
||||
display_payment_state.allow_tags = True
|
||||
display_payment_state.short_description = _("Payment")
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
fields = super(BillAdmin, self).get_readonly_fields(request, obj=obj)
|
||||
if obj and obj.status != Bill.OPEN:
|
||||
if obj and not obj.is_open:
|
||||
fields += self.add_fields
|
||||
return fields
|
||||
|
||||
def get_fieldsets(self, request, obj=None):
|
||||
fieldsets = super(BillAdmin, self).get_fieldsets(request, obj=obj)
|
||||
if obj and obj.status == obj.OPEN:
|
||||
if obj and obj.is_open:
|
||||
fieldsets = (fieldsets[0],)
|
||||
return fieldsets
|
||||
|
||||
def get_change_view_actions(self, obj=None):
|
||||
actions = super(BillAdmin, self).get_change_view_actions()
|
||||
discard = []
|
||||
exclude = []
|
||||
if obj:
|
||||
if obj.status != Bill.OPEN:
|
||||
discard = ['close_bills']
|
||||
if obj.status != Bill.CLOSED:
|
||||
discard = ['send_bills']
|
||||
if not discard:
|
||||
return actions
|
||||
return [action for action in actions if action.__name__ not in discard]
|
||||
if not obj.is_open:
|
||||
exclude.append('close_bills')
|
||||
return [action for action in actions if action.__name__ not in exclude]
|
||||
|
||||
def get_inline_instances(self, request, obj=None):
|
||||
# Make parent object available for inline.has_add_permission()
|
||||
|
|
|
@ -4,6 +4,7 @@ from dateutil.relativedelta import relativedelta
|
|||
from django.db import models
|
||||
from django.template import loader, Context
|
||||
from django.utils import timezone
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
@ -25,16 +26,13 @@ class BillManager(models.Manager):
|
|||
|
||||
|
||||
class Bill(models.Model):
|
||||
OPEN = 'OPEN'
|
||||
CLOSED = 'CLOSED'
|
||||
SENT = 'SENT'
|
||||
OPEN = ''
|
||||
PAID = 'PAID'
|
||||
PENDING = 'PENDING'
|
||||
BAD_DEBT = 'BAD_DEBT'
|
||||
STATUSES = (
|
||||
(OPEN, _("Open")),
|
||||
(CLOSED, _("Closed")),
|
||||
(SENT, _("Sent")),
|
||||
PAYMENT_STATES = (
|
||||
(PAID, _("Paid")),
|
||||
(PENDING, _("Pending")),
|
||||
(BAD_DEBT, _("Bad debt")),
|
||||
)
|
||||
|
||||
|
@ -51,10 +49,10 @@ class Bill(models.Model):
|
|||
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
|
||||
related_name='%(class)s')
|
||||
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)
|
||||
is_open = models.BooleanField(_("is open"), default=True)
|
||||
is_sent = models.BooleanField(_("is sent"), default=False)
|
||||
due_on = models.DateField(_("due on"), null=True, blank=True)
|
||||
last_modified_on = models.DateTimeField(_("last modified on"), auto_now=True)
|
||||
total = models.DecimalField(max_digits=12, decimal_places=2, default=0)
|
||||
|
@ -74,6 +72,21 @@ class Bill(models.Model):
|
|||
def buyer(self):
|
||||
return self.account.invoicecontact
|
||||
|
||||
@cached_property
|
||||
def payment_state(self):
|
||||
if self.is_open:
|
||||
return self.OPEN
|
||||
secured = self.transactions.secured().amount()
|
||||
if secured >= self.total:
|
||||
return self.PAID
|
||||
elif self.transactions.exclude_rejected().exists():
|
||||
return self.PENDING
|
||||
return self.BAD_DEBT
|
||||
|
||||
def get_payment_state_display(self):
|
||||
value = self.payment_state
|
||||
return force_text(dict(self.PAYMENT_STATES).get(value, value))
|
||||
|
||||
@classmethod
|
||||
def get_class_type(cls):
|
||||
return cls.__name__.upper()
|
||||
|
@ -87,7 +100,7 @@ class Bill(models.Model):
|
|||
if bill_type == 'BILL':
|
||||
raise TypeError("get_new_number() can not be used on a Bill class")
|
||||
prefix = getattr(settings, 'BILLS_%s_NUMBER_PREFIX' % bill_type)
|
||||
if self.status == self.OPEN:
|
||||
if self.is_open:
|
||||
prefix = 'O{}'.format(prefix)
|
||||
bills = cls.objects.filter(number__regex=r'^%s[1-9]+' % prefix)
|
||||
last_number = bills.order_by('-number').values_list('number', flat=True).first()
|
||||
|
@ -110,7 +123,7 @@ class Bill(models.Model):
|
|||
return now + relativedelta(months=1)
|
||||
|
||||
def close(self, payment=False):
|
||||
assert self.status == self.OPEN, "Bill not in Open state"
|
||||
assert self.is_open, "Bill not in Open state"
|
||||
if payment is False:
|
||||
payment = self.account.paymentsources.get_default()
|
||||
if not self.due_on:
|
||||
|
@ -119,7 +132,8 @@ class Bill(models.Model):
|
|||
self.html = self.render(payment=payment)
|
||||
self.transactions.create(bill=self, source=payment, amount=self.total)
|
||||
self.closed_on = timezone.now()
|
||||
self.status = self.CLOSED
|
||||
self.is_open = False
|
||||
self.is_sent = False
|
||||
self.save()
|
||||
|
||||
def send(self):
|
||||
|
@ -134,7 +148,7 @@ class Bill(models.Model):
|
|||
('%s.pdf' % self.number, html_to_pdf(self.html), 'application/pdf')
|
||||
]
|
||||
)
|
||||
self.status = self.SENT
|
||||
self.is_sent = True
|
||||
self.save()
|
||||
|
||||
def render(self, payment=False):
|
||||
|
@ -166,7 +180,7 @@ class Bill(models.Model):
|
|||
def save(self, *args, **kwargs):
|
||||
if not self.type:
|
||||
self.type = self.get_type()
|
||||
if not self.number or (self.number.startswith('O') and self.status != self.OPEN):
|
||||
if not self.number or (self.number.startswith('O') and not self.is_open):
|
||||
self.set_number()
|
||||
super(Bill, self).save(*args, **kwargs)
|
||||
|
||||
|
@ -217,8 +231,8 @@ class BillLine(models.Model):
|
|||
description = models.CharField(_("description"), max_length=256)
|
||||
rate = models.DecimalField(_("rate"), blank=True, null=True,
|
||||
max_digits=12, decimal_places=2)
|
||||
amount = models.DecimalField(_("amount"), max_digits=12, decimal_places=2)
|
||||
total = models.DecimalField(_("total"), max_digits=12, decimal_places=2)
|
||||
quantity = models.DecimalField(_("quantity"), max_digits=12, decimal_places=2)
|
||||
subtotal = models.DecimalField(_("subtotal"), max_digits=12, decimal_places=2)
|
||||
tax = models.PositiveIntegerField(_("tax"))
|
||||
# TODO
|
||||
# order_id = models.ForeignKey('orders.Order', null=True, blank=True,
|
||||
|
@ -236,15 +250,15 @@ class BillLine(models.Model):
|
|||
|
||||
def get_total(self):
|
||||
""" Computes subline discounts """
|
||||
subtotal = self.total
|
||||
total = self.subtotal
|
||||
for subline in self.sublines.all():
|
||||
subtotal += subline.total
|
||||
return subtotal
|
||||
total += subline.total
|
||||
return total
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# TODO cost of this shit
|
||||
super(BillLine, self).save(*args, **kwargs)
|
||||
if self.bill.status == self.bill.OPEN:
|
||||
if self.bill.is_open:
|
||||
self.bill.total = self.bill.get_total()
|
||||
self.bill.save()
|
||||
|
||||
|
@ -260,7 +274,7 @@ class BillSubline(models.Model):
|
|||
def save(self, *args, **kwargs):
|
||||
# TODO cost of this shit
|
||||
super(BillSubline, self).save(*args, **kwargs)
|
||||
if self.line.bill.status == self.line.bill.OPEN:
|
||||
if self.line.bill.is_open:
|
||||
self.line.bill.total = self.line.bill.get_total()
|
||||
self.line.bill.save()
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ from django.template.response import TemplateResponse
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.admin import ChangeListDefaultFilter, ExtendedModelAdmin
|
||||
from orchestra.admin.utils import wrap_admin_view, admin_link
|
||||
from orchestra.admin.utils import wrap_admin_view, admin_link, change_url
|
||||
from orchestra.apps.accounts.admin import AccountAdminMixin
|
||||
from orchestra.utils import apps
|
||||
|
||||
|
@ -79,7 +79,7 @@ class DomainAdmin(ChangeListDefaultFilter, AccountAdminMixin, ExtendedModelAdmin
|
|||
if webs:
|
||||
links = []
|
||||
for web in webs:
|
||||
url = reverse('admin:websites_website_change', args=(web.pk,))
|
||||
url = change_url(web)
|
||||
links.append('<a href="%s">%s</a>' % (url, web.name))
|
||||
return '<br>'.join(links)
|
||||
return _("No website")
|
||||
|
|
|
@ -5,6 +5,7 @@ from django.utils.safestring import mark_safe
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
from markdown import markdown
|
||||
|
||||
from orchestra.admin.utils import change_url
|
||||
from orchestra.apps.users.models import User
|
||||
from orchestra.forms.widgets import ReadOnlyWidget
|
||||
|
||||
|
@ -41,7 +42,7 @@ class MessageInlineForm(forms.ModelForm):
|
|||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(MessageInlineForm, self).__init__(*args, **kwargs)
|
||||
admin_link = reverse('admin:users_user_change', args=(self.user.pk,))
|
||||
admin_link = change_url(self.user)
|
||||
self.fields['created_on'].widget = ReadOnlyWidget('')
|
||||
|
||||
def clean_content(self):
|
||||
|
|
|
@ -7,7 +7,7 @@ from django.utils.safestring import mark_safe
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.admin import ExtendedModelAdmin
|
||||
from orchestra.admin.utils import insertattr, admin_link
|
||||
from orchestra.admin.utils import insertattr, admin_link, change_url
|
||||
from orchestra.apps.accounts.admin import SelectAccountAdminMixin, AccountAdminMixin
|
||||
from orchestra.apps.domains.forms import DomainIterator
|
||||
|
||||
|
@ -57,7 +57,7 @@ class MailboxAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
|||
def display_addresses(self, mailbox):
|
||||
addresses = []
|
||||
for addr in mailbox.addresses.all():
|
||||
url = reverse('admin:mails_address_change', args=(addr.pk,))
|
||||
url = change_url(addr)
|
||||
addresses.append('<a href="%s">%s</a>' % (url, addr.email))
|
||||
return '<br>'.join(addresses)
|
||||
display_addresses.short_description = _("Addresses")
|
||||
|
@ -106,7 +106,7 @@ class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
|
|||
def display_mailboxes(self, address):
|
||||
boxes = []
|
||||
for mailbox in address.mailboxes.all():
|
||||
url = reverse('admin:mails_mailbox_change', args=(mailbox.pk,))
|
||||
url = change_url(mailbox)
|
||||
boxes.append('<a href="%s">%s</a>' % (url, mailbox.name))
|
||||
return '<br>'.join(boxes)
|
||||
display_mailboxes.short_description = _("Mailboxes")
|
||||
|
|
|
@ -33,8 +33,8 @@ class BillsBackend(object):
|
|||
# Create bill line
|
||||
billine = bill.lines.create(
|
||||
rate=service.nominal_price,
|
||||
amount=line.size,
|
||||
total=line.subtotal,
|
||||
quantity=line.size,
|
||||
subtotal=line.subtotal,
|
||||
tax=service.tax,
|
||||
description=self.get_line_description(line),
|
||||
)
|
||||
|
|
|
@ -5,7 +5,7 @@ from django.utils.safestring import mark_safe
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.admin.forms import AdminFormMixin
|
||||
from orchestra.admin.utils import admin_change_url
|
||||
from orchestra.admin.utils import change_url
|
||||
|
||||
from .models import Order
|
||||
|
||||
|
@ -32,8 +32,8 @@ def selected_related_choices(queryset):
|
|||
verbose = '<a href="{order_url}">{description}</a> '
|
||||
verbose += '<a class="account" href="{account_url}">{account}</a>'
|
||||
verbose = verbose.format(
|
||||
order_url=admin_change_url(order), description=order.description,
|
||||
account_url=admin_change_url(order.account), account=str(order.account)
|
||||
order_url=change_url(order), description=order.description,
|
||||
account_url=change_url(order.account), account=str(order.account)
|
||||
)
|
||||
yield (order.pk, mark_safe(verbose))
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import sys
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.db.migrations.recorder import MigrationRecorder
|
||||
from django.db.models import F, Q
|
||||
|
@ -10,7 +9,6 @@ from django.dispatch import receiver
|
|||
from django.contrib.admin.models import LogEntry
|
||||
from django.contrib.contenttypes import generic
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.validators import ValidationError
|
||||
from django.utils import timezone
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
|
|
@ -13,7 +13,7 @@ from orchestra.apps.users.models import User
|
|||
from orchestra.utils.tests import BaseTestCase, random_ascii
|
||||
|
||||
|
||||
class ServiceTests(BaseTestCase):
|
||||
class BillingTests(BaseTestCase):
|
||||
DEPENDENCIES = (
|
||||
'orchestra.apps.services',
|
||||
'orchestra.apps.users',
|
||||
|
@ -91,3 +91,18 @@ class ServiceTests(BaseTestCase):
|
|||
error = decimal.Decimal(0.05)
|
||||
self.assertGreater(10*size+error*(10*size), bills[0].get_total())
|
||||
self.assertLess(10*size-error*(10*size), bills[0].get_total())
|
||||
|
||||
def test_ftp_account_with_compensation(self):
|
||||
account = self.create_account()
|
||||
service = self.create_ftp_service()
|
||||
user = self.create_ftp(account=account)
|
||||
bp = timezone.now().date() + relativedelta.relativedelta(years=2)
|
||||
bills = service.orders.bill(billing_point=bp, fixed_point=True)
|
||||
user.delete()
|
||||
user = self.create_ftp(account=account)
|
||||
bp = timezone.now().date() + relativedelta.relativedelta(years=1)
|
||||
bills = service.orders.bill(billing_point=bp, fixed_point=True)
|
||||
for line in bills[0].lines.all():
|
||||
print line
|
||||
print line.sublines.all()
|
||||
# TODO asserts
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
from functools import partial
|
||||
|
||||
from django.contrib import messages
|
||||
from django.db import transaction
|
||||
from django.shortcuts import render
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.text import capfirst
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.admin.decorators import action_with_confirmation
|
||||
from orchestra.admin.utils import change_url
|
||||
|
||||
from .methods import PaymentMethod
|
||||
from .models import Transaction
|
||||
|
@ -38,8 +43,7 @@ def process_transactions(modeladmin, request, queryset):
|
|||
|
||||
@transaction.atomic
|
||||
@action_with_confirmation()
|
||||
def mark_as_executed(modeladmin, request, queryset):
|
||||
""" Mark a tickets as unread """
|
||||
def mark_as_executed(modeladmin, request, queryset, extra_context={}):
|
||||
for transaction in queryset:
|
||||
transaction.mark_as_executed()
|
||||
modeladmin.log_change(request, transaction, 'Executed')
|
||||
|
@ -52,7 +56,6 @@ mark_as_executed.verbose_name = _("Mark as executed")
|
|||
@transaction.atomic
|
||||
@action_with_confirmation()
|
||||
def mark_as_secured(modeladmin, request, queryset):
|
||||
""" Mark a tickets as unread """
|
||||
for transaction in queryset:
|
||||
transaction.mark_as_secured()
|
||||
modeladmin.log_change(request, transaction, 'Secured')
|
||||
|
@ -65,7 +68,6 @@ mark_as_secured.verbose_name = _("Mark as secured")
|
|||
@transaction.atomic
|
||||
@action_with_confirmation()
|
||||
def mark_as_rejected(modeladmin, request, queryset):
|
||||
""" Mark a tickets as unread """
|
||||
for transaction in queryset:
|
||||
transaction.mark_as_rejected()
|
||||
modeladmin.log_change(request, transaction, 'Rejected')
|
||||
|
@ -73,3 +75,62 @@ def mark_as_rejected(modeladmin, request, queryset):
|
|||
modeladmin.message_user(request, msg)
|
||||
mark_as_rejected.url_name = 'reject'
|
||||
mark_as_rejected.verbose_name = _("Mark as rejected")
|
||||
|
||||
|
||||
def _format_display_objects(modeladmin, request, queryset, related):
|
||||
objects = []
|
||||
opts = modeladmin.model._meta
|
||||
for obj in queryset:
|
||||
objects.append(
|
||||
mark_safe('{0}: <a href="{1}">{2}</a>'.format(
|
||||
capfirst(opts.verbose_name), change_url(obj), obj))
|
||||
)
|
||||
subobjects = []
|
||||
attr, verb = related
|
||||
for related in getattr(obj.transactions, attr)():
|
||||
subobjects.append(
|
||||
mark_safe('{0}: <a href="{1}">{2}</a> will be marked as {3}'.format(
|
||||
capfirst(subobj.get_type().lower()), change_url(subobj), subobj, verb))
|
||||
)
|
||||
objects.append(subobjects)
|
||||
return {'display_objects': objects}
|
||||
|
||||
_format_executed = partial(_format_display_objects, related=('all', 'executed'))
|
||||
_format_abort = partial(_format_display_objects, related=('processing', 'aborted'))
|
||||
_format_commit = partial(_format_display_objects, related=('all', 'secured'))
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
@action_with_confirmation(extra_context=_format_executed)
|
||||
def mark_process_as_executed(modeladmin, request, queryset):
|
||||
for process in queryset:
|
||||
process.mark_as_executed()
|
||||
modeladmin.log_change(request, process, 'Executed')
|
||||
msg = _("%s selected processes have been marked as executed.") % queryset.count()
|
||||
modeladmin.message_user(request, msg)
|
||||
mark_process_as_executed.url_name = 'executed'
|
||||
mark_process_as_executed.verbose_name = _("Mark as executed")
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
@action_with_confirmation(extra_context=_format_abort)
|
||||
def abort(modeladmin, request, queryset):
|
||||
for process in queryset:
|
||||
process.abort()
|
||||
modeladmin.log_change(request, process, 'Aborted')
|
||||
msg = _("%s selected processes have been aborted.") % queryset.count()
|
||||
modeladmin.message_user(request, msg)
|
||||
abort.url_name = 'abort'
|
||||
abort.verbose_name = _("Abort")
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
@action_with_confirmation(extra_context=_format_commit)
|
||||
def commit(modeladmin, request, queryset):
|
||||
for transaction in queryset:
|
||||
transaction.mark_as_rejected()
|
||||
modeladmin.log_change(request, transaction, 'Rejected')
|
||||
msg = _("%s selected transactions have been marked as rejected.") % queryset.count()
|
||||
modeladmin.message_user(request, msg)
|
||||
commit.url_name = 'commit'
|
||||
commit.verbose_name = _("Commit")
|
||||
|
|
|
@ -16,76 +16,13 @@ from .models import PaymentSource, Transaction, TransactionProcess
|
|||
|
||||
STATE_COLORS = {
|
||||
Transaction.WAITTING_PROCESSING: 'darkorange',
|
||||
Transaction.WAITTING_CONFIRMATION: 'magenta',
|
||||
Transaction.WAITTING_EXECUTION: 'magenta',
|
||||
Transaction.EXECUTED: 'olive',
|
||||
Transaction.SECURED: 'green',
|
||||
Transaction.REJECTED: 'red',
|
||||
}
|
||||
|
||||
|
||||
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(ChangeViewActionsMixin, AccountAdminMixin, admin.ModelAdmin):
|
||||
list_display = (
|
||||
'id', 'bill_link', 'account_link', 'source_link', 'display_state',
|
||||
'amount', 'process_link'
|
||||
)
|
||||
list_filter = ('source__method', 'state')
|
||||
actions = (
|
||||
actions.process_transactions, actions.mark_as_executed,
|
||||
actions.mark_as_secured, actions.mark_as_rejected
|
||||
)
|
||||
change_view_actions = actions
|
||||
filter_by_account_fields = ['source']
|
||||
readonly_fields = ('bill_link', 'display_state', '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)
|
||||
|
||||
def get_queryset(self, request):
|
||||
qs = super(TransactionAdmin, self).get_queryset(request)
|
||||
return qs.select_related('source', 'bill__account__user')
|
||||
|
||||
def get_change_view_actions(self, obj=None):
|
||||
actions = super(TransactionAdmin, self).get_change_view_actions()
|
||||
discard = []
|
||||
if obj:
|
||||
if obj.state == Transaction.EXECUTED:
|
||||
discard = ['mark_as_executed']
|
||||
elif obj.state == Transaction.REJECTED:
|
||||
discard = ['mark_as_rejected']
|
||||
elif obj.state == Transaction.SECURED:
|
||||
discard = ['mark_as_secured']
|
||||
if not discard:
|
||||
return actions
|
||||
return [action for action in actions if action.__name__ not in discard]
|
||||
|
||||
|
||||
class PaymentSourceAdmin(AccountAdminMixin, admin.ModelAdmin):
|
||||
list_display = ('label', 'method', 'number', 'account_link', 'is_active')
|
||||
list_filter = ('method', 'is_active')
|
||||
|
@ -138,11 +75,74 @@ class PaymentSourceAdmin(AccountAdminMixin, admin.ModelAdmin):
|
|||
obj.save()
|
||||
|
||||
|
||||
class TransactionProcessAdmin(admin.ModelAdmin):
|
||||
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(ChangeViewActionsMixin, AccountAdminMixin, admin.ModelAdmin):
|
||||
list_display = (
|
||||
'id', 'bill_link', 'account_link', 'source_link', 'display_state',
|
||||
'amount', 'process_link'
|
||||
)
|
||||
list_filter = ('source__method', 'state')
|
||||
actions = (
|
||||
actions.process_transactions, actions.mark_as_executed,
|
||||
actions.mark_as_secured, actions.mark_as_rejected
|
||||
)
|
||||
change_view_actions = actions
|
||||
filter_by_account_fields = ['source']
|
||||
readonly_fields = ('bill_link', 'display_state', '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)
|
||||
|
||||
def get_queryset(self, request):
|
||||
qs = super(TransactionAdmin, self).get_queryset(request)
|
||||
return qs.select_related('source', 'bill__account__user')
|
||||
|
||||
def get_change_view_actions(self, obj=None):
|
||||
actions = super(TransactionAdmin, self).get_change_view_actions()
|
||||
exclude = []
|
||||
if obj:
|
||||
if obj.state == Transaction.EXECUTED:
|
||||
exclude.append('mark_as_executed')
|
||||
elif obj.state == Transaction.REJECTED:
|
||||
exclude.append('mark_as_rejected')
|
||||
elif obj.state == Transaction.SECURED:
|
||||
exclude.append('mark_as_secured')
|
||||
return [action for action in actions if action.__name__ not in exclude]
|
||||
|
||||
|
||||
class TransactionProcessAdmin(ChangeViewActionsMixin, 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]
|
||||
actions = (actions.mark_process_as_executed, actions.abort, actions.commit)
|
||||
change_view_actions = actions
|
||||
|
||||
def file_url(self, process):
|
||||
if process.file:
|
||||
|
@ -169,6 +169,18 @@ class TransactionProcessAdmin(admin.ModelAdmin):
|
|||
return '<a href="%s">%s</a>' % (url, '<br>'.join(lines))
|
||||
display_transactions.short_description = _("Transactions")
|
||||
display_transactions.allow_tags = True
|
||||
|
||||
def get_change_view_actions(self, obj=None):
|
||||
actions = super(TransactionProcessAdmin, self).get_change_view_actions()
|
||||
exclude = []
|
||||
if obj:
|
||||
if obj.state == TransactionProcess.EXECUTED:
|
||||
exclude.append('mark_process_as_executed')
|
||||
elif obj.state == TransactionProcess.COMMITED:
|
||||
exclude = ['mark_process_as_executed', 'abort', 'commit']
|
||||
elif obj.state == TransactionProcess.ABORTED:
|
||||
exclude = ['mark_process_as_executed', 'abort', 'commit']
|
||||
return [action for action in actions if action.__name__ not in exclude]
|
||||
|
||||
|
||||
admin.site.register(PaymentSource, PaymentSourceAdmin)
|
||||
|
|
|
@ -59,27 +59,36 @@ class TransactionQuerySet(models.QuerySet):
|
|||
source = kwargs.get('source')
|
||||
if source is None or not hasattr(source.method_class, 'process'):
|
||||
# Manual payments don't need processing
|
||||
kwargs['state']=self.model.WAITTING_CONFIRMATION
|
||||
kwargs['state']=self.model.WAITTING_EXECUTION
|
||||
return super(TransactionQuerySet, self).create(**kwargs)
|
||||
|
||||
def secured(self):
|
||||
return self.filter(state=Transaction.SECURED)
|
||||
|
||||
def exclude_rejected(self):
|
||||
return self.exclude(state=Transaction.REJECTED)
|
||||
|
||||
def amount(self):
|
||||
return self.aggregate(models.Sum('amount')).values()[0]
|
||||
|
||||
def processing(self):
|
||||
return self.filter(state__in=[Transaction.EXECUTED, Transaction.WAITTING_EXECUTION])
|
||||
|
||||
|
||||
# TODO lock transaction in waiting confirmation
|
||||
class Transaction(models.Model):
|
||||
WAITTING_PROCESSING = 'WAITTING_PROCESSING' # CREATED
|
||||
WAITTING_CONFIRMATION = 'WAITTING_CONFIRMATION' # PROCESSED
|
||||
WAITTING_EXECUTION = 'WAITTING_EXECUTION' # PROCESSED
|
||||
EXECUTED = 'EXECUTED'
|
||||
SECURED = 'SECURED'
|
||||
REJECTED = 'REJECTED'
|
||||
STATES = (
|
||||
(WAITTING_PROCESSING, _("Waitting processing")),
|
||||
(WAITTING_CONFIRMATION, _("Waitting confirmation")),
|
||||
(WAITTING_EXECUTION, _("Waitting execution")),
|
||||
(EXECUTED, _("Executed")),
|
||||
(SECURED, _("Secured")),
|
||||
(REJECTED, _("Rejected")),
|
||||
)
|
||||
|
||||
objects = TransactionQuerySet.as_manager()
|
||||
|
||||
bill = models.ForeignKey('bills.bill', verbose_name=_("bill"),
|
||||
related_name='transactions')
|
||||
source = models.ForeignKey(PaymentSource, null=True, blank=True,
|
||||
|
@ -93,6 +102,8 @@ class Transaction(models.Model):
|
|||
created_on = models.DateTimeField(auto_now_add=True)
|
||||
modified_on = models.DateTimeField(auto_now=True)
|
||||
|
||||
objects = TransactionQuerySet.as_manager()
|
||||
|
||||
def __unicode__(self):
|
||||
return "Transaction {}".format(self.id)
|
||||
|
||||
|
@ -100,19 +111,26 @@ class Transaction(models.Model):
|
|||
def account(self):
|
||||
return self.bill.account
|
||||
|
||||
def clean(self):
|
||||
if not self.pk:
|
||||
amount = self.bill.transactions.exclude(state=self.REJECTED).amount()
|
||||
if amount >= self.bill.total:
|
||||
raise ValidationError(_("New transactions can not be allocated for this bill"))
|
||||
|
||||
def mark_as_processed(self):
|
||||
self.state = self.WAITTING_EXECUTION
|
||||
self.save()
|
||||
|
||||
def mark_as_executed(self):
|
||||
self.state = self.EXECUTED
|
||||
self.save()
|
||||
|
||||
def mark_as_secured(self):
|
||||
self.state = self.SECURED
|
||||
# TODO think carefully about bill feedback
|
||||
self.bill.mark_as_paid()
|
||||
self.save()
|
||||
|
||||
def mark_as_rejected(self):
|
||||
self.state = self.REJECTED
|
||||
# TODO bill feedback
|
||||
self.save()
|
||||
|
||||
|
||||
|
@ -120,15 +138,49 @@ class TransactionProcess(models.Model):
|
|||
"""
|
||||
Stores arbitrary data generated by payment methods while processing transactions
|
||||
"""
|
||||
CREATED = 'CREATED'
|
||||
EXECUTED = 'EXECUTED'
|
||||
ABORTED = 'ABORTED'
|
||||
COMMITED = 'COMMITED'
|
||||
STATES = (
|
||||
(CREATED, _("Created")),
|
||||
(EXECUTED, _("Executed")),
|
||||
(ABORTED, _("Aborted")),
|
||||
(COMMITED, _("Commited")),
|
||||
)
|
||||
|
||||
data = JSONField(_("data"), blank=True)
|
||||
file = models.FileField(_("file"), blank=True)
|
||||
created_at = models.DateTimeField(_("created at"), auto_now_add=True)
|
||||
state = models.CharField(_("state"), max_length=16, choices=STATES, default=CREATED)
|
||||
created_at = models.DateTimeField(_("created"), auto_now_add=True)
|
||||
updated_at = models.DateTimeField(_("updated"), auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = _("Transaction processes")
|
||||
|
||||
def __unicode__(self):
|
||||
return str(self.id)
|
||||
|
||||
def mark_as_executed(self):
|
||||
assert self.state == self.CREATED
|
||||
self.state = self.EXECUTED
|
||||
for transaction in self.transactions.all():
|
||||
transaction.mark_as_executed()
|
||||
self.save()
|
||||
|
||||
def abort(self):
|
||||
assert self.state in [self.CREATED, self.EXCECUTED]
|
||||
self.state = self.ABORTED
|
||||
for transaction in self.transaction.all():
|
||||
transaction.mark_as_aborted()
|
||||
self.save()
|
||||
|
||||
def commit(self):
|
||||
assert self.state in [self.CREATED, self.EXECUTED]
|
||||
self.state = self.COMMITED
|
||||
for transaction in self.transactions.processing():
|
||||
transaction.mark_as_secured()
|
||||
self.save()
|
||||
|
||||
|
||||
accounts.register(PaymentSource)
|
||||
|
|
|
@ -164,7 +164,7 @@ class ServiceHandler(plugins.Plugin):
|
|||
for order in givers:
|
||||
if order.billed_until and order.cancelled_on and order.cancelled_on < order.billed_until:
|
||||
interval = helpers.Interval(order.cancelled_on, order.billed_until, order)
|
||||
compensations.append[interval]
|
||||
compensations.append(interval)
|
||||
for order in receivers:
|
||||
if not order.billed_until or order.billed_until < order.new_billed_until:
|
||||
# receiver
|
||||
|
@ -277,9 +277,10 @@ class ServiceHandler(plugins.Plugin):
|
|||
ini = min(ini, cini)
|
||||
end = max(end, bp)
|
||||
related_orders = account.orders.filter(service=self.service)
|
||||
if self.on_cancel == self.COMPENSATE:
|
||||
if self.on_cancel == self.DISCOUNT:
|
||||
# Get orders pending for compensation
|
||||
givers = related_orders.filter_givers(ini, end)
|
||||
givers = list(related_orders.filter_givers(ini, end))
|
||||
print givers
|
||||
givers.sort(cmp=helpers.cmp_billed_until_or_registered_on)
|
||||
orders.sort(cmp=helpers.cmp_billed_until_or_registered_on)
|
||||
self.compensate(givers, orders, commit=commit)
|
||||
|
@ -341,6 +342,7 @@ class ServiceHandler(plugins.Plugin):
|
|||
return lines
|
||||
|
||||
def generate_bill_lines(self, orders, account, **options):
|
||||
# TODO filter out orders with cancelled_on < billed_until ?
|
||||
if not self.metric:
|
||||
lines = self.bill_with_orders(orders, account, **options)
|
||||
else:
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import sys
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models import F, Q
|
||||
from django.db.models.signals import pre_delete, post_delete, post_save
|
||||
|
@ -90,7 +89,6 @@ class Service(models.Model):
|
|||
NOTHING = 'NOTHING'
|
||||
DISCOUNT = 'DISCOUNT'
|
||||
REFOUND = 'REFOUND'
|
||||
COMPENSATE = 'COMPENSATE'
|
||||
PREPAY = 'PREPAY'
|
||||
POSTPAY = 'POSTPAY'
|
||||
STEP_PRICE = 'STEP_PRICE'
|
||||
|
@ -174,7 +172,6 @@ class Service(models.Model):
|
|||
choices=(
|
||||
(NOTHING, _("Nothing")),
|
||||
(DISCOUNT, _("Discount")),
|
||||
(COMPENSATE, _("Discount and compensate")),
|
||||
),
|
||||
default=DISCOUNT)
|
||||
payment_style = models.CharField(_("payment style"), max_length=16,
|
||||
|
@ -229,11 +226,10 @@ class Service(models.Model):
|
|||
def clean(self):
|
||||
content_type = self.handler.get_content_type()
|
||||
if self.content_type != content_type:
|
||||
msg =_("Content type must be equal to '%s'.") % str(content_type)
|
||||
raise ValidationError(msg)
|
||||
ct = str(content_type)
|
||||
raise ValidationError(_("Content type must be equal to '%s'.") % ct)
|
||||
if not self.match:
|
||||
msg =_("Match should be provided")
|
||||
raise ValidationError(msg)
|
||||
raise ValidationError(_("Match should be provided"))
|
||||
try:
|
||||
obj = content_type.model_class().objects.all()[0]
|
||||
except IndexError:
|
||||
|
|
|
@ -136,7 +136,7 @@ class HandlerTests(BaseTestCase):
|
|||
self.assertIn([order4.billed_until, end, [order2, order3]], chunks)
|
||||
|
||||
def test_sort_billed_until_or_registered_on(self):
|
||||
now = timezone.now()
|
||||
now = timezone.now().date()
|
||||
order = Order(
|
||||
billed_until=now+datetime.timedelta(days=200))
|
||||
order1 = Order(
|
||||
|
@ -158,7 +158,7 @@ class HandlerTests(BaseTestCase):
|
|||
self.assertEqual(orders, sorted(orders, cmp=helpers.cmp_billed_until_or_registered_on))
|
||||
|
||||
def test_compensation(self):
|
||||
now = timezone.now()
|
||||
now = timezone.now().date()
|
||||
order = Order(
|
||||
description='0',
|
||||
billed_until=now+datetime.timedelta(days=220),
|
||||
|
@ -353,5 +353,16 @@ class HandlerTests(BaseTestCase):
|
|||
self.assertEqual(rate['price'], result.price)
|
||||
self.assertEqual(rate['quantity'], result.quantity)
|
||||
|
||||
def test_compensations(self):
|
||||
pass
|
||||
def test_generate_bill_lines_with_compensation(self):
|
||||
service = self.create_ftp_service()
|
||||
account = self.create_account()
|
||||
now = timezone.now().date()
|
||||
order = Order(
|
||||
cancelled_on=now,
|
||||
billed_until=now+relativedelta.relativedelta(years=2)
|
||||
)
|
||||
order1 = Order()
|
||||
orders = [order, order1]
|
||||
lines = service.handler.generate_bill_lines(orders, account, commit=False)
|
||||
print lines
|
||||
print len(lines)
|
||||
|
|
|
@ -10,7 +10,7 @@ from django.utils.encoding import force_text
|
|||
from django.utils.html import escape
|
||||
from django.utils.translation import ugettext, ugettext_lazy as _
|
||||
|
||||
from orchestra.admin.utils import get_modeladmin
|
||||
from orchestra.admin.utils import get_modeladmin, change_url
|
||||
|
||||
from .forms import role_form_factory
|
||||
from ..models import User
|
||||
|
@ -71,9 +71,8 @@ class RoleAdmin(object):
|
|||
modeladmin.log_change(request, request.user, "%s saved" % self.name.capitalize())
|
||||
msg = _('The role %(name)s for user "%(obj)s" was %(action)s successfully.') % context
|
||||
modeladmin.message_user(request, msg, messages.SUCCESS)
|
||||
url = 'admin:%s_%s_change' % (opts.app_label, opts.module_name)
|
||||
if not "_continue" in request.POST:
|
||||
return redirect(url, object_id)
|
||||
return redirect(change_url(user))
|
||||
exists = True
|
||||
|
||||
if exists:
|
||||
|
@ -117,7 +116,7 @@ class RoleAdmin(object):
|
|||
obj_display = force_text(obj)
|
||||
modeladmin.log_deletion(request, obj, obj_display)
|
||||
modeladmin.delete_model(request, obj)
|
||||
post_url = reverse('admin:users_user_change', args=(user.pk,))
|
||||
post_url = change_url(user)
|
||||
preserved_filters = modeladmin.get_preserved_filters(request)
|
||||
post_url = add_preserved_filters(
|
||||
{'preserved_filters': preserved_filters, 'opts': opts}, post_url
|
||||
|
|
|
@ -4,6 +4,7 @@ from django.core.urlresolvers import reverse
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.admin import ExtendedModelAdmin
|
||||
from orchestra.admin.utils import change_url
|
||||
from orchestra.apps.accounts.admin import SelectAccountAdminMixin
|
||||
|
||||
from .models import WebApp, WebAppOption
|
||||
|
@ -37,7 +38,7 @@ class WebAppAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
|
|||
websites = []
|
||||
for content in webapp.content_set.all().select_related('website'):
|
||||
website = content.website
|
||||
url = reverse('admin:websites_website_change', args=(website.pk,))
|
||||
url = change_url(website)
|
||||
name = "%s on %s" % (website.name, content.path)
|
||||
websites.append('<a href="%s">%s</a>' % (url, name))
|
||||
return '<br>'.join(websites)
|
||||
|
|
|
@ -5,7 +5,7 @@ from django.utils.safestring import mark_safe
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orchestra.admin import ExtendedModelAdmin
|
||||
from orchestra.admin.utils import admin_link
|
||||
from orchestra.admin.utils import admin_link, change_url
|
||||
from orchestra.apps.accounts.admin import AccountAdminMixin, SelectAccountAdminMixin
|
||||
from orchestra.apps.accounts.widgets import account_related_field_widget_factory
|
||||
|
||||
|
@ -72,7 +72,7 @@ class WebsiteAdmin(SelectAccountAdminMixin, ExtendedModelAdmin):
|
|||
webapps = []
|
||||
for content in website.content_set.all().select_related('webapp'):
|
||||
webapp = content.webapp
|
||||
url = reverse('admin:webapps_webapp_change', args=(webapp.pk,))
|
||||
url = change_url(webapp)
|
||||
name = "%s on %s" % (webapp.get_type_display(), content.path)
|
||||
webapps.append('<a href="%s">%s</a>' % (url, name))
|
||||
return '<br>'.join(webapps)
|
||||
|
|
|
@ -97,6 +97,7 @@ INSTALLED_APPS = (
|
|||
'rest_framework',
|
||||
'rest_framework.authtoken',
|
||||
'passlib.ext.django',
|
||||
'django_nose',
|
||||
|
||||
# Django.contrib
|
||||
'django.contrib.auth',
|
||||
|
@ -248,3 +249,6 @@ PASSLIB_CONFIG = (
|
|||
"superuser__django_pbkdf2_sha256__default_rounds = 15000\n"
|
||||
"superuser__sha512_crypt__default_rounds = 120000\n"
|
||||
)
|
||||
|
||||
|
||||
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
|
||||
|
|
|
@ -27,11 +27,7 @@
|
|||
<div>
|
||||
<div style="margin:20px;">
|
||||
<p>{{ content_message | safe }}</p>
|
||||
<ul>
|
||||
{% for display_object in display_objects %}
|
||||
<li> <a href="{% url opts|admin_urlname:'change' display_object.pk %}">{{ display_object }} </a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<ul>{{ display_objects | unordered_list }}</ul>
|
||||
<form action="" method="post">{% csrf_token %}
|
||||
{% if form %}
|
||||
<fieldset class="module aligned">
|
||||
|
@ -53,7 +49,6 @@
|
|||
{% if formset %}
|
||||
{{ formset.as_admin }}
|
||||
{% endif %}
|
||||
|
||||
<div>
|
||||
{% for obj in queryset %}
|
||||
<input type="hidden" name="{{ action_checkbox_name }}" value="{{ obj.pk|unlocalize }}" />
|
||||
|
|
|
@ -3,7 +3,7 @@ from django.core.urlresolvers import reverse, NoReverseMatch
|
|||
from django.forms import CheckboxInput
|
||||
|
||||
from orchestra import get_version
|
||||
from orchestra.admin.utils import admin_change_url
|
||||
from orchestra.admin.utils import change_url
|
||||
|
||||
|
||||
register = template.Library()
|
||||
|
@ -49,4 +49,4 @@ def is_checkbox(field):
|
|||
|
||||
@register.filter
|
||||
def admin_link(obj):
|
||||
return admin_change_url(obj)
|
||||
return change_url(obj)
|
||||
|
|
Loading…
Reference in New Issue