Added support for ProForma bills

This commit is contained in:
Marc 2014-09-11 14:00:20 +00:00
parent 287f03ce19
commit f6045869ac
14 changed files with 176 additions and 82 deletions

View File

@ -13,8 +13,8 @@ from orchestra.apps.accounts.admin import AccountAdminMixin
from . import settings from . import settings
from .actions import download_bills, view_bill, close_bills, send_bills from .actions import download_bills, view_bill, close_bills, send_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, ProForma,
BillLine, BudgetLine) BillLine)
class BillLineInline(admin.TabularInline): class BillLineInline(admin.TabularInline):
@ -46,11 +46,6 @@ class BillLineInline(admin.TabularInline):
return super(BillLineInline, self).formfield_for_dbfield(db_field, **kwargs) return super(BillLineInline, self).formfield_for_dbfield(db_field, **kwargs)
class BudgetLineInline(BillLineInline):
model = Budget
fields = ('description', 'rate', 'amount', 'tax', 'total')
class BillAdmin(AccountAdminMixin, ExtendedModelAdmin): class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
list_display = ( list_display = (
'number', 'status', 'type_link', 'account_link', 'created_on_display', 'number', 'status', 'type_link', 'account_link', 'created_on_display',
@ -77,8 +72,8 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
created_on_display = admin_date('created_on') created_on_display = admin_date('created_on')
def num_lines(self, bill): def num_lines(self, bill):
return bill.billlines__count return bill.lines__count
num_lines.admin_order_field = 'billlines__count' num_lines.admin_order_field = 'lines__count'
num_lines.short_description = _("lines") num_lines.short_description = _("lines")
def display_total(self, bill): def display_total(self, bill):
@ -120,8 +115,6 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
return [action for action in actions if action.__name__ not in discard] return [action for action in actions if action.__name__ not in discard]
def get_inline_instances(self, request, obj=None): def get_inline_instances(self, request, obj=None):
if self.model is Budget:
self.inlines = [BudgetLineInline]
# Make parent object available for inline.has_add_permission() # Make parent object available for inline.has_add_permission()
request.__bill__ = obj request.__bill__ = obj
return super(BillAdmin, self).get_inline_instances(request, obj=obj) return super(BillAdmin, self).get_inline_instances(request, obj=obj)
@ -136,8 +129,8 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
def get_queryset(self, request): def get_queryset(self, request):
qs = super(BillAdmin, self).get_queryset(request) qs = super(BillAdmin, self).get_queryset(request)
qs = qs.annotate(models.Count('billlines')) qs = qs.annotate(models.Count('lines'))
qs = qs.prefetch_related('billlines', 'billlines__sublines') qs = qs.prefetch_related('lines', 'lines__sublines')
return qs return qs
# def change_view(self, request, object_id, **kwargs): # def change_view(self, request, object_id, **kwargs):
@ -154,4 +147,4 @@ admin.site.register(Invoice, BillAdmin)
admin.site.register(AmendmentInvoice, BillAdmin) admin.site.register(AmendmentInvoice, BillAdmin)
admin.site.register(Fee, BillAdmin) admin.site.register(Fee, BillAdmin)
admin.site.register(AmendmentFee, BillAdmin) admin.site.register(AmendmentFee, BillAdmin)
admin.site.register(Budget, BillAdmin) admin.site.register(ProForma, BillAdmin)

View File

@ -19,7 +19,7 @@ class BillTypeListFilter(SimpleListFilter):
('amendmentinvoice', _("Amendment invoice")), ('amendmentinvoice', _("Amendment invoice")),
('fee', _("Fee")), ('fee', _("Fee")),
('fee', _("Amendment fee")), ('fee', _("Amendment fee")),
('budget', _("Budget")), ('proforma', _("Pro-forma")),
) )

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('bills', '0003_bill_total'),
]
operations = [
migrations.AlterField(
model_name='bill',
name='total',
field=models.DecimalField(default=0, max_digits=12, decimal_places=2),
),
]

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('bills', '0004_auto_20140911_1234'),
]
operations = [
migrations.RenameField(
model_name='billsubline',
old_name='bill_line',
new_name='line',
),
]

View File

@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('bills', '0005_auto_20140911_1234'),
]
operations = [
migrations.RemoveField(
model_name='budgetline',
name='bill',
),
migrations.DeleteModel(
name='BudgetLine',
),
migrations.DeleteModel(
name='Budget',
),
migrations.CreateModel(
name='ProForma',
fields=[
],
options={
'proxy': True,
},
bases=('bills.bill',),
),
migrations.RemoveField(
model_name='billline',
name='auto',
),
migrations.RemoveField(
model_name='billline',
name='order_billed_until',
),
migrations.RemoveField(
model_name='billline',
name='order_id',
),
migrations.RemoveField(
model_name='billline',
name='order_last_bill_date',
),
migrations.AlterField(
model_name='bill',
name='type',
field=models.CharField(max_length=16, verbose_name='type', choices=[(b'INVOICE', 'Invoice'), (b'AMENDMENTINVOICE', 'Amendment invoice'), (b'FEE', 'Fee'), (b'AMENDMENTFEE', 'Amendment Fee'), (b'PROFORMA', 'Pro forma')]),
),
migrations.AlterField(
model_name='billline',
name='bill',
field=models.ForeignKey(related_name=b'lines', verbose_name='bill', to='bills.Bill'),
),
]

View File

@ -43,7 +43,7 @@ class Bill(models.Model):
('AMENDMENTINVOICE', _("Amendment invoice")), ('AMENDMENTINVOICE', _("Amendment invoice")),
('FEE', _("Fee")), ('FEE', _("Fee")),
('AMENDMENTFEE', _("Amendment Fee")), ('AMENDMENTFEE', _("Amendment Fee")),
('BUDGET', _("Budget")), ('PROFORMA', _("Pro forma")),
) )
number = models.CharField(_("number"), max_length=16, unique=True, number = models.CharField(_("number"), max_length=16, unique=True,
@ -74,10 +74,6 @@ class Bill(models.Model):
def buyer(self): def buyer(self):
return self.account.invoicecontact return self.account.invoicecontact
@property
def lines(self):
return self.billlines
@classmethod @classmethod
def get_class_type(cls): def get_class_type(cls):
return cls.__name__.upper() return cls.__name__.upper()
@ -210,28 +206,22 @@ class AmendmentFee(Bill):
proxy = True proxy = True
class Budget(Bill): class ProForma(Bill):
class Meta: class Meta:
proxy = True proxy = True
@property
def lines(self):
return self.budgetlines
class BillLine(models.Model):
class BaseBillLine(models.Model):
""" Base model for bill item representation """ """ Base model for bill item representation """
bill = models.ForeignKey(Bill, verbose_name=_("bill"), bill = models.ForeignKey(Bill, verbose_name=_("bill"), related_name='lines')
related_name='%(class)ss')
description = models.CharField(_("description"), max_length=256) description = models.CharField(_("description"), max_length=256)
rate = models.DecimalField(_("rate"), blank=True, null=True, rate = models.DecimalField(_("rate"), blank=True, null=True,
max_digits=12, decimal_places=2) max_digits=12, decimal_places=2)
amount = models.DecimalField(_("amount"), max_digits=12, decimal_places=2) amount = models.DecimalField(_("amount"), max_digits=12, decimal_places=2)
total = models.DecimalField(_("total"), max_digits=12, decimal_places=2) total = models.DecimalField(_("total"), max_digits=12, decimal_places=2)
tax = models.PositiveIntegerField(_("tax")) tax = models.PositiveIntegerField(_("tax"))
amended_line = models.ForeignKey('self', verbose_name=_("amended line"),
class Meta: related_name='amendment_lines', null=True, blank=True)
abstract = True
def __unicode__(self): def __unicode__(self):
return "#%i" % self.number return "#%i" % self.number
@ -241,19 +231,6 @@ class BaseBillLine(models.Model):
lines = type(self).objects.filter(bill=self.bill_id) lines = type(self).objects.filter(bill=self.bill_id)
return lines.filter(id__lte=self.id).order_by('id').count() return lines.filter(id__lte=self.id).order_by('id').count()
class BudgetLine(BaseBillLine):
pass
class BillLine(BaseBillLine):
order_id = models.PositiveIntegerField(blank=True, null=True)
order_last_bill_date = models.DateTimeField(null=True)
order_billed_until = models.DateTimeField(null=True)
auto = models.BooleanField(default=False)
amended_line = models.ForeignKey('self', verbose_name=_("amended line"),
related_name='amendment_lines', null=True, blank=True)
def get_total(self): def get_total(self):
""" Computes subline discounts """ """ Computes subline discounts """
subtotal = self.total subtotal = self.total
@ -271,7 +248,7 @@ class BillLine(BaseBillLine):
class BillSubline(models.Model): class BillSubline(models.Model):
""" Subline used for describing an item discount """ """ Subline used for describing an item discount """
bill_line = models.ForeignKey(BillLine, verbose_name=_("bill line"), line = models.ForeignKey(BillLine, verbose_name=_("bill line"),
related_name='sublines') related_name='sublines')
description = models.CharField(_("description"), max_length=256) description = models.CharField(_("description"), max_length=256)
total = models.DecimalField(max_digits=12, decimal_places=2) total = models.DecimalField(max_digits=12, decimal_places=2)
@ -284,4 +261,5 @@ class BillSubline(models.Model):
self.line.bill.total = self.line.bill.get_total() self.line.bill.total = self.line.bill.get_total()
self.line.bill.save() self.line.bill.save()
accounts.register(Bill) accounts.register(Bill)

View File

@ -11,13 +11,14 @@ BILLS_FEE_NUMBER_PREFIX = getattr(settings, 'BILLS_FEE_NUMBER_PREFIX', 'F')
BILLS_AMENDMENT_FEE_NUMBER_PREFIX = getattr(settings, 'BILLS_AMENDMENT_FEE_NUMBER_PREFIX', 'B') BILLS_AMENDMENT_FEE_NUMBER_PREFIX = getattr(settings, 'BILLS_AMENDMENT_FEE_NUMBER_PREFIX', 'B')
BILLS_BUDGET_NUMBER_PREFIX = getattr(settings, 'BILLS_BUDGET_NUMBER_PREFIX', 'Q') BILLS_PROFORMA_NUMBER_PREFIX = getattr(settings, 'BILLS_PROFORMA_NUMBER_PREFIX', 'P')
BILLS_DEFAULT_TEMPLATE = getattr(settings, 'BILLS_DEFAULT_TEMPLATE', 'bills/microspective.html') BILLS_DEFAULT_TEMPLATE = getattr(settings, 'BILLS_DEFAULT_TEMPLATE', 'bills/microspective.html')
BILLS_FEE_TEMPLATE = getattr(settings, 'BILLS_FEE_TEMPLATE', 'bills/microspective-fee.html') BILLS_FEE_TEMPLATE = getattr(settings, 'BILLS_FEE_TEMPLATE', 'bills/microspective-fee.html')
BILLS_PROFORMA_TEMPLATE = getattr(settings, 'BILLS_PROFORMA_TEMPLATE', 'bills/microspective-proforma.html')

View File

@ -0,0 +1,9 @@
{% extends 'bills/microspective.html' %}
{% block head %}
<style type="text/css">
{% with color="#2C5899" %}
{% include 'bills/microspective.css' %}
{% endwith %}
</style>
{% endblock %}

View File

@ -14,7 +14,7 @@
{% block header %} {% block header %}
<div id="logo"> <div id="logo">
{% block logo %} {% block logo %}
<div style="border-bottom:5px solid grey; color:grey; font-size:30; margin-right: 20px;"> <div style="border-bottom:5px solid {{ color }}; color:{{ color }}; font-size:30; margin-right: 20px;">
YOUR<br> YOUR<br>
LOGO<br> LOGO<br>
HERE<br> HERE<br>

View File

@ -37,9 +37,13 @@ class BillSelectedOrders(object):
self.options = dict( self.options = dict(
billing_point=form.cleaned_data['billing_point'], billing_point=form.cleaned_data['billing_point'],
fixed_point=form.cleaned_data['fixed_point'], fixed_point=form.cleaned_data['fixed_point'],
is_proforma=form.cleaned_data['is_proforma'],
create_new_open=form.cleaned_data['create_new_open'], create_new_open=form.cleaned_data['create_new_open'],
) )
if int(request.POST.get('step')) != 3:
return self.select_related(request) return self.select_related(request)
else:
return self.confirmation(request)
self.context.update({ self.context.update({
'title': _("Options for billing selected orders, step 1 / 3"), 'title': _("Options for billing selected orders, step 1 / 3"),
'step': 1, 'step': 1,
@ -55,7 +59,7 @@ class BillSelectedOrders(object):
form = BillSelectRelatedForm(request.POST, initial=self.options) form = BillSelectRelatedForm(request.POST, initial=self.options)
if form.is_valid(): if form.is_valid():
select_related = form.cleaned_data['selected_related'] select_related = form.cleaned_data['selected_related']
self.options['selected_related'] = select_related self.queryset = self.queryset | select_related
return self.confirmation(request) return self.confirmation(request)
self.context.update({ self.context.update({
'title': _("Select related order for billing, step 2 / 3"), 'title': _("Select related order for billing, step 2 / 3"),
@ -89,6 +93,5 @@ class BillSelectedOrders(object):
'step': 3, 'step': 3,
'form': form, 'form': form,
'bills': bills, 'bills': bills,
'selected_related_objects': self.options['selected_related']
}) })
return render(request, self.template, self.context) return render(request, self.template, self.context)

View File

@ -2,42 +2,44 @@ import datetime
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.apps.bills.models import Invoice, Fee, BillLine, BillSubline from orchestra.apps.bills.models import Invoice, Fee, ProForma, BillLine, BillSubline
class BillsBackend(object): class BillsBackend(object):
def create_bills(self, account, lines): def create_bills(self, account, lines, **options):
invoice = None bill = None
bills = [] bills = []
create_new = options.get('create_new_open', False)
is_proforma = options.get('is_proforma', False)
for line in lines: for line in lines:
service = line.order.service service = line.order.service
if service.is_fee: # Create bill if needed
fee, __ = Fee.objects.get_or_create(account=account, status=Fee.OPEN) if bill is None or service.is_fee:
storedline = fee.lines.create( if is_proforma:
rate=service.nominal_price, if create_new:
amount=line.size, bill = ProForma.objects.create(account=account)
total=line.subtotal, tax=0,
description=self.format_period(line.ini, line.end),
)
self.create_sublines(storedline, line.discounts)
bills.append(fee)
else: else:
if invoice is None: bill, __ = ProForma.objects.get_or_create(account=account,
invoice, __ = Invoice.objects.get_or_create(account=account, status=ProForma.OPEN)
elif service.is_fee:
bill = Fee.objects.create(account=account)
else:
if create_new:
bill = Invoice.objects.create(account=account)
else:
bill, __ = Invoice.objects.get_or_create(account=account,
status=Invoice.OPEN) status=Invoice.OPEN)
bills.append(invoice) bills.append(bill)
description = line.order.description # Create bill line
if service.billing_period != service.NEVER: billine = bill.lines.create(
description += " %s" % self.format_period(line.ini, line.end)
storedline = invoice.lines.create(
description=description,
rate=service.nominal_price, rate=service.nominal_price,
amount=line.size, amount=line.size,
# TODO rename line.total > subtotal
total=line.subtotal, total=line.subtotal,
tax=service.tax, tax=service.tax,
description=self.get_line_description(line),
) )
self.create_sublines(storedline, line.discounts) self.create_sublines(billine, line.discounts)
print bills
return bills return bills
def format_period(self, ini, end): def format_period(self, ini, end):
@ -47,6 +49,15 @@ class BillsBackend(object):
return ini return ini
return _("{ini} to {end}").format(ini=ini, end=end) return _("{ini} to {end}").format(ini=ini, end=end)
def get_line_description(self, line):
service = line.order.service
if service.is_fee:
return self.format_period(line.ini, line.end)
else:
description = line.order.description
if service.billing_period != service.NEVER:
description += " %s" % self.format_period(line.ini, line.end)
return description
def create_sublines(self, line, discounts): def create_sublines(self, line, discounts):
for discount in discounts: for discount in discounts:

View File

@ -18,11 +18,15 @@ class BillSelectedOptionsForm(AdminFormMixin, forms.Form):
label=_("fixed point"), label=_("fixed point"),
help_text=_("Deisgnates whether you want the billing point to be an " help_text=_("Deisgnates whether you want the billing point to be an "
"exact date, or adapt it to the billing period.")) "exact date, or adapt it to the billing period."))
is_proforma = forms.BooleanField(initial=False, required=False,
label=_("Pro-forma, billing simulation"),
help_text=_("O."))
create_new_open = forms.BooleanField(initial=False, required=False, create_new_open = forms.BooleanField(initial=False, required=False,
label=_("Create a new open bill"), label=_("Create a new open bill"),
help_text=_("Deisgnates whether you want to put this orders on a new " help_text=_("Deisgnates whether you want to put this orders on a new "
"open bill, or allow to reuse an existing one.")) "open bill, or allow to reuse an existing one."))
def selected_related_choices(queryset): def selected_related_choices(queryset):
for order in queryset: for order in queryset:
verbose = '<a href="{order_url}">{description}</a> ' verbose = '<a href="{order_url}">{description}</a> '
@ -40,6 +44,7 @@ class BillSelectRelatedForm(AdminFormMixin, forms.Form):
required=False) required=False)
billing_point = forms.DateField(widget=forms.HiddenInput()) billing_point = forms.DateField(widget=forms.HiddenInput())
fixed_point = forms.BooleanField(widget=forms.HiddenInput(), required=False) fixed_point = forms.BooleanField(widget=forms.HiddenInput(), required=False)
is_proforma = forms.BooleanField(widget=forms.HiddenInput(), required=False)
create_new_open = forms.BooleanField(widget=forms.HiddenInput(), required=False) create_new_open = forms.BooleanField(widget=forms.HiddenInput(), required=False)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -51,8 +56,7 @@ class BillSelectRelatedForm(AdminFormMixin, forms.Form):
class BillSelectConfirmationForm(AdminFormMixin, forms.Form): class BillSelectConfirmationForm(AdminFormMixin, forms.Form):
# selected_related = forms.ModelMultipleChoiceField(queryset=Order.objects.none(),
# widget=forms.HiddenInput(), required=False)
billing_point = forms.DateField(widget=forms.HiddenInput()) billing_point = forms.DateField(widget=forms.HiddenInput())
fixed_point = forms.BooleanField(widget=forms.HiddenInput(), required=False) fixed_point = forms.BooleanField(widget=forms.HiddenInput(), required=False)
is_proforma = forms.BooleanField(widget=forms.HiddenInput(), required=False)
create_new_open = forms.BooleanField(widget=forms.HiddenInput(), required=False) create_new_open = forms.BooleanField(widget=forms.HiddenInput(), required=False)

View File

@ -316,7 +316,7 @@ class OrderQuerySet(models.QuerySet):
lines = service.handler.generate_bill_lines(orders, **options) lines = service.handler.generate_bill_lines(orders, **options)
bill_lines.extend(lines) bill_lines.extend(lines)
if commit: if commit:
bills += bill_backend.create_bills(account, bill_lines) bills += bill_backend.create_bills(account, bill_lines, **options)
else: else:
bills += [(account, bill_lines)] bills += [(account, bill_lines)]
return bills return bills

View File

@ -60,13 +60,11 @@
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
{{ form.as_table }}
{% else %} {% else %}
{{ form.as_admin }} {{ form.as_admin }}
{% endif %} {% endif %}
</div> </div>
{% for obj in selected_related_objects %}
<input type="hidden" name="selected_related" value="{{ obj.pk|unlocalize }}" />
{% endfor %}
{% for obj in queryset %} {% for obj in queryset %}
<input type="hidden" name="{{ action_checkbox_name }}" value="{{ obj.pk|unlocalize }}" /> <input type="hidden" name="{{ action_checkbox_name }}" value="{{ obj.pk|unlocalize }}" />
{% endfor %} {% endfor %}