Passed
Push — 2.x ( edc6a5...ff07a9 )
by Jordi
11:14
created

ajaxAnalysisRequestAddView.ajax_cancel()   A

Complexity

Conditions 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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