Automating reports for the “Humanitarian Nova Poshta” programme
Nova Poshta has launched a programme called “Humanitarian Post of the Country” that lets charitable foundations send shipments free of charge. It is still running, and a big thank you to them for that!
Now for the technical side. If you automate your work and have to file reports for this programme, here is a recipe for making it easier. When you only send a few parcels, automation never comes up — you can do the whole thing by hand quickly enough. But once the parcels start piling up, the headaches begin! As always, I have put together a skeleton of the code that automates the monthly report. All that is left for you is to adapt it to your own CRM. You could turn it into a Flask API service.
Let’s go!
Nova Poshta gives you a ready-made report template in XLSX, and that is great! Of course, if I were building a big service that churned out reports in bulk, I would generate a new xlsx document in code. But this automation is aimed at one narrow task with a handful of users, and the report only gets filed once a month, so my “lazy approach” does the job here. I just take the template file, copy it and add the rows I need.
That way I never have to fight with the layout of an xlsx document in code.
Filtering charity shipments
A question came up along the way: how do I tell charity parcels apart from the ones I send for my own private work? At first I thought the Nova Poshta API would show me whether a parcel had been paid for with bonus points — that would be the perfect marker to filter on! But the API does not give you that. So we do it another way:
- When I send a parcel, I add the word “//фонд” to the description, then pull the shipments carrying that word through the API. That works, but only when you send the cargo yourself. And what about when someone sends cargo to you?? Not every supplier is going to write in the description what I ask them to. Or what if I want to pay for a partner’s volunteer parcel? So the approach only takes you so far, and it needs something extra on top.
- In my CRM I have a separate form that lists every parcel. The ones the keyword has caught for certain I flag, and the rest I can tick by hand. That is still far quicker than keying in every waybill number. So the approach works. I really hope the API gets an update and charity parcels become filterable, but for now this is how I do it.
Classifying parcels by category
The report also asks you to state which category every parcel you send falls into — “for the Armed Forces” or “targeted aid”, for instance. You can fill that field in by hand, of course, but why would you, when we have artificial intelligence! Naturally I am not about to train my own neural network or stand up a Llama model on a server 🙂 It is cheaper to do the lot through the OpenAI API.
So we write a function that takes the parcel description, sends it to GPT and gets a category back. If none of the categories fit, GPT suggests one of its own. That way the category fields fill themselves in.
There is a catch, though: the auto-classification can make a mess of it if the parcel description is short or vague. When that happens GPT gets it wrong and a human has to step in. In my experience that is rare — a few parcels out of 50 are no trouble to fix by hand.
The upshot is that the automation saves a lot of time on the monthly report: it is no longer a chore, just a quick read-through with a few small edits!
And now the code itself
import requests
import csv
from io import StringIO
from datetime import datetime
import os
import asyncio
from openai import AsyncOpenAI
import xlmaker
# Конфігураційні змінні
NOVA_POSHTA_API_KEY = "ВАШ_API_КЛЮЧ"
REPORT_TYPE = "csv" # Формат звіту
DATE_FROM = "01.10.2024" # Початок місяця
DATE_TO = "31.10.2024" # Кінець місяця або поточна дата
file_path = '01_БФ Ромашка.xlsx' # Шаблон звіту (еталон)
foundation_name = 'БФ Ваша Назва Фонду'
month_number = datetime.now().month # Номер поточного місяця
# URL API Нової Пошти
url = "https://api.novaposhta.ua/v2.0/json/"
# Ініціалізація клієнта OpenAI
client = AsyncOpenAI(api_key=os.getenv("OPENAI_API_KEY")) # Зберігаю його в реєстрі
# Список категорій для класифікації які вказані в документі XLS
categories = [
"Одяг та взуття", "Продукти (вода)", "Продукти (їжа)", "Медицина",
"Особиста гігієна", "Домашні речі", "Зарядні та комунікаційні пристрої",
"Мілітарі (Тактичне спорядження)", "Мілітарі (Спеціальне обладнання)",
"Мілітарі (Запчасти для техники)", "Товари для дітей", "Будівельні матеріали", "Документи"
]
# Тут аналізуємо опис посилки через OpenAI API
async def get_help_category(description):
prompt = f"""
Яка категорія опису відповідає опису: {description}?
Обери одну з категорій. Якщо жодна на підходить, пиши власну українською мовою {', '.join(categories)}
"""
response = await client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": "Класифікуємо опис посилок по наведеним категоріям"},
{"role": "user", "content": prompt}
],
max_tokens=50,
temperature=0.7,
)
category = response.choices[0].message.content.strip()
return category
# Функція для отримання списку ТТН
def get_ttn_list():
data = {
"apiKey": NOVA_POSHTA_API_KEY,
"modelName": "InternetDocument",
"calledMethod": "getDocumentList",
"methodProperties": {
"DateTimeFrom": DATE_FROM,
"DateTimeTo": DATE_TO,
"Page": "1",
"GetFullList": "1"
}
}
response = requests.post(url, json=data)
if response.status_code == 200:
result = response.json()
if result.get("success"):
documents = result.get("data", [])
return [doc["Ref"] for doc in documents]
return []
# функція для генерації звіту в CSV
async def generate_report(document_refs):
report_data = {
"apiKey": NOVA_POSHTA_API_KEY,
"modelName": "InternetDocument",
"calledMethod": "generateReport",
"methodProperties": {
"DocumentRefs": document_refs,
"Type": REPORT_TYPE,
"DateTime": DATE_TO
}
}
response = requests.post(url, json=report_data)
if response.status_code == 200:
csv_data = response.text
csv_file = StringIO(csv_data)
reader = csv.DictReader(csv_file, delimiter="t")
data = []
for row in reader:
en_number = row['Номер ЕН'.strip().replace('"', '')]
city_sender = row['Місто відправника'.strip().replace('"', '')]
city_receiver = row['Місто одержувача'.strip().replace('"', '')]
creation_date = row['Дата створення документу'.strip().replace('"', '')]
descr_parcel = row['Опис відправлення'.strip().replace('"', '')]
recipient_name = row['Фактичний одержувач'.strip().replace('"', '')]
# Визначення категорії
aid_category = await get_help_category(descr_parcel)
post_link = f"https://novaposhta.ua/tracking/{en_number}"
entry = {
'en_number': en_number,
'sent_date': creation_date,
'sent_city': city_sender,
'recive_city': city_receiver,
'ricive_name': recipient_name,
'aid_category': aid_category,
'problem_solve': 'Вирішення проблеми не вказано',
'postLink': post_link
}
data.append(entry)
return data
return []
async def main():
ttn_refs = get_ttn_list()
if ttn_refs:
data = await generate_report(ttn_refs)
xlmaker.create_and_update_excel_copy(file_path, foundation_name, month_number, data)
else:
print("Немає накладних для звіту.")
if __name__ == "__main__":
asyncio.run(main())
Code language: PHP (php)