Passed
Push — 2.x ( 041ff6...87c3c9 )
by Jordi
08:54
created

AbstractBaseAnalysis.getAnalysisCategories()   A

Complexity

Conditions 4

Size

Total Lines 12
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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