Passed
Push — 2.x ( 864250...eaa7c8 )
by Jordi
06:49
created

AnalysisProfile.getServices()   A

Complexity

Conditions 2

Size

Total Lines 12
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 12
rs 10
c 0
b 0
f 0
cc 2
nop 1
1
# -*- coding: utf-8 -*-
2
#
3
# This file is part of SENAITE.CORE.
4
#
5
# SENAITE.CORE is free software: you can redistribute it and/or modify it under
6
# the terms of the GNU General Public License as published by the Free Software
7
# Foundation, version 2.
8
#
9
# This program is distributed in the hope that it will be useful, but WITHOUT
10
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12
# details.
13
#
14
# You should have received a copy of the GNU General Public License along with
15
# this program; if not, write to the Free Software Foundation, Inc., 51
16
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
#
18
# Copyright 2018-2024 by it's authors.
19
# Some rights reserved, see README and LICENSE.
20
21
from AccessControl import ClassSecurityInfo
22
from bika.lims import api
23
from bika.lims import senaiteMessageFactory as _
24
from bika.lims.interfaces import IDeactivable
25
from plone.autoform import directives
26
from plone.supermodel import model
27
from Products.CMFCore import permissions
28
from senaite.core.catalog import SETUP_CATALOG
29
from senaite.core.content.base import Container
30
from senaite.core.content.mixins import ClientAwareMixin
31
from senaite.core.interfaces import IAnalysisProfile
32
from senaite.core.schema.fields import DataGridRow
33
from senaite.core.z3cform.widgets.listing.widget import ListingWidgetFactory
34
from zope import schema
35
from zope.interface import Interface
36
from zope.interface import Invalid
37
from zope.interface import implementer
38
from zope.interface import invariant
39
40
41
class IAnalysisProfileRecord(Interface):
42
    """Record schema for selected services
43
    """
44
    uid = schema.TextLine(title=u"Profile UID")
45
    hidden = schema.Bool(title=u"Hidden")
46
47
48
class IAnalysisProfileSchema(model.Schema):
49
    """Schema interface
50
    """
51
52
    model.fieldset(
53
        "analyses",
54
        label=_(u"Analyses"),
55
        fields=[
56
            "services",
57
        ]
58
    )
59
60
    model.fieldset(
61
        "accounting",
62
        label=_(u"Accounting"),
63
        fields=[
64
            "commercial_id",
65
            "use_analysis_profile_price",
66
            "analysis_profile_price",
67
            "analysis_profile_vat",
68
        ]
69
    )
70
71
    title = schema.TextLine(
72
        title=u"Title",
73
        required=False,
74
    )
75
76
    description = schema.Text(
77
        title=u"Description",
78
        required=False,
79
    )
80
81
    profile_key = schema.TextLine(
82
        title=_(
83
            u"title_analysisprofile_profile_key",
84
            default=u"Profile Keyword"
85
        ),
86
        description=_(
87
            u"description_analysisprofile_profile_key",
88
            default=u"Please provide a unique profile keyword"
89
        ),
90
        required=False,
91
        default=u""
92
    )
93
94
    directives.widget("services",
95
                      ListingWidgetFactory,
96
                      listing_view="analysisprofiles_widget")
97
    services = schema.List(
98
        title=_(
99
            u"title_analysisprofile_services",
100
            default=u"Profile Analyses"
101
        ),
102
        description=_(
103
            u"description_analysisprofile_services",
104
            default=u"Select the included analyses for this profile"
105
        ),
106
        value_type=DataGridRow(schema=IAnalysisProfileRecord),
107
        default=[],
108
        required=True,
109
    )
110
111
    # Commecrial ID
112
    commercial_id = schema.TextLine(
113
        title=_(
114
            u"title_analysisprofile_commercial_id",
115
            default=u"Commercial ID"
116
        ),
117
        description=_(
118
            u"description_analysisprofile_commercial_id",
119
            default=u"Commercial ID used for accounting"
120
        ),
121
        required=False,
122
    )
123
124
    use_analysis_profile_price = schema.Bool(
125
        title=_(
126
            u"title_analysisprofile_use_profile_price",
127
            default=u"Use analysis profile price"
128
        ),
129
        description=_(
130
            u"description_analysisprofile_use_profile_price",
131
            default=u"Use profile price instead of single analyses prices"
132
        ),
133
        required=False,
134
    )
135
136
    analysis_profile_price = schema.Decimal(
137
        title=_(
138
            u"title_analysisprofile_profile_price",
139
            default=u"Price (excluding VAT)"
140
        ),
141
        description=_(
142
            u"description_analysisprofile_profile_price",
143
            default=u"Please provide the price excluding VAT"
144
        ),
145
        required=False,
146
    )
147
148
    analysis_profile_vat = schema.Decimal(
149
        title=_(
150
            u"title_analysisprofile_profile_vat",
151
            default=u"VAT %"
152
        ),
153
        description=_(
154
            u"description_analysisprofile_profile_vat",
155
            default=u"Please provide the VAT in percent that is added to the "
156
                    u"profile price"
157
        ),
158
        required=False,
159
    )
160
161
    @invariant
162
    def validate_profile_key(data):
163
        """Checks if the profile keyword is unique
164
        """
165
        profile_key = data.profile_key
166
        if not profile_key:
167
            # no further checks required
168
            return
169
        context = getattr(data, "__context__", None)
170
        if context and context.profile_key == profile_key:
171
            # nothing changed
172
            return
173
        query = {
174
            "portal_type": "AnalysisProfile",
175
            "profile_key": profile_key,
176
        }
177
        results = api.search(query, catalog=SETUP_CATALOG)
178
        if len(results) > 0:
179
            raise Invalid(_("Profile keyword must be unique"))
180
181
182
@implementer(IAnalysisProfile, IAnalysisProfileSchema, IDeactivable)
183
class AnalysisProfile(Container, ClientAwareMixin):
184
    """AnalysisProfile
185
    """
186
    # Catalogs where this type will be catalogued
187
    _catalogs = [SETUP_CATALOG]
188
189
    security = ClassSecurityInfo()
190
191
    @security.protected(permissions.View)
192
    def getProfileKey(self):
193
        accessor = self.accessor("profile_key")
194
        value = accessor(self) or ""
195
        return value.encode("utf-8")
196
197
    @security.protected(permissions.ModifyPortalContent)
198
    def setProfileKey(self, value):
199
        mutator = self.mutator("profile_key")
200
        mutator(self, api.safe_unicode(value))
201
202
    # BBB: AT schema field property
203
    ProfileKey = property(getProfileKey, setProfileKey)
204
205
    @security.protected(permissions.View)
206
    def getRawServices(self):
207
        """Return the raw value of the services field
208
209
        >>> self.getRawServices()
210
        [{'uid': '...', 'hidden': False}, {'uid': '...', 'hidden': True}, ...]
211
212
        :returns: List of dictionaries containing `uid` and `hidden`
213
        """
214
        accessor = self.accessor("services")
215
        return accessor(self) or []
216
217
    @security.protected(permissions.View)
218
    def getServices(self):
219
        """Returns a list of service objects
220
221
        >>> self.getServices()
222
        [<AnalysisService at ...>,  <AnalysisService at ...>, ...]
223
224
        :returns: List of analysis service objects
225
        """
226
        records = self.getRawServices()
227
        service_uids = map(lambda r: r.get("uid"), records)
228
        return list(map(api.get_object, service_uids))
229
230
    @security.protected(permissions.ModifyPortalContent)
231
    def setServices(self, value):
232
        """Set services for the profile
233
234
        This method accepts either a list of analysis service objects, a list
235
        of analysis service UIDs or a list of analysis profile service records
236
        containing the keys `uid` and `hidden`:
237
238
        >>> self.setServices([<AnalysisService at ...>, ...])
239
        >>> self.setServices(['353e1d9bd45d45dbabc837114a9c41e6', '...', ...])
240
        >>> self.setServices([{'hidden': False, 'uid': '...'}, ...])
241
242
        Raises a TypeError if the value does not match any allowed type.
243
        """
244
        if not isinstance(value, list):
245
            value = [value]
246
        records = []
247
        for v in value:
248
            uid = None
249
            hidden = False
250
            if isinstance(v, dict):
251
                uid = v.get("uid")
252
                hidden = v.get("hidden", False)
253
            elif api.is_object(v):
254
                uid = api.get_uid(v)
255
            elif api.is_uid(v):
256
                uid = v
257
            else:
258
                raise TypeError(
259
                    "Expected object, uid or record, got %r" % type(v))
260
            records.append({"uid": uid, "hidden": hidden})
261
        mutator = self.mutator("services")
262
        mutator(self, records)
263
264
    # BBB: AT schema field property
265
    Service = Services = property(getServices, setServices)
266
267
    @security.protected(permissions.View)
268
    def getServiceUIDs(self):
269
        """Returns a list of the selected service UIDs
270
        """
271
        services = self.getRawServices()
272
        return list(map(lambda record: record.get("uid"), services))
273
274
    @security.protected(permissions.View)
275
    def getCommercialID(self):
276
        accessor = self.accessor("commercial_id")
277
        value = accessor(self) or ""
278
        return value.encode("utf-8")
279
280
    @security.protected(permissions.ModifyPortalContent)
281
    def setCommercialID(self, value):
282
        mutator = self.mutator("commercial_id")
283
        mutator(self, api.safe_unicode(value))
284
285
    # BBB: AT schema field property
286
    CommercialID = property(getCommercialID, setCommercialID)
287
288
    @security.protected(permissions.View)
289
    def getUseAnalysisProfilePrice(self):
290
        accessor = self.accessor("use_analysis_profile_price")
291
        value = accessor(self)
292
        return bool(value)
293
294
    @security.protected(permissions.ModifyPortalContent)
295
    def setUseAnalysisProfilePrice(self, value):
296
        mutator = self.mutator("use_analysis_profile_price")
297
        mutator(self, bool(value))
298
299
    # BBB: AT schema field property
300
    UseAnalysisProfilePrice = property(
301
        getUseAnalysisProfilePrice, setUseAnalysisProfilePrice)
302
303
    @security.protected(permissions.View)
304
    def getAnalysisProfilePrice(self):
305
        accessor = self.accessor("analysis_profile_price")
306
        value = accessor(self) or ""
307
        return api.to_float(value, 0.0)
308
309
    @security.protected(permissions.ModifyPortalContent)
310
    def setAnalysisProfilePrice(self, value):
311
        mutator = self.mutator("analysis_profile_price")
312
        mutator(self, value)
313
314
    # BBB: AT schema field property
315
    AnalysisProfilePrice = property(
316
        getAnalysisProfilePrice, setAnalysisProfilePrice)
317
318
    @security.protected(permissions.View)
319
    def getAnalysisProfileVAT(self):
320
        accessor = self.accessor("analysis_profile_vat")
321
        value = accessor(self) or ""
322
        return api.to_float(value, 0.0)
323
324
    @security.protected(permissions.ModifyPortalContent)
325
    def setAnalysisProfileVAT(self, value):
326
        mutator = self.mutator("analysis_profile_vat")
327
        mutator(self, value)
328
329
    # BBB: AT schema field property
330
    AnalysisProfileVAT = property(getAnalysisProfileVAT, setAnalysisProfileVAT)
331
332
    @security.protected(permissions.View)
333
    def getAnalysisServiceSettings(self, uid):
334
        """Returns the hidden seettings for the given service UID
335
        """
336
        uid = api.get_uid(uid)
337
        by_uid = self.get_services_by_uid()
338
        record = by_uid.get(uid, {"uid": uid, "hidden": False})
339
        return record
340
341
    @security.protected(permissions.ModifyPortalContent)
342
    def setAnalysisServicesSettings(self, settings):
343
        """BBB: Update settings for selected service UIDs
344
345
        This method expects a list of dictionaries containing the service `uid`
346
        and the `hidden` setting.
347
348
        This is basically the same format as stored in the `services` field!
349
350
        However, we want to just update the settings for selected service UIDs"
351
352
        >>> settings =  [{'uid': '...', 'hidden': False}, ...]
353
        >>> setAnalysisServicesSettings(settings)
354
        """
355
        if not isinstance(settings, list):
356
            settings = [settings]
357
358
        by_uid = self.get_services_by_uid()
359
360
        for setting in settings:
361
            if not isinstance(setting, dict):
362
                raise TypeError(
363
                    "Expected a record containing `uid` and `hidden`, got %s"
364
                    % type(setting))
365
            uid = setting.get("uid")
366
            hidden = setting.get("hidden", False)
367
368
            if not uid:
369
                raise ValueError("UID is missing in setting %r" % setting)
370
371
            record = by_uid.get(uid)
372
            if not record:
373
                continue
374
            record["hidden"] = hidden
375
376
        # set back the new services
377
        self.setServices(by_uid.values())
378
379
    @security.protected(permissions.View)
380
    def getAnalysisServicesSettings(self):
381
        """BBB: Return hidden settings for selected services
382
383
        :returns: List of dictionaries containing `uid` and `hidden` settings
384
        """
385
        # Note: We store the selected service UIDs and the hidden setting in
386
        # the `services` field. Therefore, we can just return the raw value.
387
        return self.getRawServices()
388
389
    # BBB: AT schema (computed) field property
390
    AnalysisServicesSettings = property(getAnalysisServicesSettings)
391
392
    @security.protected(permissions.View)
393
    def get_services_by_uid(self):
394
        """Return the selected services grouped by UID
395
        """
396
        records = {}
397
        for record in self.services:
398
            records[record.get("uid")] = record
399
        return records
400
401
    def isAnalysisServiceHidden(self, service):
402
        """Check if the service is configured as hidden
403
        """
404
        obj = api.get_object(service)
405
        uid = api.get_uid(service)
406
        services = self.get_services_by_uid()
407
        record = services.get(uid)
408
        if not record:
409
            return obj.getRawHidden()
410
        return record.get("hidden", False)
411
412
    @security.protected(permissions.View)
413
    def getPrice(self):
414
        """Returns the price of the profile, without VAT
415
        """
416
        return self.getAnalysisProfilePrice()
417
418
    @security.protected(permissions.View)
419
    def getVATAmount(self):
420
        """Compute VAT amount
421
        """
422
        price = self.getPrice()
423
        vat = self.getAnalysisProfileVAT()
424
        return float(price) * float(vat) / 100
425
426
    # BBB: AT schema (computed) field property
427
    VATAmount = property(getVATAmount)
428
429
    @security.protected(permissions.View)
430
    def getTotalPrice(self):
431
        """Calculate the final price using the VAT and the subtotal price
432
        """
433
        price = self.getPrice()
434
        vat = self.getVATAmount()
435
        return float(price) + float(vat)
436
437
    # BBB: AT schema (computed) field property
438
    TotalPrice = property(getTotalPrice)
439
440
    def remove_service(self, service):
441
        """Remove the service from the profile
442
443
        If the service is not selected in the profile, returns False.
444
445
        NOTE: This method is used when an Analysis Service was deactivated.
446
447
        :param service: The service to be removed from this profile
448
        :type service: AnalysisService
449
        :return: True if the AnalysisService has been removed successfully
450
        """
451
        # get the UID of the service that should be removed
452
        uid = api.get_uid(service)
453
        # get the current raw value of the services field.
454
        current_services = self.getRawServices()
455
        # filter out the UID of the service
456
        new_services = filter(
457
            lambda record: record.get("uid") != uid, current_services)
458
459
        # check if the service was removed or not
460
        current_services_count = len(current_services)
461
        new_services_count = len(new_services)
462
463
        if current_services_count == new_services_count:
464
            # service was not part of the profile
465
            return False
466
467
        # set the new services
468
        self.setServices(new_services)
469
470
        return True
471