diff --git a/CHANGES.rst b/CHANGES.rst index 6b486dd92d..88bde1ccd1 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,7 @@ Changelog 2.5.0 (unreleased) ------------------ +- #2275 Fix wrong result when both "Result options" and "String" are enabled - #2273 Improve performance for sample listing index - #2272 Allow to configure the position of additional value columns diff --git a/src/bika/lims/content/abstractanalysis.py b/src/bika/lims/content/abstractanalysis.py index e77c9d52e2..69827a295d 100644 --- a/src/bika/lims/content/abstractanalysis.py +++ b/src/bika/lims/content/abstractanalysis.py @@ -857,28 +857,8 @@ def getFormattedResult(self, specs=None, decimalmark='.', sciformat=1, escaped: e.g: '<' and '>' (LDL and UDL for results like < 23.4). """ result = self.getResult() - - # 0: The result is a StringResult, return without any formatting - string_result = self.getStringResult() is True - if string_result: - # return without further result handling - return result - - # 1. The result is a detection limit, return '< LDL' or '> UDL' - dl = self.getDetectionLimitOperand() - if dl: - try: - res = api.float_to_string(float(result)) - fdm = formatDecimalMark(res, decimalmark) - hdl = cgi.escape(dl) if html else dl - return '%s %s' % (hdl, fdm) - except (TypeError, ValueError): - logger.warn( - "The result for the analysis %s is a detection limit, " - "but not floatable: %s" % (self.id, result)) - return formatDecimalMark(result, decimalmark=decimalmark) - # 2. Print ResultText of matching ResultOptions + # If result options, return text of matching option choices = self.getResultOptions() if choices: # Create a dict for easy mapping of result options @@ -900,14 +880,31 @@ def getFormattedResult(self, specs=None, decimalmark='.', sciformat=1, except (ValueError, TypeError): pass - # 3. If the result is not floatable, return it without being formatted + # If string result, return without any formatting + if self.getStringResult(): + return result + + # If a detection limit, return '< LDL' or '> UDL' + dl = self.getDetectionLimitOperand() + if dl: + try: + res = api.float_to_string(float(result)) + fdm = formatDecimalMark(res, decimalmark) + hdl = cgi.escape(dl) if html else dl + return '%s %s' % (hdl, fdm) + except (TypeError, ValueError): + logger.warn( + "The result for the analysis %s is a detection limit, " + "but not floatable: %s" % (self.id, result)) + return formatDecimalMark(result, decimalmark=decimalmark) + + # If not floatable, return without any formatting try: result = float(result) except (TypeError, ValueError): return formatDecimalMark(result, decimalmark=decimalmark) - # 4. If the analysis specs has enabled hidemin or hidemax and the - # result is out of range, render result as 'max' + # If specs are set, evaluate if out of range specs = specs if specs else self.getResultsRange() hidemin = specs.get('hidemin', '') hidemax = specs.get('hidemax', '') @@ -920,17 +917,17 @@ def getFormattedResult(self, specs=None, decimalmark='.', sciformat=1, except (TypeError, ValueError): abovemax = False - # 4.1. If result is below min and hidemin enabled, return 'max' + # If above max and hidemax enabled, return '>max' if abovemax: fdm = formatDecimalMark('> %s' % hidemax, decimalmark) return fdm.replace('> ', '> ', 1) if html else fdm - # Below Lower Detection Limit (LDL)? + # If below LDL, return '< LDL' ldl = self.getLowerDetectionLimit() ldl = api.to_float(ldl, 0.0) if result < ldl: @@ -939,7 +936,7 @@ def getFormattedResult(self, specs=None, decimalmark='.', sciformat=1, fdm = formatDecimalMark('< %s' % ldl, decimalmark) return fdm.replace('< ', '< ', 1) if html else fdm - # Above Upper Detection Limit (UDL)? + # If above UDL, return '< UDL' udl = self.getUpperDetectionLimit() udl = api.to_float(udl, 0.0) if result > udl: diff --git a/src/senaite/core/tests/doctests/ResultOptions.rst b/src/senaite/core/tests/doctests/ResultOptions.rst new file mode 100644 index 0000000000..b8c5d4859f --- /dev/null +++ b/src/senaite/core/tests/doctests/ResultOptions.rst @@ -0,0 +1,146 @@ +Result Options +-------------- + +An analysis can be configured so a selection list with options are displayed +for selection rather than an input text for manual introduction of a value. + +Running this test from the buildout directory:: + + bin/test test_textual_doctests -t ResultOptions + + +Test Setup +.......... + +Needed Imports: + + >>> from bika.lims import api + >>> from bika.lims.utils.analysisrequest import create_analysisrequest + >>> from bika.lims.workflow import doActionFor as do_action_for + >>> from DateTime import DateTime + >>> from plone.app.testing import setRoles + >>> from plone.app.testing import TEST_USER_ID + >>> from plone.app.testing import TEST_USER_PASSWORD + +Functional Helpers: + + >>> def start_server(): + ... from Testing.ZopeTestCase.utils import startZServer + ... ip, port = startZServer() + ... return "http://{}:{}/{}".format(ip, port, portal.id) + + >>> def timestamp(format="%Y-%m-%d"): + ... return DateTime().strftime(format) + + >>> def new_sample(services): + ... values = { + ... 'Client': client.UID(), + ... 'Contact': contact.UID(), + ... 'DateSampled': date_now, + ... 'SampleType': sampletype.UID()} + ... service_uids = map(api.get_uid, services) + ... ar = create_analysisrequest(client, request, values, service_uids) + ... transitioned = do_action_for(ar, "receive") + ... return ar + + >>> def get_analysis(sample, service): + ... service_uid = api.get_uid(service) + ... for analysis in sample.getAnalyses(full_objects=True): + ... if analysis.getServiceUID() == service_uid: + ... return analysis + ... return None + + >>> def submit_analyses(ar, result="13"): + ... for analysis in ar.getAnalyses(full_objects=True): + ... analysis.setResult(result) + ... do_action_for(analysis, "submit") + +Variables: + + >>> portal = self.portal + >>> request = self.request + >>> setup = api.get_setup() + >>> date_now = DateTime().strftime("%Y-%m-%d") + +We need to create some basic objects for the test: + + >>> setRoles(portal, TEST_USER_ID, ['LabManager',]) + >>> client = api.create(portal.clients, "Client", Name="Happy Hills", ClientID="HH", MemberDiscountApplies=True) + >>> contact = api.create(client, "Contact", Firstname="Rita", Lastname="Mohale") + >>> sampletype = api.create(setup.bika_sampletypes, "SampleType", title="Water", Prefix="W") + >>> labcontact = api.create(setup.bika_labcontacts, "LabContact", Firstname="Lab", Lastname="Manager") + >>> department = api.create(setup.bika_departments, "Department", title="Chemistry", Manager=labcontact) + >>> category = api.create(setup.bika_analysiscategories, "AnalysisCategory", title="Metals", Department=department) + >>> Cu = api.create(setup.bika_analysisservices, "AnalysisService", title="Copper", Keyword="Cu", Price="15", Category=category.UID(), Accredited=True) + >>> Fe = api.create(setup.bika_analysisservices, "AnalysisService", title="Iron", Keyword="Fe", Price="10", Category=category.UID()) + >>> Au = api.create(setup.bika_analysisservices, "AnalysisService", title="Gold", Keyword="Au", Price="20", Category=category.UID()) + >>> Zn = api.create(setup.bika_analysisservices, "AnalysisService", title="Zinc", Keyword="Zn", Price="20", Category=category.UID()) + +Apply result options to the services: + + >>> options = [ + ... {"ResultValue": "0", "ResultText": "Zero"}, + ... {"ResultValue": "1", "ResultText": "One"}, + ... {"ResultValue": "2", "ResultText": "Two"}, + ... {"ResultValue": "3", "ResultText": "Three"}, + ... ] + >>> services = [Cu, Fe, Au, Zn] + >>> for service in services: + ... service.setResultOptions(options) + +And a different control type for each service + + >>> Cu.setResultOptionsType("select") + >>> Fe.setResultOptionsType("multiselect") + >>> Au.setResultOptionsType("multiselect_duplicates") + >>> Zn.setResultOptionsType("multichoice") + +Test formatted result +..................... + +The system returns the option text as the formatted result: + + >>> sample = new_sample([Cu, Fe, Au, Zn]) + + >>> cu = get_analysis(sample, Cu) + >>> cu.setResult('0') + >>> cu.getResult() + '0' + >>> cu.getFormattedResult() + 'Zero' + + >>> fe = get_analysis(sample, Fe) + >>> fe.setResult(['0', '1']) + >>> fe.getResult() + '["0", "1"]' + >>> fe.getFormattedResult() + 'Zero
One' + + >>> au = get_analysis(sample, Au) + >>> au.setResult(['0', '1', '1']) + >>> au.getResult() + '["0", "1", "1"]' + >>> au.getFormattedResult() + 'Zero
One
One' + + >>> zn = get_analysis(sample, Zn) + >>> zn.setResult(['0', '1']) + >>> zn.getResult() + '["0", "1"]' + >>> zn.getFormattedResult() + 'Zero
One' + +Even if the analysis has the "String result" setting enabled: + + >>> analyses = [cu, fe, au, zn] + >>> for analysis in analyses: + ... analysis.setStringResult(True) + + >>> cu.getFormattedResult() + 'Zero' + >>> fe.getFormattedResult() + 'Zero
One' + >>> au.getFormattedResult() + 'Zero
One
One' + >>> zn.getFormattedResult() + 'Zero
One'