Setup.laboratory()   A
last analyzed

Complexity

Conditions 3

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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