Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Calculate accountability end date #265

Merged
merged 5 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 importer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ The coverage report `coverage.html` will be at the working directory
- See example csv [here](/importer/csv/import/inventory.csv)
- This creates a Group resource for each inventory imported
- The first two columns __name__ and __active__ is the minimum required
- The `accountabilityDate` is an optional column. If left empty, the date will be automatically calculated using the product's accountability period
- Adding a value to the Location column will create a separate List resource (or update) that links the inventory to the provided location resource
- A separate List resource with references to all the Group and List resources generated is also created
- You can pass in a `list_resource_id` to be used as the identifier for the (reference) List resource, or you can leave it empty and a random uuid will be generated
Expand Down
55 changes: 52 additions & 3 deletions importer/importer/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
import os
import pathlib
import uuid
from datetime import datetime

import click
import magic
import requests
from dateutil.relativedelta import relativedelta

from importer.config.settings import (api_service, fhir_base_url,
product_access_token)
Expand Down Expand Up @@ -389,6 +391,42 @@ def save_image(image_source_url):
return 0


def get_product_accountability_period(product_id: str) -> int:
product_endpoint = "/".join([fhir_base_url, "Group", product_id])
response = handle_request("GET", "", product_endpoint)
if response[1] != 200:
logging.error(
"Error while attempting to get the accountability period from product : "
+ product_id
)
logging.error(response[0])
return -1

json_product = json.loads(response[0])
product_characteristics = json_product["characteristic"]
for character in product_characteristics:
if (
character["code"]["coding"][0]["system"] == "http://smartregister.org/codes"
and character["code"]["coding"][0]["code"] == "67869606"
):
accountability_period = character["valueQuantity"]["value"]
return accountability_period
logging.error(
"Accountability period was not found in the product characteristics : "
+ product_id
)
return -1


def calculate_date(delivery_date: str, product_accountability_period: int) -> str:
delivery_datetime = datetime.strptime(delivery_date, "%Y-%m-%dT%H:%M:%S.%fZ")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@peterMuriuki is this date format assumption correct? or should we make this a parameter?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah that looks correct, The format is YYYY, YYYY-MM, YYYY-MM-DD or YYYY-MM-DDThh:mm:ss+zz:zz,

ref: https://www.hl7.org/fhir/R4/datatypes.html#dateTime

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@peterMuriuki I was thinking more inline of the csv. Since those are not necessarily coming from the server
Unless someone exports the resources.
I'll create a separate issue to extend this just in case

end_date = delivery_datetime + relativedelta(months=product_accountability_period)
end_date_str = end_date.strftime("%Y-%m-%dT%H:%M:%S.")
milliseconds = end_date.microsecond // 1000
end_date_str += f"{milliseconds:03d}Z"
return end_date_str


# custom extras for product import
def group_extras(resource, payload_string, group_type, created_resources):
payload_obj = json.loads(payload_string)
Expand Down Expand Up @@ -582,9 +620,20 @@ def group_extras(resource, payload_string, group_type, created_resources):
GROUP_INDEX_MAPPING["inventory_member_index"]
]["period"]["end"] = accountability_date
else:
payload_obj["resource"]["member"][
GROUP_INDEX_MAPPING["inventory_member_index"]
]["period"]["end"] = ""
product_accountability_period = get_product_accountability_period(
product_id
)
if product_accountability_period != -1:
accountability_date = calculate_date(
delivery_date, product_accountability_period
)
payload_obj["resource"]["member"][
GROUP_INDEX_MAPPING["inventory_member_index"]
]["period"]["end"] = accountability_date
else:
payload_obj["resource"]["member"][
GROUP_INDEX_MAPPING["inventory_member_index"]
]["period"]["end"] = ""

if quantity:
payload_obj["resource"]["characteristic"][
Expand Down
2 changes: 2 additions & 0 deletions importer/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,7 @@ python-magic==0.4.27
jwt
python-dotenv==1.0.1
pytest-env==1.1.3
python-dateutil==2.9.0

# Windows requirements
python-magic-bin==0.4.14; sys_platform == 'win32'
119 changes: 117 additions & 2 deletions importer/tests/test_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
from mock import patch

from importer.builder import (build_assign_payload, build_org_affiliation,
build_payload, check_parent_admin_level,
extract_matches, extract_resources,
build_payload, calculate_date,
check_parent_admin_level, extract_matches,
extract_resources,
get_product_accountability_period,
process_resources_list)
from importer.utils import read_csv

Expand Down Expand Up @@ -961,3 +963,116 @@ def test_define_own_location_type_coding_system_url(
payload_obj["entry"][0]["resource"]["type"][0]["coding"][0]["system"],
test_system_code,
)

def test_calculate_date(self):
delivery_date = "2024-06-01T10:40:10.111Z"
product_accountability_period = 12
end_date = calculate_date(delivery_date, product_accountability_period)
self.assertEqual("2025-06-01T10:40:10.111Z", end_date)

@patch("importer.builder.handle_request")
@patch("importer.builder.get_base_url")
def test_import_inventory_and_calculate_end_date_from_product(
self, mock_get_base_url, mock_handle_request
):
mock_get_base_url.return_value = "https://example.smartregister.org/fhir"
mock_response_data = {
"resourceType": "Group",
"id": "1d86d0e2-bac8-4424-90ae-e2298900ac3c",
"name": "thermometer",
"characteristic": [
{
"code": {
"coding": [
{
"system": "http://smartregister.org/codes",
"code": "23435363",
"display": "Attractive Item code",
}
]
},
"valueBoolean": True,
},
{
"code": {
"coding": [
{
"system": "http://smartregister.org/codes",
"code": "67869606",
"display": "Accountability period (in months)",
}
]
},
"valueQuantity": {"value": 12},
},
],
}
string_response = json.dumps(mock_response_data)
mock_response = (string_response, 200)
mock_handle_request.return_value = mock_response

resource_list = [
[
"Nairobi Inventory Items",
"true",
"create",
"e62a049f-8d48-456c-a387-f52e72c39c74",
"123523",
"989682",
"a065c211-cf3e-4b5b-972f-fdac0e45fef7",
"false",
"1d86d0e2-bac8-4424-90ae-e2298900ac3c",
"2024-06-01T10:40:10.111Z",
"",
"34",
"Health",
"Sample",
"8f06f052-c08f-4490-84d8-f50399081434",
]
]

json_payload = build_payload(
"Group", resource_list, json_path + "inventory_group_payload.json"
)
payload_obj = json.loads(json_payload)
self.assertEqual(
"2025-06-01T10:40:10.111Z",
payload_obj["entry"][0]["resource"]["member"][0]["period"]["end"],
)

@patch("importer.builder.logging")
@patch("importer.builder.handle_request")
@patch("importer.builder.get_base_url")
def test_missing_product_accountability_period_characteristic(
self, mock_get_base_url, mock_handle_request, mock_logging
):
mock_get_base_url.return_value = "https://example.smartregister.org/fhir"
mock_response_data = {
"resourceType": "Group",
"id": "1d86d0e2-bac8-4424-90ae-e2298900ac3c",
"name": "thermometer",
"characteristic": [
{
"code": {
"coding": [
{
"system": "http://smartregister.org/codes",
"code": "23435363",
"display": "Attractive Item code",
}
]
},
"valueBoolean": True,
},
],
}
string_response = json.dumps(mock_response_data)
mock_response = (string_response, 200)
mock_handle_request.return_value = mock_response

product_id = "0b907a28-40de-4fff-a26a-f8bace0b8652"
period = get_product_accountability_period(product_id)
self.assertEqual(-1, period)
mock_logging.error.assert_called_with(
"Accountability period was not found in the product characteristics : 0b907a28-40de-4fff-a26a-f8bace0b8652"
)