Passed
Push — master ( f42093...226b52 )
by Ramon
04:24
created

()   A

Complexity

Conditions 1

Size

Total Lines 2
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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