From 5d36dbd02d6c06a82d16ea12fdb247d3c60cac56 Mon Sep 17 00:00:00 2001 From: Marc Date: Tue, 27 May 2014 15:55:09 +0000 Subject: [PATCH] Added orders and prices apps --- TODO.md | 2 + orchestra/admin/menu.py | 6 + orchestra/apps/orders/README.md | 8 - orchestra/apps/orders/admin.py | 15 + orchestra/apps/orders/collector.py | 29 -- orchestra/apps/orders/models.py | 37 ++- orchestra/apps/orders/settings.py | 6 +- orchestra/apps/orders/tests/models.py | 21 -- orchestra/apps/orders/tests/test_collector.py | 28 -- .../apps/{orders/tests => prices}/__init__.py | 0 orchestra/apps/prices/admin.py | 23 ++ orchestra/apps/prices/models.py | 42 +++ orchestra/apps/prices/settings.py | 19 ++ orchestra/conf/base_settings.py | 6 + orchestra/static/orchestra/icons/price.png | Bin 0 -> 2149 bytes orchestra/static/orchestra/icons/price.svg | 104 +++++++ .../static/orchestra/icons/shopping-cart.png | Bin 0 -> 3048 bytes .../static/orchestra/icons/shopping-cart.svg | 283 ++++++++++++++++++ 18 files changed, 520 insertions(+), 109 deletions(-) delete mode 100644 orchestra/apps/orders/README.md create mode 100644 orchestra/apps/orders/admin.py delete mode 100644 orchestra/apps/orders/collector.py delete mode 100644 orchestra/apps/orders/tests/models.py delete mode 100644 orchestra/apps/orders/tests/test_collector.py rename orchestra/apps/{orders/tests => prices}/__init__.py (100%) create mode 100644 orchestra/apps/prices/admin.py create mode 100644 orchestra/apps/prices/models.py create mode 100644 orchestra/apps/prices/settings.py create mode 100644 orchestra/static/orchestra/icons/price.png create mode 100644 orchestra/static/orchestra/icons/price.svg create mode 100644 orchestra/static/orchestra/icons/shopping-cart.png create mode 100644 orchestra/static/orchestra/icons/shopping-cart.svg 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 0000000000000000000000000000000000000000..aaa713713185bf070ba97cbe351fa12f95460863 GIT binary patch literal 2149 zcmV-r2%7haP)-Bk{&B-ec(9G39og6{|#`qZP%HAnPH(b)yND%W{SnLP9lAes;X_< zR|G8n4sscg#8^La9Ow1X>~+B`5RFCG3c3ts2Er#wsC8nj)dX@oQJJf%ccqeTkK^&ysV7hUdk^Sj;0ecZ4yfwA zd8J9Hr0iH^(`ayviooKI^R;?Q67mIHK(O5Jl%GWDYzuva(>rx%5y%suO zk=5g?YnK2hvbGpYZ||wA{o?U;57@SSC6+k=O!TqGQ43v~NMv3DXl-q+4cCP35oNUp znwZm(N%~m3@>HU^xjEP{xxRxk;04`gMEXRz8EDFPPcz7ib}aG=&|Os>`aDXF_k3%n zAMoJg2wm1#zX`a$oQ|5n=2S;|jjG=7F+2;n0ldKjsA?sCHAE`1fPHJ*S46B0C5GQo z;Yka5KbcR+MC40;f#k)v1x6&$c3WFx&{*3T{4wDcqsaPnGW~!SIZ`g9YXSpu7qFTU zL+yDH^tnoP3sUw7iHHRI0uKXIJjOPs5~=%(!2@I-$sBhQ>Gc80UWM{rg;-v#sh)Ue zg}UdYtOX)=+xHM%JLQ32Cf?!1Z$GX^lyD)&(HNB z(AwHsTU8y}4@@sHPfdX7=^g2}VDP9#m$h#JzTq1Hq>_oW?RQ>5JbhqXb?^a7g9ng6 zPB)5(0H~@uiS#-|?*Ots_J6m(FSo{r{%|107Kp~8w+Z@iMU&Bln!rlOarz5=cDy}; zN(UoHxGct6;5bfkQs+yCSRk@Ia+RQ4DxIuI49h&WV#SJcx3klcNh2C(1WSm>#<7A2 z7XjP0!&Z(2rN7OTlAHRoXZJ;xM?PKXa5^$+RlIKFK~Z`7ni-${nQycna*lvlx*3>V z!JHR>M~nKdv2y&OXyIqPr|XBguvxKj<3{5Ts<&7G@p$W0MTcjHUfYY@=_Jxi6>A3s^^C|Ro(l-KXCBEMA0^&lVgYn?>^f#)z5u*hR$Fvmsw1@*AeBgMa}w#<7UpUT zo!i$tuxO|%UmcEJe(7{%9=lNB2lsJBp;fC^jqB;{x<`?9W#;S^)eXsH=J`?z(OC2r zLI23m{AoYN`l35xT?p~K$d)aOEK%Y2z@=s8J{k^=Te*Gv_O3#QZQEC1nKi%$hTh=a zCcy0Uj`Tk-6!3zSOeNy+C7;Y`a0gH-H9phb)l=Wu*_n+jYx|ubUjqVOimGyZDw(S2 zZt*K6v(ZFi*OAu`zHyT=W)npY3Se@%gM0SuX#l36ETRPXY<6I0+DZRKL>fe-UPLYt zk%=NwBO+lD8Pm6^Ar=sk0E3_YLJZ!dK_Iwr;f*&mG&cT9EE$qktD3HXf!x7xI5Zy* z$(h`dJx@Ke?8J!^rx|<+?vnK=13%T3E@# zs+xW6wL{-N^xB~}%77kl)y1VxRfk#Li$HZAWVl2faz8Ivc;nK>$&Gi2NVwd>$BrJ` z@%(f9{#*uZfBw0@sITAEE)Xe8U;^hY;0EvY90I~ue&)*Q*IxU%?}Wplsa}hApYA@? zx%=@g9$<PejLy~N-ah1`P(GAZG1PJ0j$B*<`Ct@fo zyXVZAe?0lbQ-z;~#iWZPP{AkMesL}m<-w0=iD4I+h@6W&`8rSpx&XXT3m~CTCfx7fbQ^c9ST8&-sb@qaxsIF%kaP b|2zK&pFd=-^zN%900000NkvXXu0mjf7nl-^ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..30e2032175af22bffb19e38719ca13542dfc5105 GIT binary patch literal 3048 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 + + + + + + + image/svg+xml + + + + + + + + +