Passed
Push — 2.x ( bb8554...7f0140 )
by Jordi
06:37
created

AnalysisRequest._renameAfterCreation()   A

Complexity

Conditions 2

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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