Passed
Push — 2.x ( 4ac573...e592c3 )
by Ramon
07:11
created

AbstractBaseAnalysis.setLowerLimitOfQuantification()   A

Complexity

Conditions 1

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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