tricc-oo 1.5.28__py3-none-any.whl → 1.6.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.
tests/build.py CHANGED
@@ -6,6 +6,7 @@ from tricc_oo.strategies.output.xls_form import XLSFormStrategy # noqa: F401
6
6
  from tricc_oo.strategies.output.openmrs_form import OpenMRSStrategy # noqa: F401
7
7
  from tricc_oo.strategies.output.fhir_form import FHIRStrategy # noqa: F401
8
8
  from tricc_oo.strategies.output.html_form import HTMLStrategy # noqa: F401
9
+ from tricc_oo.strategies.output.dhis2_form import DHIS2Strategy # noqa: F401
9
10
  from tricc_oo.strategies.input.drawio import DrawioStrategy # noqa: F401
10
11
  import getopt
11
12
  import logging
@@ -53,8 +53,8 @@ def get_export_name(node, replace_dots=True):
53
53
  export_name = BOOLEAN_MAP[str(TRICC_FALSE_VALUE)]
54
54
  elif value == '$this':
55
55
  export_name = '.'
56
- elif isinstance(value, str):
57
- export_name = f"'{clean_str(value, replace_dots=replace_dots)}'"
56
+ elif isinstance(value, str) and not isinstance(node, str):
57
+ export_name = f"'{value}'"
58
58
  else:
59
59
  export_name = value
60
60
  if hasattr(node, 'export_name'):
@@ -101,16 +101,16 @@ def create_activity(diagram, media_path, project):
101
101
 
102
102
  external_id = diagram.attrib.get("id")
103
103
  id = get_id(external_id, diagram.attrib.get("id"))
104
- root = create_root_node(diagram)
105
- name = diagram.attrib.get("name")
104
+ root, name = create_root_node(diagram)
105
+ label = diagram.attrib.get("name")
106
106
  form_id = diagram.attrib.get("name", None)
107
107
  if root is not None:
108
108
  activity = TriccNodeActivity(
109
109
  root=root,
110
- name=get_rand_name(f"a{id}"),
110
+ name=name, # start node 'name' is saved in label
111
111
  id=id,
112
112
  external_id=external_id,
113
- label=name,
113
+ label=label,
114
114
  form_id=form_id,
115
115
  )
116
116
  if root.relevance is not None:
@@ -130,7 +130,7 @@ def create_activity(diagram, media_path, project):
130
130
  if (
131
131
  issubclass(n.__class__, (TriccNodeDisplayModel, TriccNodeDisplayCalculateBase))
132
132
  and not isinstance(n, (TriccRhombusMixIn, TriccNodeRhombus, TriccNodeDisplayBridge))
133
- and not n.name.startswith("label_")
133
+ and not n.name.startswith("label_") # FIXME
134
134
  ):
135
135
  system = n.name.split(".")[0] if "." in n.name else "tricc"
136
136
  if isinstance(n, TriccNodeSelectOption) and isinstance(n.select, TriccNodeSelectNotAvailable):
@@ -444,17 +444,18 @@ def create_root_node(diagram):
444
444
  if elm is not None:
445
445
  external_id = elm.attrib.get("id")
446
446
  id = get_id(external_id, diagram.attrib.get("id"))
447
+ name = generate_id("start"+external_id)
447
448
  node = TriccNodeActivityStart(
448
449
  id=id,
449
450
  external_id=external_id,
450
451
  # parent=elm.attrib.get("parent"),
451
- name="ma" + id,
452
+ name=name,
452
453
  label=diagram.attrib.get("name"),
453
454
  relevance=elm.attrib.get("relevance"),
454
455
  instance=int(elm.attrib.get("instance") if elm.attrib.get("instance") is not None else 1),
455
456
  )
456
457
  load_expressions(node)
457
- return node
458
+ return node, elm.attrib.get("name", generate_id("act"+external_id))
458
459
 
459
460
 
460
461
  # converter XML item to object
tricc_oo/models/base.py CHANGED
@@ -494,6 +494,8 @@ class TriccOperation(BaseModel):
494
494
  return "mixed"
495
495
  else:
496
496
  return rtype.pop()
497
+ else:
498
+ return self.get_reference_datatype(self.reference)
497
499
 
498
500
  def get_reference_datatype(self, references):
499
501
  rtype = set()
@@ -0,0 +1,859 @@
1
+ import logging
2
+ import os
3
+ import json
4
+ import uuid
5
+ import string
6
+ from tricc_oo.visitors.tricc import (
7
+ is_ready_to_process,
8
+ process_reference,
9
+ generate_base,
10
+ generate_calculate,
11
+ walktrhough_tricc_node_processed_stached,
12
+ check_stashed_loop,
13
+ )
14
+ from tricc_oo.converters.tricc_to_xls_form import get_export_name
15
+ import datetime
16
+ from tricc_oo.strategies.output.base_output_strategy import BaseOutPutStrategy
17
+ from tricc_oo.models.base import (
18
+ not_clean, TriccOperation,
19
+ TriccStatic, TriccReference
20
+ )
21
+ from tricc_oo.models.tricc import (
22
+ TriccNodeSelectOption,
23
+ TriccNodeInputModel,
24
+ TriccNodeBaseModel,
25
+ TriccNodeDisplayModel,
26
+ TriccNodeCalculateBase,
27
+ TriccNodeActivity,
28
+ )
29
+ from tricc_oo.models.calculate import TriccNodeDisplayCalculateBase
30
+ from tricc_oo.models.ordered_set import OrderedSet
31
+
32
+ logger = logging.getLogger("default")
33
+
34
+ # Namespace for deterministic UUIDs
35
+ UUID_NAMESPACE = uuid.UUID('87654321-4321-8765-cba9-fed098765432')
36
+
37
+
38
+ class DHIS2Strategy(BaseOutPutStrategy):
39
+ processes = ["main"]
40
+ project = None
41
+ output_path = None
42
+
43
+ def __init__(self, project, output_path):
44
+ super().__init__(project, output_path)
45
+ form_id = getattr(self.project.start_pages["main"], 'form_id', 'dhis2_program')
46
+ self.program_metadata = {
47
+ "id": self.generate_id(form_id),
48
+ "name": form_id,
49
+ "shortName": form_id[:50], # DHIS2 shortName limit
50
+ "programType": "WITHOUT_REGISTRATION",
51
+ "programStages": [],
52
+ "programRules": []
53
+ }
54
+ self.option_sets = {}
55
+ self.options = {}
56
+ self.data_elements = {}
57
+ self.program_rules = []
58
+ self.program_rule_actions = []
59
+ self.program_rule_variables = []
60
+ self.field_counter = 1
61
+ self.current_section = None
62
+ self.concept_map = {}
63
+ # Track programRuleActions per stage
64
+ self.stage_rule_actions = {}
65
+ self.sections = {}
66
+
67
+ def get_export_name(self, r):
68
+ if isinstance(r, TriccNodeSelectOption):
69
+ return self.get_option_value(r.name)
70
+ elif isinstance(r, str):
71
+ return self.get_option_value(r)
72
+ elif isinstance(r, TriccStatic):
73
+ if isinstance(r.value, str):
74
+ return self.get_option_value(r.value)
75
+ elif isinstance(r.value, bool):
76
+ return str(r.value).lower()
77
+ else:
78
+ return r.value
79
+ else:
80
+ return get_export_name(r)
81
+
82
+ def generate_id(self, name):
83
+ """Generate DHIS2-compliant UID: 1 letter + 10 alphanumeric characters"""
84
+ # Convert UUID to base62-like string and take first 11 chars, ensuring starts with letter
85
+ # Create DHIS2 UID: start with letter, followed by 10 alphanum chars
86
+ letters = string.ascii_letters
87
+ alphanum = string.ascii_letters + string.digits
88
+
89
+ # Use hash of the name to get deterministic but varied results
90
+ import hashlib
91
+ hash_obj = hashlib.md5(name.encode('utf-8')).digest()
92
+ hash_int = int.from_bytes(hash_obj, byteorder='big')
93
+
94
+ # First character: letter
95
+ first_char = letters[hash_int % len(letters)]
96
+
97
+ # Remaining 10 characters: alphanumeric
98
+ remaining_chars = ''
99
+ for i in range(10):
100
+ remaining_chars += alphanum[(hash_int >> (i * 6)) % len(alphanum)]
101
+
102
+ return first_char + remaining_chars
103
+
104
+ def get_option_value(self, option_name):
105
+ if option_name == 'true':
106
+ return TriccStatic(True)
107
+ elif option_name == 'false':
108
+ return TriccStatic(False)
109
+ return self.concept_map.get(option_name, option_name)
110
+
111
+ def get_tricc_operation_expression(self, operation):
112
+ ref_expressions = []
113
+ if not hasattr(operation, "reference"):
114
+ return self.get_tricc_operation_operand(operation)
115
+ for r in operation.reference:
116
+ if isinstance(r, list):
117
+ r_expr = [
118
+ (
119
+ self.get_tricc_operation_expression(sr)
120
+ if isinstance(sr, TriccOperation)
121
+ else self.get_tricc_operation_operand(sr)
122
+ )
123
+ for sr in r
124
+ ]
125
+ elif isinstance(r, TriccOperation):
126
+ r_expr = self.get_tricc_operation_expression(r)
127
+ else:
128
+ r_expr = self.get_tricc_operation_operand(r)
129
+ if isinstance(r_expr, TriccReference):
130
+ r_expr = self.get_tricc_operation_operand(r_expr)
131
+ elif isinstance(r_expr, TriccStatic) and isinstance(r_expr.value, bool):
132
+ r_expr = str(r_expr.value).lower()
133
+ ref_expressions.append(r_expr)
134
+
135
+ if hasattr(self, f"tricc_operation_{operation.operator}"):
136
+ callable = getattr(self, f"tricc_operation_{operation.operator}")
137
+ return callable(ref_expressions)
138
+ else:
139
+ raise NotImplementedError(
140
+ f"This type of operation '{operation.operator}' is not supported in this strategy"
141
+ )
142
+
143
+ def execute(self):
144
+ version = datetime.datetime.now().strftime("%Y%m%d%H%M")
145
+ logger.info(f"build version: {version}")
146
+ if "main" in self.project.start_pages:
147
+ self.process_base(self.project.start_pages, pages=self.project.pages, version=version)
148
+ else:
149
+ logger.critical("Main process required")
150
+
151
+ logger.info("generate the relevance based on edges")
152
+ self.process_relevance(self.project.start_pages, pages=self.project.pages)
153
+
154
+ logger.info("generate the calculate based on edges")
155
+ self.process_calculate(self.project.start_pages, pages=self.project.pages)
156
+
157
+ logger.info("generate the export format")
158
+ self.process_export(self.project.start_pages, pages=self.project.pages)
159
+
160
+ logger.info("print the export")
161
+ self.export(self.project.start_pages, version=version)
162
+
163
+ def map_tricc_type_to_dhis2_value_type(self, node):
164
+ mapping = {
165
+ 'text': 'TEXT',
166
+ 'integer': 'INTEGER',
167
+ 'decimal': 'NUMBER',
168
+ 'date': 'DATE',
169
+ 'datetime': 'DATETIME',
170
+ 'select_one': 'TEXT', # DHIS2 handles options via optionSets
171
+ 'select_multiple': 'TEXT', # Multiple selections as comma-separated
172
+ 'select_yesno': 'BOOLEAN',
173
+ 'yesno': 'BOOLEAN',
174
+ 'boolean': 'BOOLEAN',
175
+ 'not_available': 'BOOLEAN',
176
+ 'note': 'LONG_TEXT'
177
+ }
178
+ return mapping.get(node.tricc_type, 'TEXT')
179
+
180
+ def generate_base(self, node, processed_nodes, **kwargs):
181
+ if generate_base(node, processed_nodes, **kwargs):
182
+ if getattr(node, 'name', '') not in ('true', 'false'):
183
+ self.concept_map[node.name] = self.generate_id(self.get_export_name(node))
184
+ return True
185
+ return False
186
+
187
+ def generate_relevance(self, node, processed_nodes, **kwargs):
188
+ if not is_ready_to_process(node, processed_nodes, strict=True):
189
+ return False
190
+
191
+ if node not in processed_nodes:
192
+ relevance = None
193
+ if hasattr(node, 'relevance') and node.relevance:
194
+ relevance = node.relevance
195
+ if hasattr(node, 'expression') and node.expression:
196
+ relevance = node.expression
197
+ if relevance:
198
+ relevance_str = self.convert_expression_to_string(not_clean(relevance))
199
+ if relevance_str and relevance_str != 'false':
200
+ # Create program rule action for hiding/showing based on relevance
201
+ rule_id = self.generate_id(f"rule_{node.get_name()}_relevance")
202
+ action_id = self.generate_id(f"action_{rule_id}")
203
+
204
+ if isinstance(node, TriccNodeActivity):
205
+ # For activities, use HIDESECTION action instead of HIDEFIELD
206
+ # Store activity reference for later section ID assignment
207
+ program_rule_action = {
208
+ "id": action_id,
209
+ "programRuleActionType": "HIDESECTION",
210
+ "activity_ref": node, # Temporary reference to be replaced with section ID
211
+ "programRule": {"id": rule_id},
212
+ }
213
+ else:
214
+ # For regular nodes, use HIDEFIELD action
215
+ program_rule_action = {
216
+ "id": action_id,
217
+ "programRuleActionType": "HIDEFIELD",
218
+ "dataElement": {
219
+ "id": self.generate_id(self.get_export_name(node))
220
+ },
221
+ "programRule": {"id": rule_id}
222
+ }
223
+ self.program_rule_actions.append(program_rule_action)
224
+
225
+ # Create program rule referencing the action
226
+ self.program_rules.append({
227
+ "id": rule_id,
228
+ "name": f"Hide {node.get_name()} when condition met",
229
+ "description": f"Hide {node.get_name()} based on relevance",
230
+ "condition": f"!({relevance_str})", # Negate for hide when true
231
+ "programRuleActions": [{"id": action_id}]
232
+ })
233
+ return True
234
+
235
+ def generate_data_element(self, node):
236
+ if issubclass(node.__class__, TriccNodeDisplayModel) and not isinstance(node, TriccNodeSelectOption):
237
+ de_id = self.generate_id(self.get_export_name(node))
238
+
239
+ # Check if this is a boolean question (yes/no with boolean options)
240
+ is_boolean_question = False
241
+ if hasattr(node, 'options') and node.options:
242
+ option_names = [
243
+ str(self.get_export_name(opt)).lower()
244
+ for opt in node.options.values()
245
+ if isinstance(opt, TriccNodeSelectOption)]
246
+ # If options are only true/false or yes/no variants, treat as boolean
247
+ boolean_options = {'true', 'false', 'yes', 'no', '1', '0'}
248
+ if all(opt in boolean_options for opt in option_names):
249
+ is_boolean_question = True
250
+
251
+ # Override valueType for boolean questions
252
+ value_type = self.map_tricc_type_to_dhis2_value_type(node)
253
+ if is_boolean_question:
254
+ value_type = "BOOLEAN"
255
+
256
+ data_element = {
257
+ "id": de_id,
258
+ "name": self.get_export_name(node),
259
+ "shortName": node.name[:50],
260
+ "displayFormName": getattr(node, 'label', node.name).replace('\u00a0', ' ').strip(),
261
+ "valueType": value_type,
262
+ "domainType": "TRACKER",
263
+ "aggregationType": "NONE"
264
+ }
265
+
266
+ # Only create optionSet for non-boolean select questions
267
+ if node.tricc_type in ['select_one', 'select_multiple'] and not is_boolean_question:
268
+ # Create optionSet for choices
269
+ if hasattr(node, 'options') and node.options:
270
+ option_set_id = self.generate_id(f"optionset_{node.name}")
271
+ data_element["optionSet"] = {"id": option_set_id}
272
+
273
+ # Create the actual optionSet definition
274
+ option_set = {
275
+ "id": option_set_id,
276
+ "name": f"{node.name} Options",
277
+ "shortName": f"{node.name}_opts"[:50],
278
+ "valueType": "TEXT",
279
+ "options": []
280
+ }
281
+
282
+ # Add options (node.options is a dict, not a list)
283
+ for key, option in node.options.items():
284
+ if isinstance(option, TriccNodeSelectOption):
285
+ option_id = self.generate_id(f"option_{node.name}_{option.name}")
286
+ option_name = self.get_export_name(option)
287
+ if isinstance(option_name, str):
288
+ option_name = option_name.replace('\u00a0', ' ').strip()
289
+ elif isinstance(option_name, TriccStatic):
290
+ option_name = str(option_name.value)
291
+ # Create separate option entity
292
+ option_def = {
293
+ "id": option_id,
294
+ "name": option_name,
295
+ "shortName": option.name[:50],
296
+ "code": str(self.get_export_name(option))
297
+ }
298
+ self.options[option_id] = option_def
299
+
300
+ # Add option reference to optionSet (only ID)
301
+ option_set["options"].append({"id": option_id})
302
+
303
+ self.option_sets[option_set_id] = option_set
304
+
305
+ self.data_elements[node.name] = data_element
306
+ return data_element
307
+ return None
308
+
309
+ def generate_calculate(self, node, processed_nodes, **kwargs):
310
+ if generate_calculate(node, processed_nodes, **kwargs):
311
+ if issubclass(node.__class__, TriccNodeCalculateBase) and node.expression:
312
+ # Create program rule variable for the calculate
313
+ var_id = self.generate_id(self.get_export_name(node))
314
+ expression_str = self.convert_expression_to_string(node.expression)
315
+
316
+ # Determine data type from operation
317
+ data_type = "TEXT" # default
318
+ if hasattr(node.expression, 'get_datatype'):
319
+ operation_datatype = node.expression.get_datatype()
320
+ if operation_datatype:
321
+ # Create a mock node with the datatype to use the mapping function
322
+ class MockNode:
323
+ def __init__(self, tricc_type):
324
+ self.tricc_type = tricc_type
325
+ mock_node = MockNode(operation_datatype)
326
+ data_type = self.map_tricc_type_to_dhis2_value_type(mock_node)
327
+
328
+ program_rule_variable = {
329
+ "id": var_id,
330
+ "name": self.get_export_name(node.name)[:50],
331
+ "programRuleVariableSourceType": "CALCULATED_VALUE",
332
+ "calculatedValueScript": expression_str,
333
+ "dataType": data_type,
334
+ "useCodeForOptionSet": False,
335
+ "program": {"id": self.program_metadata["id"]}
336
+ }
337
+ self.program_rule_variables.append(program_rule_variable)
338
+ # Add to concept map for potential referencing
339
+ self.concept_map[node.name] = var_id
340
+ return True
341
+ return False
342
+
343
+ def process_export(self, start_pages, **kwargs):
344
+ self.activity_export(start_pages["main"], **kwargs)
345
+
346
+ def activity_export(self, activity, processed_nodes=None, **kwargs):
347
+ if processed_nodes is None:
348
+ processed_nodes = OrderedSet()
349
+ stashed_nodes = OrderedSet()
350
+ groups = {}
351
+ groups[activity.id] = 0
352
+ path_len = 0
353
+ process = ["main"]
354
+
355
+ # Create program stage
356
+ stage_id = self.generate_id(self.get_export_name(activity))
357
+ program_stage = {
358
+ "id": stage_id,
359
+ "name": getattr(activity.root, 'label', 'Main Stage').replace('\u00a0', ' ').strip(),
360
+ "programStageDataElements": [],
361
+ "programStageSections": []
362
+ }
363
+ self.program_metadata["programStages"].append(program_stage)
364
+
365
+ # Start with the main section for this activity
366
+ self.start_section(activity, groups, processed_nodes, process, **kwargs)
367
+
368
+ walktrhough_tricc_node_processed_stached(
369
+ activity.root,
370
+ self.generate_export,
371
+ processed_nodes,
372
+ stashed_nodes,
373
+ path_len,
374
+ cur_group=activity.root.group,
375
+ process=process,
376
+ recursive=False,
377
+ **kwargs
378
+ )
379
+
380
+ # End the main section
381
+ self.end_section(activity, groups, **kwargs)
382
+
383
+ # Manage stashed nodes similar to other strategies
384
+ prev_stashed_nodes = stashed_nodes.copy()
385
+ loop_count = 0
386
+ len_prev_processed_nodes = 0
387
+ while len(stashed_nodes) > 0:
388
+ loop_count = check_stashed_loop(
389
+ stashed_nodes,
390
+ prev_stashed_nodes,
391
+ processed_nodes,
392
+ len_prev_processed_nodes,
393
+ loop_count,
394
+ )
395
+ prev_stashed_nodes = stashed_nodes.copy()
396
+ len_prev_processed_nodes = len(processed_nodes)
397
+ if len(stashed_nodes) > 0:
398
+ s_node = stashed_nodes.pop()
399
+ if s_node.group is None:
400
+ logger.critical("ERROR group is none for node {}".format(s_node.get_name()))
401
+
402
+ # Start section for stashed node if it's a different group
403
+ self.start_section(s_node.group, groups, processed_nodes, process, relevance=True, **kwargs)
404
+
405
+ walktrhough_tricc_node_processed_stached(
406
+ s_node,
407
+ self.generate_export,
408
+ processed_nodes,
409
+ stashed_nodes,
410
+ path_len,
411
+ groups=groups,
412
+ cur_group=s_node.group,
413
+ recursive=False,
414
+ process=process,
415
+ **kwargs
416
+ )
417
+
418
+ # End section for stashed node
419
+ self.end_section(s_node.group, groups, **kwargs)
420
+
421
+ return processed_nodes
422
+
423
+ def start_section(self, cur_group, groups, processed_nodes, process, relevance=False, **kwargs):
424
+ name = get_export_name(cur_group)
425
+
426
+ if name in groups:
427
+ groups[name] += 1
428
+ name = name + "_" + str(groups[name])
429
+ else:
430
+ groups[name] = 0
431
+
432
+ relevance_expression = (
433
+ cur_group.relevance if (
434
+ relevance and
435
+ cur_group.relevance is not None and
436
+ cur_group.relevance != ""
437
+ ) else ""
438
+ )
439
+
440
+ if not relevance:
441
+ relevance_expression = ""
442
+ elif isinstance(relevance_expression, (TriccOperation, TriccStatic)):
443
+ relevance_expression = self.get_tricc_operation_expression(relevance_expression)
444
+
445
+ # Create section
446
+ section_id = self.generate_id(f"section_{name}")
447
+ section_name = name
448
+ if cur_group and hasattr(cur_group, 'label') and cur_group.label:
449
+ section_name = cur_group.label.replace('\u00a0', ' ').strip()
450
+ section = {
451
+ "id": section_id,
452
+ "name": section_name,
453
+ "sortOrder": len(self.sections),
454
+ "programStage": {"id": self.program_metadata["programStages"][-1]["id"]},
455
+ "dataElements": [],
456
+ "activity_ref": cur_group
457
+ }
458
+ # Add section to program stage
459
+ if self.program_metadata["programStages"]:
460
+ self.program_metadata["programStages"][-1]["programStageSections"].append({"id": section_id})
461
+
462
+ self.sections[section_id] = section
463
+ self.current_section = section_id
464
+
465
+ def end_section(self, cur_group, groups, **kwargs):
466
+ # In DHIS2, sections don't have explicit end markers like XLSForm groups
467
+ # The section is already created and added to the program stage
468
+ pass
469
+
470
+ def generate_export(self, node, processed_nodes, **kwargs):
471
+ if not is_ready_to_process(node, processed_nodes, strict=True):
472
+ return False
473
+
474
+ if not process_reference(
475
+ node, processed_nodes, {}, replace_reference=False, codesystems=kwargs.get("codesystems", None)
476
+ ):
477
+ return False
478
+
479
+ if node not in processed_nodes:
480
+ # Skip creating data elements for calculate nodes - they should only be program rule variables
481
+ if not issubclass(node.__class__, TriccNodeCalculateBase):
482
+ data_element = self.generate_data_element(node)
483
+ if data_element:
484
+ # Add to program stage
485
+ if self.program_metadata["programStages"]:
486
+ psde_id = self.generate_id(f"psde_{node.name}")
487
+ psde = {
488
+ "id": psde_id,
489
+ "dataElement": {"id": data_element["id"]},
490
+ "compulsory": bool(getattr(node, 'required', False))
491
+ }
492
+ self.program_metadata["programStages"][-1]["programStageDataElements"].append(psde)
493
+
494
+ # Add data element to current section
495
+ if self.current_section and self.current_section in self.sections:
496
+ self.sections[self.current_section]["dataElements"].append({"id": data_element["id"]})
497
+
498
+ return True
499
+
500
+ def clean_section(self, program_stages_payload):
501
+ """Clean sections by removing empty ones and merging sections with same activity_ref"""
502
+ sections_to_remove = set()
503
+ prev_activity_ref = None
504
+ prev_section_id = None
505
+
506
+ for section in sorted(self.sections.values(), key=lambda x: x["sortOrder"]):
507
+ section_id = section["id"]
508
+ activity_ref = section.get("activity_ref")
509
+ # Remove empty sections
510
+ if not section.get("dataElements"):
511
+ sections_to_remove.add(section_id)
512
+
513
+ # Check for sections with same activity_ref
514
+ elif activity_ref == prev_activity_ref:
515
+ # Merge this section into the existing one
516
+ existing_section = self.sections[prev_section_id]
517
+
518
+ # Move data elements to existing section
519
+ existing_section["dataElements"].extend(section["dataElements"])
520
+
521
+ # Mark this section for removal
522
+ sections_to_remove.add(section_id)
523
+ else:
524
+ prev_activity_ref = activity_ref
525
+ prev_section_id = section_id
526
+
527
+ # Remove sections that should be removed
528
+ for section_id in sections_to_remove:
529
+ if section_id in self.sections:
530
+ del self.sections[section_id]
531
+
532
+ # Update stage sections to remove deleted sections
533
+ for stage in program_stages_payload:
534
+ stage["programStageSections"][:] = [
535
+ s for s in stage["programStageSections"]
536
+ if s["id"] not in sections_to_remove
537
+ ]
538
+
539
+ def export(self, start_pages, version):
540
+ form_id = start_pages["main"].root.form_id or "dhis2_program"
541
+ base_path = os.path.join(self.output_path, form_id)
542
+ if not os.path.exists(base_path):
543
+ os.makedirs(base_path)
544
+
545
+ # Prepare collections for all entities
546
+ program_rules_payload = []
547
+ program_rule_actions_payload = []
548
+ program_stages_payload = []
549
+ program_rule_variables_payload = []
550
+
551
+ if self.program_metadata["programStages"]:
552
+ # Extract full stage definitions
553
+ program_stages_payload = [
554
+ {
555
+ **stage,
556
+ "program": {"id": self.program_metadata["id"]}
557
+ }
558
+ for stage in self.program_metadata["programStages"]
559
+ ]
560
+ # Clean sections before processing actions to ensure only valid sections are used
561
+ self.clean_section(program_stages_payload)
562
+ # In program, only keep stage ID references
563
+ self.program_metadata["programStages"] = [
564
+ {"id": stage["id"]}
565
+ for stage in program_stages_payload
566
+ ]
567
+ else:
568
+ program_stages_payload = []
569
+
570
+ if self.program_rule_actions:
571
+ # Resolve activity references to section IDs for HIDESECTION actions
572
+ program_rule_actions_payload = []
573
+ for action in self.program_rule_actions:
574
+ if action.get("activity_ref"):
575
+ # Find all sections for this activity (after cleaning)
576
+ activity = action["activity_ref"]
577
+ matching_sections = [
578
+ sec_id for sec_id, section in self.sections.items()
579
+ if section.get("activity_ref") == activity
580
+ ]
581
+
582
+ # Create one action per matching section
583
+ for i, section_id in enumerate(matching_sections):
584
+ action_copy = dict(action)
585
+ action_copy["programStageSection"] = {"id": section_id}
586
+ del action_copy["activity_ref"]
587
+
588
+ if i > 0:
589
+ # For additional sections, create new IDs for action and corresponding rule
590
+ original_rule_id = action["programRule"]["id"]
591
+ new_rule_id = self.generate_id(f"{original_rule_id}_section_{i}")
592
+ new_action_id = self.generate_id(f"{action['id']}_section_{i}")
593
+
594
+ action_copy["id"] = new_action_id
595
+ action_copy["programRule"] = {"id": new_rule_id}
596
+
597
+ # Create duplicate rule with new ID
598
+ original_rule = next(
599
+ (r for r in self.program_rules if r["id"] == original_rule_id), None
600
+ )
601
+ if original_rule:
602
+ new_rule = dict(original_rule)
603
+ new_rule["id"] = new_rule_id
604
+ new_rule["name"] = f"{original_rule['name']} (Section {i})"
605
+ new_rule["programRuleActions"] = [{"id": new_action_id}]
606
+ self.program_rules.append(new_rule)
607
+
608
+ program_rule_actions_payload.append(action_copy)
609
+ else:
610
+ # Non-activity actions (HIDEFIELD) can be added directly
611
+ program_rule_actions_payload.append(action)
612
+
613
+ if self.program_rules:
614
+ program_rules_payload = [
615
+ {
616
+ **rule,
617
+ "program": {"id": self.program_metadata["id"]}
618
+ }
619
+ for rule in self.program_rules
620
+ ]
621
+
622
+ if self.program_rule_variables:
623
+ program_rule_variables_payload = self.program_rule_variables
624
+
625
+ # Build the program with references to other entities
626
+ program_payload = dict(self.program_metadata)
627
+ if program_rule_variables_payload:
628
+ program_payload["programRuleVariables"] = [
629
+ {"id": var["id"]}
630
+ for var in program_rule_variables_payload
631
+ ]
632
+ if program_rules_payload:
633
+ program_payload["programRules"] = [
634
+ {"id": rule["id"]}
635
+ for rule in program_rules_payload
636
+ ]
637
+
638
+ # Create single comprehensive payload with all entities at root level
639
+ full_payload = {
640
+ "programs": [program_payload]
641
+ }
642
+
643
+ if program_stages_payload:
644
+ full_payload["programStages"] = program_stages_payload
645
+ if program_rules_payload:
646
+ full_payload["programRules"] = program_rules_payload
647
+ if program_rule_actions_payload:
648
+ full_payload["programRuleActions"] = program_rule_actions_payload
649
+ if program_rule_variables_payload:
650
+ full_payload["programRuleVariables"] = program_rule_variables_payload
651
+ if self.data_elements:
652
+ full_payload["dataElements"] = list(self.data_elements.values())
653
+ if self.options:
654
+ full_payload["options"] = list(self.options.values())
655
+ if self.option_sets:
656
+ full_payload["optionSets"] = list(self.option_sets.values())
657
+ if self.sections:
658
+ # Remove activity_ref from sections before serialization
659
+ sections_payload = []
660
+ for section in self.sections.values():
661
+ section_copy = dict(section)
662
+ if "activity_ref" in section_copy:
663
+ del section_copy["activity_ref"]
664
+ sections_payload.append(section_copy)
665
+ full_payload["programStageSections"] = sections_payload
666
+
667
+ # Export everything to a single file
668
+ metadata_file = os.path.join(base_path, f"{form_id}_metadata.json")
669
+ with open(metadata_file, 'w') as f:
670
+ json.dump(full_payload, f, indent=2)
671
+ logger.info(f"Exported complete DHIS2 metadata to {metadata_file}")
672
+
673
+ def get_tricc_operation_operand(self, r):
674
+ if isinstance(r, TriccOperation):
675
+ return self.get_tricc_operation_expression(r)
676
+ elif isinstance(r, TriccReference):
677
+ # Use DHIS2 ID from concept_map instead of name
678
+ node_id = self.concept_map.get(r.value.name, self.get_export_name(r.value))
679
+ return f"#{{{node_id}}}"
680
+ elif isinstance(r, TriccStatic):
681
+ if isinstance(r.value, bool):
682
+ return str(r.value).lower()
683
+ if isinstance(r.value, str):
684
+ return f"'{r.value}'"
685
+ else:
686
+ return str(r.value)
687
+ elif isinstance(r, bool):
688
+ return str(r).lower()
689
+ elif isinstance(r, str):
690
+ return f"{r}"
691
+ elif isinstance(r, (int, float)):
692
+ return str(r)
693
+ elif isinstance(r, TriccNodeSelectOption):
694
+ option = self.get_option_value(r.name)
695
+ if r.name in ('true', 'false'):
696
+ return option
697
+ return f"'{option}'"
698
+ elif issubclass(r.__class__, TriccNodeDisplayCalculateBase):
699
+ # Use DHIS2 ID from concept_map instead of name
700
+ node_id = self.concept_map.get(r.name, self.get_export_name(r))
701
+ return f"#{node_id}"
702
+ elif issubclass(r.__class__, TriccNodeInputModel):
703
+ # Use DHIS2 ID from concept_map instead of name
704
+ node_id = self.concept_map.get(r.name, self.get_export_name(r))
705
+ return f"#{{{node_id}}}"
706
+ elif issubclass(r.__class__, TriccNodeBaseModel):
707
+ # Use DHIS2 ID from concept_map instead of name
708
+ node_id = self.concept_map.get(r.name, self.get_export_name(r))
709
+ return f"#{{{node_id}}}"
710
+ else:
711
+ raise NotImplementedError(f"This type of node {r.__class__.__name__} is not supported within an operation")
712
+
713
+ def convert_expression_to_string(self, expression):
714
+ if isinstance(expression, TriccOperation):
715
+ return self.get_tricc_operation_expression(expression)
716
+ else:
717
+ return self.get_tricc_operation_operand(expression)
718
+
719
+ # Operation methods for DHIS2 expressions
720
+ def tricc_operation_equal(self, ref_expressions):
721
+ return f"{ref_expressions[0]} == {ref_expressions[1]}"
722
+
723
+ def tricc_operation_not_equal(self, ref_expressions):
724
+ return f"{ref_expressions[0]} != {ref_expressions[1]}"
725
+
726
+ def tricc_operation_and(self, ref_expressions):
727
+ if len(ref_expressions) == 1:
728
+ return ref_expressions[0]
729
+ if len(ref_expressions) > 1:
730
+ return " && ".join(ref_expressions)
731
+ else:
732
+ return "true"
733
+
734
+ def tricc_operation_or(self, ref_expressions):
735
+ if len(ref_expressions) == 1:
736
+ return ref_expressions[0]
737
+ if len(ref_expressions) > 1:
738
+ return "(" + " || ".join(ref_expressions) + ")"
739
+ else:
740
+ return "true"
741
+
742
+ def tricc_operation_not(self, ref_expressions):
743
+ return f"!({ref_expressions[0]})"
744
+
745
+ def tricc_operation_plus(self, ref_expressions):
746
+ return " + ".join(ref_expressions)
747
+
748
+ def tricc_operation_minus(self, ref_expressions):
749
+ if len(ref_expressions) > 1:
750
+ return " - ".join(map(str, ref_expressions))
751
+ elif len(ref_expressions) == 1:
752
+ return f"-{ref_expressions[0]}"
753
+
754
+ def tricc_operation_more(self, ref_expressions):
755
+ return f"{ref_expressions[0]} > {ref_expressions[1]}"
756
+
757
+ def tricc_operation_less(self, ref_expressions):
758
+ return f"{ref_expressions[0]} < {ref_expressions[1]}"
759
+
760
+ def tricc_operation_more_or_equal(self, ref_expressions):
761
+ return f"{ref_expressions[0]} >= {ref_expressions[1]}"
762
+
763
+ def tricc_operation_less_or_equal(self, ref_expressions):
764
+ return f"{ref_expressions[0]} <= {ref_expressions[1]}"
765
+
766
+ def tricc_operation_selected(self, ref_expressions):
767
+ # For DHIS2, check if value is selected in multi-select
768
+ return f"d2:hasValue({ref_expressions[0]}) && d2:contains({ref_expressions[0]}, {ref_expressions[1]})"
769
+
770
+ def tricc_operation_count(self, ref_expressions):
771
+ return f"d2:count({ref_expressions[0]})"
772
+
773
+ def tricc_operation_multiplied(self, ref_expressions):
774
+ return "*".join(ref_expressions)
775
+
776
+ def tricc_operation_divided(self, ref_expressions):
777
+ return f"{ref_expressions[0]} / {ref_expressions[1]}"
778
+
779
+ def tricc_operation_modulo(self, ref_expressions):
780
+ return f"{ref_expressions[0]} % {ref_expressions[1]}"
781
+
782
+ def tricc_operation_coalesce(self, ref_expressions):
783
+ return f"d2:coalesce({','.join(ref_expressions)})"
784
+
785
+ def tricc_operation_native(self, ref_expressions):
786
+ if len(ref_expressions) > 0:
787
+ return f"{ref_expressions[0]}({','.join(ref_expressions[1:])})"
788
+
789
+ def tricc_operation_istrue(self, ref_expressions):
790
+ return f"{ref_expressions[0]} == true"
791
+
792
+ def tricc_operation_isfalse(self, ref_expressions):
793
+ return f"{ref_expressions[0]} == false"
794
+
795
+ def tricc_operation_parenthesis(self, ref_expressions):
796
+ return f"({ref_expressions[0]})"
797
+
798
+ def tricc_operation_between(self, ref_expressions):
799
+ return f"{ref_expressions[0]} >= {ref_expressions[1]} && {ref_expressions[0]} < {ref_expressions[2]}"
800
+
801
+ def tricc_operation_isnull(self, ref_expressions):
802
+ return f"!d2:hasValue({ref_expressions[0]})"
803
+
804
+ def tricc_operation_isnotnull(self, ref_expressions):
805
+ return f"d2:hasValue({ref_expressions[0]})"
806
+
807
+ def tricc_operation_isnottrue(self, ref_expressions):
808
+ return f"{ref_expressions[0]} != true"
809
+
810
+ def tricc_operation_isnotfalse(self, ref_expressions):
811
+ return f"{ref_expressions[0]} != false"
812
+
813
+ def tricc_operation_notexist(self, ref_expressions):
814
+ return f"!d2:hasValue({ref_expressions[0]})"
815
+
816
+ def tricc_operation_case(self, ref_expressions):
817
+ # Simplified case handling
818
+ parts = []
819
+ for i in range(0, len(ref_expressions), 2):
820
+ if i + 1 < len(ref_expressions):
821
+ parts.append(f"if({ref_expressions[i]}, {ref_expressions[i+1]})")
822
+ return " || ".join(parts)
823
+
824
+ def tricc_operation_ifs(self, ref_expressions):
825
+ return self.tricc_operation_case(ref_expressions[1:])
826
+
827
+ def tricc_operation_if(self, ref_expressions):
828
+ return f"if({ref_expressions[0]}, {ref_expressions[1]}, {ref_expressions[2]})"
829
+
830
+ def tricc_operation_contains(self, ref_expressions):
831
+ return f"d2:contains({ref_expressions[0]}, {ref_expressions[1]})"
832
+
833
+ def tricc_operation_exists(self, ref_expressions):
834
+ parts = []
835
+ for ref in ref_expressions:
836
+ parts.append(f"d2:hasValue({ref})")
837
+ return " && ".join(parts)
838
+
839
+ def tricc_operation_cast_number(self, ref_expressions):
840
+ return f"d2:toNumber({ref_expressions[0]})"
841
+
842
+ def tricc_operation_cast_integer(self, ref_expressions):
843
+ return f"d2:toNumber({ref_expressions[0]})"
844
+
845
+ def tricc_operation_zscore(self, ref_expressions):
846
+ # Placeholder - would need specific implementation
847
+ return f"zscore({','.join(ref_expressions)})"
848
+
849
+ def tricc_operation_datetime_to_decimal(self, ref_expressions):
850
+ return f"d2:daysBetween({ref_expressions[0]}, '1970-01-01')"
851
+
852
+ def tricc_operation_round(self, ref_expressions):
853
+ return f"d2:round({ref_expressions[0]})"
854
+
855
+ def tricc_operation_izscore(self, ref_expressions):
856
+ return f"izscore({','.join(ref_expressions)})"
857
+
858
+ def tricc_operation_concatenate(self, ref_expressions):
859
+ return f"d2:concatenate({','.join(ref_expressions)})"
@@ -21,6 +21,7 @@ from tricc_oo.models.tricc import (
21
21
  TriccNodeSelectOption,
22
22
  TriccNodeInputModel,
23
23
  TriccNodeBaseModel,
24
+ TriccNodeSelect,
24
25
  TriccNodeDisplayModel,
25
26
  )
26
27
 
@@ -69,6 +70,9 @@ class OpenMRSStrategy(BaseOutPutStrategy):
69
70
  self.current_segment = None
70
71
  self.current_activity = None
71
72
  self.concept_map = {}
73
+ self.calculated_fields = [] # Store calculated fields to add to first section of each page
74
+ self.calculated_fields_added = set() # Track which pages have had calculated fields added
75
+ self.inject_version()
72
76
 
73
77
  def get_export_name(self, r):
74
78
  if isinstance(r, TriccNodeSelectOption):
@@ -146,6 +150,9 @@ class OpenMRSStrategy(BaseOutPutStrategy):
146
150
  logger.info("generate the export format")
147
151
  self.process_export(self.project.start_pages, pages=self.project.pages)
148
152
 
153
+ logger.info("create calculation page")
154
+ self.create_calculation_page()
155
+
149
156
  logger.info("print the export")
150
157
  self.export(self.project.start_pages, version=version)
151
158
 
@@ -160,7 +167,7 @@ class OpenMRSStrategy(BaseOutPutStrategy):
160
167
  'select_multiple': 'multiCheckbox',
161
168
  'select_yesno': 'select',
162
169
  'not_available': 'checkbox',
163
- 'note': 'text'
170
+ 'note': 'markdown'
164
171
  }
165
172
 
166
173
  # if issubclass(node.__class__, TriccNodeSelectYesNo):
@@ -250,7 +257,9 @@ class OpenMRSStrategy(BaseOutPutStrategy):
250
257
  }
251
258
  }
252
259
  }
253
- return question
260
+ # Collect calculated fields to add to first section of each page
261
+ self.calculated_fields.append(question)
262
+ return None # Don't return the question, it will be added to first section
254
263
  return None
255
264
 
256
265
  def generate_calculate(self, node, processed_nodes, **kwargs):
@@ -400,6 +409,8 @@ class OpenMRSStrategy(BaseOutPutStrategy):
400
409
  return f"'{option}'"
401
410
  elif issubclass(r.__class__, TriccNodeInputModel):
402
411
  return self.get_export_name(r)
412
+ elif issubclass(r.__class__, TriccNodeSelect):
413
+ return "(" + self.get_export_name(r) + " ?? [])"
403
414
  elif issubclass(r.__class__, TriccNodeBaseModel):
404
415
  return self.get_export_name(r)
405
416
  else:
@@ -447,7 +458,7 @@ class OpenMRSStrategy(BaseOutPutStrategy):
447
458
  return f"!({ref_expressions[0]})"
448
459
 
449
460
  def tricc_operation_plus(self, ref_expressions):
450
- return " + ".join(ref_expressions)
461
+ return "(" + " + ".join(ref_expressions) +")"
451
462
 
452
463
  def tricc_operation_minus(self, ref_expressions):
453
464
  if len(ref_expressions) > 1:
@@ -472,7 +483,7 @@ class OpenMRSStrategy(BaseOutPutStrategy):
472
483
  return f"({ref_expressions[0]}.includes({ref_expressions[1]}))"
473
484
 
474
485
  def tricc_operation_count(self, ref_expressions):
475
- return f"({ref_expressions[0]}.length || 0)"
486
+ return f"{ref_expressions[0]}.length"
476
487
 
477
488
  def tricc_operation_multiplied(self, ref_expressions):
478
489
  return "*".join(ref_expressions)
@@ -638,6 +649,7 @@ class OpenMRSStrategy(BaseOutPutStrategy):
638
649
  "label": section_label,
639
650
  "questions": []
640
651
  })
652
+
641
653
  if group_node.relevance:
642
654
  relevance_str = self.convert_expression_to_string(not_clean(group_node.relevance))
643
655
  if relevance_str and relevance_str != 'false':
@@ -645,3 +657,38 @@ class OpenMRSStrategy(BaseOutPutStrategy):
645
657
  "hideWhenExpression": f"{relevance_str}"
646
658
  }
647
659
  logger.debug(f"Started section: {section_label}")
660
+
661
+ def create_calculation_page(self):
662
+ """Create a dedicated page for all calculated fields"""
663
+ if self.calculated_fields:
664
+ self.clean_sections()
665
+ self.clean_pages()
666
+ page = {
667
+ "label": "Calculations",
668
+ "sections": [
669
+ {
670
+ "label": "Calculations",
671
+ "questions": self.calculated_fields
672
+ }
673
+ ]
674
+ }
675
+ self.form_data["pages"].append(page)
676
+ logger.debug("Created calculation page")
677
+
678
+ def inject_version(self):
679
+ # Add hidden version field using version() function
680
+ question = {
681
+ "id": "version",
682
+ "type": "control",
683
+ "label": "",
684
+ "hide": {
685
+ "hideWhenExpression": "true"
686
+ },
687
+ "questionOptions": {
688
+ "calculate": {
689
+ "calculateExpression": "version()"
690
+ }
691
+ }
692
+ }
693
+ # Collect calculated fields to add to first section of each page
694
+ self.calculated_fields.append(question)
@@ -28,6 +28,7 @@ from tricc_oo.models.calculate import (
28
28
  TriccNodeActivityEnd,
29
29
  TriccNodeActivityStart,
30
30
  TriccNodeEnd,
31
+ TriccNodeDiagnosis,
31
32
  get_node_from_id,
32
33
 
33
34
  )
@@ -120,27 +121,13 @@ def get_last_version(name, processed_nodes, _list=None):
120
121
  # node is the node to calculate
121
122
  # processed_nodes are the list of processed nodes
122
123
  def get_node_expressions(node, processed_nodes, process=None):
123
- get_overall_exp = issubclass(node.__class__, TriccNodeCalculateBase) and not issubclass(
124
- node.__class__, (TriccNodeDisplayBridge, TriccNodeBridge)
125
- )
124
+ get_overall_exp = issubclass(node.__class__, (TriccNodeDisplayCalculateBase, TriccNodeProposedDiagnosis, TriccNodeDiagnosis)) and not isinstance(node, (TriccNodeDisplayBridge))
126
125
  expression = None
127
126
  # in case of recursive call processed_nodes will be None
128
127
  if processed_nodes is None or is_ready_to_process(node, processed_nodes=processed_nodes):
129
128
  expression = get_node_expression(
130
129
  node, processed_nodes=processed_nodes, get_overall_exp=get_overall_exp, process=process
131
130
  )
132
-
133
- # if get_overall_exp:
134
- # if expression and (not isinstance(expression, str) or expression != '')
135
- # and expression is not TriccStatic(True) :
136
- # num_expression = TriccOperation(
137
- # TriccOperator.CAST_NUMBER,
138
- # [expression]
139
- # )
140
- # elif expression is TriccStatic(True) or (not expression and get_overall_exp):
141
- # expression = TriccStatic(True)
142
- # else:
143
- # expression = None
144
131
  if (
145
132
  issubclass(node.__class__, TriccNodeCalculateBase)
146
133
  and not isinstance(expression, (TriccStatic, TriccReference, TriccOperation))
@@ -1564,7 +1551,12 @@ def set_prev_next_node(source_node, target_node, replaced_node=None, edge_only=F
1564
1551
  set_next_node(source_node, target_node, replaced_node, edge_only)
1565
1552
 
1566
1553
  if activity and not any([(e.source == source_id) and (e.target == target_id) for e in activity.edges]):
1567
- label = "continue" if issubclass(source_node.__class__, TriccNodeSelect) else None
1554
+ if issubclass(source_node.__class__, TriccNodeSelect):
1555
+ label = "continue"
1556
+ elif isinstance(source_node, TriccNodeRhombus):
1557
+ label = "yes"
1558
+ else:
1559
+ label = None
1568
1560
  activity.edges.append(TriccEdge(id=generate_id(), source=source_id, target=target_id, value=label))
1569
1561
 
1570
1562
 
@@ -1848,6 +1840,16 @@ def get_node_expression(in_node, processed_nodes, get_overall_exp=False, is_prev
1848
1840
  logger.critical(f"Rhombus without expression {node.get_name()}")
1849
1841
  elif is_prev and issubclass(node.__class__, TriccNodeDisplayCalculateBase):
1850
1842
  expression = TriccOperation(TriccOperator.ISTRUE, [node])
1843
+ prev_exp_overall = get_node_expression(
1844
+ node,
1845
+ processed_nodes=processed_nodes,
1846
+ get_overall_exp=False,
1847
+ is_prev=False,
1848
+ process=process,
1849
+ negate=negate
1850
+ )
1851
+ if prev_exp_overall in [TriccStatic(True), TriccStatic(False)]:
1852
+ expression = prev_exp_overall
1851
1853
  elif hasattr(node, "expression_reference") and isinstance(node.expression_reference, TriccOperation):
1852
1854
  # if issubclass(node.__class__, TriccNodeDisplayCalculateBase):
1853
1855
  # expression = TriccOperation(
@@ -2141,7 +2143,7 @@ def get_prev_node_expression(node, processed_nodes, get_overall_exp=False, exclu
2141
2143
  processed_nodes=processed_nodes,
2142
2144
  get_overall_exp=get_overall_exp,
2143
2145
  is_prev=True,
2144
- process=process,
2146
+ process=get_overall_exp,
2145
2147
  )
2146
2148
  if isinstance(node, TriccNodeActivity) or get_overall_exp:
2147
2149
  add_sub_expression(act_expression_inputs, sub)
@@ -2150,11 +2152,12 @@ def get_prev_node_expression(node, processed_nodes, get_overall_exp=False, exclu
2150
2152
 
2151
2153
  if act_expression_inputs:
2152
2154
  act_sub = or_join(act_expression_inputs)
2155
+ # if there is condition fallback on the calling activity condition
2153
2156
  if act_sub == TriccStatic(True):
2154
2157
  act_sub = get_node_expression(
2155
2158
  prev_node.activity,
2156
2159
  processed_nodes=processed_nodes,
2157
- get_overall_exp=True,
2160
+ get_overall_exp=get_overall_exp,
2158
2161
  is_prev=True,
2159
2162
  negate=False,
2160
2163
  process=process,
@@ -2241,7 +2244,7 @@ def get_count_terms_details(prev_node, processed_nodes, get_overall_exp, negate=
2241
2244
  get_node_expression(
2242
2245
  prev_node,
2243
2246
  processed_nodes=processed_nodes,
2244
- get_overall_exp=True,
2247
+ get_overall_exp=get_overall_exp,
2245
2248
  is_prev=True,
2246
2249
  process=process,
2247
2250
  )
@@ -2258,7 +2261,7 @@ def get_count_terms_details(prev_node, processed_nodes, get_overall_exp, negate=
2258
2261
  TriccOperator.CAST_NUMBER,
2259
2262
  [
2260
2263
  get_node_expression(
2261
- prev_node, processed_nodes=processed_nodes, get_overall_exp=True, is_prev=True, process=process
2264
+ prev_node, processed_nodes=processed_nodes, get_overall_exp=get_overall_exp, is_prev=True, process=process
2262
2265
  )
2263
2266
  ],
2264
2267
  )
@@ -2279,7 +2282,7 @@ def get_add_terms(node, processed_nodes, get_overall_exp=False, negate=False, pr
2279
2282
  get_node_expression(
2280
2283
  prev_node,
2281
2284
  processed_nodes=processed_nodes,
2282
- get_overall_exp=True,
2285
+ get_overall_exp=get_overall_exp,
2283
2286
  is_prev=True,
2284
2287
  process=process,
2285
2288
  )
@@ -2352,7 +2355,11 @@ def get_rhombus_terms(node, processed_nodes, get_overall_exp=False, negate=False
2352
2355
  TriccOperator.CAST_NUMBER,
2353
2356
  [
2354
2357
  get_node_expression(
2355
- expression, processed_nodes=processed_nodes, get_overall_exp=True, is_prev=True, process=process
2358
+ expression,
2359
+ processed_nodes=processed_nodes,
2360
+ get_overall_exp=get_overall_exp,
2361
+ is_prev=True,
2362
+ process=process
2356
2363
  )
2357
2364
  ],
2358
2365
  )
@@ -2390,7 +2397,7 @@ def get_calculation_terms(node, processed_nodes, get_overall_exp=False, negate=F
2390
2397
  return get_count_terms(node, False, negate, process=process)
2391
2398
  elif isinstance(node, TriccNodeRhombus):
2392
2399
  return get_rhombus_terms(
2393
- node, processed_nodes=processed_nodes, get_overall_exp=False, negate=negate, process=process
2400
+ node, processed_nodes=processed_nodes, get_overall_exp=get_overall_exp, negate=negate, process=process
2394
2401
  )
2395
2402
  elif isinstance(node, (TriccNodeWait)):
2396
2403
  # just use to force order of question
@@ -2407,7 +2414,6 @@ def get_calculation_terms(node, processed_nodes, get_overall_exp=False, negate=F
2407
2414
  )
2408
2415
  elif isinstance(node, (TriccNodeActivityStart, TriccNodeActivityEnd)):
2409
2416
  # the group have the relevance for the activity, not needed to replicate it
2410
- # return get_prev_node_expression(node.activity, processed_nodes, get_overall_exp=False, excluded_name=None)
2411
2417
  expression = None
2412
2418
  elif isinstance(node, TriccNodeExclusive):
2413
2419
  if len(node.prev_nodes) == 1:
@@ -2420,7 +2426,7 @@ def get_calculation_terms(node, processed_nodes, get_overall_exp=False, negate=F
2420
2426
  return get_node_expression(
2421
2427
  node_to_negate,
2422
2428
  processed_nodes=processed_nodes,
2423
- get_overall_exp=True,
2429
+ get_overall_exp=get_overall_exp,
2424
2430
  is_prev=True,
2425
2431
  negate=True,
2426
2432
  process=process,
@@ -2429,7 +2435,7 @@ def get_calculation_terms(node, processed_nodes, get_overall_exp=False, negate=F
2429
2435
  return get_node_expression(
2430
2436
  node_to_negate,
2431
2437
  processed_nodes=processed_nodes,
2432
- get_overall_exp=True,
2438
+ get_overall_exp=get_overall_exp,
2433
2439
  is_prev=True,
2434
2440
  negate=True,
2435
2441
  process=process,
@@ -2616,13 +2622,17 @@ def generate_base(node, processed_nodes, **kwargs):
2616
2622
  node.min = float(node.min)
2617
2623
  if int(node.min) == node.min:
2618
2624
  node.min = int(node.min)
2619
- constraints.append(TriccOperation(TriccOperator.MORE_OR_EQUAL, ["$this", TriccStatic(node.min)]))
2625
+ constraints.append(
2626
+ TriccOperation(TriccOperator.MORE_OR_EQUAL, ["$this", TriccStatic(node.min)])
2627
+ )
2620
2628
  constraints_min = "The minimun value is {0}.".format(node.min)
2621
2629
  if node.max is not None and node.max != "":
2622
2630
  node.max = float(node.max)
2623
2631
  if int(node.max) == node.max:
2624
2632
  node.max = int(node.max)
2625
- constraints.append(TriccOperation(TriccOperator.LESS_OR_EQUAL, ["$this", TriccStatic(node.max)]))
2633
+ constraints.append(
2634
+ TriccOperation(TriccOperator.LESS_OR_EQUAL, ["$this", TriccStatic(node.max)])
2635
+ )
2626
2636
  constraints_max = "The maximum value is {0}.".format(node.max)
2627
2637
  if len(constraints) > 1:
2628
2638
  node.constraint = TriccOperation(TriccOperator.AND, constraints)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tricc-oo
3
- Version: 1.5.28
3
+ Version: 1.6.0
4
4
  Summary: Python library that converts CDSS L2 in L3
5
5
  Project-URL: Homepage, https://github.com/SwissTPH/tricc
6
6
  Project-URL: Issues, https://github.com/SwissTPH/tricc/issues
@@ -1,4 +1,4 @@
1
- tests/build.py,sha256=rAYR6cSsNtQLH5vvrh_FNSBwWh_AynF3jG28jC-tiGs,6670
1
+ tests/build.py,sha256=Qbxvjkj_Wk2nQ-WjaMGiE1FIe3SRmJMRIgeoMoxqlfQ,6748
2
2
  tests/test_cql.py,sha256=dAsLMqVaS6qxnq62fg5KqTFu6UG6pHO6Ab3NZ1c9T3Y,5248
3
3
  tests/to_ocl.py,sha256=4e-i65K3UM6wHgdVcrZcM9AyL1bahIsXJiZTXhhHgQk,2048
4
4
  tricc_oo/__init__.py,sha256=oWCE1ubmC_6iqaWOMgTei4eXVQgV202Ia-tXS1NnW_4,139
@@ -7,15 +7,15 @@ tricc_oo/converters/codesystem_to_ocl.py,sha256=Fh7Vk73OsxljZKu1k6H9uzYwz334tpQT
7
7
  tricc_oo/converters/cql_to_operation.py,sha256=PUyV_YpUY98Ox0H_F_CN3UUf_I-BhFZVOcWWKTtwecM,14492
8
8
  tricc_oo/converters/datadictionnary.py,sha256=T2HLCBo4Am1p0kFqSH1r0PqbD8AC2IGuWkbvMvSCru0,3658
9
9
  tricc_oo/converters/drawio_type_map.py,sha256=UCPiGs7Lw0bigKScmZUnmOhACBz-FiDq92jHkI7RTSQ,9113
10
- tricc_oo/converters/tricc_to_xls_form.py,sha256=C7oDi1_IA0FLjoK700G9bABQZrGEqJltvBD2rGVjkIA,3494
10
+ tricc_oo/converters/tricc_to_xls_form.py,sha256=HZh0tQoKfRMPshJvEBwCOGqYGA1ZJLJ67bwV79qtlwk,3486
11
11
  tricc_oo/converters/utils.py,sha256=JZrtrvvOfXwdkw49pKauzinOcauWwsy-CVcw36TjyLo,1684
12
- tricc_oo/converters/xml_to_tricc.py,sha256=cDoLTwIMHIqyyNqZGwQte9YdX4y5j1Ac6r7M-zuKWZc,39403
12
+ tricc_oo/converters/xml_to_tricc.py,sha256=YltAT2wo6bPDMLRuKitqStjWW_OmSJhMpAwv2IAscfs,39544
13
13
  tricc_oo/converters/cql/cqlLexer.py,sha256=8HArbRphcrpnAG4uogJ2rHv4tc1WLzjN0B1uFeYILAc,49141
14
14
  tricc_oo/converters/cql/cqlListener.py,sha256=fA7-8DcS2Q69ckwjdg57-OfFHBxjTZFdoSKrtw7Hffc,57538
15
15
  tricc_oo/converters/cql/cqlParser.py,sha256=x3KdrwX9nwENSEJ5Ex7_l5NMnu3kWBO0uLdYu4moTq0,414745
16
16
  tricc_oo/converters/cql/cqlVisitor.py,sha256=iHuup2S7OGSVWLEcI4H3oecRqgXztC1sKnew_1P2iGY,33880
17
17
  tricc_oo/models/__init__.py,sha256=CgS52LLqdDIaXHvZy08hhu_VaYw80OEdfL_llM9ICBA,108
18
- tricc_oo/models/base.py,sha256=4mkAYAfvtdnTiLpUrttqQgIgfw1q_GkEgbVDKP8Txno,25849
18
+ tricc_oo/models/base.py,sha256=AaeB69vWg3ulBttoezEniUiU8HWh_pvrw7tCHpL27g4,25926
19
19
  tricc_oo/models/calculate.py,sha256=uNP0IDUqPQcJq9Co05H8eX5wbR_DikSxuOHxfVE5Dxg,8018
20
20
  tricc_oo/models/lang.py,sha256=ZMRwdoPWe01wEDhOM0uRk-6rt3BkoAAZM8mZ61--s3A,2265
21
21
  tricc_oo/models/ocl.py,sha256=MybSeB6fgCOUVJ4aektff0vrrTZsyfwZ2Gt_pPBu_FY,8728
@@ -31,20 +31,21 @@ tricc_oo/strategies/input/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJW
31
31
  tricc_oo/strategies/input/base_input_strategy.py,sha256=BEODXS74na1QRRcJVQ4cxiD8F7uRqaLyhE3QzKpGVvk,3891
32
32
  tricc_oo/strategies/input/drawio.py,sha256=uXAUPhXOeg0Uk_BNqlCqFBW4cWNox4VfH559bj1fhC0,12767
33
33
  tricc_oo/strategies/output/base_output_strategy.py,sha256=M9UFR67-_CFoW681bPAeBS1OUGuFtmUbM_rltACI0hk,8798
34
+ tricc_oo/strategies/output/dhis2_form.py,sha256=m23EeZB7uXkNCFJr9hNGevgsA1dQqBrtX5uoy_22QRI,36500
34
35
  tricc_oo/strategies/output/fhir_form.py,sha256=hbL921pe1Doun4IQrJuZ_Sq2fCh98G3grYie5olC4uc,15740
35
36
  tricc_oo/strategies/output/html_form.py,sha256=qSleEZOMV_-Z04y-i-ucyd5rgAYWAyjPwMrw0IHtCRM,8604
36
- tricc_oo/strategies/output/openmrs_form.py,sha256=t2I2IKl98dbfTsJrJ4hnQbG8_qROpZmKtyE2M1hnyD4,27192
37
+ tricc_oo/strategies/output/openmrs_form.py,sha256=zAmDGMmZdIGNpil5MD-huiUvt_Dbhc2vt5qsGaCS2_k,29003
37
38
  tricc_oo/strategies/output/spice.py,sha256=QMeoismVC3PdbvwTK0PtUjWX9jl9780fbQIXn76fMXw,10761
38
39
  tricc_oo/strategies/output/xls_form.py,sha256=26pEea0I_owpsz9S8hoHJNzChA5b2Th8KPRIeTEMfqo,29323
39
40
  tricc_oo/strategies/output/xlsform_cdss.py,sha256=X00Lt5MzV8TX14dR4dFI1MqllI5S1e13bKbeysWM9uA,17435
40
41
  tricc_oo/strategies/output/xlsform_cht.py,sha256=RY_mre9j6w2vVnRFSGn5R3CuTWFjIbQyl1uWwz9Ay5E,22965
41
42
  tricc_oo/strategies/output/xlsform_cht_hf.py,sha256=xm6SKirV3nMZvM2w54_zJcXAeAgAkq-EEqGEjnOWv6c,988
42
43
  tricc_oo/visitors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
43
- tricc_oo/visitors/tricc.py,sha256=vmOQRocfzjr-MvLNflNxV51w0vhRO53fX8tTIqjNyOU,107437
44
+ tricc_oo/visitors/tricc.py,sha256=cgmiT26pgkmmocqi7vVMtyo9WyvYyR1nMg-cPjmUawQ,107724
44
45
  tricc_oo/visitors/utils.py,sha256=j83aAq5s5atXi3OC0jc_uJd54a8XrHHmizeeEbWZQJg,421
45
46
  tricc_oo/visitors/xform_pd.py,sha256=ryAnI3V9x3eTmJ2LNsUZfvl0_yfCqo6oBgeSu-WPqaE,9613
46
- tricc_oo-1.5.28.dist-info/licenses/LICENSE,sha256=Pz2eACSxkhsGfW9_iN60pgy-enjnbGTj8df8O3ebnQQ,16726
47
- tricc_oo-1.5.28.dist-info/METADATA,sha256=bQmU_Wmm8ki1FHnl51N3l_5uBDUmWJxHo_paTGkYBks,8577
48
- tricc_oo-1.5.28.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
49
- tricc_oo-1.5.28.dist-info/top_level.txt,sha256=NvbfMNAiy9m4b1unBsqpeOQWh4IgA1Xa33BtKA4abxk,15
50
- tricc_oo-1.5.28.dist-info/RECORD,,
47
+ tricc_oo-1.6.0.dist-info/licenses/LICENSE,sha256=Pz2eACSxkhsGfW9_iN60pgy-enjnbGTj8df8O3ebnQQ,16726
48
+ tricc_oo-1.6.0.dist-info/METADATA,sha256=xhuGPgAE0OqK6f_UXRhxWsV2E5PZupPkLQ7QJzGD18k,8576
49
+ tricc_oo-1.6.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
50
+ tricc_oo-1.6.0.dist-info/top_level.txt,sha256=NvbfMNAiy9m4b1unBsqpeOQWh4IgA1Xa33BtKA4abxk,15
51
+ tricc_oo-1.6.0.dist-info/RECORD,,