diff --git a/CHANGES.rst b/CHANGES.rst
index 30291de472..3724d74c57 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -4,6 +4,7 @@ Changelog
2.0.0rc2 (unreleased)
---------------------
+- #1657 Allow to edit Profiles in Samples for pre verified/published states
- #1655 Rename service's "Result Options" and "Additional Values"
- #1655 Move service's "Additional values" to "Result Options" tab
- #1654 Fix Text of interim choices is not displayed correctly on readonly mode
diff --git a/src/bika/lims/content/analysisrequest.py b/src/bika/lims/content/analysisrequest.py
index 4d4d32d4cf..e69741c4b8 100644
--- a/src/bika/lims/content/analysisrequest.py
+++ b/src/bika/lims/content/analysisrequest.py
@@ -1465,6 +1465,34 @@ def setResultsRange(self, value, recursive=True):
if check_permission(permission, descendant):
descendant.setResultsRange(value)
+ def setProfiles(self, value):
+ """Set Analysis Profiles to the Sample
+ """
+ if not isinstance(value, (list, tuple)):
+ value = [value]
+ # filter out empties
+ value = filter(None, value)
+ # ensure we have UIDs
+ uids = map(api.get_uid, value)
+ # get the current set profiles
+ current_profiles = self.getRawProfiles()
+ # return immediately if nothing changed
+ if current_profiles == uids:
+ return
+ # get the profiles
+ profiles = map(api.get_object_by_uid, uids)
+ # get the current set of analyses/services
+ analyses = self.getAnalyses(full_objects=True)
+ services = map(lambda an: an.getAnalysisService(), analyses)
+ # determine all the services to add
+ services_to_add = set(services)
+ for profile in profiles:
+ services_to_add.update(profile.getService())
+ # set all analyses
+ self.setAnalyses(list(services_to_add))
+ # set the profiles value
+ self.getField("Profiles").set(self, value)
+
def getClient(self):
"""Returns the client this object is bound to. We override getClient
from ClientAwareMixin because the "Client" schema field is only used to
diff --git a/src/bika/lims/profiles/default/workflows/bika_ar_workflow/definition.xml b/src/bika/lims/profiles/default/workflows/bika_ar_workflow/definition.xml
index 4ba2f12be4..6c153bacfe 100644
--- a/src/bika/lims/profiles/default/workflows/bika_ar_workflow/definition.xml
+++ b/src/bika/lims/profiles/default/workflows/bika_ar_workflow/definition.xml
@@ -242,8 +242,7 @@
-
-
+
@@ -340,8 +339,7 @@
-
-
+
@@ -428,8 +426,7 @@
-
-
+
@@ -515,8 +512,7 @@
-
-
+
@@ -598,8 +594,7 @@
-
-
+
@@ -691,8 +686,7 @@
-
-
+
@@ -776,8 +770,7 @@
-
-
+
diff --git a/src/senaite/core/tests/doctests/AnalysisProfile.rst b/src/senaite/core/tests/doctests/AnalysisProfile.rst
new file mode 100644
index 0000000000..5483282e86
--- /dev/null
+++ b/src/senaite/core/tests/doctests/AnalysisProfile.rst
@@ -0,0 +1,183 @@
+Analysis Profile
+================
+
+Running this test from the buildout directory::
+
+ bin/test test_textual_doctests -t AnalysisProfile
+
+Needed Imports:
+
+ >>> import re
+ >>> from AccessControl.PermissionRole import rolesForPermissionOn
+ >>> from bika.lims import api
+ >>> from bika.lims.content.analysisrequest import AnalysisRequest
+ >>> from bika.lims.utils.analysisrequest import create_analysisrequest
+ >>> from bika.lims.utils import tmpID
+ >>> from bika.lims.interfaces import ISubmitted
+ >>> from bika.lims.workflow import doActionFor as do_action_for
+ >>> from bika.lims.workflow import getCurrentState
+ >>> from bika.lims.workflow import getAllowedTransitions
+ >>> from DateTime import DateTime
+ >>> from plone.app.testing import TEST_USER_ID
+ >>> from plone.app.testing import TEST_USER_PASSWORD
+ >>> from plone.app.testing import setRoles
+
+Functional Helpers:
+
+ >>> def start_server():
+ ... from Testing.ZopeTestCase.utils import startZServer
+ ... ip, port = startZServer()
+ ... return "http://{}:{}/{}".format(ip, port, portal.id)
+
+
+ >>> def get_services(sample):
+ ... analyses = sample.getAnalyses(full_objects=True)
+ ... services = map(lambda an: an.getAnalysisService(), analyses)
+ ... return services
+
+ >>> def receive_sample(sample):
+ ... do_action_for(sample, "receive")
+
+ >>> def submit_analyses(sample):
+ ... for analysis in sample.getAnalyses(full_objects=True):
+ ... analysis.setResult(13)
+ ... do_action_for(analysis, "submit")
+
+ >>> def verify_analyses(sample):
+ ... for analysis in sample.getAnalyses(full_objects=True):
+ ... if ISubmitted.providedBy(analysis):
+ ... do_action_for(analysis, "verify")
+
+ >>> def retract_analyses(sample):
+ ... for analysis in sample.getAnalyses(full_objects=True):
+ ... if ISubmitted.providedBy(analysis):
+ ... do_action_for(analysis, "retract")
+
+Variables:
+
+ >>> portal = self.portal
+ >>> request = self.request
+ >>> setup = portal.bika_setup
+ >>> date_now = DateTime().strftime("%Y-%m-%d")
+ >>> date_future = (DateTime() + 5).strftime("%Y-%m-%d")
+
+We need to create some basic objects for the test:
+
+ >>> setRoles(portal, TEST_USER_ID, ['LabManager',])
+ >>> client = api.create(portal.clients, "Client", Name="Happy Hills", ClientID="HH", MemberDiscountApplies=True)
+ >>> contact = api.create(client, "Contact", Firstname="Rita", Lastname="Mohale")
+ >>> sampletype = api.create(setup.bika_sampletypes, "SampleType", title="Water", Prefix="W")
+ >>> labcontact = api.create(setup.bika_labcontacts, "LabContact", Firstname="Lab", Lastname="Manager")
+ >>> department = api.create(setup.bika_departments, "Department", title="Chemistry", Manager=labcontact)
+ >>> category = api.create(setup.bika_analysiscategories, "AnalysisCategory", title="Metals", Department=department)
+ >>> supplier = api.create(setup.bika_suppliers, "Supplier", Name="Naralabs")
+ >>> Cu = api.create(setup.bika_analysisservices, "AnalysisService", title="Copper", Keyword="Cu", Price="15", Category=category.UID(), Accredited=True)
+ >>> Fe = api.create(setup.bika_analysisservices, "AnalysisService", title="Iron", Keyword="Fe", Price="10", Category=category.UID())
+ >>> Au = api.create(setup.bika_analysisservices, "AnalysisService", title="Gold", Keyword="Au", Price="20", Category=category.UID())
+ >>> Zn = api.create(setup.bika_analysisservices, "AnalysisService", title="Zink", Keyword="Zn", Price="20", Category=category.UID())
+ >>> service_uids1 = [Cu.UID(), Fe.UID(), Au.UID()]
+ >>> service_uids2 = [Zn.UID()]
+ >>> service_uids3 = [Cu.UID(), Fe.UID(), Au.UID(), Zn.UID()]
+ >>> profile1 = api.create(setup.bika_analysisprofiles, "AnalysisProfile", title="Profile", Service=service_uids1)
+ >>> profile2 = api.create(setup.bika_analysisprofiles, "AnalysisProfile", title="Profile", Service=service_uids2)
+ >>> profile3 = api.create(setup.bika_analysisprofiles, "AnalysisProfile", title="Profile", Service=service_uids3)
+
+
+Assign Profile(s)
+-----------------
+
+Assigning Analysis Profiles adds the Analyses of the profile to the sample.
+
+ >>> setup.setSelfVerificationEnabled(True)
+
+ >>> values = {
+ ... 'Client': client.UID(),
+ ... 'Contact': contact.UID(),
+ ... 'DateSampled': date_now,
+ ... 'SampleType': sampletype.UID()}
+
+Create some Analysis Requests:
+
+ >>> ar1 = create_analysisrequest(client, request, values, [Au.UID()])
+ >>> ar2 = create_analysisrequest(client, request, values, [Fe.UID()])
+ >>> ar3 = create_analysisrequest(client, request, values, [Cu.UID()])
+
+Apply the profile object. Note the custom `setProfiles` (plural) setter:
+
+ >>> ar1.setProfiles(profile1)
+
+All analyses from the profile should be added to the sample:
+
+ >>> services = get_services(ar1)
+ >>> set(map(api.get_uid, services)).issuperset(service_uids1)
+ True
+
+The profile is applied to the sample:
+
+ >>> profile1 in ar1.getProfiles()
+ True
+
+Apply the profile UID:
+
+ >>> ar2.setProfiles(profile2.UID())
+
+All analyses from the profile should be added to the sample:
+
+ >>> services = get_services(ar2)
+ >>> set(map(api.get_uid, services)).issuperset(service_uids2)
+ True
+
+The profile is applied to the sample:
+
+ >>> profile2 in ar2.getProfiles()
+ True
+
+
+Apply multiple profiles:
+
+ >>> ar3.setProfiles([profile1, profile2, profile3.UID()])
+
+All analyses from the profiles should be added to the sample:
+
+ >>> services = get_services(ar3)
+ >>> set(map(api.get_uid, services)).issuperset(service_uids1 + service_uids2 + service_uids3)
+ True
+
+
+Remove Profile(s)
+-----------------
+
+Removing an analyis Sample retains the assigned analyses:
+
+ >>> analyses = ar1.getAnalyses(full_objects=True)
+ >>> ar1.setProfiles([])
+ >>> ar1.getProfiles()
+ []
+
+ >>> set(ar1.getAnalyses(full_objects=True)) == set(analyses)
+ True
+
+
+Assigning Profiles in "to_be_verified" status
+---------------------------------------------
+
+ >>> ar4 = create_analysisrequest(client, request, values, [Au.UID()])
+
+ >>> receive_sample(ar4)
+ >>> submit_analyses(ar4)
+
+ >>> api.get_workflow_status_of(ar4)
+ 'to_be_verified'
+
+ >>> ar4.getProfiles()
+ []
+
+Setting the profile works up to this state:
+
+ >>> ar4.setProfiles(profile1.UID())
+ >>> api.get_workflow_status_of(ar4)
+ 'sample_received'
+
+ >>> services = get_services(ar3)
+ >>> set(map(api.get_uid, services)).issuperset(service_uids1 + [Au.UID()])
+ True
diff --git a/src/senaite/core/upgrade/v02_00_000.py b/src/senaite/core/upgrade/v02_00_000.py
index 4e6b1f7c4f..db20cb0c7c 100644
--- a/src/senaite/core/upgrade/v02_00_000.py
+++ b/src/senaite/core/upgrade/v02_00_000.py
@@ -19,10 +19,12 @@
# Some rights reserved, see README and LICENSE.
from bika.lims import api
+from bika.lims.catalog import CATALOG_ANALYSIS_REQUEST_LISTING
from bika.lims.catalog import SETUP_CATALOG
from bika.lims.setuphandlers import add_dexterity_setup_items
from senaite.core import logger
from senaite.core.config import PROJECTNAME as product
+from senaite.core.setuphandlers import _run_import_step
from senaite.core.upgrade import upgradestep
from senaite.core.upgrade.utils import UpgradeUtils
@@ -68,15 +70,22 @@ def upgrade(tool):
# Install the new SENAITE CORE package
install_senaite_core(portal)
- # Add Interpretation Template(s) content type
+ # run import steps located in senaite.core profiles
setup.runImportStepFromProfile(profile, "typeinfo")
setup.runImportStepFromProfile(profile, "workflow")
+ # run import steps located in bika.lims profiles
+ _run_import_step(portal, "typeinfo", profile="profile-bika.lims:default")
+ _run_import_step(portal, "workflow", profile="profile-bika.lims:default")
+
add_dexterity_setup_items(portal)
# Published results tab is not displayed to client contacts
# https://github.com/senaite/senaite.core/pull/1638
fix_published_results_permission(portal)
+ # Update workflow mappings for samples to allow profile editing
+ update_workflow_mappings_samples(portal)
+
logger.info("{0} upgraded to version {1}".format(product, version))
return True
@@ -140,3 +149,36 @@ def fix_published_results_permission(portal):
if action.id == "published_results":
action.permissions = ("View", )
break
+
+
+def update_workflow_mappings_samples(portal):
+ """Allow to edit analysis profiles
+ """
+ logger.info("Updating role mappings for Samples ...")
+ wf_id = "bika_ar_workflow"
+ query = {"portal_type": "AnalysisRequest",
+ "review_state": [
+ "sample_due",
+ "sample_registered",
+ "scheduled_sampling",
+ "to_be_sampled",
+ "sample_received",
+ "attachment_due",
+ "to_be_verified",
+ "to_be_preserved",
+ ]}
+ brains = api.search(query, CATALOG_ANALYSIS_REQUEST_LISTING)
+ update_workflow_mappings_for(portal, wf_id, brains)
+ logger.info("Updating role mappings for Samples [DONE]")
+
+
+def update_workflow_mappings_for(portal, wf_id, brains):
+ wf_tool = api.get_tool("portal_workflow")
+ workflow = wf_tool.getWorkflowById(wf_id)
+ total = len(brains)
+ for num, brain in enumerate(brains):
+ if num and num % 100 == 0:
+ logger.info("Updating role mappings: {0}/{1}".format(num, total))
+ obj = api.get_object(brain)
+ workflow.updateRoleMappingsFor(obj)
+ obj.reindexObject(idxs=["allowedRolesAndUsers"])