Passed
Push — master ( eeab6e...dd6af3 )
by Ramon
05:59
created

bika/lims/browser/analysisrequest/add2.py (7 issues)

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