1
|
1 |
#!/usr/bin/env python
|
|
2 |
# -*- coding: utf-8 -*-
|
|
3 |
import gnucash
|
|
4 |
import datetime
|
|
5 |
import logging
|
|
6 |
|
|
7 |
ZERO = gnucash.GncNumeric()
|
|
8 |
|
|
9 |
def recurse_all_splits(account):
|
|
10 |
for acc in account.get_descendants():
|
|
11 |
for split in acc.GetSplitList():
|
|
12 |
yield split
|
|
13 |
|
|
14 |
def check_in_date(start_date, end_date, d):
|
|
15 |
return start_date <= d and d <= end_date
|
|
16 |
|
|
17 |
def check_split_date_func(start_date, end_date):
|
|
18 |
def f(split):
|
|
19 |
parent = split.GetParent()
|
|
20 |
if parent == None:
|
|
21 |
return False
|
|
22 |
d = datetime.date.fromtimestamp(split.GetParent().GetDate())
|
|
23 |
return start_date <= d and d <= end_date
|
|
24 |
return f
|
|
25 |
|
|
26 |
def generate_splits(root, start, end):
|
|
27 |
f = check_split_date_func(start, end)
|
|
28 |
return (split for split in recurse_all_splits(root) if f(split))
|
|
29 |
|
|
30 |
def generate_summary_amounts(root, start, end):
|
|
31 |
prevAccount = None
|
|
32 |
prevAccountName = "<None>"
|
|
33 |
zero = (0, ZERO, ZERO)
|
|
34 |
pos = zero
|
|
35 |
neg = zero
|
|
36 |
for split in generate_splits(root, start, end):
|
|
37 |
account = split.account
|
|
38 |
amount = split.GetAmount()
|
|
39 |
value = split.GetValue()
|
|
40 |
name = account.get_full_name()
|
|
41 |
is_neg = amount.negative_p()
|
|
42 |
if account.get_full_name() != prevAccountName:
|
|
43 |
if prevAccount != None:
|
|
44 |
yield (prevAccount,) + pos
|
|
45 |
yield (prevAccount,) + neg
|
|
46 |
prevAccount = account
|
|
47 |
prevAccountName = name
|
|
48 |
if is_neg:
|
|
49 |
pos = zero
|
|
50 |
neg = (1, amount, value)
|
|
51 |
else:
|
|
52 |
pos = (1, amount, value)
|
|
53 |
neg = zero
|
|
54 |
else:
|
|
55 |
tmp = neg if is_neg else pos
|
|
56 |
tmp = (tmp[0]+1,
|
|
57 |
tmp[1].add(amount, gnucash.GNC_DENOM_AUTO, gnucash.GNC_HOW_DENOM_EXACT),
|
|
58 |
tmp[2].add(value, gnucash.GNC_DENOM_AUTO, gnucash.GNC_HOW_DENOM_EXACT))
|
|
59 |
if is_neg:
|
|
60 |
neg = tmp
|
|
61 |
else:
|
|
62 |
pos = tmp
|
|
63 |
if prevAccount != None:
|
|
64 |
yield (prevAccount,) + pos
|
|
65 |
yield (prevAccount,) + neg
|
|
66 |
|
|
67 |
def transaction_to_string(trans, currency):
|
|
68 |
result = str(datetime.date.fromtimestamp(trans.GetDate()))
|
|
69 |
result += " " + trans.GetDescription() + "\n"
|
|
70 |
for split in trans.GetSplitList():
|
|
71 |
result += "\t" + split.account.get_full_name() + " "
|
|
72 |
result += " "
|
|
73 |
c = split.account.GetCommodity()
|
|
74 |
a = split.GetAmount()
|
|
75 |
result += str(a.to_double()) + " " + c.get_nice_symbol()
|
|
76 |
if not currency.equal(c):
|
|
77 |
v = split.GetValue()
|
|
78 |
result += " => " + str(v.to_double()) + " " + currency.get_nice_symbol()
|
|
79 |
result += " ( " + str(v.div(a, gnucash.GNC_DENOM_AUTO, gnucash.GNC_HOW_DENOM_EXACT).to_double())
|
|
80 |
result += " " + c.get_nice_symbol() + " )"
|
|
81 |
result += "\n"
|
|
82 |
imbl = trans.GetImbalanceValue()
|
|
83 |
result += "Balance: " + str(imbl.to_double()) + " " + currency.get_nice_symbol()
|
|
84 |
if not imbl.equal(ZERO):
|
|
85 |
for c, v in trans.GetImbalance():
|
|
86 |
result += "\n\t"
|
|
87 |
result += " => " + str(v.to_double()) + " " + c.get_nice_symbol()
|
|
88 |
return result
|
|
89 |
|
|
90 |
def delete_transactions(root, start, end, filt):
|
|
91 |
removed = 0
|
|
92 |
for split in generate_splits(root, start, end):
|
|
93 |
t = split.GetParent()
|
|
94 |
if filt(t):
|
|
95 |
removed += t.CountSplits()
|
|
96 |
t.Destroy()
|
|
97 |
logging.info("%d splits removed.", removed)
|
|
98 |
|
|
99 |
def transaction_not_equal_p(trans):
|
|
100 |
gtrans = trans.GetGUID()
|
|
101 |
def f(t):
|
|
102 |
return not gtrans.equal(t.GetGUID())
|
|
103 |
return f
|
|
104 |
|
|
105 |
def generate_summary_splits(root, start, end):
|
|
106 |
book = root.get_book()
|
|
107 |
for account, count, amount, value in generate_summary_amounts(root, start, end):
|
|
108 |
if not amount.zero_p():
|
|
109 |
split = gnucash.Split(book)
|
|
110 |
split.SetAccount(account)
|
|
111 |
price = value.div(amount, gnucash.GNC_DENOM_AUTO, gnucash.GNC_HOW_DENOM_EXACT)
|
|
112 |
split.SetSharePriceAndAmount(price, amount)
|
|
113 |
yield split, count
|
|
114 |
|
|
115 |
def create_summary(root, start, end, currency, title):
|
|
116 |
logging.info("Creating summary %s for %s between %s and %s",
|
|
117 |
title, root.get_full_name(), start, end)
|
|
118 |
book = root.get_book()
|
|
119 |
trans = gnucash.Transaction(book)
|
|
120 |
trans.BeginEdit()
|
|
121 |
trans.SetDate(end.day, end.month, end.year)
|
|
122 |
trans.SetDescription(title)
|
|
123 |
trans.SetCurrency(currency)
|
|
124 |
total = 0
|
|
125 |
for split, count in generate_summary_splits(root, start, end):
|
|
126 |
split.SetParent(trans)
|
|
127 |
total += count
|
|
128 |
if trans.CountSplits() > 0:
|
|
129 |
logging.info("%d splits summary into one transaction '%s'.", total, title)
|
|
130 |
logging.debug(transaction_to_string(trans, currency))
|
|
131 |
assert(trans.IsBalanced())
|
|
132 |
trans.CommitEdit()
|
|
133 |
if total > 0:
|
|
134 |
delete_transactions(root, start, end, transaction_not_equal_p(trans))
|
|
135 |
else:
|
|
136 |
logging.info("No splits found for this date")
|
|
137 |
|
|
138 |
def make_summary(root, currency, start, end, title):
|
|
139 |
create_summary(root, start, end, currency, title)
|
|
140 |
|
|
141 |
def lookup_currency(book, currency):
|
|
142 |
return book.get_table().lookup("CURRENCY", currency)
|
|
143 |
|
|
144 |
## Datetime support
|
|
145 |
ONEDAY = datetime.timedelta(days=1)
|
|
146 |
|
|
147 |
def first_day_of_month(d):
|
|
148 |
return datetime.date(d.year, d.month, 1)
|
|
149 |
def first_day_of_year(d):
|
|
150 |
return datetime.date(d.year, 1, 1)
|
|
151 |
|
|
152 |
def next_day(d):
|
|
153 |
return d + ONEDAY
|
|
154 |
def next_month(d):
|
|
155 |
if d.month == 12:
|
|
156 |
return datetime.date(d.year+1, 1, 1)
|
|
157 |
else:
|
|
158 |
return datetime.date(d.year, d.month + 1, 1)
|
|
159 |
def next_year(d):
|
|
160 |
return datetime.date(d.year + 1, 1, 1)
|
|
161 |
|
|
162 |
def datetime_iter(start, end, next_f):
|
|
163 |
d = start
|
|
164 |
while end == None or d < end:
|
|
165 |
yield d
|
|
166 |
d = next_f(d)
|
|
167 |
|
|
168 |
def datetime_iter_range(start, end, next_f):
|
|
169 |
for d in datetime_iter(start, end, next_f):
|
|
170 |
yield d, next_f(d) - ONEDAY
|
|
171 |
|
|
172 |
def day_iter(start, end=None):
|
|
173 |
return (d for d in datetime_iter(start, end, next_day))
|
|
174 |
def month_iter(start, end=None):
|
|
175 |
return (d for d in datetime_iter(first_day_of_month(start), end, next_month))
|
|
176 |
def year_iter(start, end=None):
|
|
177 |
return (d for d in datetime_iter(first_day_of_year(start), end, next_year))
|
|
178 |
def day_range_iter(start, end=None):
|
|
179 |
return ((d,d) for d in datetime_iter(start, end, next_day))
|
|
180 |
def month_range_iter(start, end=None):
|
|
181 |
return (d for d in datetime_iter_range(first_day_of_month(start), end, next_month))
|
|
182 |
def year_range_iter(start, end=None):
|
|
183 |
return (d for d in datetime_iter_range(first_day_of_year(start), end, next_year))
|
|
184 |
|
|
185 |
|
|
186 |
def main(filename, title, currency, daterange):
|
|
187 |
try:
|
|
188 |
session = gnucash.Session(filename, is_new=False)
|
|
189 |
root = session.book.get_root_account()
|
|
190 |
curr = lookup_currency(session.book, currency)
|
|
191 |
for s, e in daterange:
|
|
192 |
make_summary(root, curr, s, e, title)
|
|
193 |
session.save()
|
|
194 |
session.end()
|
|
195 |
except:
|
|
196 |
logging.exception("Exception occured")
|
|
197 |
finally:
|
|
198 |
if 'session' in locals():
|
|
199 |
session.destroy()
|
|
200 |
|
|
201 |
def usage(msg):
|
|
202 |
import sys
|
|
203 |
print msg
|
|
204 |
print "Usage:", sys.argv[0], \
|
|
205 |
"filename.gnc", "transaction_title", "currency", \
|
|
206 |
"yearly|monthly|daily", \
|
|
207 |
"start_year start_month start_day", \
|
|
208 |
"end_year end_month end_day"
|
|
209 |
exit(-1)
|
|
210 |
|
|
211 |
if __name__ == '__main__':
|
|
212 |
# logging.basicConfig(level=logging.DEBUG)
|
|
213 |
import sys
|
|
214 |
if len(sys.argv) != 11:
|
|
215 |
usage("Invalid number of arguments")
|
|
216 |
exit(-1)
|
|
217 |
cmd, filename, title, currency, period, syear, smonth, sday, eyear, emonth, eday = sys.argv
|
|
218 |
start = datetime.date(int(syear), int(smonth), int(sday))
|
|
219 |
end = datetime.date(int(eyear), int(emonth), int(eday))
|
|
220 |
if period == "daily":
|
|
221 |
daterange = day_range_iter(start, end)
|
|
222 |
elif period == "monthly":
|
|
223 |
daterange = month_range_iter(start, end)
|
|
224 |
elif period == "yearly":
|
|
225 |
daterange = year_range_iter(start, end)
|
|
226 |
else:
|
|
227 |
usage("Invalid period '%s'" % (period,))
|
|
228 |
main(filename, title, currency, daterange)
|