Passed
Push — 2.x ( 9e83c7...b29d60 )
by Ramon
08:36 queued 02:51
created

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