tricc-oo 1.5.28__tar.gz → 1.5.29__tar.gz

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 (56) hide show
  1. {tricc_oo-1.5.28/tricc_oo.egg-info → tricc_oo-1.5.29}/PKG-INFO +1 -1
  2. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/pyproject.toml +1 -1
  3. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tests/build.py +1 -0
  4. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo/models/base.py +2 -0
  5. tricc_oo-1.5.29/tricc_oo/strategies/output/dhis2_form.py +859 -0
  6. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo/strategies/output/openmrs_form.py +45 -1
  7. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo/visitors/tricc.py +37 -8
  8. {tricc_oo-1.5.28 → tricc_oo-1.5.29/tricc_oo.egg-info}/PKG-INFO +1 -1
  9. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo.egg-info/SOURCES.txt +1 -0
  10. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/LICENSE +0 -0
  11. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/README.md +0 -0
  12. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/setup.cfg +0 -0
  13. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tests/test_cql.py +0 -0
  14. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tests/to_ocl.py +0 -0
  15. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo/__init__.py +0 -0
  16. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo/converters/__init__.py +0 -0
  17. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo/converters/codesystem_to_ocl.py +0 -0
  18. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo/converters/cql/cqlLexer.py +0 -0
  19. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo/converters/cql/cqlListener.py +0 -0
  20. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo/converters/cql/cqlParser.py +0 -0
  21. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo/converters/cql/cqlVisitor.py +0 -0
  22. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo/converters/cql_to_operation.py +0 -0
  23. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo/converters/datadictionnary.py +0 -0
  24. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo/converters/drawio_type_map.py +0 -0
  25. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo/converters/tricc_to_xls_form.py +0 -0
  26. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo/converters/utils.py +0 -0
  27. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo/converters/xml_to_tricc.py +0 -0
  28. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo/models/__init__.py +0 -0
  29. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo/models/calculate.py +0 -0
  30. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo/models/lang.py +0 -0
  31. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo/models/ocl.py +0 -0
  32. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo/models/ordered_set.py +0 -0
  33. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo/models/tricc.py +0 -0
  34. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo/parsers/__init__.py +0 -0
  35. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo/parsers/xml.py +0 -0
  36. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo/serializers/__init__.py +0 -0
  37. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo/serializers/planuml.py +0 -0
  38. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo/serializers/xls_form.py +0 -0
  39. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo/strategies/__init__.py +0 -0
  40. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo/strategies/input/__init__.py +0 -0
  41. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo/strategies/input/base_input_strategy.py +0 -0
  42. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo/strategies/input/drawio.py +0 -0
  43. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo/strategies/output/base_output_strategy.py +0 -0
  44. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo/strategies/output/fhir_form.py +0 -0
  45. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo/strategies/output/html_form.py +0 -0
  46. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo/strategies/output/spice.py +0 -0
  47. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo/strategies/output/xls_form.py +0 -0
  48. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo/strategies/output/xlsform_cdss.py +0 -0
  49. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo/strategies/output/xlsform_cht.py +0 -0
  50. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo/strategies/output/xlsform_cht_hf.py +0 -0
  51. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo/visitors/__init__.py +0 -0
  52. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo/visitors/utils.py +0 -0
  53. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo/visitors/xform_pd.py +0 -0
  54. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo.egg-info/dependency_links.txt +0 -0
  55. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo.egg-info/requires.txt +0 -0
  56. {tricc_oo-1.5.28 → tricc_oo-1.5.29}/tricc_oo.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tricc-oo
3
- Version: 1.5.28
3
+ Version: 1.5.29
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tricc-oo"
7
- version = "1.5.28"
7
+ version = "1.5.29"
8
8
  description = "Python library that converts CDSS L2 in L3"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -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
@@ -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)})"
@@ -69,6 +69,9 @@ class OpenMRSStrategy(BaseOutPutStrategy):
69
69
  self.current_segment = None
70
70
  self.current_activity = None
71
71
  self.concept_map = {}
72
+ self.calculated_fields = [] # Store calculated fields to add to first section of each page
73
+ self.calculated_fields_added = set() # Track which pages have had calculated fields added
74
+ self.inject_version()
72
75
 
73
76
  def get_export_name(self, r):
74
77
  if isinstance(r, TriccNodeSelectOption):
@@ -146,6 +149,9 @@ class OpenMRSStrategy(BaseOutPutStrategy):
146
149
  logger.info("generate the export format")
147
150
  self.process_export(self.project.start_pages, pages=self.project.pages)
148
151
 
152
+ logger.info("create calculation page")
153
+ self.create_calculation_page()
154
+
149
155
  logger.info("print the export")
150
156
  self.export(self.project.start_pages, version=version)
151
157
 
@@ -250,7 +256,9 @@ class OpenMRSStrategy(BaseOutPutStrategy):
250
256
  }
251
257
  }
252
258
  }
253
- return question
259
+ # Collect calculated fields to add to first section of each page
260
+ self.calculated_fields.append(question)
261
+ return None # Don't return the question, it will be added to first section
254
262
  return None
255
263
 
256
264
  def generate_calculate(self, node, processed_nodes, **kwargs):
@@ -638,6 +646,7 @@ class OpenMRSStrategy(BaseOutPutStrategy):
638
646
  "label": section_label,
639
647
  "questions": []
640
648
  })
649
+
641
650
  if group_node.relevance:
642
651
  relevance_str = self.convert_expression_to_string(not_clean(group_node.relevance))
643
652
  if relevance_str and relevance_str != 'false':
@@ -645,3 +654,38 @@ class OpenMRSStrategy(BaseOutPutStrategy):
645
654
  "hideWhenExpression": f"{relevance_str}"
646
655
  }
647
656
  logger.debug(f"Started section: {section_label}")
657
+
658
+ def create_calculation_page(self):
659
+ """Create a dedicated page for all calculated fields"""
660
+ if self.calculated_fields:
661
+ self.clean_sections()
662
+ self.clean_pages()
663
+ page = {
664
+ "label": "Calculations",
665
+ "sections": [
666
+ {
667
+ "label": "Calculations",
668
+ "questions": self.calculated_fields
669
+ }
670
+ ]
671
+ }
672
+ self.form_data["pages"].append(page)
673
+ logger.debug("Created calculation page")
674
+
675
+ def inject_version(self):
676
+ # Add hidden version field using version() function
677
+ question = {
678
+ "id": "version",
679
+ "type": "control",
680
+ "label": "",
681
+ "hide": {
682
+ "hideWhenExpression": "true"
683
+ },
684
+ "questionOptions": {
685
+ "calculate": {
686
+ "calculateExpression": "version()"
687
+ }
688
+ }
689
+ }
690
+ # Collect calculated fields to add to first section of each page
691
+ self.calculated_fields.append(question)
@@ -1564,7 +1564,12 @@ def set_prev_next_node(source_node, target_node, replaced_node=None, edge_only=F
1564
1564
  set_next_node(source_node, target_node, replaced_node, edge_only)
1565
1565
 
1566
1566
  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
1567
+ if issubclass(source_node.__class__, TriccNodeSelect):
1568
+ label = "continue"
1569
+ elif isinstance(source_node, TriccNodeRhombus):
1570
+ label = "yes"
1571
+ else:
1572
+ label = None
1568
1573
  activity.edges.append(TriccEdge(id=generate_id(), source=source_id, target=target_id, value=label))
1569
1574
 
1570
1575
 
@@ -1848,6 +1853,16 @@ def get_node_expression(in_node, processed_nodes, get_overall_exp=False, is_prev
1848
1853
  logger.critical(f"Rhombus without expression {node.get_name()}")
1849
1854
  elif is_prev and issubclass(node.__class__, TriccNodeDisplayCalculateBase):
1850
1855
  expression = TriccOperation(TriccOperator.ISTRUE, [node])
1856
+ prev_exp_overall = get_node_expression(
1857
+ node,
1858
+ processed_nodes=processed_nodes,
1859
+ get_overall_exp=False,
1860
+ is_prev=False,
1861
+ process=process,
1862
+ negate=negate
1863
+ )
1864
+ if prev_exp_overall in [TriccStatic(True), TriccStatic(False)]:
1865
+ expression = prev_exp_overall
1851
1866
  elif hasattr(node, "expression_reference") and isinstance(node.expression_reference, TriccOperation):
1852
1867
  # if issubclass(node.__class__, TriccNodeDisplayCalculateBase):
1853
1868
  # expression = TriccOperation(
@@ -2241,7 +2256,10 @@ def get_count_terms_details(prev_node, processed_nodes, get_overall_exp, negate=
2241
2256
  get_node_expression(
2242
2257
  prev_node,
2243
2258
  processed_nodes=processed_nodes,
2244
- get_overall_exp=True,
2259
+ get_overall_exp=not issubclass(
2260
+ prev_node.__class__,
2261
+ (TriccNodeBridge, TriccNodeDisplayBridge)
2262
+ ),
2245
2263
  is_prev=True,
2246
2264
  process=process,
2247
2265
  )
@@ -2279,7 +2297,10 @@ def get_add_terms(node, processed_nodes, get_overall_exp=False, negate=False, pr
2279
2297
  get_node_expression(
2280
2298
  prev_node,
2281
2299
  processed_nodes=processed_nodes,
2282
- get_overall_exp=True,
2300
+ get_overall_exp=not issubclass(
2301
+ prev_node.__class__,
2302
+ (TriccNodeBridge, TriccNodeDisplayBridge)
2303
+ ),
2283
2304
  is_prev=True,
2284
2305
  process=process,
2285
2306
  )
@@ -2352,7 +2373,11 @@ def get_rhombus_terms(node, processed_nodes, get_overall_exp=False, negate=False
2352
2373
  TriccOperator.CAST_NUMBER,
2353
2374
  [
2354
2375
  get_node_expression(
2355
- expression, processed_nodes=processed_nodes, get_overall_exp=True, is_prev=True, process=process
2376
+ expression,
2377
+ processed_nodes=processed_nodes,
2378
+ get_overall_exp=not issubclass(expression.__class__, (TriccNodeBridge, TriccNodeDisplayBridge)),
2379
+ is_prev=True,
2380
+ process=process
2356
2381
  )
2357
2382
  ],
2358
2383
  )
@@ -2420,7 +2445,7 @@ def get_calculation_terms(node, processed_nodes, get_overall_exp=False, negate=F
2420
2445
  return get_node_expression(
2421
2446
  node_to_negate,
2422
2447
  processed_nodes=processed_nodes,
2423
- get_overall_exp=True,
2448
+ get_overall_exp=not issubclass(node_to_negate.__class__, (TriccNodeBridge, TriccNodeDisplayBridge)),
2424
2449
  is_prev=True,
2425
2450
  negate=True,
2426
2451
  process=process,
@@ -2429,7 +2454,7 @@ def get_calculation_terms(node, processed_nodes, get_overall_exp=False, negate=F
2429
2454
  return get_node_expression(
2430
2455
  node_to_negate,
2431
2456
  processed_nodes=processed_nodes,
2432
- get_overall_exp=True,
2457
+ get_overall_exp=not issubclass(node_to_negate.__class__, (TriccNodeBridge, TriccNodeDisplayBridge)),
2433
2458
  is_prev=True,
2434
2459
  negate=True,
2435
2460
  process=process,
@@ -2616,13 +2641,17 @@ def generate_base(node, processed_nodes, **kwargs):
2616
2641
  node.min = float(node.min)
2617
2642
  if int(node.min) == node.min:
2618
2643
  node.min = int(node.min)
2619
- constraints.append(TriccOperation(TriccOperator.MORE_OR_EQUAL, ["$this", TriccStatic(node.min)]))
2644
+ constraints.append(
2645
+ TriccOperation(TriccOperator.MORE_OR_EQUAL, ["$this", TriccStatic(node.min)])
2646
+ )
2620
2647
  constraints_min = "The minimun value is {0}.".format(node.min)
2621
2648
  if node.max is not None and node.max != "":
2622
2649
  node.max = float(node.max)
2623
2650
  if int(node.max) == node.max:
2624
2651
  node.max = int(node.max)
2625
- constraints.append(TriccOperation(TriccOperator.LESS_OR_EQUAL, ["$this", TriccStatic(node.max)]))
2652
+ constraints.append(
2653
+ TriccOperation(TriccOperator.LESS_OR_EQUAL, ["$this", TriccStatic(node.max)])
2654
+ )
2626
2655
  constraints_max = "The maximum value is {0}.".format(node.max)
2627
2656
  if len(constraints) > 1:
2628
2657
  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.5.29
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
@@ -39,6 +39,7 @@ tricc_oo/strategies/input/__init__.py
39
39
  tricc_oo/strategies/input/base_input_strategy.py
40
40
  tricc_oo/strategies/input/drawio.py
41
41
  tricc_oo/strategies/output/base_output_strategy.py
42
+ tricc_oo/strategies/output/dhis2_form.py
42
43
  tricc_oo/strategies/output/fhir_form.py
43
44
  tricc_oo/strategies/output/html_form.py
44
45
  tricc_oo/strategies/output/openmrs_form.py
File without changes
File without changes
File without changes
File without changes
File without changes