Passed
Pull Request — 2.x (#1987)
by
unknown
05:15
created

bika.lims.content.abstractanalysis   F

Complexity

Total Complexity 179

Size/Duplication

Total Lines 1133
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 179
eloc 603
dl 0
loc 1133
rs 1.997
c 0
b 0
f 0

52 Methods

Rating   Name   Duplication   Size   Complexity  
A AbstractAnalysis.getDependents() 0 5 1
A AbstractAnalysis.getUpperDetectionLimit() 0 18 3
A AbstractAnalysis.getDetectionLimits() 0 8 1
B AbstractAnalysis.setDetectionLimitOperand() 0 43 8
A AbstractAnalysis.setUncertainty() 0 13 3
A AbstractAnalysis.isBelowLowerDetectionLimit() 0 16 5
A AbstractAnalysis.getDependencies() 0 9 1
A AbstractAnalysis.getLowerDetectionLimit() 0 18 3
A AbstractAnalysis.getNumberOfRemainingVerifications() 0 7 2
A AbstractAnalysis.isUpperDetectionLimit() 0 6 1
A AbstractAnalysis.getVerificators() 0 11 3
A AbstractAnalysis.isAboveUpperDetectionLimit() 0 16 5
A AbstractAnalysis.getLastVerificator() 0 4 1
A AbstractAnalysis.getNumberOfVerifications() 0 3 1
A AbstractAnalysis.isLowerDetectionLimit() 0 6 1
B AbstractAnalysis.getUncertainty() 0 21 7
C AbstractAnalysis.getDefaultUncertainty() 0 32 9
A AbstractAnalysis.getAllowedMethods() 0 14 2
A AbstractAnalysis.getAnalystName() 0 9 2
A AbstractAnalysis.getInterimValue() 0 14 4
A AbstractAnalysis.getAllowedInstruments() 0 11 2
A AbstractAnalysis.getPrice() 0 13 2
A AbstractAnalysis.getAssignedAnalyst() 0 9 2
A AbstractAnalysis._getExponentialFormatPrecision() 0 9 2
F AbstractAnalysis.getFormattedResult() 0 120 21
F AbstractAnalysis.setResult() 0 59 15
A AbstractAnalysis.getDateSubmitted() 0 7 1
B AbstractAnalysis.getPrecision() 0 37 7
A AbstractAnalysis.getAnalyst() 0 8 2
A AbstractAnalysis.getEarliness() 0 15 2
A AbstractAnalysis.getService() 0 5 1
A AbstractAnalysis.getStartProcessDate() 0 10 1
F AbstractAnalysis.calculateResult() 0 95 18
A AbstractAnalysis.getDuration() 0 20 2
A AbstractAnalysis.getDateVerified() 0 8 1
A AbstractAnalysis.getVATAmount() 0 8 1
A AbstractAnalysis.isMethodAllowed() 0 10 1
A AbstractAnalysis.getServiceUID() 0 4 1
A AbstractAnalysis.getWorksheetUID() 0 8 2
A AbstractAnalysis.getTotalPrice() 0 7 1
A AbstractAnalysis.getRetestOfUID() 0 6 2
A AbstractAnalysis.getSubmittedBy() 0 8 1
A AbstractAnalysis.getExponentialFormatPrecision() 0 49 5
A AbstractAnalysis.getParentURL() 0 8 2
A AbstractAnalysis.getRetest() 0 14 4
A AbstractAnalysis.remove_duplicates() 0 9 4
A AbstractAnalysis.isInstrumentAllowed() 0 10 1
A AbstractAnalysis.isLateAnalysis() 0 10 1
B AbstractAnalysis.setInterimValue() 0 19 6
A AbstractAnalysis.getWorksheet() 0 12 3
A AbstractAnalysis.getLateness() 0 10 1
A AbstractAnalysis.isRetest() 0 4 1

How to fix   Complexity   

Complexity

Complex classes like bika.lims.content.abstractanalysis often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
# -*- coding: utf-8 -*-
2
#
3
# This file is part of SENAITE.CORE.
4
#
5
# SENAITE.CORE is free software: you can redistribute it and/or modify it under
6
# the terms of the GNU General Public License as published by the Free Software
7
# Foundation, version 2.
8
#
9
# This program is distributed in the hope that it will be useful, but WITHOUT
10
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12
# details.
13
#
14
# You should have received a copy of the GNU General Public License along with
15
# this program; if not, write to the Free Software Foundation, Inc., 51
16
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
#
18
# Copyright 2018-2021 by it's authors.
19
# Some rights reserved, see README and LICENSE.
20
21
import cgi
22
import copy
23
import json
24
import math
25
from decimal import Decimal
26
from six import string_types
27
28
from AccessControl import ClassSecurityInfo
29
from bika.lims import api
30
from bika.lims import bikaMessageFactory as _
31
from bika.lims import deprecated
32
from bika.lims import logger
33
from bika.lims import workflow as wf
34
from bika.lims.browser.fields import HistoryAwareReferenceField
35
from bika.lims.browser.fields import InterimFieldsField
36
from bika.lims.browser.fields import ResultRangeField
37
from bika.lims.browser.fields import UIDReferenceField
38
from bika.lims.browser.fields.uidreferencefield import get_backreferences
39
from bika.lims.browser.widgets import RecordsWidget
40
from bika.lims.config import LDL
41
from bika.lims.config import UDL
42
from bika.lims.content.abstractbaseanalysis import AbstractBaseAnalysis
43
from bika.lims.content.abstractbaseanalysis import schema
44
from bika.lims.interfaces import IDuplicateAnalysis
45
from bika.lims.permissions import FieldEditAnalysisResult
46
from bika.lims.utils import drop_trailing_zeros_decimal
47
from bika.lims.utils import formatDecimalMark
48
from bika.lims.utils.analysis import format_numeric_result
49
from bika.lims.utils.analysis import get_significant_digits
50
from bika.lims.workflow import getTransitionActor
51
from bika.lims.workflow import getTransitionDate
52
from DateTime import DateTime
53
from Products.Archetypes.Field import DateTimeField
54
from Products.Archetypes.Field import FixedPointField
55
from Products.Archetypes.Field import IntegerField
56
from Products.Archetypes.Field import StringField
57
from Products.Archetypes.references import HoldingReference
58
from Products.Archetypes.Schema import Schema
59
from Products.CMFCore.permissions import View
60
61
# A link directly to the AnalysisService object used to create the analysis
62
AnalysisService = UIDReferenceField(
63
    'AnalysisService'
64
)
65
66
# Attachments which are added manually in the UI, or automatically when
67
# results are imported from a file supplied by an instrument.
68
Attachment = UIDReferenceField(
69
    'Attachment',
70
    multiValued=1,
71
    allowed_types=('Attachment',)
72
)
73
74
# The final result of the analysis is stored here.  The field contains a
75
# String value, but the result itself is required to be numeric.  If
76
# a non-numeric result is needed, ResultOptions can be used.
77
Result = StringField(
78
    'Result',
79
    read_permission=View,
80
    write_permission=FieldEditAnalysisResult,
81
)
82
83
# When the result is changed, this value is updated to the current time.
84
# Only the most recent result capture date is recorded here and used to
85
# populate catalog values, however the workflow review_history can be
86
# used to get all dates of result capture
87
ResultCaptureDate = DateTimeField(
88
    'ResultCaptureDate'
89
)
90
91
# Returns the retracted analysis this analysis is a retest of
92
RetestOf = UIDReferenceField(
93
    'RetestOf'
94
)
95
96
# If the result is outside of the detection limits of the method or instrument,
97
# the operand (< or >) is stored here.  For routine analyses this is taken
98
# from the Result, if the result entered explicitly startswith "<" or ">"
99
DetectionLimitOperand = StringField(
100
    'DetectionLimitOperand',
101
    read_permission=View,
102
    write_permission=FieldEditAnalysisResult,
103
)
104
105
# The ID of the logged in user who submitted the result for this Analysis.
106
Analyst = StringField(
107
    'Analyst'
108
)
109
110
# The actual uncertainty for this analysis' result, populated from the ranges
111
# specified in the analysis service when the result is submitted.
112
Uncertainty = FixedPointField(
113
    'Uncertainty',
114
    read_permission=View,
115
    write_permission="Field: Edit Result",
116
    precision=10,
117
)
118
119
# transitioned to a 'verified' state. This value is set automatically
120
# when the analysis is created, based on the value set for the property
121
# NumberOfRequiredVerifications from the Analysis Service
122
NumberOfRequiredVerifications = IntegerField(
123
    'NumberOfRequiredVerifications',
124
    default=1
125
)
126
127
# Routine Analyses and Reference Analysis have a versioned link to
128
# the calculation at creation time.
129
Calculation = HistoryAwareReferenceField(
130
    'Calculation',
131
    read_permission=View,
132
    write_permission=FieldEditAnalysisResult,
133
    allowed_types=('Calculation',),
134
    relationship='AnalysisCalculation',
135
    referenceClass=HoldingReference
136
)
137
138
# InterimFields are defined in Calculations, Services, and Analyses.
139
# In Analysis Services, the default values are taken from Calculation.
140
# In Analyses, the default values are taken from the Analysis Service.
141
# When instrument results are imported, the values in analysis are overridden
142
# before the calculation is performed.
143
InterimFields = InterimFieldsField(
144
    'InterimFields',
145
    read_permission=View,
146
    write_permission=FieldEditAnalysisResult,
147
    schemata='Method',
148
    widget=RecordsWidget(
149
        label=_("Calculation Interim Fields"),
150
        description=_(
151
            "Values can be entered here which will override the defaults "
152
            "specified in the Calculation Interim Fields."),
153
    )
154
)
155
156
# Results Range that applies to this analysis
157
ResultsRange = ResultRangeField(
158
    "ResultsRange",
159
    required=0
160
)
161
162
schema = schema.copy() + Schema((
163
    AnalysisService,
164
    Analyst,
165
    Attachment,
166
    DetectionLimitOperand,
167
    # NumberOfRequiredVerifications overrides AbstractBaseClass
168
    NumberOfRequiredVerifications,
169
    Result,
170
    ResultCaptureDate,
171
    RetestOf,
172
    Uncertainty,
173
    Calculation,
174
    InterimFields,
175
    ResultsRange,
176
))
177
178
179
class AbstractAnalysis(AbstractBaseAnalysis):
180
    security = ClassSecurityInfo()
181
    displayContentsTab = False
182
    schema = schema
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable schema does not seem to be defined.
Loading history...
183
184
    @deprecated('[1705] Currently returns the Analysis object itself.  If you '
185
                'need to get the service, use getAnalysisService instead')
186
    @security.public
187
    def getService(self):
188
        return self
189
190
    def getServiceUID(self):
191
        """Return the UID of the associated service.
192
        """
193
        return self.getRawAnalysisService()
194
195
    @security.public
196
    def getNumberOfVerifications(self):
197
        return len(self.getVerificators())
198
199
    @security.public
200
    def getNumberOfRemainingVerifications(self):
201
        required = self.getNumberOfRequiredVerifications()
202
        done = self.getNumberOfVerifications()
203
        if done >= required:
204
            return 0
205
        return required - done
206
207
    # TODO Workflow - analysis . Remove?
208
    @security.public
209
    def getLastVerificator(self):
210
        verifiers = self.getVerificators()
211
        return verifiers and verifiers[-1] or None
212
213
    @security.public
214
    def getVerificators(self):
215
        """Returns the user ids of the users that verified this analysis
216
        """
217
        verifiers = list()
218
        actions = ["verify", "multi_verify"]
219
        for event in wf.getReviewHistory(self):
220
            if event['action'] in actions:
221
                verifiers.append(event['actor'])
222
        sorted(verifiers, reverse=True)
223
        return verifiers
224
225
    @security.public
226
    def getDefaultUncertainty(self, result=None):
227
        """Return the uncertainty value, if the result falls within
228
        specified ranges for the service from which this analysis was derived.
229
        """
230
231
        if result is None:
232
            result = self.getResult()
233
234
        uncertainties = self.getUncertainties()
235
        if uncertainties:
236
            try:
237
                res = float(result)
238
            except (TypeError, ValueError):
239
                # if analysis result is not a number, then we assume in range
240
                return None
241
242
            for d in uncertainties:
243
                _min = float(d['intercept_min'])
244
                _max = float(d['intercept_max'])
245
                if _min <= res and res <= _max:
246
                    if str(d['errorvalue']).strip().endswith('%'):
247
                        try:
248
                            percvalue = float(d['errorvalue'].replace('%', ''))
249
                        except ValueError:
250
                            return None
251
                        uncertainty = res / 100 * percvalue
252
                    else:
253
                        uncertainty = float(d['errorvalue'])
254
255
                    return uncertainty
256
        return None
257
258
    @security.public
259
    def getUncertainty(self, result=None):
260
        """Returns the uncertainty for this analysis and result.
261
        Returns the value from Schema's Uncertainty field if the Service has
262
        the option 'Allow manual uncertainty'. Otherwise, do a callback to
263
        getDefaultUncertainty(). Returns None if no result specified and the
264
        current result for this analysis is below or above detections limits.
265
        """
266
        uncertainty = self.getField('Uncertainty').get(self)
267
        if result is None and (self.isAboveUpperDetectionLimit() or
268
                               self.isBelowLowerDetectionLimit()):
269
            return None
270
271
        if uncertainty and self.getAllowManualUncertainty() is True:
272
            try:
273
                uncertainty = float(uncertainty)
274
                return uncertainty
275
            except (TypeError, ValueError):
276
                # if uncertainty is not a number, return default value
277
                pass
278
        return self.getDefaultUncertainty(result)
279
280
    @security.public
281
    def setUncertainty(self, unc):
282
        """Sets the uncertainty for this analysis. If the result is a
283
        Detection Limit or the value is below LDL or upper UDL, sets the
284
        uncertainty value to 0
285
        """
286
        # Uncertainty calculation on DL
287
        # https://jira.bikalabs.com/browse/LIMS-1808
288
        if self.isAboveUpperDetectionLimit() or \
289
                self.isBelowLowerDetectionLimit():
290
            self.getField('Uncertainty').set(self, None)
291
        else:
292
            self.getField('Uncertainty').set(self, unc)
293
294
    @security.public
295
    def setDetectionLimitOperand(self, value):
296
        """Set detection limit operand for this analysis
297
        Allowed detection limit operands are `<` and `>`.
298
        """
299
        manual_dl = self.getAllowManualDetectionLimit()
300
        selector = self.getDetectionLimitSelector()
301
        if not manual_dl and not selector:
302
            # Don't allow the user to set the limit operand if manual assignment
303
            # is not allowed and selector is not visible
304
            return
305
306
        # Changing the detection limit operand has a side effect on the result
307
        result = self.getResult()
308
        if value in [LDL, UDL]:
309
            # flush uncertainty
310
            self.setUncertainty("")
311
312
            # If no previous result or user is not allowed to manually set the
313
            # the detection limit, override the result with default LDL/UDL
314
            has_result = api.is_floatable(result)
315
            if not has_result or not manual_dl:
316
                # set the result according to the system default UDL/LDL values
317
                if value == LDL:
318
                    result = self.getLowerDetectionLimit()
319
                else:
320
                    result = self.getUpperDetectionLimit()
321
322
        else:
323
            value = ""
324
            # Restore the DetectionLimitSelector, cause maybe its visibility
325
            # was changed because allow manual detection limit was enabled and
326
            # the user set a result with "<" or ">"
327
            if manual_dl:
328
                service = self.getAnalysisService()
329
                selector = service.getDetectionLimitSelector()
330
                self.setDetectionLimitSelector(selector)
331
332
        # Set the result
333
        self.getField("Result").set(self, result)
334
335
        # Set the detection limit to the field
336
        self.getField("DetectionLimitOperand").set(self, value)
337
338
    # Method getLowerDetectionLimit overrides method of class BaseAnalysis
339
    @security.public
340
    def getLowerDetectionLimit(self):
341
        """Returns the Lower Detection Limit (LDL) that applies to this
342
        analysis in particular. If no value set or the analysis service
343
        doesn't allow manual input of detection limits, returns the value set
344
        by default in the Analysis Service
345
        """
346
        if self.isLowerDetectionLimit():
347
            result = self.getResult()
348
            try:
349
                # in this case, the result itself is the LDL.
350
                return float(result)
351
            except (TypeError, ValueError):
352
                logger.warn("The result for the analysis %s is a lower "
353
                            "detection limit, but not floatable: '%s'. "
354
                            "Returnig AS's default LDL." %
355
                            (self.id, result))
356
        return AbstractBaseAnalysis.getLowerDetectionLimit(self)
357
358
    # Method getUpperDetectionLimit overrides method of class BaseAnalysis
359
    @security.public
360
    def getUpperDetectionLimit(self):
361
        """Returns the Upper Detection Limit (UDL) that applies to this
362
        analysis in particular. If no value set or the analysis service
363
        doesn't allow manual input of detection limits, returns the value set
364
        by default in the Analysis Service
365
        """
366
        if self.isUpperDetectionLimit():
367
            result = self.getResult()
368
            try:
369
                # in this case, the result itself is the LDL.
370
                return float(result)
371
            except (TypeError, ValueError):
372
                logger.warn("The result for the analysis %s is a lower "
373
                            "detection limit, but not floatable: '%s'. "
374
                            "Returnig AS's default LDL." %
375
                            (self.id, result))
376
        return AbstractBaseAnalysis.getUpperDetectionLimit(self)
377
378
    @security.public
379
    def isBelowLowerDetectionLimit(self):
380
        """Returns True if the result is below the Lower Detection Limit or
381
        if Lower Detection Limit has been manually set
382
        """
383
        if self.isLowerDetectionLimit():
384
            return True
385
386
        result = self.getResult()
387
        if result and str(result).strip().startswith(LDL):
388
            return True
389
390
        if api.is_floatable(result):
391
            return api.to_float(result) < self.getLowerDetectionLimit()
392
393
        return False
394
395
    @security.public
396
    def isAboveUpperDetectionLimit(self):
397
        """Returns True if the result is above the Upper Detection Limit or
398
        if Upper Detection Limit has been manually set
399
        """
400
        if self.isUpperDetectionLimit():
401
            return True
402
403
        result = self.getResult()
404
        if result and str(result).strip().startswith(UDL):
405
            return True
406
407
        if api.is_floatable(result):
408
            return api.to_float(result) > self.getUpperDetectionLimit()
409
410
        return False
411
412
    @security.public
413
    def getDetectionLimits(self):
414
        """Returns a two-value array with the limits of detection (LDL and
415
        UDL) that applies to this analysis in particular. If no value set or
416
        the analysis service doesn't allow manual input of detection limits,
417
        returns the value set by default in the Analysis Service
418
        """
419
        return [self.getLowerDetectionLimit(), self.getUpperDetectionLimit()]
420
421
    @security.public
422
    def isLowerDetectionLimit(self):
423
        """Returns True if the result for this analysis represents a Lower
424
        Detection Limit. Otherwise, returns False
425
        """
426
        return self.getDetectionLimitOperand() == LDL
427
428
    @security.public
429
    def isUpperDetectionLimit(self):
430
        """Returns True if the result for this analysis represents an Upper
431
        Detection Limit. Otherwise, returns False
432
        """
433
        return self.getDetectionLimitOperand() == UDL
434
435
    @security.public
436
    def getDependents(self):
437
        """Return a list of analyses who depend on us to calculate their result
438
        """
439
        raise NotImplementedError("getDependents is not implemented.")
440
441
    @security.public
442
    def getDependencies(self, with_retests=False):
443
        """Return a list of siblings who we depend on to calculate our result.
444
        :param with_retests: If false, siblings with retests are dismissed
445
        :type with_retests: bool
446
        :return: Analyses the current analysis depends on
447
        :rtype: list of IAnalysis
448
        """
449
        raise NotImplementedError("getDependencies is not implemented.")
450
451
    @security.public
452
    def setResult(self, value):
453
        """Validate and set a value into the Result field, taking into
454
        account the Detection Limits.
455
        :param value: is expected to be a string.
456
        """
457
        prev_result = self.getField("Result").get(self) or ""
458
459
        # Convert to list ff the analysis has result options set with multi
460
        if self.getResultOptions() and "multi" in self.getResultOptionsType():
461
            if not isinstance(value, (list, tuple)):
462
                value = filter(None, [value])
463
464
        # Handle list results
465
        if isinstance(value, (list, tuple)):
466
            value = json.dumps(value)
467
468
        # Ensure result integrity regards to None, empty and 0 values
469
        val = str("" if not value and value != 0 else value).strip()
470
471
        # UDL/LDL directly entered in the results field
472
        if val and val[0] in [LDL, UDL]:
473
            # Result prefixed with LDL/UDL
474
            oper = val[0]
475
            # Strip off LDL/UDL from the result
476
            val = val.replace(oper, "", 1)
477
            # Check if the value is indeterminate / non-floatable
478
            try:
479
                val = float(val)
480
            except (ValueError, TypeError):
481
                val = value
482
483
            # We dismiss the operand and the selector visibility unless the user
484
            # is allowed to manually set the detection limit or the DL selector
485
            # is visible.
486
            allow_manual = self.getAllowManualDetectionLimit()
487
            selector = self.getDetectionLimitSelector()
488
            if allow_manual or selector:
489
                # Ensure visibility of the detection limit selector
490
                self.setDetectionLimitSelector(True)
491
492
                # Set the detection limit operand
493
                self.setDetectionLimitOperand(oper)
494
495
                if not allow_manual:
496
                    # Override value by default DL
497
                    if oper == LDL:
498
                        val = self.getLowerDetectionLimit()
499
                    else:
500
                        val = self.getUpperDetectionLimit()
501
502
        # Update ResultCapture date if necessary
503
        if not val:
504
            self.setResultCaptureDate(None)
505
        elif prev_result != val:
506
            self.setResultCaptureDate(DateTime())
507
508
        # Set the result field
509
        self.getField("Result").set(self, val)
510
511
    @security.public
512
    def calculateResult(self, override=False, cascade=False):
513
        """Calculates the result for the current analysis if it depends of
514
        other analysis/interim fields. Otherwise, do nothing
515
        """
516
        if self.getResult() and override is False:
517
            return False
518
519
        calc = self.getCalculation()
520
        if not calc:
521
            return False
522
523
        # Include the current context UID in the mapping, so it can be passed
524
        # as a param in built-in functions, like 'get_result(%(context_uid)s)'
525
        mapping = {"context_uid": '"{}"'.format(self.UID())}
526
527
        # Interims' priority order (from low to high):
528
        # Calculation < Analysis
529
        interims = calc.getInterimFields() + self.getInterimFields()
530
531
        # Add interims to mapping
532
        for i in interims:
533
534
            interim_keyword = i.get("keyword")
535
            if not interim_keyword:
536
                continue
537
538
            # skip unset values
539
            interim_value = i.get("value", "")
540
            if interim_value == "":
541
                continue
542
543
            # Only floatable and UIDs are supported
544
            if api.is_floatable(interim_value):
545
                interim_value = float(interim_value)
546
547
            elif not api.is_uid(interim_value):
548
                return False
549
550
            mapping[interim_keyword] = interim_value
551
552
        # Add dependencies results to mapping
553
        dependencies = self.getDependencies()
554
        for dependency in dependencies:
555
            result = dependency.getResult()
556
            if not result:
557
                # Dependency without results found
558
                if cascade:
559
                    # Try to calculate the dependency result
560
                    dependency.calculateResult(override, cascade)
561
                    result = dependency.getResult()
562
                else:
563
                    return False
564
            if result:
565
                try:
566
                    result = float(str(result))
567
                    key = dependency.getKeyword()
568
                    ldl = dependency.getLowerDetectionLimit()
569
                    udl = dependency.getUpperDetectionLimit()
570
                    bdl = dependency.isBelowLowerDetectionLimit()
571
                    adl = dependency.isAboveUpperDetectionLimit()
572
                    mapping[key] = result
573
                    mapping['%s.%s' % (key, 'RESULT')] = result
574
                    mapping['%s.%s' % (key, 'LDL')] = ldl
575
                    mapping['%s.%s' % (key, 'UDL')] = udl
576
                    mapping['%s.%s' % (key, 'BELOWLDL')] = int(bdl)
577
                    mapping['%s.%s' % (key, 'ABOVEUDL')] = int(adl)
578
                except (TypeError, ValueError):
579
                    return False
580
581
        # Calculate
582
        formula = calc.getMinifiedFormula()
583
        formula = formula.replace('[', '%(').replace(']', ')f')
584
        try:
585
            formula = eval("'%s'%%mapping" % formula,
586
                           {"__builtins__": None,
587
                            'math': math,
588
                            'context': self},
589
                           {'mapping': mapping})
590
            result = eval(formula, calc._getGlobals())
591
        except TypeError:
592
            self.setResult("NA")
593
            return True
594
        except ZeroDivisionError:
595
            self.setResult('0/0')
596
            return True
597
        except KeyError:
598
            self.setResult("NA")
599
            return True
600
        except ImportError:
601
            self.setResult("NA")
602
            return True
603
604
        self.setResult(str(result))
605
        return True
606
607
    @security.public
608
    def getPrice(self):
609
        """The function obtains the analysis' price without VAT and without
610
        member discount
611
        :return: the price (without VAT or Member Discount) in decimal format
612
        """
613
        analysis_request = self.aq_parent
614
        client = analysis_request.aq_parent
615
        if client.getBulkDiscount():
616
            price = self.getBulkPrice()
617
        else:
618
            price = self.getField('Price').get(self)
619
        return price
620
621
    @security.public
622
    def getVATAmount(self):
623
        """Compute the VAT amount without member discount.
624
        :return: the result as a float
625
        """
626
        vat = self.getVAT()
627
        price = self.getPrice()
628
        return Decimal(price) * Decimal(vat) / 100
629
630
    @security.public
631
    def getTotalPrice(self):
632
        """Obtain the total price without client's member discount. The function
633
        keeps in mind the client's bulk discount.
634
        :return: the result as a float
635
        """
636
        return Decimal(self.getPrice()) + Decimal(self.getVATAmount())
637
638
    @security.public
639
    def getDuration(self):
640
        """Returns the time in minutes taken for this analysis.
641
        If the analysis is not yet 'ready to process', returns 0
642
        If the analysis is still in progress (not yet verified),
643
            duration = date_verified - date_start_process
644
        Otherwise:
645
            duration = current_datetime - date_start_process
646
        :return: time in minutes taken for this analysis
647
        :rtype: int
648
        """
649
        starttime = self.getStartProcessDate()
650
        if not starttime:
651
            # The analysis is not yet ready to be processed
652
            return 0
653
        endtime = self.getDateVerified() or DateTime()
654
655
        # Duration in minutes
656
        duration = (endtime - starttime) * 24 * 60
657
        return duration
658
659
    @security.public
660
    def getEarliness(self):
661
        """The remaining time in minutes for this analysis to be completed.
662
        Returns zero if the analysis is neither 'ready to process' nor a
663
        turnaround time is set.
664
            earliness = duration - max_turnaround_time
665
        The analysis is late if the earliness is negative
666
        :return: the remaining time in minutes before the analysis reaches TAT
667
        :rtype: int
668
        """
669
        maxtime = self.getMaxTimeAllowed()
670
        if not maxtime:
671
            # No Turnaround time is set for this analysis
672
            return 0
673
        return api.to_minutes(**maxtime) - self.getDuration()
674
675
    @security.public
676
    def isLateAnalysis(self):
677
        """Returns true if the analysis is late in accordance with the maximum
678
        turnaround time. If no maximum turnaround time is set for this analysis
679
        or it is not yet ready to be processed, or there is still time
680
        remaining (earliness), returns False.
681
        :return: true if the analysis is late
682
        :rtype: bool
683
        """
684
        return self.getEarliness() < 0
685
686
    @security.public
687
    def getLateness(self):
688
        """The time in minutes that exceeds the maximum turnaround set for this
689
        analysis. If the analysis has no turnaround time set or is not ready
690
        for process yet, returns 0. The analysis is not late if the lateness is
691
        negative
692
        :return: the time in minutes that exceeds the maximum turnaround time
693
        :rtype: int
694
        """
695
        return -self.getEarliness()
696
697
    @security.public
698
    def isInstrumentAllowed(self, instrument):
699
        """Checks if the specified instrument can be set for this analysis,
700
701
        :param instrument: string,Instrument
702
        :return: True if the assignment of the passed in instrument is allowed
703
        :rtype: bool
704
        """
705
        uid = api.get_uid(instrument)
706
        return uid in map(api.get_uid, self.getAllowedInstruments())
707
708
    @security.public
709
    def isMethodAllowed(self, method):
710
        """Checks if the analysis can follow the method specified
711
712
        :param method: string,Method
713
        :return: True if the analysis can follow the method specified
714
        :rtype: bool
715
        """
716
        uid = api.get_uid(method)
717
        return uid in map(api.get_uid, self.getAllowedMethods())
718
719
    @security.public
720
    def getAllowedMethods(self):
721
        """Returns the allowed methods for this analysis, either if the method
722
        was assigned directly (by using "Allows manual entry of results") or
723
        indirectly via Instrument ("Allows instrument entry of results") in
724
        Analysis Service Edit View.
725
        :return: A list with the methods allowed for this analysis
726
        :rtype: list of Methods
727
        """
728
        service = self.getAnalysisService()
729
        if not service:
730
            return []
731
        # get the available methods of the service
732
        return service.getMethods()
733
734
    @security.public
735
    def getAllowedInstruments(self):
736
        """Returns the allowed instruments from the service
737
738
        :return: A list of instruments allowed for this Analysis
739
        :rtype: list of instruments
740
        """
741
        service = self.getAnalysisService()
742
        if not service:
743
            return []
744
        return service.getInstruments()
745
746
    @security.public
747
    def getExponentialFormatPrecision(self, result=None):
748
        """ Returns the precision for the Analysis Service and result
749
        provided. Results with a precision value above this exponential
750
        format precision should be formatted as scientific notation.
751
752
        If the Calculate Precision according to Uncertainty is not set,
753
        the method will return the exponential precision value set in the
754
        Schema. Otherwise, will calculate the precision value according to
755
        the Uncertainty and the result.
756
757
        If Calculate Precision from the Uncertainty is set but no result
758
        provided neither uncertainty values are set, returns the fixed
759
        exponential precision.
760
761
        Will return positive values if the result is below 0 and will return
762
        0 or positive values if the result is above 0.
763
764
        Given an analysis service with fixed exponential format
765
        precision of 4:
766
        Result      Uncertainty     Returns
767
        5.234           0.22           0
768
        13.5            1.34           1
769
        0.0077          0.008         -3
770
        32092           0.81           4
771
        456021          423            5
772
773
        For further details, visit https://jira.bikalabs.com/browse/LIMS-1334
774
775
        :param result: if provided and "Calculate Precision according to the
776
        Uncertainty" is set, the result will be used to retrieve the
777
        uncertainty from which the precision must be calculated. Otherwise,
778
        the fixed-precision will be used.
779
        :returns: the precision
780
        """
781
        if not result or self.getPrecisionFromUncertainty() is False:
782
            return self._getExponentialFormatPrecision()
783
        else:
784
            uncertainty = self.getUncertainty(result)
785
            if uncertainty is None:
786
                return self._getExponentialFormatPrecision()
787
788
            try:
789
                float(result)
790
            except ValueError:
791
                # if analysis result is not a number, then we assume in range
792
                return self._getExponentialFormatPrecision()
793
794
            return get_significant_digits(uncertainty)
795
796
    def _getExponentialFormatPrecision(self):
797
        field = self.getField('ExponentialFormatPrecision')
798
        value = field.get(self)
799
        if value is None:
800
            # https://github.com/bikalims/bika.lims/issues/2004
801
            # We require the field, because None values make no sense at all.
802
            value = self.Schema().getField(
803
                'ExponentialFormatPrecision').getDefault(self)
804
        return value
805
806
    @security.public
807
    def getFormattedResult(self, specs=None, decimalmark='.', sciformat=1,
808
                           html=True):
809
        """Formatted result:
810
        1. If the result is a detection limit, returns '< LDL' or '> UDL'
811
        2. Print ResultText of matching ResultOptions
812
        3. If the result is not floatable, return it without being formatted
813
        4. If the analysis specs has hidemin or hidemax enabled and the
814
           result is out of range, render result as '<min' or '>max'
815
        5. If the result is below Lower Detection Limit, show '<LDL'
816
        6. If the result is above Upper Detecion Limit, show '>UDL'
817
        7. Otherwise, render numerical value
818
        :param specs: Optional result specifications, a dictionary as follows:
819
            {'min': <min_val>,
820
             'max': <max_val>,
821
             'error': <error>,
822
             'hidemin': <hidemin_val>,
823
             'hidemax': <hidemax_val>}
824
        :param decimalmark: The string to be used as a decimal separator.
825
            default is '.'
826
        :param sciformat: 1. The sci notation has to be formatted as aE^+b
827
                          2. The sci notation has to be formatted as a·10^b
828
                          3. As 2, but with super html entity for exp
829
                          4. The sci notation has to be formatted as a·10^b
830
                          5. As 4, but with super html entity for exp
831
                          By default 1
832
        :param html: if true, returns an string with the special characters
833
            escaped: e.g: '<' and '>' (LDL and UDL for results like < 23.4).
834
        """
835
        result = self.getResult()
836
837
        # 1. The result is a detection limit, return '< LDL' or '> UDL'
838
        dl = self.getDetectionLimitOperand()
839
        if dl:
840
            try:
841
                res = float(result)  # required, check if floatable
842
                res = drop_trailing_zeros_decimal(res)
843
                fdm = formatDecimalMark(res, decimalmark)
844
                hdl = cgi.escape(dl) if html else dl
845
                return '%s %s' % (hdl, fdm)
846
            except (TypeError, ValueError):
847
                logger.warn(
848
                    "The result for the analysis %s is a detection limit, "
849
                    "but not floatable: %s" % (self.id, result))
850
                return formatDecimalMark(result, decimalmark=decimalmark)
851
852
        # 2. Print ResultText of matching ResultOptions
853
        choices = self.getResultOptions()
854
        if choices:
855
            # Create a dict for easy mapping of result options
856
            values_texts = dict(map(
857
                lambda c: (str(c["ResultValue"]), c["ResultText"]), choices
858
            ))
859
860
            # Result might contain a single result option
861
            match = values_texts.get(str(result))
862
            if match:
863
                return match
864
865
            # Result might be a string with multiple options e.g. "['2', '1']"
866
            try:
867
                raw_result = json.loads(result)
868
                texts = map(lambda r: values_texts.get(str(r)), raw_result)
0 ignored issues
show
introduced by
The variable values_texts does not seem to be defined in case choices on line 854 is False. Are you sure this can never be the case?
Loading history...
869
                texts = filter(None, texts)
870
                return "<br/>".join(texts)
871
            except (ValueError, TypeError):
872
                pass
873
874
        # 3. If the result is not floatable, return it without being formatted
875
        try:
876
            result = float(result)
877
        except (TypeError, ValueError):
878
            return formatDecimalMark(result, decimalmark=decimalmark)
879
880
        # 4. If the analysis specs has enabled hidemin or hidemax and the
881
        #    result is out of range, render result as '<min' or '>max'
882
        specs = specs if specs else self.getResultsRange()
883
        hidemin = specs.get('hidemin', '')
884
        hidemax = specs.get('hidemax', '')
885
        try:
886
            belowmin = hidemin and result < float(hidemin) or False
887
        except (TypeError, ValueError):
888
            belowmin = False
889
        try:
890
            abovemax = hidemax and result > float(hidemax) or False
891
        except (TypeError, ValueError):
892
            abovemax = False
893
894
        # 4.1. If result is below min and hidemin enabled, return '<min'
895
        if belowmin:
896
            fdm = formatDecimalMark('< %s' % hidemin, decimalmark)
897
            return fdm.replace('< ', '&lt; ', 1) if html else fdm
898
899
        # 4.2. If result is above max and hidemax enabled, return '>max'
900
        if abovemax:
901
            fdm = formatDecimalMark('> %s' % hidemax, decimalmark)
902
            return fdm.replace('> ', '&gt; ', 1) if html else fdm
903
904
        # Below Lower Detection Limit (LDL)?
905
        ldl = self.getLowerDetectionLimit()
906
        if result < ldl:
907
            # LDL must not be formatted according to precision, etc.
908
            # Drop trailing zeros from decimal
909
            ldl = drop_trailing_zeros_decimal(ldl)
910
            fdm = formatDecimalMark('< %s' % ldl, decimalmark)
911
            return fdm.replace('< ', '&lt; ', 1) if html else fdm
912
913
        # Above Upper Detection Limit (UDL)?
914
        udl = self.getUpperDetectionLimit()
915
        if result > udl:
916
            # UDL must not be formatted according to precision, etc.
917
            # Drop trailing zeros from decimal
918
            udl = drop_trailing_zeros_decimal(udl)
919
            fdm = formatDecimalMark('> %s' % udl, decimalmark)
920
            return fdm.replace('> ', '&gt; ', 1) if html else fdm
921
922
        # Render numerical values
923
        return format_numeric_result(self, self.getResult(),
924
                                     decimalmark=decimalmark,
925
                                     sciformat=sciformat)
926
927
    @security.public
928
    def getPrecision(self, result=None):
929
        """Returns the precision for the Analysis.
930
931
        - If ManualUncertainty is set, calculates the precision of the result
932
          in accordance with the manual uncertainty set.
933
934
        - If Calculate Precision from Uncertainty is set in Analysis Service,
935
          calculates the precision in accordance with the uncertainty infered
936
          from uncertainties ranges.
937
938
        - If neither Manual Uncertainty nor Calculate Precision from
939
          Uncertainty are set, returns the precision from the Analysis Service
940
941
        - If you have a number with zero uncertainty: If you roll a pair of
942
        dice and observe five spots, the number of spots is 5. This is a raw
943
        data point, with no uncertainty whatsoever. So just write down the
944
        number. Similarly, the number of centimeters per inch is 2.54,
945
        by definition, with no uncertainty whatsoever. Again: just write
946
        down the number.
947
948
        Further information at AbstractBaseAnalysis.getPrecision()
949
        """
950
        allow_manual = self.getAllowManualUncertainty()
951
        precision_unc = self.getPrecisionFromUncertainty()
952
        if allow_manual or precision_unc:
953
            uncertainty = self.getUncertainty(result)
954
            if uncertainty is None:
955
                return self.getField('Precision').get(self)
956
            if uncertainty == 0 and result is None:
957
                return self.getField('Precision').get(self)
958
            if uncertainty == 0:
959
                strres = str(result)
960
                numdecimals = strres[::-1].find('.')
961
                return numdecimals
962
            return get_significant_digits(uncertainty)
963
        return self.getField('Precision').get(self)
964
965
    @security.public
966
    def getAnalyst(self):
967
        """Returns the stored Analyst or the user who submitted the result
968
        """
969
        analyst = self.getField("Analyst").get(self) or self.getAssignedAnalyst()
970
        if not analyst:
971
            analyst = self.getSubmittedBy()
972
        return analyst or ""
973
974
    @security.public
975
    def getAssignedAnalyst(self):
976
        """Returns the Analyst assigned to the worksheet this
977
        analysis is assigned to
978
        """
979
        worksheet = self.getWorksheet()
980
        if not worksheet:
981
            return ""
982
        return worksheet.getAnalyst() or ""
983
984
    @security.public
985
    def getAnalystName(self):
986
        """Returns the name of the currently assigned analyst
987
        """
988
        analyst = self.getAnalyst()
989
        if not analyst:
990
            return ""
991
        user = api.get_user(analyst.strip())
992
        return user and user.getProperty("fullname") or analyst
993
994
    @security.public
995
    def getSubmittedBy(self):
996
        """
997
        Returns the identifier of the user who submitted the result if the
998
        state of the current analysis is "to_be_verified" or "verified"
999
        :return: the user_id of the user who did the last submission of result
1000
        """
1001
        return getTransitionActor(self, 'submit')
1002
1003
    @security.public
1004
    def getDateSubmitted(self):
1005
        """Returns the time the result was submitted.
1006
        :return: a DateTime object.
1007
        :rtype: DateTime
1008
        """
1009
        return getTransitionDate(self, 'submit', return_as_datetime=True)
1010
1011
    @security.public
1012
    def getDateVerified(self):
1013
        """Returns the time the analysis was verified. If the analysis hasn't
1014
        been yet verified, returns None
1015
        :return: the time the analysis was verified or None
1016
        :rtype: DateTime
1017
        """
1018
        return getTransitionDate(self, 'verify', return_as_datetime=True)
1019
1020
    @security.public
1021
    def getStartProcessDate(self):
1022
        """Returns the date time when the analysis is ready to be processed.
1023
        It returns the datetime when the object was created, but might be
1024
        different depending on the type of analysis (e.g. "Date Received" for
1025
        routine analyses): see overriden functions.
1026
        :return: Date time when the analysis is ready to be processed.
1027
        :rtype: DateTime
1028
        """
1029
        return self.created()
1030
1031
    @security.public
1032
    def getParentURL(self):
1033
        """This method is used to populate catalog values
1034
        This function returns the analysis' parent URL
1035
        """
1036
        parent = self.aq_parent
1037
        if parent:
1038
            return parent.absolute_url_path()
1039
1040
    @security.public
1041
    def getWorksheetUID(self):
1042
        """This method is used to populate catalog values
1043
        Returns WS UID if this analysis is assigned to a worksheet, or None.
1044
        """
1045
        worksheet = self.getWorksheet()
1046
        if worksheet:
1047
            return worksheet.UID()
1048
1049
    @security.public
1050
    def getWorksheet(self):
1051
        """Returns the Worksheet to which this analysis belongs to, or None
1052
        """
1053
        worksheet = self.getBackReferences('WorksheetAnalysis')
1054
        if not worksheet:
1055
            return None
1056
        if len(worksheet) > 1:
1057
            logger.error(
1058
                "Analysis %s is assigned to more than one worksheet."
1059
                % self.getId())
1060
        return worksheet[0]
1061
1062
    @security.public
1063
    def remove_duplicates(self, ws):
1064
        """When this analysis is unassigned from a worksheet, this function
1065
        is responsible for deleting DuplicateAnalysis objects from the ws.
1066
        """
1067
        for analysis in ws.objectValues():
1068
            if IDuplicateAnalysis.providedBy(analysis) \
1069
                    and analysis.getAnalysis().UID() == self.UID():
1070
                ws.removeAnalysis(analysis)
1071
1072
    def setInterimValue(self, keyword, value):
1073
        """Sets a value to an interim of this analysis
1074
        :param keyword: the keyword of the interim
1075
        :param value: the value for the interim
1076
        """
1077
        # Ensure value format integrity
1078
        if value is None:
1079
            value = ""
1080
        elif isinstance(value, string_types):
1081
            value = value.strip()
1082
        elif isinstance(value, (list, tuple, set, dict)):
1083
            value = json.dumps(value)
1084
1085
        # Ensure result integrity regards to None, empty and 0 values
1086
        interims = copy.deepcopy(self.getInterimFields())
1087
        for interim in interims:
1088
            if interim.get("keyword") == keyword:
1089
                interim["value"] = str(value)
1090
        self.setInterimFields(interims)
1091
1092
    def getInterimValue(self, keyword):
1093
        """Returns the value of an interim of this analysis
1094
        """
1095
        interims = filter(lambda item: item["keyword"] == keyword,
1096
                          self.getInterimFields())
1097
        if not interims:
1098
            logger.warning("Interim '{}' for analysis '{}' not found"
1099
                           .format(keyword, self.getKeyword()))
1100
            return None
1101
        if len(interims) > 1:
1102
            logger.error("More than one interim '{}' found for '{}'"
1103
                         .format(keyword, self.getKeyword()))
1104
            return None
1105
        return interims[0].get('value', '')
1106
1107
    def isRetest(self):
1108
        """Returns whether this analysis is a retest or not
1109
        """
1110
        return self.getRetestOf() and True or False
1111
1112
    def getRetestOfUID(self):
1113
        """Returns the UID of the retracted analysis this is a retest of
1114
        """
1115
        retest_of = self.getRetestOf()
1116
        if retest_of:
1117
            return api.get_uid(retest_of)
1118
1119
    def getRetest(self):
1120
        """Returns the retest that comes from this analysis, if any
1121
        """
1122
        relationship = "{}RetestOf".format(self.portal_type)
1123
        back_refs = get_backreferences(self, relationship)
1124
        if not back_refs:
1125
            return None
1126
        if len(back_refs) > 1:
1127
            logger.warn("Analysis {} with multiple retests".format(self.id))
1128
        retest_uid = back_refs[0]
1129
        retest = api.get_object_by_uid(retest_uid, default=None)
1130
        if retest is None:
1131
            logger.error("Retest with UID {} not found".format(retest_uid))
1132
        return retest
1133