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-2024 by it's authors. |
19
|
|
|
# Some rights reserved, see README and LICENSE. |
20
|
|
|
|
21
|
|
|
from bika.lims import api |
22
|
|
|
from bika.lims.interfaces import IDuplicateAnalysis |
23
|
|
|
from bika.lims.interfaces import IRejected |
24
|
|
|
from bika.lims.interfaces import IRetracted |
25
|
|
|
from bika.lims.interfaces import ISubmitted |
26
|
|
|
from bika.lims.interfaces import IVerified |
27
|
|
|
from bika.lims.interfaces.analysis import IRequestAnalysis |
28
|
|
|
from bika.lims.utils.analysis import create_retest |
29
|
|
|
from bika.lims.workflow import doActionFor |
30
|
|
|
from bika.lims.workflow.analysis import STATE_REJECTED |
31
|
|
|
from bika.lims.workflow.analysis import STATE_RETRACTED |
32
|
|
|
from DateTime import DateTime |
33
|
|
|
from zope.interface import alsoProvides |
34
|
|
|
|
35
|
|
|
|
36
|
|
|
def after_assign(analysis): |
37
|
|
|
"""Function triggered after an 'assign' transition for the analysis passed |
38
|
|
|
in is performed. |
39
|
|
|
""" |
40
|
|
|
reindex_request(analysis) |
41
|
|
|
|
42
|
|
|
|
43
|
|
|
def before_unassign(analysis): |
44
|
|
|
"""Function triggered before 'unassign' transition takes place |
45
|
|
|
""" |
46
|
|
|
worksheet = analysis.getWorksheet() |
47
|
|
|
if not worksheet: |
48
|
|
|
return |
49
|
|
|
|
50
|
|
|
# Removal of a routine analysis causes the removal of their duplicates |
51
|
|
|
for dup in worksheet.get_duplicates_for(analysis): |
52
|
|
|
doActionFor(dup, "unassign") |
53
|
|
|
|
54
|
|
|
|
55
|
|
|
def before_reject(analysis): |
56
|
|
|
"""Function triggered before 'unassign' transition takes place |
57
|
|
|
""" |
58
|
|
|
worksheet = analysis.getWorksheet() |
59
|
|
|
if not worksheet: |
60
|
|
|
return |
61
|
|
|
|
62
|
|
|
# Rejection of a routine analysis causes the removal of their duplicates |
63
|
|
|
for dup in worksheet.get_duplicates_for(analysis): |
64
|
|
|
doActionFor(dup, "unassign") |
65
|
|
|
|
66
|
|
|
|
67
|
|
|
def after_retest(analysis): |
68
|
|
|
"""Function triggered before 'retest' transition takes place. Creates a |
69
|
|
|
copy of the current analysis |
70
|
|
|
""" |
71
|
|
|
# When an analysis is retested, it automatically transitions to verified, |
72
|
|
|
# so we need to mark the analysis as such |
73
|
|
|
alsoProvides(analysis, IVerified) |
74
|
|
|
|
75
|
|
|
def verify_and_retest(relative): |
76
|
|
|
if not ISubmitted.providedBy(relative): |
77
|
|
|
# Result not yet submitted, no need to create a retest |
78
|
|
|
return |
79
|
|
|
|
80
|
|
|
# Apply the transition manually, but only if analysis can be verified |
81
|
|
|
doActionFor(relative, "verify") |
82
|
|
|
|
83
|
|
|
# Create the retest |
84
|
|
|
create_retest(relative) |
85
|
|
|
|
86
|
|
|
# Retest and auto-verify relatives, from bottom to top |
87
|
|
|
relatives = list(reversed(analysis.getDependents(recursive=True))) |
88
|
|
|
relatives.extend(analysis.getDependencies(recursive=True)) |
89
|
|
|
map(verify_and_retest, relatives) |
90
|
|
|
|
91
|
|
|
# Create the retest |
92
|
|
|
create_retest(analysis) |
93
|
|
|
|
94
|
|
|
# Try to rollback the Analysis Request |
95
|
|
|
if IRequestAnalysis.providedBy(analysis): |
96
|
|
|
doActionFor(analysis.getRequest(), "rollback_to_receive") |
97
|
|
|
reindex_request(analysis) |
98
|
|
|
|
99
|
|
|
|
100
|
|
|
def after_unassign(analysis): |
101
|
|
|
"""Function triggered after an 'unassign' transition for the analysis passed |
102
|
|
|
in is performed. |
103
|
|
|
""" |
104
|
|
|
# Remove from the worksheet |
105
|
|
|
remove_analysis_from_worksheet(analysis) |
106
|
|
|
# Reindex the Analysis Request |
107
|
|
|
reindex_request(analysis) |
108
|
|
|
|
109
|
|
|
|
110
|
|
|
def after_cancel(analysis): |
111
|
|
|
"""Function triggered after a "cancel" transition is performed. Removes the |
112
|
|
|
cancelled analysis from the worksheet, if any. |
113
|
|
|
""" |
114
|
|
|
# Remove from the worksheet |
115
|
|
|
remove_analysis_from_worksheet(analysis) |
116
|
|
|
|
117
|
|
|
|
118
|
|
|
def after_reinstate(analysis): |
119
|
|
|
"""Function triggered after a "reinstate" transition is performed. |
120
|
|
|
""" |
121
|
|
|
pass |
122
|
|
|
|
123
|
|
|
|
124
|
|
|
def after_submit(analysis): |
125
|
|
|
"""Method triggered after a 'submit' transition for the analysis passed in |
126
|
|
|
is performed. Promotes the submit transition to the Worksheet to which the |
127
|
|
|
analysis belongs to. Note that for the worksheet there is already a guard |
128
|
|
|
that assures the transition to the worksheet will only be performed if all |
129
|
|
|
analyses within the worksheet have already been transitioned. |
130
|
|
|
This function is called automatically by |
131
|
|
|
bika.lims.workfow.AfterTransitionEventHandler |
132
|
|
|
""" |
133
|
|
|
# Ensure there is a Result Capture Date even if the result was set |
134
|
|
|
# automatically on creation because of a "DefaultResult" |
135
|
|
|
if not analysis.getResultCaptureDate(): |
136
|
|
|
analysis.setResultCaptureDate(DateTime()) |
137
|
|
|
|
138
|
|
|
# Mark this analysis as ISubmitted |
139
|
|
|
alsoProvides(analysis, ISubmitted) |
140
|
|
|
|
141
|
|
|
# Promote to analyses this analysis depends on |
142
|
|
|
promote_to_dependencies(analysis, "submit") |
143
|
|
|
|
144
|
|
|
# Promote transition to worksheet |
145
|
|
|
ws = analysis.getWorksheet() |
146
|
|
|
if ws: |
147
|
|
|
doActionFor(ws, "submit") |
148
|
|
|
ws.reindexObject() |
149
|
|
|
|
150
|
|
|
# Promote transition to Analysis Request |
151
|
|
|
if IRequestAnalysis.providedBy(analysis): |
152
|
|
|
doActionFor(analysis.getRequest(), 'submit') |
153
|
|
|
reindex_request(analysis) |
154
|
|
|
|
155
|
|
|
|
156
|
|
|
def after_retract(analysis): |
157
|
|
|
"""Function triggered after a 'retract' transition for the analysis passed |
158
|
|
|
in is performed. The analysis transitions to "retracted" state and a new |
159
|
|
|
copy of the analysis is created. The copy initial state is "unassigned", |
160
|
|
|
unless the the retracted analysis was assigned to a worksheet. In such |
161
|
|
|
case, the copy is transitioned to 'assigned' state too |
162
|
|
|
""" |
163
|
|
|
# Mark this analysis as IRetracted |
164
|
|
|
alsoProvides(analysis, IRetracted) |
165
|
|
|
|
166
|
|
|
# Ignore attachments of this analysis in results report |
167
|
|
|
for attachment in analysis.getAttachment(): |
168
|
|
|
attachment.setRenderInReport(False) |
169
|
|
|
|
170
|
|
|
# Retract our dependents (analyses that depend on this analysis) |
171
|
|
|
cascade_to_dependents(analysis, "retract") |
172
|
|
|
|
173
|
|
|
# Retract our dependencies (analyses this analysis depends on) |
174
|
|
|
promote_to_dependencies(analysis, "retract") |
175
|
|
|
|
176
|
|
|
# Create the retest |
177
|
|
|
create_retest(analysis) |
178
|
|
|
|
179
|
|
|
# Try to rollback the Analysis Request |
180
|
|
|
if IRequestAnalysis.providedBy(analysis): |
181
|
|
|
doActionFor(analysis.getRequest(), "rollback_to_receive") |
182
|
|
|
reindex_request(analysis) |
183
|
|
|
|
184
|
|
|
|
185
|
|
|
def after_reject(analysis): |
186
|
|
|
"""Function triggered after the "reject" transition for the analysis passed |
187
|
|
|
in is performed.""" |
188
|
|
|
# Mark this analysis with IRejected |
189
|
|
|
alsoProvides(analysis, IRejected) |
190
|
|
|
|
191
|
|
|
# Remove from the worksheet |
192
|
|
|
remove_analysis_from_worksheet(analysis) |
193
|
|
|
|
194
|
|
|
# Ignore attachments of this analysis in results report |
195
|
|
|
for attachment in analysis.getAttachment(): |
196
|
|
|
attachment.setRenderInReport(False) |
197
|
|
|
|
198
|
|
|
# Reject our dependents (analyses that depend on this analysis) |
199
|
|
|
cascade_to_dependents(analysis, "reject") |
200
|
|
|
|
201
|
|
|
if IRequestAnalysis.providedBy(analysis): |
202
|
|
|
# Try verify (for when remaining analyses are in 'verified') |
203
|
|
|
doActionFor(analysis.getRequest(), "verify") |
204
|
|
|
|
205
|
|
|
# Try submit (remaining analyses are in 'to_be_verified') |
206
|
|
|
doActionFor(analysis.getRequest(), "submit") |
207
|
|
|
|
208
|
|
|
# Try rollback (no remaining analyses or some not submitted) |
209
|
|
|
doActionFor(analysis.getRequest(), "rollback_to_receive") |
210
|
|
|
reindex_request(analysis) |
211
|
|
|
|
212
|
|
|
|
213
|
|
|
def after_verify(analysis): |
214
|
|
|
""" |
215
|
|
|
Method triggered after a 'verify' transition for the analysis passed in |
216
|
|
|
is performed. Promotes the transition to the Analysis Request and to |
217
|
|
|
Worksheet (if the analysis is assigned to any) |
218
|
|
|
This function is called automatically by |
219
|
|
|
bika.lims.workfow.AfterTransitionEventHandler |
220
|
|
|
""" |
221
|
|
|
# Mark this analysis as IVerified |
222
|
|
|
alsoProvides(analysis, IVerified) |
223
|
|
|
|
224
|
|
|
# Promote to analyses this analysis depends on |
225
|
|
|
promote_to_dependencies(analysis, "verify") |
226
|
|
|
|
227
|
|
|
# Promote transition to worksheet |
228
|
|
|
ws = analysis.getWorksheet() |
229
|
|
|
if ws: |
230
|
|
|
doActionFor(ws, "verify") |
231
|
|
|
ws.reindexObject() |
232
|
|
|
|
233
|
|
|
# Promote transition to Analysis Request if Sample auto-verify is enabled |
234
|
|
|
if IRequestAnalysis.providedBy(analysis) and check_all_verified(analysis): |
235
|
|
|
setup = api.get_setup() |
236
|
|
|
if setup.getAutoVerifySamples(): |
237
|
|
|
doActionFor(analysis.getRequest(), "verify") |
238
|
|
|
|
239
|
|
|
# Reindex the sample (and ancestors) this analysis belongs to |
240
|
|
|
reindex_request(analysis) |
241
|
|
|
|
242
|
|
|
|
243
|
|
|
def check_all_verified(analysis): |
244
|
|
|
"""Checks if all analyses are verified |
245
|
|
|
|
246
|
|
|
NOTE: This check is provided solely for performance reasons of the `verify` |
247
|
|
|
transition, because it is a less expensive calculation than executing the |
248
|
|
|
`doActionFor` method on the sample for each verified analysis. |
249
|
|
|
|
250
|
|
|
The worst case that can happen is that the sample does not get |
251
|
|
|
automatically verified and needs to be transitioned manually. |
252
|
|
|
|
253
|
|
|
:param analysis: The current verified analysis |
254
|
|
|
:returns: True if all other routine analyses of the sample are verified |
255
|
|
|
""" |
256
|
|
|
|
257
|
|
|
parent = api.get_parent(analysis) |
258
|
|
|
sample = analysis.getRequest() |
259
|
|
|
uid = api.get_uid(analysis) |
260
|
|
|
|
261
|
|
|
def is_valid(an): |
262
|
|
|
state = api.get_review_status(an) |
263
|
|
|
return state not in [STATE_REJECTED, STATE_RETRACTED] |
264
|
|
|
|
265
|
|
|
# get all *valid* analyses of the sample |
266
|
|
|
analyses = filter(is_valid, sample.getAnalyses()) |
267
|
|
|
# get all *verified* analyses of the sample |
268
|
|
|
verified = sample.getAnalyses(object_provides=IVerified.__identifier__) |
269
|
|
|
|
270
|
|
|
# NOTE: We remove the current processed routine analysis (if not a WS |
271
|
|
|
# duplicate/reference analysis), because it is either not yet |
272
|
|
|
# verified or processed already in multi-verify scenarios. |
273
|
|
|
if sample == parent: |
274
|
|
|
analyses = filter(lambda x: api.get_uid(x) != uid, analyses) |
275
|
|
|
verified = filter(lambda x: api.get_uid(x) != uid, verified) |
276
|
|
|
|
277
|
|
|
return len(analyses) == len(verified) |
278
|
|
|
|
279
|
|
|
|
280
|
|
|
def after_publish(analysis): |
281
|
|
|
"""Function triggered after a "publish" transition is performed. |
282
|
|
|
""" |
283
|
|
|
pass |
284
|
|
|
|
285
|
|
|
|
286
|
|
|
# TODO Workflow - Analysis - revisit reindexing of ancestors |
287
|
|
|
def reindex_request(analysis, idxs=None): |
288
|
|
|
"""Reindex the Analysis Request the analysis belongs to, as well as the |
289
|
|
|
ancestors recursively |
290
|
|
|
""" |
291
|
|
|
if not IRequestAnalysis.providedBy(analysis) or \ |
292
|
|
|
IDuplicateAnalysis.providedBy(analysis): |
293
|
|
|
# Analysis not directly bound to an Analysis Request. Do nothing |
294
|
|
|
return |
295
|
|
|
|
296
|
|
|
request = analysis.getRequest() |
297
|
|
|
ancestors = [request] + request.getAncestors(all_ancestors=True) |
298
|
|
|
for ancestor in ancestors: |
299
|
|
|
ancestor.reindexObject() |
300
|
|
|
|
301
|
|
|
|
302
|
|
|
def remove_analysis_from_worksheet(analysis): |
303
|
|
|
"""Removes the analysis passed in from the worksheet, if assigned to any |
304
|
|
|
""" |
305
|
|
|
worksheet = analysis.getWorksheet() |
306
|
|
|
if not worksheet: |
307
|
|
|
return |
308
|
|
|
|
309
|
|
|
analyses = filter(lambda an: an != analysis, worksheet.getAnalyses()) |
310
|
|
|
worksheet.setAnalyses(analyses) |
311
|
|
|
worksheet.purgeLayout() |
312
|
|
|
if analyses: |
313
|
|
|
# Maybe this analysis was the only one that was not yet submitted or |
314
|
|
|
# verified, so try to submit or verify the Worksheet to be aligned |
315
|
|
|
# with the current states of the analyses it contains. |
316
|
|
|
doActionFor(worksheet, "submit") |
317
|
|
|
doActionFor(worksheet, "verify") |
318
|
|
|
else: |
319
|
|
|
# We've removed all analyses. Rollback to "open" |
320
|
|
|
doActionFor(worksheet, "rollback_to_open") |
321
|
|
|
|
322
|
|
|
# Reindex the Worksheet |
323
|
|
|
worksheet.reindexObject() |
324
|
|
|
|
325
|
|
|
|
326
|
|
|
def cascade_to_dependents(analysis, transition_id): |
327
|
|
|
"""Cascades the transition to dependent analyses (those that depend on the |
328
|
|
|
analysis passed in), if any |
329
|
|
|
""" |
330
|
|
|
for dependent in analysis.getDependents(): |
331
|
|
|
doActionFor(dependent, transition_id) |
332
|
|
|
|
333
|
|
|
|
334
|
|
|
def promote_to_dependencies(analysis, transition_id): |
335
|
|
|
"""Promotes the transition to the analyses this analysis depends on |
336
|
|
|
(dependencies), if any |
337
|
|
|
""" |
338
|
|
|
for dependency in analysis.getDependencies(): |
339
|
|
|
doActionFor(dependency, transition_id) |
340
|
|
|
|