|
GnuCash 2.4.99
|
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
1.7.4