Skip to content

Commit

Permalink
refactored the document overrides into overrides folder containing cl…
Browse files Browse the repository at this point in the history
…ient and server overrides. Implemented client and server functionality for Sales Invoice. Added mandatory dependencies to hooks.py. Made communication key non-mandatory in communication key doctype. Refactored function for clean project structure.
  • Loading branch information
GichanaMayaka committed Mar 6, 2024
1 parent 60558ca commit 024ce84
Show file tree
Hide file tree
Showing 9 changed files with 191 additions and 57 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*.swp
tags

/.vscode
**/.vscode
/.idea
/node_modules
/__pycache__
4 changes: 2 additions & 2 deletions kenya_compliance/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
# page_js = {"page" : "public/js/file.js"}

# include js in doctype views
# doctype_js = {"doctype" : "public/js/doctype.js"}
doctype_js = {"Sales Invoice": "kenya_compliance/overrides/client/sales_invoice.js"}
# doctype_list_js = {"doctype" : "public/js/doctype_list.js"}
# doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"}
# doctype_calendar_js = {"doctype" : "public/js/doctype_calendar.js"}
Expand Down Expand Up @@ -144,7 +144,7 @@
# }
"Sales Invoice": {
"on_submit": [
"kenya_compliance.kenya_compliance.interceptors.invoice_on_submit"
"kenya_compliance.kenya_compliance.overrides.server.sales_invoice.on_submit"
]
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
LAST_REQUEST_DATE_DOCTYPE_NAME: Final[str] = "Navari KRA eTims Last Request Date"
ROUTES_TABLE_DOCTYPE_NAME: Final[str] = "Navari KRA eTims Route Table"
ROUTES_TABLE_CHILD_DOCTYPE_NAME: Final[str] = "Navari KRA eTims Route Table Item"
ITEM_CLASSIFICATIONS_DOCTYPE_NAME: Final[str] = "Navari KRA eTims Item Classification"
TAXATION_TYPE_DOCTYPE_NAME: Final[str] = "Navari KRA eTims Taxation Type"

# Global Variables
SANDBOX_SERVER_URL: Final[str] = "https://etims-api-sbx.kra.go.ke/etims-api"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,13 @@
"fieldtype": "Data",
"in_list_view": 1,
"label": "Communication Key",
"read_only": 1,
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2024-02-21 03:54:59.139563",
"modified": "2024-03-06 09:51:57.506571",
"modified_by": "Administrator",
"module": "Kenya Compliance",
"name": "Navari KRA eTims Communication Key",
Expand Down
2 changes: 1 addition & 1 deletion kenya_compliance/kenya_compliance/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def fetch_communication_key(response: dict[str, str]) -> str | None:
communication_key, datetime.now()
)

return communication_key
return saved_key.cmckey

except KeyError as error:
etims_logger.exception(error)
Expand Down
41 changes: 0 additions & 41 deletions kenya_compliance/kenya_compliance/interceptors.py

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
const doctype = "Sales Invoice";
const childDoctype = "Sales Invoice Item";

frappe.ui.form.on(doctype, {
refresh: function (frm) {
frm.fields_dict.items.grid.get_field("item_classification_code").get_query =
function (doc, cdt, cdn) {
const itemDescription = locals[cdt][cdn].description;
const descriptionText = parseItemDescriptText(itemDescription);

return {
filters: [
[
"Navari KRA eTims Item Classification",
"itemclsnm",
"like",
`%${descriptionText}%`,
],
],
};
};
},
});

frappe.ui.form.on(childDoctype, {
item_classification_code: async function (frm, cdt, cdn) {
const itemClassificationCode = locals[cdt][cdn].item_classification_code;

if (itemClassificationCode) {
const response = await frappe.db.get_value(
"Navari KRA eTims Item Classification",
{ itemclscd: itemClassificationCode },
["*"]
);

frappe.model.set_value(
cdt,
cdn,
"taxation_type",
response.message?.taxtycd
);
}
},
});

function parseItemDescriptText(description) {
const temp = document.createElement("div");

temp.innerHTML = description;
const descriptionText = temp.textContent || temp.innerText;

return descriptionText;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import asyncio

import aiohttp
import frappe
from frappe.model.document import Document

from ...logger import etims_logger
from ...utils import (
build_headers,
get_last_request_date,
get_route_path,
get_server_url,
make_post_request,
queue_request,
)


def on_submit(doc: Document, method: str) -> None:
"""Intercepts submit event for document"""
error_messages = None
headers = build_headers(doc)
last_request_date = get_last_request_date()

if headers and last_request_date:
server_url = get_server_url(doc)
route_path = get_route_path("CodeSearchReq")

if server_url and route_path:
url = f"{server_url}{route_path}"

try:
# TODO: Run job in background
response = asyncio.run(
make_post_request(url, {"lastReqDt": "20230101000000"}, headers)
)

if response:
print(f"{response}")
raise Exception
except aiohttp.client_exceptions.ClientConnectorError as error:
etims_logger.exception(error, exc_info=True)
frappe.throw(
"Connection failed",
error,
title="Connection Error",
)

elif not headers:
error_messages = (
"Headers not set for %s. Please ensure the tax Id is properly set"
% doc.name
)
etims_logger.error(error_messages)
frappe.throw(error_messages, title="Incorrect Setup")

elif not last_request_date:
error_messages = (
"Last Request Date is not set for %s. Please ensure it is properly set"
% doc.name
)
etims_logger.error(error_messages)
frappe.throw(error_messages, title="Incorrect Setup")
79 changes: 69 additions & 10 deletions kenya_compliance/kenya_compliance/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import re
from datetime import datetime
from typing import Any, Callable, Literal

import aiohttp
import frappe
Expand All @@ -14,6 +15,7 @@
ROUTES_TABLE_DOCTYPE_NAME,
SETTINGS_DOCTYPE_NAME,
)
from .logger import etims_logger


def is_valid_kra_pin(pin: str) -> bool:
Expand Down Expand Up @@ -46,21 +48,24 @@ async def make_get_request(url: str) -> dict[str, str]:


async def make_post_request(
url: str, data: dict[str, str] | None = None
url: str,
data: dict[str, str] | None = None,
headers: dict[str, str | int] | None = None,
) -> dict[str, str]:
"""Make an Asynchronous POST Request to specified URL
Args:
url (str): The URL
data (dict[str, str] | None, optional): Data to send to server. Defaults to None.
headers (dict[str, str | int] | None, optional): Headers to set. Defaults to None.
Returns:
dict: The Server Response
"""
# TODO: Refactor to a more efficient handling of creation of the session object
# as described in documentation
async with aiohttp.ClientSession() as session:
async with session.post(url, json=data) as response:
async with session.post(url, json=data, headers=headers) as response:
return await response.json()


Expand Down Expand Up @@ -98,11 +103,16 @@ def get_communication_key(doctype: str = COMMUNICATION_KEYS_DOCTYPE_NAME) -> str
Returns:
str: The fetched communication key
"""
error_messages = None
communication_key = frappe.db.get_single_value(doctype, "cmckey")

if communication_key:
return communication_key

error_messages = "No Communication Key found in %s" % doctype
etims_logger.error(error_messages)
frappe.throw(error_messages, title="Incorrect Setup")


def build_datetime_from_string(
date_string: str, format: str = "%Y-%m-%d %H:%M:%S"
Expand Down Expand Up @@ -181,13 +191,13 @@ def get_current_user_timezone(current_user: str) -> str | None:


def get_route_path(
search_field: str = "CodeSearchReq",
search_field: str,
routes_table_doctype: str = ROUTES_TABLE_CHILD_DOCTYPE_NAME,
) -> str | None:
"""Searches and retrieves the route path from the KRA eTims Route Table Navari doctype
Args:
search_field (str): The field to search
search_field (str): The field to search.
routes_table (str, optional): _description_. Defaults to ROUTES_TABLE_CHILD_DOCTYPE_NAME.
Returns:
Expand Down Expand Up @@ -221,8 +231,9 @@ def get_environment_settings(
environment (str, optional): The environment state. Defaults to "Sandbox".
Returns:
_type_: _description_
Document | None: The fetched document.
"""
error_message = None
query = f"""
SELECT sandbox,
server_url,
Expand All @@ -234,12 +245,15 @@ def get_environment_settings(
FROM `tab{doctype}`
WHERE name like '{company_pin}-{environment}%'
"""

setting_doctype = frappe.db.sql(query, as_dict=True)

if setting_doctype:
return setting_doctype[0]

error_message = "No environment setting created. Please ensure a valid Navari eTims Integration Setting record exists"
etims_logger.error(error_message)
frappe.throw(error_message, title="Incorrect Setup")


def get_server_url(document: Document, environment: str = "Sandbox") -> str | None:
settings = get_environment_settings(
Expand All @@ -252,21 +266,66 @@ def get_server_url(document: Document, environment: str = "Sandbox") -> str | No
return server_url


def build_common_payload(
def build_headers(
document: Document, environment: str = "Sandbox"
) -> dict[str, str] | None:
"""Builds the header required for communication with the eTims Server
Args:
document (Document): The document to fetch setting information from
environment (str, optional): Variable denoting environment of current instance. Defaults to "Sandbox".
Returns:
dict[str, str] | None: The headers as a dictionary
"""
settings = get_environment_settings(
document.company_tax_id, environment=environment
)
# TODO: Handle no communication key and request date
communication_key = get_communication_key()
last_request_date = get_last_request_date()

if settings and communication_key and last_request_date:
if settings and communication_key:
payload = {
"tin": settings.get("tin"),
"bhfId": settings.get("bhfid"),
"cmcKey": communication_key,
"lastReqDt": last_request_date,
}

return payload


def queue_request(
function_to_queue: Callable[[str, dict[str, str]], dict[str, str]],
url: str,
payload: dict,
headers: dict,
success_callback: Callable[[Any, Any, Any], Any],
failure_callback: Callable[[Any, Any, Any, Any, Any], Any],
queue_type: Literal["default", "short", "long"],
) -> Any:
"""Queues a request function to the Redis Queue
Args:
function_to_queue (Callable[[str, dict[str, str]], dict[str, str]]): The function to queue
url (str): The target request url
payload (dict): The json request data
headers (dict): The request headers
success_callback (Callable[[Any, Any, Any], Any]): Callback to handle successful responses
failure_callback (Callable[[Any, Any, Any, Any, Any], Any]): Callback to handle failure responses
queue_type (Literal["default", "short", "long"]): Type of Queue to enqueue job to
Returns:
Any: The job id of current job
"""
job = frappe.enqueue(
function_to_queue,
url=url,
data=payload,
headers=headers,
is_async=True,
queue=queue_type,
on_success=success_callback,
on_failure=failure_callback,
)

return job.id

0 comments on commit 024ce84

Please sign in to comment.