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 .actions import download_bills, view_bill, close_bills, send_bills
from .filters import BillTypeListFilter
from .models import (Bill, Invoice, AmendmentInvoice, Fee, AmendmentFee, Budget,
BillLine, BudgetLine)
from .models import (Bill, Invoice, AmendmentInvoice, Fee, AmendmentFee, ProForma,
BillLine)
class BillLineInline(admin.TabularInline):
@ -46,11 +46,6 @@ class BillLineInline(admin.TabularInline):
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):
list_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')
def num_lines(self, bill):
return bill.billlines__count
num_lines.admin_order_field = 'billlines__count'
return bill.lines__count
num_lines.admin_order_field = 'lines__count'
num_lines.short_description = _("lines")
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]
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()
request.__bill__ = obj
return super(BillAdmin, self).get_inline_instances(request, obj=obj)
@ -136,8 +129,8 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
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')
qs = qs.annotate(models.Count('lines'))
qs = qs.prefetch_related('lines', 'lines__sublines')
return qs
# 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(Fee, 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")),
('fee', _("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")),
('FEE', _("Fee")),
('AMENDMENTFEE', _("Amendment Fee")),
('BUDGET', _("Budget")),
('PROFORMA', _("Pro forma")),
)
number = models.CharField(_("number"), max_length=16, unique=True,
@ -74,10 +74,6 @@ class Bill(models.Model):
def buyer(self):
return self.account.invoicecontact
@property
def lines(self):
return self.billlines
@classmethod
def get_class_type(cls):
return cls.__name__.upper()
@ -210,28 +206,22 @@ class AmendmentFee(Bill):
proxy = True
class Budget(Bill):
class ProForma(Bill):
class Meta:
proxy = True
@property
def lines(self):
return self.budgetlines
class BaseBillLine(models.Model):
class BillLine(models.Model):
""" Base model for bill item representation """
bill = models.ForeignKey(Bill, verbose_name=_("bill"),
related_name='%(class)ss')
bill = models.ForeignKey(Bill, verbose_name=_("bill"), related_name='lines')
description = models.CharField(_("description"), max_length=256)
rate = models.DecimalField(_("rate"), blank=True, null=True,
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)
tax = models.PositiveIntegerField(_("tax"))
class Meta:
abstract = True
amended_line = models.ForeignKey('self', verbose_name=_("amended line"),
related_name='amendment_lines', null=True, blank=True)
def __unicode__(self):
return "#%i" % self.number
@ -241,19 +231,6 @@ class BaseBillLine(models.Model):
lines = type(self).objects.filter(bill=self.bill_id)
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):
""" Computes subline discounts """
subtotal = self.total
@ -271,7 +248,7 @@ class BillLine(BaseBillLine):
class BillSubline(models.Model):
""" 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')
description = models.CharField(_("description"), max_length=256)
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.save()
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_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_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 %}
<div id="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>
LOGO<br>
HERE<br>

View File

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

View File

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

View File

@ -18,11 +18,15 @@ class BillSelectedOptionsForm(AdminFormMixin, forms.Form):
label=_("fixed point"),
help_text=_("Deisgnates whether you want the billing point to be an "
"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,
label=_("Create a new open bill"),
help_text=_("Deisgnates whether you want to put this orders on a new "
"open bill, or allow to reuse an existing one."))
def selected_related_choices(queryset):
for order in queryset:
verbose = '<a href="{order_url}">{description}</a> '
@ -40,6 +44,7 @@ class BillSelectRelatedForm(AdminFormMixin, forms.Form):
required=False)
billing_point = forms.DateField(widget=forms.HiddenInput())
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)
def __init__(self, *args, **kwargs):
@ -51,8 +56,7 @@ class BillSelectRelatedForm(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())
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)

View File

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

View File

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