Passed
Push — 2.x ( 286f28...aded3d )
by Ramon
06:10
created

ajaxAnalysisRequestAddView.create_samples()   A

Complexity

Conditions 5

Size

Total Lines 26
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

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