Passed
Push — master ( 1513bd...0193a4 )
by Ramon
05:35
created

AnalysisRequest.guard_publish_transition()   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nop 1
1
# -*- coding: utf-8 -*-
2
#
3
# This file is part of SENAITE.CORE
4
#
5
# Copyright 2018 by it's authors.
6
# Some rights reserved. See LICENSE.rst, CONTRIBUTORS.rst.
7
8
import sys
9
from decimal import Decimal
10
from operator import methodcaller
11
12
from AccessControl import ClassSecurityInfo
13
from bika.lims import api
14
from bika.lims import bikaMessageFactory as _
15
from bika.lims import deprecated
16
from bika.lims import logger
17
# Bika Fields
18
from bika.lims.browser.fields import ARAnalysesField
19
from bika.lims.browser.fields import DateTimeField
20
from bika.lims.browser.fields import ProxyField
21
from bika.lims.browser.fields import UIDReferenceField
22
# Bika Widgets
23
from bika.lims.browser.fields.remarksfield import RemarksField
24
from bika.lims.browser.widgets import DateTimeWidget
25
from bika.lims.browser.widgets import RemarksWidget
26
from bika.lims.browser.widgets import DecimalWidget
27
from bika.lims.browser.widgets import PrioritySelectionWidget
28
from bika.lims.browser.widgets import ReferenceWidget
29
from bika.lims.browser.widgets import RejectionWidget
30
from bika.lims.browser.widgets import SelectionWidget as BikaSelectionWidget
31
from bika.lims.catalog import CATALOG_ANALYSIS_LISTING
32
from bika.lims.config import PRIORITIES
33
from bika.lims.config import PROJECTNAME
34
from bika.lims.content.analysisspec import ResultsRangeDict
35
from bika.lims.content.bikaschema import BikaSchema
36
# Bika Interfaces
37
from bika.lims.interfaces import IAnalysisRequest
38
# Bika Permissions
39
from bika.lims.permissions import EditARContact
40
from bika.lims.permissions import ManageInvoices
41
from bika.lims.permissions import SampleSample
42
from bika.lims.permissions import ScheduleSampling
43
from bika.lims.permissions import Verify as VerifyPermission
44
# Bika Utils
45
from bika.lims.utils import getUsers
46
from bika.lims.utils import user_email
47
from bika.lims.utils import user_fullname
48
# Bika Workflow
49
from bika.lims.workflow import getReviewHistoryActionsList
50
from bika.lims.workflow import getTransitionDate
51
from bika.lims.workflow import getTransitionUsers
52
from bika.lims.workflow.analysisrequest import events
53
from bika.lims.workflow.analysisrequest import guards
54
from DateTime import DateTime
55
from plone import api as ploneapi
56
from Products.Archetypes.atapi import BaseFolder
57
from Products.Archetypes.atapi import BooleanField
58
from Products.Archetypes.atapi import BooleanWidget
59
from Products.Archetypes.atapi import ComputedField
60
from Products.Archetypes.atapi import ComputedWidget
61
from Products.Archetypes.atapi import DisplayList
62
from Products.Archetypes.atapi import FileField
63
from Products.Archetypes.atapi import FileWidget
64
from Products.Archetypes.atapi import FixedPointField
65
from Products.Archetypes.atapi import ReferenceField
66
from Products.Archetypes.atapi import StringField
67
from Products.Archetypes.atapi import StringWidget
68
from Products.Archetypes.atapi import TextAreaWidget
69
from Products.Archetypes.atapi import TextField
70
from Products.Archetypes.atapi import registerType
71
from Products.Archetypes.config import REFERENCE_CATALOG
72
from Products.Archetypes.public import Schema
73
from Products.Archetypes.references import HoldingReference
74
from Products.Archetypes.Widget import RichWidget
75
# AT Fields and AT Widgets
76
from Products.ATExtensions.field import RecordsField
77
from Products.CMFCore.permissions import ModifyPortalContent
78
from Products.CMFCore.permissions import View
79
from Products.CMFCore.utils import getToolByName
80
from Products.CMFPlone.utils import _createObjectByType
81
from Products.CMFPlone.utils import safe_unicode
82
from zope.interface import implements
83
84
85
# SCHEMA DEFINITION
86
schema = BikaSchema.copy() + Schema((
87
88
    UIDReferenceField(
89
        'Contact',
90
        required=1,
91
        default_method='getContactUIDForUser',
92
        allowed_types=('Contact',),
93
        mode="rw",
94
        read_permission=View,
95
        write_permission=EditARContact,
96
        widget=ReferenceWidget(
97
            label=_("Contact"),
98
            render_own_label=True,
99
            size=20,
100
            helper_js=("bika_widgets/referencewidget.js",
101
                       "++resource++bika.lims.js/contact.js"),
102
            description=_("The primary contact of this analysis request, "
103
                          "who will receive notifications and publications "
104
                          "via email"),
105
            visible={
106
                'edit': 'visible',
107
                'view': 'visible',
108
                'add': 'edit',
109
                'header_table': 'prominent',
110
                'sample_registered': {
111
                    'view': 'visible', 'edit': 'visible', 'add': 'edit'},
112
                'to_be_sampled': {'view': 'visible', 'edit': 'visible'},
113
                'scheduled_sampling': {'view': 'visible', 'edit': 'visible'},
114
                'sampled': {'view': 'visible', 'edit': 'visible'},
115
                'to_be_preserved': {'view': 'visible', 'edit': 'visible'},
116
                'sample_due': {'view': 'visible', 'edit': 'visible'},
117
                'sample_received': {'view': 'visible', 'edit': 'visible'},
118
                'attachment_due': {'view': 'visible', 'edit': 'visible'},
119
                'to_be_verified': {'view': 'visible', 'edit': 'visible'},
120
                'verified': {'view': 'visible', 'edit': 'invisible'},
121
                'published': {'view': 'visible', 'edit': 'invisible'},
122
                'invalid': {'view': 'visible', 'edit': 'invisible'},
123
                'rejected': {'view': 'visible', 'edit': 'invisible'},
124
            },
125
            base_query={'inactive_state': 'active'},
126
            showOn=True,
127
            popup_width='400px',
128
            colModel=[
129
                {'columnName': 'UID', 'hidden': True},
130
                {'columnName': 'Fullname', 'width': '50',
131
                 'label': _('Name')},
132
                {'columnName': 'EmailAddress', 'width': '50',
133
                 'label': _('Email Address')},
134
            ],
135
        ),
136
    ),
137
138
    ReferenceField(
139
        'CCContact',
140
        multiValued=1,
141
        vocabulary_display_path_bound=sys.maxsize,
142
        allowed_types=('Contact',),
143
        referenceClass=HoldingReference,
144
        relationship='AnalysisRequestCCContact',
145
        mode="rw",
146
        read_permission=View,
147
        write_permission=EditARContact,
148
        widget=ReferenceWidget(
149
            label=_("CC Contacts"),
150
            description=_("The contacts used in CC for email notifications"),
151
            render_own_label=True,
152
            size=20,
153
            visible={
154
                'edit': 'visible',
155
                'view': 'visible',
156
                'add': 'edit',
157
                'header_table': 'prominent',
158
                'sample_registered': {
159
                    'view': 'visible', 'edit': 'visible', 'add': 'edit'},
160
                'to_be_sampled': {'view': 'visible', 'edit': 'visible'},
161
                'scheduled_sampling': {'view': 'visible', 'edit': 'visible'},
162
                'sampled': {'view': 'visible', 'edit': 'visible'},
163
                'to_be_preserved': {'view': 'visible', 'edit': 'visible'},
164
                'sample_due': {'view': 'visible', 'edit': 'visible'},
165
                'sample_received': {'view': 'visible', 'edit': 'visible'},
166
                'attachment_due': {'view': 'visible', 'edit': 'visible'},
167
                'to_be_verified': {'view': 'visible', 'edit': 'visible'},
168
                'verified': {'view': 'visible', 'edit': 'invisible'},
169
                'published': {'view': 'visible', 'edit': 'invisible'},
170
                'invalid': {'view': 'visible', 'edit': 'invisible'},
171
                'rejected': {'view': 'visible', 'edit': 'invisible'},
172
            },
173
            base_query={'inactive_state': 'active'},
174
            showOn=True,
175
            popup_width='400px',
176
            colModel=[
177
                {'columnName': 'UID', 'hidden': True},
178
                {'columnName': 'Fullname', 'width': '50',
179
                 'label': _('Name')},
180
                {'columnName': 'EmailAddress', 'width': '50',
181
                 'label': _('Email Address')},
182
            ],
183
        ),
184
    ),
185
186
    StringField(
187
        'CCEmails',
188
        mode="rw",
189
        read_permission=View,
190
        write_permission=EditARContact,
191
        acquire=True,
192
        acquire_fieldname="CCEmails",
193
        widget=StringWidget(
194
            label=_("CC Emails"),
195
            description=_("Additional email addresses to be notified"),
196
            visible={
197
                'edit': 'visible',
198
                'view': 'visible',
199
                'add': 'edit',
200
                'header_table': 'prominent',
201
                'sample_registered': {
202
                    'view': 'visible', 'edit': 'visible', 'add': 'edit'},
203
                'to_be_sampled': {'view': 'visible', 'edit': 'visible'},
204
                'scheduled_sampling': {'view': 'visible', 'edit': 'visible'},
205
                'sampled': {'view': 'visible', 'edit': 'visible'},
206
                'to_be_preserved': {'view': 'visible', 'edit': 'visible'},
207
                'sample_received': {'view': 'visible', 'edit': 'visible'},
208
                'attachment_due': {'view': 'visible', 'edit': 'visible'},
209
                'to_be_verified': {'view': 'visible', 'edit': 'visible'},
210
                'verified': {'view': 'visible', 'edit': 'invisible'},
211
                'published': {'view': 'visible', 'edit': 'invisible'},
212
                'invalid': {'view': 'visible', 'edit': 'invisible'},
213
                'rejected': {'view': 'visible', 'edit': 'invisible'},
214
            },
215
            render_own_label=True,
216
            size=20,
217
        ),
218
    ),
219
220
    ReferenceField(
221
        'Client',
222
        required=1,
223
        allowed_types=('Client',),
224
        relationship='AnalysisRequestClient',
225
        mode="rw",
226
        read_permission=View,
227
        write_permission=ModifyPortalContent,
228
        widget=ReferenceWidget(
229
            label=_("Client"),
230
            description=_("The assigned client of this request"),
231
            size=20,
232
            render_own_label=True,
233
            visible={
234
                'edit': 'visible',
235
                'view': 'visible',
236
                'add': 'edit',
237
                'header_table': 'visible',
238
                'sample_registered': {
239
                    'view': 'invisible', 'edit': 'visible', 'add': 'edit'},
240
                'to_be_sampled': {'view': 'invisible', 'edit': 'invisible'},
241
                'scheduled_sampling': {
242
                    'view': 'invisible', 'edit': 'invisible'},
243
                'sampled': {'view': 'invisible', 'edit': 'invisible'},
244
                'to_be_preserved': {'view': 'invisible', 'edit': 'invisible'},
245
                'sample_received': {'view': 'invisible', 'edit': 'invisible'},
246
                'attachment_due': {'view': 'invisible', 'edit': 'invisible'},
247
                'to_be_verified': {'view': 'invisible', 'edit': 'invisible'},
248
                'verified': {'view': 'invisible', 'edit': 'invisible'},
249
                'published': {'view': 'invisible', 'edit': 'invisible'},
250
                'invalid': {'view': 'invisible', 'edit': 'invisible'},
251
                'rejected': {'view': 'invisible', 'edit': 'invisible'},
252
            },
253
            base_query={'review_state': 'active'},
254
            showOn=True,
255
            add_button={
256
                    'visible': True,
257
                    'url': 'clients/createObject?type_name=Client',
258
                    'return_fields': ['Title'],
259
                    'js_controllers': ['#client-base-edit'],
260
                    'overlay_handler': 'ClientOverlayHandler',
261
                }
262
        ),
263
    ),
264
265
    UIDReferenceField(
266
        'Sample',
267
        allowed_types=('Sample',),
268
        mode="rw",
269
        read_permission=View,
270
        write_permission=ModifyPortalContent,
271
        widget=ReferenceWidget(
272
            label=_("Sample"),
273
            description=_("Select a sample to create a secondary AR"),
274
            size=20,
275
            render_own_label=True,
276
            visible={
277
                'edit': 'visible',
278
                'view': 'visible',
279
                'add': 'edit',
280
                'header_table': 'visible',
281
                'sample_registered': {
282
                    'view': 'visible', 'edit': 'visible', 'add': 'edit'},
283
                'to_be_sampled': {'view': 'visible', 'edit': 'invisible'},
284
                'scheduled_sampling': {'view': 'visible', 'edit': 'invisible'},
285
                'sampled': {'view': 'visible', 'edit': 'invisible'},
286
                'to_be_preserved': {'view': 'visible', 'edit': 'invisible'},
287
                'sample_due': {'view': 'visible', 'edit': 'invisible'},
288
                'sample_received': {'view': 'visible', 'edit': 'invisible'},
289
                'attachment_due': {'view': 'visible', 'edit': 'invisible'},
290
                'to_be_verified': {'view': 'visible', 'edit': 'invisible'},
291
                'verified': {'view': 'visible', 'edit': 'invisible'},
292
                'published': {'view': 'visible', 'edit': 'invisible'},
293
                'invalid': {'view': 'visible', 'edit': 'invisible'},
294
                'rejected': {'view': 'visible', 'edit': 'invisible'},
295
            },
296
            catalog_name='bika_catalog',
297
            base_query={'cancellation_state': 'active',
298
                        'review_state': ['sample_due', 'sample_received', ]},
299
            showOn=True,
300
        ),
301
    ),
302
303
    ReferenceField(
304
        'Batch',
305
        allowed_types=('Batch',),
306
        relationship='AnalysisRequestBatch',
307
        mode="rw",
308
        read_permission=View,
309
        write_permission=ModifyPortalContent,
310
        widget=ReferenceWidget(
311
            label=_("Batch"),
312
            size=20,
313
            description=_("The assigned batch of this request"),
314
            render_own_label=True,
315
            visible={
316
                'edit': 'visible',
317
                'view': 'visible',
318
                'add': 'edit',
319
                'header_table': 'visible',
320
                'sample_registered': {
321
                    'view': 'visible', 'edit': 'visible', 'add': 'edit'},
322
                'to_be_sampled': {'view': 'visible', 'edit': 'visible'},
323
                'scheduled_sampling': {'view': 'visible', 'edit': 'visible'},
324
                'sampled': {'view': 'visible', 'edit': 'visible'},
325
                'to_be_preserved': {'view': 'visible', 'edit': 'visible'},
326
                'sample_due': {'view': 'visible', 'edit': 'visible'},
327
                'sample_received': {'view': 'visible', 'edit': 'visible'},
328
                'attachment_due': {'view': 'visible', 'edit': 'visible'},
329
                'to_be_verified': {'view': 'visible', 'edit': 'visible'},
330
                'verified': {'view': 'visible', 'edit': 'visible'},
331
                'published': {'view': 'visible', 'edit': 'invisible'},
332
                'invalid': {'view': 'visible', 'edit': 'invisible'},
333
                'rejected': {'view': 'visible', 'edit': 'invisible'},
334
            },
335
            catalog_name='bika_catalog',
336
            base_query={'review_state': 'open',
337
                        'cancellation_state': 'active'},
338
            showOn=True,
339
        ),
340
    ),
341
342
    ReferenceField(
343
        'SamplingRound',
344
        allowed_types=('SamplingRound',),
345
        relationship='AnalysisRequestSamplingRound',
346
        mode="rw",
347
        read_permission=View,
348
        write_permission=ModifyPortalContent,
349
        widget=ReferenceWidget(
350
            label=_("Sampling Round"),
351
            description=_("The assigned sampling round of this request"),
352
            size=20,
353
            render_own_label=True,
354
            visible={
355
                'edit': 'visible',
356
                'view': 'visible',
357
                'add': 'edit',
358
                'header_table': 'visible',
359
                'sample_registered': {
360
                    'view': 'visible', 'edit': 'visible', 'add': 'edit'},
361
                'to_be_sampled': {'view': 'visible', 'edit': 'visible'},
362
                'scheduled_sampling': {'view': 'visible', 'edit': 'visible'},
363
                'sampled': {'view': 'visible', 'edit': 'visible'},
364
                'to_be_preserved': {'view': 'visible', 'edit': 'visible'},
365
                'sample_due': {'view': 'visible', 'edit': 'visible'},
366
                'sample_received': {'view': 'visible', 'edit': 'visible'},
367
                'attachment_due': {'view': 'visible', 'edit': 'visible'},
368
                'to_be_verified': {'view': 'visible', 'edit': 'visible'},
369
                'verified': {'view': 'visible', 'edit': 'visible'},
370
                'published': {'view': 'visible', 'edit': 'invisible'},
371
                'invalid': {'view': 'visible', 'edit': 'invisible'},
372
                'rejected': {'view': 'visible', 'edit': 'invisible'},
373
            },
374
            catalog_name='portal_catalog',
375
            base_query={},
376
            showOn=True,
377
        ),
378
    ),
379
380
    ReferenceField(
381
        'SubGroup',
382
        required=False,
383
        allowed_types=('SubGroup',),
384
        referenceClass=HoldingReference,
385
        relationship='AnalysisRequestSubGroup',
386
        widget=ReferenceWidget(
387
            label=_("Batch Sub-group"),
388
            description=_("The assigned batch sub group of this request"),
389
            size=20,
390
            render_own_label=True,
391
            visible={
392
                'edit': 'visible',
393
                'view': 'visible',
394
                'add': 'edit',
395
                'header_table': 'visible',
396
                'sample_registered': {
397
                    'view': 'visible', 'edit': 'visible', 'add': 'edit'},
398
                'to_be_sampled': {'view': 'visible', 'edit': 'visible'},
399
                'scheduled_sampling': {'view': 'visible', 'edit': 'visible'},
400
                'sampled': {'view': 'visible', 'edit': 'visible'},
401
                'to_be_preserved': {'view': 'visible', 'edit': 'visible'},
402
                'sample_due': {'view': 'visible', 'edit': 'visible'},
403
                'sample_received': {'view': 'visible', 'edit': 'visible'},
404
                'attachment_due': {'view': 'visible', 'edit': 'visible'},
405
                'to_be_verified': {'view': 'visible', 'edit': 'visible'},
406
                'verified': {'view': 'visible', 'edit': 'visible'},
407
                'published': {'view': 'visible', 'edit': 'invisible'},
408
                'invalid': {'view': 'visible', 'edit': 'invisible'},
409
                'rejected': {'view': 'visible', 'edit': 'invisible'},
410
            },
411
            catalog_name='bika_setup_catalog',
412
            colModel=[
413
                {'columnName': 'Title', 'width': '30',
414
                 'label': _('Title'), 'align': 'left'},
415
                {'columnName': 'Description', 'width': '70',
416
                 'label': _('Description'), 'align': 'left'},
417
                {'columnName': 'SortKey', 'hidden': True},
418
                {'columnName': 'UID', 'hidden': True},
419
            ],
420
            base_query={'inactive_state': 'active'},
421
            sidx='SortKey',
422
            sord='asc',
423
            showOn=True,
424
        ),
425
    ),
426
427
    ReferenceField(
428
        'Template',
429
        allowed_types=('ARTemplate',),
430
        referenceClass=HoldingReference,
431
        relationship='AnalysisRequestARTemplate',
432
        mode="rw",
433
        read_permission=View,
434
        write_permission=ModifyPortalContent,
435
        widget=ReferenceWidget(
436
            label=_("AR Template"),
437
            description=_("The predefined values of the AR template are set "
438
                          "in the request"),
439
            size=20,
440
            render_own_label=True,
441
            visible={
442
                'edit': 'visible',
443
                'view': 'visible',
444
                'add': 'edit',
445
                'secondary': 'disabled',
446
                'header_table': 'visible',
447
                'sample_registered': {
448
                    'view': 'visible', 'edit': 'visible', 'add': 'edit'},
449
                'to_be_sampled': {'view': 'visible', 'edit': 'invisible'},
450
                'scheduled_sampling': {'view': 'visible', 'edit': 'invisible'},
451
                'sampled': {'view': 'visible', 'edit': 'invisible'},
452
                'to_be_preserved': {'view': 'visible', 'edit': 'invisible'},
453
                'sample_due': {'view': 'visible', 'edit': 'invisible'},
454
                'sample_received': {'view': 'visible', 'edit': 'invisible'},
455
                'attachment_due': {'view': 'visible', 'edit': 'invisible'},
456
                'to_be_verified': {'view': 'visible', 'edit': 'invisible'},
457
                'verified': {'view': 'visible', 'edit': 'invisible'},
458
                'published': {'view': 'visible', 'edit': 'invisible'},
459
                'invalid': {'view': 'visible', 'edit': 'invisible'},
460
                'rejected': {'view': 'visible', 'edit': 'invisible'},
461
            },
462
            catalog_name='bika_setup_catalog',
463
            base_query={'inactive_state': 'active'},
464
            showOn=True,
465
        ),
466
    ),
467
468
    # TODO: Profile'll be delated
469
    ReferenceField(
470
        'Profile',
471
        allowed_types=('AnalysisProfile',),
472
        referenceClass=HoldingReference,
473
        relationship='AnalysisRequestAnalysisProfile',
474
        mode="rw",
475
        read_permission=View,
476
        write_permission=ModifyPortalContent,
477
        widget=ReferenceWidget(
478
            label=_("Analysis Profile"),
479
            description=_("Analysis profiles apply a certain set of analyses"),
480
            size=20,
481
            render_own_label=True,
482
            visible=False,
483
            catalog_name='bika_setup_catalog',
484
            base_query={'inactive_state': 'active'},
485
            showOn=False,
486
        ),
487
    ),
488
489
    ReferenceField(
490
        'Profiles',
491
        multiValued=1,
492
        allowed_types=('AnalysisProfile',),
493
        referenceClass=HoldingReference,
494
        vocabulary_display_path_bound=sys.maxsize,
495
        relationship='AnalysisRequestAnalysisProfiles',
496
        mode="rw",
497
        read_permission=View,
498
        write_permission=ModifyPortalContent,
499
        widget=ReferenceWidget(
500
            label=_("Analysis Profiles"),
501
            description=_("Analysis profiles apply a certain set of analyses"),
502
            size=20,
503
            render_own_label=True,
504
            visible={
505
                'edit': 'visible',
506
                'view': 'visible',
507
                'add': 'edit',
508
                'header_table': 'visible',
509
                'sample_registered': {
510
                    'view': 'visible', 'edit': 'visible', 'add': 'edit'},
511
                'to_be_sampled': {'view': 'visible', 'edit': 'invisible'},
512
                'scheduled_sampling': {'view': 'visible', 'edit': 'invisible'},
513
                'sampled': {'view': 'visible', 'edit': 'invisible'},
514
                'to_be_preserved': {'view': 'visible', 'edit': 'invisible'},
515
                'sample_due': {'view': 'visible', 'edit': 'invisible'},
516
                'sample_received': {'view': 'visible', 'edit': 'invisible'},
517
                'attachment_due': {'view': 'visible', 'edit': 'invisible'},
518
                'to_be_verified': {'view': 'visible', 'edit': 'invisible'},
519
                'verified': {'view': 'visible', 'edit': 'invisible'},
520
                'published': {'view': 'visible', 'edit': 'invisible'},
521
                'invalid': {'view': 'visible', 'edit': 'invisible'},
522
                'rejected': {'view': 'visible', 'edit': 'invisible'},
523
            },
524
            catalog_name='bika_setup_catalog',
525
            base_query={'inactive_state': 'active'},
526
            showOn=True,
527
        ),
528
    ),
529
    # This field is a mirror of a field in Sample with the same name
530
    # TODO Workflow - Request - Fix DateSampled inconsistencies. At the moment,
531
    # one can create an AR (using code) with DateSampled set when sampling_wf at
532
    # the same time sampling workflow is active. This might cause
533
    # inconsistencies: AR still in `to_be_sampled`, but getDateSampled returns
534
    # a valid date!
535
    ProxyField(
536
        'DateSampled',
537
        proxy="context.getSample()",
538
        mode="rw",
539
        read_permission=View,
540
        write_permission=ModifyPortalContent,
541
        widget=DateTimeWidget(
542
            label=_("Date Sampled"),
543
            description=_("The date when the sample was taken"),
544
            size=20,
545
            show_time=True,
546
            datepicker_nofuture=1,
547
            visible={
548
                'edit': 'visible',
549
                'view': 'visible',
550
                'add': 'edit',
551
                'secondary': 'disabled',
552
                'header_table': 'prominent',
553
                'sample_registered': {
554
                    'view': 'invisible', 'edit': 'invisible'},
555
                'to_be_sampled': {'view': 'invisible', 'edit': 'visible'},
556
                'scheduled_sampling': {'view': 'invisible', 'edit': 'visible'},
557
                'sampled': {'view': 'visible', 'edit': 'invisible'},
558
                'to_be_preserved': {'view': 'visible', 'edit': 'invisible'},
559
                'sample_due': {'view': 'visible', 'edit': 'invisible'},
560
                'sample_received': {'view': 'visible', 'edit': 'invisible'},
561
                'attachment_due': {'view': 'visible', 'edit': 'invisible'},
562
                'to_be_verified': {'view': 'visible', 'edit': 'invisible'},
563
                'verified': {'view': 'visible', 'edit': 'invisible'},
564
                'published': {'view': 'visible', 'edit': 'invisible'},
565
                'invalid': {'view': 'visible', 'edit': 'invisible'},
566
                'rejected': {'view': 'visible', 'edit': 'invisible'},
567
            },
568
            render_own_label=True,
569
        ),
570
    ),
571
    # This field is a mirror of a field in Sample with the same name
572
    ProxyField(
573
        'Sampler',
574
        proxy="context.getSample()",
575
        mode="rw",
576
        read_permission=View,
577
        write_permission=SampleSample,
578
        vocabulary='getSamplers',
579
        widget=BikaSelectionWidget(
580
            format='select',
581
            label=_("Sampler"),
582
            description=_("The person who took the sample"),
583
            # see SamplingWOrkflowWidgetVisibility
584
            visible={
585
                'edit': 'visible',
586
                'view': 'visible',
587
                'add': 'edit',
588
                'header_table': 'prominent',
589
                'sample_registered': {
590
                    'view': 'invisible', 'edit': 'invisible'},
591
                'to_be_sampled': {'view': 'invisible', 'edit': 'visible'},
592
                'scheduled_sampling': {'view': 'invisible', 'edit': 'visible'},
593
                'sampled': {'view': 'visible', 'edit': 'invisible'},
594
                'to_be_preserved': {'view': 'visible', 'edit': 'invisible'},
595
                'sample_due': {'view': 'visible', 'edit': 'invisible'},
596
                'sample_received': {'view': 'visible', 'edit': 'invisible'},
597
                'attachment_due': {'view': 'visible', 'edit': 'invisible'},
598
                'to_be_verified': {'view': 'visible', 'edit': 'invisible'},
599
                'verified': {'view': 'visible', 'edit': 'invisible'},
600
                'published': {'view': 'visible', 'edit': 'invisible'},
601
                'invalid': {'view': 'visible', 'edit': 'invisible'},
602
                'rejected': {'view': 'visible', 'edit': 'invisible'},
603
            },
604
            render_own_label=True,
605
        ),
606
    ),
607
608
    # Sample field
609
    ProxyField(
610
        'ScheduledSamplingSampler',
611
        proxy="context.getSample()",
612
        mode="rw",
613
        read_permission=View,
614
        write_permission=ScheduleSampling,
615
        vocabulary='getSamplers',
616
        widget=BikaSelectionWidget(
617
            description=_("Define the sampler supposed to do the sample in "
618
                          "the scheduled date"),
619
            format='select',
620
            label=_("Sampler for scheduled sampling"),
621
            visible={
622
                'edit': 'visible',
623
                'view': 'visible',
624
                'header_table': 'visible',
625
                'sample_registered': {
626
                    'view': 'invisible', 'edit': 'invisible'},
627
                'to_be_sampled': {'view': 'visible', 'edit': 'visible'},
628
                'scheduled_sampling': {'view': 'visible', 'edit': 'visible'},
629
                'sampled': {'view': 'invisible', 'edit': 'invisible'},
630
                'to_be_preserved': {'view': 'invisible', 'edit': 'invisible'},
631
                'sample_due': {'view': 'invisible', 'edit': 'invisible'},
632
                'sample_received': {'view': 'invisible', 'edit': 'invisible'},
633
                'expired': {'view': 'invisible', 'edit': 'invisible'},
634
                'disposed': {'view': 'invisible', 'edit': 'invisible'},
635
            },
636
            render_own_label=True,
637
        ),
638
    ),  # This field is a mirror of a Sample field with the same name
639
640
    # Sample field
641
    ProxyField(
642
        'SamplingDate',
643
        required=0,
644
        proxy="context.getSample()",
645
        mode="rw",
646
        read_permission=View,
647
        write_permission=ModifyPortalContent,
648
        widget=DateTimeWidget(
649
            label=_("Expected Sampling Date"),
650
            description=_("The date when the sample will be taken"),
651
            size=20,
652
            show_time=True,
653
            render_own_label=True,
654
            datepicker_nopast=1,
655
            # We must use SamplingWOrkflowWidgetVisibility soon.
656
            # For now we will handle it through JS
657
            # see SamplingWOrkflowWidgetVisibility
658
            visible={
659
                'edit': 'visible',
660
                'view': 'visible',
661
                'add': 'edit',
662
                'header_table': 'visible',
663
                'secondary': 'disabled',
664
                'sample_registered': {
665
                    'view': 'visible', 'edit': 'visible', 'add': 'edit'},
666
                'to_be_sampled': {'view': 'visible', 'edit': 'visible'},
667
                'scheduled_sampling': {'view': 'visible', 'edit': 'visible'},
668
                'sampled': {'view': 'visible', 'edit': 'invisible'},
669
                'to_be_preserved': {'view': 'visible', 'edit': 'invisible'},
670
                'sample_due': {'view': 'visible', 'edit': 'invisible'},
671
                'sample_received': {'view': 'visible', 'edit': 'invisible'},
672
                'attachment_due': {'view': 'visible', 'edit': 'invisible'},
673
                'to_be_verified': {'view': 'visible', 'edit': 'invisible'},
674
                'verified': {'view': 'visible', 'edit': 'invisible'},
675
                'published': {'view': 'visible', 'edit': 'invisible'},
676
                'invalid': {'view': 'visible', 'edit': 'invisible'},
677
                'rejected': {'view': 'visible', 'edit': 'invisible'},
678
            },
679
        ),
680
    ),
681
682
    # Sample field
683
    ProxyField(
684
        'SampleType',
685
        proxy="context.getSample()",
686
        required=1,
687
        allowed_types='SampleType',
688
        relationship='AnalysisRequestSampleType',
689
        mode="rw",
690
        read_permission=View,
691
        write_permission=ModifyPortalContent,
692
        widget=ReferenceWidget(
693
            label=_("Sample Type"),
694
            description=_("Create a new sample of this type"),
695
            size=20,
696
            render_own_label=True,
697
            visible={
698
                'edit': 'visible',
699
                'view': 'visible',
700
                'add': 'edit',
701
                'secondary': 'disabled',
702
                'header_table': 'visible',
703
                'sample_registered': {'view': 'visible', 'edit': 'visible', 'add': 'edit'},
704
                'to_be_sampled': {'view': 'visible', 'edit': 'visible'},
705
                'scheduled_sampling': {'view': 'invisible', 'edit': 'visible'},
706
                'sampled': {'view': 'visible', 'edit': 'visible'},
707
                'to_be_preserved': {'view': 'visible', 'edit': 'visible'},
708
                'sample_due': {'view': 'visible', 'edit': 'visible'},
709
                'sample_received': {'view': 'visible', 'edit': 'invisible'},
710
                'attachment_due': {'view': 'visible', 'edit': 'invisible'},
711
                'to_be_verified': {'view': 'visible', 'edit': 'invisible'},
712
                'verified': {'view': 'visible', 'edit': 'invisible'},
713
                'published': {'view': 'visible', 'edit': 'invisible'},
714
                'invalid': {'view': 'visible', 'edit': 'invisible'},
715
                'rejected': {'view': 'visible', 'edit': 'invisible'},
716
            },
717
            catalog_name='bika_setup_catalog',
718
            base_query={'inactive_state': 'active'},
719
            showOn=True,
720
        ),
721
    ),
722
723
    RecordsField(
724
        'RejectionReasons',
725
        widget=RejectionWidget(
726
            label=_("Sample Rejection"),
727
            description=_("Set the Sample Rejection workflow and the reasons"),
728
            render_own_label=False,
729
            visible={
730
                'edit': 'visible',
731
                'view': 'visible',
732
                'add': 'edit',
733
                'secondary': 'disabled',
734
                'header_table': 'visible',
735
                'sample_registered': {
736
                    'view': 'visible', 'edit': 'visible', 'add': 'edit'},
737
                'to_be_sampled': {'view': 'visible', 'edit': 'visible'},
738
                'sampled': {'view': 'visible', 'edit': 'visible'},
739
                'to_be_preserved': {'view': 'visible', 'edit': 'visible'},
740
                'sample_due': {'view': 'visible', 'edit': 'visible'},
741
                'sample_received': {'view': 'visible', 'edit': 'visible'},
742
                'attachment_due': {'view': 'visible', 'edit': 'visible'},
743
                'to_be_verified': {'view': 'visible', 'edit': 'visible'},
744
                'verified': {'view': 'visible', 'edit': 'visible'},
745
                'published': {'view': 'visible', 'edit': 'visible'},
746
                'invalid': {'view': 'visible', 'edit': 'visible'},
747
                'rejected': {'view': 'visible', 'edit': 'invisible'},
748
            },
749
        ),
750
    ),
751
752
    ReferenceField(
753
        'Specification',
754
        required=0,
755
        allowed_types='AnalysisSpec',
756
        relationship='AnalysisRequestAnalysisSpec',
757
        mode="rw",
758
        read_permission=View,
759
        write_permission=ModifyPortalContent,
760
        widget=ReferenceWidget(
761
            label=_("Analysis Specification"),
762
            description=_("Choose default AR specification values"),
763
            size=20,
764
            render_own_label=True,
765
            visible={
766
                'edit': 'visible',
767
                'view': 'visible',
768
                'add': 'edit',
769
                'header_table': 'visible',
770
                'sample_registered': {
771
                    'view': 'visible', 'edit': 'visible', 'add': 'edit'},
772
                'to_be_sampled': {'view': 'visible', 'edit': 'visible'},
773
                'scheduled_sampling': {'view': 'visible', 'edit': 'visible'},
774
                'sampled': {'view': 'visible', 'edit': 'visible'},
775
                'to_be_preserved': {'view': 'visible', 'edit': 'visible'},
776
                'sample_due': {'view': 'visible', 'edit': 'visible'},
777
                'sample_received': {'view': 'visible', 'edit': 'visible'},
778
                'attachment_due': {'view': 'visible', 'edit': 'visible'},
779
                'to_be_verified': {'view': 'visible', 'edit': 'invisible'},
780
                'verified': {'view': 'visible', 'edit': 'invisible'},
781
                'published': {'view': 'visible', 'edit': 'invisible'},
782
                'invalid': {'view': 'visible', 'edit': 'invisible'},
783
                'rejected': {'view': 'visible', 'edit': 'invisible'},
784
            },
785
            catalog_name='bika_setup_catalog',
786
            colModel=[
787
                {'columnName': 'contextual_title',
788
                 'width': '30',
789
                 'label': _('Title'),
790
                 'align': 'left'},
791
                {'columnName': 'SampleTypeTitle',
792
                 'width': '70',
793
                 'label': _('SampleType'),
794
                 'align': 'left'},
795
                # UID is required in colModel
796
                {'columnName': 'UID', 'hidden': True},
797
            ],
798
            showOn=True,
799
        ),
800
    ),
801
802
    # see setResultsRange below.
803
    RecordsField(
804
        'ResultsRange',
805
        required=0,
806
        type='resultsrange',
807
        subfields=('keyword', 'min', 'max', 'warn_min', 'warn_max', 'hidemin',
808
                   'hidemax', 'rangecomment', 'min_operator', 'max_operator'),
809
        widget=ComputedWidget(visible=False),
810
    ),
811
812
    ReferenceField(
813
        'PublicationSpecification',
814
        required=0,
815
        allowed_types='AnalysisSpec',
816
        relationship='AnalysisRequestPublicationSpec',
817
        mode="rw",
818
        read_permission=View,
819
        write_permission=View,
820
        widget=ReferenceWidget(
821
            label=_("Publication Specification"),
822
            description=_(
823
                "Set the specification to be used before publishing an AR."),
824
            size=20,
825
            render_own_label=True,
826
            visible={
827
                'edit': 'visible',
828
                'view': 'visible',
829
                'header_table': 'visible',
830
                'sample_registered': {
831
                    'view': 'invisible', 'edit': 'invisible'},
832
                'to_be_sampled': {'view': 'invisible', 'edit': 'invisible'},
833
                'scheduled_sampling': {
834
                    'view': 'invisible', 'edit': 'invisible'},
835
                'sampled': {'view': 'invisible', 'edit': 'invisible'},
836
                'to_be_preserved': {'view': 'invisible', 'edit': 'invisible'},
837
                'sample_due': {'view': 'invisible', 'edit': 'invisible'},
838
                'sample_received': {'view': 'invisible', 'edit': 'invisible'},
839
                'attachment_due': {'view': 'invisible', 'edit': 'invisible'},
840
                'to_be_verified': {'view': 'invisible', 'edit': 'invisible'},
841
                'verified': {'view': 'visible', 'edit': 'visible'},
842
                'published': {'view': 'visible', 'edit': 'visible'},
843
                'invalid': {'view': 'visible', 'edit': 'invisible'},
844
                'rejected': {'view': 'visible', 'edit': 'invisible'},
845
            },
846
            catalog_name='bika_setup_catalog',
847
            base_query={'inactive_state': 'active'},
848
            showOn=True,
849
        ),
850
    ),
851
852
    # Sample field
853
    ProxyField(
854
        'SamplePoint',
855
        proxy="context.getSample()",
856
        allowed_types='SamplePoint',
857
        relationship='AnalysisRequestSamplePoint',
858
        mode="rw",
859
        read_permission=View,
860
        write_permission=ModifyPortalContent,
861
        widget=ReferenceWidget(
862
            label=_("Sample Point"),
863
            description=_("Location where sample was taken"),
864
            size=20,
865
            render_own_label=True,
866
            visible={
867
                'edit': 'visible',
868
                'view': 'visible',
869
                'add': 'edit',
870
                'secondary': 'disabled',
871
                'header_table': 'visible',
872
                'sample_registered': {
873
                    'view': 'visible', 'edit': 'visible', 'add': 'edit'},
874
                'to_be_sampled': {'view': 'visible', 'edit': 'visible'},
875
                # LIMS-1159
876
                'scheduled_sampling': {'view': 'visible', 'edit': 'visible'},
877
                'sampled': {'view': 'visible', 'edit': 'visible'},
878
                'to_be_preserved': {'view': 'visible', 'edit': 'visible'},
879
                'sample_due': {'view': 'visible', 'edit': 'visible'},
880
                'sample_received': {'view': 'visible', 'edit': 'visible'},
881
                'attachment_due': {'view': 'visible', 'edit': 'visible'},
882
                'to_be_verified': {'view': 'visible', 'edit': 'visible'},
883
                'verified': {'view': 'visible', 'edit': 'invisible'},
884
                'published': {'view': 'visible', 'edit': 'invisible'},
885
                'invalid': {'view': 'visible', 'edit': 'invisible'},
886
                'rejected': {'view': 'visible', 'edit': 'invisible'},
887
            },
888
            catalog_name='bika_setup_catalog',
889
            base_query={'inactive_state': 'active'},
890
            showOn=True,
891
        ),
892
    ),
893
894
    # Sample field
895
    ProxyField(
896
        'StorageLocation',
897
        proxy="context.getSample()",
898
        allowed_types='StorageLocation',
899
        relationship='AnalysisRequestStorageLocation',
900
        mode="rw",
901
        read_permission=View,
902
        write_permission=ModifyPortalContent,
903
        widget=ReferenceWidget(
904
            label=_("Storage Location"),
905
            description=_("Location where sample is kept"),
906
            size=20,
907
            render_own_label=True,
908
            visible={
909
                'edit': 'visible',
910
                'view': 'visible',
911
                'add': 'edit',
912
                'secondary': 'disabled',
913
                'header_table': 'visible',
914
                'sample_registered':
915
                    {'view': 'visible', 'edit': 'visible', 'add': 'edit'},
916
                'to_be_sampled': {'view': 'visible', 'edit': 'visible'},
917
                'scheduled_sampling': {'view': 'visible', 'edit': 'visible'},
918
                'sampled': {'view': 'visible', 'edit': 'visible'},
919
                'to_be_preserved': {'view': 'visible', 'edit': 'visible'},
920
                'sample_due': {'view': 'visible', 'edit': 'visible'},
921
                'sample_received': {'view': 'visible', 'edit': 'visible'},
922
                'attachment_due': {'view': 'visible', 'edit': 'visible'},
923
                'to_be_verified': {'view': 'visible', 'edit': 'visible'},
924
                'verified': {'view': 'visible', 'edit': 'visible'},
925
                'published': {'view': 'visible', 'edit': 'invisible'},
926
                'invalid': {'view': 'visible', 'edit': 'invisible'},
927
                'rejected': {'view': 'visible', 'edit': 'invisible'},
928
            },
929
            catalog_name='bika_setup_catalog',
930
            base_query={'inactive_state': 'active'},
931
            showOn=True,
932
        ),
933
    ),
934
935
    StringField(
936
        'ClientOrderNumber',
937
        mode="rw",
938
        read_permission=View,
939
        write_permission=ModifyPortalContent,
940
        widget=StringWidget(
941
            label=_("Client Order Number"),
942
            description=_("The client side order number for this request"),
943
            size=20,
944
            render_own_label=True,
945
            visible={
946
                'edit': 'visible',
947
                'view': 'visible',
948
                'add': 'edit',
949
                'header_table': 'visible',
950
                'sample_registered': {
951
                    'view': 'visible', 'edit': 'visible', 'add': 'edit'},
952
                'to_be_sampled': {'view': 'visible', 'edit': 'visible'},
953
                'scheduled_sampling': {'view': 'visible', 'edit': 'visible'},
954
                'sampled': {'view': 'visible', 'edit': 'visible'},
955
                'to_be_preserved': {'view': 'visible', 'edit': 'visible'},
956
                'sample_due': {'view': 'visible', 'edit': 'visible'},
957
                'sample_received': {'view': 'visible', 'edit': 'visible'},
958
                'attachment_due': {'view': 'visible', 'edit': 'visible'},
959
                'to_be_verified': {'view': 'visible', 'edit': 'visible'},
960
                'verified': {'view': 'visible', 'edit': 'visible'},
961
                'published': {'view': 'visible', 'edit': 'invisible'},
962
                'invalid': {'view': 'visible', 'edit': 'invisible'},
963
                'rejected': {'view': 'visible', 'edit': 'invisible'},
964
            },
965
        ),
966
    ),
967
968
    # Sample field
969
    ProxyField(
970
        'ClientReference',
971
        proxy="context.getSample()",
972
        mode="rw",
973
        read_permission=View,
974
        write_permission=ModifyPortalContent,
975
        widget=StringWidget(
976
            label=_("Client Reference"),
977
            description=_("The client side reference for this request"),
978
            size=20,
979
            render_own_label=True,
980
            visible={
981
                'edit': 'visible',
982
                'view': 'visible',
983
                'add': 'edit',
984
                'secondary': 'disabled',
985
                'header_table': 'visible',
986
                'sample_registered':
987
                    {'view': 'visible', 'edit': 'visible', 'add': 'edit'},
988
                'to_be_sampled': {'view': 'visible', 'edit': 'visible'},
989
                'scheduled_sampling': {'view': 'visible', 'edit': 'visible'},
990
                'sampled': {'view': 'visible', 'edit': 'visible'},
991
                'to_be_preserved': {'view': 'visible', 'edit': 'visible'},
992
                'sample_due': {'view': 'visible', 'edit': 'visible'},
993
                'sample_received': {'view': 'visible', 'edit': 'visible'},
994
                'attachment_due': {'view': 'visible', 'edit': 'visible'},
995
                'to_be_verified': {'view': 'visible', 'edit': 'visible'},
996
                'verified': {'view': 'visible', 'edit': 'visible'},
997
                'published': {'view': 'visible', 'edit': 'invisible'},
998
                'invalid': {'view': 'visible', 'edit': 'invisible'},
999
                'rejected': {'view': 'visible', 'edit': 'invisible'},
1000
            },
1001
        ),
1002
    ),
1003
1004
    # Sample field
1005
    ProxyField(
1006
        'ClientSampleID',
1007
        proxy="context.getSample()",
1008
        mode="rw",
1009
        read_permission=View,
1010
        write_permission=ModifyPortalContent,
1011
        widget=StringWidget(
1012
            label=_("Client Sample ID"),
1013
            description=_("The client side identifier of the sample"),
1014
            size=20,
1015
            render_own_label=True,
1016
            visible={
1017
                'edit': 'visible',
1018
                'view': 'visible',
1019
                'add': 'edit',
1020
                'secondary': 'disabled',
1021
                'header_table': 'visible',
1022
                'sample_registered': {'view': 'visible', 'edit': 'visible'},
1023
                'to_be_sampled': {'view': 'visible', 'edit': 'invisible'},
1024
                'scheduled_sampling': {'view': 'visible', 'edit': 'invisible'},
1025
                'sampled': {'view': 'visible', 'edit': 'invisible'},
1026
                'to_be_preserved': {'view': 'visible', 'edit': 'invisible'},
1027
                'sample_due': {'view': 'visible', 'edit': 'invisible'},
1028
                'sample_received': {'view': 'visible', 'edit': 'invisible'},
1029
                'attachment_due': {'view': 'visible', 'edit': 'invisible'},
1030
                'to_be_verified': {'view': 'visible', 'edit': 'invisible'},
1031
                'verified': {'view': 'visible', 'edit': 'invisible'},
1032
                'published': {'view': 'visible', 'edit': 'invisible'},
1033
                'invalid': {'view': 'visible', 'edit': 'invisible'},
1034
                'rejected': {'view': 'visible', 'edit': 'invisible'},
1035
            },
1036
        ),
1037
    ),
1038
1039
    # Sample field
1040
    ProxyField(
1041
        'SamplingDeviation',
1042
        proxy="context.getSample()",
1043
        allowed_types=('SamplingDeviation',),
1044
        relationship='AnalysisRequestSamplingDeviation',
1045
        referenceClass=HoldingReference,
1046
        mode="rw",
1047
        read_permission=View,
1048
        write_permission=ModifyPortalContent,
1049
        widget=ReferenceWidget(
1050
            label=_("Sampling Deviation"),
1051
            description=_("Deviation between the sample and how it "
1052
                          "was sampled"),
1053
            size=20,
1054
            render_own_label=True,
1055
            visible={
1056
                'edit': 'visible',
1057
                'view': 'visible',
1058
                'add': 'edit',
1059
                'secondary': 'disabled',
1060
                'header_table': 'visible',
1061
                'sample_registered': {
1062
                    'view': 'visible', 'edit': 'visible', 'add': 'edit'},
1063
                'to_be_sampled': {'view': 'visible', 'edit': 'visible'},
1064
                'scheduled_sampling': {'view': 'visible', 'edit': 'visible'},
1065
                'sampled': {'view': 'visible', 'edit': 'visible'},
1066
                'to_be_preserved': {'view': 'visible', 'edit': 'visible'},
1067
                'sample_due': {'view': 'visible', 'edit': 'visible'},
1068
                'sample_received': {'view': 'visible', 'edit': 'invisible'},
1069
                'attachment_due': {'view': 'visible', 'edit': 'invisible'},
1070
                'to_be_verified': {'view': 'visible', 'edit': 'invisible'},
1071
                'verified': {'view': 'visible', 'edit': 'invisible'},
1072
                'published': {'view': 'visible', 'edit': 'invisible'},
1073
                'invalid': {'view': 'visible', 'edit': 'invisible'},
1074
                'rejected': {'view': 'visible', 'edit': 'invisible'},
1075
            },
1076
            catalog_name='bika_setup_catalog',
1077
            base_query={'inactive_state': 'active'},
1078
            showOn=True,
1079
        ),
1080
    ),
1081
1082
    # Sample field
1083
    ProxyField(
1084
        'SampleCondition',
1085
        proxy="context.getSample()",
1086
        allowed_types=('SampleCondition',),
1087
        relationship='AnalysisRequestSampleCondition',
1088
        referenceClass=HoldingReference,
1089
        mode="rw",
1090
        read_permission=View,
1091
        write_permission=ModifyPortalContent,
1092
        widget=ReferenceWidget(
1093
            label=_("Sample condition"),
1094
            description=_("The current condition of the sample"),
1095
            size=20,
1096
            render_own_label=True,
1097
            visible={
1098
                'edit': 'visible',
1099
                'view': 'visible',
1100
                'add': 'edit',
1101
                'secondary': 'disabled',
1102
                'header_table': 'visible',
1103
                'sample_registered': {
1104
                    'view': 'visible', 'edit': 'visible', 'add': 'edit'},
1105
                'to_be_sampled': {'view': 'visible', 'edit': 'visible'},
1106
                'scheduled_sampling': {'view': 'visible', 'edit': 'visible'},
1107
                'sampled': {'view': 'visible', 'edit': 'visible'},
1108
                'to_be_preserved': {'view': 'visible', 'edit': 'visible'},
1109
                'sample_due': {'view': 'visible', 'edit': 'visible'},
1110
                'sample_received': {'view': 'visible', 'edit': 'invisible'},
1111
                'attachment_due': {'view': 'visible', 'edit': 'invisible'},
1112
                'to_be_verified': {'view': 'visible', 'edit': 'invisible'},
1113
                'verified': {'view': 'visible', 'edit': 'invisible'},
1114
                'published': {'view': 'visible', 'edit': 'invisible'},
1115
                'invalid': {'view': 'visible', 'edit': 'invisible'},
1116
                'rejected': {'view': 'visible', 'edit': 'invisible'},
1117
            },
1118
            catalog_name='bika_setup_catalog',
1119
            base_query={'inactive_state': 'active'},
1120
            showOn=True,
1121
        ),
1122
    ),
1123
1124
    StringField(
1125
        'Priority',
1126
        default='3',
1127
        vocabulary=PRIORITIES,
1128
        mode='rw',
1129
        read_permission=View,
1130
        write_permission=ModifyPortalContent,
1131
        widget=PrioritySelectionWidget(
1132
            label=_('Priority'),
1133
            format='select',
1134
            visible={
1135
                'edit': 'visible',
1136
                'view': 'visible',
1137
                'add': 'edit',
1138
                'header_table': 'visible',
1139
                'sample_registered':
1140
                    {'view': 'visible', 'edit': 'visible', 'add': 'edit'},
1141
                'to_be_sampled': {'view': 'visible', 'edit': 'visible'},
1142
                'scheduled_sampling': {'view': 'visible', 'edit': 'visible'},
1143
                'sampled': {'view': 'visible', 'edit': 'visible'},
1144
                'to_be_preserved': {'view': 'visible', 'edit': 'visible'},
1145
                'sample_due': {'view': 'visible', 'edit': 'visible'},
1146
                'sample_received': {'view': 'visible', 'edit': 'visible'},
1147
                'attachment_due': {'view': 'visible', 'edit': 'visible'},
1148
                'to_be_verified': {'view': 'visible', 'edit': 'visible'},
1149
                'verified': {'view': 'visible', 'edit': 'visible'},
1150
                'published': {'view': 'visible', 'edit': 'invisible'},
1151
                'invalid': {'view': 'visible', 'edit': 'invisible'},
1152
                'rejected': {'view': 'visible', 'edit': 'invisible'},
1153
            },
1154
        ),
1155
    ),
1156
    # Sample field
1157
    ProxyField(
1158
        'EnvironmentalConditions',
1159
        proxy="context.getSample()",
1160
        mode="rw",
1161
        read_permission=View,
1162
        write_permission=ModifyPortalContent,
1163
        widget=StringWidget(
1164
            label=_("Environmental conditions"),
1165
            description=_("The environmental condition during sampling"),
1166
            visible={
1167
                'edit': 'visible',
1168
                'view': 'visible',
1169
                'add': 'edit',
1170
                'header_table': 'prominent',
1171
                'sample_registered': {
1172
                    'view': 'visible', 'edit': 'visible', 'add': 'edit'},
1173
                'to_be_sampled': {'view': 'visible', 'edit': 'visible'},
1174
                'scheduled_sampling': {'view': 'visible', 'edit': 'visible'},
1175
                'sampled': {'view': 'visible', 'edit': 'visible'},
1176
                'to_be_preserved': {'view': 'visible', 'edit': 'visible'},
1177
                'sample_received': {'view': 'visible', 'edit': 'visible'},
1178
                'attachment_due': {'view': 'visible', 'edit': 'visible'},
1179
                'to_be_verified': {'view': 'visible', 'edit': 'visible'},
1180
                'verified': {'view': 'visible', 'edit': 'invisible'},
1181
                'published': {'view': 'visible', 'edit': 'invisible'},
1182
                'invalid': {'view': 'visible', 'edit': 'invisible'},
1183
                'rejected': {'view': 'visible', 'edit': 'invisible'},
1184
            },
1185
            render_own_label=True,
1186
            size=20,
1187
        ),
1188
    ),
1189
1190
    ReferenceField(
1191
        'DefaultContainerType',
1192
        allowed_types=('ContainerType',),
1193
        relationship='AnalysisRequestContainerType',
1194
        referenceClass=HoldingReference,
1195
        mode="rw",
1196
        read_permission=View,
1197
        write_permission=ModifyPortalContent,
1198
        widget=ReferenceWidget(
1199
            label=_("Default Container"),
1200
            description=_("Default container for new sample partitions"),
1201
            size=20,
1202
            render_own_label=True,
1203
            visible={
1204
                'edit': 'visible',
1205
                'view': 'visible',
1206
                'add': 'edit',
1207
                'secondary': 'disabled',
1208
                'header_table': 'visible',
1209
                'sample_registered': {
1210
                    'view': 'visible', 'edit': 'visible', 'add': 'edit'},
1211
                'to_be_sampled': {'view': 'visible', 'edit': 'invisible'},
1212
                'scheduled_sampling': {'view': 'visible', 'edit': 'invisible'},
1213
                'sampled': {'view': 'visible', 'edit': 'invisible'},
1214
                'to_be_preserved': {'view': 'visible', 'edit': 'invisible'},
1215
                'sample_due': {'view': 'visible', 'edit': 'invisible'},
1216
                'sample_received': {'view': 'visible', 'edit': 'invisible'},
1217
                'attachment_due': {'view': 'visible', 'edit': 'invisible'},
1218
                'to_be_verified': {'view': 'visible', 'edit': 'invisible'},
1219
                'verified': {'view': 'visible', 'edit': 'invisible'},
1220
                'published': {'view': 'visible', 'edit': 'invisible'},
1221
                'invalid': {'view': 'visible', 'edit': 'invisible'},
1222
                'rejected': {'view': 'visible', 'edit': 'invisible'},
1223
            },
1224
            catalog_name='bika_setup_catalog',
1225
            base_query={'inactive_state': 'active'},
1226
            showOn=True,
1227
        ),
1228
    ),
1229
1230
    # Sample field
1231
    ProxyField(
1232
        'AdHoc',
1233
        proxy="context.getSample()",
1234
        default=False,
1235
        mode="rw",
1236
        read_permission=View,
1237
        write_permission=ModifyPortalContent,
1238
        widget=BooleanWidget(
1239
            label=_("Sampled AdHoc"),
1240
            description=_("Was the sample taken in non-scheduled matter, " \
1241
                          "e.g. out of a recurring sampling schedule?"),
1242
            render_own_label=True,
1243
            visible={
1244
                'edit': 'visible',
1245
                'view': 'visible',
1246
                'add': 'edit',
1247
                'secondary': 'disabled',
1248
                'header_table': 'visible',
1249
                'sample_registered':
1250
                    {'view': 'visible', 'edit': 'visible', 'add': 'edit'},
1251
                'to_be_sampled': {'view': 'visible', 'edit': 'invisible'},
1252
                'scheduled_sampling': {'view': 'visible', 'edit': 'invisible'},
1253
                'sampled': {'view': 'visible', 'edit': 'invisible'},
1254
                'to_be_preserved': {'view': 'visible', 'edit': 'invisible'},
1255
                'sample_due': {'view': 'visible', 'edit': 'invisible'},
1256
                'sample_received': {'view': 'visible', 'edit': 'invisible'},
1257
                'attachment_due': {'view': 'visible', 'edit': 'invisible'},
1258
                'to_be_verified': {'view': 'visible', 'edit': 'invisible'},
1259
                'verified': {'view': 'visible', 'edit': 'invisible'},
1260
                'published': {'view': 'visible', 'edit': 'invisible'},
1261
                'invalid': {'view': 'visible', 'edit': 'invisible'},
1262
                'rejected': {'view': 'visible', 'edit': 'invisible'},
1263
            },
1264
        ),
1265
    ),
1266
1267
    # Sample field
1268
    ProxyField(
1269
        'Composite',
1270
        proxy="context.getSample()",
1271
        default=False,
1272
        mode="rw",
1273
        read_permission=View,
1274
        write_permission=ModifyPortalContent,
1275
        widget=BooleanWidget(
1276
            label=_("Composite"),
1277
            render_own_label=True,
1278
            visible={
1279
                'edit': 'visible',
1280
                'view': 'visible',
1281
                'add': 'edit',
1282
                'secondary': 'disabled',
1283
                'header_table': 'visible',
1284
                'sample_registered': {
1285
                    'view': 'visible', 'edit': 'visible', 'add': 'edit'},
1286
                'to_be_sampled': {'view': 'visible', 'edit': 'visible'},
1287
                'scheduled_sampling': {'view': 'visible', 'edit': 'visible'},
1288
                'sampled': {'view': 'visible', 'edit': 'visible'},
1289
                'to_be_preserved': {'view': 'visible', 'edit': 'visible'},
1290
                'sample_due': {'view': 'visible', 'edit': 'visible'},
1291
                'sample_received': {'view': 'visible', 'edit': 'visible'},
1292
                'attachment_due': {'view': 'visible', 'edit': 'visible'},
1293
                'to_be_verified': {'view': 'visible', 'edit': 'visible'},
1294
                'verified': {'view': 'visible', 'edit': 'invisible'},
1295
                'published': {'view': 'visible', 'edit': 'invisible'},
1296
                'invalid': {'view': 'visible', 'edit': 'invisible'},
1297
                'rejected': {'view': 'visible', 'edit': 'invisible'},
1298
            },
1299
        ),
1300
    ),
1301
1302
    BooleanField(
1303
        'InvoiceExclude',
1304
        default=False,
1305
        mode="rw",
1306
        read_permission=View,
1307
        write_permission=ModifyPortalContent,
1308
        widget=BooleanWidget(
1309
            label=_("Invoice Exclude"),
1310
            description=_("Should the analyses be excluded from the invoice?"),
1311
            render_own_label=True,
1312
            visible={
1313
                'edit': 'visible',
1314
                'view': 'visible',
1315
                'add': 'edit',
1316
                'header_table': 'visible',
1317
                'sample_registered': {
1318
                    'view': 'visible', 'edit': 'visible', 'add': 'edit'},
1319
                'to_be_sampled': {'view': 'visible', 'edit': 'visible'},
1320
                'scheduled_sampling': {'view': 'visible', 'edit': 'visible'},
1321
                'sampled': {'view': 'visible', 'edit': 'visible'},
1322
                'to_be_preserved': {'view': 'visible', 'edit': 'visible'},
1323
                'sample_due': {'view': 'visible', 'edit': 'visible'},
1324
                'sample_received': {'view': 'visible', 'edit': 'invisible'},
1325
                'attachment_due': {'view': 'visible', 'edit': 'invisible'},
1326
                'to_be_verified': {'view': 'visible', 'edit': 'invisible'},
1327
                'verified': {'view': 'visible', 'edit': 'invisible'},
1328
                'published': {'view': 'visible', 'edit': 'invisible'},
1329
                'invalid': {'view': 'visible', 'edit': 'invisible'},
1330
                'rejected': {'view': 'visible', 'edit': 'invisible'},
1331
            },
1332
        ),
1333
    ),
1334
1335
    ARAnalysesField(
1336
        'Analyses',
1337
        required=1,
1338
        mode="rw",
1339
        read_permission=View,
1340
        write_permission=ModifyPortalContent,
1341
        widget=ComputedWidget(
1342
            visible={
1343
                'edit': 'invisible',
1344
                'view': 'invisible',
1345
                'sample_registered': {
1346
                    'view': 'visible', 'edit': 'visible', 'add': 'invisible'},
1347
            }
1348
        ),
1349
    ),
1350
1351
    ReferenceField(
1352
        'Attachment',
1353
        multiValued=1,
1354
        allowed_types=('Attachment',),
1355
        referenceClass=HoldingReference,
1356
        relationship='AnalysisRequestAttachment',
1357
        mode="rw",
1358
        read_permission=View,
1359
        write_permission=ModifyPortalContent,
1360
        widget=ComputedWidget(
1361
            visible={
1362
                'edit': 'invisible',
1363
                'view': 'invisible',
1364
            },
1365
        )
1366
    ),
1367
1368
    # This is a virtual field and handled only by AR Add View to allow multi
1369
    # attachment upload in AR Add. It should never contain an own value!
1370
    FileField(
1371
        '_ARAttachment',
1372
        widget=FileWidget(
1373
            label=_("Attachment"),
1374
            description=_("Add one or more attachments to describe the "
1375
                          "sample in this analysis request, or to specify "
1376
                          "your request."),
1377
            render_own_label=True,
1378
            visible={
1379
                'view': 'invisible',
1380
                'add': 'edit',
1381
                'header_table': 'invisible',
1382
            },
1383
        )
1384
    ),
1385
1386
    ReferenceField(
1387
        'Invoice',
1388
        vocabulary_display_path_bound=sys.maxsize,
1389
        allowed_types=('Invoice',),
1390
        referenceClass=HoldingReference,
1391
        relationship='AnalysisRequestInvoice',
1392
        mode="rw",
1393
        read_permission=View,
1394
        write_permission=ModifyPortalContent,
1395
        widget=ComputedWidget(
1396
            visible={
1397
                'edit': 'invisible',
1398
                'view': 'invisible',
1399
            },
1400
        )
1401
    ),
1402
    # This field is a mirror of a field in Sample with the same name
1403
    ProxyField(
1404
        'DateReceived',
1405
        proxy="context.getSample()",
1406
        mode="rw",
1407
        read_permission=View,
1408
        write_permission=ModifyPortalContent,
1409
        widget=DateTimeWidget(
1410
            label=_("Date Sample Received"),
1411
            show_time=True,
1412
            description=_("The date when the sample was received"),
1413
            render_own_label=True,
1414
            visible={
1415
                'edit': 'invisible',
1416
                'view': 'visible',
1417
                'header_table': 'visible',
1418
            },
1419
        ),
1420
    ),
1421
    ComputedField(
1422
        'DatePublished',
1423
        mode="r",
1424
        read_permission=View,
1425
        expression="here.getDatePublished().strftime('%Y-%m-%d %H:%M %p') if here.getDatePublished() else ''",
1426
        widget=DateTimeWidget(
1427
            label=_("Date Published"),
1428
            visible={
1429
                'edit': 'visible',
1430
                'view': 'visible',
1431
                'add': 'invisible',
1432
                'secondary': 'invisible',
1433
                'header_table': 'visible',
1434
                'sample_registered': {
1435
                    'view': 'invisible', 'edit': 'invisible'},
1436
                'to_be_sampled': {'view': 'invisible', 'edit': 'invisible'},
1437
                'scheduled_sampling': {
1438
                    'view': 'invisible', 'edit': 'invisible'},
1439
                'sampled': {'view': 'invisible', 'edit': 'invisible'},
1440
                'to_be_preserved': {'view': 'invisible', 'edit': 'invisible'},
1441
                'sample_due': {'view': 'invisible', 'edit': 'invisible'},
1442
                'sample_received': {'view': 'invisible', 'edit': 'invisible'},
1443
                'attachment_due': {'view': 'invisible', 'edit': 'invisible'},
1444
                'to_be_verified': {'view': 'invisible', 'edit': 'invisible'},
1445
                'verified': {'view': 'invisible', 'edit': 'invisible'},
1446
                'published': {'view': 'visible', 'edit': 'invisible'},
1447
                'invalid': {'view': 'visible', 'edit': 'invisible'},
1448
                'rejected': {'view': 'invisible', 'edit': 'invisible'},
1449
            },
1450
        ),
1451
    ),
1452
1453
    RemarksField(
1454
        'Remarks',
1455
        searchable=True,
1456
        widget=RemarksWidget(
1457
            label=_("Remarks"),
1458
            description=_("Remarks and comments for this request"),
1459
            render_own_label=True,
1460
            visible={
1461
                'edit': 'visible',
1462
                'view': 'visible',
1463
                'add': 'edit'
1464
            },
1465
        ),
1466
    ),
1467
1468
    FixedPointField(
1469
        'MemberDiscount',
1470
        default_method='getDefaultMemberDiscount',
1471
        mode="rw",
1472
        read_permission=View,
1473
        write_permission=ModifyPortalContent,
1474
        widget=DecimalWidget(
1475
            label=_("Member discount %"),
1476
            description=_("Enter percentage value eg. 33.0"),
1477
            render_own_label=True,
1478
            visible={
1479
                'edit': 'visible',
1480
                'view': 'visible',
1481
                'add': 'invisible',
1482
                'sample_registered': {
1483
                    'view': 'invisible', 'edit': 'invisible'},
1484
            },
1485
        ),
1486
    ),
1487
    # TODO-catalog: move all these computed fields to methods
1488
    ComputedField(
1489
        'ClientUID',
1490
        expression='here.aq_parent.UID()',
1491
        widget=ComputedWidget(
1492
            visible=False,
1493
        ),
1494
    ),
1495
1496
    ComputedField(
1497
        'SampleTypeTitle',
1498
        expression="here.getSampleType().Title() if here.getSampleType() "
1499
                   "else ''",
1500
        widget=ComputedWidget(
1501
            visible=False,
1502
        ),
1503
    ),
1504
1505
    ComputedField(
1506
        'SamplePointTitle',
1507
        expression="here.getSamplePoint().Title() if here.getSamplePoint() "
1508
                   "else ''",
1509
        widget=ComputedWidget(
1510
            visible=False,
1511
        ),
1512
    ),
1513
1514
    ComputedField(
1515
        'SampleUID',
1516
        expression="here.getSample() and here.getSample().UID() or ''",
1517
        widget=ComputedWidget(
1518
            visible=False,
1519
        ),
1520
    ),
1521
1522
    ComputedField(
1523
        'SampleID',
1524
        expression="here.getSample() and here.getSample().getId() or ''",
1525
        widget=ComputedWidget(
1526
            visible=False,
1527
        ),
1528
    ),
1529
1530
    ComputedField(
1531
        'ContactUID',
1532
        expression="here.getContact() and here.getContact().UID() or ''",
1533
        widget=ComputedWidget(
1534
            visible=False,
1535
        ),
1536
    ),
1537
1538
    ComputedField(
1539
        'ProfilesUID',
1540
        expression="[p.UID() for p in here.getProfiles()] " \
1541
                   "if here.getProfiles() else []",
1542
        widget=ComputedWidget(
1543
            visible=False,
1544
        ),
1545
    ),
1546
1547
    ComputedField(
1548
        'Invoiced',
1549
        expression='here.getInvoice() and True or False',
1550
        default=False,
1551
        widget=ComputedWidget(
1552
            visible=False,
1553
        ),
1554
    ),
1555
    ComputedField(
1556
        'ReceivedBy',
1557
        expression='here.getReceivedBy()',
1558
        default='',
1559
        widget=ComputedWidget(visible=False,),
1560
    ),
1561
    ComputedField(
1562
        'CreatorFullName',
1563
        expression="here._getCreatorFullName()",
1564
        widget=ComputedWidget(visible=False),
1565
    ),
1566
    ComputedField(
1567
        'CreatorEmail',
1568
        expression="here._getCreatorEmail()",
1569
        widget=ComputedWidget(visible=False),
1570
    ),
1571
    ComputedField(
1572
        'SamplingRoundUID',
1573
        expression="here.getSamplingRound().UID() " \
1574
                   "if here.getSamplingRound() else ''",
1575
        widget=ComputedWidget(visible=False),
1576
    ),
1577
    ComputedField(
1578
        'SampleURL',
1579
        expression="here.getSample().absolute_url_path() " \
1580
                   "if here.getSample() else ''",
1581
        widget=ComputedWidget(visible=False),
1582
    ),
1583
    ComputedField(
1584
        'SamplerFullName',
1585
        expression="here._getSamplerFullName()",
1586
        widget=ComputedWidget(visible=False),
1587
    ),
1588
    ComputedField(
1589
        'SamplerEmail',
1590
        expression="here._getSamplerEmail()",
1591
        widget=ComputedWidget(visible=False),
1592
    ),
1593
    ComputedField(
1594
        'BatchID',
1595
        expression="here.getBatch().getId() if here.getBatch() else ''",
1596
        widget=ComputedWidget(visible=False),
1597
    ),
1598
    ComputedField(
1599
        'BatchURL',
1600
        expression="here.getBatch().absolute_url_path() " \
1601
                   "if here.getBatch() else ''",
1602
        widget=ComputedWidget(visible=False),
1603
    ),
1604
    ComputedField(
1605
        'ClientUID',
1606
        expression="here.getClient().UID() if here.getClient() else ''",
1607
        widget=ComputedWidget(visible=False),
1608
    ),
1609
    ComputedField(
1610
        'ClientTitle',
1611
        expression="here.getClient().Title() if here.getClient() else ''",
1612
        widget=ComputedWidget(visible=False),
1613
    ),
1614
    ComputedField(
1615
        'ClientURL',
1616
        expression="here.getClient().absolute_url_path() " \
1617
                   "if here.getClient() else ''",
1618
        widget=ComputedWidget(visible=False),
1619
    ),
1620
    ComputedField(
1621
        'ContactUsername',
1622
        expression="here.getContact().getUsername() " \
1623
                   "if here.getContact() else ''",
1624
        widget=ComputedWidget(visible=False),
1625
    ),
1626
    ComputedField(
1627
        'ContactFullName',
1628
        expression="here.getContact().getFullname() " \
1629
                   "if here.getContact() else ''",
1630
        widget=ComputedWidget(visible=False),
1631
    ),
1632
    ComputedField(
1633
        'ContactEmail',
1634
        expression="here.getContact().getEmailAddress() " \
1635
                   "if here.getContact() else ''",
1636
        widget=ComputedWidget(visible=False),
1637
    ),
1638
    ComputedField(
1639
        'SampleTypeUID',
1640
        expression="here.getSampleType().UID() " \
1641
                   "if here.getSampleType() else ''",
1642
        widget=ComputedWidget(visible=False),
1643
    ),
1644
    ComputedField(
1645
        'SamplePointUID',
1646
        expression="here.getSamplePoint().UID() " \
1647
                   "if here.getSamplePoint() else ''",
1648
        widget=ComputedWidget(visible=False),
1649
    ),
1650
    ComputedField(
1651
        'StorageLocationUID',
1652
        expression="here.getStorageLocation().UID() " \
1653
                   "if here.getStorageLocation() else ''",
1654
        widget=ComputedWidget(visible=False),
1655
    ),
1656
    ComputedField(
1657
        'ProfilesURL',
1658
        expression="[p.absolute_url_path() for p in here.getProfiles()] " \
1659
                   "if here.getProfiles() else []",
1660
        widget=ComputedWidget(visible=False),
1661
    ),
1662
    ComputedField(
1663
        'ProfilesTitle',
1664
        expression="[p.Title() for p in here.getProfiles()] " \
1665
                   "if here.getProfiles() else []",
1666
        widget=ComputedWidget(visible=False),
1667
    ),
1668
    ComputedField(
1669
        'ProfilesTitleStr',
1670
        expression="', '.join([p.Title() for p in here.getProfiles()]) " \
1671
                   "if here.getProfiles() else ''",
1672
        widget=ComputedWidget(visible=False),
1673
    ),
1674
    ComputedField(
1675
        'TemplateUID',
1676
        expression="here.getTemplate().UID() if here.getTemplate() else ''",
1677
        widget=ComputedWidget(visible=False),
1678
    ),
1679
    ComputedField(
1680
        'TemplateURL',
1681
        expression="here.getTemplate().absolute_url_path() " \
1682
                   "if here.getTemplate() else ''",
1683
        widget=ComputedWidget(visible=False),
1684
    ),
1685
    ComputedField(
1686
        'TemplateTitle',
1687
        expression="here.getTemplate().Title() if here.getTemplate() else ''",
1688
        widget=ComputedWidget(visible=False),
1689
    ),
1690
1691
    ReferenceField(
1692
        'ParentAnalysisRequest',
1693
        allowed_types=('AnalysisRequest',),
1694
        relationship='AnalysisRequestParentAnalysisRequest',
1695
        referenceClass=HoldingReference,
1696
        mode="rw",
1697
        read_permission=View,
1698
        write_permission=ModifyPortalContent,
1699
        widget=ReferenceWidget(
1700
            visible=False,
1701
        ),
1702
    ),
1703
1704
    # The Analysis Request the current Analysis Request comes from because of
1705
    # an invalidation of the former
1706
    ReferenceField(
1707
        'Invalidated',
1708
        allowed_types=('AnalysisRequest',),
1709
        relationship='AnalysisRequestRetracted',
1710
        referenceClass=HoldingReference,
1711
        mode="rw",
1712
        read_permission=View,
1713
        write_permission=ModifyPortalContent,
1714
        widget=ReferenceWidget(
1715
            visible=False,
1716
        ),
1717
    ),
1718
1719
    # The Analysis Request that was automatically generated due to the
1720
    # invalidation of the current Analysis Request
1721
    ComputedField(
1722
        'Retest',
1723
        expression="here.get_retest()",
1724
        widget=ComputedWidget(visible=False)
1725
    ),
1726
1727
    # For comments or results interpretation
1728
    # Old one, to be removed because of the incorporation of
1729
    # ResultsInterpretationDepts (due to LIMS-1628)
1730
    TextField(
1731
        'ResultsInterpretation',
1732
        mode="rw",
1733
        default_content_type='text/html',
1734
        # Input content type for the textfield
1735
        default_output_type='text/x-html-safe',
1736
        # getResultsInterpretation returns a str with html tags
1737
        # to conserve the txt format in the report.
1738
        read_permission=View,
1739
        write_permission=ModifyPortalContent,
1740
        widget=RichWidget(
1741
            description=_("Comments or results interpretation"),
1742
            label=_("Results Interpretation"),
1743
            size=10,
1744
            allow_file_upload=False,
1745
            default_mime_type='text/x-rst',
1746
            output_mime_type='text/x-html',
1747
            rows=3,
1748
            visible=False),
1749
    ),
1750
1751
    RecordsField('ResultsInterpretationDepts',
1752
                 subfields=('uid',
1753
                            'richtext'),
1754
                 subfield_labels={'uid': _('Department'),
1755
                                  'richtext': _('Results Interpreation')},
1756
                 widget=RichWidget(visible=False),
1757
                 ),
1758
    # Custom settings for the assigned analysis services
1759
    # https://jira.bikalabs.com/browse/LIMS-1324
1760
    # Fields:
1761
    #   - uid: Analysis Service UID
1762
    #   - hidden: True/False. Hide/Display in results reports
1763
    RecordsField('AnalysisServicesSettings',
1764
                 required=0,
1765
                 subfields=('uid', 'hidden',),
1766
                 widget=ComputedWidget(visible=False),
1767
                 ),
1768
    StringField(
1769
        'Printed',
1770
        mode="rw",
1771
        read_permission=View,
1772
        widget=StringWidget(
1773
            label=_("Printed"),
1774
            description=_("Indicates if the last ARReport is printed,"),
1775
            visible={'view': 'invisible',
1776
                     'edit': 'invisible'},
1777
        ),
1778
    ),
1779
)
1780
)
1781
1782
1783
# Some schema rearrangement
1784
schema['title'].required = False
1785
schema['id'].widget.visible = {
1786
    'edit': 'invisible',
1787
    'view': 'invisible',
1788
}
1789
schema['title'].widget.visible = {
1790
    'edit': 'invisible',
1791
    'view': 'invisible',
1792
}
1793
schema.moveField('Client', before='Contact')
1794
schema.moveField('ResultsInterpretation', pos='bottom')
1795
schema.moveField('ResultsInterpretationDepts', pos='bottom')
1796
1797
1798
class AnalysisRequest(BaseFolder):
1799
    implements(IAnalysisRequest)
1800
    security = ClassSecurityInfo()
1801
    displayContentsTab = False
1802
    schema = schema
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable schema does not seem to be defined.
Loading history...
1803
1804
    _at_rename_after_creation = True
1805
1806
    def _renameAfterCreation(self, check_auto_id=False):
1807
        from bika.lims.idserver import renameAfterCreation
1808
        renameAfterCreation(self)
1809
1810
    def _getCatalogTool(self):
1811
        from bika.lims.catalog import getCatalog
1812
        return getCatalog(self)
1813
1814
    def Title(self):
1815
        """ Return the Request ID as title """
1816
        return self.getId()
1817
1818
    def sortable_title(self):
1819
        """
1820
        Some lists expects this index
1821
        """
1822
        return self.getId()
1823
1824
    def Description(self):
1825
        """Returns searchable data as Description"""
1826
        descr = " ".join((self.getId(), self.aq_parent.Title()))
1827
        return safe_unicode(descr).encode('utf-8')
1828
1829
    def getClient(self):
1830
        if self.aq_parent.portal_type == 'Client':
1831
            return self.aq_parent
1832
        if self.aq_parent.portal_type == 'Batch':
1833
            return self.aq_parent.getClient()
1834
        return ''
1835
1836
    def getClientPath(self):
1837
        return "/".join(self.aq_parent.getPhysicalPath())
1838
1839
    def getProfilesTitle(self):
1840
        return [profile.Title() for profile in self.getProfiles()]
1841
1842
    def setPublicationSpecification(self, value):
1843
        """Never contains a value; this field is here for the UI." \
1844
        """
1845
        return value
1846
1847
    def getAnalysisService(self):
1848
        proxies = self.getAnalyses(full_objects=False)
1849
        value = set()
1850
        for proxy in proxies:
1851
            value.add(proxy.Title)
1852
        return list(value)
1853
1854
    def getAnalysts(self):
1855
        proxies = self.getAnalyses(full_objects=True)
1856
        value = []
1857
        for proxy in proxies:
1858
            val = proxy.getAnalyst()
1859
            if val not in value:
1860
                value.append(val)
1861
        return value
1862
1863
    def getDistrict(self):
1864
        client = self.aq_parent
1865
        return client.getDistrict()
1866
1867
    def getProvince(self):
1868
        client = self.aq_parent
1869
        return client.getProvince()
1870
1871
    @security.public
1872
    def getBatch(self):
1873
        # The parent type may be "Batch" during ar_add.
1874
        # This function fills the hidden field in ar_add.pt
1875
        if self.aq_parent.portal_type == 'Batch':
1876
            return self.aq_parent
1877
        else:
1878
            return self.Schema()['Batch'].get(self)
1879
1880
    @security.public
1881
    def getBatchUID(self):
1882
        batch = self.getBatch()
1883
        if batch:
1884
            return batch.UID()
1885
1886
    @security.public
1887
    def setBatch(self, value=None):
1888
        original_value = self.Schema().getField('Batch').get(self)
1889
        if original_value != value:
1890
            self.Schema().getField('Batch').set(self, value)
1891
            self._reindexAnalyses(['getBatchUID'], False)
1892
1893
    def getDefaultMemberDiscount(self):
1894
        """Compute default member discount if it applies
1895
        """
1896
        if hasattr(self, 'getMemberDiscountApplies'):
1897
            if self.getMemberDiscountApplies():
1898
                settings = self.bika_setup
1899
                return settings.getMemberDiscount()
1900
            else:
1901
                return "0.00"
1902
1903
    @security.public
1904
    def getAnalysesNum(self):
1905
        """ Returns an array with the number of analyses for the current AR in
1906
            different statuses, like follows:
1907
                [verified, total, not_submitted, to_be_verified]
1908
        """
1909
        an_nums = [0, 0, 0, 0]
1910
        for analysis in self.getAnalyses():
1911
            review_state = analysis.review_state
1912
            if review_state in ['retracted', 'rejected']:
1913
                # Discard retracted analyses
1914
                continue
1915
1916
            if not api.is_active(analysis):
1917
                # Discard non-active analyses
1918
                continue
1919
1920
            analysis_object = api.get_object(analysis)
1921
            actions = getReviewHistoryActionsList(analysis_object)
1922
            if 'verify' in actions:
1923
                # Assume the "last" state of analysis is verified
1924
                index = 0
1925
1926
            elif 'submit' not in actions or review_state != 'to_be_verified':
1927
                # Assume the "first" state of analysis is results_pending
1928
                index = 2
1929
1930
            else:
1931
                index = 3
1932
            an_nums[index] += 1
1933
            an_nums[1] += 1
1934
        return an_nums
1935
1936
    @security.public
1937
    def getResponsible(self):
1938
        """Return all manager info of responsible departments
1939
        """
1940
        managers = {}
1941
        for department in self.getDepartments():
1942
            manager = department.getManager()
1943
            if manager is None:
1944
                continue
1945
            manager_id = manager.getId()
1946
            if manager_id not in managers:
1947
                managers[manager_id] = {}
1948
                managers[manager_id]['salutation'] = safe_unicode(
1949
                    manager.getSalutation())
1950
                managers[manager_id]['name'] = safe_unicode(
1951
                    manager.getFullname())
1952
                managers[manager_id]['email'] = safe_unicode(
1953
                    manager.getEmailAddress())
1954
                managers[manager_id]['phone'] = safe_unicode(
1955
                    manager.getBusinessPhone())
1956
                managers[manager_id]['job_title'] = safe_unicode(
1957
                    manager.getJobTitle())
1958
                if manager.getSignature():
1959
                    managers[manager_id]['signature'] = \
1960
                        '{}/Signature'.format(manager.absolute_url())
1961
                else:
1962
                    managers[manager_id]['signature'] = False
1963
                managers[manager_id]['departments'] = ''
1964
            mngr_dept = managers[manager_id]['departments']
1965
            if mngr_dept:
1966
                mngr_dept += ', '
1967
            mngr_dept += safe_unicode(department.Title())
1968
            managers[manager_id]['departments'] = mngr_dept
1969
        mngr_keys = managers.keys()
1970
        mngr_info = {'ids': mngr_keys, 'dict': managers}
1971
1972
        return mngr_info
1973
1974
    @security.public
1975
    def getManagers(self):
1976
        """Return all managers of responsible departments
1977
        """
1978
        manager_ids = []
1979
        manager_list = []
1980
        for department in self.getDepartments():
1981
            manager = department.getManager()
1982
            if manager is None:
1983
                continue
1984
            manager_id = manager.getId()
1985
            if manager_id not in manager_ids:
1986
                manager_ids.append(manager_id)
1987
                manager_list.append(manager)
1988
        return manager_list
1989
1990
    def getDueDate(self):
1991
        """Returns the earliest due date of the analyses this Analysis Request
1992
        contains."""
1993
        due_dates = map(lambda an: an.getDueDate, self.getAnalyses())
1994
        return due_dates and min(due_dates) or None
1995
1996
    security.declareProtected(View, 'getLate')
1997
1998
    def getLate(self):
1999
        """Return True if there is at least one late analysis in this Request
2000
        """
2001
        for analysis in self.getAnalyses():
2002
            if analysis.review_state == "retracted":
2003
                continue
2004
            analysis_obj = api.get_object(analysis)
2005
            if analysis_obj.isLateAnalysis():
2006
                return True
2007
        return False
2008
2009
    def getPrinted(self):
2010
        """ returns "0", "1" or "2" to indicate Printed state.
2011
            0 -> Never printed.
2012
            1 -> Printed after last publish
2013
            2 -> Printed but republished afterwards.
2014
        """
2015
        workflow = getToolByName(self, 'portal_workflow')
2016
        review_state = workflow.getInfoFor(self, 'review_state', '')
2017
        if review_state not in ['published']:
2018
            return "0"
2019
        report_list = sorted(self.objectValues('ARReport'),
2020
                             key=lambda report: report.getDatePublished())
2021
        if not report_list:
2022
            return "0"
2023
        last_report = report_list[-1]
2024
        if last_report.getDatePrinted():
2025
            return "1"
2026
        else:
2027
            for report in report_list:
2028
                if report.getDatePrinted():
2029
                    return "2"
2030
        return "0"
2031
2032
    def printLastReport(self):
2033
        """Setting Printed Time of the last report, so its Printed value will be 1
2034
        """
2035
        workflow = getToolByName(self, 'portal_workflow')
2036
        review_state = workflow.getInfoFor(self, 'review_state', '')
2037
        if review_state not in ['published']:
2038
            return
2039
        last_report = sorted(self.objectValues('ARReport'),
2040
                             key=lambda report: report.getDatePublished())[-1]
2041
        if last_report and not last_report.getDatePrinted():
2042
            last_report.setDatePrinted(DateTime())
2043
            self.reindexObject(idxs=['getPrinted'])
2044
2045
    security.declareProtected(View, 'getBillableItems')
2046
2047
    def getBillableItems(self):
2048
        """Returns the items to be billed
2049
        """
2050
        def get_keywords_set(profiles):
2051
            keys = list()
2052
            for profile in profiles:
2053
                keys += map(lambda s: s.getKeyword(), profile.getService())
2054
            return set(keys)
2055
2056
        # Profiles with a fixed price, regardless of their analyses
2057
        profiles = self.getProfiles()
2058
        billable_items = filter(lambda pr: pr.getUseAnalysisProfilePrice(),
2059
                                 profiles)
2060
        # Profiles w/o a fixed price. The price is the sum of the individual
2061
        # price for each analysis
2062
        non_billable = filter(lambda p: p not in billable_items, profiles)
2063
        billable_keys = get_keywords_set(non_billable) - \
2064
                        get_keywords_set(billable_items)
2065
2066
        # Get the analyses to be billed
2067
        exclude_rs = ['retracted', 'rejected']
2068
        for analysis in self.getAnalyses(cancellation_state="active"):
2069
            if analysis.review_state in exclude_rs:
2070
                continue
2071
            if analysis.getKeyword not in billable_keys:
2072
                continue
2073
            billable_items.append(api.get_object(analysis))
2074
2075
        # Return the analyses that need to be billed individually, together with
2076
        # the profiles with a fixed price
2077
        return billable_items
2078
2079
    # TODO Cleanup - Remove this function, only used in invoice and too complex
2080
    def getServicesAndProfiles(self):
2081
        """This function gets all analysis services and all profiles and removes
2082
        the services belonging to a profile.
2083
2084
        :returns: a tuple of three lists, where the first list contains the
2085
        analyses and the second list the profiles.
2086
        The third contains the analyses objects used by the profiles.
2087
        """
2088
        # profile_analyses contains the profile's analyses (analysis !=
2089
        # service") objects to obtain
2090
        # the correct price later
2091
        exclude_rs = ['retracted', 'rejected']
2092
        analyses = filter(lambda an: an.review_state not in exclude_rs,
2093
                          self.getAnalyses(cancellation_state='active'))
2094
        analyses = map(api.get_object, analyses)
2095
        profiles = self.getProfiles()
2096
2097
        # Get the service keys from all profiles
2098
        profiles_keys = list()
2099
        for profile in profiles:
2100
            profile_keys = map(lambda s: s.getKeyword(), profile.getService())
2101
            profiles_keys.extend(profile_keys)
2102
2103
        # Extract the analyses which service is present in at least one profile
2104
        # and those not present (orphan)
2105
        profile_analyses = list()
2106
        orphan_analyses = list()
2107
        for an in analyses:
2108
            if an.getKeyword() in profiles_keys:
2109
                profile_analyses.append(an)
2110
            else:
2111
                orphan_analyses.append(an)
2112
        return analyses, profiles, profile_analyses
2113
2114
    security.declareProtected(View, 'getSubtotal')
2115
2116
    def getSubtotal(self):
2117
        """Compute Subtotal (without member discount and without vat)
2118
        """
2119
        return sum([Decimal(obj.getPrice()) for obj in self.getBillableItems()])
2120
2121
    security.declareProtected(View, 'getSubtotalVATAmount')
2122
2123
    def getSubtotalVATAmount(self):
2124
        """Compute VAT amount without member discount
2125
        """
2126
        return sum([Decimal(o.getVATAmount()) for o in self.getBillableItems()])
2127
2128
    security.declareProtected(View, 'getSubtotalTotalPrice')
2129
2130
    def getSubtotalTotalPrice(self):
2131
        """Compute the price with VAT but no member discount
2132
        """
2133
        return self.getSubtotal() + self.getSubtotalVATAmount()
2134
2135
    security.declareProtected(View, 'getDiscountAmount')
2136
2137
    def getDiscountAmount(self):
2138
        """It computes and returns the analysis service's discount amount
2139
        without VAT
2140
        """
2141
        has_client_discount = self.aq_parent.getMemberDiscountApplies()
2142
        if has_client_discount:
2143
            discount = Decimal(self.getDefaultMemberDiscount())
2144
            return Decimal(self.getSubtotal() * discount / 100)
2145
        else:
2146
            return 0
2147
2148
    def getVATAmount(self):
2149
        """It computes the VAT amount from (subtotal-discount.)*VAT/100, but
2150
        each analysis has its own VAT!
2151
2152
        :returns: the analysis request VAT amount with the discount
2153
        """
2154
        has_client_discount = self.aq_parent.getMemberDiscountApplies()
2155
        VATAmount = self.getSubtotalVATAmount()
2156
        if has_client_discount:
2157
            discount = Decimal(self.getDefaultMemberDiscount())
2158
            return Decimal((1 - discount / 100) * VATAmount)
2159
        else:
2160
            return VATAmount
2161
2162
    security.declareProtected(View, 'getTotalPrice')
2163
2164
    def getTotalPrice(self):
2165
        """It gets the discounted price from analyses and profiles to obtain the
2166
        total value with the VAT and the discount applied
2167
2168
        :returns: analysis request's total price including VATs and discounts
2169
        """
2170
        price = (self.getSubtotal() - self.getDiscountAmount() +
2171
                 self.getVATAmount())
2172
        return price
2173
2174
    getTotal = getTotalPrice
2175
2176
    security.declareProtected(ManageInvoices, 'issueInvoice')
2177
2178
    # noinspection PyUnusedLocal
2179
    def issueInvoice(self, REQUEST=None, RESPONSE=None):
2180
        """Issue invoice
2181
        """
2182
        # check for an adhoc invoice batch for this month
2183
        # noinspection PyCallingNonCallable
2184
        now = DateTime()
2185
        batch_month = now.strftime('%b %Y')
2186
        batch_title = '%s - %s' % (batch_month, 'ad hoc')
2187
        invoice_batch = None
2188
        for b_proxy in self.portal_catalog(portal_type='InvoiceBatch',
2189
                                           Title=batch_title):
2190
            invoice_batch = b_proxy.getObject()
2191
        if not invoice_batch:
2192
            # noinspection PyCallingNonCallable
2193
            first_day = DateTime(now.year(), now.month(), 1)
2194
            start_of_month = first_day.earliestTime()
2195
            last_day = first_day + 31
2196
            # noinspection PyUnresolvedReferences
2197
            while last_day.month() != now.month():
2198
                last_day -= 1
2199
            # noinspection PyUnresolvedReferences
2200
            end_of_month = last_day.latestTime()
2201
2202
            invoices = self.invoices
2203
            batch_id = invoices.generateUniqueId('InvoiceBatch')
2204
            invoice_batch = _createObjectByType("InvoiceBatch", invoices,
2205
                                                batch_id)
2206
            invoice_batch.edit(
2207
                title=batch_title,
2208
                BatchStartDate=start_of_month,
2209
                BatchEndDate=end_of_month,
2210
            )
2211
            invoice_batch.processForm()
2212
2213
        client_uid = self.getClientUID()
2214
        # Get the created invoice
2215
        invoice = invoice_batch.createInvoice(client_uid, [self, ])
2216
        invoice.setAnalysisRequest(self)
2217
        # Set the created invoice in the schema
2218
        self.Schema()['Invoice'].set(self, invoice)
2219
2220
    security.declarePublic('printInvoice')
2221
2222
    # noinspection PyUnusedLocal
2223
    def printInvoice(self, REQUEST=None, RESPONSE=None):
2224
        """Print invoice
2225
        """
2226
        invoice = self.getInvoice()
2227
        invoice_url = invoice.absolute_url()
2228
        RESPONSE.redirect('{}/invoice_print'.format(invoice_url))
2229
2230
    @deprecated("addARAttachment will be removed in senaite.core 1.3.0")
2231
    def addARAttachment(self, REQUEST=None, RESPONSE=None):
2232
        """Add the file as an attachment
2233
        """
2234
        workflow = getToolByName(self, 'portal_workflow')
2235
2236
        this_file = self.REQUEST.form['AttachmentFile_file']
2237
        if 'Analysis' in self.REQUEST.form:
2238
            analysis_uid = self.REQUEST.form['Analysis']
2239
        else:
2240
            analysis_uid = None
2241
2242
        attachmentid = self.generateUniqueId('Attachment')
2243
        attachment = _createObjectByType("Attachment", self.aq_parent,
2244
                                         attachmentid)
2245
        attachment.edit(
2246
            AttachmentFile=this_file,
2247
            AttachmentType=self.REQUEST.form.get('AttachmentType', ''),
2248
            AttachmentKeys=self.REQUEST.form['AttachmentKeys'])
2249
        attachment.processForm()
2250
        attachment.reindexObject()
2251
2252
        if analysis_uid:
2253
            tool = getToolByName(self, REFERENCE_CATALOG)
2254
            analysis = tool.lookupObject(analysis_uid)
2255
            others = analysis.getAttachment()
2256
            attachments = []
2257
            for other in others:
2258
                attachments.append(other.UID())
2259
            attachments.append(attachment.UID())
2260
            analysis.setAttachment(attachments)
2261
            if workflow.getInfoFor(analysis,
2262
                                   'review_state') == 'attachment_due':
2263
                workflow.doActionFor(analysis, 'attach')
2264
        else:
2265
            others = self.getAttachment()
2266
            attachments = []
2267
            for other in others:
2268
                attachments.append(other.UID())
2269
            attachments.append(attachment.UID())
2270
2271
            self.setAttachment(attachments)
2272
2273
        if REQUEST['HTTP_REFERER'].endswith('manage_results'):
2274
            RESPONSE.redirect('{}/manage_results'.format(self.absolute_url()))
2275
        else:
2276
            RESPONSE.redirect(self.absolute_url())
2277
2278
    @deprecated("delARAttachment will be removed in senaite.core 1.3.0")
2279
    def delARAttachment(self, REQUEST=None, RESPONSE=None):
2280
        """Delete the attachment
2281
        """
2282
        tool = getToolByName(self, REFERENCE_CATALOG)
2283
        if 'Attachment' in self.REQUEST.form:
2284
            attachment_uid = self.REQUEST.form['Attachment']
2285
            attachment = tool.lookupObject(attachment_uid)
2286
            parent_r = attachment.getRequest()
2287
            parent_a = attachment.getAnalysis()
2288
2289
            parent = parent_a if parent_a else parent_r
2290
            others = parent.getAttachment()
2291
            attachments = []
2292
            for other in others:
2293
                if not other.UID() == attachment_uid:
2294
                    attachments.append(other.UID())
2295
            parent.setAttachment(attachments)
2296
            client = attachment.aq_parent
2297
            ids = [attachment.getId(), ]
2298
            BaseFolder.manage_delObjects(client, ids, REQUEST)
2299
2300
        RESPONSE.redirect(self.REQUEST.get_header('referer'))
2301
2302
    security.declarePublic('getVerifier')
2303
2304
    @deprecated("Use getVerifiers instead")
2305
    def getVerifier(self):
2306
        """Returns the user that verified the whole Analysis Request. Since the
2307
        verification is done automatically as soon as all the analyses it
2308
        contains are verified, this function returns the user that verified the
2309
        last analysis pending.
2310
        """
2311
        wtool = getToolByName(self, 'portal_workflow')
2312
        mtool = getToolByName(self, 'portal_membership')
2313
2314
        verifier = None
2315
        # noinspection PyBroadException
2316
        try:
2317
            review_history = wtool.getInfoFor(self, 'review_history')
2318
        except:  # noqa FIXME: remove blind except!
2319
            return 'access denied'
2320
2321
        if not review_history:
2322
            return 'no history'
2323
        for items in review_history:
2324
            action = items.get('action')
2325
            if action != 'verify':
2326
                continue
2327
            actor = items.get('actor')
2328
            member = mtool.getMemberById(actor)
2329
            verifier = member.getProperty('fullname')
2330
            if verifier is None or verifier == '':
2331
                verifier = actor
2332
        return verifier
2333
2334
    @security.public
2335
    def getVerifiersIDs(self):
2336
        """Returns the ids from users that have verified at least one analysis
2337
        from this Analysis Request
2338
        """
2339
        verifiers_ids = list()
2340
        for brain in self.getAnalyses():
2341
            verifiers_ids += brain.getVerificators
2342
        return list(set(verifiers_ids))
2343
2344
    @security.public
2345
    def getVerifiers(self):
2346
        """Returns the list of lab contacts that have verified at least one
2347
        analysis from this Analysis Request
2348
        """
2349
        contacts = list()
2350
        for verifier in self.getVerifiersIDs():
2351
            user = api.get_user(verifier)
2352
            contact = api.get_user_contact(user, ["LabContact"])
2353
            if contact:
2354
                contacts.append(contact)
2355
        return contacts
2356
2357
    security.declarePublic('getContactUIDForUser')
2358
2359
    def getContactUIDForUser(self):
2360
        """get the UID of the contact associated with the authenticated user
2361
        """
2362
        mt = getToolByName(self, 'portal_membership')
2363
        user = mt.getAuthenticatedMember()
2364
        user_id = user.getUserName()
2365
        pc = getToolByName(self, 'portal_catalog')
2366
        r = pc(portal_type='Contact',
2367
               getUsername=user_id)
2368
        if len(r) == 1:
2369
            return r[0].UID
2370
2371
    security.declarePublic('current_date')
2372
2373
    def current_date(self):
2374
        """return current date
2375
        """
2376
        # noinspection PyCallingNonCallable
2377
        return DateTime()
2378
2379
    def getQCAnalyses(self, qctype=None, review_state=None):
2380
        """return the QC analyses performed in the worksheet in which, at
2381
        least, one sample of this AR is present.
2382
2383
        Depending on qctype value, returns the analyses of:
2384
2385
            - 'b': all Blank Reference Samples used in related worksheet/s
2386
            - 'c': all Control Reference Samples used in related worksheet/s
2387
            - 'd': duplicates only for samples contained in this AR
2388
2389
        If qctype==None, returns all type of qc analyses mentioned above
2390
        """
2391
        qcanalyses = []
2392
        suids = []
2393
        ans = self.getAnalyses()
2394
        wf = getToolByName(self, 'portal_workflow')
2395
        for an in ans:
2396
            an = an.getObject()
2397
            if an.getServiceUID() not in suids:
2398
                suids.append(an.getServiceUID())
2399
2400
        def valid_dup(wan):
2401
            if wan.portal_type == 'ReferenceAnalysis':
2402
                return False
2403
            an_state = wf.getInfoFor(wan, 'review_state')
2404
            return \
2405
                wan.portal_type == 'DuplicateAnalysis' \
2406
                and wan.getRequestID() == self.id \
2407
                and (review_state is None or an_state in review_state)
2408
2409
        def valid_ref(wan):
2410
            if wan.portal_type != 'ReferenceAnalysis':
2411
                return False
2412
            an_state = wf.getInfoFor(wan, 'review_state')
2413
            an_reftype = wan.getReferenceType()
2414
            return wan.getServiceUID() in suids \
2415
                and wan not in qcanalyses \
2416
                and (qctype is None or an_reftype == qctype) \
2417
                and (review_state is None or an_state in review_state)
2418
2419
        for an in ans:
2420
            an = an.getObject()
2421
            ws = an.getWorksheet()
2422
            if not ws:
2423
                continue
2424
            was = ws.getAnalyses()
2425
            for wa in was:
2426
                if valid_dup(wa):
2427
                    qcanalyses.append(wa)
2428
                elif valid_ref(wa):
2429
                    qcanalyses.append(wa)
2430
2431
        return qcanalyses
2432
2433
    def isInvalid(self):
2434
        """return if the Analysis Request has been invalidated
2435
        """
2436
        workflow = getToolByName(self, 'portal_workflow')
2437
        return workflow.getInfoFor(self, 'review_state') == 'invalid'
2438
2439
    def getSamplingRoundUID(self):
2440
        """Obtains the sampling round UID
2441
        :returns: UID
2442
        """
2443
        sr = self.getSamplingRound()
2444
        if sr:
2445
            return sr.UID()
2446
        else:
2447
            return ''
2448
2449
    def getStorageLocationTitle(self):
2450
        """ A method for AR listing catalog metadata
2451
        :return: Title of Storage Location
2452
        """
2453
        sl = self.getStorageLocation()
2454
        if sl:
2455
            return sl.Title()
2456
        return ''
2457
2458
    @security.public
2459
    def getResultsRange(self):
2460
        """Returns the valid result ranges for the analyses this Analysis
2461
        Request contains.
2462
2463
        By default uses the result ranges defined in the Analysis Specification
2464
        set in "Specification" field if any. Values manually set through
2465
        `ResultsRange` field for any given analysis keyword have priority over
2466
        the result ranges defined in "Specification" field.
2467
2468
        :return: A list of dictionaries, where each dictionary defines the
2469
            result range to use for any analysis contained in this Analysis
2470
            Request for the keyword specified. Each dictionary has, at least,
2471
                the following keys: "keyword", "min", "max"
2472
        :rtype: dict
2473
        """
2474
        specs_range = []
2475
        specification = self.getSpecification()
2476
        if specification:
2477
            specs_range = specification.getResultsRange()
2478
            specs_range = specs_range and specs_range or []
2479
2480
        # Override with AR's custom ranges
2481
        ar_range = self.Schema().getField("ResultsRange").get(self)
2482
        if not ar_range:
2483
            return specs_range
2484
2485
        # Remove those analysis ranges that neither min nor max are floatable
2486
        an_specs = [an for an in ar_range if
2487
                    api.is_floatable(an.get('min', None)) or
2488
                    api.is_floatable(an.get('max', None))]
2489
        # Want to know which are the analyses that needs to be overriden
2490
        keywords = map(lambda item: item.get('keyword'), an_specs)
2491
        # Get rid of those analyses to be overriden
2492
        out_specs = [sp for sp in specs_range if sp['keyword'] not in keywords]
2493
        # Add manually set ranges
2494
        out_specs.extend(an_specs)
2495
        return map(lambda spec: ResultsRangeDict(spec), out_specs)
2496
2497
    def getDatePublished(self):
2498
        """
2499
        Returns the transition date from the Analysis Request object
2500
        """
2501
        return getTransitionDate(self, 'publish', return_as_datetime=True)
2502
2503
    security.declarePublic('getSamplingDeviationTitle')
2504
2505
    def getSamplingDeviationTitle(self):
2506
        """
2507
        It works as a metacolumn.
2508
        """
2509
        sd = self.getSamplingDeviation()
2510
        if sd:
2511
            return sd.Title()
2512
        return ''
2513
2514
    security.declarePublic('getHazardous')
2515
2516
    def getHazardous(self):
2517
        """
2518
        It works as a metacolumn.
2519
        """
2520
        sample_type = self.getSampleType()
2521
        if sample_type:
2522
            return sample_type.getHazardous()
2523
        return False
2524
2525
    security.declarePublic('getContactURL')
2526
2527
    def getContactURL(self):
2528
        """
2529
        It works as a metacolumn.
2530
        """
2531
        contact = self.getContact()
2532
        if contact:
2533
            return contact.absolute_url_path()
2534
        return ''
2535
2536
    security.declarePublic('getSamplingWorkflowEnabled')
2537
2538
    def getSamplingWorkflowEnabled(self):
2539
        """
2540
        It works as a metacolumn.
2541
        """
2542
        sample = self.getSample()
2543
        if sample:
2544
            return sample.getSamplingWorkflowEnabled()
2545
        return ''
2546
2547
    def getSamplers(self):
2548
        return getUsers(self, ['Sampler', ])
2549
2550
    def getDepartments(self):
2551
        """Returns a list of the departments assigned to the Analyses
2552
        from this Analysis Request
2553
        """
2554
        bsc = getToolByName(self, 'bika_setup_catalog')
2555
        dept_uids = []
2556
        for an in self.getAnalyses():
2557
            deptuid = None
2558
            if hasattr(an, 'getDepartmentUID'):
2559
                deptuid = an.getDepartmentUID
2560
            if not deptuid and hasattr(an, 'getObject'):
2561
                deptuid = an.getObject().getDepartmentUID()
2562
            if deptuid:
2563
                dept_uids.append(deptuid)
2564
        brains = bsc(portal_type='Department', UID=dept_uids)
2565
        depts = [b.getObject() for b in brains]
2566
        return list(set(depts))
2567
2568
    def getDepartmentUIDs(self):
2569
        """Return a list of the UIDs of departments assigned to the Analyses
2570
        from this Analysis Request.
2571
        """
2572
        return [dept.UID() for dept in self.getDepartments()]
2573
2574
    def getResultsInterpretationByDepartment(self, department=None):
2575
        """Returns the results interpretation for this Analysis Request
2576
           and department. If department not set, returns the results
2577
           interpretation tagged as 'General'.
2578
2579
        :returns: a dict with the following keys:
2580
            {'uid': <department_uid> or 'general', 'richtext': <text/plain>}
2581
        """
2582
        uid = department.UID() if department else 'general'
2583
        rows = self.Schema()['ResultsInterpretationDepts'].get(self)
2584
        row = [row for row in rows if row.get('uid') == uid]
2585
        if len(row) > 0:
2586
            row = row[0]
2587
        elif uid == 'general' \
2588
                and hasattr(self, 'getResultsInterpretation') \
2589
                and self.getResultsInterpretation():
2590
            row = {'uid': uid, 'richtext': self.getResultsInterpretation()}
2591
        else:
2592
            row = {'uid': uid, 'richtext': ''}
2593
        return row
2594
2595
    def getAnalysisServiceSettings(self, uid):
2596
        """Returns a dictionary with the settings for the analysis service that
2597
        match with the uid provided.
2598
2599
        If there are no settings for the analysis service and
2600
        analysis requests:
2601
2602
        1. looks for settings in AR's ARTemplate. If found, returns the
2603
           settings for the AnalysisService set in the Template
2604
        2. If no settings found, looks in AR's ARProfile. If found, returns the
2605
           settings for the AnalysisService from the AR Profile. Otherwise,
2606
           returns a one entry dictionary with only the key 'uid'
2607
        """
2608
        sets = [s for s in self.getAnalysisServicesSettings()
2609
                if s.get('uid', '') == uid]
2610
2611
        # Created by using an ARTemplate?
2612
        if not sets and self.getTemplate():
2613
            adv = self.getTemplate().getAnalysisServiceSettings(uid)
2614
            sets = [adv] if 'hidden' in adv else []
2615
2616
        # Created by using an AR Profile?
2617
        if not sets and self.getProfiles():
2618
            adv = []
2619
            adv += [profile.getAnalysisServiceSettings(uid) for profile in
2620
                    self.getProfiles()]
2621
            sets = adv if 'hidden' in adv[0] else []
2622
2623
        return sets[0] if sets else {'uid': uid}
2624
2625
    def getPartitions(self):
2626
        """This functions returns the partitions from the analysis request's
2627
        analyses.
2628
2629
        :returns: a list with the full partition objects
2630
        """
2631
        partitions = []
2632
        for analysis in self.getAnalyses(full_objects=True):
2633
            if analysis.getSamplePartition() not in partitions:
2634
                partitions.append(analysis.getSamplePartition())
2635
        return partitions
2636
2637
    def getContainers(self):
2638
        """This functions returns the containers from the analysis request's
2639
        analyses
2640
2641
        :returns: a list with the full partition objects
2642
        """
2643
        partitions = self.getPartitions()
2644
        containers = []
2645
        for partition in partitions:
2646
            if partition.getContainer():
2647
                containers.append(partition.getContainer())
2648
        return containers
2649
2650 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...
2651
        """Checks if the analysis service that match with the uid provided must
2652
        be hidden in results. If no hidden assignment has been set for the
2653
        analysis in this request, returns the visibility set to the analysis
2654
        itself.
2655
2656
        Raise a TypeError if the uid is empty or None
2657
2658
        Raise a ValueError if there is no hidden assignment in this request or
2659
        no analysis service found for this uid.
2660
        """
2661
        if not uid:
2662
            raise TypeError('None type or empty uid')
2663
        sets = self.getAnalysisServiceSettings(uid)
2664
        if 'hidden' not in sets:
2665
            uc = getToolByName(self, 'uid_catalog')
2666
            serv = uc(UID=uid)
2667
            if serv and len(serv) == 1:
2668
                return serv[0].getObject().getRawHidden()
2669
            else:
2670
                raise ValueError('{} is not valid'.format(uid))
2671
        return sets.get('hidden', False)
2672
2673
    def getRejecter(self):
2674
        """If the Analysis Request has been rejected, returns the user who did the
2675
        rejection. If it was not rejected or the current user has not enough
2676
        privileges to access to this information, returns None.
2677
        """
2678
        wtool = getToolByName(self, 'portal_workflow')
2679
        mtool = getToolByName(self, 'portal_membership')
2680
        # noinspection PyBroadException
2681
        try:
2682
            review_history = wtool.getInfoFor(self, 'review_history')
2683
        except:  # noqa FIXME: remove blind except!
2684
            return None
2685
        for items in review_history:
2686
            action = items.get('action')
2687
            if action != 'reject':
2688
                continue
2689
            actor = items.get('actor')
2690
            return mtool.getMemberById(actor)
2691
        return None
2692
2693
    def getReceivedBy(self):
2694
        """
2695
        Returns the User who received the analysis request.
2696
        :returns: the user id
2697
        """
2698
        user = getTransitionUsers(self, 'receive', last_user=True)
2699
        return user[0] if user else ''
2700
2701
    def getDateVerified(self):
2702
        """
2703
        Returns the date of verification as a DateTime object.
2704
        """
2705
        return getTransitionDate(self, 'verify', return_as_datetime=True)
2706
2707
    @security.public
2708
    def getPrioritySortkey(self):
2709
        """Returns the key that will be used to sort the current Analysis
2710
        Request based on both its priority and creation date. On ASC sorting,
2711
        the oldest item with highest priority will be displayed.
2712
        :return: string used for sorting
2713
        """
2714
        priority = self.getPriority()
2715
        created_date = self.created().ISO8601()
2716
        return '%s.%s' % (priority, created_date)
2717
2718
    @security.public
2719
    def setPriority(self, value):
2720
        if not value:
2721
            value = self.Schema().getField('Priority').getDefault(self)
2722
        original_value = self.Schema().getField('Priority').get(self)
2723
        if original_value != value:
2724
            self.Schema().getField('Priority').set(self, value)
2725
            self._reindexAnalyses(['getPrioritySortkey'], True)
2726
2727
    @security.private
2728
    def _reindexAnalyses(self, idxs=None, update_metadata=False):
2729
        if not idxs and not update_metadata:
2730
            return
2731
        if not idxs:
2732
            idxs = []
2733
        analyses = self.getAnalyses()
2734
        catalog = getToolByName(self, CATALOG_ANALYSIS_LISTING)
2735
        for analysis in analyses:
2736
            analysis_obj = analysis.getObject()
2737
            catalog.reindexObject(analysis_obj, idxs=idxs, update_metadata=1)
2738
2739
    def _getCreatorFullName(self):
2740
        """
2741
        Returns the full name of this analysis request's creator.
2742
        """
2743
        return user_fullname(self, self.Creator())
2744
2745
    def _getCreatorEmail(self):
2746
        """
2747
        Returns the email of this analysis request's creator.
2748
        """
2749
        return user_email(self, self.Creator())
2750
2751
    def _getSamplerFullName(self):
2752
        """
2753
        Returns the full name's defined sampler.
2754
        """
2755
        return user_fullname(self, self.getSampler())
2756
2757
    def _getSamplerEmail(self):
2758
        """
2759
        Returns the email of this analysis request's sampler.
2760
        """
2761
        return user_email(self, self.Creator())
2762
2763
    # TODO Workflow, AnalysisRequest Move to guards.verify?
2764
    def isVerifiable(self):
2765
        """Checks it the current Analysis Request can be verified. This is, its
2766
        not a cancelled Analysis Request and all the analyses that contains
2767
        are verifiable too. Note that verifying an Analysis Request is in fact,
2768
        the same as verifying all the analyses that contains. Therefore, the
2769
        'verified' state of an Analysis Request shouldn't be a 'real' state,
2770
        rather a kind-of computed state, based on the statuses of the analyses
2771
        it contains. This is why this function checks if the analyses
2772
        contained are verifiable, cause otherwise, the Analysis Request will
2773
        never be able to reach a 'verified' state.
2774
        :returns: True or False
2775
        """
2776
        # Check if the analysis request is active
2777
        workflow = getToolByName(self, "portal_workflow")
2778
        objstate = workflow.getInfoFor(self, 'cancellation_state', 'active')
2779
        if objstate == "cancelled":
2780
            return False
2781
2782
        # Check if the analysis request state is to_be_verified
2783
        review_state = workflow.getInfoFor(self, "review_state")
2784
        if review_state == 'to_be_verified':
2785
            # This means that all the analyses from this analysis request have
2786
            # already been transitioned to a 'verified' state, and so the
2787
            # analysis request itself
2788
            return True
2789
        else:
2790
            # Check if the analyses contained in this analysis request are
2791
            # verifiable. Only check those analyses not cancelled and that
2792
            # are not in a kind-of already verified state
2793
            canbeverified = True
2794
            omit = ['published', 'retracted', 'rejected', 'verified']
2795
            for a in self.getAnalyses(full_objects=True):
2796
                st = workflow.getInfoFor(a, 'cancellation_state', 'active')
2797
                if st == 'cancelled':
2798
                    continue
2799
                st = workflow.getInfoFor(a, 'review_state')
2800
                if st in omit:
2801
                    continue
2802
                # Can the analysis be verified?
2803
                if not a.isVerifiable(self):
2804
                    canbeverified = False
2805
                    break
2806
            return canbeverified
2807
2808 View Code Duplication
    def getObjectWorkflowStates(self):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
2809
        """
2810
        This method is used as a metacolumn.
2811
        Returns a dictionary with the workflow id as key and workflow state as
2812
        value.
2813
        :returns: {'review_state':'active',...}
2814
        """
2815
        workflow = getToolByName(self, 'portal_workflow')
2816
        states = {}
2817
        for w in workflow.getWorkflowsFor(self):
2818
            state = w._getWorkflowStateOf(self).id
2819
            states[w.state_var] = state
2820
        return states
2821
2822
    # TODO Workflow, AnalysisRequest Move to guards.verify?
2823
    def isUserAllowedToVerify(self, member):
2824
        """Checks if the specified user has enough privileges to verify the
2825
        current AR. Apart from the roles, this function also checks if the
2826
        current user has enough privileges to verify all the analyses contained
2827
        in this Analysis Request. Note that this function only returns if the
2828
        user can verify the analysis request according to his/her privileges
2829
        and the analyses contained (see isVerifiable function)
2830
2831
        :member: user to be tested
2832
        :returns: true or false
2833
        """
2834
        # Check if the user has "Bika: Verify" privileges
2835
        username = member.getUserName()
2836
        allowed = ploneapi.user.has_permission(VerifyPermission,
2837
                                               username=username)
2838
        if not allowed:
2839
            return False
2840
        # Check if the user is allowed to verify all the contained analyses
2841
        # TODO-performance: gettin all analysis each time this function is
2842
        # called
2843
        notallowed = [a for a in self.getAnalyses(full_objects=True)
2844
                      if not a.isUserAllowedToVerify(member)]
2845
        return not notallowed
2846
2847
    @security.public
2848
    def guard_to_be_preserved(self):
2849
        return guards.to_be_preserved(self)
2850
2851
    @security.public
2852
    def guard_unassign_transition(self):
2853
        return guards.unassign(self)
2854
2855
    @security.public
2856
    def guard_assign_transition(self):
2857
        return guards.assign(self)
2858
2859
    @security.public
2860
    def guard_receive_transition(self):
2861
        return guards.receive(self)
2862
2863
    @security.public
2864
    def guard_schedule_sampling_transition(self):
2865
        return guards.schedule_sampling(self)
2866
2867
    @security.public
2868
    def guard_prepublish_transition(self):
2869
        return guards.prepublish(self)
2870
2871
    def SearchableText(self):
2872
        """
2873
        Override searchable text logic based on the requirements.
2874
2875
        This method constructs a text blob which contains all full-text
2876
        searchable text for this content item.
2877
        https://docs.plone.org/develop/plone/searching_and_indexing/indexing.html#full-text-searching
2878
        """
2879
2880
        # Speed up string concatenation ops by using a buffer
2881
        entries = []
2882
2883
        # plain text fields we index from ourself,
2884
        # a list of accessor methods of the class
2885
        plain_text_fields = ("getId", )
2886
2887
        def read(acc):
2888
            """
2889
            Call a class accessor method to give a value for certain Archetypes
2890
            field.
2891
            """
2892
            try:
2893
                val = acc()
2894
            except Exception as e:
2895
                message = "Error getting the accessor parameter in " \
2896
                          "SearchableText from the Analysis Request Object " \
2897
                          "{}: {}".format(self.getId(), e.message)
2898
                logger.error(message)
2899
                val = ""
2900
2901
            if val is None:
2902
                val = ""
2903
2904
            return val
2905
2906
        # Concatenate plain text fields as they are
2907
        for f in plain_text_fields:
2908
            accessor = getattr(self, f)
2909
            value = read(accessor)
2910
            entries.append(value)
2911
2912
        # Adding HTML Fields to SearchableText can be uncommented if necessary
2913
        # transforms = getToolByName(self, 'portal_transforms')
2914
        #
2915
        # # Run HTML valued fields through text/plain conversion
2916
        # for f in html_fields:
2917
        #     accessor = getattr(self, f)
2918
        #     value = read(accessor)
2919
        #
2920
        #     if value != "":
2921
        #         stream = transforms.convertTo('text/plain', value,
2922
        #                                       mimetype='text/html')
2923
        #         value = stream.getData()
2924
        #
2925
        #     entries.append(value)
2926
2927
        # Plone accessor methods assume utf-8
2928
        def convertToUTF8(text):
2929
            if type(text) == unicode:
2930
                return text.encode("utf-8")
2931
            return text
2932
2933
        entries = [convertToUTF8(entry) for entry in entries]
2934
2935
        # Concatenate all strings to one text blob
2936
        return " ".join(entries)
2937
2938
    def getPriorityText(self):
2939
        """
2940
        This function looks up the priority text from priorities vocab
2941
        :returns: the priority text or ''
2942
        """
2943
        if self.getPriority():
2944
            return PRIORITIES.getValue(self.getPriority())
2945
        return ''
2946
2947
    def get_ARAttachment(self):
2948
        logger.warn("_ARAttachment is a virtual field used in AR Add. "
2949
                    "It can not hold an own value!")
2950
        return None
2951
2952
    def set_ARAttachment(self, value):
2953
        logger.warn("_ARAttachment is a virtual field used in AR Add. "
2954
                    "It can not hold an own value!")
2955
        return None
2956
2957
    def get_retest(self):
2958
        """Returns the Analysis Request automatically generated because of the
2959
        retraction of the current analysis request
2960
        """
2961
        relationship = "AnalysisRequestRetracted"
2962
        retest = self.getBackReferences(relationship=relationship)
2963
        if retest and len(retest) > 1:
2964
            logger.warn("More than one retest for {0}".format(self.getId()))
2965
        return retest and retest[0] or None
2966
2967
    def getAncestors(self, all_ancestors=True):
2968
        """Returns the ancestor(s) of this Analysis Request
2969
        param all_ancestors: include all ancestors, not only the parent
2970
        """
2971
        parent = self.getParentAnalysisRequest()
2972
        if not parent:
2973
            return list()
2974
        if not all_ancestors:
2975
            return [parent]
2976
        return [parent] + parent.getAncestors(all_ancestors=True)
2977
2978
    def isRootAncestor(self):
2979
        """Returns True if the AR is the root ancestor
2980
2981
        :returns: True if the AR has no more parents
2982
        """
2983
        parent = self.getParentAnalysisRequest()
2984
        if parent:
2985
            return False
2986
        return True
2987
2988
    def getDescendants(self, all_descendants=False):
2989
        """Returns the descendant Analysis Requests
2990
2991
        :param all_descendants: recursively include all descendants
2992
        """
2993
2994
        # N.B. full objects returned here from
2995
        #      `Products.Archetypes.Referenceable.getBRefs`
2996
        #      -> don't add this method into Metadata
2997
        children = self.getBackReferences(
2998
            "AnalysisRequestParentAnalysisRequest")
2999
3000
        descendants = []
3001
3002
        # recursively include all children
3003
        if all_descendants:
3004
            for child in children:
3005
                descendants.append(child)
3006
                descendants += child.getDescendants(all_descendants=True)
3007
        else:
3008
            descendants = children
3009
3010
        return descendants
3011
3012
    def getDescendantsUIDs(self, all_descendants=False):
3013
        """Returns the UIDs of the descendant Analysis Requests
3014
3015
        This method is used as metadata
3016
        """
3017
        descendants = self.getDescendants(all_descendants=all_descendants)
3018
        return map(api.get_uid, descendants)
3019
3020
    def isPartition(self):
3021
        """Returns true if this Analysis Request is a partition
3022
        """
3023
        return not self.isRootAncestor()
3024
3025
3026
registerType(AnalysisRequest, PROJECTNAME)
3027