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)