Fixes on bills and implemented domains serializer create() and update()

This commit is contained in:
Marc Aymerich 2015-05-28 09:43:57 +00:00
parent fd8d805b5e
commit 1223ed85e6
13 changed files with 692 additions and 147 deletions

View File

@ -412,5 +412,4 @@ touch /tmp/somefile
# batch zone edditing # batch zone edditing
# inherit registers from parent? # inherit registers from parent?
# Bill metric disk 5 GB: unialber: include not include 5, unialbert recheck period # Disable pagination on membership fees (allways one page)

View File

@ -32,7 +32,7 @@ def download_bills(modeladmin, request, queryset):
response['Content-Disposition'] = 'attachment; filename="orchestra-bills.zip"' response['Content-Disposition'] = 'attachment; filename="orchestra-bills.zip"'
return response return response
bill = queryset.get() bill = queryset.get()
pdf = html_to_pdf(bill.html or bill.render()) pdf = html_to_pdf(bill.html or bill.render(), pagination=bill.has_multiple_pages)
return HttpResponse(pdf, content_type='application/pdf') return HttpResponse(pdf, content_type='application/pdf')
download_bills.verbose_name = _("Download") download_bills.verbose_name = _("Download")
download_bills.url_name = 'download' download_bills.url_name = 'download'
@ -78,9 +78,13 @@ def close_bills(modeladmin, request, queryset):
else: else:
url = reverse('admin:transactions_transaction_changelist') url = reverse('admin:transactions_transaction_changelist')
url += 'id__in=%s' % ','.join(map(str, transactions)) url += 'id__in=%s' % ','.join(map(str, transactions))
context = {
'url': url,
'num': num,
}
message = ungettext( message = ungettext(
_('<a href="%s">One related transaction</a> has been created') % url, _('<a href="%(url)s">One related transaction</a> has been created') % context,
_('<a href="%s">%i related transactions</a> have been created') % (url, num), _('<a href="%(url)s">%(num)i related transactions</a> have been created') % context,
num) num)
messages.success(request, mark_safe(message)) messages.success(request, mark_safe(message))
return return
@ -105,12 +109,22 @@ close_bills.url_name = 'close'
def send_bills(modeladmin, request, queryset): def send_bills(modeladmin, request, queryset):
num = 0
for bill in queryset: for bill in queryset:
if not validate_contact(request, bill): if not validate_contact(request, bill):
return return
for bill in queryset: num += 1
if num == 1:
bill.send() bill.send()
else:
# Batch email
queryset.send()
for bill in queryset:
modeladmin.log_change(request, bill, 'Sent') modeladmin.log_change(request, bill, 'Sent')
messages.success(request, ungetetx(
_("One bill has been sent."),
_("%i bills have been sent.") % num,
num))
send_bills.verbose_name = lambda bill: _("Resend" if getattr(bill, 'is_sent', False) else "Send") send_bills.verbose_name = lambda bill: _("Resend" if getattr(bill, 'is_sent', False) else "Send")
send_bills.url_name = 'send' send_bills.url_name = 'send'
@ -153,7 +167,7 @@ def undo_billing(modeladmin, request, queryset):
else: else:
# First iteration # First iteration
if order.billed_on < line.start_on: if order.billed_on < line.start_on:
messages.error(request, "billed on is smaller than first line start_on.") messages.error(request, "Billed on is smaller than first line start_on.")
return return
prev = line.end_on prev = line.end_on
nlines += 1 nlines += 1
@ -168,6 +182,7 @@ def undo_billing(modeladmin, request, queryset):
for line in lines: for line in lines:
nlines += 1 nlines += 1
line.delete() line.delete()
# TODO update order history undo billing
order.save(update_fields=('billed_until', 'billed_on')) order.save(update_fields=('billed_until', 'billed_on'))
norders += 1 norders += 1
@ -177,29 +192,14 @@ def undo_billing(modeladmin, request, queryset):
}) })
# TODO son't check for account equality def move_lines(modeladmin, request, queryset, action=None):
def move_lines(modeladmin, request, queryset):
# Validate # Validate
account = None
for line in queryset.select_related('bill'):
bill = line.bill
if not bill.is_open:
messages.error(request, _("Can not move lines from a closed bill."))
return
elif not account:
account = bill.account
elif bill.account != account:
messages.error(request, _("Can not move lines from different accounts"))
return
target = request.GET.get('target') target = request.GET.get('target')
if not target: if not target:
# select target # select target
context = {} context = {}
return render(request, 'admin/orchestra/generic_confirmation.html', context) return render(request, 'admin/orchestra/generic_confirmation.html', context)
target = Bill.objects.get(pk=int(pk)) target = Bill.objects.get(pk=int(pk))
if target.account != account:
messages.error(request, _("Target account different than lines account."))
return
if request.POST.get('post') == 'generic_confirmation': if request.POST.get('post') == 'generic_confirmation':
for line in queryset: for line in queryset:
line.bill = target line.bill = target
@ -212,9 +212,4 @@ def move_lines(modeladmin, request, queryset):
def copy_lines(modeladmin, request, queryset): def copy_lines(modeladmin, request, queryset):
# same as move, but changing action behaviour # same as move, but changing action behaviour
pass return move_lines(modeladmin, request, queryset)
def delete_lines(modeladmin, request, queryset):
# Call contrib.admin delete action if all lines in open bill
pass

View File

@ -1,6 +1,6 @@
from django import forms from django import forms
from django.conf.urls import url from django.conf.urls import url
from django.contrib import admin from django.contrib import admin, messages
from django.contrib.admin.utils import unquote from django.contrib.admin.utils import unquote
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db import models from django.db import models
@ -9,6 +9,7 @@ from django.db.models.functions import Coalesce
from django.templatetags.static import static from django.templatetags.static import static
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.shortcuts import redirect
from orchestra.admin import ExtendedModelAdmin from orchestra.admin import ExtendedModelAdmin
from orchestra.admin.utils import admin_date, insertattr, admin_link from orchestra.admin.utils import admin_date, insertattr, admin_link
@ -101,16 +102,22 @@ class ClosedBillLineInline(BillLineInline):
class BillLineAdmin(admin.ModelAdmin): class BillLineAdmin(admin.ModelAdmin):
list_display = ( list_display = (
'description', 'bill_link', 'rate', 'quantity', 'tax', 'subtotal', 'display_sublinetotal', 'description', 'bill_link', 'display_is_open', 'account_link', 'rate', 'quantity', 'tax',
'display_total' 'subtotal', 'display_sublinetotal', 'display_total'
) )
actions = (actions.undo_billing, actions.move_lines, actions.copy_lines,) actions = (actions.undo_billing, actions.move_lines, actions.copy_lines,)
list_filter = ('tax', ('bill', admin.RelatedOnlyFieldListFilter)) list_filter = ('tax', ('bill', admin.RelatedOnlyFieldListFilter), 'bill__is_open')
list_select_related = ('bill',) list_select_related = ('bill',)
search_fields = ('description', 'bill__number') search_fields = ('description', 'bill__number')
account_link = admin_link('bill__account')
bill_link = admin_link('bill') bill_link = admin_link('bill')
def display_is_open(self, instance):
return instance.bill.is_open
display_is_open.short_description = _("Is open")
display_is_open.boolean = True
def display_sublinetotal(self, instance): def display_sublinetotal(self, instance):
return instance.subline_total or '' return instance.subline_total or ''
display_sublinetotal.short_description = _("Subline") display_sublinetotal.short_description = _("Subline")
@ -140,18 +147,26 @@ class BillLineManagerAdmin(BillLineAdmin):
return qset return qset
def changelist_view(self, request, extra_context=None): def changelist_view(self, request, extra_context=None):
GET = request.GET.copy() GET_copy = request.GET.copy()
bill_ids = GET.pop('ids', None) bill_ids = GET_copy.pop('ids', None)
if bill_ids: if bill_ids:
request.GET = GET bill_ids = bill_ids[0]
bill_ids = list(map(int, bill_ids)) request.GET = GET_copy
bill_ids = list(map(int, bill_ids.split(',')))
else:
messages.error(request, _("No bills selected."))
return redirect('..')
self.bill_ids = bill_ids self.bill_ids = bill_ids
if bill_ids and len(bill_ids) == 1: if len(bill_ids) == 1:
bill_url = reverse('admin:bills_bill_change', args=(bill_ids[0],)) bill_url = reverse('admin:bills_bill_change', args=(bill_ids[0],))
bill = Bill.objects.get(pk=bill_ids[0]) bill = Bill.objects.get(pk=bill_ids[0])
bill_link = '<a href="%s">%s</a>' % (bill_url, bill.number) bill_link = '<a href="%s">%s</a>' % (bill_url, bill.number)
title = mark_safe(_("Manage %s bill lines.") % bill_link) title = mark_safe(_("Manage %s bill lines.") % bill_link)
if not bill.is_open:
messages.warning(request, _("Bill not in open state."))
else: else:
if Bill.objects.filter(id__in=bill_ids, is_open=False).exists():
messages.warning(request, _("Not all bills are in open state."))
title = _("Manage bill lines of multiple bills.") title = _("Manage bill lines of multiple bills.")
context = { context = {
'title': title, 'title': title,
@ -177,13 +192,15 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
'fields': ('html',), 'fields': ('html',),
}), }),
) )
list_prefetch_related = ('transactions',)
search_fields = ('number', 'account__username', 'comments')
change_view_actions = [ change_view_actions = [
actions.manage_lines, actions.view_bill, actions.download_bills, actions.send_bills, actions.manage_lines, actions.view_bill, actions.download_bills, actions.send_bills,
actions.close_bills actions.close_bills
] ]
list_prefetch_related = ('transactions',) actions = [
search_fields = ('number', 'account__username', 'comments') actions.manage_lines, actions.download_bills, actions.close_bills, actions.send_bills
actions = [actions.download_bills, actions.close_bills, actions.send_bills] ]
change_readonly_fields = ('account_link', 'type', 'is_open') change_readonly_fields = ('account_link', 'type', 'is_open')
readonly_fields = ('number', 'display_total', 'is_sent', 'display_payment_state') readonly_fields = ('number', 'display_total', 'is_sent', 'display_payment_state')
inlines = [BillLineInline, ClosedBillLineInline] inlines = [BillLineInline, ClosedBillLineInline]

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-05-27 13:28+0000\n" "POT-Creation-Date: 2015-05-28 09:23+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -34,97 +34,114 @@ msgstr ""
msgid "Selected bills have been closed" msgid "Selected bills have been closed"
msgstr "" msgstr ""
#: actions.py:82 #: actions.py:86
#, python-format #, python-format
msgid "<a href=\"%s\">One related transaction</a> has been created" msgid "<a href=\"%(url)s\">One related transaction</a> has been created"
msgstr "" msgstr ""
#: actions.py:83 #: actions.py:87
#, python-format #, python-format
msgid "<a href=\"%s\">%i related transactions</a> have been created" msgid "<a href=\"%(url)s\">%(num)i related transactions</a> have been created"
msgstr "" msgstr ""
#: actions.py:89 #: actions.py:93
msgid "Are you sure about closing the following bills?" msgid "Are you sure about closing the following bills?"
msgstr "" msgstr ""
#: actions.py:90 #: actions.py:94
msgid "" msgid ""
"Once a bill is closed it can not be further modified.</p><p>Please select a " "Once a bill is closed it can not be further modified.</p><p>Please select a "
"payment source for the selected bills" "payment source for the selected bills"
msgstr "" msgstr ""
#: actions.py:103 #: actions.py:107
msgid "Close" msgid "Close"
msgstr "" msgstr ""
#: actions.py:114 #: actions.py:125
msgid "One bill has been sent."
msgstr ""
#: actions.py:126
#, python-format
msgid "%i bills have been sent."
msgstr ""
#: actions.py:128
msgid "Resend" msgid "Resend"
msgstr "" msgstr ""
#: actions.py:174 #: actions.py:189
#, python-format #, python-format
msgid "%(norders)s orders and %(nlines)s lines undoed." msgid "%(norders)s orders and %(nlines)s lines undoed."
msgstr "" msgstr ""
#: actions.py:187
msgid "Can not move lines from a closed bill."
msgstr ""
#: actions.py:192
msgid "Can not move lines from different accounts"
msgstr ""
#: actions.py:201
msgid "Target account different than lines account."
msgstr ""
#: actions.py:208 #: actions.py:208
msgid "Lines moved" msgid "Lines moved"
msgstr "" msgstr ""
#: admin.py:48 admin.py:92 admin.py:121 forms.py:11 #: admin.py:49 admin.py:93 admin.py:128 forms.py:11
msgid "Total" msgid "Total"
msgstr "" msgstr ""
#: admin.py:79 #: admin.py:80
msgid "Description" msgid "Description"
msgstr "Descripció" msgstr "Descripció"
#: admin.py:87 #: admin.py:88
msgid "Subtotal" msgid "Subtotal"
msgstr "" msgstr ""
#: admin.py:116 #: admin.py:118
msgid "Is open"
msgstr ""
#: admin.py:123
msgid "Subline" msgid "Subline"
msgstr "" msgstr ""
#: admin.py:153 #: admin.py:157
msgid "No bills selected."
msgstr ""
#: admin.py:164
#, python-format #, python-format
msgid "Manage %s bill lines." msgid "Manage %s bill lines."
msgstr "" msgstr ""
#: admin.py:155 #: admin.py:166
msgid "Bill not in open state."
msgstr ""
#: admin.py:169
msgid "Not all bills are in open state."
msgstr ""
#: admin.py:170
msgid "Manage bill lines of multiple bills." msgid "Manage bill lines of multiple bills."
msgstr "" msgstr ""
#: admin.py:175 #: admin.py:190
msgid "Raw" msgid "Raw"
msgstr "" msgstr ""
#: admin.py:196 #: admin.py:208
msgid "Created"
msgstr ""
#: admin.py:213
msgid "lines" msgid "lines"
msgstr "" msgstr ""
#: admin.py:201 templates/bills/microspective.html:118 #: admin.py:218 templates/bills/microspective.html:118
msgid "total" msgid "total"
msgstr "" msgstr ""
#: admin.py:209 models.py:88 models.py:340 #: admin.py:226 models.py:88 models.py:352
msgid "type" msgid "type"
msgstr "tipus" msgstr "tipus"
#: admin.py:226 #: admin.py:243
msgid "Payment" msgid "Payment"
msgstr "Pagament" msgstr "Pagament"
@ -290,82 +307,78 @@ msgstr "comentaris"
msgid "HTML" msgid "HTML"
msgstr "" msgstr ""
#: models.py:280 #: models.py:285
msgid "bill" msgid "bill"
msgstr "" msgstr ""
#: models.py:281 models.py:338 templates/bills/microspective.html:73 #: models.py:286 models.py:350 templates/bills/microspective.html:73
msgid "description" msgid "description"
msgstr "descripció" msgstr "descripció"
#: models.py:282 #: models.py:287
msgid "rate" msgid "rate"
msgstr "tarifa" msgstr "tarifa"
#: models.py:283 #: models.py:288
msgid "quantity" msgid "quantity"
msgstr "quantitat" msgstr "quantitat"
#: models.py:284 #: models.py:289
#, fuzzy #, fuzzy
#| msgid "quantity" #| msgid "quantity"
msgid "Verbose quantity" msgid "Verbose quantity"
msgstr "quantitat" msgstr "quantitat"
#: models.py:285 templates/bills/microspective.html:77 #: models.py:290 templates/bills/microspective.html:77
#: templates/bills/microspective.html:111 #: templates/bills/microspective.html:111
msgid "subtotal" msgid "subtotal"
msgstr "" msgstr ""
#: models.py:286 #: models.py:291
msgid "tax" msgid "tax"
msgstr "impostos" msgstr "impostos"
#: models.py:287 #: models.py:292
msgid "start" msgid "start"
msgstr "" msgstr ""
#: models.py:288 #: models.py:293
msgid "end" msgid "end"
msgstr "" msgstr ""
#: models.py:290 #: models.py:295
msgid "Informative link back to the order" msgid "Informative link back to the order"
msgstr "" msgstr ""
#: models.py:291 #: models.py:296
msgid "order billed" msgid "order billed"
msgstr "" msgstr ""
#: models.py:292 #: models.py:297
msgid "order billed until" msgid "order billed until"
msgstr "" msgstr ""
#: models.py:293 #: models.py:298
msgid "created" msgid "created"
msgstr "" msgstr ""
#: models.py:295 #: models.py:300
msgid "amended line" msgid "amended line"
msgstr "" msgstr ""
#: models.py:316 #: models.py:343
msgid "{ini} to {end}"
msgstr ""
#: models.py:331
msgid "Volume" msgid "Volume"
msgstr "" msgstr ""
#: models.py:332 #: models.py:344
msgid "Compensation" msgid "Compensation"
msgstr "" msgstr ""
#: models.py:333 #: models.py:345
msgid "Other" msgid "Other"
msgstr "" msgstr ""
#: models.py:337 #: models.py:349
msgid "bill line" msgid "bill line"
msgstr "" msgstr ""
@ -378,20 +391,15 @@ msgstr ""
msgid "On %(bank_account)s" msgid "On %(bank_account)s"
msgstr "" msgstr ""
#: templates/bills/microspective-fee.html:113 #: templates/bills/microspective-fee.html:114
#, python-format #, python-format
msgid "From %(period)s" msgid "From %(ini)s to %(end)s"
msgstr "" msgstr ""
#: templates/bills/microspective-fee.html:118 #: templates/bills/microspective-fee.html:121
msgid "" msgid ""
"\n" "\n"
"<strong>Con vuestras cuotas</strong>, ademas de obtener conexion y multitud " "<strong>With your membership</strong> you are supporting ...\n"
"de servicios, estais<br>\n"
" Con vuestras cuotas, ademas de obtener conexion y multitud de servicios,"
"<br>\n"
" Con vuestras cuotas, ademas de obtener conexion <br>\n"
" Con vuestras cuotas, ademas de obtener <br>\n"
msgstr "" msgstr ""
#: templates/bills/microspective.html:49 #: templates/bills/microspective.html:49
@ -403,8 +411,9 @@ msgid "TOTAL"
msgstr "TOTAL" msgstr "TOTAL"
#: templates/bills/microspective.html:57 #: templates/bills/microspective.html:57
#, python-format #, fuzzy, python-format
msgid "%(bill_type|upper)s DATE " #| msgid "%(bill_type|upper)s DATE "
msgid "%(bill_type)s DATE"
msgstr "DATA %(bill_type|upper)s" msgstr "DATA %(bill_type|upper)s"
#: templates/bills/microspective.html:74 #: templates/bills/microspective.html:74
@ -437,10 +446,18 @@ msgid "PAYMENT"
msgstr "PAGAMENT" msgstr "PAGAMENT"
#: templates/bills/microspective.html:140 #: templates/bills/microspective.html:140
#, python-format #, fuzzy, python-format
#| msgid ""
#| "\n"
#| " You can pay our <i>%(type)s</i> by bank transfer. "
#| "<br>\n"
#| " Please make sure to state your name and the <i>"
#| "%(type)s</i> number.\n"
#| " Our bank account number is <br>\n"
#| " "
msgid "" msgid ""
"\n" "\n"
" You can pay our <i>%(type)s</i> by bank transfer. <br>\n" " You can pay our <i>%(type)s</i> by bank transfer.<br>\n"
" Please make sure to state your name and the <i>%(type)s</" " Please make sure to state your name and the <i>%(type)s</"
"i> number.\n" "i> number.\n"
" Our bank account number is <br>\n" " Our bank account number is <br>\n"
@ -459,8 +476,8 @@ msgstr "PREGUNTES"
msgid "" msgid ""
"\n" "\n"
" If you have any question about your <i>%(type)s</i>, please\n" " If you have any question about your <i>%(type)s</i>, please\n"
" feel free to contact us at your convinience. We will reply as " " feel free to write us at %(email)s. We will reply as soon as we "
"soon as we get\n" "get\n"
" your message.\n" " your message.\n"
" " " "
msgstr "" msgstr ""

View File

@ -0,0 +1,469 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-05-28 09:23+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"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: actions.py:37
msgid "Download"
msgstr ""
#: actions.py:47
msgid "View"
msgstr ""
#: actions.py:55
msgid "Selected bills should be in open state"
msgstr ""
#: actions.py:73
msgid "Selected bills have been closed"
msgstr ""
#: actions.py:86
#, python-format
msgid "<a href=\"%(url)s\">One related transaction</a> has been created"
msgstr ""
#: actions.py:87
#, python-format
msgid "<a href=\"%(url)s\">%(num)i related transactions</a> have been created"
msgstr ""
#: actions.py:93
msgid "Are you sure about closing the following bills?"
msgstr ""
#: actions.py:94
msgid ""
"Once a bill is closed it can not be further modified.</p><p>Please select a "
"payment source for the selected bills"
msgstr ""
#: actions.py:107
msgid "Close"
msgstr ""
#: actions.py:125
msgid "One bill has been sent."
msgstr ""
#: actions.py:126
#, python-format
msgid "%i bills have been sent."
msgstr ""
#: actions.py:128
msgid "Resend"
msgstr ""
#: actions.py:189
#, python-format
msgid "%(norders)s orders and %(nlines)s lines undoed."
msgstr ""
#: actions.py:208
msgid "Lines moved"
msgstr ""
#: admin.py:49 admin.py:93 admin.py:128 forms.py:11
msgid "Total"
msgstr ""
#: admin.py:80
msgid "Description"
msgstr ""
#: admin.py:88
msgid "Subtotal"
msgstr ""
#: admin.py:118
msgid "Is open"
msgstr ""
#: admin.py:123
msgid "Subline"
msgstr ""
#: admin.py:157
msgid "No bills selected."
msgstr ""
#: admin.py:164
#, python-format
msgid "Manage %s bill lines."
msgstr ""
#: admin.py:166
msgid "Bill not in open state."
msgstr ""
#: admin.py:169
msgid "Not all bills are in open state."
msgstr ""
#: admin.py:170
msgid "Manage bill lines of multiple bills."
msgstr ""
#: admin.py:190
msgid "Raw"
msgstr ""
#: admin.py:208
msgid "Created"
msgstr ""
#: admin.py:213
msgid "lines"
msgstr ""
#: admin.py:218 templates/bills/microspective.html:118
msgid "total"
msgstr ""
#: admin.py:226 models.py:88 models.py:352
msgid "type"
msgstr ""
#: admin.py:243
msgid "Payment"
msgstr ""
#: filters.py:17
msgid "All"
msgstr ""
#: filters.py:18 models.py:78
msgid "Invoice"
msgstr ""
#: filters.py:19 models.py:79
msgid "Amendment invoice"
msgstr ""
#: filters.py:20 models.py:80
msgid "Fee"
msgstr ""
#: filters.py:21
msgid "Amendment fee"
msgstr ""
#: filters.py:22
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
msgid "has bill contact"
msgstr ""
#: forms.py:9
msgid "Number"
msgstr ""
#: forms.py:10
msgid "Account"
msgstr ""
#: forms.py:12
msgid "Type"
msgstr ""
#: forms.py:13
msgid "Source"
msgstr ""
#: helpers.py:10
msgid ""
"{relation} account \"{account}\" does not have a declared invoice contact. "
"You should <a href=\"{url}#invoicecontact-group\">provide one</a>"
msgstr ""
#: helpers.py:17
msgid "Related"
msgstr ""
#: helpers.py:24
msgid "Main"
msgstr ""
#: models.py:23 models.py:86
msgid "account"
msgstr ""
#: models.py:25
msgid "name"
msgstr ""
#: models.py:26
msgid "Account full name will be used when left blank."
msgstr ""
#: models.py:27
msgid "address"
msgstr ""
#: models.py:28
msgid "city"
msgstr ""
#: models.py:30
msgid "zip code"
msgstr ""
#: models.py:31
msgid "Enter a valid zipcode."
msgstr ""
#: models.py:32
msgid "country"
msgstr ""
#: models.py:35
msgid "VAT number"
msgstr ""
#: models.py:67
msgid "Paid"
msgstr ""
#: models.py:68
msgid "Pending"
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"
msgstr ""
#: models.py:90
msgid "closed on"
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"
msgstr ""
#: models.py:98
msgid "HTML"
msgstr ""
#: models.py:285
msgid "bill"
msgstr ""
#: models.py:286 models.py:350 templates/bills/microspective.html:73
msgid "description"
msgstr ""
#: models.py:287
msgid "rate"
msgstr ""
#: models.py:288
msgid "quantity"
msgstr ""
#: models.py:289
msgid "Verbose quantity"
msgstr ""
#: models.py:290 templates/bills/microspective.html:77
#: templates/bills/microspective.html:111
msgid "subtotal"
msgstr ""
#: models.py:291
msgid "tax"
msgstr ""
#: models.py:292
msgid "start"
msgstr ""
#: models.py:293
msgid "end"
msgstr ""
#: models.py:295
msgid "Informative link back to the order"
msgstr ""
#: models.py:296
msgid "order billed"
msgstr ""
#: models.py:297
msgid "order billed until"
msgstr ""
#: models.py:298
msgid "created"
msgstr ""
#: models.py:300
msgid "amended line"
msgstr ""
#: models.py:343
msgid "Volume"
msgstr ""
#: models.py:344
msgid "Compensation"
msgstr ""
#: models.py:345
msgid "Other"
msgstr ""
#: models.py:349
msgid "bill line"
msgstr ""
#: templates/bills/microspective-fee.html:107
msgid "Due date"
msgstr ""
#: templates/bills/microspective-fee.html:108
#, python-format
msgid "On %(bank_account)s"
msgstr ""
#: templates/bills/microspective-fee.html:114
#, python-format
msgid "From %(ini)s to %(end)s"
msgstr ""
#: templates/bills/microspective-fee.html:121
msgid ""
"\n"
"<strong>With your membership</strong> you are supporting ...\n"
msgstr ""
#: templates/bills/microspective.html:49
msgid "DUE DATE"
msgstr ""
#: templates/bills/microspective.html:53
msgid "TOTAL"
msgstr ""
#: templates/bills/microspective.html:57
#, python-format
msgid "%(bill_type)s DATE"
msgstr ""
#: templates/bills/microspective.html:74
msgid "period"
msgstr ""
#: templates/bills/microspective.html:75
msgid "hrs/qty"
msgstr ""
#: templates/bills/microspective.html:76
msgid "rate/price"
msgstr ""
#: templates/bills/microspective.html:111
#: templates/bills/microspective.html:114
msgid "VAT"
msgstr ""
#: templates/bills/microspective.html:114
msgid "taxes"
msgstr ""
#: templates/bills/microspective.html:130
msgid "COMMENTS"
msgstr ""
#: templates/bills/microspective.html:136
msgid "PAYMENT"
msgstr ""
#: templates/bills/microspective.html:140
#, python-format
msgid ""
"\n"
" You can pay our <i>%(type)s</i> by bank transfer.<br>\n"
" Please make sure to state your name and the <i>%(type)s</"
"i> number.\n"
" Our bank account number is <br>\n"
" "
msgstr ""
#: templates/bills/microspective.html:149
msgid "QUESTIONS"
msgstr ""
#: templates/bills/microspective.html:150
#, python-format
msgid ""
"\n"
" If you have any question about your <i>%(type)s</i>, please\n"
" feel free to write us at %(email)s. We will reply as soon as we "
"get\n"
" your message.\n"
" "
msgstr ""

View File

@ -124,6 +124,10 @@ class Bill(models.Model):
return self.PENDING return self.PENDING
return self.BAD_DEBT return self.BAD_DEBT
@property
def has_multiple_pages(self):
return self.type != self.FEE
def get_payment_state_display(self): def get_payment_state_display(self):
value = self.payment_state value = self.payment_state
return force_text(dict(self.PAYMENT_STATES).get(value, value)) return force_text(dict(self.PAYMENT_STATES).get(value, value))
@ -186,6 +190,7 @@ class Bill(models.Model):
def send(self): def send(self):
html = self.html or self.render() html = self.html or self.render()
pdf = html_to_pdf(html, pagination=self.has_multiple_pages)
self.account.send_email( self.account.send_email(
template=settings.BILLS_EMAIL_NOTIFICATION_TEMPLATE, template=settings.BILLS_EMAIL_NOTIFICATION_TEMPLATE,
context={ context={
@ -195,7 +200,7 @@ class Bill(models.Model):
email_from=settings.BILLS_SELLER_EMAIL, email_from=settings.BILLS_SELLER_EMAIL,
contacts=(Contact.BILLING,), contacts=(Contact.BILLING,),
attachments=[ attachments=[
('%s.pdf' % self.number, html_to_pdf(html), 'application/pdf') ('%s.pdf' % self.number, pdf, 'application/pdf')
] ]
) )
self.is_sent = True self.is_sent = True
@ -312,12 +317,10 @@ class BillLine(models.Model):
if self.start_on.day != 1 or self.end_on.day != 1: if self.start_on.day != 1 or self.end_on.day != 1:
date_format = "N j, 'y" date_format = "N j, 'y"
end = date(self.end_on, date_format) end = date(self.end_on, date_format)
# .strftime(date_format)
else: else:
end = date((self.end_on - datetime.timedelta(days=1)), date_format) end = date((self.end_on - datetime.timedelta(days=1)), date_format)
# ).strftime(date_format) ini = date(self.start_on, date_format).capitalize()
ini = date(self.start_on, date_format) end = end.capitalize()
#.strftime(date_format)
if not self.end_on: if not self.end_on:
return ini return ini
if ini == end: if ini == end:

View File

@ -99,30 +99,31 @@ hr {
<div id="number" class="column-1"> <div id="number" class="column-1">
<span id="number-title">{% filter title %}{% trans bill.get_type_display %}{% endfilter %}</span><br> <span id="number-title">{% filter title %}{% trans bill.get_type_display %}{% endfilter %}</span><br>
<span id="number-value">{{ bill.number }}</span><br> <span id="number-value">{{ bill.number }}</span><br>
<span id="number-date">{{ bill.closed_on | default:now | date | capfirst }}</span><br> <span id="number-date">{{ bill.closed_on | default:now | date:"F j, Y" | capfirst }}</span><br>
</div> </div>
<div id="amount" class="column-2"> <div id="amount" class="column-2">
<span id="amount-value">{{ bill.get_total }} &{{ currency.lower }};</span><br> <span id="amount-value">{{ bill.get_total }} &{{ currency.lower }};</span><br>
<span id="amount-note">{% trans "Due date" %} {{ payment.due_date| default:default_due_date | date }}<br> <span id="amount-note">{% trans "Due date" %} {{ payment.due_date| default:default_due_date | date:"F j, Y" }}<br>
{% if not payment.message %}{% blocktrans with bank_account=seller_info.bank_account %}On {{ bank_account }}{% endblocktrans %}{% endif %}<br> {% if not payment.message %}{% blocktrans with bank_account=seller_info.bank_account %}On {{ bank_account }}{% endblocktrans %}{% endif %}<br>
</span> </span>
</div> </div>
<div id="date" class="column-2"> <div id="date" class="column-2">
{% blocktrans with period=bill.lines.get.get_verbose_period %}From {{ period }}{% endblocktrans %} {% with line=bill.lines.get %}
{% blocktrans with ini=line.start_on|date:"F j, Y" end=line.end_on|date:"F j, Y" %}From {{ ini }} to {{ end }}{% endblocktrans %}
{% endwith %}
</div> </div>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div id="text"> <div id="text">
{% blocktrans %} {% blocktrans %}
<strong>Con vuestras cuotas</strong>, ademas de obtener conexion y multitud de servicios, estais<br> <strong>With your membership</strong> you are supporting ...
Con vuestras cuotas, ademas de obtener conexion y multitud de servicios,<br>
Con vuestras cuotas, ademas de obtener conexion <br>
Con vuestras cuotas, ademas de obtener <br>
{% endblocktrans %} {% endblocktrans %}
</div> </div>
{% endblock %} {% endblock %}
{% block footer %} {% block footer %}
<hr> <hr>
{{ block.super }} {{ block.super }}

View File

@ -175,12 +175,12 @@ a:hover {
} }
#lines .column-description { #lines .column-description {
width: 42%; width: 40%;
text-align: left; text-align: left;
} }
#lines .column-period { #lines .column-period {
width: 20%; width: 22%;
} }
#lines .column-quantity { #lines .column-quantity {

View File

@ -47,15 +47,15 @@
<hr> <hr>
<div id="due-date"> <div id="due-date">
<span class="title">{% trans "DUE DATE" %}</span><br> <span class="title">{% trans "DUE DATE" %}</span><br>
<psan class="value">{{ bill.due_on | default:default_due_date | date }}</span> <psan class="value">{{ bill.due_on | default:default_due_date | date | capfirst }}</span>
</div> </div>
<div id="total"> <div id="total">
<span class="title">{% trans "TOTAL" %}</span><br> <span class="title">{% trans "TOTAL" %}</span><br>
<psan class="value">{{ bill.get_total }} &{{ currency.lower }};</span> <psan class="value">{{ bill.get_total }} &{{ currency.lower }};</span>
</div> </div>
<div id="bill-date"> <div id="bill-date">
<span class="title">{% blocktrans with bill_type=bill.get_type_display.upper %}{{ bill_type }} DATE {% endblocktrans %}</span><br> <span class="title">{% blocktrans with bill_type=bill.get_type_display.upper %}{{ bill_type }} DATE{% endblocktrans %}</span><br>
<psan class="value">{{ bill.closed_on | default:now | date }}</span> <psan class="value">{{ bill.closed_on | default:now | date | capfirst }}</span>
</div> </div>
</div> </div>
<div id="buyer-details"> <div id="buyer-details">
@ -77,9 +77,9 @@
<span class="title column-subtotal">{% trans "subtotal" %}</span> <span class="title column-subtotal">{% trans "subtotal" %}</span>
<br> <br>
{% for line in lines %} {% for line in lines %}
{% with sublines=line.sublines.all description=line.description|slice:"40:" %} {% with sublines=line.sublines.all description=line.description|slice:"38:" %}
<span class="{% if not sublines and not description %}last {% endif %}column-id">{% if not line.order_id %}L{% endif %}{{ line.order_id }}</span> <span class="{% if not sublines and not description %}last {% endif %}column-id">{% if not line.order_id %}L{% endif %}{{ line.order_id }}</span>
<span class="{% if not sublines and not description %}last {% endif %}column-description">{{ line.description|slice:":40" }}</span> <span class="{% if not sublines and not description %}last {% endif %}column-description">{{ line.description|slice:":38" }}</span>
<span class="{% if not sublines and not description %}last {% endif %}column-period">{{ line.get_verbose_period }}</span> <span class="{% if not sublines and not description %}last {% endif %}column-period">{{ line.get_verbose_period }}</span>
<span class="{% if not sublines and not description %}last {% endif %}column-quantity">{{ line.get_verbose_quantity|default:"&nbsp;"|safe }}</span> <span class="{% if not sublines and not description %}last {% endif %}column-quantity">{{ line.get_verbose_quantity|default:"&nbsp;"|safe }}</span>
<span class="{% if not sublines and not description %}last {% endif %}column-rate">{% if line.rate %}{{ line.rate }} &{{ currency.lower }};{% else %}&nbsp;{% endif %}</span> <span class="{% if not sublines and not description %}last {% endif %}column-rate">{% if line.rate %}{{ line.rate }} &{{ currency.lower }};{% else %}&nbsp;{% endif %}</span>
@ -87,7 +87,7 @@
<br> <br>
{% if description %} {% if description %}
<span class="{% if not sublines %}last {% endif %}subline column-id">&nbsp;</span> <span class="{% if not sublines %}last {% endif %}subline column-id">&nbsp;</span>
<span class="{% if not sublines %}last {% endif %}subline column-description">{{ description|truncatechars:41 }}</span> <span class="{% if not sublines %}last {% endif %}subline column-description">{{ description|truncatechars:39 }}</span>
<span class="{% if not sublines %}last {% endif %}subline column-period">&nbsp;</span> <span class="{% if not sublines %}last {% endif %}subline column-period">&nbsp;</span>
<span class="{% if not sublines %}last {% endif %}subline column-quantity">&nbsp;</span> <span class="{% if not sublines %}last {% endif %}subline column-quantity">&nbsp;</span>
<span class="{% if not sublines %}last {% endif %}subline column-rate">&nbsp;</span> <span class="{% if not sublines %}last {% endif %}subline column-rate">&nbsp;</span>
@ -95,7 +95,7 @@
{% endif %} {% endif %}
{% for subline in sublines %} {% for subline in sublines %}
<span class="{% if forloop.last %}last {% endif %}subline column-id">&nbsp;</span> <span class="{% if forloop.last %}last {% endif %}subline column-id">&nbsp;</span>
<span class="{% if forloop.last %}last {% endif %}subline column-description">{{ subline.description|truncatechars:41 }}</span> <span class="{% if forloop.last %}last {% endif %}subline column-description">{{ subline.description|truncatechars:39 }}</span>
<span class="{% if forloop.last %}last {% endif %}subline column-period">&nbsp;</span> <span class="{% if forloop.last %}last {% endif %}subline column-period">&nbsp;</span>
<span class="{% if forloop.last %}last {% endif %}subline column-quantity">&nbsp;</span> <span class="{% if forloop.last %}last {% endif %}subline column-quantity">&nbsp;</span>
<span class="{% if forloop.last %}last {% endif %}subline column-rate">&nbsp;</span> <span class="{% if forloop.last %}last {% endif %}subline column-rate">&nbsp;</span>
@ -138,7 +138,7 @@
{{ payment.message | safe }} {{ payment.message | safe }}
{% else %} {% else %}
{% blocktrans with type=bill.get_type_display.lower %} {% blocktrans with type=bill.get_type_display.lower %}
You can pay our <i>{{ type }}</i> by bank transfer. <br> You can pay our <i>{{ type }}</i> by bank transfer.<br>
Please make sure to state your name and the <i>{{ type }}</i> number. Please make sure to state your name and the <i>{{ type }}</i> number.
Our bank account number is <br> Our bank account number is <br>
{% endblocktrans %} {% endblocktrans %}
@ -147,9 +147,9 @@
</div> </div>
<div id="questions"> <div id="questions">
<span class="title">{% trans "QUESTIONS" %}</span> <span class="title">{% trans "QUESTIONS" %}</span>
{% blocktrans with type=bill.get_type_display.lower %} {% blocktrans with type=bill.get_type_display.lower email=seller_info.email %}
If you have any question about your <i>{{ type }}</i>, please If you have any question about your <i>{{ type }}</i>, please
feel free to contact us at your convinience. We will reply as soon as we get feel free to write us at {{ email }}. We will reply as soon as we get
your message. your message.
{% endblocktrans %} {% endblocktrans %}
</div> </div>

View File

@ -21,7 +21,7 @@ class RecordSerializer(serializers.ModelSerializer):
class DomainSerializer(AccountSerializerMixin, HyperlinkedModelSerializer): class DomainSerializer(AccountSerializerMixin, HyperlinkedModelSerializer):
""" Validates if this zone generates a correct zone file """ """ Validates if this zone generates a correct zone file """
records = RecordSerializer(required=False, many=True) #allow_add_remove=True) records = RecordSerializer(required=False, many=True)
class Meta: class Meta:
model = Domain model = Domain
@ -44,3 +44,35 @@ class DomainSerializer(AccountSerializerMixin, HyperlinkedModelSerializer):
domain = domain_for_validation(self.instance, records) domain = domain_for_validation(self.instance, records)
validators.validate_zone(domain.render_zone()) validators.validate_zone(domain.render_zone())
return data return data
def create(self, validated_data):
records = validated_data.pop('records')
domain = super(DomainSerializer, self).create(validated_data)
for record in records:
domain.records.create(type=record['type'], value=record['value'])
return domain
def update(self, validated_data):
precords = validated_data.pop('records')
domain = super(DomainSerializer, self).update(validated_data)
to_delete = []
for erecord in domain.records.all():
match = False
for ix, precord in enumerate(precords):
if erecord.type == precord['type'] and erecord.value == precord['value']:
match = True
break
if match:
precords.remove(ix)
else:
to_delete.append(erecord)
for precord in precords:
try:
recycled = to_delete.pop()
except IndexError:
domain.records.create(type=precord['type'], value=precord['value'])
else:
recycled.type = precord['type']
recycled.value = precord['value']
recycled.save()
return domain

View File

@ -92,7 +92,7 @@ class BillSelectedOrders(object):
url = change_url(bills[0]) url = change_url(bills[0])
else: else:
url = reverse('admin:bills_bill_changelist') url = reverse('admin:bills_bill_changelist')
ids = ','.join(map(str, bills)) ids = ','.join([str(b.id) for b in bills])
url += '?id__in=%s' % ids url += '?id__in=%s' % ids
msg = ungettext( msg = ungettext(
'<a href="{url}">One bill</a> has been created.', '<a href="{url}">One bill</a> has been created.',

View File

@ -245,6 +245,10 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
})) }))
def generate_line(self, order, price, *dates, metric=1, discounts=None, computed=False): def generate_line(self, order, price, *dates, metric=1, discounts=None, computed=False):
"""
discounts: already applied discounts on price
computed: price = price*size already performed
"""
if len(dates) == 2: if len(dates) == 2:
ini, end = dates ini, end = dates
elif len(dates) == 1: elif len(dates) == 1:
@ -377,7 +381,8 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
size = self.get_price_size(order.new_billed_until, new_end) size = self.get_price_size(order.new_billed_until, new_end)
price += price*size price += price*size
order.new_billed_until = new_end order.new_billed_until = new_end
line = self.generate_line(order, price, ini, new_end or end, discounts=discounts, computed=True) line = self.generate_line(
order, price, ini, new_end or end, discounts=discounts, computed=True)
lines.append(line) lines.append(line)
return lines return lines

View File

@ -3,17 +3,24 @@ import textwrap
from orchestra.utils.sys import run from orchestra.utils.sys import run
def html_to_pdf(html): def html_to_pdf(html, pagination=False):
""" converts HTL to PDF using wkhtmltopdf """ """ converts HTL to PDF using wkhtmltopdf """
return run(textwrap.dedent("""\ print(pagination)
PATH=$PATH:/usr/local/bin/ context = {
xvfb-run -a -s "-screen 0 2480x3508x16" wkhtmltopdf -q \\ 'pagination': textwrap.dedent("""\
--use-xserver \\ --footer-center "Page [page] of [topage]"\\
--footer-center "Page [page] of [topage]" \\ --footer-center "Page [page] of [topage]" \\
--footer-font-name sans \\ --footer-font-name sans \\
--footer-font-size 7 \\ --footer-font-size 7 \\
--footer-spacing 7 \\ --footer-spacing 7"""
) if pagination else '',
}
cmd = textwrap.dedent("""\
PATH=$PATH:/usr/local/bin/
xvfb-run -a -s "-screen 0 2480x3508x16" wkhtmltopdf -q \\
--use-xserver \\
%(pagination)s \\
--margin-bottom 22 \\ --margin-bottom 22 \\
--margin-top 20 - - """), --margin-top 20 - -\
stdin=html.encode('utf-8') """) % context
).stdout return run(cmd, stdin=html.encode('utf-8')).stdout