Passed
Push — master ( 70be41...e1228c )
by Ramon
11:12
created

AbstractBaseAnalysis.getDiscountedBulkPrice()   A

Complexity

Conditions 1

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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