Passed
Push — 2.x ( ca0980...375d4d )
by Ramon
08:08
created

senaite.core.content.analysisprofile   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 551
Duplicated Lines 7.44 %

Importance

Changes 0
Metric Value
wmc 53
eloc 321
dl 41
loc 551
rs 6.96
c 0
b 0
f 0

28 Methods

Rating   Name   Duplication   Size   Complexity  
A AnalysisProfile.setProfileKey() 0 4 1
A AnalysisProfile.getRawServices() 0 11 1
A AnalysisProfile.getProfileKey() 0 5 1
A IAnalysisProfileSchema.validate_profile_key() 0 19 5
A AnalysisProfile.setSampleTypes() 0 4 1
A AnalysisProfile.setUseAnalysisProfilePrice() 0 4 1
A AnalysisProfile.getTotalPrice() 0 7 1
A AnalysisProfile.getServiceUIDs() 0 11 2
A AnalysisProfile.get_services_by_uid() 0 8 2
A AnalysisProfile.getAnalysisProfileVAT() 0 5 1
A AnalysisProfile.getUseAnalysisProfilePrice() 0 5 1
A AnalysisProfile.getAnalysisProfilePrice() 0 5 1
A AnalysisProfile.getRawServiceUIDs() 0 8 2
A AnalysisProfile.getAnalysisServiceSettings() 0 8 1
A AnalysisProfile.isAnalysisServiceHidden() 0 10 2
A AnalysisProfile.getAnalysisServicesSettings() 0 9 1
A AnalysisProfile.getServices() 0 14 2
A AnalysisProfile.setAnalysisProfilePrice() 0 4 1
A AnalysisProfile.setCommercialID() 0 4 1
C AnalysisProfile.setServices() 10 45 10
A AnalysisProfile.getRawSampleTypes() 0 4 1
A AnalysisProfile.remove_service() 31 31 3
A AnalysisProfile.getVATAmount() 0 7 1
A AnalysisProfile.getCommercialID() 0 5 1
A AnalysisProfile.getPrice() 0 5 1
A AnalysisProfile.setAnalysisProfileVAT() 0 4 1
A AnalysisProfile.getSampleTypes() 0 4 1
B AnalysisProfile.setAnalysisServicesSettings() 0 37 6

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complexity

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like senaite.core.content.analysisprofile often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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 import UIDReferenceField
33
from senaite.core.schema.fields import DataGridRow
34
from senaite.core.z3cform.widgets.listing.widget import ListingWidgetFactory
35
from senaite.core.z3cform.widgets.uidreference import UIDReferenceWidgetFactory
36
from zope import schema
37
from zope.interface import Interface
38
from zope.interface import Invalid
39
from zope.interface import implementer
40
from zope.interface import invariant
41
42
43
class IAnalysisProfileRecord(Interface):
44
    """Record schema for selected services
45
    """
46
    uid = schema.TextLine(title=u"Profile UID")
47
    hidden = schema.Bool(title=u"Hidden")
48
49
50
class IAnalysisProfileSchema(model.Schema):
51
    """Schema interface
52
    """
53
54
    model.fieldset(
55
        "analyses",
56
        label=_(u"Analyses"),
57
        fields=[
58
            "services",
59
        ]
60
    )
61
62
    model.fieldset(
63
        "accounting",
64
        label=_(u"Accounting"),
65
        fields=[
66
            "commercial_id",
67
            "use_analysis_profile_price",
68
            "analysis_profile_price",
69
            "analysis_profile_vat",
70
        ]
71
    )
72
73
    title = schema.TextLine(
74
        title=_(
75
            u"title_analysisprofile_title",
76
            default=u"Name"
77
        ),
78
        required=True,
79
    )
80
81
    description = schema.Text(
82
        title=_(
83
            u"title_analysisprofile_description",
84
            default=u"Description"
85
        ),
86
        required=False,
87
    )
88
89
    profile_key = schema.TextLine(
90
        title=_(
91
            u"title_analysisprofile_profile_key",
92
            default=u"Profile Keyword"
93
        ),
94
        description=_(
95
            u"description_analysisprofile_profile_key",
96
            default=u"Please provide a unique profile keyword"
97
        ),
98
        required=False,
99
        default=u""
100
    )
101
102
    directives.widget("services",
103
                      ListingWidgetFactory,
104
                      listing_view="analysisprofile_services_widget")
105
    services = schema.List(
106
        title=_(
107
            u"title_analysisprofile_services",
108
            default=u"Profile Analyses"
109
        ),
110
        description=_(
111
            u"description_analysisprofile_services",
112
            default=u"Select the included analyses for this profile"
113
        ),
114
        value_type=DataGridRow(schema=IAnalysisProfileRecord),
115
        default=[],
116
        required=True,
117
    )
118
119
    # Commecrial ID
120
    commercial_id = schema.TextLine(
121
        title=_(
122
            u"title_analysisprofile_commercial_id",
123
            default=u"Commercial ID"
124
        ),
125
        description=_(
126
            u"description_analysisprofile_commercial_id",
127
            default=u"Commercial ID used for accounting"
128
        ),
129
        required=False,
130
    )
131
132
    use_analysis_profile_price = schema.Bool(
133
        title=_(
134
            u"title_analysisprofile_use_profile_price",
135
            default=u"Use analysis profile price"
136
        ),
137
        description=_(
138
            u"description_analysisprofile_use_profile_price",
139
            default=u"Use profile price instead of single analyses prices"
140
        ),
141
        required=False,
142
    )
143
144
    analysis_profile_price = schema.Decimal(
145
        title=_(
146
            u"title_analysisprofile_profile_price",
147
            default=u"Price (excluding VAT)"
148
        ),
149
        description=_(
150
            u"description_analysisprofile_profile_price",
151
            default=u"Please provide the price excluding VAT"
152
        ),
153
        required=False,
154
    )
155
156
    analysis_profile_vat = schema.Decimal(
157
        title=_(
158
            u"title_analysisprofile_profile_vat",
159
            default=u"VAT %"
160
        ),
161
        description=_(
162
            u"description_analysisprofile_profile_vat",
163
            default=u"Please provide the VAT in percent that is added to the "
164
                    u"profile price"
165
        ),
166
        required=False,
167
    )
168
169
    directives.widget(
170
        "sample_types",
171
        UIDReferenceWidgetFactory,
172
        catalog=SETUP_CATALOG,
173
        query={
174
            "is_active": True,
175
            "sort_on": "title",
176
            "sort_order": "ascending",
177
        },
178
    )
179
    sample_types = UIDReferenceField(
180
        title=_(
181
            u"label_analysisprofile_sampletypes",
182
            default=u"Sample types"
183
        ),
184
        description=_(
185
            u"description_analysisprofile_sampletypes",
186
            default=u"Sample types for which this analysis profile is "
187
                    u"supported. This profile won't be available for "
188
                    u"selection in sample creation and edit forms unless the "
189
                    u"selected sample type is one of these. If no sample type "
190
                    u"is set here, this profile will always be available for "
191
                    u"selection, regardless of the sample type of the sample."
192
        ),
193
        allowed_types=("SampleType", ),
194
        multi_valued=True,
195
        required=False,
196
    )
197
198
    @invariant
199
    def validate_profile_key(data):
200
        """Checks if the profile keyword is unique
201
        """
202
        profile_key = data.profile_key
203
        if not profile_key:
204
            # no further checks required
205
            return
206
        context = getattr(data, "__context__", None)
207
        if context and context.profile_key == profile_key:
208
            # nothing changed
209
            return
210
        query = {
211
            "portal_type": "AnalysisProfile",
212
            "profile_key": profile_key,
213
        }
214
        results = api.search(query, catalog=SETUP_CATALOG)
215
        if len(results) > 0:
216
            raise Invalid(_("Profile keyword must be unique"))
217
218
219
@implementer(IAnalysisProfile, IAnalysisProfileSchema, IDeactivable)
220
class AnalysisProfile(Container, ClientAwareMixin):
221
    """AnalysisProfile
222
    """
223
    # Catalogs where this type will be catalogued
224
    _catalogs = [SETUP_CATALOG]
225
226
    security = ClassSecurityInfo()
227
228
    @security.protected(permissions.View)
229
    def getProfileKey(self):
230
        accessor = self.accessor("profile_key")
231
        value = accessor(self) or ""
232
        return api.to_utf8(value)
233
234
    @security.protected(permissions.ModifyPortalContent)
235
    def setProfileKey(self, value):
236
        mutator = self.mutator("profile_key")
237
        mutator(self, api.safe_unicode(value))
238
239
    # BBB: AT schema field property
240
    ProfileKey = property(getProfileKey, setProfileKey)
241
242
    @security.protected(permissions.View)
243
    def getRawServices(self):
244
        """Return the raw value of the services field
245
246
        >>> self.getRawServices()
247
        [{'uid': '...', 'hidden': False}, {'uid': '...', 'hidden': True}, ...]
248
249
        :returns: List of dictionaries containing `uid` and `hidden`
250
        """
251
        accessor = self.accessor("services")
252
        return accessor(self) or []
253
254
    @security.protected(permissions.View)
255
    def getServices(self, active_only=True):
256
        """Returns a list of service objects
257
258
        >>> self.getServices()
259
        [<AnalysisService at ...>,  <AnalysisService at ...>, ...]
260
261
        :returns: List of analysis service objects
262
        """
263
        services = map(api.get_object, self.getRawServiceUIDs())
264
        if active_only:
265
            # filter out inactive services
266
            services = filter(api.is_active, services)
267
        return list(services)
268
269
    @security.protected(permissions.ModifyPortalContent)
270
    def setServices(self, value, keep_inactive=True):
271
        """Set services for the profile
272
273
        This method accepts either a list of analysis service objects, a list
274
        of analysis service UIDs or a list of analysis profile service records
275
        containing the keys `uid` and `hidden`:
276
277
        >>> self.setServices([<AnalysisService at ...>, ...])
278
        >>> self.setServices(['353e1d9bd45d45dbabc837114a9c41e6', '...', ...])
279
        >>> self.setServices([{'hidden': False, 'uid': '...'}, ...])
280
281
        Raises a TypeError if the value does not match any allowed type.
282
        """
283
        if not isinstance(value, list):
284
            value = [value]
285
        records = []
286
        for v in value:
287
            uid = None
288
            hidden = False
289
            if isinstance(v, dict):
290
                uid = v.get("uid")
291
                hidden = v.get("hidden", False)
292
            elif api.is_object(v):
293
                uid = api.get_uid(v)
294
            elif api.is_uid(v):
295
                uid = v
296
            else:
297
                raise TypeError(
298
                    "Expected object, uid or record, got %r" % type(v))
299
            records.append({"uid": uid, "hidden": hidden})
300
301 View Code Duplication
        if keep_inactive:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
302
            # keep inactive services so they come up again when reactivated
303
            uids = [record.get("uid") for record in records]
304
            for record in self.getRawServices():
305
                uid = record.get("uid")
306
                if uid in uids:
307
                    continue
308
                obj = api.get_object(uid)
309
                if not api.is_active(obj):
310
                    records.append(record)
311
312
        mutator = self.mutator("services")
313
        mutator(self, records)
314
315
    # BBB: AT schema field property
316
    Service = Services = property(getServices, setServices)
317
318
    @security.protected(permissions.View)
319
    def getServiceUIDs(self, active_only=True):
320
        """Returns a list of UIDs for the referenced AnalysisService objects
321
322
        :param active_only: If True, only UIDs of active services are returned
323
        :returns: A list of unique identifiers (UIDs)
324
        """
325
        if active_only:
326
            services = self.getServices(active_only=active_only)
327
            return list(map(api.get_uid, services))
328
        return self.getRawServiceUIDs()
329
330
    @security.protected(permissions.View)
331
    def getRawServiceUIDs(self):
332
        """Returns the list of UIDs stored as raw data in the 'Services' field
333
334
        :returns: A list of UIDs extracted from the raw 'Services' data.
335
        """
336
        services = self.getRawServices()
337
        return list(map(lambda record: record.get("uid"), services))
338
339
    @security.protected(permissions.View)
340
    def getCommercialID(self):
341
        accessor = self.accessor("commercial_id")
342
        value = accessor(self) or ""
343
        return api.to_utf8(value)
344
345
    @security.protected(permissions.ModifyPortalContent)
346
    def setCommercialID(self, value):
347
        mutator = self.mutator("commercial_id")
348
        mutator(self, api.safe_unicode(value))
349
350
    # BBB: AT schema field property
351
    CommercialID = property(getCommercialID, setCommercialID)
352
353
    @security.protected(permissions.View)
354
    def getUseAnalysisProfilePrice(self):
355
        accessor = self.accessor("use_analysis_profile_price")
356
        value = accessor(self)
357
        return bool(value)
358
359
    @security.protected(permissions.ModifyPortalContent)
360
    def setUseAnalysisProfilePrice(self, value):
361
        mutator = self.mutator("use_analysis_profile_price")
362
        mutator(self, bool(value))
363
364
    # BBB: AT schema field property
365
    UseAnalysisProfilePrice = property(
366
        getUseAnalysisProfilePrice, setUseAnalysisProfilePrice)
367
368
    @security.protected(permissions.View)
369
    def getAnalysisProfilePrice(self):
370
        accessor = self.accessor("analysis_profile_price")
371
        value = accessor(self) or ""
372
        return api.to_float(value, 0.0)
373
374
    @security.protected(permissions.ModifyPortalContent)
375
    def setAnalysisProfilePrice(self, value):
376
        mutator = self.mutator("analysis_profile_price")
377
        mutator(self, value)
378
379
    # BBB: AT schema field property
380
    AnalysisProfilePrice = property(
381
        getAnalysisProfilePrice, setAnalysisProfilePrice)
382
383
    @security.protected(permissions.View)
384
    def getAnalysisProfileVAT(self):
385
        accessor = self.accessor("analysis_profile_vat")
386
        value = accessor(self) or ""
387
        return api.to_float(value, 0.0)
388
389
    @security.protected(permissions.ModifyPortalContent)
390
    def setAnalysisProfileVAT(self, value):
391
        mutator = self.mutator("analysis_profile_vat")
392
        mutator(self, value)
393
394
    # BBB: AT schema field property
395
    AnalysisProfileVAT = property(getAnalysisProfileVAT, setAnalysisProfileVAT)
396
397
    @security.protected(permissions.View)
398
    def getAnalysisServiceSettings(self, uid):
399
        """Returns the hidden seettings for the given service UID
400
        """
401
        uid = api.get_uid(uid)
402
        by_uid = self.get_services_by_uid()
403
        record = by_uid.get(uid, {"uid": uid, "hidden": False})
404
        return record
405
406
    @security.protected(permissions.ModifyPortalContent)
407
    def setAnalysisServicesSettings(self, settings):
408
        """BBB: Update settings for selected service UIDs
409
410
        This method expects a list of dictionaries containing the service `uid`
411
        and the `hidden` setting.
412
413
        This is basically the same format as stored in the `services` field!
414
415
        However, we want to just update the settings for selected service UIDs"
416
417
        >>> settings =  [{'uid': '...', 'hidden': False}, ...]
418
        >>> setAnalysisServicesSettings(settings)
419
        """
420
        if not isinstance(settings, list):
421
            settings = [settings]
422
423
        by_uid = self.get_services_by_uid()
424
425
        for setting in settings:
426
            if not isinstance(setting, dict):
427
                raise TypeError(
428
                    "Expected a record containing `uid` and `hidden`, got %s"
429
                    % type(setting))
430
            uid = setting.get("uid")
431
            hidden = setting.get("hidden", False)
432
433
            if not uid:
434
                raise ValueError("UID is missing in setting %r" % setting)
435
436
            record = by_uid.get(uid)
437
            if not record:
438
                continue
439
            record["hidden"] = hidden
440
441
        # set back the new services
442
        self.setServices(by_uid.values())
443
444
    @security.protected(permissions.View)
445
    def getAnalysisServicesSettings(self):
446
        """BBB: Return hidden settings for selected services
447
448
        :returns: List of dictionaries containing `uid` and `hidden` settings
449
        """
450
        # Note: We store the selected service UIDs and the hidden setting in
451
        # the `services` field. Therefore, we can just return the raw value.
452
        return self.getRawServices()
453
454
    # BBB: AT schema (computed) field property
455
    AnalysisServicesSettings = property(getAnalysisServicesSettings)
456
457
    @security.protected(permissions.View)
458
    def get_services_by_uid(self):
459
        """Return the selected services grouped by UID
460
        """
461
        records = {}
462
        for record in self.services:
463
            records[record.get("uid")] = record
464
        return records
465
466
    def isAnalysisServiceHidden(self, service):
467
        """Check if the service is configured as hidden
468
        """
469
        obj = api.get_object(service)
470
        uid = api.get_uid(service)
471
        services = self.get_services_by_uid()
472
        record = services.get(uid)
473
        if not record:
474
            return obj.getRawHidden()
475
        return record.get("hidden", False)
476
477
    @security.protected(permissions.View)
478
    def getPrice(self):
479
        """Returns the price of the profile, without VAT
480
        """
481
        return self.getAnalysisProfilePrice()
482
483
    @security.protected(permissions.View)
484
    def getVATAmount(self):
485
        """Compute VAT amount
486
        """
487
        price = self.getPrice()
488
        vat = self.getAnalysisProfileVAT()
489
        return float(price) * float(vat) / 100
490
491
    # BBB: AT schema (computed) field property
492
    VATAmount = property(getVATAmount)
493
494
    @security.protected(permissions.View)
495
    def getTotalPrice(self):
496
        """Calculate the final price using the VAT and the subtotal price
497
        """
498
        price = self.getPrice()
499
        vat = self.getVATAmount()
500
        return float(price) + float(vat)
501
502
    # BBB: AT schema (computed) field property
503
    TotalPrice = property(getTotalPrice)
504
505 View Code Duplication
    def remove_service(self, service):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
506
        """Remove the service from the profile
507
508
        If the service is not selected in the profile, returns False.
509
510
        NOTE: This method is used when an Analysis Service was deactivated.
511
512
        :param service: The service to be removed from this profile
513
        :type service: AnalysisService
514
        :return: True if the AnalysisService has been removed successfully
515
        """
516
        # get the UID of the service that should be removed
517
        uid = api.get_uid(service)
518
        # get the current raw value of the services field.
519
        current_services = self.getRawServices()
520
        # filter out the UID of the service
521
        new_services = filter(
522
            lambda record: record.get("uid") != uid, current_services)
523
524
        # check if the service was removed or not
525
        current_services_count = len(current_services)
526
        new_services_count = len(new_services)
527
528
        if current_services_count == new_services_count:
529
            # service was not part of the profile
530
            return False
531
532
        # set the new services
533
        self.setServices(new_services)
534
535
        return True
536
537
    @security.protected(permissions.View)
538
    def getRawSampleTypes(self):
539
        accessor = self.accessor("sample_types", raw=True)
540
        return accessor(self) or []
541
542
    @security.protected(permissions.View)
543
    def getSampleTypes(self):
544
        accessor = self.accessor("sample_types")
545
        return accessor(self) or []
546
547
    @security.protected(permissions.ModifyPortalContent)
548
    def setSampleTypes(self, value):
549
        mutator = self.mutator("sample_types")
550
        mutator(self, value)
551