diff --git a/TODO.md b/TODO.md index d2bc862b..57e0b36a 100644 --- a/TODO.md +++ b/TODO.md @@ -78,3 +78,4 @@ at + clock time, midnight, noon- At 3:30 p.m., At 4:01, At noon * make account_link to autoreplace account on change view. * LAST version of this shit http://wkhtmltopdf.org/downloads.html + diff --git a/orchestra/admin/options.py b/orchestra/admin/options.py index 6adaf842..53d65d81 100644 --- a/orchestra/admin/options.py +++ b/orchestra/admin/options.py @@ -1,10 +1,9 @@ from django import forms from django.conf.urls import patterns, url from django.contrib import admin +from django.contrib.admin.utils import unquote from django.forms.models import BaseInlineFormSet -from orchestra.utils.functional import cached - from .utils import set_url_query, action_to_view @@ -59,12 +58,11 @@ class ChangeViewActionsMixin(object): url('^(\d+)/%s/$' % action.url_name, admin_site.admin_view(action), name='%s_%s_%s' % (opts.app_label, - opts.module_name, + opts.model_name, action.url_name))) return new_urls + urls - @cached - def get_change_view_actions(self): + def get_change_view_actions(self, obj=None): views = [] for action in self.change_view_actions: if isinstance(action, basestring): @@ -75,16 +73,18 @@ class ChangeViewActionsMixin(object): view.url_name.capitalize().replace('_', ' ')) view.css_class = getattr(action, 'css_class', 'historylink') view.description = getattr(action, 'description', '') + view.__name__ = action.__name__ views.append(view) return views - def change_view(self, *args, **kwargs): + def change_view(self, request, object_id, **kwargs): + obj = self.get_object(request, unquote(object_id)) if not 'extra_context' in kwargs: kwargs['extra_context'] = {} kwargs['extra_context']['object_tools_items'] = [ - action.__dict__ for action in self.get_change_view_actions() + action.__dict__ for action in self.get_change_view_actions(obj) ] - return super(ChangeViewActionsMixin, self).change_view(*args, **kwargs) + return super(ChangeViewActionsMixin, self).change_view(request, object_id, **kwargs) class ChangeAddFieldsMixin(object): diff --git a/orchestra/apps/bills/actions.py b/orchestra/apps/bills/actions.py index 5161f2c3..4a800ed7 100644 --- a/orchestra/apps/bills/actions.py +++ b/orchestra/apps/bills/actions.py @@ -1,13 +1,48 @@ +import StringIO +import zipfile + from django.http import HttpResponse +from django.utils.translation import ugettext_lazy as _ -from orchestra.utils.system import run +from orchestra.utils.html import html_to_pdf -def generate_bill(modeladmin, request, queryset): +def render_bills(modeladmin, request, queryset): + for bill in queryset: + bill.html = bill.render() + bill.save() +render_bills.verbose_name = _("Render") +render_bills.url_name = 'render' + + +def download_bills(modeladmin, request, queryset): + if queryset.count() > 1: + stringio = StringIO.StringIO() + archive = zipfile.ZipFile(stringio, 'w') + for bill in queryset: + pdf = html_to_pdf(bill.html) + archive.writestr('%s.pdf' % bill.number, pdf) + archive.close() + response = HttpResponse(stringio.getvalue(), content_type='application/pdf') + response['Content-Disposition'] = 'attachment; filename="orchestra-bills.zip"' + return response bill = queryset.get() - bill.close() - return HttpResponse(bill.html) - pdf = run('xvfb-run -a -s "-screen 0 640x4800x16" ' - 'wkhtmltopdf --footer-center "Page [page] of [topage]" --footer-font-size 9 - -', - stdin=bill.html.encode('utf-8'), display=False) + pdf = html_to_pdf(bill.html) return HttpResponse(pdf, content_type='application/pdf') +download_bills.verbose_name = _("Download") +download_bills.url_name = 'download' + + +def view_bill(modeladmin, request, queryset): + bill = queryset.get() + bill.html = bill.render() + return HttpResponse(bill.html) +view_bill.verbose_name = _("View") +view_bill.url_name = 'view' + + +def close_bills(modeladmin, request, queryset): + for bill in queryset: + bill.close() +close_bills.verbose_name = _("Close") +close_bills.url_name = 'close' diff --git a/orchestra/apps/bills/admin.py b/orchestra/apps/bills/admin.py index 643ed8af..e6fbf3f3 100644 --- a/orchestra/apps/bills/admin.py +++ b/orchestra/apps/bills/admin.py @@ -1,7 +1,9 @@ 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 @@ -9,7 +11,7 @@ from orchestra.admin.utils import admin_link, admin_date from orchestra.apps.accounts.admin import AccountAdminMixin from . import settings -from .actions import generate_bill +from .actions import render_bills, download_bills, view_bill, close_bills from .filters import BillTypeListFilter from .models import (Bill, Invoice, AmendmentInvoice, Fee, AmendmentFee, Budget, BillLine, BudgetLine) @@ -66,7 +68,8 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin): 'fields': ('html',), }), ) - change_view_actions = [generate_bill] + actions = [render_bills, download_bills, close_bills] + change_view_actions = [render_bills, view_bill, download_bills] change_readonly_fields = ('account_link', 'type', 'status') readonly_fields = ('number', 'display_total') inlines = [BillLineInline] @@ -97,6 +100,13 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin): fields += self.add_fields return fields + def get_change_view_actions(self, obj=None): + actions = super(BillAdmin, self).get_change_view_actions(obj) + if obj and not obj.html: + actions = [action for action in actions + if action.__name__ not in ('view_bill', 'download_bills')] + return actions + def get_inline_instances(self, request, obj=None): if self.model is Budget: self.inlines = [BudgetLineInline] @@ -112,12 +122,20 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin): kwargs['widget'] = forms.Textarea(attrs={'cols': 150, 'rows': 20}) return super(BillAdmin, self).formfield_for_dbfield(db_field, **kwargs) - def queryset(self, request): - qs = super(BillAdmin, self).queryset(request) + def get_queryset(self, request): + qs = super(BillAdmin, self).get_queryset(request) qs = qs.annotate(models.Count('billlines')) qs = qs.prefetch_related('billlines', 'billlines__sublines') return qs +# def change_view(self, request, object_id, **kwargs): +# opts = self.model._meta +# if opts.module_name == 'bill': +# obj = self.get_object(request, unquote(object_id)) +# return redirect( +# reverse('admin:bills_%s_change' % obj.type.lower(), args=[obj.pk])) +# return super(BillAdmin, self).change_view(request, object_id, **kwargs) + admin.site.register(Bill, BillAdmin) admin.site.register(Invoice, BillAdmin) diff --git a/orchestra/apps/bills/models.py b/orchestra/apps/bills/models.py index 33a902bf..6e5680a3 100644 --- a/orchestra/apps/bills/models.py +++ b/orchestra/apps/bills/models.py @@ -118,7 +118,7 @@ class Bill(models.Model): def render(self): context = Context({ 'bill': self, - 'lines': self.lines.all(), + 'lines': self.lines.all().prefetch_related('sublines'), 'seller': self.seller, 'buyer': self.buyer, 'seller_info': { @@ -145,7 +145,7 @@ class Bill(models.Model): @cached def get_subtotals(self): subtotals = {} - for line in self.lines.all(): + for line in self.lines.all().prefetch_related('sublines'): subtotal, taxes = subtotals.get(line.tax, (0, 0)) subtotal += line.total for subline in line.sublines.all(): @@ -155,6 +155,7 @@ class Bill(models.Model): @cached def get_total(self): + # TODO self.total = self.get_total on self.save() total = 0 for tax, subtotal in self.get_subtotals().iteritems(): subtotal, taxes = subtotal diff --git a/orchestra/apps/bills/templates/bills/microspective.css b/orchestra/apps/bills/templates/bills/microspective.css index bfd5b68d..f04ba7c3 100644 --- a/orchestra/apps/bills/templates/bills/microspective.css +++ b/orchestra/apps/bills/templates/bills/microspective.css @@ -1,7 +1,8 @@ body { /* max-width: 650px;*/ - max-width: 800px; + max-width: 670px; margin: 40 auto !important; +/* margin-bottom: 30 !important;*/ float: none !important; font-family: Arial, 'Liberation Sans', 'DejaVu Sans', sans-serif; } @@ -34,7 +35,7 @@ a:hover { font-size: 20; font-weight: bold; color: grey; - margin-top: 15px; + margin-top: 30px; margin-bottom: 10px; } @@ -44,11 +45,9 @@ a:hover { font-weight: normal; } -#pagination { - font-size: small; -} /* SUMMARY */ + #bill-summary { clear: right; } @@ -113,6 +112,10 @@ a:hover { margin: 40px; } +#seller-details { + margin-top: 0px; +} + #seller-details p { margin-top: 5px; } @@ -158,10 +161,14 @@ a:hover { color: {{ color }}; } -#lines .value { +#lines .last { border-bottom: 1px solid #CCC; } +#lines .subline { + padding-top: 0px; +} + #lines .column-id { width: 5%; text-align: right; @@ -230,27 +237,32 @@ a:hover { /* FOOTER */ +.content { + display: table-row; /* height is dynamic, and will expand... */ + height: 100%; /* ...as content is added (won't scroll) */ +} .wrapper { - min-height: 100%; - height: auto !important; + display: table; height: 100%; - margin: 0 auto -4em; + width: 100%; } -#footer, .push { - height: 4em; +.footer { + display: table-row; } -#footer .title { +.footer .title { color: {{ color }}; font-weight: bold; } -#footer > * > * { +.footer > * > * { margin: 5px; + margin-bottom: 8px; color: #666; font-size: small; + text-align: justify; } #footer-column-1 { @@ -262,3 +274,7 @@ a:hover { float: right; width: 48%; } + +#questions { + margin-bottom: 0px; +} diff --git a/orchestra/apps/bills/templates/bills/microspective.html b/orchestra/apps/bills/templates/bills/microspective.html index 981cd10f..54d96dc0 100644 --- a/orchestra/apps/bills/templates/bills/microspective.html +++ b/orchestra/apps/bills/templates/bills/microspective.html @@ -10,6 +10,7 @@ {% block body %}
+
{% block header %}