Passed
Push — master ( f42093...226b52 )
by Ramon
04:24
created

ajaxAnalysisRequestAddView.get_client_info()   A

Complexity

Conditions 2

Size

Total Lines 54
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 33
dl 0
loc 54
rs 9.0879
c 0
b 0
f 0
cc 2
nop 2

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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