О проекте
Команда: prompt
Кейс HackAlem.ai: Логистика — Электрокомплект · ekt.kz
Стадия: Доведённый продукт
Задача кейса: Автоматический расчёт заказов поставщикам для пополнения склада, с утверждением менеджером
Суть
Сервис для закупщика «Электрокомплект»: по Excel-выгрузкам 1С прогнозирует спрос CatBoost и формирует рекомендуемые заказы поставщикам со срочностью; руководитель утверждает или возвращает заказ в мобильном приложении.
Как устроено
pipeline.py читает шесть Excel из 1С, prepare_dataset_v2 строит признаки (лаги 1/2/3/6/12, скользящие среднее и σ, сезонность, month_sin/cos), чистит выбросы (скользящая медиана + 6×MAD) и добавляет упущенный спрос в месяцы без остатка. CatBoost прогнозирует продажи следующего месяца, рекомендация = прогноз + 0,5σ − остаток − в пути с округлением до MOQ, срочность по покрытию. Next.js-сайт показывает таблицу заказов и шаги расчёта, Flutter-приложение даёт руководителю утверждение, FastAPI хранит статусы; отдельный AI-ассистент с tool calling отвечает только по данным расчёта.
Ключевые решения
- CatBoost
- MAD-детекция выбросов
- коррекция stockout/упущенного спроса
- страховой запас 0,5σ
- округление до MOQ
- walk-forward бэктест
- function calling
Особенности
Полный цикл «расчёт → правка закупщиком → утверждение руководителем на телефоне» с ролями и 403/409 на API; прозрачная формула заказа и очистка спроса от разовых отгрузок и пустых складов.
Стек
Python · FastAPI · CatBoost · pandas · Next.js 15 · React 19 · TypeScript · Tailwind CSS · Flutter · Docker Compose · Playwright · Jest
Модели
OpenAI
Проект создан за 5 часов на финале хакатона HackAlem.ai (23 сентября 2026, Астана). Код — BAITC-Hacks/hack-0d09e99e-prompt.
README
README.mdQor — заказы поставщикам для ekt.kz
Qor (қаз. қор — запас). Сервис для менеджера закупа ТОО «Электрокомплект»: по выгрузке из 1С считает, что пополнить на складе, в каком количестве и насколько срочно.
Сейчас это делают вручную в Excel и редко. Из-за этого по одним позициям копится лишний запас, по другим возникает дефицит, а разовая крупная отгрузка искажает регулярную потребность. Qor собирает рекомендацию по каждому артикулу и не отправляет заказ поставщику сам: человек проверяет количество и отдельным шагом утверждает.
Два клиента, один расчёт:
- Сайт — рабочее место закупщика: загрузка выгрузки, таблица заказов, правка количества, карточка артикула с шагами расчёта. В правом нижнем углу чат: он отвечает только цифрами этого расчёта.
- Телефон — согласование. Руководитель видит тот же заказ, график продаж за 12 месяцев, спрашивает «почему столько» и нажимает «Утвердить» или «Вернуть». Поставщику ничего не уходит: после утверждения менеджер выгружает заказ в 1С на сайте.
Как считается заказ
Рабочий расчёт — backend/qor/pipeline.py. Он читает Excel, строит признаки скриптом scripts/prepare_dataset_v2.py и прогоняет модель ml/models/demand_model_v2.cbm (CatBoost, цель — продажи следующего месяца).
На каждый артикул:
рекомендация = прогноз + 0,5 × σ за 6 месяцев − остаток − в пути
Результат округляется вверх до минимальной партии (MOQ). Срочность — покрытие остатком и товаром в пути: меньше 0,5 месяца спроса — «Критично», меньше 1 — «Скоро», иначе «Норма». У каждой строки есть шаги: прогноз, страховой запас, остаток, в пути, округление.
Признаки модели: лаги очищенного спроса 1/2/3/6/12, скользящие среднее и σ, рост за 3 месяца, остаток, календарь (month_sin / month_cos), индекс сезонности, а также лаги выброса, пустого склада и упущенного спроса. Клиентов в расчёте нет: в выгрузке нет персонального идентификатора, крупную отгрузку можно увидеть только по номеру документа.
Та же модель обучена с поправкой на выбросы и пустые месяцы (scripts/prepare_dataset_v2.py, scripts/train_model_v2.py):
- Разовая крупная продажа. По каждому артикулу порог = скользящая медиана за 6 месяцев + 6 × MAD. Если MAD равен нулю, порог = max(4 × медиана, медиана + 10). Месяц выше порога заменяется медианой и не входит в регулярный спрос.
- Упущенный спрос. Месяц без остатка не считается нулевым спросом: оценка потерянных продаж добавляется к очищенному ряду (
demand_adjusted).
Сайт и телефон берут заказ из run_workspace.py: он вызывает этот же расчёт и пишет backend/data/workspace.json.
Технологии
| Часть | Стек |
|---|---|
| Сайт | Next.js 15 (App Router), React 19, TypeScript, Tailwind CSS |
| API согласования | Python, FastAPI, uvicorn. Папка backend/ |
| Модель | CatBoost, pandas. Файл ml/models/demand_model_v2.cbm, сервис ml |
| Телефон | Flutter, riverpod, dio. Тот же REST |
| Данные | Шесть Excel из 1С. Схема полей — docs/06-datasets.md |
Роли: aigerim / buyer — менеджер закупа, daniyar / director — руководитель. Пароль для демо совпадает с ролью. Утверждать и возвращать заказ может только руководитель.
Запуск
Модель ml/models/demand_model_v2.cbm уже в репозитории.
docker compose up --build
Три контейнера: сайт, API и CatBoost. Считать заказ умеет только ml. API сохраняет результат и отдаёт его сайту и телефону.
| Сайт | http://localhost:3000 |
| API | http://localhost:8000/docs |
| Вход | aigerim / buyer, daniyar / director |
Модель наружу не публикуется: API ходит к ней по http://ml:8002. Готовый заказ лежит в volume qor-data. Свою выгрузку 1С можно примонтировать в сервис ml как /data/iek.
Чат в углу сайта — четвёртый процесс, нужен OPENAI_API_KEY в .env:
uvicorn api.main:app --host 127.0.0.1 --port 8001
Сайт зовёт его через POST /api/chat (QOR_AI_API, по умолчанию http://127.0.0.1:8001). Без этого процесса кнопка чата пишет, что ассистент не запущен.
Без Docker
Нужны Python 3.11+ и Node.js 20+.
pip install -r backend/requirements.txt
cd frontend && npm install
- Посчитать заказ. Excel — в
IEK_DATA_DIR(по умолчанию/Users/azamatomirtaj/Documents/IEK): динамика продаж, ежемесячные продажи, ежемесячные остатки, путь, MOQ, сезонность.
cd backend && python3 run_workspace.py
Появится backend/data/workspace.json. В git он не коммитится.
-
API для сайта и телефона:
cd backend && uvicorn main:app --host 0.0.0.0 --port 8000. -
Сайт:
cd frontend && npm run dev— http://localhost:3000 (/import,/orders,/sku/[артикул],/anomalies,/suppliers).
Телефон: cd mobile && flutter pub get && flutter run. Симулятор iOS ходит на http://127.0.0.1:8000, эмулятор Android — на http://10.0.2.2:8000. Реальный телефон в той же сети: flutter run --dart-define=QOR_API=http://<IP компьютера>:8000. Сценарий проверки — mobile/README.md.
Как проверить
python3 backend/run_workspace.pyзавершается без ошибки и пишет число SKU к заказу.curl -s http://127.0.0.1:8000/healthвозвращает"ready": trueи"model": "demand_model_v2.cbm".- На сайте
/ordersесть строки с артикулом, остатком, «в пути», рекомендуемым количеством и бейджем срочности. Раскрытие строки показывает шаги расчёта. - Измените количество в файле «Путь» и пересчитайте: рекомендуемое количество по этому артикулу изменится. Так же на результат влияют остаток и история продаж.
- Войдите в API как
daniyarи откройтеGET /v1/bundle— тот же JSON, что на сайте.POST /v1/orders/iek-current/approveпереводит заказ вapproved. Повторное утверждение — 409. Тем же методом подaigerim— 403. - На телефоне тот же заказ: «Утвердить» или «Вернуть» с комментарием меняет статус, который читает
GET /v1/orders/current. Поставщику запрос не уходит. - Выбросы:
scripts/prepare_dataset_v2.pyпомечаетoutlier_flagи заменяет всплеск медианой. Компенсация пустого склада — колонкаdemand_adjustedв том же скрипте. Эти колонки входят в признакиdemand_model_v2.cbm.
Авторы коммитов
История коммитов
- 2026-09-23 17:59refactor(agent): answer from the same workspace the UI rendersAzamat Omirtay
- 2026-09-23 17:59fix(engine): stop forecasting zero for chronically out-of-stock SKUsAzamat Omirtay
- 2026-09-23 17:58fix(web): sanitize uploaded filenames before writing them to diskAzamat Omirtay
- 2026-09-23 17:54docs: lead the README with Docker and the demo loginsAzamat Omirtay
- 2026-09-23 17:49feat: run the site, API, and CatBoost model in DockerAzamat Omirtay
- 2026-09-23 17:12fix(mobile): show monthly sales on the SKU chartAzamat Omirtay
- 2026-09-23 17:02docs(mobile): add phone check steps for the jurybektaszhaksylyk
- 2026-09-23 16:54feat: buyer auth, director notifications, v2 model, quieter desk UIAzamat Omirtay
Все коммиты (21)
- 2026-09-23 16:09fix(agent): enforce dataset-only responsesHarry
- 2026-09-23 15:59feat(agent): restrict AI responses to QOR datasetHarry
- 2026-09-23 15:51feat(mobile): talk to the Qor API instead of local databektaszhaksylyk
- 2026-09-23 15:47feat(agent): add AI procurement assistant with tool callingHarry
- 2026-09-23 15:33feat(web): split buyer screens out of the route filesAzamat Omirtay
- 2026-09-23 15:33feat(backend): build supplier orders from the trained demand modelAzamat Omirtay
- 2026-09-23 15:33feat(api): add buyer and director login for the mobile approval flowAzamat Omirtay
- 2026-09-23 15:31feat(model): add procurement engine with CatBoost V2 forecastsHarry
- 2026-09-23 15:23feat(model): add demand forecasting with outlier and stockout correctionHarry
- 2026-09-23 14:50feat(model): add CatBoost demand forecasting and procurement recommendationsHarry
- 2026-09-23 14:47feat: split the buyer UI and the order engineAzamat Omirtay
- 2026-09-23 14:46feat(mobile): add Qor approval app on Flutterbektaszhaksylyk
- 2026-09-23 13:57feat: add Qor procurement workspace on IEK 1C dumpsAzamat Omirtay
Структура репозитория (211 файлов)
.dockerignore .gitignore README.md api/__init__.py api/agent.py api/main.py backend/Dockerfile backend/data/README.md backend/main.py backend/pytest.ini backend/qor/__init__.py backend/qor/auth.py backend/qor/contract.py backend/qor/explain.py backend/qor/ml_server.py backend/qor/orders.py backend/qor/pipeline.py backend/requirements-api.txt backend/requirements.txt backend/run_workspace.py backend/tests/conftest.py backend/tests/test_api.py backend/tests/test_auth.py backend/tests/test_contract.py backend/tests/test_orders.py data/processed/backtest_results.csv data/processed/recommended_orders.csv data/processed/recommended_orders_v2.csv data/processed/recommended_orders_v2.json data/processed/training_dataset.csv data/processed/training_dataset_v2.csv data/processed/validation_predictions.csv data/processed/validation_predictions_v2.csv data/processed/walk_forward_results.csv data/processed/walk_forward_v2_results.csv docker-compose.yml docs/01-pain-point.md docs/02-stitch-web-prompt.md docs/03-stitch-mobile-prompt.md docs/04-design-references.md docs/05-tech-stack.md docs/06-datasets.md docs/07-roles-and-screens.md frontend/.dockerignore frontend/Dockerfile frontend/__tests__/AssistantChat.test.tsx frontend/__tests__/DirectorHome.test.tsx frontend/__tests__/LoginScreen.test.tsx frontend/__tests__/StatusBadge.test.tsx frontend/__tests__/catalog.test.ts frontend/__tests__/import-safeName.test.ts frontend/app/anomalies/page.tsx frontend/app/api/auth/login/route.ts frontend/app/api/auth/logout/route.ts frontend/app/api/auth/me/route.ts frontend/app/api/chat/route.ts frontend/app/api/import/route.ts frontend/app/api/orders/current/route.ts frontend/app/api/orders/submit/route.ts frontend/app/api/workspace/route.ts frontend/app/globals.css frontend/app/icon.svg frontend/app/import/page.tsx frontend/app/layout.tsx frontend/app/orders/page.tsx frontend/app/page.tsx frontend/app/sku/[code]/page.tsx frontend/app/suppliers/page.tsx frontend/components/AppShell.tsx frontend/components/AssistantChat.tsx frontend/components/DirectorHome.tsx frontend/components/ForecastChart.tsx frontend/components/Icon.tsx frontend/components/ImportPanel.tsx frontend/components/KpiCard.tsx frontend/components/LoginScreen.tsx frontend/components/Logo.tsx frontend/components/OrdersWorkspace.tsx frontend/components/SessionProvider.tsx frontend/components/SettingsDrawer.tsx frontend/components/Sidebar.tsx frontend/components/StatusBadge.tsx frontend/components/WorkspaceProvider.tsx frontend/data/catalog.ts frontend/data/workspace.ts frontend/e2e/auth.spec.ts frontend/e2e/director.spec.ts frontend/e2e/helpers.ts frontend/e2e/order-flow.spec.ts frontend/e2e/orders.spec.ts frontend/jest.config.mjs frontend/jest.setup.ts frontend/next-env.d.ts frontend/next.config.ts frontend/package-lock.json frontend/package.json frontend/playwright.config.ts frontend/postcss.config.mjs frontend/screens/Anomalies.tsx frontend/screens/Dashboard.tsx frontend/screens/Sku.tsx frontend/screens/Suppliers.tsx frontend/server/import.ts frontend/server/qor.ts frontend/server/upload-name.ts frontend/server/workspace.ts frontend/tailwind.config.ts frontend/tsconfig.json ml/Dockerfile ml/models/demand_model_v2.cbm mobile/.gitignore mobile/.metadata mobile/README.md mobile/analysis_options.yaml mobile/android/.gitignore mobile/android/app/build.gradle.kts mobile/android/app/src/debug/AndroidManifest.xml mobile/android/app/src/main/AndroidManifest.xml mobile/android/app/src/main/kotlin/kz/ekt/qor_mobile/MainActivity.kt mobile/android/app/src/main/res/drawable-v21/launch_background.xml mobile/android/app/src/main/res/drawable/launch_background.xml mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png mobile/android/app/src/main/res/values-night/styles.xml mobile/android/app/src/main/res/values/styles.xml mobile/android/app/src/profile/AndroidManifest.xml mobile/android/build.gradle.kts mobile/android/gradle.properties mobile/android/gradle/wrapper/gradle-wrapper.properties mobile/android/settings.gradle.kts mobile/ios/.gitignore mobile/ios/Flutter/AppFrameworkInfo.plist mobile/ios/Flutter/Debug.xcconfig mobile/ios/Flutter/Release.xcconfig mobile/ios/Runner.xcodeproj/project.pbxproj mobile/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme mobile/ios/Runner.xcworkspace/contents.xcworkspacedata mobile/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist mobile/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings mobile/ios/Runner/AppDelegate.swift mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected] mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected] mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected] mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected] mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected] mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected] mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected] mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected] mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected] mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected] mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected] mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected] mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected] mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected] mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/[email protected] mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/[email protected] mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/[email protected] mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md mobile/ios/Runner/Base.lproj/LaunchScreen.storyboard mobile/ios/Runner/Base.lproj/Main.storyboard mobile/ios/Runner/Info.plist mobile/ios/Runner/Runner-Bridging-Header.h mobile/ios/RunnerTests/RunnerTests.swift mobile/lib/data/format.dart mobile/lib/data/models.dart mobile/lib/data/providers.dart mobile/lib/data/repository.dart mobile/lib/data/why.dart mobile/lib/main.dart mobile/lib/screens/chat_screen.dart mobile/lib/screens/home_screen.dart mobile/lib/screens/lock_screen_push.dart mobile/lib/screens/login_screen.dart mobile/lib/screens/notifications_screen.dart mobile/lib/screens/order_screen.dart mobile/lib/screens/profile_screen.dart mobile/lib/screens/sku_screen.dart mobile/lib/theme/app_theme.dart mobile/lib/theme/tokens.dart mobile/lib/widgets/common.dart mobile/lib/widgets/mini_chart.dart mobile/lib/widgets/position_card.dart mobile/pubspec.lock mobile/pubspec.yaml mobile/test/fake_repository.dart mobile/test/live_api_test.dart mobile/test/screens_smoke_test.dart package-lock.json package.json scripts/backtest.py scripts/generate_orders.py scripts/generate_orders_v2.py scripts/hourly-push.sh scripts/inspect_data.py scripts/prepare_dataset.py scripts/prepare_dataset_v2.py scripts/train_model.py scripts/train_model_v2.py scripts/walk_forward_backtest.py scripts/walk_forward_v2.py tests/test_agent.py tests/test_sku_series.py
Обсуждение 0 комментариев
Войдите, чтобы комментироватьПока нет комментариев — будьте первым.