Fixes on billing

This commit is contained in:
Marc Aymerich 2015-05-27 14:05:25 +00:00
parent ca6b479e17
commit fd8d805b5e
15 changed files with 291 additions and 169 deletions

View file

@ -413,3 +413,4 @@ touch /tmp/somefile
# inherit registers from parent?
# Bill metric disk 5 GB: unialber: include not include 5, unialbert recheck period

View file

@ -1,8 +1,9 @@
from django.contrib.auth import models as auth
from django.conf import settings as djsettings
from django.core import validators
from django.db import models
from django.apps import apps
from django.utils import timezone
from django.utils import timezone, translation
from django.utils.translation import ugettext_lazy as _
from orchestra.contrib.orchestration.middlewares import OperationsMiddleware
@ -91,10 +92,18 @@ class Account(auth.AbstractBaseUser):
for obj in getattr(self, rel.get_accessor_name()).all():
OperationsMiddleware.collect(Operation.SAVE, instance=obj, update_fields=[])
def send_email(self, template, context, contacts=[], attachments=[], html=None):
def send_email(self, template, context, email_from=None, contacts=[], attachments=[], html=None):
contacts = self.contacts.filter(email_usages=contacts)
email_to = contacts.values_list('email', flat=True)
send_email_template(template, context, email_to, html=html, attachments=attachments)
extra_context = {
'account': self,
'email_from': email_from or djsettings.SERVER_EMAIL,
}
extra_context.update(context)
with translation.override(self.language):
send_email_template(
template, extra_context, ('marcay@pangea.org',), email_from=email_from, html=html,
attachments=attachments)
def get_full_name(self):
return self.full_name or self.short_name or self.username

View file

@ -1,4 +1,5 @@
import zipfile
from datetime import date
from io import StringIO
from django.contrib import messages
@ -128,21 +129,53 @@ def undo_billing(modeladmin, request, queryset):
group[line.order].append(line)
except KeyError:
group[line.order] = [line]
# TODO force incomplete info
# Validate
for order, lines in group.items():
# Find path from ini to end
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)
if 'a' != order.billed_on:
raise ValidationError(_("Dates don't match"))
prev = order.billed_on
for ix in range(0, len(lines)):
if lines[ix].order_b: # TODO we need to look at the periods here
pass
order.billed_until = self.order_billed_until
order.billed_on = self.order_billed_on
prev = None
billed_on = date.max
billed_until = date.max
for line in sorted(lines, key=lambda l: l.start_on):
if billed_on is not None:
if line.order_billed_on is None:
billed_on = line.order_billed_on
else:
billed_on = min(billed_on, line.order_billed_on)
if billed_until is not None:
if line.order_billed_until is None:
billed_until = line.order_billed_until
else:
billed_until = min(billed_until, line.order_billed_until)
if prev:
if line.start_on != prev:
messages.error(request, "Line dates doesn't match.")
return
else:
# First iteration
if order.billed_on < line.start_on:
messages.error(request, "billed on is smaller than first line start_on.")
return
prev = line.end_on
nlines += 1
if not prev:
messages.error(request, "Order does not have lines!.")
order.billed_until = billed_until
order.billed_on = billed_on
# Commit changes
norders, nlines = 0, 0
for order, lines in group.items():
for line in lines:
nlines += 1
line.delete()
order.save(update_fields=('billed_until', 'billed_on'))
norders += 1
messages.success(request, _("%(norders)s orders and %(nlines)s lines undoed.") % {
'nlines': nlines,
'norders': norders
})
# TODO son't check for account equality
def move_lines(modeladmin, request, queryset):

View file

@ -16,7 +16,7 @@ from orchestra.contrib.accounts.admin import AccountAdminMixin, AccountAdmin
from orchestra.forms.widgets import paddingCheckboxSelectMultiple
from . import settings, actions
from .filters import BillTypeListFilter, HasBillContactListFilter
from .filters import BillTypeListFilter, HasBillContactListFilter, PositivePriceListFilter
from .models import Bill, Invoice, AmendmentInvoice, Fee, AmendmentFee, ProForma, BillLine, BillContact
@ -165,7 +165,7 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
'number', 'type_link', 'account_link', 'created_on_display',
'num_lines', 'display_total', 'display_payment_state', 'is_open', 'is_sent'
)
list_filter = (BillTypeListFilter, 'is_open', 'is_sent')
list_filter = (BillTypeListFilter, 'is_open', 'is_sent', PositivePriceListFilter)
add_fields = ('account', 'type', 'is_open', 'due_on', 'comments')
fieldsets = (
(None, {
@ -188,7 +188,7 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
readonly_fields = ('number', 'display_total', 'is_sent', 'display_payment_state')
inlines = [BillLineInline, ClosedBillLineInline]
created_on_display = admin_date('created_on')
created_on_display = admin_date('created_on', short_description=_("Created"))
def num_lines(self, bill):
return bill.lines__count

View file

@ -37,6 +37,23 @@ class BillTypeListFilter(SimpleListFilter):
}
class PositivePriceListFilter(SimpleListFilter):
title = _("positive price")
parameter_name = 'positive_price'
def lookups(self, request, model_admin):
return (
('True', _("Yes")),
('False', _("No")),
)
def queryset(self, request, queryset):
if self.value() == 'True':
return queryset.filter(computed_total__gt=0)
if self.value() == 'False':
return queryset.filter(computed_total__lte=0)
class HasBillContactListFilter(SimpleListFilter):
""" Filter Nodes by group according to request.user """
title = _("has bill contact")

View file

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-04-20 11:02+0000\n"
"POT-Creation-Date: 2015-05-27 13:28+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,112 +18,113 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: actions.py:36
#: actions.py:37
msgid "Download"
msgstr "Descarrega"
#: actions.py:46
#: actions.py:47
msgid "View"
msgstr "Vista"
#: actions.py:54
#: actions.py:55
msgid "Selected bills should be in open state"
msgstr ""
#: actions.py:72
#: actions.py:73
msgid "Selected bills have been closed"
msgstr ""
#: actions.py:81
#, python-format
msgid "<a href=\"%s\">One related transaction</a> has been created"
msgstr ""
#: actions.py:82
#, python-format
msgid "<a href=\"%s\">One related transaction</a> has been created"
msgstr ""
#: actions.py:83
#, python-format
msgid "<a href=\"%s\">%i related transactions</a> have been created"
msgstr ""
#: actions.py:88
#: actions.py:89
msgid "Are you sure about closing the following bills?"
msgstr ""
#: actions.py:89
#: actions.py:90
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:102
#: actions.py:103
msgid "Close"
msgstr ""
#: actions.py:113
#: actions.py:114
msgid "Resend"
msgstr ""
#: actions.py:130 models.py:312
msgid "Not enough information stored for undoing"
#: actions.py:174
#, python-format
msgid "%(norders)s orders and %(nlines)s lines undoed."
msgstr ""
#: actions.py:133 models.py:314
msgid "Dates don't match"
#: actions.py:187
msgid "Can not move lines from a closed bill."
msgstr ""
#: actions.py:148
msgid "Can not move lines which are not in open state."
msgstr ""
#: actions.py:153
#: actions.py:192
msgid "Can not move lines from different accounts"
msgstr ""
#: actions.py:161
#: actions.py:201
msgid "Target account different than lines account."
msgstr ""
#: actions.py:168
#: actions.py:208
msgid "Lines moved"
msgstr ""
#: admin.py:43 admin.py:86 forms.py:11
#: admin.py:48 admin.py:92 admin.py:121 forms.py:11
msgid "Total"
msgstr ""
#: admin.py:73
#: admin.py:79
msgid "Description"
msgstr "Descripció"
#: admin.py:81
#: admin.py:87
msgid "Subtotal"
msgstr ""
#: admin.py:113
msgid "Manage bill lines of multiple bills."
#: admin.py:116
msgid "Subline"
msgstr ""
#: admin.py:118
#: admin.py:153
#, python-format
msgid "Manage %s bill lines."
msgstr ""
#: admin.py:138
#: admin.py:155
msgid "Manage bill lines of multiple bills."
msgstr ""
#: admin.py:175
msgid "Raw"
msgstr ""
#: admin.py:157
#: admin.py:196
msgid "lines"
msgstr ""
#: admin.py:162 templates/bills/microspective.html:107
#: admin.py:201 templates/bills/microspective.html:118
msgid "total"
msgstr ""
#: admin.py:170 models.py:87 models.py:342
#: admin.py:209 models.py:88 models.py:340
msgid "type"
msgstr "tipus"
#: admin.py:187
#: admin.py:226
msgid "Payment"
msgstr "Pagament"
@ -131,15 +132,15 @@ msgstr "Pagament"
msgid "All"
msgstr "Tot"
#: filters.py:18 models.py:77
#: filters.py:18 models.py:78
msgid "Invoice"
msgstr "Factura"
#: filters.py:19 models.py:78
#: filters.py:19 models.py:79
msgid "Amendment invoice"
msgstr "Factura rectificativa"
#: filters.py:20 models.py:79
#: filters.py:20 models.py:80
msgid "Fee"
msgstr "Quota de soci"
@ -151,18 +152,22 @@ msgstr "Rectificació de quota de soci"
msgid "Pro-forma"
msgstr ""
#: filters.py:42
msgid "has bill contact"
#: filters.py:41
msgid "positive price"
msgstr ""
#: filters.py:47
#: filters.py:46 filters.py:64
msgid "Yes"
msgstr "Si"
#: filters.py:48
#: filters.py:47 filters.py:65
msgid "No"
msgstr "No"
#: filters.py:59
msgid "has bill contact"
msgstr ""
#: forms.py:9
msgid "Number"
msgstr ""
@ -193,165 +198,202 @@ msgstr ""
msgid "Main"
msgstr ""
#: models.py:22 models.py:85
#: models.py:23 models.py:86
msgid "account"
msgstr ""
#: models.py:24
#: models.py:25
msgid "name"
msgstr ""
#: models.py:25
#: models.py:26
msgid "Account full name will be used when left blank."
msgstr ""
#: models.py:26
#: models.py:27
msgid "address"
msgstr ""
#: models.py:27
#: models.py:28
msgid "city"
msgstr ""
#: models.py:29
#: models.py:30
msgid "zip code"
msgstr ""
#: models.py:30
#: models.py:31
msgid "Enter a valid zipcode."
msgstr ""
#: models.py:31
#: models.py:32
msgid "country"
msgstr ""
#: models.py:34
#: models.py:35
msgid "VAT number"
msgstr "NIF"
#: models.py:66
#: models.py:67
msgid "Paid"
msgstr ""
#: models.py:67
#: models.py:68
msgid "Pending"
msgstr ""
#: models.py:68
#: models.py:69
msgid "Bad debt"
msgstr ""
#: models.py:80
#: models.py:81
msgid "Amendment Fee"
msgstr ""
#: models.py:81
#: models.py:82
msgid "Pro forma"
msgstr ""
#: models.py:84
#: models.py:85
msgid "number"
msgstr ""
#: models.py:88
#: models.py:89
msgid "created on"
msgstr ""
#: models.py:89
#: models.py:90
msgid "closed on"
msgstr ""
#: models.py:90
#: models.py:91
msgid "open"
msgstr ""
#: models.py:91
#: models.py:92
msgid "sent"
msgstr ""
#: models.py:92
#: models.py:93
msgid "due on"
msgstr ""
#: models.py:93
#: models.py:94
msgid "updated on"
msgstr ""
#: models.py:96
#: models.py:97
msgid "comments"
msgstr "comentaris"
#: models.py:97
#: models.py:98
msgid "HTML"
msgstr ""
#: models.py:273
#: models.py:280
msgid "bill"
msgstr ""
#: models.py:274 models.py:339 templates/bills/microspective.html:73
#: models.py:281 models.py:338 templates/bills/microspective.html:73
msgid "description"
msgstr "descripció"
#: models.py:275
#: models.py:282
msgid "rate"
msgstr "tarifa"
#: models.py:276
#: models.py:283
msgid "quantity"
msgstr "quantitat"
#: models.py:277
#: models.py:284
#, fuzzy
#| msgid "quantity"
msgid "Verbose quantity"
msgstr "quantitat"
#: models.py:278 templates/bills/microspective.html:76
#: templates/bills/microspective.html:100
#: models.py:285 templates/bills/microspective.html:77
#: templates/bills/microspective.html:111
msgid "subtotal"
msgstr ""
#: models.py:279
#: models.py:286
msgid "tax"
msgstr "impostos"
#: models.py:284
#: models.py:287
msgid "start"
msgstr ""
#: models.py:288
msgid "end"
msgstr ""
#: models.py:290
msgid "Informative link back to the order"
msgstr ""
#: models.py:285
#: models.py:291
msgid "order billed"
msgstr ""
#: models.py:286
#: models.py:292
msgid "order billed until"
msgstr ""
#: models.py:287
#: models.py:293
msgid "created"
msgstr ""
#: models.py:289
#: models.py:295
msgid "amended line"
msgstr ""
#: models.py:332
#: models.py:316
msgid "{ini} to {end}"
msgstr ""
#: models.py:331
msgid "Volume"
msgstr ""
#: models.py:333
#: models.py:332
msgid "Compensation"
msgstr ""
#: models.py:334
#: models.py:333
msgid "Other"
msgstr ""
#: models.py:338
#: models.py:337
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:113
#, python-format
msgid "From %(period)s"
msgstr ""
#: templates/bills/microspective-fee.html:118
msgid ""
"\n"
"<strong>Con vuestras cuotas</strong>, ademas de obtener conexion y multitud "
"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 ""
#: templates/bills/microspective.html:49
msgid "DUE DATE"
msgstr "VENCIMENT"
@ -366,52 +408,57 @@ msgid "%(bill_type|upper)s DATE "
msgstr "DATA %(bill_type|upper)s"
#: templates/bills/microspective.html:74
msgid "period"
msgstr ""
#: templates/bills/microspective.html:75
msgid "hrs/qty"
msgstr "hrs/quant"
#: templates/bills/microspective.html:75
#: templates/bills/microspective.html:76
msgid "rate/price"
msgstr "tarifa/preu"
#: templates/bills/microspective.html:100
#: templates/bills/microspective.html:103
#: templates/bills/microspective.html:111
#: templates/bills/microspective.html:114
msgid "VAT"
msgstr "IVA"
#: templates/bills/microspective.html:103
#: templates/bills/microspective.html:114
msgid "taxes"
msgstr "impostos"
#: templates/bills/microspective.html:119
#: templates/bills/microspective.html:130
msgid "COMMENTS"
msgstr "COMENTARIS"
#: templates/bills/microspective.html:125
#: templates/bills/microspective.html:136
msgid "PAYMENT"
msgstr "PAGAMENT"
#: templates/bills/microspective.html:129
#: templates/bills/microspective.html:140
#, python-format
msgid ""
"\n"
" You can pay our %(type)s by bank transfer. <br>\n"
" Please make sure to state your name and the %(type)s "
"number.\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 ""
"\n"
"Pots pagar aquesta %(type)s per transferencia banacaria.<br>Inclou el teu "
"nom i el numero de %(type)s. El nostre compte bancari és"
"Pots pagar aquesta <i>%(type)s</i> per transferencia banacaria.<br>Inclou el "
"teu nom i el numero de <i>%(type)s</i>. El nostre compte bancari és"
#: templates/bills/microspective.html:138
#: templates/bills/microspective.html:149
msgid "QUESTIONS"
msgstr "PREGUNTES"
#: templates/bills/microspective.html:139
#: templates/bills/microspective.html:150
#, python-format
msgid ""
"\n"
" If you have any question about your %(type)s, 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 "
"soon as we get\n"
" your message.\n"

View file

@ -128,6 +128,9 @@ class Bill(models.Model):
value = self.payment_state
return force_text(dict(self.PAYMENT_STATES).get(value, value))
def get_current_transaction(self):
return self.transactions.exclude_rejected().first()
@classmethod
def get_class_type(cls):
return cls.__name__.upper()
@ -187,7 +190,9 @@ class Bill(models.Model):
template=settings.BILLS_EMAIL_NOTIFICATION_TEMPLATE,
context={
'bill': self,
'settings': settings,
},
email_from=settings.BILLS_SELLER_EMAIL,
contacts=(Contact.BILLING,),
attachments=[
('%s.pdf' % self.number, html_to_pdf(html), 'application/pdf')
@ -288,7 +293,7 @@ class BillLine(models.Model):
created_on = models.DateField(_("created"), auto_now_add=True)
# Amendment
amended_line = models.ForeignKey('self', verbose_name=_("amended line"),
related_name='amendment_lines', null=True, blank=True)
related_name='amendment_lines', null=True, blank=True)
def __str__(self):
return "#%i" % self.pk
@ -302,24 +307,22 @@ class BillLine(models.Model):
return self.verbose_quantity or self.quantity
def get_verbose_period(self):
ini = self.start_on.strftime("%b, %Y")
from django.template.defaultfilters import date
date_format = "N 'y"
if self.start_on.day != 1 or self.end_on.day != 1:
date_format = "N j, 'y"
end = date(self.end_on, date_format)
# .strftime(date_format)
else:
end = date((self.end_on - datetime.timedelta(days=1)), date_format)
# ).strftime(date_format)
ini = date(self.start_on, date_format)
#.strftime(date_format)
if not self.end_on:
return ini
end = (self.end_on - datetime.timedelta(seconds=1)).strftime("%b, %Y")
if ini == end:
return ini
return _("{ini} to {end}").format(ini=ini, end=end)
def undo(self):
# TODO warn user that undoing bills with compensations lead to compensation lost
for attr in ['order_id', 'order_billed_on', 'order_billed_until']:
if not getattr(self, attr):
raise ValidationError(_("Not enough information stored for undoing"))
if self.created_on != self.order.billed_on:
raise ValidationError(_("Dates don't match"))
self.order.billed_until = self.order_billed_until
self.order.billed_on = self.order_billed_on
self.delete()
return "{ini} / {end}".format(ini=ini, end=end)
# def save(self, *args, **kwargs):
# super(BillLine, self).save(*args, **kwargs)
@ -342,7 +345,6 @@ class BillSubline(models.Model):
# TODO: order info for undoing
line = models.ForeignKey(BillLine, verbose_name=_("bill line"), related_name='sublines')
description = models.CharField(_("description"), max_length=256)
# TODO rename to subtotal
total = models.DecimalField(max_digits=12, decimal_places=2)
type = models.CharField(_("type"), max_length=16, choices=TYPES, default=OTHER)

View file

@ -1,6 +1,6 @@
{% if subject %}Bill {{ bill.number }}{% endif %}
{% if message %}Dear {{ bill.account.username }},
Find your {{ bill.get_type.lower }} attached.
Find your {{ bill.get_type_display.lower }} attached.
If you have any question, please write us at support@orchestra.lan
{% endif %}

View file

@ -1,4 +1,6 @@
{% extends 'bills/microspective.html' %}
{% load i18n %}
{% block head %}
<style type="text/css">
{% with color="#809708" %}
@ -91,36 +93,37 @@ hr {
{{ buyer.vat }}<br>
{{ buyer.address }}<br>
{{ buyer.zipcode }} - {{ buyer.city }}<br>
{{ buyer.country }}<br>
{{ buyer.get_country_display }}<br>
</div>
<div id="number" class="column-1">
<span id="number-title">Membership Fee</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-date">{{ bill.closed_on | default:now | date }}</span><br>
<span id="number-date">{{ bill.closed_on | default:now | date | capfirst }}</span><br>
</div>
<div id="amount" class="column-2">
<span id="amount-value">{{ bill.get_total }} &euro;</span><br>
<span id="amount-note">Due date {{ payment.due_date | default:default_due_date | date }}<br>
{% if not payment.message %}On {{ seller_info.bank_account }}{% endif %}<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>
{% if not payment.message %}{% blocktrans with bank_account=seller_info.bank_account %}On {{ bank_account }}{% endblocktrans %}{% endif %}<br>
</span>
</div>
<div id="date" class="column-2">
From {{ bill.lines.get.get_verbose_period }}
{% blocktrans with period=bill.lines.get.get_verbose_period %}From {{ period }}{% endblocktrans %}
</div>
{% endblock %}
{% block content %}
<div id="text">
{% blocktrans %}
<strong>Con vuestras cuotas</strong>, ademas de obtener conexion y multitud de servicios, estais<br>
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 %}
</div>
{% endblock %}
{% block footer %}
<hr>
{{ block.super }}
{% endblock %}

View file

@ -40,7 +40,7 @@
{% block summary %}
<div id="bill-number">
{% trans bill.get_type_display.capitalize %}<br>
{% filter title %}{% trans bill.get_type_display %}{% endfilter %}<br>
<span class="value">{{ bill.number }}</span><br>
</div>
<div id="bill-summary">
@ -54,7 +54,7 @@
<psan class="value">{{ bill.get_total }} &{{ currency.lower }};</span>
</div>
<div id="bill-date">
<span class="title">{% blocktrans with bill_type=bill.get_type_display %}{{ bill_type|upper }} 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>
</div>
</div>
@ -137,9 +137,9 @@
{% if payment.message %}
{{ payment.message | safe }}
{% else %}
{% blocktrans with type=bill.get_type_display %}
You can pay our {{ type }} by bank transfer. <br>
Please make sure to state your name and the {{ type }} number.
{% blocktrans with type=bill.get_type_display.lower %}
You can pay our <i>{{ type }}</i> by bank transfer. <br>
Please make sure to state your name and the <i>{{ type }}</i> number.
Our bank account number is <br>
{% endblocktrans %}
<strong>{{ seller_info.bank_account }}</strong>
@ -148,7 +148,7 @@
<div id="questions">
<span class="title">{% trans "QUESTIONS" %}</span>
{% blocktrans with type=bill.get_type_display.lower %}
If you have any question about your {{ type }}, 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
your message.
{% endblocktrans %}

View file

@ -113,7 +113,7 @@ class BillSelectedOrders(object):
'title': _("Confirmation for billing selected orders"),
'step': 3,
'form': form,
'bills': bills_with_total,
'bills': sorted(bills_with_total, key=lambda i: -i[1]),
})
return render(request, self.template, self.context)

View file

@ -73,12 +73,18 @@ class OrderAdmin(AccountAdminMixin, ExtendedModelAdmin):
display_cancelled_on = admin_date('cancelled_on')
def display_billed_until(self, order):
value = order.billed_until
color = ''
if value and value < timezone.now().date():
color = 'style="color:red;"'
billed_until = order.billed_until
red = False
if billed_until:
if order.service.payment_style == order.service.POSTPAY:
boundary = order.service.handler.get_billing_point(order)
if billed_until < boundary:
red = True
elif billed_until < timezone.now().date():
red = True
color = 'style="color:red;"' if red else ''
return '<span title="{raw}" {color}>{human}</span>'.format(
raw=escape(str(value)), color=color, human=escape(naturaldate(value)),
raw=escape(str(billed_until)), color=color, human=escape(naturaldate(billed_until)),
)
display_billed_until.short_description = _("billed until")
display_billed_until.allow_tags = True

View file

@ -244,18 +244,14 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
'total': price,
}))
def generate_line(self, order, price, *dates, **kwargs):
def generate_line(self, order, price, *dates, metric=1, discounts=None, computed=False):
if len(dates) == 2:
ini, end = dates
elif len(dates) == 1:
ini, end = dates[0], dates[0]
else:
raise AttributeError
metric = kwargs.pop('metric', 1)
discounts = kwargs.pop('discounts', ())
computed = kwargs.pop('computed', False)
if kwargs:
raise AttributeError
raise AttributeError("WTF is '%s'?" % str(dates))
discounts = discounts or ()
size = self.get_price_size(ini, end)
if not computed:
@ -274,6 +270,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
for dtype, dprice in discounts:
self.generate_discount(line, dtype, dprice)
discounted += dprice
# TODO this is needed for all discounts?
subtotal += discounted
if subtotal > price:
self.generate_discount(line, self._PLAN, price-subtotal)
@ -490,6 +487,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
lines = []
bp = None
for order in orders:
recharges = []
bp = self.get_billing_point(order, bp=bp, **options)
if (self.billing_period != self.NEVER and
self.get_pricing_period() == self.NEVER and
@ -508,14 +506,20 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
price = self.get_price(account, metric) * size
discounts = ()
discount = min(price, max(cprice, 0))
if pre_discount:
if discount:
cprice -= price
price -= discount
discounts = (
('prepay', -discount),
)
# if price-pre_discount:
lines.append(self.generate_line(order, price, cini, cend, metric=metric,
computed=True, discounts=discounts))
# if price-discount:
recharges.append((order, price, cini, cend, metric, discounts))
# only recharge when appropiate in order to preserve bigger prepays.
if cmetric < metric or bp > order.billed_until:
for order, price, cini, cend, metric, discounts in recharges:
line = self.generate_line(order, price, cini, cend, metric=metric,
computed=True, discounts=discounts)
lines.append(line)
if order.billed_until and order.cancelled_on and order.cancelled_on >= order.billed_until:
continue
if self.billing_period != self.NEVER:

View file

@ -56,9 +56,10 @@ class SiteDirective(Plugin):
def validate_uniqueness(self, directive, values, locations):
""" Validates uniqueness location, name and value """
errors = defaultdict(list)
value = directive.get('value', None)
# location uniqueness
location = None
if self.unique_location:
if self.unique_location and value is not None:
location = normurlpath(directive['value'].split()[0])
if location is not None and location in locations:
errors['value'].append(ValidationError(
@ -74,7 +75,6 @@ class SiteDirective(Plugin):
))
# value uniqueness
value = directive.get('value', None)
if value is not None:
if self.unique_value and value in values.get(self.name, []):
errors['value'].append(ValidationError(

View file

@ -30,7 +30,7 @@ def send_email_template(template, context, to, email_from=None, html=None, attac
#subject cannot have new lines
subject = render_to_string(template, {'subject': True}, context).strip()
message = render_to_string(template, {'message': True}, context)
message = render_to_string(template, {'message': True}, context).strip()
msg = EmailMultiAlternatives(subject, message, email_from, to, attachments=attachments)
if html:
html_message = render_to_string(html, {'message': True}, context)