From 9472b6efd3dc113d053a1d2b43a7e7213fe642dd Mon Sep 17 00:00:00 2001 From: Mohammed Irfan Date: Sat, 4 Oct 2025 14:48:50 +0530 Subject: [PATCH] fix: Allow observations with components(group observations) as components in observation template fix: Approve/Reject changes for nested observations --- .../doctype/observation/observation.html | 3 + .../doctype/observation/observation.py | 44 ++++-- .../observation_template.js | 7 - .../observation_template.py | 60 +++++++++ healthcare/healthcare/utils.py | 99 ++++++++++---- healthcare/public/js/observation_widget.js | 125 +++++++++++------- 6 files changed, 249 insertions(+), 89 deletions(-) diff --git a/healthcare/healthcare/doctype/observation/observation.html b/healthcare/healthcare/doctype/observation/observation.html index f3d85b307c..ff606418c5 100644 --- a/healthcare/healthcare/doctype/observation/observation.html +++ b/healthcare/healthcare/doctype/observation/observation.html @@ -245,6 +245,8 @@ {% for comps in data[data.get("observation")] %} + {% set comps_dict = comps.get("observation") %} + {% if comps_dict and isinstance(comps_dict, dict) %} {% if comps.get("observation").get("preferred_display_name") %} {% set observation_name = comps.get("observation").get("preferred_display_name") %} {% else %} @@ -345,6 +347,7 @@ {% endif %} {% endif %} + {% endif %} {% endfor %}
{{data.get("description") or ""}} diff --git a/healthcare/healthcare/doctype/observation/observation.py b/healthcare/healthcare/doctype/observation/observation.py index f357ce7ef8..a9078746d1 100644 --- a/healthcare/healthcare/doctype/observation/observation.py +++ b/healthcare/healthcare/doctype/observation/observation.py @@ -98,8 +98,12 @@ def component_has_result(self): component_obs = frappe.db.get_all("Observation", {"parent_observation": self.name}, pluck="name") for obs in component_obs: obs_doc = frappe.get_doc("Observation", obs) - if not obs_doc.has_result(): - return False + if obs_doc.has_component: + if not obs_doc.component_has_result(): + return False + else: + if not obs_doc.has_result(): + return False return True @@ -220,16 +224,28 @@ def return_child_observation_data_as_dict(child_observations, obs, obs_length=0) obs_list = [] has_result = False obs_approved = False + all_children_approved = True for child in child_observations: - if child.get("permitted_data_type"): - obs_length += 1 - if child.get("permitted_data_type") == "Select" and child.get("options"): - child["options_list"] = child.get("options").split("\n") - if child.get("specimen"): - child["received_time"] = frappe.get_value("Specimen", child.get("specimen"), "received_time") - observation_data = {"observation": child} - obs_list.append(observation_data) + if child.get("has_component"): + grand_children = get_child_observations(child) + grand_dict, obs_length = return_child_observation_data_as_dict( + grand_children, child, obs_length + ) + obs_list.append(grand_dict) + if not grand_dict.get("obs_approved", False): + all_children_approved = False + else: + if child.get("permitted_data_type"): + obs_length += 1 + if child.get("permitted_data_type") == "Select" and child.get("options"): + child["options_list"] = child.get("options").split("\n") + if child.get("specimen"): + child["received_time"] = frappe.get_value("Specimen", child.get("specimen"), "received_time") + observation_data = {"observation": child} + obs_list.append(observation_data) + if child.get("status") != "Approved": + all_children_approved = False if ( child.get("result_data") @@ -237,8 +253,9 @@ def return_child_observation_data_as_dict(child_observations, obs, obs_length=0) or child.get("result_select") not in [None, "", "Null"] ): has_result = True - if child.get("status") == "Approved": - obs_approved = True + + if all_children_approved and child_observations: + obs_approved = True obs_dict = { "has_component": True, @@ -495,11 +512,10 @@ def set_observation_status(observation, status, reason=None, parent_obs=None): parent_obs = new_doc.name if observation_doc.has_component: - docstatus_filter = 0 if status == "Approved" else 1 component_obs = frappe.db.get_all( "Observation", - filters={"parent_observation": observation, "docstatus": docstatus_filter}, + filters={"parent_observation": observation}, pluck="name", ) diff --git a/healthcare/healthcare/doctype/observation_template/observation_template.js b/healthcare/healthcare/doctype/observation_template/observation_template.js index fab3db20a2..615c924429 100644 --- a/healthcare/healthcare/doctype/observation_template/observation_template.js +++ b/healthcare/healthcare/doctype/observation_template/observation_template.js @@ -28,13 +28,6 @@ frappe.ui.form.on("Observation Template", { }, refresh: function(frm) { - frm.set_query("observation_template", "observation_component", function () { - return { - "filters": { - "has_component": 0, - } - }; - }); frm.set_query("method", function () { return { "filters": { diff --git a/healthcare/healthcare/doctype/observation_template/observation_template.py b/healthcare/healthcare/doctype/observation_template/observation_template.py index af4b0825e0..591058e5bf 100644 --- a/healthcare/healthcare/doctype/observation_template/observation_template.py +++ b/healthcare/healthcare/doctype/observation_template/observation_template.py @@ -31,6 +31,8 @@ def on_update(self): if not self.item and self.is_billable: create_item_from_template(self) + MAX_NESTING_LEVEL = 3 + def validate(self): if self.has_component and self.sample_collection_required: self.sample_collection_required = 0 @@ -43,9 +45,67 @@ def validate(self): if self.has_component: self.abbr = "" + + # Prevent self-referencing + for row in self.observation_component: + if row.observation_template == self.name: + frappe.throw( + _("Observation Template '{0}' cannot be added as a component of itself.").format(self.name) + ) + + # Prevent circular / nested self-reference + for row in self.observation_component: + if ObservationTemplate.is_parent_in_child(row.observation_template, self.name): + frappe.throw( + _( + "Circular reference detected: '{0}' is already a component (directly or indirectly) of '{1}'" + ).format(self.name, row.observation_template) + ) + + child_depth = ObservationTemplate.get_nesting_depth(row.observation_template) + + if child_depth >= ObservationTemplate.MAX_NESTING_LEVEL: + frappe.throw( + _( + "You cannot add '{0}' because it already contains {1} levels of nested components. The maximum allowed depth is {2}." + ).format( + row.observation_template, child_depth, ObservationTemplate.MAX_NESTING_LEVEL + ) + ) else: self.validate_abbr() + @staticmethod + def is_parent_in_child(child_name, parent_name): + """Check recursively if parent_name exists as a component inside child_name""" + child_doc = frappe.get_doc("Observation Template", child_name) + if not child_doc.has_component: + return False + + for comp in child_doc.observation_component: + if comp.observation_template == parent_name: + return True + # recursive check (grandchildren) + if ObservationTemplate.is_parent_in_child(comp.observation_template, parent_name): + return True + + return False + + @staticmethod + def get_nesting_depth(template_name, current_level=0): + # Recursively count how deep the component hierarchy goes + template = frappe.get_doc("Observation Template", template_name) + if not template.has_component: + return current_level + + max_depth = current_level + for comp in template.observation_component: + depth = ObservationTemplate.get_nesting_depth(comp.observation_template, current_level + 1) + if depth > max_depth: + max_depth = depth + + return max_depth + def validate_abbr(self): if not self.abbr: self.abbr = frappe.utils.get_abbr(self.observation) diff --git a/healthcare/healthcare/utils.py b/healthcare/healthcare/utils.py index a1a0dd968e..6506695c11 100644 --- a/healthcare/healthcare/utils.py +++ b/healthcare/healthcare/utils.py @@ -1208,17 +1208,31 @@ def insert_diagnostic_report(doc, patient, sample_collection=None): diagnostic_report.save(ignore_permissions=True) -def insert_observation_and_sample_collection(doc, patient, grp, sample_collection, child=None): +def insert_observation_and_sample_collection( + doc, patient, grp, sample_collection, child=None, parent_observation=None +): diag_report_required = False if grp.get("has_component"): diag_report_required = True # parent observation - parent_observation = add_observation( + current_parent_observation = add_observation( patient=patient, template=grp.get("name"), practitioner=doc.ref_practitioner, invoice=doc.name, child=child if child else "", + parent=parent_observation, + ) + sample_collection.append( + "observation_sample_collection", + { + "observation_template": grp.get("name"), + "container_closure_color": grp.get("container_closure_color"), + "sample": grp.get("sample"), + "sample_type": grp.get("sample_type"), + "component_observation_parent": current_parent_observation, + "reference_child": child if child else "", + }, ) sample_reqd_component_obs, non_sample_reqd_component_obs = get_observation_template_details( @@ -1228,27 +1242,66 @@ def insert_observation_and_sample_collection(doc, patient, grp, sample_collectio if len(non_sample_reqd_component_obs) > 0: for comp in non_sample_reqd_component_obs: - add_observation( - patient=patient, - template=comp, - practitioner=doc.ref_practitioner, - parent=parent_observation, - invoice=doc.name, - child=child if child else "", + comp_details = frappe.get_value( + "Observation Template", + comp, + [ + "name", + "has_component", + "sample_collection_required", + "sample", + "sample_type", + "container_closure_color", + ], + as_dict=True, ) - # create sample_colleciton child row for sample_collection_reqd grouped templates + if comp_details.get("has_component"): + # recurse if component is also a template with components + sample_collection, diag_report_required = insert_observation_and_sample_collection( + doc, + patient, + comp_details, + sample_collection, + child, + parent_observation=current_parent_observation, + ) + else: + add_observation( + patient=patient, + template=comp, + practitioner=doc.ref_practitioner, + parent=current_parent_observation, + invoice=doc.name, + child=child if child else "", + ) + # create sample_colleciton child row for sample_collection_reqd grouped templates if len(sample_reqd_component_obs) > 0: - sample_collection.append( - "observation_sample_collection", - { - "observation_template": grp.get("name"), - "container_closure_color": grp.get("color"), - "sample": grp.get("sample"), - "sample_type": grp.get("sample_type"), - "component_observation_parent": parent_observation, - "reference_child": child if child else "", - }, - ) + for comp in sample_reqd_component_obs: + comp_details = frappe.get_value( + "Observation Template", + comp, + [ + "name", + "has_component", + "sample_collection_required", + "sample", + "sample_type", + "container_closure_color", + ], + as_dict=True, + ) + if comp_details.get("has_component"): + # recurse into nested template + sub_sc, sub_drc = insert_observation_and_sample_collection( + doc, + patient, + comp_details, + sample_collection, + child, + parent_observation=current_parent_observation, + ) + sample_collection = sub_sc + diag_report_required = diag_report_required or sub_drc else: diag_report_required = True @@ -1262,12 +1315,12 @@ def insert_observation_and_sample_collection(doc, patient, grp, sample_collectio child=child if child else "", ) else: - # create sample_colleciton child row for sample_collection_reqd individual templates + # create sample_colleciton child row for sample_collection_reqd individual templates sample_collection.append( "observation_sample_collection", { "observation_template": grp.get("name"), - "container_closure_color": grp.get("color"), + "container_closure_color": grp.get("container_closure_color"), "sample": grp.get("sample"), "sample_type": grp.get("sample_type"), "reference_child": child if child else "", diff --git a/healthcare/public/js/observation_widget.js b/healthcare/public/js/observation_widget.js index e24fbe47a5..66f9d77f86 100644 --- a/healthcare/public/js/observation_widget.js +++ b/healthcare/public/js/observation_widget.js @@ -8,59 +8,25 @@ healthcare.ObservationWidget = class { init_widget() { var me = this; + + const is_approved = me.data.obs_approved; + var btn_action = is_approved ? "Rejected" : "Approved"; + var btn_label = is_approved ? "Reject" : "Approve"; + if (me.data.has_component || me.data.has_component == "true") { - if (!me.wrapper.find(`.${me.data.observation}`).length==0) { - return + if (me.wrapper.find(`.${me.data.observation}`).length!==0) { + return; } + me.render_parent_observation(me.data, btn_label) - const is_approved = me.data.obs_approved; - var btn_action = is_approved ? "Rejected" : "Approved"; - var btn_label = is_approved ? "Reject" : "Approve"; - - let grouped_html = ( - `
- - - ${me.data.display_name} - - -
- -
-
`) - me.wrapper.append(grouped_html) let component_wrapper = me.wrapper.find(`.${me.data.observation}`) for(var j=0, k=me.data[me.data.observation].length; j -
`) - - me.init_field_group(obs_data, component_wrapper.find(`.observations-${obs_data.name}`)) + var nested_obs = me.data[me.data.observation][j] + me.render_component_observations(component_wrapper, obs_data, nested_obs, btn_action) } } else { - if (!me.wrapper.find(`.observations-${me.data.observation.name}`).length==0) { + if (me.wrapper.find(`.children-${me.data.observation.name}`).length!==0) { return } me.wrapper.append( @@ -102,6 +68,75 @@ healthcare.ObservationWidget = class { } } + render_parent_observation(me_data, btn_label, parent_wrapper=null) { + let grouped_html = ( + `
+ + + ${me_data.display_name} + + +
+ +
+
+
`) + if (parent_wrapper) { + parent_wrapper.append(grouped_html); + } else { + this.wrapper.append(grouped_html); + } + } + + render_component_observations(component_wrapper, obs_data, nested_obs, btn_action) { + var me = this; + if (nested_obs.has_component) { + const is_approved = nested_obs.obs_approved; + var btn_action = is_approved ? "Rejected" : "Approved"; + var btn_label = is_approved ? "Reject" : "Approve"; + + me.render_parent_observation(nested_obs, btn_label, component_wrapper) + let current_wrapper = component_wrapper.find(`.children-${nested_obs.observation}`); + for (var j = 0, k = nested_obs[nested_obs.observation].length; j < k; j++) { + var child_obs = nested_obs[nested_obs.observation][j].observation; + var child_nested_obs = nested_obs[nested_obs.observation][j]; + this.render_component_observations(current_wrapper, child_obs, child_nested_obs, btn_action); + } + } else { + component_wrapper.append(`
+
`) + me.init_field_group(obs_data, component_wrapper.find(`.observations-${obs_data.name}`)) + } + var authbutton = document.getElementById(`authorise-observation-btn-${nested_obs.observation}`); + if (authbutton) { + authbutton.addEventListener("click", function() { + me.auth_observation(nested_obs.observation, btn_action) + }); + } + } + init_field_group(obs_data, wrapper) { var me = this; var default_input = ""