Fixes on billing
This commit is contained in:
parent
cf2215f604
commit
3963f6ce86
8
TODO.md
8
TODO.md
|
@ -13,7 +13,10 @@
|
||||||
|
|
||||||
* backend logs with hal logo
|
* backend logs with hal logo
|
||||||
|
|
||||||
* LAST version of this shit http://wkhtmltopdf.org/downloads.h otml
|
# LAST version of this shit http://wkhtmltopdf.org/downloads.h otml
|
||||||
|
#apt-get install xfonts-75dpi
|
||||||
|
#wget http://downloads.sourceforge.net/wkhtmltopdf/wkhtmltox-0.12.2.1_linux-jessie-amd64.deb
|
||||||
|
#dpkg -i wkhtmltox-0.12.2.1_linux-jessie-amd64.deb
|
||||||
|
|
||||||
* help_text on readonly_fields specialy Bill.state. (eg. A bill is in OPEN state when bla bla )
|
* help_text on readonly_fields specialy Bill.state. (eg. A bill is in OPEN state when bla bla )
|
||||||
|
|
||||||
|
@ -363,3 +366,6 @@ pip3 install https://github.com/fantix/gevent/archive/master.zip
|
||||||
# SIgnal handler for notify workers to reload stuff, like resource sync: https://docs.python.org/2/library/signal.html
|
# SIgnal handler for notify workers to reload stuff, like resource sync: https://docs.python.org/2/library/signal.html
|
||||||
|
|
||||||
# INVOICE fucking Id based on order ID or what?
|
# INVOICE fucking Id based on order ID or what?
|
||||||
|
|
||||||
|
# user order_id as bill line id
|
||||||
|
# BUG Delete related services also deletes account!
|
||||||
|
|
|
@ -123,6 +123,7 @@ function install_requirements () {
|
||||||
ORCHESTRA_PATH=$(get_orchestra_dir) || true
|
ORCHESTRA_PATH=$(get_orchestra_dir) || true
|
||||||
|
|
||||||
# lxml: libxml2-dev, libxslt1-dev, zlib1g-dev
|
# lxml: libxml2-dev, libxslt1-dev, zlib1g-dev
|
||||||
|
# wkhtmltopdf: xfonts-75dpi, xvfb
|
||||||
APT="python3 \
|
APT="python3 \
|
||||||
python3-pip \
|
python3-pip \
|
||||||
python3-dev \
|
python3-dev \
|
||||||
|
@ -131,12 +132,13 @@ function install_requirements () {
|
||||||
zlib1g-dev \
|
zlib1g-dev \
|
||||||
bind9utils \
|
bind9utils \
|
||||||
wkhtmltopdf \
|
wkhtmltopdf \
|
||||||
|
xfonts-75dpi \
|
||||||
xvfb \
|
xvfb \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
gettext \
|
gettext \
|
||||||
libcrack2-dev"
|
libcrack2-dev"
|
||||||
|
|
||||||
# cracklib and lxml are excluded on the requirements because they are hard to build
|
# cracklib and lxml are excluded on the requirements.txt because they need unconvinient system dependencies
|
||||||
PIP="$(wget https://raw.githubusercontent.com/glic3rinu/django-orchestra/master/requirements.txt -q -O - | tr '\n' ' ') \
|
PIP="$(wget https://raw.githubusercontent.com/glic3rinu/django-orchestra/master/requirements.txt -q -O - | tr '\n' ' ') \
|
||||||
cracklib \
|
cracklib \
|
||||||
lxml==3.3.5"
|
lxml==3.3.5"
|
||||||
|
@ -153,12 +155,17 @@ function install_requirements () {
|
||||||
coverage \
|
coverage \
|
||||||
flake8 \
|
flake8 \
|
||||||
django-debug-toolbar==1.3.0 \
|
django-debug-toolbar==1.3.0 \
|
||||||
https://github.com/django-nose/django-nose/archive/master.zip \
|
django-nose==1.4 \
|
||||||
sqlparse \
|
sqlparse \
|
||||||
pyinotify \
|
pyinotify \
|
||||||
PyMySQL"
|
PyMySQL"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Install a more recent version of wkhtmltopdf (0.12.2) (PDF page number support)
|
||||||
|
wkhtmltox=$(mktemp)
|
||||||
|
wget http://downloads.sourceforge.net/wkhtmltopdf/wkhtmltox-0.12.2.1_linux-jessie-amd64.deb -O ${wkhtmltox}
|
||||||
|
dpkg -i ${wkhtmltox}
|
||||||
|
|
||||||
# Make sure locales are in place before installing postgres
|
# Make sure locales are in place before installing postgres
|
||||||
if [[ $({ perl --help > /dev/null; } 2>&1|grep 'locale failed') ]]; then
|
if [[ $({ perl --help > /dev/null; } 2>&1|grep 'locale failed') ]]; then
|
||||||
run sed -i "s/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/" /etc/locale.gen
|
run sed -i "s/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/" /etc/locale.gen
|
||||||
|
|
|
@ -4,7 +4,7 @@ body {
|
||||||
margin: 40 auto !important;
|
margin: 40 auto !important;
|
||||||
/* margin-bottom: 30 !important;*/
|
/* margin-bottom: 30 !important;*/
|
||||||
float: none !important;
|
float: none !important;
|
||||||
font-family: Arial, 'Liberation Sans', 'DejaVu Sans', sans-serif;
|
font-family: sans;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
|
|
@ -35,8 +35,7 @@ class BatchDomainCreationAdminForm(forms.ModelForm):
|
||||||
if not cleaned_data['account']:
|
if not cleaned_data['account']:
|
||||||
account = None
|
account = None
|
||||||
for name in [cleaned_data['name']] + self.extra_names:
|
for name in [cleaned_data['name']] + self.extra_names:
|
||||||
domain = Domain(name=name)
|
parent = Domain.get_parent_domain(name)
|
||||||
parent = domain.get_parent()
|
|
||||||
if not parent:
|
if not parent:
|
||||||
# Fake an account to make django validation happy
|
# Fake an account to make django validation happy
|
||||||
account_model = self.fields['account']._queryset.model
|
account_model = self.fields['account']._queryset.model
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from django.conf import settings as djsettings
|
||||||
from django.core.mail.backends.base import BaseEmailBackend
|
from django.core.mail.backends.base import BaseEmailBackend
|
||||||
|
|
||||||
from .models import Message
|
from .models import Message
|
||||||
|
@ -20,7 +21,7 @@ class EmailBackend(BaseEmailBackend):
|
||||||
message = Message.objects.create(
|
message = Message.objects.create(
|
||||||
priority=priority,
|
priority=priority,
|
||||||
to_address=to_email,
|
to_address=to_email,
|
||||||
from_address=message.from_email,
|
from_address=getattr(message, 'from_email', djsettings.DEFAULT_FROM_EMAIL),
|
||||||
subject=message.subject,
|
subject=message.subject,
|
||||||
content=content,
|
content=content,
|
||||||
)
|
)
|
||||||
|
|
|
@ -38,12 +38,11 @@ class BilledOrderListFilter(SimpleListFilter):
|
||||||
return (
|
return (
|
||||||
('yes', _("Billed")),
|
('yes', _("Billed")),
|
||||||
('no', _("Not billed")),
|
('no', _("Not billed")),
|
||||||
|
('pending', _("Pending (re-evaluate metric)")),
|
||||||
|
('not_pending', _("Not pending (re-evaluate metric)")),
|
||||||
)
|
)
|
||||||
|
|
||||||
def queryset(self, request, queryset):
|
def get_pending_metric_pks(self, queryset):
|
||||||
if self.value() == 'yes':
|
|
||||||
return queryset.filter(billed_until__isnull=False, billed_until__gte=timezone.now())
|
|
||||||
elif self.value() == 'no':
|
|
||||||
mindelta = timedelta(days=2) # TODO
|
mindelta = timedelta(days=2) # TODO
|
||||||
metric_pks = []
|
metric_pks = []
|
||||||
prefetch_valid_metrics = Prefetch('metrics', to_attr='valid_metrics',
|
prefetch_valid_metrics = Prefetch('metrics', to_attr='valid_metrics',
|
||||||
|
@ -57,7 +56,7 @@ class BilledOrderListFilter(SimpleListFilter):
|
||||||
metric_queryset = queryset.exclude(service__metric='').exclude(billed_on__isnull=True)
|
metric_queryset = queryset.exclude(service__metric='').exclude(billed_on__isnull=True)
|
||||||
for order in metric_queryset.prefetch_related(prefetch_valid_metrics, prefetch_billed_metric):
|
for order in metric_queryset.prefetch_related(prefetch_valid_metrics, prefetch_billed_metric):
|
||||||
if len(order.billed_metric) != 1:
|
if len(order.billed_metric) != 1:
|
||||||
raise ValueError("Data inconsistency.")
|
raise ValueError("Data inconsistency #metrics %i != 1." % len(order.billed_metric))
|
||||||
billed_metric = order.billed_metric[0].value
|
billed_metric = order.billed_metric[0].value
|
||||||
for metric in order.valid_metrics:
|
for metric in order.valid_metrics:
|
||||||
if metric.created_on <= order.billed_on:
|
if metric.created_on <= order.billed_on:
|
||||||
|
@ -65,8 +64,22 @@ class BilledOrderListFilter(SimpleListFilter):
|
||||||
if metric.value > billed_metric:
|
if metric.value > billed_metric:
|
||||||
metric_pks.append(order.pk)
|
metric_pks.append(order.pk)
|
||||||
break
|
break
|
||||||
|
return metric_pks
|
||||||
|
|
||||||
|
def queryset(self, request, queryset):
|
||||||
|
if self.value() == 'yes':
|
||||||
|
return queryset.filter(billed_until__isnull=False, billed_until__gte=timezone.now())
|
||||||
|
elif self.value() == 'no':
|
||||||
|
return queryset.exclude(billed_until__isnull=False, billed_until__gte=timezone.now())
|
||||||
|
elif self.value() == 'pending':
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(pk__in=metric_pks) | Q(
|
Q(pk__in=self.get_pending_metric_pks(queryset)) | Q(
|
||||||
|
Q(billed_until__isnull=True) | Q(billed_until__lt=timezone.now())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif self.value() == 'not_pending':
|
||||||
|
return queryset.exclude(
|
||||||
|
Q(pk__in=self.get_pending_metric_pks(queryset)) | Q(
|
||||||
Q(billed_until__isnull=True) | Q(billed_until__lt=timezone.now())
|
Q(billed_until__isnull=True) | Q(billed_until__lt=timezone.now())
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -59,15 +59,15 @@ class OrderQuerySet(models.QuerySet):
|
||||||
|
|
||||||
def get_related(self, **options):
|
def get_related(self, **options):
|
||||||
""" returns related orders that could have a pricing effect """
|
""" returns related orders that could have a pricing effect """
|
||||||
|
# TODO for performance reasons get missing from queryset:
|
||||||
|
# TODO optimize this shit, don't get related if all objects are here
|
||||||
Service = apps.get_model(settings.ORDERS_SERVICE_MODEL)
|
Service = apps.get_model(settings.ORDERS_SERVICE_MODEL)
|
||||||
conflictive = self.filter(service__metric='')
|
conflictive = self.filter(service__metric='')
|
||||||
conflictive = conflictive.exclude(service__billing_period=Service.NEVER)
|
conflictive = conflictive.exclude(service__billing_period=Service.NEVER).exclude(service__rates__isnull=True)
|
||||||
conflictive = conflictive.select_related('service').group_by('account_id', 'service')
|
conflictive = conflictive.select_related('service').distinct().group_by('account_id', 'service')
|
||||||
qs = Q()
|
qs = Q()
|
||||||
for account_id, services in conflictive.items():
|
for account_id, services in conflictive.items():
|
||||||
for service, orders in services.items():
|
for service, orders in services.items():
|
||||||
if not service.rates.exists():
|
|
||||||
continue
|
|
||||||
ini = datetime.date.max
|
ini = datetime.date.max
|
||||||
end = datetime.date.min
|
end = datetime.date.min
|
||||||
bp = None
|
bp = None
|
||||||
|
@ -264,6 +264,12 @@ class MetricStorage(models.Model):
|
||||||
last = cls.objects.filter(order=order).latest()
|
last = cls.objects.filter(order=order).latest()
|
||||||
except cls.DoesNotExist:
|
except cls.DoesNotExist:
|
||||||
cls.objects.create(order=order, value=value, updated_on=now)
|
cls.objects.create(order=order, value=value, updated_on=now)
|
||||||
|
else:
|
||||||
|
# Metric storage has per-day granularity (last value of the day is what counts)
|
||||||
|
if last.created_on == now.date():
|
||||||
|
last.value = value
|
||||||
|
last.updated_on = now
|
||||||
|
last.save()
|
||||||
else:
|
else:
|
||||||
error = decimal.Decimal(str(settings.ORDERS_METRIC_ERROR))
|
error = decimal.Decimal(str(settings.ORDERS_METRIC_ERROR))
|
||||||
if value > last.value+error or value < last.value-error:
|
if value > last.value+error or value < last.value-error:
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
<h2><a href="{% url 'admin:accounts_account_change' account.pk %}">{{ account }}</a><span style="float:right">{{ total | floatformat:"-2" }} €</span></h2>
|
<h2><a href="{% url 'admin:accounts_account_change' account.pk %}">{{ account }}</a><span style="float:right">{{ total | floatformat:"-2" }} €</span></h2>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th style="width:30%;">Description</th> <th style="width:30%;">Period</th> <th style="width:10%;">Quantity</th> <th style="width:10%;">Price</th></tr>
|
<tr><th style="width:30%;">Description</th> <th style="width:30%;">Period</th> <th style="width:10%;">Size×Quantity</th> <th style="width:10%;">Price</th></tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for line in lines %}
|
{% for line in lines %}
|
||||||
|
@ -47,7 +47,7 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ line.ini | date }} to {{ line.end | date }}</td>
|
<td>{{ line.ini | date }} to {{ line.end | date }}</td>
|
||||||
<td>{{ line.size | floatformat:"-2" }}</td>
|
<td>{{ line.size | floatformat:"-2" }}×{{ line.metric | floatformat:"-2"}}</td>
|
||||||
<td>
|
<td>
|
||||||
{{ line.subtotal | floatformat:"-2" }} €
|
{{ line.subtotal | floatformat:"-2" }} €
|
||||||
{% for discount in line.discounts %}<br>{{ discount.total | floatformat:"-2" }} €{% endfor %}
|
{% for discount in line.discounts %}<br>{{ discount.total | floatformat:"-2" }} €{% endfor %}
|
||||||
|
|
|
@ -1,12 +1,19 @@
|
||||||
|
import textwrap
|
||||||
|
|
||||||
from orchestra.utils.sys import run
|
from orchestra.utils.sys import run
|
||||||
|
|
||||||
|
|
||||||
def html_to_pdf(html):
|
def html_to_pdf(html):
|
||||||
""" converts HTL to PDF using wkhtmltopdf """
|
""" converts HTL to PDF using wkhtmltopdf """
|
||||||
return run(
|
return run(textwrap.dedent("""\
|
||||||
'PATH=$PATH:/usr/local/bin/\n'
|
PATH=$PATH:/usr/local/bin/
|
||||||
'xvfb-run -a -s "-screen 0 640x4800x16" '
|
xvfb-run -a -s "-screen 0 2480x3508x16" wkhtmltopdf -q \\
|
||||||
'wkhtmltopdf -q --footer-center "Page [page] of [topage]" '
|
--use-xserver \\
|
||||||
' --footer-font-size 9 --margin-bottom 20 --margin-top 20 - -',
|
--footer-center "Page [page] of [topage]" \\
|
||||||
|
--footer-font-name sans \\
|
||||||
|
--footer-font-size 7 \\
|
||||||
|
--footer-spacing 7 \\
|
||||||
|
--margin-bottom 22 \\
|
||||||
|
--margin-top 20 - - """),
|
||||||
stdin=html.encode('utf-8')
|
stdin=html.encode('utf-8')
|
||||||
).stdout
|
).stdout
|
||||||
|
|
Loading…
Reference in a new issue