diff --git a/CHANGES.rst b/CHANGES.rst index aa25e1fac8..6b4267fc40 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,7 @@ Changelog **Added** +- #607 Ability to choose sticker template based on sample type - #480 Sample panel in dashboard - #617 Instrument import interface: 2-Dimensional-CSV - #617 Instrument import interface: Agilent Masshunter @@ -16,6 +17,7 @@ Changelog - #617 Instrument import interface: Shimadzu LC MS/MS Nexera X2 - #537 Instrument import interface: Sysmex XT-4000i - #536 Instrument import interface: Sysmex XT-1800i +- #607 Barcode and labelling depending on Sample Type - #618 When previewing stickers the number of copies to print for each sticker can be modified. - #618 The default number of sticker copies can be set and edited in the setup Sticker's tab. diff --git a/bika/lims/adapters/configure.zcml b/bika/lims/adapters/configure.zcml index 7d583697fe..472cf15ef9 100644 --- a/bika/lims/adapters/configure.zcml +++ b/bika/lims/adapters/configure.zcml @@ -114,4 +114,17 @@ preserveOriginal="True" /> + + + + + diff --git a/bika/lims/adapters/stickers.py b/bika/lims/adapters/stickers.py new file mode 100644 index 0000000000..bc443955dd --- /dev/null +++ b/bika/lims/adapters/stickers.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +# +# This file is part of SENAITE.CORE +# +# Copyright 2018 by it's authors. +# Some rights reserved. See LICENSE.rst, CONTRIBUTORS.rst. + +from zope.interface import implements + +from bika.lims import logger +from bika.lims.interfaces import IGetStickerTemplates +from bika.lims.vocabularies import getStickerTemplates + + +class GetSampleStickers(object): + """ + Returns an array with the templates of stickers available for Sample + object in context. + Each array item is a dictionary with the following structure: + [{'id': , + 'title': , + 'selected: True/False'}, ] + """ + + implements(IGetStickerTemplates) + + def __init__(self, context): + self.context = context + self.request = None + self.sample_type = None + + def __call__(self, request): + self.request = request + # Stickers admittance are saved in sample type + if not hasattr(self.context, 'getSampleType'): + logger.warning( + "{} has no attribute 'getSampleType', so no sticker will be " + "returned.". format(self.context.getId()) + ) + return [] + self.sample_type = self.context.getSampleType() + sticker_ids = self.sample_type.getAdmittedStickers() + default_sticker_id = self.get_default_sticker_id() + result = [] + # Getting only existing templates and its info + stickers = getStickerTemplates() + for sticker in stickers: + if sticker.get('id') in sticker_ids: + sticker_info = sticker.copy() + sticker_info['selected'] = \ + default_sticker_id == sticker.get('id') + result.append(sticker_info) + return result + + def get_default_sticker_id(self): + """ + Gets the default sticker for that content type depending on the + requested size. + + :return: An sticker ID as string + """ + size = self.request.get('size', '') + if size == 'small': + return self.sample_type.getDefaultSmallSticker() + return self.sample_type.getDefaultLargeSticker() diff --git a/bika/lims/browser/stickers.py b/bika/lims/browser/stickers.py index 5e89190bfb..52d25ce681 100644 --- a/bika/lims/browser/stickers.py +++ b/bika/lims/browser/stickers.py @@ -10,11 +10,14 @@ from bika.lims import bikaMessageFactory as _, t from bika.lims import logger from bika.lims.browser import BrowserView +from zope.component.interfaces import ComponentLookupError from bika.lims.utils import createPdf, to_int from bika.lims.vocabularies import getStickerTemplates from plone.resource.utils import iterDirectoriesOfType, queryResourceDirectory from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile import glob, os, os.path, sys, traceback +from bika.lims.interfaces import IGetStickerTemplates +from zope.component import getAdapters import os import App @@ -168,8 +171,22 @@ def getAvailableTemplates(self): 'title': , 'selected: True/False'} """ - seltemplate = self.getSelectedTemplate() + # Getting adapters for current context. those adapters will return + # the desired sticker templates for the current context: + try: + adapters = getAdapters((self.context, ), IGetStickerTemplates) + except ComponentLookupError: + logger.info('No IGetStickerTemplates adapters found.') + adapters = None templates = [] + if adapters is not None: + # Gather all templates + for name, adapter in adapters: + templates += adapter(self.request) + if templates: + return templates + # If there are no adapters, get all sticker templates in the system + seltemplate = self.getSelectedTemplate() for temp in getStickerTemplates(filter_by_type=self.filter_by_type): out = temp out['selected'] = temp.get('id', '') == seltemplate diff --git a/bika/lims/browser/widgets/__init__.py b/bika/lims/browser/widgets/__init__.py index e91e0b208f..4c7332f5d3 100644 --- a/bika/lims/browser/widgets/__init__.py +++ b/bika/lims/browser/widgets/__init__.py @@ -29,3 +29,4 @@ from .rejectionwidget import RejectionWidget from .priorityselectionwidget import PrioritySelectionWidget from .comboboxwidget import ComboBoxWidget +from .sampletypestickerswidget import SampleTypeStickersWidget diff --git a/bika/lims/browser/widgets/sampletypestickerswidget.py b/bika/lims/browser/widgets/sampletypestickerswidget.py new file mode 100644 index 0000000000..d118e72508 --- /dev/null +++ b/bika/lims/browser/widgets/sampletypestickerswidget.py @@ -0,0 +1,22 @@ +from AccessControl import ClassSecurityInfo + +from Products.Archetypes.Registry import registerWidget + +from bika.lims.browser.widgets import RecordsWidget + + +class SampleTypeStickersWidget(RecordsWidget): + security = ClassSecurityInfo() + _properties = RecordsWidget._properties.copy() + _properties.update({ + 'helper_js': ( + "bika_widgets/recordswidget.js", + "bika_widgets/sampletypestickerswidget.js",), + }) + + +registerWidget( + SampleTypeStickersWidget, + title="Sample type stickers widget", + description='Defines the available stickers for a sample type.', + ) diff --git a/bika/lims/content/sampletype.py b/bika/lims/content/sampletype.py index 8757f2e991..14f33a8452 100644 --- a/bika/lims/content/sampletype.py +++ b/bika/lims/content/sampletype.py @@ -6,26 +6,44 @@ # Some rights reserved. See LICENSE.rst, CONTRIBUTORS.rst. from AccessControl import ClassSecurityInfo + from Products.ATContentTypes.lib.historyaware import HistoryAwareMixin +from Products.ATExtensions.ateapi import RecordsField from Products.Archetypes.public import * from Products.Archetypes.references import HoldingReference -from Products.CMFCore.permissions import View, ModifyPortalContent from Products.CMFCore.utils import getToolByName from Products.CMFPlone.utils import safe_unicode -from bika.lims.browser import BrowserView +from magnitude import mg +from zope.interface import implements + from bika.lims import bikaMessageFactory as _ -from bika.lims.utils import t -from bika.lims.config import PROJECTNAME -from bika.lims.browser.widgets import DurationWidget +from bika.lims import logger from bika.lims.browser.fields import DurationField +from bika.lims.browser.widgets import DurationWidget +from bika.lims.browser.widgets import SampleTypeStickersWidget +from bika.lims.browser.widgets.referencewidget import ReferenceWidget as brw +from bika.lims.config import PROJECTNAME from bika.lims.content.bikaschema import BikaSchema from bika.lims.interfaces import ISampleType -from magnitude import mg, MagnitudeError -from zope.interface import implements -from bika.lims.browser.widgets.referencewidget import ReferenceWidget as brw -import json -import plone -import sys +from bika.lims.vocabularies import getStickerTemplates + +SMALL_DEFAULT_STICKER = 'small_default' +LARGE_DEFAULT_STICKER = 'large_default' + + +def sticker_templates(): + """ + It returns the registered stickers in the system. + :return: a DisplayList object + """ + voc = DisplayList() + stickers = getStickerTemplates() + for sticker in stickers: + voc.add(sticker.get('id'), sticker.get('title')) + if voc.index == 0: + logger.warning('Sampletype: getStickerTemplates is empty!') + return voc + schema = BikaSchema.copy() + Schema(( DurationField('RetentionPeriod', @@ -106,6 +124,47 @@ visibile=False, ) ), + RecordsField( + 'AdmittedStickerTemplates', + subfields=( + 'admitted', + SMALL_DEFAULT_STICKER, + LARGE_DEFAULT_STICKER, + ), + subfield_labels={ + 'admitted': _( + 'Admitted stickers for the sample type'), + SMALL_DEFAULT_STICKER: _( + 'Default small sticker'), + LARGE_DEFAULT_STICKER: _( + 'Default large sticker')}, + subfield_sizes={ + 'admitted': 6, + SMALL_DEFAULT_STICKER: 1, + LARGE_DEFAULT_STICKER: 1}, + subfield_types={ + 'admitted': 'selection', + SMALL_DEFAULT_STICKER: 'selection', + LARGE_DEFAULT_STICKER: 'selection' + }, + subfield_vocabularies={ + 'admitted': sticker_templates(), + SMALL_DEFAULT_STICKER: '_sticker_templates_vocabularies', + LARGE_DEFAULT_STICKER: '_sticker_templates_vocabularies', + }, + required_subfields={ + 'admitted': 1, + SMALL_DEFAULT_STICKER: 1, + LARGE_DEFAULT_STICKER: 1}, + default=[{}], + fixedSize=1, + widget=SampleTypeStickersWidget( + label=_("Admitted sticker templates"), + description=_( + "Defines the stickers to use for this sample type."), + allowDelete=False, + ), + ), )) schema['description'].schemata = 'default' @@ -198,6 +257,104 @@ def ContainerTypesVocabulary(self): from bika.lims.content.containertype import ContainerTypes return ContainerTypes(self, allow_blank=True) + def _get_sticker_subfield(self, subfield): + values = self.getField('AdmittedStickerTemplates').get(self) + if not values: + return '' + value = values[0].get(subfield) + return value + + def getDefaultSmallSticker(self): + """ + Returns the small sticker ID defined as default. + + :return: A string as an sticker ID + """ + return self._get_sticker_subfield(SMALL_DEFAULT_STICKER) + + def getDefaultLargeSticker(self): + """ + Returns the large sticker ID defined as default. + + :return: A string as an sticker ID + """ + return self._get_sticker_subfield(LARGE_DEFAULT_STICKER) + + def getAdmittedStickers(self): + """ + Returns the admitted sticker IDs defined. + + :return: An array of sticker IDs + """ + admitted = self._get_sticker_subfield('admitted') + if admitted: + return admitted + return [] + + def _sticker_templates_vocabularies(self): + """ + Returns the vocabulary to be used in + AdmittedStickerTemplates.small_default + + If the object has saved not AdmittedStickerTemplates.admitted stickers, + this method will return an empty DisplayList. Otherwise it returns + the stickers selected in admitted. + + :return: A DisplayList + """ + admitted = self.getAdmittedStickers() + if not admitted: + return DisplayList() + voc = DisplayList() + stickers = getStickerTemplates() + for sticker in stickers: + if sticker.get('id') in admitted: + voc.add(sticker.get('id'), sticker.get('title')) + return voc + + def setDefaultSmallSticker(self, value): + """ + Sets the small sticker ID defined as default. + + :param value: A sticker ID + """ + self._set_sticker_subfield(SMALL_DEFAULT_STICKER, value) + + def setDefaultLargeSticker(self, value): + """ + Sets the large sticker ID defined as default. + + :param value: A sticker ID + """ + self._set_sticker_subfield(LARGE_DEFAULT_STICKER, value) + + def setAdmittedStickers(self, value): + """ + Sets the admitted sticker IDs. + + :param value: An array of sticker IDs + """ + self._set_sticker_subfield('admitted', value) + + def _set_sticker_subfield(self, subfield, value): + if value is None: + logger.error( + "Setting wrong 'AdmittedStickerTemplates/admitted' value" + " to Sample Type '{}'" + .format(self.getId())) + return + if not isinstance(value, list): + logger.error( + "Setting wrong 'AdmittedStickerTemplates/admitted' value" + " type to Sample Type '{}'" + .format(self.getId())) + return + field = self.getField('AdmittedStickerTemplates') + stickers = field.get(self) + stickers[0][subfield] = value + field.set(self, stickers) + + registerType(SampleType, PROJECTNAME) def SampleTypes(self, instance=None, allow_blank=False): diff --git a/bika/lims/interfaces/__init__.py b/bika/lims/interfaces/__init__.py index 0c6ba00a40..1c890d6bf8 100644 --- a/bika/lims/interfaces/__init__.py +++ b/bika/lims/interfaces/__init__.py @@ -823,3 +823,15 @@ class ITopWideHTMLComponentsHook(Interface): """ Marker interface to hook html components in bikalisting """ + + +class IGetStickerTemplates(Interface): + """ + Marker interface to get stickers for a specific content type. + + An IGetStickerTemplates adapter should return a result with the + following format: + + :return: [{'id': , + 'title': }, ...] + """ diff --git a/bika/lims/skins/bika/bika_widgets/sampletypestickerswidget.js b/bika/lims/skins/bika/bika_widgets/sampletypestickerswidget.js new file mode 100644 index 0000000000..3b7c22fd0d --- /dev/null +++ b/bika/lims/skins/bika/bika_widgets/sampletypestickerswidget.js @@ -0,0 +1,47 @@ +jQuery(function($){ + $(document).ready(function(){ + // Controller for admitted stickers multi selection input. + $("#AdmittedStickerTemplates-admitted-0") + .bind('change', function(){ + on_admitted_change(); + } + ); + }); + /** + * When "admitted sticker selection" input changes, "default small" and + * "default large" selector options must be updated in acordance with the + * new admitted options. + * @return {None} nothing. + */ + function on_admitted_change(){ + // Get admitted options + var admitted_ops = $("#AdmittedStickerTemplates-admitted-0") + .find('option:selected'); + + // Clean small/large select options + $("#AdmittedStickerTemplates-small_default-0").find('option').remove(); + $("#AdmittedStickerTemplates-large_default-0").find('option').remove(); + + // Set small/large options from admitted ones + var i; + var small_opt_clone; + var large_opt_clone; + for(i=0; admitted_ops.length > i; i++){ + small_opt_clone = $(admitted_ops[i]).clone(); + $("#AdmittedStickerTemplates-small_default-0") + .append(small_opt_clone); + large_opt_clone = $(admitted_ops[i]).clone(); + $("#AdmittedStickerTemplates-large_default-0") + .append(large_opt_clone); + } + // Select the last cloned option. This way we give a value to the + // selected input. + if (small_opt_clone != undefined){ + $(small_opt_clone).attr('selected', 'selected'); + } + if (large_opt_clone != undefined){ + $(large_opt_clone).attr('selected', 'selected'); + } + } + +}); diff --git a/bika/lims/upgrade/v01_02_001.py b/bika/lims/upgrade/v01_02_001.py index d494c55178..72a98c8c07 100644 --- a/bika/lims/upgrade/v01_02_001.py +++ b/bika/lims/upgrade/v01_02_001.py @@ -106,4 +106,4 @@ def _change_inactive_state(service, new_state): } wtool.setStatusOf('bika_inactive_workflow', service, wf_state) workflow.updateRoleMappingsFor(service) - service.reindexObject(idxs=['allowedRolesAndUsers', 'inactive_state']) \ No newline at end of file + service.reindexObject(idxs=['allowedRolesAndUsers', 'inactive_state']) diff --git a/bika/lims/upgrade/v01_02_002.py b/bika/lims/upgrade/v01_02_002.py index 87692a3812..6410a886ae 100644 --- a/bika/lims/upgrade/v01_02_002.py +++ b/bika/lims/upgrade/v01_02_002.py @@ -6,6 +6,7 @@ # Some rights reserved. See LICENSE.rst, CONTRIBUTORS.rst. from Products.Archetypes.config import REFERENCE_CATALOG from Products.CMFCore.utils import getToolByName +from bika.lims import api from bika.lims import logger from bika.lims.catalog.worksheet_catalog import CATALOG_WORKSHEET_LISTING from bika.lims.browser.dashboard.dashboard import \ @@ -13,6 +14,7 @@ from bika.lims.config import PROJECTNAME as product from bika.lims.upgrade import upgradestep from bika.lims.upgrade.utils import UpgradeUtils +from bika.lims.vocabularies import getStickerTemplates version = '1.2.2' # Remember version number in metadata.xml and setup.py profile = 'profile-{0}:default'.format(product) @@ -47,6 +49,9 @@ def upgrade(tool): # section from Dashboard add_sample_section_in_dashboard(portal) + # Ability to choose the sticker templates based on sample types (#607) + set_sample_type_default_stickers(portal) + logger.info("{0} upgraded to version {1}".format(product, version)) return True @@ -69,3 +74,26 @@ def fix_worksheet_template_index(portal, ut): def add_sample_section_in_dashboard(portal): setup_dashboard_panels_visibility_registry('samples') + +def set_sample_type_default_stickers(portal): + """ + Fills the admitted stickers and their default stickers to every sample + type. + """ + # Getting all sticker templates + stickers = getStickerTemplates() + sticker_ids = [] + for sticker in stickers: + sticker_ids.append(sticker.get('id')) + def_small_template = portal.bika_setup.getSmallStickerTemplate() + def_large_template = portal.bika_setup.getLargeStickerTemplate() + # Getting all Sample Type objects + catalog = api.get_tool('bika_setup_catalog') + brains = catalog(portal_type='SampleType') + for brain in brains: + obj = api.get_object(brain) + if obj.getAdmittedStickers() is not None: + continue + obj.setAdmittedStickers(sticker_ids) + obj.setDefaultLargeSticker(def_large_template) + obj.setDefaultSmallSticker(def_small_template) diff --git a/bika/lims/vocabularies/__init__.py b/bika/lims/vocabularies/__init__.py index f42d5d87c4..36c8a81425 100644 --- a/bika/lims/vocabularies/__init__.py +++ b/bika/lims/vocabularies/__init__.py @@ -539,9 +539,9 @@ class StickerTemplatesVocabulary(object): """ implements(IVocabularyFactory) - def __call__(self, context): + def __call__(self, context, filter_by_type=False): out = [SimpleTerm(x['id'], x['id'], x['title']) for x in - getStickerTemplates()] + getStickerTemplates(filter_by_type=filter_by_type)] return SimpleVocabulary(out)