GnuCash  2.6.99
 All Data Structures Namespaces Files Functions Variables Typedefs Enumerations Enumerator Macros Groups Pages
new_book_with_opening_balances.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 
3 # new_book_with_opening_balances.py -- Replicate the account structure of a
4 # book and apply basis opening balances from the original
5 #
6 # Copyright (C) 2009, 2010 ParIT Worker Co-operative <[email protected]>
7 # This program is free software; you can redistribute it and/or
8 # modify it under the terms of the GNU General Public License as
9 # published by the Free Software Foundation; either version 2 of
10 # the License, or (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with this program; if not, contact:
19 # Free Software Foundation Voice: +1-617-542-5942
20 # 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652
21 # Boston, MA 02110-1301, USA [email protected]
22 #
23 # @author Mark Jenkins, ParIT Worker Co-operative <[email protected]>
24 
25 ## @file
26 # @brief Replicate the account structure of a
27 # book and apply basis opening balances from the original
28 # @author Mark Jenkins, ParIT Worker Co-operative <[email protected]>
29 # @ingroup python_bindings_examples
30 
31 from gnucash import Session, Account, Transaction, Split, GncNumeric
32 from gnucash.gnucash_core_c import \
33  GNC_DENOM_AUTO, GNC_HOW_DENOM_EXACT, \
34  ACCT_TYPE_ASSET, ACCT_TYPE_BANK, ACCT_TYPE_CASH, ACCT_TYPE_CHECKING, \
35  ACCT_TYPE_CREDIT, ACCT_TYPE_EQUITY, ACCT_TYPE_EXPENSE, ACCT_TYPE_INCOME, \
36  ACCT_TYPE_LIABILITY, ACCT_TYPE_MUTUAL, ACCT_TYPE_PAYABLE, \
37  ACCT_TYPE_RECEIVABLE, ACCT_TYPE_STOCK, ACCT_TYPE_ROOT, ACCT_TYPE_TRADING
38 
39 from sys import argv
40 from os.path import abspath
41 from datetime import date
42 
43 # This script takes a gnucash url
44 # and creates a new file/db at a second url that has the same
45 # account tree and an equivilent opening balance on all the simple balance
46 # sheet accounts (not for income and expense accounts)
47 #
48 # This is done a per currency basis, one opening balance account for each
49 #
50 # For non-simple balance sheet accounts (like payable, recievable, stock,
51 # mutual, and trading, you'll have to put the opening balance in yourself
52 #
53 # Invocation examples:
54 # gnucash-env python new_book_with_opening_balances.py \
55 # '/home/mark/test.gnucash'
56 # 'sqlite3:///home/mark/new_test.gnucash'
57 #
58 # gnucash-env python new_book_with_opening_balances.py \
59 # '/home/mark/test.gnucash' \
60 # 'xml:///crypthome/mark/parit-financial-system/new_test.gnucash'
61 #
62 # Remember that the gnucash python package has to be in your PYTHONPATH
63 # if you're installed GnuCash in a non-standard location, you'll have to do
64 # something like this
65 # export PYTHONPATH=gnucash_install_path/lib/python2.x/site-packages/
66 
67 # argv[1] should be the path to an existing gnucash file/database
68 # For a file, simply pass the pathname. GnuCash will determine the data format
69 # xml or sqlite3 automatically.
70 # For a database you can use these forms:
71 # mysql://user:password@host/dbname
72 # postgres://user:password@host[:port]/dbname (the port is optional)
73 #
74 # argv[2] should be the path for the new gnucash file/database
75 # For a file, simply pass the pathname prefixed with the requested data format
76 # like:
77 # xml:///home/blah/blah.gnucash
78 # sqlite3:///home/blah/blah.gnucash
79 # Paths can also be relative, for example:
80 # xml://from-here/to/there/blah.gnucash
81 # For a database you can use these forms:
82 # mysql://user:password@host/dbname
83 # postgres://user:password@host[:port]/dbname (the port is optional)
84 
85 
86 OPENING_DATE = (1, 1, 2011) # day, month, year
87 
88 # possible acccount types of interest for opening balances
89 ACCOUNT_TYPES_TO_OPEN = set( (
90  ACCT_TYPE_BANK,
91  ACCT_TYPE_CASH,
92  ACCT_TYPE_CREDIT,
93  ACCT_TYPE_ASSET,
94  ACCT_TYPE_LIABILITY,
95  ACCT_TYPE_STOCK,
96  ACCT_TYPE_MUTUAL,
97  ACCT_TYPE_INCOME,
98  ACCT_TYPE_EXPENSE,
99  ACCT_TYPE_EQUITY,
100  ACCT_TYPE_RECEIVABLE,
101  ACCT_TYPE_PAYABLE,
102  ACCT_TYPE_TRADING,
103 ))
104 
105 # You don't need an opening balance for income and expenses, past income
106 # and expenses should be in Equity->retained earnings
107 # so we remove them from the above set
108 ACCOUNT_TYPES_TO_OPEN = ACCOUNT_TYPES_TO_OPEN.difference( set((
109  ACCT_TYPE_INCOME,
110  ACCT_TYPE_EXPENSE,
111  )) )
112 
113 # This script isn't capable of properly creating the lots required for
114 # STOCK, MUTUAL, RECEIVABLE, and PAYABLE -- you'll have to create opening
115 # balances for them manually; so they are not included in the set for
116 # opening balances
117 ACCOUNT_TYPES_TO_OPEN = ACCOUNT_TYPES_TO_OPEN.difference( set((
118  ACCT_TYPE_STOCK,
119  ACCT_TYPE_MUTUAL,
120  ACCT_TYPE_RECEIVABLE,
121  ACCT_TYPE_PAYABLE,
122  )) )
123 
124 # this script isn't capable of properly setting up the transactions for
125 # ACCT_TYPE_TRADING, you'll have to create opening balances for them mannually;
126 # so, they are not included in the set of accounts used for opening balances
127 ACCOUNT_TYPES_TO_OPEN.remove(ACCT_TYPE_TRADING)
128 
129 OPENING_BALANCE_ACCOUNT = ( 'Equity', 'Opening Balances')
130 
131 # if possible, this program will try to use the account above for the
132 # currency listed below, and a variation of the above
133 # Equity->"Opening Balances Symbol" for all other currencies
134 PREFERED_CURRENCY_FOR_SIMPLE_OPENING_BALANCE = ("CURRENCY", "CAD")
135 
136 def initialize_split(book, value, account, trans):
137  split = Split(book)
138  split.SetValue(value)
139  split.SetAccount(account)
140  split.SetParent(trans)
141  return split
142 
143 
144 def record_opening_balance(original_account, new_account, new_book,
145  opening_balance_per_currency, commodity_tuple
146  ):
147  # create an opening balance if the account type is right
148  if new_account.GetType() in ACCOUNT_TYPES_TO_OPEN:
149  final_balance = original_account.GetBalance()
150  if final_balance.num() != 0:
151  # if there is a new currency type, associate with the currency
152  # a Transaction which will be the opening transaction for that
153  # currency and a GncNumeric value which will be the opening
154  # balance acccount amount
155  if commodity_tuple not in opening_balance_per_currency:
156  trans = Transaction(new_book)
157  trans.BeginEdit()
158  opening_balance_per_currency[commodity_tuple] = (
159  trans, GncNumeric(0, 1) )
160  trans, total = opening_balance_per_currency[commodity_tuple]
161 
162  new_total = total.sub(
163  final_balance,
164  GNC_DENOM_AUTO, GNC_HOW_DENOM_EXACT )
165 
166  initialize_split(
167  new_book,
168  final_balance,
169  new_account, trans)
170  opening_balance_per_currency[commodity_tuple] = \
171  (trans, new_total )
172 
173 def recursivly_build_account_tree(original_parent_account,
174  new_parent_account,
175  new_book,
176  new_commodity_table,
177  opening_balance_per_currency,
178  account_types_to_open ):
179 
180  for child in original_parent_account.get_children():
181  original_account = child
182  new_account = Account(new_book)
183  # attach new account to its parent
184  new_parent_account.append_child(new_account)
185 
186  # copy simple attributes
187  for attribute in ('Name', 'Type', 'Description', 'Notes',
188  'Code', 'TaxRelated', 'Placeholder'):
189  # new_account.SetAttribute( original_account.GetAttribute() )
190  getattr(new_account, 'Set' + attribute)(
191  getattr(original_account, 'Get' + attribute)() )
192 
193  # copy commodity
194  orig_commodity = original_account.GetCommodity()
195  namespace = orig_commodity.get_namespace()
196  mnemonic = orig_commodity.get_mnemonic()
197  new_commodity = new_commodity_table.lookup(namespace, mnemonic)
198  if new_commodity == None:
199  new_commodity = orig_commodity.clone(new_book)
200  new_commodity_table.insert(new_commodity)
201  new_account.SetCommodity(new_commodity)
202 
203  record_opening_balance( original_account, new_account,
204  new_book, opening_balance_per_currency,
205  (namespace, mnemonic),
206  )
207 
208  recursivly_build_account_tree(original_account,
209  new_account,
210  new_book,
211  new_commodity_table,
212  opening_balance_per_currency,
213  account_types_to_open)
214 
215 def reconstruct_account_name_with_mnemonic(account_tuple, mnemonic):
216  opening_balance_account_pieces = list(account_tuple)
217  opening_balance_account_pieces[
218  len(opening_balance_account_pieces) - 1 ] += " - " + mnemonic
219  return opening_balance_account_pieces
220 
221 def find_or_make_account(account_tuple, root_account, book,
222  currency ):
223  current_account_name, account_path = account_tuple[0], account_tuple[1:]
224  current_account = root_account.lookup_by_name(current_account_name)
225  if current_account == None:
226  current_account = Account(book)
227  current_account.SetName(current_account_name)
228  current_account.SetCommodity(currency)
229  root_account.append_child(current_account)
230 
231  if len(account_path) > 0:
232  return find_or_make_account(account_path, current_account, book,
233  currency)
234  else:
235  account_commod = current_account.GetCommodity()
236  if (account_commod.get_mnemonic(),
237  account_commod.get_namespace() ) == \
238  (currency.get_mnemonic(),
239  currency.get_namespace()) :
240  return current_account
241  else:
242  return None
243 
244 def choke_on_none_for_no_account(opening_account, extra_string ):
245  if opening_account == None:
246  raise Exception("account currency and name mismatch, " + extra_string)
247 
248 def create_opening_balance_transaction(commodtable, namespace, mnemonic,
249  new_book_root, new_book,
250  opening_trans, opening_amount,
251  simple_opening_name_used):
252  currency = commodtable.lookup(namespace, mnemonic)
253  assert( currency.get_instance() != None )
254 
255  if simple_opening_name_used:
256  account_pieces = reconstruct_account_name_with_mnemonic(
257  OPENING_BALANCE_ACCOUNT,
258  mnemonic)
259  opening_account = find_or_make_account(
260  account_pieces, new_book_root, new_book, currency )
261  choke_on_none_for_no_account(opening_account,
262  ', '.join(account_pieces) )
263  else:
264  opening_account = find_or_make_account(OPENING_BALANCE_ACCOUNT,
265  new_book_root, new_book,
266  currency )
267  simple_opening_name_used = True
268  if opening_account == None:
269  account_pieces = reconstruct_account_name_with_mnemonic(
270  OPENING_BALANCE_ACCOUNT,
271  mnemonic)
272  opening_account = find_or_make_account(
273  account_pieces, new_book_root, new_book, currency )
274  choke_on_none_for_no_account(opening_account,
275  ', '.join(account_pieces) )
276 
277  # we don't need to use the opening balance account at all if all
278  # the accounts being given an opening balance balance out
279  if opening_amount.num() != 0:
280  initialize_split(new_book, opening_amount, opening_account,
281  opening_trans)
282 
283  opening_trans.SetDate( *OPENING_DATE )
284  opening_trans.SetCurrency(currency)
285  opening_trans.SetDescription("Opening Balance")
286  opening_trans.CommitEdit()
287 
288  return simple_opening_name_used
289 
290 def main():
291 
292  if len(argv) < 3:
293  print 'not enough parameters'
294  print 'usage: new_book_with_opening_balances.py {source_book_url} {destination_book_url}'
295  print 'examples:'
296  print "gnucash-env python new_book_with_opening_balances.py '/home/username/test.gnucash' 'sqlite3:///home/username/new_test.gnucash'"
297  print "gnucash-env python new_book_with_opening_balances.py '/home/username/test.gnucash' 'xml:///crypthome/username/finances/new_test.gnucash'"
298  return
299 
300  #have everything in a try block to unable us to release our hold on stuff to the extent possible
301  try:
302  original_book_session = Session(argv[1], is_new=False)
303  new_book_session = Session(argv[2], is_new=True)
304  new_book = new_book_session.get_book()
305  new_book_root = new_book.get_root_account()
306 
307  commodtable = new_book.get_table()
308  # we discovered that if we didn't have this save early on, there would
309  # be trouble later
310  new_book_session.save()
311 
312  opening_balance_per_currency = {}
313  recursivly_build_account_tree(
314  original_book_session.get_book().get_root_account(),
315  new_book_root,
316  new_book,
317  commodtable,
318  opening_balance_per_currency,
319  ACCOUNT_TYPES_TO_OPEN
320  )
321 
322  (namespace, mnemonic) = PREFERED_CURRENCY_FOR_SIMPLE_OPENING_BALANCE
323  if (namespace, mnemonic) in opening_balance_per_currency:
324  opening_trans, opening_amount = opening_balance_per_currency[
325  (namespace, mnemonic)]
326  simple_opening_name_used = create_opening_balance_transaction(
327  commodtable, namespace, mnemonic,
328  new_book_root, new_book,
329  opening_trans, opening_amount,
330  False )
331  del opening_balance_per_currency[
332  PREFERED_CURRENCY_FOR_SIMPLE_OPENING_BALANCE]
333  else:
334  simple_opening_name_used = False
335 
336  for (namespace, mnemonic), (opening_trans, opening_amount) in \
337  opening_balance_per_currency.iteritems() :
338  simple_opening_name_used = create_opening_balance_transaction(
339  commodtable, namespace, mnemonic,
340  new_book_root, new_book,
341  opening_trans, opening_amount,
342  simple_opening_name_used )
343 
344  new_book_session.save()
345  new_book_session.end()
346  original_book_session.end()
347  except:
348  if "original_book_session" in locals():
349  original_book_session.end()
350 
351  if "new_book_session" in locals():
352  new_book_session.end()
353 
354  raise
355 
356 
357 if __name__ == "__main__":
358  main()
359 
360 
Definition: SplitP.h:71