Passed
Push — 2.x ( 0ba30f...7ab1ba )
by Jordi
08:49
created

BikaSetup.setImmediateResultsEntry()   A

Complexity

Conditions 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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