Passed
Push — 2.x ( f9f320...b159fd )
by Ramon
05:53
created

tle()   A

Complexity

Conditions 1

Size

Total Lines 2
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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