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.
- scriptplan/__init__.py +22 -0
- scriptplan/cli/__init__.py +7 -0
- scriptplan/cli/main.py +546 -0
- scriptplan/core/__init__.py +0 -0
- scriptplan/core/account.py +125 -0
- scriptplan/core/allocation.py +69 -0
- scriptplan/core/booking.py +39 -0
- scriptplan/core/journal.py +377 -0
- scriptplan/core/leave.py +14 -0
- scriptplan/core/limits.py +354 -0
- scriptplan/core/project.py +924 -0
- scriptplan/core/property.py +1290 -0
- scriptplan/core/resource.py +198 -0
- scriptplan/core/resource_scenario.py +711 -0
- scriptplan/core/scenario.py +5 -0
- scriptplan/core/scenario_data.py +39 -0
- scriptplan/core/shift.py +71 -0
- scriptplan/core/task.py +77 -0
- scriptplan/core/task_scenario.py +1515 -0
- scriptplan/core/timesheet.py +457 -0
- scriptplan/core/working_hours.py +231 -0
- scriptplan/parser/__init__.py +0 -0
- scriptplan/parser/macro_processor.py +264 -0
- scriptplan/parser/tjp.lark +412 -0
- scriptplan/parser/tjp_parser.py +1904 -0
- scriptplan/py.typed +0 -0
- scriptplan/report/__init__.py +75 -0
- scriptplan/report/html_generator.py +477 -0
- scriptplan/report/report.py +466 -0
- scriptplan/report/report_base.py +397 -0
- scriptplan/report/report_context.py +248 -0
- scriptplan/report/resource_report.py +341 -0
- scriptplan/report/table_report.py +693 -0
- scriptplan/report/task_report.py +362 -0
- scriptplan/report/text_report.py +172 -0
- scriptplan/scheduler/__init__.py +0 -0
- scriptplan/scheduler/batch_processor.py +238 -0
- scriptplan/scheduler/scoreboard.py +120 -0
- scriptplan/utils/__init__.py +0 -0
- scriptplan/utils/data_cache.py +46 -0
- scriptplan/utils/logger.py +243 -0
- scriptplan/utils/message_handler.py +515 -0
- scriptplan/utils/time.py +195 -0
- scriptplan-0.9.0.dist-info/METADATA +161 -0
- scriptplan-0.9.0.dist-info/RECORD +49 -0
- scriptplan-0.9.0.dist-info/WHEEL +5 -0
- scriptplan-0.9.0.dist-info/entry_points.txt +2 -0
- scriptplan-0.9.0.dist-info/licenses/LICENSE +201 -0
- 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
|
+
]
|
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)
|