Passed
Push — master ( 5b4d0f...b63a29 )
by Ramon
04:30
created

bika.lims.browser.analysisrequest.add2   F

Complexity

Total Complexity 255

Size/Duplication

Total Lines 1731
Duplicated Lines 1.39 %

Importance

Changes 0
Metric Value
wmc 255
eloc 1095
dl 24
loc 1731
rs 1.22
c 0
b 0
f 0

80 Methods

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