Passed
Push — 2.x ( 4648d7...097676 )
by Ramon
06:35
created

AnalysisRequest.getMaxDateSampled()   A

Complexity

Conditions 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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