Passed
Push — 2.x ( 4b71c6...a1ea4f )
by Jordi
07:12
created

ajaxAnalysisRequestAddView.ajax_get_flush_settings()   A

Complexity

Conditions 3

Size

Total Lines 29
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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