diff --git a/docs/changelog.rst b/docs/changelog.rst index 47b9a16..678c16c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog 2.0.0 (Unreleased) ------------------ +- #35 Rejection inbound samples - #32 Fix build test after adding default contact - #31 Add default contact for ExternalLaboratory and InboundSampleShipment content - First version diff --git a/src/senaite/referral/browser/inbound/configure.zcml b/src/senaite/referral/browser/inbound/configure.zcml index e9ba0cf..86082b2 100644 --- a/src/senaite/referral/browser/inbound/configure.zcml +++ b/src/senaite/referral/browser/inbound/configure.zcml @@ -3,6 +3,9 @@ xmlns:browser="http://namespaces.zope.org/browser" i18n_domain="senaite.referral"> + + + - - - diff --git a/src/senaite/referral/browser/inbound/samples/__init__.py b/src/senaite/referral/browser/inbound/samples/__init__.py new file mode 100644 index 0000000..b7329f3 --- /dev/null +++ b/src/senaite/referral/browser/inbound/samples/__init__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# +# This file is part of SENAITE.REFERRAL. +# +# SENAITE.REFERRAL is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation, version 2. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., 51 +# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Copyright 2021-2025 by it's authors. +# Some rights reserved, see README and LICENSE. diff --git a/src/senaite/referral/browser/inbound/samples/configure.zcml b/src/senaite/referral/browser/inbound/samples/configure.zcml new file mode 100644 index 0000000..954404d --- /dev/null +++ b/src/senaite/referral/browser/inbound/samples/configure.zcml @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/src/senaite/referral/browser/inbound/samples/rejection/__init__.py b/src/senaite/referral/browser/inbound/samples/rejection/__init__.py new file mode 100644 index 0000000..b7329f3 --- /dev/null +++ b/src/senaite/referral/browser/inbound/samples/rejection/__init__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# +# This file is part of SENAITE.REFERRAL. +# +# SENAITE.REFERRAL is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation, version 2. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., 51 +# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Copyright 2021-2025 by it's authors. +# Some rights reserved, see README and LICENSE. diff --git a/src/senaite/referral/browser/inbound/samples/rejection/configure.zcml b/src/senaite/referral/browser/inbound/samples/rejection/configure.zcml new file mode 100644 index 0000000..2ad6fba --- /dev/null +++ b/src/senaite/referral/browser/inbound/samples/rejection/configure.zcml @@ -0,0 +1,14 @@ + + + + + + diff --git a/src/senaite/referral/browser/inbound/samples/rejection/reject_inbound_samples.py b/src/senaite/referral/browser/inbound/samples/rejection/reject_inbound_samples.py new file mode 100644 index 0000000..8339e37 --- /dev/null +++ b/src/senaite/referral/browser/inbound/samples/rejection/reject_inbound_samples.py @@ -0,0 +1,151 @@ +# -*- coding: utf-8 -*- +# +# This file is part of SENAITE.REFERRAL. +# +# SENAITE.REFERRAL is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation, version 2. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., 51 +# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Copyright 2021-2025 by it's authors. +# Some rights reserved, see README and LICENSE. + +import six +from bika.lims import api +from bika.lims import senaiteMessageFactory as _ +from plone.memoize import view +from Products.Five.browser import BrowserView +from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile +from senaite.core import logger +from senaite.core.api.dtime import to_localized_time +from senaite.referral.catalog import INBOUND_SAMPLE_CATALOG + + +class RejectInboundSamplesView(BrowserView): + """View that renders the Inbound Samples rejection view + """ + template = ViewPageTemplateFile("templates/reject_inbound_samples.pt") + + def __init__(self, context, request): + super(RejectInboundSamplesView, self).__init__(context, request) + self.context = context + self.request = request + self.back_url = api.get_url(self.context.getInboundShipment()) + + def __call__(self): + form = self.request.form + + # Form submit toggle + form_submitted = form.get("submitted", False) + + # Buttons + form_continue = form.get("button_continue", False) + form_cancel = form.get("button_cancel", False) + + # Get the objects from request + inbound_samples = self.get_inbound_samples_from_request() + + # No Inbound Samples selected + if not inbound_samples: + return self.redirect(message=_("No items selected"), + level="warning") + + # Handle rejection + if form_submitted and form_continue: + logger.info("*** REJECT INBOUND SAMPLES ***") + processed = [] + for inbound_sample in form.get("inbound_samples", []): + inbound_sample_uid = inbound_sample.get("uid", "") + reasons = inbound_sample.get("reasons", []) + other = inbound_sample.get("other_reasons", "") + if not inbound_sample_uid: + continue + + # Omit if no rejection reason specified + if not any([reasons, other]): + continue + + obj = api.get_object_by_uid(inbound_sample_uid) + + obj.setSelectedRejectionReasons(reasons) + obj.setOtherRejectionReasons(other) + + # Reject the sample + processed.append(obj) + + if not processed: + return self.redirect(message=_("No samples were rejected")) + + message = _("Rejected {} samples: {}").format( + len(processed), ", ".join(map(api.get_id, processed))) + return self.redirect(message=message) + + # Handle cancel + if form_submitted and form_cancel: + logger.info("*** CANCEL REJECTION ***") + return self.redirect(message=_("Rejection cancelled")) + + return self.template() + + @view.memoize + def get_inbound_samples_from_request(self): + """Returns a list of objects coming from the "uids" request parameter + """ + uids = self.request.form.get("uids", "") + if isinstance(uids, six.string_types): + uids = uids.split(",") + + uids = list(set(uids)) + if not uids: + return [] + + inbound_samples = [] + query = dict(portal_type="InboundSample", UID=uids) + for brain in api.search(query, INBOUND_SAMPLE_CATALOG): + inbound_sample = api.get_object(brain) + inbound_samples.append(inbound_sample) + return inbound_samples + + @view.memoize + def get_rejection_reasons(self): + """Returns the list of available rejection reasons + """ + return api.get_setup().getRejectionReasonsItems() + + def get_inbound_samples_data(self): + """Returns a list of Inbound Samples data (dictionary) + """ + for obj in self.get_inbound_samples_from_request(): + yield { + "obj": obj, + "id": api.get_id(obj), + "uid": api.get_uid(obj), + "title": obj.Title(), + "sample_type": obj.getSampleType(), + "analyses": obj.getAnalyses(), + "date": to_localized_time( + obj.getDateSampled(), long_format=True + ), + } + + def redirect(self, redirect_url=None, message=None, level="info"): + """Redirect with a message + """ + if redirect_url is None: + redirect_url = self.back_url + if message is not None: + self.add_status_message(message, level) + return self.request.response.redirect(redirect_url) + + def add_status_message(self, message, level="info"): + """Set a portal status message + """ + return self.context.plone_utils.addPortalMessage(message, level) diff --git a/src/senaite/referral/browser/inbound/samples/rejection/templates/reject_inbound_samples.pt b/src/senaite/referral/browser/inbound/samples/rejection/templates/reject_inbound_samples.pt new file mode 100644 index 0000000..6f5298c --- /dev/null +++ b/src/senaite/referral/browser/inbound/samples/rejection/templates/reject_inbound_samples.pt @@ -0,0 +1,131 @@ + + + + + + + + + + + +

+ Reject Inbound Samples +

+
+ + + + + + + + + + +
+ +
+ + + + + + + + + + +
+ +
+ +
+ + +
+
+
+ + +
+
+
+
+ +
+ + + + +
+ +
+ + +
+
+ There are no pre-defined conditions set +
+
+ + +
+ + + +
+ +
+
+
+ + +
+ + + + +
+
+
+
+ + diff --git a/src/senaite/referral/browser/inbound/samples.py b/src/senaite/referral/browser/inbound/samples/view.py similarity index 98% rename from src/senaite/referral/browser/inbound/samples.py rename to src/senaite/referral/browser/inbound/samples/view.py index c3947d5..6fe0c06 100644 --- a/src/senaite/referral/browser/inbound/samples.py +++ b/src/senaite/referral/browser/inbound/samples/view.py @@ -15,7 +15,7 @@ # this program; if not, write to the Free Software Foundation, Inc., 51 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # -# Copyright 2021-2022 by it's authors. +# Copyright 2021-2025 by it's authors. # Some rights reserved, see README and LICENSE. import collections @@ -115,15 +115,16 @@ def default_review_states(self): }, "custom_transitions": [print_stickers], "columns": self.columns.keys(), - }, { + }, + { "id": "rejected", "title": _("Rejected"), "contentFilter": { "review_state": "rejected", }, - "custom_transitions": [], "columns": self.columns.keys(), - }, { + }, + { "id": "all", "title": _("All"), "contentFilter": {}, diff --git a/src/senaite/referral/browser/viewlets/configure.zcml b/src/senaite/referral/browser/viewlets/configure.zcml index 9b1edc9..932faaf 100644 --- a/src/senaite/referral/browser/viewlets/configure.zcml +++ b/src/senaite/referral/browser/viewlets/configure.zcml @@ -52,4 +52,15 @@ permission="zope2.View" layer="senaite.referral.interfaces.ISenaiteReferralLayer" /> + + + + diff --git a/src/senaite/referral/browser/viewlets/inboundsample.py b/src/senaite/referral/browser/viewlets/inboundsample.py new file mode 100644 index 0000000..608c3de --- /dev/null +++ b/src/senaite/referral/browser/viewlets/inboundsample.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# +# This file is part of SENAITE.REFERRAL. +# +# SENAITE.REFERRAL is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation, version 2. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., 51 +# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Copyright 2021-2025 by it's authors. +# Some rights reserved, see README and LICENSE. + +from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile +from bika.lims import api +from plone.app.layout.viewlets import ViewletBase +from plone.memoize import view +from senaite.referral import check_installed +from senaite.referral.catalog import INBOUND_SAMPLE_CATALOG + + +class RejectedInboundSamplesViewlet(ViewletBase): + """Current Inbound Samples were rejected. Display the reasons + """ + template = ViewPageTemplateFile("templates/rejected_inbound_samples_viewlet.pt") # noqa: E501 + + @check_installed(False) + def is_visible(self): + """Returns whether the viewlet must be visible or not + """ + rejected_inbound_samples = self.get_rejected_inbound_samples() + if not rejected_inbound_samples: + return False + + return True + + def get_rejected_inbound_samples(self): + """Returns the rejected inbound samples + """ + query = dict( + portal_type="InboundSample", + path=dict(query=api.get_path(self.context), level=0), + review_state="rejected") + brains = api.search(query, INBOUND_SAMPLE_CATALOG) + return [api.get_object(brain) for brain in brains] + + @view.memoize + def has_reasons(self, inbound_sample): + """Returns whether the sample has reasons or not + """ + return ( + inbound_sample.getSelectedRejectionReasons() or + inbound_sample.getOtherRejectionReasons() + ) diff --git a/src/senaite/referral/browser/viewlets/templates/rejected_inbound_samples_viewlet.pt b/src/senaite/referral/browser/viewlets/templates/rejected_inbound_samples_viewlet.pt new file mode 100644 index 0000000..30946f2 --- /dev/null +++ b/src/senaite/referral/browser/viewlets/templates/rejected_inbound_samples_viewlet.pt @@ -0,0 +1,29 @@ +
+ +
+ + +
+
+ + Rejected Inbound Samples + +

+ Sample has been rejected due to the following reasons: + Sample has been rejected +

+
    +
  • +
+
    +
  • Other reasons:
  • +
+
+
+
+
diff --git a/src/senaite/referral/browser/workflow/configure.zcml b/src/senaite/referral/browser/workflow/configure.zcml index 702b7cc..a281ce0 100644 --- a/src/senaite/referral/browser/workflow/configure.zcml +++ b/src/senaite/referral/browser/workflow/configure.zcml @@ -38,4 +38,15 @@ provides="bika.lims.interfaces.IWorkflowActionAdapter" permission="zope.Public" /> + + + diff --git a/src/senaite/referral/browser/workflow/inboundsample.py b/src/senaite/referral/browser/workflow/inboundsample.py index 1b40beb..837038a 100644 --- a/src/senaite/referral/browser/workflow/inboundsample.py +++ b/src/senaite/referral/browser/workflow/inboundsample.py @@ -18,6 +18,8 @@ # Copyright 2021-2022 by it's authors. # Some rights reserved, see README and LICENSE. +from senaite.app.listing.adapters.workflow import ListingWorkflowTransition +from senaite.app.listing.interfaces import IListingWorkflowTransition from senaite.referral import messageFactory as _ from senaite.referral import PRODUCT_NAME from senaite.referral.workflow import do_queue_or_action_for @@ -25,6 +27,7 @@ from bika.lims import api from bika.lims import senaiteMessageFactory as _s from bika.lims.browser.workflow import WorkflowActionGenericAdapter +from zope.interface import implementer class WorkflowActionReceiveAdapter(WorkflowActionGenericAdapter): @@ -68,3 +71,32 @@ def is_barcodes_preview_on_reception_enabled(self): """ key = "{}.barcodes_preview_reception".format(PRODUCT_NAME) return api.get_registry_record(key, default=False) + + +@implementer(IListingWorkflowTransition) +class InboundSampleRejectWorkflowTransition(ListingWorkflowTransition): + """Adapter in charge of InboundSample's 'reject' action + """ + def __init__(self, view, context, request): + super(InboundSampleRejectWorkflowTransition, self).__init__( + view, context, request) + self.back_url = self.context.absolute_url() + self.chained_uids = [] + + def do_transition( + self, transition, chained_uids, failed_transitions, **kw + ): + """Execute the workflow transition + """ + super(InboundSampleRejectWorkflowTransition, self).do_transition( + transition, chained_uids, failed_transitions, **kw) + self.chained_uids = chained_uids + + def get_redirect_url(self): + """Redirect after reject_inbound_sample transition + """ + uids = ",".join(self.chained_uids) + url = "{}/reject_inbound_samples?uids={}".format( + self.back_url, uids + ) + return url diff --git a/src/senaite/referral/configure.zcml b/src/senaite/referral/configure.zcml index 54066a0..270d6ae 100644 --- a/src/senaite/referral/configure.zcml +++ b/src/senaite/referral/configure.zcml @@ -31,6 +31,10 @@ component="senaite.referral.vocabularies.ReferenceLaboratoriesVocabularyFactory" name="senaite.referral.vocabularies.referencelaboratories" /> + +