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

Proposed by wolfer
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 Needs Information
Review via email: mp+7343@code.launchpad.net
To post a comment you must log in.
Revision history for this message
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

Revision history for this message
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>

remove todofile. bugs did not reappear

Revision history for this message
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>

merge trunk

Revision history for this message
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

merge lp:~kolmis/wxbanker/transaction-tagging

297. By wolfer

merged trunk

298. By wolfer

bugfix: saving category of a transaction

299. By wolfer

fixed loading of categories

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

merge lp:~kolmis/wxbanker/transaction-tagging

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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'wxbanker/bankexceptions.py' (properties changed: +x to -x)
--- wxbanker/bankexceptions.py 2010-02-21 21:53:07 +0000
+++ wxbanker/bankexceptions.py 2010-03-09 19:09:26 +0000
@@ -29,6 +29,14 @@
2929
30 def __str__(self):30 def __str__(self):
31 return "Account '%s' already exists."%self.account31 return "Account '%s' already exists."%self.account
32
33class CategoryAlreadyExistsException(Exception):
34 def __init__(self, category):
35 self.category = category
36
37 def __str__(self):
38 return "Category '%s' already exists."%self.category
39
3240
33class BlankAccountNameException(Exception):41class BlankAccountNameException(Exception):
34 def __str__(self):42 def __str__(self):
3543
=== modified file 'wxbanker/bankobjects/account.py'
--- wxbanker/bankobjects/account.py 2010-02-22 00:32:00 +0000
+++ wxbanker/bankobjects/account.py 2010-03-09 19:09:26 +0000
@@ -265,6 +265,7 @@
265 else:265 else:
266 debug.debug("Ignoring transaction because I am %s: %s" % (self.Name, transaction))266 debug.debug("Ignoring transaction because I am %s: %s" % (self.Name, transaction))
267267
268
268 def float2str(self, *args, **kwargs):269 def float2str(self, *args, **kwargs):
269 return self.Currency.float2str(*args, **kwargs)270 return self.Currency.float2str(*args, **kwargs)
270271
@@ -286,4 +287,4 @@
286 Name = property(GetName, SetName)287 Name = property(GetName, SetName)
287 Transactions = property(GetTransactions)288 Transactions = property(GetTransactions)
288 RecurringTransactions = property(GetRecurringTransactions)289 RecurringTransactions = property(GetRecurringTransactions)
289 Currency = property(GetCurrency, SetCurrency)
290\ No newline at end of file290\ No newline at end of file
291 Currency = property(GetCurrency, SetCurrency)
291292
=== modified file 'wxbanker/bankobjects/bankmodel.py'
--- wxbanker/bankobjects/bankmodel.py 2010-02-22 00:32:00 +0000
+++ wxbanker/bankobjects/bankmodel.py 2010-03-09 19:09:26 +0000
@@ -23,7 +23,11 @@
2323
24from wxbanker import currencies24from wxbanker import currencies
25from wxbanker.bankobjects.ormobject import ORMKeyValueObject25from wxbanker.bankobjects.ormobject import ORMKeyValueObject
26<<<<<<< TREE
26from wxbanker.mint import MintDotCom27from wxbanker.mint import MintDotCom
28=======
29from wxbanker.bankobjects.categorylist import CategoryList
30>>>>>>> MERGE-SOURCE
2731
28class BankModel(ORMKeyValueObject):32class BankModel(ORMKeyValueObject):
29 ORM_TABLE = "meta"33 ORM_TABLE = "meta"
@@ -33,6 +37,8 @@
33 ORMKeyValueObject.__init__(self, store)37 ORMKeyValueObject.__init__(self, store)
34 self.Store = store38 self.Store = store
35 self.Accounts = accountList39 self.Accounts = accountList
40 self.Tags = store.getTags()
41 self.Categories = CategoryList(store, store.getCategories())
3642
37 self.Mint = None43 self.Mint = None
38 if self.MintEnabled:44 if self.MintEnabled:
@@ -46,7 +52,62 @@
4652
47 def GetBalance(self):53 def GetBalance(self):
48 return self.Accounts.Balance54 return self.Accounts.Balance
55
56 def GetTagById(self, id):
57 for tag in self.Tags:
58 if tag.ID == id:
59 return tag
60 return None
61
62 def GetTagByName(self, name):
63 ''' returns a tag by name, creates a new one if none by that name exists '''
64 for tag in self.Tags:
65 if tag.Name == name:
66 return tag
67 newTag = self.Store.CreateTag(name)
68 self.Tags.append(newTag)
69 return newTag
70
71 def GetCategoryById(self, id):
72 return self.Categories.ByID(id)
73
74 def GetTransactionById(self, id):
75 transactions = self.GetTransactions()
76 for t in transactions:
77 if t.ID == id:
78 return t
79 return None
80
81 def GetCategoryByName(self, name, parent = None):
82 ''' returns a category by name, creates a new one if none by that name exists '''
83 for cat in self.Categories:
84 if cat.Name == name:
85 return cat
86 newCat = self.Categories.Create(name, parent)
87 return newCat
49 88
89 def GetChildCategories(self, category):
90 childs = []
91 for cat in self.Categories:
92 if cat.ParentID == category.ID:
93 childs.append(cat)
94 childs.extend(self.GetChildCategories(cat))
95 return childs
96
97 def GetTopCategories(self, sorted = False):
98 result = []
99 for cat in self.Categories:
100 if not cat.ParentID:
101 result.append(cat)
102 if sorted:
103 result.sort(foo)
104 return result
105
106 def GetTransactionsByCategory(self, category):
107 transactions = self.GetTransactions()
108 return [t for t in transactions if category == t.Category]
109
110
50 def GetRecurringTransactions(self):111 def GetRecurringTransactions(self):
51 return self.Accounts.GetRecurringTransactions()112 return self.Accounts.GetRecurringTransactions()
52113
@@ -131,10 +192,22 @@
131192
132 def RemoveAccount(self, accountName):193 def RemoveAccount(self, accountName):
133 return self.Accounts.Remove(accountName)194 return self.Accounts.Remove(accountName)
195
196 def CreateCategory(self, name, parentID):
197 return self.Categories.Create(name, parentID)
198
199 def RemoveCategory(self, cat):
200 for t in self.GetTransactionsByCategory(cat):
201 t.Category = None
202 return self.RemoveCategoryName(cat.Name)
203
204 def RemoveCategoryName(self, catName):
205
206 return self.Categories.Remove(catName)
134207
135 def Search(self, searchString, account=None, matchIndex=1):208 def Search(self, searchString, account=None, matchIndex=1):
136 """209 """
137 matchIndex: 0: Amount, 1: Description, 2: Date210 matchIndex: 0: Amount, 1: Description, 2: Date, 3: Tags, 4: Category
138 I originally used strings here but passing around and then validating on translated211 I originally used strings here but passing around and then validating on translated
139 strings seems like a bad and fragile idea.212 strings seems like a bad and fragile idea.
140 """213 """
@@ -194,4 +267,6 @@
194 for t in a.Transactions:267 for t in a.Transactions:
195 print t268 print t
196269
197 Balance = property(GetBalance)
198\ No newline at end of file270\ No newline at end of file
271 Balance = property(GetBalance)
272
273
199274
=== added file 'wxbanker/bankobjects/category.py'
--- wxbanker/bankobjects/category.py 1970-01-01 00:00:00 +0000
+++ wxbanker/bankobjects/category.py 2010-03-09 19:09:26 +0000
@@ -0,0 +1,73 @@
1#!/usr/bin/env python
2#
3# https://launchpad.net/wxbanker
4# account.py: Copyright 2007-2009 Mike Rooney <mrooney@ubuntu.com>
5#
6# This file is part of wxBanker.
7#
8# wxBanker is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 3 of the License, or
11# (at your option) any later version.
12#
13# wxBanker is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with wxBanker. If not, see <http://www.gnu.org/licenses/>.
20
21from wx.lib.pubsub import Publisher
22
23from wxbanker.bankobjects.ormobject import ORMObject
24
25
26class Category(ORMObject):
27 ORM_TABLE = "categories"
28 ORM_ATTRIBUTES = ["Name", "ParentID"]
29
30
31 def __init__(self, cID, name, parent = None):
32 self.IsFrozen = True
33 self.ID = cID
34 self._Name = name
35 self._ParentID = parent
36 Publisher.sendMessage("category.created.%s" % name, self)
37 self.IsFrozen = False
38
39 def __str__(self):
40 return self._Name
41
42 def __repr__(self):
43 return self.__class__.__name__ + '<' + 'ID=' + str(self.ID) + " Name=" + self._Name + " Parent=" + str(self._ParentID)+'>'
44
45 def __cmp__(self, other):
46 if other is not None:
47 if other == "":
48 return -1
49 else:
50 return cmp(self.ID, other.ID)
51 else:
52 return -1
53
54 def GetParentID(self):
55 return self._ParentID
56
57 def SetParentID(self, parent):
58 self._ParentID = parent
59 #print self._Name, "now has parentID ", self._ParentID
60 Publisher.sendMessage("category.parentID", (self, parent))
61
62 ParentID = property(GetParentID, SetParentID)
63
64 def GetName(self):
65 return self._Name
66
67 def SetName(self, name):
68 #print "in Category.SetName", name
69 oldName = self._Name
70 self._Name = unicode(name)
71 Publisher.sendMessage("category.renamed", (oldName, self))
72
73 Name = property(GetName, SetName)
074
=== added file 'wxbanker/bankobjects/categorylist.py'
--- wxbanker/bankobjects/categorylist.py 1970-01-01 00:00:00 +0000
+++ wxbanker/bankobjects/categorylist.py 2010-03-09 19:09:26 +0000
@@ -0,0 +1,73 @@
1#!/usr/bin/env python
2#
3# https://launchpad.net/wxbanker
4# accountlist.py: Copyright 2007-2009 Mike Rooney <mrooney@ubuntu.com>
5#
6# This file is part of wxBanker.
7#
8# wxBanker is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 3 of the License, or
11# (at your option) any later version.
12#
13# wxBanker is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with wxBanker. If not, see <http://www.gnu.org/licenses/>.
20
21from wx.lib.pubsub import Publisher
22
23
24class CategoryList(list):
25
26 def __init__(self, store, categories):
27 list.__init__(self,categories)
28 for cat in self:
29 cat.ParentList = self
30 self.Store = store
31 #print self
32
33 def CategoryIndex(self, catName):
34 #print self
35 for i, cat in enumerate(self):
36 if cat.Name == catName:
37 return i
38 return -1
39
40 def ByID(self, id):
41 for cat in self:
42 if cat.ID == id:
43 return cat
44 return None
45
46 def Create(self, catName, parentID):
47 # First, ensure a category by that name doesn't already exist.
48 if self.CategoryIndex(catName) >= 0:
49 raise bankexceptions.CategoryAlreadyExistsException(catName)
50 cat = self.Store.CreateCategory(catName, parentID)
51 # Make sure this category knows its parent.
52 cat.ParentList = self
53 self.append(cat)
54 return cat
55
56 def Remove(self, catName):
57 index = self.CategoryIndex(catName)
58 if index == -1:
59 raise bankexceptions.InvalidCategoryException(catName)
60 cat = self.pop(index)
61 self.Store.RemoveCategory(cat)
62 Publisher.sendMessage("category.removed.%s"%catName, cat)
63
64 def __eq__(self, other):
65 if len(self) != len(other):
66 return False
67 for left, right in zip(self, other):
68 if not left == right:
69 return False
70 return True
71
72 def __cmp__(self, other):
73 return cmp(self.Name.lower(), other.Name.lower())
074
=== modified file 'wxbanker/bankobjects/recurringtransaction.py'
--- wxbanker/bankobjects/recurringtransaction.py 2009-12-06 03:14:06 +0000
+++ wxbanker/bankobjects/recurringtransaction.py 2010-03-09 19:09:26 +0000
@@ -37,8 +37,8 @@
37 MONTLY = 237 MONTLY = 2
38 YEARLY = 338 YEARLY = 3
39 39
40 def __init__(self, tID, parent, amount, description, date, repeatType, repeatEvery=1, repeatOn=None, endDate=None, source=None, lastTransacted=None):40 def __init__(self, store, tID, parent, amount, description, date, repeatType, repeatEvery=1, repeatOn=None, endDate=None, source=None, lastTransacted=None):
41 Transaction.__init__(self, tID, parent, amount, description, date)41 Transaction.__init__(self, store, tID, parent, amount, description, date)
42 ORMObject.__init__(self)42 ORMObject.__init__(self)
43 43
44 # If the transaction recurs weekly and repeatsOn isn't specified, use the starting date.44 # If the transaction recurs weekly and repeatsOn isn't specified, use the starting date.
@@ -217,4 +217,4 @@
217 217
218 LastTransacted = property(GetLastTransacted, SetLastTransacted)218 LastTransacted = property(GetLastTransacted, SetLastTransacted)
219 EndDate = property(GetEndDate, SetEndDate)219 EndDate = property(GetEndDate, SetEndDate)
220
221\ No newline at end of file220\ No newline at end of file
221
222222
=== added file 'wxbanker/bankobjects/tag.py'
--- wxbanker/bankobjects/tag.py 1970-01-01 00:00:00 +0000
+++ wxbanker/bankobjects/tag.py 2010-03-09 19:09:26 +0000
@@ -0,0 +1,40 @@
1#!/usr/bin/env python
2#
3# https://launchpad.net/wxbanker
4# account.py: Copyright 2007-2009 Mike Rooney <mrooney@ubuntu.com>
5#
6# This file is part of wxBanker.
7#
8# wxBanker is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 3 of the License, or
11# (at your option) any later version.
12#
13# wxBanker is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with wxBanker. If not, see <http://www.gnu.org/licenses/>.
20
21from wx.lib.pubsub import Publisher
22
23from wxbanker.bankobjects.ormobject import ORMObject
24
25
26class Tag(ORMObject):
27 ORM_TABLE = "tags"
28 ORM_ATTRIBUTES = ["Name",]
29
30 def __init__(self, aID, name):
31 self.IsFrozen = True
32 self.ID = aID
33 self.Name = name
34 Publisher.sendMessage("tag.created.%s" % name, self)
35
36 def __str__(self):
37 return self.Name
38
39 def __cmp__(self, other):
40 return cmp(self.ID, other.ID)
041
=== modified file 'wxbanker/bankobjects/transaction.py'
--- wxbanker/bankobjects/transaction.py 2010-01-19 00:17:01 +0000
+++ wxbanker/bankobjects/transaction.py 2010-03-09 19:09:26 +0000
@@ -34,20 +34,52 @@
34 ORM_TABLE = "transactions"34 ORM_TABLE = "transactions"
35 ORM_ATTRIBUTES = ["_Amount", "_Description", "_Date", "LinkedTransaction", "RecurringParent"]35 ORM_ATTRIBUTES = ["_Amount", "_Description", "_Date", "LinkedTransaction", "RecurringParent"]
36 36
37 def __init__(self, tID, parent, amount, description, date):37 def __init__(self, store, tID, parent, amount, description, date, tags = None, category = None):
38 ORMObject.__init__(self)38 ORMObject.__init__(self)
39 self.IsFrozen = True39 self.IsFrozen = True
4040 self.Store = store
41 self.ID = tID41 self.ID = tID
42 self.LinkedTransaction = None42 self.LinkedTransaction = None
43 self.Parent = parent43 self.Parent = parent
44 self.Date = date44 self.Date = date
45 self.Description = description45 self.Description = description
46 self.Amount = amount46 self.Amount = amount
47 self._Tags = tags
48 self._Category = category
47 self.RecurringParent = None49 self.RecurringParent = None
4850
49 self.IsFrozen = False51 self.IsFrozen = False
5052
53 def GetTags(self):
54 if self._Tags is None:
55 self._Tags = self.Store.getTagsFrom(self)
56 return self._Tags
57
58 def SetTags(self, tags):
59 self._Tags = tags
60 for tag in tags:
61 self.AddTag(tag)
62
63 def AddTag(self, tag):
64 if not tag in self.Tags:
65 self.Tags.append(tag)
66 Publisher.sendMessage("transaction.tags.added", (self, tag))
67
68 def RemoveTag(self, tag):
69 if tag in self.Tags:
70 self.Tags.remove(tag)
71 Publisher.sendMessage("transaction.tags.removed", (self, tag))
72
73 def GetCategory(self):
74 if self._Category is None:
75 self._Category = self.Store.getCategoryFrom(self)
76 return self._Category
77
78 def SetCategory(self, category):
79 self._Category = category
80 if not self.IsFrozen:
81 Publisher.sendMessage("transaction.category.updated", self)
82
51 def GetDate(self):83 def GetDate(self):
52 return self._Date84 return self._Date
5385
@@ -167,4 +199,6 @@
167 Date = property(GetDate, SetDate)199 Date = property(GetDate, SetDate)
168 Description = property(GetDescription, SetDescription)200 Description = property(GetDescription, SetDescription)
169 Amount = property(GetAmount, SetAmount)201 Amount = property(GetAmount, SetAmount)
170 LinkedTransaction = property(GetLinkedTransaction, SetLinkedTransaction)
171\ No newline at end of file202\ No newline at end of file
203 Category = property(GetCategory, SetCategory)
204 LinkedTransaction = property(GetLinkedTransaction, SetLinkedTransaction)
205 Tags = property(GetTags, SetTags)
172206
=== added file 'wxbanker/categoryTreectrl.py'
--- wxbanker/categoryTreectrl.py 1970-01-01 00:00:00 +0000
+++ wxbanker/categoryTreectrl.py 2010-03-09 19:09:26 +0000
@@ -0,0 +1,408 @@
1# -*- coding: utf-8 -*-
2# https://launchpad.net/wxbanker
3# calculator.py: Copyright 2007, 2008 Mike Rooney <mrooney@ubuntu.com>
4#
5# This file is part of wxBanker.
6#
7# wxBanker is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 3 of the License, or
10# (at your option) any later version.
11#
12# wxBanker is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with wxBanker. If not, see <http://www.gnu.org/licenses/>.
19
20import wx
21import bankcontrols, bankexceptions
22from wx.lib.pubsub import Publisher
23import pickle
24
25class CategoryBox(wx.Panel):
26 """
27 This control manages the list of categories.
28 """
29
30 def __init__(self, parent, bankController, autoPopulate=True):
31 wx.Panel.__init__(self, parent)
32 self.model = bankController.Model
33
34 # Initialize some attributes to their default values
35 self.boxLabel = _("Categories")
36 self.editCtrl = None
37 self.categories = {}
38
39 # Create the staticboxsizer which is the home for everything.
40 # This *MUST* be created first to ensure proper z-ordering (as per docs).
41 self.staticBox = wx.StaticBox(self, label=self.boxLabel)
42
43 # Create a single panel to be the "child" of the static box sizer,
44 # to work around a wxPython regression that prevents tooltips. lp: xxxxxx
45 self.childPanel = wx.Panel(self)
46 self.childSizer = childSizer = wx.BoxSizer(wx.VERTICAL)
47
48
49 ## Create and set up the buttons.
50 # The ADD category button.
51 BMP = self.addBMP = wx.ArtProvider.GetBitmap('wxART_add')
52 self.addButton = addButton = wx.BitmapButton(self.childPanel, bitmap=BMP)
53 addButton.SetToolTipString(_("Add a new category"))
54 # The REMOVE category button.
55 BMP = wx.ArtProvider.GetBitmap('wxART_delete')
56 self.removeButton = removeButton = wx.BitmapButton(self.childPanel, bitmap=BMP)
57 removeButton.SetToolTipString(_("Remove the selected category"))
58 removeButton.Enabled = False
59 # The EDIT category button.
60 BMP = wx.ArtProvider.GetBitmap('wxART_textfield_rename')
61 self.editButton = editButton = wx.BitmapButton(self.childPanel, bitmap=BMP)
62 editButton.SetToolTipString(_("Rename the selected category"))
63 editButton.Enabled = False
64 # the DESELECT category button
65 BMP = wx.ArtProvider.GetBitmap('wxART_UNDO', size=(16,16))
66 self.deselectButton = wx.BitmapButton(self.childPanel, bitmap = BMP)
67 self.deselectButton.SetToolTipString(_("Deselect any selected Category"))
68
69
70
71 # Layout the buttons.
72 buttonSizer = wx.BoxSizer()
73 buttonSizer.Add(addButton)
74 buttonSizer.Add(removeButton)
75 buttonSizer.Add(editButton)
76 buttonSizer.Add(self.deselectButton)
77
78 ## Create and set up the treectrl
79 self.treeCtrl = TreeCtrl(self, self.childPanel, style = (wx.TR_HIDE_ROOT + wx.TR_DEFAULT_STYLE+ wx.SUNKEN_BORDER))
80 self.root = self.treeCtrl.AddRoot("")
81 self.treeCtrl.SetIndent(5)
82
83 self.staticBoxSizer = wx.StaticBoxSizer(self.staticBox, wx.VERTICAL)
84 childSizer.Add(buttonSizer, 0, wx.BOTTOM, 4)
85 childSizer.Add(self.treeCtrl, 1, wx.EXPAND)
86 self.childPanel.Sizer = childSizer
87 self.staticBoxSizer.Add(self.childPanel, 1, wx.EXPAND)
88
89 #self.Sizer = aSizer = wx.BoxSizer()
90 #aSizer.Add(self.childPanel, 1, wx.EXPAND)
91
92 # Set up the button bindings.
93 addButton.Bind(wx.EVT_BUTTON, self.onAddButton)
94 removeButton.Bind(wx.EVT_BUTTON, self.onRemoveButton)
95 editButton.Bind(wx.EVT_BUTTON, self.onRenameButton)
96 self.deselectButton.Bind(wx.EVT_BUTTON, self.onDeselect)
97 # and others
98 self.treeCtrl.Bind(wx.EVT_TREE_SEL_CHANGED, self.onCategorySelected)
99 self.treeCtrl.Bind(wx.EVT_TREE_END_LABEL_EDIT, self.onRenameCategory)
100 self.treeCtrl.Bind(wx.EVT_TREE_ITEM_ACTIVATED, self.onRenameButton)
101 self.treeCtrl.Bind(wx.EVT_RIGHT_DOWN, self.onRightDown)
102
103 # Subscribe to messages we are concerned about.
104 Publisher().subscribe(self.onPushChars, "CATEGORYCTRL.PUSH_CHARS")
105 Publisher().subscribe(self.onCategoryCreated, "category.newToDisplay")
106
107 # Populate ourselves initially unless explicitly told not to.
108 if autoPopulate:
109 categories = self.model.Categories
110 for category in categories:
111 self._InsertItem(category)
112
113 self.Sizer = self.staticBoxSizer
114
115
116 #Signal Handler
117 #---------------
118
119 def onDragBegin(self, event):
120 item, where = self.treeCtrl.HitTest(event.GetPoint())
121 cat = self.treeCtrl.get_category_from_item(item)
122 #print "Begin Drag at ", cat
123
124 def onDragEnd(self, event):
125 item, where = self.treeCtrl.HitTest(event.GetPoint())
126 cat = self.treeCtrl.get_category_from_item(item)
127 #print "End Drag at ", cat
128
129 def onCategorySelected(self, event):
130 cat = self._get_category_from_event(event)
131 if cat:
132 self._disable_Buttons(True, True, True, True)
133 # Tell the parent we changed.
134 Publisher().sendMessage("view.category changed", cat)
135
136 def onPushChars(self, message):
137 print message
138
139 def onChar(self, event=None, char=None):
140 print event,char
141
142 def onAddButton(self, event):
143 self.showEditCtrl()
144 self._disable_Buttons()
145
146 def onAddCategoryFromContextMenu(self):
147 self.showEditCtrl()
148 self._disable_Buttons()
149
150 def onRemoveButton(self, event):
151 self.onRemoveCategory(self.treeCtrl.GetSelection())
152
153 def onRenameButton(self, event):
154 item = self.treeCtrl.GetSelection()
155 category = self.treeCtrl.get_selected_category()
156 self.treeCtrl.EditLabel(item)
157
158 def onRenameCategoryFromContextMenu(self, item):
159 self.treeCtrl.EditLabel(item)
160
161 def onRightDown(self, event):
162 item, where = self.treeCtrl.HitTest(event.Position)
163 self.treeCtrl.SelectItem(item, True)
164 self.show_context_menu(item)
165
166 def onEditCtrlKey(self, event):
167 if event.GetKeyCode() == wx.WXK_ESCAPE:
168 self.onHideEditCtrl()
169 else:
170 event.Skip()
171
172 def onDeselect(self, even):
173 self.treeCtrl.UnselectAll()
174 self._disable_Buttons(add = True)
175
176 def onHideEditCtrl(self, event=None, restore=True):
177 # Hide and remove the control and re-layout.
178 self.childSizer.Hide(self.editCtrl)#, smooth=True)
179 self.childSizer.Detach(self.editCtrl)
180 self.Layout()
181 # Re-enable the add button.
182 self._disable_Buttons( add = True, deselect = True)
183
184 def onAddCategory(self, event):
185 # Grab the category name and add it.
186 categoryName = self.editCtrl.Value
187 if self.check_category_name(categoryName):
188 parent = self.treeCtrl.get_selected_category()
189 if parent:
190 category = self.model.GetCategoryByName(categoryName, parent.ID)
191 else:
192 category = self.model.GetCategoryByName(categoryName)
193 Publisher.sendMessage("category.newToDisplay.%s" % categoryName, category)
194 self.onHideEditCtrl()
195
196 def onCategoryCreated(self, message):
197 #print message
198 newcat = self.model.GetCategoryByName(message.topic[2])
199 self._InsertItem(newcat)
200
201 def onRemoveCategory(self, item):
202 category = self.treeCtrl.get_category_from_item(item)
203 if category:
204 warningMsg = _("This will permanently remove the category '%s'. Continue?")
205 dlg = wx.MessageDialog(self, warningMsg%category.Name, _("Warning"), style=wx.YES_NO|wx.ICON_EXCLAMATION)
206 if dlg.ShowModal() == wx.ID_YES:
207 # Remove the category from db
208 self.model.RemoveCategoryName(category.Name)
209 self._RemoveItem(item)
210 #print "verarschen?"
211 self._disable_Buttons(add = True)
212 self.treeCtrl.UnselectAll()
213
214 def onRenameCategory(self, event):
215 #print dir(event)
216 cat = self._get_category_from_event(event)
217 newName = event.GetLabel()
218 if self.check_category_name(newName):
219 cat.Name = newName
220 else:
221 #setting the label does not work (why?) ...
222 self.treeCtrl.SetItemText(event.GetItem(), cat.Name)
223
224
225 def check_category_name(self, name):
226 """checks whether a category name is valid or not"""
227 if name == '':
228 wx.TipWindow(self, _("Empty category names are not allowed!"))
229 return False
230 return True
231
232
233 # Helper Functions
234 #--------------------
235
236 def _get_category_from_event(self, event):
237 return self.treeCtrl.get_category_from_item(event.GetItem())
238
239 def _disable_Buttons(self, add = False, remove = False, edit = False, deselect = False):
240 #print "anscheinend.."
241 self.addButton.Enabled = add
242 self.removeButton.Enabled = remove
243 self.editButton.Enabled = edit
244 self.deselectButton.Enabled = deselect
245
246 def _InsertItemRecursive(self, category):
247 self._InsertItem(category)
248 for child in self.model.GetChildCategories(category):
249 self._InsertItemRecursive(child)
250
251
252 def _InsertItem(self, category):
253 """
254 Insert a category into the given position.
255 """
256 #print "inserting category: ", category, category.ParentID
257 if category.ParentID:
258 #print category, " has parent item ", category.ParentID
259 index = self.treeCtrl.AppendItem(self.categories[category.ParentID], category.Name)
260 self.treeCtrl.SortChildren(self.categories[category.ParentID])
261 self.treeCtrl.Expand(self.categories[category.ParentID])
262 else:
263 index = self.treeCtrl.AppendItem(self.root, category.Name)
264 self.treeCtrl.SetItemPyData(index, category)
265 self.categories[category.ID] = index
266 self.treeCtrl.SortChildren(self.root)
267
268 def _RemoveItem(self, item):
269 self.treeCtrl.Delete(item)
270
271
272 def showEditCtrl(self, focus=True):
273 if self.editCtrl:
274 self.editCtrl.Value = ''
275 self.editCtrl.Show()
276 else:
277 self.editCtrl = wx.TextCtrl(self.childPanel, style=wx.TE_PROCESS_ENTER)
278 self.editCtrl.Bind(wx.EVT_KILL_FOCUS, self.onHideEditCtrl)
279 self.editCtrl.Bind(wx.EVT_KEY_DOWN, self.onEditCtrlKey)
280
281
282 self.editCtrl.Bind(wx.EVT_TEXT_ENTER, self.onAddCategory)
283 self.childSizer.Insert(0, self.editCtrl, 0, wx.EXPAND)#, smooth=True)
284 self.Parent.Layout()
285
286 if focus:
287 self.editCtrl.SetFocus()
288
289 def GetCount(self):
290 return self.treeCtrl.GetCount()
291
292 def show_context_menu(self, item):
293 menu = wx.Menu()
294
295 addItem = wx.MenuItem(menu, -1, _("Add category"))
296 menu.Bind(wx.EVT_MENU, lambda e: self.onAddCategoryFromContextMenu(), source = addItem )
297 addItem.SetBitmap(wx.ArtProvider.GetBitmap('wxART_add'))
298 menu.AppendItem(addItem)
299
300 removeItem = wx.MenuItem(menu, 0, _("Remove category"))
301 menu.Bind(wx.EVT_MENU, lambda e: self.onRemoveCategory(item), source = removeItem)
302 removeItem.SetBitmap(wx.ArtProvider.GetBitmap('wxART_delete'))
303 menu.AppendItem(removeItem)
304
305 editItem = wx.MenuItem(menu, 1, _("Rename category"))
306 menu.Bind(wx.EVT_MENU, lambda e: self.onRenameCategoryFromContextMenu(item), source = editItem)
307 editItem.SetBitmap(wx.ArtProvider.GetBitmap('wxART_textfield_rename'))
308 menu.AppendItem(editItem)
309
310 # Show the menu and then destroy it afterwards.
311 self.treeCtrl.PopupMenu(menu)
312 menu.Destroy()
313
314 def process_transactions(self, transids , category):
315 for tid in transids:
316 transaction = self.model.GetTransactionById(tid)
317 transaction.Category = category
318
319
320class CategoryViewDropTarget(wx.TextDropTarget):
321 def __init__(self, obj):
322 wx.TextDropTarget.__init__(self)
323 self.obj = obj
324
325 def OnDropText(self, x,y,dragResult):
326 self.obj.process_drop_transaction((x,y), dragResult)
327
328class TreeCtrl(wx.TreeCtrl):
329
330 def __init__(self, parent, *args, **kwargs):
331 wx.TreeCtrl.__init__(self, *args, **kwargs)
332 self.parent = parent
333 self.dropTarget = CategoryViewDropTarget(self)
334 self.SetDropTarget(self.dropTarget)
335 self.Bind(wx.EVT_TREE_BEGIN_DRAG, self.on_drag_begin)
336
337 def process_drop_transaction(self, point, drag):
338 item = self.HitTest(wx.Point(*point))
339 category = self.get_category_from_item(item[0])
340 unpickled = pickle.loads(str(drag))
341 flag = unpickled[0]
342 data = unpickled[1:]
343 if category:
344 if flag == 'transaction':
345 self.parent.process_transactions(data, category)
346 elif flag == 'category':
347 self.process_category_drop(data, category)
348 else:
349 pass
350 #print "No Category found for point ", point
351
352 def process_category_drop(self, id_list, target):
353 newID = id_list[0]
354 item = self.parent.categories[newID]
355 cat = self.parent.model.GetCategoryById(id_list[0])
356 if cat == target:
357 #print "No Dragging on oneself!!"
358 return
359 self.parent._RemoveItem(item)
360 if target:
361 cat.ParentID = target.ID
362 else:
363 cat.ParentID = None
364 self.parent._InsertItemRecursive(cat)
365
366 def get_category_from_item(self, item):
367 return self.GetItemPyData(item)
368
369 def get_selected_category(self):
370 if self.hasSelected():
371 return self.GetItemPyData(self.Selection)
372 else:
373 return None
374
375 def OnCompareItems(self, item1, item2):
376 if not self.get_category_from_item(item1):
377 return -1
378 else:
379 if not self.get_category_from_item(item2):
380 return 1
381 else:
382 return cmp(self.get_category_from_item(item1).Name.lower(),self.get_category_from_item(item2).Name.lower())
383
384 def hasSelected(self):
385 selection = self.Selection
386 if selection:
387 if selection.IsOk():
388 if self.GetItemPyData(selection):
389 return True
390 return False
391
392 def _dropData(self):
393 return pickle.dumps(['category',self.get_selected_category().ID])
394
395 def on_drag_begin(self, event):
396 item, where = self.HitTest(event.GetPoint())
397 #print item, item.IsOk()
398 if self.IsExpanded(item):
399 self.Collapse(item)
400 #return
401 if not item.IsOk():
402 #print "Not an okay Item to drag"
403 return
404 text = self._dropData()
405 do = wx.PyTextDataObject(text)
406 dropSource = wx.DropSource(self)
407 dropSource.SetData(do)
408 dropSource.DoDragDrop(True)
0409
=== modified file 'wxbanker/main.py' (properties changed: +x to -x)
=== modified file 'wxbanker/managetab.py' (properties changed: +x to -x)
--- wxbanker/managetab.py 2010-02-03 06:26:15 +0000
+++ wxbanker/managetab.py 2010-03-09 19:09:26 +0000
@@ -21,6 +21,7 @@
21from wxbanker import searchctrl, accountlistctrl, transactionctrl21from wxbanker import searchctrl, accountlistctrl, transactionctrl
22from wxbanker.transactionolv import TransactionOLV as TransactionCtrl22from wxbanker.transactionolv import TransactionOLV as TransactionCtrl
23from wxbanker.calculator import CollapsableWidget, SimpleCalculator23from wxbanker.calculator import CollapsableWidget, SimpleCalculator
24from wxbanker.categoryTreectrl import CategoryBox
24from wx.lib.pubsub import Publisher25from wx.lib.pubsub import Publisher
25from wxbanker import localization, summarytab26from wxbanker import localization, summarytab
26from wxbanker.plots.plotfactory import PlotFactory27from wxbanker.plots.plotfactory import PlotFactory
@@ -34,16 +35,17 @@
34 def __init__(self, parent, bankController):35 def __init__(self, parent, bankController):
35 wx.Panel.__init__(self, parent)36 wx.Panel.__init__(self, parent)
3637
37 ## Left side, the account list and calculator38 ## Left side, the account list, category list and calculator
38 self.leftPanel = leftPanel = wx.Panel(self)39 self.leftPanel = leftPanel = wx.Panel(self)
39 leftPanel.Sizer = wx.BoxSizer(wx.VERTICAL)40 leftPanel.Sizer = wx.BoxSizer(wx.VERTICAL)
4041
41 self.accountCtrl = accountCtrl = accountlistctrl.AccountListCtrl(leftPanel, bankController)42 self.accountCtrl = accountCtrl = accountlistctrl.AccountListCtrl(leftPanel, bankController)
42 43 self.categoryCtrl = categoryCtrl = CategoryBox(leftPanel, bankController)
43 calcWidget = CollapsableWidget(leftPanel, SimpleCalculator, (_("Show Calculator"), _("Hide Calculator")))44 calcWidget = CollapsableWidget(leftPanel, SimpleCalculator, (_("Show Calculator"), _("Hide Calculator")))
4445
45 leftPanel.Sizer.Add(accountCtrl, 0, wx.EXPAND)46 leftPanel.Sizer.Add(accountCtrl, 0, wx.EXPAND)
46 leftPanel.Sizer.AddStretchSpacer(1)47 leftPanel.Sizer.Add(categoryCtrl, 1, wx.EXPAND|wx.ALL)
48 #leftPanel.Sizer.AddStretchSpacer(1)
47 leftPanel.Sizer.Add(calcWidget, 0, wx.EXPAND)49 leftPanel.Sizer.Add(calcWidget, 0, wx.EXPAND)
4850
49 # Force the calculator widget (and parent) to take on the desired size.51 # Force the calculator widget (and parent) to take on the desired size.
@@ -59,6 +61,7 @@
5961
60 # Subscribe to messages that interest us.62 # Subscribe to messages that interest us.
61 Publisher.subscribe(self.onChangeAccount, "view.account changed")63 Publisher.subscribe(self.onChangeAccount, "view.account changed")
64 Publisher.subscribe(self.onChangeCategory, "view.category changed")
62 Publisher.subscribe(self.onCalculatorToggled, "CALCULATOR.TOGGLED")65 Publisher.subscribe(self.onCalculatorToggled, "CALCULATOR.TOGGLED")
6366
64 # Select the last-selected account.67 # Select the last-selected account.
@@ -81,6 +84,10 @@
81 def onChangeAccount(self, message):84 def onChangeAccount(self, message):
82 account = message.data85 account = message.data
83 self.rightPanel.transactionPanel.setAccount(account)86 self.rightPanel.transactionPanel.setAccount(account)
87
88 def onChangeCategory(self, message):
89 category = message.data
90 self.rightPanel.transactionPanel.setCategory(category)
8491
85 def getCurrentAccount(self):92 def getCurrentAccount(self):
86 return self.accountCtrl.GetCurrentAccount()93 return self.accountCtrl.GetCurrentAccount()
@@ -142,7 +149,10 @@
142149
143 def setAccount(self, *args, **kwargs):150 def setAccount(self, *args, **kwargs):
144 self.transactionCtrl.setAccount(*args, **kwargs)151 self.transactionCtrl.setAccount(*args, **kwargs)
145152
153 def setCategory(self, *args, **kwargs):
154 self.transactionCtrl.setCategory(*args, **kwargs)
155
146 def onSearchInvalidatingChange(self, event):156 def onSearchInvalidatingChange(self, event):
147 """157 """
148 Some event has occurred which trumps any active search, so make the158 Some event has occurred which trumps any active search, so make the
149159
=== modified file 'wxbanker/menubar.py' (properties changed: +x to -x)
=== modified file 'wxbanker/persistentstore.py' (properties changed: +x to -x)
--- wxbanker/persistentstore.py 2010-02-21 21:53:07 +0000
+++ wxbanker/persistentstore.py 2010-03-09 19:09:26 +0000
@@ -30,6 +30,28 @@
30|------------------------+-------------------+--------------+--------------------------+----------------|----------------|30|------------------------+-------------------+--------------+--------------------------+----------------|----------------|
31| 1 | 1 | 100.00 | "Initial Balance" | "2007/01/06" | null |31| 1 | 1 | 100.00 | "Initial Balance" | "2007/01/06" | null |
32+-------------------------------------------------------------------------------------------------------+----------------+32+-------------------------------------------------------------------------------------------------------+----------------+
33
34Table: categories
35+--------------------------------------------+
36| id INTEGER PRIMARY KEY | name VARCHAR(255) |
37|------------------------+-------------------|
38| 1 | "Some Category" |
39+--------------------------------------------+
40
41Table: categories_hierarchy
42+--------------------------------------------+------------------+
43| id INTEGER PRIMARY KEY | parentID INTEGER | childID INTEGER |
44|------------------------+-------------------+------------------|
45| 1 | 2 | 1 |
46+--------------------------------------------+------------------+
47
48Table: transactions_categories_link
49+------------------------------------------------+--------------------+
50| id INTEGER PRIMARY KEY | transactionID INTEGER | categoryID INTEGER |
51|------------------------+-----------------------+--------------------|
52| 1 | 2 | 1 |
53+------------------------------------------------+--------------------s+
54
33"""55"""
34import sys, os, datetime56import sys, os, datetime
35from sqlite3 import dbapi2 as sqlite57from sqlite3 import dbapi2 as sqlite
@@ -38,6 +60,8 @@
3860
39from wxbanker import currencies, debug61from wxbanker import currencies, debug
40from wxbanker.bankobjects.account import Account62from wxbanker.bankobjects.account import Account
63from wxbanker.bankobjects.category import Category
64from wxbanker.bankobjects.tag import Tag
41from wxbanker.bankobjects.accountlist import AccountList65from wxbanker.bankobjects.accountlist import AccountList
42from wxbanker.bankobjects.bankmodel import BankModel66from wxbanker.bankobjects.bankmodel import BankModel
43from wxbanker.bankobjects.transaction import Transaction67from wxbanker.bankobjects.transaction import Transaction
@@ -91,8 +115,12 @@
91 # We have to subscribe before syncing otherwise it won't get synced if there aren't other changes.115 # We have to subscribe before syncing otherwise it won't get synced if there aren't other changes.
92 self.Subscriptions = (116 self.Subscriptions = (
93 (self.onORMObjectUpdated, "ormobject.updated"),117 (self.onORMObjectUpdated, "ormobject.updated"),
118 (self.onCategoryRenamed, "category.renamed"),
94 (self.onAccountBalanceChanged, "account.balance changed"),119 (self.onAccountBalanceChanged, "account.balance changed"),
95 (self.onAccountRemoved, "account.removed"),120 (self.onAccountRemoved, "account.removed"),
121 (self.onTransactionCategory, "transaction.category"),
122 (self.onTransactionTagAdded, "transaction.tags.added"),
123 (self.onTransactionTagRemoved, "transaction.tags.removed"),
96 (self.onBatchEvent, "batch"),124 (self.onBatchEvent, "batch"),
97 (self.onExit, "exiting"),125 (self.onExit, "exiting"),
98 )126 )
@@ -135,6 +163,24 @@
135163
136 return account164 return account
137165
166
167 def RemoveChildCategories(self, categoryID):
168 children = self.dbconn.cursor().execute('''
169 SELECT childId FROM categories_hierarchy
170 WHERE parentID=?''',(categoryID, )).fetchall()
171 for child in children:
172 self.RemoveChildCategories(child[0])
173 self.dbconn.cursor().execute('DELETE FROM categories WHERE id=?',(child[0],))
174 self.dbconn.cursor().execute('DELETE FROM categories_hierarchy WHERE childId=?',(child[0],))
175 self.commitIfAppropriate()
176
177 def RemoveCategory(self, category):
178 category.SetParentID(None)
179 self.RemoveChildCategories(category.ID)
180 self.dbconn.cursor().execute('DELETE FROM categories WHERE id=?',(category.ID,))
181 self.dbconn.cursor().execute('DELETE FROM categories_hierarchy WHERE childId=?',(category.ID,))
182 self.commitIfAppropriate()
183
138 def RemoveAccount(self, account):184 def RemoveAccount(self, account):
139 self.dbconn.cursor().execute('DELETE FROM accounts WHERE id=?',(account.ID,))185 self.dbconn.cursor().execute('DELETE FROM accounts WHERE id=?',(account.ID,))
140 self.commitIfAppropriate()186 self.commitIfAppropriate()
@@ -154,6 +200,7 @@
154 return transaction200 return transaction
155201
156 def RemoveTransaction(self, transaction):202 def RemoveTransaction(self, transaction):
203 self.dbconn.cursor().execute('DELETE FROM transactions_categories_link WHERE transactionId=?', (transaction.ID,))
157 result = self.dbconn.cursor().execute('DELETE FROM transactions WHERE id=?', (transaction.ID,)).fetchone()204 result = self.dbconn.cursor().execute('DELETE FROM transactions WHERE id=?', (transaction.ID,)).fetchone()
158 self.commitIfAppropriate()205 self.commitIfAppropriate()
159 # The result doesn't appear to be useful here, it is None regardless of whether the DELETE matched anything206 # The result doesn't appear to be useful here, it is None regardless of whether the DELETE matched anything
@@ -161,6 +208,28 @@
161 # everything is fine. So just return True, as there we no errors that we are aware of.208 # everything is fine. So just return True, as there we no errors that we are aware of.
162 return True209 return True
163210
211 def CreateTag(self, name):
212 cursor = self.dbconn.cursor()
213 cursor.execute('INSERT INTO tags (name) VALUES (?)', [name])
214 ID = cursor.lastrowid
215 self.commitIfAppropriate()
216
217 return Tag(ID, name)
218
219 def CreateCategory(self, name, parent):
220 cursor = self.dbconn.cursor()
221 cursor.execute('INSERT INTO categories (name) VALUES (?)', [name])
222 ID = cursor.lastrowid
223 if parent:
224 cursor.execute('INSERT INTO categories_hierarchy (parentId, childId) VALUES (?,?)', [parent, ID])
225 self.commitIfAppropriate()
226 return Category(ID, name, parent)
227
228 def DeleteUnusedTags(self):
229 cursor = self.dbconn.cursor()
230 cursor.execute('DELETE FROM tags WHERE NOT EXISTS (SELECT 1 FROM transactions_tags_link l WHERE tagId = tags.id)')
231 self.commitIfAppropriate()
232
164 def Save(self):233 def Save(self):
165 import time; t = time.time()234 import time; t = time.time()
166 self.dbconn.commit()235 self.dbconn.commit()
@@ -258,12 +327,20 @@
258 elif fromVer == 3:327 elif fromVer == 3:
259 # Add `linkId` column to transactions for transfers.328 # Add `linkId` column to transactions for transfers.
260 cursor.execute('ALTER TABLE transactions ADD linkId INTEGER')329 cursor.execute('ALTER TABLE transactions ADD linkId INTEGER')
330# tags table; transactions - tags link table
331 cursor.execute('CREATE TABLE tags (id INTEGER PRIMARY KEY, name VARCHAR(255))')
332 cursor.execute('CREATE TABLE transactions_tags_link (id INTEGER PRIMARY KEY, transactionId INTEGER, tagId INTEGER)')
333 cursor.execute('CREATE INDEX transactions_tags_transactionId_idx ON transactions_tags_link(transactionId)')
334 cursor.execute('CREATE INDEX transactions_tags_tagId_idx ON transactions_tags_link(tagId)')
261 elif fromVer == 4:335 elif fromVer == 4:
336 cursor.execute('CREATE TABLE categories (id INTEGER PRIMARY KEY, name VARCHAR(255))')
337 cursor.execute('CREATE TABLE transactions_categories_link (id INTEGER PRIMARY KEY, transactionId INTEGER, categoryId INTEGER)')
262 # Add recurring transactions table.338 # Add recurring transactions table.
263 transactionBase = "id INTEGER PRIMARY KEY, accountId INTEGER, amount FLOAT, description VARCHAR(255), date CHAR(10)"339 transactionBase = "id INTEGER PRIMARY KEY, accountId INTEGER, amount FLOAT, description VARCHAR(255), date CHAR(10)"
264 recurringExtra = "repeatType INTEGER, repeatEvery INTEGER, repeatsOn VARCHAR(255), endDate CHAR(10)"340 recurringExtra = "repeatType INTEGER, repeatEvery INTEGER, repeatsOn VARCHAR(255), endDate CHAR(10)"
265 cursor.execute('CREATE TABLE recurring_transactions (%s, %s)' % (transactionBase, recurringExtra))341 cursor.execute('CREATE TABLE recurring_transactions (%s, %s)' % (transactionBase, recurringExtra))
266 elif fromVer == 5:342 elif fromVer == 5:
343 cursor.execute('CREATE TABLE categories_hierarchy(id INTEGER PRIMARY KEY, parentId INTEGER, childId INTEGER)')
267 cursor.execute('ALTER TABLE recurring_transactions ADD sourceId INTEGER')344 cursor.execute('ALTER TABLE recurring_transactions ADD sourceId INTEGER')
268 cursor.execute('ALTER TABLE recurring_transactions ADD lastTransacted CHAR(10)')345 cursor.execute('ALTER TABLE recurring_transactions ADD lastTransacted CHAR(10)')
269 elif fromVer == 6:346 elif fromVer == 6:
@@ -326,7 +403,7 @@
326 else:403 else:
327 sourceAccount = None404 sourceAccount = None
328405
329 return RecurringTransaction(rId, parentAccount, amount, description, date, repeatType, repeatEvery, repeatOn, endDate, sourceAccount, lastTransacted)406 return RecurringTransaction(self, rId, parentAccount, amount, description, date, repeatType, repeatEvery, repeatOn, endDate, sourceAccount, lastTransacted)
330407
331 def getAccounts(self):408 def getAccounts(self):
332 # Fetch all the accounts.409 # Fetch all the accounts.
@@ -350,7 +427,7 @@
350427
351 def result2transaction(self, result, parentObj, linkedTransaction=None, recurringCache=None):428 def result2transaction(self, result, parentObj, linkedTransaction=None, recurringCache=None):
352 tid, pid, amount, description, date, linkId, recurringId = result429 tid, pid, amount, description, date, linkId, recurringId = result
353 t = Transaction(tid, parentObj, amount, description, date)430 t = Transaction(self, tid, parentObj, amount, description, date)
354431
355 # Handle a linked transaction being passed in, a special case called from a few lines down.432 # Handle a linked transaction being passed in, a special case called from a few lines down.
356 if linkedTransaction:433 if linkedTransaction:
@@ -377,6 +454,72 @@
377 t.LinkedTransaction.RecurringParent = t.RecurringParent454 t.LinkedTransaction.RecurringParent = t.RecurringParent
378 return t455 return t
379456
457 def getCategories(self, check = False):
458 categories = [self.result2cat(result) for result in self.dbconn.cursor().execute("SELECT * FROM categories").fetchall()]
459 if check:
460 categories = self._check_for_orphans(categories)
461 #return categories
462 return self._sort_categories(categories)
463
464 def getCategoriesForParent(self, parent):
465 #print parent.ID
466 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()
467 return [self.result2cat(result) for result in results]
468
469 def _check_for_orphans(self, categories):
470 ''' Check for categories whose parents don't exist'''
471 for category in categories:
472 if not self._parent_in_list(category, categories):
473 category.ParentID = None
474 return categories
475
476 def _sort_categories(self, categories):
477 result = []
478 while len(categories) > 0:
479 #print categories
480 current = categories.pop(0)
481 if not current.ParentID or self._parent_in_list(current, result):
482 result.append(current)
483 else:
484 categories.append(current)
485 #print result
486 return result
487
488 def _parent_in_list(self, item, list):
489 for possible in list:
490 if possible.ID == item.ParentID:
491 return True
492 return False
493
494 def result2cat(self, result):
495 ID, name = result
496 res = self.dbconn.cursor().execute("Select parentID from categories_hierarchy where childId =?", (ID,)).fetchone()
497 if res:
498 parent = res[0]
499 else:
500 parent = None
501 return Category(ID, name, parent)
502
503 def getTags(self):
504 return [self.result2tag(result) for result in self.dbconn.cursor().execute("SELECT * FROM tags").fetchall()]
505
506 def result2tag(self, result):
507 ID, name = result
508 return Tag(ID, name)
509
510 def getTagsFrom(self, trans):
511 result = self.dbconn.cursor().execute(
512 'SELECT tagId FROM transactions_tags_link WHERE transactionId=?', (trans.ID, )).fetchall()
513 return [self.cachedModel.GetTagById(row[0]) for row in result]
514
515 def getCategoryFrom(self, trans):
516 result = self.dbconn.cursor().execute(
517 'SELECT categoryId FROM transactions_categories_link WHERE transactionId=?', (trans.ID, )).fetchone()
518 if result:
519 return self.cachedModel.GetCategoryById(result[0])
520 else:
521 return None
522
380 def getTransactionsFrom(self, account):523 def getTransactionsFrom(self, account):
381 transactions = TransactionList()524 transactions = TransactionList()
382 # Generate a map of recurring transaction IDs to the objects for fast look-up.525 # Generate a map of recurring transaction IDs to the objects for fast look-up.
@@ -407,10 +550,36 @@
407 transaction = self.result2transaction(result, linkedParent, linkedTransaction=linked)550 transaction = self.result2transaction(result, linkedParent, linkedTransaction=linked)
408 return transaction, linkedParent551 return transaction, linkedParent
409552
553 def getTransactionForCategory(self, category):
554 transactions = bankobjects.TransactionList()
555 for result in self.dbconn.cursor().execute('SELECT transactionId FROM transactions_tags_link, WHERE categoryId=?', (category.ID,)).fetchall():
556 tid, pif, amount, description, date = self.dbconn.cursor().execute('SELECT * FROM transactions WHERE id=?', (tid,)).fetchone()
557 transactions.append(bankobjects.Transaction(tid, account, amount, description, date, self.getTagsForTransaction(tid), category ))
558
559 def onTransactionCategory(self, msg):
560 transObj = msg.data
561 #result = self.transaction2result(transObj)
562 #result.append( result.pop(0) ) # Move the uid to the back as it is last in the args below.
563 self.dbconn.cursor().execute('DELETE FROM transactions_categories_link WHERE transactionId= ?', (transObj.ID, ))
564 self.dbconn.cursor().execute('INSERT INTO transactions_categories_link (transactionId, categoryId) VALUES (?, ?)', (transObj.ID, transObj.Category.ID))
565 self.commitIfAppropriate()
566
567 def onTransactionTagAdded(self, msg):
568 trans, tag = msg.data
569 self.dbconn.cursor().execute('INSERT INTO transactions_tags_link (transactionId, tagId) VALUES (?, ?)', (trans.ID, tag.ID))
570
571 def onTransactionTagRemoved(self, msg):
572 trans, tag = msg.data
573 self.dbconn.cursor().execute('DELETE FROM transactions_tags_link WHERE transactionId=? AND tagId=?', (trans.ID, tag.ID))
574
410 def renameAccount(self, oldName, account):575 def renameAccount(self, oldName, account):
411 self.dbconn.cursor().execute("UPDATE accounts SET name=? WHERE name=?", (account.Name, oldName))576 self.dbconn.cursor().execute("UPDATE accounts SET name=? WHERE name=?", (account.Name, oldName))
412 self.commitIfAppropriate()577 self.commitIfAppropriate()
413578
579 def renameCategory(self, oldName, category):
580 self.dbconn.cursor().execute("UPDATE categories SET name=? WHERE name=?", (category.Name, oldName))
581 self.commitIfAppropriate()
582
414 def setCurrency(self, currencyIndex):583 def setCurrency(self, currencyIndex):
415 self.dbconn.cursor().execute('UPDATE accounts SET currency=?', (currencyIndex,))584 self.dbconn.cursor().execute('UPDATE accounts SET currency=?', (currencyIndex,))
416 self.commitIfAppropriate()585 self.commitIfAppropriate()
@@ -422,6 +591,16 @@
422 for trans in cursor.execute("SELECT * FROM transactions WHERE accountId=?", (account[0],)).fetchall():591 for trans in cursor.execute("SELECT * FROM transactions WHERE accountId=?", (account[0],)).fetchall():
423 print ' -',trans592 print ' -',trans
424593
594
595 def onCategoryRenamed(self, message):
596 #debug.debug("in PersistentStore.onCategoryRenamed", message)
597 oldName, category = message.data
598 self.renameCategory(oldName, category)
599
600 def onCategoryParent(self, message):
601 child, parent = message.data
602 self.changeCategoryParent(child, parent)
603
425 def onAccountRenamed(self, message):604 def onAccountRenamed(self, message):
426 oldName, account = message.data605 oldName, account = message.data
427 self.renameAccount(oldName, account)606 self.renameAccount(oldName, account)
428607
=== modified file 'wxbanker/searchctrl.py' (properties changed: +x to -x)
=== added file 'wxbanker/tests/categorytests.py'
--- wxbanker/tests/categorytests.py 1970-01-01 00:00:00 +0000
+++ wxbanker/tests/categorytests.py 2010-03-09 19:09:26 +0000
@@ -0,0 +1,62 @@
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3# https://launchpad.net/wxbanker
4# testbase.py: Copyright 2007-2009 Mike Rooney <mrooney@ubuntu.com>
5#
6# This file is part of wxBanker.
7#
8# wxBanker is free software: you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation, either version 3 of the License, or
11# (at your option) any later version.
12#
13# wxBanker is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with wxBanker. If not, see <http://www.gnu.org/licenses/>.
20
21import testbase
22import unittest, random
23
24class CategoryTests(testbase.wxBankerComplexTestCase):
25
26 def testTrivia(self):
27 # no initial categories should be in the db
28 self.assertEquals(len(self.Model.Categories),0)
29
30 def testCRUD(self):
31 names = ['FirstTopLevel', 'SecondTopLevel', 'ThirdTopLevel']
32 # create, read
33 for name in names:
34 self.Model.GetCategoryByName(name)
35 #print self.Model.Categories
36 self.assertEquals(len(names), len(self.Model.Categories))
37 self.assertEquals(len(names), len(self.Model.GetTopCategories()))
38 createdNames = [c.Name for c in self.Model.Categories]
39 for name in createdNames:
40 self.assertTrue(name in names)
41 max = 3
42 for cat in self.Model.GetTopCategories():
43 for i in range(0,max):
44 self.Model.CreateCategory("Child " + str(i) + " of " + cat.Name, cat.ID)
45 self.assertEquals(len(self.Model.Categories), len(names)*max + len(names))
46 # delete
47 oldLength = len(self.Model.Categories)
48 toDelete = random.choice(self.Model.Categories)
49 deletions = max + 1
50 self.Model.RemoveCategory(toDelete)
51 self.assertTrue(oldLength, len(self.Model.Categories) + deletions)
52 self.assertTrue(toDelete not in self.Model.Categories)
53 # update
54 toRename = random.choice(self.Model.Categories)
55 newname = "RENAMED"
56 oldname = toRename.Name
57 toRename.Name = newname
58 self.assertTrue(oldname not in [cat.Name for cat in self.Model.Categories])
59 self.assertTrue(newname in [cat.Name for cat in self.Model.Categories])
60
61if __name__ == "__main__":
62 unittest.main()
063
=== modified file 'wxbanker/tests/testbase.py' (properties changed: +x to -x)
=== modified file 'wxbanker/transactionctrl.py'
--- wxbanker/transactionctrl.py 2010-01-27 09:14:20 +0000
+++ wxbanker/transactionctrl.py 2010-03-09 19:09:26 +0000
@@ -33,7 +33,7 @@
33 def __init__(self, parent, editing=None):33 def __init__(self, parent, editing=None):
34 wx.Panel.__init__(self, parent)34 wx.Panel.__init__(self, parent)
35 # Create the recurring object we will use internally.35 # Create the recurring object we will use internally.
36 self.recurringObj = RecurringTransaction(None, None, 0, "", datetime.date.today(), RecurringTransaction.DAILY)36 self.recurringObj = RecurringTransaction(None, None, None, 0, "", datetime.date.today(), RecurringTransaction.DAILY)
37 37
38 self.Sizer = wx.GridBagSizer(0, 3)38 self.Sizer = wx.GridBagSizer(0, 3)
39 self.Sizer.SetEmptyCellSize((0,0))39 self.Sizer.SetEmptyCellSize((0,0))
4040
=== modified file 'wxbanker/transactionolv.py' (properties changed: +x to -x)
--- wxbanker/transactionolv.py 2010-01-19 00:01:20 +0000
+++ wxbanker/transactionolv.py 2010-03-09 19:09:26 +0000
@@ -32,7 +32,7 @@
32from wx.lib.pubsub import Publisher32from wx.lib.pubsub import Publisher
33from wxbanker.ObjectListView import GroupListView, ColumnDefn, CellEditorRegistry33from wxbanker.ObjectListView import GroupListView, ColumnDefn, CellEditorRegistry
34from wxbanker import bankcontrols34from wxbanker import bankcontrols
3535import pickle
3636
37class TransactionOLV(GroupListView):37class TransactionOLV(GroupListView):
38 def __init__(self, parent, bankController):38 def __init__(self, parent, bankController):
@@ -46,6 +46,7 @@
46 self.oddRowsBackColor = wx.WHITE46 self.oddRowsBackColor = wx.WHITE
47 self.cellEditMode = GroupListView.CELLEDIT_DOUBLECLICK47 self.cellEditMode = GroupListView.CELLEDIT_DOUBLECLICK
48 self.SetEmptyListMsg(_("No transactions entered."))48 self.SetEmptyListMsg(_("No transactions entered."))
49 self.TagSeparator = ','
4950
50 # Calculate the necessary width for the date column.51 # Calculate the necessary width for the date column.
51 dateStr = str(datetime.date.today())52 dateStr = str(datetime.date.today())
@@ -61,6 +62,8 @@
61 ColumnDefn(_("Description"), valueGetter="Description", isSpaceFilling=True, editFormatter=self.renderEditDescription),62 ColumnDefn(_("Description"), valueGetter="Description", isSpaceFilling=True, editFormatter=self.renderEditDescription),
62 ColumnDefn(_("Amount"), "right", valueGetter="Amount", stringConverter=self.renderFloat, editFormatter=self.renderEditFloat),63 ColumnDefn(_("Amount"), "right", valueGetter="Amount", stringConverter=self.renderFloat, editFormatter=self.renderEditFloat),
63 ColumnDefn(_("Total"), "right", valueGetter=self.getTotal, stringConverter=self.renderFloat, isEditable=False),64 ColumnDefn(_("Total"), "right", valueGetter=self.getTotal, stringConverter=self.renderFloat, isEditable=False),
65 ColumnDefn(_("Category"), valueGetter="Category", valueSetter=self.setCategoryFromString),
66 ColumnDefn(_("Tags"), valueGetter=self.getTagsString, valueSetter=self.setTagsFromString),
64 ])67 ])
65 # Our custom hack in OLV.py:2017 will render amount floats appropriately as %.2f when editing.68 # Our custom hack in OLV.py:2017 will render amount floats appropriately as %.2f when editing.
6669
@@ -70,6 +73,8 @@
70 73
71 self.Bind(wx.EVT_RIGHT_DOWN, self.onRightDown)74 self.Bind(wx.EVT_RIGHT_DOWN, self.onRightDown)
7275
76 self.Bind(wx.EVT_LIST_BEGIN_DRAG, self.onDragBegin)
77
73 self.Subscriptions = (78 self.Subscriptions = (
74 (self.onSearch, "SEARCH.INITIATED"),79 (self.onSearch, "SEARCH.INITIATED"),
75 (self.onSearchCancelled, "SEARCH.CANCELLED"),80 (self.onSearchCancelled, "SEARCH.CANCELLED"),
@@ -84,6 +89,19 @@
84 for callback, topic in self.Subscriptions:89 for callback, topic in self.Subscriptions:
85 Publisher.subscribe(callback, topic)90 Publisher.subscribe(callback, topic)
8691
92 def onDragBegin(self, event):
93 text = self._get_selection_ids(flag = "transaction")
94 do = wx.PyTextDataObject(text)
95 dropSource = wx.DropSource(self)
96 dropSource.SetData(do)
97 dropSource.DoDragDrop(True)
98
99 def _get_selection_ids(self, flag = None):
100 data = [t.ID for t in self.GetSelectedObjects()]
101 if flag:
102 data.insert(0,flag)
103 return pickle.dumps(data)
104
87 def SetObjects(self, objs, *args, **kwargs):105 def SetObjects(self, objs, *args, **kwargs):
88 """106 """
89 Override the default SetObjects to properly refresh the auto-size,107 Override the default SetObjects to properly refresh the auto-size,
@@ -119,6 +137,16 @@
119 self.SortBy(self.SORT_COL)137 self.SortBy(self.SORT_COL)
120 self.Thaw()138 self.Thaw()
121139
140 def getTagsString(self, transaction):
141 return (self.TagSeparator + ' ').join([tag.Name for tag in transaction.Tags])
142
143 def setTagsFromString(self, transaction, tags):
144 tagNames = [tag.strip() for tag in tags.split(self.TagSeparator) if tag.strip()]
145 transaction.Tags = [self.BankController.Model.GetTagByName(name) for name in tagNames]
146
147 def setCategoryFromString(self, transaction, name):
148 transaction.Category = self.BankController.Model.GetCategoryByName(name)
149
122 def getTotal(self, transObj):150 def getTotal(self, transObj):
123 if not hasattr(transObj, "_Total"):151 if not hasattr(transObj, "_Total"):
124 self.updateTotals()152 self.updateTotals()
@@ -159,7 +187,10 @@
159 transactions = self.BankController.Model.GetTransactions()187 transactions = self.BankController.Model.GetTransactions()
160 else:188 else:
161 transactions = account.Transactions189 transactions = account.Transactions
162190 self.loadTransactions(transactions, scrollToBottom=True)
191
192
193 def loadTransactions(self, transactions, scrollToBottom=True):
163 self.SetObjects(transactions)194 self.SetObjects(transactions)
164 # Unselect everything.195 # Unselect everything.
165 self.SelectObjects([], deselectOthers=True)196 self.SelectObjects([], deselectOthers=True)
@@ -169,6 +200,13 @@
169 if self.IsSearchActive():200 if self.IsSearchActive():
170 self.doSearch(self.LastSearch)201 self.doSearch(self.LastSearch)
171202
203 def setCategory(self, category, scrollToBottom=True):
204 if category is None:
205 transactions = []
206 else:
207 transactions = self.BankController.Model.GetTransactionsByCategory(category)
208 self.loadTransactions(transactions, scrollToBottom=True)
209
172 def ensureVisible(self, index):210 def ensureVisible(self, index):
173 length = self.GetItemCount()211 length = self.GetItemCount()
174 # If there are no items, ensure a no-op (LP: #338697)212 # If there are no items, ensure a no-op (LP: #338697)
@@ -217,9 +255,15 @@
217 if len(transactions) == 1:255 if len(transactions) == 1:
218 removeStr = _("Remove this transaction")256 removeStr = _("Remove this transaction")
219 moveStr = _("Move this transaction to account")257 moveStr = _("Move this transaction to account")
258 addTagStr = _("Tag the transaction with")
259 removeTagStr = _("Untag the transaction with")
260 categoryStr = _("Move to category")
220 else:261 else:
221 removeStr = _("Remove these %i transactions") % len(transactions)262 removeStr = _("Remove these %i transactions") % len(transactions)
222 moveStr = _("Move these %i transactions to account") % len(transactions)263 moveStr = _("Move these %i transactions to account") % len(transactions)
264 addTagStr = _("Tag these %i transactions with") % len(transactions)
265 removeTagStr = _("Untag these %i transactions with") % len(transactions)
266 categoryStr = _("Move these %i to category") % len(transactions)
223267
224 removeItem = wx.MenuItem(menu, -1, removeStr)268 removeItem = wx.MenuItem(menu, -1, removeStr)
225 menu.Bind(wx.EVT_MENU, lambda e: self.onRemoveTransactions(transactions), source=removeItem)269 menu.Bind(wx.EVT_MENU, lambda e: self.onRemoveTransactions(transactions), source=removeItem)
@@ -243,6 +287,55 @@
243 # If there are no siblings, disable the item, but leave it there for consistency.287 # If there are no siblings, disable the item, but leave it there for consistency.
244 if not siblings:288 if not siblings:
245 menu.Enable(moveMenuItem.Id, False)289 menu.Enable(moveMenuItem.Id, False)
290 # Tagging
291 tags = sorted(self.BankController.Model.Tags, key=lambda x: x.Name)
292
293 tagActionItem = wx.MenuItem(menu, -1, addTagStr)
294 tagsMenu = wx.Menu()
295 for tag in tags:
296 tagItem = wx.MenuItem(menu, -1, tag.Name)
297 tagsMenu.AppendItem(tagItem)
298 tagsMenu.Bind(wx.EVT_MENU, lambda e, tag=tag: self.onTagTransactions(transactions, tag), source=tagItem)
299
300 tagsMenu.AppendSeparator()
301
302 newTagItem = wx.MenuItem(menu,-1, _("New Tag"))
303 tagsMenu.AppendItem(newTagItem)
304 tagsMenu.Bind(wx.EVT_MENU, lambda e: self.onTagTransactions(transactions), source=newTagItem)
305
306 tagActionItem.SetSubMenu(tagsMenu)
307 menu.AppendItem(tagActionItem)
308
309 # get the tags currently applied to the selected transactions
310 currentTags = set()
311 for t in transactions:
312 currentTags.update(t.Tags)
313 currentTags = sorted(currentTags, key=lambda x: x.Name)
314
315 if len(currentTags) > 0:
316 tagActionItem = wx.MenuItem(menu, -1, removeTagStr)
317 tagsMenu = wx.Menu()
318
319 for tag in currentTags:
320 tagItem = wx.MenuItem(menu, -1, tag.Name)
321 tagsMenu.AppendItem(tagItem)
322 tagsMenu.Bind(wx.EVT_MENU, lambda e, tag=tag: self.onUntagTransactions(transactions, tag), source=tagItem)
323 tagActionItem.SetSubMenu(tagsMenu)
324 menu.AppendItem(tagActionItem)
325
326 # Categories
327 menu.AppendSeparator()
328 cats = sorted(self.BankController.Model.Categories, key=lambda x: x.Name)
329
330 catActionItem = wx.MenuItem(menu, -1, categoryStr)
331 catMenu = wx.Menu()
332 for cat in cats:
333 catItem = wx.MenuItem(menu, -1, cat.Name)
334 catMenu.AppendItem(catItem)
335 catMenu.Bind(wx.EVT_MENU, lambda e, cat=cat: self.onCategoryTransactions(transactions, cat), source=catItem)
336
337 catActionItem.SetSubMenu(catMenu)
338 menu.AppendItem(catActionItem)
246339
247 # Show the menu and then destroy it afterwards.340 # Show the menu and then destroy it afterwards.
248 self.PopupMenu(menu)341 self.PopupMenu(menu)
@@ -273,6 +366,31 @@
273 """Move the transactions to the target account."""366 """Move the transactions to the target account."""
274 self.CurrentAccount.MoveTransactions(transactions, targetAccount)367 self.CurrentAccount.MoveTransactions(transactions, targetAccount)
275368
369 def onTagTransactions(self, transactions, tag=None):
370 if tag is None:
371 dialog = wx.TextEntryDialog(self, _("Enter new tag"), _("New Tag"))
372 dialog.ShowModal()
373 dialog.Destroy()
374 tagName = dialog.GetValue().strip()
375 if len(tagName) == 0:
376 return
377 tag = self.BankController.Model.GetTagByName(tagName)
378 Publisher.sendMessage("batch.start")
379 for t in transactions:
380 t.AddTag(tag)
381 Publisher.sendMessage("batch.end")
382
383 def onCategoryTransactions(self, transactions, category=None):
384 for t in transactions:
385 t.Category = category
386
387 def onUntagTransactions(self, transactions, tag):
388 self.CurrentAccount.UntagTransactions(transactions, tag)
389 Publisher.sendMessage("batch.start")
390 for t in transactions:
391 t.RemoveTag(tag)
392 Publisher.sendMessage("batch.end")
393
276 def frozenResize(self):394 def frozenResize(self):
277 self.Parent.Layout()395 self.Parent.Layout()
278 self.Parent.Thaw()396 self.Parent.Thaw()

Subscribers

People subscribed via source and target branches

to all changes: