Дворниченко Михайло

Blog

Автоматизація звітів для “Гуманітарна Нова Пошта”

Компанія Нова Пошта запустила програму “Гуманітарна пошта країни”, яка дозволяє благодійним фондам безкоштовно відправляти вантажі. Програма працює й досі і велика вдячність їм за це!

А тепер про технічну сторону. Для тих, хто автоматизує свою роботу, та формує звіти для цієї програми, – ось рецепт, як це зробити простіше. Якщо відправлень мало, питання автоматизації не піднімається, ви ручками все швидко можете зробити, але коли посилок стає багато, починається головний біль! Як завжди я підготував каркас коду, який допоможе автоматизувати формування щомісячного звіту. Єдине, вам лишається тільки адаптувати цей код під свою CRM. Можете зробити Flask API сервіс.

Поїхали!

Нова Пошта надає готовий шаблон звіту у форматі XLSX і це класно! Звичайно, якби я створював великий сервіс масової генерації звітів, то програмно генерував би новий документ xlsx. Але оскільки ця автоматизація розрахована під вузьку задачу з обмеженою кількістю користувачів, і звіт формується лише один раз на місяць, тут працює мій “лінивий підхід”. Я просто беру готовий файл шаблону, роблю його копію та додаю в нього потрібні рядки.

Такий підхід дозволяє не морочитися програмно з версткою xlsx-документа.

Фільтрація благодійних відправлень

В процесі роботи з’явилася питання/задача, як відрізнити благодійні посилки від тих, що відправляються в рамках моєї приватної діяльності? Спершу я думав, що можна буде через API Нової Пошти побачити, якщо посилка сплачена бонусами, тоді це ідеальний маркер і по ньому фільтрувати! Але API таких даних не дає. Тому робимо інше рішення:

  1. При відправленні посилки, до опису посилки я додаю слово “//фонд” і далі, через API відбираю відправлення за цим словом. Це працює, але тільки коли ти відправляєш вантаж самостійно. А коли вантаж відправляють тобі?? Не всі постачальники будуть писати в описі те, що я прошу. Або, якщо я хочу сплатити волонтерську посилку партнерів? Тому такий підхід працює частково і необхідно додати додатковий функционал.
  2. В своїй 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)

Leave a Reply

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *