Passed
Push — 2.x ( 171335...eef595 )
by Ramon
05:32
created

BikaSetup.getImmediateResultsEntry()   A

Complexity

Conditions 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 8
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-2021 by it's authors.
19
# Some rights reserved, see README and LICENSE.
20
21
from AccessControl import ClassSecurityInfo
22
from bika.lims import api
23
from bika.lims import bikaMessageFactory as _
24
from bika.lims.browser.fields import DurationField
25
from bika.lims.browser.fields import UIDReferenceField
26
from bika.lims.browser.widgets import DurationWidget
27
from bika.lims.browser.widgets import RecordsWidget
28
from bika.lims.browser.widgets import ReferenceWidget
29
from bika.lims.browser.widgets import RejectionSetupWidget
30
from bika.lims.browser.worksheet.tools import getWorksheetLayouts
31
from bika.lims.config import CURRENCIES
32
from bika.lims.config import DECIMAL_MARKS
33
from bika.lims.config import DEFAULT_WORKSHEET_LAYOUT
34
from bika.lims.config import MULTI_VERIFICATION_TYPE
35
from bika.lims.config import PROJECTNAME
36
from bika.lims.config import SCINOTATION_OPTIONS
37
from bika.lims.config import WEEKDAYS
38
from bika.lims.content.bikaschema import BikaFolderSchema
39
from bika.lims.interfaces import IBikaSetup
40
from bika.lims.numbergenerator import INumberGenerator
41
from bika.lims.vocabularies import getStickerTemplates as _getStickerTemplates
42
from plone.app.folder import folder
43
from Products.Archetypes.atapi import BooleanField
44
from Products.Archetypes.atapi import BooleanWidget
45
from Products.Archetypes.atapi import DecimalWidget
46
from Products.Archetypes.atapi import FixedPointField
47
from Products.Archetypes.atapi import InAndOutWidget
48
from Products.Archetypes.atapi import IntegerField
49
from Products.Archetypes.atapi import IntegerWidget
50
from Products.Archetypes.atapi import LinesField
51
from Products.Archetypes.atapi import Schema
52
from Products.Archetypes.atapi import SelectionWidget
53
from Products.Archetypes.atapi import StringField
54
from Products.Archetypes.atapi import TextAreaWidget
55
from Products.Archetypes.atapi import registerType
56
from Products.Archetypes.Field import TextField
57
from Products.Archetypes.utils import DisplayList
58
from Products.Archetypes.utils import IntDisplayList
59
from Products.Archetypes.Widget import RichWidget
60
from Products.CMFCore.utils import getToolByName
61
from senaite.core.api import geo
62
from senaite.core.browser.fields.records import RecordsField
63
from senaite.core.interfaces import IHideActionsMenu
64
from senaite.core.p3compat import cmp
65
from zope.component import getUtility
66
from zope.interface import implements
67
68
69
class IDFormattingField(RecordsField):
70
    """A list of prefixes per portal_type
71
    """
72
    _properties = RecordsField._properties.copy()
73
    _properties.update({
74
        'type': 'prefixes',
75
        'subfields': (
76
            'portal_type',
77
            'form',
78
            'sequence_type',
79
            'context',
80
            'counter_type',
81
            'counter_reference',
82
            'prefix',
83
            'split_length'
84
        ),
85
        'subfield_labels': {
86
            'portal_type': 'Portal Type',
87
            'form': 'Format',
88
            'sequence_type': 'Seq Type',
89
            'context': 'Context',
90
            'counter_type': 'Counter Type',
91
            'counter_reference': 'Counter Ref',
92
            'prefix': 'Prefix',
93
            'split_length': 'Split Length',
94
        },
95
        'subfield_readonly': {
96
            'portal_type': False,
97
            'form': False,
98
            'sequence_type': False,
99
            'context': False,
100
            'counter_type': False,
101
            'counter_reference': False,
102
            'prefix': False,
103
            'split_length': False,
104
        },
105
        'subfield_sizes': {
106
            'portal_type': 20,
107
            'form': 30,
108
            'sequence_type': 1,
109
            'context': 12,
110
            'counter_type': 1,
111
            'counter_reference': 12,
112
            'prefix': 12,
113
            'split_length': 5,
114
        },
115
        'subfield_types': {
116
            'sequence_type': 'selection',
117
            'counter_type': 'selection',
118
            'split_length': 'int',
119
        },
120
        'subfield_vocabularies': {
121
            'sequence_type': 'getSequenceTypes',
122
            'counter_type': 'getCounterTypes',
123
        },
124
        'subfield_maxlength': {
125
            'form': 256,
126
        },
127
    })
128
129
    security = ClassSecurityInfo()
130
131
    def getSequenceTypes(self, instance=None):
132
        return DisplayList([
133
            ('', ''),
134
            ('counter', 'Counter'),
135
            ('generated', 'Generated')
136
        ])
137
138
    def getCounterTypes(self, instance=None):
139
        return DisplayList([
140
            ('', ''),
141
            ('backreference', 'Backreference'),
142
            ('contained', 'Contained')
143
        ])
144
145
146
STICKER_AUTO_OPTIONS = DisplayList((
147
    ('None', _('None')),
148
    ('register', _('Register')),
149
    ('receive', _('Receive')),
150
))
151
152
153
schema = BikaFolderSchema.copy() + Schema((
154
    IntegerField(
155
        'AutoLogOff',
156
        schemata="Security",
157
        required=1,
158
        default=0,
159
        widget=IntegerWidget(
160
            label=_("Automatic log-off"),
161
            description=_(
162
                "The number of minutes before a user is automatically logged off. "
163
                "0 disables automatic log-off"),
164
        )
165
    ),
166
    BooleanField(
167
        'RestrictWorksheetUsersAccess',
168
        schemata="Security",
169
        default=True,
170
        widget=BooleanWidget(
171
            label=_("Allow access to worksheets only to assigned analysts"),
172
            description=_("If unchecked, analysts will have access to all worksheets.")
173
        )
174
    ),
175
    BooleanField(
176
        'AllowToSubmitNotAssigned',
177
        schemata="Security",
178
        default=True,
179
        widget=BooleanWidget(
180
            label=_("Allow to submit results for unassigned analyses or for "
181
                    "analyses assigned to others"),
182
            description=_(
183
                "If unchecked, users will only be able to submit results "
184
                "for the analyses they are assigned to, and the submission of "
185
                "results for unassigned analyses won't be permitted. This "
186
                "setting does not apply to users with role Lab Manager")
187
        )
188
    ),
189
    BooleanField(
190
        'RestrictWorksheetManagement',
191
        schemata="Security",
192
        default=True,
193
        widget=BooleanWidget(
194
            label=_("Only lab managers can create and manage worksheets"),
195
            description=_("If unchecked, analysts and lab clerks will "
196
                          "be able to manage Worksheets, too. If the "
197
                          "users have restricted access only to those "
198
                          "worksheets for which they are assigned, "
199
                          "this option will be checked and readonly.")
200
        )
201
    ),
202
    # NOTE: This is a Proxy Field which delegates to the SENAITE Registry!
203
    BooleanField(
204
        "EnableGlobalAuditlog",
205
        schemata="Security",
206
        default=False,
207
        widget=BooleanWidget(
208
            label=_("Enable global Audit Log"),
209
            description=_(
210
                "The global Auditlog shows all modifications of the system. "
211
                "When enabled, all entities will be indexed in a separate "
212
                "catalog. This will increase the time when objects are "
213
                "created or modified."
214
            )
215
        )
216
    ),
217
    BooleanField(
218
        'ShowPrices',
219
        schemata="Accounting",
220
        default=True,
221
        widget=BooleanWidget(
222
            label=_("Include and display pricing information"),
223
        )
224
    ),
225
    StringField(
226
        'Currency',
227
        schemata="Accounting",
228
        required=1,
229
        vocabulary=CURRENCIES,
230
        default='EUR',
231
        widget=SelectionWidget(
232
            label=_("Currency"),
233
            description=_("Select the currency the site will use to display prices."),
234
            format='select',
235
        )
236
    ),
237
    StringField(
238
        'DefaultCountry',
239
        schemata="Accounting",
240
        required=1,
241
        vocabulary='getCountries',
242
        default='',
243
        widget=SelectionWidget(
244
            label=_("Country"),
245
            description=_("Select the country the site will show by default"),
246
            format='select',
247
        )
248
    ),
249
    FixedPointField(
250
        'MemberDiscount',
251
        schemata="Accounting",
252
        default='33.33',
253
        widget=DecimalWidget(
254
            label=_("Member discount %"),
255
            description=_(
256
                "The discount percentage entered here, is applied to the prices for clients "
257
                "flagged as 'members', normally co-operative members or associates deserving "
258
                "of this discount"),
259
        )
260
    ),
261
    FixedPointField(
262
        'VAT',
263
        schemata="Accounting",
264
        default='19.00',
265
        widget=DecimalWidget(
266
            label=_("VAT %"),
267
            description=_(
268
                "Enter percentage value eg. 14.0. This percentage is applied system wide "
269
                "but can be overwrittem on individual items"),
270
        )
271
    ),
272
    StringField(
273
        'DecimalMark',
274
        schemata="Results Reports",
275
        vocabulary=DECIMAL_MARKS,
276
        default=".",
277
        widget=SelectionWidget(
278
            label=_("Default decimal mark"),
279
            description=_("Preferred decimal mark for reports."),
280
            format='select',
281
        )
282
    ),
283
    StringField(
284
        'ScientificNotationReport',
285
        schemata="Results Reports",
286
        default='1',
287
        vocabulary=SCINOTATION_OPTIONS,
288
        widget=SelectionWidget(
289
            label=_("Default scientific notation format for reports"),
290
            description=_("Preferred scientific notation format for reports"),
291
            format='select',
292
        )
293
    ),
294
    IntegerField(
295
        'MinimumResults',
296
        schemata="Results Reports",
297
        required=1,
298
        default=5,
299
        widget=IntegerWidget(
300
            label=_("Minimum number of results for QC stats calculations"),
301
            description=_(
302
                "Using too few data points does not make statistical sense. "
303
                "Set an acceptable minimum number of results before QC statistics "
304
                "will be calculated and plotted"),
305
        )
306
    ),
307
    BooleanField(
308
        'CategoriseAnalysisServices',
309
        schemata="Analyses",
310
        default=False,
311
        widget=BooleanWidget(
312
            label=_("Categorise analysis services"),
313
            description=_("Group analysis services by category in the LIMS tables, helpful when the list is long")
314
        ),
315
    ),
316
    BooleanField(
317
        "CategorizeSampleAnalyses",
318
        schemata="Analyses",
319
        default=False,
320
        widget=BooleanWidget(
321
            label=_("label_bikasetup_categorizesampleanalyses",
322
                    default="Categorize sample analyses"),
323
            description=_("description_bikasetup_categorizesampleanalyses",
324
                          "Group analyses by category for samples")
325
        ),
326
    ),
327
    BooleanField(
328
        'EnableARSpecs',
329
        schemata="Analyses",
330
        default=False,
331
        widget=BooleanWidget(
332
            label=_("Enable Sample Specifications"),
333
            description=_(
334
                "Analysis specifications which are edited directly on the "
335
                "Sample."),
336
        ),
337
    ),
338
    IntegerField(
339
        'ExponentialFormatThreshold',
340
        schemata="Analyses",
341
        required=1,
342
        default=7,
343
        widget=IntegerWidget(
344
            label=_("Exponential format threshold"),
345
            description=_(
346
                "Result values with at least this number of significant "
347
                "digits are displayed in scientific notation using the "
348
                "letter 'e' to indicate the exponent.  The precision can be "
349
                "configured in individual Analysis Services."),
350
        )
351
    ),
352
    BooleanField(
353
        "ImmediateResultsEntry",
354
        schemata="Analyses",
355
        default=False,
356
        widget=BooleanWidget(
357
            label=_("label_bikasetup_immediateresultsentry",
358
                    default=u"Immediate results entry"),
359
            description=_(
360
                "description_bikasetup_immediateresultsentry",
361
                default=u"Allow the user to directly enter results after "
362
                "sample creation, e.g. to enter field results immediately, or "
363
                "lab results, when the automatic sample reception is "
364
                "activated."
365
            ),
366
        ),
367
    ),
368
    BooleanField(
369
        'EnableAnalysisRemarks',
370
        schemata="Analyses",
371
        default=False,
372
        widget=BooleanWidget(
373
            label=_("Add a remarks field to all analyses"),
374
            description=_(
375
                "If enabled, a free text field will be displayed close to "
376
                "each analysis in results entry view"
377
            )
378
        ),
379
    ),
380
    BooleanField(
381
        "AutoVerifySamples",
382
        schemata="Analyses",
383
        default=True,
384
        widget=BooleanWidget(
385
            label=_("Automatic verification of samples"),
386
            description=_(
387
                "When enabled, the sample is automatically verified as soon as "
388
                "all results are verified. Otherwise, users with enough "
389
                "privileges have to manually verify the sample afterwards. "
390
                "Default: enabled"
391
            )
392
        )
393
    ),
394
    BooleanField(
395
        'SelfVerificationEnabled',
396
        schemata="Analyses",
397
        default=False,
398
        widget=BooleanWidget(
399
            label=_("Allow self-verification of results"),
400
            description=_(
401
                "If enabled, a user who submitted a result will also be able "
402
                "to verify it. This setting only take effect for those users "
403
                "with a role assigned that allows them to verify results "
404
                "(by default, managers, labmanagers and verifiers)."
405
                "This setting can be overrided for a given Analysis in "
406
                "Analysis Service edit view. By default, disabled."),
407
        ),
408
    ),
409
    IntegerField(
410
        'NumberOfRequiredVerifications',
411
        schemata="Analyses",
412
        default=1,
413
        vocabulary="_getNumberOfRequiredVerificationsVocabulary",
414
        widget=SelectionWidget(
415
            format="select",
416
            label=_("Number of required verifications"),
417
            description=_(
418
                "Number of required verifications before a given result being "
419
                "considered as 'verified'. This setting can be overrided for "
420
                "any Analysis in Analysis Service edit view. By default, 1"),
421
        ),
422
    ),
423
    StringField(
424
        'TypeOfmultiVerification',
425
        schemata="Analyses",
426
        default='self_multi_enabled',
427
        vocabulary=MULTI_VERIFICATION_TYPE,
428
        widget=SelectionWidget(
429
            label=_("Multi Verification type"),
430
            description=_(
431
                "Choose type of multiple verification for the same user."
432
                "This setting can enable/disable verifying/consecutively verifying"
433
                "more than once for the same user."),
434
            format='select',
435
        )
436
    ),
437
    StringField(
438
        'ResultsDecimalMark',
439
        schemata="Analyses",
440
        vocabulary=DECIMAL_MARKS,
441
        default=".",
442
        widget=SelectionWidget(
443
            label=_("Default decimal mark"),
444
            description=_("Preferred decimal mark for results"),
445
            format='select',
446
        )
447
    ),
448
    StringField(
449
        'ScientificNotationResults',
450
        schemata="Analyses",
451
        default='1',
452
        vocabulary=SCINOTATION_OPTIONS,
453
        widget=SelectionWidget(
454
            label=_("Default scientific notation format for results"),
455
            description=_("Preferred scientific notation format for results"),
456
            format='select',
457
        )
458
    ),
459
    StringField(
460
        'WorksheetLayout',
461
        schemata="Appearance",
462
        default=DEFAULT_WORKSHEET_LAYOUT,
463
        vocabulary=getWorksheetLayouts(),
464
        widget=SelectionWidget(
465
            label=_("Default layout in worksheet view"),
466
            description=_("Preferred layout of the results entry table "
467
                          "in the Worksheet view. Classic layout displays "
468
                          "the Samples in rows and the analyses "
469
                          "in columns. Transposed layout displays the "
470
                          "Samples in columns and the analyses "
471
                          "in rows."),
472
            format='select',
473
        )
474
    ),
475
    BooleanField(
476
        'DashboardByDefault',
477
        schemata="Appearance",
478
        default=True,
479
        widget=BooleanWidget(
480
            label=_("Use Dashboard as default front page"),
481
            description=_("Select this to activate the dashboard as a default front page.")
482
        ),
483
    ),
484
    UIDReferenceField(
485
        "LandingPage",
486
        schemata="Appearance",
487
        required=0,
488
        allowed_types=(
489
            "Document",
490
            "Client",
491
            "ClientFolder",
492
            "Samples",
493
            "WorksheetFolder",
494
        ),
495
        mode="rw",
496
        multiValued=0,
497
        relationship='SetupLandingPage',
498
        widget=ReferenceWidget(
499
            label=_("Landing Page"),
500
            description=_(
501
                "The landing page is shown for non-authenticated users "
502
                "if the Dashboard is not selected as the default front page. "
503
                "If no landing page is selected, the default frontpage is displayed."),
504
            catalog_name="portal_catalog",
505
            base_query={"review_State": "published"},
506
            showOn=True,
507
        ),
508
    ),
509
    BooleanField(
510
        'PrintingWorkflowEnabled',
511
        schemata="Sampling",
512
        default=False,
513
        widget=BooleanWidget(
514
            label=_("Enable the Results Report Printing workflow"),
515
            description=_("Select this to allow the user to set an "
516
                          "additional 'Printed' status to those Analysis "
517
                          "Requests tha have been Published. "
518
                          "Disabled by default.")
519
        ),
520
    ),
521
    BooleanField(
522
        'SamplingWorkflowEnabled',
523
        schemata="Sampling",
524
        default=False,
525
        widget=BooleanWidget(
526
            label=_("Enable Sampling"),
527
            description=_("Select this to activate the sample collection workflow steps.")
528
        ),
529
    ),
530
    BooleanField(
531
        'ScheduleSamplingEnabled',
532
        schemata="Sampling",
533
        default=False,
534
        widget=BooleanWidget(
535
            label=_("Enable Sampling Scheduling"),
536
            description=_(
537
                "Select this to allow a Sampling Coordinator to" +
538
                " schedule a sampling. This functionality only takes effect" +
539
                " when 'Sampling workflow' is active")
540
        ),
541
    ),
542
    BooleanField(
543
        "AutoreceiveSamples",
544
        schemata="Sampling",
545
        default=False,
546
        widget=BooleanWidget(
547
            label=_("Auto-receive samples"),
548
            description=_(
549
                "Select to receive the samples automatically when created by "
550
                "lab personnel and sampling workflow is disabled. Samples "
551
                "created by client contacts won't be received automatically"
552
            ),
553
        ),
554
    ),
555
    BooleanField(
556
        'ShowPartitions',
557
        schemata="Appearance",
558
        default=False,
559
        widget=BooleanWidget(
560
            label=_("Display sample partitions to clients"),
561
            description=_(
562
                "Select to show sample partitions to client contacts. "
563
                "If deactivated, partitions won't be included in listings "
564
                "and no info message with links to the primary sample will "
565
                "be displayed to client contacts.")
566
        ),
567
    ),
568
    BooleanField(
569
        'SamplePreservationEnabled',
570
        schemata="Sampling",
571
        default=False,
572
        widget=BooleanWidget(
573
            label=_("Enable Sample Preservation"),
574
            description=_("")
575
        ),
576
    ),
577
    LinesField(
578
        "Workdays",
579
        schemata="Sampling",
580
        vocabulary=WEEKDAYS,
581
        default=tuple(map(str, range(7))),
582
        required=1,
583
        widget=InAndOutWidget(
584
            visible=True,
585
            label=_("Laboratory Workdays"),
586
            description=_("Only laboratory workdays are considered for the "
587
                          "analysis turnaround time calculation. "),
588
            format="checkbox",
589
        )
590
    ),
591
    DurationField(
592
        'DefaultTurnaroundTime',
593
        schemata="Sampling",
594
        required=1,
595
        default={"days": 5, "hours": 0, "minutes": 0},
596
        widget=DurationWidget(
597
            label=_("Default turnaround time for analyses."),
598
            description=_(
599
                "This is the default maximum time allowed for performing "
600
                "analyses.  It is only used for analyses where the analysis "
601
                "service does not specify a turnaround time. "
602
                "Only laboratory workdays are considered."
603
            ),
604
        )
605
    ),
606
    DurationField(
607
        'DefaultSampleLifetime',
608
        schemata="Sampling",
609
        required=1,
610
        default={"days": 30, "hours": 0, "minutes": 0},
611
        widget=DurationWidget(
612
            label=_("Default sample retention period"),
613
            description=_(
614
                "The number of days before a sample expires and cannot be analysed "
615
                "any more. This setting can be overwritten per individual sample type "
616
                "in the sample types setup"),
617
        )
618
    ),
619
    # NOTE: This is a Proxy Field which delegates to the SENAITE Registry!
620
    TextField(
621
        "EmailBodySamplePublication",
622
        default_content_type="text/html",
623
        default_output_type="text/x-html-safe",
624
        schemata="Notifications",
625
        # Needed to fetch the default value from the registry
626
        edit_accessor="getEmailBodySamplePublication",
627
        widget=RichWidget(
628
            label=_("Email body for Sample publication notifications"),
629
            description=_(
630
                "The default text that is used for the publication email. "
631
                " sending publication reports."),
632
            default_mime_type="text/x-html",
633
            output_mime_type="text/x-html",
634
            allow_file_upload=False,
635
            rows=15,
636
        ),
637
    ),
638
    BooleanField(
639
        'NotifyOnSampleRejection',
640
        schemata="Notifications",
641
        default=False,
642
        widget=BooleanWidget(
643
            label=_("Email notification on Sample rejection"),
644
            description=_("Select this to activate automatic notifications "
645
                          "via email to the Client when a Sample is rejected.")
646
        ),
647
    ),
648
    TextField(
649
        "EmailBodySampleRejection",
650
        default_content_type='text/html',
651
        default_output_type='text/x-html-safe',
652
        schemata="Notifications",
653
        label=_("Email body for Sample Rejection notifications"),
654
        default="The sample $sample_link has been rejected because of the "
655
                "following reasons:"
656
                "<br/><br/>$reasons<br/><br/>"
657
                "For further information, please contact us under the "
658
                "following address.<br/><br/>"
659
                "$lab_address",
660
        widget=RichWidget(
661
            label=_("Email body for Sample Rejection notifications"),
662
            description=_(
663
                "Set the text for the body of the email to be sent to the "
664
                "Sample's client contact if the option 'Email notification on "
665
                "Sample rejection' is enabled. You can use reserved keywords: "
666
                "$sample_id, $sample_link, $reasons, $lab_address"),
667
            default_mime_type='text/x-rst',
668
            output_mime_type='text/x-html',
669
            allow_file_upload=False,
670
            rows=15,
671
        ),
672
    ),
673
    BooleanField(
674
        'NotifyOnSampleInvalidation',
675
        schemata="Notifications",
676
        default=True,
677
        widget=BooleanWidget(
678
            label=_("Email notification on Sample invalidation"),
679
            description=_("Select this to activate automatic notifications "
680
                          "via email to the Client and Lab Managers when a "
681
                          "Sample is invalidated.")
682
        ),
683
    ),
684
    TextField(
685
        "EmailBodySampleInvalidation",
686
        default_content_type='text/html',
687
        default_output_type='text/x-html-safe',
688
        schemata="Notifications",
689
        label=_("Email body for Sample Invalidation notifications"),
690
        default=
691
            "Some non-conformities have been detected in the results report "
692
            "published for Sample $sample_link. "
693
            "<br/><br/> "
694
            "A new Sample $retest_link has been created automatically, and the "
695
            "previous request has been invalidated. "
696
            "<br/><br/> "
697
            "The root cause is under investigation and corrective "
698
            "action has been initiated. "
699
            "<br/><br/> "
700
            "$lab_address",
701
        widget=RichWidget(
702
            label=_("Email body for Sample Invalidation notifications"),
703
            description=_("Set the text for the body of the email to be sent, "
704
                          ", if option 'Email notification on Sample "
705
                          "'invalidation' enabled,  to the Sample's client "
706
                          "contact. You can use reserved keywords: $sample_id, "
707
                          "$sample_link, $retest_id, $retest_link, "
708
                          "$lab_address"),
709
            default_mime_type='text/x-rst',
710
            output_mime_type='text/x-html',
711
            allow_file_upload=False,
712
            rows=15,
713
        ),
714
    ),
715
    StringField(
716
        'AutoPrintStickers',
717
        schemata="Sticker",
718
        vocabulary=STICKER_AUTO_OPTIONS,
719
        widget=SelectionWidget(
720
            format='select',
721
            label=_("Automatic sticker printing"),
722
            description=_(
723
                "Select 'Register' if you want stickers to be automatically printed when "
724
                "new Samples or sample records are created. Select 'Receive' to print stickers "
725
                "when Samples or Samples are received. Select 'None' to disable automatic printing"),
726
        )
727
    ),
728
    StringField(
729
        'AutoStickerTemplate',
730
        schemata="Sticker",
731
        vocabulary="getStickerTemplates",
732
        widget=SelectionWidget(
733
            format='select',
734
            label=_("Sticker templates"),
735
            description=_("Select which sticker to print when automatic sticker printing is enabled"),
736
        )
737
    ),
738
    StringField(
739
        'SmallStickerTemplate',
740
        schemata="Sticker",
741
        vocabulary="getStickerTemplates",
742
        default="Code_128_1x48mm.pt",
743
        widget=SelectionWidget(
744
            format='select',
745
            label=_("Small sticker"),
746
            description=_("Select which sticker should be used as the 'small' sticker by default")
747
        )
748
    ),
749
    StringField(
750
        'LargeStickerTemplate',
751
        schemata="Sticker",
752
        vocabulary="getStickerTemplates",
753
        default="Code_128_1x72mm.pt",
754
        widget=SelectionWidget(
755
            format='select',
756
            label=_("Large sticker"),
757
            description=_("Select which sticker should be used as the 'large' sticker by default")
758
        )
759
    ),
760
    IntegerField(
761
        'DefaultNumberOfCopies',
762
        schemata="Sticker",
763
        required="1",
764
        default="1",
765
        widget=IntegerWidget(
766
            label=_("Number of copies"),
767
            description=_("Set the default number of copies to be printed for each sticker")
768
        )
769
    ),
770
    IDFormattingField(
771
        'IDFormatting',
772
        schemata="ID Server",
773
        default=[
774
            {
775
                'form': 'B-{seq:03d}',
776
                'portal_type': 'Batch',
777
                'prefix': 'batch',
778
                'sequence_type': 'generated',
779
                'split_length': 1
780
            }, {
781
                'form': 'D-{seq:03d}',
782
                'portal_type': 'DuplicateAnalysis',
783
                'prefix': 'duplicate',
784
                'sequence_type': 'generated',
785
                'split_length': 1
786
            }, {
787
                'form': 'I-{seq:03d}',
788
                'portal_type': 'Invoice',
789
                'prefix': 'invoice',
790
                'sequence_type': 'generated',
791
                'split_length': 1
792
            }, {
793
                'form': 'QC-{seq:03d}',
794
                'portal_type': 'ReferenceSample',
795
                'prefix': 'refsample',
796
                'sequence_type': 'generated',
797
                'split_length': 1
798
            }, {
799
                'form': 'SA-{seq:03d}',
800
                'portal_type': 'ReferenceAnalysis',
801
                'prefix': 'refanalysis',
802
                'sequence_type': 'generated',
803
                'split_length': 1
804
            }, {
805
                'form': 'WS-{seq:03d}',
806
                'portal_type': 'Worksheet',
807
                'prefix': 'worksheet',
808
                'sequence_type': 'generated',
809
                'split_length': 1
810
            }, {
811
                'form': '{sampleType}-{seq:04d}',
812
                'portal_type': 'AnalysisRequest',
813
                'prefix': 'analysisrequest',
814
                'sequence_type': 'generated',
815
                'split_length': 1
816
            }, {
817
                'form': '{parent_ar_id}-P{partition_count:02d}',
818
                'portal_type': 'AnalysisRequestPartition',
819
                'prefix': 'analysisrequestpartition',
820
                'sequence_type': '',
821
                'split-length': 1
822
            }, {
823
                'form': '{parent_base_id}-R{retest_count:02d}',
824
                'portal_type': 'AnalysisRequestRetest',
825
                'prefix': 'analysisrequestretest',
826
                'sequence_type': '',
827
                'split-length': 1
828
            }, {
829
                'form': '{parent_ar_id}-S{secondary_count:02d}',
830
                'portal_type': 'AnalysisRequestSecondary',
831
                'prefix': 'analysisrequestsecondary',
832
                'sequence_type': '',
833
                'split-length': 1
834
            },
835
        ],
836
        widget=RecordsWidget(
837
            label=_("Formatting Configuration"),
838
            allowDelete=True,
839
            description=_(
840
                " <p>The ID Server provides unique sequential IDs "
841
                "for objects such as Samples and Worksheets etc, based on a "
842
                "format specified for each content type.</p>"
843
                "<p>The format is constructed similarly to the Python format"
844
                " syntax, using predefined variables per content type, and"
845
                " advancing the IDs through a sequence number, 'seq' and its"
846
                " padding as a number of digits, e.g. '03d' for a sequence of"
847
                " IDs from 001 to 999.</p>"
848
                "<p>Alphanumeric prefixes for IDs are included as is in the"
849
                " formats, e.g. WS for Worksheet in WS-{seq:03d} produces"
850
                " sequential Worksheet IDs: WS-001, WS-002, WS-003 etc.</p>"
851
                "<p>For dynamic generation of alphanumeric and sequential IDs,"
852
                " the wildcard {alpha} can be used. E.g WS-{alpha:2a3d}"
853
                " produces WS-AA001, WS-AA002, WS-AB034, etc.</p>"
854
                "<p>Variables that can be used include:"
855
                "<table>"
856
                "<tr>"
857
                "<th style='width:150px'>Content Type</th><th>Variables</th>"
858
                "</tr>"
859
                "<tr><td>Client ID</td><td>{clientId}</td></tr>"
860
                "<tr><td>Year</td><td>{year}</td></tr>"
861
                "<tr><td>Sample ID</td><td>{sampleId}</td></tr>"
862
                "<tr><td>Sample Type</td><td>{sampleType}</td></tr>"
863
                "<tr><td>Sampling Date</td><td>{samplingDate}</td></tr>"
864
                "<tr><td>Date Sampled</td><td>{dateSampled}</td></tr>"
865
                "</table>"
866
                "</p>"
867
                "<p>Configuration Settings:"
868
                "<ul>"
869
                "<li>format:"
870
                "<ul><li>a python format string constructed from predefined"
871
                " variables like sampleId, clientId, sampleType.</li>"
872
                "<li>special variable 'seq' must be positioned last in the"
873
                "format string</li></ul></li>"
874
                "<li>sequence type: [generated|counter]</li>"
875
                "<li>context: if type counter, provides context the counting"
876
                " function</li>"
877
                "<li>counter type: [backreference|contained]</li>"
878
                "<li>counter reference: a parameter to the counting"
879
                " function</li>"
880
                "<li>prefix: default prefix if none provided in format"
881
                " string</li>"
882
                "<li>split length: the number of parts to be included in the"
883
                " prefix</li>"
884
                "</ul></p>")
885
        )
886
    ),
887
    StringField(
888
        'IDServerValues',
889
        schemata="ID Server",
890
        accessor="getIDServerValuesHTML",
891
        readonly=True,
892
        widget=TextAreaWidget(
893
            label=_("ID Server Values"),
894
            cols=30,
895
            rows=30,
896
        ),
897
    ),
898
    RecordsField(
899
        'RejectionReasons',
900
        schemata="Analyses",
901
        widget=RejectionSetupWidget(
902
            label=_("Enable the rejection workflow"),
903
            description=_("Select this to activate the rejection workflow "
904
                          "for Samples. A 'Reject' option will be displayed in "
905
                          "the actions menu.")
906
        ),
907
    ),
908
    IntegerField(
909
        'DefaultNumberOfARsToAdd',
910
        schemata="Analyses",
911
        required=0,
912
        default=4,
913
        widget=IntegerWidget(
914
            label=_("Default count of Sample to add."),
915
            description=_("Default value of the 'Sample count' when users click 'ADD' button to create new Samples"),
916
        )
917
    ),
918
))
919
920
schema['title'].validators = ()
921
schema['title'].widget.visible = False
922
# Update the validation layer after change the validator in runtime
923
schema['title']._validationLayer()
924
925
926
class BikaSetup(folder.ATFolder):
927
    """LIMS Setup
928
    """
929
    implements(IBikaSetup, IHideActionsMenu)
930
931
    schema = schema
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable schema does not seem to be defined.
Loading history...
932
    security = ClassSecurityInfo()
933
934
    def setAutoLogOff(self, value):
935
        """set session lifetime
936
        """
937
        value = int(value)
938
        if value < 0:
939
            value = 0
940
        value = value * 60
941
        acl = api.get_tool("acl_users")
942
        session = acl.get("session")
943
        if session:
944
            session.timeout = value
945
946
    def getAutoLogOff(self):
947
        """get session lifetime
948
        """
949
        acl = api.get_tool("acl_users")
950
        session = acl.get("session")
951
        if not session:
952
            return 0
953
        return session.timeout // 60
954
955
    def getStickerTemplates(self):
956
        """Get the sticker templates
957
        """
958
        out = [[t['id'], t['title']] for t in _getStickerTemplates()]
959
        return DisplayList(out)
960
961
    def getAnalysisServicesVocabulary(self):
962
        """
963
        Get all active Analysis Services from Bika Setup and return them as Display List.
964
        """
965
        bsc = getToolByName(self, 'senaite_catalog_setup')
966
        brains = bsc(portal_type='AnalysisService',
967
                     is_active=True)
968
        items = [(b.UID, b.Title) for b in brains]
969
        items.insert(0, ("", ""))
970
        items.sort(lambda x, y: cmp(x[1], y[1]))
971
        return DisplayList(list(items))
972
973
    def getPrefixFor(self, portal_type):
974
        """Return the prefix for a portal_type.
975
           If not found, simply uses the portal_type itself
976
        """
977
        prefix = [p for p in self.getIDFormatting() if p['portal_type'] == portal_type]
978
        if prefix:
979
            return prefix[0]['prefix']
980
        else:
981
            return portal_type
982
983
    def getCountries(self):
984
        items = geo.get_countries()
985
        items = map(lambda country: (country.alpha_2, country.name), items)
986
        return items
987
988
    def isRejectionWorkflowEnabled(self):
989
        """Return true if the rejection workflow is enabled (its checkbox is set)
990
        """
991
        widget = self.getRejectionReasons()
992
        # widget will be something like:
993
        # [{'checkbox': u'on', 'textfield-2': u'b', 'textfield-1': u'c', 'textfield-0': u'a'}]
994
        if len(widget) > 0:
995
            checkbox = widget[0].get('checkbox', False)
996
            return True if checkbox == 'on' and len(widget[0]) > 1 else False
997
        else:
998
            return False
999
1000
    def getRejectionReasonsItems(self):
1001
        """Return the list of predefined rejection reasons
1002
        """
1003
        reasons = self.getRejectionReasons()
1004
        if not reasons:
1005
            return []
1006
        reasons = reasons[0]
1007
        keys = filter(lambda key: key != "checkbox", reasons.keys())
1008
        return map(lambda key: reasons[key], sorted(keys)) or []
1009
1010
    def _getNumberOfRequiredVerificationsVocabulary(self):
1011
        """
1012
        Returns a DisplayList with the available options for the
1013
        multi-verification list: '1', '2', '3', '4'
1014
        :returns: DisplayList with the available options for the
1015
            multi-verification list
1016
        """
1017
        items = [(1, '1'), (2, '2'), (3, '3'), (4, '4')]
1018
        return IntDisplayList(list(items))
1019
1020
    def getIDServerValuesHTML(self):
1021
        number_generator = getUtility(INumberGenerator)
1022
        keys = number_generator.keys()
1023
        values = number_generator.values()
1024
        results = []
1025
        for i in range(len(keys)):
1026
            results.append('%s: %s' % (keys[i], values[i]))
1027
        return "\n".join(results)
1028
1029
    def getEmailBodySamplePublication(self):
1030
        """Get the value from the senaite setup
1031
        """
1032
        setup = api.get_senaite_setup()
1033
        # setup is `None` during initial site content structure installation
1034
        if setup:
1035
            return setup.getEmailBodySamplePublication()
1036
1037
    def setEmailBodySamplePublication(self, value):
1038
        """Set the value in the senaite setup
1039
        """
1040
        setup = api.get_senaite_setup()
1041
        # setup is `None` during initial site content structure installation
1042
        if setup:
1043
            setup.setEmailBodySamplePublication(value)
1044
1045
    def getEnableGlobalAuditlog(self):
1046
        """Get the value from the senaite setup
1047
        """
1048
        setup = api.get_senaite_setup()
1049
        # setup is `None` during initial site content structure installation
1050
        if setup:
1051
            return setup.getEnableGlobalAuditlog()
1052
        return False
1053
1054
    def setEnableGlobalAuditlog(self, value):
1055
        """Set the value in the senaite setup
1056
        """
1057
        setup = api.get_senaite_setup()
1058
        # setup is `None` during initial site content structure installation
1059
        if setup:
1060
            setup.setEnableGlobalAuditlog(value)
1061
1062
    def getImmediateResultsEntry(self):
1063
        """Get the value from the senaite setup
1064
        """
1065
        setup = api.get_senaite_setup()
1066
        # setup is `None` during initial site content structure installation
1067
        if setup:
1068
            return setup.getImmediateResultsEntry()
1069
        return False
1070
1071
    def setImmediateResultsEntry(self, value):
1072
        """Set the value in the senaite setup
1073
        """
1074
        setup = api.get_senaite_setup()
1075
        # setup is `None` during initial site content structure installation
1076
        if setup:
1077
            setup.setImmediateResultsEntry(value)
1078
1079
    def getCategorizeSampleAnalyses(self):
1080
        """Get the value from the senaite setup
1081
        """
1082
        setup = api.get_senaite_setup()
1083
        # setup is `None` during initial site content structure installation
1084
        if setup:
1085
            return setup.getCategorizeSampleAnalyses()
1086
        return False
1087
1088
    def setCategorizeSampleAnalyses(self, value):
1089
        """Set the value in the senaite setup
1090
        """
1091
        setup = api.get_senaite_setup()
1092
        # setup is `None` during initial site content structure installation
1093
        if setup:
1094
            setup.setCategorizeSampleAnalyses(value)
1095
1096
1097
registerType(BikaSetup, PROJECTNAME)
1098