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"])