Passed
Push — master ( d8e2ec...90ae0b )
by Jordi
10:07 queued 04:19
created

AnalysisRequestManageView.get_field()   A

Complexity

Conditions 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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