Passed
Push — master ( 78fa88...5582cc )
by Ramon
05:00
created

bika.lims.content.analysisrequest   F

Complexity

Total Complexity 221

Size/Duplication

Total Lines 2391
Duplicated Lines 1.46 %

Importance

Changes 0
Metric Value
wmc 221
eloc 1616
dl 35
loc 2391
rs 0.8
c 0
b 0
f 0

83 Methods

Rating   Name   Duplication   Size   Complexity  
A AnalysisRequest.getPartitions() 0 11 3
A AnalysisRequest.getObjectWorkflowStates() 13 13 2
A AnalysisRequest.getAnalysisService() 0 6 2
B AnalysisRequest.getBillableItems() 0 31 8
A AnalysisRequest._reindexAnalyses() 0 11 5
A AnalysisRequest.getSamplingWorkflowEnabled() 0 8 2
B AnalysisRequest.addARAttachment() 0 44 6
A AnalysisRequest.getVATAmount() 0 13 2
B AnalysisRequest.getAnalysisServiceSettings() 0 29 8
A AnalysisRequest.getDiscountAmount() 0 10 2
A AnalysisRequest.getSubtotal() 0 4 1
A AnalysisRequest.getDueDate() 0 5 2
A AnalysisRequest.getClient() 0 6 3
A AnalysisRequest.isAnalysisServiceHidden() 22 22 5
A AnalysisRequest.getHazardous() 0 8 2
A AnalysisRequest.getDateVerified() 0 5 1
A AnalysisRequest.getPriorityText() 0 8 2
A AnalysisRequest.printInvoice() 0 6 1
A AnalysisRequest.getResultsRange() 0 38 5
A AnalysisRequest.isOpen() 0 8 3
A AnalysisRequest.getSubtotalTotalPrice() 0 4 1
A AnalysisRequest.isRootAncestor() 0 9 2
A AnalysisRequest._getCreatorEmail() 0 5 1
B AnalysisRequest.delARAttachment() 0 23 5
A AnalysisRequest.getProfilesTitle() 0 2 1
A AnalysisRequest.getSamplingRoundUID() 0 9 2
A AnalysisRequest.setBatch() 0 6 2
A AnalysisRequest.getBatch() 0 8 2
A AnalysisRequest.getVerifiersIDs() 0 9 2
A AnalysisRequest.getAnalysesNum() 0 19 5
A AnalysisRequest.get_retest() 0 9 3
A AnalysisRequest.getDescendantsUIDs() 0 7 1
A AnalysisRequest.current_date() 0 5 1
B AnalysisRequest.getServicesAndProfiles() 0 33 6
A AnalysisRequest._renameAfterCreation() 0 3 1
B AnalysisRequest.getPrinted() 0 22 7
A AnalysisRequest.getContactUIDForUser() 0 11 2
A AnalysisRequest.setPublicationSpecification() 0 4 1
A AnalysisRequest.getDescendants() 0 23 3
A AnalysisRequest.getPrioritySortkey() 0 10 1
A AnalysisRequest.getContactURL() 0 8 2
A AnalysisRequest._getSamplerEmail() 0 5 1
A AnalysisRequest._getSamplerFullName() 0 5 1
A AnalysisRequest.getAnalysts() 0 8 3
A AnalysisRequest.getManagers() 0 15 4
A AnalysisRequest.getVerifiers() 0 12 3
A AnalysisRequest.isInvalid() 0 5 1
A AnalysisRequest.setParentAnalysisRequest() 0 8 2
A AnalysisRequest.get_ARAttachment() 0 4 1
A AnalysisRequest._getCatalogTool() 0 3 1
B AnalysisRequest.getVerifier() 0 29 7
A AnalysisRequest.sortable_title() 0 5 1
C AnalysisRequest.getQCAnalyses() 0 53 10
A AnalysisRequest.getDefaultMemberDiscount() 0 9 3
A AnalysisRequest.getRejecter() 0 19 4
A AnalysisRequest.printLastReport() 0 12 5
A AnalysisRequest.getLate() 0 10 4
A AnalysisRequest.getContainers() 0 7 1
A AnalysisRequest.getDepartments() 0 10 4
A AnalysisRequest.isPartition() 0 4 1
A AnalysisRequest.getSubtotalVATAmount() 0 4 1
A AnalysisRequest.getDistrict() 0 3 1
A AnalysisRequest.setPriority() 0 8 3
A AnalysisRequest.issueInvoice() 0 40 4
A AnalysisRequest.set_ARAttachment() 0 4 1
A AnalysisRequest.getPreservers() 0 2 1
A AnalysisRequest.getReceivedBy() 0 7 2
A AnalysisRequest._getCreatorFullName() 0 5 1
B AnalysisRequest.getResultsInterpretationByDepartment() 0 20 6
A AnalysisRequest.Description() 0 4 1
A AnalysisRequest.getSamplers() 0 2 1
A AnalysisRequest.Title() 0 3 1
A AnalysisRequest.getTotalPrice() 0 9 1
A AnalysisRequest.getStorageLocationTitle() 0 8 2
A AnalysisRequest.getClientPath() 0 2 1
A AnalysisRequest.getBatchUID() 0 5 2
A AnalysisRequest.getSamplingDeviationTitle() 0 8 2
B AnalysisRequest.getResponsible() 0 37 6
A AnalysisRequest.getProvince() 0 3 1
A AnalysisRequest.getDatePublished() 0 5 1
A AnalysisRequest.getSamplingRequired() 0 5 1
B AnalysisRequest.SearchableText() 0 66 5
A AnalysisRequest.getAncestors() 0 10 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complexity

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like bika.lims.content.analysisrequest often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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