1975 lines
89 KiB
Markdown
1975 lines
89 KiB
Markdown
# Обзор проекта Invoice Az
|
||
|
||
## Оглавление
|
||
1. [Общая информация](#общая-информация)
|
||
2. [Архитектура проекта](#архитектура-проекта)
|
||
3. [Технологический стек](#технологический-стек)
|
||
4. [Структура файлов](#структура-файлов)
|
||
5. [Основные модули](#основные-модули)
|
||
6. [DocTypes](#doctypes)
|
||
7. [API интеграция](#api-интеграция)
|
||
8. [Рабочие процессы](#рабочие-процессы)
|
||
9. [Установка и настройка](#установка-и-настройка)
|
||
10. [Разработка](#разработка)
|
||
|
||
---
|
||
|
||
## Общая информация
|
||
|
||
**Название проекта:** Invoice Az (Invoices Azerbaijan)
|
||
**Версия:** 0.0.1
|
||
**Автор:** Jey ERP (info@jeyerp.az)
|
||
**Лицензия:** Unlicense (Общественное достояние)
|
||
**Требования:** Python 3.10+
|
||
|
||
### Назначение
|
||
|
||
Invoice Az - это приложение интеграции для Frappe/ERPNext, которое обеспечивает двустороннюю синхронизацию с государственной системой электронного налогообложения Азербайджана E-Taxes (https://new.e-taxes.gov.az).
|
||
|
||
### Основные возможности
|
||
|
||
- **Импорт счетов-фактур** - автоматический импорт входящих счетов от поставщиков
|
||
- **Экспорт счетов-фактур** - отправка исходящих счетов клиентам через E-Taxes
|
||
- **Управление справочниками** - синхронизация товаров, единиц измерения, контрагентов
|
||
- **Интеллектуальное сопоставление** - автоматическое сопоставление данных между системами с точностью 95%
|
||
- **ASAN аутентификация** - интеграция с государственной системой аутентификации ASAN Login
|
||
- **Массовые операции** - обработка до 200 счетов за один запрос
|
||
- **Отслеживание и аудит** - полная история операций и связей между документами
|
||
|
||
---
|
||
|
||
## Архитектура проекта
|
||
|
||
### Структура директорий
|
||
|
||
```
|
||
/home/frappe/frappe-bench/apps/invoice_az/
|
||
├── README.md # Краткая документация
|
||
├── claude.md # Подробная документация на русском (34 КБ)
|
||
├── QA_SEND_INVOICE.md # Q&A по отправке счетов
|
||
├── pyproject.toml # Конфигурация Python проекта
|
||
├── license.txt # Лицензия
|
||
├── invoice_az/ # Основная директория приложения
|
||
│ ├── api.py # API для закупок (4,320 строк)
|
||
│ ├── auth.py # ASAN аутентификация (968 строк)
|
||
│ ├── sales_api.py # API для продаж (887 строк)
|
||
│ ├── send_sales_api.py # Отправка счетов (730 строк)
|
||
│ ├── hooks.py # Интеграция с Frappe (292 строк)
|
||
│ ├── client/ # JavaScript клиентские скрипты
|
||
│ │ ├── etaxes_common.js # Общие утилиты (1,052 строки)
|
||
│ │ ├── purchase_order.js # Заказы на покупку (1,532 строки)
|
||
│ │ ├── sales_order.js # Заказы на продажу (1,585 строк)
|
||
│ │ ├── sales_invoice.js # Счета на оплату (1,856 строк)
|
||
│ │ └── [списки для справочников] # 4 файла по ~900 строк
|
||
│ └── invoice_az/ # Определения DocTypes
|
||
│ └── doctype/ # 17 кастомных DocTypes
|
||
│ ├── asan_login/ # Хранение токенов ASAN
|
||
│ ├── e_taxes_settings/ # Настройки E-Taxes
|
||
│ ├── e_taxes_purchase/ # Входящие счета
|
||
│ ├── e_taxes_sales/ # Исходящие счета
|
||
│ ├── e_taxes_item/ # Товары из E-Taxes
|
||
│ ├── e_taxes_unit/ # Единицы измерения
|
||
│ ├── e_taxes_customers/ # Клиенты
|
||
│ ├── e_taxes_suppliers/ # Поставщики
|
||
│ └── [mapping doctypes] # Таблицы сопоставления
|
||
```
|
||
|
||
### Уровни приложения
|
||
|
||
1. **Уровень представления (Frontend)**
|
||
- JavaScript клиентские скрипты
|
||
- Интеграция с Frappe UI
|
||
- Диалоги и формы
|
||
- Списки с возможностью массовых операций
|
||
|
||
2. **Уровень бизнес-логики (Backend)**
|
||
- Python модули API
|
||
- Обработка данных E-Taxes
|
||
- Интеллектуальное сопоставление
|
||
- Создание документов в ERPNext
|
||
|
||
3. **Уровень данных (Database)**
|
||
- 17 кастомных DocTypes
|
||
- Связи с стандартными DocTypes ERPNext
|
||
- Индексированные поля для быстрого поиска
|
||
|
||
4. **Внешние интеграции**
|
||
- REST API E-Taxes
|
||
- ASAN Login аутентификация
|
||
- Планировщик задач Frappe
|
||
|
||
---
|
||
|
||
## Технологический стек
|
||
|
||
### Backend
|
||
|
||
| Технология | Назначение |
|
||
|-----------|-----------|
|
||
| **Python 3.10+** | Основной язык программирования |
|
||
| **Frappe Framework** | Веб-фреймворк для бизнес-приложений |
|
||
| **Frappe ORM** | Работа с базой данных |
|
||
| **requests** | HTTP клиент для API |
|
||
| **difflib.SequenceMatcher** | Нечеткое сопоставление (95%) |
|
||
| **re (regex)** | Обработка строк и валидация |
|
||
| **json** | Сериализация/десериализация данных |
|
||
|
||
### Frontend
|
||
|
||
| Технология | Назначение |
|
||
|-----------|-----------|
|
||
| **JavaScript ES6+** | Клиентские скрипты |
|
||
| **jQuery** | Манипуляция DOM |
|
||
| **Frappe UI** | Готовые UI компоненты |
|
||
| **frappe.call()** | AJAX коммуникация |
|
||
|
||
### Database
|
||
|
||
| Технология | Назначение |
|
||
|-----------|-----------|
|
||
| **MariaDB** | Реляционная СУБД |
|
||
| **InnoDB** | Storage engine для всех DocTypes |
|
||
| **Индексы** | etaxes_item_name, etaxes_unit_name, etaxes_party_name |
|
||
|
||
### Внешние API
|
||
|
||
- **E-Taxes REST API** (https://new.e-taxes.gov.az)
|
||
- Аутентификация: Bearer Token через ASAN Login
|
||
- Формат: JSON
|
||
- Протокол: HTTPS
|
||
- Ограничение: 100мс между запросами
|
||
|
||
### Инструменты разработки
|
||
|
||
- **ruff** - Линтинг и форматирование Python
|
||
- **eslint** - Линтинг JavaScript
|
||
- **prettier** - Форматирование кода
|
||
- **pyupgrade** - Модернизация синтаксиса Python
|
||
- **pre-commit** - Git хуки для качества кода
|
||
|
||
---
|
||
|
||
## Структура файлов
|
||
|
||
### Статистика кода
|
||
|
||
| Категория | Количество | Строки кода |
|
||
|-----------|-----------|-------------|
|
||
| **Python модули** | 5 основных | 7,198 строк |
|
||
| **JavaScript файлы** | 9 файлов | 9,786 строк |
|
||
| **DocTypes** | 17 кастомных | ~80 файлов |
|
||
| **Документация** | 3 MD файла | 50 КБ |
|
||
| **Конфигурация** | 5 файлов | ~2.5 КБ |
|
||
|
||
### Основные конфигурационные файлы
|
||
|
||
**pyproject.toml** - Метаданные проекта, зависимости, настройки ruff/eslint
|
||
```toml
|
||
[project]
|
||
name = "invoice_az"
|
||
version = "0.0.1"
|
||
requires-python = ">=3.10"
|
||
|
||
[tool.ruff]
|
||
line-length = 110
|
||
target-version = "py310"
|
||
```
|
||
|
||
**hooks.py** - Точки интеграции с Frappe
|
||
- Обработчики событий DocType
|
||
- Задачи планировщика (каждые 4 минуты)
|
||
- Клиентские скрипты
|
||
- Хуки жизненного цикла
|
||
|
||
**.editorconfig** - Настройки редактора
|
||
- Отступы, окончания строк
|
||
- Кодировка UTF-8
|
||
|
||
**.gitignore** - Правила исключения Git
|
||
- Байт-код Python
|
||
- node_modules
|
||
- Логи и кэш
|
||
|
||
---
|
||
|
||
## Основные модули
|
||
|
||
### 1. api.py (4,320 строк)
|
||
|
||
**Назначение:** Основной модуль для работы с входящими счетами-фактурами
|
||
|
||
**Ключевые функции:**
|
||
|
||
```python
|
||
@frappe.whitelist()
|
||
def load_items_from_invoices(from_date, to_date, page, company, page_size=200)
|
||
# Загрузка счетов из E-Taxes за период
|
||
# Пагинация, кэширование (5 минут)
|
||
# Возвращает список счетов с деталями
|
||
|
||
@frappe.whitelist()
|
||
def create_purchase_order(invoice_data, company, set_warehouse=None, supplier=None, update_rate=True, expand_desc=True)
|
||
# Создание Purchase Order из счета E-Taxes
|
||
# Интеллектуальное сопоставление товаров/поставщиков
|
||
# Автоматическое создание недостающих записей
|
||
|
||
def load_items()
|
||
# Синхронизация справочника товаров из E-Taxes
|
||
# Массовая загрузка до 200 записей
|
||
|
||
def load_units()
|
||
# Синхронизация единиц измерения
|
||
|
||
def load_parties()
|
||
# Синхронизация контрагентов (клиенты/поставщики)
|
||
```
|
||
|
||
**Интеллектуальное сопоставление:**
|
||
```python
|
||
def find_best_match(etaxes_name, existing_names):
|
||
# Использует difflib.SequenceMatcher
|
||
# Порог совпадения: 95%
|
||
# Поддержка азербайджанских символов (ə, ö, ü, ğ, ş, ç, ı)
|
||
# Автоматическое сопоставление при высокой схожести
|
||
```
|
||
|
||
### 2. auth.py (968 строк)
|
||
|
||
**Назначение:** Аутентификация через ASAN Login и управление токенами
|
||
|
||
**Этапы аутентификации:**
|
||
|
||
1. **Инициализация входа**
|
||
```python
|
||
@frappe.whitelist()
|
||
def initiate_asan_login()
|
||
# Получение session_id от E-Taxes
|
||
# Возврат QR кода для мобильного приложения ASAN
|
||
```
|
||
|
||
2. **Проверка статуса**
|
||
```python
|
||
@frappe.whitelist()
|
||
def check_asan_login_status(session_id, docname)
|
||
# Опрос статуса каждые 2 секунды
|
||
# Обработка подтверждения через мобильное приложение
|
||
```
|
||
|
||
3. **Выбор сертификата**
|
||
```python
|
||
@frappe.whitelist()
|
||
def select_certificate(session_id, cert_id, docname)
|
||
# Получение access_token
|
||
```
|
||
|
||
4. **Выбор налогоплательщика**
|
||
```python
|
||
@frappe.whitelist()
|
||
def select_taxpayer(taxpayer_id, docname)
|
||
# Получение main_token для работы с E-Taxes API
|
||
# Сохранение в Asan Login doctype
|
||
```
|
||
|
||
**Автоматическое обновление токена:**
|
||
```python
|
||
def renew_token_background()
|
||
# Cron задача: каждые 4 минуты
|
||
# Пропускает, если нет активности
|
||
# Обрабатывает ошибки 401/500
|
||
# Устанавливает auth_status при неудаче
|
||
```
|
||
|
||
### 3. sales_api.py (887 строк)
|
||
|
||
**Назначение:** Обработка исходящих счетов-фактур
|
||
|
||
**Основные функции:**
|
||
|
||
```python
|
||
@frappe.whitelist()
|
||
def load_sales_invoices_from_etaxes(from_date, to_date, page, company, page_size=200)
|
||
# Загрузка исходящих счетов из E-Taxes
|
||
# Структура аналогична load_items_from_invoices
|
||
|
||
@frappe.whitelist()
|
||
def create_sales_order(invoice_data, company, set_warehouse=None, customer=None, update_rate=True, expand_desc=True)
|
||
# Создание Sales Order из счета E-Taxes
|
||
# Сопоставление клиентов и товаров
|
||
# Автоматическое создание недостающих записей
|
||
```
|
||
|
||
### 4. send_sales_api.py (730 строк)
|
||
|
||
**Назначение:** Отправка счетов в E-Taxes
|
||
|
||
**Процесс отправки:**
|
||
|
||
```python
|
||
@frappe.whitelist()
|
||
def send_sales_invoice_to_etaxes(sales_invoice_name)
|
||
# 1. Валидация данных
|
||
# - Проверка customer.tax_id (ИНН)
|
||
# - Проверка item.product_group_code
|
||
# - Защита от дубликатов
|
||
|
||
# 2. Генерация серийного номера
|
||
serial_number = generate_serial_number()
|
||
|
||
# 3. Формирование JSON payload
|
||
invoice_json = {
|
||
"receiver": {"name": customer_name, "tin": tin},
|
||
"invoiceLines": [
|
||
{
|
||
"productName": item_name,
|
||
"quantity": qty,
|
||
"pricePerUnit": rate,
|
||
"vatRate": vat_rate,
|
||
"vatAmount": vat_amount
|
||
}
|
||
],
|
||
"comment": comments
|
||
}
|
||
|
||
# 4. Отправка в E-Taxes
|
||
response = submit_invoice(invoice_json)
|
||
|
||
# 5. Автоматическая подпись через ASAN
|
||
sign_invoice_with_asan(invoice_id)
|
||
|
||
# 6. Сохранение результата
|
||
create_etaxes_sales_outbox_record()
|
||
```
|
||
|
||
### 5. hooks.py (292 строки)
|
||
|
||
**Назначение:** Интеграция с Frappe Framework
|
||
|
||
**Обработчики событий:**
|
||
```python
|
||
doc_events = {
|
||
"Purchase Order": {
|
||
"on_trash": "invoice_az.api.unlink_etaxes_record",
|
||
"on_cancel": "invoice_az.api.unlink_etaxes_record",
|
||
},
|
||
"Sales Order": {
|
||
"on_trash": "invoice_az.sales_api.unlink_sales_etaxes_record",
|
||
"on_cancel": "invoice_az.sales_api.unlink_sales_etaxes_record",
|
||
}
|
||
}
|
||
```
|
||
|
||
**Планировщик задач:**
|
||
```python
|
||
scheduler_events = {
|
||
"cron": {
|
||
"*/4 * * * *": [ # Каждые 4 минуты
|
||
"invoice_az.auth.renew_token_background"
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
**Клиентские скрипты:**
|
||
```python
|
||
doctype_js = {
|
||
"Purchase Order": "client/purchase_order.js",
|
||
"Sales Order": "client/sales_order.js",
|
||
"Sales Invoice": "client/sales_invoice.js",
|
||
"Purchase Invoice": "client/purchase_invoice.js"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## DocTypes
|
||
|
||
### Конфигурационные DocTypes (4)
|
||
|
||
#### 1. Asan Login
|
||
**Назначение:** Хранение токенов и данных аутентификации ASAN
|
||
|
||
**Основные поля:**
|
||
- `session_id` - ID сессии ASAN
|
||
- `access_token` - Токен доступа
|
||
- `main_token` - Основной токен для E-Taxes API
|
||
- `certificate_id` - ID выбранного сертификата
|
||
- `taxpayer_id` - ID налогоплательщика
|
||
- `taxpayer_name` - Название организации
|
||
- `auth_status` - Статус: Authenticated / Not Authenticated
|
||
- `last_activity` - Время последней активности
|
||
- `token_expiry` - Время истечения токена
|
||
|
||
**Методы:**
|
||
- `get_headers()` - Формирование заголовков для API запросов
|
||
|
||
#### 2. E-Taxes Settings
|
||
**Назначение:** Центральная конфигурация приложения
|
||
|
||
**Основные поля:**
|
||
- `api_base_url` - Базовый URL E-Taxes API
|
||
- `company` - Компания по умолчанию
|
||
- `default_warehouse` - Склад по умолчанию
|
||
- `item_mappings` - Таблица сопоставления товаров
|
||
- `unit_mappings` - Таблица сопоставления единиц
|
||
- `party_mappings` - Таблица сопоставления контрагентов
|
||
- `auto_create_items` - Автосоздание товаров
|
||
- `fuzzy_match_threshold` - Порог нечеткого совпадения (0.95)
|
||
|
||
#### 3. E-Taxes Purchase
|
||
**Назначение:** Отслеживание импортированных счетов закупок
|
||
|
||
**Основные поля:**
|
||
- `invoice_id` - ID счета в E-Taxes
|
||
- `invoice_serial_number` - Серийный номер
|
||
- `invoice_date` - Дата счета
|
||
- `supplier_name` - Название поставщика
|
||
- `supplier_tin` - ИНН поставщика
|
||
- `total_amount` - Общая сумма
|
||
- `vat_amount` - Сумма НДС
|
||
- `purchase_order` - Связь с Purchase Order
|
||
- `purchase_invoice` - Связь с Purchase Invoice
|
||
- `verification_code` - Код проверки
|
||
- `status` - Статус: New / Mapped / Imported
|
||
|
||
#### 4. E-Taxes Sales
|
||
**Назначение:** Отслеживание экспортированных счетов продаж
|
||
|
||
**Аналогичная структура E-Taxes Purchase, но для исходящих счетов**
|
||
|
||
### Справочные DocTypes (5)
|
||
|
||
#### 5. E-Taxes Item
|
||
**Назначение:** Товары/услуги из E-Taxes
|
||
|
||
**Основные поля:**
|
||
- `etaxes_item_code` - Код товара в E-Taxes
|
||
- `etaxes_item_name` - Название (индексировано)
|
||
- `category` - Категория
|
||
- `unit_code` - Код единицы измерения
|
||
- `mapped_item` - Связь с Item (ERPNext)
|
||
- `mapping_status` - Статус: Unmapped / Auto-Mapped / Manually Mapped
|
||
|
||
#### 6. E-Taxes Unit
|
||
**Назначение:** Единицы измерения из E-Taxes
|
||
|
||
**Основные поля:**
|
||
- `etaxes_unit_code` - Код единицы
|
||
- `etaxes_unit_name` - Название (индексировано)
|
||
- `mapped_uom` - Связь с UOM (ERPNext)
|
||
- `mapping_status` - Статус сопоставления
|
||
|
||
#### 7. E-Taxes Customers
|
||
**Назначение:** Клиенты из E-Taxes
|
||
|
||
**Основные поля:**
|
||
- `customer_tin` - ИНН клиента
|
||
- `customer_name` - Название
|
||
- `mapped_customer` - Связь с Customer (ERPNext)
|
||
- `mapping_status` - Статус сопоставления
|
||
|
||
#### 8. E-Taxes Suppliers
|
||
**Назначение:** Поставщики из E-Taxes
|
||
|
||
**Основные поля:**
|
||
- `supplier_tin` - ИНН поставщика
|
||
- `supplier_name` - Название
|
||
- `mapped_supplier` - Связь с Supplier (ERPNext)
|
||
- `mapping_status` - Статус сопоставления
|
||
|
||
#### 9. E-Taxes Parties
|
||
**Назначение:** Универсальные записи контрагентов
|
||
|
||
**Основные поля:**
|
||
- `party_tin` - ИНН контрагента
|
||
- `party_name` - Название
|
||
- `party_type` - Тип: Customer / Supplier / Both
|
||
|
||
### Таблицы сопоставления (5 Child Tables)
|
||
|
||
#### 10. E-Taxes Item Mapping
|
||
- `etaxes_item_code` - Код товара E-Taxes
|
||
- `erp_item_code` - Код товара ERPNext
|
||
- `mapping_type` - Тип: Auto / Manual
|
||
|
||
#### 11. E-Taxes Unit Mapping
|
||
- `etaxes_unit_code` - Код единицы E-Taxes
|
||
- `erp_uom` - UOM в ERPNext
|
||
|
||
#### 12. E-Taxes Customer Mappings
|
||
- `etaxes_customer_tin` - ИНН клиента E-Taxes
|
||
- `erp_customer` - Customer в ERPNext
|
||
|
||
#### 13. E-Taxes Supplier Mappings
|
||
- `etaxes_supplier_tin` - ИНН поставщика E-Taxes
|
||
- `erp_supplier` - Supplier в ERPNext
|
||
|
||
#### 14. E-Taxes Party Mapping
|
||
- `etaxes_party_tin` - ИНН контрагента E-Taxes
|
||
- `erp_party` - Контрагент в ERPNext
|
||
- `party_type` - Тип контрагента
|
||
|
||
### Отслеживание отправки (1)
|
||
|
||
#### 15. E-Taxes Sales Outbox
|
||
**Назначение:** Журнал отправленных счетов в E-Taxes
|
||
|
||
**Основные поля:**
|
||
- `sales_invoice` - Связь с Sales Invoice
|
||
- `etaxes_invoice_id` - ID счета в E-Taxes
|
||
- `serial_number` - Серийный номер
|
||
- `verification_code` - Код проверки
|
||
- `send_date` - Дата отправки
|
||
- `send_status` - Статус: Pending / Sent / Signed / Failed
|
||
- `error_message` - Сообщение об ошибке
|
||
|
||
### Тестовый DocType (1)
|
||
|
||
#### 16. TestAPI
|
||
**Назначение:** Разработка и отладка API вызовов
|
||
|
||
---
|
||
|
||
## API интеграция
|
||
|
||
### Эндпоинты E-Taxes
|
||
|
||
#### Аутентификация
|
||
|
||
**Инициализация входа**
|
||
```
|
||
POST /api/po/auth/public/v1/initiate
|
||
```
|
||
|
||
**Проверка статуса**
|
||
```
|
||
POST /api/po/auth/public/v1/status/{session_id}
|
||
```
|
||
|
||
**Выбор сертификата**
|
||
```
|
||
POST /api/po/auth/public/v1/certificate/select
|
||
Body: {"sessionId": "...", "certificateId": "..."}
|
||
```
|
||
|
||
**Выбор налогоплательщика**
|
||
```
|
||
POST /api/po/auth/public/v1/taxpayer/select
|
||
Body: {"accessToken": "...", "taxpayerId": "..."}
|
||
```
|
||
|
||
**Обновление токена**
|
||
```
|
||
POST /api/po/auth/public/v1/renew
|
||
Headers: {"x-authorization": "Bearer {main_token}"}
|
||
```
|
||
|
||
#### Работа со счетами
|
||
|
||
**Получение входящих счетов (inbox)**
|
||
```
|
||
POST /api/po/invoice/public/v2/invoice/find.inbox
|
||
Body: {
|
||
"from": "2024-01-01",
|
||
"to": "2024-12-31",
|
||
"page": 0,
|
||
"size": 200
|
||
}
|
||
```
|
||
|
||
**Получение исходящих счетов (outbox)**
|
||
```
|
||
POST /api/po/invoice/public/v2/invoice/find.outbox
|
||
Body: {
|
||
"from": "2024-01-01",
|
||
"to": "2024-12-31",
|
||
"page": 0,
|
||
"size": 200
|
||
}
|
||
```
|
||
|
||
**Детали счета**
|
||
```
|
||
GET /api/po/invoice/public/v2/invoice/{invoice_id}
|
||
```
|
||
|
||
**Создание счета**
|
||
```
|
||
POST /api/po/invoice/public/v2/invoice
|
||
Body: {
|
||
"receiver": {
|
||
"name": "ООО Компания",
|
||
"tin": "1234567890"
|
||
},
|
||
"invoiceLines": [
|
||
{
|
||
"productName": "Товар 1",
|
||
"quantity": 10,
|
||
"pricePerUnit": 100,
|
||
"vatRate": 18,
|
||
"vatAmount": 180
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
**Генерация серийного номера**
|
||
```
|
||
POST /api/po/invoice/public/v1/generateSerialNumber/defaultInvoice
|
||
```
|
||
|
||
**Подпись счета через ASAN**
|
||
```
|
||
POST /api/po/invoice/public/v1/invoice/sign/withAsanImza
|
||
Body: {"invoiceId": "..."}
|
||
```
|
||
|
||
### Заголовки запросов
|
||
|
||
Все запросы к E-Taxes API требуют следующих заголовков:
|
||
|
||
```python
|
||
headers = {
|
||
"Content-Type": "application/json",
|
||
"x-authorization": f"Bearer {main_token}",
|
||
"User-Agent": "Mozilla/5.0...",
|
||
"Accept": "application/json"
|
||
}
|
||
```
|
||
|
||
### Обработка ошибок
|
||
|
||
```python
|
||
try:
|
||
response = requests.post(url, json=data, headers=headers)
|
||
response.raise_for_status()
|
||
except requests.exceptions.HTTPError as e:
|
||
if e.response.status_code == 401:
|
||
# Токен истек - обновить токен
|
||
renew_token()
|
||
elif e.response.status_code == 500:
|
||
# Ошибка сервера - повторить с задержкой
|
||
time.sleep(1)
|
||
retry_request()
|
||
else:
|
||
frappe.log_error(str(e), "E-Taxes API Error")
|
||
```
|
||
|
||
### Ограничения API
|
||
|
||
- **Размер страницы:** максимум 200 записей
|
||
- **Задержка между запросами:** минимум 100мс
|
||
- **Время жизни токена:** обновление каждые 4 минуты
|
||
- **Кэширование:** 5 минут для списков счетов
|
||
|
||
---
|
||
|
||
## Рабочие процессы
|
||
|
||
### Процесс импорта счета закупки (Inbox)
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 1. Пользователь открывает Purchase Order │
|
||
└─────────────────────┬───────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 2. Нажимает кнопку "Load from E-Taxes" │
|
||
└─────────────────────┬───────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 3. Выбирает диапазон дат (from_date, to_date) │
|
||
└─────────────────────┬───────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 4. Frontend → load_items_from_invoices() │
|
||
│ - Проверка кэша (5 минут) │
|
||
│ - Если нет - запрос к E-Taxes API │
|
||
└─────────────────────┬───────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 5. E-Taxes API возвращает список счетов │
|
||
│ - Номер счета, дата │
|
||
│ - Поставщик (название, ИНН) │
|
||
│ - Список позиций (товар, кол-во, цена) │
|
||
│ - НДС │
|
||
└─────────────────────┬───────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 6. Отображается браузер счетов │
|
||
│ - Таблица со всеми счетами │
|
||
│ - Фильтрация, поиск │
|
||
│ - Просмотр позиций счета │
|
||
└─────────────────────┬───────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 7. Пользователь выбирает счет и нажимает "Import" │
|
||
└─────────────────────┬───────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 8. create_purchase_order(invoice_data) │
|
||
│ │
|
||
│ A. Сопоставление поставщика: │
|
||
│ - Поиск по ИНН в Supplier │
|
||
│ - Если нет - нечеткое сопоставление по имени │
|
||
│ - Если совпадение < 95% - создать нового │
|
||
│ │
|
||
│ B. Сопоставление товаров: │
|
||
│ ┌─ Для каждой позиции счета ─┐ │
|
||
│ │ │ │
|
||
│ │ 1. Поиск в Item Mapping │ │
|
||
│ │ 2. Нечеткое сопоставление │ │
|
||
│ │ 3. Если нет - создать Item │ │
|
||
│ └──────────────────────────────┘ │
|
||
│ │
|
||
│ C. Сопоставление единиц измерения: │
|
||
│ - Поиск в UOM Mapping │
|
||
│ - Если нет - создать UOM │
|
||
│ │
|
||
│ D. Создание Purchase Order: │
|
||
│ - Заполнение полей │
|
||
│ - Добавление позиций (items) │
|
||
│ - Расчет сумм и НДС │
|
||
│ - Сохранение документа │
|
||
│ │
|
||
│ E. Создание E-Taxes Purchase record: │
|
||
│ - Сохранение ID счета E-Taxes │
|
||
│ - Связь с Purchase Order │
|
||
│ - Статус: Imported │
|
||
└─────────────────────┬───────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 9. Purchase Order готов к отправке (Submit) │
|
||
└─────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### Процесс экспорта счета продажи (Outbox)
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 1. Пользователь создает Sales Invoice в ERPNext │
|
||
│ - Выбирает клиента (обязательно tax_id) │
|
||
│ - Добавляет позиции (обязательно product_group_code)│
|
||
│ - Заполняет количество, цену │
|
||
└─────────────────────┬───────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 2. Отправляет документ (Submit) │
|
||
└─────────────────────┬───────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 3. Нажимает "Send to E-Taxes" │
|
||
└─────────────────────┬───────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 4. Показывается первое подтверждение │
|
||
│ "Вы уверены, что хотите отправить счет?" │
|
||
└─────────────────────┬───────────────────────────────────┘
|
||
│ Да
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 5. Показывается второе подтверждение │
|
||
│ "Это действие необратимо. Продолжить?" │
|
||
└─────────────────────┬───────────────────────────────────┘
|
||
│ Да
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 6. send_sales_invoice_to_etaxes() │
|
||
│ │
|
||
│ A. Валидация данных: │
|
||
│ - Проверка customer.tax_id (ИНН) │
|
||
│ - Проверка items.product_group_code │
|
||
│ - Проверка дубликатов в Outbox │
|
||
│ │
|
||
│ B. Генерация серийного номера: │
|
||
│ POST /generateSerialNumber/defaultInvoice │
|
||
│ Получение уникального номера для счета │
|
||
│ │
|
||
│ C. Формирование JSON payload: │
|
||
│ { │
|
||
│ "serialNumber": "...", │
|
||
│ "receiver": { │
|
||
│ "name": "ООО Клиент", │
|
||
│ "tin": "1234567890" │
|
||
│ }, │
|
||
│ "invoiceLines": [ │
|
||
│ { │
|
||
│ "productName": "Товар", │
|
||
│ "productGroupCode": "12345", │
|
||
│ "quantity": 10, │
|
||
│ "pricePerUnit": 100, │
|
||
│ "totalPrice": 1000, │
|
||
│ "vatRate": 18, │
|
||
│ "vatAmount": 180 │
|
||
│ } │
|
||
│ ], │
|
||
│ "totalAmount": 1180, │
|
||
│ "comment": "Примечание" │
|
||
│ } │
|
||
│ │
|
||
│ D. Отправка в E-Taxes: │
|
||
│ POST /api/po/invoice/public/v2/invoice │
|
||
│ Получение invoice_id │
|
||
│ │
|
||
│ E. Автоматическая подпись через ASAN: │
|
||
│ POST /invoice/sign/withAsanImza │
|
||
│ Body: {"invoiceId": "..."} │
|
||
│ │
|
||
│ F. Создание E-Taxes Sales Outbox record: │
|
||
│ - Сохранение invoice_id │
|
||
│ - Сохранение serial_number │
|
||
│ - Сохранение verification_code │
|
||
│ - Связь с Sales Invoice │
|
||
│ - Статус: Sent & Signed │
|
||
└─────────────────────┬───────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 7. Показывается сообщение об успехе │
|
||
│ "Счет успешно отправлен в E-Taxes" │
|
||
│ "Invoice ID: 12345" │
|
||
│ "Serial Number: ABC-123" │
|
||
└─────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### Процесс аутентификации ASAN
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 1. Пользователь открывает ASAN Login doctype │
|
||
└─────────────────────┬───────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 2. Нажимает "Login with ASAN" │
|
||
└─────────────────────┬───────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 3. initiate_asan_login() │
|
||
│ POST /api/po/auth/public/v1/initiate │
|
||
│ Получение session_id и qrCodeValue │
|
||
└─────────────────────┬───────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 4. Отображается QR код │
|
||
│ "Отсканируйте QR код в мобильном приложении ASAN" │
|
||
└─────────────────────┬───────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 5. Опрос статуса каждые 2 секунды │
|
||
│ check_asan_login_status(session_id) │
|
||
│ POST /api/po/auth/public/v1/status/{session_id} │
|
||
│ │
|
||
│ Статусы: │
|
||
│ - PENDING → продолжить опрос │
|
||
│ - APPROVED → переход к шагу 6 │
|
||
│ - REJECTED → показать ошибку │
|
||
└─────────────────────┬───────────────────────────────────┘
|
||
│ APPROVED
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 6. Получение списка сертификатов │
|
||
│ Отображается диалог выбора сертификата │
|
||
│ - Название сертификата │
|
||
│ - Срок действия │
|
||
│ - Организация │
|
||
└─────────────────────┬───────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 7. Пользователь выбирает сертификат │
|
||
│ select_certificate(session_id, cert_id) │
|
||
│ POST /certificate/select │
|
||
│ Получение access_token │
|
||
└─────────────────────┬───────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 8. Получение списка налогоплательщиков │
|
||
│ Отображается диалог выбора налогоплательщика │
|
||
│ - Название организации │
|
||
│ - ИНН │
|
||
│ - Тип налогоплательщика │
|
||
└─────────────────────┬───────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 9. Пользователь выбирает налогоплательщика │
|
||
│ select_taxpayer(taxpayer_id) │
|
||
│ POST /taxpayer/select │
|
||
│ Получение main_token │
|
||
└─────────────────────┬───────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 10. Сохранение в ASAN Login doctype: │
|
||
│ - session_id │
|
||
│ - access_token │
|
||
│ - main_token ← используется для всех API запросов │
|
||
│ - certificate_id │
|
||
│ - taxpayer_id │
|
||
│ - taxpayer_name │
|
||
│ - auth_status = "Authenticated" │
|
||
│ - last_activity = now() │
|
||
│ - token_expiry = now() + 4 minutes │
|
||
└─────────────────────┬───────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 11. Аутентификация завершена │
|
||
│ "Вы успешно вошли как [taxpayer_name]" │
|
||
└─────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### Процесс автоматического обновления токена
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ Cron задача: Каждые 4 минуты │
|
||
└─────────────────────┬───────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ renew_token_background() │
|
||
└─────────────────────┬───────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ Проверка: Есть ли активные ASAN Login записи? │
|
||
└─────────────────────┬───────────────────────────────────┘
|
||
│ Нет → Завершить
|
||
│ Да ↓
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ Проверка: Была ли активность за последние 10 минут? │
|
||
└─────────────────────┬───────────────────────────────────┘
|
||
│ Нет → Завершить (экономия ресурсов)
|
||
│ Да ↓
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ Отправка запроса на обновление токена │
|
||
│ POST /api/po/auth/public/v1/renew │
|
||
│ Headers: {"x-authorization": "Bearer {main_token}"} │
|
||
└─────────────────────┬───────────────────────────────────┘
|
||
│
|
||
▼
|
||
├─ Успех (200) ─────────────────────┐
|
||
│ │
|
||
│ ┌──────────────────────────────┐ │
|
||
│ │ Получение нового main_token │ │
|
||
│ │ Обновление ASAN Login record │ │
|
||
│ │ last_activity = now() │ │
|
||
│ │ token_expiry = now() + 4min │ │
|
||
│ └──────────────────────────────┘ │
|
||
│ │
|
||
├─ Ошибка 401 (Unauthorized) ───────┤
|
||
│ │
|
||
│ ┌──────────────────────────────┐ │
|
||
│ │ Токен истек │ │
|
||
│ │ auth_status = "Not Auth..." │ │
|
||
│ │ Требуется повторный вход │ │
|
||
│ └──────────────────────────────┘ │
|
||
│ │
|
||
└─ Ошибка 500 (Server Error) ───────┤
|
||
│
|
||
┌──────────────────────────────┐ │
|
||
│ Задержка 1 секунда │ │
|
||
│ Повторная попытка │ │
|
||
│ Если неудача - лог ошибки │ │
|
||
└──────────────────────────────┘ │
|
||
│
|
||
┌────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ Следующий запуск через 4 минуты │
|
||
└─────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## Установка и настройка
|
||
|
||
### Требования
|
||
|
||
- Frappe Framework (версия 14+)
|
||
- ERPNext (версия 14+)
|
||
- Python 3.10+
|
||
- MariaDB 10.6+
|
||
- Node.js 16+
|
||
|
||
### Установка через bench
|
||
|
||
1. **Переход в директорию bench**
|
||
```bash
|
||
cd frappe-bench
|
||
```
|
||
|
||
2. **Получение приложения**
|
||
```bash
|
||
bench get-app https://github.com/your-org/invoice_az.git
|
||
```
|
||
|
||
3. **Установка на сайт**
|
||
```bash
|
||
bench --site your-site.local install-app invoice_az
|
||
```
|
||
|
||
4. **Миграция базы данных**
|
||
```bash
|
||
bench --site your-site.local migrate
|
||
```
|
||
|
||
5. **Очистка кэша**
|
||
```bash
|
||
bench --site your-site.local clear-cache
|
||
```
|
||
|
||
6. **Перезапуск bench**
|
||
```bash
|
||
bench restart
|
||
```
|
||
|
||
### Первоначальная настройка
|
||
|
||
#### 1. Настройка ASAN Login
|
||
|
||
1. Откройте "ASAN Login" в Desk
|
||
2. Создайте новую запись
|
||
3. Нажмите "Login with ASAN"
|
||
4. Отсканируйте QR код мобильным приложением ASAN
|
||
5. Подтвердите вход в приложении
|
||
6. Выберите сертификат
|
||
7. Выберите налогоплательщика
|
||
8. Сохраните запись
|
||
|
||
#### 2. Настройка E-Taxes Settings
|
||
|
||
1. Откройте "E-Taxes Settings"
|
||
2. Заполните основные поля:
|
||
- **API Base URL:** `https://new.e-taxes.gov.az`
|
||
- **Company:** Ваша компания по умолчанию
|
||
- **Default Warehouse:** Склад по умолчанию
|
||
3. Настройте параметры:
|
||
- **Auto Create Items:** Включить/выключить автосоздание товаров
|
||
- **Fuzzy Match Threshold:** 0.95 (95% совпадения)
|
||
4. Сохраните настройки
|
||
|
||
#### 3. Настройка кастомных полей (в приложении jey_erp)
|
||
|
||
Убедитесь, что следующие кастомные поля существуют:
|
||
|
||
**Customer:**
|
||
- `tax_id` (Data) - ИНН клиента
|
||
|
||
**Item:**
|
||
- `product_group_code` (Data) - Код товарной группы
|
||
|
||
**Sales Invoice Item:**
|
||
- Поля для расчета НДС
|
||
|
||
### Синхронизация справочников
|
||
|
||
После установки рекомендуется синхронизировать справочные данные:
|
||
|
||
1. **Синхронизация товаров**
|
||
- Откройте "E-Taxes Item"
|
||
- Нажмите "Sync from E-Taxes"
|
||
- Дождитесь завершения загрузки
|
||
|
||
2. **Синхронизация единиц измерения**
|
||
- Откройте "E-Taxes Unit"
|
||
- Нажмите "Sync from E-Taxes"
|
||
|
||
3. **Синхронизация контрагентов**
|
||
- Откройте "E-Taxes Parties"
|
||
- Нажмите "Sync from E-Taxes"
|
||
|
||
---
|
||
|
||
## Разработка
|
||
|
||
### Настройка окружения разработки
|
||
|
||
1. **Клонирование репозитория**
|
||
```bash
|
||
cd frappe-bench/apps
|
||
git clone https://github.com/your-org/invoice_az.git
|
||
```
|
||
|
||
2. **Установка pre-commit хуков**
|
||
```bash
|
||
cd invoice_az
|
||
pre-commit install
|
||
```
|
||
|
||
3. **Установка зависимостей разработки**
|
||
```bash
|
||
pip install ruff pytest
|
||
npm install -g eslint prettier
|
||
```
|
||
|
||
### Структура кода
|
||
|
||
**Python модули** (`invoice_az/`)
|
||
- `api.py` - API для закупок
|
||
- `auth.py` - Аутентификация
|
||
- `sales_api.py` - API для продаж
|
||
- `send_sales_api.py` - Отправка счетов
|
||
- `hooks.py` - Интеграция с Frappe
|
||
|
||
**JavaScript клиенты** (`invoice_az/client/`)
|
||
- `etaxes_common.js` - Общие утилиты
|
||
- `purchase_order.js` - Заказы на покупку
|
||
- `sales_order.js` - Заказы на продажу
|
||
- `sales_invoice.js` - Счета на оплату
|
||
- `[entity]_list.js` - Списки справочников
|
||
|
||
**DocTypes** (`invoice_az/invoice_az/doctype/`)
|
||
- Каждый DocType в отдельной папке
|
||
- Файлы: `.py`, `.js`, `.json`, `test_*.py`
|
||
|
||
### Соглашения кодирования
|
||
|
||
**Python (ruff)**
|
||
- Длина строки: 110 символов
|
||
- Целевая версия: Python 3.10
|
||
- Кавычки: двойные
|
||
- Отступы: табы
|
||
- Правила: F, E, W, I, UP, B
|
||
|
||
**JavaScript (eslint)**
|
||
- Кавычки: одинарные
|
||
- Точка с запятой: обязательна
|
||
- Отступы: табы
|
||
|
||
### Тестирование
|
||
|
||
**Запуск тестов Python**
|
||
```bash
|
||
cd frappe-bench
|
||
bench --site your-site.local run-tests --app invoice_az
|
||
```
|
||
|
||
**Запуск конкретного теста**
|
||
```bash
|
||
bench --site your-site.local run-tests --test invoice_az.invoice_az.doctype.asan_login.test_asan_login
|
||
```
|
||
|
||
**Запуск линтера**
|
||
```bash
|
||
ruff check invoice_az/
|
||
ruff format invoice_az/
|
||
```
|
||
|
||
### Логирование и отладка
|
||
|
||
**Логирование ошибок**
|
||
```python
|
||
frappe.log_error(
|
||
title="E-Taxes API Error",
|
||
message=str(error),
|
||
reference_doctype="E-Taxes Purchase",
|
||
reference_name=purchase_name
|
||
)
|
||
```
|
||
|
||
**Информационные логи**
|
||
```python
|
||
import logging
|
||
logger = logging.getLogger(__name__)
|
||
logger.info(f"Processing invoice {invoice_id}")
|
||
```
|
||
|
||
**Отладочный вывод**
|
||
```python
|
||
frappe.msgprint(f"Debug: {variable}")
|
||
print(f"Debug: {variable}") # Для консоли bench
|
||
```
|
||
|
||
### Создание нового DocType
|
||
|
||
1. **Создание через bench**
|
||
```bash
|
||
bench --site your-site.local new-doctype "E-Taxes New Feature"
|
||
```
|
||
|
||
2. **Редактирование JSON**
|
||
```json
|
||
{
|
||
"name": "E-Taxes New Feature",
|
||
"module": "Invoice Az",
|
||
"autoname": "field:name",
|
||
"fields": [
|
||
{
|
||
"fieldname": "name",
|
||
"fieldtype": "Data",
|
||
"label": "Name",
|
||
"reqd": 1
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
3. **Создание Python контроллера**
|
||
```python
|
||
import frappe
|
||
from frappe.model.document import Document
|
||
|
||
class ETaxesNewFeature(Document):
|
||
def validate(self):
|
||
# Валидация при сохранении
|
||
pass
|
||
|
||
def on_submit(self):
|
||
# Действия при отправке
|
||
pass
|
||
```
|
||
|
||
4. **Создание JavaScript контроллера**
|
||
```javascript
|
||
frappe.ui.form.on('E-Taxes New Feature', {
|
||
refresh: function(frm) {
|
||
// Логика формы
|
||
}
|
||
});
|
||
```
|
||
|
||
### Создание нового API endpoint
|
||
|
||
```python
|
||
@frappe.whitelist()
|
||
def my_new_api_function(param1, param2):
|
||
"""
|
||
Описание функции
|
||
|
||
Args:
|
||
param1: Описание параметра 1
|
||
param2: Описание параметра 2
|
||
|
||
Returns:
|
||
dict: Результат операции
|
||
"""
|
||
try:
|
||
# Валидация прав
|
||
frappe.only_for("System Manager")
|
||
|
||
# Логика обработки
|
||
result = process_data(param1, param2)
|
||
|
||
return {
|
||
"success": True,
|
||
"data": result
|
||
}
|
||
except Exception as e:
|
||
frappe.log_error(str(e), "API Error")
|
||
return {
|
||
"success": False,
|
||
"error": str(e)
|
||
}
|
||
```
|
||
|
||
**Вызов из JavaScript:**
|
||
```javascript
|
||
frappe.call({
|
||
method: 'invoice_az.api.my_new_api_function',
|
||
args: {
|
||
param1: 'value1',
|
||
param2: 'value2'
|
||
},
|
||
callback: function(r) {
|
||
if (r.message.success) {
|
||
frappe.msgprint('Success!');
|
||
} else {
|
||
frappe.msgprint('Error: ' + r.message.error);
|
||
}
|
||
}
|
||
});
|
||
```
|
||
|
||
### Добавление нового клиентского скрипта
|
||
|
||
1. **Создание файла** `invoice_az/client/my_script.js`
|
||
```javascript
|
||
frappe.ui.form.on('My DocType', {
|
||
refresh: function(frm) {
|
||
frm.add_custom_button(__('My Action'), function() {
|
||
// Логика кнопки
|
||
});
|
||
},
|
||
|
||
field_name: function(frm) {
|
||
// Обработчик изменения поля
|
||
}
|
||
});
|
||
```
|
||
|
||
2. **Регистрация в hooks.py**
|
||
```python
|
||
doctype_js = {
|
||
"My DocType": "client/my_script.js"
|
||
}
|
||
```
|
||
|
||
### Git workflow
|
||
|
||
**Создание новой фичи:**
|
||
```bash
|
||
git checkout -b feature/my-new-feature
|
||
# Разработка
|
||
git add .
|
||
git commit -m "feat: Add my new feature"
|
||
git push origin feature/my-new-feature
|
||
# Создание Pull Request
|
||
```
|
||
|
||
**Pre-commit хуки автоматически проверят:**
|
||
- Линтинг Python (ruff)
|
||
- Линтинг JavaScript (eslint)
|
||
- Форматирование (prettier)
|
||
- Синтаксис Python (pyupgrade)
|
||
|
||
### Производительность и оптимизация
|
||
|
||
**Кэширование:**
|
||
```python
|
||
# Кэш на 5 минут
|
||
cache_key = f"etaxes_invoices_{from_date}_{to_date}"
|
||
cached_data = frappe.cache().get_value(cache_key)
|
||
if cached_data:
|
||
return cached_data
|
||
|
||
data = fetch_from_api()
|
||
frappe.cache().set_value(cache_key, data, expires_in_sec=300)
|
||
```
|
||
|
||
**Массовые операции:**
|
||
```python
|
||
# Вместо цикла с save()
|
||
for item in items:
|
||
item.save()
|
||
|
||
# Используйте bulk_insert
|
||
frappe.db.bulk_insert(
|
||
"E-Taxes Item",
|
||
["name", "etaxes_item_code", "etaxes_item_name"],
|
||
[[item.name, item.code, item.name] for item in items]
|
||
)
|
||
```
|
||
|
||
**Индексация:**
|
||
```python
|
||
# В DocType JSON
|
||
"fields": [
|
||
{
|
||
"fieldname": "etaxes_item_name",
|
||
"fieldtype": "Data",
|
||
"in_list_view": 1,
|
||
"in_standard_filter": 1,
|
||
"search_index": 1 # Создает индекс
|
||
}
|
||
]
|
||
```
|
||
|
||
---
|
||
|
||
## Документация
|
||
|
||
### Существующие файлы документации
|
||
|
||
1. **README.md** (685 байт)
|
||
- Краткое описание проекта
|
||
- Инструкции по установке
|
||
- Инструкции для разработчиков (pre-commit)
|
||
|
||
2. **claude.md** (34 КБ, 1,400+ строк)
|
||
- Подробная документация на русском языке
|
||
- Архитектура системы
|
||
- Описание API
|
||
- Примеры использования
|
||
- Технические детали
|
||
|
||
3. **QA_SEND_INVOICE.md** (15 КБ)
|
||
- 27 вопросов и ответов
|
||
- Функциональность отправки счетов
|
||
- Технические спецификации
|
||
- Примеры использования
|
||
|
||
### Примеры файлов
|
||
|
||
**invoice.txt** - Пример JSON для создания счета
|
||
**invoice asan.txt** - Пример JSON для подписи через ASAN
|
||
|
||
---
|
||
|
||
## Вопросы и Ответы (Q&A) - Отправка Sales Invoice в E-Taxes
|
||
|
||
Дата: 2025-12-15
|
||
Задача: Добавление функционала отправки Sales Invoice в E-Taxes
|
||
|
||
### Технические вопросы и ответы (27 вопросов)
|
||
|
||
#### 1. Аутентификация
|
||
**Q:** Должны ли мы использовать существующую систему аутентификации ASAN Login (которая уже есть в приложении для импорта инвойсов), или нужны отдельные credentials для отправки?
|
||
|
||
**A:** Использовать существующий ASAN Login
|
||
|
||
---
|
||
|
||
#### 2. Доступность кнопки
|
||
**Q:** Когда кнопка 'Отправить в E-Taxes' должна быть доступна?
|
||
|
||
**A:** После Submit (рекомендуется) - кнопка доступна только после того, как Sales Invoice в статусе Submitted
|
||
|
||
---
|
||
|
||
#### 3. Процесс подписания
|
||
**Q:** Должен ли процесс подписания (sign/withAsanImza) происходить автоматически после создания инвойса, или это должна быть отдельная кнопка?
|
||
|
||
**A:** Да, автоматически после создания. Кастомные поля создаются в приложении jey_erp (файл custom_fields.py).
|
||
|
||
---
|
||
|
||
#### 4. Данные клиента (TIN и Object Name)
|
||
**Q:** Где хранится информация о клиенте для отправки в E-Taxes (TIN, objectName)?
|
||
|
||
**A:** Поле TIN - это tax_id. Поле object_name пока создается как тестовое текстовое поле, потом будет заменено.
|
||
|
||
---
|
||
|
||
#### 5. Product Group Code для товаров
|
||
**Q:** Как определять productGroupCode для товаров (items)?
|
||
|
||
**A:** Есть поле в Item, которое называется product_group_code, текстового типа.
|
||
|
||
---
|
||
|
||
#### 6. Сохранение данных после отправки
|
||
**Q:** Какие данные нужно сохранить после успешной отправки?
|
||
|
||
**A:**
|
||
- Invoice ID
|
||
- Serial Number
|
||
- Verification Code
|
||
- Статус отправки
|
||
|
||
---
|
||
|
||
#### 7. Обработка ошибок API
|
||
**Q:** Как обрабатывать ошибки API?
|
||
|
||
**A:** Показать ошибку и остановить процесс.
|
||
|
||
---
|
||
|
||
#### 8. Тип НДС для товаров
|
||
**Q:** Как определять тип НДС для товаров (vat18, vat0, vatFree, exempt)?
|
||
|
||
**A:** В файле custom_fields.py уже созданы поля для Sales Invoice Item:
|
||
- `vat_18_percent_with_amount` - VAT 18% with amount
|
||
- `vat_amount` - VAT Amount (calculated)
|
||
- `vat_0_percent_with_amount` - VAT 0% with amount
|
||
- `amount_without_vat` - Amount without VAT
|
||
- `vat_free_amount` - VAT free amount
|
||
- `tax_article` - Tax Article (Link to Tax Article doctype)
|
||
|
||
Пример кода из custom_fields.py:
|
||
```python
|
||
"Sales Invoice Item": [
|
||
dict(
|
||
fieldname='vat_18_percent_with_amount',
|
||
label='VAT 18% with amount',
|
||
fieldtype='Currency',
|
||
insert_after='amount',
|
||
in_list_view=1,
|
||
read_only=1,
|
||
columns=2,
|
||
precision=2
|
||
),
|
||
dict(
|
||
fieldname='vat_amount',
|
||
label='VAT Amount',
|
||
fieldtype='Currency',
|
||
insert_after='vat_18_percent_with_amount',
|
||
in_list_view=1,
|
||
columns=2,
|
||
precision=2,
|
||
read_only=1,
|
||
description='Calculated as: VAT 18% with amount - Amount'
|
||
),
|
||
# ... другие поля
|
||
]
|
||
```
|
||
|
||
---
|
||
|
||
#### 9. Unit Code для единиц измерения
|
||
**Q:** Откуда брать unitCode для единиц измерения?
|
||
|
||
**A:** Поле unit_code необязательно. Можно просто использовать текстовое имя единицы измерения из табличной части Sales Invoice Item. E-Taxes принимает любой текст (даже "test123"), но для стандартных единиц можно использовать следующие коды:
|
||
|
||
```json
|
||
{
|
||
"items": [
|
||
{"code": "666", "name": {"az": "ədəd", "ru": "штук", "en": "piece"}},
|
||
{"code": "391836", "name": {"az": "km", "ru": "км", "en": "km"}},
|
||
{"code": "529937", "name": {"az": "kq", "ru": "кг", "en": "kg"}},
|
||
{"code": "581879", "name": {"az": "l", "ru": "л", "en": "l"}},
|
||
{"code": "207201", "name": {"az": "m", "ru": "м", "en": "m"}},
|
||
{"code": "6666", "name": {"az": "m2", "ru": "м2", "en": "m2"}},
|
||
{"code": "839111", "name": {"az": "m3", "ru": "м3", "en": "m3"}},
|
||
{"code": "525166", "name": {"az": "q", "ru": "г", "en": "g"}},
|
||
{"code": "116094", "name": {"az": "sm", "ru": "см", "en": "cm"}},
|
||
{"code": "864211", "name": {"az": "sm3", "ru": "см3", "en": "cm3"}},
|
||
{"code": "544580", "name": {"az": "t", "ru": "т", "en": "t"}}
|
||
]
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
#### 10. Дополнительные поля (senderObjectCode, vehicleRegistrationNumber)
|
||
**Q:** Что делать с полями senderObjectCode и vehicleRegistrationNumber?
|
||
|
||
**A:** Оставить пустыми
|
||
|
||
---
|
||
|
||
#### 11. Предотвращение повторной отправки
|
||
**Q:** Должны ли мы предотвращать повторную отправку?
|
||
|
||
**A:** Да, блокировать повтор
|
||
|
||
---
|
||
|
||
#### 12. Текст кнопки
|
||
**Q:** Какой текст должен быть на кнопке отправки?
|
||
|
||
**A:** "Send to E-Taxes"
|
||
|
||
---
|
||
|
||
#### 13. Комментарии в инвойсе
|
||
**Q:** Что делать с полями invoiceComment и invoiceComment2?
|
||
|
||
**A:** Есть поля comment и additional_comment в Sales Invoice.
|
||
|
||
---
|
||
|
||
#### 14. Диалог подтверждения
|
||
**Q:** Показывать ли диалог подтверждения перед отправкой?
|
||
|
||
**A:** Да, два диалога подтверждения:
|
||
1. "Send this invoice to E-Taxes?"
|
||
2. "Are you sure?"
|
||
|
||
---
|
||
|
||
#### 15. Кнопка в списке инвойсов
|
||
**Q:** Нужна ли кнопка в list view (списке инвойсов)?
|
||
|
||
**A:** Пока что без этого. Возможно будет добавлено позже.
|
||
|
||
---
|
||
|
||
#### 16. Генерация ID для товаров
|
||
**Q:** Как генерировать ID для товаров (item id в формате '44f5986a-2d3c-45d6-bb14-79a899******')?
|
||
|
||
**A:** Случайный UUID
|
||
|
||
---
|
||
|
||
#### 17. Фиксированные поля JSON
|
||
**Q:** Какие значения использовать для полей type, kind, sourceSystem?
|
||
|
||
**A:** Использовать значения как в примере:
|
||
- `type` = "current"
|
||
- `kind` = "defaultInvoice"
|
||
- `sourceSystem` = "lk"
|
||
|
||
---
|
||
|
||
#### 18. Валидация данных
|
||
**Q:** Нужно ли валидировать данные перед отправкой?
|
||
|
||
**A:** Да, необходимо проверить:
|
||
- TIN клиента (tax_id)
|
||
- Product codes у товаров
|
||
- Unit codes (опционально)
|
||
|
||
---
|
||
|
||
#### 19. Маппинг единиц измерения (UOM)
|
||
**Q:** Как маппить единицы измерения из ERPNext UOM в коды E-Taxes? Например, если в Sales Invoice Item указано 'Nos' или 'Unit' - какой код использовать?
|
||
|
||
**A:** Просто брать имя единицы измерения из табличной части. Поле unit_code необязательно, E-Taxes принимает текстовое название.
|
||
|
||
---
|
||
|
||
#### 20. Pricing поля для товаров
|
||
**Q:** Что использовать для pricePerUnit и cost в items? В примере оба = 1, но у нас есть rate, amount и т.д.
|
||
|
||
**A:**
|
||
- `rate` → `pricePerUnit`
|
||
- `amount` → `cost`
|
||
|
||
---
|
||
|
||
#### 21. Кнопка после отправки
|
||
**Q:** Что делать с кнопкой после успешной отправки (ты говорил блокировать повтор)?
|
||
|
||
**A:** Заменить кнопку на индикатор статуса с информацией об отправке.
|
||
|
||
---
|
||
|
||
#### 22. Язык сообщений
|
||
**Q:** На каком языке показывать сообщения об ошибках и подтверждения?
|
||
|
||
**A:** English
|
||
|
||
---
|
||
|
||
#### 23. Receiver Name (юридическое название)
|
||
**Q:** Про receiver.name - нужно искать клиента в E-Taxes Customer Mappings, найти соответствующий E-Taxes Customers и оттуда брать name?
|
||
|
||
**A:** Да, из E-Taxes Customers. Искать соответствие в e-taxes-settings (в том, который помечен как default), там будет название из E-Taxes.
|
||
|
||
---
|
||
|
||
#### 24. Частичная ошибка (создание успешно, подписание failed)
|
||
**Q:** Если создание инвойса (invoice create) прошло успешно, но подписание (signing) провалилось - что делать?
|
||
|
||
**A:** Сохранить invoice ID и serial number, но показать ошибку подписания. Установить статус = "Created, not signed".
|
||
|
||
---
|
||
|
||
#### 25. Progress dialog
|
||
**Q:** Нужно ли показывать progress dialog во время отправки (как в существующей функции импорта)?
|
||
|
||
**A:** Простой loading индикатор (frappe.show_alert с loading).
|
||
|
||
---
|
||
|
||
#### 26. Тексты подтверждения
|
||
**Q:** Тексты для двух диалогов подтверждения - какие должны быть?
|
||
|
||
**A:**
|
||
1. Первый диалог: "Send this invoice to E-Taxes?"
|
||
2. Второй диалог: "Are you sure?"
|
||
|
||
---
|
||
|
||
#### 27. Tracking doctype
|
||
**Q:** Нужно ли создавать tracking doctype (типа E-Taxes Sales) для отправленных инвойсов?
|
||
|
||
**A:** Да, создать **E-Taxes Sales Outbox** для отслеживания отправленных инвойсов.
|
||
|
||
---
|
||
|
||
### Техническая информация по API Endpoints
|
||
|
||
#### 1. Generate Serial Number
|
||
```
|
||
POST https://new.e-taxes.gov.az/api/po/invoice/public/v1/generateSerialNumber/defaultInvoice
|
||
```
|
||
**Response:**
|
||
```json
|
||
{
|
||
"serialNumber": "MT25121128****"
|
||
}
|
||
```
|
||
|
||
#### 2. Create Invoice
|
||
```
|
||
POST https://new.e-taxes.gov.az/api/po/invoice/public/v2/invoice
|
||
```
|
||
**Response:**
|
||
```json
|
||
{
|
||
"id": "b7bd1f20-4bdb-4e7b-bf18-c1f9bd******"
|
||
}
|
||
```
|
||
|
||
#### 3. Sign Invoice
|
||
```
|
||
POST https://new.e-taxes.gov.az/api/po/invoice/public/v1/invoice/sign/withAsanImza
|
||
```
|
||
**Request:**
|
||
```json
|
||
{
|
||
"type": "send",
|
||
"invoiceIds": ["invoice-id-here"]
|
||
}
|
||
```
|
||
**Response:**
|
||
```json
|
||
{
|
||
"status": "ok",
|
||
"error": null,
|
||
"verificationCode": "4147"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Примеры файлов с данными
|
||
|
||
**Пример создания инвойса:**
|
||
`/home/frappe/frappe-bench/apps/invoice_az/invoice.txt`
|
||
|
||
**Пример подписания:**
|
||
`/home/frappe/frappe-bench/apps/invoice_az/invoice asan.txt`
|
||
|
||
**Кастомные поля:**
|
||
`/home/frappe/frappe-bench/apps/jey_erp/jey_erp/custom_fields.py`
|
||
|
||
---
|
||
|
||
### Существующие поля
|
||
|
||
**Sales Invoice Item:**
|
||
- vat_18_percent_with_amount
|
||
- vat_amount
|
||
- vat_0_percent_with_amount
|
||
- amount_without_vat
|
||
- vat_free_amount
|
||
- tax_article
|
||
|
||
**Sales Invoice:**
|
||
- comment
|
||
- additional_comment
|
||
|
||
**Customer:**
|
||
- tax_id (TIN)
|
||
|
||
**Item:**
|
||
- product_group_code
|
||
|
||
---
|
||
|
||
### Workflow процесса отправки Sales Invoice
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 1. Пользователь нажимает "Send to E-Taxes" │
|
||
│ на submitted Sales Invoice │
|
||
└─────────────────────┬───────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 2. Первый диалог подтверждения: │
|
||
│ "Send this invoice to E-Taxes?" │
|
||
└─────────────────────┬───────────────────────────────────┘
|
||
│ Да
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 3. Второй диалог подтверждения: │
|
||
│ "Are you sure?" │
|
||
└─────────────────────┬───────────────────────────────────┘
|
||
│ Да
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 4. Показать loading индикатор │
|
||
└─────────────────────┬───────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 5. Валидация данных: │
|
||
│ ✓ Проверка tax_id клиента (TIN) │
|
||
│ ✓ Проверка product_group_code у всех товаров │
|
||
│ ✓ Проверка что инвойс еще не отправлен │
|
||
│ (блокировка повтора через Outbox) │
|
||
└─────────────────────┬───────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 6. Получение serial number от API │
|
||
│ POST /generateSerialNumber/defaultInvoice │
|
||
└─────────────────────┬───────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 7. Формирование JSON payload: │
|
||
│ │
|
||
│ receiver: │
|
||
│ - name: из E-Taxes Customers (через mapping) │
|
||
│ - tin: из tax_id │
|
||
│ - objectName: из custom field │
|
||
│ │
|
||
│ items: (для каждого item) │
|
||
│ - id: случайный UUID │
|
||
│ - productName: item_name │
|
||
│ - productGroupCode: product_group_code │
|
||
│ - quantity: qty │
|
||
│ - pricePerUnit: rate │
|
||
│ - cost: amount │
|
||
│ - unit: UOM name (текст) │
|
||
│ │
|
||
│ VAT mapping: │
|
||
│ - vat_18_percent_with_amount → vat18 │
|
||
│ - vat_0_percent_with_amount → vat0 │
|
||
│ - amount_without_vat → exempt │
|
||
│ - vat_free_amount → vatFree │
|
||
│ │
|
||
│ comments: │
|
||
│ - invoiceComment: comment │
|
||
│ - invoiceComment2: additional_comment │
|
||
│ │
|
||
│ Фиксированные значения: │
|
||
│ - type: "current" │
|
||
│ - kind: "defaultInvoice" │
|
||
│ - sourceSystem: "lk" │
|
||
│ - senderObjectCode: "" (пусто) │
|
||
│ - vehicleRegistrationNumber: "" (пусто) │
|
||
└─────────────────────┬───────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 8. Отправка на создание инвойса │
|
||
│ POST /api/po/invoice/public/v2/invoice │
|
||
│ Получение invoice_id │
|
||
└─────────────────────┬───────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 9. Автоматическое подписание инвойса │
|
||
│ POST /invoice/sign/withAsanImza │
|
||
│ Body: {"type": "send", "invoiceIds": [...]} │
|
||
│ Получение verification_code │
|
||
└─────────────────────┬───────────────────────────────────┘
|
||
│
|
||
├─ Успех ────────────────────────────┐
|
||
│ │
|
||
│ Статус: "Sent and Signed" │
|
||
│ │
|
||
└─ Ошибка подписания ───────────────┤
|
||
│
|
||
Статус: "Created, not signed" │
|
||
Сохранить ID и serial number │
|
||
Показать ошибку │
|
||
│
|
||
┌────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 10. Создание E-Taxes Sales Outbox record │
|
||
│ - sales_invoice: link to Sales Invoice │
|
||
│ - etaxes_invoice_id: invoice ID │
|
||
│ - serial_number: serial number │
|
||
│ - verification_code: verification code (if signed) │
|
||
│ - send_date: now() │
|
||
│ - send_status: статус отправки │
|
||
│ - error_message: сообщение об ошибке (if any) │
|
||
└─────────────────────┬───────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 11. Сохранение результатов в Sales Invoice │
|
||
│ (опционально - через custom fields или связь) │
|
||
└─────────────────────┬───────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 12. Заменить кнопку на индикатор статуса │
|
||
│ Показать: │
|
||
│ - Invoice ID │
|
||
│ - Serial Number │
|
||
│ - Verification Code │
|
||
│ - Статус отправки │
|
||
└─────────────────────┬───────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ 13. Показать сообщение об успехе пользователю │
|
||
│ "Invoice successfully sent to E-Taxes" │
|
||
│ или сообщение об ошибке │
|
||
└─────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
### Важные замечания по реализации
|
||
|
||
1. **Двойное подтверждение** - обязательно два диалога для предотвращения случайной отправки
|
||
2. **Блокировка повтора** - проверять существование записи в E-Taxes Sales Outbox
|
||
3. **Частичные ошибки** - если создание прошло, но подписание нет - сохранить данные и показать ошибку
|
||
4. **Маппинг клиента** - брать название из E-Taxes Customers через маппинг в E-Taxes Settings
|
||
5. **Единицы измерения** - можно использовать текстовое название, unit_code необязателен
|
||
6. **VAT поля** - использовать существующие кастомные поля из Sales Invoice Item
|
||
7. **UUID для items** - генерировать случайный UUID для каждого товара
|
||
8. **Сообщения на английском** - все пользовательские сообщения на английском языке
|
||
9. **Loading индикатор** - простой frappe.show_alert, не сложный progress dialog
|
||
10. **Tracking** - создать E-Taxes Sales Outbox для истории отправок
|
||
|
||
---
|
||
|
||
## Контакты и поддержка
|
||
|
||
**Автор:** Jey ERP
|
||
**Email:** info@jeyerp.az
|
||
**Website:** https://jeyerp.az
|
||
|
||
**E-Taxes (Правительство Азербайджана):**
|
||
**Website:** https://new.e-taxes.gov.az
|
||
**ASAN Login:** https://asan.gov.az
|
||
|
||
---
|
||
|
||
## Лицензия
|
||
|
||
Проект распространяется под лицензией **Unlicense** (общественное достояние).
|
||
|
||
```
|
||
This is free and unencumbered software released into the public domain.
|
||
|
||
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||
distribute this software, either in source code form or as a compiled
|
||
binary, for any purpose, commercial or non-commercial, and by any means.
|
||
```
|
||
|
||
Полный текст лицензии см. в файле `license.txt`.
|
||
|
||
---
|
||
|
||
## Заключение
|
||
|
||
Проект **Invoice Az** представляет собой комплексное решение для интеграции Frappe/ERPNext с государственной системой электронного налогообложения Азербайджана E-Taxes.
|
||
|
||
### Ключевые преимущества:
|
||
|
||
1. **Полная автоматизация** - импорт входящих и экспорт исходящих счетов
|
||
2. **Интеллектуальное сопоставление** - автоматическое сопоставление данных с точностью 95%
|
||
3. **Безопасность** - интеграция с государственной системой ASAN Login
|
||
4. **Масштабируемость** - обработка до 200 счетов за запрос
|
||
5. **Надежность** - автоматическое обновление токенов, обработка ошибок, логирование
|
||
6. **Удобство** - интуитивный интерфейс, массовые операции, подробные подсказки
|
||
|
||
### Технические характеристики:
|
||
|
||
- **17 кастомных DocTypes** для хранения данных
|
||
- **7,198 строк Python кода** для бизнес-логики
|
||
- **9,786 строк JavaScript кода** для клиентской части
|
||
- **3 файла документации** (50 КБ) для пользователей и разработчиков
|
||
- **Полная интеграция** с стандартными DocTypes ERPNext
|
||
|
||
Проект активно развивается и поддерживается командой Jey ERP.
|
||
|
||
---
|
||
|
||
*Версия документа: 1.0*
|
||
*Дата создания: 2025-12-16*
|
||
*Автор документа: Claude Code*
|