Passed
Push — 2.x ( 7be1e3...9538a4 )
by Jordi
07:43
created

AbstractBaseAnalysis.Title()   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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