|
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
|
|
|
|