diff --git a/orchestra/admin/menu.py b/orchestra/admin/menu.py index 4d95731b..04b7a7a5 100644 --- a/orchestra/admin/menu.py +++ b/orchestra/admin/menu.py @@ -53,11 +53,13 @@ def get_accounts(): users.append(items.MenuItem(_("Tokens"), tokens)) accounts.append(items.MenuItem(_("Users"), url, children=users)) if isinstalled('orchestra.apps.prices'): - url = reverse('admin:prices_price_changelist') - accounts.append(items.MenuItem(_("Prices"), url)) + url = reverse('admin:prices_pack_changelist') + accounts.append(items.MenuItem(_("Packs"), url)) if isinstalled('orchestra.apps.orders'): url = reverse('admin:orders_order_changelist') accounts.append(items.MenuItem(_("Orders"), url)) + url = reverse('admin:orders_service_changelist') + accounts.append(items.MenuItem(_("Services"), url)) return accounts @@ -76,6 +78,8 @@ def get_administration_models(): administration_models.append('orchestra.apps.issues.*') if isinstalled('orchestra.apps.resources'): administration_models.append('orchestra.apps.resources.*') + if isinstalled('orchestra.apps.miscellaneous'): + administration_models.append('orchestra.apps.miscellaneous.models.MiscService') return administration_models diff --git a/orchestra/apps/accounts/settings.py b/orchestra/apps/accounts/settings.py index 5d82ed2e..a6d2d95b 100644 --- a/orchestra/apps/accounts/settings.py +++ b/orchestra/apps/accounts/settings.py @@ -5,6 +5,7 @@ from django.utils.translation import ugettext_lazy as _ ACCOUNTS_TYPES = getattr(settings, 'ACCOUNTS_TYPES', ( ('INDIVIDUAL', _("Individual")), ('ASSOCIATION', _("Association")), + ('CUSTOMER', _("Customer")), ('COMPANY', _("Company")), ('PUBLICBODY', _("Public body")), )) diff --git a/orchestra/apps/databases/backends.py b/orchestra/apps/databases/backends.py index 1511d62b..5606e657 100644 --- a/orchestra/apps/databases/backends.py +++ b/orchestra/apps/databases/backends.py @@ -74,5 +74,34 @@ class MySQLPermissionBackend(ServiceController): class MysqlDisk(ServiceMonitor): model = 'database.Database' - resource = ServiceMonitor.DISK verbose_name = _("MySQL disk") + + def exceeded(self, db): + context = self.get_context(obj) + self.append("mysql -e '" + "UPDATE db SET Insert_priv=\"N\", Create_priv=\"N\"" + " WHERE Db=\"%(db_name)s\";'" % context + ) + + def recovery(self, db): + context = self.get_context(obj) + self.append("mysql -e '" + "UPDATE db SET Insert_priv=\"Y\", Create_priv=\"Y\"" + " WHERE Db=\"%(db_name)s\";'" % context + ) + + def monitor(self, db): + context = self.get_context(obj) + self.append( + "echo %(db_id)s $(mysql -B -e '" + " SELECT sum( data_length + index_length ) \"Size\"\n" + " FROM information_schema.TABLES\n" + " WHERE table_schema=\"gisp\"\n" + " GROUP BY table_schema;' | tail -n 1)" % context + ) + + def get_context(self, db): + return { + 'db_name': db.name, + 'db_id': db.pk, + } diff --git a/orchestra/apps/miscellaneous/__init__.py b/orchestra/apps/miscellaneous/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/orchestra/apps/miscellaneous/admin.py b/orchestra/apps/miscellaneous/admin.py new file mode 100644 index 00000000..d8f9eccd --- /dev/null +++ b/orchestra/apps/miscellaneous/admin.py @@ -0,0 +1,34 @@ +from django.contrib import admin +from django.core.urlresolvers import reverse +from django.db import models +from django.utils.safestring import mark_safe +from django.utils.translation import ugettext_lazy as _ + +from orchestra.apps.accounts.admin import AccountAdminMixin + +from .models import MiscService, Miscellaneous + + +class MiscServiceAdmin(admin.ModelAdmin): + list_display = ('name', 'num_instances') + + def num_instances(self, misc): + """ return num slivers as a link to slivers changelist view """ + num = misc.instances.count() + url = reverse('admin:miscellaneous_miscellaneous_changelist') + url += '?service={}'.format(misc.pk) + return mark_safe('{1}'.format(url, num)) + num_instances.short_description = _("Instances") + num_instances.admin_order_field = 'instances__count' + + def get_queryset(self, request): + qs = super(MiscServiceAdmin, self).queryset(request) + return qs.annotate(models.Count('instances', distinct=True)) + + +class MiscellaneousAdmin(AccountAdminMixin, admin.ModelAdmin): + list_display = ('service', 'amount', 'account_link') + + +admin.site.register(MiscService, MiscServiceAdmin) +admin.site.register(Miscellaneous, MiscellaneousAdmin) diff --git a/orchestra/apps/miscellaneous/models.py b/orchestra/apps/miscellaneous/models.py new file mode 100644 index 00000000..6bb4c83d --- /dev/null +++ b/orchestra/apps/miscellaneous/models.py @@ -0,0 +1,36 @@ +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from orchestra.core import services + + +class MiscService(models.Model): + name = models.CharField(_("name"), max_length=256) + description = models.TextField(blank=True) + is_active = models.BooleanField(default=True, + help_text=_("Whether new instances of this service can be created " + "or not. Unselect this instead of deleting services.")) + + def __unicode__(self): + return self.name + + +class Miscellaneous(models.Model): + service = models.ForeignKey(MiscService, verbose_name=_("service"), + related_name='instances') + account = models.ForeignKey('accounts.Account', verbose_name=_("account"), + related_name='miscellaneous') + description = models.TextField(_("description"), blank=True) + amount = models.PositiveIntegerField(_("amount"), default=1) + is_active = models.BooleanField(default=True, + help_text=_("Designates whether this service should be treated as " + "active. Unselect this instead of deleting services.")) + + class Meta: + verbose_name_plural = _("miscellaneous") + + def __unicode__(self): + return "{0}-{1}".format(str(self.service), str(self.account)) + + +services.register(Miscellaneous) diff --git a/orchestra/apps/orchestration/backends.py b/orchestra/apps/orchestration/backends.py index b1f6de6a..a3626b7d 100644 --- a/orchestra/apps/orchestration/backends.py +++ b/orchestra/apps/orchestration/backends.py @@ -130,4 +130,6 @@ class ServiceController(ServiceBackend): @classmethod def get_backends(cls): """ filter controller classes """ - return [ plugin for plugin in cls.plugins if ServiceController in plugin.__mro__ ] + return [ + plugin for plugin in cls.plugins if ServiceController in plugin.__mro__ + ] diff --git a/orchestra/apps/orders/admin.py b/orchestra/apps/orders/admin.py index 54f9ec06..99f83e3f 100644 --- a/orchestra/apps/orders/admin.py +++ b/orchestra/apps/orders/admin.py @@ -1,15 +1,49 @@ +from django import forms from django.contrib import admin +from django.utils.translation import ugettext_lazy as _ -from .models import Order, QuotaStorage +from orchestra.core import services + +from .models import Service, Order, MetricStorage + + +class ServiceAdmin(admin.ModelAdmin): + fieldsets = ( + (None, { + 'classes': ('wide',), + 'fields': ('description', 'model', 'match', 'is_active') + }), + (_("Billing options"), { + 'classes': ('wide',), + 'fields': ('billing_period', 'billing_point', 'delayed_billing', + 'is_fee') + }), + (_("Pricing options"), { + 'classes': ('wide',), + 'fields': ('metric', 'pricing_period', 'rate_algorithm', + 'orders_effect', ('on_cancel', 'on_disable', 'on_register'), + 'payment_style', 'trial_period', 'refound_period', 'tax',) + }), + ) + + def formfield_for_dbfield(self, db_field, **kwargs): + """ Improve performance of account field and filter by account """ + if db_field.name == 'model': + models = [model._meta.model_name for model in services.get().keys()] + kwargs['queryset'] = db_field.rel.to.objects.filter(model__in=models) + if db_field.name in ['match', 'metric']: + kwargs['widget'] = forms.TextInput(attrs={'size':'160'}) + return super(ServiceAdmin, self).formfield_for_dbfield(db_field, **kwargs) class OrderAdmin(admin.ModelAdmin): pass -class QuotaStorageAdmin(admin.ModelAdmin): +class MetricStorageAdmin(admin.ModelAdmin): pass +admin.site.register(Service, ServiceAdmin) admin.site.register(Order, OrderAdmin) -admin.site.register(QuotaStorage, QuotaStorageAdmin) +admin.site.register(MetricStorage, MetricStorageAdmin) diff --git a/orchestra/apps/orders/models.py b/orchestra/apps/orders/models.py index 780a352c..bdd7d9e3 100644 --- a/orchestra/apps/orders/models.py +++ b/orchestra/apps/orders/models.py @@ -6,13 +6,151 @@ from django.utils.translation import ugettext_lazy as _ from . import settings +class Service(models.Model): + NEVER = 'NEVER' + MONTHLY = 'MONTHLY' + ANUAL = 'ANUAL' + TEN_DAYS = 'TEN_DAYS' + ONE_MONTH = 'ONE_MONTH' + ALWAYS = 'ALWAYS' + ON_REGISTER = 'ON_REGISTER' + FIXED_DATE = 'ON_FIXED_DATE' + BILLING_PERIOD = 'BILLING_PERIOD' + REGISTER_OR_RENEW = 'REGISTER_OR_RENEW' + CONCURRENT = 'CONCURRENT' + NOTHING = 'NOTHING' + DISCOUNT = 'DISCOUNT' + REFOUND = 'REFOUND' + PREPAY = 'PREPAY' + POSTPAY = 'POSTPAY' + BEST_PRICE = 'BEST_PRICE' + PROGRESSIVE_PRICE = 'PROGRESSIVE_PRICE' + MATCH_PRICE = 'MATCH_PRICE' + + description = models.CharField(_("description"), max_length=256, unique=True) + model = models.ForeignKey(ContentType, verbose_name=_("model")) + match = models.CharField(_("match"), max_length=256) + is_active = models.BooleanField(_("is active"), default=True) + # Billing + billing_period = models.CharField(_("billing period"), max_length=16, + help_text=_("Renewal period for recurring invoicing"), + choices=( + (NEVER, _("One time service")), + (MONTHLY, _("Monthly billing")), + (ANUAL, _("Anual billing")), + ), + default=ANUAL) + billing_point = models.CharField(_("billing point"), max_length=16, + help_text=_("Reference point for calculating the renewal date " + "on recurring invoices"), + choices=( + (ON_REGISTER, _("Registration date")), + (FIXED_DATE, _("Fixed billing date")), + ), + default=FIXED_DATE) + delayed_billing = models.CharField(_("delayed billing"), max_length=16, + help_text=_("Period in which this service will be ignored for billing"), + choices=( + (NEVER, _("No delay (inmediate billing)")), + (TEN_DAYS, _("Ten days")), + (ONE_MONTH, _("One month")), + ), + default=ONE_MONTH) + is_fee = models.BooleanField(_("is fee"), default=False, + help_text=_("Designates whether this service should be billed as " + " membership fee or not")) + # Pricing + metric = models.CharField(_("metric"), max_length=256, blank=True, + help_text=_("Metric used to compute the pricing rate. " + "Number of orders is used when left blank.")) + tax = models.IntegerField(_("tax"), choices=settings.ORDERS_SERVICE_TAXES, + default=settings.ORDERS_SERVICE_DEFAUL_TAX) + pricing_period = models.CharField(_("pricing period"), max_length=16, + help_text=_("Period used for calculating the metric used on the " + "pricing rate"), + choices=( + (BILLING_PERIOD, _("Same as billing period")), + (MONTHLY, _("Monthly data")), + (ANUAL, _("Anual data")), + ), + default=BILLING_PERIOD) + rate_algorithm = models.CharField(_("rate algorithm"), max_length=16, + help_text=_("Algorithm used to interprete the rating table"), + choices=( + (BEST_PRICE, _("Best price")), + (PROGRESSIVE_PRICE, _("Progressive price")), + (MATCH_PRICE, _("Match price")), + ), + default=BEST_PRICE) + orders_effect = models.CharField(_("orders effect"), max_length=16, + help_text=_("Defines the lookup behaviour when using orders for " + "the pricing rate computation of this service."), + choices=( + (REGISTER_OR_RENEW, _("Register or renew events")), + (CONCURRENT, _("Active at every given time")), + ), + default=CONCURRENT) + on_cancel = models.CharField(_("on cancel"), max_length=16, + help_text=_("Defines the cancellation behaviour of this service"), + choices=( + (NOTHING, _("Nothing")), + (DISCOUNT, _("Discount")), + (REFOUND, _("Refound")), + ), + default=DISCOUNT) + on_disable = models.CharField(_("on disable"), max_length=16, + help_text=_("Defines the behaviour of this service when disabled"), + choices=( + (NOTHING, _("Nothing")), + (DISCOUNT, _("Discount")), + (REFOUND, _("Refound")), + ), + default=DISCOUNT) + on_register = models.CharField(_("on register"), max_length=16, + help_text=_("Defines the behaviour of this service on registration"), + choices=( + (NOTHING, _("Nothing")), + (DISCOUNT, _("Discount (fixed BP)")), + ), + default=DISCOUNT) + payment_style = models.CharField(_("payment style"), max_length=16, + help_text=_("Designates whether this service should be paid after " + "consumtion (postpay/on demand) or prepaid"), + choices=( + (PREPAY, _("Prepay")), + (POSTPAY, _("Postpay (on demand)")), + ), + default=PREPAY) + trial_period = models.CharField(_("trial period"), max_length=16, + help_text=_("Period in which no charge will be issued"), + choices=( + (NEVER, _("No trial")), + (TEN_DAYS, _("Ten days")), + (ONE_MONTH, _("One month")), + ), + default=NEVER) + refound_period = models.CharField(_("refound period"), max_length=16, + help_text=_("Period in which automatic refound will be performed on " + "service cancellation"), + choices=( + (NEVER, _("Never refound")), + (TEN_DAYS, _("Ten days")), + (ONE_MONTH, _("One month")), + (ALWAYS, _("Always refound")), + ), + default=ONE_MONTH) + + def __unicode__(self): + return self.description + + class Order(models.Model): account = models.ForeignKey('accounts.Account', verbose_name=_("account"), related_name='orders') content_type = models.ForeignKey(ContentType) object_id = models.PositiveIntegerField(null=True) - price = models.ForeignKey(settings.ORDERS_PRICE_MODEL, - verbose_name=_("price"), related_name='orders') + service = models.ForeignKey(Service, verbose_name=_("price"), + related_name='orders') registered_on = models.DateTimeField(_("registered on"), auto_now_add=True) cancelled_on = models.DateTimeField(_("cancelled on"), null=True, blank=True) billed_on = models.DateTimeField(_("billed on"), null=True, blank=True) @@ -26,7 +164,7 @@ class Order(models.Model): return self.service -class QuotaStorage(models.Model): +class MetricStorage(models.Model): order = models.ForeignKey(Order, verbose_name=_("order")) value = models.BigIntegerField(_("value")) date = models.DateTimeField(_("date")) diff --git a/orchestra/apps/orders/settings.py b/orchestra/apps/orders/settings.py index d8de9e98..e8b41f7e 100644 --- a/orchestra/apps/orders/settings.py +++ b/orchestra/apps/orders/settings.py @@ -2,4 +2,10 @@ from django.conf import settings from django.utils.translation import ugettext_lazy as _ -ORDERS_PRICE_MODEL = getattr(settings, 'ORDERS_PRICE_MODEL', 'prices.Price') +ORDERS_SERVICE_TAXES = getattr(settings, 'ORDERS_SERVICE_TAXES', ( + (0, _("Duty free")), + (7, _("7%")), + (21, _("21%")), +)) + +ORDERS_SERVICE_DEFAUL_TAX = getattr(settings, 'ORDERS_SERVICE_DFAULT_TAX', 0) diff --git a/orchestra/apps/prices/admin.py b/orchestra/apps/prices/admin.py index 3a507e43..8a6749db 100644 --- a/orchestra/apps/prices/admin.py +++ b/orchestra/apps/prices/admin.py @@ -1,23 +1,20 @@ from django.contrib import admin -from orchestra.core import services +from orchestra.admin.utils import insertattr +from orchestra.apps.orders.models import Service -from .models import Pack, Price, Rate +from .models import Pack, Rate + + +class PackAdmin(admin.ModelAdmin): + pass + +admin.site.register(Pack, PackAdmin) class RateInline(admin.TabularInline): model = Rate + ordering = ('pack', 'quantity') -class PriceAdmin(admin.ModelAdmin): - inlines = [RateInline] - - def formfield_for_dbfield(self, db_field, **kwargs): - """ Improve performance of account field and filter by account """ - if db_field.name == 'service': - models = [model._meta.model_name for model in services.get().keys()] - kwargs['queryset'] = db_field.rel.to.objects.filter(model__in=models) - return super(PriceAdmin, self).formfield_for_dbfield(db_field, **kwargs) - - -admin.site.register(Price, PriceAdmin) +insertattr(Service, 'inlines', RateInline) diff --git a/orchestra/apps/prices/models.py b/orchestra/apps/prices/models.py index 7e550a2a..c43021a4 100644 --- a/orchestra/apps/prices/models.py +++ b/orchestra/apps/prices/models.py @@ -2,6 +2,8 @@ from django.db import models from django.contrib.contenttypes.models import ContentType from django.utils.translation import ugettext_lazy as _ +from orchestra.core import services + from . import settings @@ -16,27 +18,18 @@ class Pack(models.Model): return self.pack -class Price(models.Model): - description = models.CharField(_("description"), max_length=256, unique=True) - service = models.ForeignKey(ContentType, verbose_name=_("service")) - expression = models.CharField(_("match"), max_length=256) - tax = models.IntegerField(_("tax"), choices=settings.PRICES_TAXES, - default=settings.PRICES_DEFAUL_TAX) - active = models.BooleanField(_("is active"), default=True) - - def __unicode__(self): - return self.description - - class Rate(models.Model): - price = models.ForeignKey('prices.Price', verbose_name=_("price")) + service = models.ForeignKey('orders.Service', verbose_name=_("service")) pack = models.CharField(_("pack"), max_length=128, blank=True, choices=(('', _("default")),) + settings.PRICES_PACKS) quantity = models.PositiveIntegerField(_("quantity"), null=True, blank=True) - value = models.DecimalField(_("price"), max_digits=12, decimal_places=2) + value = models.DecimalField(_("value"), max_digits=12, decimal_places=2) class Meta: - unique_together = ('price', 'pack', 'quantity') + unique_together = ('service', 'pack', 'quantity') def __unicode__(self): - return self.price + return "{}-{}".format(str(self.value), self.quantity) + + +services.register(Pack, menu=False) diff --git a/orchestra/apps/prices/settings.py b/orchestra/apps/prices/settings.py index 5ef9920a..885657b5 100644 --- a/orchestra/apps/prices/settings.py +++ b/orchestra/apps/prices/settings.py @@ -8,12 +8,3 @@ PRICES_PACKS = getattr(settings, 'PRICES_PACKS', ( )) PRICES_DEFAULT_PACK = getattr(settings, 'PRICES_DEFAULT_PACK', 'basic') - - -PRICES_TAXES = getattr(settings, 'PRICES_TAXES', ( - (0, _("Duty free")), - (7, _("7%")), - (21, _("21%")), -)) - -PRICES_DEFAUL_TAX = getattr(settings, 'PRICES_DFAULT_TAX', 0) diff --git a/orchestra/apps/resources/admin.py b/orchestra/apps/resources/admin.py index ea4aa9b8..bb0956c9 100644 --- a/orchestra/apps/resources/admin.py +++ b/orchestra/apps/resources/admin.py @@ -7,7 +7,7 @@ from djcelery.humanize import naturaldate from orchestra.admin import ExtendedModelAdmin from orchestra.admin.filters import UsedContentTypeFilter -from orchestra.admin.utils import insertattr, get_modeladmin +from orchestra.admin.utils import insertattr, get_modeladmin, link from orchestra.core import services from orchestra.utils import running_syncdb @@ -17,7 +17,7 @@ from .models import Resource, ResourceData, MonitorData class ResourceAdmin(ExtendedModelAdmin): list_display = ( - 'name', 'verbose_name', 'content_type', 'period', 'ondemand', + 'id', 'name', 'verbose_name', 'content_type', 'period', 'ondemand', 'default_allocation', 'disable_trigger', 'crontab', ) list_filter = (UsedContentTypeFilter, 'period', 'ondemand', 'disable_trigger') @@ -26,8 +26,8 @@ class ResourceAdmin(ExtendedModelAdmin): 'fields': ('name', 'content_type', 'period'), }), (_("Configuration"), { - 'fields': ('verbose_name', 'default_allocation', 'ondemand', - 'disable_trigger', 'is_active'), + 'fields': ('verbose_name', 'unit', 'scale', 'ondemand', + 'default_allocation', 'disable_trigger', 'is_active'), }), (_("Monitoring"), { 'fields': ('monitors', 'crontab'), @@ -65,16 +65,27 @@ class ResourceAdmin(ExtendedModelAdmin): class ResourceDataAdmin(admin.ModelAdmin): - list_display = ('id', 'resource', 'used', 'allocated', 'last_update', 'content_type') # TODO content_object + list_display = ( + 'id', 'resource', 'used', 'allocated', 'last_update', 'content_object_link' + ) list_filter = ('resource',) + readonly_fields = ('content_object_link',) + + def content_object_link(self, data): + return link('content_object')(self, data) + content_object_link.allow_tags = True + content_object_link.short_description = _("Content object") class MonitorDataAdmin(admin.ModelAdmin): - list_display = ('id', 'monitor', 'date', 'value', 'ct', 'object_id') # TODO content_object + list_display = ('id', 'monitor', 'date', 'value', 'content_object_link') list_filter = ('monitor',) + readonly_fields = ('content_object_link',) - def ct(self, i): - return i.content_type_id + def content_object_link(self, data): + return link('content_object')(self, data) + content_object_link.allow_tags = True + content_object_link.short_description = _("Content object") admin.site.register(Resource, ResourceAdmin) @@ -102,8 +113,10 @@ def resource_inline_factory(resources): form = ResourceForm formset = ResourceInlineFormSet can_delete = False - fields = ('verbose_name', 'used', 'display_last_update', 'allocated',) - readonly_fields = ('used', 'display_last_update',) + fields = ( + 'verbose_name', 'used', 'display_last_update', 'allocated', 'unit' + ) + readonly_fields = ('used', 'display_last_update') class Media: css = { @@ -114,9 +127,9 @@ def resource_inline_factory(resources): """ Hidde add another """ return False - def display_last_update(self, log): + def display_last_update(self, data): return '