Fixed ransom bugs
This commit is contained in:
parent
711951d1bd
commit
5606b14e28
5
TODO.md
5
TODO.md
|
@ -186,7 +186,6 @@ require_once(‘/etc/moodles/’.$moodle_host.‘config.php’);``` moodle/drupl
|
||||||
* use server.name | server.address on python backends, like gitlab instead of settings?
|
* use server.name | server.address on python backends, like gitlab instead of settings?
|
||||||
|
|
||||||
* TODO raise404, here and everywhere
|
* TODO raise404, here and everywhere
|
||||||
# display subline links on billlines, to show that they exists.
|
|
||||||
* update service orders on a celery task? because it take alot
|
* update service orders on a celery task? because it take alot
|
||||||
|
|
||||||
# billline quantity eval('10x100') instead of miningless description '(10*100)' line.verbose_quantity
|
# billline quantity eval('10x100') instead of miningless description '(10*100)' line.verbose_quantity
|
||||||
|
@ -246,7 +245,6 @@ celery max-tasks-per-child
|
||||||
|
|
||||||
* autoscale celery workers http://docs.celeryproject.org/en/latest/userguide/workers.html#autoscaling
|
* autoscale celery workers http://docs.celeryproject.org/en/latest/userguide/workers.html#autoscaling
|
||||||
|
|
||||||
* webapp has_website list filter
|
|
||||||
|
|
||||||
glic3rinu's django-fluent-dashboard
|
glic3rinu's django-fluent-dashboard
|
||||||
* gevent is not ported to python3 :'(
|
* gevent is not ported to python3 :'(
|
||||||
|
@ -294,3 +292,6 @@ https://code.djangoproject.com/ticket/24576
|
||||||
* fpm reload starts new pools?
|
* fpm reload starts new pools?
|
||||||
* rename resource.monitors to resource.backends ?
|
* rename resource.monitors to resource.backends ?
|
||||||
* abstract model classes enabling overriding?
|
* abstract model classes enabling overriding?
|
||||||
|
|
||||||
|
# Ignore superusers & co on billing
|
||||||
|
# bill.totals make it 100% computed?
|
||||||
|
|
|
@ -36,7 +36,7 @@ def get_modeladmin(model, import_module=True):
|
||||||
def insertattr(model, name, value):
|
def insertattr(model, name, value):
|
||||||
""" Inserts attribute to a modeladmin """
|
""" Inserts attribute to a modeladmin """
|
||||||
modeladmin = None
|
modeladmin = None
|
||||||
if models.Model in model.__mro__:
|
if isinstance(model, models.Model)
|
||||||
modeladmin = get_modeladmin(model)
|
modeladmin = get_modeladmin(model)
|
||||||
modeladmin_class = type(modeladmin)
|
modeladmin_class = type(modeladmin)
|
||||||
elif not inspect.isclass(model):
|
elif not inspect.isclass(model):
|
||||||
|
|
|
@ -4,6 +4,8 @@ from django.contrib import admin
|
||||||
from django.contrib.admin.utils import unquote
|
from django.contrib.admin.utils import unquote
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.db.models import F, Sum
|
||||||
|
from django.db.models.functions import Coalesce
|
||||||
from django.templatetags.static import static
|
from django.templatetags.static import static
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
@ -58,7 +60,9 @@ class ClosedBillLineInline(BillLineInline):
|
||||||
# TODO reimplement as nested inlines when upstream
|
# TODO reimplement as nested inlines when upstream
|
||||||
# https://code.djangoproject.com/ticket/9025
|
# https://code.djangoproject.com/ticket/9025
|
||||||
|
|
||||||
fields = ('display_description', 'rate', 'quantity', 'tax', 'display_subtotal', 'display_total')
|
fields = (
|
||||||
|
'display_description', 'rate', 'quantity', 'tax', 'display_subtotal', 'display_total'
|
||||||
|
)
|
||||||
readonly_fields = fields
|
readonly_fields = fields
|
||||||
|
|
||||||
def display_description(self, line):
|
def display_description(self, line):
|
||||||
|
@ -77,6 +81,11 @@ class ClosedBillLineInline(BillLineInline):
|
||||||
display_subtotal.short_description = _("Subtotal")
|
display_subtotal.short_description = _("Subtotal")
|
||||||
display_subtotal.allow_tags = True
|
display_subtotal.allow_tags = True
|
||||||
|
|
||||||
|
def display_total(self, line):
|
||||||
|
return line.get_total()
|
||||||
|
display_total.short_description = _("Total")
|
||||||
|
display_total.allow_tags = True
|
||||||
|
|
||||||
def has_add_permission(self, request):
|
def has_add_permission(self, request):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -134,6 +143,7 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
change_view_actions = [
|
change_view_actions = [
|
||||||
actions.view_bill, actions.download_bills, actions.send_bills, actions.close_bills
|
actions.view_bill, actions.download_bills, actions.send_bills, actions.close_bills
|
||||||
]
|
]
|
||||||
|
search_fields = ('number', 'account__username', 'comments')
|
||||||
actions = [actions.download_bills, actions.close_bills, actions.send_bills]
|
actions = [actions.download_bills, actions.close_bills, actions.send_bills]
|
||||||
change_readonly_fields = ('account_link', 'type', 'is_open')
|
change_readonly_fields = ('account_link', 'type', 'is_open')
|
||||||
readonly_fields = ('number', 'display_total', 'is_sent', 'display_payment_state')
|
readonly_fields = ('number', 'display_total', 'is_sent', 'display_payment_state')
|
||||||
|
@ -147,10 +157,10 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
num_lines.short_description = _("lines")
|
num_lines.short_description = _("lines")
|
||||||
|
|
||||||
def display_total(self, bill):
|
def display_total(self, bill):
|
||||||
return "%s &%s;" % (bill.total, settings.BILLS_CURRENCY.lower())
|
return "%s &%s;" % (round(bill.totals, 2), settings.BILLS_CURRENCY.lower())
|
||||||
display_total.allow_tags = True
|
display_total.allow_tags = True
|
||||||
display_total.short_description = _("total")
|
display_total.short_description = _("total")
|
||||||
display_total.admin_order_field = 'total'
|
display_total.admin_order_field = 'totals'
|
||||||
|
|
||||||
def type_link(self, bill):
|
def type_link(self, bill):
|
||||||
bill_type = bill.type.lower()
|
bill_type = bill.type.lower()
|
||||||
|
@ -210,8 +220,8 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
def get_inline_instances(self, request, obj=None):
|
def get_inline_instances(self, request, obj=None):
|
||||||
inlines = super(BillAdmin, self).get_inline_instances(request, obj)
|
inlines = super(BillAdmin, self).get_inline_instances(request, obj)
|
||||||
if obj and not obj.is_open:
|
if obj and not obj.is_open:
|
||||||
return [inline for inline in inlines if not isinstance(inline, BillLineInline)]
|
return [inline for inline in inlines if type(inline) is not BillLineInline]
|
||||||
return [inline for inline in inlines if not isinstance(inline, ClosedBillLineInline)]
|
return [inline for inline in inlines if type(inline) is not ClosedBillLineInline]
|
||||||
|
|
||||||
def formfield_for_dbfield(self, db_field, **kwargs):
|
def formfield_for_dbfield(self, db_field, **kwargs):
|
||||||
""" Make value input widget bigger """
|
""" Make value input widget bigger """
|
||||||
|
@ -223,8 +233,13 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
qs = super(BillAdmin, self).get_queryset(request)
|
qs = super(BillAdmin, self).get_queryset(request)
|
||||||
qs = qs.annotate(models.Count('lines'))
|
qs = qs.annotate(
|
||||||
qs = qs.prefetch_related('lines', 'lines__sublines', 'transactions')
|
models.Count('lines'),
|
||||||
|
totals=Sum(
|
||||||
|
(F('lines__subtotal') + Coalesce(F('lines__sublines__total'), 0)) * (1+F('lines__tax')/100)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
qs = qs.prefetch_related('transactions')
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
def change_view(self, request, object_id, **kwargs):
|
def change_view(self, request, object_id, **kwargs):
|
||||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -2,6 +2,8 @@ from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
from django.core.validators import ValidationError, RegexValidator
|
from django.core.validators import ValidationError, RegexValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.db.models import F, Sum
|
||||||
|
from django.db.models.functions import Coalesce
|
||||||
from django.template import loader, Context
|
from django.template import loader, Context
|
||||||
from django.utils import timezone, translation
|
from django.utils import timezone, translation
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
|
@ -89,6 +91,7 @@ class Bill(models.Model):
|
||||||
is_sent = models.BooleanField(_("sent"), default=False)
|
is_sent = models.BooleanField(_("sent"), default=False)
|
||||||
due_on = models.DateField(_("due on"), null=True, blank=True)
|
due_on = models.DateField(_("due on"), null=True, blank=True)
|
||||||
updated_on = models.DateField(_("updated on"), auto_now=True)
|
updated_on = models.DateField(_("updated on"), auto_now=True)
|
||||||
|
# TODO allways compute total or what?
|
||||||
total = models.DecimalField(max_digits=12, decimal_places=2, default=0)
|
total = models.DecimalField(max_digits=12, decimal_places=2, default=0)
|
||||||
comments = models.TextField(_("comments"), blank=True)
|
comments = models.TextField(_("comments"), blank=True)
|
||||||
html = models.TextField(_("HTML"), blank=True)
|
html = models.TextField(_("HTML"), blank=True)
|
||||||
|
@ -227,18 +230,17 @@ class Bill(models.Model):
|
||||||
|
|
||||||
def get_subtotals(self):
|
def get_subtotals(self):
|
||||||
subtotals = {}
|
subtotals = {}
|
||||||
for line in self.lines.all():
|
lines = self.lines.annotate(totals=(F('subtotal') + Coalesce(F('sublines__total'), 0)))
|
||||||
subtotal, taxes = subtotals.get(line.tax, (0, 0))
|
for tax, total in lines.values_list('tax', 'totals'):
|
||||||
subtotal += line.get_total()
|
subtotal, taxes = subtotals.get(tax, (0, 0))
|
||||||
subtotals[line.tax] = (subtotal, (line.tax/100)*subtotal)
|
subtotal += total
|
||||||
|
subtotals[tax] = (subtotal, round(tax/100*subtotal, 2))
|
||||||
return subtotals
|
return subtotals
|
||||||
|
|
||||||
def get_total(self):
|
def get_total(self):
|
||||||
total = 0
|
totals = self.lines.annotate(
|
||||||
for tax, subtotal in self.get_subtotals().items():
|
totals=(F('subtotal') + Coalesce(F('sublines__total'), 0)) * (1+F('tax')/100))
|
||||||
subtotal, taxes = subtotal
|
return round(totals.aggregate(Sum('totals'))['totals__sum'], 2)
|
||||||
total += subtotal + taxes
|
|
||||||
return total
|
|
||||||
|
|
||||||
|
|
||||||
class Invoice(Bill):
|
class Invoice(Bill):
|
||||||
|
@ -272,12 +274,12 @@ class BillLine(models.Model):
|
||||||
description = models.CharField(_("description"), max_length=256)
|
description = models.CharField(_("description"), max_length=256)
|
||||||
rate = models.DecimalField(_("rate"), blank=True, null=True, max_digits=12, decimal_places=2)
|
rate = models.DecimalField(_("rate"), blank=True, null=True, max_digits=12, decimal_places=2)
|
||||||
quantity = models.DecimalField(_("quantity"), max_digits=12, decimal_places=2)
|
quantity = models.DecimalField(_("quantity"), max_digits=12, decimal_places=2)
|
||||||
|
verbose_quantity = models.CharField(_("Verbose quantity"), max_length=16)
|
||||||
subtotal = models.DecimalField(_("subtotal"), max_digits=12, decimal_places=2)
|
subtotal = models.DecimalField(_("subtotal"), max_digits=12, decimal_places=2)
|
||||||
tax = models.DecimalField(_("tax"), max_digits=2, decimal_places=2)
|
tax = models.DecimalField(_("tax"), max_digits=4, decimal_places=2)
|
||||||
# Undo
|
# Undo
|
||||||
# initial = models.DateTimeField(null=True)
|
# initial = models.DateTimeField(null=True)
|
||||||
# end = models.DateTimeField(null=True)
|
# end = models.DateTimeField(null=True)
|
||||||
|
|
||||||
order = models.ForeignKey(settings.BILLS_ORDER_MODEL, null=True, blank=True,
|
order = models.ForeignKey(settings.BILLS_ORDER_MODEL, null=True, blank=True,
|
||||||
help_text=_("Informative link back to the order"), on_delete=models.SET_NULL)
|
help_text=_("Informative link back to the order"), on_delete=models.SET_NULL)
|
||||||
order_billed_on = models.DateField(_("order billed"), null=True, blank=True)
|
order_billed_on = models.DateField(_("order billed"), null=True, blank=True)
|
||||||
|
@ -297,10 +299,11 @@ class BillLine(models.Model):
|
||||||
|
|
||||||
def get_total(self):
|
def get_total(self):
|
||||||
""" Computes subline discounts """
|
""" Computes subline discounts """
|
||||||
total = self.subtotal
|
if self.pk:
|
||||||
for subline in self.sublines.all():
|
return self.subtotal + sum(self.sublines.values_list('total', flat=True))
|
||||||
total += subline.total
|
|
||||||
return total
|
def get_verbose_quantity(self):
|
||||||
|
return self.verbose_quantity or self.quantity
|
||||||
|
|
||||||
def undo(self):
|
def undo(self):
|
||||||
# TODO warn user that undoing bills with compensations lead to compensation lost
|
# TODO warn user that undoing bills with compensations lead to compensation lost
|
||||||
|
@ -313,12 +316,11 @@ class BillLine(models.Model):
|
||||||
self.order.billed_on = self.order_billed_on
|
self.order.billed_on = self.order_billed_on
|
||||||
self.delete()
|
self.delete()
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
# def save(self, *args, **kwargs):
|
||||||
# TODO cost and consistency of this shit
|
# super(BillLine, self).save(*args, **kwargs)
|
||||||
super(BillLine, self).save(*args, **kwargs)
|
# if self.bill.is_open:
|
||||||
if self.bill.is_open:
|
# self.bill.total = self.bill.get_total()
|
||||||
self.bill.total = self.bill.get_total()
|
# self.bill.save(update_fields=['total'])
|
||||||
self.bill.save(update_fields=['total'])
|
|
||||||
|
|
||||||
|
|
||||||
class BillSubline(models.Model):
|
class BillSubline(models.Model):
|
||||||
|
@ -339,12 +341,12 @@ class BillSubline(models.Model):
|
||||||
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)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
# def save(self, *args, **kwargs):
|
||||||
# TODO cost of this shit
|
# # TODO cost of this shit
|
||||||
super(BillSubline, self).save(*args, **kwargs)
|
# super(BillSubline, self).save(*args, **kwargs)
|
||||||
if self.line.bill.is_open:
|
# if self.line.bill.is_open:
|
||||||
self.line.bill.total = self.line.bill.get_total()
|
# self.line.bill.total = self.line.bill.get_total()
|
||||||
self.line.bill.save(update_fields=['total'])
|
# self.line.bill.save(update_fields=['total'])
|
||||||
|
|
||||||
|
|
||||||
accounts.register(Bill)
|
accounts.register(Bill)
|
||||||
|
|
|
@ -79,7 +79,7 @@
|
||||||
{% with sublines=line.sublines.all %}
|
{% with sublines=line.sublines.all %}
|
||||||
<span class="{% if not sublines %}last {% endif %}column-id">{{ line.id }}</span>
|
<span class="{% if not sublines %}last {% endif %}column-id">{{ line.id }}</span>
|
||||||
<span class="{% if not sublines %}last {% endif %}column-description">{{ line.description }}</span>
|
<span class="{% if not sublines %}last {% endif %}column-description">{{ line.description }}</span>
|
||||||
<span class="{% if not sublines %}last {% endif %}column-quantity">{{ line.quantity|default:" " }}</span>
|
<span class="{% if not sublines %}last {% endif %}column-quantity">{{ line.get_verbose_quantity|default:" "|safe }}</span>
|
||||||
<span class="{% if not sublines %}last {% endif %}column-rate">{% if line.rate %}{{ line.rate }} &{{ currency.lower }};{% else %} {% endif %}</span>
|
<span class="{% if not sublines %}last {% endif %}column-rate">{% if line.rate %}{{ line.rate }} &{{ currency.lower }};{% else %} {% endif %}</span>
|
||||||
<span class="{% if not sublines %}last {% endif %}column-subtotal">{{ line.subtotal }} &{{ currency.lower }};</span>
|
<span class="{% if not sublines %}last {% endif %}column-subtotal">{{ line.subtotal }} &{{ currency.lower }};</span>
|
||||||
<br>
|
<br>
|
||||||
|
@ -96,7 +96,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div id="totals">
|
<div id="totals">
|
||||||
<br> <br>
|
<br> <br>
|
||||||
{% for tax, subtotal in bill.get_subtotals.iteritems %}
|
{% for tax, subtotal in bill.get_subtotals.items %}
|
||||||
<span class="subtotal column-title">subtotal {{ tax }}% {% trans "VAT" %}</span>
|
<span class="subtotal column-title">subtotal {{ tax }}% {% trans "VAT" %}</span>
|
||||||
<span class="subtotal column-value">{{ subtotal | first }} &{{ currency.lower }};</span>
|
<span class="subtotal column-value">{{ subtotal | first }} &{{ currency.lower }};</span>
|
||||||
<br>
|
<br>
|
||||||
|
|
|
@ -37,7 +37,7 @@ class RouteAdmin(admin.ModelAdmin):
|
||||||
for backend, __ in ServiceBackend.get_choices()
|
for backend, __ in ServiceBackend.get_choices()
|
||||||
}
|
}
|
||||||
DEFAULT_MATCH = {
|
DEFAULT_MATCH = {
|
||||||
backend.get_name(): backend.default_route_match for backend in ServiceBackend.get_backends(active=False)
|
backend.get_name(): backend.default_route_match for backend in ServiceBackend.get_backends()
|
||||||
}
|
}
|
||||||
|
|
||||||
def display_model(self, route):
|
def display_model(self, route):
|
||||||
|
|
|
@ -99,15 +99,12 @@ class ServiceBackend(plugins.Plugin, metaclass=ServiceMount):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_backends(cls, instance=None, action=None, active=True):
|
def get_backends(cls, instance=None, action=None, active=None):
|
||||||
from .models import Route
|
|
||||||
backends = cls.get_plugins()
|
backends = cls.get_plugins()
|
||||||
included = []
|
included = []
|
||||||
if active:
|
|
||||||
active_backends = Route.objects.filter(is_active=True).values_list('backend', flat=True)
|
|
||||||
# Filter for instance or action
|
# Filter for instance or action
|
||||||
for backend in backends:
|
for backend in backends:
|
||||||
if active and backend.get_name() not in active_backends:
|
if active is not None and backend.get_name() not in active:
|
||||||
continue
|
continue
|
||||||
include = True
|
include = True
|
||||||
if instance:
|
if instance:
|
||||||
|
@ -208,5 +205,5 @@ class ServiceController(ServiceBackend):
|
||||||
""" filter controller classes """
|
""" filter controller classes """
|
||||||
backends = super(ServiceController, cls).get_backends()
|
backends = super(ServiceController, cls).get_backends()
|
||||||
return [
|
return [
|
||||||
backend for backend in backends if ServiceController in backend.__mro__
|
backend for backend in backends if isinstance(backend, ServiceController)
|
||||||
]
|
]
|
||||||
|
|
|
@ -141,7 +141,8 @@ def collect(instance, action, **kwargs):
|
||||||
""" collect operations """
|
""" collect operations """
|
||||||
operations = kwargs.get('operations', set())
|
operations = kwargs.get('operations', set())
|
||||||
route_cache = kwargs.get('route_cache', {})
|
route_cache = kwargs.get('route_cache', {})
|
||||||
for backend_cls in ServiceBackend.get_backends():
|
active_backends = kwargs.get('active_backends', None)
|
||||||
|
for backend_cls in ServiceBackend.get_backends(active=active_backends):
|
||||||
# Check if there exists a related instance to be executed for this backend and action
|
# Check if there exists a related instance to be executed for this backend and action
|
||||||
instances = []
|
instances = []
|
||||||
if action in backend_cls.actions:
|
if action in backend_cls.actions:
|
||||||
|
|
|
@ -6,13 +6,16 @@ from django.db.models.signals import pre_delete, post_save, m2m_changed
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.http.response import HttpResponseServerError
|
from django.http.response import HttpResponseServerError
|
||||||
|
|
||||||
from orchestra.utils.python import OrderedSet
|
from orchestra.utils.python import OrderedSet, import_class
|
||||||
|
|
||||||
from . import manager, Operation
|
from . import manager, Operation, settings
|
||||||
from .helpers import message_user
|
from .helpers import message_user
|
||||||
from .models import BackendLog
|
from .models import BackendLog
|
||||||
|
|
||||||
|
|
||||||
|
router = import_class(settings.ORCHESTRATION_ROUTER)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, dispatch_uid='orchestration.post_save_collector')
|
@receiver(post_save, dispatch_uid='orchestration.post_save_collector')
|
||||||
def post_save_collector(sender, *args, **kwargs):
|
def post_save_collector(sender, *args, **kwargs):
|
||||||
if sender not in [BackendLog, Operation]:
|
if sender not in [BackendLog, Operation]:
|
||||||
|
@ -63,6 +66,16 @@ class OperationsMiddleware(object):
|
||||||
return request.route_cache
|
return request.route_cache
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_active_cache(cls):
|
||||||
|
""" chache the routes to save sql queries """
|
||||||
|
if hasattr(cls.thread_locals, 'request'):
|
||||||
|
request = cls.thread_locals.request
|
||||||
|
if not hasattr(request, 'active_cache'):
|
||||||
|
request.active_cache = router.get_active_backends()
|
||||||
|
return request.active_cache
|
||||||
|
return router.get_active_backends()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def collect(cls, action, **kwargs):
|
def collect(cls, action, **kwargs):
|
||||||
""" Collects all pending operations derived from model signals """
|
""" Collects all pending operations derived from model signals """
|
||||||
|
@ -71,6 +84,7 @@ class OperationsMiddleware(object):
|
||||||
return
|
return
|
||||||
kwargs['operations'] = cls.get_pending_operations()
|
kwargs['operations'] = cls.get_pending_operations()
|
||||||
kwargs['route_cache'] = cls.get_route_cache()
|
kwargs['route_cache'] = cls.get_route_cache()
|
||||||
|
kwargs['active_backends'] = cls.get_active_cache()
|
||||||
instance = kwargs.pop('instance')
|
instance = kwargs.pop('instance')
|
||||||
manager.collect(instance, action, **kwargs)
|
manager.collect(instance, action, **kwargs)
|
||||||
|
|
||||||
|
|
|
@ -169,6 +169,10 @@ class Route(models.Model):
|
||||||
servers.append(route.host)
|
servers.append(route.host)
|
||||||
return servers
|
return servers
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_active_backends(cls):
|
||||||
|
return cls.objects.filter(is_active=True).values_list('backend', flat=True)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if not self.match:
|
if not self.match:
|
||||||
self.match = 'True'
|
self.match = 'True'
|
||||||
|
|
|
@ -33,18 +33,20 @@ class BillsBackend(object):
|
||||||
bill = Invoice.objects.create(account=account, is_open=True)
|
bill = Invoice.objects.create(account=account, is_open=True)
|
||||||
bills.append(bill)
|
bills.append(bill)
|
||||||
# Create bill line
|
# Create bill line
|
||||||
billine = bill.lines.create(
|
quantity = line.metric*line.size
|
||||||
|
if quantity != 0:
|
||||||
|
billine = bill.lines.create(
|
||||||
rate=service.nominal_price,
|
rate=service.nominal_price,
|
||||||
quantity=line.metric*line.size,
|
quantity=line.metric*line.size,
|
||||||
|
verbose_quantity=self.get_verbose_quantity(line),
|
||||||
subtotal=line.subtotal,
|
subtotal=line.subtotal,
|
||||||
tax=service.tax,
|
tax=service.tax,
|
||||||
description=self.get_line_description(line),
|
description=self.get_line_description(line),
|
||||||
|
|
||||||
order=line.order,
|
order=line.order,
|
||||||
order_billed_on=line.order.old_billed_on,
|
order_billed_on=line.order.old_billed_on,
|
||||||
order_billed_until=line.order.old_billed_until
|
order_billed_until=line.order.old_billed_until
|
||||||
)
|
)
|
||||||
self.create_sublines(billine, line.discounts)
|
self.create_sublines(billine, line.discounts)
|
||||||
return bills
|
return bills
|
||||||
|
|
||||||
def format_period(self, ini, end):
|
def format_period(self, ini, end):
|
||||||
|
@ -61,12 +63,24 @@ class BillsBackend(object):
|
||||||
description = line.order.description
|
description = line.order.description
|
||||||
if service.billing_period != service.NEVER:
|
if service.billing_period != service.NEVER:
|
||||||
description += " %s" % self.format_period(line.ini, line.end)
|
description += " %s" % self.format_period(line.ini, line.end)
|
||||||
if service.metric and service.billing_period != service.NEVER and service.pricing_period == service.NEVER:
|
|
||||||
metric = format(line.metric, '.2f').rstrip('0').rstrip('.')
|
|
||||||
size = format(line.size, '.2f').rstrip('0').rstrip('.')
|
|
||||||
description += " (%sx%s)" % (metric, size)
|
|
||||||
return description
|
return description
|
||||||
|
|
||||||
|
def get_verbose_quantity(self, line):
|
||||||
|
# service = line.order.service
|
||||||
|
# if service.metric and service.billing_period != service.NEVER and service.pricing_period == service.NEVER:
|
||||||
|
metric = format(line.metric, '.2f').rstrip('0').rstrip('.')
|
||||||
|
if metric.endswith('.00'):
|
||||||
|
metric = metric.split('.')[0]
|
||||||
|
size = format(line.size, '.2f').rstrip('0').rstrip('.')
|
||||||
|
if size.endswith('.00'):
|
||||||
|
size = metric.split('.')[0]
|
||||||
|
if metric == '1':
|
||||||
|
return size
|
||||||
|
if size == '1':
|
||||||
|
return metric
|
||||||
|
return "%s×%s" % (metric, size)
|
||||||
|
# return ''
|
||||||
|
|
||||||
def create_sublines(self, line, discounts):
|
def create_sublines(self, line, discounts):
|
||||||
for discount in discounts:
|
for discount in discounts:
|
||||||
line.sublines.create(
|
line.sublines.create(
|
||||||
|
|
|
@ -30,8 +30,6 @@ class Last(Aggregation):
|
||||||
return dataset.none()
|
return dataset.none()
|
||||||
|
|
||||||
def compute_usage(self, dataset):
|
def compute_usage(self, dataset):
|
||||||
# FIXME Aggregation of 0s returns None! django bug?
|
|
||||||
# value = dataset.aggregate(models.Sum('value'))['value__sum']
|
|
||||||
values = dataset.values_list('value', flat=True)
|
values = dataset.values_list('value', flat=True)
|
||||||
if values:
|
if values:
|
||||||
return sum(values)
|
return sum(values)
|
||||||
|
|
|
@ -21,7 +21,7 @@ class ServiceMonitor(ServiceBackend):
|
||||||
def get_plugins(cls):
|
def get_plugins(cls):
|
||||||
""" filter controller classes """
|
""" filter controller classes """
|
||||||
return [
|
return [
|
||||||
plugin for plugin in cls.plugins if ServiceMonitor in plugin.__mro__
|
plugin for plugin in cls.plugins if isinstance(plugin, ServiceMonitor)
|
||||||
]
|
]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
@ -11,7 +11,7 @@ from django.utils.translation import ugettext, ugettext_lazy as _
|
||||||
|
|
||||||
from orchestra import plugins
|
from orchestra import plugins
|
||||||
from orchestra.utils.humanize import text2int
|
from orchestra.utils.humanize import text2int
|
||||||
from orchestra.utils.python import AttrDict
|
from orchestra.utils.python import AttrDict, cmp_to_key
|
||||||
|
|
||||||
from . import settings, helpers
|
from . import settings, helpers
|
||||||
|
|
||||||
|
@ -399,6 +399,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
||||||
pend = order.billed_until or order.registered_on
|
pend = order.billed_until or order.registered_on
|
||||||
pini = pend - rdelta
|
pini = pend - rdelta
|
||||||
metric = self.get_register_or_renew_events(porders, pini, pend)
|
metric = self.get_register_or_renew_events(porders, pini, pend)
|
||||||
|
position = min(position, metric)
|
||||||
price = self.get_price(account, metric, position=position, rates=rates)
|
price = self.get_price(account, metric, position=position, rates=rates)
|
||||||
ini = order.billed_until or order.registered_on
|
ini = order.billed_until or order.registered_on
|
||||||
end = order.new_billed_until
|
end = order.new_billed_until
|
||||||
|
@ -445,8 +446,8 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
||||||
if self.payment_style == self.PREPAY and self.on_cancel == self.COMPENSATE:
|
if self.payment_style == self.PREPAY and self.on_cancel == self.COMPENSATE:
|
||||||
# Get orders pending for compensation
|
# Get orders pending for compensation
|
||||||
givers = list(related_orders.givers(ini, end))
|
givers = list(related_orders.givers(ini, end))
|
||||||
givers.sort(cmp=helpers.cmp_billed_until_or_registered_on)
|
givers = sorted(givers, key=cmp_to_key(helpers.cmp_billed_until_or_registered_on))
|
||||||
orders.sort(cmp=helpers.cmp_billed_until_or_registered_on)
|
orders = sorted(orders, key=cmp_to_key(helpers.cmp_billed_until_or_registered_on))
|
||||||
self.assign_compensations(givers, orders, **options)
|
self.assign_compensations(givers, orders, **options)
|
||||||
|
|
||||||
rates = self.get_rates(account)
|
rates = self.get_rates(account)
|
||||||
|
@ -459,7 +460,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
|
||||||
ini -= rdelta
|
ini -= rdelta
|
||||||
porders = related_orders.pricing_orders(ini, end)
|
porders = related_orders.pricing_orders(ini, end)
|
||||||
porders = list(set(orders).union(set(porders)))
|
porders = list(set(orders).union(set(porders)))
|
||||||
porders.sort(cmp=helpers.cmp_billed_until_or_registered_on)
|
porders = sorted(porders, key=cmp_to_key(helpers.cmp_billed_until_or_registered_on))
|
||||||
if concurrent:
|
if concurrent:
|
||||||
# Periodic billing with no pricing period
|
# Periodic billing with no pricing period
|
||||||
lines = self.bill_concurrent_orders(account, porders, rates, ini, end)
|
lines = self.bill_concurrent_orders(account, porders, rates, ini, end)
|
||||||
|
|
|
@ -214,6 +214,8 @@ class Service(models.Model):
|
||||||
ant_counter = counter
|
ant_counter = counter
|
||||||
accumulated += rate['price'] * rate['quantity']
|
accumulated += rate['price'] * rate['quantity']
|
||||||
else:
|
else:
|
||||||
|
if metric < position:
|
||||||
|
raise ValueError("Metric can not be less than the position.")
|
||||||
for rate in rates:
|
for rate in rates:
|
||||||
counter += rate['quantity']
|
counter += rate['quantity']
|
||||||
if counter >= position:
|
if counter >= position:
|
||||||
|
|
|
@ -118,12 +118,11 @@ class PHPBackend(WebAppServiceMixin, ServiceController):
|
||||||
super(PHPBackend, self).commit()
|
super(PHPBackend, self).commit()
|
||||||
|
|
||||||
def get_fpm_config(self, webapp, context):
|
def get_fpm_config(self, webapp, context):
|
||||||
merge = settings.WEBAPPS_MERGE_PHP_WEBAPPS
|
options = webapp.get_options(merge=self.MERGE)
|
||||||
context.update({
|
context.update({
|
||||||
'init_vars': webapp.type_instance.get_php_init_vars(merge=self.MERGE),
|
'init_vars': webapp.type_instance.get_php_init_vars(merge=self.MERGE),
|
||||||
'max_children': webapp.get_options().get('processes',
|
'max_children': options.get('processes', settings.WEBAPPS_FPM_DEFAULT_MAX_CHILDREN),
|
||||||
settings.WEBAPPS_FPM_DEFAULT_MAX_CHILDREN),
|
'request_terminate_timeout': options.get('timeout', False),
|
||||||
'request_terminate_timeout': webapp.get_options().get('timeout', False),
|
|
||||||
})
|
})
|
||||||
context['fpm_listen'] = webapp.type_instance.FPM_LISTEN % context
|
context['fpm_listen'] = webapp.type_instance.FPM_LISTEN % context
|
||||||
fpm_config = Template(textwrap.dedent("""\
|
fpm_config = Template(textwrap.dedent("""\
|
||||||
|
@ -139,7 +138,7 @@ class PHPBackend(WebAppServiceMixin, ServiceController):
|
||||||
pm.max_requests = {{ max_requests }}
|
pm.max_requests = {{ max_requests }}
|
||||||
pm.max_children = {{ max_children }}
|
pm.max_children = {{ max_children }}
|
||||||
{% if request_terminate_timeout %}request_terminate_timeout = {{ request_terminate_timeout }}{% endif %}
|
{% if request_terminate_timeout %}request_terminate_timeout = {{ request_terminate_timeout }}{% endif %}
|
||||||
{% for name, value in init_vars.iteritems %}
|
{% for name, value in init_vars.items %}
|
||||||
php_admin_value[{{ name | safe }}] = {{ value | safe }}{% endfor %}
|
php_admin_value[{{ name | safe }}] = {{ value | safe }}{% endfor %}
|
||||||
"""
|
"""
|
||||||
))
|
))
|
||||||
|
@ -168,9 +167,10 @@ class PHPBackend(WebAppServiceMixin, ServiceController):
|
||||||
exec %(php_binary_path)s %(php_init_vars)s""") % context
|
exec %(php_binary_path)s %(php_init_vars)s""") % context
|
||||||
|
|
||||||
def get_fcgid_cmd_options(self, webapp, context):
|
def get_fcgid_cmd_options(self, webapp, context):
|
||||||
|
options = webapp.get_options(merge=self.MERGE)
|
||||||
maps = {
|
maps = {
|
||||||
'MaxProcesses': webapp.get_options().get('processes', None),
|
'MaxProcesses': options.get('processes', None),
|
||||||
'IOTimeout': webapp.get_options().get('timeout', None),
|
'IOTimeout': options.get('timeout', None),
|
||||||
}
|
}
|
||||||
cmd_options = []
|
cmd_options = []
|
||||||
for directive, value in maps.items():
|
for directive, value in maps.items():
|
||||||
|
|
|
@ -39,7 +39,6 @@ class WordPressBackend(WebAppServiceMixin, ServiceController):
|
||||||
exc('wget http://wordpress.org/latest.tar.gz -O - --no-check-certificate | tar -xzvf - -C %(app_path)s --strip-components=1');
|
exc('wget http://wordpress.org/latest.tar.gz -O - --no-check-certificate | tar -xzvf - -C %(app_path)s --strip-components=1');
|
||||||
exc('mkdir %(app_path)s/wp-content/uploads');
|
exc('mkdir %(app_path)s/wp-content/uploads');
|
||||||
exc('chmod 750 %(app_path)s/wp-content/uploads');
|
exc('chmod 750 %(app_path)s/wp-content/uploads');
|
||||||
exc('chown -R %(user)s:%(group)s %(app_path)s');
|
|
||||||
|
|
||||||
$config_file = file('%(app_path)s/' . 'wp-config-sample.php');
|
$config_file = file('%(app_path)s/' . 'wp-config-sample.php');
|
||||||
$secret_keys = file_get_contents('https://api.wordpress.org/secret-key/1.1/salt/');
|
$secret_keys = file_get_contents('https://api.wordpress.org/secret-key/1.1/salt/');
|
||||||
|
@ -70,6 +69,8 @@ class WordPressBackend(WebAppServiceMixin, ServiceController):
|
||||||
foreach ( $config_file as $line_num => $line ) {
|
foreach ( $config_file as $line_num => $line ) {
|
||||||
fwrite($fw, $line);
|
fwrite($fw, $line);
|
||||||
}
|
}
|
||||||
|
exc('chown -R %(user)s:%(group)s %(app_path)s');
|
||||||
|
|
||||||
define('WP_CONTENT_DIR', 'wp-content/');
|
define('WP_CONTENT_DIR', 'wp-content/');
|
||||||
define('WP_LANG_DIR', WP_CONTENT_DIR . '/languages' );
|
define('WP_LANG_DIR', WP_CONTENT_DIR . '/languages' );
|
||||||
define('WP_USE_THEMES', true);
|
define('WP_USE_THEMES', true);
|
||||||
|
|
|
@ -56,8 +56,17 @@ class WebApp(models.Model):
|
||||||
self.data = apptype.clean_data()
|
self.data = apptype.clean_data()
|
||||||
|
|
||||||
@cached
|
@cached
|
||||||
def get_options(self):
|
def get_options(self, merge=False):
|
||||||
return OrderedDict((opt.name, opt.value) for opt in self.options.all().order_by('name'))
|
if merge:
|
||||||
|
options = OrderedDict()
|
||||||
|
qs = WebAppOption.objects.filter(webapp__account=self.account, webapp__type=self.type)
|
||||||
|
for name, value in qs.values_list('name', 'value').order_by('name'):
|
||||||
|
if name in options:
|
||||||
|
options[name] = max(options[name], value)
|
||||||
|
else:
|
||||||
|
options[name] = value
|
||||||
|
return options
|
||||||
|
return OrderedDict(self.options.values_list('name', 'value').order_by('name'))
|
||||||
|
|
||||||
def get_directive(self):
|
def get_directive(self):
|
||||||
return self.type_instance.get_directive()
|
return self.type_instance.get_directive()
|
||||||
|
|
|
@ -24,6 +24,7 @@ WEBAPPS_PHPFPM_POOL_PATH = getattr(settings, 'WEBAPPS_PHPFPM_POOL_PATH',
|
||||||
|
|
||||||
WEBAPPS_FCGID_WRAPPER_PATH = getattr(settings, 'WEBAPPS_FCGID_WRAPPER_PATH',
|
WEBAPPS_FCGID_WRAPPER_PATH = getattr(settings, 'WEBAPPS_FCGID_WRAPPER_PATH',
|
||||||
# Inside SuExec Document root
|
# Inside SuExec Document root
|
||||||
|
# Make sure all account wrappers are in the same DIR
|
||||||
'/home/httpd/fcgi-bin.d/%(user)s/%(app_name)s-wrapper'
|
'/home/httpd/fcgi-bin.d/%(user)s/%(app_name)s-wrapper'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -192,10 +192,11 @@ class Apache2Backend(ServiceController):
|
||||||
'wrapper_name': os.path.basename(wrapper_path),
|
'wrapper_name': os.path.basename(wrapper_path),
|
||||||
})
|
})
|
||||||
directives = ''
|
directives = ''
|
||||||
# This Alias trick is used instead of FcgidWrapper because we don't want to define
|
# This Action trick is used instead of FcgidWrapper because we don't want to define
|
||||||
# a new fcgid process class each time an app is mounted (num proc limits enforcement).
|
# a new fcgid process class each time an app is mounted (num proc limits enforcement).
|
||||||
if 'wrapper_dir' not in context:
|
if 'wrapper_dir' not in context:
|
||||||
# fcgi-bin only needs to be defined once per vhots
|
# fcgi-bin only needs to be defined once per vhots
|
||||||
|
# We assume that all account wrapper paths will share the same dir
|
||||||
context['wrapper_dir'] = os.path.dirname(wrapper_path)
|
context['wrapper_dir'] = os.path.dirname(wrapper_path)
|
||||||
directives = textwrap.dedent("""\
|
directives = textwrap.dedent("""\
|
||||||
Alias /fcgi-bin/ %(wrapper_dir)s/
|
Alias /fcgi-bin/ %(wrapper_dir)s/
|
||||||
|
|
|
@ -89,3 +89,24 @@ class CaptureStdout(list):
|
||||||
def __exit__(self, *args):
|
def __exit__(self, *args):
|
||||||
self.extend(self._stringio.getvalue().splitlines())
|
self.extend(self._stringio.getvalue().splitlines())
|
||||||
sys.stdout = self._stdout
|
sys.stdout = self._stdout
|
||||||
|
|
||||||
|
|
||||||
|
def cmp_to_key(mycmp):
|
||||||
|
'Convert a cmp= function into a key= function'
|
||||||
|
class K(object):
|
||||||
|
def __init__(self, obj, *args):
|
||||||
|
self.obj = obj
|
||||||
|
def __lt__(self, other):
|
||||||
|
return mycmp(self.obj, other.obj) < 0
|
||||||
|
def __gt__(self, other):
|
||||||
|
return mycmp(self.obj, other.obj) > 0
|
||||||
|
def __eq__(self, other):
|
||||||
|
return mycmp(self.obj, other.obj) == 0
|
||||||
|
def __le__(self, other):
|
||||||
|
return mycmp(self.obj, other.obj) <= 0
|
||||||
|
def __ge__(self, other):
|
||||||
|
return mycmp(self.obj, other.obj) >= 0
|
||||||
|
def __ne__(self, other):
|
||||||
|
return mycmp(self.obj, other.obj) != 0
|
||||||
|
return K
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue