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