Improvements on bills app

This commit is contained in:
Marc 2014-08-22 11:28:46 +00:00
parent 2a37cfc8d7
commit 3ea6fde7bd
11 changed files with 239 additions and 91 deletions

View File

@ -12,12 +12,13 @@
2. [x] [Orchestra-orm](https://github.com/glic3rinu/orchestra-orm) a Python library for easily interacting with the REST API 2. [x] [Orchestra-orm](https://github.com/glic3rinu/orchestra-orm) a Python library for easily interacting with the REST API
3. [x] Service orchestration framework 3. [x] Service orchestration framework
4. [ ] Data model, input validation, admin and REST interfaces, permissions, unit and functional tests, service management, migration scripts and some documentation of: 4. [ ] Data model, input validation, admin and REST interfaces, permissions, unit and functional tests, service management, migration scripts and some documentation of:
1. [ ] Web applications and FTP accounts 1. [x] Web applications
2. [ ] FTP accounts
2. [ ] Databases 2. [ ] Databases
1. [ ] Mail accounts, aliases, forwards 1. [ ] Mail accounts, aliases, forwards
1. [ ] DNS 1. [x] DNS
1. [ ] Mailing lists 1. [ ] Mailing lists
1. [ ] Contact management and service contraction 1. [x] Contact management and service contraction
1. [ ] Object level permissions system 1. [ ] Object level permissions system
1. [ ] Unittests of all the logic 1. [ ] Unittests of all the logic
2. [ ] Functional tests of all Admin and REST interations 2. [ ] Functional tests of all Admin and REST interations
@ -26,12 +27,12 @@
### 1.0b1 Milestone (first beta release on Jul '14) ### 1.0b1 Milestone (first beta release on Jul '14)
1. [ ] Resource monitoring 1. [x] Resource monitoring
1. [ ] Orders 1. [ ] Orders
2. [ ] Pricing 2. [ ] Pricing
3. [ ] Billing 3. [ ] Billing
1. [ ] Payment gateways 1. [ ] Payment methods
2. [ ] Scheduling of service cancellations 2. [ ] Scheduling of service cancellations and deactivations
1. [ ] Full documentation 1. [ ] Full documentation
@ -41,3 +42,4 @@
1. [ ] Integration with third-party service providers, e.g. Gandi 1. [ ] Integration with third-party service providers, e.g. Gandi
1. [ ] Support for additional services like VPS 1. [ ] Support for additional services like VPS
2. [ ] Issue tracking system 2. [ ] Issue tracking system
3. [ ] Translation to Spanish and Catalan

View File

@ -42,6 +42,7 @@ class AccountAdmin(ExtendedModelAdmin):
search_fields = ('users__username',) search_fields = ('users__username',)
add_form = AccountCreationForm add_form = AccountCreationForm
form = AccountChangeForm form = AccountChangeForm
change_form_template = 'admin/accounts/account/change_form.html'
user_link = admin_link('user', order='user__username') user_link = admin_link('user', order='user__username')

View File

@ -19,6 +19,9 @@
{% block object-tools-items %} {% block object-tools-items %}
{% if from_account %} {% if from_account %}
<li>
<a href="{% url account_opts|admin_urlname:'change' account.pk|admin_urlquote %}" class="historylink">{{ account|truncatewords:"18" }}</a>
</li>
<li> <li>
<a href="./" class="historylink">{% trans 'Show all' %}</a> <a href="./" class="historylink">{% trans 'Show all' %}</a>
</li> </li>

View File

@ -1,3 +1,4 @@
from django import forms
from django.contrib import admin 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 _
@ -18,6 +19,21 @@ class BillLineInline(admin.TabularInline):
'description', 'initial_date', 'final_date', 'price', 'amount', 'tax' 'description', 'initial_date', 'final_date', 'price', 'amount', 'tax'
) )
def get_readonly_fields(self, request, obj=None):
if obj and obj.status != Bill.OPEN:
return self.fields
return super(BillLineInline, self).get_readonly_fields(request, obj=obj)
def has_add_permission(self, request):
if request.__bill__ and request.__bill__.status != Bill.OPEN:
return False
return super(BillLineInline, self).has_add_permission(request)
def has_delete_permission(self, request, obj=None):
if obj and obj.status != Bill.OPEN:
return False
return super(BillLineInline, self).has_delete_permission(request, obj=obj)
class BudgetLineInline(admin.TabularInline): class BudgetLineInline(admin.TabularInline):
model = Budget model = Budget
@ -28,15 +44,25 @@ class BudgetLineInline(admin.TabularInline):
class BillAdmin(AccountAdminMixin, ExtendedModelAdmin): class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
list_display = ( list_display = (
'ident', 'status', 'type_link', 'account_link', 'created_on_display' 'number', 'status', 'type_link', 'account_link', 'created_on_display'
) )
list_filter = (BillTypeListFilter, 'status',) list_filter = (BillTypeListFilter, 'status',)
add_fields = ('account', 'type', 'status', 'due_on', 'comments')
fieldsets = (
(None, {
'fields': ('number', 'account_link', 'type', 'status', 'due_on',
'comments'),
}),
(_("Raw"), {
'classes': ('collapse',),
'fields': ('html',),
}),
)
change_view_actions = [generate_bill] change_view_actions = [generate_bill]
change_readonly_fields = ('account', 'type', 'status') change_readonly_fields = ('account_link', 'type', 'status')
readonly_fields = ('ident',) readonly_fields = ('number',)
inlines = [BillLineInline] inlines = [BillLineInline]
account_link = admin_link('account')
created_on_display = admin_date('created_on') created_on_display = admin_date('created_on')
def type_link(self, bill): def type_link(self, bill):
@ -47,11 +73,27 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
type_link.short_description = _("type") type_link.short_description = _("type")
type_link.admin_order_field = 'type' type_link.admin_order_field = 'type'
def get_readonly_fields(self, request, obj=None):
fields = super(BillAdmin, self).get_readonly_fields(request, obj=obj)
if obj and obj.status != Bill.OPEN:
fields += self.add_fields
return fields
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:
self.inlines = [BudgetLineInline] 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) return super(BillAdmin, self).get_inline_instances(request, obj=obj)
def formfield_for_dbfield(self, db_field, **kwargs):
""" Make value input widget bigger """
if db_field.name == 'comments':
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 4})
if db_field.name == 'html':
kwargs['widget'] = forms.Textarea(attrs={'cols': 150, 'rows': 20})
return super(BillAdmin, self).formfield_for_dbfield(db_field, **kwargs)
admin.site.register(Bill, BillAdmin) admin.site.register(Bill, BillAdmin)
admin.site.register(Invoice, BillAdmin) admin.site.register(Invoice, BillAdmin)

View File

@ -1,3 +1,5 @@
import inspect
from django.db import models from django.db import models
from django.template import loader, Context from django.template import loader, Context
from django.utils import timezone from django.utils import timezone
@ -43,7 +45,7 @@ class Bill(models.Model):
('BUDGET', _("Budget")), ('BUDGET', _("Budget")),
) )
ident = models.CharField(_("identifier"), max_length=16, unique=True, number = models.CharField(_("number"), max_length=16, unique=True,
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')
@ -62,7 +64,7 @@ class Bill(models.Model):
objects = BillManager() objects = BillManager()
def __unicode__(self): def __unicode__(self):
return self.ident return self.number
@cached_property @cached_property
def seller(self): def seller(self):
@ -77,31 +79,34 @@ class Bill(models.Model):
return self.billlines return self.billlines
@classmethod @classmethod
def get_type(cls): def get_class_type(cls):
return cls.__name__.upper() return cls.__name__.upper()
def set_ident(self): def get_type(self):
return self.type or self.get_class_type()
def set_number(self):
cls = type(self) cls = type(self)
bill_type = self.type or cls.get_type() bill_type = self.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_number() can not be used on a Bill class")
# Bill number resets every natural year # Bill number resets every natural year
year = timezone.now().strftime("%Y") year = timezone.now().strftime("%Y")
bills = cls.objects.filter(created_on__year=year) bills = cls.objects.filter(created_on__year=year)
number_length = settings.BILLS_IDENT_NUMBER_LENGTH number_length = settings.BILLS_NUMBER_LENGTH
prefix = getattr(settings, 'BILLS_%s_IDENT_PREFIX' % bill_type) prefix = getattr(settings, 'BILLS_%s_NUMBER_PREFIX' % bill_type)
if self.status == self.OPEN: if self.status == self.OPEN:
prefix = 'O{}'.format(prefix) prefix = 'O{}'.format(prefix)
bills = bills.filter(status=self.OPEN) bills = bills.filter(status=self.OPEN)
num_bills = bills.order_by('-ident').first() or 0 num_bills = bills.order_by('-number').first() or 0
if num_bills is not 0: if num_bills is not 0:
num_bills = int(num_bills.ident[-number_length:]) num_bills = int(num_bills.number[-number_length:])
else: else:
bills = bills.exclude(status=self.OPEN) bills = bills.exclude(status=self.OPEN)
num_bills = bills.count() num_bills = bills.count()
zeros = (number_length - len(str(num_bills))) * '0' zeros = (number_length - len(str(num_bills))) * '0'
number = zeros + str(num_bills + 1) number = zeros + str(num_bills + 1)
self.ident = '{prefix}{year}{number}'.format( self.number = '{prefix}{year}{number}'.format(
prefix=prefix, year=year, number=number) prefix=prefix, year=year, number=number)
def close(self): def close(self):
@ -130,9 +135,9 @@ class Bill(models.Model):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not self.type: if not self.type:
self.type = type(self).get_type() self.type = self.get_type()
if not self.ident or (self.ident.startswith('O') and self.status != self.OPEN): if not self.number or (self.number.startswith('O') and self.status != self.OPEN):
self.set_ident() self.set_number()
super(Bill, self).save(*args, **kwargs) super(Bill, self).save(*args, **kwargs)
@ -178,6 +183,14 @@ class BaseBillLine(models.Model):
class Meta: class Meta:
abstract = True abstract = True
def __unicode__(self):
return "#%i" % self.number
@property
def number(self):
lines = type(self).objects.filter(bill=self.bill_id)
return lines.filter(id__lte=self.id).order_by('id').count()
class BudgetLine(BaseBillLine): class BudgetLine(BaseBillLine):
pass pass

View File

@ -17,6 +17,6 @@ class BillSerializer(AccountSerializerMixin, serializers.HyperlinkedModelSeriali
class Meta: class Meta:
model = Bill model = Bill
fields = ( fields = (
'url', 'ident', 'bill_type', 'status', 'created_on', 'due_on', 'url', 'number', 'bill_type', 'status', 'created_on', 'due_on',
'comments', 'html', 'lines' 'comments', 'html', 'lines'
) )

View File

@ -1,17 +1,17 @@
from django.conf import settings from django.conf import settings
BILLS_IDENT_NUMBER_LENGTH = getattr(settings, 'BILLS_IDENT_NUMBER_LENGTH', 4) BILLS_NUMBER_LENGTH = getattr(settings, 'BILLS_NUMBER_LENGTH', 4)
BILLS_INVOICE_IDENT_PREFIX = getattr(settings, 'BILLS_INVOICE_IDENT_PREFIX', 'I') BILLS_INVOICE_NUMBER_PREFIX = getattr(settings, 'BILLS_INVOICE_NUMBER_PREFIX', 'I')
BILLS_AMENDMENT_INVOICE_IDENT_PREFIX = getattr(settings, 'BILLS_AMENDMENT_INVOICE_IDENT_PREFIX', 'A') BILLS_AMENDMENT_INVOICE_NUMBER_PREFIX = getattr(settings, 'BILLS_AMENDMENT_INVOICE_NUMBER_PREFIX', 'A')
BILLS_FEE_IDENT_PREFIX = getattr(settings, 'BILLS_FEE_IDENT_PREFIX', 'F') BILLS_FEE_NUMBER_PREFIX = getattr(settings, 'BILLS_FEE_NUMBER_PREFIX', 'F')
BILLS_AMENDMENT_FEE_IDENT_PREFIX = getattr(settings, 'BILLS_AMENDMENT_FEE_IDENT_PREFIX', 'B') BILLS_AMENDMENT_FEE_NUMBER_PREFIX = getattr(settings, 'BILLS_AMENDMENT_FEE_NUMBER_PREFIX', 'B')
BILLS_BUDGET_IDENT_PREFIX = getattr(settings, 'BILLS_BUDGET_IDENT_PREFIX', 'Q') BILLS_BUDGET_NUMBER_PREFIX = getattr(settings, 'BILLS_BUDGET_NUMBER_PREFIX', 'Q')
BILLS_INVOICE_TEMPLATE = getattr(settings, 'BILLS_INVOICE_TEMPLATE', 'bills/microspective.html') BILLS_INVOICE_TEMPLATE = getattr(settings, 'BILLS_INVOICE_TEMPLATE', 'bills/microspective.html')

View File

@ -1,6 +1,6 @@
<html> <html>
<head> <head>
<title>{% block title %}Invoice - I20110223{% endblock %}</title> <title>{% block title %}{{ bill.get_type_display }} - {{ bill.number }}{% endblock %}</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
{% block head %}{% endblock %} {% block head %}{% endblock %}
</head> </head>

View File

@ -21,48 +21,47 @@
</div> </div>
<div id="seller-details"> <div id="seller-details">
<div claas="address"> <div claas="address">
<span class="name">Associacio Pangea -<br> <span class="name">{{ seller.name }}</span>
Coordinadora Comunicacio per a la Cooperacio</span>
</div> </div>
<div class="contact"> <div class="contact">
<p>Pl. Eusebi Guell 6-7, planta 0<br> <p>{{ seller.address }}<br>
08034 - Barcelona<br> {{ seller.zipcode }} - {{ seller.city }}<br>
Spain<br> {{ seller.country }}<br>
</p> </p>
<p><a href="tel:93-803-21-32">93-803-21-32</a><br> <p><a href="tel:93-803-21-32">{{ seller_info.phone }}</a><br>
<a href="mailto:sales@pangea.org">sales@pangea.org</a><br> <a href="mailto:sales@pangea.org">{{ seller_info.email }}</a><br>
<a href="http://www.pangea.org">www.pangea.org</a></p> <a href="http://www.pangea.org">{{ seller_info.website }}</a></p>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block summary %} {% block summary %}
<div id="bill-number"> <div id="bill-number">
Invoice<br> {{ bill.get_type_display }}<br>
<span class="value">F20110232</span><br> <span class="value">{{ bill.number }}</span><br>
<span id="pagination">Page 1 of 1</span> <span id="pagination">Page 1 of 1</span>
</div> </div>
<div id="bill-summary"> <div id="bill-summary">
<hr> <hr>
<div id="due-date"> <div id="due-date">
<span class="title">DUE DATE</span><br> <span class="title">DUE DATE</span><br>
<psan class="value">Nov 21, 2011</span> <psan class="value">{{ bill.due_on|date }}</span>
</div> </div>
<div id="total"> <div id="total">
<span class="title">TOTAL</span><br> <span class="title">TOTAL</span><br>
<psan class="value">122,03 &euro;</span> <psan class="value">{{ bill.total }} &{{ currency.lower }};</span>
</div> </div>
<div id="bill-date"> <div id="bill-date">
<span class="title">INVOICE DATE</span><br> <span class="title">{{ bill.get_type_display.upper }} DATE</span><br>
<psan class="value">Oct 20, 2012</span> <psan class="value">{{ bill.created_on|date }}</span>
</div> </div>
</div> </div>
<div id="buyer-details"> <div id="buyer-details">
<span class="name">Aadults</span><br> <span class="name">{{ buyer.name }}</span><br>
ES01939933<br> {{ buyer.vat }}<br>
Carrer nnoseque, 0<br> {{ buyer.address }}<br>
08034 - Barcelona<br> {{ buyer.zipcode }} - {{ buyer.city }}<br>
Spain<br> {{ buyer.country }}<br>
</div> </div>
{% endblock %} {% endblock %}
@ -74,50 +73,24 @@
<span class="title column-rate">rate/price</span> <span class="title column-rate">rate/price</span>
<span class="title column-subtotal">subtotal</span> <span class="title column-subtotal">subtotal</span>
<br> <br>
<span class="value column-id"> {% for line in bill.lines.all %}
1<br> <span class="value column-id">{{ line.id }}</span>
&nbsp;</span> <span class="value column-description">{{ line.description }}</span>
<span class="value column-description"> <span class="value column-quantity">{{ line.amount }}</span>
Hola que passa<br> <span class="value column-rate">{{ line.rate }}</span>
nosquevols</span> <span class="value column-subtotal">{{ line.price }} &{{ currency.lower }};</span>
<span class="value column-quantity">
1<br>
&nbsp;</span>
<span class="value column-rate">
1,00 &euro;<br>
&nbsp;</span>
<span class="value column-subtotal">
111,00 &euro;<br>
-10,00 &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,00 &euro;</span>
<span class="value column-subtotal">111,00 &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,00 &euro;</span>
<span class="value column-subtotal">111,00 &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,00 &euro;</span>
<span class="value column-subtotal">111,00 &euro;</span>
<br> <br>
{% endfor %}
</div> </div>
<div id="totals"> <div id="totals">
<span class="subtotal column-title">subtotal</span> <span class="subtotal column-title">subtotal</span>
<span class="subtotal column-value">33,03 &euro;</span> <span class="subtotal column-value">{{ bill.subtotal }} &{{ currency.lower }};</span>
<br> <br>
<span class="tax column-title">tax</span> <span class="tax column-title">tax</span>
<span class="tax column-value">33,03 &euro;</span> <span class="tax column-value">{{ bill.taxes }} &{{ currency.lower }};</span>
<br> <br>
<span class="total column-title">total</span> <span class="total column-title">total</span>
<span class="total column-value">33,03 &euro;</span> <span class="total column-value">{{ bill.total }} &{{ currency.lower }};</span>
<br> <br>
</div> </div>
{% endblock %} {% endblock %}
@ -126,13 +99,14 @@
<div id="footer"> <div id="footer">
<div id="footer-column-1"> <div id="footer-column-1">
<div id="comments"> <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. {% if bill.comments %}
<span class="title">COMMENTS</span> {{ bill.comments|linebreaksbr }}
{% endif %}
</div> </div>
</div> </div>
<div id="footer-column-2"> <div id="footer-column-2">
<div id="payment"> <div id="payment">
<span class="title">PAYMENT</span> You can pay our invoice by bank transfer <span class="title">PAYMENT</span> {{ bill.payment.message }}
llkdskdlsdk The comments should be here. The comments should be here.
</div> </div>
<div id="questions"> <div id="questions">
<span class="title">QUESTIONS</span> If you have any question about your bill, please <span class="title">QUESTIONS</span> If you have any question about your bill, please

View File

View File

@ -0,0 +1,113 @@
import re
from django.contrib.auth.hashers import check_password, make_password
from django.core.validators import RegexValidator
from django.db import models
from django.utils.translation import ugettext_lazy as _
from orchestra.core import services
from . import validators, settings
class Mailbox(models.Model):
name = models.CharField(_("name"), max_length=64, unique=True,
help_text=_("Required. 30 characters or fewer. Letters, digits and "
"@/./+/-/_ only."),
validators=[RegexValidator(r'^[\w.@+-]+$',
_("Enter a valid username."), 'invalid')])
use_custom_filtering = models.BooleanField(_("Use custom filtering"),
default=False)
custom_filtering = models.TextField(_("filtering"), blank=True,
validators=[validators.validate_sieve],
help_text=_("Arbitrary email filtering in sieve language."))
class Meta:
verbose_name_plural = _("mailboxes")
def __unicode__(self):
return self.user.username
# def get_addresses(self):
# regex = r'(^|\s)+%s(\s|$)+' % self.user.username
# return Address.objects.filter(destination__regex=regex)
#
# def delete(self, *args, **kwargs):
# """ Update related addresses """
# regex = re.compile(r'(^|\s)+(\s*%s)(\s|$)+' % self.user.username)
# super(Mailbox, self).delete(*args, **kwargs)
# for address in self.get_addresses():
# address.destination = regex.sub(r'\3', address.destination).strip()
# if not address.destination:
# address.delete()
# else:
# address.save()
#class Address(models.Model):
# name = models.CharField(_("name"), max_length=64,
# validators=[validators.validate_emailname])
# domain = models.ForeignKey(settings.EMAILS_DOMAIN_MODEL,
# verbose_name=_("domain"),
# related_name='addresses')
# destination = models.CharField(_("destination"), max_length=256,
# validators=[validators.validate_destination],
# help_text=_("Space separated mailbox names or email addresses"))
# account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
# related_name='addresses')
#
# class Meta:
# verbose_name_plural = _("addresses")
# unique_together = ('name', 'domain')
#
# def __unicode__(self):
# return self.email
#
# @property
# def email(self):
# return "%s@%s" % (self.name, self.domain)
#
# def get_mailboxes(self):
# for dest in self.destination.split():
# if '@' not in dest:
# yield Mailbox.objects.select_related('user').get(user__username=dest)
class Address(models.Model):
name = models.CharField(_("name"), max_length=64,
validators=[validators.validate_emailname])
domain = models.ForeignKey(settings.EMAILS_DOMAIN_MODEL,
verbose_name=_("domain"),
related_name='addresses')
mailboxes = models.ManyToManyField('mail.Mailbox', verbose_name=_("mailboxes"),
related_name='addresses', blank=True)
forward = models.CharField(_("forward"), max_length=256, blank=True,
validators=[validators.validate_forward])
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
related_name='addresses')
class Meta:
verbose_name_plural = _("addresses")
unique_together = ('name', 'domain')
def __unicode__(self):
return self.email
@property
def email(self):
return "%s@%s" % (self.name, self.domain)
class Autoresponse(models.Model):
address = models.OneToOneField(Address, verbose_name=_("address"),
related_name='autoresponse')
# TODO initial_date
subject = models.CharField(_("subject"), max_length=256)
message = models.TextField(_("message"))
enabled = models.BooleanField(_("enabled"), default=False)
def __unicode__(self):
return self.address
services.register(Address)