Passed
Push — 2.x ( 0c2f0b...2aa223 )
by Ramon
06:00
created

AnalysisRequestAddView.get_view_url()   A

Complexity

Conditions 2

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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