Passed
Push — 2.x ( 864250...eaa7c8 )
by Jordi
06:49
created

AnalysisRequest.isAnalysisServiceHidden()   B

Complexity

Conditions 6

Size

Total Lines 28
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 10
dl 0
loc 28
rs 8.6666
c 0
b 0
f 0
cc 6
nop 2
1
# -*- coding: utf-8 -*-
2
#
3
# This file is part of SENAITE.CORE.
4
#
5
# SENAITE.CORE is free software: you can redistribute it and/or modify it under
6
# the terms of the GNU General Public License as published by the Free Software
7
# Foundation, version 2.
8
#
9
# This program is distributed in the hope that it will be useful, but WITHOUT
10
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12
# details.
13
#
14
# You should have received a copy of the GNU General Public License along with
15
# this program; if not, write to the Free Software Foundation, Inc., 51
16
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
#
18
# Copyright 2018-2024 by it's authors.
19
# Some rights reserved, see README and LICENSE.
20
21
import base64
22
import functools
23
import re
24
from datetime import datetime
25
from decimal import Decimal
26
27
from AccessControl import ClassSecurityInfo
28
from bika.lims import api
29
from bika.lims import bikaMessageFactory as _
30
from bika.lims import deprecated
31
from bika.lims import logger
32
from bika.lims.api.security import check_permission
33
from bika.lims.browser.fields import ARAnalysesField
34
from bika.lims.browser.fields import DurationField
35
from bika.lims.browser.fields import EmailsField
36
from bika.lims.browser.fields import ResultsRangesField
37
from bika.lims.browser.fields import UIDReferenceField
38
from bika.lims.browser.fields.remarksfield import RemarksField
39
from bika.lims.browser.fields.uidreferencefield import get_backreferences
40
from bika.lims.browser.widgets import DateTimeWidget
41
from bika.lims.browser.widgets import DecimalWidget
42
from bika.lims.browser.widgets import PrioritySelectionWidget
43
from bika.lims.browser.widgets import RejectionWidget
44
from bika.lims.browser.widgets import RemarksWidget
45
from bika.lims.browser.widgets import SelectionWidget as BikaSelectionWidget
46
from bika.lims.browser.widgets.durationwidget import DurationWidget
47
from bika.lims.config import PRIORITIES
48
from bika.lims.config import PROJECTNAME
49
from bika.lims.content.bikaschema import BikaSchema
50
from bika.lims.content.clientawaremixin import ClientAwareMixin
51
from bika.lims.interfaces import IAnalysisRequest
52
from bika.lims.interfaces import IAnalysisRequestPartition
53
from bika.lims.interfaces import IAnalysisRequestWithPartitions
54
from bika.lims.interfaces import IBatch
55
from bika.lims.interfaces import ICancellable
56
from bika.lims.interfaces import IClient
57
from bika.lims.interfaces import ISubmitted
58
from bika.lims.utils import getUsers
59
from bika.lims.utils import tmpID
60
from bika.lims.utils import user_email
61
from bika.lims.utils import user_fullname
62
from bika.lims.utils.analysisrequest import apply_hidden_services
63
from bika.lims.workflow import getTransitionDate
64
from bika.lims.workflow import getTransitionUsers
65
from DateTime import DateTime
66
from Products.Archetypes.atapi import BaseFolder
67
from Products.Archetypes.atapi import BooleanField
68
from Products.Archetypes.atapi import BooleanWidget
69
from Products.Archetypes.atapi import ComputedField
70
from Products.Archetypes.atapi import ComputedWidget
71
from Products.Archetypes.atapi import FileField
72
from Products.Archetypes.atapi import FileWidget
73
from Products.Archetypes.atapi import FixedPointField
74
from Products.Archetypes.atapi import StringField
75
from Products.Archetypes.atapi import StringWidget
76
from Products.Archetypes.atapi import TextField
77
from Products.Archetypes.atapi import registerType
78
from Products.Archetypes.config import UID_CATALOG
79
from Products.Archetypes.Field import IntegerField
80
from Products.Archetypes.public import Schema
81
from Products.Archetypes.Widget import IntegerWidget
82
from Products.Archetypes.Widget import RichWidget
83
from Products.CMFCore.permissions import ModifyPortalContent
84
from Products.CMFCore.permissions import View
85
from Products.CMFCore.utils import getToolByName
86
from Products.CMFPlone.utils import _createObjectByType
87
from Products.CMFPlone.utils import safe_unicode
88
from senaite.core.browser.fields.datetime import DateTimeField
89
from senaite.core.browser.fields.records import RecordsField
90
from senaite.core.browser.widgets.referencewidget import ReferenceWidget
91
from senaite.core.catalog import ANALYSIS_CATALOG
92
from senaite.core.catalog import CLIENT_CATALOG
93
from senaite.core.catalog import CONTACT_CATALOG
94
from senaite.core.catalog import SAMPLE_CATALOG
95
from senaite.core.catalog import SENAITE_CATALOG
96
from senaite.core.catalog import SETUP_CATALOG
97
from senaite.core.catalog import WORKSHEET_CATALOG
98
from senaite.core.permissions import FieldEditBatch
99
from senaite.core.permissions import FieldEditClient
100
from senaite.core.permissions import FieldEditClientOrderNumber
101
from senaite.core.permissions import FieldEditClientReference
102
from senaite.core.permissions import FieldEditClientSampleID
103
from senaite.core.permissions import FieldEditComposite
104
from senaite.core.permissions import FieldEditContact
105
from senaite.core.permissions import FieldEditContainer
106
from senaite.core.permissions import FieldEditDatePreserved
107
from senaite.core.permissions import FieldEditDateReceived
108
from senaite.core.permissions import FieldEditDateSampled
109
from senaite.core.permissions import FieldEditEnvironmentalConditions
110
from senaite.core.permissions import FieldEditInternalUse
111
from senaite.core.permissions import FieldEditInvoiceExclude
112
from senaite.core.permissions import FieldEditMemberDiscount
113
from senaite.core.permissions import FieldEditPreservation
114
from senaite.core.permissions import FieldEditPreserver
115
from senaite.core.permissions import FieldEditPriority
116
from senaite.core.permissions import FieldEditProfiles
117
from senaite.core.permissions import FieldEditPublicationSpecifications
118
from senaite.core.permissions import FieldEditRejectionReasons
119
from senaite.core.permissions import FieldEditRemarks
120
from senaite.core.permissions import FieldEditResultsInterpretation
121
from senaite.core.permissions import FieldEditSampleCondition
122
from senaite.core.permissions import FieldEditSamplePoint
123
from senaite.core.permissions import FieldEditSampler
124
from senaite.core.permissions import FieldEditSampleType
125
from senaite.core.permissions import FieldEditSamplingDate
126
from senaite.core.permissions import FieldEditSamplingDeviation
127
from senaite.core.permissions import FieldEditScheduledSampler
128
from senaite.core.permissions import FieldEditSpecification
129
from senaite.core.permissions import FieldEditStorageLocation
130
from senaite.core.permissions import FieldEditTemplate
131
from senaite.core.permissions import ManageInvoices
132
from six.moves.urllib.parse import urljoin
133
from zope.interface import alsoProvides
134
from zope.interface import implements
135
from zope.interface import noLongerProvides
136
137
IMG_SRC_RX = re.compile(r'<img.*?src="(.*?)"')
138
IMG_DATA_SRC_RX = re.compile(r'<img.*?src="(data:image/.*?;base64,)(.*?)"')
139
FINAL_STATES = ["published", "retracted", "rejected", "cancelled"]
140
141
142
# SCHEMA DEFINITION
143
schema = BikaSchema.copy() + Schema((
144
145
    UIDReferenceField(
146
        "Contact",
147
        required=1,
148
        allowed_types=("Contact",),
149
        mode="rw",
150
        read_permission=View,
151
        write_permission=FieldEditContact,
152
        widget=ReferenceWidget(
153
            label=_(
154
                "label_sample_contact",
155
                default="Contact"),
156
            description=_(
157
                "description_sample_contact",
158
                default="Select the primary contact for this sample"),
159
            render_own_label=True,
160
            visible={
161
                "add": "edit",
162
                "header_table": "prominent",
163
            },
164
            ui_item="Title",
165
            catalog=CONTACT_CATALOG,
166
            # TODO: Make custom query to handle parent client UID
167
            query={
168
                "getParentUID": "",
169
                "is_active": True,
170
                "sort_on": "sortable_title",
171
                "sort_order": "ascending"
172
            },
173
            columns=[
174
                {"name": "Title", "label": _("Name")},
175
                {"name": "getEmailAddress", "label": _("Email")},
176
            ],
177
        ),
178
    ),
179
180
    UIDReferenceField(
181
        "CCContact",
182
        multiValued=1,
183
        allowed_types=("Contact",),
184
        mode="rw",
185
        read_permission=View,
186
        write_permission=FieldEditContact,
187
        widget=ReferenceWidget(
188
            label=_(
189
                "label_sample_cccontact",
190
                default="CC Contact"),
191
            description=_(
192
                "description_sample_cccontact",
193
                default="The contacts used in CC for email notifications"),
194
            render_own_label=True,
195
            visible={
196
                "add": "edit",
197
                "header_table": "prominent",
198
            },
199
            ui_item="Title",
200
            catalog=CONTACT_CATALOG,
201
            # TODO: Make custom query to handle parent client UID
202
            query={
203
                "getParentUID": "",
204
                "is_active": True,
205
                "sort_on": "sortable_title",
206
                "sort_order": "ascending"
207
            },
208
            columns=[
209
                {"name": "Title", "label": _("Name")},
210
                {"name": "getEmailAddress", "label": _("Email")},
211
            ],
212
        ),
213
    ),
214
215
    EmailsField(
216
        'CCEmails',
217
        mode="rw",
218
        read_permission=View,
219
        write_permission=FieldEditContact,
220
        widget=StringWidget(
221
            label=_("CC Emails"),
222
            description=_("Additional email addresses to be notified"),
223
            visible={
224
                'add': 'edit',
225
                'header_table': 'prominent',
226
            },
227
            render_own_label=True,
228
            size=20,
229
        ),
230
    ),
231
232
    UIDReferenceField(
233
        "Client",
234
        required=1,
235
        allowed_types=("Client",),
236
        mode="rw",
237
        read_permission=View,
238
        write_permission=FieldEditClient,
239
        widget=ReferenceWidget(
240
            label=_(
241
                "label_sample_client",
242
                default="Client"),
243
            description=_(
244
                "description_sample_client",
245
                default="Select the client for this sample"),
246
            render_own_label=True,
247
            visible={
248
                "add": "edit",
249
                "header_table": "prominent",
250
            },
251
            ui_item="getName",
252
            catalog=CLIENT_CATALOG,
253
            search_index="client_searchable_text",
254
            query={
255
                "is_active": True,
256
                "sort_on": "sortable_title",
257
                "sort_order": "ascending"
258
            },
259
            columns=[
260
                {"name": "getName", "label": _("Name")},
261
                {"name": "getClientID", "label": _("Client ID")},
262
            ],
263
        ),
264
    ),
265
266
    # Field for the creation of Secondary Analysis Requests.
267
    # This field is meant to be displayed in AR Add form only. A viewlet exists
268
    # to inform the user this Analysis Request is secondary
269
    UIDReferenceField(
270
        "PrimaryAnalysisRequest",
271
        allowed_types=("AnalysisRequest",),
272
        relationship='AnalysisRequestPrimaryAnalysisRequest',
273
        mode="rw",
274
        read_permission=View,
275
        write_permission=FieldEditClient,
276
        widget=ReferenceWidget(
277
            label=_(
278
                "label_sample_primary",
279
                default="Primary Sample"),
280
            description=_(
281
                "description_sample_primary",
282
                default="Select a sample to create a secondary Sample"),
283
            render_own_label=True,
284
            visible={
285
                "add": "edit",
286
                "header_table": "prominent",
287
            },
288
            catalog_name=SAMPLE_CATALOG,
289
            search_index="listing_searchable_text",
290
            query={
291
                "is_active": True,
292
                "is_received": True,
293
                "sort_on": "sortable_title",
294
                "sort_order": "ascending"
295
            },
296
            columns=[
297
                {"name": "getId", "label": _("Sample ID")},
298
                {"name": "getClientSampleID", "label": _("Client SID")},
299
                {"name": "getSampleTypeTitle", "label": _("Sample Type")},
300
                {"name": "getClientTitle", "label": _("Client")},
301
            ],
302
            ui_item="getId",
303
        )
304
    ),
305
306
    UIDReferenceField(
307
        "Batch",
308
        allowed_types=("Batch",),
309
        mode="rw",
310
        read_permission=View,
311
        write_permission=FieldEditBatch,
312
        widget=ReferenceWidget(
313
            label=_(
314
                "label_sample_batch",
315
                default="Batch"),
316
            description=_(
317
                "description_sample_batch",
318
                default="Assign sample to a batch"),
319
            render_own_label=True,
320
            visible={
321
                "add": "edit",
322
            },
323
            catalog_name=SENAITE_CATALOG,
324
            search_index="listing_searchable_text",
325
            query={
326
                "is_active": True,
327
                "sort_on": "sortable_title",
328
                "sort_order": "ascending"
329
            },
330
            columns=[
331
                {"name": "getId", "label": _("Batch ID")},
332
                {"name": "Title", "label": _("Title")},
333
                {"name": "getClientBatchID", "label": _("CBID")},
334
                {"name": "getClientTitle", "label": _("Client")},
335
            ],
336
            ui_item="getId",
337
        )
338
    ),
339
340
    UIDReferenceField(
341
        "SubGroup",
342
        required=False,
343
        allowed_types=("SubGroup",),
344
        mode="rw",
345
        read_permission=View,
346
        write_permission=FieldEditBatch,
347
        widget=ReferenceWidget(
348
            label=_(
349
                "label_sample_subgroup",
350
                default="Batch Sub-group"),
351
            description=_(
352
                "description_sample_subgroup",
353
                default="The assigned batch sub group of this request"),
354
            render_own_label=True,
355
            visible={
356
                "add": "edit",
357
            },
358
            catalog_name=SETUP_CATALOG,
359
            query={
360
                "is_active": True,
361
                "sort_on": "sortable_title",
362
                "sort_order": "ascending"
363
            },
364
            ui_item="Title",
365
        )
366
    ),
367
368
    UIDReferenceField(
369
        "Template",
370
        allowed_types=("ARTemplate",),
371
        mode="rw",
372
        read_permission=View,
373
        write_permission=FieldEditTemplate,
374
        widget=ReferenceWidget(
375
            label=_(
376
                "label_sample_template",
377
                default="Sample Template"),
378
            description=_(
379
                "description_sample_template",
380
                default="Select an analysis template for this sample"),
381
            render_own_label=True,
382
            visible={
383
                "add": "edit",
384
                "secondary": "disabled",
385
            },
386
            catalog=SETUP_CATALOG,
387
            query={
388
                "is_active": True,
389
                "sort_on": "sortable_title",
390
                "sort_order": "ascending"
391
            },
392
        ),
393
    ),
394
395
    UIDReferenceField(
396
        "Profiles",
397
        multiValued=1,
398
        allowed_types=("AnalysisProfile",),
399
        mode="rw",
400
        read_permission=View,
401
        write_permission=FieldEditProfiles,
402
        widget=ReferenceWidget(
403
            label=_(
404
                "label_sample_profiles",
405
                default="Analysis Profiles"),
406
            description=_(
407
                "description_sample_profiles",
408
                default="Select an analysis profile for this sample"),
409
            render_own_label=True,
410
            visible={
411
                "add": "edit",
412
            },
413
            catalog_name=SETUP_CATALOG,
414
            query={
415
                "is_active": True,
416
                "sort_on": "sortable_title",
417
                "sort_order": "ascending"
418
            },
419
            columns=[
420
                {"name": "Title", "label": _("Profile Name")},
421
                {"name": "getProfileKey", "label": _("Profile Key")},
422
            ],
423
        )
424
    ),
425
426
    # TODO Workflow - Request - Fix DateSampled inconsistencies. At the moment,
427
    # one can create an AR (with code) with DateSampled set when sampling_wf at
428
    # the same time sampling workflow is active. This might cause
429
    # inconsistencies: AR still in `to_be_sampled`, but getDateSampled returns
430
    # a valid date!
431
    DateTimeField(
432
        'DateSampled',
433
        mode="rw",
434
        max="getMaxDateSampled",
435
        read_permission=View,
436
        write_permission=FieldEditDateSampled,
437
        widget=DateTimeWidget(
438
            label=_(
439
                "label_sample_datesampled",
440
                default="Date Sampled"
441
            ),
442
            description=_(
443
                "description_sample_datesampled",
444
                default="The date when the sample was taken"
445
            ),
446
            size=20,
447
            show_time=True,
448
            visible={
449
                'add': 'edit',
450
                'secondary': 'disabled',
451
                'header_table': 'prominent',
452
            },
453
            render_own_label=True,
454
        ),
455
    ),
456
457
    StringField(
458
        'Sampler',
459
        mode="rw",
460
        read_permission=View,
461
        write_permission=FieldEditSampler,
462
        vocabulary='getSamplers',
463
        widget=BikaSelectionWidget(
464
            format='select',
465
            label=_("Sampler"),
466
            description=_("The person who took the sample"),
467
            # see SamplingWOrkflowWidgetVisibility
468
            visible={
469
                'add': 'edit',
470
                'header_table': 'prominent',
471
            },
472
            render_own_label=True,
473
        ),
474
    ),
475
476
    StringField(
477
        'ScheduledSamplingSampler',
478
        mode="rw",
479
        read_permission=View,
480
        write_permission=FieldEditScheduledSampler,
481
        vocabulary='getSamplers',
482
        widget=BikaSelectionWidget(
483
            description=_("Define the sampler supposed to do the sample in "
484
                          "the scheduled date"),
485
            format='select',
486
            label=_("Sampler for scheduled sampling"),
487
            visible={
488
                'add': 'edit',
489
            },
490
            render_own_label=True,
491
        ),
492
    ),
493
494
    DateTimeField(
495
        'SamplingDate',
496
        mode="rw",
497
        min="created",
498
        read_permission=View,
499
        write_permission=FieldEditSamplingDate,
500
        widget=DateTimeWidget(
501
            label=_(
502
                "label_sample_samplingdate",
503
                default="Expected Sampling Date"
504
            ),
505
            description=_(
506
                "description_sample_samplingdate",
507
                default="The date when the sample will be taken"
508
            ),
509
            size=20,
510
            show_time=True,
511
            render_own_label=True,
512
            visible={
513
                'add': 'edit',
514
                'secondary': 'disabled',
515
            },
516
        ),
517
    ),
518
519
    UIDReferenceField(
520
        "SampleType",
521
        required=1,
522
        allowed_types=("SampleType",),
523
        mode="rw",
524
        read_permission=View,
525
        write_permission=FieldEditSampleType,
526
        widget=ReferenceWidget(
527
            label=_(
528
                "label_sample_sampletype",
529
                default="Sample Type"),
530
            description=_(
531
                "description_sample_sampletype",
532
                default="Select the sample type of this sample"),
533
            render_own_label=True,
534
            visible={
535
                "add": "edit",
536
                "secondary": "disabled",
537
            },
538
            catalog=SETUP_CATALOG,
539
            query={
540
                "is_active": True,
541
                "sort_on": "sortable_title",
542
                "sort_order": "ascending"
543
            },
544
        ),
545
    ),
546
547
    UIDReferenceField(
548
        "Container",
549
        required=0,
550
        allowed_types=("SampleContainer",),
551
        mode="rw",
552
        read_permission=View,
553
        write_permission=FieldEditContainer,
554
        widget=ReferenceWidget(
555
            label=_(
556
                "label_sample_container",
557
                default="Container"),
558
            description=_(
559
                "description_sample_container",
560
                default="Select a container for this sample"),
561
            render_own_label=True,
562
            visible={
563
                "add": "edit",
564
            },
565
            catalog_name=SETUP_CATALOG,
566
            query={
567
                "is_active": True,
568
                "sort_on": "sortable_title",
569
                "sort_order": "ascending"
570
            },
571
            columns=[
572
                {"name": "Title", "label": _("Container")},
573
                {"name": "getCapacity", "label": _("Capacity")},
574
            ],
575
        )
576
    ),
577
578
    UIDReferenceField(
579
        "Preservation",
580
        required=0,
581
        allowed_types=("SamplePreservation",),
582
        mode="rw",
583
        read_permission=View,
584
        write_permission=FieldEditPreservation,
585
        widget=ReferenceWidget(
586
            label=_(
587
                "label_sample_preservation",
588
                default="Preservation"),
589
            description=_(
590
                "description_sample_preservation",
591
                default="Select the needed preservation for this sample"),
592
            render_own_label=True,
593
            visible={
594
                "add": "edit",
595
            },
596
            catalog_name=SETUP_CATALOG,
597
            query={
598
                "is_active": True,
599
                "sort_on": "sortable_title",
600
                "sort_order": "ascending"
601
            },
602
        )
603
    ),
604
605
    DateTimeField(
606
        "DatePreserved",
607
        mode="rw",
608
        read_permission=View,
609
        write_permission=FieldEditDatePreserved,
610
        widget=DateTimeWidget(
611
            label=_("Date Preserved"),
612
            description=_("The date when the sample was preserved"),
613
            size=20,
614
            show_time=True,
615
            render_own_label=True,
616
            visible={
617
                'add': 'edit',
618
                'header_table': 'prominent',
619
            },
620
        ),
621
    ),
622
623
    StringField(
624
        "Preserver",
625
        required=0,
626
        mode="rw",
627
        read_permission=View,
628
        write_permission=FieldEditPreserver,
629
        vocabulary='getPreservers',
630
        widget=BikaSelectionWidget(
631
            format='select',
632
            label=_("Preserver"),
633
            description=_("The person who preserved the sample"),
634
            visible={
635
                'add': 'edit',
636
                'header_table': 'prominent',
637
            },
638
            render_own_label=True,
639
        ),
640
    ),
641
642
    # TODO Sample cleanup - This comes from partition
643
    DurationField(
644
        "RetentionPeriod",
645
        required=0,
646
        mode="r",
647
        read_permission=View,
648
        widget=DurationWidget(
649
            label=_("Retention Period"),
650
            visible=False,
651
        ),
652
    ),
653
654
    RecordsField(
655
        'RejectionReasons',
656
        mode="rw",
657
        read_permission=View,
658
        write_permission=FieldEditRejectionReasons,
659
        widget=RejectionWidget(
660
            label=_("Sample Rejection"),
661
            description=_("Set the Sample Rejection workflow and the reasons"),
662
            render_own_label=False,
663
            visible={
664
                'add': 'edit',
665
                'secondary': 'disabled',
666
            },
667
        ),
668
    ),
669
670
    UIDReferenceField(
671
        "Specification",
672
        required=0,
673
        primary_bound=True,  # field changes propagate to partitions
674
        allowed_types=("AnalysisSpec",),
675
        mode="rw",
676
        read_permission=View,
677
        write_permission=FieldEditSpecification,
678
        widget=ReferenceWidget(
679
            label=_(
680
                "label_sample_specification",
681
                default="Analysis Specification"),
682
            description=_(
683
                "description_sample_specification",
684
                default="Select an analysis specification for this sample"),
685
            render_own_label=True,
686
            visible={
687
                "add": "edit",
688
            },
689
            catalog_name=SETUP_CATALOG,
690
            query={
691
                "is_active": True,
692
                "sort_on": "sortable_title",
693
                "sort_order": "ascending"
694
            },
695
            columns=[
696
                {"name": "Title", "label": _("Specification Name")},
697
                {"name": "getSampleTypeTitle", "label": _("Sample Type")},
698
            ],
699
        )
700
    ),
701
702
    # Field to keep the result ranges from the specification initially set
703
    # through "Specifications" field. This guarantees that the result ranges
704
    # set by default to this Sample won't change even if the Specifications
705
    # object referenced gets modified thereafter.
706
    # This field does not consider result ranges manually set to analyses.
707
    # Therefore, is also used to "detect" changes between the result ranges
708
    # specifically set to analyses and the results ranges set to the sample
709
    ResultsRangesField(
710
        "ResultsRange",
711
        write_permission=FieldEditSpecification,
712
        widget=ComputedWidget(visible=False),
713
    ),
714
715
    UIDReferenceField(
716
        "PublicationSpecification",
717
        required=0,
718
        allowed_types=("AnalysisSpec",),
719
        mode="rw",
720
        read_permission=View,
721
        write_permission=FieldEditPublicationSpecifications,
722
        widget=ReferenceWidget(
723
            label=_(
724
                "label_sample_publicationspecification",
725
                default="Publication Specification"),
726
            description=_(
727
                "description_sample_publicationspecification",
728
                default="Select an analysis specification that should be used "
729
                        "in the sample publication report"),
730
            render_own_label=True,
731
            visible={
732
                "add": "invisible",
733
                "secondary": "disabled",
734
            },
735
            catalog_name=SETUP_CATALOG,
736
            query={
737
                "is_active": True,
738
                "sort_on": "sortable_title",
739
                "sort_order": "ascending"
740
            },
741
            columns=[
742
                {"name": "Title", "label": _("Specification Name")},
743
                {"name": "getSampleTypeTitle", "label": _("Sample Type")},
744
            ],
745
        )
746
    ),
747
748
    # Sample field
749
    UIDReferenceField(
750
        "SamplePoint",
751
        allowed_types=("SamplePoint",),
752
        mode="rw",
753
        read_permission=View,
754
        write_permission=FieldEditSamplePoint,
755
        widget=ReferenceWidget(
756
            label=_(
757
                "label_sample_samplepoint",
758
                default="Sample Point"),
759
            description=_(
760
                "description_sample_samplepoint",
761
                default="Location where the sample was taken"),
762
            render_own_label=True,
763
            visible={
764
                "add": "edit",
765
                "secondary": "disabled",
766
            },
767
            catalog_name=SETUP_CATALOG,
768
            query={
769
                "is_active": True,
770
                "sort_on": "sortable_title",
771
                "sort_order": "ascending"
772
            },
773
        )
774
    ),
775
776
    # Remove in favor of senaite.storage?
777
    UIDReferenceField(
778
        "StorageLocation",
779
        allowed_types=("StorageLocation",),
780
        mode="rw",
781
        read_permission=View,
782
        write_permission=FieldEditStorageLocation,
783
        widget=ReferenceWidget(
784
            label=_(
785
                "label_sample_storagelocation",
786
                default="Storage Location"),
787
            description=_(
788
                "description_sample_storagelocation",
789
                default="Location where the sample is kept"),
790
            render_own_label=True,
791
            visible={
792
                "add": "edit",
793
                "secondary": "disabled",
794
            },
795
            catalog_name=SETUP_CATALOG,
796
            query={
797
                "is_active": True,
798
                "sort_on": "sortable_title",
799
                "sort_order": "ascending"
800
            },
801
        )
802
    ),
803
804
    StringField(
805
        'ClientOrderNumber',
806
        mode="rw",
807
        read_permission=View,
808
        write_permission=FieldEditClientOrderNumber,
809
        widget=StringWidget(
810
            label=_("Client Order Number"),
811
            description=_("The client side order number for this request"),
812
            size=20,
813
            render_own_label=True,
814
            visible={
815
                'add': 'edit',
816
                'secondary': 'disabled',
817
            },
818
        ),
819
    ),
820
821
    StringField(
822
        'ClientReference',
823
        mode="rw",
824
        read_permission=View,
825
        write_permission=FieldEditClientReference,
826
        widget=StringWidget(
827
            label=_("Client Reference"),
828
            description=_("The client side reference for this request"),
829
            size=20,
830
            render_own_label=True,
831
            visible={
832
                'add': 'edit',
833
                'secondary': 'disabled',
834
            },
835
        ),
836
    ),
837
838
    StringField(
839
        'ClientSampleID',
840
        mode="rw",
841
        read_permission=View,
842
        write_permission=FieldEditClientSampleID,
843
        widget=StringWidget(
844
            label=_("Client Sample ID"),
845
            description=_("The client side identifier of the sample"),
846
            size=20,
847
            render_own_label=True,
848
            visible={
849
                'add': 'edit',
850
                'secondary': 'disabled',
851
            },
852
        ),
853
    ),
854
855
    UIDReferenceField(
856
        "SamplingDeviation",
857
        allowed_types=("SamplingDeviation",),
858
        mode="rw",
859
        read_permission=View,
860
        write_permission=FieldEditSamplingDeviation,
861
        widget=ReferenceWidget(
862
            label=_(
863
                "label_sample_samplingdeviation",
864
                default="Sampling Deviation"),
865
            description=_(
866
                "description_sample_samplingdeviation",
867
                default="Deviation between the sample and how it "
868
                        "was sampled"),
869
            render_own_label=True,
870
            visible={
871
                "add": "edit",
872
                "secondary": "disabled",
873
            },
874
            catalog_name=SETUP_CATALOG,
875
            query={
876
                "is_active": True,
877
                "sort_on": "sortable_title",
878
                "sort_order": "ascending"
879
            },
880
        )
881
    ),
882
883
    UIDReferenceField(
884
        "SampleCondition",
885
        allowed_types=("SampleCondition",),
886
        mode="rw",
887
        read_permission=View,
888
        write_permission=FieldEditSampleCondition,
889
        widget=ReferenceWidget(
890
            label=_(
891
                "label_sample_samplecondition",
892
                default="Sample Condition"),
893
            description=_(
894
                "description_sample_samplecondition",
895
                default="The condition of the sample"),
896
            render_own_label=True,
897
            visible={
898
                "add": "edit",
899
                "secondary": "disabled",
900
            },
901
            catalog_name=SETUP_CATALOG,
902
            query={
903
                "is_active": True,
904
                "sort_on": "sortable_title",
905
                "sort_order": "ascending"
906
            },
907
        )
908
    ),
909
910
    StringField(
911
        'Priority',
912
        default='3',
913
        vocabulary=PRIORITIES,
914
        mode='rw',
915
        read_permission=View,
916
        write_permission=FieldEditPriority,
917
        widget=PrioritySelectionWidget(
918
            label=_('Priority'),
919
            format='select',
920
            visible={
921
                'add': 'edit',
922
            },
923
        ),
924
    ),
925
926
    StringField(
927
        'EnvironmentalConditions',
928
        mode="rw",
929
        read_permission=View,
930
        write_permission=FieldEditEnvironmentalConditions,
931
        widget=StringWidget(
932
            label=_("Environmental conditions"),
933
            description=_("The environmental condition during sampling"),
934
            visible={
935
                'add': 'edit',
936
                'header_table': 'prominent',
937
            },
938
            render_own_label=True,
939
            size=20,
940
        ),
941
    ),
942
943
    BooleanField(
944
        'Composite',
945
        default=False,
946
        mode="rw",
947
        read_permission=View,
948
        write_permission=FieldEditComposite,
949
        widget=BooleanWidget(
950
            label=_("Composite"),
951
            render_own_label=True,
952
            visible={
953
                'add': 'edit',
954
                'secondary': 'disabled',
955
            },
956
        ),
957
    ),
958
959
    BooleanField(
960
        'InvoiceExclude',
961
        default=False,
962
        mode="rw",
963
        read_permission=View,
964
        write_permission=FieldEditInvoiceExclude,
965
        widget=BooleanWidget(
966
            label=_("Invoice Exclude"),
967
            description=_("Should the analyses be excluded from the invoice?"),
968
            render_own_label=True,
969
            visible={
970
                'add': 'edit',
971
                'header_table': 'visible',
972
            },
973
        ),
974
    ),
975
976
    # TODO Review permission for this field Analyses
977
    ARAnalysesField(
978
        'Analyses',
979
        required=1,
980
        mode="rw",
981
        read_permission=View,
982
        write_permission=ModifyPortalContent,
983
        widget=ComputedWidget(
984
            visible={
985
                'edit': 'invisible',
986
                'view': 'invisible',
987
                'sample_registered': {
988
                    'view': 'visible', 'edit': 'visible', 'add': 'invisible'},
989
            }
990
        ),
991
    ),
992
993
    UIDReferenceField(
994
        'Attachment',
995
        multiValued=1,
996
        allowed_types=('Attachment',),
997
        relationship='AnalysisRequestAttachment',
998
        mode="rw",
999
        read_permission=View,
1000
        # The addition and removal of attachments is governed by the specific
1001
        # permissions "Add Sample Attachment" and "Delete Sample Attachment",
1002
        # so we assume here that the write permission is the less restrictive
1003
        # "ModifyPortalContent"
1004
        write_permission=ModifyPortalContent,
1005
        widget=ComputedWidget(
1006
            visible={
1007
                'edit': 'invisible',
1008
                'view': 'invisible',
1009
            },
1010
        )
1011
    ),
1012
1013
    # This is a virtual field and handled only by AR Add View to allow multi
1014
    # attachment upload in AR Add. It should never contain an own value!
1015
    FileField(
1016
        '_ARAttachment',
1017
        widget=FileWidget(
1018
            label=_("Attachment"),
1019
            description=_("Add one or more attachments to describe the "
1020
                          "sample in this sample, or to specify "
1021
                          "your request."),
1022
            render_own_label=True,
1023
            visible={
1024
                'view': 'invisible',
1025
                'add': 'edit',
1026
                'header_table': 'invisible',
1027
            },
1028
        )
1029
    ),
1030
1031
    # readonly field
1032
    UIDReferenceField(
1033
        "Invoice",
1034
        allowed_types=("Invoice",),
1035
        mode="rw",
1036
        read_permission=View,
1037
        write_permission=ModifyPortalContent,
1038
        widget=ReferenceWidget(
1039
            label=_(
1040
                "label_sample_invoice",
1041
                default="Invoice"),
1042
            description=_(
1043
                "description_sample_invoice",
1044
                default="Generated invoice for this sample"),
1045
            render_own_label=True,
1046
            readonly=True,
1047
            visible={
1048
                "add": "invisible",
1049
                "view": "visible",
1050
            },
1051
            catalog_name=SENAITE_CATALOG,
1052
            query={
1053
                "is_active": True,
1054
                "sort_on": "sortable_title",
1055
                "sort_order": "ascending"
1056
            },
1057
        )
1058
    ),
1059
1060
    DateTimeField(
1061
        'DateReceived',
1062
        mode="rw",
1063
        min="DateSampled",
1064
        max="current",
1065
        read_permission=View,
1066
        write_permission=FieldEditDateReceived,
1067
        widget=DateTimeWidget(
1068
            label=_("Date Sample Received"),
1069
            show_time=True,
1070
            description=_("The date when the sample was received"),
1071
            render_own_label=True,
1072
        ),
1073
    ),
1074
1075
    ComputedField(
1076
        'DatePublished',
1077
        mode="r",
1078
        read_permission=View,
1079
        expression="here.getDatePublished().strftime('%Y-%m-%d %H:%M %p') if here.getDatePublished() else ''",
1080
        widget=DateTimeWidget(
1081
            label=_("Date Published"),
1082
            visible={
1083
                'edit': 'invisible',
1084
                'add': 'invisible',
1085
                'secondary': 'invisible',
1086
            },
1087
        ),
1088
    ),
1089
1090
    RemarksField(
1091
        'Remarks',
1092
        read_permission=View,
1093
        write_permission=FieldEditRemarks,
1094
        widget=RemarksWidget(
1095
            label=_("Remarks"),
1096
            description=_("Remarks and comments for this request"),
1097
            render_own_label=True,
1098
            visible={
1099
                'add': 'edit',
1100
                'header_table': 'invisible',
1101
            },
1102
        ),
1103
    ),
1104
1105
    FixedPointField(
1106
        'MemberDiscount',
1107
        default_method='getDefaultMemberDiscount',
1108
        mode="rw",
1109
        read_permission=View,
1110
        write_permission=FieldEditMemberDiscount,
1111
        widget=DecimalWidget(
1112
            label=_("Member discount %"),
1113
            description=_("Enter percentage value eg. 33.0"),
1114
            render_own_label=True,
1115
            visible={
1116
                'add': 'invisible',
1117
            },
1118
        ),
1119
    ),
1120
1121
    ComputedField(
1122
        'SampleTypeTitle',
1123
        expression="here.getSampleType().Title() if here.getSampleType() "
1124
                   "else ''",
1125
        widget=ComputedWidget(
1126
            visible=False,
1127
        ),
1128
    ),
1129
1130
    ComputedField(
1131
        'SamplePointTitle',
1132
        expression="here.getSamplePoint().Title() if here.getSamplePoint() "
1133
                   "else ''",
1134
        widget=ComputedWidget(
1135
            visible=False,
1136
        ),
1137
    ),
1138
1139
    ComputedField(
1140
        'ContactUID',
1141
        expression="here.getContact() and here.getContact().UID() or ''",
1142
        widget=ComputedWidget(
1143
            visible=False,
1144
        ),
1145
    ),
1146
1147
    ComputedField(
1148
        'Invoiced',
1149
        expression='here.getInvoice() and True or False',
1150
        default=False,
1151
        widget=ComputedWidget(
1152
            visible=False,
1153
        ),
1154
    ),
1155
1156
    ComputedField(
1157
        'ReceivedBy',
1158
        expression='here.getReceivedBy()',
1159
        default='',
1160
        widget=ComputedWidget(visible=False,),
1161
    ),
1162
1163
    ComputedField(
1164
        'CreatorFullName',
1165
        expression="here._getCreatorFullName()",
1166
        widget=ComputedWidget(visible=False),
1167
    ),
1168
1169
    ComputedField(
1170
        'CreatorEmail',
1171
        expression="here._getCreatorEmail()",
1172
        widget=ComputedWidget(visible=False),
1173
    ),
1174
1175
    ComputedField(
1176
        'SamplerFullName',
1177
        expression="here._getSamplerFullName()",
1178
        widget=ComputedWidget(visible=False),
1179
    ),
1180
1181
    ComputedField(
1182
        'SamplerEmail',
1183
        expression="here._getSamplerEmail()",
1184
        widget=ComputedWidget(visible=False),
1185
    ),
1186
1187
    ComputedField(
1188
        'BatchID',
1189
        expression="here.getBatch().getId() if here.getBatch() else ''",
1190
        widget=ComputedWidget(visible=False),
1191
    ),
1192
1193
    ComputedField(
1194
        'BatchURL',
1195
        expression="here.getBatch().absolute_url_path() " \
1196
                   "if here.getBatch() else ''",
1197
        widget=ComputedWidget(visible=False),
1198
    ),
1199
1200
    ComputedField(
1201
        'ContactUsername',
1202
        expression="here.getContact().getUsername() " \
1203
                   "if here.getContact() else ''",
1204
        widget=ComputedWidget(visible=False),
1205
    ),
1206
1207
    ComputedField(
1208
        'ContactFullName',
1209
        expression="here.getContact().getFullname() " \
1210
                   "if here.getContact() else ''",
1211
        widget=ComputedWidget(visible=False),
1212
    ),
1213
1214
    ComputedField(
1215
        'ContactEmail',
1216
        expression="here.getContact().getEmailAddress() " \
1217
                   "if here.getContact() else ''",
1218
        widget=ComputedWidget(visible=False),
1219
    ),
1220
1221
    ComputedField(
1222
        'SampleTypeUID',
1223
        expression="here.getSampleType().UID() " \
1224
                   "if here.getSampleType() else ''",
1225
        widget=ComputedWidget(visible=False),
1226
    ),
1227
1228
    ComputedField(
1229
        'SamplePointUID',
1230
        expression="here.getSamplePoint().UID() " \
1231
                   "if here.getSamplePoint() else ''",
1232
        widget=ComputedWidget(visible=False),
1233
    ),
1234
    ComputedField(
1235
        'StorageLocationUID',
1236
        expression="here.getStorageLocation().UID() " \
1237
                   "if here.getStorageLocation() else ''",
1238
        widget=ComputedWidget(visible=False),
1239
    ),
1240
1241
    ComputedField(
1242
        'TemplateUID',
1243
        expression="here.getTemplate().UID() if here.getTemplate() else ''",
1244
        widget=ComputedWidget(visible=False),
1245
    ),
1246
1247
    ComputedField(
1248
        'TemplateURL',
1249
        expression="here.getTemplate().absolute_url_path() " \
1250
                   "if here.getTemplate() else ''",
1251
        widget=ComputedWidget(visible=False),
1252
    ),
1253
1254
    ComputedField(
1255
        'TemplateTitle',
1256
        expression="here.getTemplate().Title() if here.getTemplate() else ''",
1257
        widget=ComputedWidget(visible=False),
1258
    ),
1259
1260
    # readonly field
1261
    UIDReferenceField(
1262
        "ParentAnalysisRequest",
1263
        allowed_types=("AnalysisRequest",),
1264
        relationship="AnalysisRequestParentAnalysisRequest",
1265
        mode="rw",
1266
        read_permission=View,
1267
        write_permission=ModifyPortalContent,
1268
        widget=ReferenceWidget(
1269
            label=_(
1270
                "label_sample_parent_sample",
1271
                default="Parent sample"),
1272
            description=_(
1273
                "description_sample_parent_sample",
1274
                default="Reference to parent sample"),
1275
            render_own_label=True,
1276
            readonly=True,
1277
            visible=False,
1278
            catalog_name=SAMPLE_CATALOG,
1279
            query={
1280
                "is_active": True,
1281
                "sort_on": "sortable_title",
1282
                "sort_order": "ascending"
1283
            },
1284
        )
1285
    ),
1286
1287
    # The Primary Sample the current sample was detached from
1288
    UIDReferenceField(
1289
        "DetachedFrom",
1290
        allowed_types=("AnalysisRequest",),
1291
        mode="rw",
1292
        read_permission=View,
1293
        write_permission=ModifyPortalContent,
1294
        widget=ReferenceWidget(
1295
            label=_(
1296
                "label_sample_detached_from",
1297
                default="Detached from sample"),
1298
            description=_(
1299
                "description_sample_detached_from",
1300
                default="Reference to detached sample"),
1301
            render_own_label=True,
1302
            readonly=True,
1303
            visible=False,
1304
            catalog_name=SAMPLE_CATALOG,
1305
            query={
1306
                "is_active": True,
1307
                "sort_on": "sortable_title",
1308
                "sort_order": "ascending"
1309
            },
1310
        )
1311
    ),
1312
1313
    # The Analysis Request the current Analysis Request comes from because of
1314
    # an invalidation of the former
1315
    UIDReferenceField(
1316
        "Invalidated",
1317
        allowed_types=("AnalysisRequest",),
1318
        relationship="AnalysisRequestRetracted",
1319
        mode="rw",
1320
        read_permission=View,
1321
        write_permission=ModifyPortalContent,
1322
        widget=ReferenceWidget(
1323
            label=_(
1324
                "label_sample_retracted",
1325
                default="Retest from sample"),
1326
            description=_(
1327
                "description_sample_retracted",
1328
                default="Reference to retracted sample"),
1329
            render_own_label=True,
1330
            readonly=True,
1331
            visible=False,
1332
            catalog_name=SAMPLE_CATALOG,
1333
            query={
1334
                "is_active": True,
1335
                "sort_on": "sortable_title",
1336
                "sort_order": "ascending"
1337
            },
1338
        )
1339
    ),
1340
1341
    # For comments or results interpretation
1342
    # Old one, to be removed because of the incorporation of
1343
    # ResultsInterpretationDepts (due to LIMS-1628)
1344
    TextField(
1345
        'ResultsInterpretation',
1346
        mode="rw",
1347
        default_content_type='text/html',
1348
        # Input content type for the textfield
1349
        default_output_type='text/x-html-safe',
1350
        # getResultsInterpretation returns a str with html tags
1351
        # to conserve the txt format in the report.
1352
        read_permission=View,
1353
        write_permission=FieldEditResultsInterpretation,
1354
        widget=RichWidget(
1355
            description=_("Comments or results interpretation"),
1356
            label=_("Results Interpretation"),
1357
            size=10,
1358
            allow_file_upload=False,
1359
            default_mime_type='text/x-rst',
1360
            output_mime_type='text/x-html',
1361
            rows=3,
1362
            visible=False),
1363
    ),
1364
1365
    RecordsField(
1366
        'ResultsInterpretationDepts',
1367
        read_permission=View,
1368
        write_permission=FieldEditResultsInterpretation,
1369
        subfields=('uid', 'richtext'),
1370
        subfield_labels={
1371
            'uid': _('Department'),
1372
            'richtext': _('Results Interpretation')},
1373
        widget=RichWidget(visible=False),
1374
     ),
1375
    # Custom settings for the assigned analysis services
1376
    # https://jira.bikalabs.com/browse/LIMS-1324
1377
    # Fields:
1378
    #   - uid: Analysis Service UID
1379
    #   - hidden: True/False. Hide/Display in results reports
1380
    RecordsField('AnalysisServicesSettings',
1381
                 required=0,
1382
                 subfields=('uid', 'hidden',),
1383
                 widget=ComputedWidget(visible=False),
1384
                 ),
1385
    StringField(
1386
        'Printed',
1387
        mode="rw",
1388
        read_permission=View,
1389
        widget=StringWidget(
1390
            label=_("Printed"),
1391
            description=_("Indicates if the last SampleReport is printed,"),
1392
            visible=False,
1393
        ),
1394
    ),
1395
    BooleanField(
1396
        "InternalUse",
1397
        mode="rw",
1398
        required=0,
1399
        default=False,
1400
        read_permission=View,
1401
        write_permission=FieldEditInternalUse,
1402
        widget=BooleanWidget(
1403
            label=_("Internal use"),
1404
            description=_("Mark the sample for internal use only. This means "
1405
                          "it is only accessible to lab personnel and not to "
1406
                          "clients."),
1407
            format="radio",
1408
            render_own_label=True,
1409
            visible={'add': 'edit'}
1410
        ),
1411
    ),
1412
1413
    # Initial conditions for analyses set on Sample registration
1414
    RecordsField(
1415
        "ServiceConditions",
1416
        widget=ComputedWidget(visible=False)
1417
    ),
1418
1419
    # Number of samples to create on add form
1420
    IntegerField(
1421
        "NumSamples",
1422
        default=1,
1423
        widget=IntegerWidget(
1424
            label=_(
1425
                u"label_analysisrequest_numsamples",
1426
                default=u"Number of samples"
1427
            ),
1428
            description=_(
1429
                u"description_analysisrequest_numsamples",
1430
                default=u"Number of samples to create with the information "
1431
                        u"provided"),
1432
            # This field is only visible in add sample form
1433
            visible={
1434
                "add": "edit",
1435
                "view": "invisible",
1436
                "header_table": "invisible",
1437
                "secondary": "invisible",
1438
            },
1439
            render_own_label=True,
1440
        ),
1441
    ),
1442
))
1443
1444
1445
# Some schema rearrangement
1446
schema['title'].required = False
1447
schema['id'].widget.visible = False
1448
schema['title'].widget.visible = False
1449
schema.moveField('Client', before='Contact')
1450
schema.moveField('ResultsInterpretation', pos='bottom')
1451
schema.moveField('ResultsInterpretationDepts', pos='bottom')
1452
schema.moveField("PrimaryAnalysisRequest", before="Client")
1453
1454
1455
class AnalysisRequest(BaseFolder, ClientAwareMixin):
1456
    implements(IAnalysisRequest, ICancellable)
1457
    security = ClassSecurityInfo()
1458
    displayContentsTab = False
1459
    schema = schema
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable schema does not seem to be defined.
Loading history...
1460
1461
    def _getCatalogTool(self):
1462
        from bika.lims.catalog import getCatalog
1463
        return getCatalog(self)
1464
1465
    def Title(self):
1466
        """ Return the Request ID as title """
1467
        return self.getId()
1468
1469
    def sortable_title(self):
1470
        """
1471
        Some lists expects this index
1472
        """
1473
        return self.getId()
1474
1475
    def Description(self):
1476
        """Returns searchable data as Description"""
1477
        descr = " ".join((self.getId(), self.aq_parent.Title()))
1478
        return safe_unicode(descr).encode('utf-8')
1479
1480
    def setSpecification(self, value):
1481
        """Sets the Specifications and ResultRange values
1482
        """
1483
        current_spec = self.getRawSpecification()
1484
        if value and current_spec == api.get_uid(value):
1485
            # Specification has not changed, preserve the current value to
1486
            # prevent result ranges (both from Sample and from analyses) from
1487
            # being overriden
1488
            return
1489
1490
        self.getField("Specification").set(self, value)
1491
1492
        # Set the value for field ResultsRange, cause Specification is only
1493
        # used as a template: all the results range logic relies on
1494
        # ResultsRange field, so changes in setup's Specification object won't
1495
        # have effect to already created samples
1496
        spec = self.getSpecification()
1497
        if spec:
1498
            # Update only results ranges if specs is not None, so results
1499
            # ranges manually set previously (e.g. via ManageAnalyses view) are
1500
            # preserved unless a new Specification overrides them
1501
            self.setResultsRange(spec.getResultsRange(), recursive=False)
1502
1503
        # Cascade the changes to partitions, but only to those that are in a
1504
        # status in which the specification can be updated. This prevents the
1505
        # re-assignment of Specifications to already verified or published
1506
        # samples
1507
        permission = self.getField("Specification").write_permission
1508
        for descendant in self.getDescendants():
1509
            if check_permission(permission, descendant):
1510
                descendant.setSpecification(spec)
1511
1512
    def setResultsRange(self, value, recursive=True):
1513
        """Sets the results range for this Sample and analyses it contains.
1514
        If recursive is True, then applies the results ranges to descendants
1515
        (partitions) as well as their analyses too
1516
        """
1517
        # Set Results Range to the Sample
1518
        field = self.getField("ResultsRange")
1519
        field.set(self, value)
1520
1521
        # Set Results Range to analyses
1522
        for analysis in self.objectValues("Analysis"):
1523
            if not ISubmitted.providedBy(analysis):
1524
                service_uid = analysis.getRawAnalysisService()
1525
                result_range = field.get(self, search_by=service_uid)
1526
                analysis.setResultsRange(result_range)
1527
                analysis.reindexObject()
1528
1529
        if recursive:
1530
            # Cascade the changes to partitions
1531
            permission = self.getField("Specification").write_permission
1532
            for descendant in self.getDescendants():
1533
                if check_permission(permission, descendant):
1534
                    descendant.setResultsRange(value)
1535
1536
    def setProfiles(self, value):
1537
        """Set Analysis Profiles to the Sample
1538
        """
1539
        if not isinstance(value, (list, tuple)):
1540
            value = [value]
1541
        # filter out empties
1542
        value = filter(None, value)
1543
        # ensure we have UIDs
1544
        uids = map(api.get_uid, value)
1545
        # get the current set profiles
1546
        current_profiles = self.getRawProfiles()
1547
        # return immediately if nothing changed
1548
        if current_profiles == uids:
1549
            return
1550
1551
        # Don't add analyses from profiles during sample creation.
1552
        # In this case the required analyses are added afterwards explicitly.
1553
        if not api.is_temporary(self):
1554
            # get the profiles
1555
            profiles = map(api.get_object_by_uid, uids)
1556
            # get the current set of analyses/services
1557
            analyses = self.getAnalyses(full_objects=True)
1558
            services = map(lambda an: an.getAnalysisService(), analyses)
1559
            # determine all the services to add
1560
            services_to_add = set(services)
1561
            for profile in profiles:
1562
                services_to_add.update(profile.getServices())
1563
            # set all analyses
1564
            self.setAnalyses(list(services_to_add))
1565
1566
        # set the profiles value
1567
        self.getField("Profiles").set(self, value)
1568
1569
        # apply hidden services *after* the profiles have been set
1570
        apply_hidden_services(self)
1571
1572
    def getClient(self):
1573
        """Returns the client this object is bound to. We override getClient
1574
        from ClientAwareMixin because the "Client" schema field is only used to
1575
        allow the user to set the client while creating the Sample through
1576
        Sample Add form, but cannot be changed afterwards. The Sample is
1577
        created directly inside the selected client folder on submit
1578
        """
1579
        parent = self.aq_parent
1580
        if IClient.providedBy(parent):
1581
            return parent
1582
        elif IBatch.providedBy(parent):
1583
            return parent.getClient()
1584
        # Fallback to UID reference field value
1585
        field = self.getField("Client")
1586
        return field.get(self)
1587
1588
    @deprecated("Will be removed in SENAITE 3.0")
1589
    def getProfilesURL(self):
1590
        """Returns a list of all profile URLs
1591
1592
        Backwards compatibility for removed computed field:
1593
        https://github.com/senaite/senaite.core/pull/2213
1594
        """
1595
        return [profile.absolute_url_path() for profile in self.getProfiles()]
1596
1597
    @deprecated("Please use getRawProfiles instead. Will be removed in SENAITE 3.0")
1598
    def getProfilesUID(self):
1599
        """Returns a list of all profile UIDs
1600
1601
        Backwards compatibility for removed computed field:
1602
        https://github.com/senaite/senaite.core/pull/2213
1603
        """
1604
        return self.getRawProfiles()
1605
1606
    def getProfilesTitle(self):
1607
        """Returns a list of all profile titles
1608
1609
        Backwards compatibility for removed computed field:
1610
        https://github.com/senaite/senaite.core/pull/2213
1611
        """
1612
        return [profile.Title() for profile in self.getProfiles()]
1613
1614
    def getProfilesTitleStr(self, separator=", "):
1615
        """Returns a comma-separated string withg the titles of the profiles
1616
        assigned to this Sample. Used to populate a metadata field
1617
        """
1618
        return separator.join(self.getProfilesTitle())
1619
1620
    def getAnalysisService(self):
1621
        proxies = self.getAnalyses(full_objects=False)
1622
        value = set()
1623
        for proxy in proxies:
1624
            value.add(proxy.Title)
1625
        return list(value)
1626
1627
    def getAnalysts(self):
1628
        proxies = self.getAnalyses(full_objects=True)
1629
        value = []
1630
        for proxy in proxies:
1631
            val = proxy.getAnalyst()
1632
            if val not in value:
1633
                value.append(val)
1634
        return value
1635
1636
    def getDistrict(self):
1637
        client = self.aq_parent
1638
        return client.getDistrict()
1639
1640
    def getProvince(self):
1641
        client = self.aq_parent
1642
        return client.getProvince()
1643
1644
    @security.public
1645
    def getBatch(self):
1646
        # The parent type may be "Batch" during ar_add.
1647
        # This function fills the hidden field in ar_add.pt
1648
        if self.aq_parent.portal_type == 'Batch':
1649
            return self.aq_parent
1650
        else:
1651
            return self.Schema()['Batch'].get(self)
1652
1653
    @security.public
1654
    def getBatchUID(self):
1655
        batch = self.getBatch()
1656
        if batch:
1657
            return batch.UID()
1658
1659
    @security.public
1660
    def setBatch(self, value=None):
1661
        original_value = self.Schema().getField('Batch').get(self)
1662
        if original_value != value:
1663
            self.Schema().getField('Batch').set(self, value)
1664
1665
    def getDefaultMemberDiscount(self):
1666
        """Compute default member discount if it applies
1667
        """
1668
        if hasattr(self, 'getMemberDiscountApplies'):
1669
            if self.getMemberDiscountApplies():
1670
                settings = self.bika_setup
1671
                return settings.getMemberDiscount()
1672
            else:
1673
                return "0.00"
1674
1675
    @security.public
1676
    def getAnalysesNum(self):
1677
        """ Returns an array with the number of analyses for the current AR in
1678
            different statuses, like follows:
1679
                [verified, total, not_submitted, to_be_verified]
1680
        """
1681
        an_nums = [0, 0, 0, 0]
1682
        for analysis in self.getAnalyses():
1683
            review_state = analysis.review_state
1684
            if review_state in ['retracted', 'rejected', 'cancelled']:
1685
                continue
1686
            if review_state == 'to_be_verified':
1687
                an_nums[3] += 1
1688
            elif review_state in ['published', 'verified']:
1689
                an_nums[0] += 1
1690
            else:
1691
                an_nums[2] += 1
1692
            an_nums[1] += 1
1693
        return an_nums
1694
1695
    @security.public
1696
    def getResponsible(self):
1697
        """Return all manager info of responsible departments
1698
        """
1699
        managers = {}
1700
        for department in self.getDepartments():
1701
            manager = department.getManager()
1702
            if manager is None:
1703
                continue
1704
            manager_id = manager.getId()
1705
            if manager_id not in managers:
1706
                managers[manager_id] = {}
1707
                managers[manager_id]['salutation'] = safe_unicode(
1708
                    manager.getSalutation())
1709
                managers[manager_id]['name'] = safe_unicode(
1710
                    manager.getFullname())
1711
                managers[manager_id]['email'] = safe_unicode(
1712
                    manager.getEmailAddress())
1713
                managers[manager_id]['phone'] = safe_unicode(
1714
                    manager.getBusinessPhone())
1715
                managers[manager_id]['job_title'] = safe_unicode(
1716
                    manager.getJobTitle())
1717
                if manager.getSignature():
1718
                    managers[manager_id]['signature'] = \
1719
                        '{}/Signature'.format(manager.absolute_url())
1720
                else:
1721
                    managers[manager_id]['signature'] = False
1722
                managers[manager_id]['departments'] = ''
1723
            mngr_dept = managers[manager_id]['departments']
1724
            if mngr_dept:
1725
                mngr_dept += ', '
1726
            mngr_dept += safe_unicode(department.Title())
1727
            managers[manager_id]['departments'] = mngr_dept
1728
        mngr_keys = managers.keys()
1729
        mngr_info = {'ids': mngr_keys, 'dict': managers}
1730
1731
        return mngr_info
1732
1733
    @security.public
1734
    def getManagers(self):
1735
        """Return all managers of responsible departments
1736
        """
1737
        manager_ids = []
1738
        manager_list = []
1739
        for department in self.getDepartments():
1740
            manager = department.getManager()
1741
            if manager is None:
1742
                continue
1743
            manager_id = manager.getId()
1744
            if manager_id not in manager_ids:
1745
                manager_ids.append(manager_id)
1746
                manager_list.append(manager)
1747
        return manager_list
1748
1749
    def getDueDate(self):
1750
        """Returns the earliest due date of the analyses this Analysis Request
1751
        contains."""
1752
        due_dates = map(lambda an: an.getDueDate, self.getAnalyses())
1753
        return due_dates and min(due_dates) or None
1754
1755
    security.declareProtected(View, 'getLate')
1756
1757
    def getLate(self):
1758
        """Return True if there is at least one late analysis in this Request
1759
        """
1760
        for analysis in self.getAnalyses():
1761
            if analysis.review_state == "retracted":
1762
                continue
1763
            analysis_obj = api.get_object(analysis)
1764
            if analysis_obj.isLateAnalysis():
1765
                return True
1766
        return False
1767
1768
    def getRawReports(self):
1769
        """Returns UIDs of reports with a reference to this sample
1770
1771
        see: ARReport.ContainedAnalysisRequests field
1772
1773
        :returns: List of report UIDs
1774
        """
1775
        return get_backreferences(self, "ARReportAnalysisRequest")
1776
1777
    def getReports(self):
1778
        """Returns a list of report objects
1779
1780
        :returns: List of report objects
1781
        """
1782
        return list(map(api.get_object, self.getRawReports()))
1783
1784
    def getPrinted(self):
1785
        """ returns "0", "1" or "2" to indicate Printed state.
1786
            0 -> Never printed.
1787
            1 -> Printed after last publish
1788
            2 -> Printed but republished afterwards.
1789
        """
1790
        if not self.getDatePublished():
1791
            return "0"
1792
1793
        report_uids = self.getRawReports()
1794
        if not report_uids:
1795
            return "0"
1796
1797
        last_report = api.get_object(report_uids[-1])
1798
        if last_report.getDatePrinted():
1799
            return "1"
1800
1801
        for report_uid in report_uids[:-1]:
1802
            report = api.get_object(report_uid)
1803
            if report.getDatePrinted():
1804
                return "2"
1805
1806
        return "0"
1807
1808
    @security.protected(View)
1809
    def getBillableItems(self):
1810
        """Returns the items to be billed
1811
        """
1812
        # Assigned profiles
1813
        profiles = self.getProfiles()
1814
        # Billable profiles which have a fixed price set
1815
        billable_profiles = filter(
1816
            lambda pr: pr.getUseAnalysisProfilePrice(), profiles)
1817
        # All services contained in the billable profiles
1818
        billable_profile_services = functools.reduce(lambda a, b: a+b, map(
1819
            lambda profile: profile.getServices(), billable_profiles), [])
1820
        # Keywords of the contained services
1821
        billable_service_keys = map(
1822
            lambda s: s.getKeyword(), set(billable_profile_services))
1823
        # Billable items contain billable profiles and single selected analyses
1824
        billable_items = billable_profiles
1825
        # Get the analyses to be billed
1826
        exclude_rs = ["retracted", "rejected"]
1827
        for analysis in self.getAnalyses(is_active=True):
1828
            if analysis.review_state in exclude_rs:
1829
                continue
1830
            if analysis.getKeyword in billable_service_keys:
1831
                continue
1832
            billable_items.append(api.get_object(analysis))
1833
        return billable_items
1834
1835
    @security.protected(View)
1836
    def getSubtotal(self):
1837
        """Compute Subtotal (without member discount and without vat)
1838
        """
1839
        return sum([Decimal(obj.getPrice()) for obj in self.getBillableItems()])
1840
1841
    @security.protected(View)
1842
    def getSubtotalVATAmount(self):
1843
        """Compute VAT amount without member discount
1844
        """
1845
        return sum([Decimal(o.getVATAmount()) for o in self.getBillableItems()])
1846
1847
    @security.protected(View)
1848
    def getSubtotalTotalPrice(self):
1849
        """Compute the price with VAT but no member discount
1850
        """
1851
        return self.getSubtotal() + self.getSubtotalVATAmount()
1852
1853
    @security.protected(View)
1854
    def getDiscountAmount(self):
1855
        """It computes and returns the analysis service's discount amount
1856
        without VAT
1857
        """
1858
        has_client_discount = self.aq_parent.getMemberDiscountApplies()
1859
        if has_client_discount:
1860
            discount = Decimal(self.getDefaultMemberDiscount())
1861
            return Decimal(self.getSubtotal() * discount / 100)
1862
        else:
1863
            return 0
1864
1865
    @security.protected(View)
1866
    def getVATAmount(self):
1867
        """It computes the VAT amount from (subtotal-discount.)*VAT/100, but
1868
        each analysis has its own VAT!
1869
1870
        :returns: the analysis request VAT amount with the discount
1871
        """
1872
        has_client_discount = self.aq_parent.getMemberDiscountApplies()
1873
        VATAmount = self.getSubtotalVATAmount()
1874
        if has_client_discount:
1875
            discount = Decimal(self.getDefaultMemberDiscount())
1876
            return Decimal((1 - discount / 100) * VATAmount)
1877
        else:
1878
            return VATAmount
1879
1880
    @security.protected(View)
1881
    def getTotalPrice(self):
1882
        """It gets the discounted price from analyses and profiles to obtain the
1883
        total value with the VAT and the discount applied
1884
1885
        :returns: analysis request's total price including VATs and discounts
1886
        """
1887
        price = (self.getSubtotal() - self.getDiscountAmount() +
1888
                 self.getVATAmount())
1889
        return price
1890
1891
    getTotal = getTotalPrice
1892
1893
    @security.protected(ManageInvoices)
1894
    def createInvoice(self, pdf):
1895
        """Issue invoice
1896
        """
1897
        client = self.getClient()
1898
        invoice = self.getInvoice()
1899
        if not invoice:
1900
            invoice = _createObjectByType("Invoice", client, tmpID())
1901
        invoice.edit(
1902
            AnalysisRequest=self,
1903
            Client=client,
1904
            InvoiceDate=DateTime(),
1905
            InvoicePDF=pdf
1906
        )
1907
        invoice.processForm()
1908
        self.setInvoice(invoice)
1909
        return invoice
1910
1911
    @security.public
1912
    def printInvoice(self, REQUEST=None, RESPONSE=None):
1913
        """Print invoice
1914
        """
1915
        invoice = self.getInvoice()
1916
        invoice_url = invoice.absolute_url()
1917
        RESPONSE.redirect('{}/invoice_print'.format(invoice_url))
1918
1919
    @deprecated("Use getVerifiers instead. Will be removed in SENAITE 3.0")
1920
    @security.public
1921
    def getVerifier(self):
1922
        """Returns the user that verified the whole Analysis Request. Since the
1923
        verification is done automatically as soon as all the analyses it
1924
        contains are verified, this function returns the user that verified the
1925
        last analysis pending.
1926
        """
1927
        wtool = getToolByName(self, 'portal_workflow')
1928
        mtool = getToolByName(self, 'portal_membership')
1929
1930
        verifier = None
1931
        try:
1932
            review_history = wtool.getInfoFor(self, 'review_history')
1933
        except Exception:
1934
            return 'access denied'
1935
1936
        if not review_history:
1937
            return 'no history'
1938
        for items in review_history:
1939
            action = items.get('action')
1940
            if action != 'verify':
1941
                continue
1942
            actor = items.get('actor')
1943
            member = mtool.getMemberById(actor)
1944
            verifier = member.getProperty('fullname')
1945
            if verifier is None or verifier == '':
1946
                verifier = actor
1947
        return verifier
1948
1949
    @security.public
1950
    def getVerifiersIDs(self):
1951
        """Returns the ids from users that have verified at least one analysis
1952
        from this Analysis Request
1953
        """
1954
        verifiers_ids = list()
1955
        for brain in self.getAnalyses():
1956
            verifiers_ids += brain.getVerificators
1957
        return list(set(verifiers_ids))
1958
1959
    @security.public
1960
    def getVerifiers(self):
1961
        """Returns the list of lab contacts that have verified at least one
1962
        analysis from this Analysis Request
1963
        """
1964
        contacts = list()
1965
        for verifier in self.getVerifiersIDs():
1966
            user = api.get_user(verifier)
1967
            contact = api.get_user_contact(user, ["LabContact"])
1968
            if contact:
1969
                contacts.append(contact)
1970
        return contacts
1971
1972
    security.declarePublic('current_date')
1973
1974
    def current_date(self):
1975
        """return current date
1976
        """
1977
        # noinspection PyCallingNonCallable
1978
        return DateTime()
1979
1980
    def getWorksheets(self, full_objects=False):
1981
        """Returns the worksheets that contains analyses from this Sample
1982
        """
1983
        # Get the Analyses UIDs of this Sample
1984
        analyses_uids = map(api.get_uid, self.getAnalyses())
1985
        if not analyses_uids:
1986
            return []
1987
1988
        # Get the worksheets that contain any of these analyses
1989
        query = dict(getAnalysesUIDs=analyses_uids)
1990
        worksheets = api.search(query, WORKSHEET_CATALOG)
1991
        if full_objects:
1992
            worksheets = map(api.get_object, worksheets)
1993
        return worksheets
1994
1995
    def getQCAnalyses(self, review_state=None):
1996
        """Returns the Quality Control analyses assigned to worksheets that
1997
        contains analyses from this Sample
1998
        """
1999
        # Get the worksheet uids
2000
        worksheet_uids = map(api.get_uid, self.getWorksheets())
2001
        if not worksheet_uids:
2002
            return []
2003
2004
        # Get reference qc analyses from these worksheets
2005
        query = dict(portal_type="ReferenceAnalysis",
2006
                     getWorksheetUID=worksheet_uids)
2007
        qc_analyses = api.search(query, ANALYSIS_CATALOG)
2008
2009
        # Extend with duplicate qc analyses from these worksheets and Sample
2010
        query = dict(portal_type="DuplicateAnalysis",
2011
                     getWorksheetUID=worksheet_uids,
2012
                     getAncestorsUIDs=[api.get_uid(self)])
2013
        qc_analyses += api.search(query, ANALYSIS_CATALOG)
2014
2015
        # Bail out analyses with a different review_state
2016
        if review_state:
2017
            qc_analyses = filter(
2018
                lambda an: api.get_review_status(an) in review_state,
2019
                qc_analyses
2020
            )
2021
2022
        # Return the objects
2023
        return map(api.get_object, qc_analyses)
2024
2025
    def isInvalid(self):
2026
        """return if the Analysis Request has been invalidated
2027
        """
2028
        workflow = getToolByName(self, 'portal_workflow')
2029
        return workflow.getInfoFor(self, 'review_state') == 'invalid'
2030
2031
    def getStorageLocationTitle(self):
2032
        """ A method for AR listing catalog metadata
2033
        :return: Title of Storage Location
2034
        """
2035
        sl = self.getStorageLocation()
2036
        if sl:
2037
            return sl.Title()
2038
        return ''
2039
2040
    def getDatePublished(self):
2041
        """
2042
        Returns the transition date from the Analysis Request object
2043
        """
2044
        return getTransitionDate(self, 'publish', return_as_datetime=True)
2045
2046
    @security.public
2047
    def getSamplingDeviationTitle(self):
2048
        """
2049
        It works as a metacolumn.
2050
        """
2051
        sd = self.getSamplingDeviation()
2052
        if sd:
2053
            return sd.Title()
2054
        return ''
2055
2056
    @security.public
2057
    def getSampleConditionTitle(self):
2058
        """Helper method to access the title of the sample condition
2059
        """
2060
        obj = self.getSampleCondition()
2061
        if not obj:
2062
            return ""
2063
        return api.get_title(obj)
2064
2065
    @security.public
2066
    def getHazardous(self):
2067
        """
2068
        It works as a metacolumn.
2069
        """
2070
        sample_type = self.getSampleType()
2071
        if sample_type:
2072
            return sample_type.getHazardous()
2073
        return False
2074
2075
    @security.public
2076
    def getContactURL(self):
2077
        """
2078
        It works as a metacolumn.
2079
        """
2080
        contact = self.getContact()
2081
        if contact:
2082
            return contact.absolute_url_path()
2083
        return ''
2084
2085
    @security.public
2086
    def getSamplingWorkflowEnabled(self):
2087
        """Returns True if the sample of this Analysis Request has to be
2088
        collected by the laboratory personnel
2089
        """
2090
        template = self.getTemplate()
2091
        if template:
2092
            return template.getSamplingRequired()
2093
        return self.bika_setup.getSamplingWorkflowEnabled()
2094
2095
    def getSamplers(self):
2096
        return getUsers(self, ['Sampler', ])
2097
2098
    def getPreservers(self):
2099
        return getUsers(self, ['Preserver', 'Sampler'])
2100
2101
    def getDepartments(self):
2102
        """Returns a list of the departments assigned to the Analyses
2103
        from this Analysis Request
2104
        """
2105
        departments = list()
2106
        for analysis in self.getAnalyses(full_objects=True):
2107
            department = analysis.getDepartment()
2108
            if department and department not in departments:
2109
                departments.append(department)
2110
        return departments
2111
2112
    def getResultsInterpretationByDepartment(self, department=None):
2113
        """Returns the results interpretation for this Analysis Request
2114
           and department. If department not set, returns the results
2115
           interpretation tagged as 'General'.
2116
2117
        :returns: a dict with the following keys:
2118
            {'uid': <department_uid> or 'general', 'richtext': <text/plain>}
2119
        """
2120
        uid = department.UID() if department else 'general'
2121
        rows = self.Schema()['ResultsInterpretationDepts'].get(self)
2122
        row = [row for row in rows if row.get('uid') == uid]
2123
        if len(row) > 0:
2124
            row = row[0]
2125
        elif uid == 'general' \
2126
                and hasattr(self, 'getResultsInterpretation') \
2127
                and self.getResultsInterpretation():
2128
            row = {'uid': uid, 'richtext': self.getResultsInterpretation()}
2129
        else:
2130
            row = {'uid': uid, 'richtext': ''}
2131
        return row
2132
2133
    def getAnalysisServiceSettings(self, uid):
2134
        """Returns a dictionary with the settings for the analysis service that
2135
        match with the uid provided.
2136
2137
        If there are no settings for the analysis service and
2138
        analysis requests:
2139
2140
        1. looks for settings in AR's ARTemplate. If found, returns the
2141
           settings for the AnalysisService set in the Template
2142
        2. If no settings found, looks in AR's ARProfile. If found, returns the
2143
           settings for the AnalysisService from the AR Profile. Otherwise,
2144
           returns a one entry dictionary with only the key 'uid'
2145
        """
2146
        sets = [s for s in self.getAnalysisServicesSettings()
2147
                if s.get("uid", "") == uid]
2148
2149
        # Created by using an ARTemplate?
2150
        if not sets and self.getTemplate():
2151
            adv = self.getTemplate().getAnalysisServiceSettings(uid)
2152
            sets = [adv] if "hidden" in adv else []
2153
2154
        # Created by using an AR Profile?
2155
        profiles = self.getProfiles()
2156
        if not sets and profiles:
2157
            adv = [profile.getAnalysisServiceSettings(uid) for profile in
2158
                   profiles]
2159
            sets = adv if adv[0].get("hidden") else []
2160
2161
        return sets[0] if sets else {"uid": uid}
2162
2163
    # TODO Sample Cleanup - Remove (Use getContainer instead)
2164
    def getContainers(self):
2165
        """This functions returns the containers from the analysis request's
2166
        analyses
2167
2168
        :returns: a list with the full partition objects
2169
        """
2170
        return self.getContainer() and [self.getContainer] or []
2171
2172
    def isAnalysisServiceHidden(self, uid):
2173
        """Checks if the analysis service that match with the uid provided must
2174
        be hidden in results. If no hidden assignment has been set for the
2175
        analysis in this request, returns the visibility set to the analysis
2176
        itself.
2177
2178
        Raise a TypeError if the uid is empty or None
2179
2180
        Raise a ValueError if there is no hidden assignment in this request or
2181
        no analysis service found for this uid.
2182
        """
2183
        if not api.is_uid(uid):
2184
            raise TypeError("Expected a UID, got '%s'" % type(uid))
2185
2186
        # get the local (analysis/template/profile) service settings
2187
        settings = self.getAnalysisServiceSettings(uid)
2188
2189
        # TODO: Rethink this logic and remove it afterwards!
2190
        # NOTE: profiles provide always the "hidden" key now!
2191
        if not settings or "hidden" not in settings.keys():
2192
            # lookup the service
2193
            serv = api.search({"UID": uid}, catalog="uid_catalog")
2194
            if serv and len(serv) == 1:
2195
                return serv[0].getObject().getRawHidden()
2196
            else:
2197
                raise ValueError("{} is not valid".format(uid))
2198
2199
        return settings.get("hidden", False)
2200
2201
    def getRejecter(self):
2202
        """If the Analysis Request has been rejected, returns the user who did the
2203
        rejection. If it was not rejected or the current user has not enough
2204
        privileges to access to this information, returns None.
2205
        """
2206
        wtool = getToolByName(self, 'portal_workflow')
2207
        mtool = getToolByName(self, 'portal_membership')
2208
        try:
2209
            review_history = wtool.getInfoFor(self, 'review_history')
2210
        except Exception:
2211
            return None
2212
        for items in review_history:
2213
            action = items.get('action')
2214
            if action != 'reject':
2215
                continue
2216
            actor = items.get('actor')
2217
            return mtool.getMemberById(actor)
2218
        return None
2219
2220
    def getReceivedBy(self):
2221
        """
2222
        Returns the User who received the analysis request.
2223
        :returns: the user id
2224
        """
2225
        user = getTransitionUsers(self, 'receive', last_user=True)
2226
        return user[0] if user else ''
2227
2228
    def getDateVerified(self):
2229
        """
2230
        Returns the date of verification as a DateTime object.
2231
        """
2232
        return getTransitionDate(self, 'verify', return_as_datetime=True)
2233
2234
    @security.public
2235
    def getPrioritySortkey(self):
2236
        """Returns the key that will be used to sort the current Analysis
2237
        Request based on both its priority and creation date. On ASC sorting,
2238
        the oldest item with highest priority will be displayed.
2239
        :return: string used for sorting
2240
        """
2241
        priority = self.getPriority()
2242
        created_date = self.created().ISO8601()
2243
        return '%s.%s' % (priority, created_date)
2244
2245
    @security.public
2246
    def setPriority(self, value):
2247
        if not value:
2248
            value = self.Schema().getField('Priority').getDefault(self)
2249
        original_value = self.Schema().getField('Priority').get(self)
2250
        if original_value != value:
2251
            self.Schema().getField('Priority').set(self, value)
2252
            self._reindexAnalyses(['getPrioritySortkey'], True)
2253
2254
    @security.private
2255
    def _reindexAnalyses(self, idxs=None, update_metadata=False):
2256
        if not idxs and not update_metadata:
2257
            return
2258
        if not idxs:
2259
            idxs = []
2260
        analyses = self.getAnalyses()
2261
        catalog = getToolByName(self, ANALYSIS_CATALOG)
2262
        for analysis in analyses:
2263
            analysis_obj = analysis.getObject()
2264
            catalog.reindexObject(analysis_obj, idxs=idxs, update_metadata=1)
2265
2266
    def _getCreatorFullName(self):
2267
        """
2268
        Returns the full name of this analysis request's creator.
2269
        """
2270
        return user_fullname(self, self.Creator())
2271
2272
    def _getCreatorEmail(self):
2273
        """
2274
        Returns the email of this analysis request's creator.
2275
        """
2276
        return user_email(self, self.Creator())
2277
2278
    def _getSamplerFullName(self):
2279
        """
2280
        Returns the full name's defined sampler.
2281
        """
2282
        return user_fullname(self, self.getSampler())
2283
2284
    def _getSamplerEmail(self):
2285
        """
2286
        Returns the email of this analysis request's sampler.
2287
        """
2288
        return user_email(self, self.getSampler())
2289
2290
    def getPriorityText(self):
2291
        """
2292
        This function looks up the priority text from priorities vocab
2293
        :returns: the priority text or ''
2294
        """
2295
        if self.getPriority():
2296
            return PRIORITIES.getValue(self.getPriority())
2297
        return ''
2298
2299
    def get_ARAttachment(self):
2300
        return None
2301
2302
    def set_ARAttachment(self, value):
2303
        return None
2304
2305
    def getRawRetest(self):
2306
        """Returns the UID of the Analysis Request that has been generated
2307
        automatically because of the retraction of the current Analysis Request
2308
        """
2309
        relationship = self.getField("Invalidated").relationship
2310
        uids = get_backreferences(self, relationship=relationship)
2311
        return uids[0] if uids else None
2312
2313
    def getRetest(self):
2314
        """Returns the Analysis Request that has been generated automatically
2315
        because of the retraction of the current Analysis Request
2316
        """
2317
        uid = self.getRawRetest()
2318
        return api.get_object_by_uid(uid, default=None)
2319
2320
    def getAncestors(self, all_ancestors=True):
2321
        """Returns the ancestor(s) of this Analysis Request
2322
        param all_ancestors: include all ancestors, not only the parent
2323
        """
2324
        parent = self.getParentAnalysisRequest()
2325
        if not parent:
2326
            return list()
2327
        if not all_ancestors:
2328
            return [parent]
2329
        return [parent] + parent.getAncestors(all_ancestors=True)
2330
2331
    def isRootAncestor(self):
2332
        """Returns True if the AR is the root ancestor
2333
2334
        :returns: True if the AR has no more parents
2335
        """
2336
        parent = self.getParentAnalysisRequest()
2337
        if parent:
2338
            return False
2339
        return True
2340
2341
    def getDescendants(self, all_descendants=False):
2342
        """Returns the descendant Analysis Requests
2343
2344
        :param all_descendants: recursively include all descendants
2345
        """
2346
2347
        uids = self.getDescendantsUIDs()
2348
        if not uids:
2349
            return []
2350
2351
        # Extract the descendant objects
2352
        descendants = []
2353
        cat = api.get_tool(UID_CATALOG)
2354
        for brain in cat(UID=uids):
2355
            descendant = api.get_object(brain)
2356
            descendants.append(descendant)
2357
            if all_descendants:
2358
                # Extend with grandchildren
2359
                descendants += descendant.getDescendants(all_descendants=True)
2360
2361
        return descendants
2362
2363
    def getDescendantsUIDs(self):
2364
        """Returns the UIDs of the descendant Analysis Requests
2365
2366
        This method is used as metadata
2367
        """
2368
        relationship = self.getField("ParentAnalysisRequest").relationship
2369
        return get_backreferences(self, relationship=relationship)
2370
2371
    def isPartition(self):
2372
        """Returns true if this Analysis Request is a partition
2373
        """
2374
        return not self.isRootAncestor()
2375
2376
    # TODO Remove in favour of getSamplingWorkflowEnabled
2377
    def getSamplingRequired(self):
2378
        """Returns True if the sample of this Analysis Request has to be
2379
        collected by the laboratory personnel
2380
        """
2381
        return self.getSamplingWorkflowEnabled()
2382
2383
    def isOpen(self):
2384
        """Returns whether all analyses from this Analysis Request haven't been
2385
        submitted yet (are in a open status)
2386
        """
2387
        for analysis in self.getAnalyses():
2388
            if ISubmitted.providedBy(api.get_object(analysis)):
2389
                return False
2390
        return True
2391
2392
    def setParentAnalysisRequest(self, value):
2393
        """Sets a parent analysis request, making the current a partition
2394
        """
2395
        parent = self.getParentAnalysisRequest()
2396
        self.Schema().getField("ParentAnalysisRequest").set(self, value)
2397
        if not value:
2398
            noLongerProvides(self, IAnalysisRequestPartition)
2399
            if parent and not parent.getDescendants(all_descendants=False):
2400
                noLongerProvides(self, IAnalysisRequestWithPartitions)
2401
        else:
2402
            alsoProvides(self, IAnalysisRequestPartition)
2403
            parent = self.getParentAnalysisRequest()
2404
            alsoProvides(parent, IAnalysisRequestWithPartitions)
2405
2406
    def getRawSecondaryAnalysisRequests(self):
2407
        """Returns the UIDs of the secondary Analysis Requests from this
2408
        Analysis Request
2409
        """
2410
        relationship = self.getField("PrimaryAnalysisRequest").relationship
2411
        return get_backreferences(self, relationship)
2412
2413
    def getSecondaryAnalysisRequests(self):
2414
        """Returns the secondary analysis requests from this analysis request
2415
        """
2416
        uids = self.getRawSecondaryAnalysisRequests()
2417
        uc = api.get_tool("uid_catalog")
2418
        return [api.get_object(brain) for brain in uc(UID=uids)]
2419
2420
    def setDateReceived(self, value):
2421
        """Sets the date received to this analysis request and to secondary
2422
        analysis requests
2423
        """
2424
        self.Schema().getField('DateReceived').set(self, value)
2425
        for secondary in self.getSecondaryAnalysisRequests():
2426
            secondary.setDateReceived(value)
2427
            secondary.reindexObject(idxs=["getDateReceived", "is_received"])
2428
2429
    def setDateSampled(self, value):
2430
        """Sets the date sampled to this analysis request and to secondary
2431
        analysis requests
2432
        """
2433
        self.Schema().getField('DateSampled').set(self, value)
2434
        for secondary in self.getSecondaryAnalysisRequests():
2435
            secondary.setDateSampled(value)
2436
            secondary.reindexObject(idxs="getDateSampled")
2437
2438
    def setSamplingDate(self, value):
2439
        """Sets the sampling date to this analysis request and to secondary
2440
        analysis requests
2441
        """
2442
        self.Schema().getField('SamplingDate').set(self, value)
2443
        for secondary in self.getSecondaryAnalysisRequests():
2444
            secondary.setSamplingDate(value)
2445
            secondary.reindexObject(idxs="getSamplingDate")
2446
2447
    def getSelectedRejectionReasons(self):
2448
        """Returns a list with the selected rejection reasons, if any
2449
        """
2450
        reasons = self.getRejectionReasons()
2451
        if not reasons:
2452
            return []
2453
2454
        # Return a copy of the list to avoid accidental writes
2455
        reasons = reasons[0].get("selected", [])[:]
2456
        return filter(None, reasons)
2457
2458
    def getOtherRejectionReasons(self):
2459
        """Returns other rejection reasons custom text, if any
2460
        """
2461
        reasons = self.getRejectionReasons()
2462
        if not reasons:
2463
            return ""
2464
        return reasons[0].get("other", "").strip()
2465
2466
    def createAttachment(self, filedata, filename="", **kw):
2467
        """Add a new attachment to the sample
2468
2469
        :param filedata: Raw filedata of the attachment (not base64)
2470
        :param filename: Filename + extension, e.g. `image.png`
2471
        :param kw: Additional keywords set to the attachment
2472
        :returns: New created and added attachment
2473
        """
2474
        # Add a new Attachment
2475
        attachment = api.create(self.getClient(), "Attachment")
2476
        attachment.setAttachmentFile(filedata)
2477
        fileobj = attachment.getAttachmentFile()
2478
        fileobj.filename = filename
2479
        attachment.edit(**kw)
2480
        attachment.processForm()
2481
        self.addAttachment(attachment)
2482
        return attachment
2483
2484
    def addAttachment(self, attachment):
2485
        """Adds an attachment or a list of attachments to the Analysis Request
2486
        """
2487
        if not isinstance(attachment, (list, tuple)):
2488
            attachment = [attachment]
2489
2490
        original = self.getAttachment() or []
2491
2492
        # Function addAttachment can accept brain, objects or uids
2493
        original = map(api.get_uid, original)
2494
        attachment = map(api.get_uid, attachment)
2495
2496
        # Boil out attachments already assigned to this Analysis Request
2497
        attachment = filter(lambda at: at not in original, attachment)
2498
        if attachment:
2499
            original.extend(attachment)
2500
            self.setAttachment(original)
2501
2502
    def setResultsInterpretationDepts(self, value):
2503
        """Custom setter which converts inline images to attachments
2504
2505
        https://github.com/senaite/senaite.core/pull/1344
2506
2507
        :param value: list of dictionary records
2508
        """
2509
        if not isinstance(value, list):
2510
            raise TypeError("Expected list, got {}".format(type(value)))
2511
2512
        # Convert inline images -> attachment files
2513
        records = []
2514
        for record in value:
2515
            # N.B. we might here a ZPublisher record. Converting to dict
2516
            #      ensures we can set values as well.
2517
            record = dict(record)
2518
            # Handle inline images in the HTML
2519
            html = record.get("richtext", "")
2520
            # Process inline images to attachments
2521
            record["richtext"] = self.process_inline_images(html)
2522
            # append the processed record for storage
2523
            records.append(record)
2524
2525
        # set the field
2526
        self.getField("ResultsInterpretationDepts").set(self, records)
2527
2528
    def process_inline_images(self, html):
2529
        """Convert inline images in the HTML to attachments
2530
2531
        https://github.com/senaite/senaite.core/pull/1344
2532
2533
        :param html: The richtext HTML
2534
        :returns: HTML with converted images
2535
        """
2536
        # Check for inline images
2537
        inline_images = re.findall(IMG_DATA_SRC_RX, html)
2538
2539
        # convert to inline images -> attachments
2540
        for data_type, data in inline_images:
2541
            # decode the base64 data to filedata
2542
            filedata = base64.decodestring(data)
2543
            # extract the file extension from the data type
2544
            extension = data_type.lstrip("data:image/").rstrip(";base64,")
2545
            # generate filename + extension
2546
            filename = "attachment.{}".format(extension or "png")
2547
            # create a new attachment
2548
            attachment = self.createAttachment(filedata, filename)
2549
            # ignore the attachment in report
2550
            attachment.setRenderInReport(False)
2551
            # remove the image data base64 prefix
2552
            html = html.replace(data_type, "")
2553
            # remove the base64 image data with the attachment link
2554
            html = html.replace(data, "resolve_attachment?uid={}".format(
2555
                api.get_uid(attachment)))
2556
            size = attachment.getAttachmentFile().get_size()
2557
            logger.info("Converted {:.2f} Kb inline image for {}"
2558
                        .format(size/1024, api.get_url(self)))
2559
2560
        # convert relative URLs to absolute URLs
2561
        # N.B. This is actually a TinyMCE issue, but hardcoded in Plone:
2562
        #  https://www.tiny.cloud/docs/configure/url-handling/#relative_urls
2563
        image_sources = re.findall(IMG_SRC_RX, html)
2564
2565
        # add a trailing slash so that urljoin doesn't remove the last segment
2566
        base_url = "{}/".format(api.get_url(self))
2567
2568
        for src in image_sources:
2569
            if re.match("(http|https|data)", src):
2570
                continue
2571
            obj = self.restrictedTraverse(src, None)
2572
            if obj is None:
2573
                continue
2574
            # ensure we have an absolute URL
2575
            html = html.replace(src, urljoin(base_url, src))
2576
2577
        return html
2578
2579
    def getProgress(self):
2580
        """Returns the progress in percent of all analyses
2581
        """
2582
        review_state = api.get_review_status(self)
2583
2584
        # Consider final states as 100%
2585
        # https://github.com/senaite/senaite.core/pull/1544#discussion_r379821841
2586
        if review_state in FINAL_STATES:
2587
            return 100
2588
2589
        numbers = self.getAnalysesNum()
2590
2591
        num_analyses = numbers[1] or 0
2592
        if not num_analyses:
2593
            return 0
2594
2595
        # [verified, total, not_submitted, to_be_verified]
2596
        num_to_be_verified = numbers[3] or 0
2597
        num_verified = numbers[0] or 0
2598
2599
        # 2 steps per analysis (submit, verify) plus one step for publish
2600
        max_num_steps = (num_analyses * 2) + 1
2601
        num_steps = num_to_be_verified + (num_verified * 2)
2602
        if not num_steps:
2603
            return 0
2604
        if num_steps > max_num_steps:
2605
            return 100
2606
        return (num_steps * 100) / max_num_steps
2607
2608
    def getMaxDateSampled(self):
2609
        """Returns the maximum date for sample collection
2610
        """
2611
        if not self.getSamplingWorkflowEnabled():
2612
            # no future, has to be collected before registration
2613
            return api.get_creation_date(self)
2614
        return datetime.max
2615
2616
2617
registerType(AnalysisRequest, PROJECTNAME)
2618