Passed
Push — 2.x ( da17b1...a43b53 )
by Jordi
06:56
created

BikaSetup.setImmediateResultsEntry()   A

Complexity

Conditions 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 7
rs 10
c 0
b 0
f 0
cc 2
nop 2
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
        'EnableARSpecs',
318
        schemata="Analyses",
319
        default=False,
320
        widget=BooleanWidget(
321
            label=_("Enable Sample Specifications"),
322
            description=_(
323
                "Analysis specifications which are edited directly on the "
324
                "Sample."),
325
        ),
326
    ),
327
    IntegerField(
328
        'ExponentialFormatThreshold',
329
        schemata="Analyses",
330
        required=1,
331
        default=7,
332
        widget=IntegerWidget(
333
            label=_("Exponential format threshold"),
334
            description=_(
335
                "Result values with at least this number of significant "
336
                "digits are displayed in scientific notation using the "
337
                "letter 'e' to indicate the exponent.  The precision can be "
338
                "configured in individual Analysis Services."),
339
        )
340
    ),
341
    BooleanField(
342
        "ImmediateResultsEntry",
343
        schemata="Analyses",
344
        default=False,
345
        widget=BooleanWidget(
346
            label=_("label_bikasetup_immediateresultsentry",
347
                    default=u"Immediate results entry"),
348
            description=_(
349
                "description_bikasetup_immediateresultsentry",
350
                default=u"Allow the user to directly enter results after "
351
                "sample creation, e.g. to enter field results immediately, or "
352
                "lab results, when the automatic sample reception is "
353
                "activated."
354
            ),
355
        ),
356
    ),
357
    BooleanField(
358
        'EnableAnalysisRemarks',
359
        schemata="Analyses",
360
        default=False,
361
        widget=BooleanWidget(
362
            label=_("Add a remarks field to all analyses"),
363
            description=_(
364
                "If enabled, a free text field will be displayed close to "
365
                "each analysis in results entry view"
366
            )
367
        ),
368
    ),
369
    BooleanField(
370
        "AutoVerifySamples",
371
        schemata="Analyses",
372
        default=True,
373
        widget=BooleanWidget(
374
            label=_("Automatic verification of samples"),
375
            description=_(
376
                "When enabled, the sample is automatically verified as soon as "
377
                "all results are verified. Otherwise, users with enough "
378
                "privileges have to manually verify the sample afterwards. "
379
                "Default: enabled"
380
            )
381
        )
382
    ),
383
    BooleanField(
384
        'SelfVerificationEnabled',
385
        schemata="Analyses",
386
        default=False,
387
        widget=BooleanWidget(
388
            label=_("Allow self-verification of results"),
389
            description=_(
390
                "If enabled, a user who submitted a result will also be able "
391
                "to verify it. This setting only take effect for those users "
392
                "with a role assigned that allows them to verify results "
393
                "(by default, managers, labmanagers and verifiers)."
394
                "This setting can be overrided for a given Analysis in "
395
                "Analysis Service edit view. By default, disabled."),
396
        ),
397
    ),
398
    IntegerField(
399
        'NumberOfRequiredVerifications',
400
        schemata="Analyses",
401
        default=1,
402
        vocabulary="_getNumberOfRequiredVerificationsVocabulary",
403
        widget=SelectionWidget(
404
            format="select",
405
            label=_("Number of required verifications"),
406
            description=_(
407
                "Number of required verifications before a given result being "
408
                "considered as 'verified'. This setting can be overrided for "
409
                "any Analysis in Analysis Service edit view. By default, 1"),
410
        ),
411
    ),
412
    StringField(
413
        'TypeOfmultiVerification',
414
        schemata="Analyses",
415
        default='self_multi_enabled',
416
        vocabulary=MULTI_VERIFICATION_TYPE,
417
        widget=SelectionWidget(
418
            label=_("Multi Verification type"),
419
            description=_(
420
                "Choose type of multiple verification for the same user."
421
                "This setting can enable/disable verifying/consecutively verifying"
422
                "more than once for the same user."),
423
            format='select',
424
        )
425
    ),
426
    StringField(
427
        'ResultsDecimalMark',
428
        schemata="Analyses",
429
        vocabulary=DECIMAL_MARKS,
430
        default=".",
431
        widget=SelectionWidget(
432
            label=_("Default decimal mark"),
433
            description=_("Preferred decimal mark for results"),
434
            format='select',
435
        )
436
    ),
437
    StringField(
438
        'ScientificNotationResults',
439
        schemata="Analyses",
440
        default='1',
441
        vocabulary=SCINOTATION_OPTIONS,
442
        widget=SelectionWidget(
443
            label=_("Default scientific notation format for results"),
444
            description=_("Preferred scientific notation format for results"),
445
            format='select',
446
        )
447
    ),
448
    StringField(
449
        'WorksheetLayout',
450
        schemata="Appearance",
451
        default=DEFAULT_WORKSHEET_LAYOUT,
452
        vocabulary=getWorksheetLayouts(),
453
        widget=SelectionWidget(
454
            label=_("Default layout in worksheet view"),
455
            description=_("Preferred layout of the results entry table "
456
                          "in the Worksheet view. Classic layout displays "
457
                          "the Samples in rows and the analyses "
458
                          "in columns. Transposed layout displays the "
459
                          "Samples in columns and the analyses "
460
                          "in rows."),
461
            format='select',
462
        )
463
    ),
464
    BooleanField(
465
        'DashboardByDefault',
466
        schemata="Appearance",
467
        default=True,
468
        widget=BooleanWidget(
469
            label=_("Use Dashboard as default front page"),
470
            description=_("Select this to activate the dashboard as a default front page.")
471
        ),
472
    ),
473
    UIDReferenceField(
474
        "LandingPage",
475
        schemata="Appearance",
476
        required=0,
477
        allowed_types=(
478
            "Document",
479
            "Client",
480
            "ClientFolder",
481
            "Samples",
482
            "WorksheetFolder",
483
        ),
484
        mode="rw",
485
        multiValued=0,
486
        relationship='SetupLandingPage',
487
        widget=ReferenceWidget(
488
            label=_("Landing Page"),
489
            description=_(
490
                "The landing page is shown for non-authenticated users "
491
                "if the Dashboard is not selected as the default front page. "
492
                "If no landing page is selected, the default frontpage is displayed."),
493
            catalog_name="portal_catalog",
494
            base_query={"review_State": "published"},
495
            showOn=True,
496
        ),
497
    ),
498
    BooleanField(
499
        'PrintingWorkflowEnabled',
500
        schemata="Sampling",
501
        default=False,
502
        widget=BooleanWidget(
503
            label=_("Enable the Results Report Printing workflow"),
504
            description=_("Select this to allow the user to set an "
505
                          "additional 'Printed' status to those Analysis "
506
                          "Requests tha have been Published. "
507
                          "Disabled by default.")
508
        ),
509
    ),
510
    BooleanField(
511
        'SamplingWorkflowEnabled',
512
        schemata="Sampling",
513
        default=False,
514
        widget=BooleanWidget(
515
            label=_("Enable Sampling"),
516
            description=_("Select this to activate the sample collection workflow steps.")
517
        ),
518
    ),
519
    BooleanField(
520
        'ScheduleSamplingEnabled',
521
        schemata="Sampling",
522
        default=False,
523
        widget=BooleanWidget(
524
            label=_("Enable Sampling Scheduling"),
525
            description=_(
526
                "Select this to allow a Sampling Coordinator to" +
527
                " schedule a sampling. This functionality only takes effect" +
528
                " when 'Sampling workflow' is active")
529
        ),
530
    ),
531
    BooleanField(
532
        "AutoreceiveSamples",
533
        schemata="Sampling",
534
        default=False,
535
        widget=BooleanWidget(
536
            label=_("Auto-receive samples"),
537
            description=_(
538
                "Select to receive the samples automatically when created by "
539
                "lab personnel and sampling workflow is disabled. Samples "
540
                "created by client contacts won't be received automatically"
541
            ),
542
        ),
543
    ),
544
    BooleanField(
545
        'ShowPartitions',
546
        schemata="Appearance",
547
        default=False,
548
        widget=BooleanWidget(
549
            label=_("Display sample partitions to clients"),
550
            description=_(
551
                "Select to show sample partitions to client contacts. "
552
                "If deactivated, partitions won't be included in listings "
553
                "and no info message with links to the primary sample will "
554
                "be displayed to client contacts.")
555
        ),
556
    ),
557
    BooleanField(
558
        'SamplePreservationEnabled',
559
        schemata="Sampling",
560
        default=False,
561
        widget=BooleanWidget(
562
            label=_("Enable Sample Preservation"),
563
            description=_("")
564
        ),
565
    ),
566
    LinesField(
567
        "Workdays",
568
        schemata="Sampling",
569
        vocabulary=WEEKDAYS,
570
        default=tuple(map(str, range(7))),
571
        required=1,
572
        widget=InAndOutWidget(
573
            visible=True,
574
            label=_("Laboratory Workdays"),
575
            description=_("Only laboratory workdays are considered for the "
576
                          "analysis turnaround time calculation. "),
577
            format="checkbox",
578
        )
579
    ),
580
    DurationField(
581
        'DefaultTurnaroundTime',
582
        schemata="Sampling",
583
        required=1,
584
        default={"days": 5, "hours": 0, "minutes": 0},
585
        widget=DurationWidget(
586
            label=_("Default turnaround time for analyses."),
587
            description=_(
588
                "This is the default maximum time allowed for performing "
589
                "analyses.  It is only used for analyses where the analysis "
590
                "service does not specify a turnaround time. "
591
                "Only laboratory workdays are considered."
592
            ),
593
        )
594
    ),
595
    DurationField(
596
        'DefaultSampleLifetime',
597
        schemata="Sampling",
598
        required=1,
599
        default={"days": 30, "hours": 0, "minutes": 0},
600
        widget=DurationWidget(
601
            label=_("Default sample retention period"),
602
            description=_(
603
                "The number of days before a sample expires and cannot be analysed "
604
                "any more. This setting can be overwritten per individual sample type "
605
                "in the sample types setup"),
606
        )
607
    ),
608
    # NOTE: This is a Proxy Field which delegates to the SENAITE Registry!
609
    TextField(
610
        "EmailBodySamplePublication",
611
        default_content_type="text/html",
612
        default_output_type="text/x-html-safe",
613
        schemata="Notifications",
614
        # Needed to fetch the default value from the registry
615
        edit_accessor="getEmailBodySamplePublication",
616
        widget=RichWidget(
617
            label=_("Email body for Sample publication notifications"),
618
            description=_(
619
                "The default text that is used for the publication email. "
620
                " sending publication reports."),
621
            default_mime_type="text/x-html",
622
            output_mime_type="text/x-html",
623
            allow_file_upload=False,
624
            rows=15,
625
        ),
626
    ),
627
    BooleanField(
628
        'NotifyOnSampleRejection',
629
        schemata="Notifications",
630
        default=False,
631
        widget=BooleanWidget(
632
            label=_("Email notification on Sample rejection"),
633
            description=_("Select this to activate automatic notifications "
634
                          "via email to the Client when a Sample is rejected.")
635
        ),
636
    ),
637
    TextField(
638
        "EmailBodySampleRejection",
639
        default_content_type='text/html',
640
        default_output_type='text/x-html-safe',
641
        schemata="Notifications",
642
        label=_("Email body for Sample Rejection notifications"),
643
        default="The sample $sample_link has been rejected because of the "
644
                "following reasons:"
645
                "<br/><br/>$reasons<br/><br/>"
646
                "For further information, please contact us under the "
647
                "following address.<br/><br/>"
648
                "$lab_address",
649
        widget=RichWidget(
650
            label=_("Email body for Sample Rejection notifications"),
651
            description=_(
652
                "Set the text for the body of the email to be sent to the "
653
                "Sample's client contact if the option 'Email notification on "
654
                "Sample rejection' is enabled. You can use reserved keywords: "
655
                "$sample_id, $sample_link, $reasons, $lab_address"),
656
            default_mime_type='text/x-rst',
657
            output_mime_type='text/x-html',
658
            allow_file_upload=False,
659
            rows=15,
660
        ),
661
    ),
662
    BooleanField(
663
        'NotifyOnSampleInvalidation',
664
        schemata="Notifications",
665
        default=True,
666
        widget=BooleanWidget(
667
            label=_("Email notification on Sample invalidation"),
668
            description=_("Select this to activate automatic notifications "
669
                          "via email to the Client and Lab Managers when a "
670
                          "Sample is invalidated.")
671
        ),
672
    ),
673
    TextField(
674
        "EmailBodySampleInvalidation",
675
        default_content_type='text/html',
676
        default_output_type='text/x-html-safe',
677
        schemata="Notifications",
678
        label=_("Email body for Sample Invalidation notifications"),
679
        default=
680
            "Some non-conformities have been detected in the results report "
681
            "published for Sample $sample_link. "
682
            "<br/><br/> "
683
            "A new Sample $retest_link has been created automatically, and the "
684
            "previous request has been invalidated. "
685
            "<br/><br/> "
686
            "The root cause is under investigation and corrective "
687
            "action has been initiated. "
688
            "<br/><br/> "
689
            "$lab_address",
690
        widget=RichWidget(
691
            label=_("Email body for Sample Invalidation notifications"),
692
            description=_("Set the text for the body of the email to be sent, "
693
                          ", if option 'Email notification on Sample "
694
                          "'invalidation' enabled,  to the Sample's client "
695
                          "contact. You can use reserved keywords: $sample_id, "
696
                          "$sample_link, $retest_id, $retest_link, "
697
                          "$lab_address"),
698
            default_mime_type='text/x-rst',
699
            output_mime_type='text/x-html',
700
            allow_file_upload=False,
701
            rows=15,
702
        ),
703
    ),
704
    StringField(
705
        'AutoPrintStickers',
706
        schemata="Sticker",
707
        vocabulary=STICKER_AUTO_OPTIONS,
708
        widget=SelectionWidget(
709
            format='select',
710
            label=_("Automatic sticker printing"),
711
            description=_(
712
                "Select 'Register' if you want stickers to be automatically printed when "
713
                "new Samples or sample records are created. Select 'Receive' to print stickers "
714
                "when Samples or Samples are received. Select 'None' to disable automatic printing"),
715
        )
716
    ),
717
    StringField(
718
        'AutoStickerTemplate',
719
        schemata="Sticker",
720
        vocabulary="getStickerTemplates",
721
        widget=SelectionWidget(
722
            format='select',
723
            label=_("Sticker templates"),
724
            description=_("Select which sticker to print when automatic sticker printing is enabled"),
725
        )
726
    ),
727
    StringField(
728
        'SmallStickerTemplate',
729
        schemata="Sticker",
730
        vocabulary="getStickerTemplates",
731
        default="Code_128_1x48mm.pt",
732
        widget=SelectionWidget(
733
            format='select',
734
            label=_("Small sticker"),
735
            description=_("Select which sticker should be used as the 'small' sticker by default")
736
        )
737
    ),
738
    StringField(
739
        'LargeStickerTemplate',
740
        schemata="Sticker",
741
        vocabulary="getStickerTemplates",
742
        default="Code_128_1x72mm.pt",
743
        widget=SelectionWidget(
744
            format='select',
745
            label=_("Large sticker"),
746
            description=_("Select which sticker should be used as the 'large' sticker by default")
747
        )
748
    ),
749
    IntegerField(
750
        'DefaultNumberOfCopies',
751
        schemata="Sticker",
752
        required="1",
753
        default="1",
754
        widget=IntegerWidget(
755
            label=_("Number of copies"),
756
            description=_("Set the default number of copies to be printed for each sticker")
757
        )
758
    ),
759
    IDFormattingField(
760
        'IDFormatting',
761
        schemata="ID Server",
762
        default=[
763
            {
764
                'form': 'B-{seq:03d}',
765
                'portal_type': 'Batch',
766
                'prefix': 'batch',
767
                'sequence_type': 'generated',
768
                'split_length': 1
769
            }, {
770
                'form': 'D-{seq:03d}',
771
                'portal_type': 'DuplicateAnalysis',
772
                'prefix': 'duplicate',
773
                'sequence_type': 'generated',
774
                'split_length': 1
775
            }, {
776
                'form': 'I-{seq:03d}',
777
                'portal_type': 'Invoice',
778
                'prefix': 'invoice',
779
                'sequence_type': 'generated',
780
                'split_length': 1
781
            }, {
782
                'form': 'QC-{seq:03d}',
783
                'portal_type': 'ReferenceSample',
784
                'prefix': 'refsample',
785
                'sequence_type': 'generated',
786
                'split_length': 1
787
            }, {
788
                'form': 'SA-{seq:03d}',
789
                'portal_type': 'ReferenceAnalysis',
790
                'prefix': 'refanalysis',
791
                'sequence_type': 'generated',
792
                'split_length': 1
793
            }, {
794
                'form': 'WS-{seq:03d}',
795
                'portal_type': 'Worksheet',
796
                'prefix': 'worksheet',
797
                'sequence_type': 'generated',
798
                'split_length': 1
799
            }, {
800
                'form': '{sampleType}-{seq:04d}',
801
                'portal_type': 'AnalysisRequest',
802
                'prefix': 'analysisrequest',
803
                'sequence_type': 'generated',
804
                'split_length': 1
805
            }, {
806
                'form': '{parent_ar_id}-P{partition_count:02d}',
807
                'portal_type': 'AnalysisRequestPartition',
808
                'prefix': 'analysisrequestpartition',
809
                'sequence_type': '',
810
                'split-length': 1
811
            }, {
812
                'form': '{parent_base_id}-R{retest_count:02d}',
813
                'portal_type': 'AnalysisRequestRetest',
814
                'prefix': 'analysisrequestretest',
815
                'sequence_type': '',
816
                'split-length': 1
817
            }, {
818
                'form': '{parent_ar_id}-S{secondary_count:02d}',
819
                'portal_type': 'AnalysisRequestSecondary',
820
                'prefix': 'analysisrequestsecondary',
821
                'sequence_type': '',
822
                'split-length': 1
823
            },
824
        ],
825
        widget=RecordsWidget(
826
            label=_("Formatting Configuration"),
827
            allowDelete=True,
828
            description=_(
829
                " <p>The ID Server provides unique sequential IDs "
830
                "for objects such as Samples and Worksheets etc, based on a "
831
                "format specified for each content type.</p>"
832
                "<p>The format is constructed similarly to the Python format"
833
                " syntax, using predefined variables per content type, and"
834
                " advancing the IDs through a sequence number, 'seq' and its"
835
                " padding as a number of digits, e.g. '03d' for a sequence of"
836
                " IDs from 001 to 999.</p>"
837
                "<p>Alphanumeric prefixes for IDs are included as is in the"
838
                " formats, e.g. WS for Worksheet in WS-{seq:03d} produces"
839
                " sequential Worksheet IDs: WS-001, WS-002, WS-003 etc.</p>"
840
                "<p>For dynamic generation of alphanumeric and sequential IDs,"
841
                " the wildcard {alpha} can be used. E.g WS-{alpha:2a3d}"
842
                " produces WS-AA001, WS-AA002, WS-AB034, etc.</p>"
843
                "<p>Variables that can be used include:"
844
                "<table>"
845
                "<tr>"
846
                "<th style='width:150px'>Content Type</th><th>Variables</th>"
847
                "</tr>"
848
                "<tr><td>Client ID</td><td>{clientId}</td></tr>"
849
                "<tr><td>Year</td><td>{year}</td></tr>"
850
                "<tr><td>Sample ID</td><td>{sampleId}</td></tr>"
851
                "<tr><td>Sample Type</td><td>{sampleType}</td></tr>"
852
                "<tr><td>Sampling Date</td><td>{samplingDate}</td></tr>"
853
                "<tr><td>Date Sampled</td><td>{dateSampled}</td></tr>"
854
                "</table>"
855
                "</p>"
856
                "<p>Configuration Settings:"
857
                "<ul>"
858
                "<li>format:"
859
                "<ul><li>a python format string constructed from predefined"
860
                " variables like sampleId, clientId, sampleType.</li>"
861
                "<li>special variable 'seq' must be positioned last in the"
862
                "format string</li></ul></li>"
863
                "<li>sequence type: [generated|counter]</li>"
864
                "<li>context: if type counter, provides context the counting"
865
                " function</li>"
866
                "<li>counter type: [backreference|contained]</li>"
867
                "<li>counter reference: a parameter to the counting"
868
                " function</li>"
869
                "<li>prefix: default prefix if none provided in format"
870
                " string</li>"
871
                "<li>split length: the number of parts to be included in the"
872
                " prefix</li>"
873
                "</ul></p>")
874
        )
875
    ),
876
    StringField(
877
        'IDServerValues',
878
        schemata="ID Server",
879
        accessor="getIDServerValuesHTML",
880
        readonly=True,
881
        widget=TextAreaWidget(
882
            label=_("ID Server Values"),
883
            cols=30,
884
            rows=30,
885
        ),
886
    ),
887
    RecordsField(
888
        'RejectionReasons',
889
        schemata="Analyses",
890
        widget=RejectionSetupWidget(
891
            label=_("Enable the rejection workflow"),
892
            description=_("Select this to activate the rejection workflow "
893
                          "for Samples. A 'Reject' option will be displayed in "
894
                          "the actions menu.")
895
        ),
896
    ),
897
    IntegerField(
898
        'DefaultNumberOfARsToAdd',
899
        schemata="Analyses",
900
        required=0,
901
        default=4,
902
        widget=IntegerWidget(
903
            label=_("Default count of Sample to add."),
904
            description=_("Default value of the 'Sample count' when users click 'ADD' button to create new Samples"),
905
        )
906
    ),
907
))
908
909
schema['title'].validators = ()
910
schema['title'].widget.visible = False
911
# Update the validation layer after change the validator in runtime
912
schema['title']._validationLayer()
913
914
915
class BikaSetup(folder.ATFolder):
916
    """LIMS Setup
917
    """
918
    implements(IBikaSetup, IHideActionsMenu)
919
920
    schema = schema
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable schema does not seem to be defined.
Loading history...
921
    security = ClassSecurityInfo()
922
923
    def setAutoLogOff(self, value):
924
        """set session lifetime
925
        """
926
        value = int(value)
927
        if value < 0:
928
            value = 0
929
        value = value * 60
930
        acl = api.get_tool("acl_users")
931
        session = acl.get("session")
932
        if session:
933
            session.timeout = value
934
935
    def getAutoLogOff(self):
936
        """get session lifetime
937
        """
938
        acl = api.get_tool("acl_users")
939
        session = acl.get("session")
940
        if not session:
941
            return 0
942
        return session.timeout // 60
943
944
    def getStickerTemplates(self):
945
        """Get the sticker templates
946
        """
947
        out = [[t['id'], t['title']] for t in _getStickerTemplates()]
948
        return DisplayList(out)
949
950
    def getAnalysisServicesVocabulary(self):
951
        """
952
        Get all active Analysis Services from Bika Setup and return them as Display List.
953
        """
954
        bsc = getToolByName(self, 'senaite_catalog_setup')
955
        brains = bsc(portal_type='AnalysisService',
956
                     is_active=True)
957
        items = [(b.UID, b.Title) for b in brains]
958
        items.insert(0, ("", ""))
959
        items.sort(lambda x, y: cmp(x[1], y[1]))
960
        return DisplayList(list(items))
961
962
    def getPrefixFor(self, portal_type):
963
        """Return the prefix for a portal_type.
964
           If not found, simply uses the portal_type itself
965
        """
966
        prefix = [p for p in self.getIDFormatting() if p['portal_type'] == portal_type]
967
        if prefix:
968
            return prefix[0]['prefix']
969
        else:
970
            return portal_type
971
972
    def getCountries(self):
973
        items = geo.get_countries()
974
        items = map(lambda country: (country.alpha_2, country.name), items)
975
        return items
976
977
    def isRejectionWorkflowEnabled(self):
978
        """Return true if the rejection workflow is enabled (its checkbox is set)
979
        """
980
        widget = self.getRejectionReasons()
981
        # widget will be something like:
982
        # [{'checkbox': u'on', 'textfield-2': u'b', 'textfield-1': u'c', 'textfield-0': u'a'}]
983
        if len(widget) > 0:
984
            checkbox = widget[0].get('checkbox', False)
985
            return True if checkbox == 'on' and len(widget[0]) > 1 else False
986
        else:
987
            return False
988
989
    def getRejectionReasonsItems(self):
990
        """Return the list of predefined rejection reasons
991
        """
992
        reasons = self.getRejectionReasons()
993
        if not reasons:
994
            return []
995
        reasons = reasons[0]
996
        keys = filter(lambda key: key != "checkbox", reasons.keys())
997
        return map(lambda key: reasons[key], sorted(keys)) or []
998
999
    def _getNumberOfRequiredVerificationsVocabulary(self):
1000
        """
1001
        Returns a DisplayList with the available options for the
1002
        multi-verification list: '1', '2', '3', '4'
1003
        :returns: DisplayList with the available options for the
1004
            multi-verification list
1005
        """
1006
        items = [(1, '1'), (2, '2'), (3, '3'), (4, '4')]
1007
        return IntDisplayList(list(items))
1008
1009
    def getIDServerValuesHTML(self):
1010
        number_generator = getUtility(INumberGenerator)
1011
        keys = number_generator.keys()
1012
        values = number_generator.values()
1013
        results = []
1014
        for i in range(len(keys)):
1015
            results.append('%s: %s' % (keys[i], values[i]))
1016
        return "\n".join(results)
1017
1018
    def getEmailBodySamplePublication(self):
1019
        """Get the value from the senaite setup
1020
        """
1021
        setup = api.get_senaite_setup()
1022
        # setup is `None` during initial site content structure installation
1023
        if setup:
1024
            return setup.getEmailBodySamplePublication()
1025
1026
    def setEmailBodySamplePublication(self, value):
1027
        """Set the value in the senaite setup
1028
        """
1029
        setup = api.get_senaite_setup()
1030
        # setup is `None` during initial site content structure installation
1031
        if setup:
1032
            setup.setEmailBodySamplePublication(value)
1033
1034
    def getEnableGlobalAuditlog(self):
1035
        """Get the value from the senaite setup
1036
        """
1037
        setup = api.get_senaite_setup()
1038
        # setup is `None` during initial site content structure installation
1039
        if setup:
1040
            return setup.getEnableGlobalAuditlog()
1041
        return False
1042
1043
    def setEnableGlobalAuditlog(self, value):
1044
        """Set the value in the senaite setup
1045
        """
1046
        setup = api.get_senaite_setup()
1047
        # setup is `None` during initial site content structure installation
1048
        if setup:
1049
            setup.setEnableGlobalAuditlog(value)
1050
1051
    def getImmediateResultsEntry(self):
1052
        """Get the value from the senaite setup
1053
        """
1054
        setup = api.get_senaite_setup()
1055
        # setup is `None` during initial site content structure installation
1056
        if setup:
1057
            return setup.getImmediateResultsEntry()
1058
        return False
1059
1060
    def setImmediateResultsEntry(self, value):
1061
        """Set the value in the senaite setup
1062
        """
1063
        setup = api.get_senaite_setup()
1064
        # setup is `None` during initial site content structure installation
1065
        if setup:
1066
            setup.setImmediateResultsEntry(value)
1067
1068
1069
registerType(BikaSetup, PROJECTNAME)
1070