Passed
Push — 2.x ( 342369...b8bca3 )
by Jordi
07:24 queued 01:04
created

Setup.getIDFormatting()   B

Complexity

Conditions 7

Size

Total Lines 28
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 17
dl 0
loc 28
rs 8
c 0
b 0
f 0
cc 7
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
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
    # Sampling
850
    printing_workflow_enabled = schema.Bool(
851
        title=_(u"Enable the Results Report Printing workflow"),
852
        description=_(
853
            u"Select this to allow the user to set an additional 'Printed' "
854
            u"status to those Analysis Requests that have been Published. "
855
            u"Disabled by default."
856
        ),
857
        default=False,
858
    )
859
860
    sampling_workflow_enabled = schema.Bool(
861
        title=_(u"Enable Sampling"),
862
        description=_(
863
            u"Select this to activate the sample collection workflow steps."
864
        ),
865
        default=False,
866
    )
867
868
    schedule_sampling_enabled = schema.Bool(
869
        title=_(u"Enable Sampling Scheduling"),
870
        description=_(
871
            u"Select this to allow a Sampling Coordinator to schedule a "
872
            u"sampling. This functionality only takes effect when 'Sampling "
873
            u"workflow' is active"
874
        ),
875
        default=False,
876
    )
877
878
    autoreceive_samples = schema.Bool(
879
        title=_(u"Auto-receive samples"),
880
        description=_(
881
            u"Select to receive the samples automatically when created by lab "
882
            u"personnel and sampling workflow is disabled. Samples created by "
883
            u"client contacts won't be received automatically"
884
        ),
885
        default=False,
886
    )
887
888
    sample_preservation_enabled = schema.Bool(
889
        title=_(u"Enable Sample Preservation"),
890
        description=u"",
891
        default=False,
892
    )
893
894
    workdays = schema.List(
895
        title=_(u"Laboratory Workdays"),
896
        description=_(
897
            u"Only laboratory workdays are considered for the analysis "
898
            u"turnaround time calculation."
899
        ),
900
        value_type=schema.Choice(
901
            vocabulary="senaite.core.vocabularies.weekdays"
902
        ),
903
        default=[u"0", u"1", u"2", u"3", u"4", u"5", u"6"],
904
        required=True,
905
    )
906
907
    directives.widget("default_turnaround_time", DurationWidgetFactory)
908
    default_turnaround_time = DurationField(
909
        title=_(u"Default turnaround time for analyses."),
910
        description=_(
911
            u"This is the default maximum time allowed for performing "
912
            u"analyses. It is only used for analyses where the analysis "
913
            u"service does not specify a turnaround time. Only laboratory "
914
            u"workdays are considered."
915
        ),
916
        required=True,
917
        default=timedelta(days=5),
918
    )
919
920
    directives.widget("default_sample_lifetime", DurationWidgetFactory)
921
    default_sample_lifetime = DurationField(
922
        title=_(u"Default sample retention period"),
923
        description=_(
924
            u"The number of days before a sample expires and cannot be "
925
            u"analysed any more. This setting can be overwritten per "
926
            u"individual sample type in the sample types setup"
927
        ),
928
        required=True,
929
        default=timedelta(days=30),
930
    )
931
932
    # Notifications
933
    notify_on_sample_rejection = schema.Bool(
934
        title=_(u"Email notification on Sample rejection"),
935
        description=_(
936
            u"Select this to activate automatic notifications via email to "
937
            u"the Client when a Sample is rejected."
938
        ),
939
        default=False,
940
    )
941
942
    directives.widget("email_body_sample_rejection", RichTextFieldWidget)
943
    email_body_sample_rejection = RichTextField(
944
        title=_(u"Email body for Sample Rejection notifications"),
945
        description=_(
946
            u"Set the text for the body of the email to be sent to the "
947
            u"Sample's client contact if the option 'Email notification on "
948
            u"Sample rejection' is enabled. You can use reserved keywords: "
949
            u"$sample_id, $sample_link, $reasons, $lab_address"
950
        ),
951
        default=u"The sample $sample_link has been rejected because of the "
952
                u"following reasons:<br/><br/>$reasons<br/><br/>For further "
953
                u"information, please contact us under the following address."
954
                u"<br/><br/>$lab_address",
955
        required=False,
956
    )
957
958
    directives.widget("email_body_sample_invalidation", RichTextFieldWidget)
959
    email_body_sample_invalidation = RichTextField(
960
        title=_(u"Email body for Sample Invalidation notifications"),
961
        description=_(
962
            u"Define the template for the email body that will be "
963
            u"automatically sent to primary contacts and laboratory managers "
964
            u"when a sample is invalidated. The following placeholders are "
965
            u"supported: $sample_id, $retest_id, $retest_link, $reason, "
966
            u"$lab_address."
967
        ),
968
        default=u"Some non-conformities have been detected in the results "
969
                u"report published for Sample $sample_link.<br/><br/>A new "
970
                u"Sample $retest_link has been created automatically, and the "
971
                u"previous request has been invalidated.<br/><br/>The root "
972
                u"cause is under investigation and corrective action has been "
973
                u"initiated.<br/><br/>$lab_address",
974
        required=False,
975
    )
976
977
    # Sticker
978
    auto_print_stickers = schema.Choice(
979
        title=_(u"Automatic Sticker Printing"),
980
        description=_(
981
            u"Choose when stickers should be automatically printed:<br/>"
982
            u"<ul><li><strong>Register:</strong> Stickers are printed "
983
            u"automatically when new samples are created.</li>"
984
            u"<li><strong>Receive:</strong> Stickers are printed automatically "
985
            u"when samples are received.</li>"
986
            u"<li><strong>None:</strong> Disables automatic sticker printing."
987
            u"</li></ul>"
988
        ),
989
        vocabulary=schema.vocabulary.SimpleVocabulary([
990
            schema.vocabulary.SimpleTerm("None", "None", _(u"None")),
991
            schema.vocabulary.SimpleTerm("register", "register",
992
                                          _(u"Register")),
993
            schema.vocabulary.SimpleTerm("receive", "receive", _(u"Receive")),
994
        ]),
995
        required=False,
996
        default="None",
997
    )
998
999
    auto_sticker_template = schema.TextLine(
1000
        title=_(u"Default Sticker Template"),
1001
        description=_(
1002
            u"Select the default sticker template used for automatic printing."
1003
        ),
1004
        required=False,
1005
    )
1006
1007
    small_sticker_template = schema.TextLine(
1008
        title=_(u"Small Sticker Template"),
1009
        description=_(
1010
            u"Choose the default template for 'small' stickers. Note: "
1011
            u"Sample-specific 'small' stickers are configured based on their "
1012
            u"sample type."
1013
        ),
1014
        default=u"Code_128_1x48mm.pt",
1015
        required=False,
1016
    )
1017
1018
    large_sticker_template = schema.TextLine(
1019
        title=_(u"Large Sticker Template"),
1020
        description=_(
1021
            u"Choose the default template for 'large' stickers. Note: "
1022
            u"Sample-specific 'large' stickers are configured based on their "
1023
            u"sample type."
1024
        ),
1025
        default=u"Code_128_1x72mm.pt",
1026
        required=False,
1027
    )
1028
1029
    default_number_of_copies = schema.Int(
1030
        title=_(u"Default Number of Copies"),
1031
        description=_(
1032
            u"Specify how many copies of each sticker should be printed by "
1033
            u"default."
1034
        ),
1035
        required=True,
1036
        default=1,
1037
    )
1038
1039
    # ID Server
1040
    directives.widget(
1041
        "id_formatting",
1042
        DataGridWidgetFactory,
1043
        allow_insert=True,
1044
        allow_delete=True,
1045
        allow_reorder=True,
1046
        auto_append=False)
1047
    id_formatting = DataGridField(
1048
        title=_(u"Formatting Configuration"),
1049
        description=_(
1050
            u"<p>The ID Server provides unique sequential IDs for objects "
1051
            u"such as Samples and Worksheets etc, based on a format "
1052
            u"specified for each content type.</p>"
1053
            u"<p>The format is constructed similarly to the Python format "
1054
            u"syntax, using predefined variables per content type, and "
1055
            u"advancing the IDs through a sequence number, 'seq' and its "
1056
            u"padding as a number of digits, e.g. '03d' for a sequence of "
1057
            u"IDs from 001 to 999.</p>"
1058
            u"<p>Alphanumeric prefixes for IDs are included as is in the "
1059
            u"formats, e.g. WS for Worksheet in WS-{seq:03d} produces "
1060
            u"sequential Worksheet IDs: WS-001, WS-002, WS-003 etc.</p>"
1061
            u"<p>For dynamic generation of alphanumeric and sequential IDs, "
1062
            u"the wildcard {alpha} can be used. E.g WS-{alpha:2a3d} produces "
1063
            u"WS-AA001, WS-AA002, WS-AB034, etc.</p>"
1064
            u"<p>Variables that can be used include:"
1065
            u"<table>"
1066
            u"<tr>"
1067
            u"<th style='width:150px'>Content Type</th><th>Variables</th>"
1068
            u"</tr>"
1069
            u"<tr><td>Client ID</td><td>{clientId}</td></tr>"
1070
            u"<tr><td>Year</td><td>{year}</td></tr>"
1071
            u"<tr><td>Sample ID</td><td>{sampleId}</td></tr>"
1072
            u"<tr><td>Sample Type</td><td>{sampleType}</td></tr>"
1073
            u"<tr><td>Sampling Date</td><td>{samplingDate}</td></tr>"
1074
            u"<tr><td>Date Sampled</td><td>{dateSampled}</td></tr>"
1075
            u"</table>"
1076
            u"</p>"
1077
            u"<p>Configuration Settings:"
1078
            u"<ul>"
1079
            u"<li>format:"
1080
            u"<ul><li>a python format string constructed from predefined "
1081
            u"variables like sampleId, clientId, sampleType.</li>"
1082
            u"<li>special variable 'seq' must be positioned last in the "
1083
            u"format string</li></ul></li>"
1084
            u"<li>sequence type: [generated|counter]</li>"
1085
            u"<li>context: if type counter, provides context the counting "
1086
            u"function</li>"
1087
            u"<li>counter type: [backreference|contained]</li>"
1088
            u"<li>counter reference: a parameter to the counting function</li>"
1089
            u"<li>prefix: default prefix if none provided in format string</li>"
1090
            u"<li>split length: the number of parts to be included in the "
1091
            u"prefix</li>"
1092
            u"</ul></p>"
1093
        ),
1094
        value_type=DataGridRow(schema=IIDFormattingRecordSchema),
1095
        required=False,
1096
        default=DEFAULT_ID_FORMATTING,
1097
    )
1098
1099
    id_server_values = schema.Text(
1100
        title=_(u"ID Server Values"),
1101
        description=_(u"Current ID server counter values"),
1102
        required=False,
1103
        readonly=True,
1104
    )
1105
1106
    ###
1107
    # Fieldsets
1108
    ###
1109
    model.fieldset(
1110
        "security",
1111
        label=_(u"Security"),
1112
        fields=[
1113
            "auto_log_off",
1114
            "restrict_worksheet_users_access",
1115
            "allow_to_submit_not_assigned",
1116
            "restrict_worksheet_management",
1117
            "enable_global_auditlog",
1118
        ]
1119
    )
1120
1121
    model.fieldset(
1122
        "accounting",
1123
        label=_(u"Accounting"),
1124
        fields=[
1125
            "show_prices",
1126
            "currency",
1127
            "default_country",
1128
            "member_discount",
1129
            "vat",
1130
        ]
1131
    )
1132
1133
    model.fieldset(
1134
        "results_reports",
1135
        label=_(u"Results Reports"),
1136
        fields=[
1137
            "decimal_mark",
1138
            "scientific_notation_report",
1139
            "minimum_results",
1140
        ]
1141
    )
1142
1143
    model.fieldset(
1144
        "analyses",
1145
        label=_(u"Analyses"),
1146
        fields=[
1147
            "categorise_analysis_services",
1148
            "categorize_sample_analyses",
1149
            "sample_analyses_required",
1150
            "allow_manual_result_capture_date",
1151
            "enable_ar_specs",
1152
            "exponential_format_threshold",
1153
            "immediate_results_entry",
1154
            "enable_analysis_remarks",
1155
            "auto_verify_samples",
1156
            "self_verification_enabled",
1157
            "number_of_required_verifications",
1158
            "type_of_multi_verification",
1159
            "results_decimal_mark",
1160
            "scientific_notation_results",
1161
            "enable_rejection_workflow",
1162
            "rejection_reasons",
1163
            "default_number_of_ars_to_add",
1164
            "max_number_of_samples_add",
1165
        ]
1166
    )
1167
1168
    model.fieldset(
1169
        "appearance",
1170
        label=_(u"Appearance"),
1171
        fields=[
1172
            "worksheet_layout",
1173
            "dashboard_by_default",
1174
            "landing_page",
1175
            "show_partitions",
1176
            "site_logo",
1177
            "site_logo_css",
1178
            "show_lab_name_in_login",
1179
        ]
1180
    )
1181
1182
    model.fieldset(
1183
        "sampling",
1184
        label=_(u"Sampling"),
1185
        fields=[
1186
            "printing_workflow_enabled",
1187
            "sampling_workflow_enabled",
1188
            "schedule_sampling_enabled",
1189
            "date_sampled_required",
1190
            "autoreceive_samples",
1191
            "sample_preservation_enabled",
1192
            "workdays",
1193
            "default_turnaround_time",
1194
            "default_sample_lifetime",
1195
        ]
1196
    )
1197
1198
    model.fieldset(
1199
        "notifications",
1200
        label=_(u"Notifications"),
1201
        fields=[
1202
            "email_from_sample_publication",
1203
            "email_body_sample_publication",
1204
            "always_cc_responsibles_in_report_emails",
1205
            "notify_on_sample_rejection",
1206
            "email_body_sample_rejection",
1207
            "invalidation_reason_required",
1208
            "email_body_sample_invalidation",
1209
        ]
1210
    )
1211
1212
    model.fieldset(
1213
        "sticker",
1214
        label=_(u"Sticker"),
1215
        fields=[
1216
            "auto_print_stickers",
1217
            "auto_sticker_template",
1218
            "small_sticker_template",
1219
            "large_sticker_template",
1220
            "default_number_of_copies",
1221
        ]
1222
    )
1223
1224
    model.fieldset(
1225
        "id_server",
1226
        label=_(u"ID Server"),
1227
        fields=[
1228
            "id_formatting",
1229
            "id_server_values",
1230
        ]
1231
    )
1232
1233
1234
@implementer(ISetup, ISetupSchema, IHideActionsMenu)
1235
class Setup(Container):
1236
    """SENAITE Setup Folder
1237
    """
1238
    security = ClassSecurityInfo()
1239
1240
    @security.protected(permissions.View)
1241
    def getEmailFromSamplePublication(self):
1242
        """Returns the 'From' address for publication emails
1243
        """
1244
        accessor = self.accessor("email_from_sample_publication")
1245
        email = accessor(self)
1246
        if not email:
1247
            email = default_email_from_sample_publication(self)
1248
        return email
1249
1250
    @security.protected(permissions.ModifyPortalContent)
1251
    def setEmailFromSamplePublication(self, value):
1252
        """Set the 'From' address for publication emails
1253
        """
1254
        mutator = self.mutator("email_from_sample_publication")
1255
        return mutator(self, value)
1256
1257
    @security.protected(permissions.View)
1258
    def getEmailBodySamplePublication(self):
1259
        """Returns the transformed email body text for publication emails
1260
        """
1261
        accessor = self.accessor("email_body_sample_publication")
1262
        value = accessor(self)
1263
        if IRichTextValue.providedBy(value):
1264
            # Transforms the raw value to the output mimetype
1265
            value = value.output_relative_to(self)
1266
        if not value:
1267
            # Always fallback to default value
1268
            value = default_email_body_sample_publication(self)
1269
        return value
1270
1271
    @security.protected(permissions.ModifyPortalContent)
1272
    def setEmailBodySamplePublication(self, value):
1273
        """Set email body text for publication emails
1274
        """
1275
        mutator = self.mutator("email_body_sample_publication")
1276
        return mutator(self, value)
1277
1278
    @security.protected(permissions.View)
1279
    def getAlwaysCCResponsiblesInReportEmail(self):
1280
        """Returns if responsibles should always receive publication emails
1281
        """
1282
        accessor = self.accessor("always_cc_responsibles_in_report_emails")
1283
        return accessor(self)
1284
1285
    @security.protected(permissions.View)
1286
    def setAlwaysCCResponsiblesInReportEmail(self, value):
1287
        """Set if responsibles should always receive publication emails
1288
        """
1289
        mutator = self.mutator("always_cc_responsibles_in_report_emails")
1290
        return mutator(self, value)
1291
1292
    @security.protected(permissions.View)
1293
    def getEnableGlobalAuditlog(self):
1294
        """Returns if the global Auditlog is enabled
1295
        """
1296
        accessor = self.accessor("enable_global_auditlog")
1297
        return accessor(self)
1298
1299
    @security.protected(permissions.ModifyPortalContent)
1300
    def setEnableGlobalAuditlog(self, value):
1301
        """Enable/Disable global Auditlogging
1302
        """
1303
        if value is False:
1304
            # clear the auditlog catalog
1305
            catalog = api.get_tool(AUDITLOG_CATALOG)
1306
            catalog.manage_catalogClear()
1307
        mutator = self.mutator("enable_global_auditlog")
1308
        return mutator(self, value)
1309
1310
    @security.protected(permissions.View)
1311
    def getSiteLogo(self):
1312
        """Returns the global site logo
1313
        """
1314
        accessor = self.accessor("site_logo")
1315
        return accessor(self)
1316
1317
    @security.protected(permissions.ModifyPortalContent)
1318
    def setSiteLogo(self, value):
1319
        """Set the site logo
1320
        """
1321
        mutator = self.mutator("site_logo")
1322
        return mutator(self, value)
1323
1324
    @security.protected(permissions.View)
1325
    def getSiteLogoCSS(self):
1326
        """Returns the global site logo
1327
        """
1328
        accessor = self.accessor("site_logo_css")
1329
        return accessor(self)
1330
1331
    @security.protected(permissions.ModifyPortalContent)
1332
    def setSiteLogoCSS(self, value):
1333
        """Set the site logo
1334
        """
1335
        mutator = self.mutator("site_logo_css")
1336
        return mutator(self, value)
1337
1338
    @security.protected(permissions.View)
1339
    def getImmediateResultsEntry(self):
1340
        """Returns if immediate results entry is enabled or not
1341
        """
1342
        accessor = self.accessor("immediate_results_entry")
1343
        return accessor(self)
1344
1345
    @security.protected(permissions.ModifyPortalContent)
1346
    def setImmediateResultsEntry(self, value):
1347
        """Enable/Disable global Auditlogging
1348
        """
1349
        mutator = self.mutator("immediate_results_entry")
1350
        return mutator(self, value)
1351
1352
    @security.protected(permissions.View)
1353
    def getCategorizeSampleAnalyses(self):
1354
        """Returns if analyses should be grouped by category for samples
1355
        """
1356
        accessor = self.accessor("categorize_sample_analyses")
1357
        return accessor(self)
1358
1359
    @security.protected(permissions.ModifyPortalContent)
1360
    def setCategorizeSampleAnalyses(self, value):
1361
        """Enable/Disable grouping of analyses by category for samples
1362
        """
1363
        mutator = self.mutator("categorize_sample_analyses")
1364
        return mutator(self, value)
1365
1366
    @security.protected(permissions.View)
1367
    def getSampleAnalysesRequired(self):
1368
        """Returns if analyses are required in sample add form
1369
        """
1370
        accessor = self.accessor("sample_analyses_required")
1371
        return accessor(self)
1372
1373
    @security.protected(permissions.ModifyPortalContent)
1374
    def setSampleAnalysesRequired(self, value):
1375
        """Allow/Disallow to create samples without analyses
1376
        """
1377
        mutator = self.mutator("sample_analyses_required")
1378
        return mutator(self, value)
1379
1380
    @security.protected(permissions.View)
1381
    def getAllowManualResultCaptureDate(self):
1382
        """Returns if analyses are required in sample add form
1383
        """
1384
        accessor = self.accessor("allow_manual_result_capture_date")
1385
        return accessor(self)
1386
1387
    @security.protected(permissions.ModifyPortalContent)
1388
    def setAllowManualResultCaptureDate(self, value):
1389
        """Allow/Disallow to create samples without analyses
1390
        """
1391
        mutator = self.mutator("allow_manual_result_capture_date")
1392
        return mutator(self, value)
1393
1394
    @security.protected(permissions.View)
1395
    def getDateSampledRequired(self):
1396
        """Returns whether the DateSampled field is required on sample creation
1397
        when the sampling workflow is not active
1398
        """
1399
        accessor = self.accessor("date_sampled_required")
1400
        return accessor(self)
1401
1402
    @security.protected(permissions.ModifyPortalContent)
1403
    def setDateSampledRequired(self, value):
1404
        """Sets whether the entry of a value for DateSampled field on sample
1405
        creation is required when the sampling workflow is not active
1406
        """
1407
        mutator = self.mutator("date_sampled_required")
1408
        return mutator(self, value)
1409
1410
    @security.protected(permissions.View)
1411
    def getShowLabNameInLogin(self):
1412
        """Returns if the laboratory name has to be displayed in login page
1413
        """
1414
        accessor = self.accessor("show_lab_name_in_login")
1415
        return accessor(self)
1416
1417
    @security.protected(permissions.ModifyPortalContent)
1418
    def setShowLabNameInLogin(self, value):
1419
        """Show/hide the laboratory name in the login page
1420
        """
1421
        mutator = self.mutator("show_lab_name_in_login")
1422
        return mutator(self, value)
1423
1424
    @security.protected(permissions.View)
1425
    def getInvalidationReasonRequired(self):
1426
        """Returns whether the introduction of a reason is required when
1427
        invalidating a sample
1428
        """
1429
        accessor = self.accessor("invalidation_reason_required")
1430
        return accessor(self)
1431
1432
    @security.protected(permissions.ModifyPortalContent)
1433
    def setInvalidationReasonRequired(self, value):
1434
        """Set whether the introduction of a reason is required when
1435
        invalidating a sample
1436
        """
1437
        mutator = self.mutator("invalidation_reason_required")
1438
        return mutator(self, value)
1439
1440
    # Auto Log Off - special handling with session timeout
1441
    @security.protected(permissions.View)
1442
    def getAutoLogOff(self):
1443
        """Get session lifetime in minutes
1444
        """
1445
        acl = api.get_tool("acl_users")
1446
        session = acl.get("session")
1447
        if not session:
1448
            return 0
1449
        return session.timeout // 60
1450
1451
    @security.protected(permissions.ModifyPortalContent)
1452
    def setAutoLogOff(self, value):
1453
        """Set session lifetime in minutes
1454
        """
1455
        value = api.to_int(value, default=0)
1456
        if value < 0:
1457
            value = 0
1458
        value = value * 60
1459
        acl = api.get_tool("acl_users")
1460
        session = acl.get("session")
1461
        if session:
1462
            session.timeout = value
1463
        mutator = self.mutator("auto_log_off")
1464
        return mutator(self, value // 60)
1465
1466
    # Security fields
1467
    @security.protected(permissions.View)
1468
    def getRestrictWorksheetUsersAccess(self):
1469
        """Get restrict worksheet users access setting
1470
        """
1471
        accessor = self.accessor("restrict_worksheet_users_access")
1472
        return accessor(self)
1473
1474
    @security.protected(permissions.ModifyPortalContent)
1475
    def setRestrictWorksheetUsersAccess(self, value):
1476
        """Set restrict worksheet users access setting
1477
        """
1478
        mutator = self.mutator("restrict_worksheet_users_access")
1479
        return mutator(self, value)
1480
1481
    @security.protected(permissions.View)
1482
    def getAllowToSubmitNotAssigned(self):
1483
        """Get allow to submit not assigned setting
1484
        """
1485
        accessor = self.accessor("allow_to_submit_not_assigned")
1486
        return accessor(self)
1487
1488
    @security.protected(permissions.ModifyPortalContent)
1489
    def setAllowToSubmitNotAssigned(self, value):
1490
        """Set allow to submit not assigned setting
1491
        """
1492
        mutator = self.mutator("allow_to_submit_not_assigned")
1493
        return mutator(self, value)
1494
1495
    @security.protected(permissions.View)
1496
    def getRestrictWorksheetManagement(self):
1497
        """Get restrict worksheet management setting
1498
        """
1499
        accessor = self.accessor("restrict_worksheet_management")
1500
        return accessor(self)
1501
1502
    @security.protected(permissions.ModifyPortalContent)
1503
    def setRestrictWorksheetManagement(self, value):
1504
        """Set restrict worksheet management setting
1505
        """
1506
        mutator = self.mutator("restrict_worksheet_management")
1507
        return mutator(self, value)
1508
1509
    # Accounting fields
1510
    @security.protected(permissions.View)
1511
    def getShowPrices(self):
1512
        """Get show prices setting
1513
        """
1514
        accessor = self.accessor("show_prices")
1515
        return accessor(self)
1516
1517
    @security.protected(permissions.ModifyPortalContent)
1518
    def setShowPrices(self, value):
1519
        """Set show prices setting
1520
        """
1521
        mutator = self.mutator("show_prices")
1522
        return mutator(self, value)
1523
1524
    @security.protected(permissions.View)
1525
    def getCurrency(self):
1526
        """Get currency setting
1527
        """
1528
        accessor = self.accessor("currency")
1529
        return accessor(self)
1530
1531
    @security.protected(permissions.ModifyPortalContent)
1532
    def setCurrency(self, value):
1533
        """Set currency setting
1534
        """
1535
        mutator = self.mutator("currency")
1536
        return mutator(self, value)
1537
1538
    @security.protected(permissions.View)
1539
    def getDefaultCountry(self):
1540
        """Get default country setting
1541
        """
1542
        accessor = self.accessor("default_country")
1543
        return accessor(self)
1544
1545
    @security.protected(permissions.ModifyPortalContent)
1546
    def setDefaultCountry(self, value):
1547
        """Set default country setting
1548
        """
1549
        mutator = self.mutator("default_country")
1550
        return mutator(self, value)
1551
1552
    @security.protected(permissions.View)
1553
    def getMemberDiscount(self):
1554
        """Get member discount percentage
1555
        Returns string value, defaults to "0.0" if empty
1556
        """
1557
        accessor = self.accessor("member_discount")
1558
        value = accessor(self)
1559
        # Convert to string if numeric (from AT FixedPointField)
1560
        if value is None or value == "":
1561
            return u"0.0"
1562
        if isinstance(value, (int, float)):
1563
            return api.safe_unicode(str(value))
1564
        return api.safe_unicode(value)
1565
1566
    @security.protected(permissions.ModifyPortalContent)
1567
    def setMemberDiscount(self, value):
1568
        """Set member discount percentage
1569
        Accepts string or numeric value
1570
        """
1571
        # Convert numeric to string
1572
        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...
1573
            value = api.safe_unicode(str(value))
1574
        elif value:
1575
            value = api.safe_unicode(value)
1576
        else:
1577
            value = u""
1578
        mutator = self.mutator("member_discount")
1579
        return mutator(self, value)
1580
1581
    @security.protected(permissions.View)
1582
    def getVAT(self):
1583
        """Get VAT percentage
1584
        Returns string value, defaults to "0.0" if empty
1585
        """
1586
        accessor = self.accessor("vat")
1587
        value = accessor(self)
1588
        # Convert to string if numeric (from AT FixedPointField)
1589
        if value is None or value == "":
1590
            return u"0.0"
1591
        if isinstance(value, (int, float)):
1592
            return api.safe_unicode(str(value))
1593
        return api.safe_unicode(value)
1594
1595
    @security.protected(permissions.ModifyPortalContent)
1596
    def setVAT(self, value):
1597
        """Set VAT percentage
1598
        Accepts string or numeric value
1599
        """
1600
        # Convert numeric to string
1601
        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...
1602
            value = api.safe_unicode(str(value))
1603
        elif value:
1604
            value = api.safe_unicode(value)
1605
        else:
1606
            value = u""
1607
        mutator = self.mutator("vat")
1608
        return mutator(self, value)
1609
1610
    @security.protected(permissions.View)
1611
    def getDecimalMark(self):
1612
        """Get decimal mark for reports
1613
        """
1614
        accessor = self.accessor("decimal_mark")
1615
        value = accessor(self)
1616
        # Return default if value is empty
1617
        return value or "."
1618
1619
    @security.protected(permissions.ModifyPortalContent)
1620
    def setDecimalMark(self, value):
1621
        """Set decimal mark for reports
1622
        """
1623
        mutator = self.mutator("decimal_mark")
1624
        return mutator(self, value)
1625
1626
    @security.protected(permissions.View)
1627
    def getScientificNotationReport(self):
1628
        """Get scientific notation format for reports
1629
        """
1630
        accessor = self.accessor("scientific_notation_report")
1631
        value = accessor(self)
1632
        # Return default if value is empty
1633
        return value or "1"
1634
1635
    @security.protected(permissions.ModifyPortalContent)
1636
    def setScientificNotationReport(self, value):
1637
        """Set scientific notation format for reports
1638
        """
1639
        mutator = self.mutator("scientific_notation_report")
1640
        return mutator(self, value)
1641
1642
    @security.protected(permissions.View)
1643
    def getMinimumResults(self):
1644
        """Get minimum number of results for QC stats
1645
        """
1646
        accessor = self.accessor("minimum_results")
1647
        return accessor(self)
1648
1649
    @security.protected(permissions.ModifyPortalContent)
1650
    def setMinimumResults(self, value):
1651
        """Set minimum number of results for QC stats
1652
        Converts to int if needed
1653
        """
1654
        value = api.to_int(value, default=None)
1655
        mutator = self.mutator("minimum_results")
1656
        return mutator(self, value)
1657
1658
    @security.protected(permissions.View)
1659
    def getCategoriseAnalysisServices(self):
1660
        """Get categorise analysis services setting
1661
        """
1662
        accessor = self.accessor("categorise_analysis_services")
1663
        return accessor(self)
1664
1665
    @security.protected(permissions.ModifyPortalContent)
1666
    def setCategoriseAnalysisServices(self, value):
1667
        """Set categorise analysis services setting
1668
        """
1669
        mutator = self.mutator("categorise_analysis_services")
1670
        return mutator(self, value)
1671
1672
    @security.protected(permissions.View)
1673
    def getEnableARSpecs(self):
1674
        """Get enable AR specs setting
1675
        """
1676
        accessor = self.accessor("enable_ar_specs")
1677
        return accessor(self)
1678
1679
    @security.protected(permissions.ModifyPortalContent)
1680
    def setEnableARSpecs(self, value):
1681
        """Set enable AR specs setting
1682
        """
1683
        mutator = self.mutator("enable_ar_specs")
1684
        return mutator(self, value)
1685
1686
    @security.protected(permissions.View)
1687
    def getExponentialFormatThreshold(self):
1688
        """Get exponential format threshold
1689
        """
1690
        accessor = self.accessor("exponential_format_threshold")
1691
        return accessor(self)
1692
1693
    @security.protected(permissions.ModifyPortalContent)
1694
    def setExponentialFormatThreshold(self, value):
1695
        """Set exponential format threshold
1696
        Converts to int if needed
1697
        """
1698
        value = api.to_int(value, default=None)
1699
        mutator = self.mutator("exponential_format_threshold")
1700
        return mutator(self, value)
1701
1702
    @security.protected(permissions.View)
1703
    def getEnableAnalysisRemarks(self):
1704
        """Get enable analysis remarks setting
1705
        """
1706
        accessor = self.accessor("enable_analysis_remarks")
1707
        return accessor(self)
1708
1709
    @security.protected(permissions.ModifyPortalContent)
1710
    def setEnableAnalysisRemarks(self, value):
1711
        """Set enable analysis remarks setting
1712
        """
1713
        mutator = self.mutator("enable_analysis_remarks")
1714
        return mutator(self, value)
1715
1716
    @security.protected(permissions.View)
1717
    def getAutoVerifySamples(self):
1718
        """Get auto verify samples setting
1719
        """
1720
        accessor = self.accessor("auto_verify_samples")
1721
        return accessor(self)
1722
1723
    @security.protected(permissions.ModifyPortalContent)
1724
    def setAutoVerifySamples(self, value):
1725
        """Set auto verify samples setting
1726
        """
1727
        mutator = self.mutator("auto_verify_samples")
1728
        return mutator(self, value)
1729
1730
    @security.protected(permissions.View)
1731
    def getSelfVerificationEnabled(self):
1732
        """Get self verification enabled setting
1733
        """
1734
        accessor = self.accessor("self_verification_enabled")
1735
        return accessor(self)
1736
1737
    @security.protected(permissions.ModifyPortalContent)
1738
    def setSelfVerificationEnabled(self, value):
1739
        """Set self verification enabled setting
1740
        """
1741
        mutator = self.mutator("self_verification_enabled")
1742
        return mutator(self, value)
1743
1744
    @security.protected(permissions.View)
1745
    def getNumberOfRequiredVerifications(self):
1746
        """Get number of required verifications
1747
        Returns 1 (default) if not set
1748
        """
1749
        accessor = self.accessor("number_of_required_verifications")
1750
        value = accessor(self)
1751
        # Return default of 1 if not set
1752
        if value is None:
1753
            return 1
1754
        return value
1755
1756
    @security.protected(permissions.ModifyPortalContent)
1757
    def setNumberOfRequiredVerifications(self, value):
1758
        """Set number of required verifications
1759
        """
1760
        mutator = self.mutator("number_of_required_verifications")
1761
        return mutator(self, value)
1762
1763
    @security.protected(permissions.View)
1764
    def getTypeOfmultiVerification(self):
1765
        """Get type of multi verification
1766
        """
1767
        accessor = self.accessor("type_of_multi_verification")
1768
        return accessor(self)
1769
1770
    @security.protected(permissions.ModifyPortalContent)
1771
    def setTypeOfmultiVerification(self, value):
1772
        """Set type of multi verification
1773
        """
1774
        mutator = self.mutator("type_of_multi_verification")
1775
        return mutator(self, value)
1776
1777
    @security.protected(permissions.View)
1778
    def getResultsDecimalMark(self):
1779
        """Get decimal mark for results
1780
        """
1781
        accessor = self.accessor("results_decimal_mark")
1782
        value = accessor(self)
1783
        # Return default if value is empty
1784
        return value or "."
1785
1786
    @security.protected(permissions.ModifyPortalContent)
1787
    def setResultsDecimalMark(self, value):
1788
        """Set decimal mark for results
1789
        """
1790
        mutator = self.mutator("results_decimal_mark")
1791
        return mutator(self, value)
1792
1793
    @security.protected(permissions.View)
1794
    def getScientificNotationResults(self):
1795
        """Get scientific notation format for results
1796
        """
1797
        accessor = self.accessor("scientific_notation_results")
1798
        value = accessor(self)
1799
        # Return default if value is empty
1800
        return value or "1"
1801
1802
    @security.protected(permissions.ModifyPortalContent)
1803
    def setScientificNotationResults(self, value):
1804
        """Set scientific notation format for results
1805
        """
1806
        mutator = self.mutator("scientific_notation_results")
1807
        return mutator(self, value)
1808
1809
    @security.protected(permissions.View)
1810
    def getDefaultNumberOfARsToAdd(self):
1811
        """Get default number of ARs to add
1812
        """
1813
        accessor = self.accessor("default_number_of_ars_to_add")
1814
        return accessor(self)
1815
1816
    @security.protected(permissions.ModifyPortalContent)
1817
    def setDefaultNumberOfARsToAdd(self, value):
1818
        """Set default number of ARs to add
1819
        Converts to int if needed
1820
        """
1821
        value = api.to_int(value, default=None)
1822
        mutator = self.mutator("default_number_of_ars_to_add")
1823
        return mutator(self, value)
1824
1825
    @security.protected(permissions.View)
1826
    def getEnableRejectionWorkflow(self):
1827
        """Get enable rejection workflow
1828
        """
1829
        accessor = self.accessor("enable_rejection_workflow")
1830
        return accessor(self)
1831
1832
    @security.protected(permissions.ModifyPortalContent)
1833
    def setEnableRejectionWorkflow(self, value):
1834
        """Set enable rejection workflow
1835
        """
1836
        mutator = self.mutator("enable_rejection_workflow")
1837
        return mutator(self, value)
1838
1839
    @security.protected(permissions.View)
1840
    def getRejectionReasons(self):
1841
        """Get rejection reasons
1842
        Returns a list of unicode strings. The v02_07_000 upgrade step
1843
        converts old AT RecordsField data to this format.
1844
        """
1845
        accessor = self.accessor("rejection_reasons")
1846
        reasons = accessor(self)
1847
        if not reasons:
1848
            return []
1849
        # Ensure all reasons are unicode
1850
        return [api.safe_unicode(r) for r in reasons]
1851
1852
    @security.protected(permissions.ModifyPortalContent)
1853
    def setRejectionReasons(self, value):
1854
        """Set rejection reasons
1855
        Accepts a simple list of strings (DX format).
1856
        The v02_07_000 upgrade step handles AT→DX conversion before calling
1857
        this setter, so no format conversion is needed here.
1858
        """
1859
        # Ensure all values are unicode
1860
        if value and isinstance(value, (list, tuple)):
1861
            value = [api.safe_unicode(v) for v in value if v]
1862
        else:
1863
            value = []
1864
        mutator = self.mutator("rejection_reasons")
1865
        return mutator(self, value)
1866
1867
    @deprecate("Method is kept for backwards compatibility only")
1868
    def getRejectionReasonsItems(self):
1869
        """Return the list of predefined rejection reasons
1870
1871
        .. deprecated::
1872
            Use getRejectionReasons() instead. This method is kept for
1873
            backwards compatibility only and will be removed in a future
1874
            version.
1875
        """
1876
        return self.getRejectionReasons()
1877
1878
    @security.protected(permissions.View)
1879
    def getMaxNumberOfSamplesAdd(self):
1880
        """Get maximum number of samples to add
1881
        """
1882
        accessor = self.accessor("max_number_of_samples_add")
1883
        return accessor(self)
1884
1885
    @security.protected(permissions.ModifyPortalContent)
1886
    def setMaxNumberOfSamplesAdd(self, value):
1887
        """Set maximum number of samples to add
1888
        Converts to int if needed
1889
        """
1890
        value = api.to_int(value, default=None)
1891
        mutator = self.mutator("max_number_of_samples_add")
1892
        return mutator(self, value)
1893
1894
    @security.protected(permissions.View)
1895
    def getWorksheetLayout(self):
1896
        """Get worksheet layout
1897
        """
1898
        accessor = self.accessor("worksheet_layout")
1899
        return accessor(self)
1900
1901
    @security.protected(permissions.ModifyPortalContent)
1902
    def setWorksheetLayout(self, value):
1903
        """Set worksheet layout
1904
        """
1905
        mutator = self.mutator("worksheet_layout")
1906
        return mutator(self, value)
1907
1908
    @security.protected(permissions.View)
1909
    def getDashboardByDefault(self):
1910
        """Get dashboard by default setting
1911
        """
1912
        accessor = self.accessor("dashboard_by_default")
1913
        return accessor(self)
1914
1915
    @security.protected(permissions.ModifyPortalContent)
1916
    def setDashboardByDefault(self, value):
1917
        """Set dashboard by default setting
1918
        """
1919
        mutator = self.mutator("dashboard_by_default")
1920
        return mutator(self, value)
1921
1922
    @security.protected(permissions.View)
1923
    def getLandingPage(self):
1924
        """Get landing page
1925
        """
1926
        accessor = self.accessor("landing_page")
1927
        return accessor(self)
1928
1929
    @security.protected(permissions.ModifyPortalContent)
1930
    def setLandingPage(self, value):
1931
        """Set landing page
1932
        """
1933
        mutator = self.mutator("landing_page")
1934
        return mutator(self, value)
1935
1936
    @security.protected(permissions.View)
1937
    def getShowPartitions(self):
1938
        """Get show partitions setting
1939
        """
1940
        accessor = self.accessor("show_partitions")
1941
        return accessor(self)
1942
1943
    @security.protected(permissions.ModifyPortalContent)
1944
    def setShowPartitions(self, value):
1945
        """Set show partitions setting
1946
        """
1947
        mutator = self.mutator("show_partitions")
1948
        return mutator(self, value)
1949
1950
    @security.protected(permissions.View)
1951
    def getPrintingWorkflowEnabled(self):
1952
        """Get printing workflow enabled setting
1953
        """
1954
        accessor = self.accessor("printing_workflow_enabled")
1955
        return accessor(self)
1956
1957
    @security.protected(permissions.ModifyPortalContent)
1958
    def setPrintingWorkflowEnabled(self, value):
1959
        """Set printing workflow enabled setting
1960
        """
1961
        mutator = self.mutator("printing_workflow_enabled")
1962
        return mutator(self, value)
1963
1964
    @security.protected(permissions.View)
1965
    def getSamplingWorkflowEnabled(self):
1966
        """Get sampling workflow enabled setting
1967
        """
1968
        accessor = self.accessor("sampling_workflow_enabled")
1969
        return accessor(self)
1970
1971
    @security.protected(permissions.ModifyPortalContent)
1972
    def setSamplingWorkflowEnabled(self, value):
1973
        """Set sampling workflow enabled setting
1974
        """
1975
        mutator = self.mutator("sampling_workflow_enabled")
1976
        return mutator(self, value)
1977
1978
    @security.protected(permissions.View)
1979
    def getScheduleSamplingEnabled(self):
1980
        """Get schedule sampling enabled setting
1981
        """
1982
        accessor = self.accessor("schedule_sampling_enabled")
1983
        return accessor(self)
1984
1985
    @security.protected(permissions.ModifyPortalContent)
1986
    def setScheduleSamplingEnabled(self, value):
1987
        """Set schedule sampling enabled setting
1988
        """
1989
        mutator = self.mutator("schedule_sampling_enabled")
1990
        return mutator(self, value)
1991
1992
    @security.protected(permissions.View)
1993
    def getAutoreceiveSamples(self):
1994
        """Get autoreceive samples setting
1995
        """
1996
        accessor = self.accessor("autoreceive_samples")
1997
        return accessor(self)
1998
1999
    @security.protected(permissions.ModifyPortalContent)
2000
    def setAutoreceiveSamples(self, value):
2001
        """Set autoreceive samples setting
2002
        """
2003
        mutator = self.mutator("autoreceive_samples")
2004
        return mutator(self, value)
2005
2006
    @security.protected(permissions.View)
2007
    def getSamplePreservationEnabled(self):
2008
        """Get sample preservation enabled setting
2009
        """
2010
        accessor = self.accessor("sample_preservation_enabled")
2011
        return accessor(self)
2012
2013
    @security.protected(permissions.ModifyPortalContent)
2014
    def setSamplePreservationEnabled(self, value):
2015
        """Set sample preservation enabled setting
2016
        """
2017
        mutator = self.mutator("sample_preservation_enabled")
2018
        return mutator(self, value)
2019
2020
    @security.protected(permissions.View)
2021
    def getWorkdays(self):
2022
        """Get laboratory workdays
2023
        """
2024
        accessor = self.accessor("workdays")
2025
        return accessor(self)
2026
2027
    @security.protected(permissions.ModifyPortalContent)
2028
    def setWorkdays(self, value):
2029
        """Set laboratory workdays
2030
        """
2031
        mutator = self.mutator("workdays")
2032
        return mutator(self, value)
2033
2034
    @security.protected(permissions.View)
2035
    def getDefaultTurnaroundTime(self):
2036
        """Get default turnaround time
2037
        """
2038
        accessor = self.accessor("default_turnaround_time")
2039
        return accessor(self)
2040
2041
    @security.protected(permissions.ModifyPortalContent)
2042
    def setDefaultTurnaroundTime(self, value):
2043
        """Set default turnaround time
2044
        """
2045
        # Handle both dict (from AT) and timedelta (from DX) formats
2046
        if isinstance(value, dict):
2047
            value = dtime.to_timedelta(value)
2048
        mutator = self.mutator("default_turnaround_time")
2049
        return mutator(self, value)
2050
2051
    @security.protected(permissions.View)
2052
    def getDefaultSampleLifetime(self):
2053
        """Get default sample lifetime
2054
        """
2055
        accessor = self.accessor("default_sample_lifetime")
2056
        return accessor(self)
2057
2058
    @security.protected(permissions.ModifyPortalContent)
2059
    def setDefaultSampleLifetime(self, value):
2060
        """Set default sample lifetime
2061
        """
2062
        # Handle both dict (from AT) and timedelta (from DX) formats
2063
        if isinstance(value, dict):
2064
            value = dtime.to_timedelta(value)
2065
        mutator = self.mutator("default_sample_lifetime")
2066
        return mutator(self, value)
2067
2068
    @security.protected(permissions.View)
2069
    def getNotifyOnSampleRejection(self):
2070
        """Get notify on sample rejection setting
2071
        """
2072
        accessor = self.accessor("notify_on_sample_rejection")
2073
        return accessor(self)
2074
2075
    @security.protected(permissions.ModifyPortalContent)
2076
    def setNotifyOnSampleRejection(self, value):
2077
        """Set notify on sample rejection setting
2078
        """
2079
        mutator = self.mutator("notify_on_sample_rejection")
2080
        return mutator(self, value)
2081
2082
    @security.protected(permissions.View)
2083
    def getEmailBodySampleRejection(self):
2084
        """Returns the transformed email body text for rejection emails
2085
        """
2086
        accessor = self.accessor("email_body_sample_rejection")
2087
        value = accessor(self)
2088
        if IRichTextValue.providedBy(value):
2089
            # Transforms the raw value to the output mimetype
2090
            value = value.output_relative_to(self)
2091
        return value
2092
2093
    @security.protected(permissions.ModifyPortalContent)
2094
    def setEmailBodySampleRejection(self, value):
2095
        """Set email body for sample rejection
2096
        """
2097
        mutator = self.mutator("email_body_sample_rejection")
2098
        return mutator(self, value)
2099
2100
    @security.protected(permissions.View)
2101
    def getEmailBodySampleInvalidation(self):
2102
        """Returns the transformed email body text for invalidation emails
2103
        """
2104
        accessor = self.accessor("email_body_sample_invalidation")
2105
        value = accessor(self)
2106
        if IRichTextValue.providedBy(value):
2107
            # Transforms the raw value to the output mimetype
2108
            value = value.output_relative_to(self)
2109
        return value
2110
2111
    @security.protected(permissions.ModifyPortalContent)
2112
    def setEmailBodySampleInvalidation(self, value):
2113
        """Set email body for sample invalidation
2114
        """
2115
        mutator = self.mutator("email_body_sample_invalidation")
2116
        return mutator(self, value)
2117
2118
    @security.protected(permissions.View)
2119
    def getAutoPrintStickers(self):
2120
        """Get auto print stickers setting
2121
        Returns "None" (string) if not set
2122
        """
2123
        accessor = self.accessor("auto_print_stickers")
2124
        value = accessor(self)
2125
        # Return "None" string as default if not set
2126
        if value is None:
2127
            return "None"
2128
        return value
2129
2130
    @security.protected(permissions.ModifyPortalContent)
2131
    def setAutoPrintStickers(self, value):
2132
        """Set auto print stickers setting
2133
        """
2134
        mutator = self.mutator("auto_print_stickers")
2135
        return mutator(self, value)
2136
2137
    @security.protected(permissions.View)
2138
    def getAutoStickerTemplate(self):
2139
        """Get auto sticker template
2140
        """
2141
        accessor = self.accessor("auto_sticker_template")
2142
        return accessor(self)
2143
2144
    @security.protected(permissions.ModifyPortalContent)
2145
    def setAutoStickerTemplate(self, value):
2146
        """Set auto sticker template
2147
        """
2148
        mutator = self.mutator("auto_sticker_template")
2149
        return mutator(self, value)
2150
2151
    @security.protected(permissions.View)
2152
    def getSmallStickerTemplate(self):
2153
        """Get small sticker template
2154
        """
2155
        accessor = self.accessor("small_sticker_template")
2156
        return accessor(self)
2157
2158
    @security.protected(permissions.ModifyPortalContent)
2159
    def setSmallStickerTemplate(self, value):
2160
        """Set small sticker template
2161
        """
2162
        mutator = self.mutator("small_sticker_template")
2163
        return mutator(self, value)
2164
2165
    @security.protected(permissions.View)
2166
    def getLargeStickerTemplate(self):
2167
        """Get large sticker template
2168
        """
2169
        accessor = self.accessor("large_sticker_template")
2170
        return accessor(self)
2171
2172
    @security.protected(permissions.ModifyPortalContent)
2173
    def setLargeStickerTemplate(self, value):
2174
        """Set large sticker template
2175
        """
2176
        mutator = self.mutator("large_sticker_template")
2177
        return mutator(self, value)
2178
2179
    @security.protected(permissions.View)
2180
    def getDefaultNumberOfCopies(self):
2181
        """Get default number of copies
2182
        """
2183
        accessor = self.accessor("default_number_of_copies")
2184
        return accessor(self)
2185
2186
    @security.protected(permissions.ModifyPortalContent)
2187
    def setDefaultNumberOfCopies(self, value):
2188
        """Set default number of copies
2189
        Converts to int if needed
2190
        """
2191
        value = api.to_int(value, default=None)
2192
        mutator = self.mutator("default_number_of_copies")
2193
        return mutator(self, value)
2194
2195
    @security.protected(permissions.View)
2196
    def getIDFormatting(self):
2197
        """Get ID formatting configuration
2198
        Normalizes None values to empty strings for Choice fields
2199
        Ensures string values are returned as byte strings (not unicode)
2200
        """
2201
        accessor = self.accessor("id_formatting")
2202
        value = accessor(self)
2203
        if not value:
2204
            return DEFAULT_ID_FORMATTING
2205
2206
        # Normalize values: convert `None` to empty strings
2207
        # In Python 2: convert unicode to bytes to prevent `UnicodeDecodeError`
2208
        # when formatting with UTF-8 encoded values
2209
        # In Python 3: keep strings as unicode (no conversion needed)
2210
        normalized = []
2211
        for row in value:
2212
            normalized_row = {}
2213
            for key, val in row.items():
2214
                if val is None:
2215
                    normalized_row[key] = ""
2216
                elif six.PY2 and isinstance(val, six.text_type):
2217
                    normalized_row[key] = val.encode("utf-8")
2218
                else:
2219
                    normalized_row[key] = val
2220
            normalized.append(normalized_row)
2221
2222
        return normalized
2223
2224
    @security.protected(permissions.ModifyPortalContent)
2225
    def setIDFormatting(self, value):
2226
        """Set ID formatting configuration
2227
        Normalizes None values to empty strings for Choice fields
2228
        """
2229
        if value:
2230
            # Normalize None values to empty strings for Choice fields
2231
            normalized = []
2232
            for row in value:
2233
                normalized_row = dict(row)
2234
                # Convert None to empty string for Choice fields
2235
                if normalized_row.get("sequence_type") is None:
2236
                    normalized_row["sequence_type"] = ""
2237
                if normalized_row.get("counter_type") is None:
2238
                    normalized_row["counter_type"] = ""
2239
                # Convert None to empty string for text fields
2240
                for key in ["context", "counter_reference", "prefix",
2241
                            "portal_type", "form"]:
2242
                    if normalized_row.get(key) is None:
2243
                        normalized_row[key] = ""
2244
                # Ensure split_length is an int
2245
                if normalized_row.get("split_length"):
2246
                    normalized_row["split_length"] = api.to_int(
2247
                        normalized_row["split_length"], default=1)
2248
                normalized.append(normalized_row)
2249
            value = normalized
2250
2251
        mutator = self.mutator("id_formatting")
2252
        return mutator(self, value)
2253
2254
    @security.protected(permissions.View)
2255
    def getIDServerValues(self):
2256
        """Get current ID server values
2257
        This is a computed field - returns current counter values
2258
        """
2259
        from senaite.core.interfaces import INumberGenerator
2260
        number_generator = getUtility(INumberGenerator)
2261
        keys = number_generator.keys()
2262
        values = number_generator.values()
2263
        results = []
2264
        for i in range(len(keys)):
2265
            results.append("{}: {}".format(keys[i], values[i]))
2266
        return "\n".join(results)
2267
2268
    @security.protected(permissions.View)
2269
    def getIDServerValuesHTML(self):
2270
        """Get current ID server values as HTML
2271
        Alias for backwards compatibility with AT BikaSetup
2272
        """
2273
        return self.getIDServerValues()
2274
2275
    @security.protected(permissions.View)
2276
    def isRejectionWorkflowEnabled(self):
2277
        """Return true if the rejection workflow is enabled
2278
        """
2279
        return self.getEnableRejectionWorkflow()
2280
2281
    @property
2282
    def laboratory(self):
2283
        """Get the laboratory object via acquisition
2284
        The laboratory is stored in bika_setup which is in the portal root
2285
        """
2286
        bika_setup = api.get_bika_setup()
2287
        if bika_setup:
2288
            return bika_setup.laboratory
2289
        # when we finally migrated it...
2290
        elif "laboratory" in self.objectIds():
2291
            return self["laboratry"]
2292
        return None
2293