Passed
Push — master ( 746555...f38d56 )
by Jordi
09:48 queued 04:16
created

AnalysisRequest.getSampleConditionTitle()   A

Complexity

Conditions 2

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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