|
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) |