def isOpen(self): """ Returns true if the Batch is in 'open' state """ revstatus = getCurrentState(self, StateFlow.review) canstatus = getCurrentState(self, StateFlow.cancellation) return revstatus == BatchState.open \ and canstatus == CancellationState.active
def workflow_guard_close(self): """ Permitted if current review_state is 'open'. The close transition is already controlled by 'Bika: Close Batch' permission, but left here for security reasons and also for the capability of being expanded/overrided by child products or instance-specific needs. """ revstatus = getCurrentState(self, StateFlow.review) canstatus = getCurrentState(self, StateFlow.cancellation) return revstatus == BatchState.open and canstatus == CancellationState.active
def workflow_guard_close(self): """ Permitted if current review_state is 'open'. The close transition is already controlled by 'Bika: Close Batch' permission, but left here for security reasons and also for the capability of being expanded/overrided by child products or instance-specific needs. """ revstatus = getCurrentState(self, StateFlow.review) canstatus = getCurrentState(self, StateFlow.cancellation) return revstatus == BatchState.open \ and canstatus == CancellationState.active
def after_reject(obj): """Method triggered after a 'reject' transition for the Analysis Request passed in is performed. Transitions and sets the rejection reasons to the parent Sample. Also transitions the analyses assigned to the AR bika.lims.workflow.AfterTransitionEventHandler :param obj: Analysis Request affected by the transition :type obj: AnalysisRequest """ sample = obj.getSample() if not sample: return if getCurrentState(sample) != 'rejected': doActionFor(sample, 'reject') reasons = obj.getRejectionReasons() sample.setRejectionReasons(reasons) # Deactivate all analyses from this Analysis Request ans = obj.getAnalyses(full_objects=True) for analysis in ans: doActionFor(analysis, 'reject') if obj.bika_setup.getNotifyOnRejection(): # Import here to brake circular importing somewhere from bika.lims.utils.analysisrequest import notify_rejection # Notify the Client about the Rejection. notify_rejection(obj)
def prepublish(obj): """Returns True if 'prepublish' transition can be applied to the Analysis Request passed in. Returns true if the Analysis Request is active (not in a cancelled/inactive state), the 'publish' transition cannot be performed yet, and at least one of its analysis is under to_be_verified state or has been already verified. As per default DC workflow definition in bika_ar_workflow, note that prepublish does not transitions the Analysis Request to any other state different from the actual one, neither its children. This 'fake' transition is only used for the prepublish action to be displayed when the Analysis Request' status is other than verified, so the labman can generate a provisional report, also if results are not yet definitive. :returns: true or false """ if not isBasicTransitionAllowed(obj): return False if isTransitionAllowed(obj, 'publish'): return False analyses = obj.getAnalyses(full_objects=True) for an in analyses: # If the analysis is not active, omit if not isActive(an): continue # Check if the current state is 'verified' status = getCurrentState(an) if status in ['verified', 'to_be_verified']: return True # This analysis request has no single result ready to be verified or # verified yet. In this situation, it doesn't make sense to publish a # provisional results reports without a single result to display return False
def after_reject(obj): """Method triggered after a 'reject' transition for the Sample passed in is performed. Transitions children (Analysis Requests and Sample Partitions) bika.lims.workflow.AfterTransitionEventHandler :param obj: Sample affected by the transition :type obj: Sample """ ars = obj.getAnalysisRequests() for ar in ars: if getCurrentState(ar) != 'rejected': doActionFor(ar, 'reject') reasons = obj.getRejectionReasons() ar.setRejectionReasons(reasons) parts = obj.objectValues('SamplePartition') for part in parts: if getCurrentState(part) != 'rejected': doActionFor(part, "reject")
def getContacts(self, only_active=True): """ Return an array containing the contacts from this Client """ contacts = [] if only_active: contacts = [c for c in self.objectValues('Contact') if getCurrentState(c, StateFlow.inactive) == InactiveState.active] else: contacts = self.objectValues('Contact') return contacts;
def __call__(self): plone.protect.CheckAuthenticator(self.request) searchTerm = 'searchTerm' in self.request and self.request['searchTerm'].lower() or '' page = self.request['page'] nr_rows = self.request['rows'] sord = self.request['sord'] sidx = self.request['sidx'] attachable_states = ('assigned', 'unassigned', 'to_be_verified') analysis_to_slot = {} for s in self.context.getLayout(): analysis_to_slot[s['analysis_uid']] = int(s['position']) analyses = list(self.context.getAnalyses(full_objects=True)) # Duplicates belong to the worksheet, so we must add them individually for i in self.context.objectValues(): if i.portal_type == 'DuplicateAnalysis': analyses.append(i) rows = [] for analysis in analyses: review_state = getCurrentState(analysis) if review_state not in attachable_states: continue parent = analysis.getParentTitle() rows.append({'analysis_uid': analysis.UID(), 'slot': analysis_to_slot[analysis.UID()], 'service': analysis.Title(), 'parent': parent, 'type': analysis.portal_type}) # if there's a searchTerm supplied, restrict rows to those # who contain at least one field that starts with the chars from # searchTerm. if searchTerm: orig_rows = rows rows = [] for row in orig_rows: matches = [v for v in row.values() if str(v).lower().startswith(searchTerm)] if matches: rows.append(row) rows = sorted(rows, cmp=lambda x, y: cmp(x, y), key=itemgetter(sidx and sidx or 'slot')) if sord == 'desc': rows.reverse() pages = len(rows) / int(nr_rows) pages += divmod(len(rows), int(nr_rows))[1] and 1 or 0 start = (int(page)-1) * int(nr_rows) end = int(page) * int(nr_rows) ret = {'page': page, 'total': pages, 'records': len(rows), 'rows': rows[start:end]} return json.dumps(ret)
def guard_rollback_to_receive(analysis_request): """Return whether 'rollback_to_receive' transition can be performed or not """ # Can rollback to receive if at least one analysis hasn't been submitted yet # or if all analyses have been rejected or retracted analyses = analysis_request.getAnalyses() skipped = 0 for analysis in analyses: analysis_object = api.get_object(analysis) state = getCurrentState(analysis_object) if state in ["unassigned", "assigned"]: return True if state in ["retracted", "rejected"]: skipped += 1 return len(analyses) == skipped
def test_LIMS_2080_correctly_interpret_false_and_blank_values(self): client = self.portal.clients.objectValues()[0] arimport = self.addthing(client, 'ARImport') arimport.unmarkCreationFlag() arimport.setFilename("test1.csv") arimport.setOriginalFile(""" Header, File name, Client name, Client ID, Contact, CC Names - Report, CC Emails - Report, CC Names - Invoice, CC Emails - Invoice, No of Samples, Client Order Number, Client Reference,, Header Data, test1.csv, Happy Hills, HH, Rita Mohale, , , , , 10, HHPO-001, ,, Samples, ClientSampleID, SamplingDate,DateSampled,SamplePoint,SampleMatrix,SampleType,ContainerType,ReportDryMatter,Priority,Total number of Analyses or Profiles,Price excl Tax,ECO,SAL,COL,TAS,MicroBio,Properties Analysis price,,,,,,,,,,,,,, "Total Analyses or Profiles",,,,,,,,,,,,,9,,, Total price excl Tax,,,,,,,,,,,,,, "Sample 1", HHS14001, 3/9/2014, 3/9/2014, , , Water, Cup, 0, Normal, 1, 0, 0,0,0,0,0,1 "Sample 2", HHS14002, 3/9/2014, 3/9/2014, , , Water, Cup, 0, Normal, 2, 0, 0,0,0,0,1,1 "Sample 3", HHS14002, 3/9/2014, 3/9/2014, Toilet, Liquids, Water, Cup, 1, Normal, 4, 0, 1,1,1,1,0,0 "Sample 4", HHS14002, 3/9/2014, 3/9/2014, Toilet, Liquids, Water, Cup, 1, Normal, 2, 0, 1,0,0,0,1,0 """) # check that values are saved without errors arimport.setErrors([]) arimport.save_header_data() arimport.save_sample_data() errors = arimport.getErrors() if errors: self.fail("Unexpected errors while saving data: " + str(errors)) transaction.commit() browser = self.getBrowser( username=TEST_USER_NAME, password=TEST_USER_PASSWORD, loggedIn=True) doActionFor(arimport, 'validate') c_state = getCurrentState(arimport) self.assertTrue( c_state == 'valid', "ARrimport in 'invalid' state after it has been transitioned to " "'valid'.") browser.open(arimport.absolute_url() + "/edit") content = browser.contents re.match( '<option selected=\"selected\" value=\"\d+\">Toilet</option>', content) if len(re.findall('<.*selected.*Toilet', content)) != 2: self.fail("Should be two empty SamplePoints, and two with values") if len(re.findall('<.*selected.*Liquids', content)) != 2: self.fail("Should be two empty Matrix fields, and two with values") if len(re.findall('<.*checked.*ReportDry', content)) != 2: self.fail("Should be two False DryMatters, and two True")
def guard_submit(context): if not IAnalysisRequest.providedBy(context): # Note that this guard is only used for bika_ar_workflow! return True logger.info("*** Custom Guard: submit **") if not isBasicTransitionAllowed(context): return False invalid = 0 analyses = context.getAnalyses() for an in analyses: if an.review_state == 'to_be_verified': continue # The analysis has already been verified? an = api.get_object(an) if wasTransitionPerformed(an, 'submit'): continue # Maybe the analysis is in an 'inactive' state? if not isActive(an): invalid += 1 continue # Maybe the analysis has been rejected or retracted? dettached = ['rejected', 'retracted', 'attachments_due'] status = getCurrentState(an) if status in dettached: invalid += 1 continue # At this point we can assume this analysis is an a valid state and # the AR could potentially be submitted, but the Analysis Request can # only be submitted if all the analyses have been submitted already return False # Be sure that at least there is one analysis in an active state, it # doesn't make sense to submit an Analysis Request if all the analyses that # contains are rejected or cancelled! return len(analyses) - invalid > 0
def verify(obj): """Returns True if 'verify' transition can be applied to the Analysis Request passed in. This is, returns true if all the analyses that contains have already been verified. Those analyses that are in an inactive state (cancelled, inactive) are dismissed, but at least one analysis must be in an active state (and verified), otherwise always return False. If the Analysis Request is in inactive state (cancelled/inactive), returns False Note this guard depends entirely on the current status of the children :returns: true or false """ if not isBasicTransitionAllowed(obj): return False analyses = obj.getAnalyses(full_objects=True) invalid = 0 for an in analyses: # The analysis has already been verified? if wasTransitionPerformed(an, 'verify'): continue # Maybe the analysis is in an 'inactive' state? if not isActive(an): invalid += 1 continue # Maybe the analysis has been rejected or retracted? dettached = ['rejected', 'retracted', 'attachments_due'] status = getCurrentState(an) if status in dettached: invalid += 1 continue # At this point we can assume this analysis is an a valid state and # could potentially be verified, but the Analysis Request can only be # verified if all the analyses have been transitioned to verified return False # Be sure that at least there is one analysis in an active state, it # doesn't make sense to verify an Analysis Request if all the analyses that # contains are rejected or cancelled! return len(analyses) - invalid > 0
def _children_are_ready(obj, transition_id, dettached_states=None): """Returns true if the children of the object passed in (worksheet) have been all transitioned in accordance with the 'transition_id' passed in. If dettached_states is provided, children with those states are dismissed, so they will not be taken into account in the evaluation. Nevertheless, at least one child with for which the transition_id performed is required for this function to return true (if all children are in dettached states, it always return False). """ analyses = obj.getAnalyses() invalid = 0 for an in analyses: # The analysis has already been transitioned? if wasTransitionPerformed(an, transition_id): continue # Maybe the analysis is in an 'inactive' state? if not isActive(an): invalid += 1 continue # Maybe the analysis is in a dettached state? if dettached_states: status = getCurrentState(an) if status in dettached_states: invalid += 1 continue # At this point we can assume this analysis is an a valid state and # could potentially be transitioned, but the Worksheet can only be # transitioned if all the analyses have been transitioned previously return False # Be sure that at least there is one analysis in an active state, it # doesn't make sense to transition a Worksheet if all the analyses that # contains are not valid return len(analyses) - invalid > 0
def workflow_action_save_analyses_button(self): form = self.request.form workflow = getToolByName(self.context, 'portal_workflow') bsc = self.context.bika_setup_catalog action, came_from = WorkflowAction._get_form_workflow_action(self) # AR Manage Analyses: save Analyses ar = self.context sample = ar.getSample() objects = WorkflowAction._get_selected_items(self) if not objects: message = _("No analyses have been selected") self.context.plone_utils.addPortalMessage(message, 'info') self.destination_url = self.context.absolute_url() + "/analyses" self.request.response.redirect(self.destination_url) return Analyses = objects.keys() prices = form.get("Price", [None])[0] # Hidden analyses? # https://jira.bikalabs.com/browse/LIMS-1324 outs = [] hiddenans = form.get('Hidden', {}) for uid in Analyses: hidden = hiddenans.get(uid, '') hidden = True if hidden == 'on' else False outs.append({'uid': uid, 'hidden': hidden}) ar.setAnalysisServicesSettings(outs) specs = {} if form.get("min", None): for service_uid in Analyses: service = objects[service_uid] keyword = service.getKeyword() specs[service_uid] = { "min": form["min"][0][service_uid], "max": form["max"][0][service_uid], "warn_min": form["warn_min"][0][service_uid], "warn_max": form["warn_max"][0][service_uid], "keyword": keyword, "uid": service_uid, } else: for service_uid in Analyses: service = objects[service_uid] keyword = service.getKeyword() specs[service_uid] = ResultsRangeDict(keyword=keyword, uid=service_uid) new = ar.setAnalyses(Analyses, prices=prices, specs=specs.values()) # link analyses and partitions # If Bika Setup > Analyses > 'Display individual sample # partitions' is checked, no Partitions available. # https://github.com/bikalabs/Bika-LIMS/issues/1030 if 'Partition' in form: for service_uid, service in objects.items(): part_id = form['Partition'][0][service_uid] part = sample[part_id] analysis = ar[service.getKeyword()] analysis.setSamplePartition(part) analysis.reindexObject() partans = part.getAnalyses() partans.append(analysis) part.setAnalyses(partans) part.reindexObject() if new: ar_state = getCurrentState(ar) if wasTransitionPerformed(ar, 'to_be_verified'): # Apply to AR only; we don't want this transition to cascade. ar.REQUEST['workflow_skiplist'].append("retract all analyses") workflow.doActionFor(ar, 'retract') ar.REQUEST['workflow_skiplist'].remove("retract all analyses") ar_state = getCurrentState(ar) for analysis in new: changeWorkflowState(analysis, 'bika_analysis_workflow', ar_state) message = PMF("Changes saved.") self.context.plone_utils.addPortalMessage(message, 'info') self.destination_url = self.context.absolute_url() self.request.response.redirect(self.destination_url)