Merge lp:~kaboom/wxbanker/advancedCategories into lp:wxbanker
- advancedCategories
- Merge into trunk
Status: | Needs review |
---|---|
Proposed branch: | lp:~kaboom/wxbanker/advancedCategories |
Merge into: | lp:wxbanker |
Diff against target: |
1499 lines (+1099/-18) (has conflicts) 14 files modified
wxbanker/bankexceptions.py (+8/-0) wxbanker/bankobjects/account.py (+2/-1) wxbanker/bankobjects/bankmodel.py (+77/-2) wxbanker/bankobjects/category.py (+73/-0) wxbanker/bankobjects/categorylist.py (+73/-0) wxbanker/bankobjects/recurringtransaction.py (+3/-3) wxbanker/bankobjects/tag.py (+40/-0) wxbanker/bankobjects/transaction.py (+37/-3) wxbanker/categoryTreectrl.py (+408/-0) wxbanker/managetab.py (+14/-4) wxbanker/persistentstore.py (+181/-2) wxbanker/tests/categorytests.py (+62/-0) wxbanker/transactionctrl.py (+1/-1) wxbanker/transactionolv.py (+120/-2) Text conflict in wxbanker/bankobjects/bankmodel.py |
To merge this branch: | bzr merge lp:~kaboom/wxbanker/advancedCategories |
Related bugs: | |
Related blueprints: |
Transaction Tagging
(Medium)
advanced Category Management
(Medium)
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Michael Rooney | Needs Information | ||
Review via email: mp+7343@code.launchpad.net |
Commit message
Description of the change
Michael Rooney (mrooney) wrote : | # |
Hello! I just have a few initial questions.
* Does this depend on lp:~kolmis/wxbanker/transaction-tagging being merged first?
* It says "TODO!!! By no means complete!
- 294. By Wolfgang Steitz <wolfer@franzi>
-
remove todofile. bugs did not reappear
wolfer (wolfer7) wrote : | # |
> Hello! I just have a few initial questions.
>
> * Does this depend on lp:~kolmis/wxbanker/transaction-tagging being merged
> first?
> * It says "TODO!!! By no means
> complete!
> the case?
* we started our branch from the lp:~kolmis/wxbanker/transaction-tagging branch, which was not the best idea i guess. so basically our categories to not depend on transaction-
* i just removed the todo-file. there were two bugs left, but they did not reappear and i was not able to reproduce them respectively.
- 295. By Wolfgang Steitz <wolfer@franzi>
-
merge trunk
Michael Rooney (mrooney) wrote : | # |
> > Hello! I just have a few initial questions.
> >
> > * Does this depend on lp:~kolmis/wxbanker/transaction-tagging being merged
> > first?
> > * It says "TODO!!! By no means
> > complete!
> still
> > the case?
>
>
> * we started our branch from the lp:~kolmis/wxbanker/transaction-tagging
> branch, which was not the best idea i guess. so basically our categories to
> not depend on transaction-
> too. i think we should either remove all the tagging stuff from our branch, or
> wait for the transaction-tagging branch beeing merged.
>
> * i just removed the todo-file. there were two bugs left, but they did not
> reappear and i was not able to reproduce them respectively.
I think you did the right thing, basing it on Karel's tagging branch is probably the right thing to do. I will talk to him and get that in good shape and merge that, then can better review this. Thanks for the clean up :)
Unmerged revisions
- 299. By wolfer
-
fixed loading of categories
- 298. By wolfer
-
bugfix: saving category of a transaction
- 297. By wolfer
-
merged trunk
- 296. By wolfer
- 295. By Wolfgang Steitz <wolfer@franzi>
-
merge trunk
- 294. By Wolfgang Steitz <wolfer@franzi>
-
remove todofile. bugs did not reappear
- 293. By Wolfgang Steitz <wolfer@franzi>
-
remove prints in category treectrl
- 292. By Wolfgang Steitz <wolfer@franzi>
-
drag-n-drop bugfix
- 291. By Wolfgang Steitz <wolfer@franzi>
-
bugfix
- 290. By Wolfgang Steitz <wolfer@franzi>
-
category drag-n-drop now works for categories with child categories
Preview Diff
1 | === modified file 'wxbanker/bankexceptions.py' (properties changed: +x to -x) |
2 | --- wxbanker/bankexceptions.py 2010-02-21 21:53:07 +0000 |
3 | +++ wxbanker/bankexceptions.py 2010-03-09 19:09:26 +0000 |
4 | @@ -29,6 +29,14 @@ |
5 | |
6 | def __str__(self): |
7 | return "Account '%s' already exists."%self.account |
8 | + |
9 | +class CategoryAlreadyExistsException(Exception): |
10 | + def __init__(self, category): |
11 | + self.category = category |
12 | + |
13 | + def __str__(self): |
14 | + return "Category '%s' already exists."%self.category |
15 | + |
16 | |
17 | class BlankAccountNameException(Exception): |
18 | def __str__(self): |
19 | |
20 | === modified file 'wxbanker/bankobjects/account.py' |
21 | --- wxbanker/bankobjects/account.py 2010-02-22 00:32:00 +0000 |
22 | +++ wxbanker/bankobjects/account.py 2010-03-09 19:09:26 +0000 |
23 | @@ -265,6 +265,7 @@ |
24 | else: |
25 | debug.debug("Ignoring transaction because I am %s: %s" % (self.Name, transaction)) |
26 | |
27 | + |
28 | def float2str(self, *args, **kwargs): |
29 | return self.Currency.float2str(*args, **kwargs) |
30 | |
31 | @@ -286,4 +287,4 @@ |
32 | Name = property(GetName, SetName) |
33 | Transactions = property(GetTransactions) |
34 | RecurringTransactions = property(GetRecurringTransactions) |
35 | - Currency = property(GetCurrency, SetCurrency) |
36 | \ No newline at end of file |
37 | + Currency = property(GetCurrency, SetCurrency) |
38 | |
39 | === modified file 'wxbanker/bankobjects/bankmodel.py' |
40 | --- wxbanker/bankobjects/bankmodel.py 2010-02-22 00:32:00 +0000 |
41 | +++ wxbanker/bankobjects/bankmodel.py 2010-03-09 19:09:26 +0000 |
42 | @@ -23,7 +23,11 @@ |
43 | |
44 | from wxbanker import currencies |
45 | from wxbanker.bankobjects.ormobject import ORMKeyValueObject |
46 | +<<<<<<< TREE |
47 | from wxbanker.mint import MintDotCom |
48 | +======= |
49 | +from wxbanker.bankobjects.categorylist import CategoryList |
50 | +>>>>>>> MERGE-SOURCE |
51 | |
52 | class BankModel(ORMKeyValueObject): |
53 | ORM_TABLE = "meta" |
54 | @@ -33,6 +37,8 @@ |
55 | ORMKeyValueObject.__init__(self, store) |
56 | self.Store = store |
57 | self.Accounts = accountList |
58 | + self.Tags = store.getTags() |
59 | + self.Categories = CategoryList(store, store.getCategories()) |
60 | |
61 | self.Mint = None |
62 | if self.MintEnabled: |
63 | @@ -46,7 +52,62 @@ |
64 | |
65 | def GetBalance(self): |
66 | return self.Accounts.Balance |
67 | + |
68 | + def GetTagById(self, id): |
69 | + for tag in self.Tags: |
70 | + if tag.ID == id: |
71 | + return tag |
72 | + return None |
73 | + |
74 | + def GetTagByName(self, name): |
75 | + ''' returns a tag by name, creates a new one if none by that name exists ''' |
76 | + for tag in self.Tags: |
77 | + if tag.Name == name: |
78 | + return tag |
79 | + newTag = self.Store.CreateTag(name) |
80 | + self.Tags.append(newTag) |
81 | + return newTag |
82 | + |
83 | + def GetCategoryById(self, id): |
84 | + return self.Categories.ByID(id) |
85 | + |
86 | + def GetTransactionById(self, id): |
87 | + transactions = self.GetTransactions() |
88 | + for t in transactions: |
89 | + if t.ID == id: |
90 | + return t |
91 | + return None |
92 | + |
93 | + def GetCategoryByName(self, name, parent = None): |
94 | + ''' returns a category by name, creates a new one if none by that name exists ''' |
95 | + for cat in self.Categories: |
96 | + if cat.Name == name: |
97 | + return cat |
98 | + newCat = self.Categories.Create(name, parent) |
99 | + return newCat |
100 | |
101 | + def GetChildCategories(self, category): |
102 | + childs = [] |
103 | + for cat in self.Categories: |
104 | + if cat.ParentID == category.ID: |
105 | + childs.append(cat) |
106 | + childs.extend(self.GetChildCategories(cat)) |
107 | + return childs |
108 | + |
109 | + def GetTopCategories(self, sorted = False): |
110 | + result = [] |
111 | + for cat in self.Categories: |
112 | + if not cat.ParentID: |
113 | + result.append(cat) |
114 | + if sorted: |
115 | + result.sort(foo) |
116 | + return result |
117 | + |
118 | + def GetTransactionsByCategory(self, category): |
119 | + transactions = self.GetTransactions() |
120 | + return [t for t in transactions if category == t.Category] |
121 | + |
122 | + |
123 | def GetRecurringTransactions(self): |
124 | return self.Accounts.GetRecurringTransactions() |
125 | |
126 | @@ -131,10 +192,22 @@ |
127 | |
128 | def RemoveAccount(self, accountName): |
129 | return self.Accounts.Remove(accountName) |
130 | + |
131 | + def CreateCategory(self, name, parentID): |
132 | + return self.Categories.Create(name, parentID) |
133 | + |
134 | + def RemoveCategory(self, cat): |
135 | + for t in self.GetTransactionsByCategory(cat): |
136 | + t.Category = None |
137 | + return self.RemoveCategoryName(cat.Name) |
138 | + |
139 | + def RemoveCategoryName(self, catName): |
140 | + |
141 | + return self.Categories.Remove(catName) |
142 | |
143 | def Search(self, searchString, account=None, matchIndex=1): |
144 | """ |
145 | - matchIndex: 0: Amount, 1: Description, 2: Date |
146 | + matchIndex: 0: Amount, 1: Description, 2: Date, 3: Tags, 4: Category |
147 | I originally used strings here but passing around and then validating on translated |
148 | strings seems like a bad and fragile idea. |
149 | """ |
150 | @@ -194,4 +267,6 @@ |
151 | for t in a.Transactions: |
152 | print t |
153 | |
154 | - Balance = property(GetBalance) |
155 | \ No newline at end of file |
156 | + Balance = property(GetBalance) |
157 | + |
158 | + |
159 | |
160 | === added file 'wxbanker/bankobjects/category.py' |
161 | --- wxbanker/bankobjects/category.py 1970-01-01 00:00:00 +0000 |
162 | +++ wxbanker/bankobjects/category.py 2010-03-09 19:09:26 +0000 |
163 | @@ -0,0 +1,73 @@ |
164 | +#!/usr/bin/env python |
165 | +# |
166 | +# https://launchpad.net/wxbanker |
167 | +# account.py: Copyright 2007-2009 Mike Rooney <mrooney@ubuntu.com> |
168 | +# |
169 | +# This file is part of wxBanker. |
170 | +# |
171 | +# wxBanker is free software: you can redistribute it and/or modify |
172 | +# it under the terms of the GNU General Public License as published by |
173 | +# the Free Software Foundation, either version 3 of the License, or |
174 | +# (at your option) any later version. |
175 | +# |
176 | +# wxBanker is distributed in the hope that it will be useful, |
177 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
178 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
179 | +# GNU General Public License for more details. |
180 | +# |
181 | +# You should have received a copy of the GNU General Public License |
182 | +# along with wxBanker. If not, see <http://www.gnu.org/licenses/>. |
183 | + |
184 | +from wx.lib.pubsub import Publisher |
185 | + |
186 | +from wxbanker.bankobjects.ormobject import ORMObject |
187 | + |
188 | + |
189 | +class Category(ORMObject): |
190 | + ORM_TABLE = "categories" |
191 | + ORM_ATTRIBUTES = ["Name", "ParentID"] |
192 | + |
193 | + |
194 | + def __init__(self, cID, name, parent = None): |
195 | + self.IsFrozen = True |
196 | + self.ID = cID |
197 | + self._Name = name |
198 | + self._ParentID = parent |
199 | + Publisher.sendMessage("category.created.%s" % name, self) |
200 | + self.IsFrozen = False |
201 | + |
202 | + def __str__(self): |
203 | + return self._Name |
204 | + |
205 | + def __repr__(self): |
206 | + return self.__class__.__name__ + '<' + 'ID=' + str(self.ID) + " Name=" + self._Name + " Parent=" + str(self._ParentID)+'>' |
207 | + |
208 | + def __cmp__(self, other): |
209 | + if other is not None: |
210 | + if other == "": |
211 | + return -1 |
212 | + else: |
213 | + return cmp(self.ID, other.ID) |
214 | + else: |
215 | + return -1 |
216 | + |
217 | + def GetParentID(self): |
218 | + return self._ParentID |
219 | + |
220 | + def SetParentID(self, parent): |
221 | + self._ParentID = parent |
222 | + #print self._Name, "now has parentID ", self._ParentID |
223 | + Publisher.sendMessage("category.parentID", (self, parent)) |
224 | + |
225 | + ParentID = property(GetParentID, SetParentID) |
226 | + |
227 | + def GetName(self): |
228 | + return self._Name |
229 | + |
230 | + def SetName(self, name): |
231 | + #print "in Category.SetName", name |
232 | + oldName = self._Name |
233 | + self._Name = unicode(name) |
234 | + Publisher.sendMessage("category.renamed", (oldName, self)) |
235 | + |
236 | + Name = property(GetName, SetName) |
237 | |
238 | === added file 'wxbanker/bankobjects/categorylist.py' |
239 | --- wxbanker/bankobjects/categorylist.py 1970-01-01 00:00:00 +0000 |
240 | +++ wxbanker/bankobjects/categorylist.py 2010-03-09 19:09:26 +0000 |
241 | @@ -0,0 +1,73 @@ |
242 | +#!/usr/bin/env python |
243 | +# |
244 | +# https://launchpad.net/wxbanker |
245 | +# accountlist.py: Copyright 2007-2009 Mike Rooney <mrooney@ubuntu.com> |
246 | +# |
247 | +# This file is part of wxBanker. |
248 | +# |
249 | +# wxBanker is free software: you can redistribute it and/or modify |
250 | +# it under the terms of the GNU General Public License as published by |
251 | +# the Free Software Foundation, either version 3 of the License, or |
252 | +# (at your option) any later version. |
253 | +# |
254 | +# wxBanker is distributed in the hope that it will be useful, |
255 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
256 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
257 | +# GNU General Public License for more details. |
258 | +# |
259 | +# You should have received a copy of the GNU General Public License |
260 | +# along with wxBanker. If not, see <http://www.gnu.org/licenses/>. |
261 | + |
262 | +from wx.lib.pubsub import Publisher |
263 | + |
264 | + |
265 | +class CategoryList(list): |
266 | + |
267 | + def __init__(self, store, categories): |
268 | + list.__init__(self,categories) |
269 | + for cat in self: |
270 | + cat.ParentList = self |
271 | + self.Store = store |
272 | + #print self |
273 | + |
274 | + def CategoryIndex(self, catName): |
275 | + #print self |
276 | + for i, cat in enumerate(self): |
277 | + if cat.Name == catName: |
278 | + return i |
279 | + return -1 |
280 | + |
281 | + def ByID(self, id): |
282 | + for cat in self: |
283 | + if cat.ID == id: |
284 | + return cat |
285 | + return None |
286 | + |
287 | + def Create(self, catName, parentID): |
288 | + # First, ensure a category by that name doesn't already exist. |
289 | + if self.CategoryIndex(catName) >= 0: |
290 | + raise bankexceptions.CategoryAlreadyExistsException(catName) |
291 | + cat = self.Store.CreateCategory(catName, parentID) |
292 | + # Make sure this category knows its parent. |
293 | + cat.ParentList = self |
294 | + self.append(cat) |
295 | + return cat |
296 | + |
297 | + def Remove(self, catName): |
298 | + index = self.CategoryIndex(catName) |
299 | + if index == -1: |
300 | + raise bankexceptions.InvalidCategoryException(catName) |
301 | + cat = self.pop(index) |
302 | + self.Store.RemoveCategory(cat) |
303 | + Publisher.sendMessage("category.removed.%s"%catName, cat) |
304 | + |
305 | + def __eq__(self, other): |
306 | + if len(self) != len(other): |
307 | + return False |
308 | + for left, right in zip(self, other): |
309 | + if not left == right: |
310 | + return False |
311 | + return True |
312 | + |
313 | + def __cmp__(self, other): |
314 | + return cmp(self.Name.lower(), other.Name.lower()) |
315 | |
316 | === modified file 'wxbanker/bankobjects/recurringtransaction.py' |
317 | --- wxbanker/bankobjects/recurringtransaction.py 2009-12-06 03:14:06 +0000 |
318 | +++ wxbanker/bankobjects/recurringtransaction.py 2010-03-09 19:09:26 +0000 |
319 | @@ -37,8 +37,8 @@ |
320 | MONTLY = 2 |
321 | YEARLY = 3 |
322 | |
323 | - def __init__(self, tID, parent, amount, description, date, repeatType, repeatEvery=1, repeatOn=None, endDate=None, source=None, lastTransacted=None): |
324 | - Transaction.__init__(self, tID, parent, amount, description, date) |
325 | + def __init__(self, store, tID, parent, amount, description, date, repeatType, repeatEvery=1, repeatOn=None, endDate=None, source=None, lastTransacted=None): |
326 | + Transaction.__init__(self, store, tID, parent, amount, description, date) |
327 | ORMObject.__init__(self) |
328 | |
329 | # If the transaction recurs weekly and repeatsOn isn't specified, use the starting date. |
330 | @@ -217,4 +217,4 @@ |
331 | |
332 | LastTransacted = property(GetLastTransacted, SetLastTransacted) |
333 | EndDate = property(GetEndDate, SetEndDate) |
334 | - |
335 | \ No newline at end of file |
336 | + |
337 | |
338 | === added file 'wxbanker/bankobjects/tag.py' |
339 | --- wxbanker/bankobjects/tag.py 1970-01-01 00:00:00 +0000 |
340 | +++ wxbanker/bankobjects/tag.py 2010-03-09 19:09:26 +0000 |
341 | @@ -0,0 +1,40 @@ |
342 | +#!/usr/bin/env python |
343 | +# |
344 | +# https://launchpad.net/wxbanker |
345 | +# account.py: Copyright 2007-2009 Mike Rooney <mrooney@ubuntu.com> |
346 | +# |
347 | +# This file is part of wxBanker. |
348 | +# |
349 | +# wxBanker is free software: you can redistribute it and/or modify |
350 | +# it under the terms of the GNU General Public License as published by |
351 | +# the Free Software Foundation, either version 3 of the License, or |
352 | +# (at your option) any later version. |
353 | +# |
354 | +# wxBanker is distributed in the hope that it will be useful, |
355 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
356 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
357 | +# GNU General Public License for more details. |
358 | +# |
359 | +# You should have received a copy of the GNU General Public License |
360 | +# along with wxBanker. If not, see <http://www.gnu.org/licenses/>. |
361 | + |
362 | +from wx.lib.pubsub import Publisher |
363 | + |
364 | +from wxbanker.bankobjects.ormobject import ORMObject |
365 | + |
366 | + |
367 | +class Tag(ORMObject): |
368 | + ORM_TABLE = "tags" |
369 | + ORM_ATTRIBUTES = ["Name",] |
370 | + |
371 | + def __init__(self, aID, name): |
372 | + self.IsFrozen = True |
373 | + self.ID = aID |
374 | + self.Name = name |
375 | + Publisher.sendMessage("tag.created.%s" % name, self) |
376 | + |
377 | + def __str__(self): |
378 | + return self.Name |
379 | + |
380 | + def __cmp__(self, other): |
381 | + return cmp(self.ID, other.ID) |
382 | |
383 | === modified file 'wxbanker/bankobjects/transaction.py' |
384 | --- wxbanker/bankobjects/transaction.py 2010-01-19 00:17:01 +0000 |
385 | +++ wxbanker/bankobjects/transaction.py 2010-03-09 19:09:26 +0000 |
386 | @@ -34,20 +34,52 @@ |
387 | ORM_TABLE = "transactions" |
388 | ORM_ATTRIBUTES = ["_Amount", "_Description", "_Date", "LinkedTransaction", "RecurringParent"] |
389 | |
390 | - def __init__(self, tID, parent, amount, description, date): |
391 | + def __init__(self, store, tID, parent, amount, description, date, tags = None, category = None): |
392 | ORMObject.__init__(self) |
393 | self.IsFrozen = True |
394 | - |
395 | + self.Store = store |
396 | self.ID = tID |
397 | self.LinkedTransaction = None |
398 | self.Parent = parent |
399 | self.Date = date |
400 | self.Description = description |
401 | self.Amount = amount |
402 | + self._Tags = tags |
403 | + self._Category = category |
404 | self.RecurringParent = None |
405 | |
406 | self.IsFrozen = False |
407 | |
408 | + def GetTags(self): |
409 | + if self._Tags is None: |
410 | + self._Tags = self.Store.getTagsFrom(self) |
411 | + return self._Tags |
412 | + |
413 | + def SetTags(self, tags): |
414 | + self._Tags = tags |
415 | + for tag in tags: |
416 | + self.AddTag(tag) |
417 | + |
418 | + def AddTag(self, tag): |
419 | + if not tag in self.Tags: |
420 | + self.Tags.append(tag) |
421 | + Publisher.sendMessage("transaction.tags.added", (self, tag)) |
422 | + |
423 | + def RemoveTag(self, tag): |
424 | + if tag in self.Tags: |
425 | + self.Tags.remove(tag) |
426 | + Publisher.sendMessage("transaction.tags.removed", (self, tag)) |
427 | + |
428 | + def GetCategory(self): |
429 | + if self._Category is None: |
430 | + self._Category = self.Store.getCategoryFrom(self) |
431 | + return self._Category |
432 | + |
433 | + def SetCategory(self, category): |
434 | + self._Category = category |
435 | + if not self.IsFrozen: |
436 | + Publisher.sendMessage("transaction.category.updated", self) |
437 | + |
438 | def GetDate(self): |
439 | return self._Date |
440 | |
441 | @@ -167,4 +199,6 @@ |
442 | Date = property(GetDate, SetDate) |
443 | Description = property(GetDescription, SetDescription) |
444 | Amount = property(GetAmount, SetAmount) |
445 | - LinkedTransaction = property(GetLinkedTransaction, SetLinkedTransaction) |
446 | \ No newline at end of file |
447 | + Category = property(GetCategory, SetCategory) |
448 | + LinkedTransaction = property(GetLinkedTransaction, SetLinkedTransaction) |
449 | + Tags = property(GetTags, SetTags) |
450 | |
451 | === added file 'wxbanker/categoryTreectrl.py' |
452 | --- wxbanker/categoryTreectrl.py 1970-01-01 00:00:00 +0000 |
453 | +++ wxbanker/categoryTreectrl.py 2010-03-09 19:09:26 +0000 |
454 | @@ -0,0 +1,408 @@ |
455 | +# -*- coding: utf-8 -*- |
456 | +# https://launchpad.net/wxbanker |
457 | +# calculator.py: Copyright 2007, 2008 Mike Rooney <mrooney@ubuntu.com> |
458 | +# |
459 | +# This file is part of wxBanker. |
460 | +# |
461 | +# wxBanker is free software: you can redistribute it and/or modify |
462 | +# it under the terms of the GNU General Public License as published by |
463 | +# the Free Software Foundation, either version 3 of the License, or |
464 | +# (at your option) any later version. |
465 | +# |
466 | +# wxBanker is distributed in the hope that it will be useful, |
467 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
468 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
469 | +# GNU General Public License for more details. |
470 | +# |
471 | +# You should have received a copy of the GNU General Public License |
472 | +# along with wxBanker. If not, see <http://www.gnu.org/licenses/>. |
473 | + |
474 | +import wx |
475 | +import bankcontrols, bankexceptions |
476 | +from wx.lib.pubsub import Publisher |
477 | +import pickle |
478 | + |
479 | +class CategoryBox(wx.Panel): |
480 | + """ |
481 | + This control manages the list of categories. |
482 | + """ |
483 | + |
484 | + def __init__(self, parent, bankController, autoPopulate=True): |
485 | + wx.Panel.__init__(self, parent) |
486 | + self.model = bankController.Model |
487 | + |
488 | + # Initialize some attributes to their default values |
489 | + self.boxLabel = _("Categories") |
490 | + self.editCtrl = None |
491 | + self.categories = {} |
492 | + |
493 | + # Create the staticboxsizer which is the home for everything. |
494 | + # This *MUST* be created first to ensure proper z-ordering (as per docs). |
495 | + self.staticBox = wx.StaticBox(self, label=self.boxLabel) |
496 | + |
497 | + # Create a single panel to be the "child" of the static box sizer, |
498 | + # to work around a wxPython regression that prevents tooltips. lp: xxxxxx |
499 | + self.childPanel = wx.Panel(self) |
500 | + self.childSizer = childSizer = wx.BoxSizer(wx.VERTICAL) |
501 | + |
502 | + |
503 | + ## Create and set up the buttons. |
504 | + # The ADD category button. |
505 | + BMP = self.addBMP = wx.ArtProvider.GetBitmap('wxART_add') |
506 | + self.addButton = addButton = wx.BitmapButton(self.childPanel, bitmap=BMP) |
507 | + addButton.SetToolTipString(_("Add a new category")) |
508 | + # The REMOVE category button. |
509 | + BMP = wx.ArtProvider.GetBitmap('wxART_delete') |
510 | + self.removeButton = removeButton = wx.BitmapButton(self.childPanel, bitmap=BMP) |
511 | + removeButton.SetToolTipString(_("Remove the selected category")) |
512 | + removeButton.Enabled = False |
513 | + # The EDIT category button. |
514 | + BMP = wx.ArtProvider.GetBitmap('wxART_textfield_rename') |
515 | + self.editButton = editButton = wx.BitmapButton(self.childPanel, bitmap=BMP) |
516 | + editButton.SetToolTipString(_("Rename the selected category")) |
517 | + editButton.Enabled = False |
518 | + # the DESELECT category button |
519 | + BMP = wx.ArtProvider.GetBitmap('wxART_UNDO', size=(16,16)) |
520 | + self.deselectButton = wx.BitmapButton(self.childPanel, bitmap = BMP) |
521 | + self.deselectButton.SetToolTipString(_("Deselect any selected Category")) |
522 | + |
523 | + |
524 | + |
525 | + # Layout the buttons. |
526 | + buttonSizer = wx.BoxSizer() |
527 | + buttonSizer.Add(addButton) |
528 | + buttonSizer.Add(removeButton) |
529 | + buttonSizer.Add(editButton) |
530 | + buttonSizer.Add(self.deselectButton) |
531 | + |
532 | + ## Create and set up the treectrl |
533 | + self.treeCtrl = TreeCtrl(self, self.childPanel, style = (wx.TR_HIDE_ROOT + wx.TR_DEFAULT_STYLE+ wx.SUNKEN_BORDER)) |
534 | + self.root = self.treeCtrl.AddRoot("") |
535 | + self.treeCtrl.SetIndent(5) |
536 | + |
537 | + self.staticBoxSizer = wx.StaticBoxSizer(self.staticBox, wx.VERTICAL) |
538 | + childSizer.Add(buttonSizer, 0, wx.BOTTOM, 4) |
539 | + childSizer.Add(self.treeCtrl, 1, wx.EXPAND) |
540 | + self.childPanel.Sizer = childSizer |
541 | + self.staticBoxSizer.Add(self.childPanel, 1, wx.EXPAND) |
542 | + |
543 | + #self.Sizer = aSizer = wx.BoxSizer() |
544 | + #aSizer.Add(self.childPanel, 1, wx.EXPAND) |
545 | + |
546 | + # Set up the button bindings. |
547 | + addButton.Bind(wx.EVT_BUTTON, self.onAddButton) |
548 | + removeButton.Bind(wx.EVT_BUTTON, self.onRemoveButton) |
549 | + editButton.Bind(wx.EVT_BUTTON, self.onRenameButton) |
550 | + self.deselectButton.Bind(wx.EVT_BUTTON, self.onDeselect) |
551 | + # and others |
552 | + self.treeCtrl.Bind(wx.EVT_TREE_SEL_CHANGED, self.onCategorySelected) |
553 | + self.treeCtrl.Bind(wx.EVT_TREE_END_LABEL_EDIT, self.onRenameCategory) |
554 | + self.treeCtrl.Bind(wx.EVT_TREE_ITEM_ACTIVATED, self.onRenameButton) |
555 | + self.treeCtrl.Bind(wx.EVT_RIGHT_DOWN, self.onRightDown) |
556 | + |
557 | + # Subscribe to messages we are concerned about. |
558 | + Publisher().subscribe(self.onPushChars, "CATEGORYCTRL.PUSH_CHARS") |
559 | + Publisher().subscribe(self.onCategoryCreated, "category.newToDisplay") |
560 | + |
561 | + # Populate ourselves initially unless explicitly told not to. |
562 | + if autoPopulate: |
563 | + categories = self.model.Categories |
564 | + for category in categories: |
565 | + self._InsertItem(category) |
566 | + |
567 | + self.Sizer = self.staticBoxSizer |
568 | + |
569 | + |
570 | + #Signal Handler |
571 | + #--------------- |
572 | + |
573 | + def onDragBegin(self, event): |
574 | + item, where = self.treeCtrl.HitTest(event.GetPoint()) |
575 | + cat = self.treeCtrl.get_category_from_item(item) |
576 | + #print "Begin Drag at ", cat |
577 | + |
578 | + def onDragEnd(self, event): |
579 | + item, where = self.treeCtrl.HitTest(event.GetPoint()) |
580 | + cat = self.treeCtrl.get_category_from_item(item) |
581 | + #print "End Drag at ", cat |
582 | + |
583 | + def onCategorySelected(self, event): |
584 | + cat = self._get_category_from_event(event) |
585 | + if cat: |
586 | + self._disable_Buttons(True, True, True, True) |
587 | + # Tell the parent we changed. |
588 | + Publisher().sendMessage("view.category changed", cat) |
589 | + |
590 | + def onPushChars(self, message): |
591 | + print message |
592 | + |
593 | + def onChar(self, event=None, char=None): |
594 | + print event,char |
595 | + |
596 | + def onAddButton(self, event): |
597 | + self.showEditCtrl() |
598 | + self._disable_Buttons() |
599 | + |
600 | + def onAddCategoryFromContextMenu(self): |
601 | + self.showEditCtrl() |
602 | + self._disable_Buttons() |
603 | + |
604 | + def onRemoveButton(self, event): |
605 | + self.onRemoveCategory(self.treeCtrl.GetSelection()) |
606 | + |
607 | + def onRenameButton(self, event): |
608 | + item = self.treeCtrl.GetSelection() |
609 | + category = self.treeCtrl.get_selected_category() |
610 | + self.treeCtrl.EditLabel(item) |
611 | + |
612 | + def onRenameCategoryFromContextMenu(self, item): |
613 | + self.treeCtrl.EditLabel(item) |
614 | + |
615 | + def onRightDown(self, event): |
616 | + item, where = self.treeCtrl.HitTest(event.Position) |
617 | + self.treeCtrl.SelectItem(item, True) |
618 | + self.show_context_menu(item) |
619 | + |
620 | + def onEditCtrlKey(self, event): |
621 | + if event.GetKeyCode() == wx.WXK_ESCAPE: |
622 | + self.onHideEditCtrl() |
623 | + else: |
624 | + event.Skip() |
625 | + |
626 | + def onDeselect(self, even): |
627 | + self.treeCtrl.UnselectAll() |
628 | + self._disable_Buttons(add = True) |
629 | + |
630 | + def onHideEditCtrl(self, event=None, restore=True): |
631 | + # Hide and remove the control and re-layout. |
632 | + self.childSizer.Hide(self.editCtrl)#, smooth=True) |
633 | + self.childSizer.Detach(self.editCtrl) |
634 | + self.Layout() |
635 | + # Re-enable the add button. |
636 | + self._disable_Buttons( add = True, deselect = True) |
637 | + |
638 | + def onAddCategory(self, event): |
639 | + # Grab the category name and add it. |
640 | + categoryName = self.editCtrl.Value |
641 | + if self.check_category_name(categoryName): |
642 | + parent = self.treeCtrl.get_selected_category() |
643 | + if parent: |
644 | + category = self.model.GetCategoryByName(categoryName, parent.ID) |
645 | + else: |
646 | + category = self.model.GetCategoryByName(categoryName) |
647 | + Publisher.sendMessage("category.newToDisplay.%s" % categoryName, category) |
648 | + self.onHideEditCtrl() |
649 | + |
650 | + def onCategoryCreated(self, message): |
651 | + #print message |
652 | + newcat = self.model.GetCategoryByName(message.topic[2]) |
653 | + self._InsertItem(newcat) |
654 | + |
655 | + def onRemoveCategory(self, item): |
656 | + category = self.treeCtrl.get_category_from_item(item) |
657 | + if category: |
658 | + warningMsg = _("This will permanently remove the category '%s'. Continue?") |
659 | + dlg = wx.MessageDialog(self, warningMsg%category.Name, _("Warning"), style=wx.YES_NO|wx.ICON_EXCLAMATION) |
660 | + if dlg.ShowModal() == wx.ID_YES: |
661 | + # Remove the category from db |
662 | + self.model.RemoveCategoryName(category.Name) |
663 | + self._RemoveItem(item) |
664 | + #print "verarschen?" |
665 | + self._disable_Buttons(add = True) |
666 | + self.treeCtrl.UnselectAll() |
667 | + |
668 | + def onRenameCategory(self, event): |
669 | + #print dir(event) |
670 | + cat = self._get_category_from_event(event) |
671 | + newName = event.GetLabel() |
672 | + if self.check_category_name(newName): |
673 | + cat.Name = newName |
674 | + else: |
675 | + #setting the label does not work (why?) ... |
676 | + self.treeCtrl.SetItemText(event.GetItem(), cat.Name) |
677 | + |
678 | + |
679 | + def check_category_name(self, name): |
680 | + """checks whether a category name is valid or not""" |
681 | + if name == '': |
682 | + wx.TipWindow(self, _("Empty category names are not allowed!")) |
683 | + return False |
684 | + return True |
685 | + |
686 | + |
687 | + # Helper Functions |
688 | + #-------------------- |
689 | + |
690 | + def _get_category_from_event(self, event): |
691 | + return self.treeCtrl.get_category_from_item(event.GetItem()) |
692 | + |
693 | + def _disable_Buttons(self, add = False, remove = False, edit = False, deselect = False): |
694 | + #print "anscheinend.." |
695 | + self.addButton.Enabled = add |
696 | + self.removeButton.Enabled = remove |
697 | + self.editButton.Enabled = edit |
698 | + self.deselectButton.Enabled = deselect |
699 | + |
700 | + def _InsertItemRecursive(self, category): |
701 | + self._InsertItem(category) |
702 | + for child in self.model.GetChildCategories(category): |
703 | + self._InsertItemRecursive(child) |
704 | + |
705 | + |
706 | + def _InsertItem(self, category): |
707 | + """ |
708 | + Insert a category into the given position. |
709 | + """ |
710 | + #print "inserting category: ", category, category.ParentID |
711 | + if category.ParentID: |
712 | + #print category, " has parent item ", category.ParentID |
713 | + index = self.treeCtrl.AppendItem(self.categories[category.ParentID], category.Name) |
714 | + self.treeCtrl.SortChildren(self.categories[category.ParentID]) |
715 | + self.treeCtrl.Expand(self.categories[category.ParentID]) |
716 | + else: |
717 | + index = self.treeCtrl.AppendItem(self.root, category.Name) |
718 | + self.treeCtrl.SetItemPyData(index, category) |
719 | + self.categories[category.ID] = index |
720 | + self.treeCtrl.SortChildren(self.root) |
721 | + |
722 | + def _RemoveItem(self, item): |
723 | + self.treeCtrl.Delete(item) |
724 | + |
725 | + |
726 | + def showEditCtrl(self, focus=True): |
727 | + if self.editCtrl: |
728 | + self.editCtrl.Value = '' |
729 | + self.editCtrl.Show() |
730 | + else: |
731 | + self.editCtrl = wx.TextCtrl(self.childPanel, style=wx.TE_PROCESS_ENTER) |
732 | + self.editCtrl.Bind(wx.EVT_KILL_FOCUS, self.onHideEditCtrl) |
733 | + self.editCtrl.Bind(wx.EVT_KEY_DOWN, self.onEditCtrlKey) |
734 | + |
735 | + |
736 | + self.editCtrl.Bind(wx.EVT_TEXT_ENTER, self.onAddCategory) |
737 | + self.childSizer.Insert(0, self.editCtrl, 0, wx.EXPAND)#, smooth=True) |
738 | + self.Parent.Layout() |
739 | + |
740 | + if focus: |
741 | + self.editCtrl.SetFocus() |
742 | + |
743 | + def GetCount(self): |
744 | + return self.treeCtrl.GetCount() |
745 | + |
746 | + def show_context_menu(self, item): |
747 | + menu = wx.Menu() |
748 | + |
749 | + addItem = wx.MenuItem(menu, -1, _("Add category")) |
750 | + menu.Bind(wx.EVT_MENU, lambda e: self.onAddCategoryFromContextMenu(), source = addItem ) |
751 | + addItem.SetBitmap(wx.ArtProvider.GetBitmap('wxART_add')) |
752 | + menu.AppendItem(addItem) |
753 | + |
754 | + removeItem = wx.MenuItem(menu, 0, _("Remove category")) |
755 | + menu.Bind(wx.EVT_MENU, lambda e: self.onRemoveCategory(item), source = removeItem) |
756 | + removeItem.SetBitmap(wx.ArtProvider.GetBitmap('wxART_delete')) |
757 | + menu.AppendItem(removeItem) |
758 | + |
759 | + editItem = wx.MenuItem(menu, 1, _("Rename category")) |
760 | + menu.Bind(wx.EVT_MENU, lambda e: self.onRenameCategoryFromContextMenu(item), source = editItem) |
761 | + editItem.SetBitmap(wx.ArtProvider.GetBitmap('wxART_textfield_rename')) |
762 | + menu.AppendItem(editItem) |
763 | + |
764 | + # Show the menu and then destroy it afterwards. |
765 | + self.treeCtrl.PopupMenu(menu) |
766 | + menu.Destroy() |
767 | + |
768 | + def process_transactions(self, transids , category): |
769 | + for tid in transids: |
770 | + transaction = self.model.GetTransactionById(tid) |
771 | + transaction.Category = category |
772 | + |
773 | + |
774 | +class CategoryViewDropTarget(wx.TextDropTarget): |
775 | + def __init__(self, obj): |
776 | + wx.TextDropTarget.__init__(self) |
777 | + self.obj = obj |
778 | + |
779 | + def OnDropText(self, x,y,dragResult): |
780 | + self.obj.process_drop_transaction((x,y), dragResult) |
781 | + |
782 | +class TreeCtrl(wx.TreeCtrl): |
783 | + |
784 | + def __init__(self, parent, *args, **kwargs): |
785 | + wx.TreeCtrl.__init__(self, *args, **kwargs) |
786 | + self.parent = parent |
787 | + self.dropTarget = CategoryViewDropTarget(self) |
788 | + self.SetDropTarget(self.dropTarget) |
789 | + self.Bind(wx.EVT_TREE_BEGIN_DRAG, self.on_drag_begin) |
790 | + |
791 | + def process_drop_transaction(self, point, drag): |
792 | + item = self.HitTest(wx.Point(*point)) |
793 | + category = self.get_category_from_item(item[0]) |
794 | + unpickled = pickle.loads(str(drag)) |
795 | + flag = unpickled[0] |
796 | + data = unpickled[1:] |
797 | + if category: |
798 | + if flag == 'transaction': |
799 | + self.parent.process_transactions(data, category) |
800 | + elif flag == 'category': |
801 | + self.process_category_drop(data, category) |
802 | + else: |
803 | + pass |
804 | + #print "No Category found for point ", point |
805 | + |
806 | + def process_category_drop(self, id_list, target): |
807 | + newID = id_list[0] |
808 | + item = self.parent.categories[newID] |
809 | + cat = self.parent.model.GetCategoryById(id_list[0]) |
810 | + if cat == target: |
811 | + #print "No Dragging on oneself!!" |
812 | + return |
813 | + self.parent._RemoveItem(item) |
814 | + if target: |
815 | + cat.ParentID = target.ID |
816 | + else: |
817 | + cat.ParentID = None |
818 | + self.parent._InsertItemRecursive(cat) |
819 | + |
820 | + def get_category_from_item(self, item): |
821 | + return self.GetItemPyData(item) |
822 | + |
823 | + def get_selected_category(self): |
824 | + if self.hasSelected(): |
825 | + return self.GetItemPyData(self.Selection) |
826 | + else: |
827 | + return None |
828 | + |
829 | + def OnCompareItems(self, item1, item2): |
830 | + if not self.get_category_from_item(item1): |
831 | + return -1 |
832 | + else: |
833 | + if not self.get_category_from_item(item2): |
834 | + return 1 |
835 | + else: |
836 | + return cmp(self.get_category_from_item(item1).Name.lower(),self.get_category_from_item(item2).Name.lower()) |
837 | + |
838 | + def hasSelected(self): |
839 | + selection = self.Selection |
840 | + if selection: |
841 | + if selection.IsOk(): |
842 | + if self.GetItemPyData(selection): |
843 | + return True |
844 | + return False |
845 | + |
846 | + def _dropData(self): |
847 | + return pickle.dumps(['category',self.get_selected_category().ID]) |
848 | + |
849 | + def on_drag_begin(self, event): |
850 | + item, where = self.HitTest(event.GetPoint()) |
851 | + #print item, item.IsOk() |
852 | + if self.IsExpanded(item): |
853 | + self.Collapse(item) |
854 | + #return |
855 | + if not item.IsOk(): |
856 | + #print "Not an okay Item to drag" |
857 | + return |
858 | + text = self._dropData() |
859 | + do = wx.PyTextDataObject(text) |
860 | + dropSource = wx.DropSource(self) |
861 | + dropSource.SetData(do) |
862 | + dropSource.DoDragDrop(True) |
863 | |
864 | === modified file 'wxbanker/main.py' (properties changed: +x to -x) |
865 | === modified file 'wxbanker/managetab.py' (properties changed: +x to -x) |
866 | --- wxbanker/managetab.py 2010-02-03 06:26:15 +0000 |
867 | +++ wxbanker/managetab.py 2010-03-09 19:09:26 +0000 |
868 | @@ -21,6 +21,7 @@ |
869 | from wxbanker import searchctrl, accountlistctrl, transactionctrl |
870 | from wxbanker.transactionolv import TransactionOLV as TransactionCtrl |
871 | from wxbanker.calculator import CollapsableWidget, SimpleCalculator |
872 | +from wxbanker.categoryTreectrl import CategoryBox |
873 | from wx.lib.pubsub import Publisher |
874 | from wxbanker import localization, summarytab |
875 | from wxbanker.plots.plotfactory import PlotFactory |
876 | @@ -34,16 +35,17 @@ |
877 | def __init__(self, parent, bankController): |
878 | wx.Panel.__init__(self, parent) |
879 | |
880 | - ## Left side, the account list and calculator |
881 | + ## Left side, the account list, category list and calculator |
882 | self.leftPanel = leftPanel = wx.Panel(self) |
883 | leftPanel.Sizer = wx.BoxSizer(wx.VERTICAL) |
884 | |
885 | self.accountCtrl = accountCtrl = accountlistctrl.AccountListCtrl(leftPanel, bankController) |
886 | - |
887 | + self.categoryCtrl = categoryCtrl = CategoryBox(leftPanel, bankController) |
888 | calcWidget = CollapsableWidget(leftPanel, SimpleCalculator, (_("Show Calculator"), _("Hide Calculator"))) |
889 | |
890 | leftPanel.Sizer.Add(accountCtrl, 0, wx.EXPAND) |
891 | - leftPanel.Sizer.AddStretchSpacer(1) |
892 | + leftPanel.Sizer.Add(categoryCtrl, 1, wx.EXPAND|wx.ALL) |
893 | + #leftPanel.Sizer.AddStretchSpacer(1) |
894 | leftPanel.Sizer.Add(calcWidget, 0, wx.EXPAND) |
895 | |
896 | # Force the calculator widget (and parent) to take on the desired size. |
897 | @@ -59,6 +61,7 @@ |
898 | |
899 | # Subscribe to messages that interest us. |
900 | Publisher.subscribe(self.onChangeAccount, "view.account changed") |
901 | + Publisher.subscribe(self.onChangeCategory, "view.category changed") |
902 | Publisher.subscribe(self.onCalculatorToggled, "CALCULATOR.TOGGLED") |
903 | |
904 | # Select the last-selected account. |
905 | @@ -81,6 +84,10 @@ |
906 | def onChangeAccount(self, message): |
907 | account = message.data |
908 | self.rightPanel.transactionPanel.setAccount(account) |
909 | + |
910 | + def onChangeCategory(self, message): |
911 | + category = message.data |
912 | + self.rightPanel.transactionPanel.setCategory(category) |
913 | |
914 | def getCurrentAccount(self): |
915 | return self.accountCtrl.GetCurrentAccount() |
916 | @@ -142,7 +149,10 @@ |
917 | |
918 | def setAccount(self, *args, **kwargs): |
919 | self.transactionCtrl.setAccount(*args, **kwargs) |
920 | - |
921 | + |
922 | + def setCategory(self, *args, **kwargs): |
923 | + self.transactionCtrl.setCategory(*args, **kwargs) |
924 | + |
925 | def onSearchInvalidatingChange(self, event): |
926 | """ |
927 | Some event has occurred which trumps any active search, so make the |
928 | |
929 | === modified file 'wxbanker/menubar.py' (properties changed: +x to -x) |
930 | === modified file 'wxbanker/persistentstore.py' (properties changed: +x to -x) |
931 | --- wxbanker/persistentstore.py 2010-02-21 21:53:07 +0000 |
932 | +++ wxbanker/persistentstore.py 2010-03-09 19:09:26 +0000 |
933 | @@ -30,6 +30,28 @@ |
934 | |------------------------+-------------------+--------------+--------------------------+----------------|----------------| |
935 | | 1 | 1 | 100.00 | "Initial Balance" | "2007/01/06" | null | |
936 | +-------------------------------------------------------------------------------------------------------+----------------+ |
937 | + |
938 | +Table: categories |
939 | ++--------------------------------------------+ |
940 | +| id INTEGER PRIMARY KEY | name VARCHAR(255) | |
941 | +|------------------------+-------------------| |
942 | +| 1 | "Some Category" | |
943 | ++--------------------------------------------+ |
944 | + |
945 | +Table: categories_hierarchy |
946 | ++--------------------------------------------+------------------+ |
947 | +| id INTEGER PRIMARY KEY | parentID INTEGER | childID INTEGER | |
948 | +|------------------------+-------------------+------------------| |
949 | +| 1 | 2 | 1 | |
950 | ++--------------------------------------------+------------------+ |
951 | + |
952 | +Table: transactions_categories_link |
953 | ++------------------------------------------------+--------------------+ |
954 | +| id INTEGER PRIMARY KEY | transactionID INTEGER | categoryID INTEGER | |
955 | +|------------------------+-----------------------+--------------------| |
956 | +| 1 | 2 | 1 | |
957 | ++------------------------------------------------+--------------------s+ |
958 | + |
959 | """ |
960 | import sys, os, datetime |
961 | from sqlite3 import dbapi2 as sqlite |
962 | @@ -38,6 +60,8 @@ |
963 | |
964 | from wxbanker import currencies, debug |
965 | from wxbanker.bankobjects.account import Account |
966 | +from wxbanker.bankobjects.category import Category |
967 | +from wxbanker.bankobjects.tag import Tag |
968 | from wxbanker.bankobjects.accountlist import AccountList |
969 | from wxbanker.bankobjects.bankmodel import BankModel |
970 | from wxbanker.bankobjects.transaction import Transaction |
971 | @@ -91,8 +115,12 @@ |
972 | # We have to subscribe before syncing otherwise it won't get synced if there aren't other changes. |
973 | self.Subscriptions = ( |
974 | (self.onORMObjectUpdated, "ormobject.updated"), |
975 | + (self.onCategoryRenamed, "category.renamed"), |
976 | (self.onAccountBalanceChanged, "account.balance changed"), |
977 | (self.onAccountRemoved, "account.removed"), |
978 | + (self.onTransactionCategory, "transaction.category"), |
979 | + (self.onTransactionTagAdded, "transaction.tags.added"), |
980 | + (self.onTransactionTagRemoved, "transaction.tags.removed"), |
981 | (self.onBatchEvent, "batch"), |
982 | (self.onExit, "exiting"), |
983 | ) |
984 | @@ -135,6 +163,24 @@ |
985 | |
986 | return account |
987 | |
988 | + |
989 | + def RemoveChildCategories(self, categoryID): |
990 | + children = self.dbconn.cursor().execute(''' |
991 | + SELECT childId FROM categories_hierarchy |
992 | + WHERE parentID=?''',(categoryID, )).fetchall() |
993 | + for child in children: |
994 | + self.RemoveChildCategories(child[0]) |
995 | + self.dbconn.cursor().execute('DELETE FROM categories WHERE id=?',(child[0],)) |
996 | + self.dbconn.cursor().execute('DELETE FROM categories_hierarchy WHERE childId=?',(child[0],)) |
997 | + self.commitIfAppropriate() |
998 | + |
999 | + def RemoveCategory(self, category): |
1000 | + category.SetParentID(None) |
1001 | + self.RemoveChildCategories(category.ID) |
1002 | + self.dbconn.cursor().execute('DELETE FROM categories WHERE id=?',(category.ID,)) |
1003 | + self.dbconn.cursor().execute('DELETE FROM categories_hierarchy WHERE childId=?',(category.ID,)) |
1004 | + self.commitIfAppropriate() |
1005 | + |
1006 | def RemoveAccount(self, account): |
1007 | self.dbconn.cursor().execute('DELETE FROM accounts WHERE id=?',(account.ID,)) |
1008 | self.commitIfAppropriate() |
1009 | @@ -154,6 +200,7 @@ |
1010 | return transaction |
1011 | |
1012 | def RemoveTransaction(self, transaction): |
1013 | + self.dbconn.cursor().execute('DELETE FROM transactions_categories_link WHERE transactionId=?', (transaction.ID,)) |
1014 | result = self.dbconn.cursor().execute('DELETE FROM transactions WHERE id=?', (transaction.ID,)).fetchone() |
1015 | self.commitIfAppropriate() |
1016 | # The result doesn't appear to be useful here, it is None regardless of whether the DELETE matched anything |
1017 | @@ -161,6 +208,28 @@ |
1018 | # everything is fine. So just return True, as there we no errors that we are aware of. |
1019 | return True |
1020 | |
1021 | + def CreateTag(self, name): |
1022 | + cursor = self.dbconn.cursor() |
1023 | + cursor.execute('INSERT INTO tags (name) VALUES (?)', [name]) |
1024 | + ID = cursor.lastrowid |
1025 | + self.commitIfAppropriate() |
1026 | + |
1027 | + return Tag(ID, name) |
1028 | + |
1029 | + def CreateCategory(self, name, parent): |
1030 | + cursor = self.dbconn.cursor() |
1031 | + cursor.execute('INSERT INTO categories (name) VALUES (?)', [name]) |
1032 | + ID = cursor.lastrowid |
1033 | + if parent: |
1034 | + cursor.execute('INSERT INTO categories_hierarchy (parentId, childId) VALUES (?,?)', [parent, ID]) |
1035 | + self.commitIfAppropriate() |
1036 | + return Category(ID, name, parent) |
1037 | + |
1038 | + def DeleteUnusedTags(self): |
1039 | + cursor = self.dbconn.cursor() |
1040 | + cursor.execute('DELETE FROM tags WHERE NOT EXISTS (SELECT 1 FROM transactions_tags_link l WHERE tagId = tags.id)') |
1041 | + self.commitIfAppropriate() |
1042 | + |
1043 | def Save(self): |
1044 | import time; t = time.time() |
1045 | self.dbconn.commit() |
1046 | @@ -258,12 +327,20 @@ |
1047 | elif fromVer == 3: |
1048 | # Add `linkId` column to transactions for transfers. |
1049 | cursor.execute('ALTER TABLE transactions ADD linkId INTEGER') |
1050 | +# tags table; transactions - tags link table |
1051 | + cursor.execute('CREATE TABLE tags (id INTEGER PRIMARY KEY, name VARCHAR(255))') |
1052 | + cursor.execute('CREATE TABLE transactions_tags_link (id INTEGER PRIMARY KEY, transactionId INTEGER, tagId INTEGER)') |
1053 | + cursor.execute('CREATE INDEX transactions_tags_transactionId_idx ON transactions_tags_link(transactionId)') |
1054 | + cursor.execute('CREATE INDEX transactions_tags_tagId_idx ON transactions_tags_link(tagId)') |
1055 | elif fromVer == 4: |
1056 | + cursor.execute('CREATE TABLE categories (id INTEGER PRIMARY KEY, name VARCHAR(255))') |
1057 | + cursor.execute('CREATE TABLE transactions_categories_link (id INTEGER PRIMARY KEY, transactionId INTEGER, categoryId INTEGER)') |
1058 | # Add recurring transactions table. |
1059 | transactionBase = "id INTEGER PRIMARY KEY, accountId INTEGER, amount FLOAT, description VARCHAR(255), date CHAR(10)" |
1060 | recurringExtra = "repeatType INTEGER, repeatEvery INTEGER, repeatsOn VARCHAR(255), endDate CHAR(10)" |
1061 | cursor.execute('CREATE TABLE recurring_transactions (%s, %s)' % (transactionBase, recurringExtra)) |
1062 | elif fromVer == 5: |
1063 | + cursor.execute('CREATE TABLE categories_hierarchy(id INTEGER PRIMARY KEY, parentId INTEGER, childId INTEGER)') |
1064 | cursor.execute('ALTER TABLE recurring_transactions ADD sourceId INTEGER') |
1065 | cursor.execute('ALTER TABLE recurring_transactions ADD lastTransacted CHAR(10)') |
1066 | elif fromVer == 6: |
1067 | @@ -326,7 +403,7 @@ |
1068 | else: |
1069 | sourceAccount = None |
1070 | |
1071 | - return RecurringTransaction(rId, parentAccount, amount, description, date, repeatType, repeatEvery, repeatOn, endDate, sourceAccount, lastTransacted) |
1072 | + return RecurringTransaction(self, rId, parentAccount, amount, description, date, repeatType, repeatEvery, repeatOn, endDate, sourceAccount, lastTransacted) |
1073 | |
1074 | def getAccounts(self): |
1075 | # Fetch all the accounts. |
1076 | @@ -350,7 +427,7 @@ |
1077 | |
1078 | def result2transaction(self, result, parentObj, linkedTransaction=None, recurringCache=None): |
1079 | tid, pid, amount, description, date, linkId, recurringId = result |
1080 | - t = Transaction(tid, parentObj, amount, description, date) |
1081 | + t = Transaction(self, tid, parentObj, amount, description, date) |
1082 | |
1083 | # Handle a linked transaction being passed in, a special case called from a few lines down. |
1084 | if linkedTransaction: |
1085 | @@ -377,6 +454,72 @@ |
1086 | t.LinkedTransaction.RecurringParent = t.RecurringParent |
1087 | return t |
1088 | |
1089 | + def getCategories(self, check = False): |
1090 | + categories = [self.result2cat(result) for result in self.dbconn.cursor().execute("SELECT * FROM categories").fetchall()] |
1091 | + if check: |
1092 | + categories = self._check_for_orphans(categories) |
1093 | + #return categories |
1094 | + return self._sort_categories(categories) |
1095 | + |
1096 | + def getCategoriesForParent(self, parent): |
1097 | + #print parent.ID |
1098 | + results = self.dbconn.cursor().execute('select categories.id, categories.name from categories, categories_hierarchy where categories.id = categories_hierarchy.childId and categories_hierarchy.parentID = ?', (parent.ID,)).fetchall() |
1099 | + return [self.result2cat(result) for result in results] |
1100 | + |
1101 | + def _check_for_orphans(self, categories): |
1102 | + ''' Check for categories whose parents don't exist''' |
1103 | + for category in categories: |
1104 | + if not self._parent_in_list(category, categories): |
1105 | + category.ParentID = None |
1106 | + return categories |
1107 | + |
1108 | + def _sort_categories(self, categories): |
1109 | + result = [] |
1110 | + while len(categories) > 0: |
1111 | + #print categories |
1112 | + current = categories.pop(0) |
1113 | + if not current.ParentID or self._parent_in_list(current, result): |
1114 | + result.append(current) |
1115 | + else: |
1116 | + categories.append(current) |
1117 | + #print result |
1118 | + return result |
1119 | + |
1120 | + def _parent_in_list(self, item, list): |
1121 | + for possible in list: |
1122 | + if possible.ID == item.ParentID: |
1123 | + return True |
1124 | + return False |
1125 | + |
1126 | + def result2cat(self, result): |
1127 | + ID, name = result |
1128 | + res = self.dbconn.cursor().execute("Select parentID from categories_hierarchy where childId =?", (ID,)).fetchone() |
1129 | + if res: |
1130 | + parent = res[0] |
1131 | + else: |
1132 | + parent = None |
1133 | + return Category(ID, name, parent) |
1134 | + |
1135 | + def getTags(self): |
1136 | + return [self.result2tag(result) for result in self.dbconn.cursor().execute("SELECT * FROM tags").fetchall()] |
1137 | + |
1138 | + def result2tag(self, result): |
1139 | + ID, name = result |
1140 | + return Tag(ID, name) |
1141 | + |
1142 | + def getTagsFrom(self, trans): |
1143 | + result = self.dbconn.cursor().execute( |
1144 | + 'SELECT tagId FROM transactions_tags_link WHERE transactionId=?', (trans.ID, )).fetchall() |
1145 | + return [self.cachedModel.GetTagById(row[0]) for row in result] |
1146 | + |
1147 | + def getCategoryFrom(self, trans): |
1148 | + result = self.dbconn.cursor().execute( |
1149 | + 'SELECT categoryId FROM transactions_categories_link WHERE transactionId=?', (trans.ID, )).fetchone() |
1150 | + if result: |
1151 | + return self.cachedModel.GetCategoryById(result[0]) |
1152 | + else: |
1153 | + return None |
1154 | + |
1155 | def getTransactionsFrom(self, account): |
1156 | transactions = TransactionList() |
1157 | # Generate a map of recurring transaction IDs to the objects for fast look-up. |
1158 | @@ -407,10 +550,36 @@ |
1159 | transaction = self.result2transaction(result, linkedParent, linkedTransaction=linked) |
1160 | return transaction, linkedParent |
1161 | |
1162 | + def getTransactionForCategory(self, category): |
1163 | + transactions = bankobjects.TransactionList() |
1164 | + for result in self.dbconn.cursor().execute('SELECT transactionId FROM transactions_tags_link, WHERE categoryId=?', (category.ID,)).fetchall(): |
1165 | + tid, pif, amount, description, date = self.dbconn.cursor().execute('SELECT * FROM transactions WHERE id=?', (tid,)).fetchone() |
1166 | + transactions.append(bankobjects.Transaction(tid, account, amount, description, date, self.getTagsForTransaction(tid), category )) |
1167 | + |
1168 | + def onTransactionCategory(self, msg): |
1169 | + transObj = msg.data |
1170 | + #result = self.transaction2result(transObj) |
1171 | + #result.append( result.pop(0) ) # Move the uid to the back as it is last in the args below. |
1172 | + self.dbconn.cursor().execute('DELETE FROM transactions_categories_link WHERE transactionId= ?', (transObj.ID, )) |
1173 | + self.dbconn.cursor().execute('INSERT INTO transactions_categories_link (transactionId, categoryId) VALUES (?, ?)', (transObj.ID, transObj.Category.ID)) |
1174 | + self.commitIfAppropriate() |
1175 | + |
1176 | + def onTransactionTagAdded(self, msg): |
1177 | + trans, tag = msg.data |
1178 | + self.dbconn.cursor().execute('INSERT INTO transactions_tags_link (transactionId, tagId) VALUES (?, ?)', (trans.ID, tag.ID)) |
1179 | + |
1180 | + def onTransactionTagRemoved(self, msg): |
1181 | + trans, tag = msg.data |
1182 | + self.dbconn.cursor().execute('DELETE FROM transactions_tags_link WHERE transactionId=? AND tagId=?', (trans.ID, tag.ID)) |
1183 | + |
1184 | def renameAccount(self, oldName, account): |
1185 | self.dbconn.cursor().execute("UPDATE accounts SET name=? WHERE name=?", (account.Name, oldName)) |
1186 | self.commitIfAppropriate() |
1187 | |
1188 | + def renameCategory(self, oldName, category): |
1189 | + self.dbconn.cursor().execute("UPDATE categories SET name=? WHERE name=?", (category.Name, oldName)) |
1190 | + self.commitIfAppropriate() |
1191 | + |
1192 | def setCurrency(self, currencyIndex): |
1193 | self.dbconn.cursor().execute('UPDATE accounts SET currency=?', (currencyIndex,)) |
1194 | self.commitIfAppropriate() |
1195 | @@ -422,6 +591,16 @@ |
1196 | for trans in cursor.execute("SELECT * FROM transactions WHERE accountId=?", (account[0],)).fetchall(): |
1197 | print ' -',trans |
1198 | |
1199 | + |
1200 | + def onCategoryRenamed(self, message): |
1201 | + #debug.debug("in PersistentStore.onCategoryRenamed", message) |
1202 | + oldName, category = message.data |
1203 | + self.renameCategory(oldName, category) |
1204 | + |
1205 | + def onCategoryParent(self, message): |
1206 | + child, parent = message.data |
1207 | + self.changeCategoryParent(child, parent) |
1208 | + |
1209 | def onAccountRenamed(self, message): |
1210 | oldName, account = message.data |
1211 | self.renameAccount(oldName, account) |
1212 | |
1213 | === modified file 'wxbanker/searchctrl.py' (properties changed: +x to -x) |
1214 | === added file 'wxbanker/tests/categorytests.py' |
1215 | --- wxbanker/tests/categorytests.py 1970-01-01 00:00:00 +0000 |
1216 | +++ wxbanker/tests/categorytests.py 2010-03-09 19:09:26 +0000 |
1217 | @@ -0,0 +1,62 @@ |
1218 | +#!/usr/bin/env python |
1219 | +# -*- coding: utf-8 -*- |
1220 | +# https://launchpad.net/wxbanker |
1221 | +# testbase.py: Copyright 2007-2009 Mike Rooney <mrooney@ubuntu.com> |
1222 | +# |
1223 | +# This file is part of wxBanker. |
1224 | +# |
1225 | +# wxBanker is free software: you can redistribute it and/or modify |
1226 | +# it under the terms of the GNU General Public License as published by |
1227 | +# the Free Software Foundation, either version 3 of the License, or |
1228 | +# (at your option) any later version. |
1229 | +# |
1230 | +# wxBanker is distributed in the hope that it will be useful, |
1231 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1232 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1233 | +# GNU General Public License for more details. |
1234 | +# |
1235 | +# You should have received a copy of the GNU General Public License |
1236 | +# along with wxBanker. If not, see <http://www.gnu.org/licenses/>. |
1237 | + |
1238 | +import testbase |
1239 | +import unittest, random |
1240 | + |
1241 | +class CategoryTests(testbase.wxBankerComplexTestCase): |
1242 | + |
1243 | + def testTrivia(self): |
1244 | + # no initial categories should be in the db |
1245 | + self.assertEquals(len(self.Model.Categories),0) |
1246 | + |
1247 | + def testCRUD(self): |
1248 | + names = ['FirstTopLevel', 'SecondTopLevel', 'ThirdTopLevel'] |
1249 | + # create, read |
1250 | + for name in names: |
1251 | + self.Model.GetCategoryByName(name) |
1252 | + #print self.Model.Categories |
1253 | + self.assertEquals(len(names), len(self.Model.Categories)) |
1254 | + self.assertEquals(len(names), len(self.Model.GetTopCategories())) |
1255 | + createdNames = [c.Name for c in self.Model.Categories] |
1256 | + for name in createdNames: |
1257 | + self.assertTrue(name in names) |
1258 | + max = 3 |
1259 | + for cat in self.Model.GetTopCategories(): |
1260 | + for i in range(0,max): |
1261 | + self.Model.CreateCategory("Child " + str(i) + " of " + cat.Name, cat.ID) |
1262 | + self.assertEquals(len(self.Model.Categories), len(names)*max + len(names)) |
1263 | + # delete |
1264 | + oldLength = len(self.Model.Categories) |
1265 | + toDelete = random.choice(self.Model.Categories) |
1266 | + deletions = max + 1 |
1267 | + self.Model.RemoveCategory(toDelete) |
1268 | + self.assertTrue(oldLength, len(self.Model.Categories) + deletions) |
1269 | + self.assertTrue(toDelete not in self.Model.Categories) |
1270 | + # update |
1271 | + toRename = random.choice(self.Model.Categories) |
1272 | + newname = "RENAMED" |
1273 | + oldname = toRename.Name |
1274 | + toRename.Name = newname |
1275 | + self.assertTrue(oldname not in [cat.Name for cat in self.Model.Categories]) |
1276 | + self.assertTrue(newname in [cat.Name for cat in self.Model.Categories]) |
1277 | + |
1278 | +if __name__ == "__main__": |
1279 | + unittest.main() |
1280 | |
1281 | === modified file 'wxbanker/tests/testbase.py' (properties changed: +x to -x) |
1282 | === modified file 'wxbanker/transactionctrl.py' |
1283 | --- wxbanker/transactionctrl.py 2010-01-27 09:14:20 +0000 |
1284 | +++ wxbanker/transactionctrl.py 2010-03-09 19:09:26 +0000 |
1285 | @@ -33,7 +33,7 @@ |
1286 | def __init__(self, parent, editing=None): |
1287 | wx.Panel.__init__(self, parent) |
1288 | # Create the recurring object we will use internally. |
1289 | - self.recurringObj = RecurringTransaction(None, None, 0, "", datetime.date.today(), RecurringTransaction.DAILY) |
1290 | + self.recurringObj = RecurringTransaction(None, None, None, 0, "", datetime.date.today(), RecurringTransaction.DAILY) |
1291 | |
1292 | self.Sizer = wx.GridBagSizer(0, 3) |
1293 | self.Sizer.SetEmptyCellSize((0,0)) |
1294 | |
1295 | === modified file 'wxbanker/transactionolv.py' (properties changed: +x to -x) |
1296 | --- wxbanker/transactionolv.py 2010-01-19 00:01:20 +0000 |
1297 | +++ wxbanker/transactionolv.py 2010-03-09 19:09:26 +0000 |
1298 | @@ -32,7 +32,7 @@ |
1299 | from wx.lib.pubsub import Publisher |
1300 | from wxbanker.ObjectListView import GroupListView, ColumnDefn, CellEditorRegistry |
1301 | from wxbanker import bankcontrols |
1302 | - |
1303 | +import pickle |
1304 | |
1305 | class TransactionOLV(GroupListView): |
1306 | def __init__(self, parent, bankController): |
1307 | @@ -46,6 +46,7 @@ |
1308 | self.oddRowsBackColor = wx.WHITE |
1309 | self.cellEditMode = GroupListView.CELLEDIT_DOUBLECLICK |
1310 | self.SetEmptyListMsg(_("No transactions entered.")) |
1311 | + self.TagSeparator = ',' |
1312 | |
1313 | # Calculate the necessary width for the date column. |
1314 | dateStr = str(datetime.date.today()) |
1315 | @@ -61,6 +62,8 @@ |
1316 | ColumnDefn(_("Description"), valueGetter="Description", isSpaceFilling=True, editFormatter=self.renderEditDescription), |
1317 | ColumnDefn(_("Amount"), "right", valueGetter="Amount", stringConverter=self.renderFloat, editFormatter=self.renderEditFloat), |
1318 | ColumnDefn(_("Total"), "right", valueGetter=self.getTotal, stringConverter=self.renderFloat, isEditable=False), |
1319 | + ColumnDefn(_("Category"), valueGetter="Category", valueSetter=self.setCategoryFromString), |
1320 | + ColumnDefn(_("Tags"), valueGetter=self.getTagsString, valueSetter=self.setTagsFromString), |
1321 | ]) |
1322 | # Our custom hack in OLV.py:2017 will render amount floats appropriately as %.2f when editing. |
1323 | |
1324 | @@ -70,6 +73,8 @@ |
1325 | |
1326 | self.Bind(wx.EVT_RIGHT_DOWN, self.onRightDown) |
1327 | |
1328 | + self.Bind(wx.EVT_LIST_BEGIN_DRAG, self.onDragBegin) |
1329 | + |
1330 | self.Subscriptions = ( |
1331 | (self.onSearch, "SEARCH.INITIATED"), |
1332 | (self.onSearchCancelled, "SEARCH.CANCELLED"), |
1333 | @@ -84,6 +89,19 @@ |
1334 | for callback, topic in self.Subscriptions: |
1335 | Publisher.subscribe(callback, topic) |
1336 | |
1337 | + def onDragBegin(self, event): |
1338 | + text = self._get_selection_ids(flag = "transaction") |
1339 | + do = wx.PyTextDataObject(text) |
1340 | + dropSource = wx.DropSource(self) |
1341 | + dropSource.SetData(do) |
1342 | + dropSource.DoDragDrop(True) |
1343 | + |
1344 | + def _get_selection_ids(self, flag = None): |
1345 | + data = [t.ID for t in self.GetSelectedObjects()] |
1346 | + if flag: |
1347 | + data.insert(0,flag) |
1348 | + return pickle.dumps(data) |
1349 | + |
1350 | def SetObjects(self, objs, *args, **kwargs): |
1351 | """ |
1352 | Override the default SetObjects to properly refresh the auto-size, |
1353 | @@ -119,6 +137,16 @@ |
1354 | self.SortBy(self.SORT_COL) |
1355 | self.Thaw() |
1356 | |
1357 | + def getTagsString(self, transaction): |
1358 | + return (self.TagSeparator + ' ').join([tag.Name for tag in transaction.Tags]) |
1359 | + |
1360 | + def setTagsFromString(self, transaction, tags): |
1361 | + tagNames = [tag.strip() for tag in tags.split(self.TagSeparator) if tag.strip()] |
1362 | + transaction.Tags = [self.BankController.Model.GetTagByName(name) for name in tagNames] |
1363 | + |
1364 | + def setCategoryFromString(self, transaction, name): |
1365 | + transaction.Category = self.BankController.Model.GetCategoryByName(name) |
1366 | + |
1367 | def getTotal(self, transObj): |
1368 | if not hasattr(transObj, "_Total"): |
1369 | self.updateTotals() |
1370 | @@ -159,7 +187,10 @@ |
1371 | transactions = self.BankController.Model.GetTransactions() |
1372 | else: |
1373 | transactions = account.Transactions |
1374 | - |
1375 | + self.loadTransactions(transactions, scrollToBottom=True) |
1376 | + |
1377 | + |
1378 | + def loadTransactions(self, transactions, scrollToBottom=True): |
1379 | self.SetObjects(transactions) |
1380 | # Unselect everything. |
1381 | self.SelectObjects([], deselectOthers=True) |
1382 | @@ -169,6 +200,13 @@ |
1383 | if self.IsSearchActive(): |
1384 | self.doSearch(self.LastSearch) |
1385 | |
1386 | + def setCategory(self, category, scrollToBottom=True): |
1387 | + if category is None: |
1388 | + transactions = [] |
1389 | + else: |
1390 | + transactions = self.BankController.Model.GetTransactionsByCategory(category) |
1391 | + self.loadTransactions(transactions, scrollToBottom=True) |
1392 | + |
1393 | def ensureVisible(self, index): |
1394 | length = self.GetItemCount() |
1395 | # If there are no items, ensure a no-op (LP: #338697) |
1396 | @@ -217,9 +255,15 @@ |
1397 | if len(transactions) == 1: |
1398 | removeStr = _("Remove this transaction") |
1399 | moveStr = _("Move this transaction to account") |
1400 | + addTagStr = _("Tag the transaction with") |
1401 | + removeTagStr = _("Untag the transaction with") |
1402 | + categoryStr = _("Move to category") |
1403 | else: |
1404 | removeStr = _("Remove these %i transactions") % len(transactions) |
1405 | moveStr = _("Move these %i transactions to account") % len(transactions) |
1406 | + addTagStr = _("Tag these %i transactions with") % len(transactions) |
1407 | + removeTagStr = _("Untag these %i transactions with") % len(transactions) |
1408 | + categoryStr = _("Move these %i to category") % len(transactions) |
1409 | |
1410 | removeItem = wx.MenuItem(menu, -1, removeStr) |
1411 | menu.Bind(wx.EVT_MENU, lambda e: self.onRemoveTransactions(transactions), source=removeItem) |
1412 | @@ -243,6 +287,55 @@ |
1413 | # If there are no siblings, disable the item, but leave it there for consistency. |
1414 | if not siblings: |
1415 | menu.Enable(moveMenuItem.Id, False) |
1416 | + # Tagging |
1417 | + tags = sorted(self.BankController.Model.Tags, key=lambda x: x.Name) |
1418 | + |
1419 | + tagActionItem = wx.MenuItem(menu, -1, addTagStr) |
1420 | + tagsMenu = wx.Menu() |
1421 | + for tag in tags: |
1422 | + tagItem = wx.MenuItem(menu, -1, tag.Name) |
1423 | + tagsMenu.AppendItem(tagItem) |
1424 | + tagsMenu.Bind(wx.EVT_MENU, lambda e, tag=tag: self.onTagTransactions(transactions, tag), source=tagItem) |
1425 | + |
1426 | + tagsMenu.AppendSeparator() |
1427 | + |
1428 | + newTagItem = wx.MenuItem(menu,-1, _("New Tag")) |
1429 | + tagsMenu.AppendItem(newTagItem) |
1430 | + tagsMenu.Bind(wx.EVT_MENU, lambda e: self.onTagTransactions(transactions), source=newTagItem) |
1431 | + |
1432 | + tagActionItem.SetSubMenu(tagsMenu) |
1433 | + menu.AppendItem(tagActionItem) |
1434 | + |
1435 | + # get the tags currently applied to the selected transactions |
1436 | + currentTags = set() |
1437 | + for t in transactions: |
1438 | + currentTags.update(t.Tags) |
1439 | + currentTags = sorted(currentTags, key=lambda x: x.Name) |
1440 | + |
1441 | + if len(currentTags) > 0: |
1442 | + tagActionItem = wx.MenuItem(menu, -1, removeTagStr) |
1443 | + tagsMenu = wx.Menu() |
1444 | + |
1445 | + for tag in currentTags: |
1446 | + tagItem = wx.MenuItem(menu, -1, tag.Name) |
1447 | + tagsMenu.AppendItem(tagItem) |
1448 | + tagsMenu.Bind(wx.EVT_MENU, lambda e, tag=tag: self.onUntagTransactions(transactions, tag), source=tagItem) |
1449 | + tagActionItem.SetSubMenu(tagsMenu) |
1450 | + menu.AppendItem(tagActionItem) |
1451 | + |
1452 | + # Categories |
1453 | + menu.AppendSeparator() |
1454 | + cats = sorted(self.BankController.Model.Categories, key=lambda x: x.Name) |
1455 | + |
1456 | + catActionItem = wx.MenuItem(menu, -1, categoryStr) |
1457 | + catMenu = wx.Menu() |
1458 | + for cat in cats: |
1459 | + catItem = wx.MenuItem(menu, -1, cat.Name) |
1460 | + catMenu.AppendItem(catItem) |
1461 | + catMenu.Bind(wx.EVT_MENU, lambda e, cat=cat: self.onCategoryTransactions(transactions, cat), source=catItem) |
1462 | + |
1463 | + catActionItem.SetSubMenu(catMenu) |
1464 | + menu.AppendItem(catActionItem) |
1465 | |
1466 | # Show the menu and then destroy it afterwards. |
1467 | self.PopupMenu(menu) |
1468 | @@ -273,6 +366,31 @@ |
1469 | """Move the transactions to the target account.""" |
1470 | self.CurrentAccount.MoveTransactions(transactions, targetAccount) |
1471 | |
1472 | + def onTagTransactions(self, transactions, tag=None): |
1473 | + if tag is None: |
1474 | + dialog = wx.TextEntryDialog(self, _("Enter new tag"), _("New Tag")) |
1475 | + dialog.ShowModal() |
1476 | + dialog.Destroy() |
1477 | + tagName = dialog.GetValue().strip() |
1478 | + if len(tagName) == 0: |
1479 | + return |
1480 | + tag = self.BankController.Model.GetTagByName(tagName) |
1481 | + Publisher.sendMessage("batch.start") |
1482 | + for t in transactions: |
1483 | + t.AddTag(tag) |
1484 | + Publisher.sendMessage("batch.end") |
1485 | + |
1486 | + def onCategoryTransactions(self, transactions, category=None): |
1487 | + for t in transactions: |
1488 | + t.Category = category |
1489 | + |
1490 | + def onUntagTransactions(self, transactions, tag): |
1491 | + self.CurrentAccount.UntagTransactions(transactions, tag) |
1492 | + Publisher.sendMessage("batch.start") |
1493 | + for t in transactions: |
1494 | + t.RemoveTag(tag) |
1495 | + Publisher.sendMessage("batch.end") |
1496 | + |
1497 | def frozenResize(self): |
1498 | self.Parent.Layout() |
1499 | self.Parent.Thaw() |
this branch is the implementation of blueprint "advanced Category Management". it adds categories to wxbankers functionality. categories are hierarchical and exclusive (one transaction - one category).
working features:
- adding / removing / renaming categories (via button and via context menu)
- subcategories
- assigning a category to a transaction (via context menu, via edit in listctrl and via drag-n-drop)
- display transactions with selected category
- drag-n-drop to change the category hierarchy
- search by category
not implemented:
- split transaction value across categories