Passed
Push — master ( de3e2a...b30da7 )
by Ramon
06:38
created

AnalysisRequest.setPublicationSpecification()   A

Complexity

Conditions 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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