Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
61f1be1
fix Client restricted categories and added getCategoryTitle column to…
Bugerman58 May 29, 2025
17fa621
udpate changelog
Bugerman58 May 29, 2025
44bd320
micro change after micro review
Bugerman58 May 29, 2025
88fdd96
changes by commit above
Bugerman58 May 29, 2025
09e2cb5
use category uid for getting title
Bugerman58 May 30, 2025
7195f74
remove unused import and getting title via api method
Bugerman58 May 30, 2025
17892bb
added info icon for restricted services by category
Bugerman58 May 30, 2025
5c6c832
Merge branch '2.x' into fix-restricted-categories
Bugerman58 Jun 3, 2025
0bfc10f
check services for restricted by categories of the client
Bugerman58 Jun 3, 2025
fe2110f
check if not found categories
Bugerman58 Jun 4, 2025
cf1adf4
check tests
Bugerman58 Jun 4, 2025
ada8862
Merge branch '2.x' into fix-restricted-categories
Bugerman58 Jun 4, 2025
1ce04d3
check tests
Bugerman58 Jun 4, 2025
1f1799c
Merge branch '2.x' into fix-restricted-categories
Bugerman58 Jun 4, 2025
8237b4a
filter for none
Bugerman58 Jun 4, 2025
0255361
comment restricted
Bugerman58 Jun 4, 2025
096c7ac
comment restricted
Bugerman58 Jun 4, 2025
fc63b5b
merge 2.x
Bugerman58 Jun 5, 2025
24a86d0
compile coffee script
Bugerman58 Jun 5, 2025
5fa4f67
added none for search services without category
Bugerman58 Jun 5, 2025
7e2c978
Merge branch '2.x' into fix-restricted-categories
Bugerman58 Jun 5, 2025
242d233
BLANKLINE to bool
Bugerman58 Jun 5, 2025
f28c5ad
Merge branch '2.x' into fix-restricted-categories
Bugerman58 Jun 5, 2025
e0f2b29
fix return available service uids
Bugerman58 Jun 5, 2025
4989b7a
Merge branch '2.x' into fix-restricted-categories
Bugerman58 Jun 6, 2025
f1e5343
add popup for not allowed services, add available categories for reca…
Bugerman58 Jun 6, 2025
2575e80
Merge branch '2.x' into fix-restricted-categories
Bugerman58 Jun 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Changelog
2.7.0 (unreleased)
------------------

- #2734 Fixed Client restricted categories in the Sample add view form
- #2743 Added client_sampleid to worksheet printview
- #2729 Fix rejected analyses reassigned from profile
- #2732 Added a sample invalidation form with support for entering a reason
Expand Down
86 changes: 48 additions & 38 deletions src/bika/lims/browser/analysisrequest/add2.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
from bika.lims.interfaces import IGetDefaultFieldValueARAddHook
from bika.lims.interfaces.field import IUIDReferenceField
from bika.lims.utils.analysisrequest import create_analysisrequest as crar
from bika.lims.utils.analysisrequest import get_categories
from bika.lims.utils.analysisrequest import get_available_service_uids
from BTrees.OOBTree import OOBTree
from DateTime import DateTime
from plone import protect
Expand Down Expand Up @@ -487,31 +489,14 @@ def get_fields_with_visibility(self, visibility, mode="add"):
out.append(field)
return out

def get_service_categories(self, restricted=True):
def get_service_categories(self):
"""Return all service categories in the right order

:param restricted: Client settings restrict categories
:type restricted: bool
:returns: Category catalog results
:rtype: brains
"""
bsc = api.get_tool("senaite_catalog_setup")
query = {
"portal_type": "AnalysisCategory",
"is_active": True,
"sort_on": "sortable_title",
}
categories = bsc(query)
client = self.get_client()
if client and restricted:
restricted_categories = client.getRestrictedCategories()
restricted_category_ids = map(
lambda c: c.getId(), restricted_categories)
# keep correct order of categories
if restricted_category_ids:
categories = filter(
lambda c: c.getId in restricted_category_ids, categories)
return categories
return get_categories(client)

def get_points_of_capture(self):
items = POINTS_OF_CAPTURE.items()
Expand All @@ -525,35 +510,27 @@ def get_services(self, poc="lab"):
:returns: Mapping of category -> list of services
:rtype: dict
"""
bsc = api.get_tool("senaite_catalog_setup")
setup_catalog = api.get_tool(SETUP_CATALOG)
query = {
"portal_type": "AnalysisService",
"point_of_capture": poc,
"is_active": True,
"sort_on": "sortable_title",
}
services = bsc(query)
categories = self.get_service_categories(restricted=False)
analyses = {key: [] for key in map(lambda c: c.Title, categories)}

# append the empty category as well
analyses[""] = []
services = setup_catalog(query)
analyses = {}

for brain in services:
category = self.get_category_title(brain)
if category in analyses:
analyses[category].append(brain)
cat_uid = brain.getCategoryUID
cat_title = ""
if cat_uid:
cat_brain = api.get_brain_by_uid(cat_uid)
cat_title = cat_brain and api.get_title(cat_brain)
if cat_title not in analyses:
analyses[cat_title] = []
analyses[cat_title].append(brain)
return analyses

def get_category_title(self, service):
"""Return the title of the category the service is assigned to
"""
service = api.get_object(service)
cat_uid = service.getRawCategory()
if not cat_uid:
return ""
cat = self.get_object_by_uid(cat_uid)
return api.get_title(cat)

@cache(cache_key)
def get_service_uid_from(self, analysis):
Expand Down Expand Up @@ -1268,6 +1245,14 @@ def ajax_recalculate_records(self):
dependencies = self.get_unmet_dependencies_info(metadata)
metadata.update(dependencies)

# check available services by categories
available_services = self.get_available_services(metadata)
metadata.update(available_services)

# get available categories for client
available_categories = self.get_available_categories(metadata)
metadata.update(available_categories)

# services conducted beyond the holding time limit
beyond = self.get_services_beyond_holding_time(record)
metadata["beyond_holding_time"] = beyond
Expand All @@ -1277,6 +1262,31 @@ def ajax_recalculate_records(self):

return out

def get_available_services(self, metadata):
"""Getting services by category for client or all available
"""
client_metadata = metadata.get("client_metadata", None)
client_uid = client_metadata and client_metadata.items()[0][0]
uids = get_available_service_uids(client_uid)
return {
"available_services": uids,
}

def get_available_categories(self, metadata):
"""Getting available categories by client from form
"""
client_metadata = metadata.get("client_metadata", None)
client_uid = client_metadata and client_metadata.items()[0][0]
categories = []
for cat in get_categories(client_uid):
categories.append({
"uid": cat.UID,
"title": cat.Title,
})
return {
"available_categories": categories,
}

@viewcache.memoize
def get_services_max_holding_time(self):
"""Returns a dict where the key is the uid of active services and the
Expand Down
27 changes: 27 additions & 0 deletions src/bika/lims/browser/analysisrequest/templates/ar_add2.pt
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,23 @@
</div>
</script>

<!-- Add Client restricted category popup -->
<script id="service-not-allowed" type="text/x-handlebars-template">
<div title="Service restricted" i18n:attributes="title">
<p i18n:translate="">The '{{service_title}}' cannot be selected, because it is restricted allowed categories.</p>
<div class="dialog-dependencies dialog-listing">
{{#with client}}
<p i18n:translate="">For the '{{title}}' available categories:</p>
{{/with}}
{{#each categories}}
<div class="dialog-dependency">
<span>{{title}}</span>
</div>
{{/each}}
</div>
</div>
</script>

<!-- Dependant services popup -->
<script id="service-dependant-template" type="text/x-handlebars-template">
<div title="Service locked" i18n:attributes="title">
Expand Down Expand Up @@ -598,6 +615,16 @@
class python:cls;
arnum python:arnum;">

<!-- Service not allowed by category -->
<div tal:attributes="uid python:service_uid;
arnum python:arnum;
id python:'{}-{}-restricted'.format(service_uid, arnum);
class python:'service-restricted {}-restricted'.format(service_uid);"
title="Restricted by category for Client"
i18n:attributes="title">
&#128683;
</div>

<!-- Service locked button -->
<div tal:attributes="uid python:service_uid;
arnum python:arnum;
Expand Down
1 change: 0 additions & 1 deletion src/bika/lims/content/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,6 @@
),
),

# TODO Fix Client restricted categories are not considered in Add sample
UIDReferenceField(
"RestrictedCategories",
schemata="Preferences",
Expand Down
68 changes: 67 additions & 1 deletion src/bika/lims/utils/analysisrequest.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,13 +269,23 @@ def to_list(value):
# Convert them to a list of service uids
uids = filter(None, map(to_service_uid, uids))

# Extract and append the service UIDs from the profiles
# Extract and append the service UIDs from the profiles if contained
# into available services by category
for profile in to_list(values.get("Profiles", [])):
profile = api.get_object(profile, None)
if not profile:
continue
uids.extend(profile.getServiceUIDs())

# TODO: add intersection between services from profiles and
# available services for client and remove 'empty' profiles from values,
# but running test is failed

# Exclude service uids if not available in categories for Client
client = values.get("Client")
available_uids = get_available_service_uids(client)
uids = [uid for uid in uids if uid in available_uids]

# Get the service uids without duplicates, but preserving the order
return list(OrderedDict.fromkeys(uids).keys())

Expand Down Expand Up @@ -316,6 +326,62 @@ def to_service_uid(uid_brain_obj_str):
return None


def get_available_service_uids(client=None):
"""Getting services by category for client or all available

:param client: A Client object, uid or brain
:returns: a list of Analyses Services UIDs
"""
query = {
"portal_type": "AnalysisService",
"is_active": True,
}
setup_catalog = api.get_tool(SETUP_CATALOG)
service_brains = setup_catalog(query)
categories = get_categories(client)
if categories:
available_uids = []
cat_uids = list(map(lambda c: c.UID, categories))
for brain in service_brains:
cat_uid = brain.getCategoryUID
# also if services without category
# brain without category returned <BLANKLINE> that cast to bool
if not bool(cat_uid) or cat_uid in cat_uids:
available_uids.append(brain.UID)
return available_uids

return list(map(lambda s: s.UID, service_brains))


def get_categories(for_client=None):
"""Return service categories in the right order

:param for_client: A Client object, uid or brain
:returns: a list of brains for Categories
"""
setup_catalog = api.get_tool(SETUP_CATALOG)
query = {
"portal_type": "AnalysisCategory",
"is_active": True,
"sort_on": "sortable_title",
}
categories = setup_catalog(query)
if not categories:
return []
if not for_client:
return categories
client = api.get_object(for_client, None)
if client:
restricted_categories = client.getRestrictedCategories()
restricted_category_ids = map(
lambda c: c.getId(), restricted_categories)
# keep correct order of categories
if restricted_category_ids:
categories = filter(
lambda c: c.getId in restricted_category_ids, categories)
return categories


def create_retest(ar):
"""Creates a retest (Analysis Request) from an invalidated Analysis Request
:param ar: The invalidated Analysis Request
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@
cursor: pointer;
display: none;
}
.service-restricted {
cursor: default;
display: none;
}
.service-info-table td.service-info-key {
width: 33%!important;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,8 @@ class window.AnalysisRequestAdd

# Analysis lock button clicked
$("body").on "click", ".service-lockbtn", @on_analysis_lock_button_click
# Analysis restricted button clicked
$("body").on "click", ".service-restricted", @on_analysis_restricted_button_click
# Analysis info button clicked
$("body").on "click", ".service-infobtn", @on_analysis_details_click
# Copy button clicked
Expand Down Expand Up @@ -388,6 +390,7 @@ class window.AnalysisRequestAdd

# initially hide all service-related icons
$(".service-lockbtn").hide()
$(".service-restricted").hide()

# hide all holding time related icons and set checks enabled by default
$(".analysisservice").show()
Expand Down Expand Up @@ -461,6 +464,20 @@ class window.AnalysisRequestAdd
parent = service_cb.parent "div.analysisservice"
parent.hide()

# disable(and uncheck) services that are not includes allowed categories
all_services = $("input[name='Analyses-#{arnum}:list']")
all_services.each (index_service, service) ->
service_element = $(service)
uid_service = service_element.val()
restricted = $("##{uid_service}-#{arnum}-restricted")
lock_btn = $("##{uid_service}-#{arnum}-lockbtn")
if not record.available_services.includes(uid_service)
service_element.prop "checked": no
service_element.prop "disabled": yes
restricted.show()
lock_btn.hide()


###*
* Return the portal url (calculated in code)
*
Expand Down Expand Up @@ -1546,6 +1563,35 @@ class window.AnalysisRequestAdd
dialog = @template_dialog "service-dependant-template", context, buttons


###*
* Event handler when an Analysis Service restricted by category.
*
* @param event {Object} The event object
###
on_analysis_restricted_button_click: (event) =>
console.debug "°°° on_analysis_restricted_button_click °°°"

me = this
el = event.currentTarget
$el = $(el)
uid = $el.attr "uid"
arnum = $el.attr "arnum"

record = me.records_snapshot[arnum]
service_title = $("[data-uid='#{uid}'] .service-title").text()

context = {}
context["service_title"] = service_title
context["client"] = Object.values(record.client_metadata)[0]
context["categories"] = record.available_categories

buttons =
OK: ->
$(@).dialog "destroy"

dialog = @template_dialog "service-not-allowed", context, buttons


###*
* Event handler when an Analysis Template was selected.
*
Expand Down
Loading