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? # inherit registers from parent?
# Bill metric disk 5 GB: unialber: include not include 5, unialbert recheck period # 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.contrib.auth import models as auth
from django.conf import settings as djsettings
from django.core import validators from django.core import validators
from django.db import models from django.db import models
from django.apps import apps 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 django.utils.translation import ugettext_lazy as _
from orchestra.contrib.orchestration.middlewares import OperationsMiddleware 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(): for obj in getattr(self, rel.get_accessor_name()).all():
OperationsMiddleware.collect(Operation.SAVE, instance=obj, update_fields=[]) 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) contacts = self.contacts.filter(email_usages=contacts)
email_to = contacts.values_list('email', flat=True) 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): def get_full_name(self):
return self.full_name or self.short_name or self.username return self.full_name or self.short_name or self.username

View File

@ -1,4 +1,5 @@
import zipfile import zipfile
from datetime import date
from io import StringIO from io import StringIO
from django.contrib import messages from django.contrib import messages
@ -128,21 +129,53 @@ def undo_billing(modeladmin, request, queryset):
group[line.order].append(line) group[line.order].append(line)
except KeyError: except KeyError:
group[line.order] = [line] group[line.order] = [line]
# TODO force incomplete info
# Validate
for order, lines in group.items(): for order, lines in group.items():
# Find path from ini to end prev = None
for attr in ('order_id', 'order_billed_on', 'order_billed_until'): billed_on = date.max
if not getattr(self, attr): billed_until = date.max
raise ValidationError(_("Not enough information stored for undoing")) for line in sorted(lines, key=lambda l: l.start_on):
sorted(lines, key=lambda l: l.created_on) if billed_on is not None:
if 'a' != order.billed_on: if line.order_billed_on is None:
raise ValidationError(_("Dates don't match")) billed_on = line.order_billed_on
prev = order.billed_on else:
for ix in range(0, len(lines)): billed_on = min(billed_on, line.order_billed_on)
if lines[ix].order_b: # TODO we need to look at the periods here if billed_until is not None:
pass if line.order_billed_until is None:
order.billed_until = self.order_billed_until billed_until = line.order_billed_until
order.billed_on = self.order_billed_on 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 # TODO son't check for account equality
def move_lines(modeladmin, request, queryset): 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 orchestra.forms.widgets import paddingCheckboxSelectMultiple
from . import settings, actions 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 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', 'number', 'type_link', 'account_link', 'created_on_display',
'num_lines', 'display_total', 'display_payment_state', 'is_open', 'is_sent' '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') add_fields = ('account', 'type', 'is_open', 'due_on', 'comments')
fieldsets = ( fieldsets = (
(None, { (None, {
@ -188,7 +188,7 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
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]
created_on_display = admin_date('created_on') created_on_display = admin_date('created_on', short_description=_("Created"))
def num_lines(self, bill): def num_lines(self, bill):
return bill.lines__count 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): class HasBillContactListFilter(SimpleListFilter):
""" Filter Nodes by group according to request.user """ """ Filter Nodes by group according to request.user """
title = _("has bill contact") title = _("has bill contact")

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

View File

@ -128,6 +128,9 @@ class Bill(models.Model):
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))
def get_current_transaction(self):
return self.transactions.exclude_rejected().first()
@classmethod @classmethod
def get_class_type(cls): def get_class_type(cls):
return cls.__name__.upper() return cls.__name__.upper()
@ -187,7 +190,9 @@ class Bill(models.Model):
template=settings.BILLS_EMAIL_NOTIFICATION_TEMPLATE, template=settings.BILLS_EMAIL_NOTIFICATION_TEMPLATE,
context={ context={
'bill': self, 'bill': self,
'settings': settings,
}, },
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, html_to_pdf(html), 'application/pdf')
@ -288,7 +293,7 @@ class BillLine(models.Model):
created_on = models.DateField(_("created"), auto_now_add=True) created_on = models.DateField(_("created"), auto_now_add=True)
# Amendment # Amendment
amended_line = models.ForeignKey('self', verbose_name=_("amended line"), 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): def __str__(self):
return "#%i" % self.pk return "#%i" % self.pk
@ -302,24 +307,22 @@ class BillLine(models.Model):
return self.verbose_quantity or self.quantity return self.verbose_quantity or self.quantity
def get_verbose_period(self): 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: if not self.end_on:
return ini return ini
end = (self.end_on - datetime.timedelta(seconds=1)).strftime("%b, %Y")
if ini == end: if ini == end:
return ini return ini
return _("{ini} to {end}").format(ini=ini, end=end) return "{ini} / {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()
# def save(self, *args, **kwargs): # def save(self, *args, **kwargs):
# super(BillLine, self).save(*args, **kwargs) # super(BillLine, self).save(*args, **kwargs)
@ -342,7 +345,6 @@ class BillSubline(models.Model):
# TODO: order info for undoing # TODO: order info for undoing
line = models.ForeignKey(BillLine, verbose_name=_("bill line"), related_name='sublines') line = models.ForeignKey(BillLine, verbose_name=_("bill line"), related_name='sublines')
description = models.CharField(_("description"), max_length=256) description = models.CharField(_("description"), max_length=256)
# TODO rename to subtotal
total = models.DecimalField(max_digits=12, decimal_places=2) total = models.DecimalField(max_digits=12, decimal_places=2)
type = models.CharField(_("type"), max_length=16, choices=TYPES, default=OTHER) type = models.CharField(_("type"), max_length=16, choices=TYPES, default=OTHER)

View File

@ -1,6 +1,6 @@
{% if subject %}Bill {{ bill.number }}{% endif %} {% if subject %}Bill {{ bill.number }}{% endif %}
{% if message %}Dear {{ bill.account.username }}, {% 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 If you have any question, please write us at support@orchestra.lan
{% endif %} {% endif %}

View File

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

View File

@ -40,7 +40,7 @@
{% block summary %} {% block summary %}
<div id="bill-number"> <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> <span class="value">{{ bill.number }}</span><br>
</div> </div>
<div id="bill-summary"> <div id="bill-summary">
@ -54,7 +54,7 @@
<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 %}{{ 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> <psan class="value">{{ bill.closed_on | default:now | date }}</span>
</div> </div>
</div> </div>
@ -137,9 +137,9 @@
{% if payment.message %} {% if payment.message %}
{{ payment.message | safe }} {{ payment.message | safe }}
{% else %} {% else %}
{% blocktrans with type=bill.get_type_display %} {% blocktrans with type=bill.get_type_display.lower %}
You can pay our {{ type }} by bank transfer. <br> You can pay our <i>{{ type }}</i> by bank transfer. <br>
Please make sure to state your name and the {{ type }} 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 %}
<strong>{{ seller_info.bank_account }}</strong> <strong>{{ seller_info.bank_account }}</strong>
@ -148,7 +148,7 @@
<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 %}
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 feel free to contact us at your convinience. We will reply as soon as we get
your message. your message.
{% endblocktrans %} {% endblocktrans %}

View File

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

View File

@ -73,12 +73,18 @@ class OrderAdmin(AccountAdminMixin, ExtendedModelAdmin):
display_cancelled_on = admin_date('cancelled_on') display_cancelled_on = admin_date('cancelled_on')
def display_billed_until(self, order): def display_billed_until(self, order):
value = order.billed_until billed_until = order.billed_until
color = '' red = False
if value and value < timezone.now().date(): if billed_until:
color = 'style="color:red;"' 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( 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.short_description = _("billed until")
display_billed_until.allow_tags = True display_billed_until.allow_tags = True

View File

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

View File

@ -56,9 +56,10 @@ class SiteDirective(Plugin):
def validate_uniqueness(self, directive, values, locations): def validate_uniqueness(self, directive, values, locations):
""" Validates uniqueness location, name and value """ """ Validates uniqueness location, name and value """
errors = defaultdict(list) errors = defaultdict(list)
value = directive.get('value', None)
# location uniqueness # location uniqueness
location = None location = None
if self.unique_location: if self.unique_location and value is not None:
location = normurlpath(directive['value'].split()[0]) location = normurlpath(directive['value'].split()[0])
if location is not None and location in locations: if location is not None and location in locations:
errors['value'].append(ValidationError( errors['value'].append(ValidationError(
@ -74,7 +75,6 @@ class SiteDirective(Plugin):
)) ))
# value uniqueness # value uniqueness
value = directive.get('value', None)
if value is not None: if value is not None:
if self.unique_value and value in values.get(self.name, []): if self.unique_value and value in values.get(self.name, []):
errors['value'].append(ValidationError( 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 cannot have new lines
subject = render_to_string(template, {'subject': True}, context).strip() 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) msg = EmailMultiAlternatives(subject, message, email_from, to, attachments=attachments)
if html: if html:
html_message = render_to_string(html, {'message': True}, context) html_message = render_to_string(html, {'message': True}, context)