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