Passed
Push — 2.x ( 17cdd3...bd647c )
by Ramon
06:06
created

AnalysisRequestManageView.is_field_visible()   A

Complexity

Conditions 3

Size

Total Lines 7
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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