tricc-oo 1.6.8__py3-none-any.whl → 1.6.10__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.
@@ -36,7 +36,7 @@ def get_export_name(node, replace_dots=True):
36
36
  elif isinstance(node, bool):
37
37
  return BOOLEAN_MAP[str(TRICC_TRUE_VALUE)] if node else BOOLEAN_MAP[str(TRICC_FALSE_VALUE)]
38
38
  elif isinstance(node, TriccReference):
39
- logger.warning(f"Reference {node.value} use in export, bad serialiuation probable")
39
+ logger.warning(f"Reference {node.value} use in export, bad serialization probable")
40
40
  return str(node.value)
41
41
  elif isinstance(node, (str, TriccStatic, TriccNodeSelectOption)):
42
42
  if isinstance(node, TriccNodeSelectOption):
@@ -625,6 +625,8 @@ def enrich_node(diagram, media_path, edge, node, activity, help_before=False):
625
625
  name=f"{node.name}.more_info",
626
626
  label=message,
627
627
  parent=node,
628
+ group=node.group,
629
+ activity=node.activity,
628
630
  required=None,
629
631
  )
630
632
  # node.help = message
@@ -64,19 +64,30 @@ def start_group(
64
64
  groups[name] = 0
65
65
  relevance = relevance and cur_group.relevance is not None and cur_group.relevance != ""
66
66
  past_instances = len(getattr(cur_group.base_instance, "instances", []))
67
- group_calc_required = relevance is not None and (len(str(relevance)) > 100 or past_instances > 1)
67
+ group_calc_required = False and relevance is not None and (len(str(relevance)) > 100 or past_instances > 1)
68
68
  calc = None
69
69
  if group_calc_required and getattr(cur_group.relevance, 'operator', None) != TriccOperator.ISTRUE:
70
70
 
71
71
  calc = TriccNodeCalculate(
72
72
  id=generate_id(get_export_group_name(name)),
73
- group=cur_group,
73
+ group=cur_group.group,
74
74
  activity=cur_group.activity,
75
75
  name=get_export_group_name(name),
76
- expression=cur_group.relevance
76
+ expression=cur_group.relevance.copy()
77
77
  )
78
- if calc not in cur_group.calculates:
79
- cur_group.calculates.append(calc)
78
+
79
+ if calc not in cur_group.activity.calculates:
80
+ process_reference(
81
+ calc,
82
+ processed_nodes,
83
+ calculates=kwargs.get('calculates', None),
84
+ used_calculates=kwargs.get('used_calculates', None),
85
+ replace_reference=True,
86
+ warn=False,
87
+ codesystems=kwargs.get('codesystems', None)
88
+ )
89
+ cur_group.activity.calculates.append(calc)
90
+ cur_group.activity.nodes[calc.id] = calc
80
91
  processed_nodes.add(calc)
81
92
 
82
93
  cur_group.relevance = TriccOperation(
@@ -123,7 +134,7 @@ def start_group(
123
134
  value = get_export_name(calc)
124
135
  calc_values.append(value)
125
136
  elif column == "calculation":
126
- calc_values.append(f"number({strategy.get_tricc_operation_expression(calc.expression)}")
137
+ calc_values.append(f"number({strategy.get_tricc_operation_expression(calc.expression)})")
127
138
  elif column == "relevance":
128
139
  calc_values.append("")
129
140
  else:
@@ -16,7 +16,7 @@ import datetime
16
16
  from tricc_oo.strategies.output.base_output_strategy import BaseOutPutStrategy
17
17
  from tricc_oo.models.base import (
18
18
  not_clean, TriccOperation,
19
- TriccStatic, TriccReference
19
+ TriccStatic, TriccReference, TriccOperator
20
20
  )
21
21
  from tricc_oo.models.tricc import (
22
22
  TriccNodeSelectOption,
@@ -27,6 +27,8 @@ from tricc_oo.models.tricc import (
27
27
  TriccNodeActivity,
28
28
  TriccNodeSelect,
29
29
  TriccNodeSelectYesNo,
30
+ TriccNodeNote,
31
+ TriccNodeMoreInfo,
30
32
  )
31
33
  from tricc_oo.models.calculate import TriccNodeDisplayCalculateBase
32
34
  from tricc_oo.models.ordered_set import OrderedSet
@@ -44,7 +46,7 @@ class DHIS2Strategy(BaseOutPutStrategy):
44
46
 
45
47
  def __init__(self, project, output_path):
46
48
  super().__init__(project, output_path)
47
- form_id = getattr(self.project.start_pages["main"], 'form_id', 'dhis2_program')
49
+ form_id = getattr(self.project.start_pages["main"].root, 'form_id', 'dhis2_program')
48
50
  self.program_metadata = {
49
51
  "id": self.generate_id(form_id),
50
52
  "name": form_id,
@@ -204,13 +206,17 @@ class DHIS2Strategy(BaseOutPutStrategy):
204
206
  return False
205
207
 
206
208
  if node not in processed_nodes:
209
+ # Skip relevance generation for TriccNodeMoreInfo as they don't create DataElements
210
+ if isinstance(node, TriccNodeMoreInfo):
211
+ return True
212
+
207
213
  relevance = None
208
214
  if hasattr(node, 'relevance') and node.relevance:
209
215
  relevance = node.relevance
210
216
  if hasattr(node, 'expression') and node.expression:
211
217
  relevance = node.expression
212
218
  if relevance:
213
- relevance_str = self.convert_expression_to_string(not_clean(relevance))
219
+ relevance_str = self.convert_expression_to_string(relevance)
214
220
  if relevance_str and relevance_str != 'false':
215
221
  # Create program rule action for hiding/showing based on relevance
216
222
  rule_id = self.generate_id(f"rule_{node.get_name()}_relevance")
@@ -225,8 +231,10 @@ class DHIS2Strategy(BaseOutPutStrategy):
225
231
  "activity_ref": node, # Temporary reference to be replaced with section ID
226
232
  "programRule": {"id": rule_id},
227
233
  }
228
- else:
229
- # For regular nodes, use HIDEFIELD action
234
+ self.program_rule_actions.append(program_rule_action)
235
+ elif issubclass(node.__class__, TriccNodeDisplayModel) and not issubclass(node.__class__, TriccNodeCalculateBase) and not isinstance(node, (TriccNodeNote, TriccNodeMoreInfo, TriccNodeSelectOption)):
236
+ # For regular nodes that get DataElements, use HIDEFIELD action
237
+ # Exclude TriccNodeNote and TriccNodeMoreInfo as they don't get DataElements
230
238
  program_rule_action = {
231
239
  "id": action_id,
232
240
  "programRuleActionType": "HIDEFIELD",
@@ -235,18 +243,45 @@ class DHIS2Strategy(BaseOutPutStrategy):
235
243
  },
236
244
  "programRule": {"id": rule_id}
237
245
  }
238
- self.program_rule_actions.append(program_rule_action)
246
+ self.program_rule_actions.append(program_rule_action)
239
247
 
240
248
  # Create program rule referencing the action
241
- condition = self.simplify_expression(f"!({relevance_str})") # Negate for hide when true
249
+ condition = self.simplify_expression(f"({relevance_str})==false") # Negate for hide when true
242
250
  condition = self.simplify_expression(condition)
243
251
  self.program_rules.append({
244
252
  "id": rule_id,
245
253
  "name": f"Hide `{self.get_export_name(node)}` when condition met",
246
- "description": f"Hide `{self.get_display(node)}` based on relevance",
254
+ "description": f"Hide `{self.get_display(node)[:128]}` based on relevance",
247
255
  "condition": condition,
248
256
  "programRuleActions": [{"id": action_id}]
249
257
  })
258
+
259
+ # Check if field should be mandatory based on 'required' attribute
260
+ if bool(getattr(node, 'required', False)):
261
+ # Create program rule for mandatory field using relevance function
262
+ mandatory_rule_id = self.generate_id(f"rule_{node.get_name()}_mandatory")
263
+ mandatory_action_id = self.generate_id(f"action_{mandatory_rule_id}")
264
+
265
+ mandatory_program_rule_action = {
266
+ "id": mandatory_action_id,
267
+ "programRuleActionType": "SETMANDATORYFIELD",
268
+ "dataElement": {
269
+ "id": self.generate_id(self.get_export_name(node))
270
+ },
271
+ "programRule": {"id": mandatory_rule_id}
272
+ }
273
+ self.program_rule_actions.append(mandatory_program_rule_action)
274
+
275
+ # Create program rule for mandatory field - use relevance condition if available
276
+ mandatory_condition = relevance_str if relevance_str and relevance_str != 'false' else "true"
277
+ self.program_rules.append({
278
+ "id": mandatory_rule_id,
279
+ "name": f"Make `{self.get_export_name(node)}` mandatory",
280
+ "description": f"Set `{self.get_display(node)[:128]}` as mandatory field",
281
+ "condition": mandatory_condition,
282
+ "programRuleActions": [{"id": mandatory_action_id}]
283
+ })
284
+
250
285
  return True
251
286
 
252
287
  def generate_data_element(self, node):
@@ -274,8 +309,8 @@ class DHIS2Strategy(BaseOutPutStrategy):
274
309
  "id": de_id,
275
310
  "name": self.get_export_name(node),
276
311
  "shortName": node.name[:50],
277
- "displayFormName": self.get_display(node),
278
- "formName": self.get_display(node),
312
+ "displayFormName": "More infromation" if isinstance(node, TriccNodeMoreInfo) else self.get_display(node),
313
+ "formName": "More infromation" if isinstance(node, TriccNodeMoreInfo) else self.get_display(node),
279
314
  "valueType": value_type,
280
315
  "domainType": "TRACKER",
281
316
  "aggregationType": "NONE"
@@ -512,8 +547,36 @@ class DHIS2Strategy(BaseOutPutStrategy):
512
547
  return False
513
548
 
514
549
  if node not in processed_nodes:
550
+ logger.debug(f"generate_export processing node: {node.get_name()} type: {type(node)}")
551
+ # Special handling for TriccNodeNote - transform into section description
552
+ if isinstance(node, TriccNodeNote):
553
+ logger.info(f"Found TriccNodeNote: {node.get_name()}")
554
+ self.handle_note_as_section_description(node, processed_nodes, **kwargs)
555
+ elif isinstance(node, TriccNodeMoreInfo):
556
+ logger.info(f"Found TriccNodeMoreInfo: {node.get_name()}")
557
+ # Create boolean checkbox data element for moreinfo
558
+ data_element = self.generate_data_element(node)
559
+ if data_element:
560
+ # Add to program stage
561
+ if self.program_metadata["programStages"]:
562
+ psde_id = self.generate_id(f"psde_{node.name}")
563
+ psde = {
564
+ "id": psde_id,
565
+ "dataElement": {"id": data_element["id"]},
566
+ "compulsory": False
567
+ }
568
+ self.program_metadata["programStages"][-1]["programStageDataElements"].append(psde)
569
+
570
+ # Add data element to current section
571
+ if self.current_section and self.current_section in self.sections:
572
+ self.sections[self.current_section]["dataElements"].append({"id": data_element["id"]})
573
+
574
+ # Handle the note section with checkbox conditioning
575
+ checkbox_reference = TriccReference(node.name)
576
+ checkbox_condition = TriccOperation(TriccOperator.ISTRUE, [checkbox_reference])
577
+ self.handle_note_as_section_description(node, processed_nodes, additional_condition=checkbox_condition, **kwargs)
515
578
  # Skip creating data elements for calculate nodes - they should only be program rule variables
516
- if not issubclass(node.__class__, TriccNodeCalculateBase):
579
+ elif not issubclass(node.__class__, TriccNodeCalculateBase):
517
580
  data_element = self.generate_data_element(node)
518
581
  if data_element:
519
582
  # Add to program stage
@@ -522,7 +585,7 @@ class DHIS2Strategy(BaseOutPutStrategy):
522
585
  psde = {
523
586
  "id": psde_id,
524
587
  "dataElement": {"id": data_element["id"]},
525
- "compulsory": bool(getattr(node, 'required', False))
588
+ "compulsory": False
526
589
  }
527
590
  self.program_metadata["programStages"][-1]["programStageDataElements"].append(psde)
528
591
 
@@ -532,6 +595,136 @@ class DHIS2Strategy(BaseOutPutStrategy):
532
595
 
533
596
  return True
534
597
 
598
+ def handle_note_as_section_description(self, node, processed_nodes, additional_condition=None, **kwargs):
599
+ """Transform TriccNodeNote into section description and create hide logic with section duplication
600
+
601
+ Args:
602
+ node: The node to process (TriccNodeNote or TriccNodeMoreInfo)
603
+ processed_nodes: Set of already processed nodes
604
+ additional_condition: Optional additional condition to combine with existing relevance
605
+ """
606
+ logger.info(f"Processing note {node.get_name()} for section description")
607
+ if not self.current_section or self.current_section not in self.sections:
608
+ logger.warning(f"No current section found for note {node.get_name()}")
609
+ return
610
+
611
+ # 1. Duplicate the previous section with incremented ID (like XLS form group numbering)
612
+ original_section = self.sections[self.current_section]
613
+ section_name = f"{original_section['name']}_{node.name}"
614
+ duplicated_section_id = self.generate_id(section_name)
615
+ duplicated_section = {
616
+ "id": duplicated_section_id,
617
+ "name": section_name, # Use incremented naming pattern
618
+ "sortOrder": len(self.sections),
619
+ "programStage": {"id": self.program_metadata["programStages"][-1]["id"]},
620
+ "dataElements": original_section.get("dataElements", []).copy(), # Copy data elements
621
+ "activity_ref": original_section.get("activity_ref")
622
+ }
623
+
624
+ # 2. Create a new section for the note/moreinfo with name and ID from the node
625
+ note_section_id = self.generate_id(self.get_export_name(node))
626
+ note_section_name = self.get_export_name(node)
627
+
628
+ note_section = {
629
+ "id": note_section_id,
630
+ "name": note_section_name,
631
+ "sortOrder": len(self.sections),
632
+ "programStage": {"id": self.program_metadata["programStages"][-1]["id"]},
633
+ "dataElements": [],
634
+ "activity_ref": original_section.get("activity_ref") # Same activity reference
635
+ }
636
+
637
+ # Set section description to the note's label
638
+ if hasattr(node, 'label') and node.label:
639
+ note_section["description"] = node.label.replace('\u00a0', ' ').strip()
640
+ logger.info(f"Set note section {note_section_id} description to: {note_section['description']}")
641
+ elif hasattr(node, 'name') and node.name:
642
+ note_section["description"] = node.name.replace('\u00a0', ' ').strip()
643
+ logger.info(f"Set note section {note_section_id} description to: {note_section['description']}")
644
+ else:
645
+ note_section["description"] = str(node.id)
646
+ logger.info(f"Set note section {note_section_id} description to: {note_section['description']}")
647
+
648
+ self.sections[note_section_id] = note_section
649
+
650
+ # Add note section to program stage
651
+ if self.program_metadata["programStages"]:
652
+ self.program_metadata["programStages"][-1]["programStageSections"].append({"id": note_section_id})
653
+
654
+ logger.info(f"Created note section {note_section_id} with name '{note_section_name}'")
655
+
656
+ # 3. Inject the duplicated section as the new current section
657
+ self.sections[duplicated_section_id] = duplicated_section
658
+
659
+ # Add duplicated section to program stage
660
+ if self.program_metadata["programStages"]:
661
+ self.program_metadata["programStages"][-1]["programStageSections"].append({"id": duplicated_section_id})
662
+
663
+ logger.info(f"Duplicated section {self.current_section} as {duplicated_section_id}")
664
+
665
+ self.current_section = duplicated_section_id
666
+ logger.info(f"Set current section to duplicated section {duplicated_section_id}")
667
+
668
+ # Create hide logic for the note section based on combined relevance
669
+ combined_relevance = None
670
+
671
+ # Get parent activity relevance from the original section's activity_ref
672
+ parent_activity = original_section.get("activity_ref")
673
+ if parent_activity and hasattr(parent_activity, 'relevance') and parent_activity.relevance:
674
+ combined_relevance = parent_activity.relevance
675
+
676
+ # Combine with note relevance if it exists
677
+ if hasattr(node, 'relevance') and node.relevance:
678
+ if combined_relevance:
679
+ # Combine using AND operation
680
+ combined_relevance = TriccOperation(
681
+ TriccOperator.AND,
682
+ [combined_relevance, node.relevance]
683
+ )
684
+ else:
685
+ combined_relevance = node.relevance
686
+
687
+ # Combine with additional condition if provided
688
+ if additional_condition:
689
+ if combined_relevance:
690
+ # Combine using AND operation
691
+ combined_relevance = TriccOperation(
692
+ TriccOperator.AND,
693
+ [combined_relevance, additional_condition]
694
+ )
695
+ else:
696
+ combined_relevance = additional_condition
697
+
698
+ # Create hide logic if there's relevance - applied to the note section
699
+ if combined_relevance:
700
+ relevance_str = self.convert_expression_to_string(not_clean(combined_relevance))
701
+ if relevance_str and relevance_str != 'false':
702
+ # Create program rule action for hiding the note section
703
+ rule_id = self.generate_id(f"rule_{node.get_name()}_note_hide_section")
704
+ action_id = self.generate_id(f"action_{rule_id}")
705
+
706
+ program_rule_action = {
707
+ "id": action_id,
708
+ "programRuleActionType": "HIDESECTION",
709
+ "activity_ref": parent_activity, # Use activity reference like other HIDESECTION actions
710
+ "programRule": {"id": rule_id},
711
+ }
712
+ self.program_rule_actions.append(program_rule_action)
713
+
714
+ # Create program rule referencing the action
715
+ condition = self.simplify_expression(f"({relevance_str})==false") # Negate for hide when true
716
+ condition = self.simplify_expression(condition)
717
+ rule_name = f"Hide note section `{note_section_name}` based on relevance"
718
+ if additional_condition:
719
+ rule_name = f"Hide moreinfo section `{note_section_name}` when checkbox false"
720
+ self.program_rules.append({
721
+ "id": rule_id,
722
+ "name": rule_name,
723
+ "description": f"Hide note section `{self.get_display(node)[:128]}` based on combined relevance",
724
+ "condition": condition,
725
+ "programRuleActions": [{"id": action_id}]
726
+ })
727
+
535
728
  def clean_section(self, program_stages_payload):
536
729
  """Clean sections by removing empty ones and merging sections with same activity_ref"""
537
730
  sections_to_remove = set()
@@ -542,11 +735,11 @@ class DHIS2Strategy(BaseOutPutStrategy):
542
735
  section_id = section["id"]
543
736
  activity_ref = section.get("activity_ref")
544
737
  # Remove empty sections
545
- if not section.get("dataElements"):
738
+ if not section.get("dataElements") and not section.get("description"):
546
739
  sections_to_remove.add(section_id)
547
740
 
548
741
  # Check for sections with same activity_ref
549
- elif activity_ref == prev_activity_ref:
742
+ elif activity_ref == prev_activity_ref and not section.get("description"):
550
743
  # Merge this section into the existing one
551
744
  existing_section = self.sections[prev_section_id]
552
745
 
@@ -645,6 +838,8 @@ class DHIS2Strategy(BaseOutPutStrategy):
645
838
  # Non-activity actions (HIDEFIELD) can be added directly
646
839
  program_rule_actions_payload.append(action)
647
840
 
841
+ # Filter out rules that reference non-existent actions
842
+ valid_action_ids = {action["id"] for action in program_rule_actions_payload}
648
843
  if self.program_rules:
649
844
  program_rules_payload = [
650
845
  {
@@ -652,6 +847,7 @@ class DHIS2Strategy(BaseOutPutStrategy):
652
847
  "program": {"id": self.program_metadata["id"]}
653
848
  }
654
849
  for rule in self.program_rules
850
+ if all(action_ref["id"] in valid_action_ids for action_ref in rule["programRuleActions"])
655
851
  ]
656
852
 
657
853
  if self.program_rule_variables:
@@ -710,7 +906,7 @@ class DHIS2Strategy(BaseOutPutStrategy):
710
906
  return self.get_tricc_operation_expression(r)
711
907
  elif isinstance(r, TriccReference):
712
908
  # Use variable name from concept_map
713
- node_id = self.concept_map.get(r.value.name, self.get_export_name(r.value))
909
+ node_id = self.concept_map.get(r.value, self.get_export_name(r.value))
714
910
  return f"#{{{node_id}}}"
715
911
  elif isinstance(r, TriccStatic):
716
912
  if isinstance(r.value, bool):
@@ -97,7 +97,7 @@ class FHIRStrategy(BaseOutPutStrategy):
97
97
  "type": self.map_tricc_type_to_fhir(node.tricc_type if hasattr(node, 'tricc_type') else 'text')
98
98
  }
99
99
  if hasattr(node, 'options') and node.options:
100
- item["answerOption"] = [{"valueString": opt.name} for opt in node.options]
100
+ item["answerOption"] = [{"valueString": opt.name} for opt in node.options.values()]
101
101
  self.questionnaires[segment]["item"].append(item)
102
102
  return True
103
103
 
@@ -3,8 +3,12 @@ import logging
3
3
  import os
4
4
  import shutil
5
5
  import subprocess
6
+ import tempfile
7
+ import zipfile
6
8
  import pandas as pd
7
9
 
10
+ from pyxform.xls2xform import convert
11
+
8
12
  from tricc_oo.models.lang import SingletonLangClass
9
13
  from tricc_oo.models.calculate import TriccNodeEnd
10
14
  from tricc_oo.models.tricc import TriccNodeDisplayModel
@@ -740,10 +744,12 @@ class XLSFormCHTStrategy(XLSFormCDSSStrategy):
740
744
  generated_files = self.export(self.project.start_pages, version=version)
741
745
 
742
746
  logger.info("validate the output")
743
- self.validate(generated_files)
747
+ if not self.validate(generated_files):
748
+ logger.error("CHT validation failed - aborting build")
749
+ exit(1)
744
750
 
745
751
  def validate(self, generated_files=None):
746
- """Validate the generated XLS form(s) using xls2xform-medic."""
752
+ """Validate the generated XLS form(s) using pyxform conversion and ODK Validate JAR."""
747
753
  if generated_files is None:
748
754
  # Fallback for single file validation
749
755
  if self.project.start_pages["main"].root.form_id is not None:
@@ -753,10 +759,10 @@ class XLSFormCHTStrategy(XLSFormCDSSStrategy):
753
759
  logger.error("Form ID not found for validation")
754
760
  return False
755
761
 
756
- # Ensure xls2xform-medic is available
757
- medic_tool = self._ensure_xls2xform_medic()
758
- if not medic_tool:
759
- logger.error("xls2xform-medic tool not available, skipping CHT validation")
762
+ # Ensure ODK Validate JAR is available
763
+ jar_path = self._ensure_odk_validate_jar()
764
+ if not jar_path:
765
+ logger.error("ODK Validate JAR not available, skipping CHT validation")
760
766
  return False
761
767
 
762
768
  all_valid = True
@@ -767,49 +773,82 @@ class XLSFormCHTStrategy(XLSFormCDSSStrategy):
767
773
  continue
768
774
 
769
775
  try:
770
- # Run xls2xform-medic validation
771
- result = subprocess.run(
772
- [medic_tool, xls_file],
773
- capture_output=True,
774
- text=True,
775
- cwd=self.output_path
776
+ # Convert XLS to XForm using pyxform (without validation)
777
+ xform_path = xls_file.replace('.xlsx', '.xml')
778
+ convert_result = convert(
779
+ xlsform=xls_file,
780
+ validate=False, # Don't validate during conversion
781
+ pretty_print=True
776
782
  )
783
+ xform_content = convert_result.xform
784
+
785
+ # Write XForm to temporary file for validation
786
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.xml', delete=False) as temp_file:
787
+ temp_file.write(xform_content)
788
+ temp_xform_path = temp_file.name
789
+
790
+ try:
791
+ # Run ODK Validate JAR on the XForm
792
+ result = subprocess.run(
793
+ ["java", "-Djava.awt.headless=true", "-jar", jar_path, temp_xform_path],
794
+ capture_output=True,
795
+ text=True,
796
+ cwd=self.output_path
797
+ )
777
798
 
778
- if result.returncode == 0:
779
- logger.info(f"CHT XLSForm validation successful: {os.path.basename(xls_file)}")
780
- else:
781
- logger.error(f"CHT XLSForm validation failed for {os.path.basename(xls_file)}: {result.stderr}")
782
- all_valid = False
799
+ if result.returncode == 0 or "Cycle detected" in result.stderr:
800
+ logger.info(f"CHT XLSForm validation successful: {os.path.basename(xls_file)}")
801
+ else:
802
+ logger.error(f"CHT XLSForm validation failed for {os.path.basename(xls_file)}: {result.stderr}")
803
+ all_valid = False
804
+
805
+ finally:
806
+ # Clean up temporary XForm file
807
+ os.unlink(temp_xform_path)
783
808
 
784
809
  except Exception as e:
785
810
  logger.error(f"CHT XLSForm validation error for {os.path.basename(xls_file)}: {str(e)}")
786
811
  all_valid = False
787
812
 
788
- return all_valid
813
+ jar_in_zip = "site-packages/pyxform/validators/odk_validate/bin/ODK_Validate.jar"
814
+ zip_ref.extract(jar_in_zip, os.path.dirname(__file__))
815
+
816
+ # Move to final location
817
+ extracted_jar = os.path.join(os.path.dirname(__file__), jar_in_zip)
818
+ shutil.move(extracted_jar, jar_path)
789
819
 
790
- def _ensure_xls2xform_medic(self):
791
- """Ensure xls2xform-medic tool is available."""
792
- # Check if it's in PATH
793
- medic_tool = shutil.which("xls2xform-medic")
794
- if medic_tool:
795
- return medic_tool
820
+ logger.info(f"Extracted ODK Validate JAR to {jar_path}")
821
+ return jar_path
796
822
 
797
- # Check if we need to download it
798
- medic_path = os.path.join(os.path.dirname(__file__), "xls2xform-medic")
799
- if os.path.exists(medic_path):
800
- return medic_path
823
+ def _ensure_odk_validate_jar(self):
824
+ """Ensure ODK Validate JAR is available by extracting from medic zip."""
825
+ jar_path = os.path.join(os.path.dirname(__file__), "ODK_Validate.jar")
826
+
827
+ # Check if JAR already exists
828
+ if os.path.exists(jar_path):
829
+ return jar_path
830
+
831
+ # Extract JAR from medic zip
832
+ medic_zip_path = os.path.join(os.path.dirname(__file__), "xls2xform-medic")
833
+ if not os.path.exists(medic_zip_path):
834
+ logger.error("xls2xform-medic zip not found, cannot extract ODK Validate JAR")
835
+ return None
801
836
 
802
- # Try to download from the provided URL
803
837
  try:
804
- import urllib.request
805
- medic_url = "https://github.com/medic/pyxform/releases/download/v4.0.0-medic/xls2xform-medic"
806
- logger.info(f"Downloading xls2xform-medic from {medic_url}")
807
- urllib.request.urlretrieve(medic_url, medic_path)
808
- # Make executable
809
- os.chmod(medic_path, 0o755)
810
- return medic_path
838
+ with zipfile.ZipFile(medic_zip_path, 'r') as zip_ref:
839
+ # Extract the JAR from the zip
840
+ jar_in_zip = "site-packages/pyxform/validators/odk_validate/bin/ODK_Validate.jar"
841
+ zip_ref.extract(jar_in_zip, os.path.dirname(__file__))
842
+
843
+ # Move to final location
844
+ extracted_jar = os.path.join(os.path.dirname(__file__), jar_in_zip)
845
+ shutil.move(extracted_jar, jar_path)
846
+
847
+ logger.info(f"Extracted ODK Validate JAR to {jar_path}")
848
+ return jar_path
849
+
811
850
  except Exception as e:
812
- logger.error(f"Failed to download xls2xform-medic: {str(e)}")
851
+ logger.error(f"Failed to extract ODK Validate JAR: {str(e)}")
813
852
  return None
814
853
 
815
854
  def tricc_operation_zscore(self, ref_expressions):
@@ -836,7 +875,7 @@ class XLSFormCHTStrategy(XLSFormCDSSStrategy):
836
875
  self.clean_coalesce(ref_expressions[2])
837
876
  } ,{
838
877
  self.clean_coalesce(ref_expressions[3])
839
- }, true)"""
878
+ }, true"""
840
879
 
841
880
  def tricc_operation_drug_dosage(self, ref_expressions):
842
881
  # drug name
@@ -1758,6 +1758,11 @@ def reorder_node_list(node_list, group, processed_nodes):
1758
1758
  # Check for same group
1759
1759
  if group is not None and node_group and node_group.id == group.id:
1760
1760
  priority += SAME_GROUP_PRIORITY
1761
+ elif (
1762
+ issubclass(node.__class__, TriccNodeDisplayCalculateBase) or
1763
+ isinstance(node, TriccNodeEnd)
1764
+ ) and not isinstance(node, TriccNodeActivityEnd) and hasattr(node, 'prev_nodes') and len(node.prev_nodes) > 0:
1765
+ priority += FLOW_CALCULATE_NODE_PRIORITY
1761
1766
  # Check for parent group
1762
1767
  elif hasattr(group, "group") and group.group and node_group and node_group.id == group.group.id:
1763
1768
  priority += PARENT_GROUP_PRIORITY
@@ -1767,12 +1772,6 @@ def reorder_node_list(node_list, group, processed_nodes):
1767
1772
  # Check for non main activities
1768
1773
  elif activity and isinstance(activity.root, TriccNodeActivityStart):
1769
1774
  priority += NON_START_ACTIVITY_PRIORITY
1770
- # Check for display calculate and end nodes with prev_nodes
1771
- elif (
1772
- issubclass(node.__class__, TriccNodeDisplayCalculateBase) or
1773
- isinstance(node, TriccNodeEnd)
1774
- ) and not isinstance(node, TriccNodeActivityEnd) and hasattr(node, 'prev_nodes') and len(node.prev_nodes) > 0:
1775
- priority += FLOW_CALCULATE_NODE_PRIORITY
1776
1775
  # Check for active activities (lower priority)
1777
1776
  elif activity and activity in active_activities:
1778
1777
  priority += ACTIVE_ACTIVITY_LOWER_PRIORITY
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tricc-oo
3
- Version: 1.6.8
3
+ Version: 1.6.10
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
@@ -8,9 +8,9 @@ tricc_oo/converters/codesystem_to_ocl.py,sha256=Fh7Vk73OsxljZKu1k6H9uzYwz334tpQT
8
8
  tricc_oo/converters/cql_to_operation.py,sha256=PUyV_YpUY98Ox0H_F_CN3UUf_I-BhFZVOcWWKTtwecM,14492
9
9
  tricc_oo/converters/datadictionnary.py,sha256=T2HLCBo4Am1p0kFqSH1r0PqbD8AC2IGuWkbvMvSCru0,3658
10
10
  tricc_oo/converters/drawio_type_map.py,sha256=UCPiGs7Lw0bigKScmZUnmOhACBz-FiDq92jHkI7RTSQ,9113
11
- tricc_oo/converters/tricc_to_xls_form.py,sha256=wsWv4aA0QssY7ry9R7KsuuMzVfovj9fwE3i9AtCum0c,3842
11
+ tricc_oo/converters/tricc_to_xls_form.py,sha256=39hwWgYNitGE-AuKtjUwNLz39tEpwc7nd9gT_gw5wjc,3842
12
12
  tricc_oo/converters/utils.py,sha256=JZrtrvvOfXwdkw49pKauzinOcauWwsy-CVcw36TjyLo,1684
13
- tricc_oo/converters/xml_to_tricc.py,sha256=PEBe8N-JIGJMVX2FO3UVxRcy5GrGsMvgcArEwUXmr6o,39572
13
+ tricc_oo/converters/xml_to_tricc.py,sha256=yqnU5xg1SVoQ8vra0gOvIPwoh80pyTI332y9qJCvcRs,39654
14
14
  tricc_oo/converters/cql/cqlLexer.py,sha256=8HArbRphcrpnAG4uogJ2rHv4tc1WLzjN0B1uFeYILAc,49141
15
15
  tricc_oo/converters/cql/cqlListener.py,sha256=fA7-8DcS2Q69ckwjdg57-OfFHBxjTZFdoSKrtw7Hffc,57538
16
16
  tricc_oo/converters/cql/cqlParser.py,sha256=x3KdrwX9nwENSEJ5Ex7_l5NMnu3kWBO0uLdYu4moTq0,414745
@@ -26,27 +26,27 @@ tricc_oo/parsers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,
26
26
  tricc_oo/parsers/xml.py,sha256=uzkb1y18MHfqVFmZqVh0sKT4cx6u0-NcAT_lV_gHBt8,4208
27
27
  tricc_oo/serializers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
28
  tricc_oo/serializers/planuml.py,sha256=t57587-6L3aDncpHh58lS77Zft8yxDE9DPtXx2BeUSU,132
29
- tricc_oo/serializers/xls_form.py,sha256=L0WF774zFt6PbdqncJGeyZzM9NiLq9NY5vO98yGTLhM,22133
29
+ tricc_oo/serializers/xls_form.py,sha256=_sYYVmJkGJeeOr8l_FzRIOuCcGzrxeFc1APXBgNNs08,22585
30
30
  tricc_oo/strategies/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
31
  tricc_oo/strategies/input/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
32
  tricc_oo/strategies/input/base_input_strategy.py,sha256=BEODXS74na1QRRcJVQ4cxiD8F7uRqaLyhE3QzKpGVvk,3891
33
33
  tricc_oo/strategies/input/drawio.py,sha256=uXAUPhXOeg0Uk_BNqlCqFBW4cWNox4VfH559bj1fhC0,12767
34
34
  tricc_oo/strategies/output/base_output_strategy.py,sha256=i9L5CVUqkEAMNyBsdHJ4xA7Nptr3myHr_fHHveDX1cU,8928
35
- tricc_oo/strategies/output/dhis2_form.py,sha256=jW9NW72_61ch1bHm8ShIH4xsJH-HMlZGPTT5txJxMUk,38278
36
- tricc_oo/strategies/output/fhir_form.py,sha256=hbL921pe1Doun4IQrJuZ_Sq2fCh98G3grYie5olC4uc,15740
35
+ tricc_oo/strategies/output/dhis2_form.py,sha256=O5sBcwG_i6Vx3T5-Be4MmE50CsW4obikS3p8MOOJu1o,49317
36
+ tricc_oo/strategies/output/fhir_form.py,sha256=yDDJqdkt4PvSDEy4kmgKM8V_o757CDlE7813atyyDHM,15749
37
37
  tricc_oo/strategies/output/html_form.py,sha256=qSleEZOMV_-Z04y-i-ucyd5rgAYWAyjPwMrw0IHtCRM,8604
38
38
  tricc_oo/strategies/output/openmrs_form.py,sha256=ne6TwAyhafR-WDs27QTKKFl85VD5sij_VEJtK6ZjOIE,28996
39
39
  tricc_oo/strategies/output/spice.py,sha256=QMeoismVC3PdbvwTK0PtUjWX9jl9780fbQIXn76fMXw,10761
40
40
  tricc_oo/strategies/output/xls_form.py,sha256=_pNTND7n-55EjRphJ1hSVtRYa-UkXlmwpam2OKQ8o_w,30860
41
41
  tricc_oo/strategies/output/xlsform_cdss.py,sha256=X00Lt5MzV8TX14dR4dFI1MqllI5S1e13bKbeysWM9uA,17435
42
- tricc_oo/strategies/output/xlsform_cht.py,sha256=eKAc6LLDnvdZ5m8a2Vk6eAhvPbUvOfykgYr0ou3an9k,27164
42
+ tricc_oo/strategies/output/xlsform_cht.py,sha256=RXacGiVAib5bF_0ea564YhuJ1ASs20MuMuIeZXiP5-M,29019
43
43
  tricc_oo/strategies/output/xlsform_cht_hf.py,sha256=xm6SKirV3nMZvM2w54_zJcXAeAgAkq-EEqGEjnOWv6c,988
44
44
  tricc_oo/visitors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
- tricc_oo/visitors/tricc.py,sha256=8fULbAxKSeEtPyD5TYTaIG8-20Bjg2iZmzxcJM70n18,110295
45
+ tricc_oo/visitors/tricc.py,sha256=9jAVSxFfklJcLzwLvZAV1_sRuD8616HSgOSl8wdqPTE,110227
46
46
  tricc_oo/visitors/utils.py,sha256=j83aAq5s5atXi3OC0jc_uJd54a8XrHHmizeeEbWZQJg,421
47
47
  tricc_oo/visitors/xform_pd.py,sha256=ryAnI3V9x3eTmJ2LNsUZfvl0_yfCqo6oBgeSu-WPqaE,9613
48
- tricc_oo-1.6.8.dist-info/licenses/LICENSE,sha256=Pz2eACSxkhsGfW9_iN60pgy-enjnbGTj8df8O3ebnQQ,16726
49
- tricc_oo-1.6.8.dist-info/METADATA,sha256=g9u0_NdvNT8FFgNB3LQzsUADmGqL1PBB1DPdnCBKVuI,8599
50
- tricc_oo-1.6.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
51
- tricc_oo-1.6.8.dist-info/top_level.txt,sha256=NvbfMNAiy9m4b1unBsqpeOQWh4IgA1Xa33BtKA4abxk,15
52
- tricc_oo-1.6.8.dist-info/RECORD,,
48
+ tricc_oo-1.6.10.dist-info/licenses/LICENSE,sha256=Pz2eACSxkhsGfW9_iN60pgy-enjnbGTj8df8O3ebnQQ,16726
49
+ tricc_oo-1.6.10.dist-info/METADATA,sha256=kiJuIi8qGKOf5IP63NtQYJ7ReSJJHrReUl3fuWEJS18,8600
50
+ tricc_oo-1.6.10.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
51
+ tricc_oo-1.6.10.dist-info/top_level.txt,sha256=NvbfMNAiy9m4b1unBsqpeOQWh4IgA1Xa33BtKA4abxk,15
52
+ tricc_oo-1.6.10.dist-info/RECORD,,