Added date_hierarchy indexes and improvements on billing
This commit is contained in:
parent
a364f88452
commit
09025102bc
2
TODO.md
2
TODO.md
|
@ -450,4 +450,4 @@ def comma(value):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
# Close, send + download admin action for bills (with confirmation)
|
# db_index on date_hierarchy
|
||||||
|
|
|
@ -107,7 +107,7 @@ class ChangeViewActionsMixin(object):
|
||||||
verbose_name = verbose_name(obj)
|
verbose_name = verbose_name(obj)
|
||||||
view.verbose_name = verbose_name
|
view.verbose_name = verbose_name
|
||||||
view.css_class = getattr(action, 'css_class', 'historylink')
|
view.css_class = getattr(action, 'css_class', 'historylink')
|
||||||
view.description = getattr(action, 'description', '')
|
view.help_text = getattr(action, 'help_text', '')
|
||||||
views.append(view)
|
views.append(view)
|
||||||
return views
|
return views
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ungettext, ugettext_lazy as _
|
from django.utils.translation import ungettext, ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra.admin.forms import adminmodelformset_factory
|
from orchestra.admin.forms import adminmodelformset_factory
|
||||||
|
from orchestra.admin.decorators import action_with_confirmation
|
||||||
from orchestra.admin.utils import get_object_from_url, change_url
|
from orchestra.admin.utils import get_object_from_url, change_url
|
||||||
from orchestra.utils.html import html_to_pdf
|
from orchestra.utils.html import html_to_pdf
|
||||||
|
|
||||||
|
@ -23,24 +24,6 @@ from .helpers import validate_contact
|
||||||
from .models import Bill, BillLine
|
from .models import Bill, BillLine
|
||||||
|
|
||||||
|
|
||||||
def download_bills(modeladmin, request, queryset):
|
|
||||||
if queryset.count() > 1:
|
|
||||||
bytesio = io.BytesIO()
|
|
||||||
archive = zipfile.ZipFile(bytesio, 'w')
|
|
||||||
for bill in queryset:
|
|
||||||
pdf = bill.as_pdf()
|
|
||||||
archive.writestr('%s.pdf' % bill.number, pdf)
|
|
||||||
archive.close()
|
|
||||||
response = HttpResponse(bytesio.getvalue(), content_type='application/pdf')
|
|
||||||
response['Content-Disposition'] = 'attachment; filename="orchestra-bills.zip"'
|
|
||||||
return response
|
|
||||||
bill = queryset.get()
|
|
||||||
pdf = bill.as_pdf()
|
|
||||||
return HttpResponse(pdf, content_type='application/pdf')
|
|
||||||
download_bills.verbose_name = _("Download")
|
|
||||||
download_bills.url_name = 'download'
|
|
||||||
|
|
||||||
|
|
||||||
def view_bill(modeladmin, request, queryset):
|
def view_bill(modeladmin, request, queryset):
|
||||||
bill = queryset.get()
|
bill = queryset.get()
|
||||||
if not validate_contact(request, bill):
|
if not validate_contact(request, bill):
|
||||||
|
@ -111,6 +94,7 @@ close_bills.verbose_name = _("Close")
|
||||||
close_bills.url_name = 'close'
|
close_bills.url_name = 'close'
|
||||||
|
|
||||||
|
|
||||||
|
@action_with_confirmation()
|
||||||
def send_bills(modeladmin, request, queryset):
|
def send_bills(modeladmin, request, queryset):
|
||||||
for bill in queryset:
|
for bill in queryset:
|
||||||
if not validate_contact(request, bill):
|
if not validate_contact(request, bill):
|
||||||
|
@ -128,12 +112,41 @@ send_bills.verbose_name = lambda bill: _("Resend" if getattr(bill, 'is_sent', Fa
|
||||||
send_bills.url_name = 'send'
|
send_bills.url_name = 'send'
|
||||||
|
|
||||||
|
|
||||||
|
def download_bills(modeladmin, request, queryset):
|
||||||
|
if queryset.count() > 1:
|
||||||
|
bytesio = io.BytesIO()
|
||||||
|
archive = zipfile.ZipFile(bytesio, 'w')
|
||||||
|
for bill in queryset:
|
||||||
|
pdf = bill.as_pdf()
|
||||||
|
archive.writestr('%s.pdf' % bill.number, pdf)
|
||||||
|
archive.close()
|
||||||
|
response = HttpResponse(bytesio.getvalue(), content_type='application/pdf')
|
||||||
|
response['Content-Disposition'] = 'attachment; filename="orchestra-bills.zip"'
|
||||||
|
return response
|
||||||
|
bill = queryset.get()
|
||||||
|
pdf = bill.as_pdf()
|
||||||
|
return HttpResponse(pdf, content_type='application/pdf')
|
||||||
|
download_bills.verbose_name = _("Download")
|
||||||
|
download_bills.url_name = 'download'
|
||||||
|
|
||||||
|
|
||||||
|
def close_send_download_bills(modeladmin, request, queryset):
|
||||||
|
close_bills(modeladmin, request, queryset)
|
||||||
|
if request.POST.get('post') == 'generic_confirmation':
|
||||||
|
send_bills(modeladmin, request, queryset)
|
||||||
|
return download_bills(modeladmin, request, queryset)
|
||||||
|
close_send_download_bills.verbose_name = _("C.S.D.")
|
||||||
|
close_send_download_bills.url_name = 'close-send-download'
|
||||||
|
close_send_download_bills.help_text = _("Close, send and download bills in one shot.")
|
||||||
|
|
||||||
|
|
||||||
def manage_lines(modeladmin, request, queryset):
|
def manage_lines(modeladmin, request, queryset):
|
||||||
url = reverse('admin:bills_bill_manage_lines')
|
url = reverse('admin:bills_bill_manage_lines')
|
||||||
url += '?ids=%s' % ','.join(map(str, queryset.values_list('id', flat=True)))
|
url += '?ids=%s' % ','.join(map(str, queryset.values_list('id', flat=True)))
|
||||||
return redirect(url)
|
return redirect(url)
|
||||||
|
|
||||||
|
|
||||||
|
@action_with_confirmation()
|
||||||
def undo_billing(modeladmin, request, queryset):
|
def undo_billing(modeladmin, request, queryset):
|
||||||
group = {}
|
group = {}
|
||||||
for line in queryset.select_related('order'):
|
for line in queryset.select_related('order'):
|
||||||
|
@ -214,6 +227,7 @@ def copy_lines(modeladmin, request, queryset):
|
||||||
return move_lines(modeladmin, request, queryset)
|
return move_lines(modeladmin, request, queryset)
|
||||||
|
|
||||||
|
|
||||||
|
@action_with_confirmation()
|
||||||
def amend_bills(modeladmin, request, queryset):
|
def amend_bills(modeladmin, request, queryset):
|
||||||
if queryset.filter(is_open=True).exists():
|
if queryset.filter(is_open=True).exists():
|
||||||
messages.warning(request, _("Selected bills should be in closed state"))
|
messages.warning(request, _("Selected bills should be in closed state"))
|
||||||
|
|
|
@ -207,11 +207,11 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
search_fields = ('number', 'account__username', 'comments')
|
search_fields = ('number', 'account__username', 'comments')
|
||||||
change_view_actions = [
|
change_view_actions = [
|
||||||
actions.manage_lines, actions.view_bill, actions.download_bills, actions.send_bills,
|
actions.manage_lines, actions.view_bill, actions.download_bills, actions.send_bills,
|
||||||
actions.close_bills, actions.amend_bills,
|
actions.close_bills, actions.amend_bills, actions.close_send_download_bills,
|
||||||
]
|
]
|
||||||
actions = [
|
actions = [
|
||||||
actions.manage_lines, actions.download_bills, actions.close_bills, actions.send_bills,
|
actions.manage_lines, actions.download_bills, actions.close_bills, actions.send_bills,
|
||||||
actions.amend_bills, actions.report
|
actions.amend_bills, actions.report, actions.close_send_download_bills,
|
||||||
]
|
]
|
||||||
change_readonly_fields = ('account_link', 'type', 'is_open', 'amend_of_link', 'amend_links')
|
change_readonly_fields = ('account_link', 'type', 'is_open', 'amend_of_link', 'amend_links')
|
||||||
readonly_fields = ('number', 'display_total', 'is_sent', 'display_payment_state')
|
readonly_fields = ('number', 'display_total', 'is_sent', 'display_payment_state')
|
||||||
|
@ -303,7 +303,7 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
exclude = []
|
exclude = []
|
||||||
if obj:
|
if obj:
|
||||||
if not obj.is_open:
|
if not obj.is_open:
|
||||||
exclude.append('close_bills')
|
exclude += ['close_bills', 'close_send_download_bills']
|
||||||
return [action for action in actions if action.__name__ not in exclude]
|
return [action for action in actions if action.__name__ not in exclude]
|
||||||
|
|
||||||
def get_inline_instances(self, request, obj=None):
|
def get_inline_instances(self, request, obj=None):
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -102,7 +102,7 @@ class Bill(models.Model):
|
||||||
related_name='amends')
|
related_name='amends')
|
||||||
type = models.CharField(_("type"), max_length=16, choices=TYPES)
|
type = models.CharField(_("type"), max_length=16, choices=TYPES)
|
||||||
created_on = models.DateField(_("created on"), auto_now_add=True)
|
created_on = models.DateField(_("created on"), auto_now_add=True)
|
||||||
closed_on = models.DateField(_("closed on"), blank=True, null=True)
|
closed_on = models.DateField(_("closed on"), blank=True, null=True, db_index=True)
|
||||||
is_open = models.BooleanField(_("open"), default=True)
|
is_open = models.BooleanField(_("open"), default=True)
|
||||||
is_sent = models.BooleanField(_("sent"), default=False)
|
is_sent = models.BooleanField(_("sent"), default=False)
|
||||||
due_on = models.DateField(_("due on"), null=True, blank=True)
|
due_on = models.DateField(_("due on"), null=True, blank=True)
|
||||||
|
|
|
@ -49,7 +49,7 @@ def change_ticket_state_factory(action, final_state):
|
||||||
change_ticket_state.url_name = action
|
change_ticket_state.url_name = action
|
||||||
change_ticket_state.verbose_name = action
|
change_ticket_state.verbose_name = action
|
||||||
change_ticket_state.short_description = _('%s selected tickets') % action.capitalize()
|
change_ticket_state.short_description = _('%s selected tickets') % action.capitalize()
|
||||||
change_ticket_state.description = _('Mark ticket as %s.') % final_state.lower()
|
change_ticket_state.help_text = _('Mark ticket as %s.') % final_state.lower()
|
||||||
change_ticket_state.__name__ = action
|
change_ticket_state.__name__ = action
|
||||||
return change_ticket_state
|
return change_ticket_state
|
||||||
|
|
||||||
|
@ -90,7 +90,7 @@ def take_tickets(modeladmin, request, queryset):
|
||||||
modeladmin.message_user(request, msg)
|
modeladmin.message_user(request, msg)
|
||||||
take_tickets.url_name = 'take'
|
take_tickets.url_name = 'take'
|
||||||
take_tickets.short_description = _("Take selected tickets")
|
take_tickets.short_description = _("Take selected tickets")
|
||||||
take_tickets.description = _("Make yourself owner of the ticket.")
|
take_tickets.help_text = _("Make yourself owner of the ticket.")
|
||||||
|
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('issues', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ticket',
|
||||||
|
name='created_at',
|
||||||
|
field=models.DateTimeField(db_index=True, auto_now_add=True, verbose_name='created'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -68,7 +68,7 @@ class Ticket(models.Model):
|
||||||
priority = models.CharField(_("priority"), max_length=32, choices=PRIORITIES,
|
priority = models.CharField(_("priority"), max_length=32, choices=PRIORITIES,
|
||||||
default=MEDIUM)
|
default=MEDIUM)
|
||||||
state = models.CharField(_("state"), max_length=32, choices=STATES, default=NEW)
|
state = models.CharField(_("state"), max_length=32, choices=STATES, default=NEW)
|
||||||
created_at = models.DateTimeField(_("created"), auto_now_add=True)
|
created_at = models.DateTimeField(_("created"), auto_now_add=True, db_index=True)
|
||||||
updated_at = models.DateTimeField(_("modified"), auto_now=True)
|
updated_at = models.DateTimeField(_("modified"), auto_now=True)
|
||||||
cc = models.TextField("CC", help_text=_("emails to send a carbon copy to"),
|
cc = models.TextField("CC", help_text=_("emails to send a carbon copy to"),
|
||||||
blank=True)
|
blank=True)
|
||||||
|
|
|
@ -118,7 +118,7 @@ class BackendLogAdmin(admin.ModelAdmin):
|
||||||
'display_created', 'execution_time',
|
'display_created', 'execution_time',
|
||||||
)
|
)
|
||||||
list_display_links = ('id', 'backend')
|
list_display_links = ('id', 'backend')
|
||||||
list_filter = ('state', 'backend')
|
list_filter = ('state', 'backend', 'server')
|
||||||
date_hierarchy = 'created_at'
|
date_hierarchy = 'created_at'
|
||||||
inlines = (BackendOperationInline,)
|
inlines = (BackendOperationInline,)
|
||||||
fields = (
|
fields = (
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
import orchestra.models.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('orchestration', '0004_route_async_actions'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='backendlog',
|
||||||
|
name='created_at',
|
||||||
|
field=models.DateTimeField(db_index=True, verbose_name='created', auto_now_add=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='route',
|
||||||
|
name='async_actions',
|
||||||
|
field=orchestra.models.fields.MultiSelectField(blank=True, help_text='Specify individual actions to be executed asynchronoulsy.', max_length=256),
|
||||||
|
),
|
||||||
|
]
|
|
@ -79,7 +79,7 @@ class BackendLog(models.Model):
|
||||||
exit_code = models.IntegerField(_("exit code"), null=True)
|
exit_code = models.IntegerField(_("exit code"), null=True)
|
||||||
task_id = models.CharField(_("task ID"), max_length=36, unique=True, null=True,
|
task_id = models.CharField(_("task ID"), max_length=36, unique=True, null=True,
|
||||||
help_text="Celery task ID when used as execution backend")
|
help_text="Celery task ID when used as execution backend")
|
||||||
created_at = models.DateTimeField(_("created"), auto_now_add=True)
|
created_at = models.DateTimeField(_("created"), auto_now_add=True, db_index=True)
|
||||||
updated_at = models.DateTimeField(_("updated"), auto_now=True)
|
updated_at = models.DateTimeField(_("updated"), auto_now=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('orders', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='order',
|
||||||
|
name='registered_on',
|
||||||
|
field=models.DateField(db_index=True, default=django.utils.timezone.now, verbose_name='registered'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -112,7 +112,7 @@ class Order(models.Model):
|
||||||
object_id = models.PositiveIntegerField(null=True)
|
object_id = models.PositiveIntegerField(null=True)
|
||||||
service = models.ForeignKey(settings.ORDERS_SERVICE_MODEL, verbose_name=_("service"),
|
service = models.ForeignKey(settings.ORDERS_SERVICE_MODEL, verbose_name=_("service"),
|
||||||
related_name='orders')
|
related_name='orders')
|
||||||
registered_on = models.DateField(_("registered"), default=timezone.now)
|
registered_on = models.DateField(_("registered"), default=timezone.now, db_index=True)
|
||||||
cancelled_on = models.DateField(_("cancelled"), null=True, blank=True)
|
cancelled_on = models.DateField(_("cancelled"), null=True, blank=True)
|
||||||
billed_on = models.DateField(_("billed"), null=True, blank=True)
|
billed_on = models.DateField(_("billed"), null=True, blank=True)
|
||||||
billed_until = models.DateField(_("billed until"), null=True, blank=True)
|
billed_until = models.DateField(_("billed until"), null=True, blank=True)
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
import django.db.models.deletion
|
||||||
|
import orchestra.models.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('payments', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='paymentsource',
|
||||||
|
name='method',
|
||||||
|
field=models.CharField(max_length=32, verbose_name='method', choices=[('SEPADirectDebit', 'SEPA Direct Debit')]),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='transaction',
|
||||||
|
name='process',
|
||||||
|
field=models.ForeignKey(verbose_name='process', on_delete=django.db.models.deletion.SET_NULL, blank=True, related_name='transactions', to='payments.TransactionProcess', null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='transactionprocess',
|
||||||
|
name='created_at',
|
||||||
|
field=models.DateTimeField(db_index=True, verbose_name='created', auto_now_add=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='transactionprocess',
|
||||||
|
name='file',
|
||||||
|
field=orchestra.models.fields.PrivateFileField(blank=True, verbose_name='file', upload_to=''),
|
||||||
|
),
|
||||||
|
]
|
|
@ -166,7 +166,7 @@ class TransactionProcess(models.Model):
|
||||||
data = JSONField(_("data"), blank=True)
|
data = JSONField(_("data"), blank=True)
|
||||||
file = PrivateFileField(_("file"), blank=True)
|
file = PrivateFileField(_("file"), blank=True)
|
||||||
state = models.CharField(_("state"), max_length=16, choices=STATES, default=CREATED)
|
state = models.CharField(_("state"), max_length=16, choices=STATES, default=CREATED)
|
||||||
created_at = models.DateTimeField(_("created"), auto_now_add=True)
|
created_at = models.DateTimeField(_("created"), auto_now_add=True, db_index=True)
|
||||||
updated_at = models.DateTimeField(_("updated"), auto_now=True)
|
updated_at = models.DateTimeField(_("updated"), auto_now=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
{% block object-tools-items %}
|
{% block object-tools-items %}
|
||||||
{% for item in object_tools_items %}
|
{% for item in object_tools_items %}
|
||||||
<li><a href="{{ item.url_name }}/" class="{{ item.css_class }}" title="{{ item.description }}">{{ item.verbose_name }}</a></li>
|
<li><a href="{{ item.url_name }}/" class="{{ item.css_class }}" title="{{ item.help_text }}">{{ item.verbose_name }}</a></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
Loading…
Reference in a new issue