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 @@
+
+
\ 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 @@
+
+
+
+