Passed
Push — 2.x ( 906086...16631d )
by Jordi
08:09
created

bika.lims.content.analysisrequest   F

Complexity

Total Complexity 228

Size/Duplication

Total Lines 2511
Duplicated Lines 0.88 %

Importance

Changes 0
Metric Value
wmc 228
eloc 1708
dl 22
loc 2511
rs 0.8
c 0
b 0
f 0

88 Methods

Rating   Name   Duplication   Size   Complexity  
A AnalysisRequest._renameAfterCreation() 0 10 2
B AnalysisRequest.setSpecification() 0 31 6
A AnalysisRequest._getCatalogTool() 0 3 1
A AnalysisRequest.sortable_title() 0 5 1
B AnalysisRequest.setResultsRange() 0 28 7
A AnalysisRequest.Description() 0 4 1
A AnalysisRequest.Title() 0 3 1
A AnalysisRequest.getAnalysisService() 0 6 2
B AnalysisRequest.getBillableItems() 0 26 8
A AnalysisRequest._reindexAnalyses() 0 11 5
A AnalysisRequest.getSamplingWorkflowEnabled() 0 9 2
A AnalysisRequest.getVATAmount() 0 14 2
B AnalysisRequest.getAnalysisServiceSettings() 0 29 8
A AnalysisRequest.getDiscountAmount() 0 11 2
A AnalysisRequest.getSubtotal() 0 5 1
A AnalysisRequest.setDateReceived() 0 8 2
A AnalysisRequest.getDueDate() 0 5 2
A AnalysisRequest.getClient() 0 12 3
A AnalysisRequest.isAnalysisServiceHidden() 22 22 5
A AnalysisRequest.getDateVerified() 0 5 1
A AnalysisRequest.getHazardous() 0 9 2
A AnalysisRequest.createAttachment() 0 17 1
A AnalysisRequest.getPriorityText() 0 8 2
A AnalysisRequest.printInvoice() 0 7 1
A AnalysisRequest.isOpen() 0 8 3
A AnalysisRequest.getOtherRejectionReasons() 0 7 2
A AnalysisRequest.getSubtotalTotalPrice() 0 5 1
A AnalysisRequest.isRootAncestor() 0 9 2
A AnalysisRequest._getCreatorEmail() 0 5 1
A AnalysisRequest.getProfilesTitle() 0 2 1
A AnalysisRequest.setBatch() 0 5 2
A AnalysisRequest.getBatch() 0 8 2
A AnalysisRequest.getVerifiersIDs() 0 9 2
A AnalysisRequest.getAnalysesNum() 0 19 5
A AnalysisRequest.get_retest() 0 9 3
A AnalysisRequest.getDescendantsUIDs() 0 7 1
A AnalysisRequest.current_date() 0 5 1
B AnalysisRequest.getPrinted() 0 22 7
A AnalysisRequest.getSampleConditionTitle() 0 8 2
B AnalysisRequest.process_inline_images() 0 50 5
A AnalysisRequest.getContactUIDForUser() 0 11 2
A AnalysisRequest.getDescendants() 0 23 3
A AnalysisRequest.getPrioritySortkey() 0 10 1
A AnalysisRequest.getContactURL() 0 9 2
A AnalysisRequest._getSamplerEmail() 0 5 1
A AnalysisRequest._getSamplerFullName() 0 5 1
A AnalysisRequest.getAnalysts() 0 8 3
A AnalysisRequest.getManagers() 0 15 4
A AnalysisRequest.getVerifiers() 0 12 3
A AnalysisRequest.isInvalid() 0 5 1
A AnalysisRequest.setParentAnalysisRequest() 0 13 4
A AnalysisRequest.getSecondaryAnalysisRequests() 0 5 1
A AnalysisRequest.get_ARAttachment() 0 4 1
B AnalysisRequest.getVerifier() 0 30 7
A AnalysisRequest.getQCAnalyses() 0 29 4
A AnalysisRequest.getProgress() 0 28 5
A AnalysisRequest.getDefaultMemberDiscount() 0 9 3
A AnalysisRequest.getRejecter() 0 19 4
A AnalysisRequest.getWorksheets() 0 14 3
A AnalysisRequest.getLate() 0 10 4
A AnalysisRequest.getContainers() 0 7 1
A AnalysisRequest.setDateSampled() 0 8 2
A AnalysisRequest.getDepartments() 0 10 4
A AnalysisRequest.isPartition() 0 4 1
A AnalysisRequest.getSubtotalVATAmount() 0 5 1
A AnalysisRequest.getDistrict() 0 3 1
A AnalysisRequest.setPriority() 0 8 3
A AnalysisRequest.set_ARAttachment() 0 4 1
A AnalysisRequest.setSamplingDate() 0 8 2
A AnalysisRequest.getReceivedBy() 0 7 2
A AnalysisRequest.getPreservers() 0 2 1
A AnalysisRequest.addAttachment() 0 17 4
A AnalysisRequest.createInvoice() 0 17 2
A AnalysisRequest._getCreatorFullName() 0 5 1
B AnalysisRequest.getResultsInterpretationByDepartment() 0 20 6
A AnalysisRequest.setResultsInterpretationDepts() 0 25 3
A AnalysisRequest.getSamplers() 0 2 1
A AnalysisRequest.getTotalPrice() 0 10 1
A AnalysisRequest.getSelectedRejectionReasons() 0 10 2
A AnalysisRequest.getStorageLocationTitle() 0 8 2
A AnalysisRequest.getBatchUID() 0 5 2
A AnalysisRequest.getSamplingDeviationTitle() 0 9 2
B AnalysisRequest.getResponsible() 0 37 6
A AnalysisRequest.getProvince() 0 3 1
A AnalysisRequest.getDatePublished() 0 5 1
B AnalysisRequest.setProfiles() 0 32 6
A AnalysisRequest.getSamplingRequired() 0 5 1
A AnalysisRequest.getAncestors() 0 10 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

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

Common duplication problems, and corresponding solutions are:

Complexity

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

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

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

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