diff --git a/TODO.md b/TODO.md
index 45421380..c7d14ed1 100644
--- a/TODO.md
+++ b/TODO.md
@@ -413,3 +413,4 @@ touch /tmp/somefile
# inherit registers from parent?
# Bill metric disk 5 GB: unialber: include not include 5, unialbert recheck period
+
diff --git a/orchestra/contrib/accounts/models.py b/orchestra/contrib/accounts/models.py
index 8bdd9d1b..13a62e3a 100644
--- a/orchestra/contrib/accounts/models.py
+++ b/orchestra/contrib/accounts/models.py
@@ -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
diff --git a/orchestra/contrib/bills/actions.py b/orchestra/contrib/bills/actions.py
index ce638b5a..48162ac2 100644
--- a/orchestra/contrib/bills/actions.py
+++ b/orchestra/contrib/bills/actions.py
@@ -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):
diff --git a/orchestra/contrib/bills/admin.py b/orchestra/contrib/bills/admin.py
index f834a15b..854094c6 100644
--- a/orchestra/contrib/bills/admin.py
+++ b/orchestra/contrib/bills/admin.py
@@ -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
diff --git a/orchestra/contrib/bills/filters.py b/orchestra/contrib/bills/filters.py
index 27e3ab17..b7fdd990 100644
--- a/orchestra/contrib/bills/filters.py
+++ b/orchestra/contrib/bills/filters.py
@@ -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")
diff --git a/orchestra/contrib/bills/locale/ca/LC_MESSAGES/django.po b/orchestra/contrib/bills/locale/ca/LC_MESSAGES/django.po
index 3c731913..787abd43 100644
--- a/orchestra/contrib/bills/locale/ca/LC_MESSAGES/django.po
+++ b/orchestra/contrib/bills/locale/ca/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2015-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
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"
+"Con vuestras cuotas, ademas de obtener conexion y multitud "
+"de servicios, estais
\n"
+" Con vuestras cuotas, ademas de obtener conexion y multitud de servicios,"
+"
\n"
+" Con vuestras cuotas, ademas de obtener conexion
\n"
+" Con vuestras cuotas, ademas de obtener
\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.
\n"
-" Please make sure to state your name and the %(type)s "
-"number.\n"
+" You can pay our %(type)s by bank transfer.
\n"
+" Please make sure to state your name and the %(type)s"
+"i> number.\n"
" Our bank account number is
\n"
" "
msgstr ""
"\n"
-"Pots pagar aquesta %(type)s per transferencia banacaria.
Inclou el teu "
-"nom i el numero de %(type)s. El nostre compte bancari és"
+"Pots pagar aquesta %(type)s per transferencia banacaria.
Inclou el "
+"teu nom i el numero de %(type)s. 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 %(type)s, please\n"
" feel free to contact us at your convinience. We will reply as "
"soon as we get\n"
" your message.\n"
diff --git a/orchestra/contrib/bills/models.py b/orchestra/contrib/bills/models.py
index 15d9ef5c..4cec7a24 100644
--- a/orchestra/contrib/bills/models.py
+++ b/orchestra/contrib/bills/models.py
@@ -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)
diff --git a/orchestra/contrib/bills/templates/bills/bill-notification.email b/orchestra/contrib/bills/templates/bills/bill-notification.email
index 425fc490..10bbbcdc 100644
--- a/orchestra/contrib/bills/templates/bills/bill-notification.email
+++ b/orchestra/contrib/bills/templates/bills/bill-notification.email
@@ -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 %}
diff --git a/orchestra/contrib/bills/templates/bills/microspective-fee.html b/orchestra/contrib/bills/templates/bills/microspective-fee.html
index 7ba9d6b4..a6449d33 100644
--- a/orchestra/contrib/bills/templates/bills/microspective-fee.html
+++ b/orchestra/contrib/bills/templates/bills/microspective-fee.html
@@ -1,4 +1,6 @@
{% extends 'bills/microspective.html' %}
+{% load i18n %}
+
{% block head %}