Passed
Push — master ( 3d9c1d...19c3e0 )
by Jordi
03:55
created

AnalysisRequest.getPrinted()   B

Complexity

Conditions 7

Size

Total Lines 22
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

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