scriptplan 0.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. scriptplan/__init__.py +22 -0
  2. scriptplan/cli/__init__.py +7 -0
  3. scriptplan/cli/main.py +546 -0
  4. scriptplan/core/__init__.py +0 -0
  5. scriptplan/core/account.py +125 -0
  6. scriptplan/core/allocation.py +69 -0
  7. scriptplan/core/booking.py +39 -0
  8. scriptplan/core/journal.py +377 -0
  9. scriptplan/core/leave.py +14 -0
  10. scriptplan/core/limits.py +354 -0
  11. scriptplan/core/project.py +924 -0
  12. scriptplan/core/property.py +1290 -0
  13. scriptplan/core/resource.py +198 -0
  14. scriptplan/core/resource_scenario.py +711 -0
  15. scriptplan/core/scenario.py +5 -0
  16. scriptplan/core/scenario_data.py +39 -0
  17. scriptplan/core/shift.py +71 -0
  18. scriptplan/core/task.py +77 -0
  19. scriptplan/core/task_scenario.py +1515 -0
  20. scriptplan/core/timesheet.py +457 -0
  21. scriptplan/core/working_hours.py +231 -0
  22. scriptplan/parser/__init__.py +0 -0
  23. scriptplan/parser/macro_processor.py +264 -0
  24. scriptplan/parser/tjp.lark +412 -0
  25. scriptplan/parser/tjp_parser.py +1904 -0
  26. scriptplan/py.typed +0 -0
  27. scriptplan/report/__init__.py +75 -0
  28. scriptplan/report/html_generator.py +477 -0
  29. scriptplan/report/report.py +466 -0
  30. scriptplan/report/report_base.py +397 -0
  31. scriptplan/report/report_context.py +248 -0
  32. scriptplan/report/resource_report.py +341 -0
  33. scriptplan/report/table_report.py +693 -0
  34. scriptplan/report/task_report.py +362 -0
  35. scriptplan/report/text_report.py +172 -0
  36. scriptplan/scheduler/__init__.py +0 -0
  37. scriptplan/scheduler/batch_processor.py +238 -0
  38. scriptplan/scheduler/scoreboard.py +120 -0
  39. scriptplan/utils/__init__.py +0 -0
  40. scriptplan/utils/data_cache.py +46 -0
  41. scriptplan/utils/logger.py +243 -0
  42. scriptplan/utils/message_handler.py +515 -0
  43. scriptplan/utils/time.py +195 -0
  44. scriptplan-0.9.0.dist-info/METADATA +161 -0
  45. scriptplan-0.9.0.dist-info/RECORD +49 -0
  46. scriptplan-0.9.0.dist-info/WHEEL +5 -0
  47. scriptplan-0.9.0.dist-info/entry_points.txt +2 -0
  48. scriptplan-0.9.0.dist-info/licenses/LICENSE +201 -0
  49. scriptplan-0.9.0.dist-info/top_level.txt +1 -0
scriptplan/__init__.py ADDED
@@ -0,0 +1,22 @@
1
+ """
2
+ ScriptPlan - A Python implementation of TaskJuggler.
3
+
4
+ This package provides project scheduling and resource management capabilities
5
+ similar to TaskJuggler, implemented in Python.
6
+ """
7
+
8
+ __version__ = "0.9.0"
9
+ __author__ = "ScriptPlan Team"
10
+
11
+ from scriptplan.core.project import Project
12
+ from scriptplan.core.task import Task
13
+ from scriptplan.core.resource import Resource
14
+ from scriptplan.core.scenario import Scenario
15
+
16
+ __all__ = [
17
+ '__version__',
18
+ 'Project',
19
+ 'Task',
20
+ 'Resource',
21
+ 'Scenario',
22
+ ]
@@ -0,0 +1,7 @@
1
+ """
2
+ Command-line interface for ScriptPlan.
3
+ """
4
+
5
+ from scriptplan.cli.main import main, ScriptPlan, create_parser
6
+
7
+ __all__ = ['main', 'ScriptPlan', 'create_parser']
scriptplan/cli/main.py ADDED
@@ -0,0 +1,546 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ ScriptPlan CLI.
4
+
5
+ This is the main command-line interface for the ScriptPlan
6
+ application (Python implementation of TaskJuggler). It reads project files,
7
+ schedules the project, and generates reports.
8
+
9
+ Usage:
10
+ scriptplan [options] <project_file.tjp> [additional_files...]
11
+
12
+ Examples:
13
+ scriptplan project.tjp
14
+ scriptplan --output-dir ./reports project.tjp
15
+ scriptplan --check-syntax project.tjp
16
+ scriptplan --report task_list project.tjp
17
+ """
18
+
19
+ import argparse
20
+ import sys
21
+ import os
22
+ import logging
23
+ from pathlib import Path
24
+ from typing import List, Optional
25
+ import re
26
+
27
+ from scriptplan import __version__
28
+
29
+
30
+ def setup_logging(debug_level: int = 0, debug_modules: Optional[List[str]] = None) -> None:
31
+ """
32
+ Configure logging based on debug settings.
33
+
34
+ Args:
35
+ debug_level: Verbosity level (0=WARNING, 1=INFO, 2=DEBUG)
36
+ debug_modules: List of module names to enable debug for
37
+ """
38
+ levels = {
39
+ 0: logging.WARNING,
40
+ 1: logging.INFO,
41
+ 2: logging.DEBUG,
42
+ }
43
+ level = levels.get(debug_level, logging.DEBUG)
44
+
45
+ format_str = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
46
+ logging.basicConfig(level=level, format=format_str)
47
+
48
+ if debug_modules:
49
+ # Set all loggers to WARNING, then enable specific ones
50
+ logging.getLogger().setLevel(logging.WARNING)
51
+ for module in debug_modules:
52
+ logging.getLogger(f'scriptplan.{module}').setLevel(level)
53
+
54
+
55
+ def create_parser() -> argparse.ArgumentParser:
56
+ """
57
+ Create the argument parser for the CLI.
58
+
59
+ Returns:
60
+ Configured ArgumentParser
61
+ """
62
+ parser = argparse.ArgumentParser(
63
+ prog='scriptplan',
64
+ description='ScriptPlan - A Python implementation of TaskJuggler',
65
+ epilog='For more information, visit: https://github.com/scriptplan/scriptplan',
66
+ formatter_class=argparse.RawDescriptionHelpFormatter
67
+ )
68
+
69
+ parser.add_argument(
70
+ 'files',
71
+ nargs='*',
72
+ metavar='FILE',
73
+ help='Project file(s) to process (.tjp and .tji files)'
74
+ )
75
+
76
+ parser.add_argument(
77
+ '-V', '--version',
78
+ action='version',
79
+ version=f'%(prog)s {__version__}'
80
+ )
81
+
82
+ # Debug options
83
+ debug_group = parser.add_argument_group('Debug Options')
84
+ debug_group.add_argument(
85
+ '--debug-level',
86
+ type=int,
87
+ default=0,
88
+ metavar='N',
89
+ help='Verbosity of debug output (0-2, default: 0)'
90
+ )
91
+ debug_group.add_argument(
92
+ '--debug-modules',
93
+ type=str,
94
+ metavar='x,y,z',
95
+ help='Restrict debug output to a comma-separated list of modules'
96
+ )
97
+
98
+ # Processing options
99
+ proc_group = parser.add_argument_group('Processing Options')
100
+ proc_group.add_argument(
101
+ '--check-syntax',
102
+ action='store_true',
103
+ help='Only parse the input files and check syntax, do not schedule'
104
+ )
105
+ proc_group.add_argument(
106
+ '--no-reports',
107
+ action='store_true',
108
+ help='Schedule the project but do not generate any reports'
109
+ )
110
+ proc_group.add_argument(
111
+ '-f', '--force-reports',
112
+ action='store_true',
113
+ help='Generate reports even if there are scheduling errors'
114
+ )
115
+ proc_group.add_argument(
116
+ '--abort-on-warnings',
117
+ action='store_true',
118
+ help='Treat warnings as errors and abort'
119
+ )
120
+
121
+ # Report options
122
+ report_group = parser.add_argument_group('Report Options')
123
+ report_group.add_argument(
124
+ '-o', '--output-dir',
125
+ type=str,
126
+ metavar='DIR',
127
+ help='Directory where reports should be written'
128
+ )
129
+ report_group.add_argument(
130
+ '--report',
131
+ type=str,
132
+ action='append',
133
+ metavar='ID',
134
+ dest='report_ids',
135
+ help='Generate only the report with specified ID (can be used multiple times)'
136
+ )
137
+ report_group.add_argument(
138
+ '--reports',
139
+ type=str,
140
+ action='append',
141
+ metavar='REGEX',
142
+ dest='report_patterns',
143
+ help='Generate only reports matching the regex pattern (can be used multiple times)'
144
+ )
145
+ report_group.add_argument(
146
+ '--list-reports',
147
+ type=str,
148
+ nargs='?',
149
+ const='.*',
150
+ metavar='REGEX',
151
+ help='List all reports matching the pattern (default: all)'
152
+ )
153
+
154
+ # Freeze options
155
+ freeze_group = parser.add_argument_group('Freeze/Booking Options')
156
+ freeze_group.add_argument(
157
+ '--freeze',
158
+ action='store_true',
159
+ help='Generate or update the booking file for the project'
160
+ )
161
+ freeze_group.add_argument(
162
+ '--freeze-date',
163
+ type=str,
164
+ metavar='DATE',
165
+ help='Use a different date as cut-off for the booking file'
166
+ )
167
+ freeze_group.add_argument(
168
+ '--freeze-by-task',
169
+ action='store_true',
170
+ help='Group bookings by task instead of by resource'
171
+ )
172
+
173
+ # Time/Status sheet options
174
+ sheet_group = parser.add_argument_group('Time/Status Sheet Options')
175
+ sheet_group.add_argument(
176
+ '--check-time-sheet',
177
+ type=str,
178
+ action='append',
179
+ metavar='FILE',
180
+ dest='time_sheets',
181
+ help='Check the given time sheet file'
182
+ )
183
+ sheet_group.add_argument(
184
+ '--check-status-sheet',
185
+ type=str,
186
+ action='append',
187
+ metavar='FILE',
188
+ dest='status_sheets',
189
+ help='Check the given status sheet file'
190
+ )
191
+ sheet_group.add_argument(
192
+ '--warn-ts-deltas',
193
+ action='store_true',
194
+ help='Enable warnings for time sheet delta changes'
195
+ )
196
+
197
+ # Other options
198
+ parser.add_argument(
199
+ '-c', '--max-cores',
200
+ type=int,
201
+ default=1,
202
+ metavar='N',
203
+ help='Maximum number of CPU cores to use (default: 1)'
204
+ )
205
+ parser.add_argument(
206
+ '--add-trace',
207
+ action='store_true',
208
+ help='Append current data set to all trace reports'
209
+ )
210
+
211
+ return parser
212
+
213
+
214
+ class ScriptPlan:
215
+ """
216
+ Main application class for ScriptPlan.
217
+
218
+ This class orchestrates the parsing, scheduling, and report generation
219
+ workflow.
220
+ """
221
+
222
+ def __init__(self, args: argparse.Namespace):
223
+ """
224
+ Initialize the application with parsed arguments.
225
+
226
+ Args:
227
+ args: Parsed command-line arguments
228
+ """
229
+ self.args = args
230
+ self.project = None
231
+ self.errors = 0
232
+ self.warnings = 0
233
+ self.logger = logging.getLogger('scriptplan.cli')
234
+
235
+ def run(self) -> int:
236
+ """
237
+ Execute the main application workflow.
238
+
239
+ Returns:
240
+ Exit code (0 for success, non-zero for errors)
241
+ """
242
+ # Validate inputs
243
+ if not self.args.files:
244
+ print("Error: You must provide at least one .tjp file", file=sys.stderr)
245
+ return 1
246
+
247
+ # Validate output directory
248
+ if self.args.output_dir:
249
+ output_path = Path(self.args.output_dir)
250
+ if not output_path.exists():
251
+ print(f"Error: Output directory '{self.args.output_dir}' does not exist",
252
+ file=sys.stderr)
253
+ return 1
254
+ if not output_path.is_dir():
255
+ print(f"Error: '{self.args.output_dir}' is not a directory",
256
+ file=sys.stderr)
257
+ return 1
258
+
259
+ # Parse project files
260
+ if not self.parse_files(self.args.files):
261
+ return 1
262
+
263
+ # Syntax check only?
264
+ if self.args.check_syntax:
265
+ print("Syntax check passed.")
266
+ return 0
267
+
268
+ # Schedule the project
269
+ if not self.schedule():
270
+ if not self.args.force_reports:
271
+ return 1
272
+ print("Scheduling failed, but continuing due to --force-reports")
273
+
274
+ # Check time sheets if requested
275
+ if self.args.time_sheets:
276
+ for ts_file in self.args.time_sheets:
277
+ if not self.check_time_sheet(ts_file):
278
+ return 1
279
+
280
+ # Check status sheets if requested
281
+ if self.args.status_sheets:
282
+ for ss_file in self.args.status_sheets:
283
+ if not self.check_status_sheet(ss_file):
284
+ return 1
285
+
286
+ # Freeze (generate bookings) if requested
287
+ if self.args.freeze:
288
+ if not self.freeze_project():
289
+ return 1
290
+
291
+ # List reports if requested
292
+ if self.args.list_reports:
293
+ self.list_reports(self.args.list_reports)
294
+
295
+ # Generate reports
296
+ if not self.args.no_reports:
297
+ if not self.generate_reports():
298
+ return 1
299
+
300
+ return 0 if self.errors == 0 else 1
301
+
302
+ def parse_files(self, files: List[str]) -> bool:
303
+ """
304
+ Parse the project files.
305
+
306
+ Args:
307
+ files: List of file paths to parse
308
+
309
+ Returns:
310
+ True if parsing succeeded, False otherwise
311
+ """
312
+ from scriptplan.parser.tjp_parser import ProjectFileParser
313
+
314
+ try:
315
+ parser = ProjectFileParser()
316
+
317
+ # Parse the main project file
318
+ main_file = files[0]
319
+ self.logger.info(f"Parsing {main_file}")
320
+
321
+ if not os.path.exists(main_file):
322
+ print(f"Error: File '{main_file}' not found", file=sys.stderr)
323
+ return False
324
+
325
+ with open(main_file, 'r', encoding='utf-8') as f:
326
+ content = f.read()
327
+
328
+ self.project = parser.parse(content)
329
+ self.logger.info(f"Project '{self.project.name}' loaded successfully")
330
+
331
+ # Parse additional include files
332
+ for include_file in files[1:]:
333
+ self.logger.info(f"Parsing include file {include_file}")
334
+ if not os.path.exists(include_file):
335
+ print(f"Warning: Include file '{include_file}' not found",
336
+ file=sys.stderr)
337
+ self.warnings += 1
338
+ continue
339
+
340
+ # TODO: Parse and merge include files
341
+ # For now, just acknowledge them
342
+ self.logger.debug(f"Include file {include_file} acknowledged")
343
+
344
+ return True
345
+
346
+ except Exception as e:
347
+ print(f"Error parsing project file: {e}", file=sys.stderr)
348
+ self.errors += 1
349
+ return False
350
+
351
+ def schedule(self) -> bool:
352
+ """
353
+ Schedule the project.
354
+
355
+ Returns:
356
+ True if scheduling succeeded, False otherwise
357
+ """
358
+ if not self.project:
359
+ return False
360
+
361
+ try:
362
+ self.logger.info("Scheduling project...")
363
+ result = self.project.schedule()
364
+
365
+ if result:
366
+ self.logger.info("Scheduling completed successfully")
367
+ else:
368
+ self.logger.warning("Scheduling completed with issues")
369
+ self.warnings += 1
370
+
371
+ return result
372
+
373
+ except Exception as e:
374
+ print(f"Error during scheduling: {e}", file=sys.stderr)
375
+ self.errors += 1
376
+ return False
377
+
378
+ def check_time_sheet(self, filename: str) -> bool:
379
+ """
380
+ Check a time sheet file.
381
+
382
+ Args:
383
+ filename: Path to the time sheet file
384
+
385
+ Returns:
386
+ True if check passed, False otherwise
387
+ """
388
+ self.logger.info(f"Checking time sheet: {filename}")
389
+ # TODO: Implement time sheet checking
390
+ print(f"Time sheet checking not yet implemented: {filename}")
391
+ return True
392
+
393
+ def check_status_sheet(self, filename: str) -> bool:
394
+ """
395
+ Check a status sheet file.
396
+
397
+ Args:
398
+ filename: Path to the status sheet file
399
+
400
+ Returns:
401
+ True if check passed, False otherwise
402
+ """
403
+ self.logger.info(f"Checking status sheet: {filename}")
404
+ # TODO: Implement status sheet checking
405
+ print(f"Status sheet checking not yet implemented: {filename}")
406
+ return True
407
+
408
+ def freeze_project(self) -> bool:
409
+ """
410
+ Generate a booking file for the project.
411
+
412
+ Returns:
413
+ True if freeze succeeded, False otherwise
414
+ """
415
+ self.logger.info("Generating booking file...")
416
+ # TODO: Implement freeze/booking generation
417
+ print("Freeze/booking generation not yet implemented")
418
+ return True
419
+
420
+ def list_reports(self, pattern: str) -> None:
421
+ """
422
+ List reports matching the given pattern.
423
+
424
+ Args:
425
+ pattern: Regular expression pattern to match report IDs
426
+ """
427
+ if not self.project:
428
+ return
429
+
430
+ try:
431
+ regex = re.compile(pattern)
432
+ except re.error as e:
433
+ print(f"Invalid regex pattern: {e}", file=sys.stderr)
434
+ return
435
+
436
+ print("\nAvailable Reports:")
437
+ print("-" * 60)
438
+
439
+ report_count = 0
440
+ for report in self.project.reports:
441
+ if regex.search(report.fullId):
442
+ formats = report.get('formats') or []
443
+ format_str = ', '.join(str(f) for f in formats) if formats else 'none'
444
+ print(f" {report.fullId}: {report.name} [{format_str}]")
445
+ report_count += 1
446
+
447
+ if report_count == 0:
448
+ print(" No reports match the specified pattern")
449
+ else:
450
+ print(f"\nTotal: {report_count} report(s)")
451
+
452
+ def generate_reports(self) -> bool:
453
+ """
454
+ Generate project reports.
455
+
456
+ Returns:
457
+ True if report generation succeeded, False otherwise
458
+ """
459
+ if not self.project:
460
+ return False
461
+
462
+ from scriptplan.report import ReportContext
463
+
464
+ try:
465
+ output_dir = self.args.output_dir or './'
466
+ self.project.outputDir = output_dir
467
+
468
+ # Determine which reports to generate
469
+ report_ids = self.args.report_ids or []
470
+ report_patterns = self.args.report_patterns or []
471
+
472
+ reports_to_generate = []
473
+
474
+ if report_ids or report_patterns:
475
+ # Filter to specific reports
476
+ for report in self.project.reports:
477
+ # Check exact IDs
478
+ if report.fullId in report_ids:
479
+ reports_to_generate.append(report)
480
+ continue
481
+
482
+ # Check patterns
483
+ for pattern in report_patterns:
484
+ try:
485
+ if re.match(pattern, report.fullId):
486
+ reports_to_generate.append(report)
487
+ break
488
+ except re.error:
489
+ pass
490
+ else:
491
+ # Generate all reports
492
+ reports_to_generate = list(self.project.reports)
493
+
494
+ if not reports_to_generate:
495
+ self.logger.info("No reports to generate")
496
+ return True
497
+
498
+ self.logger.info(f"Generating {len(reports_to_generate)} report(s)...")
499
+
500
+ for report in reports_to_generate:
501
+ self.logger.info(f"Generating report: {report.fullId}")
502
+
503
+ # Create report context
504
+ context = ReportContext(self.project, report)
505
+ context.push()
506
+
507
+ try:
508
+ result = report.generate()
509
+ if result != 0:
510
+ self.warnings += 1
511
+ finally:
512
+ context.pop()
513
+
514
+ self.logger.info("Report generation completed")
515
+ return True
516
+
517
+ except Exception as e:
518
+ print(f"Error generating reports: {e}", file=sys.stderr)
519
+ self.errors += 1
520
+ return False
521
+
522
+
523
+ def main(argv: Optional[List[str]] = None) -> int:
524
+ """
525
+ Main entry point for the CLI.
526
+
527
+ Args:
528
+ argv: Command-line arguments (defaults to sys.argv[1:])
529
+
530
+ Returns:
531
+ Exit code
532
+ """
533
+ parser = create_parser()
534
+ args = parser.parse_args(argv)
535
+
536
+ # Setup logging
537
+ debug_modules = args.debug_modules.split(',') if args.debug_modules else None
538
+ setup_logging(args.debug_level, debug_modules)
539
+
540
+ # Run the application
541
+ app = ScriptPlan(args)
542
+ return app.run()
543
+
544
+
545
+ if __name__ == '__main__':
546
+ sys.exit(main())
File without changes
@@ -0,0 +1,125 @@
1
+ """Account module implementing financial transaction records.
2
+
3
+ An Account is an object to record financial transactions. Alternatively, an
4
+ Account can just be a container for a set of Accounts. In this case it
5
+ cannot directly record any transactions.
6
+ """
7
+
8
+ from scriptplan.core.property import PropertyTreeNode
9
+ from scriptplan.core.scenario_data import ScenarioData
10
+
11
+
12
+ class AccountScenario(ScenarioData):
13
+ """Handles the scenario-specific features of an Account object."""
14
+
15
+ def __init__(self, account, scenarioIdx, attributes):
16
+ super().__init__(account, scenarioIdx, attributes)
17
+ self._credits = []
18
+
19
+ def _get(self, attrName):
20
+ """Get attribute value using property's attribute access."""
21
+ return self.property.get(attrName, self.scenarioIdx)
22
+
23
+ def query_balance(self, query):
24
+ """Query the account balance.
25
+
26
+ The balance is the turnover from project start to the start of the query period.
27
+ """
28
+ startIdx = 0
29
+ endIdx = self.project.dateToIdx(query.start) if hasattr(self.project, 'dateToIdx') else 0
30
+
31
+ amount = self.turnover(startIdx, endIdx)
32
+ query.sortable = amount
33
+ query.numerical = amount
34
+ if hasattr(query, 'currencyFormat') and query.currencyFormat:
35
+ query.string = query.currencyFormat.format(amount)
36
+ else:
37
+ query.string = str(amount)
38
+
39
+ def query_turnover(self, query):
40
+ """Query the turnover for a period."""
41
+ startIdx = self.project.dateToIdx(query.start) if hasattr(self.project, 'dateToIdx') else 0
42
+ endIdx = self.project.dateToIdx(query.end) if hasattr(self.project, 'dateToIdx') else 0
43
+
44
+ amount = self.turnover(startIdx, endIdx)
45
+ query.sortable = amount
46
+ query.numerical = amount
47
+ if hasattr(query, 'currencyFormat') and query.currencyFormat:
48
+ query.string = query.currencyFormat.format(amount)
49
+ else:
50
+ query.string = str(amount)
51
+
52
+ def turnover(self, startIdx, endIdx):
53
+ """Compute the turnover for the period between startIdx and endIdx."""
54
+ amount = 0.0
55
+
56
+ # Accumulate amounts directly credited to the account during the interval
57
+ credits = self._get('credits')
58
+ if credits:
59
+ startDate = self.project.idxToDate(startIdx) if hasattr(self.project, 'idxToDate') else None
60
+ endDate = self.project.idxToDate(endIdx) if hasattr(self.project, 'idxToDate') else None
61
+
62
+ if startDate and endDate:
63
+ for credit in credits:
64
+ if hasattr(credit, 'date') and hasattr(credit, 'amount'):
65
+ if startDate <= credit.date < endDate:
66
+ amount += credit.amount
67
+
68
+ if self.property.container():
69
+ if not self.property.adoptees:
70
+ # Normal case: accumulate turnover of child accounts
71
+ for child in self.property.children:
72
+ amount += child.scenario(self.scenarioIdx).turnover(startIdx, endIdx)
73
+ else:
74
+ # Special case for meta account (balance calculation)
75
+ # First adoptee is cost account, second is revenue account
76
+ if len(self.property.adoptees) >= 2:
77
+ amount += (
78
+ -self.property.adoptees[0].scenario(self.scenarioIdx).turnover(startIdx, endIdx) +
79
+ self.property.adoptees[1].scenario(self.scenarioIdx).turnover(startIdx, endIdx)
80
+ )
81
+ else:
82
+ aggregate = self.property.get('aggregate')
83
+ if aggregate == 'tasks' or aggregate == ':tasks':
84
+ for task in self.project.tasks:
85
+ if hasattr(task.scenario(self.scenarioIdx), 'turnover'):
86
+ amount += task.scenario(self.scenarioIdx).turnover(
87
+ startIdx, endIdx, self.property, None, False
88
+ )
89
+ elif aggregate == 'resources' or aggregate == ':resources':
90
+ for resource in self.project.resources:
91
+ if resource.leaf():
92
+ if hasattr(resource.scenario(self.scenarioIdx), 'turnover'):
93
+ amount += resource.scenario(self.scenarioIdx).turnover(
94
+ startIdx, endIdx, self.property, None, False
95
+ )
96
+ return amount
97
+
98
+
99
+ class Account(PropertyTreeNode):
100
+ """An Account records financial transactions.
101
+
102
+ An Account can also be a container for other Accounts, in which case
103
+ it cannot directly record transactions but aggregates them from children.
104
+ """
105
+
106
+ def __init__(self, project, id, name, parent):
107
+ super().__init__(project.accounts, id, name, parent)
108
+ project.addAccount(self)
109
+
110
+ # Initialize scenario data array
111
+ self.data = [None] * project.scenarioCount()
112
+ for i in range(project.scenarioCount()):
113
+ AccountScenario(self, i, self._scenarioAttributes[i])
114
+
115
+ def scenario(self, scenarioIdx):
116
+ """Return a reference to the scenarioIdx-th scenario."""
117
+ return self.data[scenarioIdx]
118
+
119
+ def container(self):
120
+ """Return True if this account is a container (has children)."""
121
+ return len(self.children) > 0 or len(self.adoptees) > 0
122
+
123
+ def turnover(self, scenarioIdx, startIdx, endIdx):
124
+ """Get the turnover for the specified scenario and period."""
125
+ return self.data[scenarioIdx].turnover(startIdx, endIdx)