Passed
Push — 2.x ( 3056d4...8bc3ab )
by Ramon
07:33
created

AnalysisRequest.setResultsRange()   B

Complexity

Conditions 7

Size

Total Lines 28
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

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