Passed
Push — 2.x ( c91c12...31e120 )
by Ramon
07:00
created

AbstractBaseAnalysis.getDefaultVAT()   A

Complexity

Conditions 2

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 9
rs 10
c 0
b 0
f 0
cc 2
nop 1
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.decimal import DecimalWidget
27
from bika.lims.browser.widgets.durationwidget import DurationWidget
28
from bika.lims.browser.widgets.recordswidget import RecordsWidget
29
from senaite.core.browser.widgets.referencewidget import ReferenceWidget
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
33
from bika.lims.interfaces import IHaveDepartment
34
from bika.lims.interfaces import IHaveInstrument
35
from senaite.core.interfaces import IHaveAnalysisCategory
36
from senaite.core.permissions import FieldEditAnalysisHidden
37
from senaite.core.permissions import FieldEditAnalysisRemarks
38
from senaite.core.permissions import FieldEditAnalysisResult
39
from bika.lims.utils import to_utf8 as _c
40
from Products.Archetypes.BaseContent import BaseContent
41
from Products.Archetypes.Field import BooleanField
42
from Products.Archetypes.Field import FixedPointField
43
from Products.Archetypes.Field import FloatField
44
from Products.Archetypes.Field import IntegerField
45
from Products.Archetypes.Field import StringField
46
from Products.Archetypes.Field import TextField
47
from Products.Archetypes.Schema import Schema
48
from Products.Archetypes.utils import DisplayList
49
from Products.Archetypes.utils import IntDisplayList
50
from Products.Archetypes.Widget import BooleanWidget
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
    ("date", _("Date")),
682
    ("datetime", _("Datetime")),
683
)
684
685
# Type of control to be rendered on results entry
686
ResultType = StringField(
687
    "ResultType",
688
    schemata="Result Options",
689
    default="numeric",
690
    vocabulary=DisplayList(RESULT_TYPES),
691
    widget=SelectionWidget(
692
        label=_("Result type"),
693
        format="select",
694
    )
695
)
696
697
# Results can be selected from a dropdown list.  This prevents the analyst
698
# from entering arbitrary values.  Each result must have a ResultValue, which
699
# must be a number - it is this number which is interpreted as the actual
700
# "Result" when applying calculations.
701
ResultOptions = RecordsField(
702
    'ResultOptions',
703
    schemata="Result Options",
704
    type='resultsoptions',
705
    subfields=('ResultValue', 'ResultText'),
706
    required_subfields=('ResultValue', 'ResultText'),
707
    subfield_labels={'ResultValue': _('Result Value'),
708
                     'ResultText': _('Display Value'), },
709
    subfield_validators={'ResultValue': 'result_options_value_validator',
710
                         'ResultText': 'result_options_text_validator'},
711
    subfield_sizes={'ResultValue': 5,
712
                    'ResultText': 25,},
713
    subfield_maxlength={'ResultValue': 5,
714
                        'ResultText': 255,},
715
    widget=RecordsWidget(
716
        label=_("Predefined results"),
717
        description=_(
718
            "List of possible final results. When set, no custom result is "
719
            "allowed on results entry and user has to choose from these values"
720
        ),
721
    )
722
)
723
724
# TODO Remove ResultOptionsType field. It was Replaced by ResultType
725
ResultOptionsType = StringField(
726
    "ResultOptionsType",
727
    readonly=True,
728
    widget=StringWidget(
729
        visible=False,
730
    )
731
)
732
733
RESULT_OPTIONS_SORTING = (
734
    ("", _("Keep order above")),
735
    ("ResultValue-asc", _("By 'Result Value' ascending")),
736
    ("ResultValue-desc", _("By 'Result Value' descending")),
737
    ("ResultText-asc", _("By 'Display Value' ascending")),
738
    ("ResultText-desc", _("By 'Display Value' descending")),
739
)
740
741
ResultOptionsSorting = StringField(
742
    "ResultOptionsSorting",
743
    schemata="Result Options",
744
    default="ResultText-asc",
745
    vocabulary=DisplayList(RESULT_OPTIONS_SORTING),
746
    widget=SelectionWidget(
747
        label=_(
748
            u"label_analysis_results_options_sorting",
749
            default=u"Sorting criteria"
750
        ),
751
        description=_(
752
            u"description_analysis_results_options_sorting",
753
            default=u"Criteria to use when result options are displayed for "
754
                    u"selection in results entry listings. Note this only "
755
                    u"applies to the options displayed in the selection list. "
756
                    u"It does not have any effect to the order in which "
757
                    u"results are displayed after being submitted"
758
        ),
759
    )
760
)
761
762
# Allow/disallow the capture of text as the result of the analysis
763
# TODO Remove StringResult field. It was Replaced by ResultType
764
StringResult = BooleanField(
765
    "StringResult",
766
    readonly=True,
767
    widget=BooleanWidget(
768
        visible=False,
769
    )
770
)
771
772
# If the service is meant for providing an interim result to another analysis,
773
# or if the result is only used for internal processes, then it can be hidden
774
# from the client in the final report (and in manage_results view)
775
Hidden = BooleanField(
776
    'Hidden',
777
    schemata="Analysis",
778
    default=False,
779
    read_permission=View,
780
    write_permission=FieldEditAnalysisHidden,
781
    widget=BooleanWidget(
782
        label=_("Hidden"),
783
        description=_(
784
            "If enabled, this analysis and its results will not be displayed "
785
            "by default in reports. This setting can be overrided in Analysis "
786
            "Profile and/or Sample"),
787
    )
788
)
789
790
# Permit a user to verify their own results.  This could invalidate the
791
# accreditation for the results of this analysis!
792
SelfVerification = IntegerField(
793
    'SelfVerification',
794
    schemata="Analysis",
795
    default=-1,
796
    vocabulary="_getSelfVerificationVocabulary",
797
    widget=SelectionWidget(
798
        label=_("Self-verification of results"),
799
        description=_(
800
            "If enabled, a user who submitted a result for this analysis "
801
            "will also be able to verify it. This setting take effect for "
802
            "those users with a role assigned that allows them to verify "
803
            "results (by default, managers, labmanagers and verifiers). "
804
            "The option set here has priority over the option set in Bika "
805
            "Setup"),
806
        format="select",
807
    )
808
)
809
810
# Require more than one verification by separate Verifier or LabManager users.
811
NumberOfRequiredVerifications = IntegerField(
812
    'NumberOfRequiredVerifications',
813
    schemata="Analysis",
814
    default=-1,
815
    vocabulary="_getNumberOfRequiredVerificationsVocabulary",
816
    widget=SelectionWidget(
817
        label=_("Number of required verifications"),
818
        description=_(
819
            "Number of required verifications from different users with "
820
            "enough privileges before a given result for this analysis "
821
            "being considered as 'verified'. The option set here has "
822
            "priority over the option set in Bika Setup"),
823
        format="select",
824
    )
825
)
826
827
# Just a string displayed on various views
828
CommercialID = StringField(
829
    'CommercialID',
830
    searchable=1,
831
    schemata='Description',
832
    required=0,
833
    widget=StringWidget(
834
        label=_("Commercial ID"),
835
        description=_("The service's commercial ID for accounting purposes")
836
    )
837
)
838
839
# Just a string displayed on various views
840
ProtocolID = StringField(
841
    'ProtocolID',
842
    searchable=1,
843
    schemata='Description',
844
    required=0,
845
    widget=StringWidget(
846
        label=_("Protocol ID"),
847
        description=_("The service's analytical protocol ID")
848
    )
849
)
850
851
# Remarks are used in various ways by almost all objects in the system.
852
Remarks = TextField(
853
    'Remarks',
854
    read_permission=View,
855
    write_permission=FieldEditAnalysisRemarks,
856
    schemata='Description'
857
)
858
859
schema = BikaSchema.copy() + Schema((
860
    ShortTitle,
861
    SortKey,
862
    CommercialID,
863
    ProtocolID,
864
    ScientificName,
865
    Unit,
866
    UnitChoices,
867
    Precision,
868
    ExponentialFormatPrecision,
869
    LowerDetectionLimit,
870
    LowerLimitOfQuantification,
871
    UpperLimitOfQuantification,
872
    UpperDetectionLimit,
873
    DetectionLimitSelector,
874
    AllowManualDetectionLimit,
875
    AttachmentRequired,
876
    Keyword,
877
    ManualEntryOfResults,
878
    InstrumentEntryOfResults,
879
    Instrument,
880
    Method,
881
    MaxTimeAllowed,
882
    MaxHoldingTime,
883
    DuplicateVariation,
884
    Accredited,
885
    PointOfCapture,
886
    Category,
887
    Price,
888
    BulkPrice,
889
    VAT,
890
    Department,
891
    Uncertainties,
892
    PrecisionFromUncertainty,
893
    AllowManualUncertainty,
894
    ResultType,
895
    ResultOptions,
896
    ResultOptionsType,
897
    ResultOptionsSorting,
898
    Hidden,
899
    SelfVerification,
900
    NumberOfRequiredVerifications,
901
    Remarks,
902
    StringResult,
903
))
904
905
schema['id'].widget.visible = False
906
schema['description'].schemata = 'Description'
907
schema['description'].widget.visible = True
908
schema['title'].required = True
909
schema['title'].widget.visible = True
910
schema['title'].schemata = 'Description'
911
schema['title'].validators = ()
912
# Update the validation layer after change the validator in runtime
913
schema['title']._validationLayer()
914
915
916
class AbstractBaseAnalysis(BaseContent):  # TODO BaseContent?  is really needed?
917
    implements(IBaseAnalysis, IHaveAnalysisCategory, IHaveDepartment, IHaveInstrument)
918
    security = ClassSecurityInfo()
919
    schema = schema
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable schema does not seem to be defined.
Loading history...
920
    displayContentsTab = False
921
922
    @security.public
923
    def _getCatalogTool(self):
924
        from bika.lims.catalog import getCatalog
925
        return getCatalog(self)
926
927
    @security.public
928
    def Title(self):
929
        return _c(self.title)
930
931
    @security.public
932
    def getDefaultVAT(self):
933
        """Return default VAT from bika_setup
934
        """
935
        try:
936
            vat = self.bika_setup.getVAT()
937
            return vat
938
        except ValueError:
939
            return "0.00"
940
941
    @security.public
942
    def getVATAmount(self):
943
        """Compute VAT Amount from the Price and system configured VAT
944
        """
945
        price, vat = self.getPrice(), self.getVAT()
946
        return float(price) * (float(vat) / 100)
947
948
    @security.public
949
    def getDiscountedPrice(self):
950
        """Compute discounted price excl. VAT
951
        """
952
        price = self.getPrice()
953
        price = price and price or 0
954
        discount = self.bika_setup.getMemberDiscount()
955
        discount = discount and discount or 0
956
        return float(price) - (float(price) * float(discount)) / 100
957
958
    @security.public
959
    def getDiscountedBulkPrice(self):
960
        """Compute discounted bulk discount excl. VAT
961
        """
962
        price = self.getBulkPrice()
963
        price = price and price or 0
964
        discount = self.bika_setup.getMemberDiscount()
965
        discount = discount and discount or 0
966
        return float(price) - (float(price) * float(discount)) / 100
967
968
    @security.public
969
    def getTotalPrice(self):
970
        """Compute total price including VAT
971
        """
972
        price = self.getPrice()
973
        vat = self.getVAT()
974
        price = price and price or 0
975
        vat = vat and vat or 0
976
        return float(price) + (float(price) * float(vat)) / 100
977
978
    @security.public
979
    def getTotalBulkPrice(self):
980
        """Compute total bulk price
981
        """
982
        price = self.getBulkPrice()
983
        vat = self.getVAT()
984
        price = price and price or 0
985
        vat = vat and vat or 0
986
        return float(price) + (float(price) * float(vat)) / 100
987
988
    @security.public
989
    def getTotalDiscountedPrice(self):
990
        """Compute total discounted price
991
        """
992
        price = self.getDiscountedPrice()
993
        vat = self.getVAT()
994
        price = price and price or 0
995
        vat = vat and vat or 0
996
        return float(price) + (float(price) * float(vat)) / 100
997
998
    @security.public
999
    def getTotalDiscountedBulkPrice(self):
1000
        """Compute total discounted corporate bulk price
1001
        """
1002
        price = self.getDiscountedCorporatePrice()
1003
        vat = self.getVAT()
1004
        price = price and price or 0
1005
        vat = vat and vat or 0
1006
        return float(price) + (float(price) * float(vat)) / 100
1007
1008
    @security.public
1009
    def getLowerDetectionLimit(self):
1010
        """Get the lower detection limit
1011
        """
1012
        field = self.getField("LowerDetectionLimit")
1013
        value = field.get(self)
1014
        # cut off trailing zeros
1015
        if "." in value:
1016
            value = value.rstrip("0").rstrip(".")
1017
        return value
1018
1019
    @security.public
1020
    def getUpperDetectionLimit(self):
1021
        """Get the upper detection limit
1022
        """
1023
        field = self.getField("UpperDetectionLimit")
1024
        value = field.get(self)
1025
        # cut off trailing zeros
1026
        if "." in value:
1027
            value = value.rstrip("0").rstrip(".")
1028
        return value
1029
1030
    @security.public
1031
    def setLowerLimitOfQuantification(self, value):
1032
        """Sets the Lower Limit of Quantification (LLOQ) and ensures its value
1033
        is stored as a string without exponential notation and with whole
1034
        fraction preserved
1035
        """
1036
        value = api.float_to_string(value)
1037
        self.getField("LowerLimitOfQuantification").set(self, value)
1038
1039
    @security.public
1040
    def setUpperLimitOfQuantification(self, value):
1041
        """Sets the Upper Limit of Quantification (ULOW) and ensures its value
1042
        is stored as a string without exponential notation and with whole
1043
        fraction preserved
1044
        """
1045
        value = api.float_to_string(value)
1046
        self.getField("UpperLimitOfQuantification").set(self, value)
1047
1048
    @security.public
1049
    def isSelfVerificationEnabled(self):
1050
        """Returns if the user that submitted a result for this analysis must
1051
        also be able to verify the result
1052
        :returns: true or false
1053
        """
1054
        bsve = self.bika_setup.getSelfVerificationEnabled()
1055
        vs = self.getSelfVerification()
1056
        return bsve if vs == -1 else vs == 1
1057
1058
    @security.public
1059
    def _getSelfVerificationVocabulary(self):
1060
        """Returns a DisplayList with the available options for the
1061
        self-verification list: 'system default', 'true', 'false'
1062
        :returns: DisplayList with the available options for the
1063
        self-verification list
1064
        """
1065
        bsve = self.bika_setup.getSelfVerificationEnabled()
1066
        bsve = _('Yes') if bsve else _('No')
1067
        bsval = "%s (%s)" % (_("System default"), bsve)
1068
        items = [(-1, bsval), (0, _('No')), (1, _('Yes'))]
1069
        return IntDisplayList(list(items))
1070
1071
    @security.public
1072
    def getNumberOfRequiredVerifications(self):
1073
        """Returns the number of required verifications a test for this
1074
        analysis requires before being transitioned to 'verified' state
1075
        :returns: number of required verifications
1076
        """
1077
        num = self.getField('NumberOfRequiredVerifications').get(self)
1078
        if num < 1:
1079
            return self.bika_setup.getNumberOfRequiredVerifications()
1080
        return num
1081
1082
    @security.public
1083
    def _getNumberOfRequiredVerificationsVocabulary(self):
1084
        """Returns a DisplayList with the available options for the
1085
        multi-verification list: 'system default', '1', '2', '3', '4'
1086
        :returns: DisplayList with the available options for the
1087
        multi-verification list
1088
        """
1089
        bsve = self.bika_setup.getNumberOfRequiredVerifications()
1090
        bsval = "%s (%s)" % (_("System default"), str(bsve))
1091
        items = [(-1, bsval), (1, '1'), (2, '2'), (3, '3'), (4, '4')]
1092
        return IntDisplayList(list(items))
1093
1094
    @security.public
1095
    def getMethodTitle(self):
1096
        """This is used to populate catalog values
1097
        """
1098
        method = self.getMethod()
1099
        if method:
1100
            return method.Title()
1101
1102
    @security.public
1103
    def getMethod(self):
1104
        """Returns the assigned method
1105
1106
        :returns: Method object
1107
        """
1108
        return self.getField("Method").get(self)
1109
1110
    def getRawMethod(self):
1111
        """Returns the UID of the assigned method
1112
1113
        NOTE: This is the default accessor of the `Method` schema field
1114
        and needed for the selection widget to render the selected value
1115
        properly in _view_ mode.
1116
1117
        :returns: Method UID
1118
        """
1119
        field = self.getField("Method")
1120
        method = field.getRaw(self)
1121
        if not method:
1122
            return None
1123
        return method
1124
1125
    @security.public
1126
    def getMethodURL(self):
1127
        """This is used to populate catalog values
1128
        """
1129
        method = self.getMethod()
1130
        if method:
1131
            return method.absolute_url_path()
1132
1133
    @security.public
1134
    def getInstrument(self):
1135
        """Returns the assigned instrument
1136
1137
        :returns: Instrument object
1138
        """
1139
        return self.getField("Instrument").get(self)
1140
1141
    def getRawInstrument(self):
1142
        """Returns the UID of the assigned instrument
1143
1144
        :returns: Instrument UID
1145
        """
1146
        return self.getField("Instrument").getRaw(self)
1147
1148
    @security.public
1149
    def getInstrumentUID(self):
1150
        """Returns the UID of the assigned instrument
1151
1152
        NOTE: This is the default accessor of the `Instrument` schema field
1153
        and needed for the selection widget to render the selected value
1154
        properly in _view_ mode.
1155
1156
        :returns: Method UID
1157
        """
1158
        return self.getRawInstrument()
1159
1160
    @security.public
1161
    def getInstrumentURL(self):
1162
        """Used to populate catalog values
1163
        """
1164
        instrument = self.getInstrument()
1165
        if instrument:
1166
            return instrument.absolute_url_path()
1167
1168
    @security.public
1169
    def getCategoryTitle(self):
1170
        """Used to populate catalog values
1171
        """
1172
        category = self.getCategory()
1173
        if category:
1174
            return category.Title()
1175
1176
    @security.public
1177
    def getCategoryUID(self):
1178
        """Used to populate catalog values
1179
        """
1180
        return self.getRawCategory()
1181
1182
    @security.public
1183
    def getMaxTimeAllowed(self):
1184
        """Returns the maximum turnaround time for this analysis. If no TAT is
1185
        set for this particular analysis, it returns the value set at setup
1186
        return: a dictionary with the keys "days", "hours" and "minutes"
1187
        """
1188
        tat = self.Schema().getField("MaxTimeAllowed").get(self)
1189
        return tat or self.bika_setup.getDefaultTurnaroundTime()
1190
1191
    @security.public
1192
    def getMaxHoldingTime(self):
1193
        """Returns the maximum time since it the sample was collected for this
1194
        test/service to become available on sample creation. Returns None if no
1195
        positive maximum hold time is set. Otherwise, returns a dict with the
1196
        keys "days", "hours" and "minutes"
1197
        """
1198
        max_hold_time = self.Schema().getField("MaxHoldingTime").get(self)
1199
        if not max_hold_time:
1200
            return {}
1201
        if api.to_minutes(**max_hold_time) <= 0:
1202
            return {}
1203
        return max_hold_time
1204
1205
    def getResultOptionTextByValue(self, value, default=""):
1206
        """Returns the ResultText for a given ResultValue from the ResultOptions
1207
1208
        :param value: The ResultValue of the option to be retrieved
1209
        :type value: str
1210
        :return: Result text
1211
        """
1212
        if value is None:
1213
            return default
1214
        options = self.getResultOptions() or []
1215
        for option in options:
1216
            if api.to_float(option.get("ResultValue")) == api.to_float(value):
1217
                return option.get("ResultText", default)
1218
        return default
1219
1220
    # TODO Remove. ResultOptionsType field was replaced by ResulType field
1221
    def getResultOptionsType(self):
1222
        if self.getStringResult():
1223
            return "select"
1224
        return self.getResultType()
1225
1226
    # TODO Remove. ResultOptionsType field was replaced by ResulType field
1227
    def setResultOptionsType(self, value):
1228
        self.setResultType(value)
1229
1230
    # TODO Remove. StringResults field was replaced by ResulType field
1231
    def getStringResult(self):
1232
        result_type = self.getResultType()
1233
        return result_type in ["string", "text"]
1234
1235
    # TODO Remove. StringResults field was replaced by ResulType field
1236
    def setStringResult(self, value):
1237
        result_type = "string" if bool(value) else "numeric"
1238
        self.setResultType(result_type)
1239