Preliminar implementation of amended invoices

This commit is contained in:
Marc Aymerich 2015-07-07 10:41:34 +00:00
parent 3520b3968b
commit 75c72ce8a5
12 changed files with 570 additions and 261 deletions

View File

@ -17,6 +17,7 @@ from orchestra.admin.forms import adminmodelformset_factory
from orchestra.admin.utils import get_object_from_url, change_url
from orchestra.utils.html import html_to_pdf
from . import settings
from .forms import SelectSourceForm
from .helpers import validate_contact
from .models import Bill, BillLine
@ -217,7 +218,7 @@ def amend_bills(modeladmin, request, queryset):
if queryset.filter(is_open=True).exists():
messages.warning(request, _("Selected bills should be in closed state"))
return
ids = []
amend_ids = []
for bill in queryset:
with translation.override(bill.account.language):
amend_type = bill.get_amend_type()
@ -239,17 +240,33 @@ def amend_bills(modeladmin, request, queryset):
line = BillLine.objects.create(
bill=amend,
start_on=bill.created_on,
description=_("Amend of %(related_type)s %(number)s, tax %(tax)s%%") % context,
description=_("%(related_type)s %(number)s subtotal for tax %(tax)s%%") % context,
subtotal=subtotals[0],
tax=tax
)
ids.append(bill.pk)
amend_url = reverse('admin:bills_bill_changelist')
amend_url += '?id=%s' % ','.join(map(str, ids))
amend_ids.append(amend.pk)
num = len(amend_ids)
if num == 1:
amend_url = reverse('admin:bills_bill_change', args=amend_ids)
else:
amend_url = reverse('admin:bills_bill_changelist')
amend_url += '?id=%s' % ','.join(map(str, amend_ids))
context = {
'url': amend_url,
'num': num,
}
messages.success(request, mark_safe(ungettext(
_('<a href="%s">One amendment bill</a> have been generated.') % amend_url,
_('<a href="%s">%i amendment bills</a> have been generated.') % (amend_url, len(ids)),
len(ids)
_('<a href="%(url)s">One amendment bill</a> have been generated.') % context,
_('<a href="%(url)s">%(num)i amendment bills</a> have been generated.') % context,
num
)))
amend_bills.verbose_name = _("Amend")
amend_bills.url_name = 'amend'
def report(modeladmin, request, queryset):
context = {
'bills': queryset,
'currency': settings.BILLS_CURRENCY,
}
return render(request, 'admin/bills/report.html', context)

View File

@ -186,10 +186,10 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
'num_lines', 'display_total', 'display_payment_state', 'is_open', 'is_sent'
)
list_filter = (BillTypeListFilter, 'is_open', 'is_sent', TotalListFilter, PaymentStateListFilter)
add_fields = ('account', 'type', 'is_open', 'due_on', 'comments')
add_fields = ('account', 'type', 'amend_of', 'is_open', 'due_on', 'comments')
fieldsets = (
(None, {
'fields': ('number', 'type', 'account_link', 'display_total',
'fields': ('number', 'type', 'amend_of_link', 'account_link', 'display_total',
'display_payment_state', 'is_sent', 'due_on', 'comments'),
}),
(_("Raw"), {
@ -205,13 +205,14 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
]
actions = [
actions.manage_lines, actions.download_bills, actions.close_bills, actions.send_bills,
actions.amend_bills,
actions.amend_bills, actions.report
]
change_readonly_fields = ('account_link', 'type', 'is_open')
change_readonly_fields = ('account_link', 'type', 'is_open', 'amend_of_link')
readonly_fields = ('number', 'display_total', 'is_sent', 'display_payment_state')
inlines = [BillLineInline, ClosedBillLineInline]
created_on_display = admin_date('created_on', short_description=_("Created"))
amend_of_link = admin_link('amend_of')
def num_lines(self, bill):
return bill.lines__count
@ -267,8 +268,12 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
def get_fieldsets(self, request, obj=None):
fieldsets = super(BillAdmin, self).get_fieldsets(request, obj)
if obj and obj.is_open:
fieldsets = (fieldsets[0],)
if obj:
# if obj.amend_of_id:
# fieldsets = list(fieldsets)
# fieldsets[0][1]['fields'] = fieldsets[0][1]['fields'] + ('amend_of_link',)
if obj.is_open:
fieldsets = (fieldsets[0],)
return fieldsets
def get_change_view_actions(self, obj=None):
@ -289,10 +294,13 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
""" Make value input widget bigger """
if db_field.name == 'comments':
kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 4})
if db_field.name == 'html':
elif db_field.name == 'html':
kwargs['widget'] = forms.Textarea(attrs={'cols': 150, 'rows': 20})
return super(BillAdmin, self).formfield_for_dbfield(db_field, **kwargs)
formfield = super(BillAdmin, self).formfield_for_dbfield(db_field, **kwargs)
if db_field.name == 'amend_of':
formfield.queryset = formfield.queryset.filter(is_open=False)
return formfield
def get_queryset(self, request):
qs = super(BillAdmin, self).get_queryset(request)
qs = qs.annotate(

View File

@ -4,6 +4,8 @@ from django.db.models import Q
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from . models import Bill
class BillTypeListFilter(SimpleListFilter):
""" Filter tickets by created_by according to request.user """
@ -89,6 +91,7 @@ class PaymentStateListFilter(SimpleListFilter):
('PAID', _("Paid")),
('PENDING', _("Pending")),
('BAD_DEBT', _("Bad debt")),
('AMENDED', _("Amended")),
)
def queryset(self, request, queryset):
@ -96,10 +99,12 @@ class PaymentStateListFilter(SimpleListFilter):
if self.value() == 'OPEN':
return queryset.filter(Q(is_open=True)|Q(type=queryset.model.PROFORMA))
elif self.value() == 'PAID':
zeros = queryset.filter(computed_total=0, computed_total__isnull=True).values_list('id', flat=True)
zeros = queryset.filter(computed_total=0, computed_total__isnull=True)
zeros = zeros.values_list('id', flat=True)
ammounts = Transaction.objects.exclude(bill_id__in=zeros).secured().group_by('bill_id')
paid = []
for bill_id, total in queryset.exclude(computed_total=0, computed_total__isnull=True, is_open=True).values_list('id', 'computed_total'):
relevant = queryset.exclude(computed_total=0, computed_total__isnull=True, is_open=True)
for bill_id, total in relevant.values_list('id', 'computed_total'):
try:
ammount = sum([t.ammount for t in ammounts[bill_id]])
except KeyError:
@ -107,7 +112,11 @@ class PaymentStateListFilter(SimpleListFilter):
else:
if abs(total) <= abs(ammount):
paid.append(bill_id)
return queryset.filter(Q(computed_total=0)|Q(computed_total__isnull=True)|Q(id__in=paid)).exclude(is_open=True)
return queryset.filter(
Q(computed_total=0) |
Q(computed_total__isnull=True) |
Q(id__in=paid)
).exclude(is_open=True)
elif self.value() == 'PENDING':
has_transaction = queryset.exclude(transactions__isnull=True)
non_rejected = has_transaction.exclude(transactions__state=Transaction.REJECTED)
@ -115,4 +124,11 @@ class PaymentStateListFilter(SimpleListFilter):
return queryset.filter(pk__in=non_rejected)
elif self.value() == 'BAD_DEBT':
closed = queryset.filter(is_open=False).exclude(computed_total=0)
return closed.filter(Q(transactions__state=Transaction.REJECTED)|Q(transactions__isnull=True))
return closed.filter(
Q(transactions__state=Transaction.REJECTED) |
Q(transactions__isnull=True)
)
elif self.value() == 'AMENDED':
amendeds = queryset.filter(type__in=Bill.AMEND_MAP.values(), is_open=False)
amendeds_ids = amendeds.values_list('amend_of', flat=True)
return queryset.filter(id__in=amendeds)

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-05-29 09:39+0000\n"
"POT-Creation-Date: 2015-07-07 10:18+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,37 +18,37 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: actions.py:37
#: actions.py:40
msgid "Download"
msgstr "Descarrega"
#: actions.py:47
#: actions.py:50
msgid "View"
msgstr "Vista"
#: actions.py:55
#: actions.py:58
msgid "Selected bills should be in open state"
msgstr "Les factures seleccionades han d'estar en estat obert"
#: actions.py:73
#: actions.py:76
msgid "Selected bills have been closed"
msgstr "Les factures seleccionades han estat tancades"
#: actions.py:86
#: actions.py:89
#, python-format
msgid "<a href=\"%(url)s\">One related transaction</a> has been created"
msgstr "S'ha creat una <a href=\"%(url)s\">transacció</a>"
#: actions.py:87
#: actions.py:90
#, python-format
msgid "<a href=\"%(url)s\">%(num)i related transactions</a> have been created"
msgstr "S'han creat les <a href=\"%(url)s\">%(num)i següents transaccions</a>"
#: actions.py:93
#: actions.py:96
msgid "Are you sure about closing the following bills?"
msgstr "Estàs a punt de tancar les següents factures, estàs segur?"
#: actions.py:94
#: actions.py:97
msgid ""
"Once a bill is closed it can not be further modified.</p><p>Please select a "
"payment source for the selected bills"
@ -56,138 +56,183 @@ msgstr ""
"Una vegada la factura estigui tancada no podrà ser modificada.</p><p>Si us "
"plau selecciona un mètode de pagament per les factures seleccionades"
#: actions.py:107
#: actions.py:110
msgid "Close"
msgstr "Tanca"
#: actions.py:125
#: actions.py:124
msgid "One bill has been sent."
msgstr "S'ha creat una factura"
#: actions.py:126
#: actions.py:125
#, python-format
msgid "%i bills have been sent."
msgstr "S'han enviat %i factures."
#: actions.py:128
#: actions.py:127
msgid "Resend"
msgstr "Reenviat"
#: actions.py:189
#: actions.py:188
#, python-format
msgid "%(norders)s orders and %(nlines)s lines undoed."
msgstr "%(norders)s ordres i %(nlines)s línies desfetes."
#: actions.py:208
#: actions.py:207
msgid "Lines moved"
msgstr "Línies mogudes"
#: admin.py:49 admin.py:93 admin.py:128 forms.py:11
#: actions.py:219
msgid "Selected bills should be in closed state"
msgstr "Les factures seleccionades han d'estar en estat obert"
#: actions.py:236
#, python-format
msgid "%(type)s of %(related_type)s %(number)s and creation date %(date)s"
msgstr "%(type)s de %(related_type)s %(number)s amb data de creació %(date)s"
#: actions.py:243
#, python-format
msgid "%(related_type)s %(number)s subtotal for tax %(tax)s%%"
msgstr "%(related_type)s %(number)s subtotal %(tax)s%%"
#: actions.py:259
#, python-format
msgid "<a href=\"%(url)s\">One amendment bill</a> have been generated."
msgstr "S'ha creat una <a href=\"%(url)s\">transacció</a>"
#: actions.py:260
#, python-format
msgid "<a href=\"%(url)s\">%(num)i amendment bills</a> have been generated."
msgstr "S'han creat les <a href=\"%(url)s\">%(num)i següents transaccions</a>"
#: actions.py:263
msgid "Amend"
msgstr ""
#: admin.py:54 admin.py:98 admin.py:133 forms.py:11
#: templates/admin/bills/report.html:43
msgid "Total"
msgstr "Total"
#: admin.py:80
#: admin.py:85
msgid "Description"
msgstr "Descripció"
#: admin.py:88
#: admin.py:93
msgid "Subtotal"
msgstr "Subtotal"
#: admin.py:118
#: admin.py:123
msgid "Is open"
msgstr "És oberta"
#: admin.py:123
#: admin.py:128
msgid "Subline"
msgstr "Sublínia"
#: admin.py:157
#: admin.py:162
msgid "No bills selected."
msgstr "No hi ha factures seleccionades"
#: admin.py:164
#: admin.py:169
#, python-format
msgid "Manage %s bill lines."
msgstr "Gestiona %s línies de factura."
#: admin.py:166
#: admin.py:171
msgid "Bill not in open state."
msgstr "La factura no està en estat obert"
#: admin.py:169
#: admin.py:174
msgid "Not all bills are in open state."
msgstr "No totes les factures estan en estat obert"
#: admin.py:170
#: admin.py:175
msgid "Manage bill lines of multiple bills."
msgstr "Gestiona línies de factura de multiples factures."
#: admin.py:190
#: admin.py:195
msgid "Raw"
msgstr "Raw"
#: admin.py:208
#: admin.py:214 models.py:72
msgid "Created"
msgstr "Creada"
#: admin.py:213
#: admin.py:220
msgid "lines"
msgstr "línies"
#: admin.py:218 templates/bills/microspective.html:118
#: admin.py:225 filters.py:44 templates/bills/microspective.html:118
msgid "total"
msgstr "total"
#: admin.py:226 models.py:88 models.py:352
#: admin.py:233 models.py:103 models.py:446
msgid "type"
msgstr "tipus"
#: admin.py:243
#: admin.py:250
msgid "Payment"
msgstr "Pagament"
#: filters.py:17
#: filters.py:19
msgid "All"
msgstr "Tot"
#: filters.py:18 models.py:78
#: filters.py:20 models.py:87
msgid "Invoice"
msgstr "Factura"
#: filters.py:19 models.py:79
#: filters.py:21 models.py:88
msgid "Amendment invoice"
msgstr "Factura rectificativa"
#: filters.py:20 models.py:80
#: filters.py:22 models.py:89
msgid "Fee"
msgstr "Quota de soci"
#: filters.py:21
#: filters.py:23
msgid "Amendment fee"
msgstr "Rectificació de quota de soci"
#: filters.py:22
#: filters.py:24
msgid "Pro-forma"
msgstr "Pro-forma"
#: filters.py:41
msgid "positive price"
msgstr "preu positiu"
#: filters.py:46 filters.py:64
msgid "Yes"
msgstr "Si"
#: filters.py:47 filters.py:65
msgid "No"
msgstr "No"
#: filters.py:59
#: filters.py:66
msgid "has bill contact"
msgstr "té contacte de facturació"
#: forms.py:9
#: filters.py:71
msgid "Yes"
msgstr "Si"
#: filters.py:72
msgid "No"
msgstr "No"
#: filters.py:83
msgid "payment state"
msgstr "Pagament"
#: filters.py:88 models.py:71
msgid "Open"
msgstr ""
#: filters.py:89 models.py:75
msgid "Paid"
msgstr "Pagat"
#: filters.py:90
msgid "Pending"
msgstr "Pendent"
#: filters.py:91 models.py:78
msgid "Bad debt"
msgstr "Incobrable"
#: forms.py:9 templates/admin/bills/report.html:37
msgid "Number"
msgstr "Número"
@ -219,7 +264,7 @@ msgstr "Relacionat"
msgid "Main"
msgstr "Principal"
#: models.py:23 models.py:86
#: models.py:23 models.py:99
msgid "account"
msgstr "compte"
@ -251,141 +296,186 @@ msgstr "Introdueix un codi postal vàlid."
msgid "country"
msgstr "país"
#: models.py:35
#: models.py:35 templates/admin/bills/report.html:38
msgid "VAT number"
msgstr "NIF"
#: models.py:67
msgid "Paid"
msgstr "Pagat"
#: models.py:73
msgid "Processed"
msgstr ""
#: models.py:68
msgid "Pending"
msgstr "Pendent"
#: models.py:74
#, fuzzy
#| msgid "amended line"
msgid "Amended"
msgstr "línia rectificada"
#: models.py:69
msgid "Bad debt"
msgstr "Incobrable"
#: models.py:76
msgid "Incomplete"
msgstr ""
#: models.py:81
#: models.py:77
msgid "Executed"
msgstr ""
#: models.py:90
msgid "Amendment Fee"
msgstr "Rectificació de quota de soci"
#: models.py:82
#: models.py:91
msgid "Pro forma"
msgstr "Pro forma"
#: models.py:85
#: models.py:98
msgid "number"
msgstr "número"
#: models.py:89
#: models.py:101
#, fuzzy
#| msgid "amended line"
msgid "amend of"
msgstr "línia rectificada"
#: models.py:104
msgid "created on"
msgstr "creat el"
#: models.py:90
#: models.py:105
msgid "closed on"
msgstr "tancat el"
#: models.py:91
#: models.py:106
msgid "open"
msgstr "obert"
#: models.py:92
#: models.py:107
msgid "sent"
msgstr "enviat"
#: models.py:93
#: models.py:108
msgid "due on"
msgstr "es deu"
#: models.py:94
#: models.py:109
msgid "updated on"
msgstr "actualitzada el"
#: models.py:97
#: models.py:112
msgid "comments"
msgstr "comentaris"
#: models.py:98
#: models.py:113
msgid "HTML"
msgstr "HTML"
#: models.py:285
#: models.py:192
#, python-format
msgid "Type %s is not an amendment."
msgstr ""
#: models.py:194
msgid "Amend of related account doesn't match bill account."
msgstr ""
#: models.py:199
#, python-format
msgid "Type %s requires an amend of link."
msgstr ""
#: models.py:378
msgid "bill"
msgstr "factura"
#: models.py:286 models.py:350 templates/bills/microspective.html:73
#: models.py:379 models.py:444 templates/bills/microspective.html:73
msgid "description"
msgstr "descripció"
#: models.py:287
#: models.py:380
msgid "rate"
msgstr "tarifa"
#: models.py:288
#: models.py:381
msgid "quantity"
msgstr "quantitat"
#: models.py:289
#: models.py:383
#, fuzzy
#| msgid "quantity"
msgid "Verbose quantity"
msgstr "quantitat"
#: models.py:290 templates/bills/microspective.html:77
#: models.py:384 templates/bills/microspective.html:77
#: templates/bills/microspective.html:111
msgid "subtotal"
msgstr "subtotal"
#: models.py:291
#: models.py:385
msgid "tax"
msgstr "impostos"
#: models.py:292
#: models.py:386
msgid "start"
msgstr "iniciar"
#: models.py:293
#: models.py:387
msgid "end"
msgstr "finalitzar"
#: models.py:295
#: models.py:389
msgid "Informative link back to the order"
msgstr "Enllaç informatiu de l'ordre"
#: models.py:296
#: models.py:390
msgid "order billed"
msgstr "ordre facturada"
#: models.py:297
#: models.py:391
msgid "order billed until"
msgstr "ordre facturada fins a"
#: models.py:298
#: models.py:392
msgid "created"
msgstr "creada"
#: models.py:300
#: models.py:394
msgid "amended line"
msgstr "línia rectificada"
#: models.py:343
#: models.py:437
msgid "Volume"
msgstr "Volum"
#: models.py:344
#: models.py:438
msgid "Compensation"
msgstr "Compensació"
#: models.py:345
#: models.py:439
msgid "Other"
msgstr "Altre"
#: models.py:349
#: models.py:443
msgid "bill line"
msgstr "línia de factura"
#: templates/admin/bills/report.html:39
msgid "Contact"
msgstr ""
#: templates/admin/bills/report.html:40
#, fuzzy
#| msgid "Due date"
msgid "Close date"
msgstr "Data de pagament"
#: templates/admin/bills/report.html:41
msgid "Base"
msgstr ""
#: templates/admin/bills/report.html:42 templates/bills/microspective.html:111
#: templates/bills/microspective.html:114
msgid "VAT"
msgstr "IVA"
#: templates/bills/microspective-fee.html:107
msgid "Due date"
msgstr "Data de pagament"
@ -433,11 +523,6 @@ msgstr "hrs/qnt"
msgid "rate/price"
msgstr "tarifa/preu"
#: templates/bills/microspective.html:111
#: templates/bills/microspective.html:114
msgid "VAT"
msgstr "IVA"
#: templates/bills/microspective.html:114
msgid "taxes"
msgstr "impostos"
@ -451,6 +536,7 @@ msgid "PAYMENT"
msgstr "PAGAMENT"
#: templates/bills/microspective.html:140
#, python-format
msgid ""
"\n"
" You can pay our <i>%(type)s</i> by bank transfer.<br>\n"
@ -483,3 +569,6 @@ msgstr ""
" contacta amb nosaltres a %(email)s. Et respondrem el més "
"ràpidament possible.\n"
" "
#~ msgid "positive price"
#~ msgstr "preu positiu"

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-05-28 12:31+0000\n"
"POT-Creation-Date: 2015-07-07 10:10+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,37 +18,37 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: actions.py:37
#: actions.py:40
msgid "Download"
msgstr "Descarga"
#: actions.py:47
#: actions.py:50
msgid "View"
msgstr "Vista"
#: actions.py:55
#: actions.py:58
msgid "Selected bills should be in open state"
msgstr "Las facturas seleccionadas están en estado abierto"
#: actions.py:73
#: actions.py:76
msgid "Selected bills have been closed"
msgstr "Las facturas seleccionadas han sido cerradas"
#: actions.py:86
#: actions.py:89
#, python-format
msgid "<a href=\"%(url)s\">One related transaction</a> has been created"
msgstr "Se ha creado una <a href=\"%(url)s\">transacción</a>"
#: actions.py:87
#: actions.py:90
#, python-format
msgid "<a href=\"%(url)s\">%(num)i related transactions</a> have been created"
msgstr "Se han creado <a href=\"%(url)s\">%(num)i transacciones</a>"
#: actions.py:93
#: actions.py:96
msgid "Are you sure about closing the following bills?"
msgstr "Estás a punto de cerrar las sigüientes facturas. ¿Estás seguro?"
#: actions.py:94
#: actions.py:97
msgid ""
"Once a bill is closed it can not be further modified.</p><p>Please select a "
"payment source for the selected bills"
@ -56,138 +56,180 @@ msgstr ""
"Una vez cerrada la factura ya no se podrá modificar.</p><p>Por favor "
"seleciona un metodo de pago para las facturas seleccionadas"
#: actions.py:107
#: actions.py:110
msgid "Close"
msgstr "Cerrar"
#: actions.py:125
#: actions.py:124
msgid "One bill has been sent."
msgstr "Se ha enviado una factura"
#: actions.py:126
#: actions.py:125
#, python-format
msgid "%i bills have been sent."
msgstr ""
#: actions.py:128
#: actions.py:127
msgid "Resend"
msgstr ""
#: actions.py:189
#: actions.py:188
#, python-format
msgid "%(norders)s orders and %(nlines)s lines undoed."
msgstr ""
#: actions.py:208
#: actions.py:207
msgid "Lines moved"
msgstr ""
#: admin.py:49 admin.py:93 admin.py:128 forms.py:11
#: actions.py:219
msgid "Selected bills should be in closed state"
msgstr "Las facturas seleccionadas están en estado abierto"
#: actions.py:236
#, python-format
msgid "%(type)s of %(related_type)s %(number)s and creation date %(date)s"
msgstr "%(type)s de %(related_type)s %(number)s con fecha de creación %(date)s"
#: actions.py:243
msgid "%(related_type)s %(number)s subtotal for tax %(tax)s%%"
msgstr "%(related_type)s %(number)s subtotal %(tax)s%%"
#: actions.py:255
msgid "<a href=\"%(url)s\">One amendment bill</a> have been generated."
msgstr "Se ha creado una <a href=\"%(url)s\">transacción</a>"
#: actions.py:256
msgid "<a href=\"%(url)s\">%(num)i amendment bills</a> have been generated."
msgstr "Se han creado <a href=\"%(url)s\">%(num)i transacciones</a>"
#: actions.py:259
msgid "Amend"
msgstr ""
#: admin.py:54 admin.py:98 admin.py:133 forms.py:11
#: templates/admin/bills/report.html:43
msgid "Total"
msgstr ""
#: admin.py:80
#: admin.py:85
msgid "Description"
msgstr ""
#: admin.py:88
#: admin.py:93
msgid "Subtotal"
msgstr ""
#: admin.py:118
#: admin.py:123
msgid "Is open"
msgstr ""
#: admin.py:123
#: admin.py:128
msgid "Subline"
msgstr ""
#: admin.py:157
#: admin.py:162
msgid "No bills selected."
msgstr ""
#: admin.py:164
#: admin.py:169
#, python-format
msgid "Manage %s bill lines."
msgstr ""
#: admin.py:166
#: admin.py:171
msgid "Bill not in open state."
msgstr ""
#: admin.py:169
#: admin.py:174
msgid "Not all bills are in open state."
msgstr ""
#: admin.py:170
#: admin.py:175
msgid "Manage bill lines of multiple bills."
msgstr ""
#: admin.py:190
#: admin.py:195
msgid "Raw"
msgstr ""
#: admin.py:208
#: admin.py:214 models.py:72
msgid "Created"
msgstr ""
#: admin.py:213
#: admin.py:220
msgid "lines"
msgstr ""
#: admin.py:218 templates/bills/microspective.html:118
#: admin.py:225 filters.py:44 templates/bills/microspective.html:118
msgid "total"
msgstr ""
#: admin.py:226 models.py:88 models.py:352
#: admin.py:233 models.py:103 models.py:446
msgid "type"
msgstr ""
#: admin.py:243
#: admin.py:250
msgid "Payment"
msgstr "Pago"
#: filters.py:17
#: filters.py:19
msgid "All"
msgstr ""
#: filters.py:18 models.py:78
#: filters.py:20 models.py:87
msgid "Invoice"
msgstr "Factura"
#: filters.py:19 models.py:79
#: filters.py:21 models.py:88
msgid "Amendment invoice"
msgstr "Factura rectificative"
#: filters.py:20 models.py:80
#: filters.py:22 models.py:89
msgid "Fee"
msgstr "Quota de socio"
#: filters.py:21
#: filters.py:23
msgid "Amendment fee"
msgstr "Quota rectificativa"
#: filters.py:22
#: filters.py:24
msgid "Pro-forma"
msgstr ""
#: filters.py:41
msgid "positive price"
msgstr ""
#: filters.py:46 filters.py:64
msgid "Yes"
msgstr ""
#: filters.py:47 filters.py:65
msgid "No"
msgstr ""
#: filters.py:59
#: filters.py:66
msgid "has bill contact"
msgstr ""
#: forms.py:9
#: filters.py:71
msgid "Yes"
msgstr ""
#: filters.py:72
msgid "No"
msgstr ""
#: filters.py:83
msgid "payment state"
msgstr "Pago"
#: filters.py:88 models.py:71
msgid "Open"
msgstr ""
#: filters.py:89 models.py:75
msgid "Paid"
msgstr ""
#: filters.py:90
msgid "Pending"
msgstr ""
#: filters.py:91 models.py:78
msgid "Bad debt"
msgstr ""
#: forms.py:9 templates/admin/bills/report.html:37
msgid "Number"
msgstr ""
@ -217,7 +259,7 @@ msgstr ""
msgid "Main"
msgstr ""
#: models.py:23 models.py:86
#: models.py:23 models.py:99
msgid "account"
msgstr ""
@ -249,138 +291,179 @@ msgstr ""
msgid "country"
msgstr ""
#: models.py:35
#: models.py:35 templates/admin/bills/report.html:38
msgid "VAT number"
msgstr ""
#: models.py:67
msgid "Paid"
#: models.py:73
msgid "Processed"
msgstr ""
#: models.py:68
msgid "Pending"
#: models.py:74
msgid "Amended"
msgstr "Quota rectificativa"
#: models.py:76
msgid "Incomplete"
msgstr ""
#: models.py:69
msgid "Bad debt"
msgstr ""
#: models.py:81
msgid "Amendment Fee"
msgstr ""
#: models.py:82
msgid "Pro forma"
msgstr ""
#: models.py:85
msgid "number"
msgstr ""
#: models.py:89
msgid "created on"
#: models.py:77
msgid "Executed"
msgstr ""
#: models.py:90
msgid "closed on"
msgid "Amendment Fee"
msgstr ""
#: models.py:91
msgid "open"
msgstr ""
#: models.py:92
msgid "sent"
msgstr ""
#: models.py:93
msgid "due on"
msgstr ""
#: models.py:94
msgid "updated on"
msgstr ""
#: models.py:97
msgid "comments"
msgid "Pro forma"
msgstr ""
#: models.py:98
msgid "number"
msgstr "número"
#: models.py:101
msgid "amend of"
msgstr "rectificación de"
#: models.py:104
msgid "created on"
msgstr "creado en"
#: models.py:105
msgid "closed on"
msgstr "cerrada en"
#: models.py:106
msgid "open"
msgstr "abierta"
#: models.py:107
msgid "sent"
msgstr "enviada"
#: models.py:108
msgid "due on"
msgstr "vencimiento"
#: models.py:109
msgid "updated on"
msgstr "actualizada en"
#: models.py:112
msgid "comments"
msgstr "comentarios"
#: models.py:113
msgid "HTML"
msgstr "HTML"
#: models.py:192
#, python-format
msgid "Type %s is not an amendment."
msgstr ""
#: models.py:285
#: models.py:194
msgid "Amend of related account doesn't match bill account."
msgstr ""
#: models.py:199
#, python-format
msgid "Type %s requires an amend of link."
msgstr ""
#: models.py:378
msgid "bill"
msgstr ""
msgstr "factura"
#: models.py:286 models.py:350 templates/bills/microspective.html:73
#: models.py:379 models.py:444 templates/bills/microspective.html:73
msgid "description"
msgstr ""
msgstr "descripción"
#: models.py:287
#: models.py:380
msgid "rate"
msgstr ""
msgstr "tarifa"
#: models.py:288
#: models.py:381
msgid "quantity"
msgstr ""
msgstr "cantidad"
#: models.py:289
#: models.py:383
msgid "Verbose quantity"
msgstr ""
msgstr "Cantidad"
#: models.py:290 templates/bills/microspective.html:77
#: models.py:384 templates/bills/microspective.html:77
#: templates/bills/microspective.html:111
msgid "subtotal"
msgstr ""
msgstr "subtotal"
#: models.py:291
#: models.py:385
msgid "tax"
msgstr ""
msgstr "impuesto"
#: models.py:292
#: models.py:386
msgid "start"
msgstr ""
msgstr "inicio"
#: models.py:293
#: models.py:387
msgid "end"
msgstr ""
msgstr "fín"
#: models.py:295
#: models.py:389
msgid "Informative link back to the order"
msgstr ""
#: models.py:296
#: models.py:390
msgid "order billed"
msgstr ""
#: models.py:297
#: models.py:391
msgid "order billed until"
msgstr ""
#: models.py:298
#: models.py:392
msgid "created"
msgstr ""
msgstr "creado"
#: models.py:300
#: models.py:394
msgid "amended line"
msgstr ""
msgstr "linea rectificativa"
#: models.py:343
#: models.py:437
msgid "Volume"
msgstr ""
msgstr "Volumen"
#: models.py:344
#: models.py:438
msgid "Compensation"
msgstr ""
msgstr "Compensación"
#: models.py:345
#: models.py:439
msgid "Other"
msgstr ""
msgstr "Otro"
#: models.py:349
#: models.py:443
msgid "bill line"
msgstr ""
msgstr "linea de factura"
#: templates/admin/bills/report.html:39
msgid "Contact"
msgstr "Contacto"
#: templates/admin/bills/report.html:40
#, fuzzy
#| msgid "Due date"
msgid "Close date"
msgstr "Fecha de pago"
#: templates/admin/bills/report.html:41
msgid "Base"
msgstr "Base"
#: templates/admin/bills/report.html:42 templates/bills/microspective.html:111
#: templates/bills/microspective.html:114
msgid "VAT"
msgstr "IVA"
#: templates/bills/microspective-fee.html:107
msgid "Due date"
@ -427,11 +510,6 @@ msgstr "hrs/cant"
msgid "rate/price"
msgstr "tarifa/precio"
#: templates/bills/microspective.html:111
#: templates/bills/microspective.html:114
msgid "VAT"
msgstr "IVA"
#: templates/bills/microspective.html:114
msgid "taxes"
msgstr "impuestos"

View File

@ -90,6 +90,10 @@ class Bill(models.Model):
(AMENDMENTFEE, _("Amendment Fee")),
(PROFORMA, _("Pro forma")),
)
AMEND_MAP = {
INVOICE: AMENDMENTINVOICE,
FEE: AMENDMENTFEE,
}
number = models.CharField(_("number"), max_length=16, unique=True, blank=True)
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
@ -181,6 +185,24 @@ class Bill(models.Model):
return self.EXECUTED
return self.BAD_DEBT
def clean(self):
if self.amend_of_id:
errors = {}
if self.type not in self.AMEND_MAP.values():
errors['amend_of'] = _("Type %s is not an amendment.") % self.get_type_display()
if self.amend_of.account_id != self.account_id:
errors['account'] = _("Amend of related account doesn't match bill account.")
if self.amend_of.is_open:
errors['amend_of'] = _("Related invoice is in open state.")
if self.amend_of.type in self.AMEND_MAP.values():
errors['amend_of'] = _("Related invoice is an amendment.")
if errors:
raise ValidationError(errors)
elif self.type in self.AMEND_MAP.values():
raise ValidationError({
'amend_of': _("Type %s requires an amend of link.") % self.get_type_display()
})
def get_total(self):
if not self.is_open:
return self.total
@ -201,11 +223,7 @@ class Bill(models.Model):
return self.type or self.get_class_type()
def get_amend_type(self):
amend_map = {
self.INVOICE: self.AMENDMENTINVOICE,
self.FEE: self.AMENDMENTFEE,
}
amend_type = amend_map.get(self.type)
amend_type = self.AMEND_MAP.get(self.type)
if amend_type is None:
raise TypeError("%s has no associated amend type." % self.type)
return amend_type
@ -321,10 +339,17 @@ class Bill(models.Model):
subtotals[tax] = (subtotal, round(tax/100*subtotal, 2))
return subtotals
def compute_base(self):
bases = self.lines.annotate(
bases=F('subtotal') + Coalesce(F('sublines__total'), 0)
)
return round(bases.aggregate(Sum('bases'))['bases__sum'] or 0, 2)
def compute_total(self):
totals = self.lines.annotate(
totals=(F('subtotal') + Coalesce(F('sublines__total'), 0)) * (1+F('tax')/100))
return round(totals.aggregate(Sum('totals'))['totals__sum'], 2)
totals=(F('subtotal') + Coalesce(F('sublines__total'), 0)) * (1+F('tax')/100)
)
return round(totals.aggregate(Sum('totals'))['totals__sum'] or 0, 2)
class Invoice(Bill):
@ -363,7 +388,7 @@ class BillLine(models.Model):
subtotal = models.DecimalField(_("subtotal"), max_digits=12, decimal_places=2)
tax = models.DecimalField(_("tax"), max_digits=4, decimal_places=2)
start_on = models.DateField(_("start"))
end_on = models.DateField(_("end"), null=True)
end_on = models.DateField(_("end"), null=True, blank=True)
order = models.ForeignKey(settings.BILLS_ORDER_MODEL, null=True, blank=True,
help_text=_("Informative link back to the order"), on_delete=models.SET_NULL)
order_billed_on = models.DateField(_("order billed"), null=True, blank=True)

View File

@ -0,0 +1,60 @@
{% load i18n utils %}
<html>
<head>
<title>Bill Report</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<style type="text/css">
@page {
size: 11.69in 8.27in;
}
table {
font-family: sans;
font-size: 10px;
max-width: 10in;
}
table tr:nth-child(even) {
background-color: #eee;
}
table tr:nth-child(odd) {
background-color: #fff;
}
table th {
color: white;
background-color: grey;
}
.item.column-base, .item.column-vat, .item.column-total, .item.column-number {
text-align: right;
}
.column-vat-number {
text-align: center;
}
</style>
</head>
<body>
<table>
<tr id="transaction">
<th class="title column-number">{% trans "Number" %}</th>
<th class="title column-vat-number">{% trans "VAT number" %}</th>
<th class="title column-billcontant">{% trans "Contact" %}</th>
<th class="title column-date">{% trans "Close date" %}</th>
<th class="title column-base">{% trans "Base" %}</th>
<th class="title column-vat">{% trans "VAT" %}</th>
<th class="title column-total">{% trans "Total" %}</th>
</tr>
{% for bill in bills %}
<tr>
<td class="item column-number">{{ bill.number }}</td>
<td class="item column-vat-number">{{ bill.buyer.vat }}</td>
<td class="item column-billcontant">{{ bill.buyer.get_name }}</td>
<td class="item column-date">{{ bill.closed_on|date }}</td>
{% with base=bill.compute_base total=bill.compute_total %}
<td class="item column-base">{{ base }} &{{ currency }};</td>
<td class="item column-vat">{{ total|sub:base }} &{{ currency }};</td>
<td class="item column-total">{{ total }} &{{ currency }};</td>
{% endwith %}
</tr>
{% endfor %}
</table>
</body>
</html>

View File

@ -4,7 +4,7 @@ from django import forms
from django.core.exceptions import ValidationError
from .utils import normurlpath
from .validators import validate_domain_protocol
from .validators import validate_domain_protocol, validate_server_name
class WebsiteAdminForm(forms.ModelForm):
@ -15,12 +15,16 @@ class WebsiteAdminForm(forms.ModelForm):
if not domains:
return self.cleaned_data
protocol = self.cleaned_data.get('protocol')
for domain in domains.all():
domains = domains.all()
for domain in domains:
try:
validate_domain_protocol(self.instance, domain, protocol)
except ValidationError as e:
# TODO not sure about this one
self.add_error(None, e)
except ValidationError as err:
self.add_error(None, err)
try:
validate_server_name(domains)
except ValidationError as err:
self.add_error('domains', err)
return self.cleaned_data

View File

@ -28,3 +28,11 @@ def validate_domain_protocol(website, domain, protocol):
raise ValidationError({
'domains': 'A website is already defined for "%s" on protocol %s' % (domain, protocol),
})
def validate_server_name(domains):
if domains:
for domain in domains:
if not domain.name.startswith('*'):
return
raise ValidationError(_("At least one non-wildcard domain should be provided."))

View File

@ -96,3 +96,7 @@ def admin_url(obj):
def isactive(obj):
return getattr(obj, 'is_active', True)
@register.filter
def sub(value, arg):
return value - arg