Skip to content

Commit

Permalink
Merge pull request #380 from angledimension/condom-features
Browse files Browse the repository at this point in the history
Condom management features
  • Loading branch information
czue authored Dec 6, 2023
2 parents d0d1eef + 7d43aa5 commit 0586a8a
Show file tree
Hide file tree
Showing 11 changed files with 304 additions and 13 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.project
.spyproject
.pydevproject
.idea
.settings
Expand All @@ -14,3 +15,5 @@ logistics_project/localsettings.py
logistics_project/logistics.log*
logistics_project/apps/malawi/tests/testscripts/*
logistics_project/apps/tanzania/tests/testscripts/*
.spyproject/*
.spyproject/config
45 changes: 43 additions & 2 deletions data/cstock-logistics.json
Original file line number Diff line number Diff line change
Expand Up @@ -10216,5 +10216,46 @@
"type": "hf",
"supplied_by": 4473
}
}
]
},
{
"pk": 1,
"model": "logistics.productreporttype",
"fields": {
"id" : 1,
"name" : "stock on hand",
"code" : "soh"
}
},
{
"pk": 2,
"model": "logistics.productreporttype",
"fields": {
"name" : "stock received",
"code" : "rec"
}
},
{
"pk": 3,
"model": "logistics.productreporttype",
"fields": {
"name" : "stock given",
"code" : "give"
}
},
{
"pk": 4,
"model": "logistics.productreporttype",
"fields": {
"name" : "emergency stock on hand",
"code" : "eo"
}
},
{
"pk": 5,
"model": "logistics.productreporttype",
"fields": {
"name" : "loss or adjustment",
"code" : "la"
}
}
]
2 changes: 1 addition & 1 deletion ex-submodules/rapidsms-logistics/logistics/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1482,7 +1482,7 @@ def get_product(self, product_code):
if the product can't be found.
"""
try:
return Product.objects.get(sms_code__icontains=product_code)
return Product.objects.get(sms_code__iexact=product_code)
except (Product.DoesNotExist, Product.MultipleObjectsReturned):
raise UnknownCommodityCodeError(product_code)

Expand Down
83 changes: 83 additions & 0 deletions logistics_project/apps/malawi/handlers/map_supply_point.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from __future__ import unicode_literals
from builtins import str
from django.utils.translation import gettext as _
from rapidsms.contrib.handlers.handlers.keyword import KeywordHandler
from rapidsms.models import Contact
from logistics.util import config
from logistics.models import SupplyPoint
from rapidsms.contrib.locations.models import Location, Point
import logging

logger = logging.getLogger("django")


class MapSupplyPointHandler(KeywordHandler):
"""Set the location of a supply point using "map"."""

keyword = "map"

def help(self):
# only display help if contact is registered
if hasattr(self.msg,'logistics_contact'):
if self.msg.logistics_contact.supply_point.type == config.hsa_supply_point_type():
self.respond(_(config.Messages.MAPPING_HELP))
else:
self.respond(_(config.Messages.UNSUPPORTED_OPERATION))
else:
self.respond(_(config.Messages.NOT_REGISTERED))

def handle(self, text):
# only allow registered contact at hsa level
is_hsa = self.msg.logistics_contact.supply_point.type == config.hsa_supply_point_type()

if not hasattr(self.msg,'logistics_contact') or is_hsa == False:
self.respond(_(config.Messages.NOT_REGISTERED))
else:
words = text.split()
if len(words) < 2:
self.help()
else:
latitude = words[0]
longitude = words[1]

if(self._validate_latitude(latitude) and self._validate_longitude(longitude)):
# create location record and link to supply point

logger.info("Mapping location")
point = Point(latitude=float(latitude), longitude=float(longitude))
point.save()
self.msg.connection.contact.supply_point.location.point = point
self.msg.connection.contact.supply_point.location.save()

self.respond(_(config.Messages.MAPPING_SUCCESS), sp_name=self.msg.connection.contact.supply_point)
else:
logger.info(config.Messages.INVALID_COORDINATES)
self.respond(_(config.Messages.INVALID_COORDINATES))


def _validate_latitude(self,latitude):
if self._is_float(latitude):
lat_value = float(latitude)
if (lat_value > -90) and (lat_value < 90):
return True
else:
return False
else:
return False

def _validate_longitude(self,longitude):
if self._is_float(longitude):
long_value = float(longitude)
if (long_value > -180) and (long_value < 180):
return True
else:
return False
else:
return False

def _is_float(self,string):
try:
float(string)
return True
except ValueError:
return False
30 changes: 30 additions & 0 deletions logistics_project/apps/malawi/static/malawi/css/malawi-new.css
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,33 @@ form#login {
width: 300px;
border: none;
}

dl dt.inline {
display: inline-block;
width: 20px;
height: 20px;
vertical-align: middle;
}

dl dd.inline {
display: inline-block;
margin: 0px 10px;
padding: 2px;
vertical-align: middle;
}

dl dt.over-stock {
background: #800080;
}

dl dt.adequate-stock {
background: #008000;
}

dl dt.under-stock {
background: #FFA500;
}

div.legend-box {
margin: 5px
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,32 @@ <h2>{{ base_level_description }} months of stock by product</h2>
{% endwith %}
</div>
{% endif %}

{% if product_map %}
<div class="module">
<form method="get" class="selector">
<label for="map_product" id="map_product">Product: </label>
<select name="map_product" id="map_product">
{% for p in products %}
<option value="{{ p.sms_code }}" {% if p.pk == selected_map_product.pk %}selected{% endif %}>{{ p.name }}</option>
{% endfor %}
</select>
<input type="hidden" id="_loc" name="place" value="{{ location.code }}" />
<input type="submit" value="Go!" />
</form>
<h2>Current stock status of HSAs</h2>
<div class="legend-box">
<dl>
<dt class="inline under-stock"></dt>
<dd class="inline">Under stock</dt>
<dt class="inline adequate-stock"></dt>
<dd class="inline">Adequate stock</dd>
<dt class="inline over-stock"></dt>
<dd class="inline">Overstocked</dd>
</dl>
</div>
{{ product_map|safe }}
</div>
{% endif %}

{% endblock %}
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
from __future__ import unicode_literals
from builtins import range
import json
import folium
import logging
import random
from collections import defaultdict
from geopy.geocoders import Nominatim

from logistics.models import Product, SupplyPoint, ProductType, ProductStock
from logistics.models import (Product, SupplyPoint, SupplyPointType,
ProductType, ProductStock)
from logistics.util import config
from logistics_project.apps.malawi.util import (fmt_pct, pct,
is_country, is_district, is_facility, hsa_supply_points_below,
Expand All @@ -16,7 +21,6 @@
from django.db.models.aggregates import Sum
from django.shortcuts import get_object_or_404


class View(warehouse_view.DistrictOnlyView):

def _get_product_status_table(self, supply_points, product, date):
Expand Down Expand Up @@ -46,8 +50,8 @@ def get_selected_product_type(self, request):

return None

def get_selected_product(self, request):
pcode = request.GET.get("product")
def get_selected_product(self, request, element_id="product"):
pcode = request.GET.get(element_id)
if pcode:
return get_object_or_404(Product, sms_code=pcode, type__base_level=request.base_level)

Expand Down Expand Up @@ -204,11 +208,83 @@ def is_viewing_base_level_data(self, request, reporting_supply_point):
(is_district(reporting_supply_point) and request.base_level_is_facility)
)

def get_stock_status_map(self, request, reporting_supply_point, selected_product):
"""
Status of product in map format.
"""
product_map = folium.Map(location=(-13.9626, 33.7741), zoom_start=6)

try:
# Retrieve active product stock per supply point including location coordinates
# 1. Retrieve active supply points at hsa level
hsa_sup_type = SupplyPointType.objects.get(pk='hsa')
suppliers = SupplyPoint.objects.filter(active=True, type=hsa_sup_type)

# 2. Retrieve product details including stock levels
product = Product.by_code(selected_product.sms_code)
product_suppliers = [sup for sup in suppliers if sup.supplies(product)]

# 3. Obtain location point details for each supply point if available
for supplier in product_suppliers:
# retrieve location coordinates if set otherwise do not display
if supplier.location.point:
location_point = supplier.location.point
else:
continue

# 4. indicate stock levels using color codes
current_stock = ProductStock.objects.get(supply_point=supplier, product=product)
current_quantity = current_stock.quantity
product_amc = product.average_monthly_consumption
product_eo_level = product.emergency_order_level

# only mark location if quantity, and either amc or eo level are set
if current_quantity:
# if both amc and eo level set then any status can be set
if product_amc and product_eo_level:
if current_quantity >= product_amc:
stock_status_color = 'purple'
elif current_quantity <= product_eo_level:
stock_status_color = 'orange'
else:
stock_status_color = 'green'
elif product_amc and product_eo_level is None:
# if only product amc then either above amc or below
if current_quantity >= product_amc:
stock_status_color = 'purple'
else:
stock_status_color = 'orange'
else:
continue

# Define marker using supplier name, quantity, and stock status
label = f'{supplier.name} ({current_quantity})'

if location_point:
folium.Marker(
location=[location_point.latitude,
location_point.longitude],
tooltip=label,
popup=label,
icon=folium.Icon(color=stock_status_color)
).add_to(product_map)
except Product.DoesNotExist:
pass
except ProductStock.DoesNotExist:
pass

return product_map._repr_html_()

def custom_context(self, request):
selected_type = self.get_selected_product_type(request)
selected_product = self.get_selected_product(request)
selected_map_product = self.get_selected_product(request, "map_product")
reporting_supply_point = self.get_reporting_supply_point(request)

product_map = self.get_stock_status_map(request, reporting_supply_point,
selected_map_product)

months_of_stock_table = None
stock_status_across_location_table = None

Expand All @@ -230,10 +306,12 @@ def custom_context(self, request):
'window_date': current_report_period(),
'selected_type': selected_type,
'selected_product': selected_product,
'selected_map_product': selected_map_product,
'status_table': self.get_stock_status_by_product_table(request, reporting_supply_point),
'stock_status_across_location_table': stock_status_across_location_table,
# This apparently isn't used in the template but is needed in get_report() when export_csv=True
'stockout_table': stockout_table,
'months_of_stock_table': months_of_stock_table,
'stockout_graph': stockout_graph,
'product_map': product_map
}
2 changes: 1 addition & 1 deletion logistics_project/deployments/malawi/settings_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,4 +217,4 @@
# data warehouse config
WAREHOUSE_RUNNER = 'logistics_project.apps.malawi.warehouse.runner.MalawiWarehouseRunner'
ENABLE_FACILITY_WORKFLOWS = False
LOGISTICS_USE_DEFAULT_HANDLERS = False
LOGISTICS_USE_DEFAULT_HANDLERS = False
2 changes: 2 additions & 0 deletions requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ django-picklefield==0.2.0
sentry-sdk==0.17.7 # pinned for celery issue https://github.com/getsentry/sentry-python/issues/844
future
gunicorn
folium
geopy
Loading

0 comments on commit 0586a8a

Please sign in to comment.