Passed
Push — 2.x ( 56cf07...33260b )
by Jordi
05:47
created

bika.lims.content.abstractbaseanalysis   A

Complexity

Total Complexity 42

Size/Duplication

Total Lines 1043
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 42
eloc 619
dl 0
loc 1043
rs 9.021
c 0
b 0
f 0

28 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 11 1
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() 0 12 4
A AbstractBaseAnalysis.getInstrumentURL() 0 7 2
A AbstractBaseAnalysis.getTotalDiscountedBulkPrice() 0 9 1
A AbstractBaseAnalysis._getCatalogTool() 0 4 1
A AbstractBaseAnalysis.getVATAmount() 0 6 1
A AbstractBaseAnalysis.getCategoryUID() 0 5 1
A AbstractBaseAnalysis.getTotalPrice() 0 9 1
A AbstractBaseAnalysis.getLowerDetectionLimit() 0 10 2
A AbstractBaseAnalysis.getTotalDiscountedPrice() 0 9 1
A AbstractBaseAnalysis.getRawMethod() 0 14 2
A AbstractBaseAnalysis.Title() 0 3 1
A AbstractBaseAnalysis.getCategoryTitle() 0 7 2
A AbstractBaseAnalysis.getRawInstrument() 0 6 1
A AbstractBaseAnalysis.getDefaultVAT() 0 9 2
A AbstractBaseAnalysis.getMethodURL() 0 7 2
A AbstractBaseAnalysis.getMethod() 0 7 1
A AbstractBaseAnalysis.getUpperDetectionLimit() 0 10 2
A AbstractBaseAnalysis.getDiscountedBulkPrice() 0 9 1

How to fix   Complexity   

Complexity

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