diff --git a/orchestra/contrib/bills/actions.py b/orchestra/contrib/bills/actions.py index dc5d8618..b8dc3e7d 100644 --- a/orchestra/contrib/bills/actions.py +++ b/orchestra/contrib/bills/actions.py @@ -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( - _('One amendment bill have been generated.') % amend_url, - _('%i amendment bills have been generated.') % (amend_url, len(ids)), - len(ids) + _('One amendment bill have been generated.') % context, + _('%(num)i amendment bills 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) diff --git a/orchestra/contrib/bills/admin.py b/orchestra/contrib/bills/admin.py index 427527e3..4c5c55fb 100644 --- a/orchestra/contrib/bills/admin.py +++ b/orchestra/contrib/bills/admin.py @@ -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( diff --git a/orchestra/contrib/bills/filters.py b/orchestra/contrib/bills/filters.py index aad3d800..8df9ffee 100644 --- a/orchestra/contrib/bills/filters.py +++ b/orchestra/contrib/bills/filters.py @@ -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) diff --git a/orchestra/contrib/bills/locale/ca/LC_MESSAGES/django.mo b/orchestra/contrib/bills/locale/ca/LC_MESSAGES/django.mo index 0e97045d..943758b5 100644 Binary files a/orchestra/contrib/bills/locale/ca/LC_MESSAGES/django.mo and b/orchestra/contrib/bills/locale/ca/LC_MESSAGES/django.mo differ diff --git a/orchestra/contrib/bills/locale/ca/LC_MESSAGES/django.po b/orchestra/contrib/bills/locale/ca/LC_MESSAGES/django.po index 67cc7bf4..eb755540 100644 --- a/orchestra/contrib/bills/locale/ca/LC_MESSAGES/django.po +++ b/orchestra/contrib/bills/locale/ca/LC_MESSAGES/django.po @@ -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 \n" "Language-Team: LANGUAGE \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 "One related transaction has been created" msgstr "S'ha creat una transacció" -#: actions.py:87 +#: actions.py:90 #, python-format msgid "%(num)i related transactions have been created" msgstr "S'han creat les %(num)i següents transaccions" -#: 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.

Please select a " "payment source for the selected bills" @@ -56,138 +56,183 @@ msgstr "" "Una vegada la factura estigui tancada no podrà ser modificada.

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 "One amendment bill have been generated." +msgstr "S'ha creat una transacció" + +#: actions.py:260 +#, python-format +msgid "%(num)i amendment bills have been generated." +msgstr "S'han creat les %(num)i següents transaccions" + +#: 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 %(type)s by bank transfer.
\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" diff --git a/orchestra/contrib/bills/locale/es/LC_MESSAGES/django.mo b/orchestra/contrib/bills/locale/es/LC_MESSAGES/django.mo index 258c2c4c..a9cc33e6 100644 Binary files a/orchestra/contrib/bills/locale/es/LC_MESSAGES/django.mo and b/orchestra/contrib/bills/locale/es/LC_MESSAGES/django.mo differ diff --git a/orchestra/contrib/bills/locale/es/LC_MESSAGES/django.po b/orchestra/contrib/bills/locale/es/LC_MESSAGES/django.po index 406aa281..35811b42 100644 --- a/orchestra/contrib/bills/locale/es/LC_MESSAGES/django.po +++ b/orchestra/contrib/bills/locale/es/LC_MESSAGES/django.po @@ -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 \n" "Language-Team: LANGUAGE \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 "One related transaction has been created" msgstr "Se ha creado una transacción" -#: actions.py:87 +#: actions.py:90 #, python-format msgid "%(num)i related transactions have been created" msgstr "Se han creado %(num)i transacciones" -#: 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.

Please select a " "payment source for the selected bills" @@ -56,138 +56,180 @@ msgstr "" "Una vez cerrada la factura ya no se podrá modificar.

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 "One amendment bill have been generated." +msgstr "Se ha creado una transacción" + +#: actions.py:256 +msgid "%(num)i amendment bills have been generated." +msgstr "Se han creado %(num)i transacciones" + +#: 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" diff --git a/orchestra/contrib/bills/models.py b/orchestra/contrib/bills/models.py index 2a3e31c5..d7d933ea 100644 --- a/orchestra/contrib/bills/models.py +++ b/orchestra/contrib/bills/models.py @@ -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) diff --git a/orchestra/contrib/bills/templates/admin/bills/report.html b/orchestra/contrib/bills/templates/admin/bills/report.html new file mode 100644 index 00000000..7cb63a83 --- /dev/null +++ b/orchestra/contrib/bills/templates/admin/bills/report.html @@ -0,0 +1,60 @@ +{% load i18n utils %} + + + + Bill Report + + + + + + + + + + + + + + +{% for bill in bills %} + + + + + + {% with base=bill.compute_base total=bill.compute_total %} + + + + {% endwith %} + +{% endfor %} +
{% trans "Number" %}{% trans "VAT number" %}{% trans "Contact" %}{% trans "Close date" %}{% trans "Base" %}{% trans "VAT" %}{% trans "Total" %}
{{ bill.number }}{{ bill.buyer.vat }}{{ bill.buyer.get_name }}{{ bill.closed_on|date }}{{ base }} &{{ currency }};{{ total|sub:base }} &{{ currency }};{{ total }} &{{ currency }};
+ + diff --git a/orchestra/contrib/websites/forms.py b/orchestra/contrib/websites/forms.py index 9a3e797a..982649b2 100644 --- a/orchestra/contrib/websites/forms.py +++ b/orchestra/contrib/websites/forms.py @@ -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 diff --git a/orchestra/contrib/websites/validators.py b/orchestra/contrib/websites/validators.py index bfb8af5a..144cc52a 100644 --- a/orchestra/contrib/websites/validators.py +++ b/orchestra/contrib/websites/validators.py @@ -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.")) diff --git a/orchestra/templatetags/utils.py b/orchestra/templatetags/utils.py index 63f261cc..f94bc3f5 100644 --- a/orchestra/templatetags/utils.py +++ b/orchestra/templatetags/utils.py @@ -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