Passed
Push — master ( 0b1266...31959a )
by Ramon
10:16 queued 04:28
created

Instrument._renameAfterCreation()   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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