diff --git a/TODO.md b/TODO.md index 2e366607..5fc5201a 100644 --- a/TODO.md +++ b/TODO.md @@ -41,3 +41,5 @@ Remember that, as always with QuerySets, any subsequent chained methods which im * profile select_related vs prefetch_related * use HTTP OPTIONS instead of configuration endpoint, or rename to settings? + +* Log changes from rest api (serialized objects) diff --git a/orchestra/admin/menu.py b/orchestra/admin/menu.py index df218af8..b280796a 100644 --- a/orchestra/admin/menu.py +++ b/orchestra/admin/menu.py @@ -50,6 +50,12 @@ def get_accounts(): tokens = reverse('admin:authtoken_token_changelist') 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)) + if isinstalled('orchestra.apps.orders'): + url = reverse('admin:orders_order_changelist') + accounts.append(items.MenuItem(_("Orders"), url)) return accounts diff --git a/orchestra/apps/orders/README.md b/orchestra/apps/orders/README.md deleted file mode 100644 index 023e0337..00000000 --- a/orchestra/apps/orders/README.md +++ /dev/null @@ -1,8 +0,0 @@ -Orders -====== - -Build an asyclic graph with every `model.save()` and `model.delete()` looking for Service.content_type matches. - -`ORDERS_GRAPH_MAX_DEPTH` - -autodiscover contacts by looking for `contact` atribute on related objects with reverse relationship `null=False` diff --git a/orchestra/apps/orders/admin.py b/orchestra/apps/orders/admin.py new file mode 100644 index 00000000..54f9ec06 --- /dev/null +++ b/orchestra/apps/orders/admin.py @@ -0,0 +1,15 @@ +from django.contrib import admin + +from .models import Order, QuotaStorage + + +class OrderAdmin(admin.ModelAdmin): + pass + + +class QuotaStorageAdmin(admin.ModelAdmin): + pass + + +admin.site.register(Order, OrderAdmin) +admin.site.register(QuotaStorage, QuotaStorageAdmin) diff --git a/orchestra/apps/orders/collector.py b/orchestra/apps/orders/collector.py deleted file mode 100644 index 900215a6..00000000 --- a/orchestra/apps/orders/collector.py +++ /dev/null @@ -1,29 +0,0 @@ -from . import settings - - -class Node(object): - def __init__(self, content): - self.content = content - self.parents = [] - self.path = [] - - def __repr__(self): - return "%s:%s" % (type(self.content).__name__, self.content) - - -class Collector(object): - def __init__(self, obj, cascade_only=False): - self.obj = obj - self.cascade_only = cascade_only - - def collect(self): - depth = settings.ORDERS_COLLECTOR_MAX_DEPTH - return self._rec_collect(self.obj, [self.obj], depth) - - def _rec_collect(self, obj, path, depth): - node = Node(content=obj) - # FK lookups - for field in obj._meta.fields: - if hasattr(field, 'related') and (self.cascade_only or not field.null): - related_object = getattr(obj, field.name) - diff --git a/orchestra/apps/orders/models.py b/orchestra/apps/orders/models.py index 128a89b6..780a352c 100644 --- a/orchestra/apps/orders/models.py +++ b/orchestra/apps/orders/models.py @@ -1,36 +1,35 @@ +from django.db import models from django.contrib.contenttypes import generic from django.contrib.contenttypes.models import ContentType -from django.db import models -from django.utils.translation import ugettext as _ +from django.utils.translation import ugettext_lazy as _ from . import settings -class Service(models.Model): - name = models.CharField(_("name"), max_length=256) - content_type = models.ForeignKey(ContentType, verbose_name=_("content_type")) - match = models.CharField(_("expression"), max_length=256) - - def __unicode__(self): - return self.name - - class Order(models.Model): - contact = models.ForeignKey(settings.ORDERS_CONTACT_MODEL, - verbose_name=_("contact"), related_name='orders') + account = models.ForeignKey('accounts.Account', verbose_name=_("account"), + related_name='orders') content_type = models.ForeignKey(ContentType) object_id = models.PositiveIntegerField(null=True) - service = models.ForeignKey(Service, verbose_name=_("service"), - related_name='orders')) + price = models.ForeignKey(settings.ORDERS_PRICE_MODEL, + verbose_name=_("price"), related_name='orders') registered_on = models.DateTimeField(_("registered on"), auto_now_add=True) - canceled_on = models.DateTimeField(_("canceled on"), null=True, blank=True) - last_billed_on = models.DateTimeField(_("last billed on"), null=True, blank=True) + cancelled_on = models.DateTimeField(_("cancelled on"), null=True, blank=True) + billed_on = models.DateTimeField(_("billed on"), null=True, blank=True) billed_until = models.DateTimeField(_("billed until"), null=True, blank=True) ignore = models.BooleanField(_("ignore"), default=False) - description = models.CharField(_("description"), max_length=256, blank=True) + description = models.TextField(_("description"), blank=True) content_object = generic.GenericForeignKey() def __unicode__(self): - return "%s@%s" (self.service, self.contact) + return self.service + +class QuotaStorage(models.Model): + order = models.ForeignKey(Order, verbose_name=_("order")) + value = models.BigIntegerField(_("value")) + date = models.DateTimeField(_("date")) + + def __unicode__(self): + return self.order diff --git a/orchestra/apps/orders/settings.py b/orchestra/apps/orders/settings.py index 7a7c8a0d..d8de9e98 100644 --- a/orchestra/apps/orders/settings.py +++ b/orchestra/apps/orders/settings.py @@ -1,7 +1,5 @@ from django.conf import settings +from django.utils.translation import ugettext_lazy as _ -ORDERS_CONTACT_MODEL = getattr(settings, 'ORDERS_CONTACT_MODEL', 'contacts.Contact') - - -ORDERS_COLLECTOR_MAX_DEPTH = getattr(settings, 'ORDERS_COLLECTOR_MAX_DEPTH', 3) +ORDERS_PRICE_MODEL = getattr(settings, 'ORDERS_PRICE_MODEL', 'prices.Price') diff --git a/orchestra/apps/orders/tests/models.py b/orchestra/apps/orders/tests/models.py deleted file mode 100644 index b9c91273..00000000 --- a/orchestra/apps/orders/tests/models.py +++ /dev/null @@ -1,21 +0,0 @@ -from django.db import models - - -class Root(models.Model): - name = models.CharField(max_length=256, default='randomname') - - -class Related(models.Model): - root = models.ForeignKey(Root) - - -class TwoRelated(models.Model): - related = models.ForeignKey(Related) - - -class ThreeRelated(models.Model): - twolated = models.ForeignKey(TwoRelated) - - -class FourRelated(models.Model): - threerelated = models.ForeignKey(ThreeRelated) diff --git a/orchestra/apps/orders/tests/test_collector.py b/orchestra/apps/orders/tests/test_collector.py deleted file mode 100644 index a6382b3d..00000000 --- a/orchestra/apps/orders/tests/test_collector.py +++ /dev/null @@ -1,28 +0,0 @@ -from django.conf import settings -from django.core.management import call_command -from django.db.models import loading -from django.test import TestCase - -from .models import Root, Related, TwoRelated, ThreeRelated, FourRelated - - -#class CollectorTests(TestCase): -# def setUp(self): -# self.root = Root.objects.create(name='randomname') -# self.related = Related.objects.create(top=self.root) -# -# def _pre_setup(self): -# # Add the models to the db. -# self._original_installed_apps = list(settings.INSTALLED_APPS) -# settings.INSTALLED_APPS += ('orchestra.apps.orders.tests',) -# loading.cache.loaded = False -# call_command('syncdb', interactive=False, verbosity=0) -# super(CollectorTests, self)._pre_setup() -# -# def _post_teardown(self): -# super(CollectorTests, self)._post_teardown() -# settings.INSTALLED_APPS = self._original_installed_apps -# loading.cache.loaded = False - -# def test_models(self): -# self.assertEqual('randomname', self.root.name) diff --git a/orchestra/apps/orders/tests/__init__.py b/orchestra/apps/prices/__init__.py similarity index 100% rename from orchestra/apps/orders/tests/__init__.py rename to orchestra/apps/prices/__init__.py diff --git a/orchestra/apps/prices/admin.py b/orchestra/apps/prices/admin.py new file mode 100644 index 00000000..3a507e43 --- /dev/null +++ b/orchestra/apps/prices/admin.py @@ -0,0 +1,23 @@ +from django.contrib import admin + +from orchestra.core import services + +from .models import Pack, Price, Rate + + +class RateInline(admin.TabularInline): + model = Rate + + +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) diff --git a/orchestra/apps/prices/models.py b/orchestra/apps/prices/models.py new file mode 100644 index 00000000..7e550a2a --- /dev/null +++ b/orchestra/apps/prices/models.py @@ -0,0 +1,42 @@ +from django.db import models +from django.contrib.contenttypes.models import ContentType +from django.utils.translation import ugettext_lazy as _ + +from . import settings + + +class Pack(models.Model): + account = models.ForeignKey('accounts.Account', verbose_name=_("account"), + related_name='packs') + name = models.CharField(_("pack"), max_length=128, + choices=settings.PRICES_PACKS, + default=settings.PRICES_DEFAULT_PACK) + + def __unicode__(self): + 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")) + 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) + + class Meta: + unique_together = ('price', 'pack', 'quantity') + + def __unicode__(self): + return self.price diff --git a/orchestra/apps/prices/settings.py b/orchestra/apps/prices/settings.py new file mode 100644 index 00000000..5ef9920a --- /dev/null +++ b/orchestra/apps/prices/settings.py @@ -0,0 +1,19 @@ +from django.conf import settings +from django.utils.translation import ugettext_lazy as _ + + +PRICES_PACKS = getattr(settings, 'PRICES_PACKS', ( + ('basic', _("Basic")), + ('advanced', _("Advanced")), +)) + +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/conf/base_settings.py b/orchestra/conf/base_settings.py index 73f252ca..bce05054 100644 --- a/orchestra/conf/base_settings.py +++ b/orchestra/conf/base_settings.py @@ -76,6 +76,8 @@ INSTALLED_APPS = ( 'orchestra.apps.databases', 'orchestra.apps.vps', 'orchestra.apps.issues', + 'orchestra.apps.prices', + 'orchestra.apps.orders', # Third-party apps 'south', @@ -136,6 +138,8 @@ FLUENT_DASHBOARD_APP_GROUPS = ( 'orchestra.apps.accounts.models.Account', 'orchestra.apps.contacts.models.Contact', 'orchestra.apps.users.models.User', + 'orchestra.apps.orders.models.Order', + 'orchestra.apps.prices.models.Price', ), 'collapsible': True, }), @@ -167,6 +171,8 @@ FLUENT_DASHBOARD_APP_ICONS = { # Accounts 'accounts/account': 'Face-monkey.png', 'contacts/contact': 'contact.png', + 'orders/order': 'shopping-cart.png', + 'prices/price': 'price.png', # Administration 'users/user': 'Mr-potato.png', 'djcelery/taskstate': 'taskstate.png', diff --git a/orchestra/static/orchestra/icons/price.png b/orchestra/static/orchestra/icons/price.png new file mode 100644 index 00000000..aaa71371 Binary files /dev/null and b/orchestra/static/orchestra/icons/price.png differ diff --git a/orchestra/static/orchestra/icons/price.svg b/orchestra/static/orchestra/icons/price.svg new file mode 100644 index 00000000..ef0830b8 --- /dev/null +++ b/orchestra/static/orchestra/icons/price.svg @@ -0,0 +1,104 @@ + +image/svg+xml + + + + \ No newline at end of file diff --git a/orchestra/static/orchestra/icons/shopping-cart.png b/orchestra/static/orchestra/icons/shopping-cart.png new file mode 100644 index 00000000..30e20321 Binary files /dev/null and b/orchestra/static/orchestra/icons/shopping-cart.png differ diff --git a/orchestra/static/orchestra/icons/shopping-cart.svg b/orchestra/static/orchestra/icons/shopping-cart.svg new file mode 100644 index 00000000..da6fd6da --- /dev/null +++ b/orchestra/static/orchestra/icons/shopping-cart.svg @@ -0,0 +1,283 @@ + + + + + + + + image/svg+xml + + + + + + + + +