added invoice sending to etaxes

This commit is contained in:
Ali 2025-12-19 15:39:51 +04:00
parent 0896971525
commit bd9bf9e2cc
26 changed files with 246354 additions and 1377 deletions

1974
PROJECT_OVERVIEW.md Normal file

File diff suppressed because it is too large Load Diff

351
QA_SEND_INVOICE.md Normal file
View File

@ -0,0 +1,351 @@
# Вопросы и Ответы - Отправка инвойсов в E-Taxes
Дата: 2025-12-15
Задача: Добавить функционал отправки Sales Invoice в E-Taxes
## Технические Вопросы и Ответы
### 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.
---
### 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, там вроде бы уже созданы поля для этих целей, вот код из этого файла:
```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'
),
dict(
fieldname='vat_0_percent_with_amount',
label='VAT 0% with amount',
fieldtype='Currency',
insert_after='vat_amount',
in_list_view=1,
read_only=1,
columns=2,
precision=2
),
dict(
fieldname='amount_without_vat',
label='Amount without VAT',
fieldtype='Currency',
insert_after='vat_0_percent_with_amount',
in_list_view=1,
read_only=1,
columns=2,
precision=2
),
dict(
fieldname='vat_free_amount',
label='VAT free amount',
fieldtype='Currency',
insert_after='amount_without_vat',
in_list_view=1,
read_only=1,
columns=2,
precision=2
),
dict(
fieldname='tax_article',
label='Tax Article',
fieldtype='Link',
options='Tax Article',
insert_after='vat_free_amount',
in_list_view=1,
columns=2,
description='Tax Article field for VAT purposes'
)
]
```
---
### 9. Unit Code для единиц измерения
**Q:** Откуда брать unitCode для единиц измерения?
**A:** Вот список всех единиц измерений и их кодов из сайта:
```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.
---
### 14. Диалог подтверждения
**Q:** Показывать ли диалог подтверждения перед отправкой?
**A:** Да, аж два, типа "вы точно уверены?" "точно при точно??" (ну не дословно так, но будет 2 вопроса).
---
### 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 клиента
- Проверить product codes
- Проверить unit codes
---
## Техническая Информация
### API Endpoints:
1. **Generate Serial Number**: `https://new.e-taxes.gov.az/api/po/invoice/public/v1/generateSerialNumber/defaultInvoice`
- Response: `{"serialNumber":"MT25121128****"}`
2. **Create Invoice**: `https://new.e-taxes.gov.az/api/po/invoice/public/v2/invoice`
- Response: `{"id":"b7bd1f20-4bdb-4e7b-bf18-c1f9bd******"}`
3. **Sign Invoice**: `https://new.e-taxes.gov.az/api/po/invoice/public/v1/invoice/sign/withAsanImza`
- Request: `{"type": "send", "invoiceIds": ["invoice-id-here"]}`
- Response: `{"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
- tax_id (в Customer)
### Существующие поля в Item:
- product_group_code
---
### 19. Маппинг единиц измерения (UOM)
**Q:** Как маппить единицы измерения из ERPNext UOM в коды E-Taxes? Например, если в Sales Invoice Item указано 'Nos' или 'Unit' - какой код использовать?
**A:** Короче, я только что путем эксперементов узнал что оказвыается поле unit_code необязательно, и вообще тот список ед. измерений который я тебе скинул - это просто шаблоны из сайта, то есть можно просто текстом написать ədəd, и сайт пропустит это, даже без кода, или вообще написать test123, и сайт всеравно пропустит. ПОэтому просто бери имя единицы измерений из табличной части и все.
---
### 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), и там будет 100% название из сайта.
---
### 24. Частичная ошибка (создание успешно, подписание failed)
**Q:** Если создание инвойса (invoice create) прошло успешно, но подписание (signing) провалилось - что делать?
**A:** Сохранить ID, показать ошибку. Сохранить 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 для отслеживания отправленных инвойсов)
---
## Workflow процесса отправки:
1. Пользователь нажимает "Send to E-Taxes" на submitted Sales Invoice
2. Первый диалог подтверждения: 'Send this invoice to E-Taxes?'
3. Второй диалог подтверждения: 'Are you sure?'
4. Показать простой loading индикатор
5. Валидация данных:
- Проверка TIN клиента (tax_id)
- Проверка product_group_code у всех товаров
- Проверка что инвойс еще не отправлен (блокировка повтора)
6. Получение serial number от API
7. Формирование JSON с данными инвойса:
- receiver.name - из E-Taxes Customers (через mapping)
- receiver.tin - из tax_id
- receiver.objectName - из custom field в Sales Invoice
- items: pricePerUnit = rate, cost = amount, unit = UOM name (текст)
- VAT: vat_18_percent_with_amount -> vat18, vat_0_percent_with_amount -> vat0, etc.
- comments: comment и additional_comment
8. Отправка на создание инвойса
9. Автоматическое подписание инвойса
10. Создание E-Taxes Sales Outbox tracking document
11. Сохранение результатов в Sales Invoice:
- Invoice ID
- Serial Number
- Verification Code (если успешно)
- Статус отправки ('Sent and Signed' или 'Created, not signed')
12. Заменить кнопку на индикатор статуса
13. Показать успешное сообщение пользователю

535
claude.md Normal file
View File

@ -0,0 +1,535 @@
# Invoice Az - Система интеграции с электронными налогами Азербайджана
**Версия:** 0.0.1
**Автор:** Jey ERP (info@jeyerp.az)
**Лицензия:** Unlicense (Public Domain)
**Минимальная версия Python:** 3.10+
**Фреймворк:** Frappe/ERPNext
**Последнее обновление:** 2025-12-15
---
## Оглавление
1. [Введение и обзор проекта](#1-введение-и-обзор-проекта)
- 1.1 [Назначение приложения](#11-назначение-приложения)
- 1.2 [Ключевые возможности](#12-ключевые-возможности)
- 1.3 [Целевая аудитория](#13-целевая-аудитория)
- 1.4 [Бизнес-ценность](#14-бизнес-ценность)
2. [Архитектура системы](#2-архитектура-системы)
- 2.1 [Общая архитектура](#21-общая-архитектура)
- 2.2 [Слои приложения](#22-слои-приложения)
- 2.3 [Поток данных](#23-поток-данных)
- 2.4 [Технологический стек](#24-технологический-стек)
3. [Система аутентификации (auth.py)](#3-система-аутентификации-authpy)
- 3.1 [Поток ASAN Login](#31-поток-asan-login)
- 3.2 [Документация функций](#32-документация-функций)
- 3.3 [Управление токенами](#33-управление-токенами)
4. [API для накладных на покупку (api.py)](#4-api-для-накладных-на-покупку-apipy)
- 4.1 [Категория A: Получение накладных](#41-категория-a-получение-накладных)
- 4.2 [Категория B: Управление товарами](#42-категория-b-управление-товарами)
- 4.3 [Категория C: Управление единицами измерения](#43-категория-c-управление-единицами-измерения)
- 4.4 [Категория D: Управление контрагентами](#44-категория-d-управление-контрагентами)
- 4.5 [Категория E: Создание документов покупки](#45-категория-e-создание-документов-покупки)
- 4.6 [Категория F: Интеграция E-Taxes Purchase](#46-категория-f-интеграция-e-taxes-purchase)
- 4.7 [Категория G: Комбинированная загрузка данных](#47-категория-g-комбинированная-загрузка-данных)
- 4.8 [Категория H: Управление справочными данными](#48-категория-h-управление-справочными-данными)
- 4.9 [Категория I: Вспомогательные функции](#49-категория-i-вспомогательные-функции)
5. [API для накладных на продажу (sales_api.py)](#5-api-для-накладных-на-продажу-sales_apipy)
- 5.1 [Общие функции](#51-общие-функции)
- 5.2 [Функции получения данных](#52-функции-получения-данных)
- 5.3 [Функции создания документов](#53-функции-создания-документов)
6. [Документация DocTypes](#6-документация-doctypes)
- 6.1 [Конфигурационные DocTypes](#61-конфигурационные-doctypes)
- 6.2 [Справочные данные](#62-справочные-данные)
- 6.3 [Маппинг DocTypes](#63-маппинг-doctypes)
7. [Frontend JavaScript документация](#7-frontend-javascript-документация)
- 7.1 [Общий модуль (etaxes_common.js)](#71-общий-модуль-etaxes_commonjs)
- 7.2 [Документо-специфический JavaScript](#72-документо-специфический-javascript)
- 7.3 [List View JavaScript](#73-list-view-javascript)
8. [Интеграция и хуки](#8-интеграция-и-хуки)
- 8.1 [Frappe Hooks](#81-frappe-hooks)
- 8.2 [Пользовательские поля](#82-пользовательские-поля)
9. [Конфигурация и руководство по настройке](#9-конфигурация-и-руководство-по-настройке)
- 9.1 [Установка](#91-установка)
- 9.2 [Начальная конфигурация](#92-начальная-конфигурация)
- 9.3 [Устранение неполадок](#93-устранение-неполадок)
10. [Примеры использования и рабочие процессы](#10-примеры-использования-и-рабочие-процессы)
- 10.1 [Рабочий процесс импорта накладной на покупку](#101-рабочий-процесс-импорта-накладной-на-покупку)
- 10.2 [Рабочий процесс экспорта накладной на продажу](#102-рабочий-процесс-экспорта-накладной-на-продажу)
- 10.3 [Массовая загрузка справочных данных](#103-массовая-загрузка-справочных-данных)
- 10.4 [Управление маппингом](#104-управление-маппингом)
11. [Справочник API и модели данных](#11-справочник-api-и-модели-данных)
- 11.1 [Конечные точки E-Taxes API](#111-конечные-точки-e-taxes-api)
- 11.2 [Модели данных](#112-модели-данных)
12. [Продвинутые темы](#12-продвинутые-темы)
- 12.1 [Оптимизация производительности](#121-оптимизация-производительности)
- 12.2 [Паттерны обработки ошибок](#122-паттерны-обработки-ошибок)
- 12.3 [Соображения безопасности](#123-соображения-безопасности)
- 12.4 [Нормализация азербайджанских символов](#124-нормализация-азербайджанских-символов)
- 12.5 [Расширяемость](#125-расширяемость)
13. [Приложения](#13-приложения)
- 13.1 [Примеры кода](#131-примеры-кода)
- 13.2 [FAQ (Часто задаваемые вопросы)](#132-faq-часто-задаваемые-вопросы)
- 13.3 [Глоссарий](#133-глоссарий)
- 13.4 [Журнал изменений](#134-журнал-изменений)
---
## 1. Введение и обзор проекта
### 1.1 Назначение приложения
**Invoice Az** — это специализированное приложение для Frappe/ERPNext, предназначенное для интеграции с государственной системой электронного налогообложения Азербайджана (**E-Taxes**, доступна по адресу [https://new.e-taxes.gov.az](https://new.e-taxes.gov.az)).
Приложение обеспечивает полный цикл работы с электронными налоговыми накладными:
- **Автоматическое получение** входящих накладных от поставщиков (inbox/покупки)
- **Загрузка исходящих** накладных для клиентов (outbox/продажи)
- **Извлечение справочных данных** (товары, единицы измерения, контрагенты)
- **Интеллектуальный маппинг** между данными E-Taxes и ERPNext
- **Автоматическое создание** документов Purchase Order, Purchase Invoice, Sales Order, Sales Invoice
### 1.2 Ключевые возможности
#### Двунаправленная синхронизация
- **Входящие накладные (Inbox):** Автоматическая загрузка накладных от поставщиков с возможностью создания Purchase Orders и Purchase Invoices
- **Исходящие накладные (Outbox):** Загрузка накладных, отправленных клиентам, с созданием Sales Orders и Sales Invoices
#### Аутентификация через ASAN Login
- Интеграция с системой **ASAN** (Azerbaijan State Authentication and Notification)
- Многошаговая аутентификация с подтверждением через мобильное приложение
- Выбор цифрового сертификата и налогоплательщика
- Автоматическое обновление токенов каждые 4 минуты
#### Интеллектуальный маппинг данных
- **Fuzzy matching** (нечеткое сопоставление) с порогом схожести 95%
- Поддержка азербайджанских символов (ə, ö, ü, ğ, ş, ç, ı)
- Автоматическое создание товаров, единиц измерения, клиентов и поставщиков
- Ручное управление маппингом через удобный интерфейс
#### Массовая обработка данных
- Пакетная загрузка до 200 накладных за запрос
- Поддержка пагинации для больших объемов данных
- Прогресс-бары и возможность отмены длительных операций
- Кэширование для оптимизации производительности (5-минутный кэш)
#### Отслеживание и логирование
- Детальное логирование всех операций с E-Taxes API
- Отслеживание активности пользователей
- Сохранение истории маппинга
- Связывание документов ERPNext с записями E-Taxes
### 1.3 Целевая аудитория
Приложение предназначено для:
1. **Азербайджанских компаний**, использующих Frappe/ERPNext для управления бизнесом
2. **Бухгалтеров и финансистов**, работающих с электронными налоговыми накладными
3. **Системных администраторов**, настраивающих интеграцию между ERP и государственными системами
4. **Разработчиков**, желающих расширить функциональность или понять архитектуру интеграции
**Предварительные требования:**
- Знание Frappe/ERPNext framework
- Понимание бизнес-процессов закупок и продаж
- Базовые знания о системе E-Taxes Азербайджана
- Зарегистрированный аккаунт ASAN Login
### 1.4 Бизнес-ценность
#### Экономия времени
- **Автоматизация ввода данных:** Устраняет необходимость ручного переноса данных из E-Taxes в ERPNext
- **Массовая обработка:** Одновременная загрузка сотен накладных вместо ручной обработки каждой
- **Автоматический маппинг:** Интеллектуальное сопоставление товаров и контрагентов с точностью 95%
#### Снижение ошибок
- **Исключение человеческого фактора:** Данные переносятся напрямую из E-Taxes без ручного ввода
- **Валидация данных:** Автоматическая проверка корректности товаров, цен, контрагентов
- **Отслеживание истории:** Все операции логируются для аудита
#### Соответствие требованиям
- **Налоговая отчетность:** Полное соответствие требованиям налоговой службы Азербайджана
- **Своевременность:** Оперативное получение и обработка налоговых накладных
- **Прозрачность:** Четкая связь между документами ERPNext и записями в E-Taxes
#### Масштабируемость
- **Большие объемы:** Обработка тысяч накладных в месяц
- **Множество контрагентов:** Автоматическое создание и маппинг сотен поставщиков и клиентов
- **Расширяемость:** Возможность добавления пользовательской логики обработки
---
## 2. Архитектура системы
### 2.1 Общая архитектура
Invoice Az построено по принципу многослойной архитектуры, обеспечивающей четкое разделение ответственности между компонентами:
```
┌─────────────────────────────────────────────────────────────────┐
│ ВНЕШНИЙ СЛОЙ │
│ E-Taxes API (new.e-taxes.gov.az) │
│ - Аутентификация (ASAN Login) │
│ - Накладные (inbox/outbox) │
│ - Справочники (товары, единицы, контрагенты) │
└────────────────────────┬────────────────────────────────────────┘
│ HTTPS REST API
│ Bearer Token Authentication
┌─────────────────────────────────────────────────────────────────┐
│ СЛОЙ АУТЕНТИФИКАЦИИ │
│ auth.py (17 функций) │
│ - Получение токенов │
│ - Автообновление (каждые 4 минуты) │
│ - Управление сертификатами │
│ - Отслеживание активности │
└────────────────────────┬────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ BACKEND СЛОЙ │
│ ┌───────────────┬──────────────────┬─────────────────┐ │
│ │ api.py │ sales_api.py │ hooks.py │ │
│ │ 42 функции │ 10 функций │ Интеграция │ │
│ │ - Получение │ - Продажи │ - События │ │
│ │ - Маппинг │ - Outbox │ - Cron │ │
│ │ - Создание │ - SO/SI │ - Клиент JS │ │
│ │ документов │ создание │ │ │
│ └───────────────┴──────────────────┴─────────────────┘ │
└────────────────────────┬────────────────────────────────────────┘
│ Frappe ORM
┌─────────────────────────────────────────────────────────────────┐
│ СЛОЙ ДАННЫХ │
│ 16 Custom DocTypes │
│ ┌─────────────────┬──────────────────┬───────────────────┐ │
│ │ Конфигурация │ Справочники │ Маппинг │ │
│ │ - Asan Login │ - E-Taxes Item │ - Item Mapping │ │
│ │ - E-Taxes │ - E-Taxes Unit │ - Unit Mapping │ │
│ │ Settings │ - E-Taxes │ - Customer │ │
│ │ - E-Taxes │ Customers │ Mappings │ │
│ │ Purchase │ - E-Taxes │ - Supplier │ │
│ │ - E-Taxes Sales │ Suppliers │ Mappings │ │
│ │ │ - E-Taxes │ - Party Mapping │ │
│ │ │ Parties │ │ │
│ └─────────────────┴──────────────────┴───────────────────┘ │
│ MariaDB через Frappe ORM │
└────────────────────────┬────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ FRONTEND СЛОЙ │
│ JavaScript Client Scripts (21 файл, ~8000 строк) │
│ ┌────────────────────┬─────────────────┬──────────────────┐ │
│ │ Общие утилиты │ Документы │ List Views │ │
│ │ etaxes_common.js │ - PO (1532 л.) │ - Items (979 л.) │ │
│ │ (1052 строки) │ - SO (1585 л.) │ - Units (905 л.) │ │
│ │ - Кэш │ - PI (47 л.) │ - Customers │ │
│ │ - Диалоги │ - SI (47 л.) │ (915 л.) │ │
│ │ - API вызовы │ │ - Suppliers │ │
│ │ - Форматирование │ │ (915 л.) │ │
│ └────────────────────┴─────────────────┴──────────────────┘ │
│ Frappe UI Framework │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ ИНТЕГРАЦИОННЫЙ СЛОЙ │
│ Frappe Hooks & Event Handlers │
│ - doc_events: PO/SO on_trash, on_cancel │
│ - scheduler_events: Token renewal (*/4 * * * *) │
│ - doctype_js: Custom client scripts │
│ - doctype_list_js: Custom list views │
│ - after_install/migrate: Setup scheduler │
└─────────────────────────────────────────────────────────────────┘
```
### 2.2 Слои приложения
#### Внешний слой: E-Taxes API
**Описание:** Государственная система электронного налогообложения Азербайджана.
**Конечные точки API:**
- `POST /api/po/auth/public/v1/...` — Аутентификация и управление токенами
- `POST /api/po/invoice/public/v2/invoice/find.inbox` — Получение входящих накладных
- `POST /api/po/invoice/public/v2/invoice/find.outbox` — Получение исходящих накладных
- `GET /api/po/invoice/public/v2/invoice/{id}` — Детали накладной
**Аутентификация:** Bearer Token в заголовке `x-authorization`
#### Слой аутентификации: auth.py
**Ответственность:**
- Получение bearer токена через ASAN Login
- Автоматическое обновление main_token каждые 4 минуты через cron
- Управление цифровыми сертификатами
- Выбор налогоплательщика
- Отслеживание активности пользователя
- Обработка ошибок 401 Unauthorized
**Ключевые компоненты:**
- 17 функций аутентификации
- Retry логика (до 3 попыток)
- Activity timeout (настраиваемый параметр)
#### Backend слой: Python модули
**api.py (4,320 строк, 42 функции):**
- Получение накладных с фильтрацией
- Извлечение товаров, единиц измерения, контрагентов
- Интеллектуальный маппинг (fuzzy matching, 95% threshold)
- Создание Purchase Orders и Purchase Invoices
- Массовая обработка данных
**sales_api.py (887 строк, 10 функций):**
- Получение исходящих накладных (outbox)
- Создание Sales Orders и Sales Invoices
- Маппинг клиентов
- Обработка продаж аналогично покупкам
**hooks.py:**
- Регистрация обработчиков событий
- Настройка cron заданий
- Интеграция клиентских скриптов
- Инициализация при установке/обновлении
#### Слой данных: 16 Custom DocTypes
**Конфигурационные (4):**
- Asan Login — хранение токенов и сертификатов
- E-Taxes Settings — центральная конфигурация
- E-Taxes Purchase — отслеживание покупок
- E-Taxes Sales — отслеживание продаж
**Справочные данные (5):**
- E-Taxes Item — товары из E-Taxes
- E-Taxes Unit — единицы измерения
- E-Taxes Customers — клиенты
- E-Taxes Suppliers — поставщики
- E-Taxes Parties — универсальные контрагенты
**Маппинг (5 дочерних таблиц):**
- E-Taxes Item Mapping
- E-Taxes Unit Mapping
- E-Taxes Customer Mappings
- E-Taxes Supplier Mappings
- E-Taxes Party Mapping
**Тестирование (1):**
- TestAPI — для разработки и отладки
#### Frontend слой: JavaScript
**Общие утилиты (etaxes_common.js, 1052 строки):**
- Кэширование (5 минут)
- Управление диалогами (прогресс, ошибки)
- API вызовы с обработкой ошибок
- Форматирование валюты (AZN)
- Rate limiting
**Документо-специфические скрипты:**
- purchase_order.js (1,532 строки) — "Load from E-Taxes", браузер накладных, импорт
- sales_order.js (1,585 строк) — аналогично для продаж
- purchase_invoice.js, sales_invoice.js — легкая интеграция
**List View скрипты:**
- Массовый маппинг через UI
- Автоматическое сопоставление
- Создание новых записей
- Индикаторы статуса (New/Mapped/Processing)
#### Интеграционный слой: Frappe Hooks
- **doc_events:** Автоматическая очистка связей при удалении PO/SO
- **scheduler_events:** Cron задание обновления токена (*/4 * * * *)
- **doctype_js/list_js:** Подключение клиентских скриптов
- **after_install/migrate:** Настройка планировщика
### 2.3 Поток данных
#### Входящий поток (Покупки / Inbox)
```
1. Пользователь открывает Purchase Order
2. Нажимает кнопку "Load from E-Taxes" (JavaScript)
3. Frontend → Backend: load_items_from_invoices()
4. auth.py: record_etaxes_activity()
5. auth.py: get_default_asan_login() [с кэшем 5 мин]
6. api.py: get_invoices(token, filters)
7. E-Taxes API: POST /invoice/find.inbox
8. E-Taxes возвращает список накладных
9. Если 401: автоматический refresh токена и повтор
10. Frontend отображает список накладных
11. Пользователь выбирает накладную → get_invoice_details()
12. E-Taxes API возвращает детали (товары, цены, контрагент)
13. Пользователь нажимает "Import"
14. import_invoice_with_mapping() проверяет маппинг:
- Товары: etaxes_item_name → erp_item
- Контрагент: etaxes_party_name → mapped_supplier
- Единицы: etaxes_unit_name → mapped_unit
15. Если есть несопоставленные:
- Показать диалог маппинга
- Пользователь выбирает/создает товары
16. Создание Purchase Order:
- Supplier из маппинга
- Items с quantities/rates
- Warehouse, schedule_date
17. Создание E-Taxes Purchase record
18. Связывание PO ↔ E-Taxes Purchase
19. Сохранение, отображение результата
```
#### Исходящий поток (Продажи / Outbox)
```
1. Аналогично входящему, но:
- get_sales_invoices() вместо get_invoices()
- /invoice/find.outbox endpoint
- Customer mapping вместо Supplier
- Sales Order/Invoice вместо Purchase Order/Invoice
```
#### Поток справочных данных
```
1. Пользователь открывает E-Taxes Settings
2. Выбирает вкладку "Items" / "Parties" / "Units"
3. Устанавливает date_from, date_to
4. Нажимает "Load Items from E-Taxes"
5. load_combined_data_from_etaxes() запускается:
- Загрузка накладных пакетами (200 шт.)
- Извлечение товаров из каждой накладной
- process_single_invoice_for_items()
6. Создание E-Taxes Item records (status: New)
7. Пользователь нажимает "Match Similar Items"
8. match_similar_items():
- Нормализация азербайджанских символов
- SequenceMatcher с threshold 95%
- Автоматическое создание маппинга
9. Обновление статусов: New → Mapped
10. Для оставшихся: create_unmapped_items()
- Создание новых Item в ERPNext
- Автоматический маппинг
```
#### Поток обновления токена (Cron)
```
Каждые 4 минуты (*/4 * * * *):
1. renew_token() вызывается планировщиком
2. check_recent_activity():
- Если нет активности > ACTIVITY_TIMEOUT → skip
3. Получение текущего токена из Asan Login
4. POST /api/po/auth/public/v1/renew
Headers: { x-authorization: Bearer {current_token} }
5. E-Taxes возвращает новый токен в заголовке x-authorization
6. Обновление main_token в БД через frappe.db.set_value()
7. Commit транзакции
8. Если 401: установить auth_status = "Not Authenticated"
Если 500: retry с экспоненциальной задержкой (до 3 раз)
```
### 2.4 Технологический стек
#### Backend
- **Python 3.10+** — основной язык программирования
- **Frappe Framework** — фреймворк для бизнес-приложений
- **Frappe ORM** — объектно-реляционное отображение для работы с БД
- **requests** library — HTTP клиент для вызовов E-Taxes API
- **difflib.SequenceMatcher** — fuzzy matching для маппинга
- **re (regex)** — обработка строк и валидация
- **json** — сериализация/десериализация данных
#### Frontend
- **JavaScript ES6+** — клиентская логика
- **Frappe UI Framework** — компоненты интерфейса
- **jQuery** (через Frappe) — DOM манипуляции
- **frappe.call()** — AJAX вызовы к backend
#### База данных
- **MariaDB** — основная БД
- **InnoDB engine** — для всех doctypes
- **Индексы** на etaxes_item_name, etaxes_unit_name, etaxes_party_name
#### Внешние API
- **E-Taxes REST API** (new.e-taxes.gov.az)
- Authentication: Bearer Token
- Format: JSON
- Protocol: HTTPS
- Rate limiting: 100ms между запросами
#### Инструменты разработки
- **ruff** — Python linting
- **eslint** — JavaScript linting
- **prettier** — форматирование кода
- **pyupgrade** — модернизация Python синтаксиса
- **pre-commit** — hooks для контроля качества кода
#### Планировщик
- **Frappe Scheduler** — cron-like задания
- **Background Jobs** — длительные операции
- **Queue Workers** — асинхронная обработка
#### Логирование и мониторинг
- **frappe.log_error()** — логирование ошибок в Error Log doctype
- **frappe.logger()** — информационное логирование
- **Activity tracking** — отслеживание времени последней активности
---
окумент продолжается... (это только первые 2 раздела из 13)_
_Размер текущего документа: ~1,400 строк_
_Оценка финального размера: ~15,000-20,000 строк_
рогресс: 7% завершено_
---
**Следующие разделы:**
- Раздел 3: Подробная документация всех 17 функций auth.py
- Раздел 4: Подробная документация всех 42 функций api.py
- Раздел 5: Подробная документация всех 10 функций sales_api.py
- Разделы 6-13: DocTypes, JavaScript, руководства, примеры, FAQ
окументация создается поэтапно. Этот файл будет обновляться по мере добавления разделов._

10
invoice asan.txt Normal file
View File

@ -0,0 +1,10 @@
{
"type": "send",
"invoiceIds": [
"b7bd1f20-4bdb-4e7b-bf18-c1f9bdbbb366"
]
}
https://new.e-taxes.gov.az/api/po/invoice/public/v1/invoice/sign/withAsanImza
{"status":"ok","error":null,"verificationCode":"4147"}

53
invoice.txt Normal file
View File

@ -0,0 +1,53 @@
{
"type": "current",
"serialNumber": "MT250711762045",
"invoiceComment": "Test",
"invoiceComment2": "Test2",
"receiver": {
"name": "\"TEST TEST\" MƏHDUD MƏSULİYYƏTLİ CƏMİYYƏTİ",
"tin": "4800******",
"objectName": "4800******-84***"
},
"senderObjectCode": "",
"vehicleRegistrationNumber": "",
"items": [
{
"id": "44f5986a-2d3c-45d6-bb14-79a899******",
"productGroupCode": "9962011100",
"productName": "Test",
"unit": "ədəd",
"unitCode": "666",
"quantity": 1,
"pricePerUnit": 1,
"cost": 1,
"exciseRate": null,
"excise": null,
"vat18": 1,
"vat0": null,
"vatFree": null,
"exempt": null,
"roadTax": null,
"barcode": null,
"returnReasonCode": null,
"packagingGoodsNumber": null,
"sourceInvoiceSerialNumber": null,
"sourceInvoiceItemId": null,
"serviceCost": null,
"commission": null,
"kind": null
}
],
"kind": "defaultInvoice",
"modificationReasonCode": "",
"resaleType": null,
"sourceInvoices": null,
"selectedProducts": [],
"senderObjectName": "",
"sourceSystem": "lk"
}
https://new.e-taxes.gov.az/api/po/invoice/public/v2/invoice
{"id":"b7bd1f20-4bdb-4e7b-bf18-c1f9bd******"}

View File

@ -726,6 +726,7 @@ def create_unmapped_items(settings_name):
product_group_code_field_exists = frappe.db.exists('Custom Field', {'dt': 'Item', 'fieldname': 'etaxes_product_group_code'})
product_group_name_field_exists = frappe.db.exists('Custom Field', {'dt': 'Item', 'fieldname': 'etaxes_product_group_name'})
eqm_code_field_exists = frappe.db.exists('Custom Field', {'dt': 'Item', 'fieldname': 'eqm_code'}) # НОВОЕ: Проверяем поле eqm_code
item_group_code_field_exists = frappe.db.exists('Custom Field', {'dt': 'Item', 'fieldname': 'product_group_code'}) # НОВОЕ: Проверяем поле product_group_code
product_group_name_field_length = 140
if product_group_name_field_exists:
@ -822,7 +823,11 @@ def create_unmapped_items(settings_name):
if eqm_code_field_exists and etaxes_item_data.eqm_code:
item_doc.eqm_code = etaxes_item_data.eqm_code
frappe.log_error(f"Added EQM Code {etaxes_item_data.eqm_code} to item {item_doc.item_name}", "EQM Code Transfer")
# НОВОЕ: Добавляем product_group_code (Item Group Code) если поле существует и код есть
if item_group_code_field_exists and etaxes_item_data.etaxes_product_group_code:
item_doc.product_group_code = etaxes_item_data.etaxes_product_group_code
# Add tax template
tax_template = individual_settings.get('item_tax_template') or settings_doc.default_item_tax_template
if tax_template and frappe.db.exists('Item Tax Template', tax_template):
@ -4299,11 +4304,11 @@ def load_single_sales_invoice_batch(date_from, date_to, offset=0, max_count=200)
def refresh_token_from_asan_login():
"""Обновляет токен из Asan Login"""
try:
asan_login = frappe.get_all('Asan Login',
asan_login = frappe.get_all('Asan Login',
filters={'is_default': 1},
fields=['name', 'main_token'],
limit=1)
if asan_login and asan_login[0].main_token:
return {
'success': True,
@ -4318,4 +4323,134 @@ def refresh_token_from_asan_login():
return {
'success': False,
'message': str(e)
}
}
@frappe.whitelist()
def test_product_groups_api(product_type="service"):
"""Test API request to get product groups from e-taxes
Args:
product_type: Type of products to fetch - "service" or "good"
"""
# Записываем активность
record_etaxes_activity()
# Получаем токен из настроек
asan_login_settings = get_default_asan_login()
if not asan_login_settings.get('found'):
return {
'success': False,
'message': 'No Asan Login settings found. Please configure authentication first.'
}
token = asan_login_settings.get('main_token')
if not token:
return {
'success': False,
'message': 'No authentication token found. Please authenticate first.'
}
# Валидация типа продукта
if product_type not in ['service', 'good']:
product_type = 'service'
# URL для запроса
url = f"https://new.e-taxes.gov.az/api/po/dictionary/public/v1/productGroups/find?queryText=&type={product_type}&scope=ALL"
# Заголовки запроса
headers = {
"Accept": "application/json, text/plain, */*",
"Content-Type": "application/json",
"Cache-Control": "no-cache",
"x-authorization": f"Bearer {token}"
}
try:
# Выполняем GET запрос
response = requests.get(url, headers=headers, timeout=30)
# Проверяем статус код 500
if response.status_code == 500:
frappe.log_error(f"[TEST_PRODUCT_GROUPS] Server error 500", "E-Taxes Server Error")
return {
'success': False,
'error': 'server_error',
'message': 'Service temporarily unavailable. Please try again in a few minutes.',
'status_code': 500
}
# Проверяем статус код 401
if response.status_code == 401:
frappe.log_error(f"[TEST_PRODUCT_GROUPS] Unauthorized 401", "E-Taxes Auth Error")
return {
'success': False,
'error': 'unauthorized',
'message': 'Authentication required. Please login again.',
'status_code': 401
}
# Проверяем успешность запроса
response.raise_for_status()
# Получаем JSON данные
data = response.json()
# Логируем успешный ответ
frappe.log_error(
f"[TEST_PRODUCT_GROUPS] Success: received {len(data) if isinstance(data, list) else 'data'} items for type={product_type}",
"E-Taxes Test API Success"
)
return {
'success': True,
'message': f'Successfully retrieved product groups (type: {product_type})',
'data': data,
'count': len(data) if isinstance(data, list) else None,
'product_type': product_type
}
except requests.exceptions.HTTPError as e:
frappe.log_error(
f"[TEST_PRODUCT_GROUPS] HTTP error: {e.response.status_code} - {str(e)}",
"E-Taxes Test API Error"
)
if e.response.status_code == 401:
return {
'success': False,
'error': 'unauthorized',
'message': 'Authentication required. Please login again.',
'status_code': 401
}
elif e.response.status_code == 500:
return {
'success': False,
'error': 'server_error',
'message': 'Service temporarily unavailable. Please try again in a few minutes.',
'status_code': 500
}
return {
'success': False,
'error': 'http_error',
'message': f'HTTP Error: {str(e)}'
}
except requests.exceptions.Timeout:
frappe.log_error(f"[TEST_PRODUCT_GROUPS] Request timeout", "E-Taxes Test API Error")
return {
'success': False,
'error': 'timeout',
'message': 'Request timeout. Please try again.'
}
except Exception as e:
frappe.log_error(
f"[TEST_PRODUCT_GROUPS] Unexpected error: {str(e)}\n{frappe.get_traceback()}",
"E-Taxes Test API Error"
)
return {
'success': False,
'error': 'unknown_error',
'message': f'An error occurred: {str(e)}'
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -34,6 +34,10 @@ doc_events = {
"on_trash": "invoice_az.sales_api.on_delete_sales_order",
"on_cancel": "invoice_az.sales_api.on_delete_sales_order"
},
"Sales Invoice": {
"on_trash": "invoice_az.send_sales_api.on_delete_sales_invoice",
"on_cancel": "invoice_az.send_sales_api.on_delete_sales_invoice"
},
"E-Taxes Settings": {
"on_update": "invoice_az.api.update_mapped_statuses"
}
@ -50,6 +54,15 @@ scheduler_events = {
after_install = "invoice_az.auth.setup_token_renewal"
after_migrate = "invoice_az.auth.setup_token_renewal"
# Fixtures for master data
# ------------------------
fixtures = [
{
"doctype": "E-Taxes Item Group",
"filters": []
}
]
# Apps
# ------------------

View File

@ -66,9 +66,9 @@
},
{
"fieldname": "etaxes_product_group_code",
"fieldtype": "Data",
"fieldtype": "Link",
"label": "Product Group Code",
"length": 50,
"options": "E-Taxes Item Group",
"read_only": 1
},
{

View File

@ -0,0 +1,8 @@
// Copyright (c) 2025, Jey ERP and contributors
// For license information, please see license.txt
frappe.ui.form.on("E-Taxes Item Group", {
refresh(frm) {
// Read-only form, no actions needed
},
});

View File

@ -0,0 +1,76 @@
{
"actions": [],
"autoname": "field:code",
"creation": "2025-12-16 12:00:00.000000",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"code",
"name_az",
"item_type"
],
"fields": [
{
"fieldname": "code",
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Item Group Code",
"length": 140,
"read_only": 1,
"reqd": 1,
"unique": 1
},
{
"fieldname": "name_az",
"fieldtype": "Text",
"in_list_view": 1,
"label": "Item Group Name (AZ)",
"read_only": 1
},
{
"fieldname": "item_type",
"fieldtype": "Data",
"label": "Item Type",
"length": 20,
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-12-16 12:00:00.000000",
"modified_by": "Administrator",
"module": "Invoice Az",
"name": "E-Taxes Item Group",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"share": 1
}
],
"search_fields": "name_az",
"show_preview_popup": 1,
"show_title_field_in_link": 1,
"sort_field": "modified",
"sort_order": "ASC",
"states": [],
"title_field": "name_az",
"track_changes": 0
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2025, Jey ERP and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class ETaxesItemGroup(Document):
pass

View File

@ -0,0 +1,9 @@
# Copyright (c) 2025, Jey ERP and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestETaxesItemGroup(FrappeTestCase):
pass

View File

@ -0,0 +1,129 @@
{
"actions": [],
"allow_rename": 0,
"autoname": "format:ETSO-{####}",
"creation": "2025-12-15 18:30:00.000000",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"sales_invoice",
"send_date",
"column_break_1",
"status",
"etaxes_section",
"etaxes_invoice_id",
"serial_number",
"column_break_2",
"verification_code",
"error_section",
"error_message"
],
"fields": [
{
"fieldname": "sales_invoice",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Sales Invoice",
"options": "Sales Invoice",
"reqd": 1,
"unique": 1
},
{
"fieldname": "etaxes_invoice_id",
"fieldtype": "Data",
"in_list_view": 1,
"label": "E-Taxes Invoice ID",
"read_only": 1
},
{
"fieldname": "serial_number",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Serial Number",
"read_only": 1
},
{
"fieldname": "verification_code",
"fieldtype": "Data",
"label": "Verification Code",
"read_only": 1
},
{
"default": "Now",
"fieldname": "send_date",
"fieldtype": "Datetime",
"label": "Send Date",
"read_only": 1
},
{
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "\nSent and Signed\nCreated, not signed\nFailed",
"read_only": 1
},
{
"collapsible": 1,
"fieldname": "error_section",
"fieldtype": "Section Break",
"label": "Error Details"
},
{
"fieldname": "error_message",
"fieldtype": "Text",
"label": "Error Message",
"read_only": 1
},
{
"fieldname": "column_break_1",
"fieldtype": "Column Break"
},
{
"fieldname": "etaxes_section",
"fieldtype": "Section Break",
"label": "E-Taxes Details"
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-12-15 18:30:00.000000",
"modified_by": "Administrator",
"module": "Invoice Az",
"name": "E-Taxes Sales Outbox",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2025, Jey ERP and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
class ETaxesSalesOutbox(Document):
pass

View File

@ -1,30 +0,0 @@
# Copyright (c) 2025, Jey ERP and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase, UnitTestCase
# On IntegrationTestCase, the doctype test records and all
# link-field test record depdendencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
class UnitTestTestApi(UnitTestCase):
"""
Unit tests for TestApi.
Use this class for testing individual functions and methods.
"""
pass
class IntegrationTestTestApi(IntegrationTestCase):
"""
Integration tests for TestApi.
Use this class for testing interactions between multiple components.
"""
pass

View File

@ -1,942 +0,0 @@
frappe.ui.form.on('TestApi', {
refresh: function(frm) {
// Остановить любые существующие интервалы
if (window.authStatusInterval) {
clearInterval(window.authStatusInterval);
window.authStatusInterval = null;
}
// Кнопка для получения токена
frm.add_custom_button(__('Login with Asan Imza'), function() {
// Проверяем, что поля phone и user_id заполнены
if (!frm.doc.phone || !frm.doc.user_id) {
frappe.msgprint(__('Please fill in Phone and User ID fields'));
return;
}
// Показываем сообщение о том, что запрос отправлен
frappe.show_alert({
message: __('Authentication request sent. Please confirm on your phone.'),
indicator: 'blue'
}, 5);
// Отправляем запрос для начала процесса аутентификации
frappe.call({
method: 'invoice_az.invoice_az.doctype.testapi.testapi.get_auth_token',
args: {
'phone': frm.doc.phone,
'user_id': frm.doc.user_id
},
callback: function(r) {
if (r.message) {
// Логируем полный ответ для отладки
console.log('Auth Response:', r.message);
// Временно сохраняем токен если он есть, но не устанавливаем статус Authenticated
const bearer_token = r.message.bearer_token || '';
if (bearer_token) {
// Сохраняем токен во временном хранилище
frm.set_value('bearer_token', bearer_token);
window.tempBearerToken = bearer_token;
}
// Устанавливаем статус и сохраняем ОДИН раз
frm.set_value('auth_status', 'Waiting for confirmation');
// Сохраняем форму и после успешного сохранения запускаем опрос
frm.save()
.then(() => {
console.log('Form saved successfully, starting polling');
startPollingWithTimeout(frm); // Используем новую функцию вместо setInterval
})
.catch(err => {
console.error('Error saving form:', err);
frappe.msgprint(__('Error saving form. Please try again.'));
});
}
}
});
}, __('Login'));
// Тестовая кнопка для проверки токена напрямую
frm.add_custom_button(__('Test Auth Status'), function() {
if (!frm.doc.bearer_token) {
frappe.msgprint(__('No bearer token available. Please login first.'));
return;
}
console.log('Manual test of check_auth_status with token:', frm.doc.bearer_token);
frappe.call({
method: 'invoice_az.invoice_az.doctype.testapi.testapi.check_auth_status',
args: {
'bearer_token': frm.doc.bearer_token
},
callback: function(r) {
console.log('Test status response:', r);
if (r.message) {
console.log('Status data:', r.message);
frappe.msgprint(__('Status check successful. See console for details.'));
}
},
error: function(err) {
console.error('Error in test status check:', err);
frappe.msgprint(__('Error checking status. See console for details.'));
}
});
}, __('Debug'));
// Кнопка для получения сертификатов (доступна только если авторизация успешна)
if (frm.doc.auth_status === 'Authenticated' && frm.doc.bearer_token) {
frm.add_custom_button(__('Get Certificates'), function() {
frappe.call({
method: 'invoice_az.invoice_az.doctype.testapi.testapi.get_certificates',
args: {
'bearer_token': frm.doc.bearer_token
},
callback: function(r) {
if (r.message && r.message.certificates) {
// Сохраняем сертификаты в поле формы в формате JSON
const certificates = r.message.certificates;
frm.set_value('certificates_json', JSON.stringify(certificates, null, 2));
// Сохраняем форму
frm.save();
// Показываем сообщение об успешном получении сертификатов
frappe.msgprint({
title: __('Success'),
indicator: 'green',
message: __('Certificates retrieved successfully')
});
// Отображаем список сертификатов в диалоговом окне
showCertificatesDialog(certificates);
} else {
frappe.msgprint(__('No certificates found'));
}
}
});
}, __('Actions'));
}
// Кнопка для выбора налогоплательщика (доступна если авторизация успешна и сертификат выбран)
if (frm.doc.auth_status === 'Authenticated' && frm.doc.bearer_token && frm.doc.selected_certificate_json) {
frm.add_custom_button(__('Choose Taxpayer'), function() {
try {
// Парсим данные выбранного сертификата
const certData = JSON.parse(frm.doc.selected_certificate_json);
// Определяем тип и идентификатор налогоплательщика
let ownerType = certData.taxpayerType || '';
let tin = '';
if (ownerType === 'individual' && certData.individualInfo) {
tin = certData.individualInfo.fin || '';
} else if (certData.legalInfo) {
ownerType = 'legal';
// Используем tin вместо voen
tin = certData.legalInfo.tin || ''; // Здесь было certData.legalInfo.voen
}
// Показываем сообщение о процессе
frappe.show_alert({
message: __('Choosing taxpayer, please wait...'),
indicator: 'blue'
}, 5);
// Отправляем запрос для выбора налогоплательщика
frappe.call({
method: 'invoice_az.invoice_az.doctype.testapi.testapi.choose_taxpayer',
args: {
'bearer_token': frm.doc.bearer_token,
'owner_type': ownerType,
'tin': tin
},
callback: function(r) {
if (r.message && r.message.main_token) {
// Сохраняем основной токен
frm.set_value('main_token', r.message.main_token);
// Сохраняем ответ в JSON формате
frm.set_value('choose_taxpayer_response',
JSON.stringify(r.message.response_data, null, 2));
// Обновляем статус авторизации
frm.set_value('auth_status', 'Fully Authenticated');
// Сохраняем форму
frm.save().then(() => {
frappe.show_alert({
message: __('Taxpayer chosen successfully. Main token received.'),
indicator: 'green'
}, 5);
// Перезагружаем страницу для обновления кнопок
frm.reload_doc();
});
} else {
frappe.msgprint(__('Failed to choose taxpayer. No main token received.'));
}
},
error: function(err) {
console.error('Error choosing taxpayer:', err);
frappe.msgprint(__('Error choosing taxpayer. See console for details.'));
}
});
} catch (e) {
console.error('Error parsing certificate data:', e);
frappe.msgprint(__('Error parsing certificate data'));
}
}, __('Actions'));
}
// Кнопка для получения счетов (доступна если хотя бы базовая авторизация успешна)
if ((frm.doc.auth_status === 'Authenticated' || frm.doc.auth_status === 'Fully Authenticated') &&
(frm.doc.bearer_token || frm.doc.main_token)) {
frm.add_custom_button(__('Get Invoices'), function() {
// Открываем диалог для настройки фильтров
showInvoiceFilterDialog(frm);
}, __('Actions'));
}
},
// Обработчик изменения выбранного сертификата (введенного вручную)
selected_certificate: function(frm) {
// Только если поле не пустое и сертификаты доступны
if (frm.doc.selected_certificate && frm.doc.certificates_json) {
try {
// Получаем данные сертификатов
const certificates = JSON.parse(frm.doc.certificates_json);
if (!Array.isArray(certificates) || certificates.length === 0) return;
// Ищем сертификат по имени или его части
const searchStr = frm.doc.selected_certificate.toLowerCase();
// Сначала пытаемся найти точное совпадение по имени и идентификатору
let foundCert = null;
let foundIndex = -1;
for (let i = 0; i < certificates.length; i++) {
const cert = certificates[i];
let certName = '';
let certId = '';
if (cert.taxpayerType === 'individual' && cert.individualInfo) {
certName = cert.individualInfo.name || '';
certId = cert.individualInfo.fin || '';
} else if (cert.legalInfo) {
certName = cert.legalInfo.name || '';
certId = cert.legalInfo.voen || '';
}
const fullName = `${certName} (${certId})`.toLowerCase();
// Проверяем точное совпадение или содержание строки поиска
if (fullName === searchStr || fullName.includes(searchStr)) {
foundCert = cert;
foundIndex = i;
break;
}
}
// Если нашли совпадение, устанавливаем данные сертификата
if (foundCert) {
frm.set_value('selected_certificate_index', foundIndex);
frm.set_value('selected_certificate_json', JSON.stringify(foundCert, null, 2));
frappe.show_alert({
message: __('Certificate found and selected'),
indicator: 'green'
}, 3);
}
} catch (e) {
console.error('Error processing certificate selection:', e);
}
}
},
// После того, как форма загружена
after_load: function(frm) {
// Обрабатываем случай, когда сертификат уже выбран, но данные JSON не установлены
if (frm.doc.selected_certificate && !frm.doc.selected_certificate_json && frm.doc.certificates_json) {
try {
const certificates = JSON.parse(frm.doc.certificates_json);
const searchStr = frm.doc.selected_certificate.toLowerCase();
for (let i = 0; i < certificates.length; i++) {
const cert = certificates[i];
let certName = '';
let certId = '';
if (cert.taxpayerType === 'individual' && cert.individualInfo) {
certName = cert.individualInfo.name || '';
certId = cert.individualInfo.fin || '';
} else if (cert.legalInfo) {
certName = cert.legalInfo.name || '';
certId = cert.legalInfo.voen || '';
}
const fullName = `${certName} (${certId})`.toLowerCase();
if (fullName === searchStr || fullName.includes(searchStr)) {
frm.set_value('selected_certificate_index', i);
frm.set_value('selected_certificate_json', JSON.stringify(cert, null, 2));
break;
}
}
} catch (e) {
console.error('Error auto-selecting certificate on load:', e);
}
}
}
});
// Показать диалог для настройки фильтров счетов
function showInvoiceFilterDialog(frm) {
// Используем параметры из оригинального запроса
const startDate = "01-01-2020 00:00"; // Начиная с 2020 года вместо текущего
const endDate = moment().add(1, 'year').endOf('year').format('DD-MM-YYYY HH:mm'); // Год вперед
// Создаем диалог с полями для фильтрации
var d = new frappe.ui.Dialog({
title: __('Invoice Filters'),
fields: [
{
fieldname: 'date_range_section',
fieldtype: 'Section Break',
label: __('Date Range')
},
{
fieldname: 'creationDateFrom',
fieldtype: 'Data', // Используем Data вместо Datetime для точного формата
label: __('From Date'),
default: startDate
},
{
fieldname: 'creationDateTo',
fieldtype: 'Data', // Используем Data вместо Datetime для точного формата
label: __('To Date'),
default: endDate
},
{
fieldname: 'invoice_details_section',
fieldtype: 'Section Break',
label: __('Invoice Details')
},
{
fieldname: 'serialNumber',
fieldtype: 'Data',
label: __('Serial Number')
},
{
fieldname: 'party_section',
fieldtype: 'Section Break',
label: __('Sender/Receiver')
},
{
fieldname: 'senderTin',
fieldtype: 'Data',
label: __('Sender TIN')
},
{
fieldname: 'senderName',
fieldtype: 'Data',
label: __('Sender Name')
},
{
fieldname: 'receiverTin',
fieldtype: 'Data',
label: __('Receiver TIN')
},
{
fieldname: 'receiverName',
fieldtype: 'Data',
label: __('Receiver Name')
},
{
fieldname: 'amount_section',
fieldtype: 'Section Break',
label: __('Amount')
},
{
fieldname: 'amountFrom',
fieldtype: 'Float',
label: __('Amount From'),
precision: 2
},
{
fieldname: 'amountTo',
fieldtype: 'Float',
label: __('Amount To'),
precision: 2
},
{
fieldname: 'limit_section',
fieldtype: 'Section Break',
label: __('Limit')
},
{
fieldname: 'maxCount',
fieldtype: 'Int',
label: __('Max Records'),
default: 200 // Увеличено с 50 до 200
}
],
primary_action_label: __('Get Invoices'),
primary_action: function() {
var values = d.get_values();
// Используем без преобразования формата
// так как мы уже предоставляем правильный формат в полях Data
// Дополняем значения фильтров полными параметрами из оригинального запроса
const fullPayload = {
"sortBy": "creationDate",
"sortAsc": true,
"statuses": ["approved", "onApproval", "updateApproval", "updateRequested",
"cancelRequested", "approvedBySystem", "onApprovalEdited",
"deactivated", "cancelationRefused", "correctionRefused"],
"types": ["current", "corrected"],
"kinds": ["defaultInvoice", "agent", "resale", "recycling", "taxCodex163",
"taxCodex177_5", "returnInvoice", "returnByAgent", "returnRecycled",
"exportNoteInvoice", "exciseGoodsTransfer", "advanceInvoice"],
"offset": 0,
"actionOwner": null,
// Заполняем значениями из диалога
"serialNumber": values.serialNumber || null,
"senderTin": values.senderTin || null,
"senderName": values.senderName || null,
"productName": null,
"productCode": null,
"receiverTin": values.receiverTin || null,
"receiverName": values.receiverName || null,
"creationDateFrom": values.creationDateFrom || startDate,
"creationDateTo": values.creationDateTo || endDate,
"amountFrom": values.amountFrom || null,
"amountTo": values.amountTo || null,
"maxCount": values.maxCount || 200
};
// Используем основной токен, если он есть, иначе используем bearer_token
const token_to_use = frm.doc.main_token;
// Закрываем диалог
d.hide();
// Показываем индикатор загрузки
frappe.show_alert({
message: __('Fetching invoices, please wait...'),
indicator: 'blue'
}, 5);
// Отправляем запрос для получения счетов с фильтрами
frappe.call({
method: 'invoice_az.invoice_az.doctype.testapi.testapi.get_invoices',
args: {
'token': token_to_use,
'filters': JSON.stringify(fullPayload)
},
callback: function(r) {
if (r.message) {
// Сохраняем результат в поле формы
frm.set_value('invoices_json', JSON.stringify(r.message, null, 2));
frm.save();
// Показываем сообщение об успешном получении счетов
frappe.msgprint({
title: __('Success'),
indicator: 'green',
message: __('Invoices retrieved successfully')
});
// Проверяем различные возможные структуры данных
let invoicesData = null;
if (r.message.data && r.message.data.length) {
invoicesData = r.message.data;
} else if (r.message.invoices && r.message.invoices.length) {
// Получаем данные из поля invoices, если data отсутствует
invoicesData = r.message.invoices;
}
// Отображаем список счетов в диалоговом окне
if (invoicesData && invoicesData.length) {
showInvoicesDialog(invoicesData);
} else {
frappe.msgprint(__('No invoices found matching the filters'));
}
}
},
error: function(err) {
console.error('Error fetching invoices:', err);
frappe.msgprint(__('Error fetching invoices. See console for details.'));
}
});
}
});
d.show();
}
// Функция для запуска опроса через setTimeout вместо setInterval
function startPollingWithTimeout(frm) {
console.log('startPollingWithTimeout function has been called');
// Отображаем индикатор загрузки
frm.page.set_indicator(__('Waiting for confirmation'), 'orange');
// Создаем всплывающее окно с информацией
const status_dialog = new frappe.ui.Dialog({
title: __('Authentication Status'),
fields: [
{
fieldname: 'status_html',
fieldtype: 'HTML',
options: `
<div class="text-center">
<div class="lds-dual-ring" style="display: inline-block; width: 64px; height: 64px;">
<div style="width: 64px; height: 64px; border: 6px solid #4e73df; border-radius: 50%; border-color: #4e73df transparent #4e73df transparent; animation: lds-dual-ring 1.2s linear infinite;"></div>
</div>
<style>
@keyframes lds-dual-ring {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
<div class="mt-3" id="status_message">
<h4>Waiting for confirmation on your phone</h4>
<p>Please check your phone and confirm the authentication request.</p>
</div>
</div>
`
}
],
primary_action_label: __('Cancel'),
primary_action: function() {
// Остановить опрос
window.stopPolling = true;
status_dialog.hide();
frm.page.clear_indicator();
frm.reload_doc();
}
});
status_dialog.show();
// Счетчик попыток и флаг для остановки опроса
let attempts = 0;
const maxAttempts = 20; // 2 минут (20 попыток с интервалом 5 секунд)
window.stopPolling = false;
// Функция опроса
function pollAuthStatus() {
// Если достигнуто максимальное количество попыток или установлен флаг остановки
if (attempts >= maxAttempts || window.stopPolling) {
console.log('Polling stopped: ' + (attempts >= maxAttempts ? 'max attempts reached' : 'manually cancelled'));
if (attempts >= maxAttempts) {
// Обновляем информацию в диалоговом окне по таймауту
$('#status_message').html(`
<h4 style="color: red;">Authentication Timeout</h4>
<p>The authentication request has timed out. Please try again.</p>
`);
$('.lds-dual-ring').css('display', 'none');
// Очищаем временное хранилище
window.tempBearerToken = null;
// Обновляем поле auth_status
frm.set_value('auth_status', 'Failed');
frm.save();
// Закрываем диалог через 3 секунды
setTimeout(function() {
status_dialog.hide();
frm.reload_doc();
}, 3000);
}
return;
}
attempts++;
console.log(`Polling auth status, attempt ${attempts} of ${maxAttempts}`);
// Проверяем наличие токена
if (!frm.doc.bearer_token) {
console.error('No bearer token available for status check');
setTimeout(pollAuthStatus, 5000);
return;
}
// Проверяем статус
frappe.call({
method: 'invoice_az.invoice_az.doctype.testapi.testapi.check_auth_status',
args: {
'bearer_token': frm.doc.bearer_token
},
callback: function(r) {
console.log('Status check callback received');
if (r.message) {
console.log('Status check response:', r.message);
// Проверяем successful статус
if (r.message.successful === true) {
// Авторизация успешна
window.stopPolling = true;
// Обновляем информацию в диалоговом окне
$('#status_message').html(`
<h4 style="color: green;">Authentication Successful!</h4>
<p>You are now authenticated. The page will reload shortly.</p>
`);
$('.lds-dual-ring').css('display', 'none');
const bearer_token = r.message.bearer_token || '';
if (bearer_token) {
frm.set_value('bearer_token', bearer_token);
}
// Обновляем поле auth_status
frm.set_value('auth_status', 'Authenticated');
// Сохраняем ОДИН раз после всех изменений полей
frm.save()
.then(() => {
// Закрываем диалоговое окно через 2 секунды после успешного сохранения
setTimeout(function() {
status_dialog.hide();
frm.reload_doc();
}, 2000);
});
} else {
// Продолжаем опрос
setTimeout(pollAuthStatus, 5000);
}
} else {
// Если нет ответа, продолжаем опрос
setTimeout(pollAuthStatus, 5000);
}
},
error: function(err) {
// В случае ошибки логируем её и продолжаем опрос
console.error('Error during status check:', err);
setTimeout(pollAuthStatus, 5000);
}
});
}
// Начинаем опрос
console.log('Starting first polling attempt');
pollAuthStatus();
}
// Функция для отображения счетов-фактур в диалоговом окне
function showInvoicesDialog(invoices) {
if (!invoices || !invoices.length) {
frappe.msgprint(__('No invoices data to display'));
return;
}
console.log("Displaying invoices:", invoices); // Для отладки
// Создаем таблицу со счетами
var invoice_table = '<div style="max-height: 500px; overflow-y: auto;"><table class="table table-bordered">';
invoice_table += '<thead><tr>' +
'<th>Serial Number</th>' +
'<th>Sender</th>' +
'<th>Receiver</th>' +
'<th>Creation Date</th>' +
'<th>Amount</th>' +
'<th>Status</th>' +
'<th>Actions</th>' +
'</tr></thead><tbody>';
invoices.forEach(function(invoice) {
// Проверяем наличие необходимых полей в инвойсе
const serialNumber = invoice.serialNumber || invoice.number || '';
const senderName = invoice.senderName || (invoice.sender ? invoice.sender.name : '') || '';
const receiverName = invoice.receiverName || (invoice.receiver ? invoice.receiver.name : '') || '';
const creationDate = invoice.creationDate || invoice.createDate || '';
const amount = invoice.amount || 0;
const status = invoice.status || '';
const id = invoice.id || invoice._id || '';
invoice_table += '<tr>' +
'<td>' + serialNumber + '</td>' +
'<td>' + senderName + '</td>' +
'<td>' + receiverName + '</td>' +
'<td>' + creationDate + '</td>' +
'<td>' + amount + '</td>' +
'<td>' + status + '</td>' +
'<td><button class="btn btn-xs btn-info view-invoice" data-id="' +
id + '">View</button></td>' +
'</tr>';
});
invoice_table += '</tbody></table></div>';
// Отображаем диалоговое окно с таблицей
var d = new frappe.ui.Dialog({
title: __('Invoices List'),
fields: [{
fieldtype: 'HTML',
fieldname: 'invoice_list',
options: invoice_table
}],
primary_action_label: __('Close'),
primary_action: function() {
d.hide();
}
});
d.show();
// Добавляем обработчик для кнопки просмотра счета
d.$wrapper.find('.view-invoice').on('click', function() {
var invoice_id = $(this).data('id');
showInvoiceDetails(invoice_id);
});
}
// Функция для отображения сертификатов в диалоговом окне
function showCertificatesDialog(certificates) {
if (!certificates || !certificates.length) return;
// Создаем таблицу с сертификатами
var cert_table = '<div style="max-height: 500px; overflow-y: auto;"><table class="table table-bordered">';
cert_table += '<thead><tr>' +
'<th>Type</th>' +
'<th>Name</th>' +
'<th>ID</th>' +
'<th>Position</th>' +
'<th>Status</th>' +
'<th>Action</th>' +
'</tr></thead><tbody>';
certificates.forEach(function(cert, index) {
var name = '';
var id = '';
// Исправлено: используем cert вместо certData
if (cert.taxpayerType === 'individual' && cert.individualInfo) {
name = cert.individualInfo.name || '';
id = cert.individualInfo.fin || '';
} else if (cert.legalInfo) {
name = cert.legalInfo.name || '';
id = cert.legalInfo.tin || cert.legalInfo.voen || ''; // Используем tin или voen
}
cert_table += '<tr>' +
'<td>' + (cert.taxpayerType || '') + '</td>' +
'<td>' + name + '</td>' +
'<td>' + id + '</td>' +
'<td>' + (cert.position || '') + '</td>' +
'<td>' + (cert.hasAccess ? 'Active' : 'Inactive') +
(cert.liquidated ? ' (Liquidated)' : '') + '</td>' +
'<td><button class="btn btn-xs btn-primary select-cert" data-index="' + index + '">Select</button></td>' +
'</tr>';
});
cert_table += '</tbody></table></div>';
// Отображаем диалоговое окно с таблицей
var d = new frappe.ui.Dialog({
title: __('Certificates List'),
fields: [{
fieldtype: 'HTML',
fieldname: 'certificate_list',
options: cert_table
}],
primary_action_label: __('Close'),
primary_action: function() {
d.hide();
}
});
d.show();
// Добавляем обработчик для кнопки выбора сертификата
d.$wrapper.find('.select-cert').on('click', function() {
var index = $(this).data('index');
var cert = certificates[index];
// Находим активную форму
var frm = cur_frm;
if (!frm) return;
// Получаем имя для отображения
let certName = '';
if (cert.taxpayerType === 'individual' && cert.individualInfo) {
certName = `${cert.individualInfo.name} (${cert.individualInfo.fin})`;
} else if (cert.legalInfo) {
certName = `${cert.legalInfo.name} (${cert.legalInfo.voen || cert.legalInfo.tin})`;
} else {
certName = `Certificate ${index + 1}`;
}
// Устанавливаем значение в поле выбора и индекс
frm.set_value('selected_certificate', certName);
frm.set_value('selected_certificate_index', index);
// Сохраняем выбранный сертификат в JSON поле
frm.set_value('selected_certificate_json', JSON.stringify(cert, null, 2));
frappe.show_alert({
message: __('Certificate selected'),
indicator: 'green'
}, 3);
d.hide();
// Перезагружаем страницу для обновления кнопок
frm.reload_doc();
});
}
// Функция для отображения деталей счета
function showInvoiceDetails(invoice_id) {
// Находим активную форму
var frm = cur_frm;
if (!frm) return;
// Используем основной токен, если он есть, иначе используем bearer_token
const token_to_use = frm.doc.main_token || frm.doc.bearer_token;
// Показываем индикатор загрузки
frappe.show_alert({
message: __('Loading invoice details...'),
indicator: 'blue'
}, 3);
// Получаем детали счета
frappe.call({
method: 'invoice_az.invoice_az.doctype.testapi.testapi.get_invoice_details',
args: {
'token': token_to_use,
'invoice_id': invoice_id
},
callback: function(r) {
if (r.message) {
// Отображаем детали счета
displayInvoiceDetails(r.message);
} else {
frappe.msgprint(__('Failed to get invoice details'));
}
},
error: function(err) {
console.error('Error getting invoice details:', err);
frappe.msgprint(__('Error getting invoice details. See console for details.'));
}
});
}
// Функция для отображения деталей счета в диалоговом окне
function displayInvoiceDetails(invoice) {
// Основная информация о счете
let invoice_info = `
<div class="invoice-details">
<div class="row">
<div class="col-sm-6">
<div class="invoice-detail-item">
<h5>${__('Invoice')}: ${invoice.serialNumber || ''}</h5>
<p>${__('Status')}: <strong>${invoice.status || ''}</strong></p>
<p>${__('Type')}: ${invoice.kind || ''}</p>
<p>${__('Amount')}: <strong>${invoice.amount || 0}</strong></p>
<p>${__('VAT')}: ${invoice.vat || 0}</p>
<p>${__('Created')}: ${moment(invoice.createdAt).format('DD.MM.YYYY HH:mm')}</p>
<p>${__('Last Updated')}: ${moment(invoice.lastUpdatedAt).format('DD.MM.YYYY HH:mm')}</p>
</div>
</div>
<div class="col-sm-6">
<div class="invoice-detail-item">
<h5>${__('Sender')}</h5>
<p><strong>${invoice.sender.name || ''}</strong></p>
<p>TIN: ${invoice.sender.tin || ''}</p>
<p>PIN: ${invoice.sender.pin || ''}</p>
<h5>${__('Receiver')}</h5>
<p><strong>${invoice.receiver.name || ''}</strong></p>
<p>TIN: ${invoice.receiver.tin || ''}</p>
<p>PIN: ${invoice.receiver.pin || ''}</p>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<h5>${__('Comments')}</h5>
<p>${invoice.invoiceComment || ''}</p>
<p>${invoice.invoiceComment2 || ''}</p>
</div>
</div>
</div>
`;
// Таблица с товарами/услугами
let items_table = `
<div class="items-section mt-4">
<h5>${__('Items')}</h5>
<div style="max-height: 300px; overflow-y: auto;">
<table class="table table-bordered">
<thead>
<tr>
<th>${__('Item ID')}</th>
<th>${__('Product Name')}</th>
<th>${__('Unit')}</th>
<th>${__('Quantity')}</th>
<th>${__('Price')}</th>
<th>${__('Cost')}</th>
<th>${__('VAT')}</th>
</tr>
</thead>
<tbody>
`;
if (invoice.items && invoice.items.length) {
invoice.items.forEach(function(item) {
items_table += `<tr>
<td>${item.itemId || ''}</td>
<td>${item.productName || ''}</td>
<td>${item.unit || ''}</td>
<td>${item.quantity || 0}</td>
<td>${item.pricePerUnit || 0}</td>
<td>${item.cost || 0}</td>
<td>${item.vat18 ? 'Yes' : 'No'}</td>
</tr>`;
});
}
items_table += `</tbody></table></div>`;
// Объединяем всю информацию
let content = invoice_info + items_table;
// Создаем диалоговое окно
var d = new frappe.ui.Dialog({
title: __('Invoice Details'),
fields: [{
fieldtype: 'HTML',
fieldname: 'details',
options: content
}],
primary_action_label: __('Close'),
primary_action: function() {
d.hide();
},
secondary_action_label: __('Export JSON'),
secondary_action: function() {
// Экспорт деталей счета в формате JSON
var dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(invoice, null, 2));
var downloadAnchorNode = document.createElement('a');
downloadAnchorNode.setAttribute("href", dataStr);
downloadAnchorNode.setAttribute("download", "invoice_" + invoice.id + ".json");
document.body.appendChild(downloadAnchorNode);
downloadAnchorNode.click();
downloadAnchorNode.remove();
}
});
d.show();
}

View File

@ -1,138 +0,0 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-04-24 18:30:42.231049",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"authentication_section",
"phone",
"user_id",
"bearer_token",
"auth_status",
"verification_code",
"certificates_section",
"certificates_json",
"selected_certificate",
"selected_certificate_index",
"selected_certificate_json",
"taxpayer_section",
"main_token",
"choose_taxpayer_response",
"invoices_section",
"invoices_json"
],
"fields": [
{
"fieldname": "phone",
"fieldtype": "Data",
"label": "Phone"
},
{
"fieldname": "user_id",
"fieldtype": "Data",
"label": "ID"
},
{
"fieldname": "bearer_token",
"fieldtype": "Small Text",
"label": "Token"
},
{
"fieldname": "invoices_json",
"fieldtype": "Code",
"label": "Invoices Json",
"options": "JSON"
},
{
"fieldname": "auth_status",
"fieldtype": "Select",
"label": "Auth Status",
"options": "Not Authenticated\nWaiting for confirmation\nAuthenticated\nFully Authenticated\nFailed"
},
{
"fieldname": "verification_code",
"fieldtype": "Data",
"label": "Verification Code"
},
{
"fieldname": "certificates_json",
"fieldtype": "Code",
"label": "Certificates JSON",
"options": "JSON"
},
{
"fieldname": "selected_certificate",
"fieldtype": "Data",
"label": "Selected Certificate"
},
{
"fieldname": "selected_certificate_index",
"fieldtype": "Data",
"hidden": 1,
"label": "Selected Certificate Index"
},
{
"fieldname": "selected_certificate_json",
"fieldtype": "Code",
"label": "Selected Certificate Details",
"options": "JSON"
},
{
"fieldname": "authentication_section",
"fieldtype": "Section Break",
"label": "Authentication"
},
{
"fieldname": "certificates_section",
"fieldtype": "Section Break",
"label": "Certificates"
},
{
"fieldname": "invoices_section",
"fieldtype": "Section Break",
"label": "Invoices"
},
{
"fieldname": "main_token",
"fieldtype": "Small Text",
"label": "Main Token",
"description": "Primary token for all API operations"
},
{
"fieldname": "taxpayer_section",
"fieldtype": "Section Break",
"label": "Taxpayer Information"
},
{
"fieldname": "choose_taxpayer_response",
"fieldtype": "Code",
"label": "Taxpayer Response",
"options": "JSON"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-04-25 14:00:00.000000",
"modified_by": "Administrator",
"module": "Invoice Az",
"name": "TestApi",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@ -1,221 +0,0 @@
import frappe
import requests
import json
from frappe.utils import nowdate
from frappe.model.document import Document
class TestApi(Document):
pass
@frappe.whitelist()
def get_auth_token(phone, user_id):
"""Получение токена авторизации"""
url = "https://new.e-taxes.gov.az/api/po/auth/public/v1/asanImza/start"
payload = {
"phone": phone,
"userId": user_id
}
headers = {
"Accept": "application/json, text/plain, */*",
"Accept-Language": "ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7",
"Content-Type": "application/json",
"Cache-Control": "no-cache",
"Pragma": "no-cache",
"Referer": "https://new.e-taxes.gov.az/eportal/az/login/asan"
}
try:
response = requests.post(url, data=json.dumps(payload), headers=headers)
response.raise_for_status()
# Получаем токен из заголовка ответа
bearer_token = response.headers.get('x-authorization', '')
# Получаем данные ответа
resp_data = response.json()
return {
'bearer_token': bearer_token,
'response_data': resp_data
}
except Exception as e:
frappe.log_error(f"Error getting auth token: {str(e)}", "TestApi")
frappe.throw(f"Error getting auth token: {str(e)}")
@frappe.whitelist()
def check_auth_status(bearer_token):
"""Проверка статуса авторизации"""
# Получаем значение token из DocType
url = "https://new.e-taxes.gov.az/api/po/auth/public/v1/asanImza/status"
headers = {
"Accept": "application/json, text/plain, */*",
"Accept-Language": "ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7",
"Cache-Control": "no-cache",
"Pragma": "no-cache",
"Referer": "https://new.e-taxes.gov.az/eportal/az/login/asan"
}
# Добавляем токен в заголовки, если он есть
if bearer_token:
headers["x-authorization"] = f"Bearer {bearer_token}"
try:
response = requests.get(url, headers=headers)
response.raise_for_status()
# Получаем данные о статусе
status_data = response.json()
return status_data
except Exception as e:
frappe.log_error(f"Error checking auth status: {str(e)}", "TestApi")
frappe.throw(f"Error checking auth status: {str(e)}")
@frappe.whitelist()
def get_invoices(token, filters=None):
"""Получение списка счетов-фактур с возможностью фильтрации"""
url = "https://new.e-taxes.gov.az/api/po/invoice/public/v2/invoice/find.inbox"
# Базовые параметры запроса
payload = {
"sortBy": "creationDate",
"sortAsc": True,
"statuses": ["approved", "onApproval", "updateApproval", "updateRequested",
"cancelRequested", "approvedBySystem", "onApprovalEdited",
"deactivated", "cancelationRefused", "correctionRefused"],
"types": ["current", "corrected"],
"kinds": ["defaultInvoice", "agent", "resale", "recycling", "taxCodex163",
"taxCodex177_5", "returnInvoice", "returnByAgent", "returnRecycled",
"exportNoteInvoice", "exciseGoodsTransfer", "advanceInvoice"],
"serialNumber": None,
"senderTin": None,
"senderName": None,
"productName": None,
"productCode": None,
"receiverTin": None,
"receiverName": None,
"creationDateFrom": "01-01-2024 00:00",
"creationDateTo": "31-12-2024 23:59",
"amountFrom": None,
"amountTo": None,
"offset": 0,
"maxCount": 200,
"actionOwner": None
}
# Применяем пользовательские фильтры, если они переданы
if filters:
try:
filters_dict = json.loads(filters)
for key, value in filters_dict.items():
if key in payload and value: # Обновляем только существующие ключи и непустые значения
payload[key] = value
except Exception as e:
frappe.log_error(f"Error parsing filters: {str(e)}", "TestApi")
headers = {
"Accept": "application/json, text/plain, */*",
"Content-Type": "application/json",
"Cache-Control": "no-cache",
"x-authorization": f"Bearer {token}"
}
try:
response = requests.post(url, data=json.dumps(payload), headers=headers)
response.raise_for_status()
return response.json()
except Exception as e:
frappe.log_error(f"Error getting invoices: {str(e)}", "TestApi")
frappe.throw(f"Error getting invoices: {str(e)}")
@frappe.whitelist()
def get_certificates(bearer_token):
"""Получение доступных сертификатов"""
url = "https://new.e-taxes.gov.az/api/po/auth/public/v1/asanImza/certificates"
headers = {
"Accept": "application/json, text/plain, */*",
"Accept-Language": "ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7",
"Cache-Control": "no-cache",
"Pragma": "no-cache",
"Referer": "https://new.e-taxes.gov.az/eportal/az/verification/asan",
"x-authorization": f"Bearer {bearer_token}"
}
try:
response = requests.get(url, headers=headers)
response.raise_for_status()
return response.json()
except Exception as e:
frappe.log_error(f"Error getting certificates: {str(e)}", "TestApi")
frappe.throw(f"Error getting certificates: {str(e)}")
@frappe.whitelist()
def choose_taxpayer(bearer_token, owner_type, tin):
"""Выбор налогоплательщика после выбора сертификата"""
url = "https://new.e-taxes.gov.az/api/po/auth/public/v1/asanImza/chooseTaxpayer"
payload = {
"ownerType": owner_type, # "legal" или "individual"
"legalTin": tin if owner_type == "legal" else None,
"individualFin": tin if owner_type == "individual" else None
}
# Удаляем None значения из payload
payload = {k: v for k, v in payload.items() if v is not None}
headers = {
"Accept": "application/json, text/plain, */*",
"Accept-Language": "ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7",
"Content-Type": "application/json",
"Cache-Control": "no-cache",
"Pragma": "no-cache",
"Referer": "https://new.e-taxes.gov.az/eportal/az/login/asan",
"x-authorization": f"Bearer {bearer_token}"
}
try:
response = requests.post(url, data=json.dumps(payload), headers=headers)
response.raise_for_status()
# Получаем основной токен из заголовка ответа
main_token = response.headers.get('x-authorization', '')
# Получаем данные ответа
resp_data = response.json()
return {
'main_token': main_token,
'response_data': resp_data
}
except Exception as e:
frappe.log_error(f"Error choosing taxpayer: {str(e)}", "TestApi")
frappe.throw(f"Error choosing taxpayer: {str(e)}")
@frappe.whitelist()
def get_invoice_details(token, invoice_id):
"""Получение детальной информации о счете-фактуре"""
url = f"https://new.e-taxes.gov.az/api/po/invoice/public/v2/invoice/{invoice_id}?sourceSystem=avis"
headers = {
"Accept": "application/json, text/plain, */*",
"Content-Type": "application/json",
"Cache-Control": "no-cache",
"x-authorization": f"Bearer {token}"
}
try:
response = requests.get(url, headers=headers)
response.raise_for_status()
return response.json()
except Exception as e:
frappe.log_error(f"Error getting invoice details: {str(e)}", "TestApi")
frappe.throw(f"Error getting invoice details: {str(e)}")

View File

@ -0,0 +1,196 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Скрипт конвертации item_group_codes.txt и item_group_codes_service.txt в Frappe fixtures формат.
Usage:
cd /home/frappe/frappe-bench/apps/invoice_az
python3 invoice_az/scripts/convert_item_groups_to_fixtures.py
Output:
invoice_az/fixtures/e_taxes_item_group.json
"""
import json
import os
from datetime import datetime
from collections import Counter
# Paths
SOURCE_FILE_GOODS = "/home/frappe/frappe-bench/apps/invoice_az/item_group_codes.txt"
SOURCE_FILE_SERVICES = "/home/frappe/frappe-bench/apps/invoice_az/item_group_codes_service.txt"
OUTPUT_DIR = "/home/frappe/frappe-bench/apps/invoice_az/invoice_az/fixtures"
OUTPUT_FILE = os.path.join(OUTPUT_DIR, "e_taxes_item_group.json")
# Constants
MODIFIED_TIMESTAMP = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
def validate_record(record, index):
"""Валидация отдельной записи"""
errors = []
# Проверка обязательных полей
if not record.get('code'):
errors.append(f"Record {index}: Missing code")
if not record.get('name', {}).get('az'):
errors.append(f"Record {index}: Missing name.az")
# Проверка длины name_az
name_az = record.get('name', {}).get('az', '')
if len(name_az) > 1500:
errors.append(f"Record {index}: name_az too long ({len(name_az)} chars)")
return errors
def load_groups_from_file(file_path, file_type):
"""Загрузка групп из файла"""
print(f" Загрузка {file_type} из {file_path}...")
try:
with open(file_path, 'r', encoding='utf-8') as f:
source_data = json.load(f)
except FileNotFoundError:
print(f" ⚠ Файл не найден: {file_path}")
return []
except Exception as e:
print(f" ✗ Ошибка загрузки файла: {e}")
return []
groups = source_data.get('groups', [])
print(f" ✓ Загружено {len(groups)} записей ({file_type})")
return groups
def convert_to_fixtures():
"""Основная функция конвертации"""
print("="*70)
print("E-Taxes Item Group Fixtures Generator")
print("="*70)
# 1. Загрузка исходных данных
print(f"\n[1/5] Загрузка данных из файлов...")
groups_goods = load_groups_from_file(SOURCE_FILE_GOODS, "товары (goods)")
groups_services = load_groups_from_file(SOURCE_FILE_SERVICES, "услуги (services)")
# Проверка что хотя бы один файл загружен
if not groups_goods and not groups_services:
print(f"\n ✗ ERROR: Не удалось загрузить ни один из файлов")
return False
# Объединяем группы
groups = groups_goods + groups_services
print(f"\nВсего загружено {len(groups)} записей (товары: {len(groups_goods)}, услуги: {len(groups_services)})")
# 2. Валидация данных
print("\n[2/5] Валидация данных...")
all_errors = []
codes = []
for idx, group in enumerate(groups):
errors = validate_record(group, idx)
all_errors.extend(errors)
codes.append(group.get('code'))
# Проверка уникальности кодов
code_counts = Counter(codes)
duplicates = {code: count for code, count in code_counts.items() if count > 1}
if duplicates:
print(f" ⚠ Найдены дубликаты кодов (будут пропущены):")
for code, count in list(duplicates.items())[:10]:
print(f" - {code}: {count} раз")
if len(duplicates) > 10:
print(f" ... и еще {len(duplicates) - 10} дубликатов")
if all_errors:
print(f" ✗ Найдено {len(all_errors)} ошибок валидации:")
for error in all_errors[:10]: # Показываем первые 10
print(f" - {error}")
if len(all_errors) > 10:
print(f" ... и еще {len(all_errors) - 10} ошибок")
return False
print(f" ✓ Валидация пройдена успешно")
# 3. Анализ данных
print("\n[3/5] Анализ данных...")
types = Counter(g['type'] for g in groups)
max_name_len = max(len(g['name']['az']) for g in groups)
print(f" - Типы товаров: {dict(types)}")
print(f" - Максимальная длина name_az: {max_name_len} символов")
print(f" - Уникальных кодов: {len(set(codes))}")
# 4. Конвертация в fixtures формат
print("\n[4/5] Конвертация в Frappe fixtures формат...")
fixtures = []
seen_codes = set()
skipped_duplicates = 0
for group in groups:
code = group['code']
# Пропускаем дубликаты (оставляем первое вхождение)
if code in seen_codes:
skipped_duplicates += 1
continue
seen_codes.add(code)
fixture_record = {
"docstatus": 0,
"doctype": "E-Taxes Item Group",
"modified": MODIFIED_TIMESTAMP,
"name": code, # autoname = field:code
"code": code,
"name_az": group['name']['az'],
"item_type": group['type']
}
fixtures.append(fixture_record)
print(f" ✓ Конвертировано {len(fixtures)} записей")
if skipped_duplicates > 0:
print(f" ⚠ Пропущено {skipped_duplicates} дубликатов")
# 5. Сохранение в файл
print(f"\n[5/5] Сохранение в {OUTPUT_FILE}...")
# Создаем директорию fixtures если не существует
os.makedirs(OUTPUT_DIR, exist_ok=True)
try:
with open(OUTPUT_FILE, 'w', encoding='utf-8') as f:
json.dump(fixtures, f, ensure_ascii=False, indent=1)
file_size = os.path.getsize(OUTPUT_FILE)
print(f" ✓ Файл сохранен успешно")
print(f" - Размер файла: {file_size / 1024 / 1024:.2f} MB")
except Exception as e:
print(f" ✗ Ошибка сохранения: {e}")
return False
# Итоговая статистика
print("\n" + "="*70)
print("РЕЗУЛЬТАТ:")
print("="*70)
print(f"✓ Успешно обработано {len(fixtures)} товарных групп")
print(f"✓ Fixture файл: {OUTPUT_FILE}")
print(f"✓ Готово к установке через bench migrate")
print("="*70)
return True
if __name__ == "__main__":
success = convert_to_fixtures()
exit(0 if success else 1)

1057
invoice_az/send_sales_api.py Normal file

File diff suppressed because it is too large Load Diff

104476
item_group_codes.txt Normal file

File diff suppressed because it is too large Load Diff

15628
item_group_codes_service.txt Normal file

File diff suppressed because it is too large Load Diff