Passed
Push — master ( eb777d...88a96a )
by Ramon
04:26
created

AnalysisRequest.addAttachment()   A

Complexity

Conditions 4

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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