diff --git a/TODO.md b/TODO.md index 5edb02a1..8e9f3a4c 100644 --- a/TODO.md +++ b/TODO.md @@ -282,8 +282,7 @@ https://code.djangoproject.com/ticket/24576 # bill.totals make it 100% computed? * joomla: wget https://github.com/joomla/joomla-cms/releases/download/3.4.1/Joomla_3.4.1-Stable-Full_Package.tar.gz -O - | tar xvfz - - - -# Link related orders on bill line -# Customize those service.descriptions that are # replace multichoicefield and jsonfield by ArrayField, HStoreField +# Amend lines??? + + diff --git a/orchestra/admin/options.py b/orchestra/admin/options.py index a70dd06f..fb0b9a3c 100644 --- a/orchestra/admin/options.py +++ b/orchestra/admin/options.py @@ -5,11 +5,12 @@ from django.contrib.admin.options import IS_POPUP_VAR from django.contrib.admin.utils import unquote from django.contrib.auth import update_session_auth_hash from django.core.exceptions import PermissionDenied -from django.http import HttpResponseRedirect +from django.http import HttpResponseRedirect, Http404 from django.forms.models import BaseInlineFormSet from django.shortcuts import get_object_or_404 from django.template.response import TemplateResponse from django.utils.decorators import method_decorator +from django.utils.encoding import force_text from django.utils.html import escape from django.utils.translation import ugettext_lazy as _ from django.views.decorators.debug import sensitive_post_parameters @@ -164,6 +165,14 @@ class ExtendedModelAdmin(ChangeViewActionsMixin, ChangeAddFieldsMixin, admin.Mod if self.list_prefetch_related: qs = qs.prefetch_related(*self.list_prefetch_related) return qs + + def get_object(self, request, object_id, from_field=None): + obj = super(ExtendedModelAdmin, self).get_object(request, object_id, from_field) + if obj is None: + opts = self.model._meta + raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % { + 'name': force_text(opts.verbose_name), 'key': escape(object_id)}) + return obj class ChangePasswordAdminMixin(object): diff --git a/orchestra/contrib/bills/actions.py b/orchestra/contrib/bills/actions.py index 6edbfa81..fdebbd40 100644 --- a/orchestra/contrib/bills/actions.py +++ b/orchestra/contrib/bills/actions.py @@ -7,7 +7,7 @@ from django.core.exceptions import ValidationError from django.core.urlresolvers import reverse from django.db import transaction from django.http import HttpResponse -from django.shortcuts import render +from django.shortcuts import render, redirect from django.utils.safestring import mark_safe from django.utils.translation import ungettext, ugettext_lazy as _ @@ -76,7 +76,7 @@ def close_bills(modeladmin, request, queryset): url = change_url(transactions[0]) else: url = reverse('admin:transactions_transaction_changelist') - url += 'id__in=%s' % ','.join([str(t.id) for t in transactions]) + url += 'id__in=%s' % ','.join(map(str, transactions)) message = ungettext( _('One related transaction has been created') % url, _('%i related transactions have been created') % (url, num), @@ -114,6 +114,12 @@ send_bills.verbose_name = lambda bill: _("Resend" if getattr(bill, 'is_sent', Fa send_bills.url_name = 'send' +def manage_lines(modeladmin, request, queryset): + url = reverse('admin:bills_bill_manage_lines') + url += '?ids=%s' % ','.join(map(str, queryset.values_list('id', flat=True))) + return redirect(url) + + def undo_billing(modeladmin, request, queryset): group = {} for line in queryset.select_related('order'): @@ -125,7 +131,7 @@ def undo_billing(modeladmin, request, queryset): # TODO force incomplete info for order, lines in group.items(): # Find path from ini to end - for attr in ['order_id', 'order_billed_on', 'order_billed_until']: + for attr in ('order_id', 'order_billed_on', 'order_billed_until'): if not getattr(self, attr): raise ValidationError(_("Not enough information stored for undoing")) sorted(lines, key=lambda l: l.created_on) @@ -144,8 +150,8 @@ def move_lines(modeladmin, request, queryset): account = None for line in queryset.select_related('bill'): bill = line.bill - if bill.state != bill.OPEN: - messages.error(request, _("Can not move lines which are not in open state.")) + if not bill.is_open: + messages.error(request, _("Can not move lines from a closed bill.")) return elif not account: account = bill.account @@ -155,6 +161,7 @@ def move_lines(modeladmin, request, queryset): target = request.GET.get('target') if not target: # select target + context = {} return render(request, 'admin/orchestra/generic_confirmation.html', context) target = Bill.objects.get(pk=int(pk)) if target.account != account: diff --git a/orchestra/contrib/bills/admin.py b/orchestra/contrib/bills/admin.py index ad4e51ef..af94358e 100644 --- a/orchestra/contrib/bills/admin.py +++ b/orchestra/contrib/bills/admin.py @@ -4,7 +4,7 @@ from django.contrib import admin from django.contrib.admin.utils import unquote from django.core.urlresolvers import reverse from django.db import models -from django.db.models import F, Sum +from django.db.models import F, Sum, Prefetch from django.db.models.functions import Coalesce from django.templatetags.static import static from django.utils.safestring import mark_safe @@ -17,7 +17,7 @@ from orchestra.forms.widgets import paddingCheckboxSelectMultiple from . import settings, actions from .filters import BillTypeListFilter, HasBillContactListFilter -from .models import Bill, Invoice, AmendmentInvoice, Fee, AmendmentFee, ProForma, BillLine, BillContact +from .models import Bill, Invoice, AmendmentInvoice, Fee, AmendmentFee, ProForma, BillLine, BillSubline, BillContact PAYMENT_STATE_COLORS = { @@ -58,7 +58,7 @@ class BillLineInline(admin.TabularInline): def get_queryset(self, request): qs = super(BillLineInline, self).get_queryset(request) - return qs.prefetch_related('sublines') + return qs.prefetch_related('sublines').select_related('order') class ClosedBillLineInline(BillLineInline): @@ -99,29 +99,35 @@ class ClosedBillLineInline(BillLineInline): return False -class BillLineManagerAdmin(admin.ModelAdmin): - list_display = ('description', 'rate', 'quantity', 'tax', 'subtotal') +class BillLineAdmin(admin.ModelAdmin): + list_display = ('description', 'bill_link', 'rate', 'quantity', 'tax', 'subtotal') actions = (actions.undo_billing, actions.move_lines, actions.copy_lines,) + list_select_related = ('bill',) + bill_link = admin_link('bill') + + +class BillLineManagerAdmin(BillLineAdmin): def get_queryset(self, request): qset = super(BillLineManagerAdmin, self).get_queryset(request) - return qset.filter(bill_id__in=self.bill_ids) + if self.bill_ids: + return qset.filter(bill_id__in=self.bill_ids) + return qset def changelist_view(self, request, extra_context=None): GET = request.GET.copy() - bill_ids = GET.pop('bill_ids', ['0'])[0] - request.GET = GET - bill_ids = [int(id) for id in bill_ids.split(',')] + bill_ids = GET.pop('ids', None) + if bill_ids: + request.GET = GET + bill_ids = list(map(int, bill_ids.split(','))) self.bill_ids = bill_ids - if not bill_ids: - return - elif len(bill_ids) > 1: - title = _("Manage bill lines of multiple bills.") - else: + if bill_ids and len(bill_ids) == 1: bill_url = reverse('admin:bills_bill_change', args=(bill_ids[0],)) bill = Bill.objects.get(pk=bill_ids[0]) - bill_link = '%s' % (bill_url, bill.ident) + bill_link = '%s' % (bill_url, bill.number) title = mark_safe(_("Manage %s bill lines.") % bill_link) + else: + title = _("Manage bill lines of multiple bills.") context = { 'title': title, } @@ -147,8 +153,10 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin): }), ) change_view_actions = [ - actions.view_bill, actions.download_bills, actions.send_bills, actions.close_bills + actions.manage_lines, actions.view_bill, actions.download_bills, actions.send_bills, + actions.close_bills ] + list_prefetch_related = ('transactions',) search_fields = ('number', 'account__username', 'comments') actions = [actions.download_bills, actions.close_bills, actions.send_bills] change_readonly_fields = ('account_link', 'type', 'is_open') @@ -245,7 +253,6 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin): (F('lines__subtotal') + Coalesce(F('lines__sublines__total'), 0)) * (1+F('lines__tax')/100) ), ) - qs = qs.prefetch_related('transactions') return qs def change_view(self, request, object_id, **kwargs): @@ -261,6 +268,7 @@ admin.site.register(AmendmentInvoice, BillAdmin) admin.site.register(Fee, BillAdmin) admin.site.register(AmendmentFee, BillAdmin) admin.site.register(ProForma, BillAdmin) +admin.site.register(BillLine, BillLineAdmin) class BillContactInline(admin.StackedInline): diff --git a/orchestra/contrib/bills/migrations/0004_auto_20150421_1058.py b/orchestra/contrib/bills/migrations/0004_auto_20150421_1058.py new file mode 100644 index 00000000..8a249056 --- /dev/null +++ b/orchestra/contrib/bills/migrations/0004_auto_20150421_1058.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('bills', '0003_auto_20150420_1223'), + ] + + operations = [ + migrations.AlterField( + model_name='billcontact', + name='country', + field=models.CharField(default='ES', choices=[('NA', 'Namibia'), ('BD', 'Bangladesh'), ('RW', 'Rwanda'), ('LC', 'Saint Lucia'), ('VU', 'Vanuatu'), ('AO', 'Angola'), ('VC', 'Saint Vincent and the Grenadines'), ('ET', 'Ethiopia'), ('DZ', 'Algeria'), ('GL', 'Greenland'), ('BN', 'Brunei Darussalam'), ('AF', 'Afghanistan'), ('NL', 'Netherlands'), ('SE', 'Sweden'), ('UM', 'United States Minor Outlying Islands'), ('SN', 'Senegal'), ('YT', 'Mayotte'), ('FR', 'France'), ('GS', 'South Georgia and the South Sandwich Islands'), ('KY', 'Cayman Islands'), ('NC', 'New Caledonia'), ('JM', 'Jamaica'), ('AT', 'Austria'), ('TR', 'Turkey'), ('TF', 'French Southern Territories'), ('RS', 'Serbia'), ('CW', 'Curaçao'), ('BF', 'Burkina Faso'), ('AQ', 'Antarctica'), ('IR', 'Iran (Islamic Republic of)'), ('NE', 'Niger'), ('DJ', 'Djibouti'), ('LR', 'Liberia'), ('PK', 'Pakistan'), ('CR', 'Costa Rica'), ('EG', 'Egypt'), ('TM', 'Turkmenistan'), ('TG', 'Togo'), ('US', 'United States of America'), ('MO', 'Macao'), ('TN', 'Tunisia'), ('MS', 'Montserrat'), ('MQ', 'Martinique'), ('DM', 'Dominica'), ('BA', 'Bosnia and Herzegovina'), ('SO', 'Somalia'), ('MD', 'Moldova (the Republic of)'), ('GY', 'Guyana'), ('MV', 'Maldives'), ('BL', 'Saint Barthélemy'), ('CG', 'Congo'), ('TT', 'Trinidad and Tobago'), ('GH', 'Ghana'), ('NP', 'Nepal'), ('CA', 'Canada'), ('AL', 'Albania'), ('BG', 'Bulgaria'), ('IN', 'India'), ('LV', 'Latvia'), ('FK', 'Falkland Islands [Malvinas]'), ('CI', "Côte d'Ivoire"), ('SD', 'Sudan'), ('TK', 'Tokelau'), ('SJ', 'Svalbard and Jan Mayen'), ('LB', 'Lebanon'), ('GW', 'Guinea-Bissau'), ('SL', 'Sierra Leone'), ('MX', 'Mexico'), ('IE', 'Ireland'), ('BY', 'Belarus'), ('AD', 'Andorra'), ('CM', 'Cameroon'), ('GM', 'Gambia'), ('MU', 'Mauritius'), ('ME', 'Montenegro'), ('VN', 'Viet Nam'), ('CN', 'China'), ('PR', 'Puerto Rico'), ('RE', 'Réunion'), ('AR', 'Argentina'), ('DO', 'Dominican Republic'), ('TZ', 'Tanzania, United Republic of'), ('BB', 'Barbados'), ('BI', 'Burundi'), ('EE', 'Estonia'), ('JO', 'Jordan'), ('BE', 'Belgium'), ('OM', 'Oman'), ('SZ', 'Swaziland'), ('BO', 'Bolivia (Plurinational State of)'), ('HT', 'Haiti'), ('LY', 'Libya'), ('CO', 'Colombia'), ('NU', 'Niue'), ('AE', 'United Arab Emirates'), ('YE', 'Yemen'), ('MR', 'Mauritania'), ('CC', 'Cocos (Keeling) Islands'), ('MK', 'Macedonia (the former Yugoslav Republic of)'), ('FI', 'Finland'), ('PT', 'Portugal'), ('CX', 'Christmas Island'), ('VG', 'Virgin Islands (British)'), ('KZ', 'Kazakhstan'), ('MN', 'Mongolia'), ('UG', 'Uganda'), ('QA', 'Qatar'), ('MA', 'Morocco'), ('MT', 'Malta'), ('HN', 'Honduras'), ('LI', 'Liechtenstein'), ('DK', 'Denmark'), ('PL', 'Poland'), ('NF', 'Norfolk Island'), ('NI', 'Nicaragua'), ('KM', 'Comoros'), ('TH', 'Thailand'), ('MY', 'Malaysia'), ('SH', 'Saint Helena, Ascension and Tristan da Cunha'), ('IO', 'British Indian Ocean Territory'), ('PE', 'Peru'), ('UY', 'Uruguay'), ('TW', 'Taiwan (Province of China)'), ('FJ', 'Fiji'), ('LT', 'Lithuania'), ('JP', 'Japan'), ('AI', 'Anguilla'), ('VI', 'Virgin Islands (U.S.)'), ('MF', 'Saint Martin (French part)'), ('EC', 'Ecuador'), ('BW', 'Botswana'), ('CK', 'Cook Islands'), ('SY', 'Syrian Arab Republic'), ('HK', 'Hong Kong'), ('GP', 'Guadeloupe'), ('AX', 'Åland Islands'), ('PH', 'Philippines'), ('TO', 'Tonga'), ('MC', 'Monaco'), ('UA', 'Ukraine'), ('PY', 'Paraguay'), ('ZA', 'South Africa'), ('BV', 'Bouvet Island'), ('ZW', 'Zimbabwe'), ('KR', 'Korea (the Republic of)'), ('NO', 'Norway'), ('UZ', 'Uzbekistan'), ('IL', 'Israel'), ('GG', 'Guernsey'), ('GQ', 'Equatorial Guinea'), ('FO', 'Faroe Islands'), ('NR', 'Nauru'), ('SG', 'Singapore'), ('JE', 'Jersey'), ('ID', 'Indonesia'), ('AW', 'Aruba'), ('BT', 'Bhutan'), ('HR', 'Croatia'), ('AS', 'American Samoa'), ('SC', 'Seychelles'), ('TV', 'Tuvalu'), ('GT', 'Guatemala'), ('FM', 'Micronesia (Federated States of)'), ('IQ', 'Iraq'), ('ES', 'Spain'), ('KH', 'Cambodia'), ('BM', 'Bermuda'), ('BH', 'Bahrain'), ('TL', 'Timor-Leste'), ('CU', 'Cuba'), ('SX', 'Sint Maarten (Dutch part)'), ('BQ', 'Bonaire, Sint Eustatius and Saba'), ('BS', 'Bahamas'), ('RU', 'Russian Federation'), ('PG', 'Papua New Guinea'), ('SI', 'Slovenia'), ('KP', "Korea (the Democratic People's Republic of)"), ('IS', 'Iceland'), ('ER', 'Eritrea'), ('NG', 'Nigeria'), ('LK', 'Sri Lanka'), ('PF', 'French Polynesia'), ('DE', 'Germany'), ('MH', 'Marshall Islands'), ('TD', 'Chad'), ('KE', 'Kenya'), ('CF', 'Central African Republic'), ('IM', 'Isle of Man'), ('BR', 'Brazil'), ('CD', 'Congo (the Democratic Republic of the)'), ('KI', 'Kiribati'), ('GN', 'Guinea'), ('GB', 'United Kingdom of Great Britain and Northern Ireland'), ('ZM', 'Zambia'), ('CY', 'Cyprus'), ('ST', 'Sao Tome and Principe'), ('GE', 'Georgia'), ('SM', 'San Marino'), ('AG', 'Antigua and Barbuda'), ('MW', 'Malawi'), ('SS', 'South Sudan'), ('GU', 'Guam'), ('HU', 'Hungary'), ('CL', 'Chile'), ('LS', 'Lesotho'), ('MP', 'Northern Mariana Islands'), ('ML', 'Mali'), ('GF', 'French Guiana'), ('CH', 'Switzerland'), ('SK', 'Slovakia'), ('VA', 'Holy See'), ('WF', 'Wallis and Futuna'), ('PA', 'Panama'), ('SB', 'Solomon Islands'), ('SR', 'Suriname'), ('PN', 'Pitcairn'), ('LA', "Lao People's Democratic Republic"), ('BJ', 'Benin'), ('BZ', 'Belize'), ('PW', 'Palau'), ('IT', 'Italy'), ('MM', 'Myanmar'), ('NZ', 'New Zealand'), ('SV', 'El Salvador'), ('GR', 'Greece'), ('GA', 'Gabon'), ('LU', 'Luxembourg'), ('MZ', 'Mozambique'), ('KN', 'Saint Kitts and Nevis'), ('AZ', 'Azerbaijan'), ('CV', 'Cabo Verde'), ('SA', 'Saudi Arabia'), ('EH', 'Western Sahara'), ('AU', 'Australia'), ('WS', 'Samoa'), ('PS', 'Palestine, State of'), ('KG', 'Kyrgyzstan'), ('VE', 'Venezuela (Bolivarian Republic of)'), ('KW', 'Kuwait'), ('CZ', 'Czech Republic'), ('RO', 'Romania'), ('MG', 'Madagascar'), ('HM', 'Heard Island and McDonald Islands'), ('AM', 'Armenia'), ('GI', 'Gibraltar'), ('TC', 'Turks and Caicos Islands'), ('PM', 'Saint Pierre and Miquelon'), ('TJ', 'Tajikistan'), ('GD', 'Grenada')], max_length=20, verbose_name='country'), + ), + migrations.AlterField( + model_name='billline', + name='end_on', + field=models.DateField(verbose_name='end', null=True), + ), + migrations.AlterField( + model_name='billline', + name='start_on', + field=models.DateField(verbose_name='start'), + ), + ] diff --git a/orchestra/contrib/bills/models.py b/orchestra/contrib/bills/models.py index 83cd7fa6..9ab06ee0 100644 --- a/orchestra/contrib/bills/models.py +++ b/orchestra/contrib/bills/models.py @@ -290,17 +290,12 @@ class BillLine(models.Model): related_name='amendment_lines', null=True, blank=True) def __str__(self): - return "#%i" % self.number - - @cached_property - def number(self): - lines = type(self).objects.filter(bill=self.bill_id) - return lines.filter(id__lte=self.id).order_by('id').count() + return "#%i" % self.pk def get_total(self): """ Computes subline discounts """ if self.pk: - return self.subtotal + sum(self.sublines.values_list('total', flat=True)) + return self.subtotal + sum([sub.total for sub in self.sublines.all()]) def get_verbose_quantity(self): return self.verbose_quantity or self.quantity diff --git a/orchestra/contrib/databases/backends.py b/orchestra/contrib/databases/backends.py index 40199147..37bc0229 100644 --- a/orchestra/contrib/databases/backends.py +++ b/orchestra/contrib/databases/backends.py @@ -17,11 +17,11 @@ class MySQLBackend(ServiceController): if database.type != database.MYSQL: return context = self.get_context(database) - # Not available on delete() - context['owner'] = database.owner self.append( "mysql -e 'CREATE DATABASE `%(database)s`;' || true" % context ) + # Not available on delete() + context['owner'] = database.owner # clean previous privileges self.append("""mysql mysql -e 'DELETE FROM db WHERE db = "%(database)s";'""" % context) for user in database.users.all(): diff --git a/orchestra/contrib/databases/models.py b/orchestra/contrib/databases/models.py index 68ae8cac..b04e1ff1 100644 --- a/orchestra/contrib/databases/models.py +++ b/orchestra/contrib/databases/models.py @@ -15,7 +15,7 @@ class Database(models.Model): name = models.CharField(_("name"), max_length=64, # MySQL limit validators=[validators.validate_name]) - users = models.ManyToManyField('databases.DatabaseUser', + users = models.ManyToManyField('databases.DatabaseUser', blank=True, verbose_name=_("users"),related_name='databases') type = models.CharField(_("type"), max_length=32, choices=settings.DATABASES_TYPE_CHOICES, @@ -34,7 +34,10 @@ class Database(models.Model): """ database owner is the first user related to it """ # Accessing intermediary model to get which is the first user users = Database.users.through.objects.filter(database_id=self.id) - return users.order_by('id').first().databaseuser + user = users.order_by('id').first() + if user is not None: + return user.databaseuser + return None Database.users.through._meta.unique_together = ( diff --git a/orchestra/contrib/orders/actions.py b/orchestra/contrib/orders/actions.py index 13ee2580..f8b5e8a0 100644 --- a/orchestra/contrib/orders/actions.py +++ b/orchestra/contrib/orders/actions.py @@ -89,7 +89,7 @@ class BillSelectedOrders(object): url = change_url(bills[0]) else: url = reverse('admin:bills_bill_changelist') - ids = ','.join([str(bill.id) for bill in bills]) + ids = ','.join(map(str, bills)) url += '?id__in=%s' % ids num = len(bills) msg = ungettext( diff --git a/orchestra/contrib/plans/admin.py b/orchestra/contrib/plans/admin.py index 80ba716d..7bb9440f 100644 --- a/orchestra/contrib/plans/admin.py +++ b/orchestra/contrib/plans/admin.py @@ -10,7 +10,7 @@ from .models import Plan, ContractedPlan, Rate class RateInline(admin.TabularInline): model = Rate - ordering = ('plan', 'quantity') + ordering = ('service', 'plan', 'quantity') class PlanAdmin(ExtendedModelAdmin): diff --git a/orchestra/contrib/resources/actions.py b/orchestra/contrib/resources/actions.py index e7117340..e26b924b 100644 --- a/orchestra/contrib/resources/actions.py +++ b/orchestra/contrib/resources/actions.py @@ -12,7 +12,7 @@ def run_monitor(modeladmin, request, queryset): for resource in queryset: rlogs = resource.monitor() if not async: - logs = logs.union(set([str(rlog.pk) for rlog in rlogs])) + logs = logs.union(set(map(str, rlogs))) modeladmin.log_change(request, resource, _("Run monitors")) if async: num = len(queryset) diff --git a/orchestra/contrib/webapps/backends/webalizer.py b/orchestra/contrib/webapps/backends/webalizer.py index 68e49005..fe5bab30 100644 --- a/orchestra/contrib/webapps/backends/webalizer.py +++ b/orchestra/contrib/webapps/backends/webalizer.py @@ -5,6 +5,7 @@ from orchestra.contrib.orchestration import ServiceController from . import WebAppServiceMixin +# TODO DEPRECATE class WebalizerAppBackend(WebAppServiceMixin, ServiceController): """ Needed for cleaning up webalizer main folder when webapp deleteion withou related contents """ verbose_name = _("Webalizer App") diff --git a/orchestra/contrib/websites/backends/webalizer.py b/orchestra/contrib/websites/backends/webalizer.py index e775efe4..09a1d4cb 100644 --- a/orchestra/contrib/websites/backends/webalizer.py +++ b/orchestra/contrib/websites/backends/webalizer.py @@ -28,10 +28,11 @@ class WebalizerBackend(ServiceController): def delete(self, content): context = self.get_context(content) - delete_webapp = type(content.webapp).objects.filter(pk=content.webapp.pk).exists() + delete_webapp = not type(content.webapp).objects.filter(pk=content.webapp.pk).exists() if delete_webapp: - self.append("rm -f %(webapp_path)s" % context) - if delete_webapp or not content.webapp.content_set.filter(website=content.website).exists(): + self.append("rm -fr %(webapp_path)s" % context) + remounted = content.webapp.content_set.filter(website=content.website).exists() + if delete_webapp or not remounted: self.append("rm -fr %(webalizer_path)s" % context) self.append("rm -f %(webalizer_conf_path)s" % context) diff --git a/orchestra/utils/html.py b/orchestra/utils/html.py index 57aca198..12faebf7 100644 --- a/orchestra/utils/html.py +++ b/orchestra/utils/html.py @@ -6,6 +6,7 @@ def html_to_pdf(html): return run( 'PATH=$PATH:/usr/local/bin/\n' 'xvfb-run -a -s "-screen 0 640x4800x16" ' - 'wkhtmltopdf -q --footer-center "Page [page] of [topage]" --footer-font-size 9 - -', + 'wkhtmltopdf -q --footer-center "Page [page] of [topage]" ' + ' --footer-font-size 9 --margin-bottom 20 --margin-top 20 - -', stdin=html.encode('utf-8') ).stdout