From 92cd6273124124f0578719fbed624ec2ef4e978f Mon Sep 17 00:00:00 2001 From: Campbell Date: Wed, 3 May 2017 14:50:42 +0200 Subject: [PATCH 01/36] Create BaseAnalysis class as base for Analyis and AnalysisService --- bika/lims/browser/fields/aranalysesfield.py | 7 +- bika/lims/content/analysis.py | 661 ++++----- bika/lims/content/analysisservice.py | 1471 +++---------------- bika/lims/content/baseanalysis.py | 1250 ++++++++++++++++ bika/lims/utils/analysis.py | 23 +- 5 files changed, 1786 insertions(+), 1626 deletions(-) create mode 100644 bika/lims/content/baseanalysis.py diff --git a/bika/lims/browser/fields/aranalysesfield.py b/bika/lims/browser/fields/aranalysesfield.py index 1b7dc37980..6976747d91 100644 --- a/bika/lims/browser/fields/aranalysesfield.py +++ b/bika/lims/browser/fields/aranalysesfield.py @@ -174,12 +174,7 @@ def set(self, instance, service_uids, prices=None, specs=None, **kwargs): if shasattr(instance, keyword): analysis = instance._getOb(keyword) else: - analysis = create_analysis( - instance, - service, - keyword, - interim_fields - ) + analysis = create_analysis(instance, service) new_analyses.append(analysis) for i, r in enumerate(rr): if r['keyword'] == analysis.getService().getKeyword(): diff --git a/bika/lims/content/analysis.py b/bika/lims/content/analysis.py index 2ceab7f01b..0923980246 100644 --- a/bika/lims/content/analysis.py +++ b/bika/lims/content/analysis.py @@ -5,264 +5,347 @@ # Copyright 2011-2016 by it's authors. # Some rights reserved. See LICENSE.txt, AUTHORS.txt. +"""DuplicateAnalysis uses this as it's base. This accounts for much confusion. +""" -"DuplicateAnalysis uses this as it's base. This accounts for much confusion." +import math -import traceback -from plone import api -from AccessControl import getSecurityManager +import cgi from AccessControl import ClassSecurityInfo from DateTime import DateTime -from bika.lims import logger -from bika.lims.utils.analysis import format_numeric_result -from plone.indexer import indexer -from Products.ATContentTypes.content import schemata -from Products.ATExtensions.ateapi import DateTimeField, DateTimeWidget, RecordsField +from Products.ATExtensions.ateapi import DateTimeWidget from Products.Archetypes import atapi from Products.Archetypes.config import REFERENCE_CATALOG from Products.Archetypes.public import * from Products.Archetypes.references import HoldingReference from Products.CMFCore.WorkflowCore import WorkflowException -from Products.CMFCore.permissions import View, ModifyPortalContent from Products.CMFCore.utils import getToolByName -from Products.CMFPlone.utils import safe_unicode, _createObjectByType -from Products.CMFEditions.ArchivistTool import ArchivistRetrieveError +from Products.CMFPlone.utils import _createObjectByType from bika.lims import bikaMessageFactory as _ -from bika.lims.utils import t -from bika.lims.browser.fields import DurationField +from bika.lims import logger from bika.lims.browser.fields import HistoryAwareReferenceField from bika.lims.browser.fields import InterimFieldsField -from bika.lims.permissions import * -from bika.lims.permissions import Verify as VerifyPermission -from bika.lims.browser.widgets import DurationWidget from bika.lims.browser.widgets import RecordsWidget as BikaRecordsWidget from bika.lims.config import PROJECTNAME -from bika.lims.content.bikaschema import BikaSchema +from bika.lims.content.baseanalysis import schema as BaseAnalysisSchema, \ + BaseAnalysis +from bika.lims.content.reflexrule import doReflexRuleAction from bika.lims.interfaces import IAnalysis, IDuplicateAnalysis, IReferenceAnalysis, \ - IRoutineAnalysis, ISamplePrepWorkflow + ISamplePrepWorkflow from bika.lims.interfaces import IReferenceSample +from bika.lims.permissions import * +from bika.lims.permissions import Verify as VerifyPermission from bika.lims.utils import changeWorkflowState, formatDecimalMark from bika.lims.utils import drop_trailing_zeros_decimal +from bika.lims.utils.analysis import format_numeric_result from bika.lims.utils.analysis import get_significant_digits from bika.lims.workflow import skip -from bika.lims.workflow import doActionFor -from bika.lims.content.reflexrule import doReflexRuleAction from decimal import Decimal +from plone import api from zope.interface import implements -import cgi -import datetime -import math +# Although attributes of the service are stored directly in the Analysis, +# the originating ServiceUID is required to prevent duplication of analyses. +ServiceUID = StringField( + 'ServiceUID', +) + +Calculation = HistoryAwareReferenceField( + 'Calculation', + allowed_types=('Calculation',), + relationship='AnalysisCalculation', + referenceClass=HoldingReference, +) + +Attachment = ReferenceField( + 'Attachment', + multiValued=1, + allowed_types=('Attachment',), + referenceClass=HoldingReference, + relationship='AnalysisAttachment', +) + +Result = StringField( + 'Result' +) + +ResultCaptureDate = DateTimeField( + 'ResultCaptureDate', + widget=ComputedWidget( + visible=False + ) +) + +ResultDM = StringField( + 'ResultDM' +) + +Retested = BooleanField( + 'Retested', + default=False +) + +DateAnalysisPublished = DateTimeField( + 'DateAnalysisPublished', + widget=DateTimeWidget( + label=_("Date Published") + ) +) + +DueDate = DateTimeField( + 'DueDate', + widget=DateTimeWidget( + label=_("Due Date") + ) +) + +Duration = IntegerField( + 'Duration', + widget=IntegerWidget( + label=_("Duration") + ) +) + +Earliness = IntegerField( + 'Earliness', + widget=IntegerWidget( + label=_("Earliness") + ) +) + +Analyst = StringField( + 'Analyst' +) + +Remarks = TextField( + 'Remarks' +) + +Method = ReferenceField( + 'Method', + required=0, + allowed_types=('Method',), + relationship='AnalysisMethod', + referenceClass=HoldingReference +) + +# The analysis method can't be changed when the analysis belongs +# to a worksheet and that worksheet has a method. +CanMethodBeChanged = BooleanField( + 'CanMethodBeChanged', + default=True, + required=0, + visible=False +) + +SamplePartition = ReferenceField( + 'SamplePartition', + required=0, + allowed_types=('SamplePartition',), + relationship='AnalysisSamplePartition', + referenceClass=HoldingReference +) + +# True if the analysis is created by a reflex rule +IsReflexAnalysis = BooleanField( + 'IsReflexAnalysis', + default=False, + required=0 +) + +# This field contains the original analysis which was reflected +OriginalReflexedAnalysis = ReferenceField( + 'OriginalReflexedAnalysis', + required=0, + allowed_types=('Analysis',), + relationship='OriginalAnalysisReflectedAnalysis', + referenceClass=HoldingReference +) + +# This field contains the analysis which has been reflected +# following a reflex rule +ReflexAnalysisOf = ReferenceField( + 'ReflexAnalysisOf', + required=0, + allowed_types=('Analysis',), + relationship='AnalysisReflectedAnalysis', + referenceClass=HoldingReference +) + +# Which is the Reflex Rule action that has created this analysis +ReflexRuleAction = StringField( + 'ReflexRuleAction', + required=0, + default=0 +) + +# Which is the 'local_id' inside the reflex rule +ReflexRuleLocalID = StringField( + 'ReflexRuleLocalID', + required=0, + default=0 +) + +# Reflex rule triggered actions which the current analysis is +# responsible of. Separated by '|' +ReflexRuleActionsTriggered = StringField( + 'ReflexRuleActionsTriggered', + required=0, + default='' +) + +ClientUID = ComputedField( + 'ClientUID', + expression='context.aq_parent.aq_parent.UID()' +) + +ClientTitle = ComputedField( + 'ClientTitle', + expression='context.aq_parent.aq_parent.Title()' +) + +RequestID = ComputedField( + 'RequestID', + expression='context.aq_parent.getRequestID()' +) + +ClientOrderNumber = ComputedField( + 'ClientOrderNumber', + expression='context.aq_parent.getClientOrderNumber()' +) + +SampleTypeUID = ComputedField( + 'SampleTypeUID', + expression='context.aq_parent.getSample().getSampleType().UID()' +) + +SamplePointUID = ComputedField( + 'SamplePointUID', + expression='context.aq_parent.getSample().getSamplePoint().UID(' + ') if context.aq_parent.getSample().getSamplePoint()' + ' else ""' +) + +MethodUID = ComputedField( + 'MethodUID', + expression="context.getMethod() and context.getMethod().UID() or ''", + widget=ComputedWidget( + visible=False + ) +) + +InstrumentUID = ComputedField( + 'InstrumentUID', + expression="" + "context.getInstrument() and context.getInstrument().UID() or ''", + widget=ComputedWidget( + visible=False + ) +) + +DateReceived = ComputedField( + 'DateReceived', + expression='context.aq_parent.getDateReceived()' +) + +DateSampled = ComputedField( + 'DateSampled', + expression='context.aq_parent.getSample().getDateSampled()' +) + +InstrumentValid = ComputedField( + 'InstrumentValid', + expression='context.isInstrumentValid()' +) + +Uncertainty = FixedPointField( + 'Uncertainty', + precision=10, + widget=DecimalWidget( + label=_("Uncertainty") + ) +) + +DetectionLimitOperand = StringField( + 'DetectionLimitOperand' +) + +# Required number of required verifications before this analysis being +# transitioned to a 'verified' state. This value is set automatically +# when the analysis is created, based on the value set for the property +# NumberOfRequiredVerifications from the Analysis Service +NumberOfRequiredVerifications = IntegerField( + 'NumberOfRequiredVerifications', + default=1 +) -schema = BikaSchema.copy() + Schema(( - HistoryAwareReferenceField('Service', - required=1, - allowed_types=('AnalysisService',), - relationship='AnalysisAnalysisService', - referenceClass=HoldingReference, - widget=ReferenceWidget( - label = _("Analysis Service"), - ) - ), - HistoryAwareReferenceField('Calculation', - allowed_types=('Calculation',), - relationship='AnalysisCalculation', - referenceClass=HoldingReference, - ), - ReferenceField('Attachment', - multiValued=1, - allowed_types=('Attachment',), - referenceClass = HoldingReference, - relationship = 'AnalysisAttachment', - ), - InterimFieldsField('InterimFields', - widget = BikaRecordsWidget( - label = _("Calculation Interim Fields"), - ) - ), - StringField('Result', - ), - DateTimeField('ResultCaptureDate', - widget = ComputedWidget( - visible=False, - ), - ), - StringField('ResultDM', - ), - BooleanField('Retested', - default = False, - ), - DurationField('MaxTimeAllowed', - widget = DurationWidget( - label = _("Maximum turn-around time"), - description=_("Maximum time allowed for completion of the analysis. " - "A late analysis alert is raised when this period elapses"), - ), - ), - DateTimeField('DateAnalysisPublished', - widget = DateTimeWidget( - label = _("Date Published"), - ), - ), - DateTimeField('DueDate', - widget = DateTimeWidget( - label = _("Due Date"), - ), - ), - IntegerField('Duration', - widget = IntegerWidget( - label = _("Duration"), - ) - ), - IntegerField('Earliness', - widget = IntegerWidget( - label = _("Earliness"), - ) - ), - BooleanField('ReportDryMatter', - default = False, - ), - StringField('Analyst', - ), - TextField('Remarks', - ), - ReferenceField('Instrument', - required = 0, - allowed_types = ('Instrument',), - relationship = 'AnalysisInstrument', - referenceClass = HoldingReference, - ), - ReferenceField('Method', - required = 0, - allowed_types = ('Method',), - relationship = 'AnalysisMethod', - referenceClass = HoldingReference, - ), - # The analysis method can't be changed when the analysis belongs - # to a worksheet and that worksheet has a method. - BooleanField( - 'CanMethodBeChanged', - default=True, - required=0, - visible=False, - ), - ReferenceField('SamplePartition', - required = 0, - allowed_types = ('SamplePartition',), - relationship = 'AnalysisSamplePartition', - referenceClass = HoldingReference, - ), - # True if the analysis is created by a reflex rule - BooleanField( - 'IsReflexAnalysis', - default=False, - required=0, - ), - # This field contains the original analysis which was reflected - ReferenceField( - 'OriginalReflexedAnalysis', - required=0, - allowed_types=('Analysis',), - relationship='OriginalAnalysisReflectedAnalysis', - referenceClass=HoldingReference, - ), - # This field contains the analysis which has been reflected - # following a reflex rule - ReferenceField( - 'ReflexAnalysisOf', - required=0, - allowed_types=('Analysis',), - relationship='AnalysisReflectedAnalysis', - referenceClass=HoldingReference, - ), - # Which is the Reflex Rule action that has created this analysis - StringField('ReflexRuleAction', required=0, default=0), - # Which is the 'local_id' inside the reflex rule - StringField('ReflexRuleLocalID', required=0, default=0), - # Reflex rule triggered actions which the current analysis is - # responsible of. Separated by '|' - StringField('ReflexRuleActionsTriggered', required=0, default=''), - ComputedField('ClientUID', - expression = 'context.aq_parent.aq_parent.UID()', - ), - ComputedField('ClientTitle', - expression = 'context.aq_parent.aq_parent.Title()', - ), - ComputedField('RequestID', - expression = 'context.aq_parent.getRequestID()', - ), - ComputedField('ClientOrderNumber', - expression = 'context.aq_parent.getClientOrderNumber()', - ), - ComputedField('Keyword', - expression = 'context.getServiceUsingQuery().getKeyword() if context.getServiceUsingQuery() else ""', - ), - ComputedField('ServiceUID', - expression = 'context.getServiceUsingQuery().UID() if context.getServiceUsingQuery() else ""', - ), - ComputedField('SampleTypeUID', - expression = 'context.aq_parent.getSample().getSampleType().UID()', - ), - ComputedField('SamplePointUID', - expression = 'context.aq_parent.getSample().getSamplePoint().UID() if context.aq_parent.getSample().getSamplePoint() else ""', - ), - ComputedField('CategoryUID', - expression = 'context.getServiceUsingQuery().getCategoryUID() if context.getServiceUsingQuery() else ""', - ), - ComputedField( - 'MethodUID', - expression="context.getMethod() and context.getMethod().UID() or ''", - widget=ComputedWidget( - visible=False, - ), - ), - ComputedField( - 'InstrumentUID', - expression="context.getInstrument() and context.getInstrument().UID() or ''", - widget=ComputedWidget( - visible=False, - ), - ), - ComputedField('PointOfCapture', - expression = 'context.getServiceUsingQuery().getPointOfCapture() if context.getServiceUsingQuery() else ""', - ), - ComputedField('DateReceived', - expression = 'context.aq_parent.getDateReceived()', - ), - ComputedField('DateSampled', - expression = 'context.aq_parent.getSample().getDateSampled()', - ), - ComputedField('InstrumentValid', - expression = 'context.isInstrumentValid()' - ), - FixedPointField('Uncertainty', - precision=10, - widget=DecimalWidget( - label = _("Uncertainty"), - ), - ), - StringField('DetectionLimitOperand',), - - # Required number of required verifications before this analysis being - # transitioned to a 'verified' state. This value is set automatically - # when the analysis is created, based on the value set for the property - # NumberOfRequiredVerifications from the Analysis Service - IntegerField('NumberOfRequiredVerifications', default=1), - - # This field keeps the user_ids of members who verified this analysis. - # After each verification, user_id will be added end of this string - # seperated by comma- ',' . - StringField('Verificators',default='') -), +# This field keeps the user_ids of members who verified this analysis. +# After each verification, user_id will be added end of this string +# seperated by comma- ',' . +Verificators = StringField( + 'Verificators', + default='' ) -class Analysis(BaseContent): +schema = BaseAnalysisSchema.copy() + Schema(( + ServiceUID, + Calculation, + Attachment, + Result, + ResultCaptureDate, + ResultDM, + Retested, + DateAnalysisPublished, + DueDate, + Duration, + Earliness, + Analyst, + Remarks, + Method, + CanMethodBeChanged, + SamplePartition, + IsReflexAnalysis, + OriginalReflexedAnalysis, + ReflexAnalysisOf, + ReflexRuleAction, + ReflexRuleLocalID, + ReflexRuleActionsTriggered, + ClientUID, + ClientTitle, + RequestID, + ClientOrderNumber, + ServiceUID, + SampleTypeUID, + SamplePointUID, + MethodUID, + InstrumentUID, + DateReceived, + DateSampled, + InstrumentValid, + Uncertainty, + DetectionLimitOperand, + NumberOfRequiredVerifications, + Verificators +)) + + +class Analysis(BaseAnalysis): implements(IAnalysis, ISamplePrepWorkflow) security = ClassSecurityInfo() displayContentsTab = False schema = schema + def getServiceUID(self): + import pdb;pdb.set_trace();pass + print "XXX analysis.getServiceUID(!!!)"%self + return self.UID() + + def getService(self): + import pdb;pdb.set_trace();pass + print "XXX analysis.getService(!!!)"%self + return self.UID() + def _getCatalogTool(self): from bika.lims.catalog import getCatalog return getCatalog(self) @@ -292,63 +375,6 @@ def wasVerifiedByUser(self,username): def getLastVerificator(self): return self.getVerificators().split(',')[-1] - # TODO: This method can be improved, and maybe removed. - def getServiceUsingQuery(self): - """ - This function returns the asociated service. - Use this method to avoid some errors while rebuilding a catalog. - """ - obj = None - try: - # Try to obtain the object as usual. - obj = self.getService() - except: - # Getting the service like that because otherwise gives an error - # when rebuilding the catalogs. - logger.error(traceback.format_exc()) - obj = None - - if not obj: - # Try to obtain the object by using the getRawService getter and - # query against the catalog - logger.warn("Unable to retrieve the Service for Analysis %s " - "via a direct call to getService(). Retrying by using " - "getRawService() and querying against uid_catalog." - % self.getId()) - try: - service_uid = self.getRawService() - except: - logger.error(traceback.format_exc()) - logger.error("Corrupt Analysis UID=%s . Cannot obtain its " - "Service. Try to purge the catalog or try to fix" - " it at %s" % (self.UID(), self.absolute_path())) - return None - if not service_uid: - logger.warn("Unable to retrieve the Service for Analysis %s " - "via a direct call to getRawService()." - % self.getId()) - return None - # We got an UID, query agains the catalog to obtain the Service - catalog = getToolByName(self, "uid_catalog") - brain = catalog(UID=service_uid) - if len(brain) == 1: - obj = brain[0].getObject() - elif len(brain) == 0: - logger.error("No Service found for UID %s" % service_uid) - else: - raise RuntimeError( - "More than one Service found for UID %s" % service_uid) - return obj - - def Title(self): - """ Return the service title as title. - Some silliness here, for premature indexing, when the service - is not yet configured. - """ - s = self.getServiceUsingQuery() - s = s.Title() if s else '' - return safe_unicode(s).encode('utf-8') - def updateDueDate(self): # set the max hours allowed @@ -374,37 +400,6 @@ def updateDueDate(self): duetime = '' self.setDueDate(duetime) - # TODO-performance: improve this function using another catalog and takeing - # advantatge of the column in service, not getting the full object. - def getDepartmentUID(self): - """ - Returns the UID of the asociated service's department. - """ - # Getting the service like that because otherwise gives an error - # when rebuilding the catalogs. - service_uid = self.getRawService() - catalog = getToolByName(self, "uid_catalog") - brain = catalog(UID=service_uid) - if brain and len(brain) == 1: - dep = brain[0].getObject().getDepartment() - return dep.UID() if dep else '' - return '' - - # TODO-performance: improve this function using another catalog and takeing - # advantatge of the column in service, not getting the full object. - def getCategoryTitle(self): - """ - Returns the Title of the asociated service's department. - """ - # Getting the service like that because otherwise gives an error - # when rebuilding the catalogs. - service_uid = self.getRawService() - catalog = getToolByName(self, "uid_catalog") - brain = catalog(UID=service_uid) - if brain: - return brain[0].getObject().getCategoryTitle() - return '' - def getAnalysisRequestTitle(self): """ This is a column @@ -417,14 +412,13 @@ def getAnalysisRequestURL(self): """ return self.aq_parent.absolute_url_path() - # TODO-performance: improve this function using another catalog and takeing - # advantatge of the column in service, not getting the full object. + def getService(self): + return self + def getServiceTitle(self): + """ Returns the Title of the asociated service. """ - Returns the Title of the asociated service. - """ - obj = self.getServiceUsingQuery() - return obj.Title() if obj else '' + return self.Title() def getReviewState(self): """ Return the current analysis' state""" @@ -1041,16 +1035,6 @@ def getAllowedMethodsAsTuples(self): method in service.getMethods()] return result - def getInstrumentEntryOfResults(self): - """ - It is a metacolumn. - Returns the same value as the service. - """ - service = self.getService() - if not service: - return None - return service.getInstrumentEntryOfResults() - def getAllowedInstruments(self, onlyuids=True): """ Returns the allowed instruments for this analysis. Gets the instruments assigned to the allowed methods @@ -1289,16 +1273,6 @@ def isVerifiable(self): # All checks passsed return True - def isSelfVerificationEnabled(self): - """ - Checks if the service allows self verification of the analysis. - :returns: boolean - """ - service = self.getService() - if service: - return service.isSelfVerificationEnabled() - return False - def isUserAllowedToVerify(self, member): """ Checks if the specified user has enough privileges to verify the @@ -1459,15 +1433,6 @@ def getClientURL(self): """ return self.aq_parent.aq_parent.absolute_url_path() - def getUnit(self): - """ - This works as a metadatacolumn - """ - service = self.getService() - if not service: - return None - return service.getUnit() - def getSamplePartitionID(self): """ This works as a metadatacolumn diff --git a/bika/lims/content/analysisservice.py b/bika/lims/content/analysisservice.py index cc69007ad0..fbaf1ae714 100644 --- a/bika/lims/content/analysisservice.py +++ b/bika/lims/content/analysisservice.py @@ -7,47 +7,28 @@ import sys +import transaction from AccessControl import ClassSecurityInfo -from DateTime import DateTime -from plone.indexer import indexer -from Products.ATContentTypes.lib.historyaware import HistoryAwareMixin -from Products.ATExtensions.Extensions.utils import makeDisplayList -from Products.ATExtensions.ateapi import RecordField, RecordsField +from Products.ATExtensions.ateapi import RecordsField from Products.Archetypes.Registry import registerField from Products.Archetypes.public import DisplayList, ReferenceField, \ - ComputedField, ComputedWidget, BooleanField, \ - BooleanWidget, StringField, SelectionWidget, \ - FixedPointField, DecimalWidget, IntegerField, \ - IntegerWidget, StringWidget, BaseContent, \ - Schema, registerType, MultiSelectionWidget, \ - FloatField, DecimalWidget -from Products.Archetypes.utils import IntDisplayList + BooleanField, BooleanWidget, Schema, registerType, SelectionWidget from Products.Archetypes.references import HoldingReference -from Products.CMFCore.permissions import View, ModifyPortalContent -from Products.CMFCore.utils import getToolByName -from Products.validation import validation -from Products.validation.validators.RegexValidator import RegexValidator from Products.CMFCore.WorkflowCore import WorkflowException +from Products.CMFCore.utils import getToolByName from bika.lims import PMF, bikaMessageFactory as _ -from bika.lims.utils import to_utf8 as _c -from bika.lims.utils import to_unicode as _u -from bika.lims.utils.analysis import get_significant_digits -from bika.lims.browser.widgets.durationwidget import DurationWidget +from bika.lims.browser.fields import UIDReferenceField from bika.lims.browser.widgets.partitionsetupwidget import PartitionSetupWidget -from bika.lims.browser.widgets.recordswidget import RecordsWidget from bika.lims.browser.widgets.referencewidget import ReferenceWidget -from bika.lims.browser.fields import * -from bika.lims.config import ATTACHMENT_OPTIONS, PROJECTNAME, \ - SERVICE_POINT_OF_CAPTURE -from bika.lims.content.bikaschema import BikaSchema +from bika.lims.config import PROJECTNAME +from bika.lims.content.baseanalysis import schema as BaseAnalysisSchema, \ + BaseAnalysis from bika.lims.interfaces import IAnalysisService, IHaveIdentifiers -from magnitude import mg, MagnitudeError -from zope import i18n +from bika.lims.utils import to_utf8 as _c +from magnitude import mg +from plone.indexer import indexer from zope.interface import implements -import transaction -import math - def getContainers(instance, minvol=None, @@ -68,7 +49,7 @@ def getContainers(instance, XXX obj = bsc(getKeyword='Moist')[0].getObject() XXX u'Container Type: Canvas bag' in obj.getContainers().values() XXX True -x + """ bsc = getToolByName(instance, 'bika_setup_catalog') @@ -87,7 +68,7 @@ def getContainers(instance, capacity = mg(float(capacity[0]), capacity[1]) if capacity < minvol: continue - except: + except (ValueError, TypeError): # if there's a unit conversion error, allow the container # to be displayed. pass @@ -141,7 +122,6 @@ class PartitionSetupField(RecordsField): 'subfield_types': { 'separate': 'boolean', 'vol': 'string', - 'preservation': 'sampletype', 'container': 'selection', 'preservation': 'selection', }, @@ -197,8 +177,6 @@ def Containers(self, instance=None): registerField(PartitionSetupField, title="", description="") -# # XXX When you modify this schema, be sure to edit the list of fields -## to duplicate, in bika_analysisservices.py. @indexer(IAnalysisService) def sortable_title_with_sort_key(instance): @@ -207,823 +185,197 @@ def sortable_title_with_sort_key(instance): return "{:010.3f}{}".format(sort_key, instance.Title()) return instance.Title() -schema = BikaSchema.copy() + Schema(( - StringField('ShortTitle', - schemata="Description", - widget=StringWidget( - label = _("Short title"), - description=_( - "If text is entered here, it is used instead of the " - "title when the service is listed in column headings. " - "HTML formatting is allowed.") - ), - ), - FloatField('SortKey', - schemata="Description", - validators=('SortKeyValidator',), - widget=DecimalWidget( - label = _("Sort Key"), - description = _("Float value from 0.0 - 1000.0 indicating the sort order. Duplicate values are ordered alphabetically."), - ), - ), - BooleanField('ScientificName', - schemata="Description", - default=False, - widget=BooleanWidget( - label = _("Scientific name"), - description = _( - "If enabled, the name of the analysis will be " - "written in italics."), - ), - ), - StringField('Unit', - schemata="Description", - widget=StringWidget( - label = _("Unit"), - description=_( - "The measurement units for this analysis service' results, " - "e.g. mg/l, ppm, dB, mV, etc."), - ), - ), - IntegerField('Precision', - schemata="Analysis", - widget=IntegerWidget( - label = _("Precision as number of decimals"), - description=_( - "Define the number of decimals to be used for this result."), - ), - ), - IntegerField('ExponentialFormatPrecision', - schemata="Analysis", - default = 7, - widget=IntegerWidget( - label = _("Exponential format precision"), - description=_( - "Define the precision when converting values to exponent " - "notation. The default is 7."), - ), - ), - FixedPointField('LowerDetectionLimit', - schemata="Analysis", - default='0.0', - precision=7, - widget=DecimalWidget( - label = _("Lower Detection Limit (LDL)"), - description = _("The Lower Detection Limit is " - "the lowest value to which the " - "measured parameter can be " - "measured using the specified " - "testing methodology. Results " - "entered which are less than " - "this value will be reported " - "as < LDL") - ), - ), - FixedPointField('UpperDetectionLimit', - schemata="Analysis", - default='1000000000.0', - precision=7, - widget=DecimalWidget( - label = _("Upper Detection Limit (UDL)"), - description = _("The Upper Detection Limit is the " - "highest value to which the " - "measured parameter can be measured " - "using the specified testing " - "methodology. Results entered " - "which are greater than this value " - "will be reported as > UDL") - ), - ), - # LIMS-1775 Allow to select LDL or UDL defaults in results with readonly mode - # https://jira.bikalabs.com/browse/LIMS-1775 - # Some behavior controlled with javascript: If checked, the field - # "AllowManualDetectionLimit" will be displayed. - # See browser/js/bika.lims.analysisservice.edit.js - # - # Use cases: - # a) If "DetectionLimitSelector" is enabled and - # "AllowManualDetectionLimit" is enabled too, then: - # the analyst will be able to select an '>', '<' operand from the - # selection list and also set the LD manually. - # - # b) If "DetectionLimitSelector" is enabled and - # "AllowManualDetectionLimit" is unchecked, the analyst will be - # able to select an operator from the selection list, but not set - # the LD manually: the default LD will be displayed in the result - # field as usuall, but in read-only mode. - # - # c) If "DetectionLimitSelector" is disabled, no LD selector will be - # displayed in the results table. - BooleanField('DetectionLimitSelector', - schemata="Analysis", - default=False, - widget=BooleanWidget( - label = _("Display a Detection Limit selector"), - description = _("If checked, a selection list will be " - "displayed next to the analysis' result " - "field in results entry views. By using " - "this selector, the analyst will be able " - "to set the value as a Detection Limit " - "(LDL or UDL) instead of a regular result"), - ), - ), - # Behavior controlled with javascript: Only visible when the - # "DetectionLimitSelector" is checked - # See browser/js/bika.lims.analysisservice.edit.js - # Check inline comment for "DetecionLimitSelector" field for - # further information. - BooleanField('AllowManualDetectionLimit', - schemata="Analysis", - default=False, - widget=BooleanWidget( - label = _("Allow Manual Detection Limit input"), - description = _("Allow the analyst to manually " - "replace the default Detection Limits " - "(LDL and UDL) on results entry views"), - ), - ), - BooleanField('ReportDryMatter', - schemata="Analysis", - default=False, - widget=BooleanWidget( - label = _("Report as Dry Matter"), - description = _("These results can be reported as dry matter"), - ), - ), - StringField('AttachmentOption', - schemata="Analysis", - default='p', - vocabulary=ATTACHMENT_OPTIONS, - widget=SelectionWidget( - label = _("Attachment Option"), - description=_( - "Indicates whether file attachments, e.g. microscope images, " - "are required for this analysis and whether file upload function " - "will be available for it on data capturing screens"), - format='select', - ), - ), - StringField('Keyword', - schemata="Description", - required=1, - searchable=True, - validators=('servicekeywordvalidator'), - widget=StringWidget( - label = _("Analysis Keyword"), - description=_( - "The unique keyword used to identify the analysis service in " - "import files of bulk AR requests and results imports from instruments. " - "It is also used to identify dependent analysis services in user " - "defined results calculations"), - ), - ), - # Allow/Disallow manual entry of results - # Behavior controlled by javascript depending on Instruments field: - # - If InstrumentEntry not checked, set checked and readonly - # - If InstrumentEntry checked, set as not readonly - # See browser/js/bika.lims.analysisservice.edit.js - BooleanField('ManualEntryOfResults', - schemata="Method", - default=True, - widget=BooleanWidget( - label = _("Instrument assignment is not required"), - description=_("Select if the results for tests of this " - "type of analysis can be set manually. " - "If selected, the user will be able to " - "set a result for a test of this type of " - "analysis in manage results view without " - "the need of selecting an instrument, " - "even though the method selected for the " - "test has instruments assigned."), - ) - ), - # Allow/Disallow instrument entry of results - # Behavior controlled by javascript depending on Instruments field: - # - If no instruments available, hide and uncheck - # - If at least one instrument selected, checked, but not readonly - # See browser/js/bika.lims.analysisservice.edit.js - BooleanField('InstrumentEntryOfResults', - schemata="Method", - default=False, - widget=BooleanWidget( - label = _("Instrument assignment is allowed"), - description=_("Select if the results for tests of " - "this type of analysis can be set by " - "using an instrument. If disabled, no " - "instruments will be available for " - "tests of this type of analysis in " - "manage results view, even though " - "the method selected for the test has " - "instruments assigned."), - ) - ), - # Instruments associated to the AS - # List of instruments capable to perform the Analysis Service. The - # Instruments selected here are displayed in the Analysis Request - # Add view, closer to this Analysis Service if selected. - # - If InstrumentEntry not checked, hide and unset - # - If InstrumentEntry checked, set the first selected and show - ReferenceField('Instruments', - schemata="Method", - required=0, - multiValued=1, - vocabulary_display_path_bound=sys.maxint, - vocabulary='_getAvailableInstrumentsDisplayList', - allowed_types=('Instrument',), - relationship='AnalysisServiceInstruments', - referenceClass=HoldingReference, - widget=MultiSelectionWidget( - label = _("Instruments"), - description=_("More than one instrument can be " - "used in a test of this type of " - "analysis. A selection list " - "with the instruments selected here " - "is populated in the results manage " - "view for each test of this type of " - "analysis. The available instruments " - "in the selection list will change " - "in accordance with the method " - "selected by the user for that test " - "in the manage results view. " - "Although a method can have more " - "than one instrument assigned, the " - "selection list is only populated " - "with the instruments that are both " - "set here and allowed for the " - "selected method."), - ) - ), - # Default instrument to be used. - # Gets populated with the instruments selected in the multiselection - # box above. - # Behavior controlled by js depending on ManualEntry/Instruments: - # - Populate dynamically with selected Instruments - # - If InstrumentEntry checked, set first selected instrument - # - If InstrumentEntry not checked, hide and set None - # See browser/js/bika.lims.analysisservice.edit.js - HistoryAwareReferenceField('Instrument', - schemata="Method", - searchable=True, - required=0, - vocabulary_display_path_bound=sys.maxint, - vocabulary='_getAvailableInstrumentsDisplayList', - allowed_types=('Instrument',), - relationship='AnalysisServiceInstrument', - referenceClass=HoldingReference, - widget=SelectionWidget( - format='select', - label = _("Default Instrument"), - description = _("This is the instrument " - "the system will assign by default to " - "tests from this type of analysis in " - "manage results view. The method " - "associated to this instrument will be " - "assigned as the default method too." - "Note the instrument's method will " - "prevail over any of the methods " - "choosen if the 'Instrument assignment " - "is not required' option is enabled.") - ), - ), - # Returns the Default's instrument title. If no default instrument - # set, returns string.empty - ComputedField('InstrumentTitle', - expression="context.getInstrument() and context.getInstrument().Title() or ''", - widget=ComputedWidget( - visible=False, - ), - ), - # Manual methods associated to the AS - # List of methods capable to perform the Analysis Service. The - # Methods selected here are displayed in the Analysis Request - # Add view, closer to this Analysis Service if selected. - # Use getAvailableMethods() to retrieve the list with methods both - # from selected instruments and manually entered. - # Behavior controlled by js depending on ManualEntry/Instrument: - # - If InsrtumentEntry not checked, show - # See browser/js/bika.lims.analysisservice.edit.js - ReferenceField('Methods', - schemata = "Method", - required = 0, - multiValued = 1, - vocabulary_display_path_bound = sys.maxint, - vocabulary = '_getAvailableMethodsDisplayList', - allowed_types = ('Method',), - relationship = 'AnalysisServiceMethods', - referenceClass = HoldingReference, - widget = MultiSelectionWidget( - label = _("Methods"), - description = _("The tests of this type of analysis can be " - "performed by using more than one method with the " - "'Manual entry of results' option enabled. " - "A selection list with the methods selected here " - "is populated in the manage results view for each " - "test of this type of analysis. Note that only " - "methods with 'Allow manual entry' option enabled " - "are displayed here; if you want the user to be " - "able to assign a method that requires instrument " - "entry, enable the 'Instrument assignment is " - "allowed' option."), - ) - ), - # Default method to be used. This field is used in Analysis Service - # Edit view, use getMethod() to retrieve the Method to be used in - # this Analysis Service. - # Gets populated with the methods selected in the multiselection - # box above or with the default instrument's method. - # Behavior controlled by js depending on ManualEntry/Instrument/Methods: - # - If InstrumentEntry checked, set instrument's default method, and readonly - # - If InstrumentEntry not checked, populate dynamically with - # selected Methods, set the first method selected and non-readonly - # See browser/js/bika.lims.analysisservice.edit.js - ReferenceField('_Method', - schemata = "Method", - required = 0, - searchable = True, - vocabulary_display_path_bound = sys.maxint, - allowed_types = ('Method',), - vocabulary = '_getAvailableMethodsDisplayList', - relationship = 'AnalysisServiceMethod', - referenceClass = HoldingReference, - widget = SelectionWidget( - format='select', - label = _("Default Method"), - description=_("If 'Allow instrument entry of results' " + \ - "is selected, the method from the default instrument " + \ - "will be used. Otherwise, only the methods " + \ - "selected above will be displayed.") - ), - ), - # Allow/Disallow to set the calculation manually - # Behavior controlled by javascript depending on Instruments field: - # - If no instruments available, hide and uncheck - # - If at least one instrument selected, checked, but not readonly - # See browser/js/bika.lims.analysisservice.edit.js - BooleanField('UseDefaultCalculation', - schemata="Method", - default=True, - widget=BooleanWidget( - label = _("Use default calculation"), - description=_("Select if the calculation to be used is the " + \ - "calculation set by default in the default " + \ - "method. If unselected, the calculation can " + \ - "be selected manually"), - ) - ), - # Default calculation to be used. This field is used in Analysis Service - # Edit view, use getCalculation() to retrieve the Calculation to be used in - # this Analysis Service. - # The default calculation is the one linked to the default method - # Behavior controlled by js depending on UseDefaultCalculation: - # - If UseDefaultCalculation is set to False, show this field - # - If UseDefaultCalculation is set to True, show this field - # See browser/js/bika.lims.analysisservice.edit.js - ReferenceField('_Calculation', - schemata="Method", - required=0, - vocabulary_display_path_bound=sys.maxint, - vocabulary='_getAvailableCalculationsDisplayList', - allowed_types=('Calculation',), - relationship='AnalysisServiceCalculation', - referenceClass=HoldingReference, - widget=SelectionWidget( - format='select', - label = _("Default Calculation"), - description=_("Default calculation to be used from the " + \ - "default Method selected. The Calculation " + \ - "for a method can be assigned in the Method " + \ - "edit view."), - catalog_name='bika_setup_catalog', - base_query={'inactive_state': 'active'}, - ), - ), - # Default calculation is not longer linked directly to the AS: it - # currently uses the calculation linked to the default Method. - # Use getCalculation() to retrieve the Calculation to be used. - # Old ASes (before 3008 upgrade) can be linked to the same Method, - # but to different Calculation objects. In that case, the Calculation - # is saved as DeferredCalculation and UseDefaultCalculation is set to - # False in the upgrade. - # Behavior controlled by js depending on UseDefaultCalculation: - # - If UseDefaultCalculation is set to False, show this field - # - If UseDefaultCalculation is set to True, show this field - # See browser/js/bika.lims.analysisservice.edit.js - # bika/lims/upgrade/to3008.py - ReferenceField('DeferredCalculation', - schemata="Method", - required=0, - vocabulary_display_path_bound=sys.maxint, - vocabulary='_getAvailableCalculationsDisplayList', - allowed_types=('Calculation',), - relationship='AnalysisServiceDeferredCalculation', - referenceClass=HoldingReference, - widget=SelectionWidget( - format='select', - label = _("Alternative Calculation"), - description=_( - "If required, select a calculation for the analysis here. " - "Calculations can be configured under the calculations item " - "in the LIMS set-up"), - catalog_name='bika_setup_catalog', - base_query={'inactive_state': 'active'}, - ), - ), - - ComputedField('CalculationTitle', - expression="context.getCalculation() and context.getCalculation().Title() or ''", - searchable=True, - widget=ComputedWidget( - visible=False, - ), - ), - ComputedField('CalculationUID', - expression="context.getCalculation() and context.getCalculation().UID() or ''", - widget=ComputedWidget( - visible=False, - ), - ), - InterimFieldsField('InterimFields', - schemata='Method', - widget=RecordsWidget( - label = _("Calculation Interim Fields"), - description=_( - "Values can be entered here which will override the defaults " - "specified in the Calculation Interim Fields."), - ), - ), - DurationField('MaxTimeAllowed', - schemata="Analysis", - widget=DurationWidget( - label = _("Maximum turn-around time"), - description=_( - "Maximum time allowed for completion of the analysis. " - "A late analysis alert is raised when this period elapses"), - ), - ), - FixedPointField('DuplicateVariation', - schemata="Method", - widget=DecimalWidget( - label = _("Duplicate Variation %"), - description=_( - "When the results of duplicate analyses on worksheets, " - "carried out on the same sample, differ with more than " - "this percentage, an alert is raised"), - ), - ), - BooleanField('Accredited', - schemata="Method", - default=False, - widget=BooleanWidget( - label = _("Accredited"), - description=_( - "Check this box if the analysis service is included in the " - "laboratory's schedule of accredited analyses"), - ), - ), - StringField('PointOfCapture', - schemata="Description", - required=1, - default='lab', - vocabulary=SERVICE_POINT_OF_CAPTURE, - widget=SelectionWidget( - format='flex', - label = _("Point of Capture"), - description=_( - "The results of field analyses are captured during sampling " - "at the sample point, e.g. the temperature of a water sample " - "in the river where it is sampled. Lab analyses are done in " - "the laboratory"), - ), - ), - ReferenceField('Category', - schemata="Description", - required=1, - vocabulary_display_path_bound=sys.maxint, - allowed_types=('AnalysisCategory',), - relationship='AnalysisServiceAnalysisCategory', - referenceClass=HoldingReference, - vocabulary='getAnalysisCategories', - widget=ReferenceWidget( - checkbox_bound=0, - label = _("Analysis Category"), - description=_( - "The category the analysis service belongs to"), - catalog_name='bika_setup_catalog', - base_query={'inactive_state': 'active'}, - ), - ), - FixedPointField('Price', - schemata="Description", - default='0.00', - widget=DecimalWidget( - label = _("Price (excluding VAT)"), - ), - ), - # read access permission - FixedPointField('BulkPrice', - schemata="Description", - default='0.00', - widget=DecimalWidget( - label = _("Bulk price (excluding VAT)"), - description=_( - "The price charged per analysis for clients who qualify for bulk discounts"), - ), - ), - ComputedField('VATAmount', - schemata="Description", - expression='context.getVATAmount()', - widget=ComputedWidget( - label = _("VAT"), - visible={'edit': 'hidden', } - ), - ), - ComputedField('TotalPrice', - schemata="Description", - expression='context.getTotalPrice()', - widget=ComputedWidget( - label = _("Total price"), - visible={'edit': 'hidden', } - ), - ), - FixedPointField('VAT', - schemata="Description", - default_method='getDefaultVAT', - widget=DecimalWidget( - label = _("VAT %"), - description = _("Enter percentage value eg. 14.0"), - ), - ), - ComputedField('CategoryTitle', - expression="context.getCategory() and context.getCategory().Title() or ''", - widget=ComputedWidget( - visible=False, - ), - ), - ComputedField('CategoryUID', - expression="context.getCategory() and context.getCategory().UID() or ''", - widget=ComputedWidget( - visible=False, - ), - ), - ReferenceField('Department', - schemata="Description", - required=0, - vocabulary_display_path_bound=sys.maxint, - allowed_types=('Department',), - vocabulary='getDepartments', - relationship='AnalysisServiceDepartment', - referenceClass=HoldingReference, - widget=ReferenceWidget( - checkbox_bound=0, - label = _("Department"), - description = _("The laboratory department"), - catalog_name='bika_setup_catalog', - base_query={'inactive_state': 'active'}, - ), - ), - ComputedField('DepartmentTitle', - expression="context.getDepartment() and context.getDepartment().Title() or ''", - searchable=True, - widget=ComputedWidget( - visible=False, - ), - ), - RecordsField('Uncertainties', - schemata="Uncertainties", - type='uncertainties', - subfields=('intercept_min', 'intercept_max', 'errorvalue'), - required_subfields=( - 'intercept_min', 'intercept_max', 'errorvalue'), - subfield_sizes={'intercept_min': 10, - 'intercept_max': 10, - 'errorvalue': 10, - }, - subfield_labels={'intercept_min': _('Range min'), - 'intercept_max': _('Range max'), - 'errorvalue': _('Uncertainty value'), - }, - subfield_validators={'intercept_min': 'uncertainties_validator', - 'intercept_max': 'uncertainties_validator', - 'errorvalue': 'uncertainties_validator', - }, - widget=RecordsWidget( - label = _("Uncertainty"), - description=_( - "Specify the uncertainty value for a given range, e.g. for " - "results in a range with minimum of 0 and maximum of 10, " - "where the uncertainty value is 0.5 - a result of 6.67 will " - "be reported as 6.67 +- 0.5. You can also specify the " - "uncertainty value as a percentage of the result value, by " - "adding a '%' to the value entered in the 'Uncertainty Value' " - "column, e.g. for results in a range with minimum of 10.01 " - "and a maximum of 100, where the uncertainty value is 2% - " - "a result of 100 will be reported as 100 +- 2. Please ensure " - "successive ranges are continuous, e.g. 0.00 - 10.00 is " - "followed by 10.01 - 20.00, 20.01 - 30 .00 etc."), - ), - ), - # Calculate the precision from Uncertainty value - # Behavior controlled by javascript - # - If checked, Precision and ExponentialFormatPrecision are not displayed. - # The precision will be calculated according to the uncertainty. - # - If checked, Precision and ExponentialFormatPrecision will be displayed. - # See browser/js/bika.lims.analysisservice.edit.js - BooleanField('PrecisionFromUncertainty', - schemata="Uncertainties", - default=False, - widget=BooleanWidget( - label = _("Calculate Precision from Uncertainties"), - description=_("Precision as the number of significant " - "digits according to the uncertainty. " - "The decimal position will be given by " - "the first number different from zero in " - "the uncertainty, at that position the " - "system will round up the uncertainty " - "and results. " - "For example, with a result of 5.243 and " - "an uncertainty of 0.22, the system " - "will display correctly as 5.2+-0.2. " - "If no uncertainty range is set for the " - "result, the system will use the " - "fixed precision set."), - ), - ), - - # If checked, an additional input with the default uncertainty will - # be displayed in the manage results view. The value set by the user - # in this field will override the default uncertainty for the analysis - # result - BooleanField('AllowManualUncertainty', - schemata="Uncertainties", - default=False, - widget=BooleanWidget( - label = _("Allow manual uncertainty value input"), - description = _("Allow the analyst to manually " - "replace the default uncertainty " - "value."), - ), - ), - RecordsField('ResultOptions', - schemata="Result Options", - type='resultsoptions', - subfields=('ResultValue', 'ResultText'), - required_subfields=('ResultValue', 'ResultText'), - subfield_labels={'ResultValue': _('Result Value'), - 'ResultText': _('Display Value'), }, - subfield_validators={'ResultValue': 'resultoptionsvalidator', - 'ResultText': 'resultoptionsvalidator'}, - subfield_sizes={'ResultValue': 5, - 'ResultText': 25, - }, - widget=RecordsWidget( - label = _("Result Options"), - description=_( - "Please list all options for the analysis result if you want to restrict " - "it to specific options only, e.g. 'Positive', 'Negative' and " - "'Indeterminable'. The option's result value must be a number"), - ), - ), - BooleanField('Separate', - schemata='Container and Preservation', - default=False, - required=0, - widget=BooleanWidget( - label = _("Separate Container"), - description=_("Check this box to ensure a separate sample " + \ - "container is used for this analysis service"), - ), - ), - ReferenceField('Preservation', - schemata='Container and Preservation', - allowed_types=('Preservation',), - relationship='AnalysisServicePreservation', - referenceClass=HoldingReference, - vocabulary='getPreservations', - required=0, - multiValued=0, - widget=ReferenceWidget( - checkbox_bound=0, - label = _("Default Preservation"), - description=_("Select a default preservation for this " + \ - "analysis service. If the preservation depends on " + \ - "the sample type combination, specify a preservation " + \ - "per sample type in the table below"), - catalog_name='bika_setup_catalog', - base_query={'inactive_state': 'active'}, - ), - ), - ReferenceField('Container', - schemata='Container and Preservation', - allowed_types=('Container', 'ContainerType'), - relationship='AnalysisServiceContainer', - referenceClass=HoldingReference, - vocabulary='getContainers', - required=0, - multiValued=0, - widget=ReferenceWidget( - checkbox_bound=0, - label = _("Default Container"), - description=_( - "Select the default container to be used for this " - "analysis service. If the container to be used " - "depends on the sample type and preservation " - "combination, specify the container in the sample " - "type table below"), - catalog_name='bika_setup_catalog', - base_query={'inactive_state': 'active'}, - ), - ), - PartitionSetupField('PartitionSetup', - schemata='Container and Preservation', - widget=PartitionSetupWidget( - label=PMF("Preservation per sample type"), - description=_( - "Please specify preservations that differ from the " - "analysis service's default preservation per sample " - "type here."), - ), - ), - BooleanField('Hidden', - schemata="Analysis", - default=False, - widget=BooleanWidget( - label = _("Hidden"), - description = _( - "If enabled, this analysis and its results " - "will not be displayed by default in reports. " - "This setting can be overrided in Analysis " - "Profile and/or Analysis Request"), - ), - ), - IntegerField( - 'SelfVerification', - schemata="Analysis", - default=-1, - vocabulary="_getSelfVerificationVocabulary", - widget=SelectionWidget( - label=_("Self-verification of results"), - description=_( - "If enabled, a user who submitted a result for this analysis " - "will also be able to verify it. This setting take effect for " - "those users with a role assigned that allows them to verify " - "results (by default, managers, labmanagers and verifiers). " - "The option set here has priority over the option set in Bika " - "Setup"), - format="select", - ), - ), - IntegerField( - '_NumberOfRequiredVerifications', - schemata="Analysis", - default=-1, - vocabulary="_getNumberOfRequiredVerificationsVocabulary", - widget=SelectionWidget( - label=_("Number of required verifications"), - description=_( - "Number of required verifications from different users with " - "enough privileges before a given result for this analysis " - "being considered as 'verified'. The option set here has " - "priority over the option set in Bika Setup"), - format="select", - ), - ), - StringField('CommercialID', - searchable=1, - schemata='Description', - required=0, - widget=StringWidget( - label=_("Commercial ID"), - description=_("The service's commercial ID for accounting purposes") - ), - ), - StringField('ProtocolID', - searchable=1, - schemata = 'Description', - required=0, - widget=StringWidget( - label=_("Protocol ID"), - description=_("The service's analytical protocol ID") - ), - ), +Separate = BooleanField( + 'Separate', + schemata='Container and Preservation', + default=False, + required=0, + widget=BooleanWidget( + label=_("Separate Container"), + description=_("Check this box to ensure a separate sample container is " + "used for this analysis service"), + ) +) + +Preservation = ReferenceField( + 'Preservation', + schemata='Container and Preservation', + allowed_types=('Preservation',), + relationship='AnalysisServicePreservation', + referenceClass=HoldingReference, + vocabulary='getPreservations', + required=0, + multiValued=0, + widget=ReferenceWidget( + checkbox_bound=0, + label=_("Default Preservation"), + description=_( + "Select a default preservation for this analysis service. If the " + "preservation depends on the sample type combination, specify a " + "preservation per sample type in the table below"), + catalog_name='bika_setup_catalog', + base_query={'inactive_state': 'active'}, + ) +) + +Container = ReferenceField( + 'Container', + schemata='Container and Preservation', + allowed_types=('Container', 'ContainerType'), + relationship='AnalysisServiceContainer', + referenceClass=HoldingReference, + vocabulary='getContainers', + required=0, + multiValued=0, + widget=ReferenceWidget( + checkbox_bound=0, + label=_("Default Container"), + description=_( + "Select the default container to be used for this analysis " + "service. If the container to be used depends on the sample type " + "and preservation combination, specify the container in the " + "sample type table below"), + catalog_name='bika_setup_catalog', + base_query={'inactive_state': 'active'}, + ) +) + +PartitionSetup = PartitionSetupField( + 'PartitionSetup', + schemata='Container and Preservation', + widget=PartitionSetupWidget( + label=PMF("Preservation per sample type"), + description=_( + "Please specify preservations that differ from the analysis " + "service's default preservation per sample type here."), + ) +) + +# Default method to be used. This field is used in Analysis Service +# Edit view, use getMethod() to retrieve the Method to be used in +# this Analysis Service. +# Gets populated with the methods selected in the multiselection +# box above or with the default instrument's method. +# Behavior controlled by js depending on ManualEntry/Instrument/Methods: +# - If InstrumentEntry checked, set instrument's default method, and readonly +# - If InstrumentEntry not checked, populate dynamically with +# selected Methods, set the first method selected and non-readonly +# See browser/js/bika.lims.analysisservice.edit.js +_Method = UIDReferenceField( + '_Method', + schemata="Method", + required=0, + searchable=True, + vocabulary_display_path_bound=sys.maxint, + allowed_types=('Method',), + vocabulary='_getAvailableMethodsDisplayList', + relationship='AnalysisServiceMethod', + referenceClass=HoldingReference, + widget=SelectionWidget( + format='select', + label=_("Default Method"), + description=_( + "If 'Allow instrument entry of results' is selected, the method " + "from the default instrument will be used. Otherwise, only the " + "methods selected above will be displayed.") + ) +) + +# Allow/Disallow to set the calculation manually +# Behavior controlled by javascript depending on Instruments field: +# - If no instruments available, hide and uncheck +# - If at least one instrument selected then checked, but not readonly +# See browser/js/bika.lims.analysisservice.edit.js +UseDefaultCalculation = BooleanField( + 'UseDefaultCalculation', + schemata="Method", + default=True, + widget=BooleanWidget( + label=_("Use default calculation"), + description=_( + "Select if the calculation to be used is the calculation set by " + "default in the default method. If unselected, the calculation " + "can be selected manually"), + ) +) + +# Default calculation to be used. This field is used in Analysis Service +# Edit view, use getCalculation() to retrieve the Calculation to be used in +# this Analysis Service. +# The default calculation is the one linked to the default method +# Behavior controlled by js depending on UseDefaultCalculation: +# - If UseDefaultCalculation is set to False, show this field +# - If UseDefaultCalculation is set to True, show this field +# See browser/js/bika.lims.analysisservice.edit.js +_Calculation = UIDReferenceField( + '_Calculation', + schemata="Method", + required=0, + vocabulary_display_path_bound=sys.maxint, + vocabulary='_getAvailableCalculationsDisplayList', + allowed_types=('Calculation',), + relationship='AnalysisServiceCalculation', + referenceClass=HoldingReference, + widget=SelectionWidget( + format='select', + label=_("Default Calculation"), + description=_( + "Default calculation to be used from the default Method selected. " + "The Calculation for a method can be assigned in the Method edit " + "view."), + catalog_name='bika_setup_catalog', + base_query={'inactive_state': 'active'}, + ) +) + +# Default calculation is not longer linked directly to the AS: it +# currently uses the calculation linked to the default Method. +# Use getCalculation() to retrieve the Calculation to be used. +# Old ASes (before 3008 upgrade) can be linked to the same Method, +# but to different Calculation objects. In that case, the Calculation +# is saved as DeferredCalculation and UseDefaultCalculation is set to +# False in the upgrade. +# Behavior controlled by js depending on UseDefaultCalculation: +# - If UseDefaultCalculation is set to False, show this field +# - If UseDefaultCalculation is set to True, show this field +# See browser/js/bika.lims.analysisservice.edit.js +# bika/lims/upgrade/to3008.py +DeferredCalculation = UIDReferenceField( + 'DeferredCalculation', + schemata="Method", + required=0, + vocabulary_display_path_bound=sys.maxint, + vocabulary='_getAvailableCalculationsDisplayList', + allowed_types=('Calculation',), + relationship='AnalysisServiceDeferredCalculation', + referenceClass=HoldingReference, + widget=SelectionWidget( + format='select', + label=_("Alternative Calculation"), + description=_( + "If required, select a calculation for the analysis here. " + "Calculations can be configured under the calculations item in " + "the LIMS set-up"), + catalog_name='bika_setup_catalog', + base_query={'inactive_state': 'active'}, + ) +) + +## XXX Keep synced to service duplication code in bika_analysisservices.py +schema = BaseAnalysisSchema.copy() + Schema(( + Separate, + Preservation, + Container, + PartitionSetup, + _Method, + UseDefaultCalculation, + _Calculation, + DeferredCalculation, )) -schema['id'].widget.visible = False -schema['description'].schemata = 'Description' -schema['description'].widget.visible = True -schema['title'].required = True -schema['title'].widget.visible = True -schema['title'].schemata = 'Description' -schema['title'].validators = () -# Update the validation layer after change the validator in runtime -schema['title']._validationLayer() -schema.moveField('ShortTitle', after='title') -schema.moveField('SortKey', after='ShortTitle') -schema.moveField('CommercialID', after='SortKey') -schema.moveField('ProtocolID', after='CommercialID') - -class AnalysisService(BaseContent, HistoryAwareMixin): +class AnalysisService(BaseAnalysis): security = ClassSecurityInfo() schema = schema displayContentsTab = False @@ -1036,368 +388,6 @@ def _renameAfterCreation(self, check_auto_id=False): return renameAfterCreation(self) - def Title(self): - return _c(self.title) - - security.declarePublic('getDiscountedPrice') - - def getDiscountedPrice(self): - """ compute discounted price excl. vat """ - price = self.getPrice() - price = price and price or 0 - discount = self.bika_setup.getMemberDiscount() - discount = discount and discount or 0 - return float(price) - (float(price) * float(discount)) / 100 - - security.declarePublic('getDiscountedBulkPrice') - - def getDiscountedBulkPrice(self): - """ compute discounted bulk discount excl. vat """ - price = self.getBulkPrice() - price = price and price or 0 - discount = self.bika_setup.getMemberDiscount() - discount = discount and discount or 0 - return float(price) - (float(price) * float(discount)) / 100 - - def getTotalPrice(self): - """ compute total price """ - price = self.getPrice() - vat = self.getVAT() - price = price and price or 0 - vat = vat and vat or 0 - return float(price) + (float(price) * float(vat)) / 100 - - def getTotalBulkPrice(self): - """ compute total price """ - price = self.getBulkPrice() - vat = self.getVAT() - price = price and price or 0 - vat = vat and vat or 0 - return float(price) + (float(price) * float(vat)) / 100 - - security.declarePublic('getTotalDiscountedPrice') - - def getTotalDiscountedPrice(self): - """ compute total discounted price """ - price = self.getDiscountedPrice() - vat = self.getVAT() - price = price and price or 0 - vat = vat and vat or 0 - return float(price) + (float(price) * float(vat)) / 100 - - security.declarePublic('getTotalDiscountedCorporatePrice') - - def getTotalDiscountedBulkPrice(self): - """ compute total discounted corporate price """ - price = self.getDiscountedCorporatePrice() - vat = self.getVAT() - price = price and price or 0 - vat = vat and vat or 0 - return float(price) + (float(price) * float(vat)) / 100 - - def getDefaultVAT(self): - """ return default VAT from bika_setup """ - try: - vat = self.bika_setup.getVAT() - return vat - except ValueError: - return "0.00" - - security.declarePublic('getVATAmount') - - def getVATAmount(self): - """ Compute VATAmount - """ - price, vat = self.getPrice(), self.getVAT() - return (float(price) * (float(vat) / 100)) - - def getAnalysisCategories(self): - bsc = getToolByName(self, 'bika_setup_catalog') - items = [(o.UID, o.Title) for o in - bsc(portal_type='AnalysisCategory', - inactive_state='active')] - o = self.getCategory() - 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)) - - def _getAvailableInstrumentsDisplayList(self): - """ Returns a DisplayList with the available Instruments - registered in Bika-Setup. Only active Instruments are - fetched. Used to fill the Instruments MultiSelectionWidget - """ - bsc = getToolByName(self, 'bika_setup_catalog') - items = [(i.UID, i.Title) \ - for i in bsc(portal_type='Instrument', - inactive_state='active')] - items.sort(lambda x, y: cmp(x[1], y[1])) - return DisplayList(list(items)) - - def _getAvailableMethodsDisplayList(self): - """ Returns a DisplayList with the available Methods - registered in Bika-Setup. Only active Methods and those - with Manual Entry field active are fetched. - Used to fill the Methods MultiSelectionWidget when 'Allow - Instrument Entry of Results is not selected'. - """ - bsc = getToolByName(self, 'bika_setup_catalog') - items = [(i.UID, i.Title) \ - for i in bsc(portal_type='Method', - inactive_state='active') \ - if i.getObject().isManualEntryOfResults()] - items.sort(lambda x, y: cmp(x[1], y[1])) - items.insert(0, ('', _("None"))) - return DisplayList(list(items)) - - def _getAvailableCalculationsDisplayList(self): - """ Returns a DisplayList with the available Calculations - registered in Bika-Setup. Only active Calculations are - fetched. Used to fill the _Calculation and DeferredCalculation - List fields - """ - bsc = getToolByName(self, 'bika_setup_catalog') - items = [(i.UID, i.Title) \ - for i in bsc(portal_type='Calculation', - inactive_state='active')] - items.sort(lambda x, y: cmp(x[1], y[1])) - items.insert(0, ('', _("None"))) - return DisplayList(list(items)) - - def getCalculation(self): - """ Returns the calculation to be used in this AS. - If UseDefaultCalculation() is set, returns the calculation - from the default method selected or none (if method hasn't - defined any calculation). If UseDefaultCalculation is set - to false, returns the Deferred Calculation (manually set) - """ - if self.getUseDefaultCalculation(): - return self.getMethod().getCalculation() \ - if (self.getMethod() \ - and self.getMethod().getCalculation()) \ - else None - else: - return self.getDeferredCalculation() - - def getMethod(self): - """ Returns the method assigned by default to the AS. - If Instrument Entry Of Results selected, returns the method - from the Default Instrument or None. - If Instrument Entry of Results is not selected, returns the - method assigned directly by the user using the _Method Field - """ - # TODO This function has been modified after enabling multiple methods - # for instruments. Make sure that returning the value of _Method field - # is correct. - method = None - if self.getInstrumentEntryOfResults(): - method = self.get_Method() - else: - methods = self.getMethods() - method = methods[0] if methods else None - return method - - def getAvailableMethods(self): - """ Returns the methods available for this analysis. - If the service has the getInstrumentEntryOfResults(), returns - the methods available from the instruments capable to perform - the service, as well as the methods set manually for the - analysis on its edit view. If getInstrumentEntryOfResults() - is unset, only the methods assigned manually to that service - are returned. - """ - methods = self.getMethods() - muids = [m.UID() for m in methods] - if self.getInstrumentEntryOfResults() == True: - # Add the methods from the instruments capable to perform - # this analysis service - for ins in self.getInstruments(): - for method in ins.getMethods(): - if method and method.UID() not in muids: - methods.append(method) - muids.append(method.UID()) - - return methods - - def getAvailableMethodsUIDs(self): - """ Returns the UIDs of the available method. - """ - return [m.UID() for m in self.getAvailableMethods()] - - def getAvailableInstruments(self): - """ Returns the instruments available for this analysis. - If the service has the getInstrumentEntryOfResults(), returns - the instruments capable to perform this service. Otherwhise, - returns an empty list. - """ - instruments = self.getInstruments() \ - if self.getInstrumentEntryOfResults() == True \ - else None - return instruments if instruments else [] - - 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)) - - def getUncertainty(self, result=None): - """ - Return the uncertainty value, if the result falls within - specified ranges for this service. - """ - - if result is None: - return None - - uncertainties = self.getUncertainties() - if uncertainties: - try: - result = float(result) - except ValueError: - # if analysis result is not a number, then we assume in range - return None - - for d in uncertainties: - if float(d['intercept_min']) <= result <= float( - d['intercept_max']): - unc = 0 - if str(d['errorvalue']).strip().endswith('%'): - try: - percvalue = float(d['errorvalue'].replace('%', '')) - except ValueError: - return None - unc = result / 100 * percvalue - else: - unc = float(d['errorvalue']) - - return unc - return None - - def getLowerDetectionLimit(self): - """ Returns the Lower Detection Limit for this service as a - floatable - """ - ldl = self.Schema().getField('LowerDetectionLimit').get(self) - try: - return float(ldl) - except ValueError: - return 0 - - def getUpperDetectionLimit(self): - """ Returns the Upper Detection Limit for this service as a - floatable - """ - udl = self.Schema().getField('UpperDetectionLimit').get(self) - try: - return float(udl) - except ValueError: - return 0 - - def getPrecision(self, result=None): - """ - Returns the precision for the Analysis Service. If the - option Calculate Precision according to Uncertainty is not - set, the method will return the precision value set in the - Schema. Otherwise, will calculate the precision value - according to the Uncertainty and the result. - If Calculate Precision to Uncertainty is set but no result - provided neither uncertainty values are set, returns the - fixed precision. - - Examples: - Uncertainty Returns - 0 1 - 0.22 1 - 1.34 0 - 0.0021 3 - 0.013 2 - 2 0 - 22 0 - - For further details, visit - https://jira.bikalabs.com/browse/LIMS-1334 - - :param result: if provided and "Calculate Precision according - to the Uncertainty" is set, the result will be - used to retrieve the uncertainty from which the - precision must be calculated. Otherwise, the - fixed-precision will be used. - :returns: the precision - """ - if self.getPrecisionFromUncertainty() == False: - return self.Schema().getField('Precision').get(self) - else: - uncertainty = self.getUncertainty(result); - if uncertainty is None: - return self.Schema().getField('Precision').get(self); - - # Calculate precision according to uncertainty - # https://jira.bikalabs.com/browse/LIMS-1334 - if uncertainty == 0: - return 1 - return get_significant_digits(uncertainty) - return None - - - def getExponentialFormatPrecision(self, result=None): - """ - Returns the precision for the Analysis Service and result - provided. Results with a precision value above this exponential - format precision should be formatted as scientific notation. - - If the Calculate Precision according to Uncertainty is not set, - the method will return the exponential precision value set in - the Schema. Otherwise, will calculate the precision value - according to the Uncertainty and the result. - - If Calculate Precision from the Uncertainty is set but no - result provided neither uncertainty values are set, returns the - fixed exponential precision. - - Will return positive values if the result is below 0 and will - return 0 or positive values if the result is above 0. - - Given an analysis service with fixed exponential format - precision of 4: - Result Uncertainty Returns - 5.234 0.22 0 - 13.5 1.34 1 - 0.0077 0.008 -3 - 32092 0.81 4 - 456021 423 5 - - For further details, visit - https://jira.bikalabs.com/browse/LIMS-1334 - - :param result: if provided and "Calculate Precision according - to the Uncertainty" is set, the result will be - used to retrieve the uncertainty from which the - precision must be calculated. Otherwise, the - fixed-precision will be used. - :returns: the precision - """ - if not result or self.getPrecisionFromUncertainty() == False: - return self.Schema().getField('ExponentialFormatPrecision').get(self) - else: - uncertainty = self.getUncertainty(result) - if uncertainty is None: - return self.Schema().getField('ExponentialFormatPrecision').get(self); - - try: - result = float(result) - except ValueError: - # if analysis result is not a number, then we assume in range - return self.Schema().getField('ExponentialFormatPrecision').get(self) - - return get_significant_digits(uncertainty) - - security.declarePublic('getContainers') def getContainers(self, instance=None): @@ -1417,61 +407,16 @@ def getPreservations(self): items.sort(lambda x, y: cmp(x[1], y[1])) return DisplayList(list(items)) - def isSelfVerificationEnabled(self): - """ - Returns if the user that submitted a result for this analysis must also - be able to verify the result - :returns: true or false - """ - bsve = self.bika_setup.getSelfVerificationEnabled() - vs = self.getSelfVerification() - return bsve if vs == -1 else vs == 1 - - def _getSelfVerificationVocabulary(self): - """ - Returns a DisplayList with the available options for the - self-verification list: 'system default', 'true', 'false' - :returns: DisplayList with the available options for the - self-verification list - """ - bsve = self.bika_setup.getSelfVerificationEnabled() - bsve = _('Yes') if bsve else _('No') - bsval = "%s (%s)" % (_("System default"), bsve) - items = [(-1, bsval), (0, _('No')), (1, _('Yes'))] - return IntDisplayList(list(items)) - - def getNumberOfRequiredVerifications(self): - """ - Returns the number of required verifications a test for this analysis - requires before being transitioned to 'verified' state - :returns: number of required verifications - """ - num = self.get_NumberOfRequiredVerifications() - if num < 1: - return self.bika_setup.getNumberOfRequiredVerifications() - return num - - def _getNumberOfRequiredVerificationsVocabulary(self): - """ - Returns a DisplayList with the available options for the - multi-verification list: 'system default', '1', '2', '3', '4' - :returns: DisplayList with the available options for the - multi-verification list - """ - bsve = self.bika_setup.getNumberOfRequiredVerifications() - bsval = "%s (%s)" % (_("System default"), str(bsve)) - items = [(-1, bsval), (1, '1'), (2, '2'), (3, '3'), (4, '4')] - return IntDisplayList(list(items)) - def workflow_script_activate(self): workflow = getToolByName(self, 'portal_workflow') pu = getToolByName(self, 'plone_utils') # A service cannot be activated if it's calculation is inactive calc = self.getCalculation() - if calc and \ - workflow.getInfoFor(calc, "inactive_state") == "inactive": - message = _("This Analysis Service cannot be activated " - "because it's calculation is inactive.") + inactive_state = workflow.getInfoFor(calc, "inactive_state") + if calc and inactive_state == "inactive": + message = _( + "This Analysis Service cannot be activated because it's " + "calculation is inactive.") pu.addPortalMessage(message, 'error') transaction.get().abort() raise WorkflowException @@ -1486,9 +431,9 @@ def workflow_scipt_deactivate(self): for calc in calculations: deps = [dep.UID() for dep in calc.getDependentServices()] if self.UID() in deps: - message = _("This Analysis Service cannot be deactivated " - "because one or more active calculations list " - "it as a dependency") + message = _( + "This Analysis Service cannot be deactivated because one " + "or more active calculations list it as a dependency") pu.addPortalMessage(message, 'error') transaction.get().abort() raise WorkflowException diff --git a/bika/lims/content/baseanalysis.py b/bika/lims/content/baseanalysis.py new file mode 100644 index 0000000000..afab6300f7 --- /dev/null +++ b/bika/lims/content/baseanalysis.py @@ -0,0 +1,1250 @@ +# -*- coding: utf-8 -*- + +# This file is part of Bika LIMS +# +# Copyright 2011-2016 by it's authors. +# Some rights reserved. See LICENSE.txt, AUTHORS.txt. + +import sys + +from AccessControl import ClassSecurityInfo +from Products.ATExtensions.ateapi import RecordsField +from Products.Archetypes.public import DisplayList, ReferenceField, \ + ComputedField, ComputedWidget, BooleanField, BooleanWidget, StringField, \ + SelectionWidget, FixedPointField, IntegerField, IntegerWidget, \ + StringWidget, BaseContent, Schema, MultiSelectionWidget, FloatField, \ + DecimalWidget +from Products.Archetypes.references import HoldingReference +from Products.Archetypes.utils import IntDisplayList +from Products.CMFCore.utils import getToolByName +from bika.lims import bikaMessageFactory as _ +from bika.lims.browser.fields import * +from bika.lims.browser.widgets.durationwidget import DurationWidget +from bika.lims.browser.widgets.recordswidget import RecordsWidget +from bika.lims.browser.widgets.referencewidget import ReferenceWidget +from bika.lims.config import ATTACHMENT_OPTIONS, SERVICE_POINT_OF_CAPTURE +from bika.lims.content.bikaschema import BikaSchema +from bika.lims.utils import to_utf8 as _c +from bika.lims.utils.analysis import get_significant_digits + +ShortTitle = StringField( + 'ShortTitle', + schemata="Description", + widget=StringWidget( + label=_("Short title"), + description=_( + "If text is entered here, it is used instead of the title when " + "the service is listed in column headings. HTML formatting is " + "allowed.") + ) +) + +SortKey = FloatField( + 'SortKey', + schemata="Description", + validators=('SortKeyValidator',), + widget=DecimalWidget( + label=_("Sort Key"), + description=_( + "Float value from 0.0 - 1000.0 indicating the sort order. " + "Duplicate values are ordered alphabetically."), + ) +) + +ScientificName = BooleanField( + 'ScientificName', + schemata="Description", + default=False, + widget=BooleanWidget( + label=_("Scientific name"), + description=_( + "If enabled, the name of the analysis will be written in italics."), + ) +) + +Unit = StringField( + 'Unit', + schemata="Description", + widget=StringWidget( + label=_("Unit"), + description=_( + "The measurement units for this analysis service' results, e.g. " + "mg/l, ppm, dB, mV, etc."), + ) +) + +Precision = IntegerField( + 'Precision', + schemata="Analysis", + widget=IntegerWidget( + label=_("Precision as number of decimals"), + description=_( + "Define the number of decimals to be used for this result."), + ) +) + +ExponentialFormatPrecision = IntegerField( + 'ExponentialFormatPrecision', + schemata="Analysis", + default=7, + widget=IntegerWidget( + label=_("Exponential format precision"), + description=_( + "Define the precision when converting values to exponent " + "notation. The default is 7."), + ) +) + +LowerDetectionLimit = FixedPointField( + 'LowerDetectionLimit', + schemata="Analysis", + default='0.0', + precision=7, + widget=DecimalWidget( + label=_("Lower Detection Limit (LDL)"), + description=_( + "The Lower Detection Limit is the lowest value to which the " + "measured parameter can be measured using the specified testing " + "methodology. Results entered which are less than this value will " + "be reported as < LDL") + ) +) + +UpperDetectionLimit = FixedPointField( + 'UpperDetectionLimit', + schemata="Analysis", + default='1000000000.0', + precision=7, + widget=DecimalWidget( + label=_("Upper Detection Limit (UDL)"), + description=_( + "The Upper Detection Limit is the highest value to which the " + "measured parameter can be measured using the specified testing " + "methodology. Results entered which are greater than this value " + "will be reported as > UDL") + ) +) + +# LIMS-1775 Allow to select LDL or UDL defaults in results with readonly mode +# https://jira.bikalabs.com/browse/LIMS-1775 +# Some behavior controlled with javascript: If checked, the field +# "AllowManualDetectionLimit" will be displayed. +# See browser/js/bika.lims.analysisservice.edit.js +# +# Use cases: +# a) If "DetectionLimitSelector" is enabled and +# "AllowManualDetectionLimit" is enabled too, then: +# the analyst will be able to select an '>', '<' operand from the +# selection list and also set the LD manually. +# +# b) If "DetectionLimitSelector" is enabled and +# "AllowManualDetectionLimit" is unchecked, the analyst will be +# able to select an operator from the selection list, but not set +# the LD manually: the default LD will be displayed in the result +# field as usuall, but in read-only mode. +# +# c) If "DetectionLimitSelector" is disabled, no LD selector will be +# displayed in the results table. +DetectionLimitSelector = BooleanField( + 'DetectionLimitSelector', + schemata="Analysis", + default=False, + widget=BooleanWidget( + label=_("Display a Detection Limit selector"), + description=_( + "If checked, a selection list will be displayed next to the " + "analysis' result field in results entry views. By using this " + "selector, the analyst will be able to set the value as a " + "Detection Limit (LDL or UDL) instead of a regular result"), + ) +) + +# Behavior controlled with javascript: Only visible when the +# "DetectionLimitSelector" is checked +# See browser/js/bika.lims.analysisservice.edit.js +# Check inline comment for "DetecionLimitSelector" field for +# further information. +AllowManualDetectionLimit = BooleanField( + 'AllowManualDetectionLimit', + schemata="Analysis", + default=False, + widget=BooleanWidget( + label=_("Allow Manual Detection Limit input"), + description=_( + "Allow the analyst to manually replace the default Detection " + "Limits (LDL and UDL) on results entry views"), + ) +) + +ReportDryMatter = BooleanField( + 'ReportDryMatter', + schemata="Analysis", + default=False, + widget=BooleanWidget( + label=_("Report as Dry Matter"), + description=_("These results can be reported as dry matter"), + ) +) + +AttachmentOption = StringField( + 'AttachmentOption', + schemata="Analysis", + default='p', + vocabulary=ATTACHMENT_OPTIONS, + widget=SelectionWidget( + label=_("Attachment Option"), + description=_( + "Indicates whether file attachments, e.g. microscope images, " + "are required for this analysis and whether file upload function " + "will be available for it on data capturing screens"), + format='select', + ) +) + +Keyword = StringField( + 'Keyword', + schemata="Description", + required=1, + searchable=True, + validators=('servicekeywordvalidator',), + widget=StringWidget( + label=_("Analysis Keyword"), + description=_( + "The unique keyword used to identify the analysis service in " + "import files of bulk AR requests and results imports from " + "instruments. It is also used to identify dependent analysis " + "services in user defined results calculations"), + ) +) + +# Allow/Disallow manual entry of results +# Behavior controlled by javascript depending on Instruments field: +# - If InstrumentEntry not checked, set checked and readonly +# - If InstrumentEntry checked, set as not readonly +# See browser/js/bika.lims.analysisservice.edit.js +ManualEntryOfResults = BooleanField( + 'ManualEntryOfResults', + schemata="Method", + default=True, + widget=BooleanWidget( + label=_("Instrument assignment is not required"), + description=_( + "Select if the results for tests of this type of analysis can be " + "set manually. If selected, the user will be able to set a result " + "for a test of this type of analysis in manage results view " + "without the need of selecting an instrument, even though the " + "method selected for the test has instruments assigned."), + ) +) + +# Allow/Disallow instrument entry of results +# Behavior controlled by javascript depending on Instruments field: +# - If no instruments available, hide and uncheck +# - If at least one instrument selected, checked, but not readonly +# See browser/js/bika.lims.analysisservice.edit.js +InstrumentEntryOfResults = BooleanField( + 'InstrumentEntryOfResults', + schemata="Method", + default=False, + widget=BooleanWidget( + label=_("Instrument assignment is allowed"), + description=_( + "Select if the results for tests of this type of analysis can be " + "set by using an instrument. If disabled, no instruments will be " + "available for tests of this type of analysis in manage results " + "view, even though the method selected for the test has " + "instruments assigned."), + ) +) + +# Instruments associated to the AS +# List of instruments capable to perform the Analysis Service. The +# Instruments selected here are displayed in the Analysis Request +# Add view, closer to this Analysis Service if selected. +# - If InstrumentEntry not checked, hide and unset +# - If InstrumentEntry checked, set the first selected and show +Instruments = UIDReferenceField( + 'Instruments', + schemata="Method", + required=0, + multiValued=1, + vocabulary_display_path_bound=sys.maxint, + vocabulary='_getAvailableInstrumentsDisplayList', + allowed_types=('Instrument',), + relationship='AnalysisServiceInstruments', + referenceClass=HoldingReference, + widget=MultiSelectionWidget( + label=_("Instruments"), + description=_( + "More than one instrument can be used in a test of this type of " + "analysis. A selection list with the instruments selected here is " + "populated in the results manage view for each test of this type " + "of analysis. The available instruments in the selection list " + "will change in accordance with the method selected by the user " + "for that test in the manage results view. Although a method can " + "have more than one instrument assigned, the selection list is " + "only populated with the instruments that are both set here and " + "allowed for the selected method."), + ) +) + +# Default instrument to be used. +# Gets populated with the instruments selected in the multiselection +# box above. +# Behavior controlled by js depending on ManualEntry/Instruments: +# - Populate dynamically with selected Instruments +# - If InstrumentEntry checked, set first selected instrument +# - If InstrumentEntry not checked, hide and set None +# See browser/js/bika.lims.analysisservice.edit.js +Instrument = HistoryAwareReferenceField( + 'Instrument', + schemata="Method", + searchable=True, + required=0, + vocabulary_display_path_bound=sys.maxint, + vocabulary='_getAvailableInstrumentsDisplayList', + allowed_types=('Instrument',), + relationship='AnalysisServiceInstrument', + referenceClass=HoldingReference, + widget=SelectionWidget( + format='select', + label=_("Default Instrument"), + description=_( + "This is the instrument the system will assign by default to " + "tests from this type of analysis in manage results view. The " + "method associated to this instrument will be assigned as the " + "default method too.Note the instrument's method will prevail " + "over any of the methods choosen if the 'Instrument assignment is " + "not required' option is enabled.") + ) +) + +# Returns the Default's instrument title. If no default instrument +# set, returns string.empty +InstrumentTitle = ComputedField( + 'InstrumentTitle', + expression="" + "context.getInstrument() and context.getInstrument().Title() or ''", + widget=ComputedWidget( + visible=False, + ) +) + +# Manual methods associated to the AS +# List of methods capable to perform the Analysis Service. The +# Methods selected here are displayed in the Analysis Request +# Add view, closer to this Analysis Service if selected. +# Use getAvailableMethods() to retrieve the list with methods both +# from selected instruments and manually entered. +# Behavior controlled by js depending on ManualEntry/Instrument: +# - If InsrtumentEntry not checked, show +# See browser/js/bika.lims.analysisservice.edit.js +Methods = UIDReferenceField( + 'Methods', + schemata="Method", + required=0, + multiValued=1, + vocabulary_display_path_bound=sys.maxint, + vocabulary='_getAvailableMethodsDisplayList', + allowed_types=('Method',), + relationship='AnalysisServiceMethods', + referenceClass=HoldingReference, + widget=MultiSelectionWidget( + label=_("Methods"), + description=_( + "The tests of this type of analysis can be performed by using " + "more than one method with the 'Manual entry of results' option " + "enabled. A selection list with the methods selected here is " + "populated in the manage results view for each test of this type " + "of analysis. Note that only methods with 'Allow manual entry' " + "option enabled are displayed here; if you want the user to be " + "able to assign a method that requires instrument entry, enable " + "the 'Instrument assignment is allowed' option."), + ) +) + +CalculationTitle = ComputedField( + 'CalculationTitle', + expression="" + "context.getCalculation() and context.getCalculation().Title() or ''", + searchable=True, + widget=ComputedWidget( + visible=False, + ) +) + +CalculationUID = ComputedField( + 'CalculationUID', + expression="" + "context.getCalculation() and context.getCalculation().UID() or ''", + widget=ComputedWidget( + visible=False, + ) +) + +InterimFields = InterimFieldsField( + 'InterimFields', + schemata='Method', + widget=RecordsWidget( + label=_("Calculation Interim Fields"), + description=_( + "Values can be entered here which will override the defaults " + "specified in the Calculation Interim Fields."), + ) +) + +MaxTimeAllowed = DurationField( + 'MaxTimeAllowed', + schemata="Analysis", + widget=DurationWidget( + label=_("Maximum turn-around time"), + description=_( + "Maximum time allowed for completion of the analysis. A late " + "analysis alert is raised when this period elapses"), + ) +) + +DuplicateVariation = FixedPointField( + 'DuplicateVariation', + schemata="Method", + widget=DecimalWidget( + label=_("Duplicate Variation %"), + description=_( + "When the results of duplicate analyses on worksheets, carried " + "out on the same sample, differ with more than this percentage, " + "an alert is raised"), + ) +) + +Accredited = BooleanField( + 'Accredited', + schemata="Method", + default=False, + widget=BooleanWidget( + label=_("Accredited"), + description=_( + "Check this box if the analysis service is included in the " + "laboratory's schedule of accredited analyses"), + ) +) + +PointOfCapture = StringField( + 'PointOfCapture', + schemata="Description", + required=1, + default='lab', + vocabulary=SERVICE_POINT_OF_CAPTURE, + widget=SelectionWidget( + format='flex', + label=_("Point of Capture"), + description=_( + "The results of field analyses are captured during sampling at " + "the sample point, e.g. the temperature of a water sample in the " + "river where it is sampled. Lab analyses are done in the " + "laboratory"), + ) +) + +Category = UIDReferenceField( + 'Category', + schemata="Description", + required=1, + vocabulary_display_path_bound=sys.maxint, + allowed_types=('AnalysisCategory',), + relationship='AnalysisServiceAnalysisCategory', + referenceClass=HoldingReference, + vocabulary='getAnalysisCategories', + widget=ReferenceWidget( + checkbox_bound=0, + label=_("Analysis Category"), + description=_("The category the analysis service belongs to"), + catalog_name='bika_setup_catalog', + base_query={'inactive_state': 'active'}, + ) +) + +CategoryTitle = ComputedField( + 'CategoryTitle', + expression="" + "context.getCategory() and context.getCategory().Title() or ''", + widget=ComputedWidget( + visible=False, + ) +) + +CategoryUID = ComputedField( + 'CategoryUID', + expression="" + "context.getCategory() and context.getCategory().UID() or ''", + widget=ComputedWidget( + visible=False, + ) +) + +Price = FixedPointField( + 'Price', + schemata="Description", + default='0.00', + widget=DecimalWidget( + label=_("Price (excluding VAT)"), + ) +) + +# read access permission +BulkPrice = FixedPointField( + 'BulkPrice', + schemata="Description", + default='0.00', + widget=DecimalWidget( + label=_("Bulk price (excluding VAT)"), + description=_( + "The price charged per analysis for clients who qualify for bulk " + "discounts"), + ) +) + +VATAmount = ComputedField( + 'VATAmount', + schemata="Description", + expression='context.getVATAmount()', + widget=ComputedWidget( + label=_("VAT"), + visible={'edit': 'hidden', } + ) +) + +TotalPrice = ComputedField( + 'TotalPrice', + schemata="Description", + expression='context.getTotalPrice()', + widget=ComputedWidget( + label=_("Total price"), + visible={'edit': 'hidden', } + ) +) + +VAT = FixedPointField( + 'VAT', + schemata="Description", + default_method='getDefaultVAT', + widget=DecimalWidget( + label=_("VAT %"), + description=_("Enter percentage value eg. 14.0"), + ) +) + +Department = UIDReferenceField( + 'Department', + schemata="Description", + required=0, + vocabulary_display_path_bound=sys.maxint, + allowed_types=('Department',), + vocabulary='getDepartments', + relationship='AnalysisServiceDepartment', + referenceClass=HoldingReference, + widget=ReferenceWidget( + checkbox_bound=0, + label=_("Department"), + description=_("The laboratory department"), + catalog_name='bika_setup_catalog', + base_query={'inactive_state': 'active'}, + ) +) + +DepartmentTitle = ComputedField( + 'DepartmentTitle', + expression="" + "context.getDepartment() and context.getDepartment().Title() or ''", + searchable=True, + widget=ComputedWidget( + visible=False, + ) +) + +Uncertainties = RecordsField( + 'Uncertainties', + schemata="Uncertainties", + type='uncertainties', + subfields=('intercept_min', 'intercept_max', 'errorvalue'), + required_subfields=( + 'intercept_min', 'intercept_max', 'errorvalue'), + subfield_sizes={'intercept_min': 10, + 'intercept_max': 10, + 'errorvalue': 10, + }, + subfield_labels={'intercept_min': _('Range min'), + 'intercept_max': _('Range max'), + 'errorvalue': _('Uncertainty value'), + }, + subfield_validators={'intercept_min': 'uncertainties_validator', + 'intercept_max': 'uncertainties_validator', + 'errorvalue': 'uncertainties_validator', + }, + widget=RecordsWidget( + label=_("Uncertainty"), + description=_( + "Specify the uncertainty value for a given range, e.g. for " + "results in a range with minimum of 0 and maximum of 10, " + "where the uncertainty value is 0.5 - a result of 6.67 will be " + "reported as 6.67 +- 0.5. You can also specify the uncertainty " + "value as a percentage of the result value, by adding a '%' to " + "the value entered in the 'Uncertainty Value' column, e.g. for " + "results in a range with minimum of 10.01 and a maximum of 100, " + "where the uncertainty value is 2% - a result of 100 will be " + "reported as 100 +- 2. Please ensure successive ranges are " + "continuous, e.g. 0.00 - 10.00 is followed by 10.01 - 20.00, " + "20.01 - 30 .00 etc."), + ) +) + +# Calculate the precision from Uncertainty value +# Behavior controlled by javascript +# - If checked, Precision and ExponentialFormatPrecision are not displayed. +# The precision will be calculated according to the uncertainty. +# - If checked, Precision and ExponentialFormatPrecision will be displayed. +# See browser/js/bika.lims.analysisservice.edit.js +PrecisionFromUncertainty = BooleanField( + 'PrecisionFromUncertainty', + schemata="Uncertainties", + default=False, + widget=BooleanWidget( + label=_("Calculate Precision from Uncertainties"), + description=_( + "Precision as the number of significant digits according to the " + "uncertainty. The decimal position will be given by the first " + "number different from zero in the uncertainty, at that position " + "the system will round up the uncertainty and results. For " + "example, with a result of 5.243 and an uncertainty of 0.22, " + "the system will display correctly as 5.2+-0.2. If no uncertainty " + "range is set for the result, the system will use the fixed " + "precision set."), + ) +) + +# If checked, an additional input with the default uncertainty will +# be displayed in the manage results view. The value set by the user +# in this field will override the default uncertainty for the analysis +# result +AllowManualUncertainty = BooleanField( + 'AllowManualUncertainty', + schemata="Uncertainties", + default=False, + widget=BooleanWidget( + label=_("Allow manual uncertainty value input"), + description=_( + "Allow the analyst to manually replace the default uncertainty " + "value."), + ) +) + +ResultOptions = RecordsField( + 'ResultOptions', + schemata="Result Options", + type='resultsoptions', + subfields=('ResultValue', 'ResultText'), + required_subfields=('ResultValue', 'ResultText'), + subfield_labels={'ResultValue': _('Result Value'), + 'ResultText': _('Display Value'), }, + subfield_validators={'ResultValue': 'resultoptionsvalidator', + 'ResultText': 'resultoptionsvalidator'}, + subfield_sizes={'ResultValue': 5, + 'ResultText': 25, + }, + widget=RecordsWidget( + label=_("Result Options"), + description=_( + "Please list all options for the analysis result if you want to " + "restrict it to specific options only, e.g. 'Positive', " + "'Negative' and 'Indeterminable'. The option's result value must " + "be a number"), + ) +) + +Hidden = BooleanField( + 'Hidden', + schemata="Analysis", + default=False, + widget=BooleanWidget( + label=_("Hidden"), + description=_( + "If enabled, this analysis and its results will not be displayed " + "by default in reports. This setting can be overrided in Analysis " + "Profile and/or Analysis Request"), + ) +) + +SelfVerification = IntegerField( + 'SelfVerification', + schemata="Analysis", + default=-1, + vocabulary="_getSelfVerificationVocabulary", + widget=SelectionWidget( + label=_("Self-verification of results"), + description=_( + "If enabled, a user who submitted a result for this analysis " + "will also be able to verify it. This setting take effect for " + "those users with a role assigned that allows them to verify " + "results (by default, managers, labmanagers and verifiers). " + "The option set here has priority over the option set in Bika " + "Setup"), + format="select", + ) +) + +_NumberOfRequiredVerifications = IntegerField( + '_NumberOfRequiredVerifications', + schemata="Analysis", + default=-1, + vocabulary="_getNumberOfRequiredVerificationsVocabulary", + widget=SelectionWidget( + label=_("Number of required verifications"), + description=_( + "Number of required verifications from different users with " + "enough privileges before a given result for this analysis " + "being considered as 'verified'. The option set here has " + "priority over the option set in Bika Setup"), + format="select", + ) +) + +CommercialID = StringField( + 'CommercialID', + searchable=1, + schemata='Description', + required=0, + widget=StringWidget( + label=_("Commercial ID"), + description=_("The service's commercial ID for accounting purposes") + ) +) + +ProtocolID = StringField( + 'ProtocolID', + searchable=1, + schemata='Description', + required=0, + widget=StringWidget( + label=_("Protocol ID"), + description=_("The service's analytical protocol ID") + ) +) + +# XXX Keep synced to service duplication code in bika_analysisservices.py + +schema = BikaSchema.copy() + Schema(( + ShortTitle, + SortKey, + ScientificName, + Unit, + Precision, + ExponentialFormatPrecision, + LowerDetectionLimit, + UpperDetectionLimit, + DetectionLimitSelector, + AllowManualDetectionLimit, + ReportDryMatter, + AttachmentOption, + Keyword, + ManualEntryOfResults, + InstrumentEntryOfResults, + Instruments, + Instrument, + InstrumentTitle, + Methods, + CalculationTitle, + CalculationUID, + InterimFields, + MaxTimeAllowed, + DuplicateVariation, + Accredited, + PointOfCapture, + Category, + Price, + BulkPrice, + VATAmount, + TotalPrice, + VAT, + CategoryTitle, + CategoryUID, + Department, + DepartmentTitle, + Uncertainties, + PrecisionFromUncertainty, + AllowManualUncertainty, + ResultOptions, + Hidden, + SelfVerification, + _NumberOfRequiredVerifications, + CommercialID, + ProtocolID, +)) + +schema['id'].widget.visible = False +schema['description'].schemata = 'Description' +schema['description'].widget.visible = True +schema['title'].required = True +schema['title'].widget.visible = True +schema['title'].schemata = 'Description' +schema['title'].validators = () +# Update the validation layer after change the validator in runtime +schema['title']._validationLayer() +schema.moveField('ShortTitle', after='title') +schema.moveField('SortKey', after='ShortTitle') +schema.moveField('CommercialID', after='SortKey') +schema.moveField('ProtocolID', after='CommercialID') + + +class BaseAnalysis(BaseContent): + security = ClassSecurityInfo() + schema = schema + displayContentsTab = False + + security.declarePublic('Title') + + def Title(self): + return _c(self.title) + + security.declarePublic('getDiscountedPrice') + + def getDiscountedPrice(self): + """ compute discounted price excl. vat """ + price = self.getPrice() + price = price and price or 0 + discount = self.bika_setup.getMemberDiscount() + discount = discount and discount or 0 + return float(price) - (float(price) * float(discount)) / 100 + + security.declarePublic('getDiscountedBulkPrice') + + def getDiscountedBulkPrice(self): + """ compute discounted bulk discount excl. vat """ + price = self.getBulkPrice() + price = price and price or 0 + discount = self.bika_setup.getMemberDiscount() + discount = discount and discount or 0 + return float(price) - (float(price) * float(discount)) / 100 + + security.declarePublic('getTotalPrice') + + def getTotalPrice(self): + """ compute total price """ + price = self.getPrice() + vat = self.getVAT() + price = price and price or 0 + vat = vat and vat or 0 + return float(price) + (float(price) * float(vat)) / 100 + + security.declarePublic('getTotalBulkPrice') + + def getTotalBulkPrice(self): + """ compute total price """ + price = self.getBulkPrice() + vat = self.getVAT() + price = price and price or 0 + vat = vat and vat or 0 + return float(price) + (float(price) * float(vat)) / 100 + + security.declarePublic('getTotalDiscountedPrice') + + def getTotalDiscountedPrice(self): + """ compute total discounted price """ + price = self.getDiscountedPrice() + vat = self.getVAT() + price = price and price or 0 + vat = vat and vat or 0 + return float(price) + (float(price) * float(vat)) / 100 + + security.declarePublic('getTotalDiscountedCorporatePrice') + + def getTotalDiscountedBulkPrice(self): + """ compute total discounted corporate price """ + price = self.getDiscountedCorporatePrice() + vat = self.getVAT() + price = price and price or 0 + vat = vat and vat or 0 + return float(price) + (float(price) * float(vat)) / 100 + + security.declarePublic('getDefaultVAT') + + def getDefaultVAT(self): + """ return default VAT from bika_setup """ + try: + vat = self.bika_setup.getVAT() + return vat + except ValueError: + return "0.00" + + security.declarePublic('getVATAmount') + + def getVATAmount(self): + """ Compute VATAmount + """ + price, vat = self.getPrice(), self.getVAT() + return float(price) * (float(vat) / 100) + + security.declarePublic('getAnalysisCategories') + + def getAnalysisCategories(self): + bsc = getToolByName(self, 'bika_setup_catalog') + items = [(o.UID, o.Title) for o in + bsc(portal_type='AnalysisCategory', + inactive_state='active')] + o = self.getCategory() + 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)) + + security.declarePublic('_getAvailableInstrumentsDisplayList') + + def _getAvailableInstrumentsDisplayList(self): + """ Returns a DisplayList with the available Instruments + registered in Bika-Setup. Only active Instruments are + fetched. Used to fill the Instruments MultiSelectionWidget + """ + bsc = getToolByName(self, 'bika_setup_catalog') + items = [(i.UID, i.Title) + for i in bsc(portal_type='Instrument', + inactive_state='active')] + items.sort(lambda x, y: cmp(x[1], y[1])) + return DisplayList(list(items)) + + security.declarePublic('_getAvailableMethodsDisplayList') + + def _getAvailableMethodsDisplayList(self): + """ Returns a DisplayList with the available Methods + registered in Bika-Setup. Only active Methods and those + with Manual Entry field active are fetched. + Used to fill the Methods MultiSelectionWidget when 'Allow + Instrument Entry of Results is not selected'. + """ + bsc = getToolByName(self, 'bika_setup_catalog') + items = [(i.UID, i.Title) + for i in bsc(portal_type='Method', + inactive_state='active') + if i.getObject().isManualEntryOfResults()] + items.sort(lambda x, y: cmp(x[1], y[1])) + items.insert(0, ('', _("None"))) + return DisplayList(list(items)) + + security.declarePublic('_getAvailableCalculationsDisplayList') + + def _getAvailableCalculationsDisplayList(self): + """ Returns a DisplayList with the available Calculations + registered in Bika-Setup. Only active Calculations are + fetched. Used to fill the _Calculation and DeferredCalculation + List fields + """ + bsc = getToolByName(self, 'bika_setup_catalog') + items = [(i.UID, i.Title) + for i in bsc(portal_type='Calculation', + inactive_state='active')] + items.sort(lambda x, y: cmp(x[1], y[1])) + items.insert(0, ('', _("None"))) + return DisplayList(list(items)) + + security.declarePublic('getCalculation') + + def getCalculation(self): + """ Returns the calculation to be used in this AS. + If UseDefaultCalculation() is set, returns the calculation + from the default method selected or none (if method hasn't + defined any calculation). If UseDefaultCalculation is set + to false, returns the Deferred Calculation (manually set) + """ + if self.getUseDefaultCalculation(): + method = self.getMethod() + if method: + calculation = method.getCalculation() + return calculation if calculation else None + else: + return self.getDeferredCalculation() + + security.declarePublic('getMethod') + + def getMethod(self): + """ Returns the method assigned by default to the AS. + If Instrument Entry Of Results selected, returns the method + from the Default Instrument or None. + If Instrument Entry of Results is not selected, returns the + method assigned directly by the user using the _Method Field + """ + # TODO This function has been modified after enabling multiple methods + # for instruments. Make sure that returning the value of _Method field + # is correct. + if self.getInstrumentEntryOfResults(): + method = self.get_Method() + else: + methods = self.getMethods() + method = methods[0] if methods else None + return method + + security.declarePublic('getAvailableMethods') + + def getAvailableMethods(self): + """ Returns the methods available for this analysis. + If the service has the getInstrumentEntryOfResults(), returns + the methods available from the instruments capable to perform + the service, as well as the methods set manually for the + analysis on its edit view. If getInstrumentEntryOfResults() + is unset, only the methods assigned manually to that service + are returned. + """ + methods = self.getMethods() + muids = [m.UID() for m in methods] + if self.getInstrumentEntryOfResults(): + # Add the methods from the instruments capable to perform + # this analysis service + for ins in self.getInstruments(): + for method in ins.getMethods(): + if method and method.UID() not in muids: + methods.append(method) + muids.append(method.UID()) + + return methods + + security.declarePublic('getAvailableMethodsUIDs') + + def getAvailableMethodsUIDs(self): + """ Returns the UIDs of the available method. + """ + return [m.UID() for m in self.getAvailableMethods()] + + security.declarePublic('getAvailableInstruments') + + def getAvailableInstruments(self): + """ Returns the instruments available for this analysis. + If the service has the getInstrumentEntryOfResults(), returns + the instruments capable to perform this service. Otherwhise, + returns an empty list. + """ + instruments = self.getInstruments() \ + if self.getInstrumentEntryOfResults() is True \ + else None + return instruments if instruments else [] + + security.declarePublic('getDepartments') + + 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)) + + security.declarePublic('getUncertainty') + + def getUncertainty(self, result=None): + """ + Return the uncertainty value, if the result falls within + specified ranges for this service. + """ + + if result is None: + return None + + uncertainties = self.getUncertainties() + if uncertainties: + try: + result = float(result) + except ValueError: + # if analysis result is not a number, then we assume in range + return None + + for d in uncertainties: + if float(d['intercept_min']) <= result <= float( + d['intercept_max']): + if str(d['errorvalue']).strip().endswith('%'): + try: + percvalue = float(d['errorvalue'].replace('%', '')) + except ValueError: + return None + unc = result / 100 * percvalue + else: + unc = float(d['errorvalue']) + + return unc + return None + + security.declarePublic('getLowerDetectionLimit') + + def getLowerDetectionLimit(self): + """ Returns the Lower Detection Limit for this service as a + floatable + """ + ldl = self.Schema().getField('LowerDetectionLimit').get(self) + try: + return float(ldl) + except ValueError: + return 0 + + security.declarePublic('getUpperDetectionLimit') + + def getUpperDetectionLimit(self): + """ Returns the Upper Detection Limit for this service as a + floatable + """ + udl = self.Schema().getField('UpperDetectionLimit').get(self) + try: + return float(udl) + except ValueError: + return 0 + + security.declarePublic('getPrecision') + + def getPrecision(self, result=None): + """ + Returns the precision for the Analysis Service. If the + option Calculate Precision according to Uncertainty is not + set, the method will return the precision value set in the + Schema. Otherwise, will calculate the precision value + according to the Uncertainty and the result. + If Calculate Precision to Uncertainty is set but no result + provided neither uncertainty values are set, returns the + fixed precision. + + Examples: + Uncertainty Returns + 0 1 + 0.22 1 + 1.34 0 + 0.0021 3 + 0.013 2 + 2 0 + 22 0 + + For further details, visit + https://jira.bikalabs.com/browse/LIMS-1334 + + :param result: if provided and "Calculate Precision according + to the Uncertainty" is set, the result will be + used to retrieve the uncertainty from which the + precision must be calculated. Otherwise, the + fixed-precision will be used. + :returns: the precision + """ + if not self.getPrecisionFromUncertainty(): + return self.Schema().getField('Precision').get(self) + else: + uncertainty = self.getUncertainty(result) + if uncertainty is None: + return self.Schema().getField('Precision').get(self) + + # Calculate precision according to uncertainty + # https://jira.bikalabs.com/browse/LIMS-1334 + if uncertainty == 0: + return 1 + return get_significant_digits(uncertainty) + + security.declarePublic('getExponentialFormatPrecision') + + def getExponentialFormatPrecision(self, result=None): + """ + Returns the precision for the Analysis Service and result + provided. Results with a precision value above this exponential + format precision should be formatted as scientific notation. + + If the Calculate Precision according to Uncertainty is not set, + the method will return the exponential precision value set in + the Schema. Otherwise, will calculate the precision value + according to the Uncertainty and the result. + + If Calculate Precision from the Uncertainty is set but no + result provided neither uncertainty values are set, returns the + fixed exponential precision. + + Will return positive values if the result is below 0 and will + return 0 or positive values if the result is above 0. + + Given an analysis service with fixed exponential format + precision of 4: + Result Uncertainty Returns + 5.234 0.22 0 + 13.5 1.34 1 + 0.0077 0.008 -3 + 32092 0.81 4 + 456021 423 5 + + For further details, visit + https://jira.bikalabs.com/browse/LIMS-1334 + + :param result: if provided and "Calculate Precision according + to the Uncertainty" is set, the result will be + used to retrieve the uncertainty from which the + precision must be calculated. Otherwise, the + fixed-precision will be used. + :returns: the precision + """ + field = self.Schema().getField('ExponentialFormatPrecision') + if not result or self.getPrecisionFromUncertainty() is False: + return field.get(self) + else: + uncertainty = self.getUncertainty(result) + if uncertainty is None: + return field.get(self) + + try: + float(result) + except ValueError: + # if analysis result is not a number, then we assume in range + return field.get(self) + + return get_significant_digits(uncertainty) + + security.declarePublic('isSelfVerificationEnabled') + + def isSelfVerificationEnabled(self): + """ + Returns if the user that submitted a result for this analysis must also + be able to verify the result + :returns: true or false + """ + bsve = self.bika_setup.getSelfVerificationEnabled() + vs = self.getSelfVerification() + return bsve if vs == -1 else vs == 1 + + security.declarePublic('_getSelfVerificationVocabulary') + + def _getSelfVerificationVocabulary(self): + """ + Returns a DisplayList with the available options for the + self-verification list: 'system default', 'true', 'false' + :returns: DisplayList with the available options for the + self-verification list + """ + bsve = self.bika_setup.getSelfVerificationEnabled() + bsve = _('Yes') if bsve else _('No') + bsval = "%s (%s)" % (_("System default"), bsve) + items = [(-1, bsval), (0, _('No')), (1, _('Yes'))] + return IntDisplayList(list(items)) + + security.declarePublic('getNumberOfRequiredVerifications') + + def getNumberOfRequiredVerifications(self): + """ + Returns the number of required verifications a test for this analysis + requires before being transitioned to 'verified' state + :returns: number of required verifications + """ + num = self.get_NumberOfRequiredVerifications() + if num < 1: + return self.bika_setup.getNumberOfRequiredVerifications() + return num + + security.declarePublic('_getNumberOfRequiredVerificationsVocabulary') + + def _getNumberOfRequiredVerificationsVocabulary(self): + """ + Returns a DisplayList with the available options for the + multi-verification list: 'system default', '1', '2', '3', '4' + :returns: DisplayList with the available options for the + multi-verification list + """ + bsve = self.bika_setup.getNumberOfRequiredVerifications() + bsval = "%s (%s)" % (_("System default"), str(bsve)) + items = [(-1, bsval), (1, '1'), (2, '2'), (3, '3'), (4, '4')] + return IntDisplayList(list(items)) diff --git a/bika/lims/utils/analysis.py b/bika/lims/utils/analysis.py index ceaf129ed5..f98c311e50 100644 --- a/bika/lims/utils/analysis.py +++ b/bika/lims/utils/analysis.py @@ -8,7 +8,7 @@ import math import zope.event -from bika.lims import bikaMessageFactory as _ +from bika.lims import bikaMessageFactory as _, logger from bika.lims.utils import formatDecimalMark from Products.Archetypes.event import ObjectInitializedEvent from Products.CMFCore.WorkflowCore import WorkflowException @@ -61,7 +61,11 @@ def duplicateAnalysis(base): return analysis -def create_analysis(context, service, keyword, interim_fields): +def create_analysis(context, source): + """Create a new Analysis. The source can be an Analysis Service or + an existing Analysis, and all possible field values will be set to the + values found in the source object. + """ # Determine if the sampling workflow is enabled workflow_enabled = context.bika_setup.getSamplingWorkflowEnabled() # Create the analysis @@ -74,13 +78,14 @@ def create_analysis(context, service, keyword, interim_fields): # CMFCore.TypesTool._constructInstance # And lives here: # https://github.com/zopefoundation/Products.CMFCore/blob/2.2/Products/CMFCore/TypesTool.py#L535 - analysis = _createObjectByType( - "Analysis", - context, - keyword, - Service=service) - analysis.setInterimFields(interim_fields) - analysis.setMaxTimeAllowed(service.getMaxTimeAllowed()) + analysis = _createObjectByType("Analysis", context, source.getKeyword()) + src_schema = source.getSchema() + dst_schema = analysis.getSchema() + for field in src_schema.fields(): + fieldname = field.getName() + if fieldname in dst_schema: + value = field.get(source) + dst_schema[fieldname].set(analysis, value) # unmarkCreationFlag also reindex the object analysis.unmarkCreationFlag() # Trigger the intitialization event of the new object From 8141eeccf7df720c0541c7fac703d4eedc521672 Mon Sep 17 00:00:00 2001 From: Campbell Date: Fri, 5 May 2017 12:40:50 +0200 Subject: [PATCH 02/36] Add UIDReferenceField This is a very simple uid-based reference field which stores a UID or a list of UIDS as strings in the zodb, and reconstitutes them to objects when retrieved. It does not support backreferences, or link integrity (holdingreference) but it also does not require the overhead of AT References which is prohibitive for our use-case. --- bika/lims/browser/fields/__init__.py | 3 +- bika/lims/browser/fields/uidreferencefield.py | 112 +++++++ bika/lims/content/analysis.py | 29 +- bika/lims/content/analysisservice.py | 122 +++++++- bika/lims/content/baseanalysis.py | 125 +------- bika/lims/content/duplicateanalysis.py | 281 +++++++++++------- bika/lims/interfaces/field.py | 11 + bika/lims/utils/analysis.py | 14 +- 8 files changed, 424 insertions(+), 273 deletions(-) create mode 100644 bika/lims/browser/fields/uidreferencefield.py create mode 100644 bika/lims/interfaces/field.py diff --git a/bika/lims/browser/fields/__init__.py b/bika/lims/browser/fields/__init__.py index 95f09b8a2f..1a9d411503 100644 --- a/bika/lims/browser/fields/__init__.py +++ b/bika/lims/browser/fields/__init__.py @@ -11,4 +11,5 @@ from .referenceresultsfield import ReferenceResultsField from .historyawarereferencefield import HistoryAwareReferenceField from .coordinatefield import CoordinateField -from reflexrulefield import ReflexRuleField +from .reflexrulefield import ReflexRuleField +from .uidreferencefield import UIDReferenceField diff --git a/bika/lims/browser/fields/uidreferencefield.py b/bika/lims/browser/fields/uidreferencefield.py new file mode 100644 index 0000000000..33cc4ea91e --- /dev/null +++ b/bika/lims/browser/fields/uidreferencefield.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Bika LIMS +# +# Copyright 2011-2016 by it's authors. +# Some rights reserved. See LICENSE.txt, AUTHORS.txt. + +from AccessControl import ClassSecurityInfo +from Products.Archetypes.BaseObject import BaseObject +from Products.Archetypes.Field import ObjectField, Field +from Products.ZCatalog.interfaces import ICatalogBrain +from bika.lims import logger +from bika.lims.interfaces.field import IUIDReferenceField +from plone.api.portal import get_tool +from zope.interface import implements + + +class ReferenceException(Exception): + pass + + +def is_uid(value): + """Checks that the string passed is a valid UID of an existing object + """ + uc = get_tool('uid_catalog') + brains = uc(UID=value) + return brains and True or False + + +def is_brain(brain_or_object): + """Checks if the passed in object is a portal catalog brain + """ + return ICatalogBrain.providedBy(brain_or_object) + + +def is_at_content(brain_or_object): + """Checks if the passed in object is an AT content type + """ + return isinstance(brain_or_object, BaseObject) + + +class UIDReferenceField(ObjectField): + """A field that stores References as UID values. + """ + _properties = Field._properties.copy() + _properties.update({ + 'type': 'uidreference', + 'default': '', + 'default_content_type': 'text/plain', + }) + + implements(IUIDReferenceField) + + security = ClassSecurityInfo() + + @security.private + def get_object(self, instance, value): + """Resolve a UID to an object. + """ + if not value: + return None + elif is_at_content(value): + return value + else: + uc = get_tool('uid_catalog') + brains = uc(UID=value) + if brains: + return brains[0].getObject() + logger.error("%s.%s: Resolving UIDReference failed for %s (drop)" % + instance, self.getName(), value) + + @security.private + def get_uid(self, instance, value): + """Takes a brain or object (or UID), and returns a UID. + """ + if not value: + ret = '' + elif is_brain(value): + ret = value.UID + elif is_at_content(value): + ret = value.UID() + elif is_uid(value): + ret = value + else: + raise ReferenceException("%s.%s: Cannot resolve UID for %s" % + (instance, self.getName(), value)) + return ret + + @security.private + def get(self, instance, **kwargs): + """Grab the stored value, and resolve object(s) from UID catalog. + """ + value = ObjectField.get(self, instance, **kwargs) + if self.multiValued: + ret = filter( + lambda x: x, [self.get_object(instance, uid) for uid in value]) + else: + ret = self.get_object(instance, value) + return ret + + @security.private + def set(self, instance, value, **kwargs): + """Accepts a UID, brain, or an object (or a list of any of these), + and stores a UID or list of UIDS. + """ + if self.multiValued: + if type(value) not in (list, tuple): + value = [value, ] + ret = [self.get_uid(instance, val) for val in value] + else: + ret = self.get_uid(instance, value) + ObjectField.set(self, instance, ret, **kwargs) diff --git a/bika/lims/content/analysis.py b/bika/lims/content/analysis.py index 0923980246..907606f029 100644 --- a/bika/lims/content/analysis.py +++ b/bika/lims/content/analysis.py @@ -24,14 +24,13 @@ from bika.lims import bikaMessageFactory as _ from bika.lims import logger from bika.lims.browser.fields import HistoryAwareReferenceField -from bika.lims.browser.fields import InterimFieldsField -from bika.lims.browser.widgets import RecordsWidget as BikaRecordsWidget +from bika.lims.browser.fields import UIDReferenceField from bika.lims.config import PROJECTNAME from bika.lims.content.baseanalysis import schema as BaseAnalysisSchema, \ BaseAnalysis from bika.lims.content.reflexrule import doReflexRuleAction -from bika.lims.interfaces import IAnalysis, IDuplicateAnalysis, IReferenceAnalysis, \ - ISamplePrepWorkflow +from bika.lims.interfaces import IAnalysis, IDuplicateAnalysis, \ + IReferenceAnalysis, ISamplePrepWorkflow from bika.lims.interfaces import IReferenceSample from bika.lims.permissions import * from bika.lims.permissions import Verify as VerifyPermission @@ -57,12 +56,10 @@ referenceClass=HoldingReference, ) -Attachment = ReferenceField( +Attachment = UIDReferenceField( 'Attachment', multiValued=1, allowed_types=('Attachment',), - referenceClass=HoldingReference, - relationship='AnalysisAttachment', ) Result = StringField( @@ -121,12 +118,10 @@ 'Remarks' ) -Method = ReferenceField( +Method = UIDReferenceField( 'Method', required=0, allowed_types=('Method',), - relationship='AnalysisMethod', - referenceClass=HoldingReference ) # The analysis method can't be changed when the analysis belongs @@ -138,12 +133,10 @@ visible=False ) -SamplePartition = ReferenceField( +SamplePartition = UIDReferenceField( 'SamplePartition', required=0, allowed_types=('SamplePartition',), - relationship='AnalysisSamplePartition', - referenceClass=HoldingReference ) # True if the analysis is created by a reflex rule @@ -154,22 +147,18 @@ ) # This field contains the original analysis which was reflected -OriginalReflexedAnalysis = ReferenceField( +OriginalReflexedAnalysis = UIDReferenceField( 'OriginalReflexedAnalysis', required=0, allowed_types=('Analysis',), - relationship='OriginalAnalysisReflectedAnalysis', - referenceClass=HoldingReference ) # This field contains the analysis which has been reflected # following a reflex rule -ReflexAnalysisOf = ReferenceField( +ReflexAnalysisOf = UIDReferenceField( 'ReflexAnalysisOf', required=0, allowed_types=('Analysis',), - relationship='AnalysisReflectedAnalysis', - referenceClass=HoldingReference ) # Which is the Reflex Rule action that has created this analysis @@ -326,7 +315,7 @@ Uncertainty, DetectionLimitOperand, NumberOfRequiredVerifications, - Verificators + Verificators, )) diff --git a/bika/lims/content/analysisservice.py b/bika/lims/content/analysisservice.py index fbaf1ae714..b164cc2675 100644 --- a/bika/lims/content/analysisservice.py +++ b/bika/lims/content/analysisservice.py @@ -13,7 +13,8 @@ from Products.ATExtensions.ateapi import RecordsField from Products.Archetypes.Registry import registerField from Products.Archetypes.public import DisplayList, ReferenceField, \ - BooleanField, BooleanWidget, Schema, registerType, SelectionWidget + BooleanField, BooleanWidget, Schema, registerType, SelectionWidget, \ + MultiSelectionWidget from Products.Archetypes.references import HoldingReference from Products.CMFCore.WorkflowCore import WorkflowException from Products.CMFCore.utils import getToolByName @@ -198,12 +199,10 @@ def sortable_title_with_sort_key(instance): ) ) -Preservation = ReferenceField( +Preservation = UIDReferenceField( 'Preservation', schemata='Container and Preservation', allowed_types=('Preservation',), - relationship='AnalysisServicePreservation', - referenceClass=HoldingReference, vocabulary='getPreservations', required=0, multiValued=0, @@ -219,12 +218,10 @@ def sortable_title_with_sort_key(instance): ) ) -Container = ReferenceField( +Container = UIDReferenceField( 'Container', schemata='Container and Preservation', allowed_types=('Container', 'ContainerType'), - relationship='AnalysisServiceContainer', - referenceClass=HoldingReference, vocabulary='getContainers', required=0, multiValued=0, @@ -252,6 +249,36 @@ def sortable_title_with_sort_key(instance): ) ) +# Manual methods associated to the AS +# List of methods capable to perform the Analysis Service. The +# Methods selected here are displayed in the Analysis Request +# Add view, closer to this Analysis Service if selected. +# Use getAvailableMethods() to retrieve the list with methods both +# from selected instruments and manually entered. +# Behavior controlled by js depending on ManualEntry/Instrument: +# - If InsrtumentEntry not checked, show +# See browser/js/bika.lims.analysisservice.edit.js +Methods = UIDReferenceField( + 'Methods', + schemata="Method", + required=0, + multiValued=1, + vocabulary='_getAvailableMethodsDisplayList', + allowed_types=('Method',), + widget=MultiSelectionWidget( + label=_("Methods"), + description=_( + "The tests of this type of analysis can be performed by using " + "more than one method with the 'Manual entry of results' option " + "enabled. A selection list with the methods selected here is " + "populated in the manage results view for each test of this type " + "of analysis. Note that only methods with 'Allow manual entry' " + "option enabled are displayed here; if you want the user to be " + "able to assign a method that requires instrument entry, enable " + "the 'Instrument assignment is allowed' option."), + ) +) + # Default method to be used. This field is used in Analysis Service # Edit view, use getMethod() to retrieve the Method to be used in # this Analysis Service. @@ -270,8 +297,6 @@ def sortable_title_with_sort_key(instance): vocabulary_display_path_bound=sys.maxint, allowed_types=('Method',), vocabulary='_getAvailableMethodsDisplayList', - relationship='AnalysisServiceMethod', - referenceClass=HoldingReference, widget=SelectionWidget( format='select', label=_("Default Method"), @@ -282,6 +307,34 @@ def sortable_title_with_sort_key(instance): ) ) +# Instruments associated to the AS +# List of instruments capable to perform the Analysis Service. The +# Instruments selected here are displayed in the Analysis Request +# Add view, closer to this Analysis Service if selected. +# - If InstrumentEntry not checked, hide and unset +# - If InstrumentEntry checked, set the first selected and show +Instruments = UIDReferenceField( + 'Instruments', + schemata="Method", + required=0, + multiValued=1, + vocabulary='_getAvailableInstrumentsDisplayList', + allowed_types=('Instrument',), + widget=MultiSelectionWidget( + label=_("Instruments"), + description=_( + "More than one instrument can be used in a test of this type of " + "analysis. A selection list with the instruments selected here is " + "populated in the results manage view for each test of this type " + "of analysis. The available instruments in the selection list " + "will change in accordance with the method selected by the user " + "for that test in the manage results view. Although a method can " + "have more than one instrument assigned, the selection list is " + "only populated with the instruments that are both set here and " + "allowed for the selected method."), + ) +) + # Allow/Disallow to set the calculation manually # Behavior controlled by javascript depending on Instruments field: # - If no instruments available, hide and uncheck @@ -315,8 +368,6 @@ def sortable_title_with_sort_key(instance): vocabulary_display_path_bound=sys.maxint, vocabulary='_getAvailableCalculationsDisplayList', allowed_types=('Calculation',), - relationship='AnalysisServiceCalculation', - referenceClass=HoldingReference, widget=SelectionWidget( format='select', label=_("Default Calculation"), @@ -348,8 +399,6 @@ def sortable_title_with_sort_key(instance): vocabulary_display_path_bound=sys.maxint, vocabulary='_getAvailableCalculationsDisplayList', allowed_types=('Calculation',), - relationship='AnalysisServiceDeferredCalculation', - referenceClass=HoldingReference, widget=SelectionWidget( format='select', label=_("Alternative Calculation"), @@ -368,7 +417,9 @@ def sortable_title_with_sort_key(instance): Preservation, Container, PartitionSetup, + Methods, _Method, + Instruments, UseDefaultCalculation, _Calculation, DeferredCalculation, @@ -407,6 +458,51 @@ def getPreservations(self): items.sort(lambda x, y: cmp(x[1], y[1])) return DisplayList(list(items)) + @security.private + def _getAvailableMethodsDisplayList(self): + """ Returns a DisplayList with the available Methods + registered in Bika-Setup. Only active Methods and those + with Manual Entry field active are fetched. + Used to fill the Methods MultiSelectionWidget when 'Allow + Instrument Entry of Results is not selected'. + """ + bsc = getToolByName(self, 'bika_setup_catalog') + items = [(i.UID, i.Title) + for i in bsc(portal_type='Method', + inactive_state='active') + if i.getObject().isManualEntryOfResults()] + items.sort(lambda x, y: cmp(x[1], y[1])) + items.insert(0, ('', _("None"))) + return DisplayList(list(items)) + + @security.private + def _getAvailableCalculationsDisplayList(self): + """ Returns a DisplayList with the available Calculations + registered in Bika-Setup. Only active Calculations are + fetched. Used to fill the _Calculation and DeferredCalculation + List fields + """ + bsc = getToolByName(self, 'bika_setup_catalog') + items = [(i.UID, i.Title) + for i in bsc(portal_type='Calculation', + inactive_state='active')] + items.sort(lambda x, y: cmp(x[1], y[1])) + items.insert(0, ('', _("None"))) + return DisplayList(list(items)) + + @security.private + def _getAvailableInstrumentsDisplayList(self): + """ Returns a DisplayList with the available Instruments + registered in Bika-Setup. Only active Instruments are + fetched. Used to fill the Instruments MultiSelectionWidget + """ + bsc = getToolByName(self, 'bika_setup_catalog') + items = [(i.UID, i.Title) + for i in bsc(portal_type='Instrument', + inactive_state='active')] + items.sort(lambda x, y: cmp(x[1], y[1])) + return DisplayList(list(items)) + def workflow_script_activate(self): workflow = getToolByName(self, 'portal_workflow') pu = getToolByName(self, 'plone_utils') diff --git a/bika/lims/content/baseanalysis.py b/bika/lims/content/baseanalysis.py index afab6300f7..6ce1a6b69c 100644 --- a/bika/lims/content/baseanalysis.py +++ b/bika/lims/content/baseanalysis.py @@ -257,37 +257,6 @@ ) ) -# Instruments associated to the AS -# List of instruments capable to perform the Analysis Service. The -# Instruments selected here are displayed in the Analysis Request -# Add view, closer to this Analysis Service if selected. -# - If InstrumentEntry not checked, hide and unset -# - If InstrumentEntry checked, set the first selected and show -Instruments = UIDReferenceField( - 'Instruments', - schemata="Method", - required=0, - multiValued=1, - vocabulary_display_path_bound=sys.maxint, - vocabulary='_getAvailableInstrumentsDisplayList', - allowed_types=('Instrument',), - relationship='AnalysisServiceInstruments', - referenceClass=HoldingReference, - widget=MultiSelectionWidget( - label=_("Instruments"), - description=_( - "More than one instrument can be used in a test of this type of " - "analysis. A selection list with the instruments selected here is " - "populated in the results manage view for each test of this type " - "of analysis. The available instruments in the selection list " - "will change in accordance with the method selected by the user " - "for that test in the manage results view. Although a method can " - "have more than one instrument assigned, the selection list is " - "only populated with the instruments that are both set here and " - "allowed for the selected method."), - ) -) - # Default instrument to be used. # Gets populated with the instruments selected in the multiselection # box above. @@ -296,16 +265,13 @@ # - If InstrumentEntry checked, set first selected instrument # - If InstrumentEntry not checked, hide and set None # See browser/js/bika.lims.analysisservice.edit.js -Instrument = HistoryAwareReferenceField( +Instrument = UIDReferenceField( 'Instrument', schemata="Method", searchable=True, required=0, - vocabulary_display_path_bound=sys.maxint, vocabulary='_getAvailableInstrumentsDisplayList', allowed_types=('Instrument',), - relationship='AnalysisServiceInstrument', - referenceClass=HoldingReference, widget=SelectionWidget( format='select', label=_("Default Instrument"), @@ -330,39 +296,6 @@ ) ) -# Manual methods associated to the AS -# List of methods capable to perform the Analysis Service. The -# Methods selected here are displayed in the Analysis Request -# Add view, closer to this Analysis Service if selected. -# Use getAvailableMethods() to retrieve the list with methods both -# from selected instruments and manually entered. -# Behavior controlled by js depending on ManualEntry/Instrument: -# - If InsrtumentEntry not checked, show -# See browser/js/bika.lims.analysisservice.edit.js -Methods = UIDReferenceField( - 'Methods', - schemata="Method", - required=0, - multiValued=1, - vocabulary_display_path_bound=sys.maxint, - vocabulary='_getAvailableMethodsDisplayList', - allowed_types=('Method',), - relationship='AnalysisServiceMethods', - referenceClass=HoldingReference, - widget=MultiSelectionWidget( - label=_("Methods"), - description=_( - "The tests of this type of analysis can be performed by using " - "more than one method with the 'Manual entry of results' option " - "enabled. A selection list with the methods selected here is " - "populated in the manage results view for each test of this type " - "of analysis. Note that only methods with 'Allow manual entry' " - "option enabled are displayed here; if you want the user to be " - "able to assign a method that requires instrument entry, enable " - "the 'Instrument assignment is allowed' option."), - ) -) - CalculationTitle = ComputedField( 'CalculationTitle', expression="" @@ -449,10 +382,7 @@ 'Category', schemata="Description", required=1, - vocabulary_display_path_bound=sys.maxint, allowed_types=('AnalysisCategory',), - relationship='AnalysisServiceAnalysisCategory', - referenceClass=HoldingReference, vocabulary='getAnalysisCategories', widget=ReferenceWidget( checkbox_bound=0, @@ -537,11 +467,8 @@ 'Department', schemata="Description", required=0, - vocabulary_display_path_bound=sys.maxint, allowed_types=('Department',), vocabulary='getDepartments', - relationship='AnalysisServiceDepartment', - referenceClass=HoldingReference, widget=ReferenceWidget( checkbox_bound=0, label=_("Department"), @@ -747,10 +674,8 @@ Keyword, ManualEntryOfResults, InstrumentEntryOfResults, - Instruments, Instrument, InstrumentTitle, - Methods, CalculationTitle, CalculationUID, InterimFields, @@ -895,54 +820,6 @@ def getAnalysisCategories(self): items.sort(lambda x, y: cmp(x[1], y[1])) return DisplayList(list(items)) - security.declarePublic('_getAvailableInstrumentsDisplayList') - - def _getAvailableInstrumentsDisplayList(self): - """ Returns a DisplayList with the available Instruments - registered in Bika-Setup. Only active Instruments are - fetched. Used to fill the Instruments MultiSelectionWidget - """ - bsc = getToolByName(self, 'bika_setup_catalog') - items = [(i.UID, i.Title) - for i in bsc(portal_type='Instrument', - inactive_state='active')] - items.sort(lambda x, y: cmp(x[1], y[1])) - return DisplayList(list(items)) - - security.declarePublic('_getAvailableMethodsDisplayList') - - def _getAvailableMethodsDisplayList(self): - """ Returns a DisplayList with the available Methods - registered in Bika-Setup. Only active Methods and those - with Manual Entry field active are fetched. - Used to fill the Methods MultiSelectionWidget when 'Allow - Instrument Entry of Results is not selected'. - """ - bsc = getToolByName(self, 'bika_setup_catalog') - items = [(i.UID, i.Title) - for i in bsc(portal_type='Method', - inactive_state='active') - if i.getObject().isManualEntryOfResults()] - items.sort(lambda x, y: cmp(x[1], y[1])) - items.insert(0, ('', _("None"))) - return DisplayList(list(items)) - - security.declarePublic('_getAvailableCalculationsDisplayList') - - def _getAvailableCalculationsDisplayList(self): - """ Returns a DisplayList with the available Calculations - registered in Bika-Setup. Only active Calculations are - fetched. Used to fill the _Calculation and DeferredCalculation - List fields - """ - bsc = getToolByName(self, 'bika_setup_catalog') - items = [(i.UID, i.Title) - for i in bsc(portal_type='Calculation', - inactive_state='active')] - items.sort(lambda x, y: cmp(x[1], y[1])) - items.insert(0, ('', _("None"))) - return DisplayList(list(items)) - security.declarePublic('getCalculation') def getCalculation(self): diff --git a/bika/lims/content/duplicateanalysis.py b/bika/lims/content/duplicateanalysis.py index cc0ad5de96..493518a7dd 100644 --- a/bika/lims/content/duplicateanalysis.py +++ b/bika/lims/content/duplicateanalysis.py @@ -8,6 +8,7 @@ """ from AccessControl import ClassSecurityInfo from bika.lims import bikaMessageFactory as _ +from bika.lims.browser.fields import UIDReferenceField from bika.lims.utils import t from bika.lims.browser.fields import InterimFieldsField from bika.lims.config import PROJECTNAME @@ -20,123 +21,179 @@ from Products.CMFCore.utils import getToolByName from zope.interface import implements +AnalysisField = UIDReferenceField( + 'Analysis', + required=1, + allowed_types=('Analysis',), +) -schema = schema.copy() + Schema(( - ReferenceField( - 'Analysis', - required=1, - allowed_types=('Analysis',), - referenceClass=HoldingReference, - relationship='DuplicateAnalysisAnalysis', - ), - InterimFieldsField( - 'InterimFields', - ), - StringField( - 'Result', - ), - StringField( - 'ResultDM', - ), - BooleanField( - 'Retested', - ), - ReferenceField( - 'Attachment', - multiValued=1, - allowed_types=('Attachment',), - referenceClass=HoldingReference, - relationship='DuplicateAnalysisAttachment', - ), +InterimFields = InterimFieldsField( + 'InterimFields', +) - StringField( - 'Analyst', - ), - ReferenceField( - 'Instrument', - required=0, - allowed_types=('Instrument',), - relationship='AnalysisInstrument', - referenceClass=HoldingReference, - ), - ComputedField( - 'SamplePartition', - expression='context.getAnalysis() and context.getAnalysis().aq_parent.portal_type=="AnalysisRequest" and context.getAnalysis().getSamplePartition()', - ), - ComputedField( - 'ClientOrderNumber', - expression='context.getAnalysis() and context.getAnalysis().aq_parent.portal_type=="AnalysisRequest" and context.getAnalysis().getClientOrderNumber()', - ), - ComputedField( - 'Service', - expression='context.getAnalysis() and context.getAnalysis().getService() or ""', - ), - ComputedField( - 'ServiceUID', - expression='context.getAnalysis() and context.getAnalysis().getServiceUID()', - ), - ComputedField( - 'CategoryUID', - expression='context.getAnalysis() and context.getAnalysis().aq_parent.portal_type=="AnalysisRequest" and context.getAnalysis().getCategoryUID()', - ), - ComputedField( - 'Calculation', - expression='context.getAnalysis() and context.getAnalysis().aq_parent.portal_type=="AnalysisRequest" and context.getAnalysis().getCalculation()', - ), - ComputedField( - 'ReportDryMatter', - expression='context.getAnalysis() and context.getAnalysis().aq_parent.portal_type=="AnalysisRequest" and context.getAnalysis().getReportDryMatter()', - ), - ComputedField( - 'DateReceived', - expression='context.getAnalysis() and context.getAnalysis().aq_parent.portal_type=="AnalysisRequest" and context.getAnalysis().getDateReceived()', - ), - ComputedField( - 'MaxTimeAllowed', - expression='context.getAnalysis() and context.getAnalysis().aq_parent.portal_type=="AnalysisRequest" and context.getAnalysis().getMaxTimeAllowed()', - ), - ComputedField( - 'DueDate', - expression='context.getAnalysis() and context.getAnalysis().aq_parent.portal_type=="AnalysisRequest" and context.getAnalysis().getDueDate()', - ), - ComputedField( - 'Duration', - expression='context.getAnalysis() and context.getAnalysis().aq_parent.portal_type=="AnalysisRequest" and context.getAnalysis().getDuration()', - ), - ComputedField( - 'Earliness', - expression='context.getAnalysis() and context.getAnalysis().aq_parent.portal_type=="AnalysisRequest" and context.getAnalysis().getEarliness()', - ), - ComputedField( - 'ClientUID', - expression='context.getAnalysis() and context.getAnalysis().aq_parent.portal_type=="AnalysisRequest" and context.getAnalysis().getClientUID()', - ), - ComputedField( - 'RequestID', - expression='context.getAnalysis() and context.getAnalysis().aq_parent.portal_type=="AnalysisRequest" and context.getAnalysis().getRequestID() or ""', - ), - ComputedField( - 'PointOfCapture', - expression='context.getAnalysis() and context.getAnalysis().getPointOfCapture()', - ), - StringField( - 'ReferenceAnalysesGroupID', - widget=StringWidget( - label=_("ReferenceAnalysesGroupID"), - visible=False, - ), - ), - ComputedField( - 'Keyword', - expression="context.getAnalysis().getKeyword()", +Result = StringField( + 'Result', +) + +ResultDM = StringField( + 'ResultDM', +) + +Retested = BooleanField( + 'Retested', +) + +Analyst = StringField( + 'Analyst', +) + +SamplePartition = ComputedField( + 'SamplePartition', + expression='context.getAnalysis() and context.getAnalysis(' + ').aq_parent.portal_type=="AnalysisRequest" and ' + 'context.getAnalysis().getSamplePartition()', +) + +ClientOrderNumber = ComputedField( + 'ClientOrderNumber', + expression='context.getAnalysis() and context.getAnalysis(' + ').aq_parent.portal_type=="AnalysisRequest" and ' + 'context.getAnalysis().getClientOrderNumber()', +) + +Service = ComputedField( + 'Service', + expression='context.getAnalysis() and context.getAnalysis().getService() ' + 'or ""', +) + +ServiceUID = ComputedField( + 'ServiceUID', + expression='context.getAnalysis() and context.getAnalysis(' + ').getServiceUID()', +) + +CategoryUID = ComputedField( + 'CategoryUID', + expression='context.getAnalysis() and context.getAnalysis(' + ').aq_parent.portal_type=="AnalysisRequest" and ' + 'context.getAnalysis().getCategoryUID()', +) + +Calculation = ComputedField( + 'Calculation', + expression='context.getAnalysis() and context.getAnalysis(' + ').aq_parent.portal_type=="AnalysisRequest" and ' + 'context.getAnalysis().getCalculation()', +) + +ReportDryMatter = ComputedField( + 'ReportDryMatter', + expression='context.getAnalysis() and context.getAnalysis(' + ').aq_parent.portal_type=="AnalysisRequest" and ' + 'context.getAnalysis().getReportDryMatter()', +) + +DateReceived = ComputedField( + 'DateReceived', + expression='context.getAnalysis() and context.getAnalysis(' + ').aq_parent.portal_type=="AnalysisRequest" and ' + 'context.getAnalysis().getDateReceived()', +) + +MaxTimeAllowed = ComputedField( + 'MaxTimeAllowed', + expression='context.getAnalysis() and context.getAnalysis(' + ').aq_parent.portal_type=="AnalysisRequest" and ' + 'context.getAnalysis().getMaxTimeAllowed()', +) + +DueDate = ComputedField( + 'DueDate', + expression='context.getAnalysis() and context.getAnalysis(' + ').aq_parent.portal_type=="AnalysisRequest" and ' + 'context.getAnalysis().getDueDate()', +) + +Duration = ComputedField( + 'Duration', + expression='context.getAnalysis() and context.getAnalysis(' + ').aq_parent.portal_type=="AnalysisRequest" and ' + 'context.getAnalysis().getDuration()', +) + +Earliness = ComputedField( + 'Earliness', + expression='context.getAnalysis() and context.getAnalysis(' + ').aq_parent.portal_type=="AnalysisRequest" and ' + 'context.getAnalysis().getEarliness()', +) + +ClientUID = ComputedField( + 'ClientUID', + expression='context.getAnalysis() and context.getAnalysis(' + ').aq_parent.portal_type=="AnalysisRequest" and ' + 'context.getAnalysis().getClientUID()', +) + +RequestID = ComputedField( + 'RequestID', + expression='context.getAnalysis() and context.getAnalysis(' + ').aq_parent.portal_type=="AnalysisRequest" and ' + 'context.getAnalysis().getRequestID() or ""', +) + +PointOfCapture = ComputedField( + 'PointOfCapture', + expression='context.getAnalysis() and context.getAnalysis(' + ').getPointOfCapture()', +) + +ReferenceAnalysesGroupID = StringField( + 'ReferenceAnalysesGroupID', + widget=StringWidget( + label=_("ReferenceAnalysesGroupID"), + visible=False, ), - ComputedField( - 'NumberOfRequiredVerifications', - expression='context.getAnalysis().getNumberOfRequiredVerifications()', - ) -), ) +Keyword = ComputedField( + 'Keyword', + expression="context.getAnalysis().getKeyword()", +) + +NumberOfRequiredVerifications = ComputedField( + 'NumberOfRequiredVerifications', + expression='context.getAnalysis().getNumberOfRequiredVerifications()', +) + +schema = schema.copy() + Schema(( + AnalysisField, + InterimFields, + Result, + ResultDM, + Retested, + Analyst, + SamplePartition, + ClientOrderNumber, + Service, + ServiceUID, + CategoryUID, + Calculation, + ReportDryMatter, + DateReceived, + MaxTimeAllowed, + DueDate, + Duration, + Earliness, + ClientUID, + RequestID, + PointOfCapture, + ReferenceAnalysesGroupID, + Keyword, + NumberOfRequiredVerifications, +)) + class DuplicateAnalysis(Analysis): implements(IDuplicateAnalysis) diff --git a/bika/lims/interfaces/field.py b/bika/lims/interfaces/field.py new file mode 100644 index 0000000000..bd1d774571 --- /dev/null +++ b/bika/lims/interfaces/field.py @@ -0,0 +1,11 @@ +# This file is part of Bika LIMS +# +# Copyright 2011-2016 by it's authors. +# Some rights reserved. See LICENSE.txt, AUTHORS.txt. + +from zope.interface import Interface + + +class IUIDReferenceField(Interface): + """Marker interface for UID Reference Fields + """ diff --git a/bika/lims/utils/analysis.py b/bika/lims/utils/analysis.py index f98c311e50..a7137217d1 100644 --- a/bika/lims/utils/analysis.py +++ b/bika/lims/utils/analysis.py @@ -79,13 +79,21 @@ def create_analysis(context, source): # And lives here: # https://github.com/zopefoundation/Products.CMFCore/blob/2.2/Products/CMFCore/TypesTool.py#L535 analysis = _createObjectByType("Analysis", context, source.getKeyword()) - src_schema = source.getSchema() - dst_schema = analysis.getSchema() + src_schema = source.Schema() + dst_schema = analysis.Schema() + + # Some fields should not be copied from source! + IGNORE = ['UID', 'id'] + for field in src_schema.fields(): fieldname = field.getName() if fieldname in dst_schema: value = field.get(source) - dst_schema[fieldname].set(analysis, value) + if value: + if 'ategory' in fieldname: + import pdb;pdb.set_trace();pass + dst_schema[fieldname].set(analysis, value) + import pdb;pdb.set_trace();pass # unmarkCreationFlag also reindex the object analysis.unmarkCreationFlag() # Trigger the intitialization event of the new object From c029dbbd5da6e660a58d4f6035a8d232f978b52f Mon Sep 17 00:00:00 2001 From: Campbell Date: Wed, 10 May 2017 14:08:07 +0200 Subject: [PATCH 03/36] Finalize the BaseAnalysis/Analysis/Service schemas PEP8, cleanup, comments, and make the schemas and the migration cooperate with each other --- bika/lims/content/analysis.py | 1479 ++++++++++++-------------- bika/lims/content/analysisservice.py | 206 ++-- bika/lims/content/baseanalysis.py | 591 +++++----- 3 files changed, 1038 insertions(+), 1238 deletions(-) diff --git a/bika/lims/content/analysis.py b/bika/lims/content/analysis.py index 907606f029..548918b219 100644 --- a/bika/lims/content/analysis.py +++ b/bika/lims/content/analysis.py @@ -5,83 +5,85 @@ # Copyright 2011-2016 by it's authors. # Some rights reserved. See LICENSE.txt, AUTHORS.txt. -"""DuplicateAnalysis uses this as it's base. This accounts for much confusion. -""" - +import cgi import math +from decimal import Decimal -import cgi from AccessControl import ClassSecurityInfo from DateTime import DateTime -from Products.ATExtensions.ateapi import DateTimeWidget from Products.Archetypes import atapi -from Products.Archetypes.config import REFERENCE_CATALOG from Products.Archetypes.public import * from Products.Archetypes.references import HoldingReference from Products.CMFCore.WorkflowCore import WorkflowException -from Products.CMFCore.utils import getToolByName -from Products.CMFPlone.utils import _createObjectByType -from bika.lims import bikaMessageFactory as _ +from bika.lims import bikaMessageFactory as _, deprecated from bika.lims import logger -from bika.lims.browser.fields import HistoryAwareReferenceField -from bika.lims.browser.fields import UIDReferenceField +from bika.lims.browser.fields import UIDReferenceField, \ + HistoryAwareReferenceField +from bika.lims.browser.widgets import DateTimeWidget from bika.lims.config import PROJECTNAME -from bika.lims.content.baseanalysis import schema as BaseAnalysisSchema, \ - BaseAnalysis +from bika.lims.content.baseanalysis import BaseAnalysis +from bika.lims.content.baseanalysis import schema as BaseAnalysisSchema from bika.lims.content.reflexrule import doReflexRuleAction -from bika.lims.interfaces import IAnalysis, IDuplicateAnalysis, \ - IReferenceAnalysis, ISamplePrepWorkflow -from bika.lims.interfaces import IReferenceSample +from bika.lims.interfaces import IAnalysis, ISamplePrepWorkflow from bika.lims.permissions import * from bika.lims.permissions import Verify as VerifyPermission from bika.lims.utils import changeWorkflowState, formatDecimalMark from bika.lims.utils import drop_trailing_zeros_decimal -from bika.lims.utils.analysis import format_numeric_result +from bika.lims.utils.analysis import format_numeric_result, create_analysis from bika.lims.utils.analysis import get_significant_digits -from bika.lims.workflow import skip -from decimal import Decimal -from plone import api +from bika.lims.workflow import skip, getTransitionDate +from plone.api.portal import get_tool +from plone.api.user import has_permission from zope.interface import implements -# Although attributes of the service are stored directly in the Analysis, -# the originating ServiceUID is required to prevent duplication of analyses. +# The originating Service UID is required to prevent duplication of analyses. +# It's also used to populate catalog values. ServiceUID = StringField( 'ServiceUID', ) +# This overrides the Calculation UIDReferenceField from BaseAnalysis +# so that analysis always refers to the calculation as it was when created. Calculation = HistoryAwareReferenceField( 'Calculation', allowed_types=('Calculation',), relationship='AnalysisCalculation', - referenceClass=HoldingReference, + referenceClass=HoldingReference ) +# Attachments which are added manually in the UI, or automatically when +# results are imported from a file supplied by an instrument. Attachment = UIDReferenceField( 'Attachment', multiValued=1, allowed_types=('Attachment',), ) +# The final result of the analysis is stored here. Result = StringField( 'Result' ) +# When the result is changed, this value is updated to the current time. ResultCaptureDate = DateTimeField( 'ResultCaptureDate', - widget=ComputedWidget( - visible=False - ) ) +# If ReportDryMatter is True in the AnalysisService, the adjusted result +# is stored here. ResultDM = StringField( 'ResultDM' ) +# If the analysis has previously been retracted, this flag is set True +# to indicate that this is a re-test. Retested = BooleanField( 'Retested', default=False ) +# When the AR is published, the date of publication is recorded here. +# It's used to populate catalog values. DateAnalysisPublished = DateTimeField( 'DateAnalysisPublished', widget=DateTimeWidget( @@ -89,6 +91,9 @@ ) ) +# DueDate is calculated by adding the MaxTimeAllowed to the date that the +# Analysis' sample was received. It's used to create alerts when analyses +# are "late", and also to populate catalog values. DueDate = DateTimeField( 'DueDate', widget=DateTimeWidget( @@ -96,6 +101,8 @@ ) ) +# This is used to calculate turnaround time reports. +# The value is set when the Analysis is published. Duration = IntegerField( 'Duration', widget=IntegerWidget( @@ -103,6 +110,8 @@ ) ) +# This is used to calculate turnaround time reports. +# The value is set when the Analysis is published. Earliness = IntegerField( 'Earliness', widget=IntegerWidget( @@ -110,20 +119,16 @@ ) ) +# The ID of the logged in user who submitted the result for this Analysis. Analyst = StringField( 'Analyst' ) +# Remarks entered in the manage_results screen are stored here Remarks = TextField( 'Remarks' ) -Method = UIDReferenceField( - 'Method', - required=0, - allowed_types=('Method',), -) - # The analysis method can't be changed when the analysis belongs # to a worksheet and that worksheet has a method. CanMethodBeChanged = BooleanField( @@ -133,6 +138,7 @@ visible=False ) +# The physical sample partition linked to the Analysis. SamplePartition = UIDReferenceField( 'SamplePartition', required=0, @@ -150,15 +156,15 @@ OriginalReflexedAnalysis = UIDReferenceField( 'OriginalReflexedAnalysis', required=0, - allowed_types=('Analysis',), + allowed_types=('Analysis',) ) -# This field contains the analysis which has been reflected -# following a reflex rule +# This field contains the analysis which has been reflected following +# a reflex rule ReflexAnalysisOf = UIDReferenceField( 'ReflexAnalysisOf', required=0, - allowed_types=('Analysis',), + allowed_types=('Analysis',) ) # Which is the Reflex Rule action that has created this analysis @@ -175,78 +181,16 @@ default=0 ) -# Reflex rule triggered actions which the current analysis is -# responsible of. Separated by '|' +# Reflex rule triggered actions which the current analysis is responsible for. +# Separated by '|' ReflexRuleActionsTriggered = StringField( 'ReflexRuleActionsTriggered', required=0, default='' ) -ClientUID = ComputedField( - 'ClientUID', - expression='context.aq_parent.aq_parent.UID()' -) - -ClientTitle = ComputedField( - 'ClientTitle', - expression='context.aq_parent.aq_parent.Title()' -) - -RequestID = ComputedField( - 'RequestID', - expression='context.aq_parent.getRequestID()' -) - -ClientOrderNumber = ComputedField( - 'ClientOrderNumber', - expression='context.aq_parent.getClientOrderNumber()' -) - -SampleTypeUID = ComputedField( - 'SampleTypeUID', - expression='context.aq_parent.getSample().getSampleType().UID()' -) - -SamplePointUID = ComputedField( - 'SamplePointUID', - expression='context.aq_parent.getSample().getSamplePoint().UID(' - ') if context.aq_parent.getSample().getSamplePoint()' - ' else ""' -) - -MethodUID = ComputedField( - 'MethodUID', - expression="context.getMethod() and context.getMethod().UID() or ''", - widget=ComputedWidget( - visible=False - ) -) - -InstrumentUID = ComputedField( - 'InstrumentUID', - expression="" - "context.getInstrument() and context.getInstrument().UID() or ''", - widget=ComputedWidget( - visible=False - ) -) - -DateReceived = ComputedField( - 'DateReceived', - expression='context.aq_parent.getDateReceived()' -) - -DateSampled = ComputedField( - 'DateSampled', - expression='context.aq_parent.getSample().getDateSampled()' -) - -InstrumentValid = ComputedField( - 'InstrumentValid', - expression='context.isInstrumentValid()' -) - +# The actual uncertainty for this analysis' result, populated when the result +# is submitted. Uncertainty = FixedPointField( 'Uncertainty', precision=10, @@ -255,6 +199,8 @@ ) ) +# If the result is outside of the detection limits of the method or instrument, +# the operand (< or >) is stored here. DetectionLimitOperand = StringField( 'DetectionLimitOperand' ) @@ -276,10 +222,8 @@ default='' ) - schema = BaseAnalysisSchema.copy() + Schema(( ServiceUID, - Calculation, Attachment, Result, ResultCaptureDate, @@ -291,7 +235,6 @@ Earliness, Analyst, Remarks, - Method, CanMethodBeChanged, SamplePartition, IsReflexAnalysis, @@ -300,18 +243,6 @@ ReflexRuleAction, ReflexRuleLocalID, ReflexRuleActionsTriggered, - ClientUID, - ClientTitle, - RequestID, - ClientOrderNumber, - ServiceUID, - SampleTypeUID, - SamplePointUID, - MethodUID, - InstrumentUID, - DateReceived, - DateSampled, - InstrumentValid, Uncertainty, DetectionLimitOperand, NumberOfRequiredVerifications, @@ -325,190 +256,216 @@ class Analysis(BaseAnalysis): displayContentsTab = False schema = schema - def getServiceUID(self): - import pdb;pdb.set_trace();pass - print "XXX analysis.getServiceUID(!!!)"%self - return self.UID() - - def getService(self): - import pdb;pdb.set_trace();pass - print "XXX analysis.getService(!!!)"%self - return self.UID() - def _getCatalogTool(self): from bika.lims.catalog import getCatalog return getCatalog(self) + @deprecated("Currently it simply returns the analysis object.") + @security.public + def getService(self): + return self + + @security.public def getNumberOfVerifications(self): - verificators=self.getVerificators() + verificators = self.getVerificators() if not verificators: return 0 return len(verificators.split(',')) - def addVerificator(self,username): - verificators=self.getVerificators() + @security.public + def addVerificator(self, username): + verificators = self.getVerificators() if not verificators: self.setVerificators(username) else: - self.setVerificators(verificators+","+username) + self.setVerificators(verificators + "," + username) + @security.public def deleteLastVerificator(self): - verificators=self.getVerificators().split(',') + verificators = self.getVerificators().split(',') del verificators[-1] self.setVerificators(",".join(verificators)) - def wasVerifiedByUser(self,username): - verificators=self.getVerificators().split(',') + @security.public + def wasVerifiedByUser(self, username): + verificators = self.getVerificators().split(',') return username in verificators + @security.public def getLastVerificator(self): return self.getVerificators().split(',')[-1] + @security.public def updateDueDate(self): - # set the max hours allowed - - service = self.getService() - maxtime = service.getMaxTimeAllowed() + maxtime = self.getMaxTimeAllowed() if not maxtime: maxtime = {'days': 0, 'hours': 0, 'minutes': 0} - self.setMaxTimeAllowed(maxtime) - # set the due date - # default to old calc in case no calendars - max_days = float(maxtime.get('days', 0)) + \ - ( - (float(maxtime.get('hours', 0)) * 3600 + - float(maxtime.get('minutes', 0)) * 60) - / 86400 - ) + max_days = float(maxtime.get('days', 0)) + ( + (float(maxtime.get('hours', 0)) * 3600 + + float(maxtime.get('minutes', 0)) * 60) + / 86400 + ) part = self.getSamplePartition() if part: starttime = part.getDateReceived() - if starttime: - duetime = starttime + max_days - else: - duetime = '' + duetime = starttime + max_days if starttime else '' self.setDueDate(duetime) + @security.public def getAnalysisRequestTitle(self): - """ - This is a column + """This is a catalog metadata column """ return self.aq_parent.Title() + @security.public def getAnalysisRequestURL(self): - """ - This is a column + """This is a catalog metadata column """ return self.aq_parent.absolute_url_path() - def getService(self): - return self - + @security.public def getServiceTitle(self): - """ Returns the Title of the asociated service. + """Returns the Title of the associated service. Analysis titles are + always the same as the title of the service from which they are derived. """ return self.Title() + @security.public def getReviewState(self): - """ Return the current analysis' state""" - workflow = getToolByName(self, "portal_workflow") + """Return the current analysis' state + """ + workflow = get_tool("portal_workflow") return workflow.getInfoFor(self, "review_state") + @security.public def getDefaultUncertainty(self, result=None): - """ Calls self.Service.getUncertainty with either the provided - result value or self.Result + """Return the uncertainty value, if the result falls within + specified ranges for the service from which this analysis was derived. """ - service = self.getService() - if not service: - return None - return service.getUncertainty(result and result or self.getResult()) + if result is None: + result = self.getResult() + + uncertainties = self.getUncertainties() + if uncertainties: + try: + res = float(result) + except (TypeError, ValueError): + # if analysis result is not a number, then we assume in range + return None + + for d in uncertainties: + _min = float(d['intercept_min']) + _max = float(d['intercept_max']) + if _min <= res and res <= _max: + if str(d['errorvalue']).strip().endswith('%'): + try: + percvalue = float(d['errorvalue'].replace('%', '')) + except ValueError: + return None + uncertainty = res / 100 * percvalue + else: + uncertainty = float(d['errorvalue']) + + return uncertainty + return None + + @security.public def getUncertainty(self, result=None): - """ Returns the uncertainty for this analysis and result. - Returns the value from Schema's Uncertainty field if the - Service has the option 'Allow manual uncertainty'. Otherwise, - do a callback to getDefaultUncertainty(). - Returns None if no result specified and the current result - for this analysis is below or above detections limits. - """ - serv = self.getService() - schu = self.Schema().getField('Uncertainty').get(self) - if result is None and (self.isAboveUpperDetectionLimit() or \ + """Returns the uncertainty for this analysis and result. + Returns the value from Schema's Uncertainty field if the Service has + the option 'Allow manual uncertainty'. Otherwise, do a callback to + getDefaultUncertainty(). Returns None if no result specified and the + current result for this analysis is below or above detections limits. + """ + uncertainty = self.Schema().getField('Uncertainty').get(self) + if result is None and (self.isAboveUpperDetectionLimit() or self.isBelowLowerDetectionLimit()): return None - if schu and serv.getAllowManualUncertainty() == True: + if uncertainty and self.getAllowManualUncertainty() is True: try: - schu = float(schu) - return schu - except ValueError: + uncertainty = float(uncertainty) + return uncertainty + except (TypeError, ValueError): # if uncertainty is not a number, return default value - return self.getDefaultUncertainty(result) + pass return self.getDefaultUncertainty(result) + @security.public + def setUncertainty(self, unc): + """Sets the uncertainty for this analysis. If the result is a + Detection Limit or the value is below LDL or upper UDL, sets the + uncertainty value to 0 + """ + # Uncertainty calculation on DL + # https://jira.bikalabs.com/browse/LIMS-1808 + if self.isAboveUpperDetectionLimit() or \ + self.isBelowLowerDetectionLimit(): + self.Schema().getField('Uncertainty').set(self, None) + else: + self.Schema().getField('Uncertainty').set(self, unc) + + @security.public def setDetectionLimitOperand(self, value): - """ Sets the detection limit operand for this analysis, so - the result will be interpreted as a detection limit. - The value will only be set if the Service has - 'DetectionLimitSelector' field set to True, otherwise, - the detection limit operand will be set to None. - See LIMS-1775 for further information about the relation - amongst 'DetectionLimitSelector' and - 'AllowManualDetectionLimit'. - https://jira.bikalabs.com/browse/LIMS-1775 - """ - srv = self.getService() - md = srv.getDetectionLimitSelector() if srv else False - val = value if (md and value in ('>', '<')) else None + """Sets the detection limit operand for this analysis, so the result + will be interpreted as a detection limit. The value will only be set + if the Service has 'DetectionLimitSelector' field set to True, + otherwise, the detection limit operand will be set to None. See + LIMS-1775 for further information about the relation amongst + 'DetectionLimitSelector' and 'AllowManualDetectionLimit'. + https://jira.bikalabs.com/browse/LIMS-1775 + """ + md = self.getDetectionLimitSelector() + val = value if (md and value in '<>') else None self.Schema().getField('DetectionLimitOperand').set(self, val) + # Method getLowerDetectionLimit overrides method of class BaseAnalysis + @security.public def getLowerDetectionLimit(self): - """ Returns the Lower Detection Limit (LDL) that applies to - this analysis in particular. If no value set or the - analysis service doesn't allow manual input of detection - limits, returns the value set by default in the Analysis - Service + """Returns the Lower Detection Limit (LDL) that applies to this + analysis in particular. If no value set or the analysis service + doesn't allow manual input of detection limits, returns the value set + by default in the Analysis Service """ operand = self.getDetectionLimitOperand() if operand and operand == '<': result = self.getResult() try: + # in this case, the result itself is the LDL. return float(result) - except: + except (TypeError, ValueError): logger.warn("The result for the analysis %s is a lower " "detection limit, but not floatable: '%s'. " "Returnig AS's default LDL." % (self.id, result)) - service = self.getService() - if not service: - return None - return service.getLowerDetectionLimit() + return BaseAnalysis.getLowerDetectionLimit(self) + # Method getUpperDetectionLimit overrides method of class BaseAnalysis + @security.public def getUpperDetectionLimit(self): - """ Returns the Upper Detection Limit (UDL) that applies to - this analysis in particular. If no value set or the - analysis service doesn't allow manual input of detection - limits, returns the value set by default in the Analysis - Service + """Returns the Upper Detection Limit (UDL) that applies to this + analysis in particular. If no value set or the analysis service + doesn't allow manual input of detection limits, returns the value set + by default in the Analysis Service """ operand = self.getDetectionLimitOperand() if operand and operand == '>': result = self.getResult() try: + # in this case, the result itself is the LDL. return float(result) - except: + except (TypeError, ValueError): logger.warn("The result for the analysis %s is a lower " "detection limit, but not floatable: '%s'. " "Returnig AS's default LDL." % (self.id, result)) - service = self.getService() - if not service: - return None - return service.getUpperDetectionLimit() + return BaseAnalysis.getUpperDetectionLimit(self) + @security.public def isBelowLowerDetectionLimit(self): - """ Returns True if the result is below the Lower Detection - Limit or if Lower Detection Limit has been manually set + """Returns True if the result is below the Lower Detection Limit or + if Lower Detection Limit has been manually set """ dl = self.getDetectionLimitOperand() if dl and dl == '<': @@ -521,13 +478,14 @@ def isBelowLowerDetectionLimit(self): try: result = float(result) return result < ldl - except: + except (TypeError, ValueError): pass return False + @security.public def isAboveUpperDetectionLimit(self): - """ Returns True if the result is above the Upper Detection - Limit or if Upper Detection Limit has been manually set + """Returns True if the result is above the Upper Detection Limit or + if Upper Detection Limit has been manually set """ dl = self.getDetectionLimitOperand() if dl and dl == '>': @@ -540,190 +498,136 @@ def isAboveUpperDetectionLimit(self): try: result = float(result) return result > udl - except: + except (TypeError, ValueError): pass return False + @security.public def getDetectionLimits(self): - """ Returns a two-value array with the limits of detection - (LDL and UDL) that applies to this analysis in particular. - If no value set or the analysis service doesn't allow - manual input of detection limits, returns the value set by - default in the Analysis Service + """Returns a two-value array with the limits of detection (LDL and + UDL) that applies to this analysis in particular. If no value set or + the analysis service doesn't allow manual input of detection limits, + returns the value set by default in the Analysis Service """ return [self.getLowerDetectionLimit(), self.getUpperDetectionLimit()] + @security.public def isLowerDetectionLimit(self): - """ Returns True if the result for this analysis represents - a Lower Detection Limit. Otherwise, returns False + """Returns True if the result for this analysis represents a Lower + Detection Limit. Otherwise, returns False """ - return self.isBelowLowerDetectionLimit() and \ - self.getDetectionLimitOperand() == '<' + if self.isBelowLowerDetectionLimit(): + if self.getDetectionLimitOperand() == '<': + return True + @security.public def isUpperDetectionLimit(self): - """ Returns True if the result for this analysis represents - an Upper Detection Limit. Otherwise, returns False + """Returns True if the result for this analysis represents an Upper + Detection Limit. Otherwise, returns False """ - return self.isAboveUpperDetectionLimit() and \ - self.getDetectionLimitOperand() == '>' + if self.isAboveLowerDetectionLimit(): + if self.getDetectionLimitOperand() == '>': + return True + @security.public def getDependents(self): - """ Return a list of analyses who depend on us - to calculate their result + """Return a list of analyses who depend on us to calculate their result """ - rc = getToolByName(self, REFERENCE_CATALOG) dependents = [] - service = self.getService() ar = self.aq_parent for sibling in ar.getAnalyses(full_objects=True): if sibling == self: continue - service = rc.lookupObject(sibling.getServiceUID()) - calculation = service.getCalculation() + calculation = sibling.getCalculation() if not calculation: continue depservices = calculation.getDependentServices() dep_keywords = [x.getKeyword() for x in depservices] - if self.getService().getKeyword() in dep_keywords: + if self.getKeyword() in dep_keywords: dependents.append(sibling) return dependents + @security.public def getDependencies(self): - """ Return a list of analyses who we depend on - to calculate our result. + """Return a list of analyses who we depend on to calculate our result. """ siblings = self.aq_parent.getAnalyses(full_objects=True) - calculation = self.getService().getCalculation() + calculation = self.getCalculation() if not calculation: return [] dep_services = [d.UID() for d in calculation.getDependentServices()] - dep_analyses = [a for a in siblings if a.getServiceUID() in dep_services] + dep_analyses = [a for a in siblings if + a.getServiceUID() in dep_services] return dep_analyses - def setResult(self, value, **kw): - """ :value: must be a string + def setResult(self, value): + """Validate and set a value into the Result field, taking into + account the Detection Limits. + :param value: is expected to be a string. """ # Always update ResultCapture date when this field is modified self.setResultCaptureDate(DateTime()) # Only allow DL if manually enabled in AS - val = str(value) - if val and (val.strip().startswith('>') or val.strip().startswith('<')): - self.Schema().getField('DetectionLimitOperand').set(self, None) - oper = '<' if val.strip().startswith('<') else '>' - srv = self.getService() - if srv and srv.getDetectionLimitSelector(): - if srv.getAllowManualDetectionLimit(): + val = str(value).strip() + if val and val[0] in '<>': + self.setDetectionLimitOperand(None) + oper = val[0] + val = val.replace(oper, '', 1) + + # Check if the value is indeterminate / non-floatable + try: + str(float(val)) + except (ValueError, TypeError): + val = value + + if self.getDetectionLimitSelector(): + if self.getAllowManualDetectionLimit(): # DL allowed, try to remove the operator and set the # result as a detection limit - try: - val = val.replace(oper, '', 1) - val = str(float(val)) - self.Schema().getField('DetectionLimitOperand').set(self, oper) - except: - val = value + self.setDetectionLimitOperand(oper) else: # Trying to set a result with an '<,>' operator, # but manual DL not allowed, so override the # value with the service's default LDL or UDL # according to the operator, but only if the value # is not an indeterminate. - try: - val = val.replace(oper, '', 1) - val = str(float(val)) # An indeterminate? - if oper == '<': - val = srv.getLowerDetectionLimit() - else: - val = srv.getUpperDetectionLimit() - self.Schema().getField('DetectionLimitOperand').set(self, oper) - except: - # Oops, an indeterminate. Do nothing. - val = value - elif srv: - # Ooopps. Trying to set a result with an '<,>' operator, - # but the service doesn't allow this in any case! - # No need to check for AllowManualDetectionLimit, cause - # we assume that this will always be False unless - # DetectionLimitSelector is True. See LIMS-1775 for - # further information about the relation amongst - # 'DetectionLimitSelector' and 'AllowManualDetectionLimit'. - # https://jira.bikalabs.com/browse/LIMS-1775 - # Let's try to remove the operator and set the value as - # a regular result, but only if not an indeterminate - try: - val = val.replace(oper, '', 1) - val = str(float(val)) - except: - val = value - elif not val: + if oper == '<': + val = self.getLowerDetectionLimit() + else: + val = self.getUpperDetectionLimit() + self.setDetectionLimitOperand(oper) + elif val is '': # Reset DL - self.Schema().getField('DetectionLimitOperand').set(self, None) - self.getField('Result').set(self, val, **kw) + self.setDetectionLimitOperand(None) - # Uncertainty calculation on DL - # https://jira.bikalabs.com/browse/LIMS-1808 - if self.isAboveUpperDetectionLimit() or \ - self.isBelowLowerDetectionLimit(): - self.Schema().getField('Uncertainty').set(self, None) + self.setResult(val) - def setUncertainty(self, unc): - """ Sets the uncertainty for this analysis. If the result is - a Detection Limit or the value is below LDL or upper UDL, - sets the uncertainty value to 0 - """ # Uncertainty calculation on DL # https://jira.bikalabs.com/browse/LIMS-1808 if self.isAboveUpperDetectionLimit() or \ - self.isBelowLowerDetectionLimit(): + self.isBelowLowerDetectionLimit(): self.Schema().getField('Uncertainty').set(self, None) - else: - self.Schema().getField('Uncertainty').set(self, unc) - - def getSample(self): - # ReferenceSample cannot provide a 'getSample' - if IReferenceAnalysis.providedBy(self): - return None - if IDuplicateAnalysis.providedBy(self) \ - or self.portal_type == 'RejectAnalysis': - return self.getAnalysis().aq_parent.getSample() - return self.aq_parent.getSample() - - def getSampleTypeUID(self): - """ - It is a metacolumn - """ - sample = self.getSample() - if sample: - return sample.getSampleType().UID() - return '' + @deprecated("use Analysis.getResultOptions instead") def getResultOptionsFromService(self): + """This method is used to populate catalog values + Returns a list of dictionaries from the ResultOptions field. """ - It is a metacolumn. - Returns a list of dictionaries from the field ResultOptions from the - analysis service. - """ - service = self.getService() - if not service: - return None - return service.getResultOptions() + return self.getResultOptions() def getResultsRange(self, specification=None): - """ Returns the valid results range for this analysis, a - dictionary with the following keys: 'keyword', 'uid', 'min', - 'max', 'error', 'hidemin', 'hidemax', 'rangecomment' - Allowed values for specification='ar', 'client', 'lab', None - If specification is None, the following is the priority to - get the results range: AR > Client > Lab - If no specification available for this analysis, returns {} + """Returns the valid results range for this analysis, a dictionary + with the following keys: 'keyword', 'uid', 'min', 'max ', 'error', + 'hidemin', 'hidemax', 'rangecomment' Allowed values for + specification='ar', 'client', 'lab', None If specification is None, + the following is the priority to get the results range: AR > Client > + Lab If no specification available for this analysis, returns {} """ rr = {} an = self - while an and an.portal_type in ('DuplicateAnalysis', 'RejectAnalysis'): - an = an.getAnalysis() if specification == 'ar' or specification is None: if an.aq_parent and an.aq_parent.portal_type == 'AnalysisRequest': - key = an.getKeyword() rr = an.aq_parent.getResultsRange() rr = [r for r in rr if r.get('keyword', '') == an.getKeyword()] rr = rr[0] if rr and len(rr) > 0 else {} @@ -738,29 +642,24 @@ def getResultsRange(self, specification=None): rr['uid'] = self.UID() return rr - def getResultsRangeNoSpecs(self, specification=None): - """ - This is used as a metacolumn + @deprecated("use Analysis.getResultsRange instead") + def getResultsRangeNoSpecs(self): + """This method is used to populate catalog values """ return self.getResultsRange() def getAnalysisSpecs(self, specification=None): - """ Retrieves the analysis specs to be applied to this analysis. - Allowed values for specification= 'client', 'lab', None - If specification is None, client specification gets priority from - lab specification. - If no specification available for this analysis, returns None + """Retrieves the analysis specs to be applied to this analysis. + Allowed values for specification= 'client', 'lab', None If + specification is None, client specification gets priority from lab + specification. If no specification available for this analysis, + returns None """ sample = self.getSample() - - # No specifications available for ReferenceSamples - if IReferenceSample.providedBy(sample): - return None - sampletype = sample.getSampleType() sampletype_uid = sampletype and sampletype.UID() or '' - bsc = getToolByName(self, 'bika_setup_catalog') + bsc = get_tool('bika_setup_catalog') # retrieves the desired specs if None specs defined if not specification: @@ -770,15 +669,14 @@ def getAnalysisSpecs(self, specification=None): if len(proxies) == 0: # No client specs available, retrieve lab specs - labspecsuid = self.bika_setup.bika_analysisspecs.UID() - proxies = bsc(portal_type = 'AnalysisSpec', - getSampleTypeUID = sampletype_uid) + proxies = bsc(portal_type='AnalysisSpec', + getSampleTypeUID=sampletype_uid) else: specuid = specification == "client" and self.getClientUID() or \ - self.bika_setup.bika_analysisspecs.UID() + self.bika_setup.bika_analysisspecs.UID() proxies = bsc(portal_type='AnalysisSpec', - getSampleTypeUID=sampletype_uid, - getClientUID=specuid) + getSampleTypeUID=sampletype_uid, + getClientUID=specuid) outspecs = None for spec in (p.getObject() for p in proxies): @@ -789,25 +687,21 @@ def getAnalysisSpecs(self, specification=None): return outspecs def calculateResult(self, override=False, cascade=False): - """ Calculates the result for the current analysis if it depends of - other analysis/interim fields. Otherwise, do nothing + """Calculates the result for the current analysis if it depends of + other analysis/interim fields. Otherwise, do nothing """ - if self.getResult() and override == False: + if self.getResult() and override is False: return False - serv = self.getService() - calc = self.getCalculation() if self.getCalculation() \ - else serv.getCalculation() + calc = self.getCalculation() if not calc: return False mapping = {} # Interims' priority order (from low to high): - # Calculation < Analysis Service < Analysis - interims = calc.getInterimFields() + \ - serv.getInterimFields() + \ - self.getInterimFields() + # Calculation < Analysis + interims = calc.getInterimFields() + self.getInterimFields() # Add interims to mapping for i in interims: @@ -816,7 +710,7 @@ def calculateResult(self, override=False, cascade=False): try: ivalue = float(i['value']) mapping[i['keyword']] = ivalue - except: + except (TypeError, ValueError): # Interim not float, abort return False @@ -840,13 +734,13 @@ def calculateResult(self, override=False, cascade=False): udl = dependency.getUpperDetectionLimit() bdl = dependency.isBelowLowerDetectionLimit() adl = dependency.isAboveUpperDetectionLimit() - mapping[key]=result - mapping['%s.%s' % (key, 'RESULT')]=result - mapping['%s.%s' % (key, 'LDL')]=ldl - mapping['%s.%s' % (key, 'UDL')]=udl - mapping['%s.%s' % (key, 'BELOWLDL')]=int(bdl) - mapping['%s.%s' % (key, 'ABOVEUDL')]=int(adl) - except: + mapping[key] = result + mapping['%s.%s' % (key, 'RESULT')] = result + mapping['%s.%s' % (key, 'LDL')] = ldl + mapping['%s.%s' % (key, 'UDL')] = udl + mapping['%s.%s' % (key, 'BELOWLDL')] = int(bdl) + mapping['%s.%s' % (key, 'ABOVEUDL')] = int(adl) + except (TypeError, ValueError): return False # Calculate @@ -854,10 +748,10 @@ def calculateResult(self, override=False, cascade=False): formula = formula.replace('[', '%(').replace(']', ')f') try: formula = eval("'%s'%%mapping" % formula, - {"__builtins__": None, - 'math': math, - 'context': self}, - {'mapping': mapping}) + {"__builtins__": None, + 'math': math, + 'context': self}, + {'mapping': mapping}) result = eval(formula) except TypeError: self.setResult("NA") @@ -865,7 +759,7 @@ def calculateResult(self, override=False, cascade=False): except ZeroDivisionError: self.setResult('0/0') return True - except KeyError as e: + except KeyError: self.setResult("NA") return True @@ -873,81 +767,63 @@ def calculateResult(self, override=False, cascade=False): return True def getPriority(self): - """ get priority from AR + """get priority from AR """ - # this analysis may be a Duplicate or Reference Analysis - CAREFUL - # these types still subclass Analysis. - if self.portal_type != 'Analysis': - return None # this analysis could be in a worksheet or instrument, careful - return self.aq_parent.getPriority() \ - if hasattr(self.aq_parent, 'getPriority') else None + if hasattr(self.aq_parent, 'getPriority'): + return self.aq_parent.getPriority() def getPrice(self): - """ - The function obtains the analysis' price without VAT and without member discount - :returns: the price (without VAT or Member Discount) in decimal format + """The function obtains the analysis' price without VAT and without + member discount + :return: the price (without VAT or Member Discount) in decimal format """ analysis_request = self.aq_parent client = analysis_request.aq_parent if client.getBulkDiscount(): - price = self.getService().getBulkPrice() + price = self.getBulkPrice() else: - price = self.getService().getPrice() + price = self.getPrice() priority = self.getPriority() if priority and priority.getPricePremium() > 0: - price = Decimal(price) + ( - Decimal(price) * Decimal(priority.getPricePremium()) - / 100) + price = Decimal(price) + \ + (Decimal(price) * Decimal(priority.getPricePremium()) / 100) return price def getVATAmount(self): + """Compute the VAT amount without member discount. + :return: the result as a float """ - Compute the VAT amount without member discount. - :returns: the result as a float - """ - vat = self.getService().getVAT() + vat = self.getVAT() price = self.getPrice() - return float(price) * float(vat) / 100 + return Decimal(price) * Decimal(vat) / 100 def getTotalPrice(self): + """Obtain the total price without client's member discount. The function + keeps in mind the client's bulk discount. + :return: the result as a float """ - Obtain the total price without client's member discount. The function keeps in mind the - client's bulk discount. - :returns: the result as a float - """ - return float(self.getPrice()) + float(self.getVATAmount()) + return Decimal(self.getPrice()) + Decimal(self.getVATAmount()) def isInstrumentValid(self): - """ Checks if the instrument selected for this analysis service - is valid. Returns false if an out-of-date or uncalibrated - instrument is assigned. Returns true if the Analysis has - no instrument assigned or is valid. - """ - return self.getInstrument().isValid() \ - if self.getInstrument() else True - - def getDefaultInstrument(self): - """ Returns the default instrument for this analysis according - to its parent analysis service + """Checks if the instrument selected for this analysis is valid. + Returns false if an out-of-date or uncalibrated instrument is + assigned. Returns true if the Analysis has no instrument assigned or + is valid. """ - service = self.getService() - if not service: - return None - return service.getInstrument() \ - if service.getInstrumentEntryOfResults() \ - else None + if self.getInstrument(): + return self.getInstrument().isValid() + return True def isInstrumentAllowed(self, instrument): - """ Checks if the specified instrument can be set for this - analysis, according to the Method and Analysis Service. - If the Analysis Service hasn't set 'Allows instrument entry' - of results, returns always False. Otherwise, checks if the - method assigned is supported by the instrument specified. - Returns false, If the analysis hasn't any method assigned. - NP: The methods allowed for selection are defined at - Analysis Service level. - instrument param can be either an uid or an object + """Checks if the specified instrument can be set for this analysis, + according to the Method and Analysis Service. If the Analysis Service + hasn't set 'Allows instrument entry' of results, returns always + False. Otherwise, checks if the method assigned is supported by the + instrument specified. Returns false, If the analysis hasn't any + method assigned. NP: The methods allowed for selection are defined at + Analysis Service level. instrument param can be either an uid or an + object """ if isinstance(instrument, str): uid = instrument @@ -957,11 +833,10 @@ def isInstrumentAllowed(self, instrument): return uid in self.getAllowedInstruments() def isMethodAllowed(self, method): - """ Checks if the analysis can follow the method specified. - Looks for manually selected methods when AllowManualEntry - is set and instruments methods when AllowInstrumentResultsEntry - is set. - method param can be either an uid or an object + """Checks if the analysis can follow the method specified. Looks for + manually selected methods when AllowManualEntry is set and + instruments methods when AllowInstrumentResultsEntry is set. method + param can be either a uid or an object """ if isinstance(method, str): uid = method @@ -971,15 +846,12 @@ def isMethodAllowed(self, method): return uid in self.getAllowedMethods() def getAllowedMethods(self, onlyuids=True): - """ - Returns the allowed methods for this analysis. If manual - entry of results is set, only returns the methods set - manually. Otherwise (if Instrument Entry Of Results is set) - returns the methods assigned to the instruments allowed for - this Analysis + """Returns the allowed methods for this analysis. If manual entry of + results is set, only returns the methods set manually. Otherwise (if + Instrument Entry Of Results is set) returns the methods assigned to + the instruments allowed for this Analysis """ service = self.getService() - uids = [] if service.getInstrumentEntryOfResults(): uids = [ins.getRawMethod() for ins in service.getInstruments()] @@ -989,75 +861,72 @@ def getAllowedMethods(self, onlyuids=True): uids = service.getRawMethods() if not onlyuids: - uc = getToolByName(self, 'uid_catalog') + uc = get_tool('uid_catalog') meths = [item.getObject() for item in uc(UID=uids)] return meths return uids def getAllowedMethodsAsTuples(self): - """ - This works a a metadata column. - Returns the allowed methods for this analysis. If manual - entry of results is set, only returns the methods set - manually. Otherwise (if Instrument Entry Of Results is set) - returns the methods assigned to the instruments allowed for - this Analysis - @return: a list of tuples as [(UID,Title),(),...] + """This method is used to populate catalog values + Returns the allowed methods for this analysis. If manual entry of + results is set, only returns the methods set manually. Otherwise (if + Instrument Entry Of Results is set) returns the methods assigned to + the instruments allowed for this Analysis + :return: a list of tuples as [(UID,Title),(),...] """ service = self.getService() if not service: return None - result = [] # manual entry of results is set, only returns the methods set manually if service.getInstrumentEntryOfResults(): - result = [ - (ins.getRawMethod(), ins.getMethod().Title()) for ins in - service.getInstruments() if ins.getMethod()] + result = [(ins.getRawMethod(), ins.getMethod().Title()) + for ins in service.getInstruments() if ins.getMethod()] # Otherwise (if Instrument Entry Of Results is set) # returns the methods assigned to the instruments allowed for # this Analysis else: # Get only the methods set manually - result = [ - (method.UID(), method.Title()) for - method in service.getMethods()] + result = [(method.UID(), method.Title()) + for method in service.getMethods()] return result def getAllowedInstruments(self, onlyuids=True): - """ Returns the allowed instruments for this analysis. Gets the - instruments assigned to the allowed methods + """Returns the allowed instruments for this analysis. Gets the + instruments assigned to the allowed methods """ uids = [] service = self.getService() if not service: return None - if service.getInstrumentEntryOfResults() == True: + + if service.getInstrumentEntryOfResults(): uids = service.getRawInstruments() - elif service.getManualEntryOfResults() == True: + elif service.getManualEntryOfResults(): meths = self.getAllowedMethods(False) for meth in meths: uids += meth.getInstrumentUIDs() set(uids) - if onlyuids == False: - uc = getToolByName(self, 'uid_catalog') + if onlyuids is False: + uc = get_tool('uid_catalog') instrs = [item.getObject() for item in uc(UID=uids)] return instrs return uids def getDefaultMethod(self): - """ Returns the default method for this Analysis - according to its current instrument. If the Analysis hasn't - set yet an Instrument, looks to the Service + """Returns the default method for this Analysis according to its + current instrument. If the Analysis hasn't set yet an Instrument, + looks to the Service """ instr = self.getInstrument() \ if self.getInstrument() else self.getDefaultInstrument() return instr.getMethod() if instr else None - def getFormattedResult(self, specs=None, decimalmark='.', sciformat=1, html=True): + def getFormattedResult( + self, specs=None, decimalmark='.', sciformat=1, html=True): """Formatted result: 1. If the result is a detection limit, returns '< LDL' or '> UDL' 2. Print ResultText of matching ResultOptions @@ -1067,12 +936,14 @@ def getFormattedResult(self, specs=None, decimalmark='.', sciformat=1, html=True 5. If the result is below Lower Detection Limit, show 'UDL' 7. Otherwise, render numerical value - specs param is optional. A dictionary as follows: + :param specs: Optional result specifications, a dictionary as follows: {'min': , 'max': , 'error': , 'hidemin': , 'hidemax': } + :param decimalmark: The string to be used as a decimal separator. + default is '.' :param sciformat: 1. The sci notation has to be formatted as aE^+b 2. The sci notation has to be formatted as a·10^b 3. As 2, but with super html entity for exp @@ -1088,15 +959,15 @@ def getFormattedResult(self, specs=None, decimalmark='.', sciformat=1, html=True dl = self.getDetectionLimitOperand() if dl: try: - res = float(result) # required, check if floatable + res = float(result) # required, check if floatable res = drop_trailing_zeros_decimal(res) fdm = formatDecimalMark(res, decimalmark) hdl = cgi.escape(dl) if html else dl return '%s %s' % (hdl, fdm) - except: - logger.warn("The result for the analysis %s is a " - "detection limit, but not floatable: %s" % - (self.id, result)) + except (TypeError, ValueError): + logger.warn( + "The result for the analysis %s is a detection limit, " + "but not floatable: %s" % (self.id, result)) return formatDecimalMark(result, decimalmark=decimalmark) service = self.getService() @@ -1111,26 +982,22 @@ def getFormattedResult(self, specs=None, decimalmark='.', sciformat=1, html=True # 3. If the result is not floatable, return it without being formatted try: result = float(result) - except: + except (TypeError, ValueError): return formatDecimalMark(result, decimalmark=decimalmark) # 4. If the analysis specs has enabled hidemin or hidemax and the # result is out of range, render result as 'max' - belowmin = False - abovemax = False specs = specs if specs else self.getResultsRange() hidemin = specs.get('hidemin', '') hidemax = specs.get('hidemax', '') try: belowmin = hidemin and result < float(hidemin) or False - except: + except (TypeError, ValueError): belowmin = False - pass try: abovemax = hidemax and result > float(hidemax) or False - except: + except (TypeError, ValueError): abovemax = False - pass # 4.1. If result is below min and hidemin enabled, return '.' -> '123354.1' + """This function adds a new item to the string field + ReflexRuleActionsTriggered. From the field: Reflex rule triggered + actions from which the current analysis is responsible of. Separated + by '|' + :param text: is a str object with the format '.' -> + '123354.1' """ old = self.getReflexRuleActionsTriggered() self.setReflexRuleActionsTriggered(old + text + '|') def isVerifiable(self): - """ - Checks it the current analysis can be verified. This is, its not a + """Checks it the current analysis can be verified. This is, its not a cancelled analysis and has no dependenant analyses not yet verified - :returns: True or False + :return: True or False """ # Check if the analysis is active - workflow = getToolByName(self, "portal_workflow") + workflow = get_tool("portal_workflow") objstate = workflow.getInfoFor(self, 'cancellation_state', 'active') if objstate == "cancelled": return False @@ -1272,11 +1141,11 @@ def isUserAllowedToVerify(self, member): function only returns if the user can verify the analysis, but not if the analysis is ready to be verified (see isVerifiable) :member: user to be tested - :returns: true or false + :return: true or false """ # Check if the user has "Bika: Verify" privileges username = member.getUserName() - allowed = api.user.has_permission(VerifyPermission, username=username) + allowed = has_permission(VerifyPermission, username=username) if not allowed: return False @@ -1289,54 +1158,55 @@ def isUserAllowedToVerify(self, member): if self_submitted and not selfverification: return False - #Checking verifiability depending on multi-verification type of bika_setup - if self.bika_setup.getNumberOfRequiredVerifications()>1: - mv_type=self.bika_setup.getTypeOfmultiVerification() - #If user verified before and self_multi_disabled, then return False - if mv_type=='self_multi_disabled' and self.wasVerifiedByUser(username): + # Checking verifiability depending on multi-verification type of + # bika_setup + if self.bika_setup.getNumberOfRequiredVerifications() > 1: + mv_type = self.bika_setup.getTypeOfmultiVerification() + # If user verified before and self_multi_disabled, then return False + if mv_type == 'self_multi_disabled' and self.wasVerifiedByUser( + username): return False - # If user is the last verificator and consecutively multi-verification + # If user is the last verificator and consecutively + # multi-verification # is disabled, then return False - # Comparing was added just to check if this method is called before/after + # Comparing was added just to check if this method is called + # before/after # verification - elif mv_type=='self_multi_not_cons' and username==self.getLastVerificator() and \ - self.getNumberOfVerifications() 1: @@ -1403,24 +1269,89 @@ def getWorksheetUID(self): return worksheet[0].UID() return '' - def getParentURL(self): + def getRequestID(self): + """Used to populate catalog values. """ - This works as a metacolumn + ar = self.aq_parent + if ar: + return ar.getRequestID() + + def getParentURL(self): + """This method is used to populate catalog values This function returns the analysis' parent URL """ return self.aq_parent.absolute_url_path() - def getClientTitle(self): + def getClientUID(self): + """Used to populate catalog values. """ - This works as a column + client = self.aq_parent.getClient() + if client: + return client.UID() + + def getClientTitle(self): + """Used to populate catalog values. """ - return self.aq_parent.aq_parent.Title() + client = self.aq_parent.getClient() + if client: + return client.Title() def getClientURL(self): + """This method is used to populate catalog values + """ + client = self.aq_parent.getClient() + if client: + return client.absolute_url_path() + + def getClientOrderNumber(self): + """Used to populate catalog values. + """ + client = self.aq_parent.getClient() + if client: + return client.getClientOrderNumber() + + # The DateReceived of the sample associated with this Analysis. + def getDateReceived(self): + """Used to populate catalog values. + """ + sample = self.getSample() + if sample: + getTransitionDate(self, 'receive') + + def getDateSampled(self): + """Used to populate catalog values. + """ + sample = self.getSample() + if sample: + getTransitionDate(self, 'sample') + + def getInstrumentValid(self): + """Used to populate catalog values. + """ + return self.isInstrumentValid() + + def getSample(self): + """Return the Sample associated with this Analysis """ - This works as a column + return self.aq_parent.getSample() + + def getSampleTypeUID(self): + """Used to populate catalog values. """ - return self.aq_parent.aq_parent.absolute_url_path() + sample = self.getSample() + if sample: + sampletype = sample.getSampleType() + if sampletype: + return sampletype.UID() + + def getSamplePointUID(self): + """Used to populate catalog values. + """ + sample = self.getSample() + if sample: + samplepoint = sample.getSamplePoint() + if samplepoint: + return samplepoint.UID() def getSamplePartitionID(self): """ @@ -1433,8 +1364,7 @@ def getSamplePartitionID(self): return '' def getMethodURL(self): - """ - It is used as a metacolumn. + """This method is used to populate catalog values Returns the method url if this analysis has a method assigned """ method = self.getMethod() @@ -1443,8 +1373,7 @@ def getMethodURL(self): return '' def getMethodTitle(self): - """ - It is used as a metacolumn. + """This method is used to populate catalog values Returns the method title if this analysis has a method assigned """ method = self.getMethod() @@ -1453,21 +1382,16 @@ def getMethodTitle(self): return '' def getServiceDefaultInstrumentUID(self): - """ - It is used as a metacolumn. + """This method is used to populate catalog values Returns the default service's instrument UID """ - service = self.getService() - if not service: - return None - ins = service.getInstrument() + ins = self.getInstrument() if ins: return ins.UID() return '' def getServiceDefaultInstrumentTitle(self): - """ - It is used as a metacolumn. + """This method is used to populate catalog values Returns the default service's instrument UID """ service = self.getService() @@ -1479,8 +1403,7 @@ def getServiceDefaultInstrumentTitle(self): return '' def getServiceDefaultInstrumentURL(self): - """ - It is used as a metacolumn. + """This method is used to populate catalog values Returns the default service's instrument UID """ service = self.getService() @@ -1492,71 +1415,97 @@ def getServiceDefaultInstrumentURL(self): return '' def hasAttachment(self): - """ - It is used as a metacolumn. - Checks if the object has attachments or not. - Returns a boolean. + """This method is used to populate catalog values + Checks if the object has attachments or not. Returns a boolean. """ attachments = self.getAttachment() return len(attachments) > 0 + def _reflex_rule_process(self, wf_action): + """This function does all the reflex rule process. + :param wf_action: is a string containing the workflow action triggered + """ + workflow = get_tool('portal_workflow') + # Check out if the analysis has any reflex rule bound to it. + # First we have get the analysis' method because the Reflex Rule + # objects are related to a method. + a_method = self.getMethod() + # After getting the analysis' method we have to get all Reflex Rules + # related to that method. + if a_method: + all_rrs = a_method.getBackReferences('ReflexRuleMethod') + # Once we have all the Reflex Rules with the same method as the + # analysis has, it is time to get the rules that are bound to the + # same analysis service that is using the analysis. + for rule in all_rrs: + state = workflow.getInfoFor(rule, 'inactive_state') + if state == 'inactive': + continue + # Getting the rules to be done from the reflex rule taking + # in consideration the analysis service, the result and + # the state change + action_row = rule.getActionReflexRules(self, wf_action) + # Once we have the rules, the system has to execute its + # instructions if the result has the expected result. + doReflexRuleAction(self, action_row) + def guard_sample_transition(self): - workflow = getToolByName(self, "portal_workflow") - if workflow.getInfoFor(self, "cancellation_state", "active") == "cancelled": + workflow = get_tool("portal_workflow") + state = workflow.getInfoFor(self, "cancellation_state", "active") + if state == "cancelled": return False return True def guard_retract_transition(self): - workflow = getToolByName(self, "portal_workflow") - if workflow.getInfoFor(self, "cancellation_state", "active") == "cancelled": + workflow = get_tool("portal_workflow") + state = workflow.getInfoFor(self, "cancellation_state", "active") + if state == "cancelled": return False return True def guard_sample_prep_transition(self): - sample = self.aq_parent.getSample() + sample = self.getSample() return sample.guard_sample_prep_transition() def guard_sample_prep_complete_transition(self): - sample = self.aq_parent.getSample() + sample = self.getSample() return sample.guard_sample_prep_complete_transition() def guard_receive_transition(self): - workflow = getToolByName(self, "portal_workflow") - if workflow.getInfoFor(self, "cancellation_state", "active") == "cancelled": + workflow = get_tool("portal_workflow") + state = workflow.getInfoFor(self, "cancellation_state", "active") + if state == "cancelled": return False return True def guard_publish_transition(self): - workflow = getToolByName(self, "portal_workflow") - if workflow.getInfoFor(self, "cancellation_state", "active") == "cancelled": + workflow = get_tool("portal_workflow") + state = workflow.getInfoFor(self, "cancellation_state", "active") + if state == "cancelled": return False return True def guard_import_transition(self): - workflow = getToolByName(self, "portal_workflow") - if workflow.getInfoFor(self, "cancellation_state", "active") == "cancelled": + workflow = get_tool("portal_workflow") + state = workflow.getInfoFor(self, "cancellation_state", "active") + if state == "cancelled": return False return True def guard_attach_transition(self): - if self.portal_type in ("Analysis", - "ReferenceAnalysis", - "DuplicateAnalysis"): - if not self.getAttachment(): - service = self.getService() - if service.getAttachmentOption() == "r": - return False + if not self.getAttachment(): + service = self.getService() + if service.getAttachmentOption() == "r": + return False return True def guard_verify_transition(self): + """Checks if the verify transition can be performed to the current + Analysis by the current user depending on the user roles, as well as + the status of the analysis + :return: Boolean """ - Checks if the verify transition can be performed to the current - Analysis by the current user depending on the user roles, as - well as the status of the analysis - :returns: true or false - """ - mtool = getToolByName(self, "portal_membership") - checkPermission = mtool.checkPermission + mtool = get_tool("portal_membership") # Check if the analysis is in a "verifiable" state if self.isVerifiable(): # Check if the user can verify the analysis @@ -1565,80 +1514,48 @@ def guard_verify_transition(self): return False def guard_assign_transition(self): - workflow = getToolByName(self, "portal_workflow") - if workflow.getInfoFor(self, 'cancellation_state', 'active') == "cancelled": + workflow = get_tool("portal_workflow") + state = workflow.getInfoFor(self, 'cancellation_state', 'active') + if state == "cancelled": return False return True def guard_unassign_transition(self): - """ Check permission against parent worksheet + """Check permission against parent worksheet """ - workflow = getToolByName(self, "portal_workflow") - mtool = getToolByName(self, "portal_membership") + workflow = get_tool("portal_workflow") + mtool = get_tool("portal_membership") ws = self.getBackReferences("WorksheetAnalysis") if not ws: return False ws = ws[0] - if workflow.getInfoFor(ws, "cancellation_state", "") == "cancelled": + state = workflow.getInfoFor(ws, "cancellation_state", "") + if state == "cancelled": return False if mtool.checkPermission(Unassign, ws): return True return False def workflow_script_receive(self): - workflow = getToolByName(self, "portal_workflow") - if workflow.getInfoFor(self, 'cancellation_state', 'active') == "cancelled": - return False - # DuplicateAnalysis doesn't have analysis_workflow. - if self.portal_type == "DuplicateAnalysis": - return if skip(self, "receive"): return + workflow = get_tool("portal_workflow") + state = workflow.getInfoFor(self, 'cancellation_state', 'active') + if state == "cancelled": + return False self.updateDueDate() self.reindexObject() - def _reflex_rule_process(self, wf_action): - """ - This function does all the reflex rule process. - :wf_action: is a variable containing a string with the workflow - action triggered - """ - workflow = getToolByName(self, 'portal_workflow') - # Check out if the analysis has any reflex rule bound to it. - # First we have get the analysis' method because the Reflex Rule - # objects are related to a method. - a_method = self.getMethod() - # After getting the analysis' method we have to get all Reflex Rules - # related to that method. - if a_method: - all_rrs = a_method.getBackReferences('ReflexRuleMethod') - # Once we have all the Reflex Rules with the same method as the - # analysis has, it is time to get the rules that are bound to the - # same analysis service that is using the analysis. - rrs = [] - for rule in all_rrs: - if workflow.getInfoFor(rule, 'inactive_state') == 'inactive': - continue - # Getting the rules to be done from the reflex rule taking - # in consideration the analysis service, the result and - # the state change - action_row = rule.getActionReflexRules(self, wf_action) - # Once we have the rules, the system has to execute its - # instructions if the result has the expected result. - doReflexRuleAction(self, action_row) - def workflow_script_submit(self): - # DuplicateAnalysis doesn't have analysis_workflow. - if self.portal_type == "DuplicateAnalysis": - return if skip(self, "submit"): return - workflow = getToolByName(self, "portal_workflow") - if workflow.getInfoFor(self, 'cancellation_state', 'active') == "cancelled": + workflow = get_tool("portal_workflow") + state = workflow.getInfoFor(self, 'cancellation_state', 'active') + if state == "cancelled": return False ar = self.aq_parent # Dependencies are submitted already, ignore them. - #------------------------------------------------- + # ------------------------------------------------- # Submit our dependents # Need to check for result and status of dependencies first dependents = self.getDependents() @@ -1658,9 +1575,9 @@ def workflow_script_submit(self): if can_submit: dependencies = dependent.getDependencies() for dependency in dependencies: - if workflow.getInfoFor(dependency, "review_state") in \ - ("to_be_sampled", "to_be_preserved", - "sample_due", "sample_received",): + state = workflow.getInfoFor(dependency, "review_state") + if state in ("to_be_sampled", "to_be_preserved", + "sample_due", "sample_received"): can_submit = False if can_submit: workflow.doActionFor(dependent, "submit") @@ -1671,27 +1588,27 @@ def workflow_script_submit(self): if not skip(ar, "submit", peek=True): all_submitted = True for a in ar.getAnalyses(): - if a.review_state in \ - ("to_be_sampled", "to_be_preserved", - "sample_due", "sample_received",): + if a.review_state in ("to_be_sampled", "to_be_preserved", + "sample_due", "sample_received"): all_submitted = False break if all_submitted: workflow.doActionFor(ar, "submit") - # If assigned to a worksheet and all analyses on the worksheet have been submitted, + # If assigned to a worksheet and all analyses on the worksheet have + # been submitted, # then submit the worksheet. ws = self.getBackReferences("WorksheetAnalysis") if ws: ws = ws[0] - # if the worksheet analyst is not assigned, the worksheet can't be transitioned. + # if the worksheet analyst is not assigned, the worksheet can't + # be transitioned. if ws.getAnalyst() and not skip(ws, "submit", peek=True): all_submitted = True for a in ws.getAnalyses(): if workflow.getInfoFor(a, "review_state") in \ - ("to_be_sampled", "to_be_preserved", - "sample_due", "sample_received", "assigned",): - # Note: referenceanalyses and duplicateanalyses can still have review_state = "assigned". + ("to_be_sampled", "to_be_preserved", + "sample_due", "sample_received", "assigned",): all_submitted = False break if all_submitted: @@ -1707,8 +1624,8 @@ def workflow_script_submit(self): dependencies = self.getDependencies() for dependency in dependencies: if workflow.getInfoFor(dependency, "review_state") in \ - ("to_be_sampled", "to_be_preserved", "sample_due", - "sample_received", "attachment_due",): + ("to_be_sampled", "to_be_preserved", "sample_due", + "sample_received", "attachment_due",): can_attach = False if can_attach: try: @@ -1720,79 +1637,62 @@ def workflow_script_submit(self): self.reindexObject() def workflow_script_retract(self): - # DuplicateAnalysis doesn't have analysis_workflow. - if self.portal_type == "DuplicateAnalysis": - return if skip(self, "retract"): return ar = self.aq_parent - workflow = getToolByName(self, "portal_workflow") - if workflow.getInfoFor(self, 'cancellation_state', 'active') == "cancelled": + workflow = get_tool("portal_workflow") + state = workflow.getInfoFor(self, 'cancellation_state', 'active') + if state == "cancelled": return False - # We'll assign the new analysis to this same worksheet, if any. - ws = self.getBackReferences("WorksheetAnalysis") - if ws: - ws = ws[0] # Rename the analysis to make way for it's successor. # Support multiple retractions by renaming to *-0, *-1, etc parent = self.aq_parent kw = self.getKeyword() analyses = [x for x in parent.objectValues("Analysis") if x.getId().startswith(self.getId())] - # LIMS-1290 - Analyst must be able to retract, which creates a new Analysis. - parent._verifyObjectPaste = str # I cancel the permission check with this. + # LIMS-1290 - Analyst must be able to retract, which creates a new + # Analysis. So, _verifyObjectPaste permission check must be cancelled: + parent._verifyObjectPaste = str parent.manage_renameObject(kw, "{0}-{1}".format(kw, len(analyses))) delattr(parent, '_verifyObjectPaste') - # Create new analysis and copy values from retracted - analysis = _createObjectByType("Analysis", parent, kw) - analysis.edit( - Service=self.getService(), - Calculation=self.getCalculation(), - InterimFields=self.getInterimFields(), - ResultDM=self.getResultDM(), - Retested=True, # True - MaxTimeAllowed=self.getMaxTimeAllowed(), - DueDate=self.getDueDate(), - Duration=self.getDuration(), - ReportDryMatter=self.getReportDryMatter(), - Analyst=self.getAnalyst(), - Instrument=self.getInstrument(), - SamplePartition=self.getSamplePartition()) - analysis.setDetectionLimitOperand(self.getDetectionLimitOperand()) - analysis.setResult(self.getResult()) - # Required number of verifications - reqvers = self.getNumberOfRequiredVerifications() - analysis.setNumberOfRequiredVerifications(reqvers) - analysis.unmarkCreationFlag() - - # zope.event.notify(ObjectInitializedEvent(analysis)) - changeWorkflowState(analysis, - "bika_analysis_workflow", "sample_received") + # Create new analysis from the retracted self + analysis = create_analysis(parent, self) + changeWorkflowState( + analysis, "bika_analysis_workflow", "sample_received") + # We'll assign the new analysis to this same worksheet, if any. + ws = self.getBackReferences("WorksheetAnalysis") if ws: + ws = ws[0] ws.addAnalysis(analysis) analysis.reindexObject() # retract our dependencies - if not "retract all dependencies" in self.REQUEST["workflow_skiplist"]: + if "retract all dependencies" not in self.REQUEST["workflow_skiplist"]: for dependency in self.getDependencies(): if not skip(dependency, "retract", peek=True): - if workflow.getInfoFor(dependency, "review_state") in ("attachment_due", "to_be_verified",): + state = workflow.getInfoFor(dependency, "review_state") + if state in ("attachment_due", "to_be_verified",): # (NB: don"t retract if it"s verified) workflow.doActionFor(dependency, "retract") # Retract our dependents for dep in self.getDependents(): if not skip(dep, "retract", peek=True): - if workflow.getInfoFor(dep, "review_state") not in ("sample_received", "retracted"): - self.REQUEST["workflow_skiplist"].append("retract all dependencies") + state = workflow.getInfoFor(dep, "review_state") + if state not in ("sample_received", "retracted"): + self.REQUEST["workflow_skiplist"].append( + "retract all dependencies") # just return to "received" state, no cascade workflow.doActionFor(dep, 'retract') - self.REQUEST["workflow_skiplist"].remove("retract all dependencies") + self.REQUEST["workflow_skiplist"].remove( + "retract all dependencies") # Escalate action to the parent AR if not skip(ar, "retract", peek=True): if workflow.getInfoFor(ar, "review_state") == "sample_received": skip(ar, "retract") else: - if not "retract all analyses" in self.REQUEST["workflow_skiplist"]: - self.REQUEST["workflow_skiplist"].append("retract all analyses") + if "retract all analyses" \ + not in self.REQUEST["workflow_skiplist"]: + self.REQUEST["workflow_skiplist"].append( + "retract all analyses") workflow.doActionFor(ar, "retract") # Escalate action to the Worksheet (if it's on one). ws = self.getBackReferences("WorksheetAnalysis") @@ -1802,8 +1702,10 @@ def workflow_script_retract(self): if workflow.getInfoFor(ws, "review_state") == "open": skip(ws, "retract") else: - if not "retract all analyses" in self.REQUEST['workflow_skiplist']: - self.REQUEST["workflow_skiplist"].append("retract all analyses") + if "retract all analyses" \ + not in self.REQUEST['workflow_skiplist']: + self.REQUEST["workflow_skiplist"].append( + "retract all analyses") try: workflow.doActionFor(ws, "retract") except WorkflowException: @@ -1825,30 +1727,30 @@ def workflow_script_retract(self): self.reindexObject() def workflow_script_verify(self): - # DuplicateAnalysis doesn't have analysis_workflow. - if self.portal_type == "DuplicateAnalysis": - return if skip(self, "verify"): return - workflow = getToolByName(self, "portal_workflow") - if workflow.getInfoFor(self, 'cancellation_state', 'active') == "cancelled": + workflow = get_tool("portal_workflow") + state = workflow.getInfoFor(self, 'cancellation_state', 'active') + if state == "cancelled": return False # Do all the reflex rules process self._reflex_rule_process('verify') - # If all analyses in this AR are verified - # escalate the action to the parent AR + # If all analyses in this AR are verified escalate the action to the + # parent AR ar = self.aq_parent if not skip(ar, "verify", peek=True): all_verified = True for a in ar.getAnalyses(): - if a.review_state in \ - ("to_be_sampled", "to_be_preserved", "sample_due", - "sample_received", "attachment_due", "to_be_verified"): + if a.review_state in ("to_be_sampled", "to_be_preserved", + "sample_due", "sample_received", + "attachment_due", "to_be_verified"): all_verified = False break if all_verified: - if not "verify all analyses" in self.REQUEST['workflow_skiplist']: - self.REQUEST["workflow_skiplist"].append("verify all analyses") + if "verify all analyses" \ + not in self.REQUEST['workflow_skiplist']: + self.REQUEST["workflow_skiplist"].append( + "verify all analyses") workflow.doActionFor(ar, "verify") # If this is on a worksheet and all it's other analyses are verified, # then verify the worksheet. @@ -1856,32 +1758,32 @@ def workflow_script_verify(self): if ws: ws = ws[0] ws_state = workflow.getInfoFor(ws, "review_state") - if ws_state == "to_be_verified" and not skip(ws, "verify", peek=True): + if ws_state == "to_be_verified" and not skip(ws, "verify", + peek=True): all_verified = True for a in ws.getAnalyses(): - if workflow.getInfoFor(a, "review_state") in \ - ("to_be_sampled", "to_be_preserved", "sample_due", - "sample_received", "attachment_due", "to_be_verified", - "assigned"): - # Note: referenceanalyses and duplicateanalyses can - # still have review_state = "assigned". + state = workflow.getInfoFor(a, "review_state") + if state in ("to_be_sampled", "to_be_preserved", + "sample_due", "sample_received", + "attachment_due", "to_be_verified", + "assigned"): all_verified = False break if all_verified: - if not "verify all analyses" in self.REQUEST['workflow_skiplist']: - self.REQUEST["workflow_skiplist"].append("verify all analyses") + if "verify all analyses" \ + not in self.REQUEST['workflow_skiplist']: + self.REQUEST["workflow_skiplist"].append( + "verify all analyses") workflow.doActionFor(ws, "verify") self.reindexObject() def workflow_script_publish(self): - workflow = getToolByName(self, "portal_workflow") - if workflow.getInfoFor(self, 'cancellation_state', 'active') == "cancelled": - return False - # DuplicateAnalysis doesn't have analysis_workflow. - if self.portal_type == "DuplicateAnalysis": - return if skip(self, "publish"): return + workflow = get_tool("portal_workflow") + state = workflow.getInfoFor(self, 'cancellation_state', 'active') + if state == "cancelled": + return False endtime = DateTime() self.setDateAnalysisPublished(endtime) starttime = self.aq_parent.getDateReceived() @@ -1906,63 +1808,58 @@ def workflow_script_publish(self): def workflow_script_cancel(self): if skip(self, "cancel"): return - # DuplicateAnalysis doesn't have analysis_workflow. - if self.portal_type == "DuplicateAnalysis": - return - workflow = getToolByName(self, "portal_workflow") + workflow = get_tool("portal_workflow") # If it is assigned to a worksheet, unassign it. - if workflow.getInfoFor(self, 'worksheetanalysis_review_state') == 'assigned': + state = workflow.getInfoFor(self, 'worksheetanalysis_review_state') + if state == 'assigned': ws = self.getBackReferences("WorksheetAnalysis")[0] skip(self, "cancel", unskip=True) ws.removeAnalysis(self) self.reindexObject() def workflow_script_reject(self): - # DuplicateAnalysis doesn't have analysis_workflow. - if self.portal_type == "DuplicateAnalysis": + if skip(self, "reject"): return - workflow = getToolByName(self, "portal_workflow") + workflow = get_tool("portal_workflow") # If it is assigned to a worksheet, unassign it. - if workflow.getInfoFor(self, 'worksheetanalysis_review_state') ==\ - 'assigned': + state = workflow.getInfoFor(self, 'worksheetanalysis_review_state') + if state == 'assigned': ws = self.getBackReferences("WorksheetAnalysis")[0] ws.removeAnalysis(self) self.reindexObject() def workflow_script_attach(self): - # DuplicateAnalysis doesn't have analysis_workflow. - if self.portal_type == "DuplicateAnalysis": - return if skip(self, "attach"): return - workflow = getToolByName(self, "portal_workflow") - # If all analyses in this AR have been attached - # escalate the action to the parent AR + workflow = get_tool("portal_workflow") + # If all analyses in this AR have been attached escalate the action + # to the parent AR ar = self.aq_parent - ar_state = workflow.getInfoFor(ar, "review_state") - if ar_state == "attachment_due" and not skip(ar, "attach", peek=True): + state = workflow.getInfoFor(ar, "review_state") + if state == "attachment_due" and not skip(ar, "attach", peek=True): can_attach = True for a in ar.getAnalyses(): - if a.review_state in \ - ("to_be_sampled", "to_be_preserved", - "sample_due", "sample_received", "attachment_due",): + if a.review_state in ("to_be_sampled", "to_be_preserved", + "sample_due", "sample_received", + "attachment_due"): can_attach = False break if can_attach: workflow.doActionFor(ar, "attach") - # If assigned to a worksheet and all analyses on the worksheet have been attached, - # then attach the worksheet. + # If assigned to a worksheet and all analyses on the worksheet have + # been attached, then attach the worksheet. ws = self.getBackReferences('WorksheetAnalysis') if ws: ws = ws[0] ws_state = workflow.getInfoFor(ws, "review_state") - if ws_state == "attachment_due" and not skip(ws, "attach", peek=True): + if ws_state == "attachment_due" \ + and not skip(ws, "attach", peek=True): can_attach = True for a in ws.getAnalyses(): - if workflow.getInfoFor(a, "review_state") in \ - ("to_be_sampled", "to_be_preserved", "sample_due", - "sample_received", "attachment_due", "assigned",): - # Note: referenceanalyses and duplicateanalyses can still have review_state = "assigned". + state = workflow.getInfoFor(a, "review_state") + if state in ("to_be_sampled", "to_be_preserved", + "sample_due", "sample_received", + "attachment_due", "assigned"): can_attach = False break if can_attach: @@ -1970,15 +1867,11 @@ def workflow_script_attach(self): self.reindexObject() def workflow_script_assign(self): - # DuplicateAnalysis doesn't have analysis_workflow. - if self.portal_type == "DuplicateAnalysis": - return if skip(self, "assign"): return - workflow = getToolByName(self, "portal_workflow") - rc = getToolByName(self, REFERENCE_CATALOG) - wsUID = self.REQUEST["context_uid"] - ws = rc.lookupObject(wsUID) + workflow = get_tool("portal_workflow") + uc = get_tool("uid_catalog") + ws = uc(UID=self.REQUEST["context_uid"])[0].getObject() # retract the worksheet to 'open' ws_state = workflow.getInfoFor(ws, "review_state") if ws_state != "open": @@ -1986,36 +1879,37 @@ def workflow_script_assign(self): self.REQUEST["workflow_skiplist"] = ["retract all analyses", ] else: self.REQUEST["workflow_skiplist"].append("retract all analyses") - allowed_transitions = [t["id"] for t in workflow.getTransitionsFor(ws)] + allowed_transitions = \ + [t["id"] for t in workflow.getTransitionsFor(ws)] if "retract" in allowed_transitions: workflow.doActionFor(ws, "retract") # If all analyses in this AR have been assigned, # escalate the action to the parent AR if not skip(self, "assign", peek=True): - if not self.getAnalyses(worksheetanalysis_review_state="unassigned"): + if not self.getAnalyses( + worksheetanalysis_review_state="unassigned"): try: - allowed_transitions = [t["id"] for t in workflow.getTransitionsFor(self)] + allowed_transitions = \ + [t["id"] for t in workflow.getTransitionsFor(self)] if "assign" in allowed_transitions: workflow.doActionFor(self, "assign") - except: + except WorkflowException: logger.error( "assign action failed for analysis %s" % self.getId()) self.reindexObject() def workflow_script_unassign(self): - # DuplicateAnalysis doesn't have analysis_workflow. - if self.portal_type == "DuplicateAnalysis": - return if skip(self, "unassign"): return - workflow = getToolByName(self, "portal_workflow") - rc = getToolByName(self, REFERENCE_CATALOG) - wsUID = self.REQUEST["context_uid"] - ws = rc.lookupObject(wsUID) + workflow = get_tool("portal_workflow") + uc = get_tool("uid_catalog") + ws = uc(UID=self.REQUEST["context_uid"])[0].getObject() # Escalate the action to the parent AR if it is assigned # Note: AR adds itself to the skiplist so we have to take it off again - # to allow multiple promotions/demotions (maybe by more than one instance). - if workflow.getInfoFor(self, "worksheetanalysis_review_state") == "assigned": + # to allow multiple promotions/demotions (maybe by more than + # one instance). + state = workflow.getInfoFor(self, "worksheetanalysis_review_state") + if state == "assigned": workflow.doActionFor(self, "unassign") skip(self, "unassign", unskip=True) # If it has been duplicated on the worksheet, delete the duplicates. @@ -2023,9 +1917,8 @@ def workflow_script_unassign(self): for dup in dups: ws.removeAnalysis(dup) # May need to promote the Worksheet's review_state - # if all other analyses are at a higher state than this one was. + # if all other analyses are at a higher state than this one was. # (or maybe retract it if there are no analyses left) - # Note: duplicates, controls and blanks have 'assigned' as a review_state. can_submit = True can_attach = True can_verify = True @@ -2033,31 +1926,33 @@ def workflow_script_unassign(self): for a in ws.getAnalyses(): ws_empty = False a_state = workflow.getInfoFor(a, "review_state") - if a_state in \ - ("to_be_sampled", "to_be_preserved", "assigned", - "sample_due", "sample_received",): + if a_state in ("to_be_sampled", "to_be_preserved", "assigned", + "sample_due", "sample_received"): can_submit = False else: if not ws.getAnalyst(): can_submit = False - if a_state in \ - ("to_be_sampled", "to_be_preserved", "assigned", - "sample_due", "sample_received", "attachment_due",): + if a_state in ("to_be_sampled", "to_be_preserved", "assigned", + "sample_due", "sample_received", "attachment_due"): can_attach = False - if a_state in \ - ("to_be_sampled", "to_be_preserved", "assigned", "sample_due", - "sample_received", "attachment_due", "to_be_verified",): + if a_state in ("to_be_sampled", "to_be_preserved", "assigned", + "sample_due", "sample_received", "attachment_due", + "to_be_verified"): can_verify = False if not ws_empty: - # Note: WS adds itself to the skiplist so we have to take it off again - # to allow multiple promotions (maybe by more than one instance). - if can_submit and workflow.getInfoFor(ws, "review_state") == "open": + # Note: WS adds itself to the skiplist so we have to take it + # off again to allow multiple promotions (maybe by more than + # one instance). + state = workflow.getInfoFor(ws, "review_state") + if can_submit and state == "open": workflow.doActionFor(ws, "submit") skip(ws, 'unassign', unskip=True) - if can_attach and workflow.getInfoFor(ws, "review_state") == "attachment_due": + state = workflow.getInfoFor(ws, "review_state") + if can_attach and state == "attachment_due": workflow.doActionFor(ws, "attach") skip(ws, 'unassign', unskip=True) - if can_verify and workflow.getInfoFor(ws, "review_state") == "to_be_verified": + state = workflow.getInfoFor(ws, "review_state") + if can_verify and state == "to_be_verified": self.REQUEST['workflow_skiplist'].append("verify all analyses") workflow.doActionFor(ws, "verify") skip(ws, 'unassign', unskip=True) diff --git a/bika/lims/content/analysisservice.py b/bika/lims/content/analysisservice.py index b164cc2675..967246d82d 100644 --- a/bika/lims/content/analysisservice.py +++ b/bika/lims/content/analysisservice.py @@ -7,15 +7,13 @@ import sys -import transaction +import transaction from AccessControl import ClassSecurityInfo from Products.ATExtensions.ateapi import RecordsField from Products.Archetypes.Registry import registerField -from Products.Archetypes.public import DisplayList, ReferenceField, \ - BooleanField, BooleanWidget, Schema, registerType, SelectionWidget, \ - MultiSelectionWidget -from Products.Archetypes.references import HoldingReference +from Products.Archetypes.public import DisplayList, BooleanField, \ + BooleanWidget, Schema, registerType, SelectionWidget, MultiSelectionWidget from Products.CMFCore.WorkflowCore import WorkflowException from Products.CMFCore.utils import getToolByName from bika.lims import PMF, bikaMessageFactory as _ @@ -23,14 +21,16 @@ from bika.lims.browser.widgets.partitionsetupwidget import PartitionSetupWidget from bika.lims.browser.widgets.referencewidget import ReferenceWidget from bika.lims.config import PROJECTNAME -from bika.lims.content.baseanalysis import schema as BaseAnalysisSchema, \ - BaseAnalysis +from bika.lims.content.baseanalysis import BaseAnalysis +from bika.lims.content.baseanalysis import schema as BaseAnalysisSchema from bika.lims.interfaces import IAnalysisService, IHaveIdentifiers from bika.lims.utils import to_utf8 as _c from magnitude import mg +from plone.api.portal import get_tool from plone.indexer import indexer from zope.interface import implements + def getContainers(instance, minvol=None, allow_blank=True, @@ -186,7 +186,9 @@ def sortable_title_with_sort_key(instance): return "{:010.3f}{}".format(sort_key, instance.Title()) return instance.Title() - +# If this flag is true, then analyses created from this service will be linked +# to their own Sample Partition, and no other analyses will be linked to that +# partition. Separate = BooleanField( 'Separate', schemata='Container and Preservation', @@ -199,6 +201,9 @@ def sortable_title_with_sort_key(instance): ) ) +# The preservation for this service; If multiple services share the same +# preservation, then it's possible that they can be performed on the same +# sample partition. Preservation = UIDReferenceField( 'Preservation', schemata='Container and Preservation', @@ -218,6 +223,9 @@ def sortable_title_with_sort_key(instance): ) ) +# The container or containertype for this service's analyses can be specified. +# If multiple services share the same container or containertype, then it's +# possible that their analyses can be performed on the same partitions Container = UIDReferenceField( 'Container', schemata='Container and Preservation', @@ -238,6 +246,10 @@ def sortable_title_with_sort_key(instance): ) ) +# This is a list of dictionaries which contains the PartitionSetupWidget +# settings. This is used to decide how many distinct physical partitions +# will be created, which containers/preservations they will use, and which +# analyases can be performed on each partition. PartitionSetup = PartitionSetupField( 'PartitionSetup', schemata='Container and Preservation', @@ -249,6 +261,24 @@ def sortable_title_with_sort_key(instance): ) ) +# Allow/Disallow to set the calculation manually +# Behavior controlled by javascript depending on Instruments field: +# - If no instruments available, hide and uncheck +# - If at least one instrument selected then checked, but not readonly +# See browser/js/bika.lims.analysisservice.edit.js +UseDefaultCalculation = BooleanField( + 'UseDefaultCalculation', + schemata="Method", + default=True, + widget=BooleanWidget( + label=_("Use default calculation"), + description=_( + "Select if the calculation to be used is the calculation set by " + "default in the default method. If unselected, the calculation " + "can be selected manually"), + ) +) + # Manual methods associated to the AS # List of methods capable to perform the Analysis Service. The # Methods selected here are displayed in the Analysis Request @@ -279,34 +309,6 @@ def sortable_title_with_sort_key(instance): ) ) -# Default method to be used. This field is used in Analysis Service -# Edit view, use getMethod() to retrieve the Method to be used in -# this Analysis Service. -# Gets populated with the methods selected in the multiselection -# box above or with the default instrument's method. -# Behavior controlled by js depending on ManualEntry/Instrument/Methods: -# - If InstrumentEntry checked, set instrument's default method, and readonly -# - If InstrumentEntry not checked, populate dynamically with -# selected Methods, set the first method selected and non-readonly -# See browser/js/bika.lims.analysisservice.edit.js -_Method = UIDReferenceField( - '_Method', - schemata="Method", - required=0, - searchable=True, - vocabulary_display_path_bound=sys.maxint, - allowed_types=('Method',), - vocabulary='_getAvailableMethodsDisplayList', - widget=SelectionWidget( - format='select', - label=_("Default Method"), - description=_( - "If 'Allow instrument entry of results' is selected, the method " - "from the default instrument will be used. Otherwise, only the " - "methods selected above will be displayed.") - ) -) - # Instruments associated to the AS # List of instruments capable to perform the Analysis Service. The # Instruments selected here are displayed in the Analysis Request @@ -335,94 +337,14 @@ def sortable_title_with_sort_key(instance): ) ) -# Allow/Disallow to set the calculation manually -# Behavior controlled by javascript depending on Instruments field: -# - If no instruments available, hide and uncheck -# - If at least one instrument selected then checked, but not readonly -# See browser/js/bika.lims.analysisservice.edit.js -UseDefaultCalculation = BooleanField( - 'UseDefaultCalculation', - schemata="Method", - default=True, - widget=BooleanWidget( - label=_("Use default calculation"), - description=_( - "Select if the calculation to be used is the calculation set by " - "default in the default method. If unselected, the calculation " - "can be selected manually"), - ) -) - -# Default calculation to be used. This field is used in Analysis Service -# Edit view, use getCalculation() to retrieve the Calculation to be used in -# this Analysis Service. -# The default calculation is the one linked to the default method -# Behavior controlled by js depending on UseDefaultCalculation: -# - If UseDefaultCalculation is set to False, show this field -# - If UseDefaultCalculation is set to True, show this field -# See browser/js/bika.lims.analysisservice.edit.js -_Calculation = UIDReferenceField( - '_Calculation', - schemata="Method", - required=0, - vocabulary_display_path_bound=sys.maxint, - vocabulary='_getAvailableCalculationsDisplayList', - allowed_types=('Calculation',), - widget=SelectionWidget( - format='select', - label=_("Default Calculation"), - description=_( - "Default calculation to be used from the default Method selected. " - "The Calculation for a method can be assigned in the Method edit " - "view."), - catalog_name='bika_setup_catalog', - base_query={'inactive_state': 'active'}, - ) -) - -# Default calculation is not longer linked directly to the AS: it -# currently uses the calculation linked to the default Method. -# Use getCalculation() to retrieve the Calculation to be used. -# Old ASes (before 3008 upgrade) can be linked to the same Method, -# but to different Calculation objects. In that case, the Calculation -# is saved as DeferredCalculation and UseDefaultCalculation is set to -# False in the upgrade. -# Behavior controlled by js depending on UseDefaultCalculation: -# - If UseDefaultCalculation is set to False, show this field -# - If UseDefaultCalculation is set to True, show this field -# See browser/js/bika.lims.analysisservice.edit.js -# bika/lims/upgrade/to3008.py -DeferredCalculation = UIDReferenceField( - 'DeferredCalculation', - schemata="Method", - required=0, - vocabulary_display_path_bound=sys.maxint, - vocabulary='_getAvailableCalculationsDisplayList', - allowed_types=('Calculation',), - widget=SelectionWidget( - format='select', - label=_("Alternative Calculation"), - description=_( - "If required, select a calculation for the analysis here. " - "Calculations can be configured under the calculations item in " - "the LIMS set-up"), - catalog_name='bika_setup_catalog', - base_query={'inactive_state': 'active'}, - ) -) - -## XXX Keep synced to service duplication code in bika_analysisservices.py schema = BaseAnalysisSchema.copy() + Schema(( Separate, Preservation, Container, PartitionSetup, + UseDefaultCalculation, Methods, - _Method, Instruments, - UseDefaultCalculation, - _Calculation, - DeferredCalculation, )) @@ -458,6 +380,47 @@ def getPreservations(self): items.sort(lambda x, y: cmp(x[1], y[1])) return DisplayList(list(items)) + @security.public + def getAvailableMethods(self): + """ Returns the methods available for this analysis. + If the service has the getInstrumentEntryOfResults(), returns + the methods available from the instruments capable to perform + the service, as well as the methods set manually for the + analysis on its edit view. If getInstrumentEntryOfResults() + is unset, only the methods assigned manually to that service + are returned. + """ + methods = self.getMethods() + muids = [m.UID() for m in methods] + if self.getInstrumentEntryOfResults(): + # Add the methods from the instruments capable to perform + # this analysis service + for ins in self.getInstruments(): + for method in ins.getMethods(): + if method and method.UID() not in muids: + methods.append(method) + muids.append(method.UID()) + + return methods + + @security.public + def getAvailableMethodsUIDs(self): + """ Returns the UIDs of the available method. + """ + return [m.UID() for m in self.getAvailableMethods()] + + @security.public + def getAvailableInstruments(self): + """ Returns the instruments available for this service. + If the service has the getInstrumentEntryOfResults(), returns + the instruments capable to perform this service. Otherwhise, + returns an empty list. + """ + instruments = self.getInstruments() \ + if self.getInstrumentEntryOfResults() is True \ + else None + return instruments if instruments else [] + @security.private def _getAvailableMethodsDisplayList(self): """ Returns a DisplayList with the available Methods @@ -466,7 +429,7 @@ def _getAvailableMethodsDisplayList(self): Used to fill the Methods MultiSelectionWidget when 'Allow Instrument Entry of Results is not selected'. """ - bsc = getToolByName(self, 'bika_setup_catalog') + bsc = get_tool('bika_setup_catalog') items = [(i.UID, i.Title) for i in bsc(portal_type='Method', inactive_state='active') @@ -479,10 +442,9 @@ def _getAvailableMethodsDisplayList(self): def _getAvailableCalculationsDisplayList(self): """ Returns a DisplayList with the available Calculations registered in Bika-Setup. Only active Calculations are - fetched. Used to fill the _Calculation and DeferredCalculation - List fields + fetched. Used to fill the Calculation field """ - bsc = getToolByName(self, 'bika_setup_catalog') + bsc = get_tool('bika_setup_catalog') items = [(i.UID, i.Title) for i in bsc(portal_type='Calculation', inactive_state='active')] @@ -496,7 +458,7 @@ def _getAvailableInstrumentsDisplayList(self): registered in Bika-Setup. Only active Instruments are fetched. Used to fill the Instruments MultiSelectionWidget """ - bsc = getToolByName(self, 'bika_setup_catalog') + bsc = get_tool('bika_setup_catalog') items = [(i.UID, i.Title) for i in bsc(portal_type='Instrument', inactive_state='active')] diff --git a/bika/lims/content/baseanalysis.py b/bika/lims/content/baseanalysis.py index 6ce1a6b69c..d0567f5f7b 100644 --- a/bika/lims/content/baseanalysis.py +++ b/bika/lims/content/baseanalysis.py @@ -5,18 +5,13 @@ # Copyright 2011-2016 by it's authors. # Some rights reserved. See LICENSE.txt, AUTHORS.txt. -import sys - from AccessControl import ClassSecurityInfo from Products.ATExtensions.ateapi import RecordsField -from Products.Archetypes.public import DisplayList, ReferenceField, \ - ComputedField, ComputedWidget, BooleanField, BooleanWidget, StringField, \ - SelectionWidget, FixedPointField, IntegerField, IntegerWidget, \ - StringWidget, BaseContent, Schema, MultiSelectionWidget, FloatField, \ - DecimalWidget -from Products.Archetypes.references import HoldingReference +from Products.Archetypes.public import DisplayList, BooleanField, \ + BooleanWidget, StringField, SelectionWidget, FixedPointField, \ + IntegerField, IntegerWidget, StringWidget, BaseContent, Schema, \ + FloatField, DecimalWidget from Products.Archetypes.utils import IntDisplayList -from Products.CMFCore.utils import getToolByName from bika.lims import bikaMessageFactory as _ from bika.lims.browser.fields import * from bika.lims.browser.widgets.durationwidget import DurationWidget @@ -26,7 +21,11 @@ from bika.lims.content.bikaschema import BikaSchema from bika.lims.utils import to_utf8 as _c from bika.lims.utils.analysis import get_significant_digits +from plone.api.portal import get_tool +# Anywhere that there just isn't space for unpredictably long names, +# this value will be used instead. It's set on the AnalysisService, +# but accessed on all analysis objects. ShortTitle = StringField( 'ShortTitle', schemata="Description", @@ -39,6 +38,7 @@ ) ) +# A simple integer to sort items. SortKey = FloatField( 'SortKey', schemata="Description", @@ -51,6 +51,7 @@ ) ) +# Is the title of the analysis a proper Scientific Name? ScientificName = BooleanField( 'ScientificName', schemata="Description", @@ -62,6 +63,8 @@ ) ) +# The units of measurement used for representing results in reports and in +# manage_results screen. Unit = StringField( 'Unit', schemata="Description", @@ -73,6 +76,7 @@ ) ) +# Decimal precision for printing normal decimal results. Precision = IntegerField( 'Precision', schemata="Analysis", @@ -83,6 +87,8 @@ ) ) +# If the precision of the results as entered is higher than this value, +# the results will be represented in scientific notation. ExponentialFormatPrecision = IntegerField( 'ExponentialFormatPrecision', schemata="Analysis", @@ -95,6 +101,9 @@ ) ) +# If the value is below this limit, it means that the measurement lacks +# accuracy and this will be shown in manage_results and also on the final +# report. LowerDetectionLimit = FixedPointField( 'LowerDetectionLimit', schemata="Analysis", @@ -110,6 +119,9 @@ ) ) +# If the value is above this limit, it means that the measurement lacks +# accuracy and this will be shown in manage_results and also on the final +# report. UpperDetectionLimit = FixedPointField( 'UpperDetectionLimit', schemata="Analysis", @@ -125,10 +137,9 @@ ) ) -# LIMS-1775 Allow to select LDL or UDL defaults in results with readonly mode -# https://jira.bikalabs.com/browse/LIMS-1775 -# Some behavior controlled with javascript: If checked, the field -# "AllowManualDetectionLimit" will be displayed. +# Allow to select LDL or UDL defaults in results with readonly mode +# Some behavior of AnalysisServices is controlled with javascript: If checked, +# the field "AllowManualDetectionLimit" will be displayed. # See browser/js/bika.lims.analysisservice.edit.js # # Use cases: @@ -159,7 +170,7 @@ ) ) -# Behavior controlled with javascript: Only visible when the +# Behavior of AnalysisService controlled with javascript: Only visible when the # "DetectionLimitSelector" is checked # See browser/js/bika.lims.analysisservice.edit.js # Check inline comment for "DetecionLimitSelector" field for @@ -176,6 +187,8 @@ ) ) +# Indicates that the result should be calculated against the system "Dry Matter" +# service, and the modified result stored in Analysis.ResultDM field. ReportDryMatter = BooleanField( 'ReportDryMatter', schemata="Analysis", @@ -186,6 +199,7 @@ ) ) +# Specify attachment requirements for these analyses AttachmentOption = StringField( 'AttachmentOption', schemata="Analysis", @@ -201,6 +215,8 @@ ) ) +# The keyword for the service is used as an identifier during instrument +# imports, and other places too. It's also used as the ID analyses. Keyword = StringField( 'Keyword', schemata="Description", @@ -218,7 +234,8 @@ ) # Allow/Disallow manual entry of results -# Behavior controlled by javascript depending on Instruments field: +# Behavior of AnalysisServices controlled by javascript depending on +# Instruments field: # - If InstrumentEntry not checked, set checked and readonly # - If InstrumentEntry checked, set as not readonly # See browser/js/bika.lims.analysisservice.edit.js @@ -258,9 +275,9 @@ ) # Default instrument to be used. -# Gets populated with the instruments selected in the multiselection -# box above. -# Behavior controlled by js depending on ManualEntry/Instruments: +# Gets populated with the instruments selected in the Instruments field. +# Behavior of AnalysisServices controlled by js depending on +# ManualEntry/Instruments: # - Populate dynamically with selected Instruments # - If InstrumentEntry checked, set first selected instrument # - If InstrumentEntry not checked, hide and set None @@ -276,45 +293,73 @@ format='select', label=_("Default Instrument"), description=_( - "This is the instrument the system will assign by default to " - "tests from this type of analysis in manage results view. The " - "method associated to this instrument will be assigned as the " - "default method too.Note the instrument's method will prevail " - "over any of the methods choosen if the 'Instrument assignment is " - "not required' option is enabled.") - ) -) - -# Returns the Default's instrument title. If no default instrument -# set, returns string.empty -InstrumentTitle = ComputedField( - 'InstrumentTitle', - expression="" - "context.getInstrument() and context.getInstrument().Title() or ''", - widget=ComputedWidget( - visible=False, + "This is the instrument that is assigned to tests from this type " + "of analysis in manage results view. The method associated to " + "this instrument will be assigned as the default method too.Note " + "the instrument's method will prevail over any of the methods " + "choosen if the 'Instrument assignment is not required' option is " + "enabled.") ) ) -CalculationTitle = ComputedField( - 'CalculationTitle', - expression="" - "context.getCalculation() and context.getCalculation().Title() or ''", +# Default method to be used. This field is used in Analysis Service +# Edit view, use getMethod() to retrieve the Method to be used in +# this Analysis Service. +# Gets populated with the methods selected in the multiselection +# box above or with the default instrument's method. +# Behavior controlled by js depending on ManualEntry/Instrument/Methods: +# - If InstrumentEntry checked, set instrument's default method, and readonly +# - If InstrumentEntry not checked, populate dynamically with +# selected Methods, set the first method selected and non-readonly +# See browser/js/bika.lims.analysisservice.edit.js +Method = UIDReferenceField( + 'Method', + schemata="Method", + required=0, searchable=True, - widget=ComputedWidget( - visible=False, + allowed_types=('Method',), + vocabulary='_getAvailableMethodsDisplayList', + widget=SelectionWidget( + format='select', + label=_("Default Method"), + description=_( + "If 'Allow instrument entry of results' is selected, the method " + "from the default instrument will be used. Otherwise, only the " + "methods selected above will be displayed.") ) ) -CalculationUID = ComputedField( - 'CalculationUID', - expression="" - "context.getCalculation() and context.getCalculation().UID() or ''", - widget=ComputedWidget( - visible=False, +# Default calculation to be used. This field is used in Analysis Service +# Edit view, use getCalculation() to retrieve the Calculation to be used in +# this Analysis Service. +# The default calculation is the one linked to the default method +# Behavior controlled by js depending on UseDefaultCalculation: +# - If UseDefaultCalculation is set to False, show this field +# - If UseDefaultCalculation is set to True, show this field +# See browser/js/bika.lims.analysisservice.edit.js +Calculation = UIDReferenceField( + 'Calculation', + schemata="Method", + required=0, + vocabulary='_getAvailableCalculationsDisplayList', + allowed_types=('Calculation',), + widget=SelectionWidget( + format='select', + label=_("Default Calculation"), + description=_( + "Default calculation to be used from the default Method selected. " + "The Calculation for a method can be assigned in the Method edit " + "view."), + catalog_name='bika_setup_catalog', + base_query={'inactive_state': 'active'}, ) ) +# InterimFields are defined in Calculations, Services, and Analyses. +# In Analysis Services, the default values are taken from Calculation. +# In Analyses, the default values are taken from the Analysis Service. +# When instrument results are imported, the values in analysis are overridden +# before the calculation is performed. InterimFields = InterimFieldsField( 'InterimFields', schemata='Method', @@ -326,6 +371,9 @@ ) ) +# Maximum time (from sample reception) allowed for the analysis to be performed. +# After this amount of time, a late alert is printed, and the analysis will be +# flagged in turnaround time report. MaxTimeAllowed = DurationField( 'MaxTimeAllowed', schemata="Analysis", @@ -337,6 +385,7 @@ ) ) +# The amount of difference allowed between this analysis, and any duplicates. DuplicateVariation = FixedPointField( 'DuplicateVariation', schemata="Method", @@ -349,6 +398,8 @@ ) ) +# True if the accreditation body has approved this lab's method for +# accreditation. Accredited = BooleanField( 'Accredited', schemata="Method", @@ -361,6 +412,10 @@ ) ) +# The physical location that the analysis is tested; for some analyses, +# the sampler may capture results at the point where the sample is taken, +# and these results can be captured using different rules. For example, +# the results may be entered before the sample is received. PointOfCapture = StringField( 'PointOfCapture', schemata="Description", @@ -378,6 +433,8 @@ ) ) +# The category of the analysis service, used for filtering, collapsing and +# reporting on analyses. Category = UIDReferenceField( 'Category', schemata="Description", @@ -393,24 +450,7 @@ ) ) -CategoryTitle = ComputedField( - 'CategoryTitle', - expression="" - "context.getCategory() and context.getCategory().Title() or ''", - widget=ComputedWidget( - visible=False, - ) -) - -CategoryUID = ComputedField( - 'CategoryUID', - expression="" - "context.getCategory() and context.getCategory().UID() or ''", - widget=ComputedWidget( - visible=False, - ) -) - +# The base price for this analysis Price = FixedPointField( 'Price', schemata="Description", @@ -420,7 +460,7 @@ ) ) -# read access permission +# Some clients qualify for bulk discounts. BulkPrice = FixedPointField( 'BulkPrice', schemata="Description", @@ -433,26 +473,8 @@ ) ) -VATAmount = ComputedField( - 'VATAmount', - schemata="Description", - expression='context.getVATAmount()', - widget=ComputedWidget( - label=_("VAT"), - visible={'edit': 'hidden', } - ) -) - -TotalPrice = ComputedField( - 'TotalPrice', - schemata="Description", - expression='context.getTotalPrice()', - widget=ComputedWidget( - label=_("Total price"), - visible={'edit': 'hidden', } - ) -) - +# If VAT is charged, a different VAT value can be entered for each +# service. The default value is taken from BikaSetup VAT = FixedPointField( 'VAT', schemata="Description", @@ -463,6 +485,8 @@ ) ) +# The analysis service's Department. This is used to filter analyses, +# and for indicating the responsibile lab manager in reports. Department = UIDReferenceField( 'Department', schemata="Description", @@ -478,16 +502,8 @@ ) ) -DepartmentTitle = ComputedField( - 'DepartmentTitle', - expression="" - "context.getDepartment() and context.getDepartment().Title() or ''", - searchable=True, - widget=ComputedWidget( - visible=False, - ) -) - +# Uncertainty percentages in results can change depending on the results +# themselves. Uncertainties = RecordsField( 'Uncertainties', schemata="Uncertainties", @@ -564,6 +580,10 @@ ) ) +# Results can be selected from a dropdown list. This prevents the analyst +# from entering arbitrary values. Each result must have a ResultValue, which +# must be a number - it is this number which is interpreted as the actual +# "Result" when applying calculations. ResultOptions = RecordsField( 'ResultOptions', schemata="Result Options", @@ -587,6 +607,9 @@ ) ) +# If the service is meant for providing an interim result to another analysis, +# or if the result is only used for internal processes, then it can be hidden +# from the client in the final report (and in manage_results view) Hidden = BooleanField( 'Hidden', schemata="Analysis", @@ -600,6 +623,8 @@ ) ) +# Permit a user to verify their own results. This could invalidate the +# accreditation for the results of this analysis! SelfVerification = IntegerField( 'SelfVerification', schemata="Analysis", @@ -618,8 +643,9 @@ ) ) -_NumberOfRequiredVerifications = IntegerField( - '_NumberOfRequiredVerifications', +# Require more than one verification by separate Verifier or LabManager users. +NumberOfRequiredVerifications = IntegerField( + 'NumberOfRequiredVerifications', schemata="Analysis", default=-1, vocabulary="_getNumberOfRequiredVerificationsVocabulary", @@ -634,6 +660,7 @@ ) ) +# Just a string displayed on various views CommercialID = StringField( 'CommercialID', searchable=1, @@ -645,6 +672,7 @@ ) ) +# Just a string displayed on various views ProtocolID = StringField( 'ProtocolID', searchable=1, @@ -656,8 +684,6 @@ ) ) -# XXX Keep synced to service duplication code in bika_analysisservices.py - schema = BikaSchema.copy() + Schema(( ShortTitle, SortKey, @@ -675,9 +701,8 @@ ManualEntryOfResults, InstrumentEntryOfResults, Instrument, - InstrumentTitle, - CalculationTitle, - CalculationUID, + Method, + Calculation, InterimFields, MaxTimeAllowed, DuplicateVariation, @@ -686,20 +711,15 @@ Category, Price, BulkPrice, - VATAmount, - TotalPrice, VAT, - CategoryTitle, - CategoryUID, Department, - DepartmentTitle, Uncertainties, PrecisionFromUncertainty, AllowManualUncertainty, ResultOptions, Hidden, SelfVerification, - _NumberOfRequiredVerifications, + NumberOfRequiredVerifications, CommercialID, ProtocolID, )) @@ -724,186 +744,105 @@ class BaseAnalysis(BaseContent): schema = schema displayContentsTab = False - security.declarePublic('Title') - + @security.public def Title(self): return _c(self.title) - security.declarePublic('getDiscountedPrice') + @security.public + def getDefaultVAT(self): + """Return default VAT from bika_setup + """ + try: + vat = self.bika_setup.getVAT() + return vat + except ValueError: + return "0.00" + + @security.public + def getVATAmount(self): + """Compute VAT Amount from the Price and system configured VAT + """ + price, vat = self.getPrice(), self.getVAT() + return float(price) * (float(vat) / 100) + @security.public def getDiscountedPrice(self): - """ compute discounted price excl. vat """ + """Compute discounted price excl. VAT + """ price = self.getPrice() price = price and price or 0 discount = self.bika_setup.getMemberDiscount() discount = discount and discount or 0 return float(price) - (float(price) * float(discount)) / 100 - security.declarePublic('getDiscountedBulkPrice') - + @security.public def getDiscountedBulkPrice(self): - """ compute discounted bulk discount excl. vat """ + """Compute discounted bulk discount excl. VAT + """ price = self.getBulkPrice() price = price and price or 0 discount = self.bika_setup.getMemberDiscount() discount = discount and discount or 0 return float(price) - (float(price) * float(discount)) / 100 - security.declarePublic('getTotalPrice') - + @security.public def getTotalPrice(self): - """ compute total price """ + """Compute total price including VAT + """ price = self.getPrice() vat = self.getVAT() price = price and price or 0 vat = vat and vat or 0 return float(price) + (float(price) * float(vat)) / 100 - security.declarePublic('getTotalBulkPrice') - + @security.public def getTotalBulkPrice(self): - """ compute total price """ + """Compute total bulk price + """ price = self.getBulkPrice() vat = self.getVAT() price = price and price or 0 vat = vat and vat or 0 return float(price) + (float(price) * float(vat)) / 100 - security.declarePublic('getTotalDiscountedPrice') - + @security.public def getTotalDiscountedPrice(self): - """ compute total discounted price """ + """Compute total discounted price + """ price = self.getDiscountedPrice() vat = self.getVAT() price = price and price or 0 vat = vat and vat or 0 return float(price) + (float(price) * float(vat)) / 100 - security.declarePublic('getTotalDiscountedCorporatePrice') - + @security.public def getTotalDiscountedBulkPrice(self): - """ compute total discounted corporate price """ + """Compute total discounted corporate bulk price + """ price = self.getDiscountedCorporatePrice() vat = self.getVAT() price = price and price or 0 vat = vat and vat or 0 return float(price) + (float(price) * float(vat)) / 100 - security.declarePublic('getDefaultVAT') - - def getDefaultVAT(self): - """ return default VAT from bika_setup """ - try: - vat = self.bika_setup.getVAT() - return vat - except ValueError: - return "0.00" - - security.declarePublic('getVATAmount') - - def getVATAmount(self): - """ Compute VATAmount - """ - price, vat = self.getPrice(), self.getVAT() - return float(price) * (float(vat) / 100) - - security.declarePublic('getAnalysisCategories') - + @security.public def getAnalysisCategories(self): - bsc = getToolByName(self, 'bika_setup_catalog') - items = [(o.UID, o.Title) for o in - bsc(portal_type='AnalysisCategory', - inactive_state='active')] + """A vocabulary listing available (and activated) categories. + """ + bsc = get_tool('bika_setup_catalog') + cats = bsc(portal_type='AnalysisCategory', inactive_state='active') + items = [(o.UID, o.Title) for o in cats] o = self.getCategory() 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)) - security.declarePublic('getCalculation') - - def getCalculation(self): - """ Returns the calculation to be used in this AS. - If UseDefaultCalculation() is set, returns the calculation - from the default method selected or none (if method hasn't - defined any calculation). If UseDefaultCalculation is set - to false, returns the Deferred Calculation (manually set) - """ - if self.getUseDefaultCalculation(): - method = self.getMethod() - if method: - calculation = method.getCalculation() - return calculation if calculation else None - else: - return self.getDeferredCalculation() - - security.declarePublic('getMethod') - - def getMethod(self): - """ Returns the method assigned by default to the AS. - If Instrument Entry Of Results selected, returns the method - from the Default Instrument or None. - If Instrument Entry of Results is not selected, returns the - method assigned directly by the user using the _Method Field - """ - # TODO This function has been modified after enabling multiple methods - # for instruments. Make sure that returning the value of _Method field - # is correct. - if self.getInstrumentEntryOfResults(): - method = self.get_Method() - else: - methods = self.getMethods() - method = methods[0] if methods else None - return method - - security.declarePublic('getAvailableMethods') - - def getAvailableMethods(self): - """ Returns the methods available for this analysis. - If the service has the getInstrumentEntryOfResults(), returns - the methods available from the instruments capable to perform - the service, as well as the methods set manually for the - analysis on its edit view. If getInstrumentEntryOfResults() - is unset, only the methods assigned manually to that service - are returned. - """ - methods = self.getMethods() - muids = [m.UID() for m in methods] - if self.getInstrumentEntryOfResults(): - # Add the methods from the instruments capable to perform - # this analysis service - for ins in self.getInstruments(): - for method in ins.getMethods(): - if method and method.UID() not in muids: - methods.append(method) - muids.append(method.UID()) - - return methods - - security.declarePublic('getAvailableMethodsUIDs') - - def getAvailableMethodsUIDs(self): - """ Returns the UIDs of the available method. - """ - return [m.UID() for m in self.getAvailableMethods()] - - security.declarePublic('getAvailableInstruments') - - def getAvailableInstruments(self): - """ Returns the instruments available for this analysis. - If the service has the getInstrumentEntryOfResults(), returns - the instruments capable to perform this service. Otherwhise, - returns an empty list. - """ - instruments = self.getInstruments() \ - if self.getInstrumentEntryOfResults() is True \ - else None - return instruments if instruments else [] - - security.declarePublic('getDepartments') - + @security.public def getDepartments(self): - bsc = getToolByName(self, 'bika_setup_catalog') + """A vocabulary listing available (and activated) departments. + """ + bsc = get_tool('bika_setup_catalog') items = [('', '')] + [(o.UID, o.Title) for o in bsc(portal_type='Department', inactive_state='active')] @@ -913,45 +852,9 @@ def getDepartments(self): items.sort(lambda x, y: cmp(x[1], y[1])) return DisplayList(list(items)) - security.declarePublic('getUncertainty') - - def getUncertainty(self, result=None): - """ - Return the uncertainty value, if the result falls within - specified ranges for this service. - """ - - if result is None: - return None - - uncertainties = self.getUncertainties() - if uncertainties: - try: - result = float(result) - except ValueError: - # if analysis result is not a number, then we assume in range - return None - - for d in uncertainties: - if float(d['intercept_min']) <= result <= float( - d['intercept_max']): - if str(d['errorvalue']).strip().endswith('%'): - try: - percvalue = float(d['errorvalue'].replace('%', '')) - except ValueError: - return None - unc = result / 100 * percvalue - else: - unc = float(d['errorvalue']) - - return unc - return None - - security.declarePublic('getLowerDetectionLimit') - + @security.public def getLowerDetectionLimit(self): - """ Returns the Lower Detection Limit for this service as a - floatable + """Returns the Lower Detection Limit for this service as a floatable """ ldl = self.Schema().getField('LowerDetectionLimit').get(self) try: @@ -959,11 +862,9 @@ def getLowerDetectionLimit(self): except ValueError: return 0 - security.declarePublic('getUpperDetectionLimit') - + @security.public def getUpperDetectionLimit(self): - """ Returns the Upper Detection Limit for this service as a - floatable + """Returns the Upper Detection Limit for this service as a floatable """ udl = self.Schema().getField('UpperDetectionLimit').get(self) try: @@ -971,11 +872,9 @@ def getUpperDetectionLimit(self): except ValueError: return 0 - security.declarePublic('getPrecision') - + @security.public def getPrecision(self, result=None): - """ - Returns the precision for the Analysis Service. If the + """Returns the precision for the Analysis Service. If the option Calculate Precision according to Uncertainty is not set, the method will return the precision value set in the Schema. Otherwise, will calculate the precision value @@ -1017,25 +916,23 @@ def getPrecision(self, result=None): return 1 return get_significant_digits(uncertainty) - security.declarePublic('getExponentialFormatPrecision') - + @security.public def getExponentialFormatPrecision(self, result=None): - """ - Returns the precision for the Analysis Service and result - provided. Results with a precision value above this exponential + """ Returns the precision for the Analysis Service and result + provided. Results with a precision value above this exponential format precision should be formatted as scientific notation. - If the Calculate Precision according to Uncertainty is not set, - the method will return the exponential precision value set in - the Schema. Otherwise, will calculate the precision value - according to the Uncertainty and the result. + If the Calculate Precision according to Uncertainty is not set, + the method will return the exponential precision value set in the + Schema. Otherwise, will calculate the precision value according to + the Uncertainty and the result. - If Calculate Precision from the Uncertainty is set but no - result provided neither uncertainty values are set, returns the - fixed exponential precision. + If Calculate Precision from the Uncertainty is set but no result + provided neither uncertainty values are set, returns the fixed + exponential precision. - Will return positive values if the result is below 0 and will - return 0 or positive values if the result is above 0. + Will return positive values if the result is below 0 and will return + 0 or positive values if the result is above 0. Given an analysis service with fixed exponential format precision of 4: @@ -1046,14 +943,12 @@ def getExponentialFormatPrecision(self, result=None): 32092 0.81 4 456021 423 5 - For further details, visit - https://jira.bikalabs.com/browse/LIMS-1334 + For further details, visit https://jira.bikalabs.com/browse/LIMS-1334 - :param result: if provided and "Calculate Precision according - to the Uncertainty" is set, the result will be - used to retrieve the uncertainty from which the - precision must be calculated. Otherwise, the - fixed-precision will be used. + :param result: if provided and "Calculate Precision according to the + Uncertainty" is set, the result will be used to retrieve the + uncertainty from which the precision must be calculated. Otherwise, + the fixed-precision will be used. :returns: the precision """ field = self.Schema().getField('ExponentialFormatPrecision') @@ -1072,26 +967,22 @@ def getExponentialFormatPrecision(self, result=None): return get_significant_digits(uncertainty) - security.declarePublic('isSelfVerificationEnabled') - + @security.public def isSelfVerificationEnabled(self): - """ - Returns if the user that submitted a result for this analysis must also - be able to verify the result + """Returns if the user that submitted a result for this analysis must + also be able to verify the result :returns: true or false """ bsve = self.bika_setup.getSelfVerificationEnabled() vs = self.getSelfVerification() return bsve if vs == -1 else vs == 1 - security.declarePublic('_getSelfVerificationVocabulary') - + @security.public def _getSelfVerificationVocabulary(self): - """ - Returns a DisplayList with the available options for the + """Returns a DisplayList with the available options for the self-verification list: 'system default', 'true', 'false' - :returns: DisplayList with the available options for the - self-verification list + :returns: DisplayList with the available options for the + self-verification list """ bsve = self.bika_setup.getSelfVerificationEnabled() bsve = _('Yes') if bsve else _('No') @@ -1099,12 +990,10 @@ def _getSelfVerificationVocabulary(self): items = [(-1, bsval), (0, _('No')), (1, _('Yes'))] return IntDisplayList(list(items)) - security.declarePublic('getNumberOfRequiredVerifications') - + @security.public def getNumberOfRequiredVerifications(self): - """ - Returns the number of required verifications a test for this analysis - requires before being transitioned to 'verified' state + """Returns the number of required verifications a test for this + analysis requires before being transitioned to 'verified' state :returns: number of required verifications """ num = self.get_NumberOfRequiredVerifications() @@ -1112,16 +1001,70 @@ def getNumberOfRequiredVerifications(self): return self.bika_setup.getNumberOfRequiredVerifications() return num - security.declarePublic('_getNumberOfRequiredVerificationsVocabulary') - + @security.public def _getNumberOfRequiredVerificationsVocabulary(self): - """ - Returns a DisplayList with the available options for the + """Returns a DisplayList with the available options for the multi-verification list: 'system default', '1', '2', '3', '4' - :returns: DisplayList with the available options for the - multi-verification list + :returns: DisplayList with the available options for the + multi-verification list """ bsve = self.bika_setup.getNumberOfRequiredVerifications() bsval = "%s (%s)" % (_("System default"), str(bsve)) items = [(-1, bsval), (1, '1'), (2, '2'), (3, '3'), (4, '4')] return IntDisplayList(list(items)) + + @security.public + def getMethodUID(self): + """This is used to populate catalog values + """ + method = self.getMethod() + if method: + return method.UID() + + @security.public + def getInstrumentTitle(self): + """Used to populate catalog values + """ + instrument = self.getInstrument() + if instrument: + return instrument.Title() + + @security.public + def getCalculationTitle(self): + """Used to populate catalog values + """ + calculation = self.getCalculation() + if calculation: + return calculation.Title() + + @security.public + def getCalculationUID(self): + """Used to populate catalog values + """ + calculation = self.getCalculation() + if calculation: + return calculation.UID() + + @security.public + def getCategoryTitle(self): + """Used to populate catalog values + """ + category = self.getCategory() + if category: + return category.Title() + + @security.public + def getCategoryUID(self): + """Used to populate catalog values + """ + category = self.getCategory() + if category: + return category.UID() + + @security.public + def getDepartmentTitle(self): + """Used to populate catalog values + """ + department = self.getDepartment() + if department: + return department.Title() From a3c5d7dc34437a9acb7500ff1a35fae73430f51d Mon Sep 17 00:00:00 2001 From: Campbell Date: Mon, 8 May 2017 01:11:13 +0200 Subject: [PATCH 04/36] Add migration v3_2_0_1705 --- bika/lims/profiles/default/metadata.xml | 2 +- bika/lims/upgrade/configure.zcml | 7 + bika/lims/upgrade/v3_2_0_1704.py | 5 + bika/lims/upgrade/v3_2_0_1705.py | 201 ++++++++++++++++++++++++ 4 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 bika/lims/upgrade/v3_2_0_1705.py diff --git a/bika/lims/profiles/default/metadata.xml b/bika/lims/profiles/default/metadata.xml index 020bfd88f6..b2659bb67e 100644 --- a/bika/lims/profiles/default/metadata.xml +++ b/bika/lims/profiles/default/metadata.xml @@ -1,6 +1,6 @@ - 3.2.0.1704 + 3.2.0.1705 profile-jarn.jsi18n:default profile-Products.ATExtensions:default diff --git a/bika/lims/upgrade/configure.zcml b/bika/lims/upgrade/configure.zcml index 9cb4edfb10..45d9f221ba 100644 --- a/bika/lims/upgrade/configure.zcml +++ b/bika/lims/upgrade/configure.zcml @@ -591,4 +591,11 @@ handler="bika.lims.upgrade.v3_2_0_1704.upgrade" profile="bika.lims:default"/> + + diff --git a/bika/lims/upgrade/v3_2_0_1704.py b/bika/lims/upgrade/v3_2_0_1704.py index 1a93e04499..3bac2f36df 100644 --- a/bika/lims/upgrade/v3_2_0_1704.py +++ b/bika/lims/upgrade/v3_2_0_1704.py @@ -4,12 +4,17 @@ # Some rights reserved. See LICENSE.txt, AUTHORS.txt. from Acquisition import aq_inner from Acquisition import aq_parent +from Products.Archetypes.BaseObject import BaseObject +from Products.ZCatalog.interfaces import ICatalogBrain from bika.lims import logger +from bika.lims.browser.fields.uidreferencefield import ReferenceException +from bika.lims.catalog import getCatalog from bika.lims.upgrade import upgradestep from bika.lims.upgrade.utils import UpgradeUtils import traceback import sys import transaction +from plone.api.portal import get_tool product = 'bika.lims' version = '3.2.0.1704' diff --git a/bika/lims/upgrade/v3_2_0_1705.py b/bika/lims/upgrade/v3_2_0_1705.py new file mode 100644 index 0000000000..5753b66f37 --- /dev/null +++ b/bika/lims/upgrade/v3_2_0_1705.py @@ -0,0 +1,201 @@ +# This file is part of Bika LIMS +# +# Copyright 2011-2017 by it's authors. +# Some rights reserved. See LICENSE.txt, AUTHORS.txt. +from Acquisition import aq_inner +from Acquisition import aq_parent + +from Products.ZCatalog.interfaces import ICatalogBrain +from bika.lims import logger +from bika.lims.upgrade import upgradestep +from bika.lims.upgrade.utils import UpgradeUtils +from plone.api.portal import get_tool + +product = 'bika.lims' +version = '3.2.0.1705' + + +@upgradestep(product, version) +def upgrade(tool): + portal = aq_parent(aq_inner(tool)) + ut = UpgradeUtils(portal) + ufrom = ut.getInstalledVersion(product) + if ut.isOlderVersion(product, version): + logger.info('Skipping upgrade of {0}: {1} > {2}'.format( + product, ufrom, version)) + # The currently installed version is more recent than the target + # version of this upgradestep + return True + + logger.info('Upgrading {0}: {1} -> {2}'.format(product, ufrom, version)) + + BaseAnalysisRefactoring(portal) + + # Refresh affected catalogs + ut.refreshCatalogs() + + logger.info('{0} upgraded to version {1}'.format(product, version)) + return True + + +def BaseAnalysisRefactoring(portal): + ut = UpgradeUtils(portal) + """Upgrade steps to be taken after radical BaseAnalysis refactoring + Includes migration of ReferenceField values to the new UIDReferenceField. + + Old-style references before BaseAnalysis refactoring: + + AnalysisService + =============== + Ref=Instruments rel=AnalysisServiceInstruments + Hist=Instrument rel=AnalysisServiceInstrument + Ref=Methods rel=AnalysisServiceMethods + Ref=_Method rel=AnalysisServiceMethod + Ref=_Calculation rel=AnalysisServiceCalculation + Ref=DeferredCalculation rel=AnalysisServiceDeferredCalculation + Ref=Category rel=AnalysisServiceAnalysisCategory + Ref=Department rel=AnalysisServiceDepartment + Ref=Preservation rel=AnalysisServicePreservation + Ref=Container rel=AnalysisServiceContainer + Analysis + ======== + Hist=Service rel=AnalysisAnalysisService + Hist=Calculation rel=AnalysisCalculation + Ref=Attachment rel=AnalysisAttachment + Ref=Instrument rel=AnalysisInstrument + Ref=Method rel=AnalysisMethod + Ref=SamplePartition rel=AnalysisSamplePartition + Ref=OriginalReflexedAnalysis rel=OriginalAnalysisReflectedAnalysis + Ref=ReflexAnalysisOf rel=AnalysisReflectedAnalysis + + After refactoring, the following references exist + + BaseAnalysis + ============ + Instrument (UIDReferenceField) + Method (UIDReferenceField) + Calculation (HistoryAwareReferenceField + Category (UIDReferenceField) + Department (UIDReferenceField) + AnalysisService + =============== + Preservation (UIDReferenceField) + Container (UIDReferenceField) + Instruments (UIDReferenceField) + Methods (UIDReferenceField) + Analysis + ======== + Attachment (UIDReferenceField) + SamplePartition (UIDReferenceField) + OriginalReflexedAnalysis (UIDReferenceField) + ReflexAnalysisOf (UIDReferenceField) + """ + + # Miscellaneous fixes + logger.info('Removing bika_analysis_catalog/getServiceDefaultInstrumentUID') + ut.delColumn('bika_analysis_catalog', 'getServiceDefaultInstrumentUID') + + # Analysis Services ======================================================== + bsc = get_tool('bika_setup_catalog') + brains = bsc(portal_type='AnalysisService') + for brain in brains: + srv = brain.getObject() + logger.info('Migrating Analysis Service schema for %s' % srv.Title()) + touidref(srv, srv, 'AnalysisServiceInstrument', 'Instrument') + touidref(srv, srv, 'AnalysisServiceInstruments', 'Instruments') + touidref(srv, srv, 'AnalysisServiceMethod', 'Method') + touidref(srv, srv, 'AnalysisServiceMethods', 'Methods') + touidref(srv, srv, 'AnalysisServiceCalculation', 'Calculation') + touidref(srv, srv, 'AnalysisServiceAnalysisCategory', 'Category') + touidref(srv, srv, 'AnalysisServiceDepartment', 'Department') + touidref(srv, srv, 'AnalysisServicePreservation', 'Preservation') + touidref(srv, srv, 'AnalysisServiceContainer', 'Container') + + # Analyses ============================================================= + analyses = srv.getBRefs(relationship='AnalysisAnalysisService') + logger.info('Migrating Analyses schema for %s analyses on %s' + % (len(analyses), srv.Title())) + for an in analyses: + # retain analysis.ServiceUID + an.setServiceUID(srv.UID()) + # Migrate refs to UIDReferenceField + touidref(an, an, 'AnalysisInstrument', 'Instrument') + touidref(an, an, 'AnalysisMethod', 'Method') + an.setCategory(srv.getCategory()) + an.setDepartment(srv.getDepartment()) + touidref(an, an, 'AnalysisAttachment', 'Attachment') + touidref(an, an, 'AnalysisSamplePartition', 'SamplePartition') + touidref(an, an, 'OriginalAnalysisReflectedAnalysis', + 'OriginalReflexedAnalysis') + touidref(an, an, 'AnalysisReflectedAnalysis', 'ReflexAnalysisOf') + + # Then scoop the rest of the fields out of service + copy_field_values(srv, an) + + +def touidref(src, dst, src_relation, fieldname): + """Convert an archetypes reference in src/src_relation to a UIDReference + in dst/fieldname. + """ + refs = src.getRefs(relationship=src_relation) + if len(refs) == 1: + value = get_uid(refs[0]) + elif len(refs) > 1: + value = filter(lambda x: x, [get_uid(ref) for ref in refs]) + else: + value = '' + field = dst.getField(fieldname) + if not field: + raise Exception('Cannot find field %s/%s' % (fieldname, src)) + if field.required and not value: + raise Exception('Required field %s/%s has no value' % (src, fieldname)) + field.set(src, value) + + +def copy_field_values(src, dst): + # These fields are not copied between objects. + IGNORE_FIELDNAMES = ['UID', 'id'] + IGNORE_FIELDTYPES = ['reference'] + + src_schema = src.Schema() + dst_schema = dst.Schema() + + for field in src_schema.fields(): + fieldname = field.getName() + if fieldname in IGNORE_FIELDNAMES \ + or field.type in IGNORE_FIELDTYPES \ + or fieldname not in dst_schema: + continue + value = field.get(src) + if value: + dst_schema[fieldname].set(dst, value) + + +def get_uid(value): + """Takes a brain or object and returns a valid UID. + In this case, the object may come from portal_archivist, so we will + need to do a catalog query to get the UID of the current version + """ + if not value: + return '' + # Is value a brain? + if ICatalogBrain.providedBy(value): + value = value.getObject() + # validate UID + uid = value.UID() + uc = get_tool('uid_catalog') + if uc(UID=uid): + # The object is valid + return uid + # Otherwise the object is an old version + brains = uc(portal_type=value.portal_type, Title=value.Title()) + if not brains: + # Cannot find UID + raise RuntimeError('The UID for %s/%s cannot be discovered in the ' + 'uid_catalog or in the portal_archivist history!' % + (value.portal_type, value.Title())) + if len(brains) > 1: + # Found multiple objects, this is a failure + raise RuntimeError('Searching for %s/%s returned multiple objects.' % + (value.portal_type, value.Title())) + return brains[0].UID From 30a12399be06fdba7fe248335b39a0c59e19210f Mon Sep 17 00:00:00 2001 From: Campbell Date: Wed, 10 May 2017 13:05:35 +0200 Subject: [PATCH 05/36] Refactor all code references to reflect BaseAnalysis changes - Analysis.getService() is deprecated, and has been removed - Some catalog columns in bika_analysis_catalog are removed - Some small pep8 and cleaning up. --- bika/lims/browser/analyses.py | 108 +++---- bika/lims/browser/analysis.py | 2 +- bika/lims/browser/analysisrequest/__init__.py | 8 +- .../analysisrequests_filter_bar.py | 6 +- bika/lims/browser/analysisrequest/invoice.py | 7 +- .../analysisrequest/manage_analyses.py | 2 +- bika/lims/browser/analysisrequest/publish.py | 43 +-- .../templates/analysisrequest_retract_pdf.pt | 2 +- .../templates/reports/bydepartment.pt | 4 +- .../templates/reports/default.pt | 2 +- .../templates/reports/example_1.pt | 2 +- bika/lims/browser/analysisrequest/view.py | 10 +- bika/lims/browser/analysisrequest/workflow.py | 50 +-- bika/lims/browser/batch/batchbook.py | 19 +- bika/lims/browser/batch/publish.py | 5 +- bika/lims/browser/calcs.py | 11 +- .../browser/client/views/analysisspecs.py | 4 +- bika/lims/browser/duplicateanalysis.py | 2 +- bika/lims/browser/fields/aranalysesfield.py | 73 ++--- bika/lims/browser/instrument.py | 4 +- bika/lims/browser/referenceanalysis.py | 4 +- bika/lims/browser/reports/__init__.py | 49 ++- .../productivity_analysesperdepartment.py | 3 +- .../productivity_analysesperformedpertotal.py | 2 +- .../productivity_dailysamplesreceived.py | 2 +- .../qualitycontrol_analysesoutofrange.py | 2 +- .../qualitycontrol_analysesrepeated.py | 2 +- .../qualitycontrol_referenceanalysisqc.py | 7 +- .../qualitycontrol_resultspersamplepoint.py | 11 +- bika/lims/browser/sample/printform.py | 10 +- bika/lims/browser/samplinground/printform.py | 5 +- .../templates/stickers/Code_128_1x48mm.pt | 2 +- .../templates/stickers/Code_128_1x72mm.pt | 4 +- .../templates/stickers/Code_39_1x54mm.pt | 2 +- .../templates/stickers/Code_39_1x72mm.pt | 4 +- .../templates/stickers/Code_93_2dx38mm.pt | 2 +- .../templates/stickers/Code_93_2x38mm.pt | 2 +- bika/lims/browser/widgets/serviceswidget.py | 2 +- bika/lims/browser/worksheet/ajax.py | 4 +- bika/lims/browser/worksheet/views/analyses.py | 7 +- .../lims/browser/worksheet/views/printview.py | 13 +- bika/lims/browser/worksheet/views/results.py | 15 +- bika/lims/browser/worksheet/views/services.py | 2 +- bika/lims/catalog/analysis_catalog.py | 5 +- bika/lims/content/analysis.py | 109 +++---- bika/lims/content/analysisrequest.py | 39 +-- bika/lims/content/baseanalysis.py | 10 +- bika/lims/content/batch.py | 2 +- bika/lims/content/duplicateanalysis.py | 3 +- bika/lims/content/reflexrule.py | 5 +- bika/lims/content/sample.py | 2 +- bika/lims/content/worksheet.py | 21 +- bika/lims/exportimport/setupdata/__init__.py | 23 +- bika/lims/skins/bika/attachments.pt | 2 +- .../skins/bika/guard_attach_transition.py | 6 +- .../skins/bika/guard_submit_transition.py | 3 +- bika/lims/subscribers/objectmodified.py | 20 +- .../lims/tests/test_MultiVerificationTypes.py | 4 +- bika/lims/tests/test_limitdetections.py | 4 +- bika/lims/tools/bika_ar_export.py | 11 +- bika/lims/tools/bika_instrument_import.py | 2 +- bika/lims/upgrade/to3050.py | 2 +- bika/lims/upgrade/v3_2_0_1705.py | 4 +- bika/lims/utils/analysis.py | 301 ++++++++---------- bika/lims/utils/analysisrequest.py | 7 +- 65 files changed, 506 insertions(+), 599 deletions(-) diff --git a/bika/lims/browser/analyses.py b/bika/lims/browser/analyses.py index 875188680f..55046b1dd2 100644 --- a/bika/lims/browser/analyses.py +++ b/bika/lims/browser/analyses.py @@ -5,13 +5,11 @@ # Copyright 2011-2016 by it's authors. # Some rights reserved. See LICENSE.txt, AUTHORS.txt. -from plone import api from Products.CMFPlone.utils import safe_unicode from bika.lims import bikaMessageFactory as _ from bika.lims import deprecated from bika.lims.utils import t, dicts_to_dict, format_supsub from bika.lims.utils.analysis import format_uncertainty -from bika.lims.browser import BrowserView from bika.lims.browser.bika_listing import BikaListingView from bika.lims.config import QCANALYSIS_TYPES from bika.lims.interfaces import IResultOutOfRange @@ -19,21 +17,15 @@ from bika.lims.permissions import Verify as VerifyPermission from bika.lims.utils import isActive from bika.lims.utils import getUsers -from bika.lims.utils import to_utf8 from bika.lims.utils import formatDecimalMark from DateTime import DateTime from operator import itemgetter from Products.Archetypes.config import REFERENCE_CATALOG -from Products.CMFCore.utils import getToolByName -from Products.CMFCore.WorkflowCore import WorkflowException from Products.ZCatalog.interfaces import ICatalogBrain -from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile -from bika.lims.utils.analysis import format_numeric_result -from zope.interface import implements -from zope.interface import Interface from zope.component import getAdapters from bika.lims.catalog import CATALOG_ANALYSIS_LISTING - +from plone.api.portal import get_tool +from plone.api.user import has_permission import json @@ -67,10 +59,10 @@ def __init__(self, context, request, **kwargs): self.interim_fields = {} self.interim_columns = {} self.specs = {} - self.bsc = getToolByName(context, 'bika_setup_catalog') - self.portal = getToolByName(context, 'portal_url').getPortalObject() + self.bsc = get_tool('bika_setup_catalog') + self.portal = get_tool('portal_url').getPortalObject() self.portal_url = self.portal.absolute_url() - self.rc = getToolByName(context, REFERENCE_CATALOG) + self.rc = get_tool(REFERENCE_CATALOG) # Initializing the deximal mark variable self.dmk = '' request.set('disable_plone.rightcolumn', 1) @@ -213,7 +205,7 @@ def get_analysis_spec(self, analysis): if hasattr(analysis.aq_parent, 'getReferenceResults'): rr = dicts_to_dict(analysis.aq_parent.getReferenceResults(), 'uid') return rr.get(analysis.UID(), None) - keyword = analysis.getService().getKeyword() + keyword = analysis.getKeyword() uid = analysis.UID() return { 'keyword': keyword, 'uid': uid, 'min': '', @@ -297,14 +289,12 @@ def get_methods_vocabulary(self, analysis=None): return ret def get_instruments_vocabulary(self, analysis = None): - """ - Returns a vocabulary with the valid and active instruments available + """Returns a vocabulary with the valid and active instruments available for the analysis passed in. If the analysis is None, the function returns all the active and valid instruments registered in the system. If the option "Allow instrument entry of results" for the Analysis - Service associated to the analysis is disabled, the function returns - an empty vocabulary. + is disabled, the function returns an empty vocabulary. If the analysis passed in is a Reference Analysis (Blank or Control), the voculabury, the invalid instruments will be included in the vocabulary too. @@ -320,16 +310,14 @@ def get_instruments_vocabulary(self, analysis = None): :rtype: A list of dicts """ ret = [] - instruments = [] if analysis: - service = analysis.getService() - if service.getInstrumentEntryOfResults() == False: + if not analysis.getInstrumentEntryOfResults(): return [] method = analysis.getMethod() \ - if hasattr(analysis, 'getMethod') else None - instruments = method.getInstruments() if method \ - else analysis.getService().getInstruments() + if hasattr(analysis, 'getMethod') else None + instruments = method.getInstruments() \ + if method else analysis.getAllowedInstruments() else: # All active instruments @@ -564,42 +552,37 @@ def folderitem(self, obj, item, index): item['Instrument'] = '' item['replace']['Instrument'] = '' if obj.getInstrumentEntryOfResults: - instrumentUID = None - - # If the analysis has an instrument already assigned, use it - if obj.getInstrumentEntryOfResults and obj.getInstrumentUID: - instrumentUID = obj.getInstrumentUID - - # Otherwise, use the Service's default instrument - elif obj.getInstrumentEntryOfResults: - instrumentUID = obj.getServiceDefaultInstrumentUID + instrument_uid = obj.getInstrumentUID + uc = get_tool('uid_catalog') + brains = uc(UID=instrument_uid) + instrument = brains[0].getObject() if brains else None if can_set_instrument: # Edition allowed voc = self.get_instruments_vocabulary(obj) if voc: # The service has at least one instrument available - item['Instrument'] = instrumentUID + item['Instrument'] = instrument.UID() if instrument else '' item['choices']['Instrument'] = voc item['allow_edit'].append('Instrument') show_methodinstr_columns = True - elif instrumentUID: + elif instrument: # This should never happen # The analysis has an instrument set, but the # service hasn't any available instrument - item['Instrument'] = obj.getServiceDefaultInstrumentTitle - item['replace']['Instrument'] = "%s" % \ - (obj.getServiceDefaultInstrumentURL, - obj.getServiceDefaultInstrumentTitle) + item['Instrument'] = instrument.Title() + item['replace']['Instrument'] = \ + "%s" % (instrument.absolute_url(), + instrument.Title()) show_methodinstr_columns = True - elif instrumentUID: + elif instrument: # Edition not allowed, but instrument set - item['Instrument'] = obj.getServiceDefaultInstrumentTitle - item['replace']['Instrument'] = "%s" % \ - (obj.getServiceDefaultInstrumentURL, - obj.getServiceDefaultInstrumentTitle) + item['Instrument'] = instrument.Title() + item['replace']['Instrument'] = \ + "%s" % (instrument.absolute_url(), + instrument.Title()) show_methodinstr_columns = True else: @@ -607,8 +590,7 @@ def folderitem(self, obj, item, index): item['Instrument'] = _('Manual') msgtitle = t(_( "Instrument entry of results not allowed for ${service}", - mapping={"service": safe_unicode( - obj.getServiceDefaultInstrumentTitle)}, + mapping={"service": obj.Title}, )) item['replace']['Instrument'] = \ '%s' % (msgtitle, t(_('Manual'))) @@ -655,7 +637,6 @@ def folderitem(self, obj, item, index): # permission, otherwise just put an icon in Result column. if can_view_result: full_obj = full_obj if full_obj else obj.getObject() - service = full_obj.getService() item['Result'] = result scinot = self.context.bika_setup.getScientificNotationResults() item['formatted_result'] =\ @@ -667,7 +648,7 @@ def folderitem(self, obj, item, index): fu = format_uncertainty( full_obj, result, decimalmark=self.dmk, sciformat=int(scinot)) fu = fu if fu else '' - if can_edit_analysis and service.getAllowManualUncertainty(): + if can_edit_analysis and full_obj.getAllowManualUncertainty(): unc = full_obj.getUncertainty(result) item['allow_edit'].append('Uncertainty') item['Uncertainty'] = unc if unc else '' @@ -687,8 +668,8 @@ def folderitem(self, obj, item, index): # https://jira.bikalabs.com/browse/LIMS-1775 if can_edit_analysis and \ hasattr(full_obj, 'getDetectionLimitOperand') and \ - hasattr(service, 'getDetectionLimitSelector') and \ - service.getDetectionLimitSelector(): + hasattr(full_obj, 'getDetectionLimitSelector') and \ + full_obj.getDetectionLimitSelector(): isldl = full_obj.isBelowLowerDetectionLimit() isudl = full_obj.isAboveUpperDetectionLimit() dlval = '' @@ -701,10 +682,9 @@ def folderitem(self, obj, item, index): {'ResultValue': '>', 'ResultText': '>'}] item['choices']['DetectionLimit'] = choices self.columns['DetectionLimit']['toggle'] = True - srv = full_obj.getService() - defdls = {'min': srv.getLowerDetectionLimit(), - 'max': srv.getUpperDetectionLimit(), - 'manual': srv.getAllowManualDetectionLimit()} + defdls = {'min': full_obj.getLowerDetectionLimit(), + 'max': full_obj.getUpperDetectionLimit(), + 'manual': full_obj.getAllowManualDetectionLimit()} defin =\ '' defin = defin % (full_obj.UID(), json.dumps(defdls)) @@ -729,12 +709,12 @@ def folderitem(self, obj, item, index): dls['above_udl'] = full_obj.isBelowLowerDetectionLimit() dls['is_ldl'] = full_obj.isLowerDetectionLimit() dls['is_udl'] = full_obj.isUpperDetectionLimit() - dls['default_ldl'] = service.getLowerDetectionLimit() - dls['default_udl'] = service.getUpperDetectionLimit() + dls['default_ldl'] = full_obj.getLowerDetectionLimit() + dls['default_udl'] = full_obj.getUpperDetectionLimit() dls['manual_allowed'] =\ - service.getAllowManualDetectionLimit() + full_obj.getAllowManualDetectionLimit() dls['dlselect_allowed'] =\ - service.getDetectionLimitSelector() + full_obj.getDetectionLimitSelector() dlsin =\ '' dlsin = dlsin % (full_obj.UID(), json.dumps(dls)) @@ -819,7 +799,7 @@ def folderitem(self, obj, item, index): username = self.member.getUserName() user_id = self.member.getUser().getId() # Check if the user has "Bika: Verify" privileges - verify_permission = api.user.has_permission( + verify_permission = has_permission( VerifyPermission, username=username) isUserAllowedToVerify = True @@ -897,7 +877,7 @@ def folderitem(self, obj, item, index): def folderitems(self): # Check if mtool has been initialized self.mtool = self.mtool if self.mtool\ - else getToolByName(self.context, 'portal_membership') + else get_tool('portal_membership') # Getting the current user self.member = self.member if self.member\ else self.mtool.getAuthenticatedMember() @@ -909,7 +889,7 @@ def folderitems(self): self.analysis_categories_order = dict([ (b.Title, "{:04}".format(a)) for a, b in enumerate(analysis_categories)]) - workflow = getToolByName(self.context, 'portal_workflow') + workflow = get_tool('portal_workflow') # Can the user edit? if not self.allow_edit: can_edit_analyses = False @@ -984,7 +964,7 @@ def folderitems(self): # look through all items # if the item's Service supports ReportDryMatter, add getResultDM(). for item in items: - if item['obj'].getService().getReportDryMatter(): + if item['obj'].getReportDryMatter(): item['ResultDM'] = item['obj'].getResultDM() else: item['ResultDM'] = '' @@ -1077,11 +1057,11 @@ def folderitem(self, obj, item, index): elif obj.portal_type == 'DuplicateAnalysis': antype = QCANALYSIS_TYPES.getValue('d') imgtype = " " % (antype, self.context.absolute_url()) - item['sortcode'] = '%s_%s' % (obj.getSample().id, obj.getService().getKeyword()) + item['sortcode'] = '%s_%s' % (obj.getSample().id, obj.getKeyword()) item['before']['Service'] = imgtype item['sortcode'] = '%s_%s' % (obj.getReferenceAnalysesGroupID(), - obj.getService().getKeyword()) + obj.getKeyword()) return item def folderitems(self): diff --git a/bika/lims/browser/analysis.py b/bika/lims/browser/analysis.py index 9525aa80d1..cd9ab96b8f 100644 --- a/bika/lims/browser/analysis.py +++ b/bika/lims/browser/analysis.py @@ -193,7 +193,7 @@ def analysis_specification(self): ar = self.context.aq_parent rr = dicts_to_dict(ar.getResultsRange(),'keyword') - return rr[self.context.getService().getKeyword()] + return rr[self.context.getKeyword()] def __call__(self, request, data): self.request = request diff --git a/bika/lims/browser/analysisrequest/__init__.py b/bika/lims/browser/analysisrequest/__init__.py index a4a80e48e0..919db979ed 100644 --- a/bika/lims/browser/analysisrequest/__init__.py +++ b/bika/lims/browser/analysisrequest/__init__.py @@ -125,7 +125,6 @@ def ar_analysis_values(self): analyses = self.context.getAnalyses(cancellation_state='active') for proxy in analyses: analysis = proxy.getObject() - service = analysis.getService() if proxy.review_state == 'retracted': # these are scraped up when Retested analyses are found below. continue @@ -133,13 +132,10 @@ def ar_analysis_values(self): # These things will be included even if they are not present in # include_fields in the request. method = analysis.getMethod() - if not method: - method = service.getMethod() - service = analysis.getService() analysis_data = { - "Uncertainty": service.getUncertainty(analysis.getResult()), + "Uncertainty": analysis.getUncertainty(), "Method": method.Title() if method else '', - "Unit": service.getUnit(), + "Unit": analysis.getUnit(), } # Place all schema fields ino the result. analysis_data.update(load_brain_metadata(proxy, [])) diff --git a/bika/lims/browser/analysisrequest/analysisrequests_filter_bar.py b/bika/lims/browser/analysisrequest/analysisrequests_filter_bar.py index 95f1ee5e61..48c39da811 100644 --- a/bika/lims/browser/analysisrequest/analysisrequests_filter_bar.py +++ b/bika/lims/browser/analysisrequest/analysisrequests_filter_bar.py @@ -83,14 +83,12 @@ def filter_bar_check_item(self, item): """ dbar = self.get_filter_bar_dict() keys = dbar.keys() - final_decision = 'True' item_obj = None for key in keys: if key == 'analysis_name' and dbar.get(key, '') != '': item_obj = item.getObject() if not item_obj else item_obj - uids = [ - analysis.getService().UID() for analysis in - item_obj.getAnalyses(full_objects=True)] + uids = [analysis.getServiceUID for analysis in + item_obj.getAnalyses(full_objects=False)] if dbar.get(key, '') not in uids: return False return True diff --git a/bika/lims/browser/analysisrequest/invoice.py b/bika/lims/browser/analysisrequest/invoice.py index 760861799c..3b332d535d 100644 --- a/bika/lims/browser/analysisrequest/invoice.py +++ b/bika/lims/browser/analysisrequest/invoice.py @@ -101,8 +101,7 @@ def __call__(self): all_analyses, all_profiles, analyses_from_profiles = context.getServicesAndProfiles() # Relating category with solo analysis for analysis in all_analyses: - service = analysis.getService() - categoryName = service.getCategory().Title() + categoryName = analysis.getCategoryTitle() # Find the category try: category = ( @@ -178,9 +177,9 @@ def _getAnalysisForProfileService(self, service_keyword, analyses): :analyses: a list of analyses """ for analysis in analyses: - if service_keyword == analysis.getService().getKeyword(): + if service_keyword == analysis.getKeyword(): return analysis - return 0 + return None def getPriorityIcon(self): priority = self.context.getPriority() diff --git a/bika/lims/browser/analysisrequest/manage_analyses.py b/bika/lims/browser/analysisrequest/manage_analyses.py index dec2c393d9..2b5f3e9fef 100644 --- a/bika/lims/browser/analysisrequest/manage_analyses.py +++ b/bika/lims/browser/analysisrequest/manage_analyses.py @@ -240,7 +240,7 @@ def folderitems(self): part = part and part or obj items[x]['Partition'] = part.Title() spec = self.get_spec_from_ar(self.context, - analysis.getService().getKeyword()) + analysis.getKeyword()) items[x]["min"] = spec.get("min",'') items[x]["max"] = spec.get("max",'') items[x]["error"] = spec.get("error",'') diff --git a/bika/lims/browser/analysisrequest/publish.py b/bika/lims/browser/analysisrequest/publish.py index a821e07581..3b00b28ad3 100644 --- a/bika/lims/browser/analysisrequest/publish.py +++ b/bika/lims/browser/analysisrequest/publish.py @@ -343,7 +343,7 @@ def _ar_data(self, ar, excludearuids=[]): # Group by department too anobj = an['obj'] - dept = anobj.getService().getDepartment() if anobj.getService() else None + dept = anobj.getDepartment() if dept: dept = dept.UID() dep = data['department_analyses'].get(dept, {}) @@ -568,8 +568,8 @@ def _analyses_data(self, ar, analysis_states=['verified', 'published']): # Omit hidden analyses? if not showhidden: - serv = an.getService() - asets = ar.getAnalysisServiceSettings(serv.UID()) + service_uid = an.getServiceUID() + asets = ar.getAnalysisServiceSettings(service_uid) if asets.get('hidden'): # Hide analysis continue @@ -603,19 +603,18 @@ def _analysis_data(self, analysis, decimalmark=None): return self._cache['_analysis_data'][analysis.UID()] keyword = analysis.getKeyword() - service = analysis.getService() andict = {'obj': analysis, 'id': analysis.id, 'title': analysis.Title(), 'keyword': keyword, - 'scientific_name': service.getScientificName(), - 'accredited': service.getAccredited(), - 'point_of_capture': to_utf8(POINTS_OF_CAPTURE.getValue(service.getPointOfCapture())), - 'category': to_utf8(service.getCategoryTitle()), + 'scientific_name': analysis.getScientificName(), + 'accredited': analysis.getAccredited(), + 'point_of_capture': to_utf8(POINTS_OF_CAPTURE.getValue(analysis.getPointOfCapture())), + 'category': to_utf8(analysis.getCategoryTitle()), 'result': analysis.getResult(), 'isnumber': isnumber(analysis.getResult()), - 'unit': to_utf8(service.getUnit()), - 'formatted_unit': format_supsub(to_utf8(service.getUnit())), + 'unit': to_utf8(analysis.getUnit()), + 'formatted_unit': format_supsub(to_utf8(analysis.getUnit())), 'capture_date': analysis.getResultCaptureDate(), 'request_id': analysis.aq_parent.getId(), 'formatted_result': '', @@ -1092,24 +1091,26 @@ def getAnaysisBasedTransposedMatrix(self, ars): for ar in ars: ans = [an.getObject() for an in ar.getAnalyses()] for an in ans: - service = an.getService() - cat = service.getCategoryTitle() + cat = analysis.getCategoryTitle() + an_title = analysis.Title() if cat not in analyses: analyses[cat] = { - service.title: { - 'service': service, - 'accredited': service.getAccredited(), + an_title: { + # The report should not mind receiving 'analysis' + # here - service fields are all inside! + 'service': analysis, + 'accredited': analysis.getAccredited(), 'ars': {ar.id: an.getFormattedResult()} } } - elif service.title not in analyses[cat]: - analyses[cat][service.title] = { - 'service': service, - 'accredited': service.getAccredited(), + elif an_title not in analyses[cat]: + analyses[cat][an_title] = { + 'service': analysis, + 'accredited': analysis.getAccredited(), 'ars': {ar.id: an.getFormattedResult()} } else: - d = analyses[cat][service.title] + d = analyses[cat][an_title] d['ars'][ar.id] = an.getFormattedResult() - analyses[cat][service.title]=d + analyses[cat][an_title]=d return analyses diff --git a/bika/lims/browser/analysisrequest/templates/analysisrequest_retract_pdf.pt b/bika/lims/browser/analysisrequest/templates/analysisrequest_retract_pdf.pt index 759caa0629..eb8d69620a 100644 --- a/bika/lims/browser/analysisrequest/templates/analysisrequest_retract_pdf.pt +++ b/bika/lims/browser/analysisrequest/templates/analysisrequest_retract_pdf.pt @@ -132,7 +132,7 @@ Tests requested/Measurement Procedure + analyses python:[('%s (%s)' % (an.Title(), an.getKeyword())) for an in analyses];"> diff --git a/bika/lims/browser/analysisrequest/templates/reports/bydepartment.pt b/bika/lims/browser/analysisrequest/templates/reports/bydepartment.pt index 54066c2dc0..c2901d8bf6 100644 --- a/bika/lims/browser/analysisrequest/templates/reports/bydepartment.pt +++ b/bika/lims/browser/analysisrequest/templates/reports/bydepartment.pt @@ -274,7 +274,7 @@
@@ -304,7 +304,7 @@
+ an_attachments python:[an.getAttachment() for an in analysisrequest['obj'].getAnalyses(full_objects=True) if (an.getAttachment() and an.getDepartment() and an.getService().getDepartment().UID()==dept.UID())];">

Attachments

Analysis request attachments