Passed
Push — 2.x ( d668b2...78a11b )
by Ramon
05:54
created

  A

Complexity

Conditions 5

Size

Total Lines 15
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 14
dl 0
loc 15
rs 9.2333
c 0
b 0
f 0
cc 5
nop 2
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
from AccessControl import ClassSecurityInfo
22
from Products.Archetypes.public import Schema, registerType
23
from Products.Archetypes.public import StringField
24
from bika.lims import api
25
from bika.lims.browser.fields import UIDReferenceField
26
from bika.lims.config import PROJECTNAME
27
from bika.lims.content.abstractroutineanalysis import AbstractRoutineAnalysis
28
from bika.lims.content.abstractroutineanalysis import schema
29
from bika.lims.content.analysisspec import ResultsRangeDict
30
from bika.lims.interfaces import IDuplicateAnalysis
31
from bika.lims.interfaces.analysis import IRequestAnalysis
32
from bika.lims import logger
33
from bika.lims.workflow import in_state
34
from bika.lims.workflow.analysis import STATE_RETRACTED, STATE_REJECTED
35
from zope.interface import implements
36
37
# A reference back to the original analysis from which this one was duplicated.
38
Analysis = UIDReferenceField(
39
    'Analysis',
40
    required=1,
41
    allowed_types=('Analysis', 'ReferenceAnalysis'),
42
)
43
44
# TODO Analysis - Duplicates shouldn't have this attribute, only ReferenceAns
45
ReferenceAnalysesGroupID = StringField(
46
    'ReferenceAnalysesGroupID',
47
)
48
49
schema = schema.copy() + Schema((
50
    Analysis,
51
    ReferenceAnalysesGroupID,
52
))
53
54
55
class DuplicateAnalysis(AbstractRoutineAnalysis):
56
    implements(IDuplicateAnalysis)
57
    security = ClassSecurityInfo()
58
    displayContentsTab = False
59
    schema = schema
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable schema does not seem to be defined.
Loading history...
60
61
    @security.public
62
    def getRequest(self):
63
        """Returns the Analysis Request of the original analysis.
64
        """
65
        analysis = self.getAnalysis()
66
        if analysis:
67
            return analysis.getRequest()
68
69
    @security.public
70
    def getAnalysisPortalType(self):
71
        """This returns the portal_type of the original analysis.
72
        """
73
        analysis = self.getAnalysis()
74
        if analysis:
75
            return analysis.portal_type
76
77
    @security.public
78
    def getWorksheet(self):
79
        return self.aq_parent
80
81
    @security.public
82
    def getSiblings(self, with_retests=False):
83
        """
84
        Return the list of duplicate analyses that share the same Request and
85
        are included in the same Worksheet as the current analysis. The current
86
        duplicate is excluded from the list.
87
        :param with_retests: If false, siblings with retests are dismissed
88
        :type with_retests: bool
89
        :return: list of siblings for this analysis
90
        :rtype: list of IAnalysis
91
        """
92
        worksheet = self.getWorksheet()
93
        requestuid = self.getRequestUID()
94
        if not requestuid or not worksheet:
95
            return []
96
97
        siblings = []
98
        retracted_states = [STATE_RETRACTED, STATE_REJECTED]
99
        analyses = worksheet.getAnalyses()
100
        for analysis in analyses:
101
            if analysis.UID() == self.UID():
102
                # Exclude me from the list
103
                continue
104
105
            if not IRequestAnalysis.providedBy(analysis):
106
                # Exclude analyses that do not have an analysis request
107
                # associated
108
                continue
109
110
            if analysis.getRequestUID() != requestuid:
111
                # Exclude those analyses that does not belong to the same
112
                # analysis request I belong to
113
                continue
114
115
            if not with_retests:
116
117
                if in_state(analysis, retracted_states):
118
                    # Exclude retracted analyses
119
                    continue
120
121
                elif analysis.getRetest():
122
                    # Exclude analyses with a retest
123
                    continue
124
125
            siblings.append(analysis)
126
127
        return siblings
128
129
    @security.public
130
    def getResultsRange(self):
131
        """Returns the valid result range for this analysis duplicate, based on
132
        both on the result and duplicate variation set in the original analysis
133
134
        A Duplicate will be out of range if its result does not match with the
135
        result for the parent analysis plus the duplicate variation in % as the
136
        margin error.
137
138
        If the duplicate is from an analysis with result options and/or string
139
        results enabled (with non-numeric value), returns an empty result range
140
141
        :return: A dictionary with the keys min and max
142
        :rtype: dict
143
        """
144
        # Get the original analysis
145
        original_analysis = self.getAnalysis()
146
        if not original_analysis:
147
            logger.warn("Orphan duplicate: {}".format(repr(self)))
148
            return {}
149
150
        # Return empty if results option enabled (exact match expected)
151
        if original_analysis.getResultOptions():
152
            return {}
153
154
        # Return empty if non-floatable (exact match expected)
155
        original_result = original_analysis.getResult()
156
        if not api.is_floatable(original_result):
157
            return {}
158
159
        # Calculate the min/max based on duplicate variation %
160
        specs = ResultsRangeDict(uid=self.getServiceUID())
161
        dup_variation = original_analysis.getDuplicateVariation()
162
        dup_variation = api.to_float(dup_variation, default=0)
163
        if not dup_variation:
164
            # We expect an exact match
165
            specs.min = specs.max = original_result
166
            return specs
167
168
        original_result = api.to_float(original_result)
169
        margin = abs(original_result) * (dup_variation / 100.0)
170
        specs.min = str(original_result - margin)
171
        specs.max = str(original_result + margin)
172
        return specs
173
174
175
registerType(DuplicateAnalysis, PROJECTNAME)
176