Added support for ProForma bills
This commit is contained in:
parent
287f03ce19
commit
f6045869ac
|
@ -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)
|
||||
|
|
|
@ -19,7 +19,7 @@ class BillTypeListFilter(SimpleListFilter):
|
|||
('amendmentinvoice', _("Amendment invoice")),
|
||||
('fee', _("Fee")),
|
||||
('fee', _("Amendment fee")),
|
||||
('budget', _("Budget")),
|
||||
('proforma', _("Pro-forma")),
|
||||
)
|
||||
|
||||
|
||||
|
|
19
orchestra/apps/bills/migrations/0004_auto_20140911_1234.py
Normal file
19
orchestra/apps/bills/migrations/0004_auto_20140911_1234.py
Normal 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),
|
||||
),
|
||||
]
|
19
orchestra/apps/bills/migrations/0005_auto_20140911_1234.py
Normal file
19
orchestra/apps/bills/migrations/0005_auto_20140911_1234.py
Normal 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',
|
||||
),
|
||||
]
|
59
orchestra/apps/bills/migrations/0006_auto_20140911_1238.py
Normal file
59
orchestra/apps/bills/migrations/0006_auto_20140911_1238.py
Normal 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'),
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
{% extends 'bills/microspective.html' %}
|
||||
|
||||
{% block head %}
|
||||
<style type="text/css">
|
||||
{% with color="#2C5899" %}
|
||||
{% include 'bills/microspective.css' %}
|
||||
{% endwith %}
|
||||
</style>
|
||||
{% endblock %}
|
|
@ -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>
|
||||
|
|
|
@ -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'],
|
||||
)
|
||||
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)
|
||||
|
|
|
@ -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)
|
||||
# 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:
|
||||
if invoice is None:
|
||||
invoice, __ = Invoice.objects.get_or_create(account=account,
|
||||
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(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,
|
||||
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,
|
||||
description=self.get_line_description(line),
|
||||
)
|
||||
self.create_sublines(storedline, line.discounts)
|
||||
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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 %}
|
||||
|
|
Loading…
Reference in a new issue