-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathreport_objects.py
More file actions
596 lines (457 loc) · 21.4 KB
/
report_objects.py
File metadata and controls
596 lines (457 loc) · 21.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
# add a Report object for details of each report
# not sure what to inherit from
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import GObject
from gi.repository import Gtk
import gnucash_log
from report_options import OptionsDB
from report_options import StringOption,MultiChoiceOption
import dialog_options
from gnc_html_document import HtmlDoc
from gnc_html_document import HtmlDocument
from gnc_report_utilities import report_finished
from stylesheets import Stylesheet
import stylesheets
import xml.etree.ElementTree as ET
import pdb
import traceback
import gc
# define a function equivalent to N_ for internationalization
def N_(msg):
return msg
# hmm there seems to be a similar report class in scheme
# which stores details of the report
# for the moment lets use combo class
# still need to figure about report templates
# although for python different instances might be how to do it
# - the class is the template and each instance a specific report
# so report is going to be the base class and the different types
# become subclasses??
class ParamsData(object):
def __init__ (self):
self.win = None
self.db = None
self.options = None
self.cur_report = None
def apply_cb (self):
gnucash_log.dbglog("paramsdata apply_cb called")
self.db.commit()
self.cur_report.set_dirty(True)
def help_cb (self):
gnucash_log.dbglog("paramsdata help_cb called")
parent = self.win.dialog
dialog = gtk.MessageDialog(parent,gtk.DIALOG_DESTROY_WITH_PARENT,
gtk.MESSAGE_INFO,gtk.BUTTONS_OK,N_("Set the report options you want using this dialog."))
dialog.connect("response", self.dialog_destroy)
dialog.show()
def close_cb (self):
gnucash_log.dbglog("paramsdata close_cb called")
self.cur_report.report_editor_widget = None
self.win.dialog_destroy()
self.db.destroy()
def dialog_destroy (self, widget, response):
widget.destroy()
class ReportTemplate(object):
def __init__ (self):
# these are the scheme template data items
# the following items are defined on report creation
# seems to me in python these become arguments to the Report class instantiation
# and we define the functions as a subclass
self.version = None
self.name = "Welcome to GnuCash"
self.report_guid = None
self.menu_name = None
self.menu_tip = None
self.menu_path = None
# in scheme this contains a function which is defined in the actual report .scm file
# do NOT define these in python as defining these as variables totally
# overrides any subclass function definition
#self.options_generator = None
#self.renderer = None
# do not do any GUI stuff in here - we instantiate before actually needed
# in order to get menu name to build menu
# these are additional variables
self.parent_type = None
self.options_cleanup_cb = None
self.options_changed_cb = None
self.in_menu = False
self.export_types = None
self.export_thunk = None
# make these functions raise error if not defined in subclass
def options_generator (self):
raise NotimplementedError("options_generator function not implemented")
def renderer (self):
raise NotimplementedError("renderer function not implemented")
def init_gui (self):
# this is a function to init GUI stuff if needed
# not in scheme
pass
def cleanup_gui (self):
# this is a function to cleanup GUI stuff if report exits
# not in scheme
# this ensures GUI made sensitive again
report_finished()
def get_template_name (self):
return self.name
def new_options (self):
# OK finally figured what this does in scheme
# gnc:report-template-new-options gets the generator from self.options_generator
# in ReportTemplate
# we define these 2 options
# it then calls gnc:new-options if the generator is not defined
# gnc:new-options creates the hash table(s) databases
namer = StringOption("General","Report name", "0a", N_("Enter a descriptive name for this report."), self.name)
stylesheet = MultiChoiceOption("General","Stylesheet", "0b", N_("Select a stylesheet for the report."), 'default', stylesheets.get_html_style_sheets())
#pdb.set_trace()
# think Ive got this - the report creates the options_generator function
# which defines the reports options
if self.options_generator != None:
options = self.options_generator()
else:
options = OptionsDB()
# I think these are added in addition
options.register_option(namer)
options.register_option(stylesheet)
return options
def options_changed_cb (self):
gnucash_log.dbglog("reporttemplate options_changed_cb")
class Report(object):
# this implements the functionality of scheme reports
# this object is instantiated for each report generated
# it is passed the report template in the report_type argument
# note that the report_type is an instantiation of a ReportTemplate subclass
# not simply the subclass
# the Report instance also encapsulates the specific options for
# the report
# this encodes the scheme report id functionality
# do this as class variables or module globals??
report_next_serial_id = 0
report_ids = {}
def __init__ (self, report_type, options=None):
# this is the equivalent of the following scheme
# ;; gnc:make-report instantiates a report from a report-template.
# ;; The actual report is stored away in a hash-table -- only the id is returned.
# the report-template class is passed in report_type and we instantiate that class
# we store the instantiated reports in the current class dict variable report_ids by id
# however we pass the instance around in python rather than
# the report id as in the scheme version
pdb.set_trace()
# something about template parents here which dont understand
# apparently the passed report type could a child of a report
# somehow this is where the template is stored - possibly through hash id in scheme
# or could well be by report guid
# yes it looks as though in scheme this is actually the guid of the report template
# (see gnc:make-report in report.scm)
if report_type != None:
self.report_type = report_type
else:
self.report_type = ReportTemplate()
self.id = None
self.options = None
self.dirty = False
self.needs_save = False
self.report_editor_widget = None
self.ctext = None
self.custom_template = None
# new python feature - allow for storing per report data
self.report_data = None
# this follows the scheme more closely
if options != None:
self.options = options
else:
self.options = self.report_type.new_options()
# this does a lambda function in scheme
# need explicit function here because of python limitations in lambda definitions
# not quite sure of replacement yet
# lambda_callback should be in the OptionsDB
# this is a separate list of callbacks (multiple callbacks can be registered
# on the options)
#self.options.register_callback(None, None, lambda x : self.lambda_callback(x))
# ah - this is where the scheme id is set and returned
# these are defined in gnc-report.c
# where we see the id is actually a simple count of the number of reports
# instantiated
#(gnc:report-set-id! r (gnc-report-add r))
#(gnc:report-id r))
self.id = self.report_add()
def set_dirty (self, value):
# finally twigged this - this runs the options_changed_cb
# when an option is changed
self.dirty = value
cb = self.report_type.options_changed_cb
if cb != None:
cb()
def lambda_callback (self):
# this is a separate callbak registered with options
# but still - if this ever gets called wont this run the options_changed_cb twice
self.set_dirty(True)
cb = self.report_type.options_changed_cb
if cb != None:
cb()
def report_add (self):
if self.id != None:
if not self.id in Report.report_ids:
Report.report_ids[self.id] = self
return self.id
Report.report_next_serial_id += 1
while Report.report_next_serial_id < GObject.G_MAXINT:
new_id = Report.report_next_serial_id
if not new_id in Report.report_ids:
Report.report_ids[new_id] = self
return new_id
Report.report_next_serial_id += 1
#g_warning("Unable to add report to table. %d reports in use.", G_MAXINT);
def get_editor (self):
return self.report_editor_widget
def set_editor (self, editor):
self.report_editor_widget = editor
def get_report_type (self):
return self.report_type
def raise_editor (self):
if self.report_editor_widget != None:
#gnucash_log.dbglog(type(self.report_editor_widget))
self.report_editor_widget.present()
return True
return False
# yet I think in python we need to invert the arguments here report first, options next
# not clear why the report editor isnt just part of the Report object
# - yes Im thinking this makes much more sense - in which case the report argument becomes self
def default_params_editor (self, options, parent):
# code changed at 3.2
#editor = self.get_editor()
#if editor != None:
# editor.present()
# # why return NULL here - doesnt make sense
# # maybe we never get here in real code
# # this whole test does not make sense
# # returning this makes the process cyclic
# # return None
# return editor
if self.raise_editor():
return None
else:
# is this just used for the callbacks??
# think so
# note that in python every time this is called the previous instance will be
# set for garbage collection and new instance created
# - same goes for the GncOptionDB
default_params_data = ParamsData()
# in scheme self.options are the options in scheme which is where the option value is stored
# GncOptionDB essentially wraps those options in code for interacting with those options
# using gtk
default_params_data.options = self.options
default_params_data.cur_report = self
default_params_data.db = dialog_options.GncOptionDB(default_params_data.options)
rpttyp = self.get_report_type()
# this is C code
#tmplt = rpttyp.get_template()
#title = tmplt.get_template_name()
# so for in python only need this
title = rpttyp.get_template_name()
default_params_data.win = dialog_options.DialogOption.OptionsDialog_New(N_(title), parent)
default_params_data.win.build_contents(default_params_data.db)
default_params_data.db.clean()
# OK changing this to work in python
# we make these functions part of ParamsData then only need to pass the function!!
#default_params_data.win.set_apply_cb(default_params_data.apply_cb,default_params_data)
#default_params_data.win.set_help_cb(default_params_data.help_cb,default_params_data)
#default_params_data.win.set_close_cb(default_params_data.close_cb,default_params_data)
# oh maybe its even easier in python - we just directly set the attribute name!!
default_params_data.win.apply_cb = default_params_data.apply_cb
default_params_data.win.help_cb = default_params_data.help_cb
default_params_data.win.close_cb = default_params_data.close_cb
# could use
#return default_params_data.win.dialog
return default_params_data.win.widget()
def edit_options (self, parent):
#pdb.set_trace()
if self.raise_editor():
return True
if self.options == None:
#gnc_warning_dialog(parent, "%s",
# _("There are no options for this report."));
if parent != None:
parent = gnome_utils_ctypes.ui_get_main_window(None)
dialog = Gtk.MessageDialog(parent,Gtk.DialogFlags.MODAL|Gtk.DialogFlags.DESTROY_WITH_PARENT,
Gtk.MessageType.WARNING,Gtk.ButtonsType.CLOSE,N_("There are no options for this report."))
#if parent != None:
# dailog.set_taskbar_hint(False)
dialog.run()
dialog.destroy()
# this looks completely wrong for gi gtk
## what to do about a parent??
##parent = self.win.dialog
#dialog = gtk.MessageDialog(parent,gtk.DIALOG_DESTROY_WITH_PARENT,
# gtk.MESSAGE_WARNING,gtk.BUTTONS_OK,N_("This report has no options."))
#dialog.connect("response", self.dialog_destroy)
#dialog.show()
return False
# Multi-column type reports need a special options dialog
if self.report_type != None:
if self.report_type.report_guid == 'd8ba4a2e89e8479ca9f6eccdeb164588':
#options_widget = gnc-column-view-edit-options (options,report)
pass
else:
options_widget = self.default_params_editor(self.options, parent)
self.set_editor(options_widget)
return True
def run (self):
# this is based on gnc:report-run in report/report-system/report.scm
gnucash_log.dbglog("run_report")
#gc.collect()
#self.set_busy_cursor()
try:
# this is the call to gnc:report-render-html in report/report-system/report.scm
htmlstr = self.render_html(headers=True)
except Exception as errexc:
traceback.print_exc()
htmlstr = None
#self.unset_busy_cursor()
return htmlstr
def set_stylesheet (self, stylesheet):
# this seems to update the stylesheet name only
#pdb.set_trace()
optobj = self.options.lookup_name('General','Stylesheet')
optobj.set_value(stylesheet)
def stylesheet (self):
# lookup stylesheet option and get stylesheet
#pdb.set_trace()
optobj = self.options.lookup_name('General','Stylesheet')
optval = optobj.get_option_value()
# then get stylesheet instance
stylesheet = stylesheets.stylesheets[optval]
return stylesheet
def render_html (self, headers=None):
gnucash_log.dbglog("render_html")
# this is based on gnc:report-render-html in report/report-system/report.scm
#pdb.set_trace()
# until figure out how self.dirty is set
#if not self.dirty:
# return self.ctext
# think Im seeing how the scheme is working
# - a report can generate either an html string or
# a list of action objects which when executed generate the html string
# I think the idea behind the following is you can (in scheme)
# generate a document object which contains all the data read from
# gnucash which can then be rendered using differing style sheets
# without re-reading data??
# something about getting the template??
# for us this is the report_type object
# to follow scheme more closely we would do the following
#renderer = self.report_type.renderer
stylesheet = self.stylesheet()
# this is the weird stuff - I think the scheme runs the renderer here
# - which returns either a string or a list of html action objects
# doc = renderer()
# then if its not a string we set the stylesheet and run the html document render
# which generates a final string
# if type(doc) == str:
# htmlstr = doc
# else:
# doc.set_stylesheet(stylesheet)
# htmlstr = doc.render(headers=headers)
# in python going with using a python xml dom model - currently ElementTree
# create the html-document equivalent here
docxml = HtmlDocument()
#pdb.set_trace()
docxml.set_stylesheet(stylesheet)
html_str = docxml.render(self,headers)
self.ctext = html_str
self.dirty = False
#fds = open("junkx.html","r")
#docstr = fds.read()
#fds.close()
return html_str
# note these dicts store the report templates
python_reports_by_name = {}
python_reports_by_guid = {}
def load_python_reports ():
# yes we need the global to write into these objects
global python_reports_by_name
global python_reports_by_guid
# ok im wrong - we can instantiate here as long as do
# very little in the __init__ - in particular no GUI
# OK looks like this lookup is done by class - not instance
# no - Im thinking it is instance now
# the 'edited' reports seem to be the html text result of running the report
# cancel the above I now think its a class again
# not clear what the advantage is in python - we just loose the local variable
# space with an instance compared to the class
# we cannot do this as this resets the stored object and any file which imported
# these globals BEFORE running load_python_reports will remain with original empty
# dict
# the clear method removes all keys so should be equivalent behaviour while
# retaining original object pointer
#python_reports_by_name = {}
#python_reports_by_guid = {}
python_reports_by_name.clear()
python_reports_by_guid.clear()
if False:
try:
from reports.hello_world import HelloWorld
python_reports_by_name['HelloWorld'] = HelloWorld()
python_reports_by_guid[python_reports_by_name['HelloWorld'].report_guid] = python_reports_by_name['HelloWorld']
except Exception as errexc:
traceback.print_exc()
pdb.set_trace()
try:
from reports.price_scatter import PriceScatter
python_reports_by_name['PriceScatter'] = PriceScatter()
python_reports_by_guid[python_reports_by_name['PriceScatter'].report_guid] = python_reports_by_name['PriceScatter']
except Exception as errexc:
traceback.print_exc()
pdb.set_trace()
try:
from reports.cash_flow import CashFlow
python_reports_by_name['CashFlow'] = CashFlow()
python_reports_by_guid[python_reports_by_name['CashFlow'].report_guid] = python_reports_by_name['CashFlow']
except Exception as errexc:
traceback.print_exc()
pdb.set_trace()
try:
from reports.portfolio import Portfolio
python_reports_by_name['Portfolio'] = Portfolio()
python_reports_by_guid[python_reports_by_name['Portfolio'].report_guid] = python_reports_by_name['Portfolio']
except Exception as errexc:
traceback.print_exc()
pdb.set_trace()
try:
from reports.advanced_portfolio import AdvancedPortfolio
python_reports_by_name['AdvancedPortfolio'] = AdvancedPortfolio()
python_reports_by_guid[python_reports_by_name['AdvancedPortfolio'].report_guid] = python_reports_by_name['AdvancedPortfolio']
except Exception as errexc:
traceback.print_exc()
pdb.set_trace()
try:
from reports.gains import CapitalGains
python_reports_by_name['CapitalGains'] = CapitalGains()
python_reports_by_guid[python_reports_by_name['CapitalGains'].report_guid] = python_reports_by_name['CapitalGains']
except Exception as errexc:
traceback.print_exc()
pdb.set_trace()
try:
from reports.dividends import Dividends
python_reports_by_name['Dividends'] = Dividends()
python_reports_by_guid[python_reports_by_name['Dividends'].report_guid] = python_reports_by_name['Dividends']
except Exception as errexc:
traceback.print_exc()
pdb.set_trace()
if True:
# code to try for autoimporting - so can just add new reports
# only works assuming always use class name as reports name in python_reports_by_name
#pdb.set_trace()
try:
import reports
except Exception as errexc:
traceback.print_exc()
pdb.set_trace()
# if we want to find all new classes another way is to use introspection
# and find all classes whose __module__ attribute is the module name it was imported
# under
# this is a very sneaky way to find new classes if all classes you want
# are guaranteed to be subclasses of a specific class
__all__classes = [ cls for cls in ReportTemplate.__subclasses__() ]
for report_cls in __all__classes:
python_reports_by_name[report_cls.__name__] = report_cls()
python_reports_by_guid[python_reports_by_name[report_cls.__name__].report_guid] = python_reports_by_name[report_cls.__name__]