class WorksheetTemplate(BaseContent): security = ClassSecurityInfo() displayContentsTab = False schema = schema _at_rename_after_creation = True def _renameAfterCreation(self, check_auto_id=False): from lims.idserver import renameAfterCreation renameAfterCreation(self) security.declarePublic('getAnalysisTypes') def getAnalysisTypes(self): """ return Analysis type displaylist """ return ANALYSIS_TYPES def getInstruments(self): bsc = getToolByName(self, 'bika_setup_catalog') items = [('', '')] + [ (o.UID, o.Title) for o in bsc(portal_type='Instrument', inactive_state='active') ] o = self.getInstrument() if o and o.UID() not in [i[0] for i in items]: items.append((o.UID(), o.Title())) items.sort(lambda x, y: cmp(x[1], y[1])) return DisplayList(list(items))
class AddressWidget(TypesWidget): _properties = TypesWidget._properties.copy() _properties.update({ 'macro': "bika_widgets/addresswidget", 'helper_js': ("bika_widgets/addresswidget.js",), 'helper_css': ("bika_widgets/addresswidget.css",), 'showLegend': True, 'showDistrict': True, 'showCopyFrom': True, 'showCity': True, 'showPostalCode': True, 'showAddress': True, }) security = ClassSecurityInfo() # The values in the form/field are always # Country Name, State Name, District Name. def getCountries(self): items = [] items = [(x['ISO'], x['Country']) for x in COUNTRIES] items.sort(lambda x,y: cmp(x[1], y[1])) return items def getDefaultCountry(self): portal = getToolByName(self, 'portal_url').getPortalObject() bs = portal._getOb('bika_setup') return bs.getDefaultCountry() def getStates(self, country): items = [] if not country: return items # get ISO code for country iso = [c for c in COUNTRIES if c['Country'] == country or c['ISO'] == country] if not iso: return items iso = iso[0]['ISO'] items = [x for x in STATES if x[0] == iso] items.sort(lambda x,y: cmp(x[2], y[2])) return items def getDistricts(self, country, state): items = [] if not country or not state: return items # get ISO code for country iso = [c for c in COUNTRIES if c['Country'] == country or c['ISO'] == country] if not iso: return items iso = iso[0]['ISO'] # get NUMBER of the state for lookup snr = [s for s in STATES if s[0] == iso and s[2] == state] if not snr: return items snr = snr[0][1] items = [x for x in DISTRICTS if x[0] == iso and x[1] == snr] items.sort(lambda x,y: cmp(x[1], y[1])) return items
class SelectionWidget(_s): _properties = _s._properties.copy() _properties.update({ 'macro': "bika_widgets/selection", }) security = ClassSecurityInfo()
class WorksheetTemplateLayoutWidget(RecordsWidget): security = ClassSecurityInfo() _properties = RecordsWidget._properties.copy() _properties.update({ 'macro': "bika_widgets/worksheettemplatelayoutwidget", 'helper_js': ("bika_widgets/worksheettemplatelayoutwidget.js", ), 'helper_css': ("bika_widgets/worksheettemplatelayoutwidget.css", ), }) security.declarePublic('get_template_rows') def get_template_rows(self, num_positions, current_field_value): try: num_pos = int(num_positions) except ValueError: num_pos = 10 rows = [] i = 1 if current_field_value: for row in current_field_value: if num_pos > 0: if i > num_pos: break rows.append(row) i = i + 1 for i in range(i, (num_pos + 1)): row = {'pos': i, 'type': 'a', 'sub': 1} rows.append(row) return rows
class BikaAnalysisCatalog(CatalogTool): """Catalog for analysis types""" implements(IBikaAnalysisCatalog) security = ClassSecurityInfo() _properties = ({'id': 'title', 'type': 'string', 'mode': 'w'}, ) title = 'Bika Analysis Catalog' id = 'bika_analysis_catalog' portal_type = meta_type = 'BikaAnalysisCatalog' plone_tool = 1 def __init__(self): ZCatalog.__init__(self, self.id) security.declareProtected(ManagePortal, 'clearFindAndRebuild') def clearFindAndRebuild(self): """ """ def indexObject(obj, path): self.reindexObject(obj) at = getToolByName(self, 'archetype_tool') types = [k for k, v in at.catalog_map.items() if self.id in v] self.manage_catalogClear() portal = getToolByName(self, 'portal_url').getPortalObject() portal.ZopeFindAndApply(portal, obj_metatypes=types, search_sub=True, apply_func=indexObject)
class LabContact(Person): security = ClassSecurityInfo() displayContentsTab = False schema = schema implements(ILabContact) _at_rename_after_creation = True def _renameAfterCreation(self, check_auto_id=False): from lims.idserver import renameAfterCreation renameAfterCreation(self) def Title(self): """ Return the contact's Fullname as title """ return safe_unicode(self.getFullname()).encode('utf-8') def hasUser(self): """ check if contact has user """ return self.portal_membership.getMemberById( self.getUsername()) is not None def getDepartments(self): bsc = getToolByName(self, 'bika_setup_catalog') items = [('','')] + [(o.UID, o.Title) for o in bsc(portal_type='Department', inactive_state = 'active')] o = self.getDepartment() if o and o.UID() not in [i[0] for i in items]: items.append((o.UID(), o.Title())) items.sort(lambda x,y: cmp(x[1], y[1])) return DisplayList(list(items))
class CoordinateField(RecordField): """ Stores angle in deg, min, sec, bearing """ security = ClassSecurityInfo() _properties = RecordField._properties.copy() _properties.update({ 'type': 'angle', 'subfields': ('degrees', 'minutes', 'seconds', 'bearing'), ## 'required_subfields' : ('degrees', 'minutes', 'seconds', 'bearing'), 'subfield_labels': { 'degrees': _('Degrees'), 'minutes': _('Minutes'), 'seconds': _('Seconds'), 'bearing': _('Bearing') }, 'subfield_sizes': { 'degrees': 3, 'minutes': 2, 'seconds': 2, 'bearing': 1 }, 'subfield_validators': { 'degrees': 'coordinatevalidator', 'minutes': 'coordinatevalidator', 'seconds': 'coordinatevalidator', 'bearing': 'coordinatevalidator', }, })
class AnalysisCategory(BaseContent): implements(IAnalysisCategory) security = ClassSecurityInfo() displayContentsTab = False schema = schema _at_rename_after_creation = True def _renameAfterCreation(self, check_auto_id=False): from lims.idserver import renameAfterCreation renameAfterCreation(self) def getDepartments(self): bsc = getToolByName(self, 'bika_setup_catalog') deps = [] for d in bsc(portal_type='Department', inactive_state='active'): deps.append((d.UID, d.Title)) return DisplayList(deps) def workflow_script_deactivat(self): # A instance cannot be deactivated if it contains services pu = getToolByName(self, 'plone_utils') bsc = getToolByName(self, 'bika_setup_catalog') ars = bsc(portal_type='AnalysisService', getCategoryUID=self.UID()) if ars: message = _("Category cannot be deactivated because " "it contains Analysis Services") pu.addPortalMessage(message, 'error') transaction.get().abort() raise WorkflowException
class InstrumentCertification(BaseFolder): security = ClassSecurityInfo() schema = schema displayContentsTab = False _at_rename_after_creation = True def _renameAfterCreation(self, check_auto_id=False): from lims.idserver import renameAfterCreation renameAfterCreation(self) def getLabContacts(self): bsc = getToolByName(self, 'bika_setup_catalog') # fallback - all Lab Contacts pairs = [] for contact in bsc(portal_type='LabContact', inactive_state='active', sort_on='sortable_title'): pairs.append((contact.UID, contact.Title)) return DisplayList(pairs) def getInstrumentAssetNumber(self): """ Obtains the instrument's asset number :return: The asset number string """ return self.aq_parent.getAssetNumber() if self.aq_parent.getAssetNumber() else ''
class SRTemplateARTemplatesWidget(TypesWidget): _properties = TypesWidget._properties.copy() _properties.update({ 'macro': "bika_widgets/srtemplateartemplateswidget", }) security = ClassSecurityInfo() security.declarePublic('process_form') def process_form(self, instance, field, form, empty_marker = None, emptyReturnsMarker = False): bsc = getToolByName(instance, 'bika_setup_catalog') value = [] service_uids = form.get('uids', None) return service_uids, {} security.declarePublic('ARTemplates') def ARTemplates(self, field, allow_edit = False): fieldvalue = getattr(field, field.accessor)() view = SRTemplateARTemplatesView( self, self.REQUEST, fieldvalue = fieldvalue, allow_edit = allow_edit ) return view.contents_table(table_only = True)
class PrefixesField(RecordsField): """a list of prefixes per portal_type""" _properties = RecordsField._properties.copy() _properties.update({ 'type' : 'prefixes', 'subfields' : ('portal_type', 'prefix', 'separator', 'padding', 'sequence_start'), 'subfield_labels':{'portal_type': 'Portal type', 'prefix': 'Prefix', 'separator': 'Prefix Separator', 'padding': 'Padding', 'sequence_start': 'Sequence Start', }, 'subfield_readonly':{'portal_type': False, 'prefix': False, 'padding': False, 'separator': False, 'sequence_start': False, }, 'subfield_sizes':{'portal_type':32, 'prefix': 12, 'padding':12, 'separator': 5, 'sequence_start': 12, }, 'subfield_types':{'padding':'int', 'sequence_start': 'int'}, }) security = ClassSecurityInfo()
class AnalysisProfile(BaseContent): security = ClassSecurityInfo() schema = schema displayContentsTab = False implements(IAnalysisProfile) _at_rename_after_creation = True def _renameAfterCreation(self, check_auto_id=False): from lims.idserver import renameAfterCreation renameAfterCreation(self) def getClientUID(self): return self.aq_parent.UID() def getAnalysisServiceSettings(self, uid): """ Returns a dictionary with the settings for the analysis service that match with the uid provided. If there are no settings for the analysis service and profile, returns a dictionary with the key 'uid' """ sets = [s for s in self.getAnalysisServicesSettings() \ if s.get('uid','') == uid] return sets[0] if sets else {'uid': uid} def isAnalysisServiceHidden(self, uid): """ Checks if the analysis service that match with the uid provided must be hidden in results. If no hidden assignment has been set for the analysis in this profile, returns the visibility set to the analysis itself. Raise a TypeError if the uid is empty or None Raise a ValueError if there is no hidden assignment in this profile or no analysis service found for this uid. """ if not uid: raise TypeError('None type or empty uid') sets = self.getAnalysisServiceSettings(uid) if 'hidden' not in sets: uc = getToolByName(self, 'uid_catalog') serv = uc(UID=uid) if serv and len(serv) == 1: return serv[0].getObject().getRawHidden() else: raise ValueError('%s is not valid' % uid) return sets.get('hidden', False) def getVATAmount(self): """ Compute AnalysisProfileVATAmount """ price, vat = self.getAnalysisProfilePrice( ), self.getAnalysisProfileVAT() return float(price) * float(vat) / 100 def getTotalPrice(self): """ Computes the final price using the VATAmount and the subtotal price """ price, vat = self.getAnalysisProfilePrice(), self.getVATAmount() return float(price) + float(vat)
class IntegerWidget(_i): _properties = _i._properties.copy() _properties.update({ 'macro': "bika_widgets/integer", 'unit': '', }) security = ClassSecurityInfo()
class DecimalWidget(_d): _properties = _d._properties.copy() _properties.update({ 'macro': "bika_widgets/decimal", 'unit': '', }) security = ClassSecurityInfo()
class ARImportItem(BaseContent): security = ClassSecurityInfo() implements(IARImportItem) schema = schema displayContentsTab = False def Title(self): """ Return the Product as title """ return safe_unicode(self.getSampleName()).encode('utf-8')
class SampleMatrix(BaseFolder): security = ClassSecurityInfo() displayContentsTab = False schema = schema _at_rename_after_creation = True def _renameAfterCreation(self, check_auto_id=False): from lims.idserver import renameAfterCreation renameAfterCreation(self)
class ARPriority(BaseContent): security = ClassSecurityInfo() schema = schema displayContentsTab = False implements(IARPriority) _at_rename_after_creation = True def _renameAfterCreation(self, check_auto_id=False): renameAfterCreation(self)
class SRTemplate(BaseContent): security = ClassSecurityInfo() schema = schema displayContentsTab = False _at_rename_after_creation = True def _renameAfterCreation(self, check_auto_id=False): renameAfterCreation(self)
class AnalysisSpecificationWidget(TypesWidget): _properties = TypesWidget._properties.copy() _properties.update({ 'macro': "bika_widgets/analysisspecificationwidget", #'helper_js': ("bika_widgets/analysisspecificationwidget.js",), #'helper_css': ("bika_widgets/analysisspecificationwidget.css",), }) security = ClassSecurityInfo() security.declarePublic('process_form') def process_form(self, instance, field, form, empty_marker = None, emptyReturnsMarker = False): """ Return a list of dictionaries fit for AnalysisSpecsResultsField consumption. If neither hidemin nor hidemax are specified, only services which have float()able entries in result,min and max field will be included. If hidemin and/or hidemax specified, results might contain empty min and/or max fields. """ value = [] if 'service' in form: for uid, keyword in form['keyword'][0].items(): hidemin = form['hidemin'][0].get(uid, '') if 'hidemin' in form else '' hidemax = form['hidemax'][0].get(uid, '') if 'hidemax' in form else '' mins = form['min'][0].get(uid, '') if 'min' in form else '' maxs = form['max'][0].get(uid, '') if 'max' in form else '' err = form['error'][0].get(uid, '') if 'error' in form else '' rangecomment = form['rangecomment'][0].get(uid, '') if 'rangecomment' in form else '' if not isnumber(hidemin) and not isnumber(hidemax) and \ (not isnumber(mins) or not isnumber(maxs)): # If neither hidemin nor hidemax have been specified, # min and max values are mandatory. continue value.append({'keyword': keyword, 'uid': uid, 'min': mins if isnumber(mins) else '', 'max': maxs if isnumber(maxs) else '', 'hidemin': hidemin if isnumber(hidemin) else '', 'hidemax': hidemax if isnumber(hidemax) else '', 'error': err if isnumber(err) else '0', 'rangecomment': rangecomment}) return value, {} security.declarePublic('AnalysisSpecificationResults') def AnalysisSpecificationResults(self, field, allow_edit = False): """ Prints a bika listing with categorized services. field contains the archetypes field with a list of services in it """ fieldvalue = getattr(field, field.accessor)() view = AnalysisSpecificationView(self, self.REQUEST, fieldvalue = fieldvalue, allow_edit = allow_edit) return view.contents_table(table_only = True)
class BikaSetup(folder.ATFolder): security = ClassSecurityInfo() schema = schema implements(IBikaSetup, IHaveNoBreadCrumbs) def getAttachmentsPermitted(self): """ are any attachments permitted """ if self.getARAttachmentOption() in ['r', 'p'] \ or self.getAnalysisAttachmentOption() in ['r', 'p']: return True else: return False def getStickerTemplates(self): """ get the sticker templates """ out = [[t['id'], t['title']] for t in _getStickerTemplates()] return DisplayList(out) def getARAttachmentsPermitted(self): """ are AR attachments permitted """ if self.getARAttachmentOption() == 'n': return False else: return True def getAnalysisAttachmentsPermitted(self): """ are analysis attachments permitted """ if self.getAnalysisAttachmentOption() == 'n': return False else: return True def getAnalysisServices(self): """ """ bsc = getToolByName(self, 'bika_setup_catalog') items = [('','')] + [(o.UID, o.Title) for o in bsc(portal_type='AnalysisService', inactive_state = 'active')] items.sort(lambda x,y: cmp(x[1], y[1])) return DisplayList(list(items)) def getPrefixFor(self, portal_type): """Return the prefix for a portal_type. If not found, simply uses the portal_type itself """ prefix = [p for p in self.getPrefixes() if p['portal_type'] == portal_type] if prefix: return prefix[0]['prefix'] else: return portal_type def getCountries(self): items = [(x['ISO'], x['Country']) for x in COUNTRIES] items.sort(lambda x,y: cmp(x[1], y[1])) return items
class AttachmentType(BaseContent): security = ClassSecurityInfo() displayContentsTab = False schema = schema _at_rename_after_creation = True def _renameAfterCreation(self, check_auto_id=False): from lims.idserver import renameAfterCreation renameAfterCreation(self)
class ARTemplatePartitionsWidget(RecordsWidget): _properties = RecordsWidget._properties.copy() _properties.update({ 'helper_js': ( "bika_widgets/recordswidget.js", "bika_widgets/artemplatepartitionswidget.js", ) }) security = ClassSecurityInfo()
class SupplierContact(Person): security = ClassSecurityInfo() displayContentsTab = False schema = schema _at_rename_after_creation = True def _renameAfterCreation(self, check_auto_id=False): from lims.idserver import renameAfterCreation renameAfterCreation(self)
class ScheduleInputWidget(TypesWidget): _properties = TypesWidget._properties.copy() _properties.update({ 'ulocalized_time': ulocalized_time, 'macro': "bika_widgets/scheduleinputwidget", 'helper_js': ("bika_widgets/scheduleinputwidget.js", ), 'helper_css': ("bika_widgets/scheduleinputwidget.css", ), 'maxDate': '+0d', 'yearRange': '-100:+0' }) security = ClassSecurityInfo() def process_form(self, instance, field, form, empty_marker=None, emptyReturnsMarker=False): values = len(instance.getScheduleCriteria() ) > 0 and instance.getScheduleCriteria() or [] if "form.button.save" in form: value = [] fn = form['fieldName'] fromDate = fn + "_fromdate" in form and form[fn + "_fromdate"] or None fromEnabled = (fromDate and fn + "_fromenabled" in form and form[fn + "_fromenabled"] == 'on') and True or False repeatUnit = fn + "_repeatunit" in form and form[ fn + "_repeatunit"] or None repeatPeriod = fn + "_repeatperiodselected" in form and form[ fn + "_repeatperiodselected"] or None repeatEnabled = (repeatUnit and fn + "_repeatenabled" in form and form[fn + "_repeatenabled"] == 'on') and True or False repeatUntil = fn + "_repeatuntil" in form and form[ fn + "_repeatuntil"] or None repeatUntilEnabled = ( repeatUntil and fn + "_repeatuntilenabled" in form and form[fn + "_repeatuntilenabled"] == 'on') and True or False value.append({ 'fromenabled': fromEnabled, 'fromdate': fromDate, 'repeatenabled': repeatEnabled, 'repeatunit': repeatUnit, 'repeatperiod': repeatPeriod, 'repeatuntilenabled': repeatUntilEnabled, 'repeatuntil': repeatUntil }) return value, {}
class Organisation(ATFolder): security = ClassSecurityInfo() displayContentsTab = False schema = schema security.declareProtected(CMFCorePermissions.View, 'getSchema') def getSchema(self): return self.schema def Title(self): """ Return the Organisation's Name as its title """ field = self.getField('Name') field = field and field.get(self) or '' return safe_unicode(field).encode('utf-8') def setTitle(self, value): return self.setName(value) def getPossibleAddresses(self): return ['PhysicalAddress', 'PostalAddress', 'BillingAddress'] def getPrintAddress(self): address_lines = [] use_address = None if self.getPostalAddress().has_key('city') \ and self.getPostalAddress()['city']: use_address = self.getPostalAddress() elif self.getPhysicalAddress().has_key('city') \ and self.getPhysicalAddress()['city']: use_address = self.getPhysicalAddress() elif self.getBillingAddress().has_key('city') \ and self.getBillingAddress()['city']: use_address = self.getBillingAddress() if use_address: if use_address['address']: address_lines.append(use_address['address']) city_line = '' if use_address['city']: city_line += use_address['city'] + ' ' if use_address['zip']: city_line += use_address['zip'] + ' ' if city_line: address_lines.append(city_line) statecountry_line = '' if use_address['state']: statecountry_line += use_address['state'] + ', ' if use_address['country']: statecountry_line += use_address['country'] if statecountry_line: address_lines.append(statecountry_line) return address_lines
class Manufacturer(BaseContent): implements(IManufacturer) security = ClassSecurityInfo() displayContentsTab = False schema = schema _at_rename_after_creation = True def _renameAfterCreation(self, check_auto_id=False): from lims.idserver import renameAfterCreation renameAfterCreation(self)
class StorageLocation(BaseContent, HistoryAwareMixin): security = ClassSecurityInfo() displayContentsTab = False schema = schema _at_rename_after_creation = True def _renameAfterCreation(self, check_auto_id=False): from lims.idserver import renameAfterCreation renameAfterCreation(self) def Title(self): return safe_unicode(self.getField('title').get(self)).encode('utf-8')
class DateTimeField(DTF): """A field that stores dates and times This is identical to the AT widget on which it's based, but it checks the i18n translation values for date formats. This does not specifically check the date_format_short_datepicker, so this means that date_formats should be identical between the python strftime and the jquery version. """ _properties = Field._properties.copy() _properties.update({ 'type': 'datetime', 'widget': CalendarWidget, }) implements(IDateTimeField) security = ClassSecurityInfo() security.declarePrivate('set') def set(self, instance, value, **kwargs): """ Check if value is an actual date/time value. If not, attempt to convert it to one; otherwise, set to None. Assign all properties passed as kwargs to object. """ val = value if not value: val = None elif not isinstance(value, DateTime): for fmt in ['date_format_long', 'date_format_short']: fmtstr = instance.translate(fmt, domain='bika', mapping={}) fmtstr = fmtstr.replace(r"${", '%').replace('}', '') try: val = strptime(value, fmtstr) except ValueError: continue try: val = DateTime(*list(val)[:-6]) except DateTimeError: val = None if val.timezoneNaive(): # Use local timezone for tz naive strings # see http://dev.plone.org/plone/ticket/10141 zone = val.localZone(safelocaltime(val.timeTime())) parts = val.parts()[:-1] + (zone, ) val = DateTime(*parts) break else: logger.warning("DateTimeField failed to format date " "string '%s' with '%s'" % (value, fmtstr)) super(DateTimeField, self).set(instance, val, **kwargs)
class Invoice(BaseFolder): implements(IInvoice) security = ClassSecurityInfo() displayContentsTab = False schema = schema _at_rename_after_creation = True def _renameAfterCreation(self, check_auto_id=False): from lims.idserver import renameAfterCreation renameAfterCreation(self) def Title(self): """ Return the Invoice Id as title """ return safe_unicode(self.getId()).encode('utf-8') security.declareProtected(View, 'getSubtotal') def getSubtotal(self): """ Compute Subtotal """ return sum([float(obj['Subtotal']) for obj in self.invoice_lineitems]) security.declareProtected(View, 'getVATAmount') def getVATAmount(self): """ Compute VAT """ return Decimal(self.getTotal()) - Decimal(self.getSubtotal()) security.declareProtected(View, 'getTotal') def getTotal(self): """ Compute Total """ return sum([float(obj['Total']) for obj in self.invoice_lineitems]) security.declareProtected(View, 'getInvoiceSearchableText') def getInvoiceSearchableText(self): """ Aggregate text of all line items for querying """ s = '' for item in self.invoice_lineitems: s = s + item['ItemDescription'] return s # XXX workflow script def workflow_script_dispatch(self): """ dispatch order """ self.setDateDispatched(DateTime()) security.declarePublic('current_date') def current_date(self): """ return current date """ return DateTime()
class SamplePoint(BaseContent, HistoryAwareMixin): security = ClassSecurityInfo() displayContentsTab = False schema = schema _at_rename_after_creation = True def _renameAfterCreation(self, check_auto_id=False): from lims.idserver import renameAfterCreation renameAfterCreation(self) def Title(self): return safe_unicode(self.getField('title').get(self)).encode('utf-8') def SampleTypesVocabulary(self): from lims.content.sampletype import SampleTypes return SampleTypes(self, allow_blank=False) def setSampleTypes(self, value, **kw): """ For the moment, we're manually trimming the sampletype<>samplepoint relation to be equal on both sides, here. It's done strangely, because it may be required to behave strangely. """ bsc = getToolByName(self, 'bika_setup_catalog') ## convert value to objects if value and type(value) == str: value = [bsc(UID=value)[0].getObject(),] elif value and type(value) in (list, tuple) and type(value[0]) == str: value = [bsc(UID=uid)[0].getObject() for uid in value if uid] if not type(value) in (list, tuple): value = [value,] ## Find all SampleTypes that were removed existing = self.Schema()['SampleTypes'].get(self) removed = existing and [s for s in existing if s not in value] or [] added = value and [s for s in value if s not in existing] or [] ret = self.Schema()['SampleTypes'].set(self, value) for st in removed: samplepoints = st.getSamplePoints() if self in samplepoints: samplepoints.remove(self) st.setSamplePoints(samplepoints) for st in added: st.setSamplePoints(list(st.getSamplePoints()) + [self,]) return ret def getSampleTypes(self, **kw): return self.Schema()['SampleTypes'].get(self) def getClientUID(self): return self.aq_parent.UID()