Passed
Push — master ( d0ff45...746555 )
by Jordi
05:40
created

  A

Complexity

Conditions 3

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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