added invoice sending to etaxes
This commit is contained in:
parent
0896971525
commit
bd9bf9e2cc
File diff suppressed because it is too large
Load Diff
|
|
@ -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. Показать успешное сообщение пользователю
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
_Документация создается поэтапно. Этот файл будет обновляться по мере добавления разделов._
|
||||||
|
|
@ -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"}
|
||||||
|
|
@ -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******"}
|
||||||
|
|
@ -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_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'})
|
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
|
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
|
product_group_name_field_length = 140
|
||||||
if product_group_name_field_exists:
|
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:
|
if eqm_code_field_exists and etaxes_item_data.eqm_code:
|
||||||
item_doc.eqm_code = 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")
|
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
|
# Add tax template
|
||||||
tax_template = individual_settings.get('item_tax_template') or settings_doc.default_item_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):
|
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():
|
def refresh_token_from_asan_login():
|
||||||
"""Обновляет токен из Asan Login"""
|
"""Обновляет токен из Asan Login"""
|
||||||
try:
|
try:
|
||||||
asan_login = frappe.get_all('Asan Login',
|
asan_login = frappe.get_all('Asan Login',
|
||||||
filters={'is_default': 1},
|
filters={'is_default': 1},
|
||||||
fields=['name', 'main_token'],
|
fields=['name', 'main_token'],
|
||||||
limit=1)
|
limit=1)
|
||||||
|
|
||||||
if asan_login and asan_login[0].main_token:
|
if asan_login and asan_login[0].main_token:
|
||||||
return {
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
|
|
@ -4318,4 +4323,134 @@ def refresh_token_from_asan_login():
|
||||||
return {
|
return {
|
||||||
'success': False,
|
'success': False,
|
||||||
'message': str(e)
|
'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
|
|
@ -34,6 +34,10 @@ doc_events = {
|
||||||
"on_trash": "invoice_az.sales_api.on_delete_sales_order",
|
"on_trash": "invoice_az.sales_api.on_delete_sales_order",
|
||||||
"on_cancel": "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": {
|
"E-Taxes Settings": {
|
||||||
"on_update": "invoice_az.api.update_mapped_statuses"
|
"on_update": "invoice_az.api.update_mapped_statuses"
|
||||||
}
|
}
|
||||||
|
|
@ -50,6 +54,15 @@ scheduler_events = {
|
||||||
after_install = "invoice_az.auth.setup_token_renewal"
|
after_install = "invoice_az.auth.setup_token_renewal"
|
||||||
after_migrate = "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
|
# Apps
|
||||||
# ------------------
|
# ------------------
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -66,9 +66,9 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "etaxes_product_group_code",
|
"fieldname": "etaxes_product_group_code",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Link",
|
||||||
"label": "Product Group Code",
|
"label": "Product Group Code",
|
||||||
"length": 50,
|
"options": "E-Taxes Item Group",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
@ -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": []
|
|
||||||
}
|
|
||||||
|
|
@ -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)}")
|
|
||||||
|
|
@ -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)
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue