-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: implement google sheet import for stock verification receipts
- Loading branch information
1 parent
6f69a5a
commit 062d0c7
Showing
46 changed files
with
1,767 additions
and
65 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
{ | ||
"batch_number": { | ||
"column_index": 9 | ||
}, | ||
"comments": { | ||
"column_index": 11, | ||
"default_value": "-", | ||
"optional": true | ||
}, | ||
"commodity": { | ||
"column_index": 4, | ||
"value_mappings": { | ||
"Tenofovir/Lamivudine/Dolutegravir TLD": "Tenofovir/Lamivudine/Dolutegravir (TDF/3TC/DTG) FDC (300/300/50mg) FDC Tablets", | ||
"Dolutegravir 50mg": "Dolutegravir(DTG) 50mg tabs", | ||
"Zidovudine/Lamivudine AZT/3tC": "Zidovudine/Lamivudine (AZT/3TC) FDC (300/150mg) Tablets" | ||
}, | ||
"lookup": "name" | ||
}, | ||
"delivery_date": { | ||
"column_index": 3, | ||
"datetime_format": "%m/%d/%Y" | ||
}, | ||
"delivery_note_number": { | ||
"column_index": 6 | ||
}, | ||
"delivery_note_quantity": { | ||
"column_index": 7 | ||
}, | ||
"expiry_date": { | ||
"column_index": 10, | ||
"datetime_format": "%m/%d/%Y" | ||
}, | ||
"facility": { | ||
"column_index": 2, | ||
"lookup": "mfl_code" | ||
}, | ||
"pack_size": { | ||
"column_index": 5, | ||
"value_mappings": { | ||
"30": "Packs of 30s", | ||
"60": "Packs of 60s", | ||
"90": "Packs of 90s" | ||
}, | ||
"lookup": "name" | ||
}, | ||
"quantity_received": { | ||
"column_index": 8 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
{ | ||
"batch_number": { | ||
"column_index": 10 | ||
}, | ||
"comments": { | ||
"column_index": 12, | ||
"default_value": "-", | ||
"optional": true | ||
}, | ||
"commodity": { | ||
"column_index": 6, | ||
"value_mappings": { | ||
"Tenofovir/Lamivudine/Dolutegravir TLD": "Tenofovir/Lamivudine/Dolutegravir (TDF/3TC/DTG) FDC (300/300/50mg) FDC Tablets", | ||
"Dolutegravir 50mg": "Dolutegravir(DTG) 50mg tabs", | ||
"Zidovudine/Lamivudine AZT/3tC": "Zidovudine/Lamivudine (AZT/3TC) FDC (300/150mg) Tablets" | ||
}, | ||
"lookup": "name" | ||
}, | ||
"delivery_date": { | ||
"column_index": 5, | ||
"datetime_format": "%m/%d/%Y" | ||
}, | ||
"delivery_note_number": { | ||
"column_index": 4 | ||
}, | ||
"delivery_note_quantity": { | ||
"column_index": 8 | ||
}, | ||
"expiry_date": { | ||
"column_index": 11, | ||
"datetime_format": "%m/%d/%Y" | ||
}, | ||
"facility": { | ||
"column_index": 3, | ||
"lookup": "mfl_code" | ||
}, | ||
"pack_size": { | ||
"column_index": 7, | ||
"value_mappings": { | ||
"30": "Packs of 30s", | ||
"60": "Packs of 60s", | ||
"90": "Packs of 90s" | ||
}, | ||
"lookup": "name" | ||
}, | ||
"quantity_received": { | ||
"column_index": 9 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
from rest_framework.permissions import BasePermission | ||
from rest_framework.request import Request | ||
from rest_framework.views import APIView | ||
|
||
|
||
class CanExportData(BasePermission): | ||
"""Allows access only to users who have the export data permission.""" | ||
|
||
def has_permission(self, request: Request, view: APIView) -> bool: | ||
return ( | ||
request.user | ||
and request.user.is_authenticated | ||
and request.user.has_perm("can_export_data") | ||
) | ||
|
||
|
||
class CanImportData(BasePermission): | ||
"""Allow access only to users who have the import data permission.""" | ||
|
||
def has_permission(self, request: Request, view: APIView) -> bool: | ||
return ( | ||
request.user | ||
and request.user.is_authenticated | ||
and request.user.has_perm("can_import_data") | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
from django.contrib import admin | ||
|
||
from fahari.common.admin import BaseAdmin | ||
|
||
from .models import SheetToDBMappingsMetadata, StockVerificationReceiptsAdapter | ||
|
||
|
||
@admin.register(SheetToDBMappingsMetadata) | ||
class StockToDBMappingsMetadata(BaseAdmin): | ||
list_display = ("name", "version") | ||
|
||
|
||
@admin.register(StockVerificationReceiptsAdapter) | ||
class StockVerificationReceiptsAdapterAdmin(BaseAdmin): | ||
list_display = ("county", "position") | ||
readonly_fields = BaseAdmin.readonly_fields + ("target_model",) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
from django.apps import AppConfig | ||
from django.utils.translation import gettext_lazy as _ | ||
|
||
|
||
class MiscConfig(AppConfig): | ||
name = "fahari.misc" | ||
verbose_name = _("Miscellaneous") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
from typing import Any, Callable, Dict, Generic, Optional, TypeVar | ||
|
||
from channels.generic.websocket import JsonWebsocketConsumer | ||
|
||
from .exceptions import ProcessGoogleSheetRowError | ||
from .models import AbstractGoogleSheetToDjangoModelAdapter, StockVerificationReceiptsAdapter | ||
|
||
A = TypeVar("A", bound=AbstractGoogleSheetToDjangoModelAdapter, covariant=True) | ||
ProgressCallback = Callable[[int, int, float], None] | ||
|
||
|
||
class AbstractGoogleSheetToDjangoModelAdapterConsumer(Generic[A], JsonWebsocketConsumer): | ||
"""Read, process and persist data from a Google Sheets spreadsheet.""" | ||
|
||
def connect(self) -> None: | ||
self.user = self.scope["user"] # noqa | ||
# Ensure that the user has permissions to import data | ||
if not self.user.has_perm("can_import_data"): | ||
self.close(code=1008) | ||
self.accept() | ||
|
||
def receive_json(self, content: Any, **kwargs) -> None: | ||
adapter = self.get_adapter(content) | ||
try: | ||
ingested_rows = adapter.ingest_from_last_position( | ||
progress_callback=self.get_progress_callback(), **self.get_adapter_context() | ||
) | ||
except ProcessGoogleSheetRowError as exp: | ||
error_details = {"row_index": exp.row_index} | ||
error_messages = [] | ||
while exp is not None: | ||
error_messages.append(str(exp)) | ||
exp = exp.__cause__ # type: ignore | ||
error_details["error_messages"] = error_messages | ||
self.send_data("error", error_details) | ||
return | ||
|
||
self.send_data("success", {"ingested_rows": ingested_rows}) # noqa | ||
|
||
def send_data(self, data_type: str, data_content: Any) -> None: | ||
"""Send the given data of to the party on the other end of this connection.""" | ||
|
||
self.send_json({"type": data_type, "data": data_content}) | ||
|
||
def get_adapter(self, content: Any) -> A: | ||
"""Return an adapter instance given input.""" | ||
|
||
raise NotImplementedError("`get_adapter` must be implemented.") | ||
|
||
def get_adapter_context(self) -> Dict[str, Any]: | ||
"""Return extra context to be injected in an adapter's `ingest_from_last_position` method. | ||
Subclasses can override this method to add or change the returned | ||
context dict. | ||
""" | ||
|
||
return { | ||
"extra_kwargs": { | ||
"created_by": self.user.pk, | ||
"organisation": self.user.organisation, # noqa | ||
"updated_by": self.user.pk, | ||
} | ||
} | ||
|
||
def get_progress_callback(self) -> Optional[ProgressCallback]: | ||
"""Optionally return a function that acts a progress callback.""" | ||
|
||
def simple_progress_callback(current: int, total: int, percentage: float) -> None: | ||
"""Send progress data through a web-socket to the client on the other end.""" | ||
|
||
self.send_data( | ||
"progress", {"current": current, "total": total, "percentage": percentage} | ||
) | ||
|
||
return simple_progress_callback | ||
|
||
|
||
class StockVerificationReceiptsAdapterConsumer( | ||
AbstractGoogleSheetToDjangoModelAdapterConsumer[StockVerificationReceiptsAdapter] | ||
): | ||
"""StockVerificationReceiptsAdapter consumer.""" | ||
|
||
def get_adapter(self, content: Any) -> A: | ||
adapter: str = content["adapter"] | ||
return StockVerificationReceiptsAdapter.objects.active().get(pk=adapter) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
from typing import Sequence | ||
|
||
|
||
class ProcessGoogleSheetRowError(RuntimeError): | ||
"""Raised when processing a google sheet row fails.""" | ||
|
||
def __init__(self, row: Sequence[str], row_index: int, *args, **kwargs): | ||
self.row: Sequence[str] = tuple(row) | ||
self.row_index: int = row_index | ||
super().__init__(*args, **kwargs) # type: ignore |
Oops, something went wrong.