Merge lp:~kaboom/wxbanker/advancedCategories into lp:wxbanker

Proposed by wolfer on 2009-06-11
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
Reviewer Review Type Date Requested Status
Michael Rooney 2009-06-11 Needs Information on 2009-06-11
Review via email: mp+7343@code.launchpad.net
To post a comment you must log in.
wolfer (wolfer7) wrote :

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

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!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!", is that still the case?

review: Needs Information
294. By Wolfgang Steitz <wolfer@franzi> on 2009-06-11

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!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!", is that 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-tagging, but all the tagging stuff is in our branch 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.

295. By Wolfgang Steitz <wolfer@franzi> on 2009-06-11

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!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!", is that
> 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-tagging, but all the tagging stuff is in our branch
> 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 :)

296. By wolfer on 2010-02-11

merge lp:~kolmis/wxbanker/transaction-tagging

297. By wolfer on 2010-02-12

merged trunk

298. By wolfer on 2010-03-09

bugfix: saving category of a transaction

299. By wolfer on 2010-03-09

fixed loading of categories

Unmerged revisions

299. By wolfer on 2010-03-09

fixed loading of categories

298. By wolfer on 2010-03-09

bugfix: saving category of a transaction

297. By wolfer on 2010-02-12

merged trunk

296. By wolfer on 2010-02-11

merge lp:~kolmis/wxbanker/transaction-tagging

295. By Wolfgang Steitz <wolfer@franzi> on 2009-06-11

merge trunk

294. By Wolfgang Steitz <wolfer@franzi> on 2009-06-11

remove todofile. bugs did not reappear

293. By Wolfgang Steitz <wolfer@franzi> on 2009-06-11

remove prints in category treectrl

292. By Wolfgang Steitz <wolfer@franzi> on 2009-06-11

drag-n-drop bugfix

291. By Wolfgang Steitz <wolfer@franzi> on 2009-06-09

bugfix

290. By Wolfgang Steitz <wolfer@franzi> on 2009-06-08

category drag-n-drop now works for categories with child categories

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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()

Subscribers

People subscribed via source and target branches

to all changes: