Passed
Push — 2.x ( dbaaba...482653 )
by Ramon
05:18
created

ajaxAnalysisRequestAddView.check_confirmation()   A

Complexity

Conditions 3

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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