Improvements on bills app
This commit is contained in:
parent
2a37cfc8d7
commit
3ea6fde7bd
14
ROADMAP.md
14
ROADMAP.md
|
@ -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
|
||||||
|
|
|
@ -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')
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'
|
||||||
)
|
)
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 €</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>
|
||||||
</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>
|
|
||||||
</span>
|
|
||||||
<span class="value column-rate">
|
|
||||||
1,00 €<br>
|
|
||||||
</span>
|
|
||||||
<span class="value column-subtotal">
|
|
||||||
111,00 €<br>
|
|
||||||
-10,00 €</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 €</span>
|
|
||||||
<span class="value column-subtotal">111,00 €</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 €</span>
|
|
||||||
<span class="value column-subtotal">111,00 €</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 €</span>
|
|
||||||
<span class="value column-subtotal">111,00 €</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 €</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 €</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 €</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
|
||||||
|
|
0
orchestra/apps/mails/__init__.py
Normal file
0
orchestra/apps/mails/__init__.py
Normal file
113
orchestra/apps/mails/models.py
Normal file
113
orchestra/apps/mails/models.py
Normal 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)
|
Loading…
Reference in a new issue