Passed
Push — 2.x ( 6901b2...882482 )
by Jordi
07:20
created

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