diff --git a/CHANGES.rst b/CHANGES.rst
index 8a3a0834be..92e1f4973f 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -5,6 +5,7 @@ Changelog
2.3.0 (unreleased)
------------------
+- #2114 Allow Multi Analysis Results Entry
- #2111 Replace header table with customizable sample header viewlet
- #2110 Add a more descriptive message for "Reject" action inside a Worksheet
- #2104 Fix result formatting when result is below LDL or above UDL
diff --git a/src/bika/lims/browser/analysisrequest/add2.py b/src/bika/lims/browser/analysisrequest/add2.py
index 059120c5b5..84c0c0d1b5 100644
--- a/src/bika/lims/browser/analysisrequest/add2.py
+++ b/src/bika/lims/browser/analysisrequest/add2.py
@@ -1680,11 +1680,21 @@ def ajax_submit(self):
# Automatic label printing
setup = api.get_setup()
auto_print = setup.getAutoPrintStickers()
- if 'register' in auto_print and ARs:
- return {
- 'success': message,
- 'stickers': ARs.values(),
- 'stickertemplate': setup.getAutoStickerTemplate()
- }
- else:
- return {'success': message}
+ immediate_results_entry = setup.getImmediateResultsEntry()
+ redirect_to = self.context.absolute_url()
+ sample_uids = ARs.values()
+ if "register" in auto_print and sample_uids:
+ redirect_to = "{}/sticker?autoprint=1&template={}&items={}".format(
+ self.context.absolute_url(),
+ setup.getAutoStickerTemplate(),
+ ",".join(sample_uids)
+ )
+ elif immediate_results_entry and sample_uids:
+ redirect_to = "{}/multi_results?uids={}".format(
+ self.context.absolute_url(),
+ ",".join(sample_uids)
+ )
+ return {
+ "success": message,
+ "redirect_to": redirect_to,
+ }
diff --git a/src/bika/lims/browser/stickers.py b/src/bika/lims/browser/stickers.py
index a49f2dc1f1..7bff8661d0 100644
--- a/src/bika/lims/browser/stickers.py
+++ b/src/bika/lims/browser/stickers.py
@@ -109,6 +109,22 @@ def __call__(self):
return self.template()
+ def get_back_url(self):
+ """Calculate the Back URL
+ """
+ url = api.get_url(self.context)
+ portal_type = api.get_portal_type(self.context)
+ redirect_contexts = ['Client', 'AnalysisRequest', 'Samples', 'Batch']
+ if portal_type not in redirect_contexts:
+ parent = api.get_parent(self.context)
+ url = api.get_url(parent)
+ # redirect to direct results entry
+ setup = api.get_setup()
+ if setup.getImmediateResultsEntry():
+ url = "{}/multi_results?uids={}".format(
+ url, ",".join(self.get_uids()))
+ return url
+
def get_items(self):
"""Returns a list of SuperModel items
"""
diff --git a/src/bika/lims/browser/templates/stickers_preview.pt b/src/bika/lims/browser/templates/stickers_preview.pt
index b95ac8ea3a..b4b558ff63 100644
--- a/src/bika/lims/browser/templates/stickers_preview.pt
+++ b/src/bika/lims/browser/templates/stickers_preview.pt
@@ -6,13 +6,7 @@
tal:attributes="lang default_language|default;
xml:lang default_language|default;"
i18n:domain="senaite.core"
- tal:define="portal_state context/@@plone_portal_state;
- portal_url portal_state/portal_url;
- plone_view context/@@plone;
- portal portal_state/portal;
- portal_type python:context.portal_type;
- anchor_self python:('Client','AnalysisRequest', 'Samples', 'Batch');
- goback_url python:context.absolute_url() if portal_type in anchor_self else context.aq_parent.absolute_url();">
+ tal:define="goback_url python:view.get_back_url();">
diff --git a/src/bika/lims/content/bikasetup.py b/src/bika/lims/content/bikasetup.py
index 65803138f3..a0ab106eee 100644
--- a/src/bika/lims/content/bikasetup.py
+++ b/src/bika/lims/content/bikasetup.py
@@ -338,6 +338,22 @@ def getCounterTypes(self, instance=None):
"configured in individual Analysis Services."),
)
),
+ BooleanField(
+ "ImmediateResultsEntry",
+ schemata="Analyses",
+ default=False,
+ widget=BooleanWidget(
+ label=_("label_bikasetup_immediateresultsentry",
+ default=u"Immediate results entry"),
+ description=_(
+ "description_bikasetup_immediateresultsentry",
+ default=u"Allow the user to directly enter results after "
+ "sample creation, e.g. to enter field results immediately, or "
+ "lab results, when the automatic sample reception is "
+ "activated."
+ ),
+ ),
+ ),
BooleanField(
'EnableAnalysisRemarks',
schemata="Analyses",
@@ -1032,5 +1048,22 @@ def setEnableGlobalAuditlog(self, value):
if setup:
setup.setEnableGlobalAuditlog(value)
+ def getImmediateResultsEntry(self):
+ """Get the value from the senaite setup
+ """
+ setup = api.get_senaite_setup()
+ # setup is `None` during initial site content structure installation
+ if setup:
+ return setup.getImmediateResultsEntry()
+ return False
+
+ def setImmediateResultsEntry(self, value):
+ """Set the value in the senaite setup
+ """
+ setup = api.get_senaite_setup()
+ # setup is `None` during initial site content structure installation
+ if setup:
+ setup.setImmediateResultsEntry(value)
+
registerType(BikaSetup, PROJECTNAME)
diff --git a/src/bika/lims/permissions.py b/src/bika/lims/permissions.py
index 9ac523396d..9bbbd882fe 100644
--- a/src/bika/lims/permissions.py
+++ b/src/bika/lims/permissions.py
@@ -114,6 +114,7 @@
TransitionDispatchSample = "senaite.core: Transition: Dispatch Sample"
TransitionRestoreSample = "senaite.core: Transition: Restore Sample"
TransitionCreatePartitions = "senaite.core: Transition: Create Partitions"
+TransitionMultiResults = "senaite.core: Transition: Multi Results"
# Type-specific permissions
diff --git a/src/bika/lims/permissions.zcml b/src/bika/lims/permissions.zcml
index c77365d7bb..9c360c3f7a 100644
--- a/src/bika/lims/permissions.zcml
+++ b/src/bika/lims/permissions.zcml
@@ -86,6 +86,7 @@
+
# Object-specific permissions
# ---------------------------
diff --git a/src/bika/lims/workflow/analysisrequest/guards.py b/src/bika/lims/workflow/analysisrequest/guards.py
index 91922cd17e..9f10575da1 100644
--- a/src/bika/lims/workflow/analysisrequest/guards.py
+++ b/src/bika/lims/workflow/analysisrequest/guards.py
@@ -241,3 +241,9 @@ def guard_restore(sample):
"""Checks if the restore transition is allowed
"""
return True
+
+
+def guard_multi_results(sample):
+ """Checks if the multi results action is allowed
+ """
+ return True
diff --git a/src/senaite/core/adapters/configure.zcml b/src/senaite/core/adapters/configure.zcml
index c73e2ffe15..c6d259cd6a 100644
--- a/src/senaite/core/adapters/configure.zcml
+++ b/src/senaite/core/adapters/configure.zcml
@@ -23,6 +23,18 @@
provides="bika.lims.interfaces.IWorkflowActionAdapter"
permission="zope.Public" />
+
+
+
diff --git a/src/senaite/core/adapters/sample.py b/src/senaite/core/adapters/sample.py
index aa8b3bc633..49178e5467 100644
--- a/src/senaite/core/adapters/sample.py
+++ b/src/senaite/core/adapters/sample.py
@@ -17,3 +17,16 @@ def __call__(self, action, uids):
url = "{}/dispatch_samples?uids={}".format(
api.get_url(self.context), ",".join(uids))
return self.redirect(redirect_url=url)
+
+
+@implementer(IWorkflowActionUIDsAdapter)
+class WorkflowActionMultiResultsAdapter(RequestContextAware):
+ """Redirects to multi results view
+ """
+
+ def __call__(self, action, uids):
+ """Redirects the user to the multi results form
+ """
+ url = "{}/multi_results?uids={}".format(
+ api.get_url(self.context), ",".join(uids))
+ return self.redirect(redirect_url=url)
diff --git a/src/senaite/core/browser/contentmenu/view.py b/src/senaite/core/browser/contentmenu/view.py
index c01f5f17b9..910df3add1 100644
--- a/src/senaite/core/browser/contentmenu/view.py
+++ b/src/senaite/core/browser/contentmenu/view.py
@@ -24,6 +24,10 @@
from zope.browsermenu.interfaces import IBrowserMenu
from zope.component import getUtility
+HIDE_WF_ITEMS = [
+ "workflow-transition-advanced"
+]
+
class ContentMenuProvider(Base):
"""Content menu provider for the "view" tab: displays the menu
@@ -41,12 +45,20 @@ def fiddle_menu_item(self, item):
Unfortunately, this can not be done more elegant w/o overrides.zcml.
https://stackoverflow.com/questions/11904155/disable-advanced-in-workflow-status-menu-in-plone
"""
+ def is_wf_item_visible(item):
+ """Checks if the WF items is visible or not
+ """
+ extra = item.get("extra", {})
+ wfid = extra.get("id")
+ if wfid in HIDE_WF_ITEMS:
+ return False
+ return True
+
action = item.get("action")
if action.endswith("content_status_history"):
# remove the "Advanced ..." submenu
- submenu = filter(
- lambda m: m.get("title") != "label_advanced",
- item.get("submenu", []) or [])
+ submenu = item.get("submenu", []) or []
+ submenu = filter(is_wf_item_visible, submenu)
item["submenu"] = submenu
return item
diff --git a/src/senaite/core/browser/samples/configure.zcml b/src/senaite/core/browser/samples/configure.zcml
index 74b8d9fbae..dc58d16523 100644
--- a/src/senaite/core/browser/samples/configure.zcml
+++ b/src/senaite/core/browser/samples/configure.zcml
@@ -19,6 +19,13 @@
permission="senaite.core.permissions.TransitionDispatchSample"
layer="senaite.core.interfaces.ISenaiteCore" />
+
+
+
+
+
+
+
+
+
+
+
+ Manage Analyses Results
+
+
+
+
+
+
+ Enter or view the analyses results of multiple samples.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
No visible analyses for this sample
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/senaite/core/browser/static/js/bika.lims.analysisrequest.add.js b/src/senaite/core/browser/static/js/bika.lims.analysisrequest.add.js
index 15b92a5aeb..b2ef7e6a1b 100644
--- a/src/senaite/core/browser/static/js/bika.lims.analysisrequest.add.js
+++ b/src/senaite/core/browser/static/js/bika.lims.analysisrequest.add.js
@@ -1373,7 +1373,7 @@
* This includes GET params for printing labels, so that we do not
* have to care about this here.
*/
- var ars, destination, dialog, errorbox, field, fieldname, message, msg, parent, q, stickertemplate;
+ var dialog, errorbox, field, fieldname, message, msg, parent;
if (data['errors']) {
msg = data.errors.message;
if (msg !== "") {
@@ -1392,12 +1392,6 @@
}
window.bika.lims.portalMessage(msg);
return window.scroll(0, 0);
- } else if (data['stickers']) {
- destination = base_url;
- ars = data['stickers'];
- stickertemplate = data['stickertemplate'];
- q = '/sticker?autoprint=1&template=' + stickertemplate + '&items=' + ars.join(',');
- return window.location.replace(destination + q);
} else if (data['confirmation']) {
dialog = me.template_dialog("confirm-template", data.confirmation);
dialog.on("yes", function() {
@@ -1405,11 +1399,14 @@
return $("input[name=save_button]").trigger("click");
});
return dialog.on("no", function() {
+ var destination;
destination = data.confirmation["destination"];
if (destination) {
return window.location.replace(portal_url + '/' + destination);
}
});
+ } else if (data['redirect_to']) {
+ return window.location.replace(data['redirect_to']);
} else {
return window.location.replace(base_url);
}
diff --git a/src/senaite/core/browser/static/js/coffee/bika.lims.analysisrequest.add.coffee b/src/senaite/core/browser/static/js/coffee/bika.lims.analysisrequest.add.coffee
index 22970e85e2..61fa3f36c4 100644
--- a/src/senaite/core/browser/static/js/coffee/bika.lims.analysisrequest.add.coffee
+++ b/src/senaite/core/browser/static/js/coffee/bika.lims.analysisrequest.add.coffee
@@ -1394,13 +1394,6 @@ class window.AnalysisRequestAdd
window.bika.lims.portalMessage msg
window.scroll 0, 0
- else if data['stickers']
- destination = base_url
- ars = data['stickers']
- stickertemplate = data['stickertemplate']
- q = '/sticker?autoprint=1&template=' + stickertemplate + '&items=' + ars.join(',')
- window.location.replace destination + q
-
else if data['confirmation']
dialog = me.template_dialog "confirm-template", data.confirmation
dialog.on "yes", ->
@@ -1413,7 +1406,8 @@ class window.AnalysisRequestAdd
destination = data.confirmation["destination"]
if destination
window.location.replace portal_url + '/' + destination
-
+ else if data['redirect_to']
+ window.location.replace data['redirect_to']
else
window.location.replace base_url
diff --git a/src/senaite/core/content/senaitesetup.py b/src/senaite/core/content/senaitesetup.py
index a8ec583e5c..7d96f4a9af 100644
--- a/src/senaite/core/content/senaitesetup.py
+++ b/src/senaite/core/content/senaitesetup.py
@@ -78,9 +78,26 @@ class ISetupSchema(model.Schema):
required=False,
)
+ immediate_results_entry = schema.Bool(
+ title=_(u"Immediate results entry"),
+ description=_(
+ "description_senaitesetup_immediateresultsentry",
+ default=u"Allow the user to directly enter results after sample "
+ "creation, e.g. to enter field results immediately, or lab "
+ "results, when the automatic sample reception is activated."
+ ),
+ )
+
###
# Fieldsets
###
+ model.fieldset(
+ "analyses",
+ label=_("label_senaitesetup_fieldset_analyses", default=u"Analyses"),
+ fields=[
+ "immediate_results_entry",
+ ]
+ )
model.fieldset(
"notifications",
@@ -172,3 +189,17 @@ def setSiteLogoCSS(self, value):
"""
mutator = self.mutator("site_logo_css")
return mutator(self, value)
+
+ @security.protected(permissions.View)
+ def getImmediateResultsEntry(self):
+ """Returns if immediate results entry is enabled or not
+ """
+ accessor = self.accessor("immediate_results_entry")
+ return accessor(self)
+
+ @security.protected(permissions.ModifyPortalContent)
+ def setImmediateResultsEntry(self, value):
+ """Enable/Disable global Auditlogging
+ """
+ mutator = self.mutator("immediate_results_entry")
+ return mutator(self, value)
diff --git a/src/senaite/core/profiles/default/rolemap.xml b/src/senaite/core/profiles/default/rolemap.xml
index a1fb50efae..ea001bfd51 100644
--- a/src/senaite/core/profiles/default/rolemap.xml
+++ b/src/senaite/core/profiles/default/rolemap.xml
@@ -3,6 +3,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/senaite/core/profiles/default/workflows/senaite_sample_workflow/definition.xml b/src/senaite/core/profiles/default/workflows/senaite_sample_workflow/definition.xml
index 0522e7a80d..fe23ccf321 100644
--- a/src/senaite/core/profiles/default/workflows/senaite_sample_workflow/definition.xml
+++ b/src/senaite/core/profiles/default/workflows/senaite_sample_workflow/definition.xml
@@ -33,6 +33,7 @@
senaite.core: Transition: Dispatch Sample
senaite.core: Transition: Restore Sample
senaite.core: Transition: Create Partitions
+ senaite.core: Transition: Multi Results
senaite.core: Add Analysis
senaite.core: Edit Field Results
@@ -99,6 +100,7 @@
+
@@ -413,6 +415,7 @@
+
@@ -509,6 +512,7 @@
+
@@ -600,6 +604,7 @@
+
@@ -699,6 +704,7 @@
+
@@ -788,6 +794,7 @@
+
@@ -890,6 +897,7 @@
+
@@ -1580,6 +1588,21 @@
+
+
+
+ Multi Results
+
+ python:here.guard_handler("multi_results")
+ senaite.core: Transition: Multi Results
+
+
+