Added support for bill sublines on microspective template0
This commit is contained in:
parent
5cfb48f8df
commit
1f00b27667
1
TODO.md
1
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
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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:" " }}</span>
|
||||
<span class="value column-rate">{% if line.rate %}{{ line.rate }} &{{ currency.lower }};{% else %} {% 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:" " }}</span>
|
||||
<span class="{% if not sublines %}last {% endif %}column-rate">{% if line.rate %}{{ line.rate }} &{{ currency.lower }};{% else %} {% 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"> </span>
|
||||
<span class="{% if forloop.last %}last {% endif %}subline column-description">{{ subline.description }}</span>
|
||||
<span class="{% if forloop.last %}last {% endif %}subline column-quantity"> </span>
|
||||
<span class="{% if forloop.last %}last {% endif %}subline column-rate"> </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 %}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
@ -350,6 +351,11 @@ class MetricStorage(models.Model):
|
|||
cls.objects.create(order=order, value=value)
|
||||
else:
|
||||
metric.save()
|
||||
|
||||
@classmethod
|
||||
def get(cls, order, ini, end):
|
||||
# TODO
|
||||
pass
|
||||
|
||||
|
||||
@receiver(pre_delete, dispatch_uid="orders.cancel_orders")
|
||||
|
|
|
@ -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
8
orchestra/utils/html.py
Normal 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)
|
Loading…
Reference in a new issue