1
|
|
|
# -*- coding: utf-8 -*- |
2
|
|
|
# |
3
|
|
|
# This file is part of SENAITE.CORE. |
4
|
|
|
# |
5
|
|
|
# SENAITE.CORE is free software: you can redistribute it and/or modify it under |
6
|
|
|
# the terms of the GNU General Public License as published by the Free Software |
7
|
|
|
# Foundation, version 2. |
8
|
|
|
# |
9
|
|
|
# This program is distributed in the hope that it will be useful, but WITHOUT |
10
|
|
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
11
|
|
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
12
|
|
|
# details. |
13
|
|
|
# |
14
|
|
|
# You should have received a copy of the GNU General Public License along with |
15
|
|
|
# this program; if not, write to the Free Software Foundation, Inc., 51 |
16
|
|
|
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
17
|
|
|
# |
18
|
|
|
# Copyright 2018-2019 by it's authors. |
19
|
|
|
# Some rights reserved, see README and LICENSE. |
20
|
|
|
|
21
|
|
|
import json |
22
|
|
|
from collections import OrderedDict |
23
|
|
|
from datetime import datetime |
24
|
|
|
|
25
|
|
|
from BTrees.OOBTree import OOBTree |
26
|
|
|
from DateTime import DateTime |
27
|
|
|
from Products.CMFPlone.utils import _createObjectByType |
28
|
|
|
from Products.CMFPlone.utils import safe_unicode |
29
|
|
|
from Products.Five.browser import BrowserView |
30
|
|
|
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile |
31
|
|
|
from plone import protect |
32
|
|
|
from plone.memoize import view as viewcache |
33
|
|
|
from plone.memoize.volatile import DontCache |
34
|
|
|
from plone.memoize.volatile import cache |
35
|
|
|
from zope.annotation.interfaces import IAnnotations |
36
|
|
|
from zope.component import getAdapters |
37
|
|
|
from zope.component import queryAdapter |
38
|
|
|
from zope.i18n.locales import locales |
39
|
|
|
from zope.interface import implements |
40
|
|
|
from zope.publisher.interfaces import IPublishTraverse |
41
|
|
|
|
42
|
|
|
from bika.lims import POINTS_OF_CAPTURE |
43
|
|
|
from bika.lims import api |
44
|
|
|
from bika.lims import bikaMessageFactory as _ |
45
|
|
|
from bika.lims import logger |
46
|
|
|
from bika.lims.api.analysisservice import get_calculation_dependencies_for |
47
|
|
|
from bika.lims.api.analysisservice import get_service_dependencies_for |
48
|
|
|
from bika.lims.interfaces import IGetDefaultFieldValueARAddHook, \ |
49
|
|
|
IAddSampleFieldsFlush, IAddSampleObjectInfo |
50
|
|
|
from bika.lims.utils import tmpID |
51
|
|
|
from bika.lims.utils.analysisrequest import create_analysisrequest as crar |
52
|
|
|
from bika.lims.workflow import ActionHandlerPool |
53
|
|
|
|
54
|
|
|
AR_CONFIGURATION_STORAGE = "bika.lims.browser.analysisrequest.manage.add" |
55
|
|
|
SKIP_FIELD_ON_COPY = ["Sample", "PrimaryAnalysisRequest", "Remarks"] |
56
|
|
|
|
57
|
|
|
|
58
|
|
View Code Duplication |
def returns_json(func): |
|
|
|
|
59
|
|
|
"""Decorator for functions which return JSON |
60
|
|
|
""" |
61
|
|
|
def decorator(*args, **kwargs): |
62
|
|
|
instance = args[0] |
63
|
|
|
request = getattr(instance, 'request', None) |
64
|
|
|
request.response.setHeader("Content-Type", "application/json") |
65
|
|
|
result = func(*args, **kwargs) |
66
|
|
|
return json.dumps(result) |
67
|
|
|
return decorator |
68
|
|
|
|
69
|
|
|
|
70
|
|
|
def cache_key(method, self, obj): |
71
|
|
|
if obj is None: |
72
|
|
|
raise DontCache |
73
|
|
|
return api.get_cache_key(obj) |
74
|
|
|
|
75
|
|
|
|
76
|
|
|
class AnalysisRequestAddView(BrowserView): |
77
|
|
|
"""AR Add view |
78
|
|
|
""" |
79
|
|
|
template = ViewPageTemplateFile("templates/ar_add2.pt") |
80
|
|
|
|
81
|
|
|
def __init__(self, context, request): |
82
|
|
|
super(AnalysisRequestAddView, self).__init__(context, request) |
83
|
|
|
self.request = request |
84
|
|
|
self.context = context |
85
|
|
|
self.fieldvalues = {} |
86
|
|
|
self.tmp_ar = None |
87
|
|
|
|
88
|
|
|
def __call__(self): |
89
|
|
|
self.portal = api.get_portal() |
90
|
|
|
self.portal_url = self.portal.absolute_url() |
91
|
|
|
self.bika_setup = api.get_bika_setup() |
92
|
|
|
self.request.set('disable_plone.rightcolumn', 1) |
93
|
|
|
self.came_from = "add" |
94
|
|
|
self.tmp_ar = self.get_ar() |
95
|
|
|
self.ar_count = self.get_ar_count() |
96
|
|
|
self.fieldvalues = self.generate_fieldvalues(self.ar_count) |
97
|
|
|
self.specifications = self.generate_specifications(self.ar_count) |
98
|
|
|
self.ShowPrices = self.bika_setup.getShowPrices() |
99
|
|
|
self.icon = self.portal_url + \ |
100
|
|
|
"/++resource++bika.lims.images/sample_big.png" |
101
|
|
|
logger.info("*** Prepared data for {} ARs ***".format(self.ar_count)) |
102
|
|
|
return self.template() |
103
|
|
|
|
104
|
|
|
def get_view_url(self): |
105
|
|
|
"""Return the current view url including request parameters |
106
|
|
|
""" |
107
|
|
|
request = self.request |
108
|
|
|
url = request.getURL() |
109
|
|
|
qs = request.getHeader("query_string") |
110
|
|
|
if not qs: |
111
|
|
|
return url |
112
|
|
|
return "{}?{}".format(url, qs) |
113
|
|
|
|
114
|
|
|
# N.B.: We are caching here persistent objects! |
115
|
|
|
# It should be safe to do this but only on the view object, |
116
|
|
|
# because it get recreated per request (transaction border). |
117
|
|
|
@viewcache.memoize |
118
|
|
|
def get_object_by_uid(self, uid): |
119
|
|
|
"""Get the object by UID |
120
|
|
|
""" |
121
|
|
|
logger.debug("get_object_by_uid::UID={}".format(uid)) |
122
|
|
|
obj = api.get_object_by_uid(uid, None) |
123
|
|
|
if obj is None: |
124
|
|
|
logger.warn("!! No object found for UID #{} !!") |
125
|
|
|
return obj |
126
|
|
|
|
127
|
|
|
def get_currency(self): |
128
|
|
|
"""Returns the configured currency |
129
|
|
|
""" |
130
|
|
|
bika_setup = api.get_bika_setup() |
131
|
|
|
currency = bika_setup.getCurrency() |
132
|
|
|
currencies = locales.getLocale('en').numbers.currencies |
133
|
|
|
return currencies[currency] |
134
|
|
|
|
135
|
|
|
def is_ar_specs_allowed(self): |
136
|
|
|
"""Checks if AR Specs are allowed |
137
|
|
|
""" |
138
|
|
|
bika_setup = api.get_bika_setup() |
139
|
|
|
return bika_setup.getEnableARSpecs() |
140
|
|
|
|
141
|
|
|
def get_ar_count(self): |
142
|
|
|
"""Return the ar_count request paramteter |
143
|
|
|
""" |
144
|
|
|
ar_count = 1 |
145
|
|
|
try: |
146
|
|
|
ar_count = int(self.request.form.get("ar_count", 1)) |
147
|
|
|
except (TypeError, ValueError): |
148
|
|
|
ar_count = 1 |
149
|
|
|
return ar_count |
150
|
|
|
|
151
|
|
|
def get_ar(self): |
152
|
|
|
"""Create a temporary AR to fetch the fields from |
153
|
|
|
""" |
154
|
|
|
if not self.tmp_ar: |
155
|
|
|
logger.info("*** CREATING TEMPORARY AR ***") |
156
|
|
|
self.tmp_ar = self.context.restrictedTraverse( |
157
|
|
|
"portal_factory/AnalysisRequest/Request new analyses") |
158
|
|
|
return self.tmp_ar |
159
|
|
|
|
160
|
|
|
def get_ar_schema(self): |
161
|
|
|
"""Return the AR schema |
162
|
|
|
""" |
163
|
|
|
logger.info("*** GET AR SCHEMA ***") |
164
|
|
|
ar = self.get_ar() |
165
|
|
|
return ar.Schema() |
166
|
|
|
|
167
|
|
|
def get_ar_fields(self): |
168
|
|
|
"""Return the AR schema fields (including extendend fields) |
169
|
|
|
""" |
170
|
|
|
logger.info("*** GET AR FIELDS ***") |
171
|
|
|
schema = self.get_ar_schema() |
172
|
|
|
return schema.fields() |
173
|
|
|
|
174
|
|
|
def get_fieldname(self, field, arnum): |
175
|
|
|
"""Generate a new fieldname with a '-<arnum>' suffix |
176
|
|
|
""" |
177
|
|
|
name = field.getName() |
178
|
|
|
# ensure we have only *one* suffix |
179
|
|
|
base_name = name.split("-")[0] |
180
|
|
|
suffix = "-{}".format(arnum) |
181
|
|
|
return "{}{}".format(base_name, suffix) |
182
|
|
|
|
183
|
|
|
def generate_specifications(self, count=1): |
184
|
|
|
"""Returns a mapping of count -> specification |
185
|
|
|
""" |
186
|
|
|
|
187
|
|
|
out = {} |
188
|
|
|
|
189
|
|
|
# mapping of UID index to AR objects {1: <AR1>, 2: <AR2> ...} |
190
|
|
|
copy_from = self.get_copy_from() |
191
|
|
|
|
192
|
|
|
for arnum in range(count): |
193
|
|
|
# get the source object |
194
|
|
|
source = copy_from.get(arnum) |
195
|
|
|
|
196
|
|
|
if source is None: |
197
|
|
|
out[arnum] = {} |
198
|
|
|
continue |
199
|
|
|
|
200
|
|
|
# get the results range from the source object |
201
|
|
|
results_range = source.getResultsRange() |
202
|
|
|
|
203
|
|
|
# mapping of keyword -> rr specification |
204
|
|
|
specification = {} |
205
|
|
|
for rr in results_range: |
206
|
|
|
specification[rr.get("keyword")] = rr |
207
|
|
|
out[arnum] = specification |
208
|
|
|
|
209
|
|
|
return out |
210
|
|
|
|
211
|
|
|
def get_input_widget(self, fieldname, arnum=0, **kw): |
212
|
|
|
"""Get the field widget of the AR in column <arnum> |
213
|
|
|
|
214
|
|
|
:param fieldname: The base fieldname |
215
|
|
|
:type fieldname: string |
216
|
|
|
""" |
217
|
|
|
|
218
|
|
|
# temporary AR Context |
219
|
|
|
context = self.get_ar() |
220
|
|
|
# request = self.request |
221
|
|
|
schema = context.Schema() |
222
|
|
|
|
223
|
|
|
# get original field in the schema from the base_fieldname |
224
|
|
|
base_fieldname = fieldname.split("-")[0] |
225
|
|
|
field = context.getField(base_fieldname) |
226
|
|
|
|
227
|
|
|
# fieldname with -<arnum> suffix |
228
|
|
|
new_fieldname = self.get_fieldname(field, arnum) |
229
|
|
|
new_field = field.copy(name=new_fieldname) |
230
|
|
|
|
231
|
|
|
# get the default value for this field |
232
|
|
|
fieldvalues = self.fieldvalues |
233
|
|
|
field_value = fieldvalues.get(new_fieldname) |
234
|
|
|
# request_value = request.form.get(new_fieldname) |
235
|
|
|
# value = request_value or field_value |
236
|
|
|
value = field_value |
237
|
|
|
|
238
|
|
|
def getAccessor(instance): |
239
|
|
|
def accessor(**kw): |
240
|
|
|
return value |
241
|
|
|
return accessor |
242
|
|
|
|
243
|
|
|
# inject the new context for the widget renderer |
244
|
|
|
# see: Products.Archetypes.Renderer.render |
245
|
|
|
kw["here"] = context |
246
|
|
|
kw["context"] = context |
247
|
|
|
kw["fieldName"] = new_fieldname |
248
|
|
|
|
249
|
|
|
# make the field available with this name |
250
|
|
|
# XXX: This is a hack to make the widget available in the template |
251
|
|
|
schema._fields[new_fieldname] = new_field |
252
|
|
|
new_field.getAccessor = getAccessor |
253
|
|
|
|
254
|
|
|
# set the default value |
255
|
|
|
form = dict() |
256
|
|
|
form[new_fieldname] = value |
257
|
|
|
self.request.form.update(form) |
258
|
|
|
logger.info("get_input_widget: fieldname={} arnum={} " |
259
|
|
|
"-> new_fieldname={} value={}".format( |
260
|
|
|
fieldname, arnum, new_fieldname, value)) |
261
|
|
|
widget = context.widget(new_fieldname, **kw) |
262
|
|
|
return widget |
263
|
|
|
|
264
|
|
|
def get_copy_from(self): |
265
|
|
|
"""Returns a mapping of UID index -> AR object |
266
|
|
|
""" |
267
|
|
|
# Create a mapping of source ARs for copy |
268
|
|
|
copy_from = self.request.form.get("copy_from", "").split(",") |
269
|
|
|
# clean out empty strings |
270
|
|
|
copy_from_uids = filter(lambda x: x, copy_from) |
271
|
|
|
out = dict().fromkeys(range(len(copy_from_uids))) |
272
|
|
|
for n, uid in enumerate(copy_from_uids): |
273
|
|
|
ar = self.get_object_by_uid(uid) |
274
|
|
|
if ar is None: |
275
|
|
|
continue |
276
|
|
|
out[n] = ar |
277
|
|
|
logger.info("get_copy_from: uids={}".format(copy_from_uids)) |
278
|
|
|
return out |
279
|
|
|
|
280
|
|
|
def get_default_value(self, field, context, arnum): |
281
|
|
|
"""Get the default value of the field |
282
|
|
|
""" |
283
|
|
|
name = field.getName() |
284
|
|
|
default = field.getDefault(context) |
285
|
|
|
if name == "Batch": |
286
|
|
|
batch = self.get_batch() |
287
|
|
|
if batch is not None: |
288
|
|
|
default = batch |
289
|
|
|
if name == "Client": |
290
|
|
|
client = self.get_client() |
291
|
|
|
if client is not None: |
292
|
|
|
default = client |
293
|
|
|
# only set default contact for first column |
294
|
|
|
if name == "Contact" and arnum == 0: |
295
|
|
|
contact = self.get_default_contact() |
296
|
|
|
if contact is not None: |
297
|
|
|
default = contact |
298
|
|
|
if name == "Sample": |
299
|
|
|
sample = self.get_sample() |
300
|
|
|
if sample is not None: |
301
|
|
|
default = sample |
302
|
|
|
# Querying for adapters to get default values from add-ons': |
303
|
|
|
# We don't know which fields the form will render since |
304
|
|
|
# some of them may come from add-ons. In order to obtain the default |
305
|
|
|
# value for those fields we take advantage of adapters. Adapters |
306
|
|
|
# registration should have the following format: |
307
|
|
|
# < adapter |
308
|
|
|
# factory = ... |
309
|
|
|
# for = "*" |
310
|
|
|
# provides = "bika.lims.interfaces.IGetDefaultFieldValueARAddHook" |
311
|
|
|
# name = "<fieldName>_default_value_hook" |
312
|
|
|
# / > |
313
|
|
|
hook_name = name + '_default_value_hook' |
314
|
|
|
adapter = queryAdapter( |
315
|
|
|
self.request, |
316
|
|
|
name=hook_name, |
317
|
|
|
interface=IGetDefaultFieldValueARAddHook) |
318
|
|
|
if adapter is not None: |
319
|
|
|
default = adapter(self.context) |
320
|
|
|
logger.info("get_default_value: context={} field={} value={} arnum={}" |
321
|
|
|
.format(context, name, default, arnum)) |
322
|
|
|
return default |
323
|
|
|
|
324
|
|
|
def get_field_value(self, field, context): |
325
|
|
|
"""Get the stored value of the field |
326
|
|
|
""" |
327
|
|
|
name = field.getName() |
328
|
|
|
value = context.getField(name).get(context) |
329
|
|
|
logger.info("get_field_value: context={} field={} value={}".format( |
330
|
|
|
context, name, value)) |
331
|
|
|
return value |
332
|
|
|
|
333
|
|
|
def get_client(self): |
334
|
|
|
"""Returns the Client |
335
|
|
|
""" |
336
|
|
|
context = self.context |
337
|
|
|
parent = api.get_parent(context) |
338
|
|
|
if context.portal_type == "Client": |
339
|
|
|
return context |
340
|
|
|
elif parent.portal_type == "Client": |
341
|
|
|
return parent |
342
|
|
|
elif context.portal_type == "Batch": |
343
|
|
|
return context.getClient() |
344
|
|
|
elif parent.portal_type == "Batch": |
345
|
|
|
return context.getClient() |
346
|
|
|
return None |
347
|
|
|
|
348
|
|
|
def get_sample(self): |
349
|
|
|
"""Returns the Sample |
350
|
|
|
""" |
351
|
|
|
context = self.context |
352
|
|
|
if context.portal_type == "Sample": |
353
|
|
|
return context |
354
|
|
|
return None |
355
|
|
|
|
356
|
|
|
def get_batch(self): |
357
|
|
|
"""Returns the Batch |
358
|
|
|
""" |
359
|
|
|
context = self.context |
360
|
|
|
parent = api.get_parent(context) |
361
|
|
|
if context.portal_type == "Batch": |
362
|
|
|
return context |
363
|
|
|
elif parent.portal_type == "Batch": |
364
|
|
|
return parent |
365
|
|
|
return None |
366
|
|
|
|
367
|
|
|
def get_parent_ar(self, ar): |
368
|
|
|
"""Returns the parent AR |
369
|
|
|
""" |
370
|
|
|
parent = ar.getParentAnalysisRequest() |
371
|
|
|
|
372
|
|
|
# Return immediately if we have no parent |
373
|
|
|
if parent is None: |
374
|
|
|
return None |
375
|
|
|
|
376
|
|
|
# Walk back the chain until we reach the source AR |
377
|
|
|
while True: |
378
|
|
|
pparent = parent.getParentAnalysisRequest() |
379
|
|
|
if pparent is None: |
380
|
|
|
break |
381
|
|
|
# remember the new parent |
382
|
|
|
parent = pparent |
383
|
|
|
|
384
|
|
|
return parent |
385
|
|
|
|
386
|
|
|
def generate_fieldvalues(self, count=1): |
387
|
|
|
"""Returns a mapping of '<fieldname>-<count>' to the default value |
388
|
|
|
of the field or the field value of the source AR |
389
|
|
|
""" |
390
|
|
|
ar_context = self.get_ar() |
391
|
|
|
|
392
|
|
|
# mapping of UID index to AR objects {1: <AR1>, 2: <AR2> ...} |
393
|
|
|
copy_from = self.get_copy_from() |
394
|
|
|
|
395
|
|
|
out = {} |
396
|
|
|
# the original schema fields of an AR (including extended fields) |
397
|
|
|
fields = self.get_ar_fields() |
398
|
|
|
|
399
|
|
|
# generate fields for all requested ARs |
400
|
|
|
for arnum in range(count): |
401
|
|
|
source = copy_from.get(arnum) |
402
|
|
|
parent = None |
403
|
|
|
if source is not None: |
404
|
|
|
parent = self.get_parent_ar(source) |
405
|
|
|
for field in fields: |
406
|
|
|
value = None |
407
|
|
|
fieldname = field.getName() |
408
|
|
|
if source and fieldname not in SKIP_FIELD_ON_COPY: |
409
|
|
|
# get the field value stored on the source |
410
|
|
|
context = parent or source |
411
|
|
|
value = self.get_field_value(field, context) |
412
|
|
|
else: |
413
|
|
|
# get the default value of this field |
414
|
|
|
value = self.get_default_value( |
415
|
|
|
field, ar_context, arnum=arnum) |
416
|
|
|
# store the value on the new fieldname |
417
|
|
|
new_fieldname = self.get_fieldname(field, arnum) |
418
|
|
|
out[new_fieldname] = value |
419
|
|
|
|
420
|
|
|
return out |
421
|
|
|
|
422
|
|
|
def get_default_contact(self, client=None): |
423
|
|
|
"""Logic refactored from JavaScript: |
424
|
|
|
|
425
|
|
|
* If client only has one contact, and the analysis request comes from |
426
|
|
|
* a client, then Auto-complete first Contact field. |
427
|
|
|
* If client only has one contect, and the analysis request comes from |
428
|
|
|
* a batch, then Auto-complete all Contact field. |
429
|
|
|
|
430
|
|
|
:returns: The default contact for the AR |
431
|
|
|
:rtype: Client object or None |
432
|
|
|
""" |
433
|
|
|
catalog = api.get_tool("portal_catalog") |
434
|
|
|
client = client or self.get_client() |
435
|
|
|
path = api.get_path(self.context) |
436
|
|
|
if client: |
437
|
|
|
path = api.get_path(client) |
438
|
|
|
query = { |
439
|
|
|
"portal_type": "Contact", |
440
|
|
|
"path": { |
441
|
|
|
"query": path, |
442
|
|
|
"depth": 1 |
443
|
|
|
}, |
444
|
|
|
"is_active": True, |
445
|
|
|
} |
446
|
|
|
contacts = catalog(query) |
447
|
|
|
if len(contacts) == 1: |
448
|
|
|
return api.get_object(contacts[0]) |
449
|
|
|
elif client == api.get_current_client(): |
450
|
|
|
# Current user is a Client contact. Use current contact |
451
|
|
|
current_user = api.get_current_user() |
452
|
|
|
return api.get_user_contact(current_user, contact_types=["Contact"]) |
453
|
|
|
|
454
|
|
|
return None |
455
|
|
|
|
456
|
|
|
def getMemberDiscountApplies(self): |
457
|
|
|
"""Return if the member discount applies for this client |
458
|
|
|
|
459
|
|
|
:returns: True if member discount applies for the client |
460
|
|
|
:rtype: bool |
461
|
|
|
""" |
462
|
|
|
client = self.get_client() |
463
|
|
|
if client is None: |
464
|
|
|
return False |
465
|
|
|
return client.getMemberDiscountApplies() |
466
|
|
|
|
467
|
|
|
def is_field_visible(self, field): |
468
|
|
|
"""Check if the field is visible |
469
|
|
|
""" |
470
|
|
|
context = self.context |
471
|
|
|
fieldname = field.getName() |
472
|
|
|
|
473
|
|
|
# hide the Client field on client and batch contexts |
474
|
|
|
if fieldname == "Client" and context.portal_type in ("Client", ): |
475
|
|
|
return False |
476
|
|
|
|
477
|
|
|
# hide the Batch field on batch contexts |
478
|
|
|
if fieldname == "Batch" and context.portal_type in ("Batch", ): |
479
|
|
|
return False |
480
|
|
|
|
481
|
|
|
return True |
482
|
|
|
|
483
|
|
|
def get_fields_with_visibility(self, visibility, mode="add"): |
484
|
|
|
"""Return the AR fields with the current visibility |
485
|
|
|
""" |
486
|
|
|
ar = self.get_ar() |
487
|
|
|
mv = api.get_view("ar_add_manage", context=ar) |
488
|
|
|
mv.get_field_order() |
489
|
|
|
|
490
|
|
|
out = [] |
491
|
|
|
for field in mv.get_fields_with_visibility(visibility, mode): |
492
|
|
|
# check custom field condition |
493
|
|
|
visible = self.is_field_visible(field) |
494
|
|
|
if visible is False and visibility != "hidden": |
495
|
|
|
continue |
496
|
|
|
out.append(field) |
497
|
|
|
return out |
498
|
|
|
|
499
|
|
|
def get_service_categories(self, restricted=True): |
500
|
|
|
"""Return all service categories in the right order |
501
|
|
|
|
502
|
|
|
:param restricted: Client settings restrict categories |
503
|
|
|
:type restricted: bool |
504
|
|
|
:returns: Category catalog results |
505
|
|
|
:rtype: brains |
506
|
|
|
""" |
507
|
|
|
bsc = api.get_tool("bika_setup_catalog") |
508
|
|
|
query = { |
509
|
|
|
"portal_type": "AnalysisCategory", |
510
|
|
|
"is_active": True, |
511
|
|
|
"sort_on": "sortable_title", |
512
|
|
|
} |
513
|
|
|
categories = bsc(query) |
514
|
|
|
client = self.get_client() |
515
|
|
|
if client and restricted: |
516
|
|
|
restricted_categories = client.getRestrictedCategories() |
517
|
|
|
restricted_category_ids = map( |
518
|
|
|
lambda c: c.getId(), restricted_categories) |
519
|
|
|
# keep correct order of categories |
520
|
|
|
if restricted_category_ids: |
521
|
|
|
categories = filter( |
522
|
|
|
lambda c: c.getId in restricted_category_ids, categories) |
|
|
|
|
523
|
|
|
return categories |
524
|
|
|
|
525
|
|
|
def get_points_of_capture(self): |
526
|
|
|
items = POINTS_OF_CAPTURE.items() |
527
|
|
|
return OrderedDict(items) |
528
|
|
|
|
529
|
|
|
def get_services(self, poc="lab"): |
530
|
|
|
"""Return all Services |
531
|
|
|
|
532
|
|
|
:param poc: Point of capture (lab/field) |
533
|
|
|
:type poc: string |
534
|
|
|
:returns: Mapping of category -> list of services |
535
|
|
|
:rtype: dict |
536
|
|
|
""" |
537
|
|
|
bsc = api.get_tool("bika_setup_catalog") |
538
|
|
|
query = { |
539
|
|
|
"portal_type": "AnalysisService", |
540
|
|
|
"getPointOfCapture": poc, |
541
|
|
|
"is_active": True, |
542
|
|
|
"sort_on": "sortable_title", |
543
|
|
|
} |
544
|
|
|
services = bsc(query) |
545
|
|
|
categories = self.get_service_categories(restricted=False) |
546
|
|
|
analyses = {key: [] for key in map(lambda c: c.Title, categories)} |
547
|
|
|
|
548
|
|
|
# append the empty category as well |
549
|
|
|
analyses[""] = [] |
550
|
|
|
|
551
|
|
|
for brain in services: |
552
|
|
|
category = brain.getCategoryTitle |
553
|
|
|
if category in analyses: |
554
|
|
|
analyses[category].append(brain) |
555
|
|
|
return analyses |
556
|
|
|
|
557
|
|
|
@cache(cache_key) |
558
|
|
|
def get_service_uid_from(self, analysis): |
559
|
|
|
"""Return the service from the analysis |
560
|
|
|
""" |
561
|
|
|
analysis = api.get_object(analysis) |
562
|
|
|
return api.get_uid(analysis.getAnalysisService()) |
563
|
|
|
|
564
|
|
|
def is_service_selected(self, service): |
565
|
|
|
"""Checks if the given service is selected by one of the ARs. |
566
|
|
|
This is used to make the whole line visible or not. |
567
|
|
|
""" |
568
|
|
|
service_uid = api.get_uid(service) |
569
|
|
|
for arnum in range(self.ar_count): |
570
|
|
|
analyses = self.fieldvalues.get("Analyses-{}".format(arnum)) |
571
|
|
|
if not analyses: |
572
|
|
|
continue |
573
|
|
|
service_uids = map(self.get_service_uid_from, analyses) |
574
|
|
|
if service_uid in service_uids: |
575
|
|
|
return True |
576
|
|
|
return False |
577
|
|
|
|
578
|
|
|
|
579
|
|
|
class AnalysisRequestManageView(BrowserView): |
580
|
|
|
"""AR Manage View |
581
|
|
|
""" |
582
|
|
|
template = ViewPageTemplateFile("templates/ar_add_manage.pt") |
583
|
|
|
|
584
|
|
|
def __init__(self, context, request): |
585
|
|
|
self.context = context |
586
|
|
|
self.request = request |
587
|
|
|
self.tmp_ar = None |
588
|
|
|
|
589
|
|
|
def __call__(self): |
590
|
|
|
protect.CheckAuthenticator(self.request.form) |
591
|
|
|
form = self.request.form |
592
|
|
|
if form.get("submitted", False) and form.get("save", False): |
593
|
|
|
order = form.get("order") |
594
|
|
|
self.set_field_order(order) |
595
|
|
|
visibility = form.get("visibility") |
596
|
|
|
self.set_field_visibility(visibility) |
597
|
|
|
if form.get("submitted", False) and form.get("reset", False): |
598
|
|
|
self.flush() |
599
|
|
|
return self.template() |
600
|
|
|
|
601
|
|
|
def get_ar(self): |
602
|
|
|
if not self.tmp_ar: |
603
|
|
|
self.tmp_ar = self.context.restrictedTraverse( |
604
|
|
|
"portal_factory/AnalysisRequest/Request new analyses") |
605
|
|
|
return self.tmp_ar |
606
|
|
|
|
607
|
|
|
def get_annotation(self): |
608
|
|
|
bika_setup = api.get_bika_setup() |
609
|
|
|
return IAnnotations(bika_setup) |
610
|
|
|
|
611
|
|
|
@property |
612
|
|
|
def storage(self): |
613
|
|
|
annotation = self.get_annotation() |
614
|
|
|
if annotation.get(AR_CONFIGURATION_STORAGE) is None: |
|
|
|
|
615
|
|
|
annotation[AR_CONFIGURATION_STORAGE] = OOBTree() |
616
|
|
|
return annotation[AR_CONFIGURATION_STORAGE] |
617
|
|
|
|
618
|
|
|
def flush(self): |
619
|
|
|
annotation = self.get_annotation() |
620
|
|
|
if annotation.get(AR_CONFIGURATION_STORAGE) is not None: |
621
|
|
|
del annotation[AR_CONFIGURATION_STORAGE] |
622
|
|
|
|
623
|
|
|
def set_field_order(self, order): |
624
|
|
|
self.storage.update({"order": order}) |
625
|
|
|
|
626
|
|
|
def get_field_order(self): |
627
|
|
|
order = self.storage.get("order") |
628
|
|
|
if order is None: |
629
|
|
|
return map(lambda f: f.getName(), self.get_fields()) |
630
|
|
|
return order |
631
|
|
|
|
632
|
|
|
def set_field_visibility(self, visibility): |
633
|
|
|
self.storage.update({"visibility": visibility}) |
634
|
|
|
|
635
|
|
|
def get_field_visibility(self): |
636
|
|
|
return self.storage.get("visibility") |
637
|
|
|
|
638
|
|
|
def is_field_visible(self, field): |
639
|
|
|
if field.required: |
640
|
|
|
return True |
641
|
|
|
visibility = self.get_field_visibility() |
642
|
|
|
if visibility is None: |
643
|
|
|
return True |
644
|
|
|
return visibility.get(field.getName(), True) |
645
|
|
|
|
646
|
|
|
def get_field(self, name): |
647
|
|
|
"""Get AR field by name |
648
|
|
|
""" |
649
|
|
|
ar = self.get_ar() |
650
|
|
|
return ar.getField(name) |
651
|
|
|
|
652
|
|
|
def get_fields(self): |
653
|
|
|
"""Return all AR fields |
654
|
|
|
""" |
655
|
|
|
ar = self.get_ar() |
656
|
|
|
return ar.Schema().fields() |
657
|
|
|
|
658
|
|
|
def get_sorted_fields(self): |
659
|
|
|
"""Return the sorted fields |
660
|
|
|
""" |
661
|
|
|
inf = float("inf") |
662
|
|
|
order = self.get_field_order() |
663
|
|
|
|
664
|
|
|
def field_cmp(field1, field2): |
665
|
|
|
_n1 = field1.getName() |
666
|
|
|
_n2 = field2.getName() |
667
|
|
|
_i1 = _n1 in order and order.index(_n1) + 1 or inf |
668
|
|
|
_i2 = _n2 in order and order.index(_n2) + 1 or inf |
669
|
|
|
return cmp(_i1, _i2) |
670
|
|
|
|
671
|
|
|
return sorted(self.get_fields(), cmp=field_cmp) |
672
|
|
|
|
673
|
|
|
def get_fields_with_visibility(self, visibility="edit", mode="add"): |
674
|
|
|
"""Return the fields with visibility |
675
|
|
|
""" |
676
|
|
|
fields = self.get_sorted_fields() |
677
|
|
|
|
678
|
|
|
out = [] |
679
|
|
|
|
680
|
|
|
for field in fields: |
681
|
|
|
v = field.widget.isVisible( |
682
|
|
|
self.context, mode, default='invisible', field=field) |
683
|
|
|
|
684
|
|
|
if self.is_field_visible(field) is False: |
685
|
|
|
v = "hidden" |
686
|
|
|
|
687
|
|
|
visibility_guard = True |
688
|
|
|
# visibility_guard is a widget field defined in the schema in order |
689
|
|
|
# to know the visibility of the widget when the field is related to |
690
|
|
|
# a dynamically changing content such as workflows. For instance |
691
|
|
|
# those fields related to the workflow will be displayed only if |
692
|
|
|
# the workflow is enabled, otherwise they should not be shown. |
693
|
|
|
if 'visibility_guard' in dir(field.widget): |
694
|
|
|
visibility_guard = eval(field.widget.visibility_guard) |
695
|
|
|
if v == visibility and visibility_guard: |
696
|
|
|
out.append(field) |
697
|
|
|
|
698
|
|
|
return out |
699
|
|
|
|
700
|
|
|
|
701
|
|
|
class ajaxAnalysisRequestAddView(AnalysisRequestAddView): |
702
|
|
|
"""Ajax helpers for the analysis request add form |
703
|
|
|
""" |
704
|
|
|
implements(IPublishTraverse) |
705
|
|
|
|
706
|
|
|
def __init__(self, context, request): |
707
|
|
|
super(ajaxAnalysisRequestAddView, self).__init__(context, request) |
708
|
|
|
self.context = context |
709
|
|
|
self.request = request |
710
|
|
|
self.traverse_subpath = [] |
711
|
|
|
# Errors are aggregated here, and returned together to the browser |
712
|
|
|
self.errors = {} |
713
|
|
|
|
714
|
|
|
def publishTraverse(self, request, name): |
715
|
|
|
""" get's called before __call__ for each path name |
716
|
|
|
""" |
717
|
|
|
self.traverse_subpath.append(name) |
718
|
|
|
return self |
719
|
|
|
|
720
|
|
View Code Duplication |
@returns_json |
|
|
|
|
721
|
|
|
def __call__(self): |
722
|
|
|
"""Dispatch the path to a method and return JSON. |
723
|
|
|
""" |
724
|
|
|
protect.CheckAuthenticator(self.request.form) |
725
|
|
|
protect.PostOnly(self.request.form) |
726
|
|
|
|
727
|
|
|
if len(self.traverse_subpath) != 1: |
728
|
|
|
return self.error("Not found", status=404) |
729
|
|
|
func_name = "ajax_{}".format(self.traverse_subpath[0]) |
730
|
|
|
func = getattr(self, func_name, None) |
731
|
|
|
if func is None: |
732
|
|
|
return self.error("Invalid function", status=400) |
733
|
|
|
return func() |
734
|
|
|
|
735
|
|
|
def error(self, message, status=500, **kw): |
736
|
|
|
"""Set a JSON error object and a status to the response |
737
|
|
|
""" |
738
|
|
|
self.request.response.setStatus(status) |
739
|
|
|
result = {"success": False, "errors": message} |
740
|
|
|
result.update(kw) |
741
|
|
|
return result |
742
|
|
|
|
743
|
|
|
def to_iso_date(self, dt): |
744
|
|
|
"""Return the ISO representation of a date object |
745
|
|
|
""" |
746
|
|
|
if dt is None: |
747
|
|
|
return "" |
748
|
|
|
if isinstance(dt, DateTime): |
749
|
|
|
return dt.ISO8601() |
750
|
|
|
if isinstance(dt, datetime): |
751
|
|
|
return dt.isoformat() |
752
|
|
|
raise TypeError("{} is neiter an instance of DateTime nor datetime" |
753
|
|
|
.format(repr(dt))) |
754
|
|
|
|
755
|
|
|
def get_records(self): |
756
|
|
|
"""Returns a list of AR records |
757
|
|
|
|
758
|
|
|
Fields coming from `request.form` have a number prefix, e.g. Contact-0. |
759
|
|
|
Fields with the same suffix number are grouped together in a record. |
760
|
|
|
Each record represents the data for one column in the AR Add form and |
761
|
|
|
contains a mapping of the fieldName (w/o prefix) -> value. |
762
|
|
|
|
763
|
|
|
Example: |
764
|
|
|
[{"Contact": "Rita Mohale", ...}, {Contact: "Neil Standard"} ...] |
765
|
|
|
""" |
766
|
|
|
form = self.request.form |
767
|
|
|
ar_count = self.get_ar_count() |
768
|
|
|
|
769
|
|
|
records = [] |
770
|
|
|
# Group belonging AR fields together |
771
|
|
|
for arnum in range(ar_count): |
772
|
|
|
record = {} |
773
|
|
|
s1 = "-{}".format(arnum) |
774
|
|
|
keys = filter(lambda key: s1 in key, form.keys()) |
|
|
|
|
775
|
|
|
for key in keys: |
776
|
|
|
new_key = key.replace(s1, "") |
777
|
|
|
value = form.get(key) |
778
|
|
|
record[new_key] = value |
779
|
|
|
records.append(record) |
780
|
|
|
return records |
781
|
|
|
|
782
|
|
|
def get_uids_from_record(self, record, key): |
783
|
|
|
"""Returns a list of parsed UIDs from a single form field identified by |
784
|
|
|
the given key. |
785
|
|
|
|
786
|
|
|
A form field ending with `_uid` can contain an empty value, a |
787
|
|
|
single UID or multiple UIDs separated by a comma. |
788
|
|
|
|
789
|
|
|
This method parses the UID value and returns a list of non-empty UIDs. |
790
|
|
|
""" |
791
|
|
|
value = record.get(key, None) |
792
|
|
|
if value is None: |
793
|
|
|
return [] |
794
|
|
|
if isinstance(value, basestring): |
795
|
|
|
value = value.split(",") |
796
|
|
|
return filter(lambda uid: uid, value) |
797
|
|
|
|
798
|
|
|
@cache(cache_key) |
799
|
|
|
def get_base_info(self, obj): |
800
|
|
|
"""Returns the base info of an object |
801
|
|
|
""" |
802
|
|
|
if obj is None: |
803
|
|
|
return {} |
804
|
|
|
|
805
|
|
|
info = { |
806
|
|
|
"id": api.get_id(obj), |
807
|
|
|
"uid": api.get_uid(obj), |
808
|
|
|
"title": api.get_title(obj), |
809
|
|
|
"field_values": {}, |
810
|
|
|
"filter_queries": {}, |
811
|
|
|
} |
812
|
|
|
|
813
|
|
|
return info |
814
|
|
|
|
815
|
|
|
@cache(cache_key) |
816
|
|
|
def get_client_info(self, obj): |
817
|
|
|
"""Returns the client info of an object |
818
|
|
|
""" |
819
|
|
|
info = self.get_base_info(obj) |
820
|
|
|
default_contact = self.get_default_contact(client=obj) |
821
|
|
|
if default_contact: |
822
|
|
|
info["field_values"].update({ |
823
|
|
|
"Contact": self.get_contact_info(default_contact) |
824
|
|
|
}) |
825
|
|
|
|
826
|
|
|
# UID of the client |
827
|
|
|
uid = api.get_uid(obj) |
828
|
|
|
|
829
|
|
|
# Bika Setup folder |
830
|
|
|
bika_setup = api.get_bika_setup() |
831
|
|
|
|
832
|
|
|
# bika samplepoints |
833
|
|
|
bika_samplepoints = bika_setup.bika_samplepoints |
834
|
|
|
bika_samplepoints_uid = api.get_uid(bika_samplepoints) |
835
|
|
|
|
836
|
|
|
# bika artemplates |
837
|
|
|
bika_artemplates = bika_setup.bika_artemplates |
838
|
|
|
bika_artemplates_uid = api.get_uid(bika_artemplates) |
839
|
|
|
|
840
|
|
|
# bika analysisprofiles |
841
|
|
|
bika_analysisprofiles = bika_setup.bika_analysisprofiles |
842
|
|
|
bika_analysisprofiles_uid = api.get_uid(bika_analysisprofiles) |
843
|
|
|
|
844
|
|
|
# bika analysisspecs |
845
|
|
|
bika_analysisspecs = bika_setup.bika_analysisspecs |
846
|
|
|
bika_analysisspecs_uid = api.get_uid(bika_analysisspecs) |
847
|
|
|
|
848
|
|
|
# catalog queries for UI field filtering |
849
|
|
|
filter_queries = { |
850
|
|
|
"Contact": { |
851
|
|
|
"getParentUID": [uid] |
852
|
|
|
}, |
853
|
|
|
"CCContact": { |
854
|
|
|
"getParentUID": [uid] |
855
|
|
|
}, |
856
|
|
|
"InvoiceContact": { |
857
|
|
|
"getParentUID": [uid] |
858
|
|
|
}, |
859
|
|
|
"SamplePoint": { |
860
|
|
|
"getClientUID": [uid, bika_samplepoints_uid], |
861
|
|
|
}, |
862
|
|
|
"Template": { |
863
|
|
|
"getClientUID": [uid, bika_artemplates_uid], |
864
|
|
|
}, |
865
|
|
|
"Profiles": { |
866
|
|
|
"getClientUID": [uid, bika_analysisprofiles_uid], |
867
|
|
|
}, |
868
|
|
|
"Specification": { |
869
|
|
|
"getClientUID": [uid, bika_analysisspecs_uid], |
870
|
|
|
}, |
871
|
|
|
"SamplingRound": { |
872
|
|
|
"getParentUID": [uid], |
873
|
|
|
}, |
874
|
|
|
"Sample": { |
875
|
|
|
"getClientUID": [uid], |
876
|
|
|
}, |
877
|
|
|
"Batch": { |
878
|
|
|
"getClientUID": [uid, ""], |
879
|
|
|
} |
880
|
|
|
} |
881
|
|
|
info["filter_queries"] = filter_queries |
882
|
|
|
return info |
883
|
|
|
|
884
|
|
|
@cache(cache_key) |
885
|
|
|
def get_contact_info(self, obj): |
886
|
|
|
"""Returns the client info of an object |
887
|
|
|
""" |
888
|
|
|
|
889
|
|
|
info = self.get_base_info(obj) |
890
|
|
|
fullname = obj.getFullname() |
891
|
|
|
email = obj.getEmailAddress() |
892
|
|
|
|
893
|
|
|
# Note: It might get a circular dependency when calling: |
894
|
|
|
# map(self.get_contact_info, obj.getCCContact()) |
895
|
|
|
cccontacts = [] |
896
|
|
|
for contact in obj.getCCContact(): |
897
|
|
|
uid = api.get_uid(contact) |
898
|
|
|
fullname = contact.getFullname() |
899
|
|
|
email = contact.getEmailAddress() |
900
|
|
|
cccontacts.append({ |
901
|
|
|
"uid": uid, |
902
|
|
|
"title": fullname, |
903
|
|
|
"fullname": fullname, |
904
|
|
|
"email": email |
905
|
|
|
}) |
906
|
|
|
|
907
|
|
|
info.update({ |
908
|
|
|
"fullname": fullname, |
909
|
|
|
"email": email, |
910
|
|
|
"field_values": { |
911
|
|
|
"CCContact": cccontacts |
912
|
|
|
}, |
913
|
|
|
}) |
914
|
|
|
|
915
|
|
|
return info |
916
|
|
|
|
917
|
|
|
@cache(cache_key) |
918
|
|
|
def get_service_info(self, obj): |
919
|
|
|
"""Returns the info for a Service |
920
|
|
|
""" |
921
|
|
|
info = self.get_base_info(obj) |
922
|
|
|
|
923
|
|
|
info.update({ |
924
|
|
|
"short_title": obj.getShortTitle(), |
925
|
|
|
"scientific_name": obj.getScientificName(), |
926
|
|
|
"unit": obj.getUnit(), |
927
|
|
|
"keyword": obj.getKeyword(), |
928
|
|
|
"methods": map(self.get_method_info, obj.getMethods()), |
929
|
|
|
"calculation": self.get_calculation_info(obj.getCalculation()), |
930
|
|
|
"price": obj.getPrice(), |
931
|
|
|
"currency_symbol": self.get_currency().symbol, |
932
|
|
|
"accredited": obj.getAccredited(), |
933
|
|
|
"category": obj.getCategoryTitle(), |
934
|
|
|
"poc": obj.getPointOfCapture(), |
935
|
|
|
|
936
|
|
|
}) |
937
|
|
|
|
938
|
|
|
dependencies = get_calculation_dependencies_for(obj).values() |
939
|
|
|
info["dependencies"] = map(self.get_base_info, dependencies) |
940
|
|
|
return info |
941
|
|
|
|
942
|
|
|
@cache(cache_key) |
943
|
|
|
def get_template_info(self, obj): |
944
|
|
|
"""Returns the info for a Template |
945
|
|
|
""" |
946
|
|
|
client = self.get_client() |
947
|
|
|
client_uid = api.get_uid(client) if client else "" |
948
|
|
|
|
949
|
|
|
profile = obj.getAnalysisProfile() |
950
|
|
|
profile_uid = api.get_uid(profile) if profile else "" |
951
|
|
|
profile_title = profile.Title() if profile else "" |
952
|
|
|
|
953
|
|
|
sample_type = obj.getSampleType() |
954
|
|
|
sample_type_uid = api.get_uid(sample_type) if sample_type else "" |
955
|
|
|
sample_type_title = sample_type.Title() if sample_type else "" |
956
|
|
|
|
957
|
|
|
sample_point = obj.getSamplePoint() |
958
|
|
|
sample_point_uid = api.get_uid(sample_point) if sample_point else "" |
959
|
|
|
sample_point_title = sample_point.Title() if sample_point else "" |
960
|
|
|
|
961
|
|
|
service_uids = [] |
962
|
|
|
analyses_partitions = {} |
963
|
|
|
analyses = obj.getAnalyses() |
964
|
|
|
|
965
|
|
|
for record in analyses: |
966
|
|
|
service_uid = record.get("service_uid") |
967
|
|
|
service_uids.append(service_uid) |
968
|
|
|
analyses_partitions[service_uid] = record.get("partition") |
969
|
|
|
|
970
|
|
|
info = self.get_base_info(obj) |
971
|
|
|
info.update({ |
972
|
|
|
"analyses_partitions": analyses_partitions, |
973
|
|
|
"analysis_profile_title": profile_title, |
974
|
|
|
"analysis_profile_uid": profile_uid, |
975
|
|
|
"client_uid": client_uid, |
976
|
|
|
"composite": obj.getComposite(), |
977
|
|
|
"partitions": obj.getPartitions(), |
978
|
|
|
"remarks": obj.getRemarks(), |
979
|
|
|
"sample_point_title": sample_point_title, |
980
|
|
|
"sample_point_uid": sample_point_uid, |
981
|
|
|
"sample_type_title": sample_type_title, |
982
|
|
|
"sample_type_uid": sample_type_uid, |
983
|
|
|
"service_uids": service_uids, |
984
|
|
|
}) |
985
|
|
|
return info |
986
|
|
|
|
987
|
|
|
@cache(cache_key) |
988
|
|
|
def get_profile_info(self, obj): |
989
|
|
|
"""Returns the info for a Profile |
990
|
|
|
""" |
991
|
|
|
info = self.get_base_info(obj) |
992
|
|
|
info.update({}) |
993
|
|
|
return info |
994
|
|
|
|
995
|
|
|
@cache(cache_key) |
996
|
|
|
def get_method_info(self, obj): |
997
|
|
|
"""Returns the info for a Method |
998
|
|
|
""" |
999
|
|
|
info = self.get_base_info(obj) |
1000
|
|
|
info.update({}) |
1001
|
|
|
return info |
1002
|
|
|
|
1003
|
|
|
@cache(cache_key) |
1004
|
|
|
def get_calculation_info(self, obj): |
1005
|
|
|
"""Returns the info for a Calculation |
1006
|
|
|
""" |
1007
|
|
|
info = self.get_base_info(obj) |
1008
|
|
|
info.update({}) |
1009
|
|
|
return info |
1010
|
|
|
|
1011
|
|
|
@cache(cache_key) |
1012
|
|
|
def get_sampletype_info(self, obj): |
1013
|
|
|
"""Returns the info for a Sample Type |
1014
|
|
|
""" |
1015
|
|
|
info = self.get_base_info(obj) |
1016
|
|
|
|
1017
|
|
|
# Bika Setup folder |
1018
|
|
|
bika_setup = api.get_bika_setup() |
1019
|
|
|
|
1020
|
|
|
# bika samplepoints |
1021
|
|
|
bika_samplepoints = bika_setup.bika_samplepoints |
1022
|
|
|
bika_samplepoints_uid = api.get_uid(bika_samplepoints) |
1023
|
|
|
|
1024
|
|
|
# bika analysisspecs |
1025
|
|
|
bika_analysisspecs = bika_setup.bika_analysisspecs |
1026
|
|
|
bika_analysisspecs_uid = api.get_uid(bika_analysisspecs) |
1027
|
|
|
|
1028
|
|
|
# client |
1029
|
|
|
client = self.get_client() |
1030
|
|
|
client_uid = client and api.get_uid(client) or "" |
1031
|
|
|
|
1032
|
|
|
info.update({ |
1033
|
|
|
"prefix": obj.getPrefix(), |
1034
|
|
|
"minimum_volume": obj.getMinimumVolume(), |
1035
|
|
|
"hazardous": obj.getHazardous(), |
1036
|
|
|
"retention_period": obj.getRetentionPeriod(), |
1037
|
|
|
}) |
1038
|
|
|
|
1039
|
|
|
# catalog queries for UI field filtering |
1040
|
|
|
filter_queries = { |
1041
|
|
|
"SamplePoint": { |
1042
|
|
|
"getSampleTypeTitles": [obj.Title(), ''], |
1043
|
|
|
"getClientUID": [client_uid, bika_samplepoints_uid], |
1044
|
|
|
"sort_order": "descending", |
1045
|
|
|
}, |
1046
|
|
|
"Specification": { |
1047
|
|
|
"getSampleTypeTitle": obj.Title(), |
1048
|
|
|
"getClientUID": [client_uid, bika_analysisspecs_uid], |
1049
|
|
|
"sort_order": "descending", |
1050
|
|
|
} |
1051
|
|
|
} |
1052
|
|
|
info["filter_queries"] = filter_queries |
1053
|
|
|
|
1054
|
|
|
return info |
1055
|
|
|
|
1056
|
|
|
@cache(cache_key) |
1057
|
|
|
def get_primaryanalysisrequest_info(self, obj): |
1058
|
|
|
"""Returns the info for a Primary Sample |
1059
|
|
|
""" |
1060
|
|
|
info = self.get_base_info(obj) |
1061
|
|
|
|
1062
|
|
|
batch = obj.getBatch() |
1063
|
|
|
client = obj.getClient() |
1064
|
|
|
sample_type = obj.getSampleType() |
1065
|
|
|
sample_condition = obj.getSampleCondition() |
1066
|
|
|
storage_location = obj.getStorageLocation() |
1067
|
|
|
sample_point = obj.getSamplePoint() |
1068
|
|
|
container = obj.getContainer() |
1069
|
|
|
deviation = obj.getSamplingDeviation() |
1070
|
|
|
preservation = obj.getPreservation() |
1071
|
|
|
specification = obj.getSpecification() |
1072
|
|
|
sample_template = obj.getTemplate() |
1073
|
|
|
profiles = obj.getProfiles() or [] |
1074
|
|
|
cccontacts = obj.getCCContact() or [] |
1075
|
|
|
contact = obj.getContact() |
1076
|
|
|
|
1077
|
|
|
info.update({ |
1078
|
|
|
"composite": obj.getComposite(), |
1079
|
|
|
}) |
1080
|
|
|
|
1081
|
|
|
# Set the fields for which we want the value to be set automatically |
1082
|
|
|
# when the primary sample is selected |
1083
|
|
|
info["field_values"].update({ |
1084
|
|
|
"Client": self.to_field_value(client), |
1085
|
|
|
"Contact": self.to_field_value(contact), |
1086
|
|
|
"CCContact": map(self.to_field_value, cccontacts), |
1087
|
|
|
"CCEmails": obj.getCCEmails() or [], |
1088
|
|
|
"Batch": self.to_field_value(batch), |
1089
|
|
|
"DateSampled": {"value": self.to_iso_date(obj.getDateSampled())}, |
1090
|
|
|
"SamplingDate": {"value": self.to_iso_date(obj.getSamplingDate())}, |
1091
|
|
|
"SampleType": self.to_field_value(sample_type), |
1092
|
|
|
"EnvironmentalConditions": {"value": obj.getEnvironmentalConditions()}, |
1093
|
|
|
"ClientSampleID": {"value": obj.getClientSampleID()}, |
1094
|
|
|
"ClientReference": {"value": obj.getClientReference()}, |
1095
|
|
|
"ClientOrderNumber": {"value": obj.getClientOrderNumber()}, |
1096
|
|
|
"SampleCondition": self.to_field_value(sample_condition), |
1097
|
|
|
"SamplePoint": self.to_field_value(sample_point), |
1098
|
|
|
"StorageLocation": self.to_field_value(storage_location), |
1099
|
|
|
"Container": self.to_field_value(container), |
1100
|
|
|
"SamplingDeviation": self.to_field_value(deviation), |
1101
|
|
|
"Preservation": self.to_field_value(preservation), |
1102
|
|
|
"Specification": self.to_field_value(specification), |
1103
|
|
|
"Template": self.to_field_value(sample_template), |
1104
|
|
|
"Profiles": map(self.to_field_value, profiles), |
1105
|
|
|
"Composite": {"value": obj.getComposite()} |
1106
|
|
|
}) |
1107
|
|
|
|
1108
|
|
|
return info |
1109
|
|
|
|
1110
|
|
|
@cache(cache_key) |
1111
|
|
|
def to_field_value(self, obj): |
1112
|
|
|
return { |
1113
|
|
|
"uid": obj and api.get_uid(obj) or "", |
1114
|
|
|
"title": obj and api.get_title(obj) or "" |
1115
|
|
|
} |
1116
|
|
|
|
1117
|
|
|
@cache(cache_key) |
1118
|
|
|
def get_specification_info(self, obj): |
1119
|
|
|
"""Returns the info for a Specification |
1120
|
|
|
""" |
1121
|
|
|
info = self.get_base_info(obj) |
1122
|
|
|
|
1123
|
|
|
results_range = obj.getResultsRange() |
1124
|
|
|
info.update({ |
1125
|
|
|
"results_range": results_range, |
1126
|
|
|
"sample_type_uid": obj.getSampleTypeUID(), |
1127
|
|
|
"sample_type_title": obj.getSampleTypeTitle(), |
1128
|
|
|
"client_uid": obj.getClientUID(), |
1129
|
|
|
}) |
1130
|
|
|
|
1131
|
|
|
bsc = api.get_tool("bika_setup_catalog") |
1132
|
|
|
|
1133
|
|
|
def get_service_by_keyword(keyword): |
1134
|
|
|
if keyword is None: |
1135
|
|
|
return [] |
1136
|
|
|
return map(api.get_object, bsc({ |
1137
|
|
|
"portal_type": "AnalysisService", |
1138
|
|
|
"getKeyword": keyword |
1139
|
|
|
})) |
1140
|
|
|
|
1141
|
|
|
# append a mapping of service_uid -> specification |
1142
|
|
|
specifications = {} |
1143
|
|
|
for spec in results_range: |
1144
|
|
|
service_uid = spec.get("uid") |
1145
|
|
|
if service_uid is None: |
1146
|
|
|
# service spec is not attached to a specific service, but to a |
1147
|
|
|
# keyword |
1148
|
|
|
for service in get_service_by_keyword(spec.get("keyword")): |
1149
|
|
|
service_uid = api.get_uid(service) |
1150
|
|
|
specifications[service_uid] = spec |
1151
|
|
|
continue |
1152
|
|
|
specifications[service_uid] = spec |
1153
|
|
|
info["specifications"] = specifications |
1154
|
|
|
# spec'd service UIDs |
1155
|
|
|
info["service_uids"] = specifications.keys() |
1156
|
|
|
return info |
1157
|
|
|
|
1158
|
|
|
def ajax_get_global_settings(self): |
1159
|
|
|
"""Returns the global Bika settings |
1160
|
|
|
""" |
1161
|
|
|
bika_setup = api.get_bika_setup() |
1162
|
|
|
settings = { |
1163
|
|
|
"show_prices": bika_setup.getShowPrices(), |
1164
|
|
|
} |
1165
|
|
|
return settings |
1166
|
|
|
|
1167
|
|
|
def ajax_get_flush_settings(self): |
1168
|
|
|
"""Returns the settings for fields flush |
1169
|
|
|
""" |
1170
|
|
|
flush_settings = { |
1171
|
|
|
"Client": [ |
1172
|
|
|
"Contact", |
1173
|
|
|
"CCContact", |
1174
|
|
|
"InvoiceContact", |
1175
|
|
|
"SamplePoint", |
1176
|
|
|
"Template", |
1177
|
|
|
"Profiles", |
1178
|
|
|
"PrimaryAnalysisRequest", |
1179
|
|
|
"Specification", |
1180
|
|
|
"Batch" |
1181
|
|
|
], |
1182
|
|
|
"Contact": [ |
1183
|
|
|
"CCContact" |
1184
|
|
|
], |
1185
|
|
|
"SampleType": [ |
1186
|
|
|
"SamplePoint", |
1187
|
|
|
"Specification" |
1188
|
|
|
], |
1189
|
|
|
"PrimarySample": [ |
1190
|
|
|
"Batch" |
1191
|
|
|
"Client", |
1192
|
|
|
"Contact", |
1193
|
|
|
"CCContact", |
1194
|
|
|
"CCEmails", |
1195
|
|
|
"ClientOrderNumber", |
1196
|
|
|
"ClientReference", |
1197
|
|
|
"ClientSampleID", |
1198
|
|
|
"ContainerType", |
1199
|
|
|
"DateSampled", |
1200
|
|
|
"EnvironmentalConditions", |
1201
|
|
|
"InvoiceContact", |
1202
|
|
|
"Preservation", |
1203
|
|
|
"Profiles", |
1204
|
|
|
"SampleCondition", |
1205
|
|
|
"SamplePoint", |
1206
|
|
|
"SampleType", |
1207
|
|
|
"SamplingDate", |
1208
|
|
|
"SamplingDeviation", |
1209
|
|
|
"StorageLocation", |
1210
|
|
|
"Specification", |
1211
|
|
|
"Template", |
1212
|
|
|
] |
1213
|
|
|
} |
1214
|
|
|
|
1215
|
|
|
# Maybe other add-ons have additional fields that require flushing |
1216
|
|
|
for name, ad in getAdapters((self.context,), IAddSampleFieldsFlush): |
1217
|
|
|
logger.info("Additional flush settings from {}".format(name)) |
1218
|
|
|
additional_settings = ad.get_flush_settings() |
1219
|
|
|
for key, values in additional_settings.items(): |
1220
|
|
|
new_values = flush_settings.get(key, []) + values |
1221
|
|
|
flush_settings[key] = list(set(new_values)) |
1222
|
|
|
|
1223
|
|
|
return flush_settings |
1224
|
|
|
|
1225
|
|
|
def ajax_get_service(self): |
1226
|
|
|
"""Returns the services information |
1227
|
|
|
""" |
1228
|
|
|
uid = self.request.form.get("uid", None) |
1229
|
|
|
|
1230
|
|
|
if uid is None: |
1231
|
|
|
return self.error("Invalid UID", status=400) |
1232
|
|
|
|
1233
|
|
|
service = self.get_object_by_uid(uid) |
1234
|
|
|
if not service: |
1235
|
|
|
return self.error("Service not found", status=404) |
1236
|
|
|
|
1237
|
|
|
info = self.get_service_info(service) |
1238
|
|
|
return info |
1239
|
|
|
|
1240
|
|
|
def ajax_recalculate_records(self): |
1241
|
|
|
out = {} |
1242
|
|
|
records = self.get_records() |
1243
|
|
|
for num_sample, record in enumerate(records): |
1244
|
|
|
# Get reference fields metadata |
1245
|
|
|
metadata = self.get_record_metadata(record) |
1246
|
|
|
|
1247
|
|
|
# Extract additional metadata from this record |
1248
|
|
|
# service_to_specs |
1249
|
|
|
service_to_specs = self.get_service_to_specs_info(metadata) |
1250
|
|
|
metadata.update(service_to_specs) |
1251
|
|
|
|
1252
|
|
|
# service_to_templates, template_to_services |
1253
|
|
|
templates_additional = self.get_template_additional_info(metadata) |
1254
|
|
|
metadata.update(templates_additional) |
1255
|
|
|
|
1256
|
|
|
# service_to_profiles, profiles_to_services |
1257
|
|
|
profiles_additional = self.get_profiles_additional_info(metadata) |
1258
|
|
|
metadata.update(profiles_additional) |
1259
|
|
|
|
1260
|
|
|
# dependencies |
1261
|
|
|
dependencies = self.get_unmet_dependencies_info(metadata) |
1262
|
|
|
metadata.update(dependencies) |
1263
|
|
|
|
1264
|
|
|
# Set the metadata for current sample number (column) |
1265
|
|
|
out[num_sample] = metadata |
1266
|
|
|
|
1267
|
|
|
return out |
1268
|
|
|
|
1269
|
|
|
def get_record_metadata(self, record): |
1270
|
|
|
"""Returns the metadata for the record passed in |
1271
|
|
|
""" |
1272
|
|
|
metadata = {} |
1273
|
|
|
extra_fields = {} |
1274
|
|
|
for key, value in record.items(): |
1275
|
|
|
if not key.endswith("_uid"): |
1276
|
|
|
continue |
1277
|
|
|
|
1278
|
|
|
# This is a reference field (ends with _uid), so we add the |
1279
|
|
|
# metadata key, even if there is no way to handle objects this |
1280
|
|
|
# field refers to |
1281
|
|
|
metadata_key = key.replace("_uid", "") |
1282
|
|
|
metadata_key = "{}_metadata".format(metadata_key.lower()) |
1283
|
|
|
metadata[metadata_key] = {} |
1284
|
|
|
|
1285
|
|
|
if not value: |
1286
|
|
|
continue |
1287
|
|
|
|
1288
|
|
|
# Get objects information (metadata) |
1289
|
|
|
objs_info = self.get_objects_info(record, key) |
1290
|
|
|
objs_uids = map(lambda obj: obj["uid"], objs_info) |
1291
|
|
|
metadata[metadata_key] = dict(zip(objs_uids, objs_info)) |
1292
|
|
|
|
1293
|
|
|
# Grab 'field_values' fields to be recalculated too |
1294
|
|
|
for obj_info in objs_info: |
1295
|
|
|
field_values = obj_info.get("field_values", {}) |
1296
|
|
|
for field_name, field_value in field_values.items(): |
1297
|
|
|
if not isinstance(field_value, dict): |
1298
|
|
|
# this is probably a list, e.g. "Profiles" field |
1299
|
|
|
continue |
1300
|
|
|
uids = self.get_uids_from_record(field_value, "uid") |
1301
|
|
|
if len(uids) == 1: |
1302
|
|
|
extra_fields[field_name] = uids[0] |
1303
|
|
|
|
1304
|
|
|
# Populate metadata with object info from extra fields |
1305
|
|
|
for field_name, uid in extra_fields.items(): |
1306
|
|
|
key = "{}_metadata".format(field_name.lower()) |
1307
|
|
|
if metadata.get(key): |
1308
|
|
|
# This object has been processed already, skip |
1309
|
|
|
continue |
1310
|
|
|
obj = self.get_object_by_uid(uid) |
1311
|
|
|
if not obj: |
1312
|
|
|
continue |
1313
|
|
|
obj_info = self.get_object_info(obj, field_name) |
1314
|
|
|
if not obj_info or "uid" not in obj_info: |
1315
|
|
|
continue |
1316
|
|
|
metadata[key] = {obj_info["uid"]: obj_info} |
1317
|
|
|
|
1318
|
|
|
return metadata |
1319
|
|
|
|
1320
|
|
|
def get_service_to_specs_info(self, metadata): |
1321
|
|
|
service_to_specs = {} |
1322
|
|
|
specifications = metadata.get("specification_metadata", {}) |
1323
|
|
|
for uid, obj_info in specifications.items(): |
1324
|
|
|
service_uids = obj_info["service_uids"] |
1325
|
|
|
for service_uid in service_uids: |
1326
|
|
|
if service_uid in service_to_specs: |
1327
|
|
|
service_to_specs[service_uid].append(uid) |
1328
|
|
|
else: |
1329
|
|
|
service_to_specs[service_uid] = [uid] |
1330
|
|
|
return {"service_to_specifications": service_to_specs} |
1331
|
|
|
|
1332
|
|
|
def get_template_additional_info(self, metadata): |
1333
|
|
|
template_to_services = {} |
1334
|
|
|
service_to_templates = {} |
1335
|
|
|
service_metadata = metadata.get("service_metadata", {}) |
1336
|
|
|
profiles_metadata = metadata.get("profiles_metadata", {}) |
1337
|
|
|
template = metadata.get("template_metadata", {}) |
1338
|
|
|
# We don't expect more than one template, but who knows about future? |
1339
|
|
|
for uid, obj_info in template.items(): |
1340
|
|
|
obj = self.get_object_by_uid(uid) |
1341
|
|
|
# profile from the template |
1342
|
|
|
profile = obj.getAnalysisProfile() |
1343
|
|
|
# add the profile to the other profiles |
1344
|
|
|
if profile is not None: |
1345
|
|
|
profile_uid = api.get_uid(profile) |
1346
|
|
|
if profile_uid not in profiles_metadata: |
1347
|
|
|
profile = self.get_object_by_uid(profile_uid) |
1348
|
|
|
profile_info = self.get_profile_info(profile) |
1349
|
|
|
profiles_metadata[profile_uid] = profile_info |
1350
|
|
|
|
1351
|
|
|
# get the template analyses |
1352
|
|
|
# [{'partition': 'part-1', 'service_uid': '...'}, |
1353
|
|
|
# {'partition': 'part-1', 'service_uid': '...'}] |
1354
|
|
|
analyses = obj.getAnalyses() or [] |
1355
|
|
|
# get all UIDs of the template records |
1356
|
|
|
service_uids = map(lambda rec: rec.get("service_uid"), analyses) |
1357
|
|
|
# remember a mapping of template uid -> service |
1358
|
|
|
template_to_services[uid] = service_uids |
1359
|
|
|
# remember a mapping of service uid -> templates |
1360
|
|
|
for service_uid in service_uids: |
1361
|
|
|
# remember the template of all services |
1362
|
|
|
if service_uid in service_to_templates: |
1363
|
|
|
service_to_templates[service_uid].append(uid) |
1364
|
|
|
else: |
1365
|
|
|
service_to_templates[service_uid] = [uid] |
1366
|
|
|
# remember the service metadata |
1367
|
|
|
if service_uid not in service_metadata: |
1368
|
|
|
service = self.get_object_by_uid(service_uid) |
1369
|
|
|
service_info = self.get_service_info(service) |
1370
|
|
|
service_metadata[service_uid] = service_info |
1371
|
|
|
|
1372
|
|
|
return { |
1373
|
|
|
"service_to_templates": service_to_templates, |
1374
|
|
|
"template_to_services": template_to_services, |
1375
|
|
|
"service_metadata": service_metadata, |
1376
|
|
|
"profiles_metadata": profiles_metadata, |
1377
|
|
|
} |
1378
|
|
|
|
1379
|
|
|
def get_profiles_additional_info(self, metadata): |
1380
|
|
|
profile_to_services = {} |
1381
|
|
|
service_to_profiles = metadata.get("service_to_profiles", {}) |
1382
|
|
|
service_metadata = metadata.get("service_metadata", {}) |
1383
|
|
|
profiles = metadata.get("profiles_metadata", {}) |
1384
|
|
|
for uid, obj_info in profiles.items(): |
1385
|
|
|
obj = self.get_object_by_uid(uid) |
1386
|
|
|
# get all services of this profile |
1387
|
|
|
services = obj.getService() |
1388
|
|
|
# get all UIDs of the profile services |
1389
|
|
|
service_uids = map(api.get_uid, services) |
1390
|
|
|
# remember all services of this profile |
1391
|
|
|
profile_to_services[uid] = service_uids |
1392
|
|
|
# remember a mapping of service uid -> profiles |
1393
|
|
|
for service in services: |
1394
|
|
|
# get the UID of this service |
1395
|
|
|
service_uid = api.get_uid(service) |
1396
|
|
|
# remember the profiles of this service |
1397
|
|
|
if service_uid in service_to_profiles: |
1398
|
|
|
service_to_profiles[service_uid].append(uid) |
1399
|
|
|
else: |
1400
|
|
|
service_to_profiles[service_uid] = [uid] |
1401
|
|
|
# remember the service metadata |
1402
|
|
|
if service_uid not in service_metadata: |
1403
|
|
|
service_info = self.get_service_info(service) |
1404
|
|
|
service_metadata[service_uid] = service_info |
1405
|
|
|
|
1406
|
|
|
return { |
1407
|
|
|
"profile_to_services": profile_to_services, |
1408
|
|
|
"service_to_profiles": service_to_profiles, |
1409
|
|
|
"service_metadata": service_metadata, |
1410
|
|
|
} |
1411
|
|
|
|
1412
|
|
|
def get_unmet_dependencies_info(self, metadata): |
1413
|
|
|
# mapping of service UID -> unmet service dependency UIDs |
1414
|
|
|
unmet_dependencies = {} |
1415
|
|
|
services = metadata.get("service_metadata", {}).copy() |
1416
|
|
|
for uid, obj_info in services.items(): |
1417
|
|
|
obj = self.get_object_by_uid(uid) |
1418
|
|
|
# get the dependencies of this service |
1419
|
|
|
deps = get_service_dependencies_for(obj) |
1420
|
|
|
|
1421
|
|
|
# check for unmet dependencies |
1422
|
|
|
for dep in deps["dependencies"]: |
1423
|
|
|
# we use the UID to test for equality |
1424
|
|
|
dep_uid = api.get_uid(dep) |
1425
|
|
|
if dep_uid not in services: |
1426
|
|
|
if uid in unmet_dependencies: |
1427
|
|
|
unmet_dependencies[uid].append(self.get_base_info(dep)) |
1428
|
|
|
else: |
1429
|
|
|
unmet_dependencies[uid] = [self.get_base_info(dep)] |
1430
|
|
|
# remember the dependencies in the service metadata |
1431
|
|
|
metadata["service_metadata"][uid].update({ |
1432
|
|
|
"dependencies": map( |
1433
|
|
|
self.get_base_info, deps["dependencies"]), |
1434
|
|
|
}) |
1435
|
|
|
return { |
1436
|
|
|
"unmet_dependencies": unmet_dependencies |
1437
|
|
|
} |
1438
|
|
|
|
1439
|
|
|
def get_objects_info(self, record, key): |
1440
|
|
|
""" |
1441
|
|
|
Returns a list with the metadata for the objects the field with |
1442
|
|
|
field_name passed in refers to. Returns empty list if the field is not |
1443
|
|
|
a reference field or the record for this key cannot be handled |
1444
|
|
|
:param record: a record for a single sample (column) |
1445
|
|
|
:param key: The key of the field from the record (e.g. Client_uid) |
1446
|
|
|
:return: list of info objects |
1447
|
|
|
""" |
1448
|
|
|
# Get the objects from this record. Returns a list because the field |
1449
|
|
|
# can be multivalued |
1450
|
|
|
uids = self.get_uids_from_record(record, key) |
1451
|
|
|
objects = map(self.get_object_by_uid, uids) |
1452
|
|
|
objects = map(lambda obj: self.get_object_info(obj, key), objects) |
1453
|
|
|
return filter(None, objects) |
1454
|
|
|
|
1455
|
|
|
def object_info_cache_key(method, self, obj, key): |
1456
|
|
|
if obj is None or not key: |
1457
|
|
|
raise DontCache |
1458
|
|
|
field_name = key.replace("_uid", "").lower() |
1459
|
|
|
obj_key = api.get_cache_key(obj) |
1460
|
|
|
return "-".join([field_name, obj_key]) |
1461
|
|
|
|
1462
|
|
|
@cache(object_info_cache_key) |
1463
|
|
|
def get_object_info(self, obj, key): |
1464
|
|
|
"""Returns the object info metadata for the passed in object and key |
1465
|
|
|
:param obj: the object from which extract the info from |
1466
|
|
|
:param key: The key of the field from the record (e.g. Client_uid) |
1467
|
|
|
:return: dict that represents the object |
1468
|
|
|
""" |
1469
|
|
|
# Check if there is a function to handle objects for this field |
1470
|
|
|
field_name = key.replace("_uid", "") |
1471
|
|
|
func_name = "get_{}_info".format(field_name.lower()) |
1472
|
|
|
func = getattr(self, func_name, None) |
1473
|
|
|
|
1474
|
|
|
# Get the info for each object |
1475
|
|
|
info = callable(func) and func(obj) or self.get_base_info(obj) |
1476
|
|
|
|
1477
|
|
|
# Check if there is any adapter to handle objects for this field |
1478
|
|
|
for name, adapter in getAdapters((obj, ), IAddSampleObjectInfo): |
1479
|
|
|
logger.info("adapter for '{}': {}".format(field_name, name)) |
1480
|
|
|
ad_info = adapter.get_object_info() |
1481
|
|
|
self.update_object_info(info, ad_info) |
1482
|
|
|
|
1483
|
|
|
return info |
1484
|
|
|
|
1485
|
|
|
def update_object_info(self, base_info, additional_info): |
1486
|
|
|
"""Updates the dictionaries for keys 'field_values' and 'filter_queries' |
1487
|
|
|
from base_info with those defined in additional_info. If base_info is |
1488
|
|
|
empty or None, updates the whole base_info dict with additional_info |
1489
|
|
|
""" |
1490
|
|
|
if not base_info: |
1491
|
|
|
base_info.update(additional_info) |
1492
|
|
|
return |
1493
|
|
|
|
1494
|
|
|
# Merge field_values info |
1495
|
|
|
field_values = base_info.get("field_values", {}) |
1496
|
|
|
field_values.update(additional_info.get("field_values", {})) |
1497
|
|
|
base_info["field_values"] = field_values |
1498
|
|
|
|
1499
|
|
|
# Merge filter_queries info |
1500
|
|
|
filter_queries = base_info.get("filter_queries", {}) |
1501
|
|
|
filter_queries.update(additional_info.get("filter_queries", {})) |
1502
|
|
|
base_info["filter_queries"] = filter_queries |
1503
|
|
|
|
1504
|
|
|
def show_recalculate_prices(self): |
1505
|
|
|
bika_setup = api.get_bika_setup() |
1506
|
|
|
return bika_setup.getShowPrices() |
1507
|
|
|
|
1508
|
|
|
def ajax_recalculate_prices(self): |
1509
|
|
|
"""Recalculate prices for all ARs |
1510
|
|
|
""" |
1511
|
|
|
# When the option "Include and display pricing information" in |
1512
|
|
|
# Bika Setup Accounting tab is not selected |
1513
|
|
|
if not self.show_recalculate_prices(): |
1514
|
|
|
return {} |
1515
|
|
|
|
1516
|
|
|
# The sorted records from the request |
1517
|
|
|
records = self.get_records() |
1518
|
|
|
|
1519
|
|
|
client = self.get_client() |
1520
|
|
|
bika_setup = api.get_bika_setup() |
1521
|
|
|
|
1522
|
|
|
member_discount = float(bika_setup.getMemberDiscount()) |
1523
|
|
|
member_discount_applies = False |
1524
|
|
|
if client: |
1525
|
|
|
member_discount_applies = client.getMemberDiscountApplies() |
1526
|
|
|
|
1527
|
|
|
prices = {} |
1528
|
|
|
for n, record in enumerate(records): |
1529
|
|
|
ardiscount_amount = 0.00 |
1530
|
|
|
arservices_price = 0.00 |
1531
|
|
|
arprofiles_price = 0.00 |
1532
|
|
|
arprofiles_vat_amount = 0.00 |
1533
|
|
|
arservice_vat_amount = 0.00 |
1534
|
|
|
services_from_priced_profile = [] |
1535
|
|
|
|
1536
|
|
|
profile_uids = record.get("Profiles_uid", "").split(",") |
1537
|
|
|
profile_uids = filter(lambda x: x, profile_uids) |
1538
|
|
|
profiles = map(self.get_object_by_uid, profile_uids) |
1539
|
|
|
services = map(self.get_object_by_uid, record.get("Analyses", [])) |
1540
|
|
|
|
1541
|
|
|
# ANALYSIS PROFILES PRICE |
1542
|
|
|
for profile in profiles: |
1543
|
|
|
use_profile_price = profile.getUseAnalysisProfilePrice() |
1544
|
|
|
if not use_profile_price: |
1545
|
|
|
continue |
1546
|
|
|
|
1547
|
|
|
profile_price = float(profile.getAnalysisProfilePrice()) |
1548
|
|
|
arprofiles_price += profile_price |
1549
|
|
|
arprofiles_vat_amount += profile.getVATAmount() |
1550
|
|
|
profile_services = profile.getService() |
1551
|
|
|
services_from_priced_profile.extend(profile_services) |
1552
|
|
|
|
1553
|
|
|
# ANALYSIS SERVICES PRICE |
1554
|
|
|
for service in services: |
1555
|
|
|
if service in services_from_priced_profile: |
1556
|
|
|
continue |
1557
|
|
|
service_price = float(service.getPrice()) |
1558
|
|
|
# service_vat = float(service.getVAT()) |
1559
|
|
|
service_vat_amount = float(service.getVATAmount()) |
1560
|
|
|
arservice_vat_amount += service_vat_amount |
1561
|
|
|
arservices_price += service_price |
1562
|
|
|
|
1563
|
|
|
base_price = arservices_price + arprofiles_price |
1564
|
|
|
|
1565
|
|
|
# Calculate the member discount if it applies |
1566
|
|
|
if member_discount and member_discount_applies: |
1567
|
|
|
logger.info("Member discount applies with {}%".format( |
1568
|
|
|
member_discount)) |
1569
|
|
|
ardiscount_amount = base_price * member_discount / 100 |
1570
|
|
|
|
1571
|
|
|
subtotal = base_price - ardiscount_amount |
1572
|
|
|
vat_amount = arprofiles_vat_amount + arservice_vat_amount |
1573
|
|
|
total = subtotal + vat_amount |
1574
|
|
|
|
1575
|
|
|
prices[n] = { |
1576
|
|
|
"discount": "{0:.2f}".format(ardiscount_amount), |
1577
|
|
|
"subtotal": "{0:.2f}".format(subtotal), |
1578
|
|
|
"vat": "{0:.2f}".format(vat_amount), |
1579
|
|
|
"total": "{0:.2f}".format(total), |
1580
|
|
|
} |
1581
|
|
|
logger.info("Prices for AR {}: Discount={discount} " |
1582
|
|
|
"VAT={vat} Subtotal={subtotal} total={total}" |
1583
|
|
|
.format(n, **prices[n])) |
1584
|
|
|
|
1585
|
|
|
return prices |
1586
|
|
|
|
1587
|
|
|
def ajax_submit(self): |
1588
|
|
|
"""Submit & create the ARs |
1589
|
|
|
""" |
1590
|
|
|
|
1591
|
|
|
# Get AR required fields (including extended fields) |
1592
|
|
|
fields = self.get_ar_fields() |
1593
|
|
|
|
1594
|
|
|
# extract records from request |
1595
|
|
|
records = self.get_records() |
1596
|
|
|
|
1597
|
|
|
fielderrors = {} |
1598
|
|
|
errors = {"message": "", "fielderrors": {}} |
1599
|
|
|
|
1600
|
|
|
attachments = {} |
1601
|
|
|
valid_records = [] |
1602
|
|
|
|
1603
|
|
|
# Validate required fields |
1604
|
|
|
for n, record in enumerate(records): |
1605
|
|
|
|
1606
|
|
|
# Process UID fields first and set their values to the linked field |
1607
|
|
|
uid_fields = filter(lambda f: f.endswith("_uid"), record) |
1608
|
|
|
for field in uid_fields: |
1609
|
|
|
name = field.replace("_uid", "") |
1610
|
|
|
value = record.get(field) |
1611
|
|
|
if "," in value: |
1612
|
|
|
value = value.split(",") |
1613
|
|
|
record[name] = value |
1614
|
|
|
|
1615
|
|
|
# Extract file uploads (fields ending with _file) |
1616
|
|
|
# These files will be added later as attachments |
1617
|
|
|
file_fields = filter(lambda f: f.endswith("_file"), record) |
1618
|
|
|
attachments[n] = map(lambda f: record.pop(f), file_fields) |
|
|
|
|
1619
|
|
|
|
1620
|
|
|
# Process Specifications field (dictionary like records instance). |
1621
|
|
|
# -> Convert to a standard Python dictionary. |
1622
|
|
|
specifications = map( |
1623
|
|
|
lambda x: dict(x), record.pop("Specifications", [])) |
1624
|
|
|
record["Specifications"] = specifications |
1625
|
|
|
|
1626
|
|
|
# Required fields and their values |
1627
|
|
|
required_keys = [field.getName() for field in fields |
1628
|
|
|
if field.required] |
1629
|
|
|
required_values = [record.get(key) for key in required_keys] |
1630
|
|
|
required_fields = dict(zip(required_keys, required_values)) |
1631
|
|
|
|
1632
|
|
|
# Client field is required but hidden in the AR Add form. We remove |
1633
|
|
|
# it therefore from the list of required fields to let empty |
1634
|
|
|
# columns pass the required check below. |
1635
|
|
|
if record.get("Client", False): |
1636
|
|
|
required_fields.pop('Client', None) |
1637
|
|
|
|
1638
|
|
|
# Contacts get pre-filled out if only one contact exists. |
1639
|
|
|
# We won't force those columns with only the Contact filled out to |
1640
|
|
|
# be required. |
1641
|
|
|
contact = required_fields.pop("Contact", None) |
1642
|
|
|
|
1643
|
|
|
# None of the required fields are filled, skip this record |
1644
|
|
|
if not any(required_fields.values()): |
1645
|
|
|
continue |
1646
|
|
|
|
1647
|
|
|
# Re-add the Contact |
1648
|
|
|
required_fields["Contact"] = contact |
1649
|
|
|
|
1650
|
|
|
# Missing required fields |
1651
|
|
|
missing = [f for f in required_fields if not record.get(f, None)] |
1652
|
|
|
|
1653
|
|
|
# If there are required fields missing, flag an error |
1654
|
|
|
for field in missing: |
1655
|
|
|
fieldname = "{}-{}".format(field, n) |
1656
|
|
|
msg = _("Field '{}' is required".format(field)) |
1657
|
|
|
fielderrors[fieldname] = msg |
1658
|
|
|
|
1659
|
|
|
# Process valid record |
1660
|
|
|
valid_record = dict() |
1661
|
|
|
for fieldname, fieldvalue in record.iteritems(): |
1662
|
|
|
# clean empty |
1663
|
|
|
if fieldvalue in ['', None]: |
1664
|
|
|
continue |
1665
|
|
|
valid_record[fieldname] = fieldvalue |
1666
|
|
|
|
1667
|
|
|
# append the valid record to the list of valid records |
1668
|
|
|
valid_records.append(valid_record) |
1669
|
|
|
|
1670
|
|
|
# return immediately with an error response if some field checks failed |
1671
|
|
|
if fielderrors: |
1672
|
|
|
errors["fielderrors"] = fielderrors |
1673
|
|
|
return {'errors': errors} |
1674
|
|
|
|
1675
|
|
|
# Process Form |
1676
|
|
|
actions = ActionHandlerPool.get_instance() |
1677
|
|
|
actions.queue_pool() |
1678
|
|
|
ARs = OrderedDict() |
1679
|
|
|
for n, record in enumerate(valid_records): |
1680
|
|
|
client_uid = record.get("Client") |
1681
|
|
|
client = self.get_object_by_uid(client_uid) |
1682
|
|
|
|
1683
|
|
|
if not client: |
1684
|
|
|
actions.resume() |
1685
|
|
|
raise RuntimeError("No client found") |
1686
|
|
|
|
1687
|
|
|
# get the specifications and pass them to the AR create function. |
1688
|
|
|
specifications = record.pop("Specifications", {}) |
1689
|
|
|
|
1690
|
|
|
# Create the Analysis Request |
1691
|
|
|
try: |
1692
|
|
|
ar = crar( |
1693
|
|
|
client, |
1694
|
|
|
self.request, |
1695
|
|
|
record, |
1696
|
|
|
specifications=specifications |
1697
|
|
|
) |
1698
|
|
|
except (KeyError, RuntimeError) as e: |
1699
|
|
|
actions.resume() |
1700
|
|
|
errors["message"] = e.message |
1701
|
|
|
return {"errors": errors} |
1702
|
|
|
# We keep the title to check if AR is newly created |
1703
|
|
|
# and UID to print stickers |
1704
|
|
|
ARs[ar.Title()] = ar.UID() |
1705
|
|
|
for attachment in attachments.get(n, []): |
1706
|
|
|
if not attachment.filename: |
1707
|
|
|
continue |
1708
|
|
|
att = _createObjectByType("Attachment", client, tmpID()) |
1709
|
|
|
att.setAttachmentFile(attachment) |
1710
|
|
|
att.processForm() |
1711
|
|
|
ar.addAttachment(att) |
1712
|
|
|
actions.resume() |
1713
|
|
|
|
1714
|
|
|
level = "info" |
1715
|
|
|
if len(ARs) == 0: |
1716
|
|
|
message = _('No Samples could be created.') |
1717
|
|
|
level = "error" |
1718
|
|
|
elif len(ARs) > 1: |
1719
|
|
|
message = _('Samples ${ARs} were successfully created.', |
1720
|
|
|
mapping={'ARs': safe_unicode(', '.join(ARs.keys()))}) |
1721
|
|
|
else: |
1722
|
|
|
message = _('Sample ${AR} was successfully created.', |
1723
|
|
|
mapping={'AR': safe_unicode(ARs.keys()[0])}) |
1724
|
|
|
|
1725
|
|
|
# Display a portal message |
1726
|
|
|
self.context.plone_utils.addPortalMessage(message, level) |
1727
|
|
|
|
1728
|
|
|
# Automatic label printing |
1729
|
|
|
bika_setup = api.get_bika_setup() |
1730
|
|
|
auto_print = bika_setup.getAutoPrintStickers() |
1731
|
|
|
if 'register' in auto_print and ARs: |
1732
|
|
|
return { |
1733
|
|
|
'success': message, |
1734
|
|
|
'stickers': ARs.values(), |
1735
|
|
|
'stickertemplate': bika_setup.getAutoStickerTemplate() |
1736
|
|
|
} |
1737
|
|
|
else: |
1738
|
|
|
return {'success': message} |
1739
|
|
|
|