Passed
Push — 2.x ( 829bc3...5135ed )
by Ramon
07:04 queued 01:19
created

bika.lims.browser.analysisrequest.add2   F

Complexity

Total Complexity 283

Size/Duplication

Total Lines 1852
Duplicated Lines 0.54 %

Importance

Changes 0
Metric Value
wmc 283
eloc 1171
dl 10
loc 1852
rs 0.916
c 0
b 0
f 0

87 Methods

Rating   Name   Duplication   Size   Complexity  
A AnalysisRequestAddView.__call__() 0 14 1
A AnalysisRequestAddView.__init__() 0 8 1
A AnalysisRequestAddView.get_view_url() 0 9 2
A AnalysisRequestAddView.get_object_by_uid() 0 9 2
A AnalysisRequestManageView.get_field_visibility() 0 2 1
A ajaxAnalysisRequestAddView.get_records() 0 26 4
A AnalysisRequestManageView.flush() 0 4 2
A AnalysisRequestAddView.is_service_selected() 0 13 4
A AnalysisRequestManageView.set_field_order() 0 2 1
B AnalysisRequestManageView.get_fields_with_visibility() 0 26 6
A AnalysisRequestManageView.__call__() 0 11 5
A AnalysisRequestManageView.is_field_visible() 0 7 3
A AnalysisRequestManageView.get_fields() 0 5 1
A AnalysisRequestManageView.storage() 0 6 2
A ajaxAnalysisRequestAddView.get_uids_from_record() 0 15 4
A AnalysisRequestAddView.is_field_visible() 0 15 5
A ajaxAnalysisRequestAddView.publishTraverse() 0 5 1
A AnalysisRequestAddView.get_services() 0 27 4
A AnalysisRequestManageView.get_field() 0 5 1
A ajaxAnalysisRequestAddView.get_profile_info() 0 7 1
A AnalysisRequestManageView.__init__() 0 6 1
A AnalysisRequestAddView.get_service_uid_from() 0 6 1
B AnalysisRequestAddView.get_service_categories() 0 25 6
A ajaxAnalysisRequestAddView.get_calculation_info() 0 7 1
A ajaxAnalysisRequestAddView.get_sampletype_info() 0 41 1
A ajaxAnalysisRequestAddView.error() 0 7 1
A ajaxAnalysisRequestAddView.get_client_info() 0 57 2
A ajaxAnalysisRequestAddView.get_method_info() 0 7 1
A AnalysisRequestManageView.get_sorted_fields() 0 14 1
A AnalysisRequestAddView.getMemberDiscountApplies() 0 10 2
A ajaxAnalysisRequestAddView.get_contact_info() 0 32 2
A AnalysisRequestAddView.get_fields_with_visibility() 0 15 4
A ajaxAnalysisRequestAddView.to_iso_date() 0 11 4
A ajaxAnalysisRequestAddView.get_service_info() 0 24 1
A ajaxAnalysisRequestAddView.__init__() 0 7 1
A AnalysisRequestAddView.get_points_of_capture() 0 3 1
A ajaxAnalysisRequestAddView.__call__() 0 14 3
A AnalysisRequestManageView.get_ar() 0 5 2
A ajaxAnalysisRequestAddView.get_base_info() 0 16 2
A AnalysisRequestManageView.set_field_visibility() 0 2 1
A AnalysisRequestManageView.get_field_order() 0 5 3
C ajaxAnalysisRequestAddView.get_template_info() 0 44 9
A AnalysisRequestManageView.get_annotation() 0 3 1
A AnalysisRequestAddView.get_ar() 0 8 2
C AnalysisRequestAddView.get_default_value() 0 43 11
A AnalysisRequestAddView.analyses_required() 0 6 1
A AnalysisRequestAddView.get_currency() 0 7 1
A AnalysisRequestAddView.get_ar_fields() 0 6 1
A AnalysisRequestAddView.get_ar_schema() 0 6 1
A AnalysisRequestAddView.get_client() 0 14 5
A ajaxAnalysisRequestAddView.is_automatic_label_printing_enabled() 0 8 2
A AnalysisRequestAddView.get_input_widget() 0 53 1
C ajaxAnalysisRequestAddView.ajax_recalculate_prices() 0 78 11
A AnalysisRequestAddView.get_copy_from() 0 15 4
A ajaxAnalysisRequestAddView.ajax_cancel() 0 6 1
A ajaxAnalysisRequestAddView.to_field_value() 0 5 1
A ajaxAnalysisRequestAddView.ajax_recalculate_records() 0 23 2
A AnalysisRequestAddView.get_default_contact() 0 34 4
B ajaxAnalysisRequestAddView.create_attachment() 0 34 7
A ajaxAnalysisRequestAddView.create_samples() 0 26 5
A ajaxAnalysisRequestAddView.get_primaryanalysisrequest_info() 0 47 1
B AnalysisRequestAddView.generate_fieldvalues() 0 35 6
A AnalysisRequestAddView.get_ar_count() 0 9 2
F ajaxAnalysisRequestAddView.get_record_metadata() 0 51 14
A ajaxAnalysisRequestAddView.object_info_cache_key() 0 6 3
A ajaxAnalysisRequestAddView.get_conditions_info() 0 9 3
A AnalysisRequestAddView.get_fieldname() 0 8 1
A ajaxAnalysisRequestAddView.ajax_get_service() 0 14 3
B ajaxAnalysisRequestAddView.get_template_additional_info() 0 45 8
A ajaxAnalysisRequestAddView.get_max_samples_per_record() 0 7 1
A AnalysisRequestAddView.get_parent_ar() 0 18 4
A AnalysisRequestAddView.get_field_value() 0 8 1
A ajaxAnalysisRequestAddView.show_recalculate_prices() 0 3 1
A ajaxAnalysisRequestAddView.get_object_info() 0 26 3
A ajaxAnalysisRequestAddView.get_unmet_dependencies_info() 0 25 5
F ajaxAnalysisRequestAddView.ajax_submit() 0 181 30
B ajaxAnalysisRequestAddView.handle_redirect() 0 35 7
A ajaxAnalysisRequestAddView.check_confirmation() 0 16 3
A ajaxAnalysisRequestAddView.ajax_get_global_settings() 0 8 1
A ajaxAnalysisRequestAddView.to_attachment_record() 0 13 2
A ajaxAnalysisRequestAddView.get_num_samples() 0 6 2
A AnalysisRequestAddView.get_sample() 0 7 2
A ajaxAnalysisRequestAddView.get_objects_info() 0 16 2
A ajaxAnalysisRequestAddView.update_object_info() 0 18 2
A AnalysisRequestAddView.get_batch() 0 10 3
B ajaxAnalysisRequestAddView.ajax_get_flush_settings() 0 58 3
B ajaxAnalysisRequestAddView.get_profiles_additional_info() 0 31 5

2 Functions

Rating   Name   Duplication   Size   Complexity  
A returns_json() 10 10 1
A cache_key() 0 4 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complexity

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like bika.lims.browser.analysisrequest.add2 often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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