Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

First working version

  • Loading branch information...
commit bb6fb15eee8986cc89089a03dd03651e4fd0c320 0 parents
@codeinthehole codeinthehole authored
27 LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2011, Tangent Communications PLC and individual contributors.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+ 3. Neither the name of Tangent Communications PLC nor the names of its contributors
+ may be used to endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2  MANIFEST.in
@@ -0,0 +1,2 @@
+include *.rst
+include LICENSE
20 README.rst
@@ -0,0 +1,20 @@
+=======
+Budgets
+=======
+
+.. warning::
+ This is a work in progress
+
+This package provides managed budgets for Oscar. It uses double-ledger account
+to ensure all transaction have a source and destination.
+
+Features:
+
+* A user can have multiple budgets
+* A budget can be assigned to either a primary user or a group of other users
+* A budget can have a start and end date to allow its usage in a limited time
+ window
+
+TODO:
+
+* Make top-level models abstract for extensibility
1  budgets/__init__.py
@@ -0,0 +1 @@
+VERSION = '0.1'
10 budgets/facade.py
@@ -0,0 +1,10 @@
+from budgets import models
+
+
+def transfer(source, destination, amount):
+ models.Transaction.objects.create(
+ budget=source,
+ amount=-amount)
+ models.Transaction.objects.create(
+ budget=destination,
+ amount=amount)
39 budgets/models.py
@@ -0,0 +1,39 @@
+from decimal import Decimal as D
+
+from django.db import models
+
+
+class Budget(models.Model):
+ name = models.CharField(max_length=128, unique=True, null=True)
+
+ # Some budgets are not linked to a specific user but are activated by
+ # entering a code at checkout.
+ code = models.CharField(max_length=128, unique=True, null=True)
+
+ # Budgets can have an date range when they can be used.
+ start_date = models.DateField(null=True)
+ end_date = models.DateField(null=True)
+
+ # Each budget can have multiple users who can use it for transactions. In
+ # many cases, there will only be one user though and so we use a 'primary'
+ # user for this scenario.
+ primary_user = models.ForeignKey('auth.User', related_name="budgets",
+ null=True)
+ secondary_users = models.ManyToManyField('auth.User')
+
+ date_created = models.DateTimeField(auto_now_add=True)
+ date_last_transation = models.DateTimeField(null=True)
+
+
+class Transaction(models.Model):
+ # Every transfer of money should create two rows in this table.
+ # (a) the debit from the source budget
+ # (b) the credit to the destination budget
+ budget = models.ForeignKey(Budget)
+
+ # The sum of this field over the whole table should always be 0
+ # Credits should be positive while debits should be negative
+ amount = models.DecimalField(decimal_places=2, max_digits=12,
+ default=D('0.00'))
+
+ date_created = models.DateTimeField(auto_now_add=True)
3  makefile
@@ -0,0 +1,3 @@
+install:
+ ./setup.py develop
+ pip install -r requirements.txt
7 requirements.txt
@@ -0,0 +1,7 @@
+South==0.7.3
+mock==0.7.2
+coverage==3.5.1
+django-nose==1.0
+nose==1.1.2
+django-extensions==0.8
+django-debug-toolbar==0.9.4
91 runtests.py
@@ -0,0 +1,91 @@
+#!/usr/bin/env python
+import sys
+from coverage import coverage
+from optparse import OptionParser
+
+import logging
+logging.disable(logging.CRITICAL)
+
+from django.conf import settings
+
+if not settings.configured:
+ from oscar.defaults import *
+ extra_settings = {}
+ for key, value in locals().items():
+ if key.startswith('OSCAR'):
+ extra_settings[key] = value
+
+ from oscar import get_core_apps, OSCAR_MAIN_TEMPLATE_DIR
+
+ settings.configure(
+ DATABASES={
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ }
+ },
+ INSTALLED_APPS=[
+ 'django.contrib.auth',
+ 'django.contrib.admin',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.sites',
+ 'django.contrib.flatpages',
+ 'budgets',
+ 'south',
+ ] + get_core_apps(),
+ MIDDLEWARE_CLASSES=(
+ 'django.middleware.common.CommonMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.middleware.transaction.TransactionMiddleware',
+ 'oscar.apps.basket.middleware.BasketMiddleware',
+ ),
+ DEBUG=False,
+ SOUTH_TESTS_MIGRATE=False,
+ HAYSTACK_CONNECTIONS = {
+ 'default': {
+ 'ENGINE': 'haystack.backends.simple_backend.SimpleEngine',
+ },
+ },
+ TEMPLATE_DIRS = (OSCAR_MAIN_TEMPLATE_DIR,),
+ SITE_ID=1,
+ NOSE_ARGS=['-s', '--with-spec'],
+ **extra_settings
+ )
+
+from django_nose import NoseTestSuiteRunner
+
+
+def run_tests(*test_args):
+ if 'south' in settings.INSTALLED_APPS:
+ from south.management.commands import patch_for_test_db_setup
+ patch_for_test_db_setup()
+
+ if not test_args:
+ test_args = ['tests']
+
+ # Run tests
+ test_runner = NoseTestSuiteRunner(verbosity=1)
+
+ c = coverage(source=['budgets'], omit=['*migrations*', '*tests*'])
+ c.start()
+ num_failures = test_runner.run_tests(test_args)
+ c.stop()
+
+ if num_failures > 0:
+ sys.exit(num_failures)
+ print "Generating HTML coverage report"
+ c.html_report()
+
+def generate_migration():
+ from south.management.commands.schemamigration import Command
+ com = Command()
+ com.handle(app='paypal', initial=True)
+
+
+if __name__ == '__main__':
+ parser = OptionParser()
+ (options, args) = parser.parse_args()
+ run_tests(*args)
21 setup.py
@@ -0,0 +1,21 @@
+#!/usr/bin/env python
+from setuptools import setup, find_packages
+
+from budgets import VERSION
+
+setup(name='django-oscar-budgets',
+ version=VERSION,
+ author="David Winterbottom",
+ author_email="david.winterbottom@tangentlabs.co.uk",
+ description="Budgets module for django-oscar",
+ long_description=open('README.rst').read(),
+ license=open('LICENSE').read(),
+ packages=find_packages(exclude=['sandbox*', 'tests*']),
+ # See http://pypi.python.org/pypi?%3Aaction=list_classifiers
+ classifiers=['Environment :: Web Environment',
+ 'Framework :: Django',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: BSD License',
+ 'Operating System :: Unix',
+ 'Programming Language :: Python'],
+ install_requires=['django-oscar>=0.2'])
0  tests/__init__.py
No changes.
0  tests/unit/__init__.py
No changes.
33 tests/unit/model_tests.py
@@ -0,0 +1,33 @@
+from decimal import Decimal as D
+
+from django.test import TestCase
+from django.contrib.auth.models import User
+from django.db.models import Sum
+
+from django_dynamic_fixture import G
+
+from budgets.models import Budget, Transaction
+from budgets import facade
+
+
+class TestATransferFromCompanyBudgetToCustomer(TestCase):
+
+ def setUp(self):
+ staff_member = G(User, is_staff=True)
+ source = Budget.objects.create(
+ name="Company")
+
+ customer = G(User, is_staff=False)
+ destination = Budget.objects.create(
+ primary_user=customer)
+
+ facade.transfer(source, destination, D('100.00'))
+
+ self.assertEqual(2, Transaction.objects.all().count())
+
+ def test_creates_two_transactions(self):
+ self.assertEqual(2, Transaction.objects.all().count())
+
+ def test_preserves_zero_sum_invariant(self):
+ aggregates = Transaction.objects.aggregate(sum=Sum('amount'))
+ self.assertEqual(D('0.00'), aggregates['sum'])
Please sign in to comment.
Something went wrong with that request. Please try again.