Package: capone
Never let your books land you in the pen.Caponeis a library that provides double-entry bookkeeping (the
foundation of all modern accounting) for Django with the ability to link
each recorded transaction to zero or more other Django models as
evidence for that transaction.IntroductionIn double-entry bookkeeping (DEB), all recordable events (purchases,
sales, equipment depreciation, bad debt markdowns, etc.) are tracked as
“ledger entries” or “transactions” in “ledgers”. Each ledger entry is
made up of one or more “credit” and one or more “debit” entries. For the
sake of this brief example, you can think of credits as increasing the
amount of money recorded in a ledger and a debit decreasing it. With
that assumption, the central idea behind double-entry bookkeeping is
that the sum of every ledger entry’s debits must equal the sum of its
credits.caponeimplements a double-entry bookkeeping system by
providing an API for checking that all created entries satisfy this
condition or rolling back the transaction if not.In addition to this standard bookkeeping functionality,caponealso
allows any number of arbitrary objects to be attached, via generic
foreign keys, to a ledger entry as “evidence” for that transaction’s
having happened. For instance, a transaction recording a bank deposit
paying for several medical tests at a time from an insurance company to
your medical testing company could be linked to the originalOrderobjects that recorded the test.caponealso provides an API for the
efficient querying of ledger entries by evidence.For more information on the concept of double-entry bookkeeping itself,
we recommend the Wikipedia article:https://en.wikipedia.org/wiki/Double-entry_bookkeeping_system.Local DevelopmentSetup:First, you must set up your working environment:make setupThis will build a local virtualenv and all other requirements for local
development.Running Commands:The following commands are available for interacting with the app:To start a shell instance so that you can interact with the app via the
ORM:make shellNote: before any of the following instructions, you may have to runmake developto set up a postgres database for this app.First, activate a virtualenv so that your commands have access to the
environment built bymake setup:From the repository root, run:source .venv/bin/activateThen you should be free to run./manage.py makemigrations --settings=capone.tests.settingsor any othermanage.pycommand, even those in the Makefile.To run individual tests, use the following:./manage.py test --settings=capone.tests.settings capone.testsNotice the--settings=capone.tests.settingsargument: because this
repository is a django sub-module, it wouldn’t make sense for it to come
with its own defaultsettings.pyfile. Instead, it ships with one
used to run its tests. To usemanage.py, we have to pass an import
path to the settings file explicitly.ModelsLet’s introduce the models provided bycaponeand how they relate to
one another.Note that all objects in this library havecreated_atandmodified_atfields that areauto_now_addandauto_now,
respectively.Accounting ModelsThe models in this section are those that correspond most to well known
accounting concepts, i.e. those involved in keeping accounts using the
principles of double-entry bookkeeping. They model ledgers, journal
entries, credits and debits, and any metadata one wishes to store with
these objects.LedgerALedgeris the top-most level of organization of information in
double-entry bookkeeping as well as thecaponeapp. Most ledgers
have names familiar to those with any knowledge of accounting, such as
“revenue” or “accounts receivable”.Ledgersare synonymous with the accounting concept of an “account”,
so you may see references to accounts in this documentation or elsewhere
in the accounting literature.As a data structure, aLedgerin this library is little more than a
name, description, and unique number:LedgerEntries(see below)
point to aLedgerto represent their being “in” aLedger.Transactions(see below also) that are “between” twoLedgershave aLedgerEntrypointing to oneLedgerand anotherLedgerEntrypointing to the otherLedger.increased_by_debitsLedgeralso has the sometimes confusing fieldincreased_by_debits. AllLedgersare of one of two types: either
debits increase the “value” of an account or credits do. By convention,
asset and expense accounts are of the former type, while liabilities,
equity, and revenue are of the latter: in short, an increase to an
“asset”-type account is a debit, and an increase to a “liability” or
“equity”-type account is a credit.Here’s a handy mnemonic for the two types of accounts: The accounting
equation says (by definition) that:assets == liabilities + owner equityThe terms on the right of the equals sign are increased by debits, and
terms on the left of the equals sign are decreased by debits. We can
therefore use the accounting equation to know whether to use debits or
credits to model an increase in a ledger.So because debits and credits mean different things in different types
of accounts, we can have a transaction with an “equal and opposite”
credit and debit pair of the same currency amount, but that still
represents a net increase in the value of a company: a debit in Accounts
Receivable and a credit in Revenue increases both accounts while
satisfying the accounting equation.Currently, fieldincreased_by_debitsis not used by the code incaponebut is provided as a convenience to users who might wish to
incorporate this information into an external report or calculation.TransactionATransactionis a record of a discrete financial action,
represented by a collection of debits and credits whose sums equal one
another. Practically all models incaponelink to or throughTransaction: in a sense you could say it’s the main model provided
bycapone. ATransactioncan sometimes be referred to as a
“journal entry”.TheTransactionmodel records debits and credits by linking toLedgerEntries, which include currency amounts of the proper sign,
and thoseLedgerEntriesthemselves point toLedger. In other
words,TransactionandLedgerare linked in a many-to-many
fashion by going throughLedgerEntryas a custom through model. The
“proper sign” part is taken care of by thecreditanddebitconvenience methods (see examples below).Transactionsshould never be deleted. Instead, a newTransactionwith debits and credits swapped should be created usingcapone.api.actions.void_transactionto negate the effect of theTransactionyou’d like to remove. Thevoidsfield on the newTransactionwill automatically be filled in with the oldTransactionyou wish to remove. By this method, you’ll never have to
delete data from your system as a part of normal operation, which mimics
one of the many benefits of traditional, non-computerized double-entry
bookkeeping.Transactionalso has the following fields to provide metadata for
each transaction:created_by: The user who created thisTransaction.notes: A free-form text field for adding to aTransactionany
information not expressed in the numerous metadata fields.posted_timestamp: The time aTransactionshould be considered
valid from.capone.api.actions.create_transactionautomatically
deals with filling in this value with the current time. You can
change this value to post-date or back-dateTransactionsbecausecreated_atwill always represent the true object creation time.transaction_id: A Universally Unique Identifier (UUID) for theTransaction, useful for unambiguously referring to aTransactionwithout using primary keys or other database
internals.type: A user-defined type for theTransaction(see theTransactionTypemodel below).TransactionTypeATransactionTypeis a user-defined, human-readable “type” for aTransaction, useful for sorting, aggregating, or annotatingTransactions. The defaultTransactionTypeisMANUAL, which
is created automatically by the library, but you can define others, say
for bots or certain classes of users.Currently,TransactionTypeis not used by the code incaponebut
is provided as a convenience to users who might wish to incorporate this
information into an external report or calculation.LedgerEntryLedgerEntriesrepresent single debit or credit entries in a singleLedger.LedgerEntriesare grouped together intoTransactions(see above) with the constraint that the sum of all credit and debitLedgerEntriesfor a givenTransactionmust equal zero.LedgerEntrieshave a fieldentry_id, which is a UUID for
unambiguously referring to a singleLedgerEntry.Evidence ModelsThe models in this section deal with adding evidence toTransactionsand searching over that evidence.TransactionRelatedObjectATransactionRelatedObject(TRO) represents the “evidence”
relationship that makes thecaponelibrary more useful. ATROlinks aTransactionto an arbitrary object in the larger app that
this library is used in using a generic foreign key. OneTROlinks
oneTransactionand one arbitrary object, so we make as manyTROsas we want pieces of evidence. There are several convenience
methods incapone.api.queriesfor efficiently querying overTransactionsbased on evidence and evidence objects based on theirTransactions(see examples below).LedgerBalanceALedgerBalanceis similar to aTROin that it allows linkingledgerobjects with objects from the wider app that the library is
used in via generic foreign keys. The purpose ofLedgerBalanceis to
denormalize for more efficient querying the current sum of debits and
credits for an object in a specific Ledger. Therefore, there is only oneLedgerBalancefor each(ledger, related_object)tuple.You should never have to manually create or edit aLedgerBalance:
doing so, as well as keeping them up-to-date, is handled bycaponeinternals. For the same reasons, deleting them is not necessary or a
good idea.The purpose ofLedgerBalancecan best be demonstrated by considering
the deceptively simple query, “how many Orders (a non-caponemodel
we presumably created in the app where we includecaponeas a
library) have an Accounts Receivable balance greater than zero?” One
would have to calculate the ledger balance over literally the product of
all ledgers and all non-caponeobjects in the database, and then
filter them for all those with balances above zero, to answer this
question, which is obviously too expensive. By keeping track of the
per-Ledgerbalance for each object used as evidence in aTransaction, we can much more easily make these queries with
reasonable overhead.UsageCreating LedgersLet’s start by creating two common ledger types, “Accounts Receivable”
and “Revenue”, which usually have transactions between themselves:>>> from capone.models import Ledger
>>> ar = Ledger.objects.create(name='Accounts Receivable', number=1, increased_by_debits=True)
<Ledger: Ledger Accounts Receivable>
>>> revenue = Ledger.objects.create(name='Revenue', number=2, increased_by_debits=True)
<Ledger: Ledger Revenue>Both of these accounts are asset accounts, so they’re both increased by
debits. Please consult the double-entry bookkeeping Wikipedia article or
the explanation forincreased_by_debitsabove for a more in-depth
explanation of the “accounting equation” and whether debits increase or
decrease an account.Also, note that the default convention incaponeis to store debits
as positive numbers and credits as negative numbers. This convention is
common but completely arbitrary. If you want to switch the convention
around, you can setDEBITS_ARE_NEGATIVEtoTruein your
settings.py file. By default, that constant doesn’t need to be defined,
and if it remains undefined,caponewill interpret its value asFalse.Faking Evidence ModelsNow let’s create a fake Order, so that we have some evidence for these
ledger entries, and a fake User, so we’ll have someone to blame for
these transactions:>>> from capone.tests.factories import OrderFactory
>>> order = OrderFactory()
>>> from capone.tests.factories import UserFactory
>>> user = UserFactory()Creating TransactionsWe’re now ready to create a simple transaction:>>> from capone.api.actions import create_transaction
>>> from capone.api.actions import credit
>>> from capone.api.actions import debit
>>> from decimal import Decimal
>>> from capone.models import LedgerEntry
>>> txn = create_transaction(user, evidence=[order], ledger_entries=[LedgerEntry(amount=debit(Decimal(100)), ledger=ar), LedgerEntry(amount=credit(Decimal(100)), ledger=revenue)])
>>> txn.summary()
{
    u'entries': [
        'LedgerEntry: $100.0000 in Accounts Receivable',
        'LedgerEntry: $-100.0000 in Revenue',
    ],
    u'related_objects': [
        'TransactionRelatedObject: Order(id=1)',
    ]
}Note that we use the helper functionscreditanddebitwith
positive numbers to keep the signs consistent in our code. There should
be no reason to use negative numbers withcapone.Note also that the value for the credit and debit is the same: $100. If
we tried to create a transaction with mismatching amounts, we would get
an error:>>> create_transaction(user, evidence=[order], ledger_entries=[LedgerEntry(amount=debit(Decimal(100)), ledger=ar), LedgerEntry(amount=credit(Decimal(101)), ledger=revenue)])
---------------------------------------------------------------------------
TransactionBalanceException               Traceback (most recent call last)

[...]

TransactionBalanceException: Credits do not equal debits. Mis-match of -1.So the consistency required of double-entry bookkeeping is automatically
kept.There are many other options forcreate_transaction: see below or
its docstring for details.Ledger Balancescaponekeeps track of the balance in each ledger for each evidence
object in a denormalized and efficient way. Let’s use this behavior to
get the balances of our ledgers as well as the balances in each ledger
for ourorderobject:>>> from capone.api.queries import get_balances_for_object

>>> get_balances_for_object(order)
defaultdict(<function <lambda> at 0x7fd7ecfa96e0>, {<Ledger: Ledger Accounts Receivable>: Decimal('100.0000'), <Ledger: Ledger Revenue>: Decimal('-100.0000')})

>>> ar.get_balance()
Decimal('100.0000')

>>> revenue.get_balance()
Decimal('-100.0000')Voiding TransactionsWe can also void that transaction, which enters a transaction with the
same evidence but with all values of the opposite sign:>>> from capone.api.actions import void_transaction
>>> void = void_transaction(txn, user)
<Transaction: Transaction 9cd85014-c588-43ff-9532-a6fc2429069e>

>>> void_transaction(txn, user)
---------------------------------------------------------------------------
UnvoidableTransactionException            Traceback (most recent call last)

[...]

UnvoidableTransactionException: Cannot void the same Transaction #(e0842107-3a5b-4487-9b86-d1a5d7ab77b4) more than once.

>>> void.summary()
{u'entries': ['LedgerEntry: $-100.0000 in Accounts Receivable',
  'LedgerEntry: $100.0000 in Revenue'],
 u'related_objects': ['TransactionRelatedObject: Order(id=1)']}

>>> txn.voids

>>> void.voids
<Transaction: Transaction e0842107-3a5b-4487-9b86-d1a5d7ab77b4>Note the new balances for evidence objects andLedgers:>>> get_balances_for_object(order)
defaultdict(<function <lambda> at 0x7fd7ecfa9758>, {<Ledger: Ledger Accounts Receivable>: Decimal('0.0000'), <Ledger: Ledger Revenue>: Decimal('0.0000')})

>>> ar.get_balance()
Decimal('0.0000')

>>> revenue.get_balance()
Decimal('0.0000')Transaction TypesYou can label aTransactionusing a foreign key to theTransactionTypeto, say, distinguish between manually madeTransactionsand those made by a bot, or betweenTransactionsthat represent two different types of financial transaction, such as
“Reconciliation” and “Revenue Recognition”.By default,Transactionsare of a special, auto-generated “manual”
type:>>> txn.type
<TransactionType: Transaction Type Manual>but you can create and assignTransactionTypeswhen creatingTransactions:>>> from capone.models import TransactionType
>>> new_type = TransactionType.objects.create(name='New type')
>>> txn = create_transaction(user, evidence=[order], ledger_entries=[LedgerEntry(amount=debit(Decimal(100)), ledger=ar), LedgerEntry(amount=credit(Decimal(100)), ledger=revenue)], type=new_type)
>>> txn.type
<TransactionType: Transaction Type New type>Querying TransactionsGetting BalancesTransactionhas asummarymethod to summarize the data on the
many models that can link to it:>>> txn.summary()
{u'entries': ['LedgerEntry: $100.0000 in Accounts Receivable',
  'LedgerEntry: $-100.0000 in Revenue'],
 u'related_objects': ['TransactionRelatedObject: Order(id=1)']}To get the balance for aLedger, use itsget_balancemethod:>>> ar.get_balance()
Decimal('100.0000')To efficiently get the balance of all transactions with a particular
object as evidence, useget_balances_for_objects:>>> get_balances_for_object(order)
defaultdict(<function <lambda> at 0x7fd7ecfa9230>, {<Ledger: Ledger Accounts Receivable>: Decimal('100.0000'), <Ledger: Ledger Revenue>: Decimal('-100.0000')})Transactionsare validated before they are created, but if you need
to do this manually for some reason, use thevalidate_transactionfunction, which has the same prototype ascreate_transaction:>>> validate_transaction(user, evidence=[order], ledger_entries=[LedgerEntry(amount=debit(Decimal(100)), ledger=ar), LedgerEntry(amount=credit(Decimal(100)), ledger=revenue)], type=new_type)
>>> validate_transaction(user, evidence=[order], ledger_entries=[LedgerEntry(amount=debit(Decimal(100)), ledger=ar), LedgerEntry(amount=credit(Decimal(101)), ledger=revenue)], type=new_type)
---------------------------------------------------------------------------
TransactionBalanceException               Traceback (most recent call last)
<ipython-input-64-07b6d139bb37> in <module>()
----> 1 validate_transaction(user, evidence=[order], ledger_entries=[LedgerEntry(amount=debit(Decimal(100)), ledger=ar), LedgerEntry(amount=credit(Decimal(101)), ledger=revenue)], type=new_type)

/home/hunter/capone/capone/api/queries.pyc in validate_transaction(user, evidence, ledger_entries, notes, type, posted_timestamp)
     67     if total != Decimal(0):
     68         raise TransactionBalanceException(
---> 69             "Credits do not equal debits. Mis-match of %s." % total)
     70
     71     if not ledger_entries:

TransactionBalanceException: Credits do not equal debits. Mis-match of -1.QueriesAlong with the query possibilities from the Django ORM,caponeprovidesTransaction.filter_by_related_objectsfor findingTransactionsthat are related to certain models as evidence.>>> Transaction.objects.count()
5

>>> Transaction.objects.filter_by_related_objects([order]).count()
5

>>> order2 = OrderFactory()

>>> create_transaction(user, evidence=[order2], ledger_entries=[LedgerEntry(amount=debit(Decimal(100)), ledger=ar), LedgerEntry(amount=credit(Decimal(100)), ledger=revenue)])
<Transaction: Transaction 68a4adb1-b898-493f-b5f3-4fe7132dd28d>

>>> Transaction.objects.filter_by_related_objects([order2]).count()
1filter_by_related_objectsis defined on a customQuerySetprovided forTransaction, so calls to it can be chained like
ordinaryQuerySetfunction calls:>>> create_transaction(user, evidence=[order2], ledger_entries=[LedgerEntry(amount=debit(Decimal(100)), ledger=ar), LedgerEntry(amount=credit(Decimal(100)), ledger=revenue)])
<Transaction: Transaction 92049712-4982-4718-bc71-a405b0d762ac>

>>> Transaction.objects.filter_by_related_objects([order2]).count()
2

>>> Transaction.objects.filter_by_related_objects([order2]).filter(transaction_id='92049712-4982-4718-bc71-a405b0d762ac').count()
1filter_by_related_objectstakes an optionalmatch_typeargument,
which is of typeMatchType(Enum)that allows one to filter in
different ways, namely whether the matching transactions may have “any”,
“all”, “none”, or “exactly” the evidence provided, determined byMatchTypesANY,ALL,NONE, andEXACT, respectively.Asserting over TransactionsFor writing tests, the methodassert_transaction_in_ledgers_for_amounts_with_evidenceis provided
for convenience. As its name implies, it allows asserting the existence
of exactly oneTransactionwith the ledger amounts, evidence, and
other fields on Ledger provided to the method.>>> create_transaction(user, evidence=[order], ledger_entries=[LedgerEntry(amount=debit(Decimal(100)), ledger=ar), LedgerEntry(amount=credit(Decimal(100)), ledger=revenue)])
<Transaction: Transaction b3e73f1d-6b10-4597-b19b-84800839d5b3>
>>> with assert_raises(Transaction.DoesNotExist):
...     assert_transaction_in_ledgers_for_amounts_with_evidence(ledger_amount_pairs=[(revenue.name, credit(Decimal(100))), (ar.name, debit(Decimal(100)))], evidence=[])
...
>>> assert_transaction_in_ledgers_for_amounts_with_evidence(ledger_amount_pairs=[(revenue.name, credit(Decimal(100))), (ar.name, debit(Decimal(100)))], evidence=[order])
>>> with assert_raises(Transaction.DoesNotExist):
...     assert_transaction_in_ledgers_for_amounts_with_evidence(ledger_amount_pairs=[(revenue.name, credit(Decimal(100))), (ar.name, debit(Decimal(100)))], evidence=[order])
...
Traceback (most recent call last):
  File "<console>", line 2, in <module>
    File "/usr/lib/python2.7/unittest/case.py", line 116, in __exit__
        "{0} not raised".format(exc_name))
        AssertionError: DoesNotExist not raisedYou can seecapone.tests.test_assert_transaction_in_ledgers_for_amounts_with_evidencefor more examples!Image CreditsImage courtesyOfficeronWikipedia.
This work was created by a government unit (including state, county, and
municipal government agencies) of the U.S. state of Florida. It is a
public record that was not created by an agency which state law has
allowed to claim copyright and is therefore in the public domain in the
United States.