senaite.core.content.senaitesetup   F
last analyzed

Complexity

Total Complexity 179

Size/Duplication

Total Lines 2393
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 179
eloc 1414
dl 0
loc 2393
rs 0.8
c 0
b 0
f 0

2 Functions

Rating   Name   Duplication   Size   Complexity  
A default_email_body_sample_publication() 0 11 2
A default_email_from_sample_publication() 0 6 1

131 Methods

Rating   Name   Duplication   Size   Complexity  
A Setup.setDefaultNumberOfARsToAdd() 0 8 1
A Setup.setAlwaysCCResponsiblesInReportEmail() 0 6 1
A Setup.setScheduleSamplingEnabled() 0 6 1
A Setup.setEmailBodySamplePublication() 0 6 1
A Setup.getSampleAnalysesRequired() 0 6 1
A Setup.setSmallStickerTemplate() 0 6 1
A Setup.setSelfVerificationEnabled() 0 6 1
A Setup.getEmailBodySamplePublication() 0 13 3
A Setup.getShowPartitions() 0 6 1
A Setup.getEnableGlobalAuditlog() 0 6 1
A Setup.getLandingPage() 0 6 1
A Setup.setScientificNotationReport() 0 6 1
A Setup.setDefaultSampleLifetime() 0 9 2
A Setup.setDateSampledRequired() 0 7 1
A Setup.getEmailFromSamplePublication() 0 9 2
A Setup.setSidebarFolders() 0 6 1
A Setup.getScheduleSamplingEnabled() 0 6 1
A Setup.setSidebarNavigationDepth() 0 6 1
A Setup.getEmailBodySampleInvalidation() 0 10 2
A Setup.getRestrictWorksheetUsersAccess() 0 6 1
A Setup.getSiteLogoCSS() 0 6 1
A Setup.getAllowToSubmitNotAssigned() 0 6 1
A Setup.getDefaultSampleLifetime() 0 6 1
A Setup.setPrintingWorkflowEnabled() 0 6 1
A Setup.getShowLabNameInLogin() 0 6 1
A Setup.setEnableGlobalAuditlog() 0 10 2
A Setup.getImmediateResultsEntry() 0 6 1
A Setup.setCurrency() 0 6 1
A Setup.setCategorizeSampleAnalyses() 0 6 1
A Setup.setDefaultTurnaroundTime() 0 9 2
A Setup.getIDServerValuesHTML() 0 6 1
A Setup.setDashboardByDefault() 0 6 1
A Setup.getSidebarSkipTypes() 0 6 1
A Setup.getWorkdays() 0 6 1
A Setup.getNumberOfRequiredVerifications() 0 11 2
A Setup.setDefaultCountry() 0 6 1
A Setup.getAllowManualResultCaptureDate() 0 6 1
A Setup.setRejectionReasons() 0 14 3
A Setup.getIDServerValues() 0 13 2
A Setup.getPrintingWorkflowEnabled() 0 6 1
A Setup.getShowPrices() 0 6 1
A Setup.setTypeOfmultiVerification() 0 6 1
A Setup.setNotifyOnSampleRejection() 0 6 1
A Setup.setSampleAnalysesRequired() 0 6 1
A Setup.laboratory() 0 12 3
A Setup.setAutoLogOff() 0 14 3
A Setup.setAutoStickerTemplate() 0 6 1
A Setup.getCurrency() 0 6 1
A Setup.getMaxNumberOfSamplesAdd() 0 6 1
A Setup.getSelfVerificationEnabled() 0 6 1
A Setup.isRejectionWorkflowEnabled() 0 5 1
A Setup.setEmailBodySampleInvalidation() 0 6 1
A Setup.setAutoreceiveSamples() 0 6 1
A Setup.setShowLabNameInLogin() 0 6 1
A Setup.getSamplingWorkflowEnabled() 0 6 1
A Setup.setEnableARSpecs() 0 6 1
A Setup.setLandingPage() 0 6 1
A Setup.setAutoPrintStickers() 0 6 1
A Setup.setWorkdays() 0 6 1
A Setup.getInvalidationReasonRequired() 0 7 1
A Setup.setMaxNumberOfSamplesAdd() 0 8 1
A Setup.setMemberDiscount() 0 14 4
A Setup.getScientificNotationReport() 0 8 1
A Setup.setInvalidationReasonRequired() 0 7 1
A Setup.setAllowToSubmitNotAssigned() 0 6 1
A Setup.getSidebarNavigationDepth() 0 7 2
A Setup.setSidebarSkipTypes() 0 6 1
A Setup.getCategoriseAnalysisServices() 0 6 1
A Setup.getDecimalMark() 0 8 1
A Setup.getEnableAnalysisRemarks() 0 6 1
A Setup.setRestrictWorksheetUsersAccess() 0 6 1
A Setup.getResultsDecimalMark() 0 8 1
A Setup.setResultsDecimalMark() 0 6 1
A Setup.getExponentialFormatThreshold() 0 6 1
A Setup.getAutoPrintStickers() 0 11 2
A Setup.setEmailBodySampleRejection() 0 6 1
A Setup.getRejectionReasonsItems() 0 10 1
A Setup.getNotifyOnSampleRejection() 0 6 1
A Setup.getAutoVerifySamples() 0 6 1
A Setup.setScientificNotationResults() 0 6 1
A Setup.setAllowManualResultCaptureDate() 0 6 1
A Setup.getDefaultNumberOfARsToAdd() 0 6 1
A Setup.getAutoLogOff() 0 9 2
A Setup.getDefaultCountry() 0 6 1
A Setup.getRejectionReasons() 0 12 2
A Setup.getWorksheetLayout() 0 6 1
A Setup.getSmallStickerTemplate() 0 6 1
A Setup.getTypeOfmultiVerification() 0 6 1
A Setup.getAlwaysCCResponsiblesInReportEmail() 0 6 1
A Setup.setWorksheetLayout() 0 6 1
A Setup.setRestrictWorksheetManagement() 0 6 1
A Setup.getSidebarFolders() 0 6 1
A Setup.setDecimalMark() 0 6 1
A Setup.getAutoStickerTemplate() 0 6 1
A Setup.getRestrictWorksheetManagement() 0 6 1
A Setup.setSiteLogo() 0 6 1
A Setup.setShowPartitions() 0 6 1
A Setup.setLargeStickerTemplate() 0 6 1
A Setup.getEnableARSpecs() 0 6 1
A Setup.getAutoreceiveSamples() 0 6 1
A Setup.setMinimumResults() 0 8 1
A Setup.getScientificNotationResults() 0 8 1
A Setup.setEnableRejectionWorkflow() 0 6 1
A Setup.setSiteLogoCSS() 0 6 1
A Setup.getSiteLogo() 0 6 1
A Setup.setShowPrices() 0 6 1
A Setup.getSamplePreservationEnabled() 0 6 1
A Setup.getVAT() 0 13 4
A Setup.setVAT() 0 14 4
A Setup.getDashboardByDefault() 0 6 1
A Setup.setSamplingWorkflowEnabled() 0 6 1
A Setup.getMinimumResults() 0 6 1
A Setup.setEnableAnalysisRemarks() 0 6 1
B Setup.getIDFormatting() 0 28 7
A Setup.getDefaultNumberOfCopies() 0 6 1
A Setup.setSamplePreservationEnabled() 0 6 1
A Setup.getCategorizeSampleAnalyses() 0 6 1
A Setup.setDefaultNumberOfCopies() 0 8 1
A Setup.getLargeStickerTemplate() 0 6 1
B Setup.setIDFormatting() 0 29 8
A Setup.getDefaultTurnaroundTime() 0 6 1
A Setup.getEmailBodySampleRejection() 0 10 2
A Setup.setExponentialFormatThreshold() 0 8 1
A Setup.setNumberOfRequiredVerifications() 0 6 1
A Setup.setEmailFromSamplePublication() 0 6 1
A Setup.getEnableRejectionWorkflow() 0 6 1
A Setup.setCategoriseAnalysisServices() 0 6 1
A Setup.getMemberDiscount() 0 13 4
A Setup.setImmediateResultsEntry() 0 6 1
A Setup.getDateSampledRequired() 0 7 1
A Setup.setAutoVerifySamples() 0 6 1

How to fix   Complexity   

Complexity

Complex classes like senaite.core.content.senaitesetup 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-2025 by it's authors.
19
# Some rights reserved, see README and LICENSE.
20
21
from datetime import timedelta
22
23
import six
24
from AccessControl import ClassSecurityInfo
25
from bika.lims import _
26
from bika.lims import api
27
from plone.app.textfield import IRichTextValue
28
from plone.app.textfield.widget import RichTextFieldWidget  # TBD: port to core
29
from plone.autoform import directives
30
from plone.formwidget.namedfile.widget import NamedFileFieldWidget
31
from plone.schema.email import Email
32
from plone.supermodel import model
33
from Products.CMFCore import permissions
34
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
35
from senaite.core.api import dtime
36
from senaite.core.catalog import AUDITLOG_CATALOG
37
from senaite.core.content.base import Container
38
from senaite.core.interfaces import IHideActionsMenu
39
from senaite.core.interfaces import ISetup
40
from senaite.core.schema import DurationField
41
from senaite.core.schema import RichTextField
42
from senaite.core.schema import UIDReferenceField
43
from senaite.core.schema.fields import DataGridField
44
from senaite.core.schema.fields import DataGridRow
45
from senaite.core.schema.textlinefield import TextLineField
46
from senaite.core.z3cform.widgets.datagrid import DataGridWidgetFactory
47
from senaite.core.z3cform.widgets.duration.widget import DurationWidgetFactory
48
from zope import schema
49
from zope.component import getUtility
50
from zope.deprecation import deprecate
51
from zope.interface import Interface
52
from zope.interface import implementer
53
from zope.interface import provider
54
from zope.schema.interfaces import IContextAwareDefaultFactory
55
56
57
@provider(IContextAwareDefaultFactory)
58
def default_email_body_sample_publication(context):
59
    """Returns the default body text for publication emails
60
    """
61
    view = api.get_view("senaite_view", context=api.get_senaite_setup())
62
    if view is None:
63
        # Test fixture
64
        return u""
65
    tpl = ViewPageTemplateFile(
66
        "../browser/setup/templates/email_body_sample_publication.pt")
67
    return tpl(view)
68
69
70
@provider(IContextAwareDefaultFactory)
71
def default_email_from_sample_publication(context):
72
    """Returns the default email 'From' for results reports publish
73
    """
74
    portal_email = api.get_registry_record("plone.email_from_address")
75
    return portal_email
76
77
78
DEFAULT_ID_FORMATTING = [
79
    {
80
        "form": "B-{seq:03d}",
81
        "portal_type": "Batch",
82
        "prefix": "batch",
83
        "sequence_type": "generated",
84
        "context": "",
85
        "counter_type": "",
86
        "counter_reference": "",
87
        "split_length": 1
88
    },
89
    {
90
        "form": "D-{seq:03d}",
91
        "portal_type": "DuplicateAnalysis",
92
        "prefix": "duplicate",
93
        "sequence_type": "generated",
94
        "context": "",
95
        "counter_type": "",
96
        "counter_reference": "",
97
        "split_length": 1
98
    },
99
    {
100
        "form": "I-{seq:03d}",
101
        "portal_type": "Invoice",
102
        "prefix": "invoice",
103
        "sequence_type": "generated",
104
        "context": "",
105
        "counter_type": "",
106
        "counter_reference": "",
107
        "split_length": 1
108
    },
109
    {
110
        "form": "QC-{seq:03d}",
111
        "portal_type": "ReferenceSample",
112
        "prefix": "refsample",
113
        "sequence_type": "generated",
114
        "context": "",
115
        "counter_type": "",
116
        "counter_reference": "",
117
        "split_length": 1
118
    },
119
    {
120
        "form": "SA-{seq:03d}",
121
        "portal_type": "ReferenceAnalysis",
122
        "prefix": "refanalysis",
123
        "sequence_type": "generated",
124
        "context": "",
125
        "counter_type": "",
126
        "counter_reference": "",
127
        "split_length": 1
128
    },
129
    {
130
        "form": "WS-{seq:03d}",
131
        "portal_type": "Worksheet",
132
        "prefix": "worksheet",
133
        "sequence_type": "generated",
134
        "context": "",
135
        "counter_type": "",
136
        "counter_reference": "",
137
        "split_length": 1
138
    },
139
    {
140
        "form": "{sampleType}-{seq:04d}",
141
        "portal_type": "AnalysisRequest",
142
        "prefix": "analysisrequest",
143
        "sequence_type": "generated",
144
        "context": "",
145
        "counter_type": "",
146
        "counter_reference": "",
147
        "split_length": 1
148
    },
149
    {
150
        "form": "{parent_ar_id}-P{partition_count:02d}",
151
        "portal_type": "AnalysisRequestPartition",
152
        "prefix": "analysisrequestpartition",
153
        "sequence_type": "",
154
        "context": "",
155
        "counter_type": "",
156
        "counter_reference": "",
157
        "split_length": 1
158
    },
159
    {
160
        "form": "{parent_base_id}-R{retest_count:02d}",
161
        "portal_type": "AnalysisRequestRetest",
162
        "prefix": "analysisrequestretest",
163
        "sequence_type": "",
164
        "context": "",
165
        "counter_type": "",
166
        "counter_reference": "",
167
        "split_length": 1
168
    },
169
    {
170
        "form": "{parent_ar_id}-S{secondary_count:02d}",
171
        "portal_type": "AnalysisRequestSecondary",
172
        "prefix": "analysisrequestsecondary",
173
        "sequence_type": "",
174
        "context": "",
175
        "counter_type": "",
176
        "counter_reference": "",
177
        "split_length": 1
178
    },
179
]
180
181
182
class IIDFormattingRecordSchema(Interface):
183
    """Schema for ID formatting configuration records
184
    """
185
186
    portal_type = TextLineField(
187
        title=_(u"Portal Type"),
188
        required=False,
189
    )
190
191
    form = TextLineField(
192
        title=_(u"Format"),
193
        required=False,
194
    )
195
196
    sequence_type = schema.Choice(
197
        title=_(u"Seq Type"),
198
        values=["", "counter", "generated"],
199
        required=False,
200
        default="",
201
    )
202
203
    context = TextLineField(
204
        title=_(u"Context"),
205
        required=False,
206
    )
207
208
    counter_type = schema.Choice(
209
        title=_(u"Counter Type"),
210
        values=["", "backreference", "contained"],
211
        required=False,
212
        default="",
213
    )
214
215
    counter_reference = TextLineField(
216
        title=_(u"Counter Ref"),
217
        required=False,
218
    )
219
220
    prefix = TextLineField(
221
        title=_(u"Prefix"),
222
        required=False,
223
    )
224
225
    split_length = schema.Int(
226
        title=_(u"Split Length"),
227
        required=False,
228
        default=1,
229
    )
230
231
232
class ISetupSchema(model.Schema):
233
    """Schema and marker interface
234
    """
235
236
    email_from_sample_publication = Email(
237
        title=_(
238
            "title_senaitesetup_email_from_sample_publication",
239
            default="Publication 'From' address"
240
        ),
241
        description=_(
242
            "description_senaitesetup_email_from_sample_publication",
243
            default="E-mail to use as the 'From' address for outgoing e-mails "
244
                    "when publishing results reports. This address overrides "
245
                    "the value set at portal's 'Mail settings'."
246
        ),
247
        defaultFactory=default_email_from_sample_publication,
248
        required=False,
249
    )
250
251
    directives.widget("email_body_sample_publication", RichTextFieldWidget)
252
    email_body_sample_publication = RichTextField(
253
        title=_("title_senaitesetup_publication_email_text",
254
                default=u"Publication Email Text"),
255
        description=_(
256
            "description_senaitesetup_publication_email_text",
257
            default=u"Set the email body text to be used by default "
258
            "when sending out result reports to the selected recipients. "
259
            "You can use reserved keywords: "
260
            "$client_name, $recipients, $lab_name, $lab_address"),
261
        defaultFactory=default_email_body_sample_publication,
262
        required=False,
263
    )
264
265
    always_cc_responsibles_in_report_emails = schema.Bool(
266
        title=_(
267
            "title_senaitesetup_always_cc_responsibles_in_report_emails",
268
            default=u"Always send publication email to responsibles"),
269
        description=_(
270
            "description_senaitesetup_always_cc_responsibles_in_report_emails",
271
            default="When selected, the responsible persons of all involved "
272
            "lab departments will receive publication emails."
273
        ),
274
        default=True,
275
    )
276
277
    enable_global_auditlog = schema.Bool(
278
        title=_(u"Enable global Auditlog"),
279
        description=_(
280
            "The global Auditlog shows all modifications of the system. "
281
            "When enabled, all entities will be indexed in a separate "
282
            "catalog. This will increase the time when objects are "
283
            "created or modified."
284
        ),
285
        default=False,
286
    )
287
288
    # NOTE:
289
    # We use the `NamedFileFieldWidget` instead of `NamedImageFieldWidget`
290
    # by purpose! Using the latter rises this PIL error (appears only in log):
291
    # IOError: cannot identify image file <cStringIO.StringI object at ...>
292
    directives.widget("site_logo", NamedFileFieldWidget)
293
    site_logo = schema.Bytes(
294
        title=_(u"Site Logo"),
295
        description=_(u"This shows a custom logo on your SENAITE site."),
296
        required=False,
297
    )
298
299
    site_logo_css = schema.ASCII(
300
        title=_(u"Site Logo CSS"),
301
        description=_(
302
            u"Add custom CSS rules for the Logo, "
303
            u"e.g. height:15px; width:150px;"
304
        ),
305
        required=False,
306
    )
307
308
    immediate_results_entry = schema.Bool(
309
        title=_(u"Immediate results entry"),
310
        description=_(
311
            "description_senaitesetup_immediateresultsentry",
312
            default=u"Allow the user to directly enter results after sample "
313
            "creation, e.g. to enter field results immediately, or lab "
314
            "results, when the automatic sample reception is activated."
315
        ),
316
    )
317
318
    categorize_sample_analyses = schema.Bool(
319
        title=_("title_senaitesetup_categorizesampleanalyses",
320
                default=u"Categorize sample analyses"),
321
        description=_(
322
            "description_senaitesetup_categorizesampleanalyses",
323
            default=u"Group analyses by category for samples"
324
        ),
325
        default=False,
326
    )
327
328
    sample_analyses_required = schema.Bool(
329
        title=_("title_senaitesetup_sampleanalysesrequired",
330
                default=u"Require sample analyses"),
331
        description=_(
332
            "description_senaitesetup_sampleanalysesrequired",
333
            default=u"Analyses are required for sample registration"
334
        ),
335
        default=True,
336
    )
337
338
    # Allow Manual Analysis Result Capture Date
339
    allow_manual_result_capture_date = schema.Bool(
340
        title=_("title_senaitesetup_allow_manual_result_capture_date",
341
                default=u"Allow to set the result capture date"),
342
        description=_(
343
            "description_senaitesetup_allow_manual_result_capture_date",
344
            default=u"If this option is activated, the result capture date "
345
                    u"can be entered manually for analyses"),
346
        default=False)
347
348
    max_number_of_samples_add = schema.Int(
349
        title=_(
350
            u"label_senaitesetup_maxnumberofsamplesadd",
351
            default=u"Maximum value for 'Number of samples' field on "
352
                    u"registration"
353
        ),
354
        description=_(
355
            u"description_senaitesetup_maxnumberofsamplesadd",
356
            default=u"Maximum number of samples that can be created in "
357
                    u"accordance with the value set for the field 'Number of "
358
                    u"samples' on the sample registration form"
359
        ),
360
        default=10
361
    )
362
363
    date_sampled_required = schema.Bool(
364
        title=_(
365
            u"title_senaitesetup_date_sampled_required",
366
            default=u"Date sampled required"),
367
        description=_(
368
            u"description_senaitesetup_date_sampled_required",
369
            default=u"Select this to make DateSampled field required on "
370
                    u"sample creation. This functionality only takes effect "
371
                    u"when 'Sampling workflow' is not active"
372
        ),
373
        default=True,
374
    )
375
376
    show_lab_name_in_login = schema.Bool(
377
        title=_(
378
            u"title_senaitesetup_show_lab_name_in_login",
379
            default=u"Display laboratory name in the login page"),
380
        description=_(
381
            u"description_senaitesetup_show_lab_name_in_login",
382
            default=u"When selected, the laboratory name will be displayed"
383
                    u"in the login page, above the access credentials."
384
        ),
385
        default=False,
386
    )
387
388
    invalidation_reason_required = schema.Bool(
389
        title=_(
390
            u"title_senaitesetup_invalidation_reason_required",
391
            default=u"Invalidation reason required"),
392
        description=_(
393
            u"description_senaitesetup_invalidation_reason_required",
394
            default=u"Specify whether providing a reason is mandatory when "
395
                    u"invalidating a sample. If enabled, the '$reason' "
396
                    u"placeholder in the sample invalidation notification "
397
                    u"email body will be replaced with the entered reason."
398
        ),
399
        default=True,
400
    )
401
402
    # Security
403
    auto_log_off = schema.Int(
404
        title=_(
405
            u"title_senaitesetup_auto_log_off",
406
            default=u"Automatic log-off"
407
        ),
408
        description=_(
409
            u"description_senaitesetup_auto_log_off",
410
            default=u"The number of minutes before a user is automatically "
411
                    u"logged off. 0 disables automatic log-off"
412
        ),
413
        required=True,
414
        default=0,
415
    )
416
417
    restrict_worksheet_users_access = schema.Bool(
418
        title=_(
419
            u"title_senaitesetup_restrict_worksheet_users_access",
420
            default=u"Restrict worksheet access to assigned analysts"
421
        ),
422
        description=_(
423
            u"description_senaitesetup_restrict_worksheet_users_access",
424
            default=u"When enabled, analysts can only access worksheets to "
425
                    u"which they are assigned. When disabled, analysts have "
426
                    u"access to all worksheets."
427
        ),
428
        default=True,
429
    )
430
431
    allow_to_submit_not_assigned = schema.Bool(
432
        title=_(
433
            u"title_senaitesetup_allow_to_submit_not_assigned",
434
            default=u"Allow submission of results for unassigned analyses"
435
        ),
436
        description=_(
437
            u"description_senaitesetup_allow_to_submit_not_assigned",
438
            default=u"When enabled, users can submit results for analyses not "
439
                    u"assigned to them or for unassigned analyses. When "
440
                    u"disabled, users can only submit results for analyses "
441
                    u"assigned to themselves. This setting does not apply to "
442
                    u"users with role Lab Manager."
443
        ),
444
        default=True,
445
    )
446
447
    restrict_worksheet_management = schema.Bool(
448
        title=_(
449
            u"title_senaitesetup_restrict_worksheet_management",
450
            default=u"Restrict worksheet management to lab managers"
451
        ),
452
        description=_(
453
            u"description_senaitesetup_restrict_worksheet_management",
454
            default=u"When enabled, only lab managers can create and manage "
455
                    u"worksheets. When disabled, analysts and lab clerks can "
456
                    u"also manage worksheets. Note: This setting is "
457
                    u"automatically enabled and locked when worksheet access "
458
                    u"is restricted to assigned analysts."
459
        ),
460
        default=True,
461
    )
462
463
    # Accounting
464
    show_prices = schema.Bool(
465
        title=_(
466
            u"title_senaitesetup_show_prices",
467
            default=u"Include and display pricing information"
468
        ),
469
        default=True,
470
    )
471
472
    currency = schema.Choice(
473
        title=_(
474
            u"title_senaitesetup_currency",
475
            default=u"Currency"
476
        ),
477
        description=_(
478
            u"description_senaitesetup_currency",
479
            default=u"Select the currency the site will use to display prices."
480
        ),
481
        vocabulary="senaite.core.vocabularies.currencies",
482
        required=True,
483
        default="EUR",
484
    )
485
486
    default_country = schema.Choice(
487
        title=_(
488
            u"title_senaitesetup_default_country",
489
            default=u"Country"
490
        ),
491
        description=_(
492
            u"description_senaitesetup_default_country",
493
            default=u"Select the country the site will show by default"
494
        ),
495
        vocabulary="senaite.core.vocabularies.countries",
496
        required=False,
497
    )
498
499
    directives.widget("member_discount", klass="numeric")
500
    member_discount = schema.TextLine(
501
        title=_(
502
            u"title_senaitesetup_member_discount",
503
            default=u"Member discount %"
504
        ),
505
        description=_(
506
            u"description_senaitesetup_member_discount",
507
            default=u"The discount percentage entered here, is applied to the "
508
                    u"prices for clients flagged as 'members', normally "
509
                    u"co-operative members or associates deserving of this "
510
                    u"discount"
511
        ),
512
        required=True,
513
        default=u"0.0",
514
    )
515
516
    directives.widget("vat", klass="numeric")
517
    vat = schema.TextLine(
518
        title=_(
519
            u"title_senaitesetup_vat",
520
            default=u"VAT %"
521
        ),
522
        description=_(
523
            u"description_senaitesetup_vat",
524
            default=u"Enter percentage value eg. 14.0. This percentage is "
525
                    u"applied system wide but can be overwritten on individual "
526
                    u"items"
527
        ),
528
        required=True,
529
        default=u"0.0",
530
    )
531
532
    # Results Reports
533
    decimal_mark = schema.Choice(
534
        title=_(
535
            u"title_senaitesetup_decimal_mark",
536
            default=u"Default decimal mark"
537
        ),
538
        description=_(
539
            u"description_senaitesetup_decimal_mark",
540
            default=u"Preferred decimal mark for reports."
541
        ),
542
        vocabulary="senaite.core.vocabularies.decimal_marks",
543
        default=".",
544
    )
545
546
    scientific_notation_report = schema.Choice(
547
        title=_(
548
            u"title_senaitesetup_scientific_notation_report",
549
            default=u"Default scientific notation format for reports"
550
        ),
551
        description=_(
552
            u"description_senaitesetup_scientific_notation_report",
553
            default=u"Preferred scientific notation format for reports"
554
        ),
555
        vocabulary="senaite.core.vocabularies.scientific_notation",
556
        default="1",
557
    )
558
559
    minimum_results = schema.Int(
560
        title=_(
561
            u"title_senaitesetup_minimum_results",
562
            default=u"Minimum number of results for QC stats calculations"
563
        ),
564
        description=_(
565
            u"description_senaitesetup_minimum_results",
566
            default=u"Using too few data points does not make statistical "
567
                    u"sense. Set an acceptable minimum number of results before "
568
                    u"QC statistics will be calculated and plotted"
569
        ),
570
        required=True,
571
        default=5,
572
    )
573
574
    # Analyses
575
    categorise_analysis_services = schema.Bool(
576
        title=_(
577
            u"title_senaitesetup_categorise_analysis_services",
578
            default=u"Categorise analysis services"
579
        ),
580
        description=_(
581
            u"description_senaitesetup_categorise_analysis_services",
582
            default=u"Group analysis services by category in the LIMS tables, "
583
                    u"helpful when the list is long"
584
        ),
585
        default=False,
586
    )
587
588
    enable_ar_specs = schema.Bool(
589
        title=_(
590
            u"title_senaitesetup_enable_ar_specs",
591
            default=u"Enable Sample Specifications"
592
        ),
593
        description=_(
594
            u"description_senaitesetup_enable_ar_specs",
595
            default=u"Analysis specifications which are edited directly on the "
596
                    u"Sample."
597
        ),
598
        default=False,
599
    )
600
601
    exponential_format_threshold = schema.Int(
602
        title=_(
603
            u"title_senaitesetup_exponential_format_threshold",
604
            default=u"Exponential format threshold"
605
        ),
606
        description=_(
607
            u"description_senaitesetup_exponential_format_threshold",
608
            default=u"Result values with at least this number of significant "
609
                    u"digits are displayed in scientific notation using the "
610
                    u"letter 'e' to indicate the exponent. The precision can be "
611
                    u"configured in individual Analysis Services."
612
        ),
613
        required=True,
614
        default=7,
615
    )
616
617
    enable_analysis_remarks = schema.Bool(
618
        title=_(
619
            u"title_senaitesetup_enable_analysis_remarks",
620
            default=u"Add a remarks field to all analyses"
621
        ),
622
        description=_(
623
            u"description_senaitesetup_enable_analysis_remarks",
624
            default=u"If enabled, a free text field will be displayed close to "
625
                    u"each analysis in results entry view"
626
        ),
627
        default=False,
628
    )
629
630
    auto_verify_samples = schema.Bool(
631
        title=_(
632
            u"title_senaitesetup_auto_verify_samples",
633
            default=u"Automatic verification of samples"
634
        ),
635
        description=_(
636
            u"description_senaitesetup_auto_verify_samples",
637
            default=u"When enabled, the sample is automatically verified as "
638
                    u"soon as all results are verified. Otherwise, users with "
639
                    u"enough privileges have to manually verify the sample "
640
                    u"afterwards. Default: enabled"
641
        ),
642
        default=True,
643
    )
644
645
    self_verification_enabled = schema.Bool(
646
        title=_(
647
            u"title_senaitesetup_self_verification_enabled",
648
            default=u"Allow self-verification of results"
649
        ),
650
        description=_(
651
            u"description_senaitesetup_self_verification_enabled",
652
            default=u"If enabled, a user who submitted a result will also be "
653
                    u"able to verify it. This setting only take effect for "
654
                    u"those users with a role assigned that allows them to "
655
                    u"verify results (by default, managers, labmanagers and "
656
                    u"verifiers). This setting can be overrided for a given "
657
                    u"Analysis in Analysis Service edit view. By default, "
658
                    u"disabled."
659
        ),
660
        default=False,
661
    )
662
663
    number_of_required_verifications = schema.Choice(
664
        title=_(
665
            u"title_senaitesetup_number_of_required_verifications",
666
            default=u"Number of required verifications"
667
        ),
668
        description=_(
669
            u"description_senaitesetup_number_of_required_verifications",
670
            default=u"Number of required verifications before a given result "
671
                    u"being considered as 'verified'. This setting can be "
672
                    u"overrided for any Analysis in Analysis Service edit view. "
673
                    u"By default, 1"
674
        ),
675
        vocabulary="senaite.core.vocabularies.number_of_verifications",
676
        default=1,
677
    )
678
679
    type_of_multi_verification = schema.Choice(
680
        title=_(
681
            u"title_senaitesetup_type_of_multi_verification",
682
            default=u"Multi Verification type"
683
        ),
684
        description=_(
685
            u"description_senaitesetup_type_of_multi_verification",
686
            default=u"Choose type of multiple verification for the same user. "
687
                    u"This setting can enable/disable verifying/consecutively "
688
                    u"verifying more than once for the same user."
689
        ),
690
        vocabulary="senaite.core.vocabularies.multi_verification_type",
691
        default="self_multi_enabled",
692
    )
693
694
    results_decimal_mark = schema.Choice(
695
        title=_(
696
            u"title_senaitesetup_results_decimal_mark",
697
            default=u"Default decimal mark"
698
        ),
699
        description=_(
700
            u"description_senaitesetup_results_decimal_mark",
701
            default=u"Preferred decimal mark for results"
702
        ),
703
        vocabulary="senaite.core.vocabularies.decimal_marks",
704
        default=".",
705
    )
706
707
    scientific_notation_results = schema.Choice(
708
        title=_(
709
            u"title_senaitesetup_scientific_notation_results",
710
            default=u"Default scientific notation format for results"
711
        ),
712
        description=_(
713
            u"description_senaitesetup_scientific_notation_results",
714
            default=u"Preferred scientific notation format for results"
715
        ),
716
        vocabulary="senaite.core.vocabularies.scientific_notation",
717
        default="1",
718
    )
719
720
    default_number_of_ars_to_add = schema.Int(
721
        title=_(
722
            u"title_senaitesetup_default_number_of_ars_to_add",
723
            default=u"Default count of Sample to add."
724
        ),
725
        description=_(
726
            u"description_senaitesetup_default_number_of_ars_to_add",
727
            default=u"Default value of the 'Sample count' when users click "
728
                    u"'ADD' button to create new Samples"
729
        ),
730
        required=False,
731
        default=4,
732
    )
733
734
    enable_rejection_workflow = schema.Bool(
735
        title=_(
736
            u"title_senaitesetup_enable_rejection_workflow",
737
            default=u"Enable the rejection workflow"
738
        ),
739
        description=_(
740
            u"description_senaitesetup_enable_rejection_workflow",
741
            default=u"Select this to activate the rejection workflow for "
742
                    u"Samples. A 'Reject' option will be displayed in the "
743
                    u"actions menu."
744
        ),
745
        required=False,
746
        default=False,
747
    )
748
749
    rejection_reasons = schema.List(
750
        title=_(
751
            u"title_senaitesetup_rejection_reasons",
752
            default=u"Rejection reasons"
753
        ),
754
        description=_(
755
            u"description_senaitesetup_rejection_reasons",
756
            default=u"Enter the predefined rejection reasons that users can "
757
                    u"select when rejecting a sample."
758
        ),
759
        value_type=schema.TextLine(),
760
        required=False,
761
        default=[],
762
    )
763
764
    max_number_of_samples_add = schema.Int(
765
        title=_(
766
            u"label_senaitesetup_maxnumberofsamplesadd",
767
            default=u"Maximum value for 'Number of samples' field on "
768
                    u"registration"
769
        ),
770
        description=_(
771
            u"description_senaitesetup_maxnumberofsamplesadd",
772
            default=u"Maximum number of samples that can be created in "
773
                    u"accordance with the value set for the field 'Number "
774
                    u"of samples' on the sample registration form"
775
        ),
776
        required=False,
777
        default=10,
778
    )
779
780
    # Appearance
781
    worksheet_layout = schema.Choice(
782
        title=_(
783
            u"title_senaitesetup_worksheet_layout",
784
            default=u"Default layout in worksheet view"
785
        ),
786
        description=_(
787
            u"description_senaitesetup_worksheet_layout",
788
            default=u"Preferred layout of the results entry table in the "
789
                    u"Worksheet view. Classic layout displays the Samples in "
790
                    u"rows and the analyses in columns. Transposed layout "
791
                    u"displays the Samples in columns and the analyses in rows."
792
        ),
793
        vocabulary="senaite.core.vocabularies.worksheet_layout",
794
        default="analyses_classic_view",
795
    )
796
797
    dashboard_by_default = schema.Bool(
798
        title=_(
799
            u"title_senaitesetup_dashboard_by_default",
800
            default=u"Use Dashboard as default front page"
801
        ),
802
        description=_(
803
            u"description_senaitesetup_dashboard_by_default",
804
            default=u"Select this to activate the dashboard as a default front "
805
                    u"page."
806
        ),
807
        default=True,
808
    )
809
810
    landing_page = UIDReferenceField(
811
        title=_(
812
            u"title_senaitesetup_landing_page",
813
            default=u"Landing Page"
814
        ),
815
        description=_(
816
            u"description_senaitesetup_landing_page",
817
            default=u"The landing page is shown for non-authenticated users if "
818
                    u"the Dashboard is not selected as the default front page. "
819
                    u"If no landing page is selected, the default frontpage is "
820
                    u"displayed."
821
        ),
822
        allowed_types=(
823
            "Document",
824
            "Client",
825
            "ClientFolder",
826
            "Samples",
827
            "WorksheetFolder",
828
        ),
829
        multi_valued=False,
830
        relationship="SetupLandingPage",
831
        required=False,
832
    )
833
834
    show_partitions = schema.Bool(
835
        title=_(
836
            u"title_senaitesetup_show_partitions",
837
            default=u"Display sample partitions to clients"
838
        ),
839
        description=_(
840
            u"description_senaitesetup_show_partitions",
841
            default=u"Select to show sample partitions to client contacts. If "
842
                    u"deactivated, partitions won't be included in listings and "
843
                    u"no info message with links to the primary sample will be "
844
                    u"displayed to client contacts."
845
        ),
846
        default=False,
847
    )
848
849
    sidebar_folders = schema.Tuple(
850
        title=_(
851
            u"title_senaitesetup_sidebar_folders",
852
            default=u"Sidebar navigation folders"
853
        ),
854
        description=_(
855
            u"description_senaitesetup_sidebar_folders",
856
            default=u"Select which top-level folders should be displayed in "
857
                    u"the sidebar navigation. The order of selection determines "
858
                    u"the display order in the sidebar. If none are selected, "
859
                    u"all folders will be shown in the default order."
860
        ),
861
        value_type=schema.Choice(
862
            vocabulary="senaite.core.vocabularies.top_level_folders"
863
        ),
864
        required=False,
865
        default=("clients", "samples", "methods", "batches", "worksheets"),
866
    )
867
868
    sidebar_navigation_depth = schema.Int(
869
        title=_(
870
            u"title_senaitesetup_sidebar_navigation_depth",
871
            default=u"Sidebar navigation depth"
872
        ),
873
        description=_(
874
            u"description_senaitesetup_sidebar_navigation_depth",
875
            default=u"Maximum depth of the sidebar navigation tree. "
876
                    u"Level 1 shows only top-level folders, level 2 includes "
877
                    u"their children, and so on."
878
        ),
879
        required=True,
880
        default=1,
881
        min=1,
882
        max=3,
883
    )
884
885
    sidebar_skip_types = schema.Tuple(
886
        title=_(
887
            u"title_senaitesetup_sidebar_skip_types",
888
            default=u"Sidebar skipped portal types"
889
        ),
890
        description=_(
891
            u"description_senaitesetup_sidebar_skip_types",
892
            default=u"Select which content types should be excluded from the "
893
                    u"sidebar navigation. If none are selected, all content "
894
                    u"types will be shown."
895
        ),
896
        value_type=schema.Choice(
897
            vocabulary="senaite.core.vocabularies.navigation_portal_types"
898
        ),
899
        required=False,
900
        default=("AnalysisRequest", "Attachment", ),
901
    )
902
903
    # Sampling
904
    printing_workflow_enabled = schema.Bool(
905
        title=_(u"Enable the Results Report Printing workflow"),
906
        description=_(
907
            u"Select this to allow the user to set an additional 'Printed' "
908
            u"status to those Analysis Requests that have been Published. "
909
            u"Disabled by default."
910
        ),
911
        default=False,
912
    )
913
914
    sampling_workflow_enabled = schema.Bool(
915
        title=_(u"Enable Sampling"),
916
        description=_(
917
            u"Select this to activate the sample collection workflow steps."
918
        ),
919
        default=False,
920
    )
921
922
    schedule_sampling_enabled = schema.Bool(
923
        title=_(u"Enable Sampling Scheduling"),
924
        description=_(
925
            u"Select this to allow a Sampling Coordinator to schedule a "
926
            u"sampling. This functionality only takes effect when 'Sampling "
927
            u"workflow' is active"
928
        ),
929
        default=False,
930
    )
931
932
    autoreceive_samples = schema.Bool(
933
        title=_(u"Auto-receive samples"),
934
        description=_(
935
            u"Select to receive the samples automatically when created by lab "
936
            u"personnel and sampling workflow is disabled. Samples created by "
937
            u"client contacts won't be received automatically"
938
        ),
939
        default=False,
940
    )
941
942
    sample_preservation_enabled = schema.Bool(
943
        title=_(u"Enable Sample Preservation"),
944
        description=u"",
945
        default=False,
946
    )
947
948
    workdays = schema.List(
949
        title=_(u"Laboratory Workdays"),
950
        description=_(
951
            u"Only laboratory workdays are considered for the analysis "
952
            u"turnaround time calculation."
953
        ),
954
        value_type=schema.Choice(
955
            vocabulary="senaite.core.vocabularies.weekdays"
956
        ),
957
        default=[u"0", u"1", u"2", u"3", u"4", u"5", u"6"],
958
        required=True,
959
    )
960
961
    directives.widget("default_turnaround_time", DurationWidgetFactory)
962
    default_turnaround_time = DurationField(
963
        title=_(u"Default turnaround time for analyses."),
964
        description=_(
965
            u"This is the default maximum time allowed for performing "
966
            u"analyses. It is only used for analyses where the analysis "
967
            u"service does not specify a turnaround time. Only laboratory "
968
            u"workdays are considered."
969
        ),
970
        required=True,
971
        default=timedelta(days=5),
972
    )
973
974
    directives.widget("default_sample_lifetime", DurationWidgetFactory)
975
    default_sample_lifetime = DurationField(
976
        title=_(u"Default sample retention period"),
977
        description=_(
978
            u"The number of days before a sample expires and cannot be "
979
            u"analysed any more. This setting can be overwritten per "
980
            u"individual sample type in the sample types setup"
981
        ),
982
        required=True,
983
        default=timedelta(days=30),
984
    )
985
986
    # Notifications
987
    notify_on_sample_rejection = schema.Bool(
988
        title=_(u"Email notification on Sample rejection"),
989
        description=_(
990
            u"Select this to activate automatic notifications via email to "
991
            u"the Client when a Sample is rejected."
992
        ),
993
        default=False,
994
    )
995
996
    directives.widget("email_body_sample_rejection", RichTextFieldWidget)
997
    email_body_sample_rejection = RichTextField(
998
        title=_(u"Email body for Sample Rejection notifications"),
999
        description=_(
1000
            u"Set the text for the body of the email to be sent to the "
1001
            u"Sample's client contact if the option 'Email notification on "
1002
            u"Sample rejection' is enabled. You can use reserved keywords: "
1003
            u"$sample_id, $sample_link, $reasons, $lab_address"
1004
        ),
1005
        default=u"The sample $sample_link has been rejected because of the "
1006
                u"following reasons:<br/><br/>$reasons<br/><br/>For further "
1007
                u"information, please contact us under the following address."
1008
                u"<br/><br/>$lab_address",
1009
        required=False,
1010
    )
1011
1012
    directives.widget("email_body_sample_invalidation", RichTextFieldWidget)
1013
    email_body_sample_invalidation = RichTextField(
1014
        title=_(u"Email body for Sample Invalidation notifications"),
1015
        description=_(
1016
            u"Define the template for the email body that will be "
1017
            u"automatically sent to primary contacts and laboratory managers "
1018
            u"when a sample is invalidated. The following placeholders are "
1019
            u"supported: $sample_id, $retest_id, $retest_link, $reason, "
1020
            u"$lab_address."
1021
        ),
1022
        default=u"Some non-conformities have been detected in the results "
1023
                u"report published for Sample $sample_link.<br/><br/>A new "
1024
                u"Sample $retest_link has been created automatically, and the "
1025
                u"previous request has been invalidated.<br/><br/>The root "
1026
                u"cause is under investigation and corrective action has been "
1027
                u"initiated.<br/><br/>$lab_address",
1028
        required=False,
1029
    )
1030
1031
    # Sticker
1032
    auto_print_stickers = schema.Choice(
1033
        title=_(u"Automatic Sticker Printing"),
1034
        description=_(
1035
            u"Choose when stickers should be automatically printed:<br/>"
1036
            u"<ul><li><strong>Register:</strong> Stickers are printed "
1037
            u"automatically when new samples are created.</li>"
1038
            u"<li><strong>Receive:</strong> Stickers are printed automatically "
1039
            u"when samples are received.</li>"
1040
            u"<li><strong>None:</strong> Disables automatic sticker printing."
1041
            u"</li></ul>"
1042
        ),
1043
        vocabulary=schema.vocabulary.SimpleVocabulary([
1044
            schema.vocabulary.SimpleTerm("None", "None", _(u"None")),
1045
            schema.vocabulary.SimpleTerm("register", "register",
1046
                                          _(u"Register")),
1047
            schema.vocabulary.SimpleTerm("receive", "receive", _(u"Receive")),
1048
        ]),
1049
        required=False,
1050
        default="None",
1051
    )
1052
1053
    auto_sticker_template = schema.TextLine(
1054
        title=_(u"Default Sticker Template"),
1055
        description=_(
1056
            u"Select the default sticker template used for automatic printing."
1057
        ),
1058
        required=False,
1059
    )
1060
1061
    small_sticker_template = schema.TextLine(
1062
        title=_(u"Small Sticker Template"),
1063
        description=_(
1064
            u"Choose the default template for 'small' stickers. Note: "
1065
            u"Sample-specific 'small' stickers are configured based on their "
1066
            u"sample type."
1067
        ),
1068
        default=u"Code_128_1x48mm.pt",
1069
        required=False,
1070
    )
1071
1072
    large_sticker_template = schema.TextLine(
1073
        title=_(u"Large Sticker Template"),
1074
        description=_(
1075
            u"Choose the default template for 'large' stickers. Note: "
1076
            u"Sample-specific 'large' stickers are configured based on their "
1077
            u"sample type."
1078
        ),
1079
        default=u"Code_128_1x72mm.pt",
1080
        required=False,
1081
    )
1082
1083
    default_number_of_copies = schema.Int(
1084
        title=_(u"Default Number of Copies"),
1085
        description=_(
1086
            u"Specify how many copies of each sticker should be printed by "
1087
            u"default."
1088
        ),
1089
        required=True,
1090
        default=1,
1091
    )
1092
1093
    # ID Server
1094
    directives.widget(
1095
        "id_formatting",
1096
        DataGridWidgetFactory,
1097
        allow_insert=True,
1098
        allow_delete=True,
1099
        allow_reorder=True,
1100
        auto_append=False)
1101
    id_formatting = DataGridField(
1102
        title=_(u"Formatting Configuration"),
1103
        description=_(
1104
            u"<p>The ID Server provides unique sequential IDs for objects "
1105
            u"such as Samples and Worksheets etc, based on a format "
1106
            u"specified for each content type.</p>"
1107
            u"<p>The format is constructed similarly to the Python format "
1108
            u"syntax, using predefined variables per content type, and "
1109
            u"advancing the IDs through a sequence number, 'seq' and its "
1110
            u"padding as a number of digits, e.g. '03d' for a sequence of "
1111
            u"IDs from 001 to 999.</p>"
1112
            u"<p>Alphanumeric prefixes for IDs are included as is in the "
1113
            u"formats, e.g. WS for Worksheet in WS-{seq:03d} produces "
1114
            u"sequential Worksheet IDs: WS-001, WS-002, WS-003 etc.</p>"
1115
            u"<p>For dynamic generation of alphanumeric and sequential IDs, "
1116
            u"the wildcard {alpha} can be used. E.g WS-{alpha:2a3d} produces "
1117
            u"WS-AA001, WS-AA002, WS-AB034, etc.</p>"
1118
            u"<p>Variables that can be used include:"
1119
            u"<table>"
1120
            u"<tr>"
1121
            u"<th style='width:150px'>Content Type</th><th>Variables</th>"
1122
            u"</tr>"
1123
            u"<tr><td>Client ID</td><td>{clientId}</td></tr>"
1124
            u"<tr><td>Year</td><td>{year}</td></tr>"
1125
            u"<tr><td>Sample ID</td><td>{sampleId}</td></tr>"
1126
            u"<tr><td>Sample Type</td><td>{sampleType}</td></tr>"
1127
            u"<tr><td>Sampling Date</td><td>{samplingDate}</td></tr>"
1128
            u"<tr><td>Date Sampled</td><td>{dateSampled}</td></tr>"
1129
            u"</table>"
1130
            u"</p>"
1131
            u"<p>Configuration Settings:"
1132
            u"<ul>"
1133
            u"<li>format:"
1134
            u"<ul><li>a python format string constructed from predefined "
1135
            u"variables like sampleId, clientId, sampleType.</li>"
1136
            u"<li>special variable 'seq' must be positioned last in the "
1137
            u"format string</li></ul></li>"
1138
            u"<li>sequence type: [generated|counter]</li>"
1139
            u"<li>context: if type counter, provides context the counting "
1140
            u"function</li>"
1141
            u"<li>counter type: [backreference|contained]</li>"
1142
            u"<li>counter reference: a parameter to the counting function</li>"
1143
            u"<li>prefix: default prefix if none provided in format string</li>"
1144
            u"<li>split length: the number of parts to be included in the "
1145
            u"prefix</li>"
1146
            u"</ul></p>"
1147
        ),
1148
        value_type=DataGridRow(schema=IIDFormattingRecordSchema),
1149
        required=False,
1150
        default=DEFAULT_ID_FORMATTING,
1151
    )
1152
1153
    id_server_values = schema.Text(
1154
        title=_(u"ID Server Values"),
1155
        description=_(u"Current ID server counter values"),
1156
        required=False,
1157
        readonly=True,
1158
    )
1159
1160
    ###
1161
    # Fieldsets
1162
    ###
1163
    model.fieldset(
1164
        "security",
1165
        label=_(u"Security"),
1166
        fields=[
1167
            "auto_log_off",
1168
            "restrict_worksheet_users_access",
1169
            "allow_to_submit_not_assigned",
1170
            "restrict_worksheet_management",
1171
            "enable_global_auditlog",
1172
        ]
1173
    )
1174
1175
    model.fieldset(
1176
        "accounting",
1177
        label=_(u"Accounting"),
1178
        fields=[
1179
            "show_prices",
1180
            "currency",
1181
            "default_country",
1182
            "member_discount",
1183
            "vat",
1184
        ]
1185
    )
1186
1187
    model.fieldset(
1188
        "results_reports",
1189
        label=_(u"Results Reports"),
1190
        fields=[
1191
            "decimal_mark",
1192
            "scientific_notation_report",
1193
            "minimum_results",
1194
        ]
1195
    )
1196
1197
    model.fieldset(
1198
        "analyses",
1199
        label=_(u"Analyses"),
1200
        fields=[
1201
            "categorise_analysis_services",
1202
            "categorize_sample_analyses",
1203
            "sample_analyses_required",
1204
            "allow_manual_result_capture_date",
1205
            "enable_ar_specs",
1206
            "exponential_format_threshold",
1207
            "immediate_results_entry",
1208
            "enable_analysis_remarks",
1209
            "auto_verify_samples",
1210
            "self_verification_enabled",
1211
            "number_of_required_verifications",
1212
            "type_of_multi_verification",
1213
            "results_decimal_mark",
1214
            "scientific_notation_results",
1215
            "enable_rejection_workflow",
1216
            "rejection_reasons",
1217
            "default_number_of_ars_to_add",
1218
            "max_number_of_samples_add",
1219
        ]
1220
    )
1221
1222
    model.fieldset(
1223
        "appearance",
1224
        label=_(u"Appearance"),
1225
        fields=[
1226
            "worksheet_layout",
1227
            "dashboard_by_default",
1228
            "landing_page",
1229
            "show_partitions",
1230
            "site_logo",
1231
            "site_logo_css",
1232
            "show_lab_name_in_login",
1233
            "sidebar_folders",
1234
            "sidebar_navigation_depth",
1235
            "sidebar_skip_types",
1236
        ]
1237
    )
1238
1239
    model.fieldset(
1240
        "sampling",
1241
        label=_(u"Sampling"),
1242
        fields=[
1243
            "printing_workflow_enabled",
1244
            "sampling_workflow_enabled",
1245
            "schedule_sampling_enabled",
1246
            "date_sampled_required",
1247
            "autoreceive_samples",
1248
            "sample_preservation_enabled",
1249
            "workdays",
1250
            "default_turnaround_time",
1251
            "default_sample_lifetime",
1252
        ]
1253
    )
1254
1255
    model.fieldset(
1256
        "notifications",
1257
        label=_(u"Notifications"),
1258
        fields=[
1259
            "email_from_sample_publication",
1260
            "email_body_sample_publication",
1261
            "always_cc_responsibles_in_report_emails",
1262
            "notify_on_sample_rejection",
1263
            "email_body_sample_rejection",
1264
            "invalidation_reason_required",
1265
            "email_body_sample_invalidation",
1266
        ]
1267
    )
1268
1269
    model.fieldset(
1270
        "sticker",
1271
        label=_(u"Sticker"),
1272
        fields=[
1273
            "auto_print_stickers",
1274
            "auto_sticker_template",
1275
            "small_sticker_template",
1276
            "large_sticker_template",
1277
            "default_number_of_copies",
1278
        ]
1279
    )
1280
1281
    model.fieldset(
1282
        "id_server",
1283
        label=_(u"ID Server"),
1284
        fields=[
1285
            "id_formatting",
1286
            "id_server_values",
1287
        ]
1288
    )
1289
1290
1291
@implementer(ISetup, ISetupSchema, IHideActionsMenu)
1292
class Setup(Container):
1293
    """SENAITE Setup Folder
1294
    """
1295
    security = ClassSecurityInfo()
1296
1297
    @security.protected(permissions.View)
1298
    def getEmailFromSamplePublication(self):
1299
        """Returns the 'From' address for publication emails
1300
        """
1301
        accessor = self.accessor("email_from_sample_publication")
1302
        email = accessor(self)
1303
        if not email:
1304
            email = default_email_from_sample_publication(self)
1305
        return email
1306
1307
    @security.protected(permissions.ModifyPortalContent)
1308
    def setEmailFromSamplePublication(self, value):
1309
        """Set the 'From' address for publication emails
1310
        """
1311
        mutator = self.mutator("email_from_sample_publication")
1312
        return mutator(self, value)
1313
1314
    @security.protected(permissions.View)
1315
    def getEmailBodySamplePublication(self):
1316
        """Returns the transformed email body text for publication emails
1317
        """
1318
        accessor = self.accessor("email_body_sample_publication")
1319
        value = accessor(self)
1320
        if IRichTextValue.providedBy(value):
1321
            # Transforms the raw value to the output mimetype
1322
            value = value.output_relative_to(self)
1323
        if not value:
1324
            # Always fallback to default value
1325
            value = default_email_body_sample_publication(self)
1326
        return value
1327
1328
    @security.protected(permissions.ModifyPortalContent)
1329
    def setEmailBodySamplePublication(self, value):
1330
        """Set email body text for publication emails
1331
        """
1332
        mutator = self.mutator("email_body_sample_publication")
1333
        return mutator(self, value)
1334
1335
    @security.protected(permissions.View)
1336
    def getAlwaysCCResponsiblesInReportEmail(self):
1337
        """Returns if responsibles should always receive publication emails
1338
        """
1339
        accessor = self.accessor("always_cc_responsibles_in_report_emails")
1340
        return accessor(self)
1341
1342
    @security.protected(permissions.View)
1343
    def setAlwaysCCResponsiblesInReportEmail(self, value):
1344
        """Set if responsibles should always receive publication emails
1345
        """
1346
        mutator = self.mutator("always_cc_responsibles_in_report_emails")
1347
        return mutator(self, value)
1348
1349
    @security.protected(permissions.View)
1350
    def getEnableGlobalAuditlog(self):
1351
        """Returns if the global Auditlog is enabled
1352
        """
1353
        accessor = self.accessor("enable_global_auditlog")
1354
        return accessor(self)
1355
1356
    @security.protected(permissions.ModifyPortalContent)
1357
    def setEnableGlobalAuditlog(self, value):
1358
        """Enable/Disable global Auditlogging
1359
        """
1360
        if value is False:
1361
            # clear the auditlog catalog
1362
            catalog = api.get_tool(AUDITLOG_CATALOG)
1363
            catalog.manage_catalogClear()
1364
        mutator = self.mutator("enable_global_auditlog")
1365
        return mutator(self, value)
1366
1367
    @security.protected(permissions.View)
1368
    def getSiteLogo(self):
1369
        """Returns the global site logo
1370
        """
1371
        accessor = self.accessor("site_logo")
1372
        return accessor(self)
1373
1374
    @security.protected(permissions.ModifyPortalContent)
1375
    def setSiteLogo(self, value):
1376
        """Set the site logo
1377
        """
1378
        mutator = self.mutator("site_logo")
1379
        return mutator(self, value)
1380
1381
    @security.protected(permissions.View)
1382
    def getSiteLogoCSS(self):
1383
        """Returns the global site logo
1384
        """
1385
        accessor = self.accessor("site_logo_css")
1386
        return accessor(self)
1387
1388
    @security.protected(permissions.ModifyPortalContent)
1389
    def setSiteLogoCSS(self, value):
1390
        """Set the site logo
1391
        """
1392
        mutator = self.mutator("site_logo_css")
1393
        return mutator(self, value)
1394
1395
    @security.protected(permissions.View)
1396
    def getImmediateResultsEntry(self):
1397
        """Returns if immediate results entry is enabled or not
1398
        """
1399
        accessor = self.accessor("immediate_results_entry")
1400
        return accessor(self)
1401
1402
    @security.protected(permissions.ModifyPortalContent)
1403
    def setImmediateResultsEntry(self, value):
1404
        """Enable/Disable global Auditlogging
1405
        """
1406
        mutator = self.mutator("immediate_results_entry")
1407
        return mutator(self, value)
1408
1409
    @security.protected(permissions.View)
1410
    def getCategorizeSampleAnalyses(self):
1411
        """Returns if analyses should be grouped by category for samples
1412
        """
1413
        accessor = self.accessor("categorize_sample_analyses")
1414
        return accessor(self)
1415
1416
    @security.protected(permissions.ModifyPortalContent)
1417
    def setCategorizeSampleAnalyses(self, value):
1418
        """Enable/Disable grouping of analyses by category for samples
1419
        """
1420
        mutator = self.mutator("categorize_sample_analyses")
1421
        return mutator(self, value)
1422
1423
    @security.protected(permissions.View)
1424
    def getSampleAnalysesRequired(self):
1425
        """Returns if analyses are required in sample add form
1426
        """
1427
        accessor = self.accessor("sample_analyses_required")
1428
        return accessor(self)
1429
1430
    @security.protected(permissions.ModifyPortalContent)
1431
    def setSampleAnalysesRequired(self, value):
1432
        """Allow/Disallow to create samples without analyses
1433
        """
1434
        mutator = self.mutator("sample_analyses_required")
1435
        return mutator(self, value)
1436
1437
    @security.protected(permissions.View)
1438
    def getAllowManualResultCaptureDate(self):
1439
        """Returns if analyses are required in sample add form
1440
        """
1441
        accessor = self.accessor("allow_manual_result_capture_date")
1442
        return accessor(self)
1443
1444
    @security.protected(permissions.ModifyPortalContent)
1445
    def setAllowManualResultCaptureDate(self, value):
1446
        """Allow/Disallow to create samples without analyses
1447
        """
1448
        mutator = self.mutator("allow_manual_result_capture_date")
1449
        return mutator(self, value)
1450
1451
    @security.protected(permissions.View)
1452
    def getDateSampledRequired(self):
1453
        """Returns whether the DateSampled field is required on sample creation
1454
        when the sampling workflow is not active
1455
        """
1456
        accessor = self.accessor("date_sampled_required")
1457
        return accessor(self)
1458
1459
    @security.protected(permissions.ModifyPortalContent)
1460
    def setDateSampledRequired(self, value):
1461
        """Sets whether the entry of a value for DateSampled field on sample
1462
        creation is required when the sampling workflow is not active
1463
        """
1464
        mutator = self.mutator("date_sampled_required")
1465
        return mutator(self, value)
1466
1467
    @security.protected(permissions.View)
1468
    def getShowLabNameInLogin(self):
1469
        """Returns if the laboratory name has to be displayed in login page
1470
        """
1471
        accessor = self.accessor("show_lab_name_in_login")
1472
        return accessor(self)
1473
1474
    @security.protected(permissions.ModifyPortalContent)
1475
    def setShowLabNameInLogin(self, value):
1476
        """Show/hide the laboratory name in the login page
1477
        """
1478
        mutator = self.mutator("show_lab_name_in_login")
1479
        return mutator(self, value)
1480
1481
    @security.protected(permissions.View)
1482
    def getSidebarFolders(self):
1483
        """Returns the sidebar navigation folders
1484
        """
1485
        accessor = self.accessor("sidebar_folders")
1486
        return accessor(self) or ()
1487
1488
    @security.protected(permissions.ModifyPortalContent)
1489
    def setSidebarFolders(self, value):
1490
        """Set the sidebar navigation folders
1491
        """
1492
        mutator = self.mutator("sidebar_folders")
1493
        return mutator(self, value)
1494
1495
    @security.protected(permissions.View)
1496
    def getSidebarNavigationDepth(self):
1497
        """Returns the sidebar navigation depth
1498
        """
1499
        accessor = self.accessor("sidebar_navigation_depth")
1500
        depth = accessor(self)
1501
        return depth if depth is not None else 3
1502
1503
    @security.protected(permissions.ModifyPortalContent)
1504
    def setSidebarNavigationDepth(self, value):
1505
        """Set the sidebar navigation depth
1506
        """
1507
        mutator = self.mutator("sidebar_navigation_depth")
1508
        return mutator(self, value)
1509
1510
    @security.protected(permissions.View)
1511
    def getSidebarSkipTypes(self):
1512
        """Returns the sidebar skipped portal types
1513
        """
1514
        accessor = self.accessor("sidebar_skip_types")
1515
        return accessor(self) or ()
1516
1517
    @security.protected(permissions.ModifyPortalContent)
1518
    def setSidebarSkipTypes(self, value):
1519
        """Set the sidebar skipped portal types
1520
        """
1521
        mutator = self.mutator("sidebar_skip_types")
1522
        return mutator(self, value)
1523
1524
    @security.protected(permissions.View)
1525
    def getInvalidationReasonRequired(self):
1526
        """Returns whether the introduction of a reason is required when
1527
        invalidating a sample
1528
        """
1529
        accessor = self.accessor("invalidation_reason_required")
1530
        return accessor(self)
1531
1532
    @security.protected(permissions.ModifyPortalContent)
1533
    def setInvalidationReasonRequired(self, value):
1534
        """Set whether the introduction of a reason is required when
1535
        invalidating a sample
1536
        """
1537
        mutator = self.mutator("invalidation_reason_required")
1538
        return mutator(self, value)
1539
1540
    # Auto Log Off - special handling with session timeout
1541
    @security.protected(permissions.View)
1542
    def getAutoLogOff(self):
1543
        """Get session lifetime in minutes
1544
        """
1545
        acl = api.get_tool("acl_users")
1546
        session = acl.get("session")
1547
        if not session:
1548
            return 0
1549
        return session.timeout // 60
1550
1551
    @security.protected(permissions.ModifyPortalContent)
1552
    def setAutoLogOff(self, value):
1553
        """Set session lifetime in minutes
1554
        """
1555
        value = api.to_int(value, default=0)
1556
        if value < 0:
1557
            value = 0
1558
        value = value * 60
1559
        acl = api.get_tool("acl_users")
1560
        session = acl.get("session")
1561
        if session:
1562
            session.timeout = value
1563
        mutator = self.mutator("auto_log_off")
1564
        return mutator(self, value // 60)
1565
1566
    # Security fields
1567
    @security.protected(permissions.View)
1568
    def getRestrictWorksheetUsersAccess(self):
1569
        """Get restrict worksheet users access setting
1570
        """
1571
        accessor = self.accessor("restrict_worksheet_users_access")
1572
        return accessor(self)
1573
1574
    @security.protected(permissions.ModifyPortalContent)
1575
    def setRestrictWorksheetUsersAccess(self, value):
1576
        """Set restrict worksheet users access setting
1577
        """
1578
        mutator = self.mutator("restrict_worksheet_users_access")
1579
        return mutator(self, value)
1580
1581
    @security.protected(permissions.View)
1582
    def getAllowToSubmitNotAssigned(self):
1583
        """Get allow to submit not assigned setting
1584
        """
1585
        accessor = self.accessor("allow_to_submit_not_assigned")
1586
        return accessor(self)
1587
1588
    @security.protected(permissions.ModifyPortalContent)
1589
    def setAllowToSubmitNotAssigned(self, value):
1590
        """Set allow to submit not assigned setting
1591
        """
1592
        mutator = self.mutator("allow_to_submit_not_assigned")
1593
        return mutator(self, value)
1594
1595
    @security.protected(permissions.View)
1596
    def getRestrictWorksheetManagement(self):
1597
        """Get restrict worksheet management setting
1598
        """
1599
        accessor = self.accessor("restrict_worksheet_management")
1600
        return accessor(self)
1601
1602
    @security.protected(permissions.ModifyPortalContent)
1603
    def setRestrictWorksheetManagement(self, value):
1604
        """Set restrict worksheet management setting
1605
        """
1606
        mutator = self.mutator("restrict_worksheet_management")
1607
        return mutator(self, value)
1608
1609
    # Accounting fields
1610
    @security.protected(permissions.View)
1611
    def getShowPrices(self):
1612
        """Get show prices setting
1613
        """
1614
        accessor = self.accessor("show_prices")
1615
        return accessor(self)
1616
1617
    @security.protected(permissions.ModifyPortalContent)
1618
    def setShowPrices(self, value):
1619
        """Set show prices setting
1620
        """
1621
        mutator = self.mutator("show_prices")
1622
        return mutator(self, value)
1623
1624
    @security.protected(permissions.View)
1625
    def getCurrency(self):
1626
        """Get currency setting
1627
        """
1628
        accessor = self.accessor("currency")
1629
        return accessor(self)
1630
1631
    @security.protected(permissions.ModifyPortalContent)
1632
    def setCurrency(self, value):
1633
        """Set currency setting
1634
        """
1635
        mutator = self.mutator("currency")
1636
        return mutator(self, value)
1637
1638
    @security.protected(permissions.View)
1639
    def getDefaultCountry(self):
1640
        """Get default country setting
1641
        """
1642
        accessor = self.accessor("default_country")
1643
        return accessor(self)
1644
1645
    @security.protected(permissions.ModifyPortalContent)
1646
    def setDefaultCountry(self, value):
1647
        """Set default country setting
1648
        """
1649
        mutator = self.mutator("default_country")
1650
        return mutator(self, value)
1651
1652
    @security.protected(permissions.View)
1653
    def getMemberDiscount(self):
1654
        """Get member discount percentage
1655
        Returns string value, defaults to "0.0" if empty
1656
        """
1657
        accessor = self.accessor("member_discount")
1658
        value = accessor(self)
1659
        # Convert to string if numeric (from AT FixedPointField)
1660
        if value is None or value == "":
1661
            return u"0.0"
1662
        if isinstance(value, (int, float)):
1663
            return api.safe_unicode(str(value))
1664
        return api.safe_unicode(value)
1665
1666
    @security.protected(permissions.ModifyPortalContent)
1667
    def setMemberDiscount(self, value):
1668
        """Set member discount percentage
1669
        Accepts string or numeric value
1670
        """
1671
        # Convert numeric to string
1672
        if value is not None and not isinstance(value, basestring):
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable basestring does not seem to be defined.
Loading history...
1673
            value = api.safe_unicode(str(value))
1674
        elif value:
1675
            value = api.safe_unicode(value)
1676
        else:
1677
            value = u""
1678
        mutator = self.mutator("member_discount")
1679
        return mutator(self, value)
1680
1681
    @security.protected(permissions.View)
1682
    def getVAT(self):
1683
        """Get VAT percentage
1684
        Returns string value, defaults to "0.0" if empty
1685
        """
1686
        accessor = self.accessor("vat")
1687
        value = accessor(self)
1688
        # Convert to string if numeric (from AT FixedPointField)
1689
        if value is None or value == "":
1690
            return u"0.0"
1691
        if isinstance(value, (int, float)):
1692
            return api.safe_unicode(str(value))
1693
        return api.safe_unicode(value)
1694
1695
    @security.protected(permissions.ModifyPortalContent)
1696
    def setVAT(self, value):
1697
        """Set VAT percentage
1698
        Accepts string or numeric value
1699
        """
1700
        # Convert numeric to string
1701
        if value is not None and not isinstance(value, basestring):
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable basestring does not seem to be defined.
Loading history...
1702
            value = api.safe_unicode(str(value))
1703
        elif value:
1704
            value = api.safe_unicode(value)
1705
        else:
1706
            value = u""
1707
        mutator = self.mutator("vat")
1708
        return mutator(self, value)
1709
1710
    @security.protected(permissions.View)
1711
    def getDecimalMark(self):
1712
        """Get decimal mark for reports
1713
        """
1714
        accessor = self.accessor("decimal_mark")
1715
        value = accessor(self)
1716
        # Return default if value is empty
1717
        return value or "."
1718
1719
    @security.protected(permissions.ModifyPortalContent)
1720
    def setDecimalMark(self, value):
1721
        """Set decimal mark for reports
1722
        """
1723
        mutator = self.mutator("decimal_mark")
1724
        return mutator(self, value)
1725
1726
    @security.protected(permissions.View)
1727
    def getScientificNotationReport(self):
1728
        """Get scientific notation format for reports
1729
        """
1730
        accessor = self.accessor("scientific_notation_report")
1731
        value = accessor(self)
1732
        # Return default if value is empty
1733
        return value or "1"
1734
1735
    @security.protected(permissions.ModifyPortalContent)
1736
    def setScientificNotationReport(self, value):
1737
        """Set scientific notation format for reports
1738
        """
1739
        mutator = self.mutator("scientific_notation_report")
1740
        return mutator(self, value)
1741
1742
    @security.protected(permissions.View)
1743
    def getMinimumResults(self):
1744
        """Get minimum number of results for QC stats
1745
        """
1746
        accessor = self.accessor("minimum_results")
1747
        return accessor(self)
1748
1749
    @security.protected(permissions.ModifyPortalContent)
1750
    def setMinimumResults(self, value):
1751
        """Set minimum number of results for QC stats
1752
        Converts to int if needed
1753
        """
1754
        value = api.to_int(value, default=None)
1755
        mutator = self.mutator("minimum_results")
1756
        return mutator(self, value)
1757
1758
    @security.protected(permissions.View)
1759
    def getCategoriseAnalysisServices(self):
1760
        """Get categorise analysis services setting
1761
        """
1762
        accessor = self.accessor("categorise_analysis_services")
1763
        return accessor(self)
1764
1765
    @security.protected(permissions.ModifyPortalContent)
1766
    def setCategoriseAnalysisServices(self, value):
1767
        """Set categorise analysis services setting
1768
        """
1769
        mutator = self.mutator("categorise_analysis_services")
1770
        return mutator(self, value)
1771
1772
    @security.protected(permissions.View)
1773
    def getEnableARSpecs(self):
1774
        """Get enable AR specs setting
1775
        """
1776
        accessor = self.accessor("enable_ar_specs")
1777
        return accessor(self)
1778
1779
    @security.protected(permissions.ModifyPortalContent)
1780
    def setEnableARSpecs(self, value):
1781
        """Set enable AR specs setting
1782
        """
1783
        mutator = self.mutator("enable_ar_specs")
1784
        return mutator(self, value)
1785
1786
    @security.protected(permissions.View)
1787
    def getExponentialFormatThreshold(self):
1788
        """Get exponential format threshold
1789
        """
1790
        accessor = self.accessor("exponential_format_threshold")
1791
        return accessor(self)
1792
1793
    @security.protected(permissions.ModifyPortalContent)
1794
    def setExponentialFormatThreshold(self, value):
1795
        """Set exponential format threshold
1796
        Converts to int if needed
1797
        """
1798
        value = api.to_int(value, default=None)
1799
        mutator = self.mutator("exponential_format_threshold")
1800
        return mutator(self, value)
1801
1802
    @security.protected(permissions.View)
1803
    def getEnableAnalysisRemarks(self):
1804
        """Get enable analysis remarks setting
1805
        """
1806
        accessor = self.accessor("enable_analysis_remarks")
1807
        return accessor(self)
1808
1809
    @security.protected(permissions.ModifyPortalContent)
1810
    def setEnableAnalysisRemarks(self, value):
1811
        """Set enable analysis remarks setting
1812
        """
1813
        mutator = self.mutator("enable_analysis_remarks")
1814
        return mutator(self, value)
1815
1816
    @security.protected(permissions.View)
1817
    def getAutoVerifySamples(self):
1818
        """Get auto verify samples setting
1819
        """
1820
        accessor = self.accessor("auto_verify_samples")
1821
        return accessor(self)
1822
1823
    @security.protected(permissions.ModifyPortalContent)
1824
    def setAutoVerifySamples(self, value):
1825
        """Set auto verify samples setting
1826
        """
1827
        mutator = self.mutator("auto_verify_samples")
1828
        return mutator(self, value)
1829
1830
    @security.protected(permissions.View)
1831
    def getSelfVerificationEnabled(self):
1832
        """Get self verification enabled setting
1833
        """
1834
        accessor = self.accessor("self_verification_enabled")
1835
        return accessor(self)
1836
1837
    @security.protected(permissions.ModifyPortalContent)
1838
    def setSelfVerificationEnabled(self, value):
1839
        """Set self verification enabled setting
1840
        """
1841
        mutator = self.mutator("self_verification_enabled")
1842
        return mutator(self, value)
1843
1844
    @security.protected(permissions.View)
1845
    def getNumberOfRequiredVerifications(self):
1846
        """Get number of required verifications
1847
        Returns 1 (default) if not set
1848
        """
1849
        accessor = self.accessor("number_of_required_verifications")
1850
        value = accessor(self)
1851
        # Return default of 1 if not set
1852
        if value is None:
1853
            return 1
1854
        return value
1855
1856
    @security.protected(permissions.ModifyPortalContent)
1857
    def setNumberOfRequiredVerifications(self, value):
1858
        """Set number of required verifications
1859
        """
1860
        mutator = self.mutator("number_of_required_verifications")
1861
        return mutator(self, value)
1862
1863
    @security.protected(permissions.View)
1864
    def getTypeOfmultiVerification(self):
1865
        """Get type of multi verification
1866
        """
1867
        accessor = self.accessor("type_of_multi_verification")
1868
        return accessor(self)
1869
1870
    @security.protected(permissions.ModifyPortalContent)
1871
    def setTypeOfmultiVerification(self, value):
1872
        """Set type of multi verification
1873
        """
1874
        mutator = self.mutator("type_of_multi_verification")
1875
        return mutator(self, value)
1876
1877
    @security.protected(permissions.View)
1878
    def getResultsDecimalMark(self):
1879
        """Get decimal mark for results
1880
        """
1881
        accessor = self.accessor("results_decimal_mark")
1882
        value = accessor(self)
1883
        # Return default if value is empty
1884
        return value or "."
1885
1886
    @security.protected(permissions.ModifyPortalContent)
1887
    def setResultsDecimalMark(self, value):
1888
        """Set decimal mark for results
1889
        """
1890
        mutator = self.mutator("results_decimal_mark")
1891
        return mutator(self, value)
1892
1893
    @security.protected(permissions.View)
1894
    def getScientificNotationResults(self):
1895
        """Get scientific notation format for results
1896
        """
1897
        accessor = self.accessor("scientific_notation_results")
1898
        value = accessor(self)
1899
        # Return default if value is empty
1900
        return value or "1"
1901
1902
    @security.protected(permissions.ModifyPortalContent)
1903
    def setScientificNotationResults(self, value):
1904
        """Set scientific notation format for results
1905
        """
1906
        mutator = self.mutator("scientific_notation_results")
1907
        return mutator(self, value)
1908
1909
    @security.protected(permissions.View)
1910
    def getDefaultNumberOfARsToAdd(self):
1911
        """Get default number of ARs to add
1912
        """
1913
        accessor = self.accessor("default_number_of_ars_to_add")
1914
        return accessor(self)
1915
1916
    @security.protected(permissions.ModifyPortalContent)
1917
    def setDefaultNumberOfARsToAdd(self, value):
1918
        """Set default number of ARs to add
1919
        Converts to int if needed
1920
        """
1921
        value = api.to_int(value, default=None)
1922
        mutator = self.mutator("default_number_of_ars_to_add")
1923
        return mutator(self, value)
1924
1925
    @security.protected(permissions.View)
1926
    def getEnableRejectionWorkflow(self):
1927
        """Get enable rejection workflow
1928
        """
1929
        accessor = self.accessor("enable_rejection_workflow")
1930
        return accessor(self)
1931
1932
    @security.protected(permissions.ModifyPortalContent)
1933
    def setEnableRejectionWorkflow(self, value):
1934
        """Set enable rejection workflow
1935
        """
1936
        mutator = self.mutator("enable_rejection_workflow")
1937
        return mutator(self, value)
1938
1939
    @security.protected(permissions.View)
1940
    def getRejectionReasons(self):
1941
        """Get rejection reasons
1942
        Returns a list of unicode strings. The v02_07_000 upgrade step
1943
        converts old AT RecordsField data to this format.
1944
        """
1945
        accessor = self.accessor("rejection_reasons")
1946
        reasons = accessor(self)
1947
        if not reasons:
1948
            return []
1949
        # Ensure all reasons are unicode
1950
        return [api.safe_unicode(r) for r in reasons]
1951
1952
    @security.protected(permissions.ModifyPortalContent)
1953
    def setRejectionReasons(self, value):
1954
        """Set rejection reasons
1955
        Accepts a simple list of strings (DX format).
1956
        The v02_07_000 upgrade step handles AT→DX conversion before calling
1957
        this setter, so no format conversion is needed here.
1958
        """
1959
        # Ensure all values are unicode
1960
        if value and isinstance(value, (list, tuple)):
1961
            value = [api.safe_unicode(v) for v in value if v]
1962
        else:
1963
            value = []
1964
        mutator = self.mutator("rejection_reasons")
1965
        return mutator(self, value)
1966
1967
    @deprecate("Method is kept for backwards compatibility only")
1968
    def getRejectionReasonsItems(self):
1969
        """Return the list of predefined rejection reasons
1970
1971
        .. deprecated::
1972
            Use getRejectionReasons() instead. This method is kept for
1973
            backwards compatibility only and will be removed in a future
1974
            version.
1975
        """
1976
        return self.getRejectionReasons()
1977
1978
    @security.protected(permissions.View)
1979
    def getMaxNumberOfSamplesAdd(self):
1980
        """Get maximum number of samples to add
1981
        """
1982
        accessor = self.accessor("max_number_of_samples_add")
1983
        return accessor(self)
1984
1985
    @security.protected(permissions.ModifyPortalContent)
1986
    def setMaxNumberOfSamplesAdd(self, value):
1987
        """Set maximum number of samples to add
1988
        Converts to int if needed
1989
        """
1990
        value = api.to_int(value, default=None)
1991
        mutator = self.mutator("max_number_of_samples_add")
1992
        return mutator(self, value)
1993
1994
    @security.protected(permissions.View)
1995
    def getWorksheetLayout(self):
1996
        """Get worksheet layout
1997
        """
1998
        accessor = self.accessor("worksheet_layout")
1999
        return accessor(self)
2000
2001
    @security.protected(permissions.ModifyPortalContent)
2002
    def setWorksheetLayout(self, value):
2003
        """Set worksheet layout
2004
        """
2005
        mutator = self.mutator("worksheet_layout")
2006
        return mutator(self, value)
2007
2008
    @security.protected(permissions.View)
2009
    def getDashboardByDefault(self):
2010
        """Get dashboard by default setting
2011
        """
2012
        accessor = self.accessor("dashboard_by_default")
2013
        return accessor(self)
2014
2015
    @security.protected(permissions.ModifyPortalContent)
2016
    def setDashboardByDefault(self, value):
2017
        """Set dashboard by default setting
2018
        """
2019
        mutator = self.mutator("dashboard_by_default")
2020
        return mutator(self, value)
2021
2022
    @security.protected(permissions.View)
2023
    def getLandingPage(self):
2024
        """Get landing page
2025
        """
2026
        accessor = self.accessor("landing_page")
2027
        return accessor(self)
2028
2029
    @security.protected(permissions.ModifyPortalContent)
2030
    def setLandingPage(self, value):
2031
        """Set landing page
2032
        """
2033
        mutator = self.mutator("landing_page")
2034
        return mutator(self, value)
2035
2036
    @security.protected(permissions.View)
2037
    def getShowPartitions(self):
2038
        """Get show partitions setting
2039
        """
2040
        accessor = self.accessor("show_partitions")
2041
        return accessor(self)
2042
2043
    @security.protected(permissions.ModifyPortalContent)
2044
    def setShowPartitions(self, value):
2045
        """Set show partitions setting
2046
        """
2047
        mutator = self.mutator("show_partitions")
2048
        return mutator(self, value)
2049
2050
    @security.protected(permissions.View)
2051
    def getPrintingWorkflowEnabled(self):
2052
        """Get printing workflow enabled setting
2053
        """
2054
        accessor = self.accessor("printing_workflow_enabled")
2055
        return accessor(self)
2056
2057
    @security.protected(permissions.ModifyPortalContent)
2058
    def setPrintingWorkflowEnabled(self, value):
2059
        """Set printing workflow enabled setting
2060
        """
2061
        mutator = self.mutator("printing_workflow_enabled")
2062
        return mutator(self, value)
2063
2064
    @security.protected(permissions.View)
2065
    def getSamplingWorkflowEnabled(self):
2066
        """Get sampling workflow enabled setting
2067
        """
2068
        accessor = self.accessor("sampling_workflow_enabled")
2069
        return accessor(self)
2070
2071
    @security.protected(permissions.ModifyPortalContent)
2072
    def setSamplingWorkflowEnabled(self, value):
2073
        """Set sampling workflow enabled setting
2074
        """
2075
        mutator = self.mutator("sampling_workflow_enabled")
2076
        return mutator(self, value)
2077
2078
    @security.protected(permissions.View)
2079
    def getScheduleSamplingEnabled(self):
2080
        """Get schedule sampling enabled setting
2081
        """
2082
        accessor = self.accessor("schedule_sampling_enabled")
2083
        return accessor(self)
2084
2085
    @security.protected(permissions.ModifyPortalContent)
2086
    def setScheduleSamplingEnabled(self, value):
2087
        """Set schedule sampling enabled setting
2088
        """
2089
        mutator = self.mutator("schedule_sampling_enabled")
2090
        return mutator(self, value)
2091
2092
    @security.protected(permissions.View)
2093
    def getAutoreceiveSamples(self):
2094
        """Get autoreceive samples setting
2095
        """
2096
        accessor = self.accessor("autoreceive_samples")
2097
        return accessor(self)
2098
2099
    @security.protected(permissions.ModifyPortalContent)
2100
    def setAutoreceiveSamples(self, value):
2101
        """Set autoreceive samples setting
2102
        """
2103
        mutator = self.mutator("autoreceive_samples")
2104
        return mutator(self, value)
2105
2106
    @security.protected(permissions.View)
2107
    def getSamplePreservationEnabled(self):
2108
        """Get sample preservation enabled setting
2109
        """
2110
        accessor = self.accessor("sample_preservation_enabled")
2111
        return accessor(self)
2112
2113
    @security.protected(permissions.ModifyPortalContent)
2114
    def setSamplePreservationEnabled(self, value):
2115
        """Set sample preservation enabled setting
2116
        """
2117
        mutator = self.mutator("sample_preservation_enabled")
2118
        return mutator(self, value)
2119
2120
    @security.protected(permissions.View)
2121
    def getWorkdays(self):
2122
        """Get laboratory workdays
2123
        """
2124
        accessor = self.accessor("workdays")
2125
        return accessor(self)
2126
2127
    @security.protected(permissions.ModifyPortalContent)
2128
    def setWorkdays(self, value):
2129
        """Set laboratory workdays
2130
        """
2131
        mutator = self.mutator("workdays")
2132
        return mutator(self, value)
2133
2134
    @security.protected(permissions.View)
2135
    def getDefaultTurnaroundTime(self):
2136
        """Get default turnaround time
2137
        """
2138
        accessor = self.accessor("default_turnaround_time")
2139
        return accessor(self)
2140
2141
    @security.protected(permissions.ModifyPortalContent)
2142
    def setDefaultTurnaroundTime(self, value):
2143
        """Set default turnaround time
2144
        """
2145
        # Handle both dict (from AT) and timedelta (from DX) formats
2146
        if isinstance(value, dict):
2147
            value = dtime.to_timedelta(value)
2148
        mutator = self.mutator("default_turnaround_time")
2149
        return mutator(self, value)
2150
2151
    @security.protected(permissions.View)
2152
    def getDefaultSampleLifetime(self):
2153
        """Get default sample lifetime
2154
        """
2155
        accessor = self.accessor("default_sample_lifetime")
2156
        return accessor(self)
2157
2158
    @security.protected(permissions.ModifyPortalContent)
2159
    def setDefaultSampleLifetime(self, value):
2160
        """Set default sample lifetime
2161
        """
2162
        # Handle both dict (from AT) and timedelta (from DX) formats
2163
        if isinstance(value, dict):
2164
            value = dtime.to_timedelta(value)
2165
        mutator = self.mutator("default_sample_lifetime")
2166
        return mutator(self, value)
2167
2168
    @security.protected(permissions.View)
2169
    def getNotifyOnSampleRejection(self):
2170
        """Get notify on sample rejection setting
2171
        """
2172
        accessor = self.accessor("notify_on_sample_rejection")
2173
        return accessor(self)
2174
2175
    @security.protected(permissions.ModifyPortalContent)
2176
    def setNotifyOnSampleRejection(self, value):
2177
        """Set notify on sample rejection setting
2178
        """
2179
        mutator = self.mutator("notify_on_sample_rejection")
2180
        return mutator(self, value)
2181
2182
    @security.protected(permissions.View)
2183
    def getEmailBodySampleRejection(self):
2184
        """Returns the transformed email body text for rejection emails
2185
        """
2186
        accessor = self.accessor("email_body_sample_rejection")
2187
        value = accessor(self)
2188
        if IRichTextValue.providedBy(value):
2189
            # Transforms the raw value to the output mimetype
2190
            value = value.output_relative_to(self)
2191
        return value
2192
2193
    @security.protected(permissions.ModifyPortalContent)
2194
    def setEmailBodySampleRejection(self, value):
2195
        """Set email body for sample rejection
2196
        """
2197
        mutator = self.mutator("email_body_sample_rejection")
2198
        return mutator(self, value)
2199
2200
    @security.protected(permissions.View)
2201
    def getEmailBodySampleInvalidation(self):
2202
        """Returns the transformed email body text for invalidation emails
2203
        """
2204
        accessor = self.accessor("email_body_sample_invalidation")
2205
        value = accessor(self)
2206
        if IRichTextValue.providedBy(value):
2207
            # Transforms the raw value to the output mimetype
2208
            value = value.output_relative_to(self)
2209
        return value
2210
2211
    @security.protected(permissions.ModifyPortalContent)
2212
    def setEmailBodySampleInvalidation(self, value):
2213
        """Set email body for sample invalidation
2214
        """
2215
        mutator = self.mutator("email_body_sample_invalidation")
2216
        return mutator(self, value)
2217
2218
    @security.protected(permissions.View)
2219
    def getAutoPrintStickers(self):
2220
        """Get auto print stickers setting
2221
        Returns "None" (string) if not set
2222
        """
2223
        accessor = self.accessor("auto_print_stickers")
2224
        value = accessor(self)
2225
        # Return "None" string as default if not set
2226
        if value is None:
2227
            return "None"
2228
        return value
2229
2230
    @security.protected(permissions.ModifyPortalContent)
2231
    def setAutoPrintStickers(self, value):
2232
        """Set auto print stickers setting
2233
        """
2234
        mutator = self.mutator("auto_print_stickers")
2235
        return mutator(self, value)
2236
2237
    @security.protected(permissions.View)
2238
    def getAutoStickerTemplate(self):
2239
        """Get auto sticker template
2240
        """
2241
        accessor = self.accessor("auto_sticker_template")
2242
        return accessor(self)
2243
2244
    @security.protected(permissions.ModifyPortalContent)
2245
    def setAutoStickerTemplate(self, value):
2246
        """Set auto sticker template
2247
        """
2248
        mutator = self.mutator("auto_sticker_template")
2249
        return mutator(self, value)
2250
2251
    @security.protected(permissions.View)
2252
    def getSmallStickerTemplate(self):
2253
        """Get small sticker template
2254
        """
2255
        accessor = self.accessor("small_sticker_template")
2256
        return accessor(self)
2257
2258
    @security.protected(permissions.ModifyPortalContent)
2259
    def setSmallStickerTemplate(self, value):
2260
        """Set small sticker template
2261
        """
2262
        mutator = self.mutator("small_sticker_template")
2263
        return mutator(self, value)
2264
2265
    @security.protected(permissions.View)
2266
    def getLargeStickerTemplate(self):
2267
        """Get large sticker template
2268
        """
2269
        accessor = self.accessor("large_sticker_template")
2270
        return accessor(self)
2271
2272
    @security.protected(permissions.ModifyPortalContent)
2273
    def setLargeStickerTemplate(self, value):
2274
        """Set large sticker template
2275
        """
2276
        mutator = self.mutator("large_sticker_template")
2277
        return mutator(self, value)
2278
2279
    @security.protected(permissions.View)
2280
    def getDefaultNumberOfCopies(self):
2281
        """Get default number of copies
2282
        """
2283
        accessor = self.accessor("default_number_of_copies")
2284
        return accessor(self)
2285
2286
    @security.protected(permissions.ModifyPortalContent)
2287
    def setDefaultNumberOfCopies(self, value):
2288
        """Set default number of copies
2289
        Converts to int if needed
2290
        """
2291
        value = api.to_int(value, default=None)
2292
        mutator = self.mutator("default_number_of_copies")
2293
        return mutator(self, value)
2294
2295
    @security.protected(permissions.View)
2296
    def getIDFormatting(self):
2297
        """Get ID formatting configuration
2298
        Normalizes None values to empty strings for Choice fields
2299
        Ensures string values are returned as byte strings (not unicode)
2300
        """
2301
        accessor = self.accessor("id_formatting")
2302
        value = accessor(self)
2303
        if not value:
2304
            return DEFAULT_ID_FORMATTING
2305
2306
        # Normalize values: convert `None` to empty strings
2307
        # In Python 2: convert unicode to bytes to prevent `UnicodeDecodeError`
2308
        # when formatting with UTF-8 encoded values
2309
        # In Python 3: keep strings as unicode (no conversion needed)
2310
        normalized = []
2311
        for row in value:
2312
            normalized_row = {}
2313
            for key, val in row.items():
2314
                if val is None:
2315
                    normalized_row[key] = ""
2316
                elif six.PY2 and isinstance(val, six.text_type):
2317
                    normalized_row[key] = val.encode("utf-8")
2318
                else:
2319
                    normalized_row[key] = val
2320
            normalized.append(normalized_row)
2321
2322
        return normalized
2323
2324
    @security.protected(permissions.ModifyPortalContent)
2325
    def setIDFormatting(self, value):
2326
        """Set ID formatting configuration
2327
        Normalizes None values to empty strings for Choice fields
2328
        """
2329
        if value:
2330
            # Normalize None values to empty strings for Choice fields
2331
            normalized = []
2332
            for row in value:
2333
                normalized_row = dict(row)
2334
                # Convert None to empty string for Choice fields
2335
                if normalized_row.get("sequence_type") is None:
2336
                    normalized_row["sequence_type"] = ""
2337
                if normalized_row.get("counter_type") is None:
2338
                    normalized_row["counter_type"] = ""
2339
                # Convert None to empty string for text fields
2340
                for key in ["context", "counter_reference", "prefix",
2341
                            "portal_type", "form"]:
2342
                    if normalized_row.get(key) is None:
2343
                        normalized_row[key] = ""
2344
                # Ensure split_length is an int
2345
                if normalized_row.get("split_length"):
2346
                    normalized_row["split_length"] = api.to_int(
2347
                        normalized_row["split_length"], default=1)
2348
                normalized.append(normalized_row)
2349
            value = normalized
2350
2351
        mutator = self.mutator("id_formatting")
2352
        return mutator(self, value)
2353
2354
    @security.protected(permissions.View)
2355
    def getIDServerValues(self):
2356
        """Get current ID server values
2357
        This is a computed field - returns current counter values
2358
        """
2359
        from senaite.core.interfaces import INumberGenerator
2360
        number_generator = getUtility(INumberGenerator)
2361
        keys = number_generator.keys()
2362
        values = number_generator.values()
2363
        results = []
2364
        for i in range(len(keys)):
2365
            results.append("{}: {}".format(keys[i], values[i]))
2366
        return "\n".join(results)
2367
2368
    @security.protected(permissions.View)
2369
    def getIDServerValuesHTML(self):
2370
        """Get current ID server values as HTML
2371
        Alias for backwards compatibility with AT BikaSetup
2372
        """
2373
        return self.getIDServerValues()
2374
2375
    @security.protected(permissions.View)
2376
    def isRejectionWorkflowEnabled(self):
2377
        """Return true if the rejection workflow is enabled
2378
        """
2379
        return self.getEnableRejectionWorkflow()
2380
2381
    @property
2382
    def laboratory(self):
2383
        """Get the laboratory object via acquisition
2384
        The laboratory is stored in bika_setup which is in the portal root
2385
        """
2386
        bika_setup = api.get_bika_setup()
2387
        if bika_setup:
2388
            return bika_setup.laboratory
2389
        # when we finally migrated it...
2390
        elif "laboratory" in self.objectIds():
2391
            return self["laboratry"]
2392
        return None
2393