diff --git a/importer/README.md b/importer/README.md index e603aac0..72178fc1 100644 --- a/importer/README.md +++ b/importer/README.md @@ -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 diff --git a/importer/importer/builder.py b/importer/importer/builder.py index 57f037f9..b92781a0 100644 --- a/importer/importer/builder.py +++ b/importer/importer/builder.py @@ -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) @@ -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) @@ -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"][ diff --git a/importer/requirements.txt b/importer/requirements.txt index cfe85f12..77f04966 100644 --- a/importer/requirements.txt +++ b/importer/requirements.txt @@ -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' diff --git a/importer/tests/test_builder.py b/importer/tests/test_builder.py index 23928688..153d1d98 100644 --- a/importer/tests/test_builder.py +++ b/importer/tests/test_builder.py @@ -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 @@ -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" + )