Improvements on bills app

This commit is contained in:
Marc 2014-08-19 18:59:23 +00:00
parent 26ee8bdfab
commit 1403329d1b
17 changed files with 999 additions and 62 deletions

View file

@ -1,47 +1,11 @@
from django import forms from django import forms
from django.conf.urls import patterns, url
from django.contrib import admin from django.contrib import admin
from django.forms.models import BaseInlineFormSet from django.forms.models import BaseInlineFormSet
from .utils import set_url_query from orchestra.utils.functional import cached
from .utils import set_url_query, action_to_view
class ExtendedModelAdmin(admin.ModelAdmin):
add_fields = ()
add_fieldsets = ()
add_form = None
change_readonly_fields = ()
def get_readonly_fields(self, request, obj=None):
fields = super(ExtendedModelAdmin, self).get_readonly_fields(request, obj=obj)
if obj:
return fields + self.change_readonly_fields
return fields
def get_fieldsets(self, request, obj=None):
if not obj:
if self.add_fieldsets:
return self.add_fieldsets
elif self.add_fields:
return [(None, {'fields': self.add_fields})]
return super(ExtendedModelAdmin, self).get_fieldsets(request, obj=obj)
def get_inline_instances(self, request, obj=None):
""" add_inlines and inline.parent_object """
self.inlines = getattr(self, 'add_inlines', self.inlines)
if obj:
self.inlines = type(self).inlines
inlines = super(ExtendedModelAdmin, self).get_inline_instances(request, obj=obj)
for inline in inlines:
inline.parent_object = obj
return inlines
def get_form(self, request, obj=None, **kwargs):
""" Use special form during user creation """
defaults = {}
if obj is None and self.add_form:
defaults['form'] = self.add_form
defaults.update(kwargs)
return super(ExtendedModelAdmin, self).get_form(request, obj, **defaults)
class ChangeListDefaultFilter(object): class ChangeListDefaultFilter(object):
@ -58,8 +22,10 @@ class ChangeListDefaultFilter(object):
for key, value in self.default_changelist_filters: for key, value in self.default_changelist_filters:
set_url_query(request, key, value) set_url_query(request, key, value)
defaults.append(key) defaults.append(key)
# hack response cl context in order to hook default filter awaearness into search_form.html template # hack response cl context in order to hook default filter awaearness
response = super(ChangeListDefaultFilter, self).changelist_view(request, extra_context=extra_context) # into search_form.html template
response = super(ChangeListDefaultFilter, self).changelist_view(request,
extra_context=extra_context)
if hasattr(response, 'context_data') and 'cl' in response.context_data: if hasattr(response, 'context_data') and 'cl' in response.context_data:
response.context_data['cl'].default_changelist_filters = defaults response.context_data['cl'].default_changelist_filters = defaults
return response return response
@ -74,3 +40,92 @@ class AtLeastOneRequiredInlineFormSet(BaseInlineFormSet):
if not any(cleaned_data and not cleaned_data.get('DELETE', False) if not any(cleaned_data and not cleaned_data.get('DELETE', False)
for cleaned_data in self.cleaned_data): for cleaned_data in self.cleaned_data):
raise forms.ValidationError('At least one item required.') raise forms.ValidationError('At least one item required.')
class ChangeViewActionsMixin(object):
""" Makes actions visible on the admin change view page. """
change_view_actions = ()
change_form_template = 'orchestra/admin/change_form.html'
def get_urls(self):
"""Returns the additional urls for the change view links"""
urls = super(ChangeViewActionsMixin, self).get_urls()
admin_site = self.admin_site
opts = self.model._meta
new_urls = patterns('')
for action in self.get_change_view_actions():
new_urls += patterns('',
url('^(\d+)/%s/$' % action.url_name,
admin_site.admin_view(action),
name='%s_%s_%s' % (opts.app_label,
opts.module_name,
action.url_name)))
return new_urls + urls
@cached
def get_change_view_actions(self):
views = []
for action in self.change_view_actions:
if isinstance(action, basestring):
action = getattr(self, action)
view = action_to_view(action, self)
view.url_name = getattr(action, 'url_name', action.__name__)
view.verbose_name = getattr(action, 'verbose_name',
view.url_name.capitalize().replace('_', ' '))
view.css_class = getattr(action, 'css_class', 'historylink')
view.description = getattr(action, 'description', '')
views.append(view)
return views
def change_view(self, *args, **kwargs):
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()
]
return super(ChangeViewActionsMixin, self).change_view(*args, **kwargs)
class ChangeAddFieldsMixin(object):
""" Enables to specify different set of fields for change and add views """
add_fields = ()
add_fieldsets = ()
add_form = None
change_readonly_fields = ()
def get_readonly_fields(self, request, obj=None):
fields = super(ChangeAddFieldsMixin, self).get_readonly_fields(request, obj=obj)
if obj:
return fields + self.change_readonly_fields
return fields
def get_fieldsets(self, request, obj=None):
if not obj:
if self.add_fieldsets:
return self.add_fieldsets
elif self.add_fields:
return [(None, {'fields': self.add_fields})]
return super(ChangeAddFieldsMixin, self).get_fieldsets(request, obj=obj)
def get_inline_instances(self, request, obj=None):
""" add_inlines and inline.parent_object """
self.inlines = getattr(self, 'add_inlines', self.inlines)
if obj:
self.inlines = type(self).inlines
inlines = super(ChangeAddFieldsMixin, self).get_inline_instances(request, obj=obj)
for inline in inlines:
inline.parent_object = obj
return inlines
def get_form(self, request, obj=None, **kwargs):
""" Use special form during user creation """
defaults = {}
if obj is None and self.add_form:
defaults['form'] = self.add_form
defaults.update(kwargs)
return super(ChangeAddFieldsMixin, self).get_form(request, obj, **defaults)
class ExtendedModelAdmin(ChangeViewActionsMixin, ChangeAddFieldsMixin, admin.ModelAdmin):
pass

View file

@ -4,6 +4,7 @@ from django.conf import settings
from django.contrib import admin from django.contrib import admin
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 import importlib from django.utils import importlib
from django.utils.html import escape from django.utils.html import escape
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
@ -74,6 +75,19 @@ def set_url_query(request, key, value):
request.META['QUERY_STRING'] = request.GET.urlencode() request.META['QUERY_STRING'] = request.GET.urlencode()
def action_to_view(action, modeladmin):
""" Converts modeladmin action to view function """
def action_view(request, object_id=1, modeladmin=modeladmin, action=action):
queryset = modeladmin.model.objects.filter(pk=object_id)
response = action(modeladmin, request, queryset)
if not response:
opts = modeladmin.model._meta
url = 'admin:%s_%s_change' % (opts.app_label, opts.module_name)
return redirect(url, object_id)
return response
return action_view
@admin_field @admin_field
def admin_link(*args, **kwargs): def admin_link(*args, **kwargs):
instance = args[-1] instance = args[-1]

View file

@ -27,5 +27,9 @@ class Account(models.Model):
def name(self): def name(self):
return self.user.username return self.user.username
@classmethod
def get_main(cls):
return cls.objects.get(pk=settings.ACCOUNTS_MAIN_PK)
services.register(Account, menu=False) services.register(Account, menu=False)

View file

@ -19,3 +19,6 @@ ACCOUNTS_LANGUAGES = getattr(settings, 'ACCOUNTS_LANGUAGES', (
ACCOUNTS_DEFAULT_LANGUAGE = getattr(settings, 'ACCOUNTS_DEFAULT_LANGUAGE', 'en') ACCOUNTS_DEFAULT_LANGUAGE = getattr(settings, 'ACCOUNTS_DEFAULT_LANGUAGE', 'en')
ACCOUNTS_MAIN_PK = getattr(settings, 'ACCOUNTS_MAIN_PK', 1)

View file

@ -1,4 +1,4 @@
{% extends "admin/change_form.html" %} {% extends "orchestra/admin/change_form.html" %}
{% load i18n admin_urls admin_static admin_modify %} {% load i18n admin_urls admin_static admin_modify %}
@ -43,9 +43,6 @@
<li> <li>
<a href="disable/" class="historylink">{% trans "Disable" %}</a> <a href="disable/" class="historylink">{% trans "Disable" %}</a>
</li> </li>
<li> {{ block.super }}
{% url opts|admin_urlname:'history' original.pk|admin_urlquote as history_url %}
<a href="{% add_preserved_filters history_url %}" class="historylink">{% trans "History" %}</a>
</li>
{% endblock %} {% endblock %}

View file

@ -0,0 +1,3 @@
def generate_bill(modeladmin, request, queryset):
for bill in queryset:
bill.close()

View file

@ -2,9 +2,11 @@ from django.contrib import admin
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ExtendedModelAdmin
from orchestra.admin.utils import admin_link, admin_date from orchestra.admin.utils import admin_link, admin_date
from orchestra.apps.accounts.admin import AccountAdminMixin from orchestra.apps.accounts.admin import AccountAdminMixin
from .actions import generate_bill
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)
@ -24,24 +26,26 @@ class BudgetLineInline(admin.TabularInline):
) )
class BillAdmin(AccountAdminMixin, admin.ModelAdmin): class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
list_display = ( list_display = (
'ident', 'status', 'bill_type_link', 'account_link', 'created_on_display' 'ident', 'status', 'type_link', 'account_link', 'created_on_display'
) )
list_filter = (BillTypeListFilter, 'status',) list_filter = (BillTypeListFilter, 'status',)
change_view_actions = [generate_bill]
change_readonly_fields = ('account', 'type', 'status')
readonly_fields = ('ident',) readonly_fields = ('ident',)
inlines = [BillLineInline] inlines = [BillLineInline]
account_link = admin_link('account') account_link = admin_link('account')
created_on_display = admin_date('created_on') created_on_display = admin_date('created_on')
def bill_type_link(self, bill): def type_link(self, bill):
bill_type = bill.bill_type.lower() bill_type = bill.type.lower()
url = reverse('admin:bills_%s_changelist' % bill_type) url = reverse('admin:bills_%s_changelist' % bill_type)
return '<a href="%s">%s</a>' % (url, bill.get_bill_type_display()) return '<a href="%s">%s</a>' % (url, bill.get_type_display())
bill_type_link.allow_tags = True type_link.allow_tags = True
bill_type_link.short_description = _("type") type_link.short_description = _("type")
bill_type_link.admin_order_field = 'bill_type' type_link.admin_order_field = 'type'
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:

View file

@ -1,7 +1,10 @@
from django.db import models from django.db import models
from django.template import loader, Context
from django.utils import timezone from django.utils import timezone
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.apps.accounts.models import Account
from orchestra.core import accounts from orchestra.core import accounts
from . import settings from . import settings
@ -12,7 +15,7 @@ class BillManager(models.Manager):
queryset = super(BillManager, self).get_queryset() queryset = super(BillManager, self).get_queryset()
if self.model != Bill: if self.model != Bill:
bill_type = self.model.get_type() bill_type = self.model.get_type()
queryset = queryset.filter(bill_type=bill_type) queryset = queryset.filter(type=bill_type)
return queryset return queryset
@ -44,7 +47,7 @@ class Bill(models.Model):
blank=True) blank=True)
account = models.ForeignKey('accounts.Account', verbose_name=_("account"), account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
related_name='%(class)s') related_name='%(class)s')
bill_type = models.CharField(_("type"), max_length=16, choices=TYPES) type = models.CharField(_("type"), max_length=16, choices=TYPES)
status = models.CharField(_("status"), max_length=16, choices=STATUSES, status = models.CharField(_("status"), max_length=16, choices=STATUSES,
default=OPEN) default=OPEN)
created_on = models.DateTimeField(_("created on"), auto_now_add=True) created_on = models.DateTimeField(_("created on"), auto_now_add=True)
@ -61,13 +64,25 @@ class Bill(models.Model):
def __unicode__(self): def __unicode__(self):
return self.ident return self.ident
@cached_property
def seller(self):
return Account.get_main().invoicecontact
@cached_property
def buyer(self):
return self.account.invoicecontact
@property
def lines(self):
return self.billlines
@classmethod @classmethod
def get_type(cls): def get_type(cls):
return cls.__name__.upper() return cls.__name__.upper()
def set_ident(self): def set_ident(self):
cls = type(self) cls = type(self)
bill_type = self.bill_type or cls.get_type() bill_type = self.type or cls.get_type()
if bill_type == 'BILL': if bill_type == 'BILL':
raise TypeError("get_new_ident() can not be used on a Bill class") raise TypeError("get_new_ident() can not be used on a Bill class")
# Bill number resets every natural year # Bill number resets every natural year
@ -91,11 +106,31 @@ class Bill(models.Model):
def close(self): def close(self):
self.status = self.CLOSED self.status = self.CLOSED
self.html = self.render()
self.save() self.save()
def render(self):
context = Context({
'bill': self,
'lines': self.lines.all(),
'seller': self.seller,
'buyer': self.buyer,
'seller_info': {
'phone': settings.BILLS_SELLER_PHONE,
'website': settings.BILLS_SELLER_WEBSITE,
'email': settings.BILLS_SELLER_EMAIL,
},
'currency': settings.BILLS_CURRENCY,
})
template = getattr(settings, 'BILLS_%s_TEMPLATE' % self.get_type())
bill_template = loader.get_template(template)
html = bill_template.render(context)
html = html.replace('-pageskip-', '<pdf:nextpage />')
return html
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not self.bill_type: if not self.type:
self.bill_type = type(self).get_type() self.type = type(self).get_type()
if not self.ident or (self.ident.startswith('O') and self.status != self.OPEN): if not self.ident or (self.ident.startswith('O') and self.status != self.OPEN):
self.set_ident() self.set_ident()
super(Bill, self).save(*args, **kwargs) super(Bill, self).save(*args, **kwargs)
@ -125,6 +160,10 @@ class Budget(Bill):
class Meta: class Meta:
proxy = True proxy = True
@property
def lines(self):
return self.budgetlines
class BaseBillLine(models.Model): class BaseBillLine(models.Model):
bill = models.ForeignKey(Bill, verbose_name=_("bill"), bill = models.ForeignKey(Bill, verbose_name=_("bill"),
@ -145,7 +184,7 @@ class BudgetLine(BaseBillLine):
class BillLine(BaseBillLine): class BillLine(BaseBillLine):
order_id = models.PositiveIntegerField(blank=True) order_id = models.PositiveIntegerField(blank=True, null=True)
order_last_bill_date = models.DateTimeField(null=True) order_last_bill_date = models.DateTimeField(null=True)
order_billed_until = models.DateTimeField(null=True) order_billed_until = models.DateTimeField(null=True)
auto = models.BooleanField(default=False) auto = models.BooleanField(default=False)

View file

@ -1,8 +1,27 @@
from django.conf import settings from django.conf import settings
BILLS_IDENT_NUMBER_LENGTH = getattr(settings, 'BILLS_IDENT_NUMBER_LENGTH', 4) BILLS_IDENT_NUMBER_LENGTH = getattr(settings, 'BILLS_IDENT_NUMBER_LENGTH', 4)
BILLS_INVOICE_IDENT_PREFIX = getattr(settings, 'BILLS_INVOICE_IDENT_PREFIX', 'I') BILLS_INVOICE_IDENT_PREFIX = getattr(settings, 'BILLS_INVOICE_IDENT_PREFIX', 'I')
BILLS_AMENDMENT_INVOICE_IDENT_PREFIX = getattr(settings, 'BILLS_AMENDMENT_INVOICE_IDENT_PREFIX', 'A') BILLS_AMENDMENT_INVOICE_IDENT_PREFIX = getattr(settings, 'BILLS_AMENDMENT_INVOICE_IDENT_PREFIX', 'A')
BILLS_FEE_IDENT_PREFIX = getattr(settings, 'BILLS_FEE_IDENT_PREFIX', 'F') BILLS_FEE_IDENT_PREFIX = getattr(settings, 'BILLS_FEE_IDENT_PREFIX', 'F')
BILLS_AMENDMENT_FEE_IDENT_PREFIX = getattr(settings, 'BILLS_AMENDMENT_FEE_IDENT_PREFIX', 'B') BILLS_AMENDMENT_FEE_IDENT_PREFIX = getattr(settings, 'BILLS_AMENDMENT_FEE_IDENT_PREFIX', 'B')
BILLS_BUDGET_IDENT_PREFIX = getattr(settings, 'BILLS_BUDGET_IDENT_PREFIX', 'Q') BILLS_BUDGET_IDENT_PREFIX = getattr(settings, 'BILLS_BUDGET_IDENT_PREFIX', 'Q')
BILLS_INVOICE_TEMPLATE = getattr(settings, 'BILLS_INVOICE_TEMPLATE', 'bills/invoice.html')
BILLS_CURRENCY = getattr(settings, 'BILLS_CURRENCY', 'euro')
BILLS_SELLER_PHONE = getattr(settings, 'BILLS_SELLER_PHONE', '111-112-11-222')
BILLS_SELLER_EMAIL = getattr(settings, 'BILLS_SELLER_EMAIL', 'sales@orchestra.lan')
BILLS_SELLER_WEBSITE = getattr(settings, 'BILLS_SELLER_WEBSITE', 'www.orchestra.lan')

View file

@ -0,0 +1,309 @@
<style>
body {
max-width: 1048px;
margin: 60 auto !important;
float: none !important;
font-family: Arial, 'Liberation Sans', 'DejaVu Sans', sans-serif;
}
#logo {
float: left;
font-size: 25;
font-weight: bold;
color: grey;
border-bottom: 5px solid grey;
margin: 38px;
margin-left: 60px;
padding: 5px;
}
#bill-number {
float: right;
margin-top: 80px;
text-align: right;
font-size: 25;
font-weight: bold;
color: grey;
}
#bill-number .value {
font-size: 40;
color: #B23;
font-weight: normal;
}
hr {
margin-top: 20px;
color: #ccc;
margin-bottom: 0px;
}
#bill-summary > * {
float: right;
border-style: solid;
border-width: thin;
border-color: grey;
padding: 10px;
text-align: center;
font-size: large;
width: 130px;
}
#bill-summary .title {
color: black;
font-size: small;
font-weight: bold;
position: relative;
top: -7px;
}
#bill-summary #total, #total .title{
background-color: #B23;
color: white;
font-weight: bold;
}
#seller-details, #buyer-details {
margin: 20px;
}
#buyer-details {
margin-left: 60px;
}
#lines > * {
float: left;
padding-top: 5px;
padding-bottom: 5px;
text-align: center;
}
#lines .title {
font-weight: bold;
border-bottom: 2px solid grey;
color: #B23;
}
#lines .value {
border-bottom: 1px solid grey;
}
#lines .column-id {
width: 5%;
text-align: right;
}
#lines .column-description {
width: 55%;
text-align: left;
}
#lines .column-quantity {
width: 20%;
}
#lines .column-rate {
width: 10%;
}
#lines .column-subtotal {
width: 10%;
text-align: right;
}
#totals {
padding-top: 100px;
}
#totals > * {
text-align: right;
padding-top: 5px;
padding-bottom: 5px;
}
#totals .column-title {
font-weight: bold;
color: #B23;
width: 85%;
float: left;
}
#totals .column-value {
width: 15%;
float: left;
}
#totals .subtotal {
border-bottom: 1px solid grey;
font-weight: normal;
}
#totals .tax {
border-bottom: 2px solid grey;
font-weight: normal;
}
#pagination {
color: #B23;
font-weight: bold;
}
#footer .title {
color: #B23;
font-weight: bold;
}
#footer > * > * {
margin: 10px;
color: #666;
}
#pagination {
margin-left: 20px;
clear: right;
}
#footer-column-1 {
float: left;
width: 48%;
margin: 10px;
}
#footer-column-2 {
float: right;
width: 48%;
margin: 10px;
}
</style>
<body>
<div id="bill-number">
Invoice<br>
<div class="value">F20110232</div><br>
</div>
<div id="logo">
YOUR<br>
LOGO<br>
HERE<br>
</div>
<div id="seller-details">
<table>
<tr>
<td>
<strong>Associacio Pangea - Coordinadora Comunicacio per a la Cooperacio</strong><br>
ES2323233<br>
<address>
Pl eusebi guell 6-7, planta 0<br>
08034 - Barcelona<br>
Spain<br>
</address>
<br>
93-803-21-32<br>
<a href="mailto:sales@pangea.org">sales@pangea.org</a><br>
<a href="http://www.pangea.org">www.pangea.org</a><br>
</td>
</tr>
</table>
</div>
<hr>
<div id="bill-summary">
<div id="due-date">
<span class="title">DUE DATE</span><br>
<psan class="value">Nov 21, 2011</span>
</div>
<div id="total">
<span class="title">TOTAL</span><br>
<psan class="value">122,03 &euro;</span>
</div>
<div id="bill-date">
<span class="title">INVOICE DATE</span><br>
<psan class="value">Oct 20, 2012</span>
</div>
</div>
<div id="buyer-details">
<address>
<strong>Aadults</strong><br>
ES01939933<br>
Carrer nnoseque, 0<br>
08034 - Barcelona<br>
Spain<br>
</address>
</div>
<div id="lines">
<span class="title column-id">id</span>
<span class="title column-description">description</span>
<span class="title column-quantity">hrs/qty</span>
<span class="title column-rate">rate/price</span>
<span class="title column-subtotal">subtotal</span>
<br>
<span class="value column-id">1</span>
<span class="value column-description">Hola que passa</span>
<span class="value column-quantity">1</span>
<span class="value column-rate">1 &euro;</span>
<span class="value column-subtotal">111 &euro;</span>
<br>
<span class="value column-id">1</span>
<span class="value column-description">Merda pura</span>
<span class="value column-quantity">1</span>
<span class="value column-rate">1 &euro;</span>
<span class="value column-subtotal">111 &euro;</span>
<br>
<span class="value column-id">1</span>
<span class="value column-description">I tu que et passa</span>
<span class="value column-quantity">1</span>
<span class="value column-rate">1 &euro;</span>
<span class="value column-subtotal">111 &euro;</span>
<br>
<span class="value column-id">1</span>
<span class="value column-description">Joder hostia puta</span>
<span class="value column-quantity">1</span>
<span class="value column-rate">1 &euro;</span>
<span class="value column-subtotal">111 &euro;</span>
<br>
</div>
<div id="totals">
<span class="subtotal column-title">subtotal</span>
<span class="subtotal column-value">33,03 &euro;</span>
<br>
<span class="tax column-title">tax</span>
<span class="tax column-value">33,03 &euro;</span>
<br>
<span class="total column-title">total</span>
<span class="total column-value">33,03 &euro;</span>
<br>
</div>
<div id="footer">
<div id="pagination">
Page 1 of 1
</div>
<div id="footer-column-1">
<div id="comments">
<span class="title">COMMENTS</span> The comments should be here. The comments should be here. The comments should be here. The comments should be here.
</div>
</div>
<div id="footer-column-2">
<div id="payment">
<span class="title">PAYMENT</span> You can pay our invoice by bank transfer
llkdskdlsdk The comments should be here. The comments should be here. The comments should be here. The comments should be here.
</div>
<div id="questions">
<span class="title">QUESTIONS</span> If you have any question about your bill, please
feel free to contact us at your convinience. We will reply as soon as we get
your message.
</div>
</div>
</div>
</body>

View file

@ -0,0 +1,217 @@
<html>
<style>
@page {
margin: 1cm;
margin-bottom: 0cm;
margin-top: 3cm;
size: a4 portrait;
background-image: url('img/letter_head.png');
@frame footer {
-pdf-frame-content: footerContent;
bottom: 0cm;
margin-left: 1cm;
margin-right: 1cm;
height: 2cm;
}
@frame simple {
-pdf-frame-content: simple;
bottom: 2.0cm;
height: 2.5cm;
margin-left: 1cm;
margin-right: 1cm;
}
}
div#buyer-details{
font-size: 120%;
}
div#specification{
margin-top: 20px;
}
div#specification td{
vertical-align: middle;
padding-top: 5px;
padding-bottom: 3px;
}
table td {
vertical-align: top;
padding: 2px 0;
height: 16px;
}
table td.amount{
text-align: right;
padding-right: 2px;
}
table td.total{
padding-top: 5px;
}
table th {
text-align: left;
border-bottom: 1px solid #000;
}
tr.uneven {
background-color: #efefef;
}
div#footerContent {
color: #777777;
}
div#footerContent a {
color: #790000;
text-decoration: none;
}
.date {
font-size: 90%;
color: #777;
}
div#totals {
margin: 20px 0;
}
div#simple td {
margin-left: 10px;
background-color: #efefef;
}
div#simple tr {
border-right: 1px solid #333;
}
div#simple table{
text-align: center;
border-left: 1px solid #999;
}
</style>
<body>
<h1>{{ bill_type }}</h1>
<div id="buyer-details">
<table>
<tr>
<td width="60%">
<strong>{{ buyer.name }}</strong><br>
{{ buyer.address }}<br>
{{ buyer.zipcode }} {{ buyer.city }}<br>
{{ buyer.country }}<br>
{{ buyer.vat_number }}<br>
</td>
<td width="20%">
<strong>Invoice number</strong><br />
<strong>Date</strong><br />
<strong>Due date</strong>
</td>
<td width="20%">
: {{ bill.ident }}<br />
: {{ bill.date|date:"d F, Y" }}<br />
: {{ bill.due_on|date:"d F, Y" }}<br />
</td>
</tr>
</table>
</div>
<div id="specification">
<table width="100%">
<tr>
<th width="5%">ID</th>
<th width="65%">Description</th>
<th width="20%">Amount</th>
<th width="10%">Price</th>
</tr>
{% for line in lines %}
<tr class="{% cycle 'even' 'uneven' %}"{% if forloop.last %} style="border-bottom: 1px solid #000;"{% endif %}>
<td class="ID">{{ line.order_id }}</td>
<td style="padding-left: 2px;">{{ line.description }}
<span class="date">({{ line.initial_date|date:"d-m-Y" }}{% if line.initial_date != line.final_date %} - {{ line.final_date|date:"d-m-Y" }}{% endif %})</span></td>
<td class="quantity">{{ line.amount }}</td>
<td class="amount total">&{{ currency }}; {{ line.price }}</td>
</tr>
{% endfor %}
</table>
</div>
<div id="totals">
<table width="100%">
<tr>
{% for tax, base in bases.items %}
<td width="60%">&nbsp;</td>
<td width="20%">Subtotal{% if bases.items|length > 1 %} (for {{ tax }}% taxes){% endif %}</td>
<td width="20%" class="amount">&{{ currency }}; {{ base }}</td>
{% endfor %}
</tr>
<tr>
{% for tax, value in taxes.items %}
<td width="60%">&nbsp;</td>
<td width="20%">Total {{ tax }}%</td>
<td width="20%" class="amount" style="border-bottom: 1px solid #333;">&{{ currency }}; {{ value }}</td>
{% endfor %}
</tr>
<tr>
<td width="60%">&nbsp;</td>
<td width="20%" class="total"><strong>Total</strong></td>
<td width="20%" class="amount total">&{{ currency }}; {{ total }}</td>
</tr>
</table>
</div>
<div id="simple">
<table>
<tr>
<td width="33%" style="padding-top: 5px;">IBAN</th>
<td width="34%" style="padding-top: 5px;">Invoice ID</th>
<td width="33%" style="padding-top: 5px;">Amount {{ currency.upper }}</th>
</tr>
<tr>
<td><strong>NL28INGB0004954664</strong></td>
<td><strong>{{ bill.ident }}</strong></td>
<td><strong>{{ total }}</strong></td>
</tr>
</table>
<p style="text-align:center;">The invoice is to be paid before <strong>{{ invoice.exp_date|date:"F jS, Y" }}</strong> with the mention of the invoice id.</p>
</div>
<div id="footerContent">
<table>
<tr>
<td width="33%">
{{ seller.name }}<br />
{{ seller.address }}<br />
{{ seller.city }}<br />
{{ seller.country }}<br />
</td>
<td width="5%">
Tel<br />
Web<br />
Email<br />
</td>
<td width="29%">
{{ seller_info.phone }}<br />
<a href="http://{{ seller_info.website }}">{{ seller_info.website }}</a><br />
{{ seller_info.email }}
</td>
<td width="8%">
Bank ING<br />
IBAN<br />
BTW<br />
KvK<br />
</td>
<td width="25%">
4954664<br />
NL28INGB0004954664<br />
NL 8207.29.449.B01<br />
27343027
</td>
</tr>
</table>
</div>
Payment info
</body>
</html>

View file

@ -0,0 +1,5 @@
{% extends 'bills/plans/invoice_base.html' %}
{% block title %}{{ invoice.full_number }}{% endblock %}
{% block body %}
{% include 'bills/plans/PL_EN_layout.html' %}
{% endblock %}

View file

@ -0,0 +1,171 @@
{% if logo_url %}
<img src="{{ logo_url }}" alt="company logo">
{% endif %}
<div style="float:right; text-align: right;">
<h1>
<label><span class="pl invoice_header">{% if invoice.type == invoice.INVOICE_TYPES.INVOICE %}Faktura VAT nr{% endif %}{% if invoice.type == invoice.INVOICE_TYPES.PROFORMA %}Faktura PROFORMA nr{% endif %}{% if invoice.type == invoice.INVOICE_TYPES.DUPLICATE %}Faktura VAT DUPLIKAT nr{% endif %}</span> <span class="en">{% if invoice.type == invoice.INVOICE_TYPES.INVOICE %}Invoice ID{% endif %}{% if invoice.type == invoice.INVOICE_TYPES.PROFORMA %}Order confirmation ID{% endif %}{% if invoice.type == invoice.INVOICE_TYPES.DUPLICATE %}Invoice (duplicate) ID{% endif %}</span></label> <span id="full_number">{{ invoice.full_number }}</span>
</h1>
<h2>{% if not copy %}ORYGINAŁ{% else %}KOPIA{% endif %}</h2>
<p> <label><span class="pl">Data wystawienia:</span> <span class="en">Issued</span></label> {{ invoice.issued|date:"Y-m-d" }}</p>
{% if invoice.type != invoice.INVOICE_TYPES.PROFORMA %}{# Not a PROFORMA #}
<p> <label><span class="pl">Data sprzedaży:</span> <span class="en">Date of order</span></label> {{ invoice.selling_date|date:"Y-m-d" }}</p>
{% else %}
<p>&nbsp;</p>
{% endif %}
</div>
<table style="width: 100%; margin-bottom: 40px; font-size: 12px;" >
<tr>
<td style="width: 50%;">
</td>
<td style="width: 50%; padding-right: 4em; font-weight: bold; font-size: 15px;" id="shipping">
<strong> <label><span class="pl">Adres wysyłki:</span> <span class="en">Shipping address</span></label></strong><br><br>
{{ buyer.name }}<br>
{{ buyer.address }}<br>
{{ buyer.zipcode }}<br>
{{ buyer.country }}
</td>
</tr>
<tr>
<td style="width: 50%; vertical-align: top;">
<strong> <label><span class="pl">Sprzedawca:</span> <span class="en">Seller</span></label></strong><br><br>
{{ seller.name }}<br>
{{ seller.address }}<br>
{{ seller.zipcode }}<br>
{{ seller.country }}<p>
<label><span class="pl">Numer NIP:</span><span class="en">VAT ID</span></label> {{ invoice.issuer_tax_number }}<br>
</td>
<td style="width: 50%; vertical-align: top;">
<strong> <label><span class="pl">Nabywca:</span> <span class="en">Buyer</span></label></strong><br><br>
{{ buyer.name }}<br>
{{ buyer.address }}<br>
{{ buyer.zipcode }}<br>
{{ buyer.country }}
{% if invoice.buyer_tax_number %}
<p>
<label><span class="pl">Numer NIP:</span><span class="en">VAT ID</span></label> {{ invoice.buyer_tax_number }}
</p>
{% endif %}
<br>
</td>
</tr>
</table>
<table style="margin-bottom: 40px; width: 100%;" id="items">
<thead>
<tr>
<td>
<span class="pl">L.p.</span>
</td>
<td>
<span class="pl">Nazwa&nbsp;usługi</span><br>
<span class="en">Description</span>
</td>
<td>
<span class="pl">Cena&nbsp;j.&nbsp;netto</span><br>
<span class="en">Unit&nbsp;price</span>
</td>
<td>
<span class="pl">Ilość</span><br>
<span class="en">Qty.</span>
</td>
<td>
<span class="pl">J.m.</span>
</td>
{% if invoice.rebate %}
<td>
<span class="pl">Rabat</span><br><span class="en">Rebate</span>
</td>
{% endif %}
<td>
<span class="pl">Wartość&nbsp;netto</span><br>
<span class="en">Subtotal</span>
</td>
<td style="width: 3%;">
<span class="pl">VAT</span><br> <span class="en">TAX</span>
</td>
<td>
<span class="pl">Kwota&nbsp;VAT</span><br><span class="en">TAX/VAT&nbsp;Amount</span>
</td>
<td style="width: 8%;">
<span class="pl">Wartość&nbsp;brutto</span><br>
<span class="en">Subtotal&nbsp;with&nbsp;TAX/VAT</span>
</td>
</tr>
</thead>
<tbody>
<tr>
<td>
1
</td>
<td class="center">{{ invoice.item_description }}</td>
<td class="number">{{ invoice.unit_price_net|floatformat:2 }}&nbsp;{{ invoice.currency }}</td>
<td class="center">{{ invoice.quantity }}</td>
<td class="center"><span class="pl">sztuk</span><br><span class="en">units</span></td>
{% if invoice.rebate %}
<td class="number">{{ invoice.rebate|floatformat:2 }}&nbsp;%</td>
{% endif %}
<td class="number">{{ invoice.total_net|floatformat:2 }}&nbsp;{{ invoice.currency }}</td>
<td class="number">{% if invoice.tax != None %}{{ invoice.tax|floatformat:2 }}&nbsp;%{% else %}<span class="pl">n.p.</span><br><span class="en">n/a</span>{% endif %}</td>
<td class="number">{% if invoice.tax_total != None %}{{ invoice.tax_total|floatformat:2 }}&nbsp;{{ invoice.currency }}{% else %}<span class="pl">n.p.</span><br><span class="en">n/a</span>{% endif %}</td>
<td class="number">{{ invoice.total|floatformat:2 }}&nbsp;{{ invoice.currency }}</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="{% if invoice.rebate %}6{% else %}5{% endif %}" style="background-color: #EEE;"><label><span class="pl">Razem:</span><span class="en">Total</span></label> </td>
<td>{{ invoice.total_net|floatformat:2 }}&nbsp;{{ invoice.currency }}</td>
<td>{% if invoice.tax != None %}{{ invoice.tax|floatformat:2 }}&nbsp;%{% else %}<span class="pl">n.p.</span><br><span class="en">n/a</span>{% endif %}</td>
<td>{% if invoice.tax_total != None %}{{ invoice.tax_total|floatformat:2 }}&nbsp;{{ invoice.currency }}{% else %}<span class="pl">n.p.</span><br><span class="en">n/a</span>{% endif %}</td>
<td>{{ invoice.total|floatformat:2 }}&nbsp;{{ invoice.currency }}</td>
</tr>
</tfoot>
</table>
<div style="width: 100%;">
{% if invoice.type != invoice.INVOICE_TYPES.PROFORMA %}
<strong><label><span class="pl">Sposób zapłaty:</span><span class="en">Payment</span></label></strong> <label><span class="pl">płatność elektroniczna</span> <span class="en">electronic payment</span></label><br><br>
{% endif %}
<strong><label><span class="pl">Termin zapłaty:</span><span class="en">Payment till</span></label></strong>
{% if invoice.type == invoice.INVOICE_TYPES.PROFORMA %}
{% else %}
<label> <span class="pl">zapłacono dnia</span> <span class="en"> paid</span></label>
{% endif %}
{{ invoice.payment_date|date:"Y-m-d" }}
<br><br>
<hr>
{% if invoice.type == invoice.INVOICE_TYPES.PROFORMA %}<p><span class="pl">Ten dokument <strong>nie jest</strong> fakturą VAT (nie jest dokumentem księgowym).</span><span class="en">This document <strong>is not</strong> an invoice.</span></p> {% endif %}
{% if invoice.tax == None and invoice.is_UE_customer %}
<p>
<span class="pl">Odwrotne obciążenie.</span><span class="en">-Reverse charge.</span>
</p>
{% endif %}
</div>

View file

@ -0,0 +1,83 @@
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}{% endblock %}</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<style type="text/css">
html, body{
font-size: 12px;
font-family: Helvetica;
}
span.en {
font-size: 10px;
color:gray;
}
span.pl {
font-size: 14px;
color: black;
}
table {
border-collapse: collapse;
}
table td, table th {
border: 1px solid black;
padding: 1em;
}
table thead{
text-align: center;
}
#shipping{
text-align: right;
}
table#items thead {
background-color: #EEE;
}
table#items tfoot td{
text-align: right;
font-weight: bold;
font-size: 14px;
}
table td.number{
text-align: right;
}
table td.center{
text-align: center;
}
strong{
font-size: 14px;
}
#full_number , span.invoice_header{
font-size: 26px;
font-weight: bold;
}
label{
display: inline-block;
position: relative;
margin-bottom: .5em;
}
label span.pl {
display: block;
}
label span.en{
position: absolute;
right: .5em;
}
</style>
{% block head %}{% endblock %}
</head>
<body style="width: 960px; margin: 0 auto;" {% if auto_print %}onload="window.print();"{% endif %}>
{% block body %}{% endblock %}
</body>
</html>

View file

@ -31,6 +31,10 @@ class TransactionAdmin(admin.ModelAdmin):
account_link = admin_link('bill__account') account_link = admin_link('bill__account')
display_state = admin_colored('state', colors=STATE_COLORS) display_state = admin_colored('state', colors=STATE_COLORS)
def get_queryset(self, request):
qs = super(TransactionAdmin, self).get_queryset(request)
return qs.select_related('source', 'bill__account__user')
class PaymentSourceAdmin(AccountAdminMixin, admin.ModelAdmin): class PaymentSourceAdmin(AccountAdminMixin, admin.ModelAdmin):
list_display = ('label', 'method', 'number', 'account_link', 'is_active') list_display = ('label', 'method', 'number', 'account_link', 'is_active')
@ -54,7 +58,8 @@ class PaymentProcessAdmin(admin.ModelAdmin):
ids = [] ids = []
lines = [] lines = []
counter = 0 counter = 0
tx_ids = process.transactions.order_by('id').values_list('id', flat=True) # Because of values_list this query doesn't benefit from prefetch_related
tx_ids = process.transactions.values_list('id', flat=True)
for tx_id in tx_ids: for tx_id in tx_ids:
ids.append(str(tx_id)) ids.append(str(tx_id))
counter += 1 counter += 1

View file

@ -0,0 +1,10 @@
{% extends "admin/change_form.html" %}
{% load i18n admin_urls utils %}
{% block 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>
{% endfor %}
{{ block.super }}
{% endblock %}

View file

@ -44,4 +44,3 @@ def size(value, length):
@register.filter(name='is_checkbox') @register.filter(name='is_checkbox')
def is_checkbox(field): def is_checkbox(field):
return isinstance(field.field.widget, CheckboxInput) return isinstance(field.field.widget, CheckboxInput)