Автоматизация отчётов для «Гуманитарной Нова Пошта»
Компания Нова Пошта запустила программу «Гуманитарная почта страны»: благотворительные фонды могут отправлять грузы бесплатно. Программа работает до сих пор, и за это им большое спасибо!
А теперь о технической стороне. Тем, кто автоматизирует свою работу и делает отчёты по этой программе, — вот рецепт, как сделать это проще. Пока отправлений мало, об автоматизации никто и не думает: руками всё делается быстро. Но когда посылок становится много, начинается головная боль! Как всегда, я набросал каркас кода, который поможет автоматизировать ежемесячный отчёт. Останется только приспособить его под свою CRM. Можете сделать Flask API-сервис.
Поехали!
Нова Пошта даёт готовый шаблон отчёта в XLSX, и это здорово! Конечно, если бы я делал большой сервис массовой генерации отчётов, то собирал бы новый xlsx-документ программно. Но задача тут узкая, пользователей немного, а отчёт формируется всего раз в месяц — здесь работает мой «ленивый подход». Я просто беру готовый файл шаблона, делаю копию и дописываю в неё нужные строки.
Так не приходится возиться с вёрсткой xlsx-документа в коде.
Фильтрация благотворительных отправлений
По ходу дела возник вопрос: как отличить благотворительные посылки от тех, что я отправляю по своим частным делам? Сначала я надеялся, что через API Нова Пошта будет видно, оплачена посылка бонусами или нет: если да — идеальный маркер, по нему и фильтруем! Но таких данных API не отдаёт. Значит, решаем иначе:
- Отправляя посылку, я дописываю в её описание слово «//фонд», а потом через API отбираю отправления по этому слову. Работает, но только когда груз отправляешь ты сам. А когда груз отправляют тебе?? Не каждый поставщик напишет в описании то, что я прошу. Или, скажем, я хочу оплатить волонтёрскую посылку партнёров? Так что подход работает лишь наполовину, и нужно дописывать дополнительный функционал.
- В своей CRM я делаю отдельную форму со списком всех посылок. Те, что точно отобрались по ключевому слову, помечаю галочкой, а остальные могу отметить вручную. Но это намного быстрее, чем вбивать каждую ТТН руками. Так что подход рабочий. Очень надеюсь, что API доработают и благотворительные посылки можно будет отфильтровать сразу, но пока работаю так.
Классификация посылок по категориям
А ещё в отчёте нужно указать категорию, к которой относится каждая отправленная посылка, например «для ВСУ» или «целевая помощь». Заполнять это поле можно и вручную, но зачем, когда есть искусственный интеллект! Собственную нейросеть я, ясное дело, обучать не буду и Llama на сервере разворачивать не стану 🙂 Дешевле сделать всё через OpenAI API.
Пишем функцию: она берёт описание посылки, отправляет его в GPT и получает категорию. Если не подходит ни одна, GPT предложит свой вариант. Так поля категорий заполняются автоматически.
Но риски есть: к сожалению, автоклассификация может «накосячить», если описание посылки короткое или невнятное. Тогда GPT ошибается и нужно вмешательство человека. Хотя по моему опыту такое бывает редко — несколько посылок из 50 не проблема поправить руками.
В итоге такая автоматизация здорово экономит время на ежемесячном отчёте: это уже не рутина, а простая проверка с минимальными правками!
А теперь сам код
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)