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.
* LAST version of this shit http://wkhtmltopdf.org/downloads.html

View file

@ -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):

View file

@ -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'

View file

@ -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)

View file

@ -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

View file

@ -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;
}

View file

@ -10,6 +10,7 @@
{% block body %}
<div class="wrapper">
<div class="content">
{% block header %}
<div id="logo">
{% block logo %}
@ -40,7 +41,6 @@
<div id="bill-number">
{{ bill.get_type_display }}<br>
<span class="value">{{ bill.number }}</span><br>
<span id="pagination">Page 1 of 1</span>
</div>
<div id="bill-summary">
<hr>
@ -74,13 +74,23 @@
<span class="title column-rate">rate/price</span>
<span class="title column-subtotal">subtotal</span>
<br>
{% for line in bill.lines.all %}
<span class="value column-id">{{ line.id }}</span>
<span class="value column-description">{{ line.description }}</span>
<span class="value column-quantity">{{ line.amount|default:"&nbsp;" }}</span>
<span class="value column-rate">{% if line.rate %}{{ line.rate }} &{{ currency.lower }};{% else %}&nbsp;{% endif %}</span>
<span class="value column-subtotal">{{ line.total }} &{{ currency.lower }};</span>
{% for line in lines %}
{% with sublines=line.sublines.all %}
<span class="{% if not sublines %}last {% endif %}column-id">{{ line.id }}</span>
<span class="{% if not sublines %}last {% endif %}column-description">{{ line.description }}</span>
<span class="{% if not sublines %}last {% endif %}column-quantity">{{ line.amount|default:"&nbsp;" }}</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>
{% 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 %}
</div>
<div id="totals">
@ -100,9 +110,8 @@
{% endblock %}
{% block footer %}
<div class="push"></div>
</div>
<div id="footer">
<div class="footer">
<div id="footer-column-1">
<div id="comments">
{% if bill.comments %}
@ -112,7 +121,15 @@
</div>
<div id="footer-column-2">
<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 id="questions">
<span class="title">QUESTIONS</span> If you have any question about your bill, please
@ -121,6 +138,7 @@
</div>
</div>
</div>
</div>
{% endblock %}
{% endblock %}

View file

@ -236,7 +236,8 @@ class OrderQuerySet(models.QuerySet):
def bill(self, **options):
bills = []
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 = []
for service, orders in services:
lines = service.handler.create_bill_lines(orders, **options)
@ -351,6 +352,11 @@ class MetricStorage(models.Model):
else:
metric.save()
@classmethod
def get(cls, order, ini, end):
# TODO
pass
@receiver(pre_delete, dispatch_uid="orders.cancel_orders")
def cancel_orders(sender, **kwargs):

View file

@ -35,6 +35,8 @@ MEDIA_URL = '/media/'
ALLOWED_HOSTS = '*'
# Set this to True to wrap each HTTP request in a transaction on this database.
ATOMIC_REQUESTS = True
MIDDLEWARE_CLASSES = (
'django.middleware.gzip.GZipMiddleware',
@ -43,7 +45,6 @@ MIDDLEWARE_CLASSES = (
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.transaction.TransactionMiddleware',
'orchestra.core.cache.RequestCacheMiddleware',
'orchestra.apps.orchestration.middlewares.OperationsMiddleware',
# 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)