From 3d2c83a62ad92617179f37795da42266e5eed02b Mon Sep 17 00:00:00 2001 From: nnpvaan Date: Mon, 11 May 2026 16:33:54 +1000 Subject: [PATCH 1/2] Added AllowManualEntry for select result options --- src/bika/lims/content/abstractbaseanalysis.py | 9 ++- .../core/browser/modals/analysis/form.py | 71 +++++++++++++++++++ .../analysis/static/css/edit_analysis.css | 3 + .../analysis/static/js/edit_analysis.js | 44 ++++++++++++ .../analysis/templates/edit_analysis.pt | 23 +++++- .../core/exportimport/setupdata/__init__.py | 3 +- 6 files changed, 146 insertions(+), 7 deletions(-) diff --git a/src/bika/lims/content/abstractbaseanalysis.py b/src/bika/lims/content/abstractbaseanalysis.py index 06ebecf4e6..6c58a7e197 100644 --- a/src/bika/lims/content/abstractbaseanalysis.py +++ b/src/bika/lims/content/abstractbaseanalysis.py @@ -721,14 +721,17 @@ def getViewFor(self, instance, idx, subfield, 'ResultOptions', schemata="Result Options", type='resultsoptions', - subfields=('ResultValue', 'ResultText'), + subfields=('ResultValue', 'ResultText', 'AllowManualEntry'), required_subfields=('ResultValue', 'ResultText'), subfield_labels={'ResultValue': _('Result Value'), - 'ResultText': _('Display Value'), }, + 'ResultText': _('Display Value'), + 'AllowManualEntry': _('Allow Manual Entry'), }, + subfield_types={'AllowManualEntry': 'boolean'}, subfield_validators={'ResultValue': 'result_options_value_validator', 'ResultText': 'result_options_text_validator'}, subfield_sizes={'ResultValue': 5, - 'ResultText': 25,}, + 'ResultText': 25, + 'AllowManualEntry': 8,}, subfield_maxlength={'ResultValue': 5, 'ResultText': 255,}, widget=RecordsWidget( diff --git a/src/senaite/core/browser/modals/analysis/form.py b/src/senaite/core/browser/modals/analysis/form.py index 3a1a2dc939..bedaa0cd48 100644 --- a/src/senaite/core/browser/modals/analysis/form.py +++ b/src/senaite/core/browser/modals/analysis/form.py @@ -180,6 +180,64 @@ def get_result_options(self): ) return options + def is_result_option_manual(self, option): + """Returns whether the option should allow manual text input + """ + if not option.get("AllowManualEntry"): + value = str(option.get("ResultValue", "")).strip().lower() + text = str(option.get("ResultText", "")).strip().lower() + return value == "other" or text == "other" + return option.get("AllowManualEntry") + + def get_result_selected_value(self): + """Returns the selected option value for select-type results + + If the current result is custom free text and a manual-entry option + exists, the first manual-entry option is selected. + """ + result = self.get_result() + if not result: + return "" + + options = self.get_result_options() + manual_value = "" + for option in options: + value = option.get("ResultValue", "") + + if value == result: + return result + + if not manual_value and self.is_result_option_manual(option): + manual_value = value + + return manual_value + + def get_result_other_text(self): + """Returns the manual free-text result for select-type results + """ + result = self.get_result() + if not result: + return "" + options = self.get_result_options() + for option in options: + value = option.get("ResultValue", "") + if value == result: + return "" + return result + + def is_result_selected_option_manual(self): + """Returns whether currently selected option allows manual entry + """ + selected = self.get_result_selected_value() + if not selected: + return False + options = self.get_result_options() + for option in options: + value = option.get("ResultValue", "") + if value == selected: + return self.is_result_option_manual(option) + return False + def get_result_values(self): """Returns the current result as a list of values @@ -474,6 +532,19 @@ def handle_submit(self): form_data = self.request.form + # Normalize select + manual-entry option before persisting: + # if selected option allows manual text, persist the free text. + result_value = form_data.get("Result") + if result_value is not None: + result_value = str(result_value) + options = self.get_result_options() + selected_opt = filter( + lambda o: o.get("ResultValue", "") == result_value, + options + ) + if selected_opt and self.is_result_option_manual(selected_opt[0]): + form_data["Result"] = form_data.get("ResultOther", "") + # Map form field names to AT field names field_map = [ ("DetectionLimitOperand", "DetectionLimitOperand"), diff --git a/src/senaite/core/browser/modals/analysis/static/css/edit_analysis.css b/src/senaite/core/browser/modals/analysis/static/css/edit_analysis.css index e988e03641..f4923afa3f 100644 --- a/src/senaite/core/browser/modals/analysis/static/css/edit_analysis.css +++ b/src/senaite/core/browser/modals/analysis/static/css/edit_analysis.css @@ -39,3 +39,6 @@ border-bottom-right-radius: 0; border-right: 0; } +.edit-analysis-form .result-other-input { + max-width: 100%; +} diff --git a/src/senaite/core/browser/modals/analysis/static/js/edit_analysis.js b/src/senaite/core/browser/modals/analysis/static/js/edit_analysis.js index 1bfe69d89b..eb9333e2d0 100644 --- a/src/senaite/core/browser/modals/analysis/static/js/edit_analysis.js +++ b/src/senaite/core/browser/modals/analysis/static/js/edit_analysis.js @@ -172,6 +172,47 @@ filterInstruments(); } + /** + * Toggle manual result text input for select result options. + * + * The selected option controls the visibility by the + * "data-manual-entry" option attribute. + */ + function bindResultManualEntry() { + var selects = document.querySelectorAll( + "#edit-analysis-result-select" + ); + if (!selects.length) { + return; + } + + selects.forEach(function(select) { + var form = select.closest("form"); + if (!form) { + return; + } + var input = form.querySelector( + "#edit-analysis-result-other" + ); + if (!input) { + return; + } + + function toggle() { + var opt = select.options[select.selectedIndex]; + var show = !!opt + && opt.getAttribute("data-manual-entry") === "1"; + input.style.display = show ? "" : "none"; + if (!show) { + input.value = ""; + } + } + + select.addEventListener("change", toggle); + toggle(); + }); + } + // Initialize multiselect-duplicates document.querySelectorAll(".multiselect-duplicates") .forEach(function(el) { @@ -192,4 +233,7 @@ // Initialize method-instrument filtering bindMethodInstrumentFilter(); + + // Initialize manual-entry toggle for select results + bindResultManualEntry(); })(); diff --git a/src/senaite/core/browser/modals/analysis/templates/edit_analysis.pt b/src/senaite/core/browser/modals/analysis/templates/edit_analysis.pt index 7bf8a0925d..bbbdc49527 100644 --- a/src/senaite/core/browser/modals/analysis/templates/edit_analysis.pt +++ b/src/senaite/core/browser/modals/analysis/templates/edit_analysis.pt @@ -80,16 +80,19 @@ + diff --git a/src/senaite/core/exportimport/setupdata/__init__.py b/src/senaite/core/exportimport/setupdata/__init__.py index 72f5855c0a..5a06637b23 100644 --- a/src/senaite/core/exportimport/setupdata/__init__.py +++ b/src/senaite/core/exportimport/setupdata/__init__.py @@ -1487,7 +1487,8 @@ def load_result_options(self): return sro = service.getResultOptions() sro.append({'ResultValue': row['ResultValue'], - 'ResultText': row['ResultText']}) + 'ResultText': row['ResultText'], + 'AllowManualEntry': row['AllowManualEntry']}) service.setResultOptions(sro) def load_service_uncertainties(self): From 3d770b72328baafca89901259501b7481c8de4bf Mon Sep 17 00:00:00 2001 From: nnpvaan Date: Mon, 11 May 2026 16:35:52 +1000 Subject: [PATCH 2/2] Changelog --- CHANGES.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.rst b/CHANGES.rst index d16d98216b..8644d7cb98 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,7 @@ Changelog 2.7.0 (unreleased) ------------------ +#2898 Add AllowManualEntry for select result options - #2894 Fix DL get flushed on submit - #2891 Translate sidebar navigation titles in JSON endpoint - #2881 Add text support for interim fields