Passed
Push — master ( 7602ce...5aaf94 )
by Jordi
04:53
created

AnalysisRequest.isPartition()   A

Complexity

Conditions 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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