Passed
Push — master ( d0cde1...e5588c )
by Ramon
05:00
created

bika.lims.content.abstractbaseanalysis   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 1041
Duplicated Lines 2.4 %

Importance

Changes 0
Metric Value
wmc 51
eloc 600
dl 25
loc 1041
rs 7.92
c 0
b 0
f 0

30 Methods

Rating   Name   Duplication   Size   Complexity  
A AbstractBaseAnalysis.getNumberOfRequiredVerifications() 0 10 2
A AbstractBaseAnalysis.isSelfVerificationEnabled() 0 9 2
A AbstractBaseAnalysis.getDiscountedPrice() 0 9 1
A AbstractBaseAnalysis.getInstrumentUID() 0 14 2
A AbstractBaseAnalysis._getNumberOfRequiredVerificationsVocabulary() 0 11 1
A AbstractBaseAnalysis._getSelfVerificationVocabulary() 0 12 2
A AbstractBaseAnalysis.getInstrument() 0 7 1
A AbstractBaseAnalysis.getTotalBulkPrice() 0 9 1
A AbstractBaseAnalysis.getMethodTitle() 0 7 2
A AbstractBaseAnalysis.getMaxTimeAllowed() 0 8 1
A AbstractBaseAnalysis.getAnalysisCategories() 12 12 4
A AbstractBaseAnalysis.getInstrumentURL() 0 7 2
A AbstractBaseAnalysis.getTotalDiscountedBulkPrice() 0 9 1
A AbstractBaseAnalysis.getMethodUID() 0 14 2
A AbstractBaseAnalysis.getDepartments() 13 13 4
A AbstractBaseAnalysis._getCatalogTool() 0 4 1
A AbstractBaseAnalysis.getDepartmentTitle() 0 7 2
A AbstractBaseAnalysis.getVATAmount() 0 6 1
A AbstractBaseAnalysis.getCategoryUID() 0 7 2
A AbstractBaseAnalysis.getTotalPrice() 0 9 1
A AbstractBaseAnalysis.getLowerDetectionLimit() 0 9 2
A AbstractBaseAnalysis.getTotalDiscountedPrice() 0 9 1
A AbstractBaseAnalysis.Title() 0 3 1
A AbstractBaseAnalysis.getCategoryTitle() 0 7 2
A AbstractBaseAnalysis.getMethodURL() 0 7 2
A AbstractBaseAnalysis.getDefaultVAT() 0 9 2
A AbstractBaseAnalysis.getMethod() 0 7 1
A AbstractBaseAnalysis.getInstrumentTitle() 0 7 2
A AbstractBaseAnalysis.getUpperDetectionLimit() 0 9 2
A AbstractBaseAnalysis.getDiscountedBulkPrice() 0 9 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complexity

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like bika.lims.content.abstractbaseanalysis 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-2019 by it's authors.
19
# Some rights reserved, see README and LICENSE.
20
21
from AccessControl import ClassSecurityInfo
22
from bika.lims import api
23
from bika.lims import bikaMessageFactory as _
24
from bika.lims.browser.fields import DurationField
25
from bika.lims.browser.fields import UIDReferenceField
26
from bika.lims.browser.widgets.durationwidget import DurationWidget
27
from bika.lims.browser.widgets.recordswidget import RecordsWidget
28
from bika.lims.browser.widgets.referencewidget import ReferenceWidget
29
from bika.lims.config import ATTACHMENT_OPTIONS
30
from bika.lims.config import SERVICE_POINT_OF_CAPTURE
31
from bika.lims.content.bikaschema import BikaSchema
32
from bika.lims.interfaces import IBaseAnalysis, ISubmitted
33
from bika.lims.permissions import FieldEditAnalysisHidden
34
from bika.lims.permissions import FieldEditAnalysisRemarks
35
from bika.lims.permissions import FieldEditAnalysisResult
36
from bika.lims.utils import to_utf8 as _c
37
from Products.Archetypes.BaseContent import BaseContent
38
from Products.Archetypes.Field import BooleanField
39
from Products.Archetypes.Field import FixedPointField
40
from Products.Archetypes.Field import FloatField
41
from Products.Archetypes.Field import IntegerField
42
from Products.Archetypes.Field import StringField
43
from Products.Archetypes.Field import TextField
44
from Products.Archetypes.Schema import Schema
45
from Products.Archetypes.utils import DisplayList
46
from Products.Archetypes.utils import IntDisplayList
47
from Products.Archetypes.Widget import BooleanWidget
48
from Products.Archetypes.Widget import DecimalWidget
49
from Products.Archetypes.Widget import IntegerWidget
50
from Products.Archetypes.Widget import SelectionWidget
51
from Products.Archetypes.Widget import StringWidget
52
from Products.ATExtensions.ateapi import RecordsField
53
from Products.CMFCore.permissions import View
54
from Products.CMFCore.utils import getToolByName
55
from zope.interface import implements
56
57
# Anywhere that there just isn't space for unpredictably long names,
58
# this value will be used instead.  It's set on the AnalysisService,
59
# but accessed on all analysis objects.
60
ShortTitle = StringField(
61
    'ShortTitle',
62
    schemata="Description",
63
    widget=StringWidget(
64
        label=_("Short title"),
65
        description=_(
66
            "If text is entered here, it is used instead of the title when "
67
            "the service is listed in column headings. HTML formatting is "
68
            "allowed.")
69
    )
70
)
71
72
# A simple integer to sort items.
73
SortKey = FloatField(
74
    'SortKey',
75
    schemata="Description",
76
    validators=('SortKeyValidator',),
77
    widget=DecimalWidget(
78
        label=_("Sort Key"),
79
        description=_(
80
            "Float value from 0.0 - 1000.0 indicating the sort order. "
81
            "Duplicate values are ordered alphabetically."),
82
    )
83
)
84
85
# Is the title of the analysis a proper Scientific Name?
86
ScientificName = BooleanField(
87
    'ScientificName',
88
    schemata="Description",
89
    default=False,
90
    widget=BooleanWidget(
91
        label=_("Scientific name"),
92
        description=_(
93
            "If enabled, the name of the analysis will be written in italics."),
94
    )
95
)
96
97
# The units of measurement used for representing results in reports and in
98
# manage_results screen.
99
Unit = StringField(
100
    'Unit',
101
    schemata="Description",
102
    widget=StringWidget(
103
        label=_("Unit"),
104
        description=_(
105
            "The measurement units for this analysis service' results, e.g. "
106
            "mg/l, ppm, dB, mV, etc."),
107
    )
108
)
109
110
# Decimal precision for printing normal decimal results.
111
Precision = IntegerField(
112
    'Precision',
113
    schemata="Analysis",
114
    widget=IntegerWidget(
115
        label=_("Precision as number of decimals"),
116
        description=_(
117
            "Define the number of decimals to be used for this result."),
118
    )
119
)
120
121
# If the precision of the results as entered is higher than this value,
122
# the results will be represented in scientific notation.
123
ExponentialFormatPrecision = IntegerField(
124
    'ExponentialFormatPrecision',
125
    schemata="Analysis",
126
    required=True,
127
    default=7,
128
    widget=IntegerWidget(
129
        label=_("Exponential format precision"),
130
        description=_(
131
            "Define the precision when converting values to exponent "
132
            "notation.  The default is 7."),
133
    )
134
)
135
136
# If the value is below this limit, it means that the measurement lacks
137
# accuracy and this will be shown in manage_results and also on the final
138
# report.
139
LowerDetectionLimit = FixedPointField(
140
    'LowerDetectionLimit',
141
    schemata="Analysis",
142
    default='0.0',
143
    precision=7,
144
    widget=DecimalWidget(
145
        label=_("Lower Detection Limit (LDL)"),
146
        description=_(
147
            "The Lower Detection Limit is the lowest value to which the "
148
            "measured parameter can be measured using the specified testing "
149
            "methodology. Results entered which are less than this value will "
150
            "be reported as < LDL")
151
    )
152
)
153
154
# If the value is above this limit, it means that the measurement lacks
155
# accuracy and this will be shown in manage_results and also on the final
156
# report.
157
UpperDetectionLimit = FixedPointField(
158
    'UpperDetectionLimit',
159
    schemata="Analysis",
160
    default='1000000000.0',
161
    precision=7,
162
    widget=DecimalWidget(
163
        label=_("Upper Detection Limit (UDL)"),
164
        description=_(
165
            "The Upper Detection Limit is the highest value to which the "
166
            "measured parameter can be measured using the specified testing "
167
            "methodology. Results entered which are greater than this value "
168
            "will be reported as > UDL")
169
    )
170
)
171
172
# Allow to select LDL or UDL defaults in results with readonly mode
173
# Some behavior of AnalysisServices is controlled with javascript: If checked,
174
# the field "AllowManualDetectionLimit" will be displayed.
175
# See browser/js/bika.lims.analysisservice.edit.js
176
#
177
# Use cases:
178
# a) If "DetectionLimitSelector" is enabled and
179
# "AllowManualDetectionLimit" is enabled too, then:
180
# the analyst will be able to select an '>', '<' operand from the
181
# selection list and also set the LD manually.
182
#
183
# b) If "DetectionLimitSelector" is enabled and
184
# "AllowManualDetectionLimit" is unchecked, the analyst will be
185
# able to select an operator from the selection list, but not set
186
# the LD manually: the default LD will be displayed in the result
187
# field as usuall, but in read-only mode.
188
#
189
# c) If "DetectionLimitSelector" is disabled, no LD selector will be
190
# displayed in the results table.
191
DetectionLimitSelector = BooleanField(
192
    'DetectionLimitSelector',
193
    schemata="Analysis",
194
    default=False,
195
    widget=BooleanWidget(
196
        label=_("Display a Detection Limit selector"),
197
        description=_(
198
            "If checked, a selection list will be displayed next to the "
199
            "analysis' result field in results entry views. By using this "
200
            "selector, the analyst will be able to set the value as a "
201
            "Detection Limit (LDL or UDL) instead of a regular result"),
202
    )
203
)
204
205
# Behavior of AnalysisService controlled with javascript: Only visible when the
206
# "DetectionLimitSelector" is checked
207
# See browser/js/bika.lims.analysisservice.edit.js
208
# Check inline comment for "DetecionLimitSelector" field for
209
# further information.
210
AllowManualDetectionLimit = BooleanField(
211
    'AllowManualDetectionLimit',
212
    schemata="Analysis",
213
    default=False,
214
    widget=BooleanWidget(
215
        label=_("Allow Manual Detection Limit input"),
216
        description=_(
217
            "Allow the analyst to manually replace the default Detection "
218
            "Limits (LDL and UDL) on results entry views"),
219
    )
220
)
221
222
# Specify attachment requirements for these analyses
223
AttachmentOption = StringField(
224
    'AttachmentOption',
225
    schemata="Analysis",
226
    default='p',
227
    vocabulary=ATTACHMENT_OPTIONS,
228
    widget=SelectionWidget(
229
        label=_("Attachment Option"),
230
        description=_(
231
            "Indicates whether file attachments, e.g. microscope images, "
232
            "are required for this analysis and whether file upload function "
233
            "will be available for it on data capturing screens"),
234
        format='select',
235
    )
236
)
237
238
# The keyword for the service is used as an identifier during instrument
239
# imports, and other places too.  It's also used as the ID analyses.
240
Keyword = StringField(
241
    'Keyword',
242
    schemata="Description",
243
    required=1,
244
    searchable=True,
245
    validators=('servicekeywordvalidator',),
246
    widget=StringWidget(
247
        label=_("Analysis Keyword"),
248
        description=_(
249
            "The unique keyword used to identify the analysis service in "
250
            "import files of bulk Sample requests and results imports from "
251
            "instruments. It is also used to identify dependent analysis "
252
            "services in user defined results calculations"),
253
    )
254
)
255
256
# Allow/Disallow manual entry of results
257
# Behavior of AnalysisServices controlled by javascript depending on
258
# Instruments field:
259
# - If InstrumentEntry not checked, set checked and readonly
260
# - If InstrumentEntry checked, set as not readonly
261
# See browser/js/bika.lims.analysisservice.edit.js
262
ManualEntryOfResults = BooleanField(
263
    'ManualEntryOfResults',
264
    schemata="Method",
265
    default=True,
266
    widget=BooleanWidget(
267
        label=_("Instrument assignment is not required"),
268
        description=_(
269
            "Select if the results for tests of this type of analysis can be "
270
            "set manually. If selected, the user will be able to set a result "
271
            "for a test of this type of analysis in manage results view "
272
            "without the need of selecting an instrument, even though the "
273
            "method selected for the test has instruments assigned."),
274
    )
275
)
276
277
# Allow/Disallow instrument entry of results
278
# Behavior controlled by javascript depending on Instruments field:
279
# - If no instruments available, hide and uncheck
280
# - If at least one instrument selected, checked, but not readonly
281
# See browser/js/bika.lims.analysisservice.edit.js
282
InstrumentEntryOfResults = BooleanField(
283
    'InstrumentEntryOfResults',
284
    schemata="Method",
285
    default=False,
286
    widget=BooleanWidget(
287
        label=_("Instrument assignment is allowed"),
288
        description=_(
289
            "Select if the results for tests of this type of analysis can be "
290
            "set by using an instrument. If disabled, no instruments will be "
291
            "available for tests of this type of analysis in manage results "
292
            "view, even though the method selected for the test has "
293
            "instruments assigned."),
294
    )
295
)
296
297
# Default instrument to be used.
298
# Gets populated with the instruments selected in the Instruments field.
299
# Behavior of AnalysisServices controlled by js depending on
300
# ManualEntry/Instruments:
301
# - Populate dynamically with selected Instruments
302
# - If InstrumentEntry checked, set first selected instrument
303
# - If InstrumentEntry not checked, hide and set None
304
# See browser/js/bika.lims.analysisservice.edit.js
305
Instrument = UIDReferenceField(
306
    "Instrument",
307
    read_permission=View,
308
    write_permission=FieldEditAnalysisResult,
309
    schemata="Method",
310
    searchable=True,
311
    required=0,
312
    vocabulary="_getAvailableInstrumentsDisplayList",
313
    allowed_types=("Instrument",),
314
    accessor="getInstrumentUID",
315
    widget=SelectionWidget(
316
        format="select",
317
        label=_("Default Instrument"),
318
        description=_(
319
            "This is the instrument that is assigned to  tests from this type "
320
            "of analysis in manage results view. The method associated to "
321
            "this instrument will be assigned as the default method too.Note "
322
            "the instrument's method will prevail over any of the methods "
323
            "choosen if the 'Instrument assignment is not required' option is "
324
            "enabled.")
325
    )
326
)
327
328
# Default method to be used. This field is used in Analysis Service
329
# Edit view, use getMethod() to retrieve the Method to be used in
330
# this Analysis Service.
331
# Gets populated with the methods selected in the multiselection
332
# box above or with the default instrument's method.
333
# Behavior controlled by js depending on ManualEntry/Instrument/Methods:
334
# - If InstrumentEntry checked, set instrument's default method, and readonly
335
# - If InstrumentEntry not checked, populate dynamically with
336
#   selected Methods, set the first method selected and non-readonly
337
# See browser/js/bika.lims.analysisservice.edit.js
338
Method = UIDReferenceField(
339
    "Method",
340
    read_permission=View,
341
    write_permission=FieldEditAnalysisResult,
342
    schemata="Method",
343
    required=0,
344
    searchable=True,
345
    allowed_types=("Method",),
346
    vocabulary="_getAvailableMethodsDisplayList",
347
    accessor="getMethodUID",
348
    widget=SelectionWidget(
349
        format="select",
350
        label=_("Default Method"),
351
        description=_(
352
            "If 'Allow instrument entry of results' is selected, the method "
353
            "from the default instrument will be used. Otherwise, only the "
354
            "methods selected above will be displayed.")
355
    )
356
)
357
358
# Maximum time (from sample reception) allowed for the analysis to be performed.
359
# After this amount of time, a late alert is printed, and the analysis will be
360
# flagged in turnaround time report.
361
MaxTimeAllowed = DurationField(
362
    'MaxTimeAllowed',
363
    schemata="Analysis",
364
    widget=DurationWidget(
365
        label=_("Maximum turn-around time"),
366
        description=_(
367
            "Maximum time allowed for completion of the analysis. A late "
368
            "analysis alert is raised when this period elapses"),
369
    )
370
)
371
372
# The amount of difference allowed between this analysis, and any duplicates.
373
DuplicateVariation = FixedPointField(
374
    'DuplicateVariation',
375
    default='0.00',
376
    schemata="Method",
377
    widget=DecimalWidget(
378
        label=_("Duplicate Variation %"),
379
        description=_(
380
            "When the results of duplicate analyses on worksheets, carried "
381
            "out on the same sample, differ with more than this percentage, "
382
            "an alert is raised"),
383
    )
384
)
385
386
# True if the accreditation body has approved this lab's method for
387
# accreditation.
388
Accredited = BooleanField(
389
    'Accredited',
390
    schemata="Method",
391
    default=False,
392
    widget=BooleanWidget(
393
        label=_("Accredited"),
394
        description=_(
395
            "Check this box if the analysis service is included in the "
396
            "laboratory's schedule of accredited analyses"),
397
    )
398
)
399
400
# The physical location that the analysis is tested; for some analyses,
401
# the sampler may capture results at the point where the sample is taken,
402
# and these results can be captured using different rules.  For example,
403
# the results may be entered before the sample is received.
404
PointOfCapture = StringField(
405
    'PointOfCapture',
406
    schemata="Description",
407
    required=1,
408
    default='lab',
409
    vocabulary=SERVICE_POINT_OF_CAPTURE,
410
    widget=SelectionWidget(
411
        format='flex',
412
        label=_("Point of Capture"),
413
        description=_(
414
            "The results of field analyses are captured during sampling at "
415
            "the sample point, e.g. the temperature of a water sample in the "
416
            "river where it is sampled. Lab analyses are done in the "
417
            "laboratory"),
418
    )
419
)
420
421
# The category of the analysis service, used for filtering, collapsing and
422
# reporting on analyses.
423
Category = UIDReferenceField(
424
    'Category',
425
    schemata="Description",
426
    required=1,
427
    allowed_types=('AnalysisCategory',),
428
    vocabulary='getAnalysisCategories',
429
    widget=ReferenceWidget(
430
        checkbox_bound=0,
431
        label=_("Analysis Category"),
432
        description=_("The category the analysis service belongs to"),
433
        catalog_name='bika_setup_catalog',
434
        base_query={'is_active': True},
435
    )
436
)
437
438
# The base price for this analysis
439
Price = FixedPointField(
440
    'Price',
441
    schemata="Description",
442
    default='0.00',
443
    widget=DecimalWidget(
444
        label=_("Price (excluding VAT)"),
445
    )
446
)
447
448
# Some clients qualify for bulk discounts.
449
BulkPrice = FixedPointField(
450
    'BulkPrice',
451
    schemata="Description",
452
    default='0.00',
453
    widget=DecimalWidget(
454
        label=_("Bulk price (excluding VAT)"),
455
        description=_(
456
            "The price charged per analysis for clients who qualify for bulk "
457
            "discounts"),
458
    )
459
)
460
461
# If VAT is charged, a different VAT value can be entered for each
462
# service.  The default value is taken from BikaSetup
463
VAT = FixedPointField(
464
    'VAT',
465
    schemata="Description",
466
    default_method='getDefaultVAT',
467
    widget=DecimalWidget(
468
        label=_("VAT %"),
469
        description=_("Enter percentage value eg. 14.0"),
470
    )
471
)
472
473
# The analysis service's Department.  This is used to filter analyses,
474
# and for indicating the responsibile lab manager in reports.
475
Department = UIDReferenceField(
476
    'Department',
477
    schemata="Description",
478
    required=0,
479
    allowed_types=('Department',),
480
    vocabulary='getDepartments',
481
    widget=ReferenceWidget(
482
        checkbox_bound=0,
483
        label=_("Department"),
484
        description=_("The laboratory department"),
485
        catalog_name='bika_setup_catalog',
486
        base_query={'is_active': True},
487
    )
488
)
489
490
# Uncertainty percentages in results can change depending on the results
491
# themselves.
492
Uncertainties = RecordsField(
493
    'Uncertainties',
494
    schemata="Uncertainties",
495
    type='uncertainties',
496
    subfields=('intercept_min', 'intercept_max', 'errorvalue'),
497
    required_subfields=(
498
        'intercept_min', 'intercept_max', 'errorvalue'),
499
    subfield_sizes={'intercept_min': 10,
500
                    'intercept_max': 10,
501
                    'errorvalue': 10,
502
                    },
503
    subfield_labels={'intercept_min': _('Range min'),
504
                     'intercept_max': _('Range max'),
505
                     'errorvalue': _('Uncertainty value'),
506
                     },
507
    subfield_validators={'intercept_min': 'uncertainties_validator',
508
                         'intercept_max': 'uncertainties_validator',
509
                         'errorvalue': 'uncertainties_validator',
510
                         },
511
    widget=RecordsWidget(
512
        label=_("Uncertainty"),
513
        description=_(
514
            "Specify the uncertainty value for a given range, e.g. for "
515
            "results in a range with minimum of 0 and maximum of 10, "
516
            "where the uncertainty value is 0.5 - a result of 6.67 will be "
517
            "reported as 6.67 +- 0.5. You can also specify the uncertainty "
518
            "value as a percentage of the result value, by adding a '%' to "
519
            "the value entered in the 'Uncertainty Value' column, e.g. for "
520
            "results in a range with minimum of 10.01 and a maximum of 100, "
521
            "where the uncertainty value is 2% - a result of 100 will be "
522
            "reported as 100 +- 2. Please ensure successive ranges are "
523
            "continuous, e.g. 0.00 - 10.00 is followed by 10.01 - 20.00, "
524
            "20.01 - 30 .00 etc."),
525
    )
526
)
527
528
# Calculate the precision from Uncertainty value
529
# Behavior controlled by javascript
530
# - If checked, Precision and ExponentialFormatPrecision are not displayed.
531
#   The precision will be calculated according to the uncertainty.
532
# - If checked, Precision and ExponentialFormatPrecision will be displayed.
533
# See browser/js/bika.lims.analysisservice.edit.js
534
PrecisionFromUncertainty = BooleanField(
535
    'PrecisionFromUncertainty',
536
    schemata="Uncertainties",
537
    default=False,
538
    widget=BooleanWidget(
539
        label=_("Calculate Precision from Uncertainties"),
540
        description=_(
541
            "Precision as the number of significant digits according to the "
542
            "uncertainty. The decimal position will be given by the first "
543
            "number different from zero in the uncertainty, at that position "
544
            "the system will round up the uncertainty and results. For "
545
            "example, with a result of 5.243 and an uncertainty of 0.22, "
546
            "the system will display correctly as 5.2+-0.2. If no uncertainty "
547
            "range is set for the result, the system will use the fixed "
548
            "precision set."),
549
    )
550
)
551
552
# If checked, an additional input with the default uncertainty will
553
# be displayed in the manage results view. The value set by the user
554
# in this field will override the default uncertainty for the analysis
555
# result
556
AllowManualUncertainty = BooleanField(
557
    'AllowManualUncertainty',
558
    schemata="Uncertainties",
559
    default=False,
560
    widget=BooleanWidget(
561
        label=_("Allow manual uncertainty value input"),
562
        description=_(
563
            "Allow the analyst to manually replace the default uncertainty "
564
            "value."),
565
    )
566
)
567
568
# Results can be selected from a dropdown list.  This prevents the analyst
569
# from entering arbitrary values.  Each result must have a ResultValue, which
570
# must be a number - it is this number which is interpreted as the actual
571
# "Result" when applying calculations.
572
ResultOptions = RecordsField(
573
    'ResultOptions',
574
    schemata="Result Options",
575
    type='resultsoptions',
576
    subfields=('ResultValue', 'ResultText'),
577
    required_subfields=('ResultValue', 'ResultText'),
578
    subfield_labels={'ResultValue': _('Result Value'),
579
                     'ResultText': _('Display Value'), },
580
    subfield_validators={'ResultValue': 'resultoptionsvalidator',
581
                         'ResultText': 'resultoptionsvalidator'},
582
    subfield_sizes={'ResultValue': 5,
583
                    'ResultText': 25,},
584
    subfield_maxlength={'ResultValue': 5,
585
                        'ResultText': 255,},
586
    widget=RecordsWidget(
587
        label=_("Result Options"),
588
        description=_(
589
            "Please list all options for the analysis result if you want to "
590
            "restrict it to specific options only, e.g. 'Positive', "
591
            "'Negative' and 'Indeterminable'.  The option's result value must "
592
            "be a number"),
593
    )
594
)
595
596
# Allow/disallow the capture of text as the result of the analysis
597
StringResult = BooleanField(
598
    "StringResult",
599
    schemata="Analysis",
600
    default=False,
601
    widget=BooleanWidget(
602
        label=_("String result"),
603
        description=_(
604
            "Enable this option to allow the capture of text as result"
605
        )
606
    )
607
)
608
609
# If the service is meant for providing an interim result to another analysis,
610
# or if the result is only used for internal processes, then it can be hidden
611
# from the client in the final report (and in manage_results view)
612
Hidden = BooleanField(
613
    'Hidden',
614
    schemata="Analysis",
615
    default=False,
616
    read_permission=View,
617
    write_permission=FieldEditAnalysisHidden,
618
    widget=BooleanWidget(
619
        label=_("Hidden"),
620
        description=_(
621
            "If enabled, this analysis and its results will not be displayed "
622
            "by default in reports. This setting can be overrided in Analysis "
623
            "Profile and/or Sample"),
624
    )
625
)
626
627
# Permit a user to verify their own results.  This could invalidate the
628
# accreditation for the results of this analysis!
629
SelfVerification = IntegerField(
630
    'SelfVerification',
631
    schemata="Analysis",
632
    default=-1,
633
    vocabulary="_getSelfVerificationVocabulary",
634
    widget=SelectionWidget(
635
        label=_("Self-verification of results"),
636
        description=_(
637
            "If enabled, a user who submitted a result for this analysis "
638
            "will also be able to verify it. This setting take effect for "
639
            "those users with a role assigned that allows them to verify "
640
            "results (by default, managers, labmanagers and verifiers). "
641
            "The option set here has priority over the option set in Bika "
642
            "Setup"),
643
        format="select",
644
    )
645
)
646
647
# Require more than one verification by separate Verifier or LabManager users.
648
NumberOfRequiredVerifications = IntegerField(
649
    'NumberOfRequiredVerifications',
650
    schemata="Analysis",
651
    default=-1,
652
    vocabulary="_getNumberOfRequiredVerificationsVocabulary",
653
    widget=SelectionWidget(
654
        label=_("Number of required verifications"),
655
        description=_(
656
            "Number of required verifications from different users with "
657
            "enough privileges before a given result for this analysis "
658
            "being considered as 'verified'. The option set here has "
659
            "priority over the option set in Bika Setup"),
660
        format="select",
661
    )
662
)
663
664
# Just a string displayed on various views
665
CommercialID = StringField(
666
    'CommercialID',
667
    searchable=1,
668
    schemata='Description',
669
    required=0,
670
    widget=StringWidget(
671
        label=_("Commercial ID"),
672
        description=_("The service's commercial ID for accounting purposes")
673
    )
674
)
675
676
# Just a string displayed on various views
677
ProtocolID = StringField(
678
    'ProtocolID',
679
    searchable=1,
680
    schemata='Description',
681
    required=0,
682
    widget=StringWidget(
683
        label=_("Protocol ID"),
684
        description=_("The service's analytical protocol ID")
685
    )
686
)
687
688
# Remarks are used in various ways by almost all objects in the system.
689
Remarks = TextField(
690
    'Remarks',
691
    read_permission=View,
692
    write_permission=FieldEditAnalysisRemarks,
693
    schemata='Description'
694
)
695
696
schema = BikaSchema.copy() + Schema((
697
    ShortTitle,
698
    SortKey,
699
    CommercialID,
700
    ProtocolID,
701
    ScientificName,
702
    Unit,
703
    Precision,
704
    ExponentialFormatPrecision,
705
    LowerDetectionLimit,
706
    UpperDetectionLimit,
707
    DetectionLimitSelector,
708
    AllowManualDetectionLimit,
709
    AttachmentOption,
710
    Keyword,
711
    ManualEntryOfResults,
712
    InstrumentEntryOfResults,
713
    Instrument,
714
    Method,
715
    MaxTimeAllowed,
716
    DuplicateVariation,
717
    Accredited,
718
    PointOfCapture,
719
    Category,
720
    Price,
721
    BulkPrice,
722
    VAT,
723
    Department,
724
    Uncertainties,
725
    PrecisionFromUncertainty,
726
    AllowManualUncertainty,
727
    ResultOptions,
728
    Hidden,
729
    SelfVerification,
730
    NumberOfRequiredVerifications,
731
    Remarks,
732
    StringResult,
733
))
734
735
schema['id'].widget.visible = False
736
schema['description'].schemata = 'Description'
737
schema['description'].widget.visible = True
738
schema['title'].required = True
739
schema['title'].widget.visible = True
740
schema['title'].schemata = 'Description'
741
schema['title'].validators = ()
742
# Update the validation layer after change the validator in runtime
743
schema['title']._validationLayer()
744
745
746
class AbstractBaseAnalysis(BaseContent):  # TODO BaseContent?  is really needed?
747
    implements(IBaseAnalysis)
748
    security = ClassSecurityInfo()
749
    schema = schema
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable schema does not seem to be defined.
Loading history...
750
    displayContentsTab = False
751
752
    @security.public
753
    def _getCatalogTool(self):
754
        from bika.lims.catalog import getCatalog
755
        return getCatalog(self)
756
757
    @security.public
758
    def Title(self):
759
        return _c(self.title)
760
761
    @security.public
762
    def getDefaultVAT(self):
763
        """Return default VAT from bika_setup
764
        """
765
        try:
766
            vat = self.bika_setup.getVAT()
767
            return vat
768
        except ValueError:
769
            return "0.00"
770
771
    @security.public
772
    def getVATAmount(self):
773
        """Compute VAT Amount from the Price and system configured VAT
774
        """
775
        price, vat = self.getPrice(), self.getVAT()
776
        return float(price) * (float(vat) / 100)
777
778
    @security.public
779
    def getDiscountedPrice(self):
780
        """Compute discounted price excl. VAT
781
        """
782
        price = self.getPrice()
783
        price = price and price or 0
784
        discount = self.bika_setup.getMemberDiscount()
785
        discount = discount and discount or 0
786
        return float(price) - (float(price) * float(discount)) / 100
787
788
    @security.public
789
    def getDiscountedBulkPrice(self):
790
        """Compute discounted bulk discount excl. VAT
791
        """
792
        price = self.getBulkPrice()
793
        price = price and price or 0
794
        discount = self.bika_setup.getMemberDiscount()
795
        discount = discount and discount or 0
796
        return float(price) - (float(price) * float(discount)) / 100
797
798
    @security.public
799
    def getTotalPrice(self):
800
        """Compute total price including VAT
801
        """
802
        price = self.getPrice()
803
        vat = self.getVAT()
804
        price = price and price or 0
805
        vat = vat and vat or 0
806
        return float(price) + (float(price) * float(vat)) / 100
807
808
    @security.public
809
    def getTotalBulkPrice(self):
810
        """Compute total bulk price
811
        """
812
        price = self.getBulkPrice()
813
        vat = self.getVAT()
814
        price = price and price or 0
815
        vat = vat and vat or 0
816
        return float(price) + (float(price) * float(vat)) / 100
817
818
    @security.public
819
    def getTotalDiscountedPrice(self):
820
        """Compute total discounted price
821
        """
822
        price = self.getDiscountedPrice()
823
        vat = self.getVAT()
824
        price = price and price or 0
825
        vat = vat and vat or 0
826
        return float(price) + (float(price) * float(vat)) / 100
827
828
    @security.public
829
    def getTotalDiscountedBulkPrice(self):
830
        """Compute total discounted corporate bulk price
831
        """
832
        price = self.getDiscountedCorporatePrice()
833
        vat = self.getVAT()
834
        price = price and price or 0
835
        vat = vat and vat or 0
836
        return float(price) + (float(price) * float(vat)) / 100
837
838 View Code Duplication
    @security.public
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
839
    def getAnalysisCategories(self):
840
        """A vocabulary listing available (and activated) categories.
841
        """
842
        bsc = getToolByName(self, 'bika_setup_catalog')
843
        cats = bsc(portal_type='AnalysisCategory', is_active=True)
844
        items = [(o.UID, o.Title) for o in cats]
845
        o = self.getCategory()
846
        if o and o.UID() not in [i[0] for i in items]:
847
            items.append((o.UID(), o.Title()))
848
        items.sort(lambda x, y: cmp(x[1], y[1]))
849
        return DisplayList(list(items))
850
851 View Code Duplication
    @security.public
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
852
    def getDepartments(self):
853
        """A vocabulary listing available (and activated) departments.
854
        """
855
        bsc = getToolByName(self, 'bika_setup_catalog')
856
        items = [('', '')] + [(o.UID, o.Title) for o in
857
                              bsc(portal_type='Department',
858
                                  is_active=True)]
859
        o = self.getDepartment()
860
        if o and o.UID() not in [i[0] for i in items]:
861
            items.append((o.UID(), o.Title()))
862
        items.sort(lambda x, y: cmp(x[1], y[1]))
863
        return DisplayList(list(items))
864
865
    @security.public
866
    def getLowerDetectionLimit(self):
867
        """Returns the Lower Detection Limit for this service as a floatable
868
        """
869
        ldl = self.getField('LowerDetectionLimit').get(self)
870
        try:
871
            return float(ldl)
872
        except ValueError:
873
            return 0
874
875
    @security.public
876
    def getUpperDetectionLimit(self):
877
        """Returns the Upper Detection Limit for this service as a floatable
878
        """
879
        udl = self.getField('UpperDetectionLimit').get(self)
880
        try:
881
            return float(udl)
882
        except ValueError:
883
            return 0
884
885
    @security.public
886
    def isSelfVerificationEnabled(self):
887
        """Returns if the user that submitted a result for this analysis must
888
        also be able to verify the result
889
        :returns: true or false
890
        """
891
        bsve = self.bika_setup.getSelfVerificationEnabled()
892
        vs = self.getSelfVerification()
893
        return bsve if vs == -1 else vs == 1
894
895
    @security.public
896
    def _getSelfVerificationVocabulary(self):
897
        """Returns a DisplayList with the available options for the
898
        self-verification list: 'system default', 'true', 'false'
899
        :returns: DisplayList with the available options for the
900
        self-verification list
901
        """
902
        bsve = self.bika_setup.getSelfVerificationEnabled()
903
        bsve = _('Yes') if bsve else _('No')
904
        bsval = "%s (%s)" % (_("System default"), bsve)
905
        items = [(-1, bsval), (0, _('No')), (1, _('Yes'))]
906
        return IntDisplayList(list(items))
907
908
    @security.public
909
    def getNumberOfRequiredVerifications(self):
910
        """Returns the number of required verifications a test for this
911
        analysis requires before being transitioned to 'verified' state
912
        :returns: number of required verifications
913
        """
914
        num = self.getField('NumberOfRequiredVerifications').get(self)
915
        if num < 1:
916
            return self.bika_setup.getNumberOfRequiredVerifications()
917
        return num
918
919
    @security.public
920
    def _getNumberOfRequiredVerificationsVocabulary(self):
921
        """Returns a DisplayList with the available options for the
922
        multi-verification list: 'system default', '1', '2', '3', '4'
923
        :returns: DisplayList with the available options for the
924
        multi-verification list
925
        """
926
        bsve = self.bika_setup.getNumberOfRequiredVerifications()
927
        bsval = "%s (%s)" % (_("System default"), str(bsve))
928
        items = [(-1, bsval), (1, '1'), (2, '2'), (3, '3'), (4, '4')]
929
        return IntDisplayList(list(items))
930
931
    @security.public
932
    def getMethodTitle(self):
933
        """This is used to populate catalog values
934
        """
935
        method = self.getMethod()
936
        if method:
937
            return method.Title()
938
939
    @security.public
940
    def getMethod(self):
941
        """Returns the assigned method
942
943
        :returns: Method object
944
        """
945
        return self.getField("Method").get(self)
946
947
    @security.public
948
    def getMethodUID(self):
949
        """Returns the UID of the assigned method
950
951
        NOTE: This is the default accessor of the `Method` schema field
952
        and needed for the selection widget to render the selected value
953
        properly in _view_ mode.
954
955
        :returns: Method UID
956
        """
957
        method = self.getMethod()
958
        if not method:
959
            return None
960
        return api.get_uid(method)
961
962
    @security.public
963
    def getMethodURL(self):
964
        """This is used to populate catalog values
965
        """
966
        method = self.getMethod()
967
        if method:
968
            return method.absolute_url_path()
969
970
    @security.public
971
    def getInstrumentTitle(self):
972
        """Used to populate catalog values
973
        """
974
        instrument = self.getInstrument()
975
        if instrument:
976
            return instrument.Title()
977
978
    @security.public
979
    def getInstrument(self):
980
        """Returns the assigned instrument
981
982
        :returns: Instrument object
983
        """
984
        return self.getField("Instrument").get(self)
985
986
    @security.public
987
    def getInstrumentUID(self):
988
        """Returns the UID of the assigned instrument
989
990
        NOTE: This is the default accessor of the `Instrument` schema field
991
        and needed for the selection widget to render the selected value
992
        properly in _view_ mode.
993
994
        :returns: Method UID
995
        """
996
        instrument = self.getInstrument()
997
        if not instrument:
998
            return None
999
        return api.get_uid(instrument)
1000
1001
    @security.public
1002
    def getInstrumentURL(self):
1003
        """Used to populate catalog values
1004
        """
1005
        instrument = self.getInstrument()
1006
        if instrument:
1007
            return instrument.absolute_url_path()
1008
1009
    @security.public
1010
    def getCategoryTitle(self):
1011
        """Used to populate catalog values
1012
        """
1013
        category = self.getCategory()
1014
        if category:
1015
            return category.Title()
1016
1017
    @security.public
1018
    def getCategoryUID(self):
1019
        """Used to populate catalog values
1020
        """
1021
        category = self.getCategory()
1022
        if category:
1023
            return category.UID()
1024
1025
    @security.public
1026
    def getDepartmentTitle(self):
1027
        """Used to populate catalog values
1028
        """
1029
        department = self.getDepartment()
1030
        if department:
1031
            return department.Title()
1032
1033
    @security.public
1034
    def getMaxTimeAllowed(self):
1035
        """Returns the maximum turnaround time for this analysis. If no TAT is
1036
        set for this particular analysis, it returns the value set at setup
1037
        return: a dictionary with the keys "days", "hours" and "minutes"
1038
        """
1039
        tat = self.Schema().getField("MaxTimeAllowed").get(self)
1040
        return tat or self.bika_setup.getDefaultTurnaroundTime()
1041