Passed
Push — 2.x ( 00f734...bf6542 )
by Jordi
07:33
created

AnalysisRequestAddView.get_points_of_capture()   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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