30 from sys
import argv, stdout
31 from datetime
import date, timedelta
32 from bisect
import bisect_right
33 from decimal
import Decimal
34 from math
import log10
38 from gnucash
import Session, GncNumeric, Split, SessionOpenMode
65 PERIODS = {
"monthly": 1,
71 ONE_DAY = timedelta(days=1)
73 DEBITS_SHOW, CREDITS_SHOW = (
"debits-show",
"credits-show")
77 def gnc_numeric_to_python_Decimal(numeric):
78 negative = numeric.negative_p()
83 copy =
GncNumeric(numeric.num(), numeric.denom())
84 result = copy.to_decimal(
None)
86 raise Exception(
"gnc numeric value %s can't be converted to decimal" %
88 digit_tuple = tuple( int(char)
89 for char
in str(copy.num())
91 denominator = copy.denom()
92 exponent = int(log10(denominator))
93 assert( (10 ** exponent) == denominator )
94 return Decimal( (sign, digit_tuple, -exponent) )
97 def next_period_start(start_year, start_month, period_type):
99 end_month = start_month + PERIODS[period_type]
111 end_year = start_year + ( (end_month-1) / NUM_MONTHS )
112 end_month = ( (end_month-1) % NUM_MONTHS ) + 1
114 return end_year, end_month
117 def period_end(start_year, start_month, period_type):
118 if period_type
not in PERIODS:
119 raise Exception(
"%s is not a valid period, should be %s" % (
120 period_type, str(list(PERIODS.keys())) ) )
122 end_year, end_month = next_period_start(start_year, start_month,
128 return date(end_year, end_month, 1) - ONE_DAY
131 def generate_period_boundaries(start_year, start_month, period_type, periods):
132 for i
in range(periods):
133 yield ( date(start_year, start_month, 1),
134 period_end(start_year, start_month, period_type) )
135 start_year, start_month = next_period_start(start_year, start_month,
138 def account_from_path(top_account, account_path, original_path=None):
139 if original_path==
None: original_path = account_path
140 account, account_path = account_path[0], account_path[1:]
142 account = top_account.lookup_by_name(account)
145 "path " +
''.join(original_path) +
" could not be found")
146 if len(account_path) > 0 :
147 return account_from_path(account, account_path, original_path)
155 print(
'not enough parameters')
156 print(
'usage: account_analysis.py {book url} {start year} {start month, numeric} {period type: monthly, quarterly, or yearly} {number of periods to show, from start year and month} {whether to show debits: debits-show for true, all other values false} {whether to show credits: credits-show for true, all other values false} {space separated account path, as many nested levels as desired} ')
158 print(
"The following example analyzes 12 months of 'Assets:Test Account' from /home/username/test.gnucash, starting in January of 2010, and shows both credits and debits")
159 print(
"python3 account_analysis.py '/home/username/test.gnucash' 2010 1 monthly 12 debits-show credits-show Assets 'Test Account'\n")
160 print(
"The following example analyzes 2 quarters of 'Liabilities:First Level:Second Level' from /home/username/test.gnucash, starting March 2011, and shows credits but not debits")
161 print(
"python3 account_analysis.py '/home/username/test.gnucash' 2011 3 quarterly 2 debits-noshow credits-show Liabilities 'First Level' 'Second Level")
165 (gnucash_file, start_year, start_month, period_type, periods,
166 debits_show, credits_show) = argv[1:8]
167 start_year, start_month, periods = [int(blah)
168 for blah
in (start_year, start_month,
171 debits_show = debits_show == DEBITS_SHOW
172 credits_show = credits_show == CREDITS_SHOW
174 account_path = argv[8:]
176 gnucash_session = Session(gnucash_file, SessionOpenMode.SESSION_NORMAL_OPEN)
177 root_account = gnucash_session.book.get_root_account()
178 account_of_interest = account_from_path(root_account, account_path)
184 [start_date, end_date,
190 for start_date, end_date
in generate_period_boundaries(
191 start_year, start_month, period_type, periods)
194 period_starts = [e[0]
for e
in period_list ]
197 for split
in account_of_interest.GetSplitList():
199 trans_date = date.fromtimestamp(trans.GetDate())
203 period_index = bisect_right( period_starts, trans_date ) - 1
208 if period_index >= 0
and \
209 trans_date <= period_list[len(period_list)-1][1]:
212 period = period_list[period_index]
222 assert( trans_date>= period[0]
and trans_date <= period[1] )
224 split_amount = gnc_numeric_to_python_Decimal(split.GetAmount())
227 if split_amount < ZERO:
228 debit_credit_offset = 1
231 debit_credit_offset = 0
237 period[2+debit_credit_offset].append( (trans, split) )
241 period[4+debit_credit_offset] += split_amount
243 csv_writer = csv.writer(stdout)
244 csv_writer.writerow( (
'period start',
'period end',
'debits',
'credits') )
246 def generate_detail_rows(values):
248 (
'',
'',
'',
'', trans.GetDescription(),
249 gnc_numeric_to_python_Decimal(split.GetAmount()))
250 for trans, split
in values )
253 for start_date, end_date, debits, credits, debit_sum, credit_sum
in \
255 csv_writer.writerow( (start_date, end_date, debit_sum, credit_sum) )
257 if debits_show
and len(debits) > 0:
259 (
'DEBITS',
'',
'',
'',
'description',
'value') )
260 csv_writer.writerows( generate_detail_rows(debits) )
261 csv_writer.writerow( () )
262 if credits_show
and len(credits) > 0:
264 (
'CREDITS',
'',
'',
'',
'description',
'value') )
265 csv_writer.writerows( generate_detail_rows(credits) )
266 csv_writer.writerow( () )
269 gnucash_session.end()
271 if "gnucash_session" in locals():
272 gnucash_session.end()
276 if __name__ ==
"__main__": main()
The primary numeric class for representing amounts and values.