Skip to content

Commit

Permalink
Calculate accountability end date (#265)
Browse files Browse the repository at this point in the history
* Add functionality to calculate the accountability date using the product's accountability period

* add dateutil
  • Loading branch information
Wambere authored Aug 26, 2024
1 parent 11b7fc3 commit 20afab3
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 5 deletions.
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")
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"
)

0 comments on commit 20afab3

Please sign in to comment.