Package: django-account-balances
Accounts credit tracking for Django===========================Accounts can be used to implement a variety of interesting components, including:* Giftcards* Web accounts* Loyalty schemesBasically anything that involves tracking the movement of funds within a closedsystem.This package uses [double-entry bookkeeping](http://en.wikipedia.org/wiki/Double-entry_bookkeeping_system)where every transaction is recorded twice (once for the source and once for thedestination).  This ensures the books always balance and there is full audittrail of all transactional activity.If your project manages money, you should be using a library like this.  Yourfinance people will thank you.Features--------* An account has a credit limit which defaults to zero.  Accounts can be set upwith no credit limit so that they are a 'source' of money within the system.At least one account must be set up without a credit limit in order for moneyto move around the system.* Accounts can have:- No users assigned- A single "primary" user - this is the most common case- A set of users assigned* A user can have multiple accounts* An account can have a start and end date to allow its usage in a limited timewindow* Accounts can be categorisedInstallation------------TODO: make available on pypi for pip installation.You should set-up a cronjob that calls:./manage.py close_expired_accountsto close any expired accounts and transfer their funds to the 'expired'account.API---Create account instances using the manager:``` pythonfrom decimal import Decimalimport datetimefrom django.contrib.auth.models import Userfrom accounts import modelsanonymous_account = models.Account.objects.create()barry = User.objects.get(username="barry")user_account = models.Account.objects.create(primary_user=barry)no_credit_limit_account = models.Account.objects.create(credit_limit=None)credit_limit_account = models.Account.objects.create(credit_limit=Decimal('1000.00'))today = datetime.date.today()next_week = today + datetime.timedelta(days=7)date_limited_account = models.Account.objects.create(start_date=today, end_date=next_week)```Transfer funds using the facade:``` pythonfrom accounts import facadestaff_member = User.objects.get(username="staff")trans = facade.transfer(source=no_credit_limit_account,destination=user_account,amount=Decimal('10.00'),user=staff_member)```Reverse transfers:``` pythonfacade.reverse(trans, user=staff_member,description="Just an example")```If the proposed transfer is invalid, an exception will be raised.  Allexceptions are subclasses of `accounts.exceptions.AccountException`.  Yourclient code should look for exceptions of this type and handle themappropriately.Client code should only use the `accounts.models.Budget` class and thetwo functions from `accounts.facade` - nothing else should be required.Error handling--------------Note that the transfer operation is wrapped in its own database transaction toensure that only complete transfers are written out.  When using Django'stransaction middleware, you need to be careful.  If you have an unhandledexception,  then account transfers will still be committed even though nothingelse will be.  To handle this, you need to make sure that, if an exceptionoccurs during your post-payment code, then you roll-back any transfers.Here's a toy example:``` pythonfrom accounts import facadedef submit(self, order_total):# Take payment firsttransfer = facade.transfer(self.get_user_account(),self.get_merchant_account(),order_total)# Create order modelstry:self.place_order()except Exception, e:# Something went wrong placing the order.  Roll-back the previous# transferfacade.reverse(transfer)```In this situation, you'll end up with two transfers being created but no order.While this isn't ideal, it's the best way of handling exceptions that occurduring order placement.Multi-transfer payments-----------------------Projects will often allow users to have multiple accounts and pay for an orderusing more than one.  This will involve several transfers and needs some careful handlingin your application code.It normally makes sense to write your own wrapper around the accounts API to encapsulateyour business logic and error handling.  Here's an example:``` pythonfrom decimal import Decimal as Dfrom accounts import models, exceptions, facadedef redeem(order_number, user, amount):# Get user's non-empty accounts ordered with the first to expire firstaccounts = models.Account.active.filter(user=user, balance__gt=0).order_by('end_date')# Build up a list of potential transfers that cover the requested amounttransfers = []amount_to_allocate = amountfor account in accounts:to_transfer = min(account.balance, amount_to_allocate)transfers.append((account, to_transfer))amount_to_allocate -= to_transferif amount_to_allocate == D('0.00'):breakif amount_to_allocate > D('0.00'):raise exceptions.InsufficientFunds()# Execute transfers to some 'Sales' accountdestination = models.Account.objects.get(name="Sales")completed_transfers = []try:for account, amount in transfers:transfer = facade.transfer(account, destination, amount, user=user,description="Order %s" % order_number)completed_transfers.append(transfer)except exceptions.AccountException, transfer_exc:# Something went wrong with one of the transfers (possibly a race condition).# We try and roll back all completed ones to get us back to a clean state.try:for transfer in completed_transfers:facade.reverse(transfer)except Exception, reverse_exc:# Uh oh: No man's land.  We could be left with a partial redemption. This will# require an admin to intervene.  Make sure your logger mails admins on error.logger.error("Order %s, transfers failed (%s) and reverse failed (%s)",order_number, transfer_exc, reverse_exc)logger.exception(reverse_exc)# Raise an exception so that your client code can inform the user appropriately.raise RedemptionFailed()else:# All transfers completed okreturn completed_transfers```As you can see, there is some careful handling of the scenario where not all transfers can beexecuted.``` pythontry:transfers = api.redeem(order_number, user, total_incl_tax)except Exception:# Inform user of failed paymentelse:for transfer in transfers:source_type, __ = SourceType.objects.get_or_create(name="Accounts")source = Source(source_type=source_type,amount_allocated=transfer.amount,amount_debited=transfer.amount,reference=transfer.reference)self.add_payment_source(source)```Core accounts and account types-------------------------------A post-syncdb signal will create the common structure for account types andaccounts.  Some names can be controlled with settings, as indicated in parentheses.- **Assets**- **Sales**- Redemptions (`ACCOUNTS_REDEMPTIONS_NAME`) - where money istransferred to when an account is used to pay for something.- Lapsed (`ACCOUNTS_LAPSED_NAME`) - where money is transferred towhen an account expires.  This is done by the'close_expired_accounts' management command.  The name of thisaccount can be set using the `ACCOUNTS_LAPSED_NAME`.- **Cash**- "Bank" (`ACCOUNTS_BANK_NAME`) - the source account for creating newaccounts that are paid for by the customer (eg a giftcard).  Thisaccount will not have a credit limit and will normally have anegative balance as money is only transferred out.- **Unpaid** - This contains accounts that are used as sources for otheraccounts but aren't paid for by the customer.  For instance, you mightallow admins to create new accounts in the dashboard.  An account of thistype will be the source account for the initial transfer.- **Liabilities**- **Deferred income** - This contains customer accounts/giftcards.  You may want to createadditional account types within this type to categorise accounts.Example transactions--------------------Consider the following accounts and account types:- **Assets**- **Sales**- Redemptions- Lapsed- **Cash**- Bank- **Unpaid**- Merchant funded- **Liabilities**- **Deferred income**Note that all accounts start with a balance of 0 and the sum of all balances will always be zero.*A customer purchases a £50 giftcard*- A new account is created of type 'Deferred income' with an end date- £50 is transferred from the Bank to this new account*A customer pays for a £30 order using their £50 giftcard*- £30 is transferred from the giftcard account to the redemptions account*The customer's giftcard expires with £20 still on it*- £20 is transferred from the giftcard account to the lapsed account*The customer phones up to complain and a staff member creates a new giftcard for £20*- A new account is created of type 'Deferred income'- £20 is transferred from the "Merchant funded" account to this new accountSettings--------There are settings to control the naming and initial unpaid and deferred income accounttypes:* `ACCOUNTS_MIN_INITIAL_VALUE` The minimum value that can be used to create anaccount (or for a top-up)* `ACCOUNTS_MAX_INITIAL_VALUE` The maximum value that can be transferred to anaccount.Contributing------------Fork repo, set-up virtualenv and run:make installRun tests with:./runtests.pyHistory-------This project used to depend on the e-commerce framework [Oscar](https://github.com/tangentlabs/django-oscar) in a number of places.  You may be interested in the upstream repos if you want Oscar integration. This repo was forked off for people who want account balances without Oscar integration.