Production-ready бухгалтерский модуль для малого и среднего бизнеса с полным соблюдением принципов двойной записи, автоматизированной обработкой документов через OCR и асинхронной архитектурой.
- Возможности
- Технологический стек
- Архитектура
- Быстрый старт
- API Endpoints
- База данных
- Тестирование
- Разработка
- Production Deployment
- План счетов с типами (актив/пассив/доход/расход/капитал)
- Двойная запись с жесткой валидацией балансировки дебета и кредита
- Проводки с привязкой к первичным документам
- Финансовые отчеты:
- Оборотно-сальдовая ведомость (ОСВ)
- Баланс по счетам за период
- Обороты с группировкой по счетам
- OCR документов - автоматическое распознавание текста из PDF/изображений (Tesseract)
- Асинхронная обработка - фоновая очередь задач через Celery + RabbitMQ
- Отслеживание статусов - PENDING → PROCESSED → FAILED
- Structured logging - JSON-логи для ELK/Grafana
- Docker Compose - полная инфраструктура в контейнерах
- 100% покрытие тестами - интеграционные и unit-тесты
- Alembic миграции - версионирование схемы БД
- Type hints - полная типизация кода
- OpenAPI/Swagger - автогенерируемая документация API
| Компонент | Технология | Версия | Назначение |
|---|---|---|---|
| Web Framework | FastAPI | 0.120.4 | Async REST API |
| ORM | SQLAlchemy | 2.0.44 | Async database layer |
| DB Driver | asyncpg | 0.30.0 | PostgreSQL async driver |
| Validation | Pydantic | 2.12.3 | Data validation & serialization |
| JSON | orjson | 3.10.15 | Fast JSON encoding |
| Logging | structlog | 25.5.0 | Structured JSON logs |
| Компонент | Технология | Версия | Назначение |
|---|---|---|---|
| Database | PostgreSQL | 15 | Relational data storage |
| Task Queue | Celery | 5.5.3 | Async task processing |
| Message Broker | RabbitMQ | 3-management | Task queue broker |
| Cache | Redis | 7 | Celery backend & caching |
| Migrations | Alembic | 1.17.1 | Database versioning |
| Компонент | Технология | Назначение |
|---|---|---|
| OCR Engine | Tesseract | Text recognition |
| PDF Processing | pdf2image | PDF to image conversion |
| Image Processing | Pillow | Image manipulation |
| Компонент | Технология | Назначение |
|---|---|---|
| Testing | pytest | 8.4.2 |
| Async Tests | pytest-asyncio | 1.2.0 |
| HTTP Client | httpx | 0.28.1 |
| Coverage | pytest-cov | 7.0.0 |
MiniERP/
├── accounting/ # Основное приложение
│ ├── api/ # REST API endpoints
│ │ ├── documents.py # Загрузка документов
│ │ ├── entries.py # Бухгалтерские проводки
│ │ └── reports.py # Финансовые отчеты
│ ├── core/ # Ядро приложения
│ │ ├── config.py # Настройки (Pydantic Settings)
│ │ ├── database.py # Async engine, sessions
│ │ └── dependencies.py # FastAPI dependencies
│ ├── models/ # SQLAlchemy ORM модели
│ │ ├── account.py # План счетов
│ │ ├── document.py # Первичные документы
│ │ └── entry.py # Проводки и строки проводок
│ ├── schemas/ # Pydantic схемы
│ │ ├── document.py # DTO для документов
│ │ ├── entry.py # DTO для проводок
│ │ └── report.py # DTO для отчетов
│ ├── services/ # Бизнес-логика
│ │ ├── entry_service.py # Создание проводок
│ │ └── report_service.py # Генерация отчетов
│ ├── tasks/ # Celery задачи
│ │ └── ocr.py # OCR processing
│ ├── utils/ # Утилиты
│ │ └── ocr.py # OCR helpers
│ └── tests/ # Тесты
│ ├── conftest.py # Pytest fixtures
│ ├── test_documents.py # Тесты загрузки
│ ├── test_entries.py # Тесты проводок
│ └── test_reports.py # Тесты отчетов
├── migrations/ # Alembic миграции
│ ├── env.py # Alembic environment
│ └── versions/ # История миграций
│ ├── 20240101_0001_*.py # Создание таблиц
│ ├── 20240101_0002_*.py # Внешние ключи
│ └── 20240101_0003_*.py # Расширение precision
├── worker/ # Celery worker
│ └── celery_worker.py # Worker configuration
├── data/ # Runtime данные
│ ├── media/ # Загруженные документы
│ └── ocr_tmp/ # Временные файлы OCR
├── docker-compose.yml # Инфраструктура
├── Dockerfile # API/Worker image
├── alembic.ini # Alembic config
├── pyproject.toml # Python dependencies
└── requirements.txt # Pip freeze
┌─────────────────┐
│ Client/UI │
└────────┬────────┘
│ HTTP/REST
▼
┌─────────────────┐ ┌──────────────┐
│ FastAPI API │◄────►│ PostgreSQL │
│ (uvicorn) │ │ Database │
└────────┬────────┘ └──────────────┘
│
│ Celery Tasks
▼
┌─────────────────┐ ┌──────────────┐
│ Celery Worker │◄────►│ RabbitMQ │
│ (OCR Process) │ │ Broker │
└─────────────────┘ └──────────────┘
│
▼
┌──────────────┐
│ Redis │
│ Backend │
└──────────────┘
- Docker 24.0+
- Docker Compose 2.0+
- 4GB RAM минимум
# Клонировать репозиторий
git clone <repository-url>
cd MiniERP
# Создать .env файл (опционально, есть значения по умолчанию)
cat > .env << EOF
ENVIRONMENT=development
DATABASE_URL=postgresql+asyncpg://postgres:postgres@db:5432/accounting
CELERY_BROKER_URL=amqp://guest:guest@rabbitmq:5672//
CELERY_RESULT_BACKEND=redis://redis:6379/0
EOF
docker compose up -d
# Дождаться готовности всех сервисов (30-60 секунд)
docker compose ps# Автоматически применятся при старте API, или вручную:
docker compose exec api alembic upgrade head# Health check
curl http://localhost:8001/health/
# Ожидаемый ответ: {"status":"ok"}
# Swagger документация
open http://localhost:8001/docs
# RabbitMQ Management UI
open http://localhost:15673/
# Login: guest / guest# Все тесты
docker compose exec api pytest -v
# С coverage отчетом
docker compose exec api pytest --cov=accounting --cov-report=html
# Только определенный модуль
docker compose exec api pytest accounting/tests/test_entries.py -v- Swagger UI: http://localhost:8001/docs
- ReDoc: http://localhost:8001/redoc
- OpenAPI Schema: http://localhost:8001/openapi.json
GET /health/Ответ:
{
"status": "ok"
}POST /entries/
Content-Type: application/jsonТело запроса:
{
"date": "2025-11-02",
"description": "Оплата от клиента",
"lines": [
{
"account_id": 1,
"debit": "1000.00",
"credit": "0"
},
{
"account_id": 2,
"debit": "0",
"credit": "1000.00"
}
],
"document_id": null
}Ответ (201 Created):
{
"id": 1,
"date": "2025-11-02",
"description": "Оплата от клиента",
"document_id": null,
"created_at": "2025-11-02T10:30:00Z",
"lines": [
{
"id": 1,
"account_id": 1,
"debit": "1000.00",
"credit": "0.00"
},
{
"id": 2,
"account_id": 2,
"debit": "0.00",
"credit": "1000.00"
}
]
}Валидация:
- Дебет должен равняться кредиту (Dt = Kt)
- Минимум 2 строки в проводке
- Все account_id должны существовать
Ошибки:
// 400 Bad Request
{
"detail": "Debits must equal credits: debit=1000.00, credit=500.00"
}POST /documents/upload/
Content-Type: multipart/form-dataПараметры:
file: файл (PDF/PNG/JPG, max 10MB)
Ответ (201 Created):
{
"id": 1,
"file_path": "documents/2025/11/02/invoice_123.pdf",
"file_type": "application/pdf",
"uploaded_at": "2025-11-02T10:35:00Z",
"status": "PENDING"
}Статусы обработки:
PENDING- в очереди на обработкуPROCESSED- успешно обработанFAILED- ошибка при обработке
Пример с curl:
curl -X POST http://localhost:8001/documents/upload/ \
-F "file=@invoice.pdf"GET /reports/balance/?date_from=2025-11-01&date_to=2025-11-30Параметры:
date_from: начало периода (YYYY-MM-DD)date_to: конец периода (YYYY-MM-DD)
Ответ (200 OK):
{
"date_from": "2025-11-01",
"date_to": "2025-11-30",
"items": [
{
"account_id": 1,
"account_code": "101",
"account_name": "Касса",
"opening_balance": "0",
"debit": "5000.00",
"credit": "3000.00",
"closing_balance": "2000.00"
},
{
"account_id": 2,
"account_code": "600",
"account_name": "Расходы",
"opening_balance": "1000.00",
"debit": "3000.00",
"credit": "2000.00",
"closing_balance": "2000.00"
}
],
"total_debit": "8000.00",
"total_credit": "5000.00"
}GET /reports/turnover/?date_from=2025-11-01&date_to=2025-11-30Параметры:
date_from: начало периодаdate_to: конец периода
Ответ (200 OK):
{
"date_from": "2025-11-01",
"date_to": "2025-11-30",
"items": [
{
"account_id": 1,
"account_code": "101",
"account_name": "Касса",
"opening_debit": "0",
"opening_credit": "0",
"turnover_debit": "5000.00",
"turnover_credit": "3000.00",
"closing_debit": "5000.00",
"closing_credit": "3000.00"
}
],
"total_turnover_debit": "5000.00",
"total_turnover_credit": "3000.00"
}-- План счетов
CREATE TABLE accounts (
id SERIAL PRIMARY KEY,
code VARCHAR(32) UNIQUE NOT NULL,
name VARCHAR(255) NOT NULL,
type account_type NOT NULL, -- ASSET, LIABILITY, EQUITY, REVENUE, EXPENSE
is_active BOOLEAN NOT NULL DEFAULT true
);
-- Первичные документы
CREATE TABLE documents (
id SERIAL PRIMARY KEY,
file_path VARCHAR(512) NOT NULL,
file_type VARCHAR(128) NOT NULL,
uploaded_at TIMESTAMPTZ NOT NULL DEFAULT now(),
status document_status NOT NULL -- PENDING, PROCESSED, FAILED
);
-- Бухгалтерские проводки
CREATE TABLE entries (
id SERIAL PRIMARY KEY,
date DATE NOT NULL,
description VARCHAR(512) NOT NULL,
document_id INTEGER REFERENCES documents(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Строки проводок (двойная запись)
CREATE TABLE entry_lines (
id SERIAL PRIMARY KEY,
entry_id INTEGER NOT NULL REFERENCES entries(id) ON DELETE CASCADE,
account_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE RESTRICT,
debit NUMERIC(12,2) NOT NULL CHECK (debit >= 0),
credit NUMERIC(12,2) NOT NULL CHECK (credit >= 0)
);CREATE INDEX ix_entries_date ON entries(date);
CREATE INDEX ix_entries_document_id ON entries(document_id);
CREATE INDEX ix_entry_lines_account_id ON entry_lines(account_id);
CREATE INDEX ix_documents_status ON documents(status);# Создать новую миграцию
docker compose exec api alembic revision --autogenerate -m "Description"
# Применить миграции
docker compose exec api alembic upgrade head
# Откатить последнюю миграцию
docker compose exec api alembic downgrade -1
# Показать текущую версию
docker compose exec api alembic current
# История миграций
docker compose exec api alembic history# Все тесты
docker compose exec api pytest
# С подробным выводом
docker compose exec api pytest -v
# Только определенный файл
docker compose exec api pytest accounting/tests/test_entries.py
# Только один тест
docker compose exec api pytest accounting/tests/test_entries.py::test_create_entry_success
# С покрытием кода
docker compose exec api pytest --cov=accounting --cov-report=term-missing
# HTML отчет о покрытии
docker compose exec api pytest --cov=accounting --cov-report=html
# Открыть: htmlcov/index.html# accounting/tests/conftest.py - общие fixtures
@pytest.fixture
async def engine_for_tests() -> AsyncEngine:
"""Отдельный engine для каждого теста"""
@pytest.fixture
async def db_session(engine_for_tests) -> AsyncSession:
"""Тестовая сессия БД"""
@pytest.fixture
async def client(app, engine_for_tests) -> AsyncClient:
"""HTTP клиент с dependency overrides"""
# accounting/tests/test_entries.py
async def test_create_entry_success(client, db_session):
"""Проверка создания валидной проводки"""
async def test_create_entry_validation_error(client, db_session):
"""Проверка валидации Dt = Kt"""
# accounting/tests/test_reports.py
async def test_balance_report(client, db_session):
"""Проверка оборотно-сальдовой ведомости"""
async def test_turnover_report(client, db_session):
"""Проверка отчета по оборотам"""
# accounting/tests/test_documents.py
async def test_document_upload_triggers_task(client):
"""Проверка загрузки документа и запуска Celery task"""Текущее покрытие: 100% всех критичных модулей:
- API endpoints: 100%
- Services: 100%
- Models: 100%
- Schemas: 100%
# Установить зависимости для разработки
pip install -r requirements.txt
pip install -e .
# Установить pre-commit hooks
pre-commit install
# Запустить pre-commit на всех файлах
pre-commit run --all-files
# Форматирование кода
black accounting/
ruff check accounting/ --fix
# Проверка типов
mypy accounting/# .env файл
ENVIRONMENT=development # development/production
DATABASE_URL=postgresql+asyncpg://user:pass@host:port/db
CELERY_BROKER_URL=amqp://guest:guest@rabbitmq:5672//
CELERY_RESULT_BACKEND=redis://redis:6379/0
# Для production
SECRET_KEY=your-secret-key-here
ALLOWED_HOSTS=api.example.com,www.example.com# Structured JSON logs
import structlog
logger = structlog.get_logger(__name__)
logger.info("entry_created", entry_id=entry.id, amount=total)
logger.error("validation_failed", error=str(e), payload=data)
# Вывод:
# {"event": "entry_created", "entry_id": 123, "amount": "1000.00", "timestamp": "..."}# 1. Создать схему в accounting/schemas/
class NewFeatureCreate(BaseModel):
name: str
value: Decimal
class NewFeatureRead(NewFeatureCreate):
id: int
created_at: datetime
# 2. Создать модель в accounting/models/
class NewFeature(Base):
__tablename__ = "new_features"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(255))
value: Mapped[Decimal] = mapped_column(Numeric(12, 2))
# 3. Создать миграцию
docker compose exec api alembic revision --autogenerate -m "add new_features table"
docker compose exec api alembic upgrade head
# 4. Создать сервис в accounting/services/
class NewFeatureService:
def __init__(self, session: AsyncSession):
self.session = session
async def create(self, data: NewFeatureCreate) -> NewFeature:
feature = NewFeature(**data.model_dump())
self.session.add(feature)
await self.session.flush()
await self.session.commit()
return feature
# 5. Создать endpoint в accounting/api/
@router.post("/", response_model=NewFeatureRead)
async def create_feature(
payload: NewFeatureCreate,
session: AsyncSession = Depends(get_db)
) -> NewFeatureRead:
service = NewFeatureService(session)
feature = await service.create(payload)
return NewFeatureRead.model_validate(feature)
# 6. Зарегистрировать router в accounting/main.py
from accounting.api import new_features
app.include_router(new_features.router, prefix="/new-features", tags=["NewFeatures"])
# 7. Написать тесты в accounting/tests/test_new_features.py
async def test_create_feature(client):
response = await client.post("/new-features/", json={
"name": "test",
"value": "100.00"
})
assert response.status_code == 201# Собрать production image
docker build -t minierp:latest .
# Запустить с production настройками
docker run -d \
--name minierp-api \
-p 8000:8000 \
-e ENVIRONMENT=production \
-e DATABASE_URL=postgresql+asyncpg://... \
minierp:latest# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: minierp-api
spec:
replicas: 3
selector:
matchLabels:
app: minierp-api
template:
metadata:
labels:
app: minierp-api
spec:
containers:
- name: api
image: minierp:latest
ports:
- containerPort: 8000
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: minierp-secrets
key: database-url
- name: ENVIRONMENT
value: "production"
livenessProbe:
httpGet:
path: /health/
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health/
port: 8000
initialDelaySeconds: 10
periodSeconds: 5
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
name: minierp-api
spec:
selector:
app: minierp-api
ports:
- protocol: TCP
port: 80
targetPort: 8000
type: LoadBalancer# /etc/nginx/sites-available/minierp
upstream minierp {
server 127.0.0.1:8001;
# Для multiple workers:
# server 127.0.0.1:8001;
# server 127.0.0.1:8002;
# server 127.0.0.1:8003;
}
server {
listen 80;
server_name api.minierp.example.com;
# Redirect HTTP to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name api.minierp.example.com;
ssl_certificate /etc/letsencrypt/live/api.minierp.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.minierp.example.com/privkey.pem;
client_max_body_size 10M;
location / {
proxy_pass http://minierp;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /static/ {
alias /var/www/minierp/static/;
expires 30d;
}
}# /etc/systemd/system/minierp-api.service
[Unit]
Description=MiniERP FastAPI Application
After=network.target postgresql.service redis.service
[Service]
Type=notify
User=minierp
Group=minierp
WorkingDirectory=/opt/minierp
Environment="PATH=/opt/minierp/venv/bin"
Environment="DATABASE_URL=postgresql+asyncpg://..."
ExecStart=/opt/minierp/venv/bin/uvicorn accounting.main:app \
--host 0.0.0.0 \
--port 8000 \
--workers 4 \
--log-config logging.json
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target# Запустить сервис
sudo systemctl daemon-reload
sudo systemctl enable minierp-api
sudo systemctl start minierp-api
sudo systemctl status minierp-api
# Логи
journalctl -u minierp-api -f# /etc/systemd/system/minierp-worker.service
[Unit]
Description=MiniERP Celery Worker
After=network.target rabbitmq-server.service redis.service
[Service]
Type=forking
User=minierp
Group=minierp
WorkingDirectory=/opt/minierp
Environment="PATH=/opt/minierp/venv/bin"
ExecStart=/opt/minierp/venv/bin/celery -A worker.celery_worker worker \
--loglevel=info \
--concurrency=4 \
--pidfile=/var/run/minierp-worker.pid
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target# Интеграция с Prometheus
from prometheus_fastapi_instrumentator import Instrumentator
app = FastAPI()
Instrumentator().instrument(app).expose(app)
# Метрики доступны на /metrics# prometheus.yml
scrape_configs:
- job_name: "minierp-api"
static_configs:
- targets: ["localhost:8000"]
metrics_path: "/metrics"
scrape_interval: 15s# Ежедневный бэкап через cron
# /etc/cron.daily/minierp-backup
#!/bin/bash
BACKUP_DIR=/backups/minierp
DATE=$(date +%Y%m%d_%H%M%S)
pg_dump -U postgres accounting | gzip > $BACKUP_DIR/backup_$DATE.sql.gz
# Удалить бэкапы старше 30 дней
find $BACKUP_DIR -name "backup_*.sql.gz" -mtime +30 -delete# accounting/core/security.py
from fastapi import Security, HTTPException
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
security = HTTPBearer()
async def verify_token(credentials: HTTPAuthorizationCredentials = Security(security)):
"""Проверка JWT токена"""
if credentials.credentials != "valid-token":
raise HTTPException(status_code=401, detail="Invalid token")
return credentials.credentials
# В endpoint:
@router.get("/")
async def protected_route(token: str = Depends(verify_token)):
return {"message": "Authorized"}# Docker
docker compose logs -f api # Логи API
docker compose logs -f worker # Логи Celery worker
docker compose exec api bash # Зайти в контейнер
docker compose restart api # Перезапустить API
docker compose down -v # Остановить и удалить volumes
# PostgreSQL
docker compose exec db psql -U postgres -d accounting
# SELECT * FROM entries LIMIT 10;
# \dt -- Список таблиц
# \d entries -- Описание таблицы
# Redis
docker compose exec redis redis-cli
# KEYS *
# GET key_name
# RabbitMQ
docker compose exec rabbitmq rabbitmqctl list_queues
docker compose exec rabbitmq rabbitmqctl list_exchanges
# Python
docker compose exec api python -m accounting.main
docker compose exec api python -c "from accounting.core.database import engine; print(engine)"- Python 3.11+
- Black formatter (line length 120)
- Ruff linter
- Type hints обязательны
- Docstrings в формате Google
- Fork репозитория
- Создать feature branch (
git checkout -b feature/amazing-feature) - Сделать изменения и commit (
git commit -m 'Add amazing feature') - Push в branch (
git push origin feature/amazing-feature) - Открыть Pull Request
- Все тесты проходят
- Coverage не снижается
- Pre-commit hooks пройдены
- Документация обновлена
- Changelog обновлен
MIT License - see LICENSE file for details
- OCR работает только с русским и английским языками (можно добавить другие через Tesseract language packs)
- Максимальный размер файла для загрузки - 10MB (настраивается в config.py)
- При больших объемах данных (>100k проводок) рекомендуется партиционирование таблиц по дате
- Добавить аутентификацию (JWT)
- Реализовать RBAC (Role-Based Access Control)
- Добавить экспорт отчетов в Excel/PDF
- Интеграция с внешними банками (API банков)
- GraphQL API в дополнение к REST
- Websocket для real-time обновлений
- Grafana dashboards для мониторинга
- Автоматическое создание проводок из распознанных документов
Сделано с используя FastAPI и современный Python stack
docker compose exec api alembic upgrade head
docker compose exec api pytest -v
open http://localhost:8001/docs
## Качество и CI
- `docker compose exec api ruff check .`
- `docker compose exec api mypy .`
- `docker compose exec api pytest --cov=accounting`
- GitHub Actions: линтеры + тесты на pull request
## Переменные окружения
Задаются через `.env` (см. `.env.example`): URL базы данных, брокеры Celery/Redis, каталоги медиа и временных файлов OCR.
## Celery & OCR
- worker `minierp-worker` обрабатывает очередь `ocr`
- OCR: pytesseract + pdf2image + Pillow
- После успешного распознавания создаётся бухгалтерская проводка, документ помечается `processed`
## Полезные команды
```bash
# Запустить воркер вручную
docker compose exec worker celery -A worker.celery_worker.celery_app worker --loglevel=info
# Создать новую миграцию
docker compose exec api alembic revision --autogenerate -m "describe change"
# Запустить линтеры локально
docker compose exec api pre-commit run --all-files