Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consider analyses with result options or string result in duplicate valid range #1516

Merged
merged 9 commits into from
Feb 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Changelog

**Added**

- #1516 Consider analyses with result options or string in duplicate valid range
- #1515 Moved Setup View into Core
- #1506 Specification non-compliant viewlet in Sample
- #1506 Sample results ranges out-of-date viewlet in Sample
Expand All @@ -22,7 +23,6 @@ Changelog
- #1483 Added Accredited symbol in Analyses listings
- #1466 Support for "readonly" and "hidden" visibility modes in ReferenceWidget


**Changed**

- #1513 Better Ajax Loader for Sample Add Form
Expand All @@ -35,6 +35,7 @@ Changelog

**Removed**

- #1516 Removed getResultsRange metadata from analysis_catalog
- #1487 Dexterity Compatible Catalog Base Class
- #1482 Remove `senaite.instruments` dependency for instrument import form
- #1478 Remove AcquireFieldDefaults (was used for CCEmails field only)
Expand Down
42 changes: 40 additions & 2 deletions bika/lims/api/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
IResultOutOfRange
from zope.component._api import getAdapters

from bika.lims.interfaces import IDuplicateAnalysis
from bika.lims.interfaces import ISubmitted
from bika.lims.interfaces.analysis import IRequestAnalysis


Expand Down Expand Up @@ -56,10 +58,41 @@ def is_out_of_range(brain_or_object, result=_marker):

if result is _marker:
result = api.safe_getattr(analysis, "getResult", None)
if not api.is_floatable(result):
# Result is empty/None or not a valid number

if result in [None, '']:
# Empty result
return False, False

if IDuplicateAnalysis.providedBy(analysis):
# Result range for duplicate analyses is calculated from the original
# result, applying a variation % in shoulders. If the analysis has
# result options enabled or string results enabled, system returns an
# empty result range for the duplicate: result must match %100 with the
# original result
original = analysis.getAnalysis()
original_result = original.getResult()

# Does original analysis have a valid result?
if original_result in [None, '']:
return False, False

# Does original result type matches with duplicate result type?
if api.is_floatable(result) != api.is_floatable(original_result):
return True, True

# Does analysis has result options enabled or non-floatable?
if analysis.getResultOptions() or not api.is_floatable(original_result):
# Let's always assume the result is 'out from shoulders', cause we
# consider the shoulders are precisely the duplicate variation %
out_of_range = original_result != result
return out_of_range, out_of_range

elif not api.is_floatable(result):
# A non-duplicate with non-floatable result. There is no chance to know
# if the result is out-of-range
return False, False

# Convert result to a float
result = api.to_float(result)

# Note that routine analyses, duplicates and reference analyses all them
Expand Down Expand Up @@ -159,6 +192,11 @@ def is_result_range_compliant(analysis):
if not IRequestAnalysis.providedBy(analysis):
return True

if IDuplicateAnalysis.providedBy(analysis):
# Does not make sense to apply compliance to a duplicate, cause its
# valid range depends on the result of the original analysis
return True

rr = analysis.getResultsRange()
service_uid = rr.get("uid", None)
if not api.is_uid(service_uid):
Expand Down
51 changes: 31 additions & 20 deletions bika/lims/browser/analyses/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,8 @@ def folderitem(self, obj, item, index):
self._folder_item_detection_limits(obj, item)
# Fill Specifications
self._folder_item_specifications(obj, item)
self._folder_item_out_of_range(obj, item)
self._folder_item_result_range_compliance(obj, item)
# Fill Partition
self._folder_item_partition(obj, item)
# Fill Due Date and icon if late/overdue
Expand Down Expand Up @@ -1024,17 +1026,18 @@ def _folder_item_detection_limits(self, analysis_brain, item):

def _folder_item_specifications(self, analysis_brain, item):
"""Set the results range to the item passed in"""
# Everyone can see valid-ranges
item['Specification'] = ''
results_range = analysis_brain.getResultsRange
if not results_range:
return
analysis = self.get_object(analysis_brain)
results_range = analysis.getResultsRange()

# Display the specification interval
item["Specification"] = get_formatted_interval(results_range, "")
item["Specification"] = ""
if results_range:
item["Specification"] = get_formatted_interval(results_range, "")

# Show an icon if out of range
out_range, out_shoulders = is_out_of_range(analysis_brain)
def _folder_item_out_of_range(self, analysis_brain, item):
"""Displays an icon if result is out of range
"""
analysis = self.get_object(analysis_brain)
out_range, out_shoulders = is_out_of_range(analysis)
if out_range:
msg = _("Result out of range")
img = get_image("exclamation.png", title=msg)
Expand All @@ -1043,17 +1046,25 @@ def _folder_item_specifications(self, analysis_brain, item):
img = get_image("warning.png", title=msg)
self._append_html_element(item, "Result", img)

# Show an icon if the analysis range is different from the Sample spec
if IAnalysisRequest.providedBy(self.context):
analysis = self.get_object(analysis_brain)
if not is_result_range_compliant(analysis):
service_uid = analysis_brain.getServiceUID
original = self.context.getResultsRange(search_by=service_uid)
original = get_formatted_interval(original, "")
msg = _("Result range is different from Specification: {}"
.format(original))
img = get_image("warning.png", title=msg)
self._append_html_element(item, "Specification", img)
def _folder_item_result_range_compliance(self, analysis_brain, item):
"""Displays an icon if the range is different from the results ranges
defined in the Sample
"""
if not IAnalysisRequest.providedBy(self.context):
return

analysis = self.get_object(analysis_brain)
if is_result_range_compliant(analysis):
return

# Non-compliant range, display an icon
service_uid = analysis_brain.getServiceUID
original = self.context.getResultsRange(search_by=service_uid)
original = get_formatted_interval(original, "")
msg = _("Result range is different from Specification: {}"
.format(original))
img = get_image("warning.png", title=msg)
self._append_html_element(item, "Specification", img)

def _folder_item_verify_icons(self, analysis_brain, item):
"""Set the analysis' verification icons to the item passed in.
Expand Down
1 change: 0 additions & 1 deletion bika/lims/catalog/analysis_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,6 @@
"getInstrumentEntryOfResults",
"getAllowedInstrumentUIDs",
"getInstrumentUID",
"getResultsRange",
"getSampleTypeUID",
"getClientOrderNumber",
"getDateReceived",
Expand Down
47 changes: 31 additions & 16 deletions bika/lims/content/duplicateanalysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@
from bika.lims.content.abstractroutineanalysis import schema
from bika.lims.content.analysisspec import ResultsRangeDict
from bika.lims.interfaces import IDuplicateAnalysis
from bika.lims.interfaces import ISubmitted
from bika.lims.interfaces.analysis import IRequestAnalysis
from bika.lims import logger
from bika.lims.workflow import in_state
from bika.lims.workflow.analysis import STATE_RETRACTED, STATE_REJECTED
from zope.interface import implements
Expand Down Expand Up @@ -143,28 +145,41 @@ def getResultsRange(self):
A Duplicate will be out of range if its result does not match with the
result for the parent analysis plus the duplicate variation in % as the
margin error.

If the duplicate is from an analysis with result options and/or string
results enabled (with non-numeric value), returns an empty result range

:return: A dictionary with the keys min and max
:rtype: dict
"""
specs = ResultsRangeDict()
analysis = self.getAnalysis()
if not analysis:
return specs

result = analysis.getResult()
if not api.is_floatable(result):
return specs

specs.min = specs.max = result
result = api.to_float(result)
dup_variation = analysis.getDuplicateVariation()
dup_variation = api.to_float(dup_variation)
# Get the original analysis
original_analysis = self.getAnalysis()
if not original_analysis:
logger.warn("Orphan duplicate: {}".format(repr(self)))
return {}

# Return empty if results option enabled (exact match expected)
if original_analysis.getResultOptions():
return {}

# Return empty if non-floatable (exact match expected)
original_result = original_analysis.getResult()
if not api.is_floatable(original_result):
return {}

# Calculate the min/max based on duplicate variation %
specs = ResultsRangeDict(uid=self.getServiceUID())
dup_variation = original_analysis.getDuplicateVariation()
dup_variation = api.to_float(dup_variation, default=0)
if not dup_variation:
# We expect an exact match
specs.min = specs.max = original_result
return specs

margin = abs(result) * (dup_variation / 100.0)
specs.min = str(result - margin)
specs.max = str(result + margin)
original_result = api.to_float(original_result)
margin = abs(original_result) * (dup_variation / 100.0)
specs.min = str(original_result - margin)
specs.max = str(original_result + margin)
return specs


Expand Down
5 changes: 3 additions & 2 deletions bika/lims/content/instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -482,11 +482,12 @@ def isQCValid(self):
"getReferenceAnalysesGroupID": group_id,}
brains = api.search(query, CATALOG_ANALYSIS_LISTING)
for brain in brains:
results_range = brain.getResultsRange
analysis = api.get_object(brain)
results_range = analysis.getResultsRange()
if not results_range:
continue
# Is out of range?
out_of_range = is_out_of_range(brain)[0]
out_of_range = is_out_of_range(analysis)[0]
if out_of_range:
return False

Expand Down
Loading