Passed
Push — master ( 70be41...e1228c )
by Ramon
11:12
created

bika.lims.content.instrument   F

Complexity

Total Complexity 70

Size/Duplication

Total Lines 690
Duplicated Lines 1.74 %

Importance

Changes 0
Metric Value
wmc 70
eloc 435
dl 12
loc 690
rs 2.8
c 0
b 0
f 0

30 Methods

Rating   Name   Duplication   Size   Complexity  
B Instrument.isQCValid() 0 42 5
B Instrument.getValidCertifications() 0 15 8
A Instrument.setImportDataInterface() 0 8 2
A Instrument.getDocuments() 0 4 1
A Instrument.getCertifications() 0 5 1
A Instrument.getImportDataInterfacesList() 0 2 1
A Instrument.getScheduleTaskTypesList() 0 2 1
A Instrument.getDataInterfacesList() 0 10 5
A Instrument.getMethodUIDs() 0 5 2
A Instrument.getValidations() 0 4 1
A Instrument.isOutOfDate() 0 8 2
A Instrument.getCertificateExpireDate() 0 7 2
A Instrument.getWeeksToExpire() 0 7 2
A Instrument.isValidationInProgress() 0 7 2
A Instrument._renameAfterCreation() 0 3 1
A Instrument.getMaintenanceTasks() 0 2 1
A Instrument.displayValue() 0 22 3
A Instrument.getLatestValidCalibration() 0 19 3
A Instrument.getExportDataInterfacesList() 0 2 1
A Instrument.getCalibrations() 0 4 1
A Instrument._getAvailableMethods() 12 12 2
A Instrument.getCalibrationAgentsList() 0 2 1
A Instrument.getSchedule() 0 2 1
B Instrument.addReferences() 0 42 8
A Instrument.isCalibrationInProgress() 0 7 2
A Instrument.getLatestValidCertification() 0 20 3
A Instrument.Title() 0 2 1
A Instrument.getLatestValidValidation() 0 19 3
A Instrument.getMaintenanceTypesList() 0 2 1
A Instrument.isValid() 0 9 1

2 Functions

Rating   Name   Duplication   Size   Complexity  
A getCalibrationAgents() 0 4 1
A getMaintenanceTypes() 0 5 1

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 bika.lims.content.instrument 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-2019 by it's authors.
19
# Some rights reserved, see README and LICENSE.
20
21
from datetime import date
22
23
from AccessControl import ClassSecurityInfo
24
25
from Products.CMFCore.utils import getToolByName
26
from Products.CMFPlone.utils import safe_unicode
27
from Products.Archetypes.atapi import DisplayList, PicklistWidget
28
from Products.Archetypes.atapi import registerType
29
from bika.lims.api.analysis import is_out_of_range
30
from bika.lims.catalog.analysis_catalog import CATALOG_ANALYSIS_LISTING
31
32
from zope.interface import implements
33
from plone.app.folder.folder import ATFolder
34
35
# Schema and Fields
36
from Products.Archetypes.atapi import Schema
37
from Products.ATContentTypes.content import schemata
38
from Products.Archetypes.atapi import ReferenceField
39
from Products.Archetypes.atapi import ComputedField
40
from Products.Archetypes.atapi import DateTimeField
41
from Products.Archetypes.atapi import StringField
42
from Products.Archetypes.atapi import TextField
43
from Products.Archetypes.atapi import ImageField
44
from Products.Archetypes.atapi import BooleanField
45
from Products.ATExtensions.ateapi import RecordsField
46
from plone.app.blob.field import FileField as BlobFileField
47
from bika.lims.browser.fields import UIDReferenceField
48
49
# Widgets
50
from Products.Archetypes.atapi import ComputedWidget
51
from Products.Archetypes.atapi import StringWidget
52
from Products.Archetypes.atapi import TextAreaWidget
53
from Products.Archetypes.atapi import FileWidget
54
from Products.Archetypes.atapi import ImageWidget
55
from Products.Archetypes.atapi import BooleanWidget
56
from Products.Archetypes.atapi import SelectionWidget
57
from Products.Archetypes.atapi import MultiSelectionWidget
58
from bika.lims.browser.widgets import DateTimeWidget
59
from bika.lims.browser.widgets import RecordsWidget
60
from bika.lims.browser.widgets import ReferenceWidget
61
62
# bika.lims imports
63
from bika.lims import api
64
from bika.lims import logger
65
from bika.lims.utils import t
66
from bika.lims.utils import to_utf8
67
from bika.lims.config import PROJECTNAME
68
from bika.lims.exportimport import instruments
69
from bika.lims.interfaces import IInstrument, IDeactivable
70
from bika.lims.content.bikaschema import BikaSchema
71
from bika.lims.content.bikaschema import BikaFolderSchema
72
from bika.lims import bikaMessageFactory as _
73
74
schema = BikaFolderSchema.copy() + BikaSchema.copy() + Schema((
75
76
    ReferenceField(
77
        'InstrumentType',
78
        vocabulary='getInstrumentTypes',
79
        allowed_types=('InstrumentType',),
80
        relationship='InstrumentInstrumentType',
81
        required=1,
82
        widget=ReferenceWidget(
83
            label=_("Instrument type"),
84
            showOn=True,
85
            catalog_name='bika_setup_catalog',
86
            base_query={
87
                "is_active": True,
88
                "sort_on": "sortable_title",
89
                "sort_order": "ascending",
90
            },
91
        ),
92
    ),
93
94
    ReferenceField(
95
        'Manufacturer',
96
        allowed_types=('Manufacturer',),
97
        relationship='InstrumentManufacturer',
98
        required=1,
99
        widget=ReferenceWidget(
100
            label=_("Manufacturer"),
101
            showOn=True,
102
            catalog_name='bika_setup_catalog',
103
            base_query={
104
                "is_active": True,
105
                "sort_on": "sortable_title",
106
                "sort_order": "ascending",
107
            },
108
        ),
109
    ),
110
111
    ReferenceField(
112
        'Supplier',
113
        allowed_types=('Supplier',),
114
        relationship='InstrumentSupplier',
115
        required=1,
116
        widget=ReferenceWidget(
117
            label=_("Supplier"),
118
            showOn=True,
119
            catalog_name='bika_setup_catalog',
120
            base_query={
121
                "is_active": True,
122
                "sort_on": "sortable_title",
123
                "sort_order": "ascending",
124
            },
125
        ),
126
    ),
127
128
    StringField(
129
        'Model',
130
        widget=StringWidget(
131
            label=_("Model"),
132
            description=_("The instrument's model number"),
133
        )
134
    ),
135
136
    StringField(
137
        'SerialNo',
138
        widget=StringWidget(
139
            label=_("Serial No"),
140
            description=_("The serial number that uniquely identifies the instrument"),
141
        )
142
    ),
143
144
    # TODO Remove Instrument.Method field (functionality provided by 'Methods')
145
    UIDReferenceField(
146
        'Method',
147
        vocabulary='_getAvailableMethods',
148
        allowed_types=('Method',),
149
        required=0,
150
        widget=SelectionWidget(
151
            format='select',
152
            label=_("Method"),
153
            visible=False,
154
        ),
155
    ),
156
157
    ReferenceField(
158
        'Methods',
159
        vocabulary='_getAvailableMethods',
160
        allowed_types=('Method',),
161
        relationship='InstrumentMethods',
162
        required=0,
163
        multiValued=1,
164
        widget=PicklistWidget(
165
            size=10,
166
            label=_("Methods"),
167
        ),
168
    ),
169
170
    BooleanField(
171
        'DisposeUntilNextCalibrationTest',
172
        default=False,
173
        widget=BooleanWidget(
174
            label=_("De-activate until next calibration test"),
175
            description=_("If checked, the instrument will be unavailable until the next valid "
176
                          "calibration was performed. This checkbox will automatically be unchecked."),
177
        ),
178
    ),
179
180
    # Procedures
181
    TextField(
182
        'InlabCalibrationProcedure',
183
        schemata='Procedures',
184
        default_content_type='text/plain',
185
        allowed_content_types=('text/plain', ),
186
        default_output_type="text/plain",
187
        widget=TextAreaWidget(
188
            label=_("In-lab calibration procedure"),
189
            description=_("Instructions for in-lab regular calibration routines intended for analysts"),
190
        ),
191
    ),
192
193
    TextField(
194
        'PreventiveMaintenanceProcedure',
195
        schemata='Procedures',
196
        default_content_type='text/plain',
197
        allowed_content_types=('text/plain', ),
198
        default_output_type="text/plain",
199
        widget=TextAreaWidget(
200
            label=_("Preventive maintenance procedure"),
201
            description=_("Instructions for regular preventive and maintenance routines intended for analysts"),
202
        ),
203
    ),
204
205
    StringField(
206
        'DataInterface',
207
        vocabulary="getExportDataInterfacesList",
208
        widget=SelectionWidget(
209
            checkbox_bound=0,
210
            label=_("Data Interface"),
211
            description=_("Select an Export interface for this instrument."),
212
            format='select',
213
            default='',
214
            visible=True,
215
        ),
216
    ),
217
218
    StringField('ImportDataInterface',
219
                vocabulary="getImportDataInterfacesList",
220
                multiValued=1,
221
                widget=MultiSelectionWidget(
222
                    checkbox_bound=0,
223
                    label=_("Import Data Interface"),
224
                    description=_(
225
                        "Select an Import interface for this instrument."),
226
                    format='select',
227
                    default='',
228
                    visible=True,
229
                ),
230
                ),
231
232
    RecordsField(
233
        'ResultFilesFolder',
234
        subfields=('InterfaceName', 'Folder'),
235
        subfield_labels={'InterfaceName': _('Interface Code'),
236
                         'Folder': _('Folder that results will be saved')},
237
        subfield_readonly={'InterfaceName': True,
238
                           'Folder': False},
239
        widget=RecordsWidget(
240
            label=_("Result files folders"),
241
            description=_("For each interface of this instrument, \
242
                      you can define a folder where \
243
                      the system should look for the results files while \
244
                      automatically importing results. Having a folder \
245
                      for each Instrument and inside that folder creating \
246
                      different folders for each of its Interfaces \
247
                      can be a good approach. You can use Interface codes \
248
                      to be sure that folder names are unique."),
249
            visible=True,
250
        ),
251
    ),
252
253
    RecordsField(
254
        'DataInterfaceOptions',
255
        type='interfaceoptions',
256
        subfields=('Key', 'Value'),
257
        required_subfields=('Key', 'Value'),
258
        subfield_labels={
259
            'OptionValue': _('Key'),
260
            'OptionText': _('Value'),
261
        },
262
        widget=RecordsWidget(
263
            label=_("Data Interface Options"),
264
            description=_("Use this field to pass arbitrary parameters to the export/import modules."),
265
            visible=False,
266
        ),
267
    ),
268
269
    ComputedField(
270
        'Valid',
271
        expression="'1' if context.isValid() else '0'",
272
        widget=ComputedWidget(
273
            visible=False,
274
        ),
275
    ),
276
277
    StringField(
278
        'AssetNumber',
279
        widget=StringWidget(
280
            label=_("Asset Number"),
281
            description=_("The instrument's ID in the lab's asset register"),
282
        )
283
    ),
284
285
    ReferenceField(
286
        'InstrumentLocation',
287
        schemata='Additional info.',
288
        allowed_types=('InstrumentLocation', ),
289
        relationship='InstrumentInstrumentLocation',
290
        required=0,
291
        widget=ReferenceWidget(
292
            label=_("Instrument Location"),
293
            description=_("The room and location where the instrument is installed"),
294
            showOn=True,
295
            catalog_name='bika_setup_catalog',
296
            base_query={
297
                "is_active": True,
298
                "sort_on": "sortable_title",
299
                "sort_order": "ascending",
300
            },
301
        )
302
    ),
303
304
    ImageField(
305
        'Photo',
306
        schemata='Additional info.',
307
        widget=ImageWidget(
308
            label=_("Photo image file"),
309
            description=_("Photo of the instrument"),
310
        ),
311
    ),
312
313
    DateTimeField(
314
        'InstallationDate',
315
        schemata='Additional info.',
316
        widget=DateTimeWidget(
317
            label=_("InstallationDate"),
318
            description=_("The date the instrument was installed"),
319
        )
320
    ),
321
322
    BlobFileField(
323
        'InstallationCertificate',
324
        schemata='Additional info.',
325
        widget=FileWidget(
326
            label=_("Installation Certificate"),
327
            description=_("Installation certificate upload"),
328
        )
329
    ),
330
331
))
332
333
schema.moveField('AssetNumber', before='description')
334
335
schema['description'].widget.visible = True
336
schema['description'].schemata = 'default'
337
338
def getMaintenanceTypes(context):
339
    types = [('preventive', 'Preventive'),
340
             ('repair', 'Repair'),
341
             ('enhance', 'Enhancement')]
342
    return DisplayList(types)
343
344
345
def getCalibrationAgents(context):
346
    agents = [('analyst', 'Analyst'),
347
              ('maintainer', 'Maintainer')]
348
    return DisplayList(agents)
349
350
351
class Instrument(ATFolder):
352
    """A physical gadget of the lab
353
    """
354
    implements(IInstrument, IDeactivable)
355
    security = ClassSecurityInfo()
356
    displayContentsTab = False
357
    schema = schema
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable schema does not seem to be defined.
Loading history...
358
359
    _at_rename_after_creation = True
360
361
    def _renameAfterCreation(self, check_auto_id=False):
362
        from bika.lims.idserver import renameAfterCreation
363
        renameAfterCreation(self)
364
365
    def Title(self):
366
        return to_utf8(safe_unicode(self.title))
367
368
    def getDataInterfacesList(self, type_interface="import"):
369
        interfaces = list()
370
        if type_interface == "export":
371
            interfaces = instruments.get_instrument_export_interfaces()
372
        elif type_interface == "import":
373
            interfaces = instruments.get_instrument_import_interfaces()
374
        interfaces = map(lambda imp: (imp[0], imp[1].title), interfaces)
375
        interfaces.sort(lambda x, y: cmp(x[1].lower(), y[1].lower()))
376
        interfaces.insert(0, ('', t(_('None'))))
377
        return DisplayList(interfaces)
378
379
    def getExportDataInterfacesList(self):
380
        return self.getDataInterfacesList("export")
381
382
    def getImportDataInterfacesList(self):
383
        return self.getDataInterfacesList("import")
384
385
    def getScheduleTaskTypesList(self):
386
        return getMaintenanceTypes(self)
387
388
    def getMaintenanceTypesList(self):
389
        return getMaintenanceTypes(self)
390
391
    def getCalibrationAgentsList(self):
392
        return getCalibrationAgents(self)
393
394
    def getMethodUIDs(self):
395
        uids = []
396
        if self.getMethods():
397
            uids = [m.UID() for m in self.getMethods()]
398
        return uids
399
400 View Code Duplication
    def _getAvailableMethods(self):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
401
        """ Returns the available (active) methods.
402
            One method can be done by multiple instruments, but one
403
            instrument can only be used in one method.
404
        """
405
        bsc = getToolByName(self, 'bika_setup_catalog')
406
        items = [(c.UID, c.Title)
407
                 for c in bsc(portal_type='Method',
408
                              is_active=True)]
409
        items.sort(lambda x, y: cmp(x[1], y[1]))
410
        items.insert(0, ('', t(_('None'))))
411
        return DisplayList(items)
412
413
    def getMaintenanceTasks(self):
414
        return self.objectValues('InstrumentMaintenanceTask')
415
416
    def getCalibrations(self):
417
        """ Return all calibration objects related with the instrument
418
        """
419
        return self.objectValues('InstrumentCalibration')
420
421
    def getCertifications(self):
422
        """ Returns the certifications of the instrument. Both internal
423
            and external certifitions
424
        """
425
        return self.objectValues('InstrumentCertification')
426
427
    def getValidCertifications(self):
428
        """ Returns the certifications fully valid
429
        """
430
        certs = []
431
        today = date.today()
432
        for c in self.getCertifications():
433
            validfrom = c.getValidFrom() if c else None
434
            validto = c.getValidTo() if validfrom else None
435
            if not validfrom or not validto:
436
                continue
437
            validfrom = validfrom.asdatetime().date()
438
            validto = validto.asdatetime().date()
439
            if (today >= validfrom and today <= validto):
440
                certs.append(c)
441
        return certs
442
443
    def isValid(self):
444
        """ Returns if the current instrument is not out for verification, calibration,
445
        out-of-date regards to its certificates and if the latest QC succeed
446
        """
447
        return self.isOutOfDate() is False \
448
            and self.isQCValid() is True \
449
            and self.getDisposeUntilNextCalibrationTest() is False \
450
            and self.isValidationInProgress() is False \
451
            and self.isCalibrationInProgress() is False
452
453
    def isQCValid(self):
454
        """ Returns True if the results of the last batch of QC Analyses
455
        performed against this instrument was within the valid range.
456
457
        For a given Reference Sample, more than one Reference Analyses assigned
458
        to this same instrument can be performed and the Results Capture Date
459
        might slightly differ amongst them. Thus, this function gets the latest
460
        QC Analysis performed, looks for siblings (through RefAnalysisGroupID)
461
        and if the results for all them are valid, then returns True. If there
462
        is one single Reference Analysis from the group with an out-of-range
463
        result, the function returns False
464
        """
465
        query = {"portal_type": "ReferenceAnalysis",
466
                 "getInstrumentUID": self.UID(),
467
                 "sort_on": "getResultCaptureDate",
468
                 "sort_order": "reverse",
469
                 "sort_limit": 1,}
470
        brains = api.search(query, CATALOG_ANALYSIS_LISTING)
471
        if len(brains) == 0:
472
            # There are no Reference Analyses assigned to this instrument yet
473
            return True
474
475
        # Look for siblings. These are the QC Analyses that were created
476
        # together with this last ReferenceAnalysis and for the same Reference
477
        # Sample. If they were added through "Add Reference Analyses" in a
478
        # Worksheet, they typically appear in the same slot.
479
        group_id = brains[0].getReferenceAnalysesGroupID
480
        query = {"portal_type": "ReferenceAnalysis",
481
                 "getInstrumentUID": self.UID(),
482
                 "getReferenceAnalysesGroupID": group_id,}
483
        brains = api.search(query, CATALOG_ANALYSIS_LISTING)
484
        for brain in brains:
485
            results_range = brain.getResultsRange
486
            if not results_range:
487
                continue
488
            # Is out of range?
489
            out_of_range = is_out_of_range(brain)[0]
490
            if out_of_range:
491
                return False
492
493
        # By default, in range
494
        return True
495
496
    def isOutOfDate(self):
497
        """ Returns if the current instrument is out-of-date regards to
498
            its certifications
499
        """
500
        certification = self.getLatestValidCertification()
501
        if certification:
502
            return not certification.isValid()
503
        return True
504
505
    def isValidationInProgress(self):
506
        """ Returns if the current instrument is under validation progress
507
        """
508
        validation = self.getLatestValidValidation()
509
        if validation:
510
            return validation.isValidationInProgress()
511
        return False
512
513
    def isCalibrationInProgress(self):
514
        """ Returns if the current instrument is under calibration progress
515
        """
516
        calibration = self.getLatestValidCalibration()
517
        if calibration is not None:
518
            return calibration.isCalibrationInProgress()
519
        return False
520
521
    def getCertificateExpireDate(self):
522
        """ Returns the current instrument's data expiration certificate
523
        """
524
        certification = self.getLatestValidCertification()
525
        if certification:
526
            return certification.getValidTo()
527
        return None
528
529
    def getWeeksToExpire(self):
530
        """ Returns the amount of weeks and days untils it's certification expire
531
        """
532
        certification = self.getLatestValidCertification()
533
        if certification:
534
            return certification.getWeeksAndDaysToExpire()
535
        return 0, 0
536
537
    def getLatestValidCertification(self):
538
        """Returns the certification with the most remaining days until expiration.
539
           If no certification was found, it returns None.
540
        """
541
542
        # 1. get all certifications
543
        certifications = self.getCertifications()
544
545
        # 2. filter out certifications, which are invalid
546
        valid_certifications = filter(lambda x: x.isValid(), certifications)
547
548
        # 3. sort by the remaining days to expire, e.g. [10, 7, 6, 1]
549
        def sort_func(x, y):
550
            return cmp(x.getDaysToExpire(), y.getDaysToExpire())
551
        sorted_certifications = sorted(valid_certifications, cmp=sort_func, reverse=True)
552
553
        # 4. return the certification with the most remaining days
554
        if len(sorted_certifications) > 0:
555
            return sorted_certifications[0]
556
        return None
557
558
    def getLatestValidValidation(self):
559
        """Returns the validation with the most remaining days in validation.
560
           If no validation was found, it returns None.
561
        """
562
        # 1. get all validations
563
        validations = self.getValidations()
564
565
        # 2. filter out validations, which are not in progress
566
        active_validations = filter(lambda x: x.isValidationInProgress(), validations)
567
568
        # 3. sort by the remaining days in validation, e.g. [10, 7, 6, 1]
569
        def sort_func(x, y):
570
            return cmp(x.getRemainingDaysInValidation(), y.getRemainingDaysInValidation())
571
        sorted_validations = sorted(active_validations, cmp=sort_func, reverse=True)
572
573
        # 4. return the validation with the most remaining days
574
        if len(sorted_validations) > 0:
575
            return sorted_validations[0]
576
        return None
577
578
    def getLatestValidCalibration(self):
579
        """Returns the calibration with the most remaining days in calibration.
580
           If no calibration was found, it returns None.
581
        """
582
        # 1. get all calibrations
583
        calibrations = self.getCalibrations()
584
585
        # 2. filter out calibrations, which are not in progress
586
        active_calibrations = filter(lambda x: x.isCalibrationInProgress(), calibrations)
587
588
        # 3. sort by the remaining days in calibration, e.g. [10, 7, 6, 1]
589
        def sort_func(x, y):
590
            return cmp(x.getRemainingDaysInCalibration(), y.getRemainingDaysInCalibration())
591
        sorted_calibrations = sorted(active_calibrations, cmp=sort_func, reverse=True)
592
593
        # 4. return the calibration with the most remaining days
594
        if len(sorted_calibrations) > 0:
595
            return sorted_calibrations[0]
596
        return None
597
598
    def getValidations(self):
599
        """ Return all the validations objects related with the instrument
600
        """
601
        return self.objectValues('InstrumentValidation')
602
603
    def getDocuments(self):
604
        """ Return all the multifile objects related with the instrument
605
        """
606
        return self.objectValues('Multifile')
607
608
    def getSchedule(self):
609
        return self.objectValues('InstrumentScheduledTask')
610
611
    def addReferences(self, reference, service_uids):
612
        """ Add reference analyses to reference
613
        """
614
        # TODO Workflow - Analyses. Assignment of refanalysis to Instrument
615
        addedanalyses = []
616
        wf = getToolByName(self, 'portal_workflow')
617
        bsc = getToolByName(self, 'bika_setup_catalog')
618
        bac = getToolByName(self, 'bika_analysis_catalog')
619
        ref_type = reference.getBlank() and 'b' or 'c'
620
        ref_uid = reference.UID()
621
        postfix = 1
622
        for refa in reference.getReferenceAnalyses():
623
            grid = refa.getReferenceAnalysesGroupID()
624
            try:
625
                cand = int(grid.split('-')[2])
626
                if cand >= postfix:
627
                    postfix = cand + 1
628
            except:
629
                pass
630
        postfix = str(postfix).zfill(int(3))
631
        refgid = 'I%s-%s' % (reference.id, postfix)
632
        for service_uid in service_uids:
633
            # services with dependents don't belong in references
634
            service = bsc(portal_type='AnalysisService', UID=service_uid)[0].getObject()
635
            calc = service.getCalculation()
636
            if calc and calc.getDependentServices():
637
                continue
638
            ref_analysis = reference.addReferenceAnalysis(service)
639
640
            # Set ReferenceAnalysesGroupID (same id for the analyses from
641
            # the same Reference Sample and same Worksheet)
642
            # https://github.com/bikalabs/Bika-LIMS/issues/931
643
            ref_analysis.setReferenceAnalysesGroupID(refgid)
644
            ref_analysis.setInstrument(self)
645
            ref_analysis.reindexObject()
646
            addedanalyses.append(ref_analysis)
647
648
        # Set DisposeUntilNextCalibrationTest to False
649
        if (len(addedanalyses) > 0):
650
            self.getField('DisposeUntilNextCalibrationTest').set(self, False)
651
652
        return addedanalyses
653
654
    def setImportDataInterface(self, values):
655
        """ Return the current list of import data interfaces
656
        """
657
        exims = self.getImportDataInterfacesList()
658
        new_values = [value for value in values if value in exims]
659
        if len(new_values) < len(values):
660
            logger.warn("Some Interfaces weren't added...")
661
        self.Schema().getField('ImportDataInterface').set(self, new_values)
662
663
    def displayValue(self, vocab, value, widget):
664
        """Overwrite the Script (Python) `displayValue.py` located at
665
           `Products.Archetypes.skins.archetypes` to handle the references
666
           of our Picklist Widget (Methods) gracefully.
667
           This method gets called by the `picklist.pt` template like this:
668
669
           display python:context.displayValue(vocab, value, widget);"
670
        """
671
        # Taken from the Script (Python)
672
        t = self.restrictedTraverse('@@at_utils').translate
673
674
        # ensure we have strings, otherwise the `getValue` method of
675
        # Products.Archetypes.utils will raise a TypeError
676
        def to_string(v):
677
            if isinstance(v, basestring):
678
                return v
679
            return api.get_title(v)
680
681
        if isinstance(value, (list, tuple)):
682
            value = map(to_string, value)
683
684
        return t(vocab, value, widget)
685
686
687
schemata.finalizeATCTSchema(schema, folderish=True, moveDiscussion=False)
688
689
registerType(Instrument, PROJECTNAME)
690