Added support for bill sublines on microspective template0

This commit is contained in:
Marc 2014-09-03 22:01:44 +00:00
parent 5cfb48f8df
commit 1f00b27667
10 changed files with 150 additions and 46 deletions

View File

@ -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. * make account_link to autoreplace account on change view.
* LAST version of this shit http://wkhtmltopdf.org/downloads.html * LAST version of this shit http://wkhtmltopdf.org/downloads.html

View File

@ -1,10 +1,9 @@
from django import forms from django import forms
from django.conf.urls import patterns, url from django.conf.urls import patterns, url
from django.contrib import admin from django.contrib import admin
from django.contrib.admin.utils import unquote
from django.forms.models import BaseInlineFormSet from django.forms.models import BaseInlineFormSet
from orchestra.utils.functional import cached
from .utils import set_url_query, action_to_view from .utils import set_url_query, action_to_view
@ -59,12 +58,11 @@ class ChangeViewActionsMixin(object):
url('^(\d+)/%s/$' % action.url_name, url('^(\d+)/%s/$' % action.url_name,
admin_site.admin_view(action), admin_site.admin_view(action),
name='%s_%s_%s' % (opts.app_label, name='%s_%s_%s' % (opts.app_label,
opts.module_name, opts.model_name,
action.url_name))) action.url_name)))
return new_urls + urls return new_urls + urls
@cached def get_change_view_actions(self, obj=None):
def get_change_view_actions(self):
views = [] views = []
for action in self.change_view_actions: for action in self.change_view_actions:
if isinstance(action, basestring): if isinstance(action, basestring):
@ -75,16 +73,18 @@ class ChangeViewActionsMixin(object):
view.url_name.capitalize().replace('_', ' ')) view.url_name.capitalize().replace('_', ' '))
view.css_class = getattr(action, 'css_class', 'historylink') view.css_class = getattr(action, 'css_class', 'historylink')
view.description = getattr(action, 'description', '') view.description = getattr(action, 'description', '')
view.__name__ = action.__name__
views.append(view) views.append(view)
return views 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: if not 'extra_context' in kwargs:
kwargs['extra_context'] = {} kwargs['extra_context'] = {}
kwargs['extra_context']['object_tools_items'] = [ 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): class ChangeAddFieldsMixin(object):

View File

@ -1,13 +1,48 @@
import StringIO
import zipfile
from django.http import HttpResponse 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 = queryset.get()
bill.close() pdf = html_to_pdf(bill.html)
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)
return HttpResponse(pdf, content_type='application/pdf') 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'

View File

@ -1,7 +1,9 @@
from django import forms from django import forms
from django.contrib import admin from django.contrib import admin
#from django.contrib.admin.utils import unquote
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db import models from django.db import models
#from django.shortcuts import redirect
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ExtendedModelAdmin 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 orchestra.apps.accounts.admin import AccountAdminMixin
from . import settings from . import settings
from .actions import generate_bill from .actions import render_bills, download_bills, view_bill, close_bills
from .filters import BillTypeListFilter from .filters import BillTypeListFilter
from .models import (Bill, Invoice, AmendmentInvoice, Fee, AmendmentFee, Budget, from .models import (Bill, Invoice, AmendmentInvoice, Fee, AmendmentFee, Budget,
BillLine, BudgetLine) BillLine, BudgetLine)
@ -66,7 +68,8 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
'fields': ('html',), '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') change_readonly_fields = ('account_link', 'type', 'status')
readonly_fields = ('number', 'display_total') readonly_fields = ('number', 'display_total')
inlines = [BillLineInline] inlines = [BillLineInline]
@ -97,6 +100,13 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
fields += self.add_fields fields += self.add_fields
return 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): def get_inline_instances(self, request, obj=None):
if self.model is Budget: if self.model is Budget:
self.inlines = [BudgetLineInline] self.inlines = [BudgetLineInline]
@ -112,12 +122,20 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
kwargs['widget'] = forms.Textarea(attrs={'cols': 150, 'rows': 20}) kwargs['widget'] = forms.Textarea(attrs={'cols': 150, 'rows': 20})
return super(BillAdmin, self).formfield_for_dbfield(db_field, **kwargs) return super(BillAdmin, self).formfield_for_dbfield(db_field, **kwargs)
def queryset(self, request): def get_queryset(self, request):
qs = super(BillAdmin, self).queryset(request) qs = super(BillAdmin, self).get_queryset(request)
qs = qs.annotate(models.Count('billlines')) qs = qs.annotate(models.Count('billlines'))
qs = qs.prefetch_related('billlines', 'billlines__sublines') qs = qs.prefetch_related('billlines', 'billlines__sublines')
return qs 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(Bill, BillAdmin)
admin.site.register(Invoice, BillAdmin) admin.site.register(Invoice, BillAdmin)

View File

@ -118,7 +118,7 @@ class Bill(models.Model):
def render(self): def render(self):
context = Context({ context = Context({
'bill': self, 'bill': self,
'lines': self.lines.all(), 'lines': self.lines.all().prefetch_related('sublines'),
'seller': self.seller, 'seller': self.seller,
'buyer': self.buyer, 'buyer': self.buyer,
'seller_info': { 'seller_info': {
@ -145,7 +145,7 @@ class Bill(models.Model):
@cached @cached
def get_subtotals(self): def get_subtotals(self):
subtotals = {} 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, taxes = subtotals.get(line.tax, (0, 0))
subtotal += line.total subtotal += line.total
for subline in line.sublines.all(): for subline in line.sublines.all():
@ -155,6 +155,7 @@ class Bill(models.Model):
@cached @cached
def get_total(self): def get_total(self):
# TODO self.total = self.get_total on self.save()
total = 0 total = 0
for tax, subtotal in self.get_subtotals().iteritems(): for tax, subtotal in self.get_subtotals().iteritems():
subtotal, taxes = subtotal subtotal, taxes = subtotal

View File

@ -1,7 +1,8 @@
body { body {
/* max-width: 650px;*/ /* max-width: 650px;*/
max-width: 800px; max-width: 670px;
margin: 40 auto !important; margin: 40 auto !important;
/* margin-bottom: 30 !important;*/
float: none !important; float: none !important;
font-family: Arial, 'Liberation Sans', 'DejaVu Sans', sans-serif; font-family: Arial, 'Liberation Sans', 'DejaVu Sans', sans-serif;
} }
@ -34,7 +35,7 @@ a:hover {
font-size: 20; font-size: 20;
font-weight: bold; font-weight: bold;
color: grey; color: grey;
margin-top: 15px; margin-top: 30px;
margin-bottom: 10px; margin-bottom: 10px;
} }
@ -44,11 +45,9 @@ a:hover {
font-weight: normal; font-weight: normal;
} }
#pagination {
font-size: small;
}
/* SUMMARY */ /* SUMMARY */
#bill-summary { #bill-summary {
clear: right; clear: right;
} }
@ -113,6 +112,10 @@ a:hover {
margin: 40px; margin: 40px;
} }
#seller-details {
margin-top: 0px;
}
#seller-details p { #seller-details p {
margin-top: 5px; margin-top: 5px;
} }
@ -158,10 +161,14 @@ a:hover {
color: {{ color }}; color: {{ color }};
} }
#lines .value { #lines .last {
border-bottom: 1px solid #CCC; border-bottom: 1px solid #CCC;
} }
#lines .subline {
padding-top: 0px;
}
#lines .column-id { #lines .column-id {
width: 5%; width: 5%;
text-align: right; text-align: right;
@ -230,27 +237,32 @@ a:hover {
/* FOOTER */ /* FOOTER */
.content {
display: table-row; /* height is dynamic, and will expand... */
height: 100%; /* ...as content is added (won't scroll) */
}
.wrapper { .wrapper {
min-height: 100%; display: table;
height: auto !important;
height: 100%; height: 100%;
margin: 0 auto -4em; width: 100%;
} }
#footer, .push { .footer {
height: 4em; display: table-row;
} }
#footer .title { .footer .title {
color: {{ color }}; color: {{ color }};
font-weight: bold; font-weight: bold;
} }
#footer > * > * { .footer > * > * {
margin: 5px; margin: 5px;
margin-bottom: 8px;
color: #666; color: #666;
font-size: small; font-size: small;
text-align: justify;
} }
#footer-column-1 { #footer-column-1 {
@ -262,3 +274,7 @@ a:hover {
float: right; float: right;
width: 48%; width: 48%;
} }
#questions {
margin-bottom: 0px;
}

View File

@ -10,6 +10,7 @@
{% block body %} {% block body %}
<div class="wrapper"> <div class="wrapper">
<div class="content">
{% block header %} {% block header %}
<div id="logo"> <div id="logo">
{% block logo %} {% block logo %}
@ -40,7 +41,6 @@
<div id="bill-number"> <div id="bill-number">
{{ bill.get_type_display }}<br> {{ bill.get_type_display }}<br>
<span class="value">{{ bill.number }}</span><br> <span class="value">{{ bill.number }}</span><br>
<span id="pagination">Page 1 of 1</span>
</div> </div>
<div id="bill-summary"> <div id="bill-summary">
<hr> <hr>
@ -74,13 +74,23 @@
<span class="title column-rate">rate/price</span> <span class="title column-rate">rate/price</span>
<span class="title column-subtotal">subtotal</span> <span class="title column-subtotal">subtotal</span>
<br> <br>
{% for line in bill.lines.all %} {% for line in lines %}
<span class="value column-id">{{ line.id }}</span> {% with sublines=line.sublines.all %}
<span class="value column-description">{{ line.description }}</span> <span class="{% if not sublines %}last {% endif %}column-id">{{ line.id }}</span>
<span class="value column-quantity">{{ line.amount|default:"&nbsp;" }}</span> <span class="{% if not sublines %}last {% endif %}column-description">{{ line.description }}</span>
<span class="value column-rate">{% if line.rate %}{{ line.rate }} &{{ currency.lower }};{% else %}&nbsp;{% endif %}</span> <span class="{% if not sublines %}last {% endif %}column-quantity">{{ line.amount|default:"&nbsp;" }}</span>
<span class="value column-subtotal">{{ line.total }} &{{ currency.lower }};</span> <span class="{% if not sublines %}last {% endif %}column-rate">{% if line.rate %}{{ line.rate }} &{{ currency.lower }};{% else %}&nbsp;{% endif %}</span>
<span class="{% if not sublines %}last {% endif %}column-subtotal">{{ line.total }} &{{ currency.lower }};</span>
<br> <br>
{% for subline in sublines %}
<span class="{% if forloop.last %}last {% endif %}subline column-id">&nbsp;</span>
<span class="{% if forloop.last %}last {% endif %}subline column-description">{{ subline.description }}</span>
<span class="{% if forloop.last %}last {% endif %}subline column-quantity">&nbsp;</span>
<span class="{% if forloop.last %}last {% endif %}subline column-rate">&nbsp;</span>
<span class="{% if forloop.last %}last {% endif %}subline column-subtotal">{{ subline.total }} &{{ currency.lower }};</span>
<br>
{% endfor %}
{% endwith %}
{% endfor %} {% endfor %}
</div> </div>
<div id="totals"> <div id="totals">
@ -100,9 +110,8 @@
{% endblock %} {% endblock %}
{% block footer %} {% block footer %}
<div class="push"></div>
</div> </div>
<div id="footer"> <div class="footer">
<div id="footer-column-1"> <div id="footer-column-1">
<div id="comments"> <div id="comments">
{% if bill.comments %} {% if bill.comments %}
@ -112,7 +121,15 @@
</div> </div>
<div id="footer-column-2"> <div id="footer-column-2">
<div id="payment"> <div id="payment">
<span class="title">PAYMENT</span> {{ bill.payment.message }} <span class="title">PAYMENT</span>
{% if bill.payment.message %}
{{ bill.payment.message }}
{% else %}
You can pay our invoice by bank transfer. <br>
Please make sure to state your name and the invoice number.
Our bank account number is <br>
<strong>000-000-000-000 (Orchestra)</strong>
{% endif %}
</div> </div>
<div id="questions"> <div id="questions">
<span class="title">QUESTIONS</span> If you have any question about your bill, please <span class="title">QUESTIONS</span> If you have any question about your bill, please
@ -121,6 +138,7 @@
</div> </div>
</div> </div>
</div> </div>
</div>
{% endblock %} {% endblock %}
{% endblock %} {% endblock %}

View File

@ -236,7 +236,8 @@ class OrderQuerySet(models.QuerySet):
def bill(self, **options): def bill(self, **options):
bills = [] bills = []
bill_backend = Order.get_bill_backend() bill_backend = Order.get_bill_backend()
for account, services in self.group_by('account', 'service'): qs = self.select_related('account', 'service')
for account, services in qs.group_by('account', 'service'):
bill_lines = [] bill_lines = []
for service, orders in services: for service, orders in services:
lines = service.handler.create_bill_lines(orders, **options) lines = service.handler.create_bill_lines(orders, **options)
@ -351,6 +352,11 @@ class MetricStorage(models.Model):
else: else:
metric.save() metric.save()
@classmethod
def get(cls, order, ini, end):
# TODO
pass
@receiver(pre_delete, dispatch_uid="orders.cancel_orders") @receiver(pre_delete, dispatch_uid="orders.cancel_orders")
def cancel_orders(sender, **kwargs): def cancel_orders(sender, **kwargs):

View File

@ -35,6 +35,8 @@ MEDIA_URL = '/media/'
ALLOWED_HOSTS = '*' ALLOWED_HOSTS = '*'
# Set this to True to wrap each HTTP request in a transaction on this database.
ATOMIC_REQUESTS = True
MIDDLEWARE_CLASSES = ( MIDDLEWARE_CLASSES = (
'django.middleware.gzip.GZipMiddleware', 'django.middleware.gzip.GZipMiddleware',
@ -43,7 +45,6 @@ MIDDLEWARE_CLASSES = (
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.transaction.TransactionMiddleware',
'orchestra.core.cache.RequestCacheMiddleware', 'orchestra.core.cache.RequestCacheMiddleware',
'orchestra.apps.orchestration.middlewares.OperationsMiddleware', 'orchestra.apps.orchestration.middlewares.OperationsMiddleware',
# Uncomment the next line for simple clickjacking protection: # Uncomment the next line for simple clickjacking protection:

8
orchestra/utils/html.py Normal file
View File

@ -0,0 +1,8 @@
from orchestra.utils.system import run
def html_to_pdf(html):
""" converts HTL to PDF using wkhtmltopdf """
return run('xvfb-run -a -s "-screen 0 640x4800x16" '
'wkhtmltopdf --footer-center "Page [page] of [topage]" --footer-font-size 9 - -',
stdin=html.encode('utf-8'), display=False)