Passed
Push — 2.x ( 3f40bd...d670c9 )
by Ramon
05:54 queued 01:27
created

ajaxAnalysisRequestAddView.get_conditions_info()   A

Complexity

Conditions 3

Size

Total Lines 9
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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