Passed
Push — 2.x ( 908061...0c6678 )
by Ramon
27:59 queued 21:19
created

AbstractBaseAnalysis.getMethodURL()   A

Complexity

Conditions 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 7
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-2024 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
            u"description_analysis_uncertainty",
527
            default=u"Specify the uncertainty value for a given range, e.g. "
528
                    u"for results in a range with minimum of 0 and maximum of "
529
                    u"10, where the uncertainty value is 0.5 - a result of "
530
                    u"6.67 will be reported as 6.67 ± 0.5.<br/>"
531
                    u"You can also specify the uncertainty value as a "
532
                    u"percentage of the result value, by adding a '%' to the "
533
                    u"value entered in the 'Uncertainty Value' column, e.g. "
534
                    u"for results in a range with minimum of 10.01 and a "
535
                    u"maximum of 100, where the uncertainty value is 2%, a "
536
                    u"result of 100 will be reported as 100 ± 2.<br/>"
537
                    u"If you don't want uncertainty to be displayed for a "
538
                    u"given range, set 0 (or a value below 0) as the "
539
                    u"Uncertainty value.<br/>"
540
                    u"Please ensure successive ranges are continuous, e.g. "
541
                    u"0.00 - 10.00 is followed by 10.01 - 20.00, 20.01 - 30.00"
542
                    u" etc."
543
        ),
544
    )
545
)
546
547
# Calculate the precision from Uncertainty value
548
# Behavior controlled by javascript
549
# - If checked, Precision and ExponentialFormatPrecision are not displayed.
550
#   The precision will be calculated according to the uncertainty.
551
# - If checked, Precision and ExponentialFormatPrecision will be displayed.
552
# See browser/js/bika.lims.analysisservice.edit.js
553
PrecisionFromUncertainty = BooleanField(
554
    'PrecisionFromUncertainty',
555
    schemata="Uncertainties",
556
    default=False,
557
    widget=BooleanWidget(
558
        label=_("Calculate Precision from Uncertainties"),
559
        description=_(
560
            "Precision as the number of significant digits according to the "
561
            "uncertainty. The decimal position will be given by the first "
562
            "number different from zero in the uncertainty, at that position "
563
            "the system will round up the uncertainty and results. For "
564
            "example, with a result of 5.243 and an uncertainty of 0.22, "
565
            "the system will display correctly as 5.2+-0.2. If no uncertainty "
566
            "range is set for the result, the system will use the fixed "
567
            "precision set."),
568
    )
569
)
570
571
# If checked, an additional input with the default uncertainty will
572
# be displayed in the manage results view. The value set by the user
573
# in this field will override the default uncertainty for the analysis
574
# result
575
AllowManualUncertainty = BooleanField(
576
    'AllowManualUncertainty',
577
    schemata="Uncertainties",
578
    default=False,
579
    widget=BooleanWidget(
580
        label=_("Allow manual uncertainty value input"),
581
        description=_(
582
            "Allow the analyst to manually replace the default uncertainty "
583
            "value."),
584
    )
585
)
586
587
RESULT_TYPES = (
588
    ("numeric", _("Numeric")),
589
    ("string", _("String")),
590
    ("text", _("Text")),
591
    ("select", _("Selection list")),
592
    ("multiselect", _("Multiple selection")),
593
    ("multiselect_duplicates", _("Multiple selection (with duplicates)")),
594
    ("multichoice", _("Multiple choices")),
595
)
596
597
# Type of control to be rendered on results entry
598
ResultType = StringField(
599
    "ResultType",
600
    schemata="Result Options",
601
    default="numeric",
602
    vocabulary=DisplayList(RESULT_TYPES),
603
    widget=SelectionWidget(
604
        label=_("Result type"),
605
        format="select",
606
    )
607
)
608
609
# Results can be selected from a dropdown list.  This prevents the analyst
610
# from entering arbitrary values.  Each result must have a ResultValue, which
611
# must be a number - it is this number which is interpreted as the actual
612
# "Result" when applying calculations.
613
ResultOptions = RecordsField(
614
    'ResultOptions',
615
    schemata="Result Options",
616
    type='resultsoptions',
617
    subfields=('ResultValue', 'ResultText'),
618
    required_subfields=('ResultValue', 'ResultText'),
619
    subfield_labels={'ResultValue': _('Result Value'),
620
                     'ResultText': _('Display Value'), },
621
    subfield_validators={'ResultValue': 'result_options_value_validator',
622
                         'ResultText': 'result_options_text_validator'},
623
    subfield_sizes={'ResultValue': 5,
624
                    'ResultText': 25,},
625
    subfield_maxlength={'ResultValue': 5,
626
                        'ResultText': 255,},
627
    widget=RecordsWidget(
628
        label=_("Predefined results"),
629
        description=_(
630
            "List of possible final results. When set, no custom result is "
631
            "allowed on results entry and user has to choose from these values"
632
        ),
633
    )
634
)
635
636
# TODO Remove ResultOptionsType field. It was Replaced by ResultType
637
ResultOptionsType = StringField(
638
    "ResultOptionsType",
639
    readonly=True,
640
    widget=StringWidget(
641
        visible=False,
642
    )
643
)
644
645
RESULT_OPTIONS_SORTING = (
646
    ("", _("Keep order above")),
647
    ("ResultValue-asc", _("By 'Result Value' ascending")),
648
    ("ResultValue-desc", _("By 'Result Value' descending")),
649
    ("ResultText-asc", _("By 'Display Value' ascending")),
650
    ("ResultText-desc", _("By 'Display Value' descending")),
651
)
652
653
ResultOptionsSorting = StringField(
654
    "ResultOptionsSorting",
655
    schemata="Result Options",
656
    default="ResultText-asc",
657
    vocabulary=DisplayList(RESULT_OPTIONS_SORTING),
658
    widget=SelectionWidget(
659
        label=_(
660
            u"label_analysis_results_options_sorting",
661
            default=u"Sorting criteria"
662
        ),
663
        description=_(
664
            u"description_analysis_results_options_sorting",
665
            default=u"Criteria to use when result options are displayed for "
666
                    u"selection in results entry listings. Note this only "
667
                    u"applies to the options displayed in the selection list. "
668
                    u"It does not have any effect to the order in which "
669
                    u"results are displayed after being submitted"
670
        ),
671
    )
672
)
673
674
# Allow/disallow the capture of text as the result of the analysis
675
# TODO Remove StringResult field. It was Replaced by ResultType
676
StringResult = BooleanField(
677
    "StringResult",
678
    readonly=True,
679
    widget=BooleanWidget(
680
        visible=False,
681
    )
682
)
683
684
# If the service is meant for providing an interim result to another analysis,
685
# or if the result is only used for internal processes, then it can be hidden
686
# from the client in the final report (and in manage_results view)
687
Hidden = BooleanField(
688
    'Hidden',
689
    schemata="Analysis",
690
    default=False,
691
    read_permission=View,
692
    write_permission=FieldEditAnalysisHidden,
693
    widget=BooleanWidget(
694
        label=_("Hidden"),
695
        description=_(
696
            "If enabled, this analysis and its results will not be displayed "
697
            "by default in reports. This setting can be overrided in Analysis "
698
            "Profile and/or Sample"),
699
    )
700
)
701
702
# Permit a user to verify their own results.  This could invalidate the
703
# accreditation for the results of this analysis!
704
SelfVerification = IntegerField(
705
    'SelfVerification',
706
    schemata="Analysis",
707
    default=-1,
708
    vocabulary="_getSelfVerificationVocabulary",
709
    widget=SelectionWidget(
710
        label=_("Self-verification of results"),
711
        description=_(
712
            "If enabled, a user who submitted a result for this analysis "
713
            "will also be able to verify it. This setting take effect for "
714
            "those users with a role assigned that allows them to verify "
715
            "results (by default, managers, labmanagers and verifiers). "
716
            "The option set here has priority over the option set in Bika "
717
            "Setup"),
718
        format="select",
719
    )
720
)
721
722
# Require more than one verification by separate Verifier or LabManager users.
723
NumberOfRequiredVerifications = IntegerField(
724
    'NumberOfRequiredVerifications',
725
    schemata="Analysis",
726
    default=-1,
727
    vocabulary="_getNumberOfRequiredVerificationsVocabulary",
728
    widget=SelectionWidget(
729
        label=_("Number of required verifications"),
730
        description=_(
731
            "Number of required verifications from different users with "
732
            "enough privileges before a given result for this analysis "
733
            "being considered as 'verified'. The option set here has "
734
            "priority over the option set in Bika Setup"),
735
        format="select",
736
    )
737
)
738
739
# Just a string displayed on various views
740
CommercialID = StringField(
741
    'CommercialID',
742
    searchable=1,
743
    schemata='Description',
744
    required=0,
745
    widget=StringWidget(
746
        label=_("Commercial ID"),
747
        description=_("The service's commercial ID for accounting purposes")
748
    )
749
)
750
751
# Just a string displayed on various views
752
ProtocolID = StringField(
753
    'ProtocolID',
754
    searchable=1,
755
    schemata='Description',
756
    required=0,
757
    widget=StringWidget(
758
        label=_("Protocol ID"),
759
        description=_("The service's analytical protocol ID")
760
    )
761
)
762
763
# Remarks are used in various ways by almost all objects in the system.
764
Remarks = TextField(
765
    'Remarks',
766
    read_permission=View,
767
    write_permission=FieldEditAnalysisRemarks,
768
    schemata='Description'
769
)
770
771
schema = BikaSchema.copy() + Schema((
772
    ShortTitle,
773
    SortKey,
774
    CommercialID,
775
    ProtocolID,
776
    ScientificName,
777
    Unit,
778
    UnitChoices,
779
    Precision,
780
    ExponentialFormatPrecision,
781
    LowerDetectionLimit,
782
    UpperDetectionLimit,
783
    DetectionLimitSelector,
784
    AllowManualDetectionLimit,
785
    AttachmentRequired,
786
    Keyword,
787
    ManualEntryOfResults,
788
    InstrumentEntryOfResults,
789
    Instrument,
790
    Method,
791
    MaxTimeAllowed,
792
    DuplicateVariation,
793
    Accredited,
794
    PointOfCapture,
795
    Category,
796
    Price,
797
    BulkPrice,
798
    VAT,
799
    Department,
800
    Uncertainties,
801
    PrecisionFromUncertainty,
802
    AllowManualUncertainty,
803
    ResultType,
804
    ResultOptions,
805
    ResultOptionsType,
806
    ResultOptionsSorting,
807
    Hidden,
808
    SelfVerification,
809
    NumberOfRequiredVerifications,
810
    Remarks,
811
    StringResult,
812
))
813
814
schema['id'].widget.visible = False
815
schema['description'].schemata = 'Description'
816
schema['description'].widget.visible = True
817
schema['title'].required = True
818
schema['title'].widget.visible = True
819
schema['title'].schemata = 'Description'
820
schema['title'].validators = ()
821
# Update the validation layer after change the validator in runtime
822
schema['title']._validationLayer()
823
824
825
class AbstractBaseAnalysis(BaseContent):  # TODO BaseContent?  is really needed?
826
    implements(IBaseAnalysis, IHaveAnalysisCategory, IHaveDepartment, IHaveInstrument)
827
    security = ClassSecurityInfo()
828
    schema = schema
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable schema does not seem to be defined.
Loading history...
829
    displayContentsTab = False
830
831
    @security.public
832
    def _getCatalogTool(self):
833
        from bika.lims.catalog import getCatalog
834
        return getCatalog(self)
835
836
    @security.public
837
    def Title(self):
838
        return _c(self.title)
839
840
    @security.public
841
    def getDefaultVAT(self):
842
        """Return default VAT from bika_setup
843
        """
844
        try:
845
            vat = self.bika_setup.getVAT()
846
            return vat
847
        except ValueError:
848
            return "0.00"
849
850
    @security.public
851
    def getVATAmount(self):
852
        """Compute VAT Amount from the Price and system configured VAT
853
        """
854
        price, vat = self.getPrice(), self.getVAT()
855
        return float(price) * (float(vat) / 100)
856
857
    @security.public
858
    def getDiscountedPrice(self):
859
        """Compute discounted price excl. VAT
860
        """
861
        price = self.getPrice()
862
        price = price and price or 0
863
        discount = self.bika_setup.getMemberDiscount()
864
        discount = discount and discount or 0
865
        return float(price) - (float(price) * float(discount)) / 100
866
867
    @security.public
868
    def getDiscountedBulkPrice(self):
869
        """Compute discounted bulk discount excl. VAT
870
        """
871
        price = self.getBulkPrice()
872
        price = price and price or 0
873
        discount = self.bika_setup.getMemberDiscount()
874
        discount = discount and discount or 0
875
        return float(price) - (float(price) * float(discount)) / 100
876
877
    @security.public
878
    def getTotalPrice(self):
879
        """Compute total price including VAT
880
        """
881
        price = self.getPrice()
882
        vat = self.getVAT()
883
        price = price and price or 0
884
        vat = vat and vat or 0
885
        return float(price) + (float(price) * float(vat)) / 100
886
887
    @security.public
888
    def getTotalBulkPrice(self):
889
        """Compute total bulk price
890
        """
891
        price = self.getBulkPrice()
892
        vat = self.getVAT()
893
        price = price and price or 0
894
        vat = vat and vat or 0
895
        return float(price) + (float(price) * float(vat)) / 100
896
897
    @security.public
898
    def getTotalDiscountedPrice(self):
899
        """Compute total discounted price
900
        """
901
        price = self.getDiscountedPrice()
902
        vat = self.getVAT()
903
        price = price and price or 0
904
        vat = vat and vat or 0
905
        return float(price) + (float(price) * float(vat)) / 100
906
907
    @security.public
908
    def getTotalDiscountedBulkPrice(self):
909
        """Compute total discounted corporate bulk price
910
        """
911
        price = self.getDiscountedCorporatePrice()
912
        vat = self.getVAT()
913
        price = price and price or 0
914
        vat = vat and vat or 0
915
        return float(price) + (float(price) * float(vat)) / 100
916
917
    @security.public
918
    def getLowerDetectionLimit(self):
919
        """Get the lower detection limit
920
        """
921
        field = self.getField("LowerDetectionLimit")
922
        value = field.get(self)
923
        # cut off trailing zeros
924
        if "." in value:
925
            value = value.rstrip("0").rstrip(".")
926
        return value
927
928
    @security.public
929
    def getUpperDetectionLimit(self):
930
        """Get the upper detection limit
931
        """
932
        field = self.getField("UpperDetectionLimit")
933
        value = field.get(self)
934
        # cut off trailing zeros
935
        if "." in value:
936
            value = value.rstrip("0").rstrip(".")
937
        return value
938
939
    @security.public
940
    def isSelfVerificationEnabled(self):
941
        """Returns if the user that submitted a result for this analysis must
942
        also be able to verify the result
943
        :returns: true or false
944
        """
945
        bsve = self.bika_setup.getSelfVerificationEnabled()
946
        vs = self.getSelfVerification()
947
        return bsve if vs == -1 else vs == 1
948
949
    @security.public
950
    def _getSelfVerificationVocabulary(self):
951
        """Returns a DisplayList with the available options for the
952
        self-verification list: 'system default', 'true', 'false'
953
        :returns: DisplayList with the available options for the
954
        self-verification list
955
        """
956
        bsve = self.bika_setup.getSelfVerificationEnabled()
957
        bsve = _('Yes') if bsve else _('No')
958
        bsval = "%s (%s)" % (_("System default"), bsve)
959
        items = [(-1, bsval), (0, _('No')), (1, _('Yes'))]
960
        return IntDisplayList(list(items))
961
962
    @security.public
963
    def getNumberOfRequiredVerifications(self):
964
        """Returns the number of required verifications a test for this
965
        analysis requires before being transitioned to 'verified' state
966
        :returns: number of required verifications
967
        """
968
        num = self.getField('NumberOfRequiredVerifications').get(self)
969
        if num < 1:
970
            return self.bika_setup.getNumberOfRequiredVerifications()
971
        return num
972
973
    @security.public
974
    def _getNumberOfRequiredVerificationsVocabulary(self):
975
        """Returns a DisplayList with the available options for the
976
        multi-verification list: 'system default', '1', '2', '3', '4'
977
        :returns: DisplayList with the available options for the
978
        multi-verification list
979
        """
980
        bsve = self.bika_setup.getNumberOfRequiredVerifications()
981
        bsval = "%s (%s)" % (_("System default"), str(bsve))
982
        items = [(-1, bsval), (1, '1'), (2, '2'), (3, '3'), (4, '4')]
983
        return IntDisplayList(list(items))
984
985
    @security.public
986
    def getMethodTitle(self):
987
        """This is used to populate catalog values
988
        """
989
        method = self.getMethod()
990
        if method:
991
            return method.Title()
992
993
    @security.public
994
    def getMethod(self):
995
        """Returns the assigned method
996
997
        :returns: Method object
998
        """
999
        return self.getField("Method").get(self)
1000
1001
    def getRawMethod(self):
1002
        """Returns the UID of the assigned method
1003
1004
        NOTE: This is the default accessor of the `Method` schema field
1005
        and needed for the selection widget to render the selected value
1006
        properly in _view_ mode.
1007
1008
        :returns: Method UID
1009
        """
1010
        field = self.getField("Method")
1011
        method = field.getRaw(self)
1012
        if not method:
1013
            return None
1014
        return method
1015
1016
    @security.public
1017
    def getMethodURL(self):
1018
        """This is used to populate catalog values
1019
        """
1020
        method = self.getMethod()
1021
        if method:
1022
            return method.absolute_url_path()
1023
1024
    @security.public
1025
    def getInstrument(self):
1026
        """Returns the assigned instrument
1027
1028
        :returns: Instrument object
1029
        """
1030
        return self.getField("Instrument").get(self)
1031
1032
    def getRawInstrument(self):
1033
        """Returns the UID of the assigned instrument
1034
1035
        :returns: Instrument UID
1036
        """
1037
        return self.getField("Instrument").getRaw(self)
1038
1039
    @security.public
1040
    def getInstrumentUID(self):
1041
        """Returns the UID of the assigned instrument
1042
1043
        NOTE: This is the default accessor of the `Instrument` schema field
1044
        and needed for the selection widget to render the selected value
1045
        properly in _view_ mode.
1046
1047
        :returns: Method UID
1048
        """
1049
        return self.getRawInstrument()
1050
1051
    @security.public
1052
    def getInstrumentURL(self):
1053
        """Used to populate catalog values
1054
        """
1055
        instrument = self.getInstrument()
1056
        if instrument:
1057
            return instrument.absolute_url_path()
1058
1059
    @security.public
1060
    def getCategoryTitle(self):
1061
        """Used to populate catalog values
1062
        """
1063
        category = self.getCategory()
1064
        if category:
1065
            return category.Title()
1066
1067
    @security.public
1068
    def getCategoryUID(self):
1069
        """Used to populate catalog values
1070
        """
1071
        return self.getRawCategory()
1072
1073
    @security.public
1074
    def getMaxTimeAllowed(self):
1075
        """Returns the maximum turnaround time for this analysis. If no TAT is
1076
        set for this particular analysis, it returns the value set at setup
1077
        return: a dictionary with the keys "days", "hours" and "minutes"
1078
        """
1079
        tat = self.Schema().getField("MaxTimeAllowed").get(self)
1080
        return tat or self.bika_setup.getDefaultTurnaroundTime()
1081
1082
    # TODO Remove. ResultOptionsType field was replaced by ResulType field
1083
    def getResultOptionsType(self):
1084
        if self.getStringResult():
1085
            return "select"
1086
        return self.getResultType()
1087
1088
    # TODO Remove. ResultOptionsType field was replaced by ResulType field
1089
    def setResultOptionsType(self, value):
1090
        self.setResultType(value)
1091
1092
    # TODO Remove. StringResults field was replaced by ResulType field
1093
    def getStringResult(self):
1094
        result_type = self.getResultType()
1095
        return result_type in ["string", "text"]
1096
1097
    # TODO Remove. StringResults field was replaced by ResulType field
1098
    def setStringResult(self, value):
1099
        result_type = "string" if bool(value) else "numeric"
1100
        self.setResultType(result_type)
1101