GnuCash 2.4.99
new_book_with_opening_balances.py
Go to the documentation of this file.
00001 #!/usr/bin/env python
00002 
00003 # new_book_with_opening_balances.py -- Replicate the account structure of a
00004 # book and apply basis opening balances from the original
00005 #
00006 # Copyright (C) 2009, 2010 ParIT Worker Co-operative <transparency@parit.ca>
00007 # This program is free software; you can redistribute it and/or
00008 # modify it under the terms of the GNU General Public License as
00009 # published by the Free Software Foundation; either version 2 of
00010 # the License, or (at your option) any later version.
00011 #
00012 # This program is distributed in the hope that it will be useful,
00013 # but WITHOUT ANY WARRANTY; without even the implied warranty of
00014 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00015 # GNU General Public License for more details.
00016 #
00017 # You should have received a copy of the GNU General Public License
00018 # along with this program; if not, contact:
00019 # Free Software Foundation           Voice:  +1-617-542-5942
00020 # 51 Franklin Street, Fifth Floor    Fax:    +1-617-542-2652
00021 # Boston, MA  02110-1301,  USA       gnu@gnu.org
00022 #
00023 # @author Mark Jenkins, ParIT Worker Co-operative <mark@parit.ca>
00024 
00025 ##  @file
00026 #   @brief Replicate the account structure of a
00027 #   book and apply basis opening balances from the original
00028 #   @author Mark Jenkins, ParIT Worker Co-operative <mark@parit.ca>
00029 #   @ingroup python_bindings_examples
00030 
00031 from gnucash import Session, Account, Transaction, Split, GncNumeric
00032 from gnucash.gnucash_core_c import \
00033     GNC_DENOM_AUTO, GNC_HOW_DENOM_EXACT, \
00034     ACCT_TYPE_ASSET, ACCT_TYPE_BANK, ACCT_TYPE_CASH, ACCT_TYPE_CHECKING, \
00035     ACCT_TYPE_CREDIT, ACCT_TYPE_EQUITY, ACCT_TYPE_EXPENSE, ACCT_TYPE_INCOME, \
00036     ACCT_TYPE_LIABILITY, ACCT_TYPE_MUTUAL, ACCT_TYPE_PAYABLE, \
00037     ACCT_TYPE_RECEIVABLE, ACCT_TYPE_STOCK, ACCT_TYPE_ROOT, ACCT_TYPE_TRADING
00038 
00039 from sys import argv
00040 from os.path import abspath
00041 from datetime import date
00042 
00043 # This script takes a gnucash url
00044 # and creates a new file/db at a second url that has the same
00045 # account tree and an equivilent opening balance on all the simple balance
00046 # sheet accounts (not for income and expense accounts)
00047 #
00048 # This is done a per currency basis, one opening balance account for each
00049 #
00050 # For non-simple balance sheet accounts (like payable, recievable, stock,
00051 # mutual, and trading, you'll have to put the opening balance in yourself
00052 #
00053 # Invocation examples:
00054 # gnucash-env python new_book_with_opening_balances.py \
00055 #   '/home/mark/test.gnucash'
00056 #   'sqlite3:///home/mark/new_test.gnucash'
00057 #
00058 # gnucash-env python new_book_with_opening_balances.py \
00059 #   '/home/mark/test.gnucash' \
00060 #   'xml:///crypthome/mark/parit-financial-system/new_test.gnucash'
00061 #
00062 # Remember that the gnucash python package has to be in your PYTHONPATH
00063 # if you're installed GnuCash in a non-standard location, you'll have to do
00064 # something like this
00065 # export PYTHONPATH=gnucash_install_path/lib/python2.x/site-packages/
00066 
00067 # argv[1] should be the path to an existing gnucash file/database
00068 # For a file, simply pass the pathname. GnuCash will determine the data format
00069 # xml or sqlite3 automatically.
00070 # For a database you can use these forms:
00071 #   mysql://user:password@host/dbname
00072 #   postgres://user:password@host[:port]/dbname (the port is optional)
00073 #
00074 # argv[2] should be the path for the new gnucash file/database
00075 # For a file, simply pass the pathname prefixed with the requested data format
00076 # like:
00077 #   xml:///home/blah/blah.gnucash
00078 #   sqlite3:///home/blah/blah.gnucash
00079 # Paths can also be relative, for example:
00080 #   xml://from-here/to/there/blah.gnucash
00081 # For a database you can use these forms:
00082 #   mysql://user:password@host/dbname
00083 #   postgres://user:password@host[:port]/dbname (the port is optional)
00084 
00085 
00086 OPENING_DATE = (1, 1, 2011) # day, month, year
00087 
00088 # possible acccount types of interest for opening balances
00089 ACCOUNT_TYPES_TO_OPEN = set( (
00090         ACCT_TYPE_BANK,
00091         ACCT_TYPE_CASH,
00092         ACCT_TYPE_CREDIT,
00093         ACCT_TYPE_ASSET,
00094         ACCT_TYPE_LIABILITY,
00095         ACCT_TYPE_STOCK,
00096         ACCT_TYPE_MUTUAL,
00097         ACCT_TYPE_INCOME,
00098         ACCT_TYPE_EXPENSE,
00099         ACCT_TYPE_EQUITY,
00100         ACCT_TYPE_RECEIVABLE,
00101         ACCT_TYPE_PAYABLE,
00102         ACCT_TYPE_TRADING,
00103 ))
00104 
00105 # You don't need an opening balance for income and expenses, past income
00106 # and expenses should be in Equity->retained earnings
00107 # so we remove them from the above set
00108 ACCOUNT_TYPES_TO_OPEN = ACCOUNT_TYPES_TO_OPEN.difference( set((
00109             ACCT_TYPE_INCOME,
00110             ACCT_TYPE_EXPENSE,
00111             )) )
00112 
00113 # This script isn't capable of properly creating the lots required for
00114 # STOCK, MUTUAL, RECEIVABLE, and PAYABLE -- you'll have to create opening
00115 # balances for them manually; so they are not included in the set for
00116 # opening balances
00117 ACCOUNT_TYPES_TO_OPEN = ACCOUNT_TYPES_TO_OPEN.difference( set((
00118             ACCT_TYPE_STOCK,
00119             ACCT_TYPE_MUTUAL,
00120             ACCT_TYPE_RECEIVABLE,
00121             ACCT_TYPE_PAYABLE,
00122             )) )
00123 
00124 # this script isn't capable of properly setting up the transactions for
00125 # ACCT_TYPE_TRADING, you'll have to create opening balances for them mannually;
00126 # so, they are not included in the set of accounts used for opening balances
00127 ACCOUNT_TYPES_TO_OPEN.remove(ACCT_TYPE_TRADING)
00128 
00129 OPENING_BALANCE_ACCOUNT = ( 'Equity', 'Opening Balances')
00130 
00131 # if possible, this program will try to use the account above for the
00132 # currency listed below, and a variation of the above
00133 # Equity->"Opening Balances Symbol" for all other currencies
00134 PREFERED_CURRENCY_FOR_SIMPLE_OPENING_BALANCE = ("CURRENCY", "CAD")
00135 
00136 def initialize_split(book, value, account, trans):
00137     split = Split(book)
00138     split.SetValue(value)
00139     split.SetAccount(account)
00140     split.SetParent(trans)
00141     return split
00142 
00143 
00144 def record_opening_balance(original_account, new_account, new_book,
00145                            opening_balance_per_currency, commodity_tuple
00146                            ):
00147     # create an opening balance if the account type is right
00148     if new_account.GetType() in ACCOUNT_TYPES_TO_OPEN:
00149         final_balance = original_account.GetBalance()
00150         if final_balance.num() != 0:
00151             # if there is a new currency type, associate with the currency
00152             # a Transaction which will be the opening transaction for that
00153             # currency and a GncNumeric value which will be the opening
00154             # balance acccount amount
00155             if commodity_tuple not in opening_balance_per_currency:
00156                 trans = Transaction(new_book)
00157                 trans.BeginEdit()
00158                 opening_balance_per_currency[commodity_tuple] = (
00159                     trans, GncNumeric(0, 1) )
00160             trans, total = opening_balance_per_currency[commodity_tuple]
00161 
00162             new_total = total.sub(
00163                 final_balance,
00164                 GNC_DENOM_AUTO, GNC_HOW_DENOM_EXACT )
00165             
00166             initialize_split(
00167                 new_book,
00168                 final_balance,
00169                 new_account, trans)
00170             opening_balance_per_currency[commodity_tuple] = \
00171                 (trans, new_total )
00172 
00173 def recursivly_build_account_tree(original_parent_account,
00174                                   new_parent_account,
00175                                   new_book,
00176                                   new_commodity_table,
00177                                   opening_balance_per_currency,
00178                                   account_types_to_open ):
00179 
00180     for child in original_parent_account.get_children():
00181         original_account = Account(instance=child)
00182         new_account = Account(new_book)
00183         # attach new account to its parent
00184         new_parent_account.append_child(new_account)
00185 
00186         # copy simple attributes
00187         for attribute in ('Name', 'Type', 'Description', 'Notes',
00188                           'Code', 'TaxRelated', 'Placeholder'):
00189             # new_account.SetAttribute( original_account.GetAttribute() )
00190             getattr(new_account, 'Set' + attribute)(
00191                 getattr(original_account, 'Get' + attribute)() )
00192 
00193         # copy commodity
00194         orig_commodity = original_account.GetCommodity()
00195         namespace = orig_commodity.get_namespace()
00196         mnemonic = orig_commodity.get_mnemonic()
00197         new_commodity = new_commodity_table.lookup(namespace, mnemonic)
00198         assert(new_commodity.get_instance() != None )
00199         new_account.SetCommodity(new_commodity)
00200 
00201         record_opening_balance( original_account, new_account,
00202                                 new_book, opening_balance_per_currency,
00203                                 (namespace, mnemonic),
00204                                 )
00205 
00206         recursivly_build_account_tree(original_account,
00207                                       new_account,
00208                                       new_book,
00209                                       new_commodity_table,
00210                                       opening_balance_per_currency,
00211                                       account_types_to_open)
00212 
00213 def reconstruct_account_name_with_mnemonic(account_tuple, mnemonic):
00214     opening_balance_account_pieces = list(account_tuple)
00215     opening_balance_account_pieces[
00216         len(opening_balance_account_pieces) - 1 ] += " - " + mnemonic    
00217     return opening_balance_account_pieces
00218 
00219 def find_or_make_account(account_tuple, root_account, book,
00220                          currency ):
00221     current_account_name, account_path = account_tuple[0], account_tuple[1:]
00222     current_account = root_account.lookup_by_name(current_account_name)
00223     if current_account.get_instance() == None:
00224         current_account = Account(book)
00225         current_account.SetName(current_account_name)
00226         current_account.SetCommodity(currency)
00227         root_account.append_child(current_account)
00228     
00229     if len(account_path) > 0:
00230         return find_or_make_account(account_path, current_account, book,
00231                                     currency)
00232     else:
00233         account_commod = current_account.GetCommodity()
00234         if (account_commod.get_mnemonic(),
00235             account_commod.get_namespace() ) == \
00236             (currency.get_mnemonic(),
00237              currency.get_namespace()) :
00238             return current_account
00239         else:
00240             return None
00241 
00242 def choke_on_none_for_no_account(opening_account, extra_string ):
00243     if opening_account == None:
00244         raise Exception("account currency and name mismatch, " + extra_string)
00245 
00246 def create_opening_balance_transaction(commodtable, namespace, mnemonic,
00247                                        new_book_root, new_book,
00248                                        opening_trans, opening_amount,
00249                                        simple_opening_name_used):
00250     currency = commodtable.lookup(namespace, mnemonic)
00251     assert( currency.get_instance() != None )
00252 
00253     if simple_opening_name_used:
00254         account_pieces = reconstruct_account_name_with_mnemonic(
00255             OPENING_BALANCE_ACCOUNT,
00256             mnemonic)
00257         opening_account = find_or_make_account(
00258             account_pieces, new_book_root, new_book, currency )
00259         choke_on_none_for_no_account(opening_account,
00260                                      ', '.join(account_pieces) )
00261     else:
00262         opening_account = find_or_make_account(OPENING_BALANCE_ACCOUNT,
00263                                                new_book_root, new_book,
00264                                                currency )
00265         simple_opening_name_used = True
00266         if opening_account == None:
00267             account_pieces = reconstruct_account_name_with_mnemonic(
00268                 OPENING_BALANCE_ACCOUNT,
00269                 mnemonic)
00270             opening_account = find_or_make_account(
00271                 account_pieces, new_book_root, new_book, currency )
00272             choke_on_none_for_no_account(opening_account,
00273                                              ', '.join(account_pieces) )
00274 
00275     # we don't need to use the opening balance account at all if all
00276     # the accounts being given an opening balance balance out
00277     if opening_amount.num() != 0:
00278         initialize_split(new_book, opening_amount, opening_account,
00279                          opening_trans)
00280 
00281     opening_trans.SetDate( *OPENING_DATE )
00282     opening_trans.SetCurrency(currency)
00283     opening_trans.SetDescription("Opening Balance")
00284     opening_trans.CommitEdit()
00285 
00286     return simple_opening_name_used
00287         
00288 def main():
00289 
00290     if len(argv) < 3:
00291         print 'not enough parameters'
00292         print 'usage: new_book_with_opening_balances.py {source_book_url} {destination_book_url}'
00293         print 'examples:'
00294         print "gnucash-env python new_book_with_opening_balances.py '/home/username/test.gnucash' 'sqlite3:///home/username/new_test.gnucash'"
00295         print "gnucash-env python new_book_with_opening_balances.py '/home/username/test.gnucash' 'xml:///crypthome/username/finances/new_test.gnucash'"
00296         return
00297 
00298     #have everything in a try block to unable us to release our hold on stuff to the extent possible
00299     try:
00300         original_book_session = Session(argv[1], is_new=False)
00301         new_book_session = Session(argv[2], is_new=True)
00302         new_book = new_book_session.get_book()
00303         new_book_root = new_book.get_root_account()
00304 
00305         commodtable = new_book.get_table()
00306         # we discovered that if we didn't have this save early on, there would
00307         # be trouble later
00308         new_book_session.save()
00309 
00310         opening_balance_per_currency = {}
00311         recursivly_build_account_tree(
00312             original_book_session.get_book().get_root_account(),
00313             new_book_root,
00314             new_book,
00315             commodtable,
00316             opening_balance_per_currency,
00317             ACCOUNT_TYPES_TO_OPEN
00318             )
00319 
00320         (namespace, mnemonic) = PREFERED_CURRENCY_FOR_SIMPLE_OPENING_BALANCE
00321         if (namespace, mnemonic) in opening_balance_per_currency:
00322             opening_trans, opening_amount = opening_balance_per_currency[
00323                 (namespace, mnemonic)]
00324             simple_opening_name_used = create_opening_balance_transaction(
00325                 commodtable, namespace, mnemonic,
00326                 new_book_root, new_book,
00327                 opening_trans, opening_amount,
00328                 False )
00329             del opening_balance_per_currency[
00330                 PREFERED_CURRENCY_FOR_SIMPLE_OPENING_BALANCE]
00331         else:
00332             simple_opening_name_used = False
00333 
00334         for (namespace, mnemonic), (opening_trans, opening_amount) in \
00335                 opening_balance_per_currency.iteritems() :
00336             simple_opening_name_used = create_opening_balance_transaction(
00337                 commodtable, namespace, mnemonic,
00338                 new_book_root, new_book,
00339                 opening_trans, opening_amount,
00340                 simple_opening_name_used )
00341 
00342         new_book_session.save()
00343         new_book_session.end()
00344         original_book_session.end()
00345     except:
00346         if not original_book_session == None:
00347             original_book_session.end()
00348  
00349         if not new_book_session == None:
00350             new_book_session.end()
00351 
00352         raise
00353     
00354 
00355 if __name__ == "__main__":
00356     main()
00357 
00358 
 All Data Structures Files Functions Variables Typedefs Enumerations Enumerator Defines