1
|
|
|
# -*- coding: utf-8 -*- |
2
|
|
|
# |
3
|
|
|
# This file is part of SENAITE.CORE |
4
|
|
|
# |
5
|
|
|
# Copyright 2018 by it's authors. |
6
|
|
|
# Some rights reserved. See LICENSE.rst, CONTRIBUTORS.rst. |
7
|
|
|
|
8
|
|
|
import collections |
9
|
|
|
|
10
|
|
|
from bika.lims import bikaMessageFactory as _ |
11
|
|
|
from bika.lims.browser.bika_listing import BikaListingView |
12
|
|
|
from bika.lims.config import PROJECTNAME |
13
|
|
|
from bika.lims.idserver import renameAfterCreation |
14
|
|
|
from bika.lims.interfaces import IAnalysisServices |
15
|
|
|
from bika.lims.utils import get_image |
16
|
|
|
from bika.lims.utils import get_link |
17
|
|
|
from bika.lims.utils import tmpID |
18
|
|
|
from bika.lims.validators import ServiceKeywordValidator |
19
|
|
|
from plone.app.content.browser.interfaces import IFolderContentsView |
20
|
|
|
from plone.app.folder.folder import ATFolder |
21
|
|
|
from plone.app.folder.folder import ATFolderSchema |
22
|
|
|
from plone.app.layout.globals.interfaces import IViewView |
23
|
|
|
from Products.Archetypes import atapi |
24
|
|
|
from Products.ATContentTypes.content.schemata import finalizeATCTSchema |
25
|
|
|
from Products.CMFCore.utils import getToolByName |
26
|
|
|
from Products.CMFPlone.utils import _createObjectByType |
27
|
|
|
from Products.CMFPlone.utils import safe_unicode |
28
|
|
|
from Products.Five.browser import BrowserView |
29
|
|
|
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile |
30
|
|
|
from transaction import savepoint |
31
|
|
|
from zope.i18n.locales import locales |
32
|
|
|
from zope.interface.declarations import implements |
33
|
|
|
|
34
|
|
|
|
35
|
|
|
class AnalysisServiceCopy(BrowserView): |
36
|
|
|
template = ViewPageTemplateFile("templates/analysisservice_copy.pt") |
37
|
|
|
# should never be copied between services |
38
|
|
|
skip_fieldnames = [ |
39
|
|
|
"UID", |
40
|
|
|
"id", |
41
|
|
|
"title", |
42
|
|
|
"ShortTitle", |
43
|
|
|
"Keyword", |
44
|
|
|
] |
45
|
|
|
created = [] |
46
|
|
|
|
47
|
|
|
def create_service(self, src_uid, dst_title, dst_keyword): |
48
|
|
|
folder = self.context.bika_setup.bika_analysisservices |
49
|
|
|
dst_service = _createObjectByType("AnalysisService", folder, tmpID()) |
50
|
|
|
# manually set keyword and title |
51
|
|
|
dst_service.setKeyword(dst_keyword) |
52
|
|
|
dst_service.setTitle(dst_title) |
53
|
|
|
dst_service.unmarkCreationFlag() |
54
|
|
|
_id = renameAfterCreation(dst_service) |
55
|
|
|
dst_service = folder[_id] |
56
|
|
|
return dst_service |
57
|
|
|
|
58
|
|
|
def validate_service(self, dst_service): |
59
|
|
|
# validate entries |
60
|
|
|
validator = ServiceKeywordValidator() |
61
|
|
|
# baseschema uses uniquefieldvalidator on title, this is sufficient. |
62
|
|
|
res = validator(dst_service.getKeyword(), instance=dst_service) |
63
|
|
|
if res is not True: |
64
|
|
|
self.savepoint.rollback() |
65
|
|
|
self.created = [] |
66
|
|
|
self.context.plone_utils.addPortalMessage( |
67
|
|
|
safe_unicode(res), "info") |
68
|
|
|
return False |
69
|
|
|
return True |
70
|
|
|
|
71
|
|
|
def copy_service(self, src_uid, dst_title, dst_keyword): |
72
|
|
|
uc = getToolByName(self.context, "uid_catalog") |
73
|
|
|
src_service = uc(UID=src_uid)[0].getObject() |
74
|
|
|
dst_service = self.create_service(src_uid, dst_title, dst_keyword) |
75
|
|
|
if self.validate_service(dst_service): |
76
|
|
|
# copy field values |
77
|
|
|
for field in src_service.Schema().fields(): |
78
|
|
|
fieldname = field.getName() |
79
|
|
|
fieldtype = field.getType() |
80
|
|
|
if fieldtype == "Products.Archetypes.Field.ComputedField" \ |
81
|
|
|
or fieldname in self.skip_fieldnames: |
82
|
|
|
continue |
83
|
|
|
value = field.get(src_service) |
84
|
|
|
if value: |
85
|
|
|
# https://github.com/bikalabs/bika.lims/issues/2015 |
86
|
|
|
if fieldname in ["UpperDetectionLimit", |
87
|
|
|
"LowerDetectionLimit"]: |
88
|
|
|
value = str(value) |
89
|
|
|
mutator_name = dst_service.getField(fieldname).mutator |
90
|
|
|
mutator = getattr(dst_service, mutator_name) |
91
|
|
|
mutator(value) |
92
|
|
|
dst_service.reindexObject() |
93
|
|
|
return dst_title |
94
|
|
|
else: |
95
|
|
|
return False |
96
|
|
|
|
97
|
|
|
def __call__(self): |
98
|
|
|
uc = getToolByName(self.context, "uid_catalog") |
99
|
|
|
if "copy_form_submitted" not in self.request: |
100
|
|
|
uids = self.request.form.get("uids", []) |
101
|
|
|
self.services = [] |
102
|
|
|
for uid in uids: |
103
|
|
|
proxies = uc(UID=uid) |
104
|
|
|
if proxies: |
105
|
|
|
self.services.append(proxies[0].getObject()) |
106
|
|
|
return self.template() |
107
|
|
|
else: |
108
|
|
|
self.savepoint = savepoint() |
109
|
|
|
sources = self.request.form.get("uids", []) |
110
|
|
|
titles = self.request.form.get("dst_title", []) |
111
|
|
|
keywords = self.request.form.get("dst_keyword", []) |
112
|
|
|
self.created = [] |
113
|
|
|
for i, s in enumerate(sources): |
114
|
|
|
if not titles[i]: |
115
|
|
|
message = _("Validation failed: title is required") |
116
|
|
|
message = safe_unicode(message) |
117
|
|
|
self.context.plone_utils.addPortalMessage(message, "info") |
118
|
|
|
self.savepoint.rollback() |
119
|
|
|
self.created = [] |
120
|
|
|
break |
121
|
|
|
if not keywords[i]: |
122
|
|
|
message = _("Validation failed: keyword is required") |
123
|
|
|
message = safe_unicode(message) |
124
|
|
|
self.context.plone_utils.addPortalMessage(message, "info") |
125
|
|
|
self.savepoint.rollback() |
126
|
|
|
self.created = [] |
127
|
|
|
break |
128
|
|
|
title = self.copy_service(s, titles[i], keywords[i]) |
129
|
|
|
if title: |
130
|
|
|
self.created.append(title) |
131
|
|
|
if len(self.created) > 1: |
132
|
|
|
message = _("${items} were successfully created.", |
133
|
|
|
mapping={ |
134
|
|
|
"items": safe_unicode( |
135
|
|
|
", ".join(self.created))}) |
136
|
|
|
elif len(self.created) == 1: |
137
|
|
|
message = _("${item} was successfully created.", |
138
|
|
|
mapping={ |
139
|
|
|
"item": safe_unicode(self.created[0])}) |
140
|
|
|
else: |
141
|
|
|
message = _("No new items were created.") |
142
|
|
|
self.context.plone_utils.addPortalMessage(message, "info") |
143
|
|
|
self.request.response.redirect(self.context.absolute_url()) |
144
|
|
|
|
145
|
|
|
|
146
|
|
|
class AnalysisServicesView(BikaListingView): |
147
|
|
|
"""Listing table view for Analysis Services |
148
|
|
|
""" |
149
|
|
|
implements(IFolderContentsView, IViewView) |
150
|
|
|
|
151
|
|
|
def __init__(self, context, request): |
152
|
|
|
super(AnalysisServicesView, self).__init__(context, request) |
153
|
|
|
|
154
|
|
|
self.an_cats = None |
155
|
|
|
self.an_cats_order = None |
156
|
|
|
self.catalog = "bika_setup_catalog" |
157
|
|
|
|
158
|
|
|
self.contentFilter = { |
159
|
|
|
"portal_type": "AnalysisService", |
160
|
|
|
"sort_on": "sortable_title", |
161
|
|
|
"sort_order": "ascending", |
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
self.context_actions = { |
165
|
|
|
_("Add"): { |
166
|
|
|
"url": "createObject?type_name=AnalysisService", |
167
|
|
|
"permission": "Add portal content", |
168
|
|
|
"icon": "++resource++bika.lims.images/add.png"} |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
self.icon = "{}/{}".format( |
172
|
|
|
self.portal_url, |
173
|
|
|
"/++resource++bika.lims.images/analysisservice_big.png" |
174
|
|
|
) |
175
|
|
|
|
176
|
|
|
self.title = self.context.translate(_("Analysis Services")) |
177
|
|
|
self.form_id = "list_analysisservices" |
178
|
|
|
|
179
|
|
|
self.show_select_row = False |
180
|
|
|
self.show_select_column = True |
181
|
|
|
self.show_select_all_checkbox = False |
182
|
|
|
self.pagesize = 25 |
183
|
|
|
self.sort_on = "sortable_title" |
184
|
|
|
self.categories = [] |
185
|
|
|
self.do_cats = self.context.bika_setup.getCategoriseAnalysisServices() |
186
|
|
|
self.can_sort = not self.do_cats |
187
|
|
|
self.currency_symbol = self.get_currency_symbol() |
188
|
|
|
self.decimal_mark = self.get_decimal_mark() |
189
|
|
|
if self.do_cats: |
190
|
|
|
self.pagesize = 999999 # hide batching controls |
191
|
|
|
self.show_categories = True |
192
|
|
|
self.expand_all_categories = False |
193
|
|
|
|
194
|
|
|
self.columns = collections.OrderedDict(( |
195
|
|
|
("Title", { |
196
|
|
|
"title": _("Service"), |
197
|
|
|
"index": "sortable_title", |
198
|
|
|
"replace_url": "absolute_url", |
199
|
|
|
"sortable": self.can_sort}), |
200
|
|
|
("Keyword", { |
201
|
|
|
"title": _("Keyword"), |
202
|
|
|
"index": "getKeyword", |
203
|
|
|
"attr": "getKeyword", |
204
|
|
|
"sortable": self.can_sort}), |
205
|
|
|
("Category", { |
206
|
|
|
"title": _("Category"), |
207
|
|
|
"attr": "getCategoryTitle", |
208
|
|
|
"sortable": self.can_sort}), |
209
|
|
|
("Methods", { |
210
|
|
|
"title": _("Methods"), |
211
|
|
|
"sortable": self.can_sort}), |
212
|
|
|
("Department", { |
213
|
|
|
"title": _("Department"), |
214
|
|
|
"toggle": False, |
215
|
|
|
"attr": "getDepartment.Title", |
216
|
|
|
"sortable": self.can_sort}), |
217
|
|
|
("Unit", { |
218
|
|
|
"title": _("Unit"), |
219
|
|
|
"attr": "getUnit", |
220
|
|
|
"sortable": False}), |
221
|
|
|
("Price", { |
222
|
|
|
"title": _("Price"), |
223
|
|
|
"sortable": self.can_sort}), |
224
|
|
|
("MaxTimeAllowed", { |
225
|
|
|
"title": _("Max Time"), |
226
|
|
|
"toggle": False, |
227
|
|
|
"sortable": self.can_sort}), |
228
|
|
|
("DuplicateVariation", { |
229
|
|
|
"title": _("Dup Var"), |
230
|
|
|
"toggle": False, |
231
|
|
|
"sortable": False}), |
232
|
|
|
("Calculation", { |
233
|
|
|
"title": _("Calculation"), |
234
|
|
|
"sortable": False}), |
235
|
|
|
("SortKey", { |
236
|
|
|
"title": _("Sort Key"), |
237
|
|
|
"attr": "getSortKey", |
238
|
|
|
"sortable": False}), |
239
|
|
|
)) |
240
|
|
|
|
241
|
|
|
copy_transition = { |
242
|
|
|
"id": "duplicate", |
243
|
|
|
"title": _("Duplicate"), |
244
|
|
|
"url": "copy" |
245
|
|
|
} |
246
|
|
|
|
247
|
|
|
self.review_states = [ |
248
|
|
|
{ |
249
|
|
|
"id": "default", |
250
|
|
|
"title": _("Active"), |
251
|
|
|
"contentFilter": {"inactive_state": "active"}, |
252
|
|
|
"columns": self.columns.keys(), |
253
|
|
|
"custom_transitions": [copy_transition] |
254
|
|
|
}, { |
255
|
|
|
"id": "inactive", |
256
|
|
|
"title": _("Dormant"), |
257
|
|
|
"contentFilter": {"inactive_state": "inactive"}, |
258
|
|
|
"columns": self.columns.keys(), |
259
|
|
|
"custom_transitions": [copy_transition] |
260
|
|
|
}, { |
261
|
|
|
"id": "all", |
262
|
|
|
"title": _("All"), |
263
|
|
|
"contentFilter": {}, |
264
|
|
|
"columns": self.columns.keys(), |
265
|
|
|
"custom_transitions": [copy_transition] |
266
|
|
|
}, |
267
|
|
|
] |
268
|
|
|
|
269
|
|
|
if not self.context.bika_setup.getShowPrices(): |
270
|
|
|
for i in range(len(self.review_states)): |
271
|
|
|
self.review_states[i]["columns"].remove("Price") |
272
|
|
|
|
273
|
|
|
def before_render(self): |
274
|
|
|
"""Before template render hook |
275
|
|
|
""" |
276
|
|
|
# Don't allow any context actions |
277
|
|
|
self.request.set("disable_border", 1) |
278
|
|
|
|
279
|
|
|
def get_decimal_mark(self): |
280
|
|
|
"""Returns the decimal mark |
281
|
|
|
""" |
282
|
|
|
return self.context.bika_setup.getDecimalMark() |
283
|
|
|
|
284
|
|
|
def get_currency_symbol(self): |
285
|
|
|
"""Returns the locale currency symbol |
286
|
|
|
""" |
287
|
|
|
currency = self.context.bika_setup.getCurrency() |
288
|
|
|
locale = locales.getLocale("en") |
289
|
|
|
locale_currency = locale.numbers.currencies.get(currency) |
290
|
|
|
if locale_currency is None: |
291
|
|
|
return "$" |
292
|
|
|
return locale_currency.symbol |
293
|
|
|
|
294
|
|
|
def format_price(self, price): |
295
|
|
|
"""Formats the price with the set decimal mark and correct currency |
296
|
|
|
""" |
297
|
|
|
return u"{} {}{}{:02d}".format( |
298
|
|
|
self.currency_symbol, |
299
|
|
|
price[0], |
300
|
|
|
self.decimal_mark, |
301
|
|
|
price[1], |
302
|
|
|
) |
303
|
|
|
|
304
|
|
|
def format_maxtime(self, maxtime): |
305
|
|
|
"""Formats the max time record to a days, hours, minutes string |
306
|
|
|
""" |
307
|
|
|
minutes = maxtime.get("minutes", "0") |
308
|
|
|
hours = maxtime.get("hours", "0") |
309
|
|
|
days = maxtime.get("days", "0") |
310
|
|
|
# days, hours, minutes |
311
|
|
|
return u"{}: {} {}: {} {}: {}".format( |
312
|
|
|
_("days"), days, _("hours"), hours, _("minutes"), minutes) |
313
|
|
|
|
314
|
|
|
def format_duplication_variation(self, variation): |
315
|
|
|
"""Format duplicate variation |
316
|
|
|
""" |
317
|
|
|
return u"{}{}{:02d}".format( |
318
|
|
|
variation[0], |
319
|
|
|
self.decimal_mark, |
320
|
|
|
variation[1] |
321
|
|
|
) |
322
|
|
|
|
323
|
|
|
def folderitem(self, obj, item, index): |
324
|
|
|
"""Service triggered each time an item is iterated in folderitems. |
325
|
|
|
The use of this service prevents the extra-loops in child objects. |
326
|
|
|
:obj: the instance of the class to be foldered |
327
|
|
|
:item: dict containing the properties of the object to be used by |
328
|
|
|
the template |
329
|
|
|
:index: current index of the item |
330
|
|
|
""" |
331
|
|
|
|
332
|
|
|
cat = obj.getCategoryTitle() |
333
|
|
|
cat_order = self.an_cats_order.get(cat) |
334
|
|
|
if self.do_cats: |
335
|
|
|
# category groups entries |
336
|
|
|
item["category"] = cat |
337
|
|
|
if (cat, cat_order) not in self.categories: |
338
|
|
|
self.categories.append((cat, cat_order)) |
339
|
|
|
|
340
|
|
|
# Category |
341
|
|
|
category = obj.getCategory() |
342
|
|
|
if category: |
343
|
|
|
title = category.Title() |
344
|
|
|
url = category.absolute_url() |
345
|
|
|
item["Category"] = title |
346
|
|
|
item["replace"]["Category"] = get_link(url, value=title) |
347
|
|
|
|
348
|
|
|
# Calculation |
349
|
|
|
calculation = obj.getCalculation() |
350
|
|
|
if calculation: |
351
|
|
|
title = calculation.Title() |
352
|
|
|
url = calculation.absolute_url() |
353
|
|
|
item["Calculation"] = title |
354
|
|
|
item["replace"]["Calculation"] = get_link(url, value=title) |
355
|
|
|
|
356
|
|
|
# Methods |
357
|
|
|
methods = obj.getMethods() |
358
|
|
|
if methods: |
359
|
|
|
links = map( |
360
|
|
|
lambda m: get_link( |
361
|
|
|
m.absolute_url(), value=m.Title(), css_class="link"), |
362
|
|
|
methods) |
363
|
|
|
item["replace"]["Methods"] = ", ".join(links) |
364
|
|
|
|
365
|
|
|
# Max time allowed |
366
|
|
|
maxtime = obj.MaxTimeAllowed |
367
|
|
|
if maxtime: |
368
|
|
|
item["MaxTimeAllowed"] = self.format_maxtime(maxtime) |
369
|
|
|
|
370
|
|
|
# Price |
371
|
|
|
item["Price"] = self.format_price(obj.Price) |
372
|
|
|
|
373
|
|
|
# Duplicate Variation |
374
|
|
|
dup_variation = obj.DuplicateVariation |
375
|
|
|
if dup_variation: |
376
|
|
|
item["DuplicateVariation"] = self.format_duplication_variation( |
377
|
|
|
dup_variation) |
378
|
|
|
|
379
|
|
|
# Icons |
380
|
|
|
after_icons = "" |
381
|
|
|
if obj.getAccredited(): |
382
|
|
|
after_icons += get_image( |
383
|
|
|
"accredited.png", title=_("Accredited")) |
384
|
|
|
if obj.getAttachmentOption() == "r": |
385
|
|
|
after_icons += get_image( |
386
|
|
|
"attach_reqd.png", title=_("Attachment required")) |
387
|
|
|
if obj.getAttachmentOption() == "n": |
388
|
|
|
after_icons += get_image( |
389
|
|
|
"attach_no.png", title=_("Attachment not permitted")) |
390
|
|
|
if after_icons: |
391
|
|
|
item["after"]["Title"] = after_icons |
392
|
|
|
|
393
|
|
|
return item |
394
|
|
|
|
395
|
|
|
def folderitems(self, full_objects=False, classic=True): |
396
|
|
|
"""Sort by Categories |
397
|
|
|
""" |
398
|
|
|
bsc = getToolByName(self.context, "bika_setup_catalog") |
399
|
|
|
self.an_cats = bsc( |
400
|
|
|
portal_type="AnalysisCategory", |
401
|
|
|
sort_on="sortable_title") |
402
|
|
|
self.an_cats_order = dict([ |
403
|
|
|
(b.Title, "{:04}".format(a)) |
404
|
|
|
for a, b in enumerate(self.an_cats)]) |
405
|
|
|
items = super(AnalysisServicesView, self).folderitems() |
406
|
|
|
if self.do_cats: |
407
|
|
|
self.categories = map(lambda x: x[0], |
408
|
|
|
sorted(self.categories, key=lambda x: x[1])) |
409
|
|
|
else: |
410
|
|
|
self.categories.sort() |
411
|
|
|
return items |
412
|
|
|
|
413
|
|
|
|
414
|
|
|
schema = ATFolderSchema.copy() |
415
|
|
|
finalizeATCTSchema(schema, folderish=True, moveDiscussion=False) |
416
|
|
|
|
417
|
|
|
|
418
|
|
|
class AnalysisServices(ATFolder): |
419
|
|
|
implements(IAnalysisServices) |
420
|
|
|
displayContentsTab = False |
421
|
|
|
schema = schema |
|
|
|
|
422
|
|
|
|
423
|
|
|
|
424
|
|
|
atapi.registerType(AnalysisServices, PROJECTNAME) |
425
|
|
|
|