Passed
Push — 2.x ( 2fa8fe...c4167f )
by Ramon
06:20
created

bika.lims.content.bikasetup   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 1244
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 56
eloc 812
dl 0
loc 1244
rs 5.308
c 0
b 0
f 0

28 Methods

Rating   Name   Duplication   Size   Complexity  
A IDFormattingField.getSequenceTypes() 0 5 1
A IDFormattingField.getCounterTypes() 0 5 1
A BikaSetup.getShowLabNameInLogin() 0 8 2
A BikaSetup.setAutoLogOff() 0 11 3
A BikaSetup.isRejectionWorkflowEnabled() 0 11 3
A BikaSetup.getPrefixFor() 0 9 2
A BikaSetup.getRejectionReasonsItems() 0 9 4
A BikaSetup.getStickerTemplates() 0 5 1
A BikaSetup.getCategorizeSampleAnalyses() 0 8 2
A BikaSetup.getAutoLogOff() 0 8 2
A BikaSetup.getIDServerValuesHTML() 0 8 2
A BikaSetup.setEnableGlobalAuditlog() 0 7 2
A BikaSetup.getSampleAnalysesRequired() 0 8 2
A BikaSetup.getImmediateResultsEntry() 0 8 2
A BikaSetup.setAlwaysCCResponsiblesInReportEmail() 0 7 2
A BikaSetup.getAnalysisServicesVocabulary() 0 11 2
A BikaSetup.getCountries() 0 4 2
A BikaSetup.setSampleAnalysesRequired() 0 7 2
A BikaSetup.setMaxNumberOfSamplesAdd() 0 9 2
A BikaSetup.setCategorizeSampleAnalyses() 0 7 2
A BikaSetup.setShowLabNameInLogin() 0 7 2
A BikaSetup.getEmailBodySamplePublication() 0 7 2
A BikaSetup.getMaxNumberOfSamplesAdd() 0 8 2
A BikaSetup.setImmediateResultsEntry() 0 7 2
A BikaSetup.getAlwaysCCResponsiblesInReportEmail() 0 7 2
A BikaSetup.setEmailBodySamplePublication() 0 7 2
A BikaSetup.getEnableGlobalAuditlog() 0 8 2
A BikaSetup._getNumberOfRequiredVerificationsVocabulary() 0 9 1

How to fix   Complexity   

Complexity

Complex classes like bika.lims.content.bikasetup often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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