| Total Complexity | 78 |
| Total Lines | 739 |
| Duplicated Lines | 1.62 % |
| Changes | 0 | ||
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:
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 | # 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 |
||
|
|
|||
| 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): |
|
| 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 |