diff --git a/TODO.md b/TODO.md
index fa24b064..374f2010 100644
--- a/TODO.md
+++ b/TODO.md
@@ -182,7 +182,6 @@ ugettext("Description")
* saas validate_creation generic approach, for all backends. standard output
-* periodic task to cleanup metricstorage
# create orchestrate databases.Database pk=1 -n --dry-run | --noinput --action save (default)|delete --backend name (limit to this backend) --help
* postupgradeorchestra send signals in order to hook custom stuff
@@ -394,6 +393,3 @@ Case
# Don't enforce one contact per account? remove account.email in favour of contacts?
# Mailer: mark as sent
-
-# Pending filter filter out orders zero metric from pending
-
diff --git a/orchestra/contrib/domains/models.py b/orchestra/contrib/domains/models.py
index 8781cffc..92c8c615 100644
--- a/orchestra/contrib/domains/models.py
+++ b/orchestra/contrib/domains/models.py
@@ -33,7 +33,7 @@ class Domain(models.Model):
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), blank=True,
related_name='domains', help_text=_("Automatically selected for subdomains."))
top = models.ForeignKey('domains.Domain', null=True, related_name='subdomain_set',
- editable=False)
+ editable=False, verbose_name=_("top domain"))
serial = models.IntegerField(_("serial"), default=utils.generate_zone_serial, editable=False,
help_text=_("A revision number that changes whenever this domain is updated."))
refresh = models.CharField(_("refresh"), max_length=16, blank=True,
diff --git a/orchestra/contrib/orders/settings.py b/orchestra/contrib/orders/settings.py
index 6a59f0e3..91593b5e 100644
--- a/orchestra/contrib/orders/settings.py
+++ b/orchestra/contrib/orders/settings.py
@@ -36,3 +36,9 @@ ORDERS_METRIC_ERROR = Setting('ORDERS_METRIC_ERROR',
help_text=("Only account for significative changes.
"
"metric_storage new value: lastvalue*(1+threshold) > currentvalue or lastvalue*threshold < currentvalue."),
)
+
+
+ORDERS_BILLED_METRIC_CLEANUP_DAYS = Setting('ORDERS_BILLED_METRIC_CLEANUP_DAYS',
+ 40,
+ help_text=("Number of days after a billed stored metric is deleted."),
+)
diff --git a/orchestra/contrib/orders/tasks.py b/orchestra/contrib/orders/tasks.py
new file mode 100644
index 00000000..6089e652
--- /dev/null
+++ b/orchestra/contrib/orders/tasks.py
@@ -0,0 +1,50 @@
+import datetime
+
+from celery.task.schedules import crontab
+from django.apps import apps
+
+from orchestra.contrib.tasks import periodic_task
+
+from . import settings
+
+
+@periodic_task(run_every=crontab(hour=4, minute=30), name='orders.cleanup_metrics')
+def cleanup_metrics():
+ from .models import MetricStorage, Order
+ Service = apps.get_model(settings.ORDERS_SERVICE_MODEL)
+
+ # General cleaning: order.billed_on-delta
+ general = 0
+ delta = datetime.timedelta(days=settings.ORDERS_BILLED_METRIC_CLEANUP_DAYS)
+ for order in Order.objects.filter(billed_on__isnull=False):
+ epoch = order.billed_on-delta
+ try:
+ latest = order.metrics.filter(updated_on__lt=epoch).latest('updated_on')
+ except MetricStorage.DoesNotExist:
+ pass
+ else:
+ general += order.metrics.exclude(pk=latest.pk).filter(updated_on__lt=epoch).count()
+ order.metrics.exclude(pk=latest.pk).filter(updated_on__lt=epoch).only('id').delete()
+
+ # Reduce monthly metrics to latest
+ monthly = 0
+ monthly_services = Service.objects.exclude(metric='').filter(
+ billing_period=Service.MONTHLY, pricing_period=Service.BILLING_PERIOD
+ )
+ for service in monthly_services:
+ for order in Order.objects.filter(service=service):
+ dates = order.metrics.values_list('created_on', flat=True)
+ months = set((date.year, date.month) for date in dates)
+ for year, month in months:
+ metrics = order.metrics.filter(
+ created_on__year=year, created_on__month=month,
+ updated_on__year=year, updated_on__month=month)
+ try:
+ latest = metrics.latest('updated_on')
+ except MetricStorage.DoesNotExist:
+ pass
+ else:
+ monthly += metrics.exclude(pk=latest.pk).count()
+ metrics.exclude(pk=latest.pk).only('id').delete()
+
+ return (general, monthly)
diff --git a/orchestra/contrib/saas/backends/wordpressmu.py b/orchestra/contrib/saas/backends/wordpressmu.py
index 24daae1b..be8e8def 100644
--- a/orchestra/contrib/saas/backends/wordpressmu.py
+++ b/orchestra/contrib/saas/backends/wordpressmu.py
@@ -40,18 +40,18 @@ class WordpressMuBackend(ServiceController):
errors = re.findall(r'
(.*)
', response.content.decode('utf8')) raise RuntimeError(errors[0] if errors else 'Unknown %i error' % response.status_code) - def get_id(self, session, webapp): + def get_id(self, session, saas): search = self.get_base_url() - search += '/wp-admin/network/sites.php?s=%s&action=blogs' % webapp.name + search += '/wp-admin/network/sites.php?s=%s&action=blogs' % saas.name regex = re.compile( '%s' % webapp.name + 'class="edit">%s' % saas.name ) content = session.get(search).content.decode('utf8') # Get id ids = regex.search(content) if not ids: - raise RuntimeError("Blog '%s' not found" % webapp.name) + raise RuntimeError("Blog '%s' not found" % saas.name) ids = ids.groups() if len(ids) > 1: raise ValueError("Multiple matches") @@ -60,13 +60,13 @@ class WordpressMuBackend(ServiceController): wpnonce = re.search(r'_wpnonce=([^"]*)"', wpnonce).groups()[0] return int(ids[0]), wpnonce - def create_blog(self, webapp, server): + def create_blog(self, saas, server): session = requests.Session() self.login(session) # Check if blog already exists try: - self.get_id(session, webapp) + self.get_id(session, saas) except RuntimeError: url = self.get_base_url() url += '/wp-admin/network/site-new.php' @@ -77,9 +77,9 @@ class WordpressMuBackend(ServiceController): url += '?action=add-site' data = { - 'blog[domain]': webapp.name, - 'blog[title]': webapp.name, - 'blog[email]': webapp.account.email, + 'blog[domain]': saas.name, + 'blog[title]': saas.name, + 'blog[email]': saas.account.email, '_wpnonce_add-blog': wpnonce, } @@ -87,12 +87,12 @@ class WordpressMuBackend(ServiceController): response = session.post(url, data=data) self.validate_response(response) - def delete_blog(self, webapp, server): + def delete_blog(self, saas, server): session = requests.Session() self.login(session) try: - id, wpnonce = self.get_id(session, webapp) + id, wpnonce = self.get_id(session, saas) except RuntimeError: pass else: @@ -114,8 +114,8 @@ class WordpressMuBackend(ServiceController): response = session.post(delete, data=data) self.validate_response(response) - def save(self, webapp): - self.append(self.create_blog, webapp) + def save(self, saas): + self.append(self.create_blog, saas) - def delete(self, webapp): - self.append(self.delete_blog, webapp) + def delete(self, saas): + self.append(self.delete_blog, saas) diff --git a/orchestra/contrib/saas/settings.py b/orchestra/contrib/saas/settings.py index 6feeb5d6..2105bcf5 100644 --- a/orchestra/contrib/saas/settings.py +++ b/orchestra/contrib/saas/settings.py @@ -23,7 +23,7 @@ SAAS_ENABLED_SERVICES = Setting('SAAS_ENABLED_SERVICES', ) -SAAS_WORDPRESS_ADMIN_PASSWORD = Setting('SAAS_WORDPRESSMU_ADMIN_PASSWORD', +SAAS_WORDPRESS_ADMIN_PASSWORD = Setting('SAAS_WORDPRESS_ADMIN_PASSWORD', 'secret' ) diff --git a/orchestra/contrib/services/handlers.py b/orchestra/contrib/services/handlers.py index 7d5af6f5..d36c881e 100644 --- a/orchestra/contrib/services/handlers.py +++ b/orchestra/contrib/services/handlers.py @@ -25,6 +25,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount): """ _PLAN = 'plan' _COMPENSATION = 'compensation' + _PREPAY = 'prepay' model = None @@ -275,14 +276,15 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount): 'metric': metric, 'discounts': [], }) + if subtotal > price: plan_discount = price-subtotal self.generate_discount(line, self._PLAN, plan_discount) subtotal += plan_discount for dtype, dprice in discounts: subtotal += dprice - # Prevent compensations to refund money - if dtype == self._COMPENSATION and subtotal < 0: + # Prevent compensations/prepays to refund money + if dtype in (self._COMPENSATION, self._PREPAY) and subtotal < 0: dprice -= subtotal if dprice: self.generate_discount(line, dtype, dprice) @@ -527,7 +529,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount): if discount > 0: price -= discount discounts = ( - ('prepay', -discount), + (self._PREPAY, -discount), ) # Don't overdload bills with lots of lines if price > 0: @@ -535,6 +537,8 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount): if prepay_discount < 0: # User has prepaid less than the actual consumption for order, price, cini, cend, metric, discounts in recharges: + if discounts: + price -= discounts[0][1] line = self.generate_line(order, price, cini, cend, metric=metric, computed=True, discounts=discounts) lines.append(line) @@ -561,7 +565,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount): # price -= discount # prepay_discount -= discount # discounts = ( -# ('prepay', -discount), +# (self._PREPAY', -discount), # ) if metric > 0: line = self.generate_line(order, price, cini, cend, metric=metric, @@ -580,7 +584,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount): # price -= discount # prepay_discount -= discount # discounts = ( -# ('prepay', -discount), +# (self._PREPAY, -discount), # ) if metric > 0: line = self.generate_line(order, price, cini, cend, metric=metric,