From fd8d805b5efb1c6c0ac64f9114b82cc5fc2a518e Mon Sep 17 00:00:00 2001 From: Marc Aymerich Date: Wed, 27 May 2015 14:05:25 +0000 Subject: [PATCH] Fixes on billing --- TODO.md | 1 + orchestra/contrib/accounts/models.py | 15 +- orchestra/contrib/bills/actions.py | 61 ++++- orchestra/contrib/bills/admin.py | 6 +- orchestra/contrib/bills/filters.py | 17 ++ .../bills/locale/ca/LC_MESSAGES/django.po | 243 +++++++++++------- orchestra/contrib/bills/models.py | 34 +-- .../templates/bills/bill-notification.email | 2 +- .../templates/bills/microspective-fee.html | 19 +- .../bills/templates/bills/microspective.html | 12 +- orchestra/contrib/orders/actions.py | 2 +- orchestra/contrib/orders/admin.py | 16 +- orchestra/contrib/services/handlers.py | 26 +- orchestra/contrib/websites/directives.py | 4 +- orchestra/utils/mail.py | 2 +- 15 files changed, 291 insertions(+), 169 deletions(-) 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 \n" "Language-Team: LANGUAGE \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 "One related transaction has been created" -msgstr "" - #: actions.py:82 #, python-format +msgid "One related transaction has been created" +msgstr "" + +#: actions.py:83 +#, python-format msgid "%i related transactions 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.

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 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 %}