Passed
Push — master ( 3c757d...12c5a2 )
by Ramon
04:26
created

AnalysisRequest.getWorksheets()   A

Complexity

Conditions 3

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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