Passed
Push — 2.x ( 8a073a...44914a )
by Ramon
05:26
created

AnalysisServicesView.get_decimal_mark()   A

Complexity

Conditions 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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