From 462b76d0b9142df070de8cfcb8fd04e03ca98998 Mon Sep 17 00:00:00 2001
From: Ali <010109ali@gmail.com>
Date: Tue, 13 May 2025 16:06:06 +0400
Subject: [PATCH] added logs and some error outputs
---
invoice_az/api.py | 100 +-
invoice_az/client/e_taxes_items_list.js | 643 ++++++++-
invoice_az/client/e_taxes_parties_list.js | 637 ++++++++-
invoice_az/client/purchase_order.js | 1263 +++++++++++------
.../doctype/asan_login/asan_login.js | 41 +-
.../doctype/asan_login/asan_login.json | 12 +-
test_invoice.git/HEAD | 1 +
test_invoice.git/config | 8 +
test_invoice.git/description | 1 +
test_invoice.git/hooks/applypatch-msg.sample | 15 +
test_invoice.git/hooks/commit-msg.sample | 24 +
.../hooks/fsmonitor-watchman.sample | 174 +++
test_invoice.git/hooks/post-update.sample | 8 +
test_invoice.git/hooks/pre-applypatch.sample | 14 +
test_invoice.git/hooks/pre-commit.sample | 49 +
.../hooks/pre-merge-commit.sample | 13 +
test_invoice.git/hooks/pre-push.sample | 53 +
test_invoice.git/hooks/pre-rebase.sample | 169 +++
test_invoice.git/hooks/pre-receive.sample | 24 +
.../hooks/prepare-commit-msg.sample | 42 +
.../hooks/push-to-checkout.sample | 78 +
test_invoice.git/hooks/update.sample | 128 ++
test_invoice.git/info/exclude | 6 +
...a7e7e33bd9a362208ec9a6cbd2b910ef8a15c5.idx | Bin 0 -> 3900 bytes
...7e7e33bd9a362208ec9a6cbd2b910ef8a15c5.pack | Bin 0 -> 86481 bytes
test_invoice.git/packed-refs | 2 +
26 files changed, 2945 insertions(+), 560 deletions(-)
create mode 100644 test_invoice.git/HEAD
create mode 100644 test_invoice.git/config
create mode 100644 test_invoice.git/description
create mode 100755 test_invoice.git/hooks/applypatch-msg.sample
create mode 100755 test_invoice.git/hooks/commit-msg.sample
create mode 100755 test_invoice.git/hooks/fsmonitor-watchman.sample
create mode 100755 test_invoice.git/hooks/post-update.sample
create mode 100755 test_invoice.git/hooks/pre-applypatch.sample
create mode 100755 test_invoice.git/hooks/pre-commit.sample
create mode 100755 test_invoice.git/hooks/pre-merge-commit.sample
create mode 100755 test_invoice.git/hooks/pre-push.sample
create mode 100755 test_invoice.git/hooks/pre-rebase.sample
create mode 100755 test_invoice.git/hooks/pre-receive.sample
create mode 100755 test_invoice.git/hooks/prepare-commit-msg.sample
create mode 100755 test_invoice.git/hooks/push-to-checkout.sample
create mode 100755 test_invoice.git/hooks/update.sample
create mode 100644 test_invoice.git/info/exclude
create mode 100644 test_invoice.git/objects/pack/pack-02a7e7e33bd9a362208ec9a6cbd2b910ef8a15c5.idx
create mode 100644 test_invoice.git/objects/pack/pack-02a7e7e33bd9a362208ec9a6cbd2b910ef8a15c5.pack
create mode 100644 test_invoice.git/packed-refs
diff --git a/invoice_az/api.py b/invoice_az/api.py
index e7b5f0b..b50759f 100644
--- a/invoice_az/api.py
+++ b/invoice_az/api.py
@@ -121,7 +121,14 @@ def get_invoices(token, filters=None):
try:
response = requests.post(url, data=json.dumps(payload), headers=headers)
-
+
+ if response.status_code == 500:
+ return {
+ "error": "server_error",
+ "message": "Service temporarily unavailable. Please try again in a few minutes.",
+ "status_code": 500
+ }
+
# Проверяем статус ответа для обработки 401 ошибки
if response.status_code == 401:
return {
@@ -140,6 +147,12 @@ def get_invoices(token, filters=None):
"error": "unauthorized",
"message": "Authentication required. Please login again."
}
+ elif e.response.status_code == 500:
+ return {
+ "error": "server_error",
+ "message": "Service temporarily unavailable. Please try again in a few minutes.",
+ "status_code": 500
+ }
frappe.throw(f"Error getting invoices: {str(e)}")
except Exception as e:
@@ -162,6 +175,13 @@ def get_certificates(bearer_token):
try:
response = requests.get(url, headers=headers)
+ if response.status_code == 500:
+ return {
+ "error": "server_error",
+ "message": "Service temporarily unavailable. Please try again in a few minutes.",
+ "status_code": 500
+ }
+
# Проверяем на 401 ошибку
if response.status_code == 401:
return {
@@ -180,6 +200,12 @@ def get_certificates(bearer_token):
"error": "unauthorized",
"message": "Authentication required. Please login again."
}
+ elif e.response.status_code == 500:
+ return {
+ "error": "server_error",
+ "message": "Service temporarily unavailable. Please try again in a few minutes.",
+ "status_code": 500
+ }
frappe.throw(f"Error getting certificates: {str(e)}")
except Exception as e:
@@ -212,6 +238,13 @@ def choose_taxpayer(bearer_token, owner_type, tin):
try:
response = requests.post(url, data=json.dumps(payload), headers=headers)
+ if response.status_code == 500:
+ return {
+ "error": "server_error",
+ "message": "Service temporarily unavailable. Please try again in a few minutes.",
+ "status_code": 500
+ }
+
# Проверяем на 401 ошибку
if response.status_code == 401:
return {
@@ -239,6 +272,13 @@ def choose_taxpayer(bearer_token, owner_type, tin):
"error": "unauthorized",
"message": "Authentication required. Please login again."
}
+ elif e.response.status_code == 500:
+ return {
+ "error": "server_error",
+ "message": "Service temporarily unavailable. Please try again in a few minutes.",
+ "status_code": 500
+ }
+
frappe.throw(f"Error choosing taxpayer: {str(e)}")
except Exception as e:
@@ -259,6 +299,13 @@ def get_invoice_details(token, invoice_id):
try:
response = requests.get(url, headers=headers)
+ if response.status_code == 500:
+ return {
+ "error": "server_error",
+ "message": "Service temporarily unavailable. Please try again in a few minutes.",
+ "status_code": 500
+ }
+
# Проверяем на 401 ошибку
if response.status_code == 401:
return {
@@ -277,6 +324,12 @@ def get_invoice_details(token, invoice_id):
"error": "unauthorized",
"message": "Authentication required. Please login again."
}
+ elif e.response.status_code == 500:
+ return {
+ "error": "server_error",
+ "message": "Service temporarily unavailable. Please try again in a few minutes.",
+ "status_code": 500
+ }
frappe.throw(f"Error getting invoice details: {str(e)}")
except Exception as e:
@@ -358,18 +411,25 @@ def handle_authentication(asan_login_name=None):
asan_login.auth_status = "Waiting for confirmation"
asan_login.save()
+ # Извлекаем код верификации из ответа, если он есть
+ verification_code = None
+ if auth_result.get("response_data") and isinstance(auth_result.get("response_data"), dict):
+ verification_code = auth_result.get("response_data").get("verificationCode", None)
+
return {
"success": True,
"message": "Authentication initiated. Please confirm on your phone.",
"bearer_token": auth_result.get("bearer_token"),
- "asan_login_name": asan_login_name
+ "asan_login_name": asan_login_name,
+ "verification_code": verification_code
}
else:
return {"success": False, "message": "Failed to get authentication token."}
except Exception as e:
+ frappe.log_error(f"Error in handle_authentication: {str(e)}\n{frappe.get_traceback()}", "Authentication Error")
return {"success": False, "message": f"Error: {str(e)}"}
-
+
@frappe.whitelist()
def poll_auth_status(asan_login_name, bearer_token):
"""Проверяет статус аутентификации"""
@@ -2491,3 +2551,37 @@ def handle_unauthorized_request(asan_login_name, operation_name="API request"):
"operation": operation_name
}
+@frappe.whitelist()
+def check_token_validity(asan_login_name=None):
+ """Проверяет актуальность токена для указанной настройки Asan Login"""
+ try:
+ # Если имя не указано, получаем настройки по умолчанию
+ if not asan_login_name:
+ default_settings = get_default_asan_login()
+ if not default_settings.get("found", False):
+ return {"valid": False, "message": "No Asan Login settings found."}
+ asan_login_name = default_settings.get("name")
+
+ # Получаем документ Asan Login
+ asan_login = frappe.get_doc("Asan Login", asan_login_name)
+
+ # Проверяем наличие main_token (это важно для E-Taxes)
+ if not asan_login.main_token:
+ return {"valid": False, "message": "Main token not found."}
+
+ # Пытаемся получить сертификаты для проверки токена
+ certificates_result = get_certificates(asan_login.bearer_token)
+
+ # Проверяем на ошибку авторизации
+ if certificates_result.get("error") == "unauthorized":
+ return {
+ "valid": False,
+ "message": "Authentication required. Token is expired."
+ }
+
+ # Если нет ошибки, токен действителен
+ return {"valid": True, "message": "Token is valid."}
+
+ except Exception as e:
+ frappe.log_error(f"Error in check_token_validity: {str(e)}\n{frappe.get_traceback()}", "Token Validity Check")
+ return {"valid": False, "message": f"Error: {str(e)}"}
\ No newline at end of file
diff --git a/invoice_az/client/e_taxes_items_list.js b/invoice_az/client/e_taxes_items_list.js
index 3d14f0f..71e87b0 100644
--- a/invoice_az/client/e_taxes_items_list.js
+++ b/invoice_az/client/e_taxes_items_list.js
@@ -14,11 +14,567 @@ frappe.listview_settings['E-Taxes Item'] = {
onload: function(listview) {
// Add button to load items from E-Taxes
listview.page.add_menu_item(__('Load from E-Taxes'), function() {
- show_invoice_filter_dialog();
+ // Сначала проверяем актуальность токена без анимации загрузки
+ check_and_process_token(function() {
+ show_invoice_filter_dialog();
+ });
});
}
};
+// Глобальная переменная для хранения диалога загрузки
+var loading_dialog = null;
+
+// Функция для отображения диалога загрузки с анимацией
+function show_loading_dialog(title, message, submessage) {
+ // Если диалог уже открыт, обновляем только сообщение
+ if (loading_dialog) {
+ $('#loading_title').text(title);
+ $('#loading_message').text(message);
+ if (submessage) {
+ $('#loading_submessage').text(submessage).show();
+ } else {
+ $('#loading_submessage').hide();
+ }
+ return;
+ }
+
+ // Создаем диалог с анимацией загрузки
+ loading_dialog = new frappe.ui.Dialog({
+ title: title,
+ fields: [
+ {
+ fieldname: 'loading_html',
+ fieldtype: 'HTML',
+ options: `
+
+
+
+
+
${title}
+
${message}
+
${submessage || ''}
+
+
+ ----
+
+
Verification Code
+
+
+
+ `
+ }
+ ]
+ });
+
+ loading_dialog.show();
+ loading_dialog.$wrapper.find('.modal-dialog').css('max-width', '450px');
+}
+
+// Функция для отображения кода верификации
+function show_verification_code(code) {
+ if (loading_dialog && code) {
+ $('#verification_code').text(code);
+ $('#verification_code_container').show();
+ }
+}
+
+// Функция для скрытия диалога загрузки
+function hide_loading_dialog() {
+ if (loading_dialog) {
+ loading_dialog.hide();
+ loading_dialog = null;
+ }
+}
+
+// Функция для обновления сообщения в диалоге загрузки
+function update_loading_message(message, submessage) {
+ if (loading_dialog) {
+ $('#loading_message').text(message);
+ if (submessage !== undefined) {
+ $('#loading_submessage').text(submessage);
+ $('#loading_submessage').toggle(!!submessage);
+ }
+ }
+}
+
+// Функция для изменения статуса загрузки на успешный с автоматическим закрытием
+function set_loading_success(message, submessage, callback, delay = 1500) {
+ if (loading_dialog) {
+ // Меняем анимацию на галочку
+ loading_dialog.$wrapper.find('.lds-dual-ring').html(`
+
+
+
+ `);
+
+ // Обновляем сообщения
+ $('#loading_message').text(message);
+ if (submessage !== undefined) {
+ $('#loading_submessage').text(submessage);
+ $('#loading_submessage').toggle(!!submessage);
+ }
+
+ // Скрываем код верификации, если он был отображен
+ $('#verification_code_container').hide();
+
+ // Удаляем все существующие кнопки действий
+ loading_dialog.set_primary_action(null);
+ loading_dialog.set_secondary_action(null);
+
+ // Автоматически закрываем через указанную задержку
+ setTimeout(function() {
+ if (loading_dialog) {
+ loading_dialog.hide();
+ loading_dialog = null;
+ }
+ if (callback) callback();
+ }, delay);
+ } else if (callback) {
+ // Если диалог не открыт, но есть callback, вызываем его
+ callback();
+ }
+}
+
+// Функция для изменения статуса загрузки на ошибку
+function set_loading_error(message, submessage, show_close_button = true) {
+ if (loading_dialog) {
+ // Меняем анимацию на крестик
+ loading_dialog.$wrapper.find('.lds-dual-ring').html(`
+
+
+
+ `);
+
+ // Обновляем сообщения
+ $('#loading_message').text(message);
+ if (submessage !== undefined) {
+ $('#loading_submessage').text(submessage);
+ $('#loading_submessage').toggle(!!submessage);
+ }
+
+ // Скрываем код верификации, если он был отображен
+ $('#verification_code_container').hide();
+
+ // При необходимости добавляем кнопку закрытия
+ if (show_close_button) {
+ loading_dialog.set_primary_action(__('Close'), function() {
+ hide_loading_dialog();
+ });
+ }
+ }
+}
+
+// Функция для проверки токена и обработки аутентификации при необходимости
+function check_and_process_token(callback) {
+ // Проверка токена без отображения загрузки
+ frappe.call({
+ method: 'invoice_az.api.check_token_validity',
+ callback: function(r) {
+ if (r.message && r.message.valid) {
+ // Токен действителен, продолжаем с callback-функцией сразу
+ callback();
+ } else {
+ // Токен недействителен, спрашиваем пользователя о ре-аутентификации
+ frappe.confirm(
+ __('Your E-Taxes session has expired or authentication is required. Would you like to authenticate now?'),
+ function() {
+ // Если пользователь согласен, запускаем процесс аутентификации
+ start_authentication_process(callback);
+ },
+ function() {
+ // Если пользователь отказался
+ frappe.show_alert({
+ message: __('Authentication canceled. Operation cannot be completed.'),
+ indicator: 'red'
+ }, 5);
+ }
+ );
+ }
+ },
+ error: function() {
+ frappe.msgprint({
+ title: __('Error'),
+ indicator: 'red',
+ message: __('Failed to check authentication status')
+ });
+ }
+ });
+}
+
+// Функция для запуска полного процесса аутентификации
+function start_authentication_process(callback) {
+ show_loading_dialog(
+ __('Starting Authentication'),
+ __('Getting Asan Login settings...'),
+ __('Please wait')
+ );
+
+ frappe.call({
+ method: 'invoice_az.api.get_default_asan_login',
+ callback: function(r) {
+ if (r.message && r.message.found) {
+ var asan_login_name = r.message.name;
+ update_loading_message(
+ __('Sending authentication request...'),
+ __('This will send a request to your Asan Imza mobile app')
+ );
+
+ // Запускаем процесс аутентификации
+ frappe.call({
+ method: 'invoice_az.api.handle_authentication',
+ args: {
+ 'asan_login_name': asan_login_name
+ },
+ callback: function(r) {
+ if (r.message && r.message.success) {
+ const bearer_token = r.message.bearer_token;
+
+ // Отображаем код верификации, если он есть в ответе
+ if (r.message.verification_code) {
+ show_verification_code(r.message.verification_code);
+ }
+
+ // Изменяем сообщение для ожидания подтверждения
+ update_loading_message(
+ __('Waiting for confirmation on your phone'),
+ __('Please check your phone and confirm the authentication request')
+ );
+
+ // Опрашиваем статус аутентификации
+ poll_authentication_status(asan_login_name, bearer_token, function(success) {
+ if (success) {
+ // Если аутентификация успешна, продолжаем процесс
+ complete_authentication(asan_login_name, callback);
+ } else {
+ // Если аутентификация не удалась, показываем сообщение об ошибке
+ set_loading_error(
+ __('Authentication Failed'),
+ __('Could not authenticate with Asan Imza')
+ );
+ }
+ });
+ } else {
+ // Если запрос не удался, показываем сообщение об ошибке
+ set_loading_error(
+ __('Authentication Error'),
+ r.message ? r.message.message : __('Authentication request failed')
+ );
+ }
+ }
+ });
+ } else {
+ // Если настройки не найдены, показываем сообщение об ошибке
+ set_loading_error(
+ __('No Asan Login Settings'),
+ __('Please configure Asan Login settings first')
+ );
+ }
+ }
+ });
+}
+
+// Функция для опроса статуса аутентификации
+function poll_authentication_status(asan_login_name, bearer_token, callback) {
+ // Счетчик попыток и флаг для остановки опроса
+ let attempts = 0;
+ const maxAttempts = 20; // 20 попыток с интервалом 6 секунд
+ let stopPolling = false;
+
+ // Функция опроса
+ function pollStatus() {
+ // Если достигнуто максимальное количество попыток или установлен флаг остановки
+ if (attempts >= maxAttempts || stopPolling) {
+ if (attempts >= maxAttempts) {
+ // Показываем сообщение о таймауте
+ set_loading_error(
+ __('Authentication Timeout'),
+ __('The authentication request has timed out. Please try again.')
+ );
+ callback(false);
+ }
+ return;
+ }
+
+ attempts++;
+
+ // Проверяем статус авторизации
+ frappe.call({
+ method: 'invoice_az.api.poll_auth_status',
+ args: {
+ 'asan_login_name': asan_login_name,
+ 'bearer_token': bearer_token
+ },
+ callback: function(r) {
+ if (r.message && r.message.success) {
+ if (r.message.authenticated) {
+ // Авторизация успешна
+ stopPolling = true;
+ set_loading_success(
+ __('Authentication Successful'),
+ __('You are now authenticated with Asan Imza'),
+ function() {
+ callback(true);
+ }
+ );
+ } else {
+ // Обновляем счетчик попыток в сообщении
+ update_loading_message(
+ __('Waiting for confirmation on your phone'),
+ __('Please check your phone and confirm the authentication request') +
+ ' (' + attempts + '/' + maxAttempts + ')'
+ );
+ // Продолжаем опрос
+ setTimeout(pollStatus, 6000);
+ }
+ } else {
+ // Если ошибка, останавливаем опрос и показываем сообщение
+ stopPolling = true;
+ set_loading_error(
+ __('Authentication Error'),
+ r.message ? r.message.message : __('An unknown error occurred')
+ );
+ callback(false);
+ }
+ },
+ error: function(err) {
+ // В случае ошибки продолжаем опрос
+ console.error('Error during status check:', err);
+ setTimeout(pollStatus, 6000);
+ }
+ });
+ }
+
+ // Начинаем опрос
+ pollStatus();
+}
+
+// Функция для завершения аутентификации - получения сертификатов и выбора налогоплательщика
+function complete_authentication(asan_login_name, callback) {
+ show_loading_dialog(
+ __('Completing Authentication'),
+ __('Getting certificates...'),
+ __('Please wait')
+ );
+
+ // Получаем сертификаты
+ frappe.call({
+ method: 'invoice_az.api.get_auth_certificates',
+ args: {
+ 'asan_login_name': asan_login_name
+ },
+ callback: function(cert_r) {
+ if (cert_r.message && cert_r.message.success) {
+ update_loading_message(
+ __('Certificates retrieved'),
+ __('Selecting certificate and taxpayer')
+ );
+
+ // Получаем текущий документ для проверки выбранного сертификата
+ frappe.call({
+ method: 'frappe.client.get',
+ args: {
+ doctype: 'Asan Login',
+ name: asan_login_name
+ },
+ callback: function(r) {
+ if (r.message && r.message.selected_certificate_json) {
+ try {
+ const cert_data = JSON.parse(r.message.selected_certificate_json);
+ const cert_name = r.message.selected_certificate;
+
+ update_loading_message(
+ __('Selecting certificate'),
+ cert_name
+ );
+
+ // Выбираем сертификат
+ frappe.call({
+ method: 'invoice_az.api.select_certificate',
+ args: {
+ 'asan_login_name': asan_login_name,
+ 'certificate_data': cert_data,
+ 'certificate_name': cert_name
+ },
+ callback: function(select_r) {
+ if (select_r.message && select_r.message.success) {
+ update_loading_message(
+ __('Selecting taxpayer'),
+ __('Using certificate: ') + cert_name
+ );
+
+ // Выбираем налогоплательщика
+ frappe.call({
+ method: 'invoice_az.api.select_taxpayer',
+ args: {
+ 'asan_login_name': asan_login_name
+ },
+ callback: function(tax_r) {
+ if (tax_r.message && tax_r.message.success) {
+ set_loading_success(
+ __('Authentication Complete'),
+ __('You can now access E-Taxes services'),
+ callback,
+ 2000 // Автоматически закроется через 2 секунды
+ );
+ } else {
+ hide_loading_dialog();
+ show_certificate_selector(cert_r.message.certificates, asan_login_name, callback);
+ }
+ }
+ });
+ } else {
+ hide_loading_dialog();
+ show_certificate_selector(cert_r.message.certificates, asan_login_name, callback);
+ }
+ }
+ });
+ } catch (e) {
+ console.error('Error parsing certificate:', e);
+ hide_loading_dialog();
+ show_certificate_selector(cert_r.message.certificates, asan_login_name, callback);
+ }
+ } else {
+ hide_loading_dialog();
+ show_certificate_selector(cert_r.message.certificates, asan_login_name, callback);
+ }
+ }
+ });
+ } else {
+ set_loading_error(
+ __('Error'),
+ cert_r.message ? cert_r.message.message : __('Failed to get certificates')
+ );
+ }
+ }
+ });
+}
+
+// Функция для отображения селектора сертификатов
+function show_certificate_selector(certificates, asan_login_name, callback) {
+ if (!certificates || !certificates.length) {
+ frappe.msgprint(__('No certificates available'));
+ return;
+ }
+
+ // Создаем таблицу сертификатов
+ var cert_html = '';
+ cert_html += 'Type Name ID Position Status ';
+
+ certificates.forEach(function(cert, index) {
+ var name = '';
+ var id = '';
+
+ 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 || '';
+ }
+
+ cert_html += '' +
+ '' + (cert.taxpayerType || '') + ' ' +
+ '' + name + ' ' +
+ '' + id + ' ' +
+ '' + (cert.position || '') + ' ' +
+ '' + (cert.hasAccess ? 'Active' : 'Inactive') + (cert.liquidated ? ' (Liquidated)' : '') + ' ' +
+ 'Select ' +
+ ' ';
+ });
+
+ cert_html += '
';
+
+ var dialog = new frappe.ui.Dialog({
+ title: __('Select Certificate'),
+ fields: [{
+ fieldtype: 'HTML',
+ fieldname: 'certificates',
+ options: cert_html
+ }]
+ });
+
+ dialog.show();
+
+ // Обработчик нажатия на кнопку выбора сертификата
+ dialog.$wrapper.find('.select-cert').on('click', function() {
+ var index = $(this).data('index');
+ var cert = certificates[index];
+
+ // Формируем имя для отображения
+ 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}`;
+ }
+
+ dialog.hide();
+
+ // Показываем прогресс
+ show_loading_dialog(
+ __('Selecting Certificate'),
+ __('Processing your selection...'),
+ certName
+ );
+
+ // Отправляем запрос на выбор сертификата
+ frappe.call({
+ method: 'invoice_az.api.select_certificate',
+ args: {
+ 'asan_login_name': asan_login_name,
+ 'certificate_data': cert,
+ 'certificate_name': certName
+ },
+ callback: function(r) {
+ if (r.message && r.message.success) {
+ update_loading_message(
+ __('Certificate selected'),
+ __('Now selecting taxpayer information')
+ );
+
+ // Выбираем налогоплательщика
+ frappe.call({
+ method: 'invoice_az.api.select_taxpayer',
+ args: {
+ 'asan_login_name': asan_login_name
+ },
+ callback: function(r) {
+ if (r.message && r.message.success) {
+ set_loading_success(
+ __('Authentication Complete'),
+ __('You can now access E-Taxes services'),
+ callback,
+ 2000 // Автоматически закроется через 2 секунды
+ );
+ } else {
+ set_loading_error(
+ __('Error'),
+ r.message ? r.message.message : __('Failed to select taxpayer')
+ );
+ }
+ }
+ });
+ } else {
+ set_loading_error(
+ __('Error'),
+ r.message ? r.message.message : __('Failed to select certificate')
+ );
+ }
+ }
+ });
+ });
+}
+
// Function to display the invoice filter dialog
function show_invoice_filter_dialog() {
// Get previous year for start date and current date for end date
@@ -67,40 +623,17 @@ function show_invoice_filter_dialog() {
// Close dialog
d.hide();
- // Create a loading dialog with the spinner like in asan login
- const loading_dialog = new frappe.ui.Dialog({
- title: __('Loading Items'),
- fields: [
- {
- fieldname: 'status_html',
- fieldtype: 'HTML',
- options: `
-
-
-
-
-
Loading items from E-Taxes
-
Please wait, this process may take several minutes.
-
-
- `
- }
- ]
- });
-
- loading_dialog.show();
-
// Format dates correctly - convert from YYYY-MM-DD to DD-MM-YYYY HH:MM
let fromDate = values.creationDateFrom ? moment(values.creationDateFrom).format('DD-MM-YYYY 00:00') : moment(startDate).format('DD-MM-YYYY 00:00');
let toDate = values.creationDateTo ? moment(values.creationDateTo).format('DD-MM-YYYY 23:59') : moment(endDate).format('DD-MM-YYYY 23:59');
+ // Показываем анимацию загрузки
+ show_loading_dialog(
+ __('Loading Items from E-Taxes'),
+ __('Retrieving invoices for the selected period...'),
+ `${fromDate} - ${toDate}`
+ );
+
// Load items
frappe.call({
method: 'invoice_az.api.load_items_from_invoices',
@@ -110,24 +643,44 @@ function show_invoice_filter_dialog() {
'max_count': values.maxCount || 200
},
callback: function(r) {
- // Close progress dialog
- loading_dialog.hide();
-
if (r.message && r.message.success) {
- frappe.show_alert({
- message: __('Extracted and created {0} unique items. Skipped {1} duplicates.',
+ set_loading_success(
+ __('Items Loaded Successfully'),
+ __('Created {0} unique items. Skipped {1} duplicates.',
[r.message.created_count, r.message.skipped_count || 0]),
- indicator: 'green'
- }, 5);
+ function() {
+ // Refresh list
+ cur_list.refresh();
+ },
+ 2000 // Автоматически закроется через 2 секунды
+ );
+ } else if (r.message && r.message.unauthorized) {
+ // Если ошибка авторизации, запускаем процесс авторизации заново
+ set_loading_error(
+ __('Authentication Required'),
+ __('Your session has expired. Please authenticate again.'),
+ false // Не показываем кнопку закрытия
+ );
- // Refresh list
- cur_list.refresh();
- } else {
- frappe.msgprint({
- title: __('Error'),
- indicator: 'red',
- message: r.message ? r.message.message : __('An error occurred while loading items')
+ // Добавляем кнопку "Authenticate"
+ loading_dialog.set_primary_action(__('Authenticate'), function() {
+ hide_loading_dialog();
+
+ // Запускаем процесс авторизации и после него повторяем загрузку
+ check_and_process_token(function() {
+ show_invoice_filter_dialog();
+ });
});
+
+ // Добавляем кнопку "Cancel"
+ loading_dialog.set_secondary_action(__('Cancel'), function() {
+ hide_loading_dialog();
+ });
+ } else {
+ set_loading_error(
+ __('Error'),
+ r.message ? r.message.message : __('An error occurred while loading items')
+ );
}
}
});
diff --git a/invoice_az/client/e_taxes_parties_list.js b/invoice_az/client/e_taxes_parties_list.js
index 8bf4b45..0841cac 100644
--- a/invoice_az/client/e_taxes_parties_list.js
+++ b/invoice_az/client/e_taxes_parties_list.js
@@ -1,3 +1,5 @@
+// Добавляем механизм ре-аутентификации в список E-Taxes Parties
+
frappe.listview_settings['E-Taxes Parties'] = {
add_fields: ['status', 'mapped_party'],
@@ -14,11 +16,560 @@ frappe.listview_settings['E-Taxes Parties'] = {
onload: function(listview) {
// Add button to load parties from E-Taxes
listview.page.add_menu_item(__('Load from E-Taxes'), function() {
- show_invoice_filter_dialog();
+ // Сначала проверяем актуальность токена перед загрузкой
+ check_and_process_token(function() {
+ show_invoice_filter_dialog();
+ });
});
}
};
+// Глобальная переменная для хранения диалога загрузки
+var loading_dialog = null;
+
+// Функция для отображения диалога загрузки с анимацией
+function show_loading_dialog(title, message, submessage) {
+ // Если диалог уже открыт, обновляем только сообщение
+ if (loading_dialog) {
+ $('#loading_title').text(title);
+ $('#loading_message').text(message);
+ if (submessage) {
+ $('#loading_submessage').text(submessage).show();
+ } else {
+ $('#loading_submessage').hide();
+ }
+ return;
+ }
+
+ // Создаем диалог с анимацией загрузки
+ loading_dialog = new frappe.ui.Dialog({
+ title: title,
+ fields: [
+ {
+ fieldname: 'loading_html',
+ fieldtype: 'HTML',
+ options: `
+
+
+
+
+
${title}
+
${message}
+
${submessage || ''}
+
+
+
+ ----
+
+
Verification Code
+
+
+
+ `
+ }
+ ]
+ });
+
+ loading_dialog.show();
+ loading_dialog.$wrapper.find('.modal-dialog').css('max-width', '450px');
+}
+
+// Функция для скрытия диалога загрузки
+function hide_loading_dialog() {
+ if (loading_dialog) {
+ loading_dialog.hide();
+ loading_dialog = null;
+ }
+}
+
+// Функция для обновления сообщения в диалоге загрузки
+function update_loading_message(message, submessage) {
+ if (loading_dialog) {
+ $('#loading_message').text(message);
+ if (submessage !== undefined) {
+ $('#loading_submessage').text(submessage);
+ $('#loading_submessage').toggle(!!submessage);
+ }
+ }
+}
+
+// Функция для изменения статуса загрузки на успешный с автоматическим закрытием
+function set_loading_success(message, submessage, callback, delay = 2000) {
+ if (loading_dialog) {
+ // Меняем анимацию на галочку
+ loading_dialog.$wrapper.find('.lds-dual-ring').html(`
+
+
+
+ `);
+
+ // Обновляем сообщения
+ $('#loading_message').text(message);
+ if (submessage !== undefined) {
+ $('#loading_submessage').text(submessage);
+ $('#loading_submessage').toggle(!!submessage);
+ }
+
+ // Скрываем код верификации при успешном завершении
+ $('#verification_code_container').hide();
+
+ // Удаляем все существующие кнопки действий
+ loading_dialog.set_primary_action(null);
+ loading_dialog.set_secondary_action(null);
+
+ // Автоматически закрываем через указанную задержку
+ setTimeout(function() {
+ if (loading_dialog) {
+ loading_dialog.hide();
+ loading_dialog = null;
+ }
+ if (callback) callback();
+ }, delay);
+ } else if (callback) {
+ // Если диалог не открыт, но есть callback, вызываем его
+ callback();
+ }
+}
+
+// Функция для изменения статуса загрузки на ошибку
+function set_loading_error(message, submessage, show_close_button = true) {
+ if (loading_dialog) {
+ // Меняем анимацию на крестик
+ loading_dialog.$wrapper.find('.lds-dual-ring').html(`
+
+
+
+ `);
+
+ // Обновляем сообщения
+ $('#loading_message').text(message);
+ if (submessage !== undefined) {
+ $('#loading_submessage').text(submessage);
+ $('#loading_submessage').toggle(!!submessage);
+ }
+
+ // Скрываем код верификации при ошибке
+ $('#verification_code_container').hide();
+
+ // При необходимости добавляем кнопку закрытия
+ if (show_close_button) {
+ loading_dialog.set_primary_action(__('Close'), function() {
+ hide_loading_dialog();
+ });
+ }
+ }
+}
+
+// Функция для проверки токена и обработки аутентификации при необходимости
+function check_and_process_token(callback) {
+ // Проверка токена без отображения загрузки
+ frappe.call({
+ method: 'invoice_az.api.check_token_validity',
+ callback: function(r) {
+ if (r.message && r.message.valid) {
+ // Токен действителен, продолжаем с callback-функцией сразу
+ callback();
+ } else {
+ // Токен недействителен, спрашиваем пользователя о ре-аутентификации
+ frappe.confirm(
+ __('Your E-Taxes session has expired or authentication is required. Would you like to authenticate now?'),
+ function() {
+ // Если пользователь согласен, запускаем процесс аутентификации
+ start_authentication_process(callback);
+ },
+ function() {
+ // Если пользователь отказался
+ frappe.show_alert({
+ message: __('Authentication canceled. Operation cannot be completed.'),
+ indicator: 'red'
+ }, 5);
+ }
+ );
+ }
+ },
+ error: function() {
+ frappe.msgprint({
+ title: __('Error'),
+ indicator: 'red',
+ message: __('Failed to check authentication status')
+ });
+ }
+ });
+}
+
+// Функция для запуска полного процесса аутентификации
+function start_authentication_process(callback) {
+ show_loading_dialog(
+ __('Starting Authentication'),
+ __('Getting Asan Login settings...'),
+ __('Please wait')
+ );
+
+ frappe.call({
+ method: 'invoice_az.api.get_default_asan_login',
+ callback: function(r) {
+ if (r.message && r.message.found) {
+ var asan_login_name = r.message.name;
+ update_loading_message(
+ __('Sending authentication request...'),
+ __('This will send a request to your Asan Imza mobile app')
+ );
+
+ // Запускаем процесс аутентификации
+ frappe.call({
+ method: 'invoice_az.api.handle_authentication',
+ args: {
+ 'asan_login_name': asan_login_name
+ },
+ callback: function(r) {
+ if (r.message && r.message.success) {
+ const bearer_token = r.message.bearer_token;
+
+ // Отображаем код верификации, если он есть в ответе
+ if (r.message.verification_code) {
+ $('#verification_code').text(r.message.verification_code);
+ $('#verification_code_container').attr('style', 'display: block !important; margin-top: 15px;');
+ }
+
+ // Изменяем сообщение для ожидания подтверждения
+ update_loading_message(
+ __('Waiting for confirmation on your phone'),
+ __('Please check your phone and confirm the authentication request')
+ );
+
+ // Опрашиваем статус аутентификации
+ poll_authentication_status(asan_login_name, bearer_token, function(success) {
+ if (success) {
+ // Если аутентификация успешна, продолжаем процесс
+ complete_authentication(asan_login_name, callback);
+ } else {
+ // Если аутентификация не удалась, показываем сообщение об ошибке
+ set_loading_error(
+ __('Authentication Failed'),
+ __('Could not authenticate with Asan Imza')
+ );
+ }
+ });
+ } else {
+ // Если запрос не удался, показываем сообщение об ошибке
+ set_loading_error(
+ __('Authentication Error'),
+ r.message ? r.message.message : __('Authentication request failed')
+ );
+ }
+ }
+ });
+ } else {
+ // Если настройки не найдены, показываем сообщение об ошибке
+ set_loading_error(
+ __('No Asan Login Settings'),
+ __('Please configure Asan Login settings first')
+ );
+ }
+ }
+ });
+}
+
+// Функция для опроса статуса аутентификации
+function poll_authentication_status(asan_login_name, bearer_token, callback) {
+ // Счетчик попыток и флаг для остановки опроса
+ let attempts = 0;
+ const maxAttempts = 20; // 20 попыток с интервалом 6 секунд
+ let stopPolling = false;
+
+ // Функция опроса
+ function pollStatus() {
+ // Если достигнуто максимальное количество попыток или установлен флаг остановки
+ if (attempts >= maxAttempts || stopPolling) {
+ if (attempts >= maxAttempts) {
+ // Показываем сообщение о таймауте
+ set_loading_error(
+ __('Authentication Timeout'),
+ __('The authentication request has timed out. Please try again.')
+ );
+ callback(false);
+ }
+ return;
+ }
+
+ attempts++;
+
+ // Обновляем счетчик попыток в сообщении
+ update_loading_message(
+ __('Waiting for confirmation on your phone'),
+ __('Please check your phone and confirm the authentication request') +
+ ' (' + attempts + '/' + maxAttempts + ')'
+ );
+
+ // Проверяем статус авторизации
+ frappe.call({
+ method: 'invoice_az.api.poll_auth_status',
+ args: {
+ 'asan_login_name': asan_login_name,
+ 'bearer_token': bearer_token
+ },
+ callback: function(r) {
+ if (r.message && r.message.success) {
+ if (r.message.authenticated) {
+ // Авторизация успешна
+ stopPolling = true;
+ set_loading_success(
+ __('Authentication Successful'),
+ __('You are now authenticated with Asan Imza'),
+ function() {
+ callback(true);
+ }
+ );
+ } else {
+ // Продолжаем опрос
+ setTimeout(pollStatus, 6000);
+ }
+ } else {
+ // Если ошибка, останавливаем опрос и показываем сообщение
+ stopPolling = true;
+ set_loading_error(
+ __('Authentication Error'),
+ r.message ? r.message.message : __('An unknown error occurred')
+ );
+ callback(false);
+ }
+ },
+ error: function(err) {
+ // В случае ошибки продолжаем опрос
+ console.error('Error during status check:', err);
+ setTimeout(pollStatus, 6000);
+ }
+ });
+ }
+
+ // Начинаем опрос
+ pollStatus();
+}
+
+// Функция для завершения аутентификации - получения сертификатов и выбора налогоплательщика
+function complete_authentication(asan_login_name, callback) {
+ show_loading_dialog(
+ __('Completing Authentication'),
+ __('Getting certificates...'),
+ __('Please wait')
+ );
+
+ // Получаем сертификаты
+ frappe.call({
+ method: 'invoice_az.api.get_auth_certificates',
+ args: {
+ 'asan_login_name': asan_login_name
+ },
+ callback: function(cert_r) {
+ if (cert_r.message && cert_r.message.success) {
+ update_loading_message(
+ __('Certificates retrieved'),
+ __('Selecting certificate and taxpayer')
+ );
+
+ // Получаем текущий документ для проверки выбранного сертификата
+ frappe.call({
+ method: 'frappe.client.get',
+ args: {
+ doctype: 'Asan Login',
+ name: asan_login_name
+ },
+ callback: function(r) {
+ if (r.message && r.message.selected_certificate_json) {
+ try {
+ const cert_data = JSON.parse(r.message.selected_certificate_json);
+ const cert_name = r.message.selected_certificate;
+
+ update_loading_message(
+ __('Selecting certificate'),
+ cert_name
+ );
+
+ // Выбираем сертификат
+ frappe.call({
+ method: 'invoice_az.api.select_certificate',
+ args: {
+ 'asan_login_name': asan_login_name,
+ 'certificate_data': cert_data,
+ 'certificate_name': cert_name
+ },
+ callback: function(select_r) {
+ if (select_r.message && select_r.message.success) {
+ update_loading_message(
+ __('Selecting taxpayer'),
+ __('Using certificate: ') + cert_name
+ );
+
+ // Выбираем налогоплательщика
+ frappe.call({
+ method: 'invoice_az.api.select_taxpayer',
+ args: {
+ 'asan_login_name': asan_login_name
+ },
+ callback: function(tax_r) {
+ if (tax_r.message && tax_r.message.success) {
+ set_loading_success(
+ __('Authentication Complete'),
+ __('You can now access E-Taxes services'),
+ callback
+ );
+ } else {
+ hide_loading_dialog();
+ show_certificate_selector(cert_r.message.certificates, asan_login_name, callback);
+ }
+ }
+ });
+ } else {
+ hide_loading_dialog();
+ show_certificate_selector(cert_r.message.certificates, asan_login_name, callback);
+ }
+ }
+ });
+ } catch (e) {
+ console.error('Error parsing certificate:', e);
+ hide_loading_dialog();
+ show_certificate_selector(cert_r.message.certificates, asan_login_name, callback);
+ }
+ } else {
+ hide_loading_dialog();
+ show_certificate_selector(cert_r.message.certificates, asan_login_name, callback);
+ }
+ }
+ });
+ } else {
+ set_loading_error(
+ __('Error'),
+ cert_r.message ? cert_r.message.message : __('Failed to get certificates')
+ );
+ }
+ }
+ });
+}
+
+// Функция для отображения селектора сертификатов
+function show_certificate_selector(certificates, asan_login_name, callback) {
+ if (!certificates || !certificates.length) {
+ frappe.msgprint(__('No certificates available'));
+ return;
+ }
+
+ // Создаем таблицу сертификатов
+ var cert_html = '';
+ cert_html += 'Type Name ID Position Status ';
+
+ certificates.forEach(function(cert, index) {
+ var name = '';
+ var id = '';
+
+ 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 || '';
+ }
+
+ cert_html += '' +
+ '' + (cert.taxpayerType || '') + ' ' +
+ '' + name + ' ' +
+ '' + id + ' ' +
+ '' + (cert.position || '') + ' ' +
+ '' + (cert.hasAccess ? 'Active' : 'Inactive') + (cert.liquidated ? ' (Liquidated)' : '') + ' ' +
+ 'Select ' +
+ ' ';
+ });
+
+ cert_html += '
';
+
+ var dialog = new frappe.ui.Dialog({
+ title: __('Select Certificate'),
+ fields: [{
+ fieldtype: 'HTML',
+ fieldname: 'certificates',
+ options: cert_html
+ }]
+ });
+
+ dialog.show();
+
+ // Обработчик нажатия на кнопку выбора сертификата
+ dialog.$wrapper.find('.select-cert').on('click', function() {
+ var index = $(this).data('index');
+ var cert = certificates[index];
+
+ // Формируем имя для отображения
+ 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}`;
+ }
+
+ dialog.hide();
+
+ // Показываем прогресс
+ show_loading_dialog(
+ __('Selecting Certificate'),
+ __('Processing your selection...'),
+ certName
+ );
+
+ // Отправляем запрос на выбор сертификата
+ frappe.call({
+ method: 'invoice_az.api.select_certificate',
+ args: {
+ 'asan_login_name': asan_login_name,
+ 'certificate_data': cert,
+ 'certificate_name': certName
+ },
+ callback: function(r) {
+ if (r.message && r.message.success) {
+ update_loading_message(
+ __('Certificate selected'),
+ __('Now selecting taxpayer information')
+ );
+
+ // Выбираем налогоплательщика
+ frappe.call({
+ method: 'invoice_az.api.select_taxpayer',
+ args: {
+ 'asan_login_name': asan_login_name
+ },
+ callback: function(r) {
+ if (r.message && r.message.success) {
+ set_loading_success(
+ __('Authentication Complete'),
+ __('You can now access E-Taxes services'),
+ callback
+ );
+ } else {
+ set_loading_error(
+ __('Error'),
+ r.message ? r.message.message : __('Failed to select taxpayer')
+ );
+ }
+ }
+ });
+ } else {
+ set_loading_error(
+ __('Error'),
+ r.message ? r.message.message : __('Failed to select certificate')
+ );
+ }
+ }
+ });
+ });
+}
+
// Function to display the invoice filter dialog
function show_invoice_filter_dialog() {
// Get previous year for start date and current date for end date
@@ -67,40 +618,17 @@ function show_invoice_filter_dialog() {
// Close dialog
d.hide();
- // Create a loading dialog with the spinner like in asan login
- const loading_dialog = new frappe.ui.Dialog({
- title: __('Loading Parties'),
- fields: [
- {
- fieldname: 'status_html',
- fieldtype: 'HTML',
- options: `
-
-
-
-
-
Loading parties from E-Taxes
-
Please wait, this process may take several minutes.
-
-
- `
- }
- ]
- });
-
- loading_dialog.show();
-
// Format dates correctly - convert from YYYY-MM-DD to DD-MM-YYYY HH:MM
let fromDate = values.creationDateFrom ? moment(values.creationDateFrom).format('DD-MM-YYYY 00:00') : moment(startDate).format('DD-MM-YYYY 00:00');
let toDate = values.creationDateTo ? moment(values.creationDateTo).format('DD-MM-YYYY 23:59') : moment(endDate).format('DD-MM-YYYY 23:59');
+ // Показываем анимацию загрузки
+ show_loading_dialog(
+ __('Loading Parties from E-Taxes'),
+ __('Retrieving parties for the selected period...'),
+ `${fromDate} - ${toDate}`
+ );
+
// Load parties
frappe.call({
method: 'invoice_az.api.load_parties_from_invoices',
@@ -110,24 +638,43 @@ function show_invoice_filter_dialog() {
'max_count': values.maxCount || 200
},
callback: function(r) {
- // Close progress dialog
- loading_dialog.hide();
-
if (r.message && r.message.success) {
- frappe.show_alert({
- message: __('Extracted and created {0} unique parties. Skipped {1} duplicates.',
+ set_loading_success(
+ __('Parties Loaded Successfully'),
+ __('Created {0} unique parties. Skipped {1} duplicates.',
[r.message.created_count, r.message.skipped_count || 0]),
- indicator: 'green'
- }, 5);
+ function() {
+ // Refresh list
+ cur_list.refresh();
+ }
+ );
+ } else if (r.message && r.message.unauthorized) {
+ // Если ошибка авторизации, запускаем процесс авторизации заново
+ set_loading_error(
+ __('Authentication Required'),
+ __('Your session has expired. Please authenticate again.'),
+ false // Не показываем кнопку закрытия
+ );
- // Refresh list
- cur_list.refresh();
- } else {
- frappe.msgprint({
- title: __('Error'),
- indicator: 'red',
- message: r.message ? r.message.message : __('An error occurred while loading parties')
+ // Добавляем кнопку "Authenticate"
+ loading_dialog.set_primary_action(__('Authenticate'), function() {
+ hide_loading_dialog();
+
+ // Запускаем процесс авторизации и после него повторяем загрузку
+ check_and_process_token(function() {
+ show_invoice_filter_dialog();
+ });
});
+
+ // Добавляем кнопку "Cancel"
+ loading_dialog.set_secondary_action(__('Cancel'), function() {
+ hide_loading_dialog();
+ });
+ } else {
+ set_loading_error(
+ __('Error'),
+ r.message ? r.message.message : __('An error occurred while loading parties')
+ );
}
}
});
diff --git a/invoice_az/client/purchase_order.js b/invoice_az/client/purchase_order.js
index 6969895..ae2dde4 100644
--- a/invoice_az/client/purchase_order.js
+++ b/invoice_az/client/purchase_order.js
@@ -39,148 +39,781 @@ frappe.listview_settings['Purchase Order'] = {
}
};
-// Функция для отображения диалога импорта
-function show_etaxes_import_dialog(frm) {
- // Создаем диалог для выбора периода и склада
- var d = new frappe.ui.Dialog({
- title: __('Импорт счета-фактуры из E-Taxes'),
- fields: [
- {
- fieldname: 'date_range_section',
- fieldtype: 'Section Break',
- label: __('Период')
- },
- {
- fieldname: 'creationDateFrom',
- fieldtype: 'Date',
- label: __('Дата с'),
- default: moment().subtract(1, 'month').format('YYYY-MM-DD')
- },
- {
- fieldname: 'creationDateTo',
- fieldtype: 'Date',
- label: __('Дата по'),
- default: moment().format('YYYY-MM-DD')
- },
- {
- fieldname: 'settings_section',
- fieldtype: 'Section Break',
- label: __('Настройки')
- },
- {
- fieldname: 'warehouse',
- fieldtype: 'Link',
- label: __('Склад по умолчанию'),
- options: 'Warehouse',
- reqd: true,
- get_query: function() {
- return {
- filters: {
- 'is_group': 0,
- 'disabled': 0
- }
- };
- }
- }
- ],
- primary_action_label: __('Поиск'),
- primary_action: function() {
- var values = d.get_values();
-
- // Форматируем даты
- let fromDate = values.creationDateFrom ? moment(values.creationDateFrom).format('DD-MM-YYYY 00:00') : '';
- let toDate = values.creationDateTo ? moment(values.creationDateTo).format('DD-MM-YYYY 23:59') : '';
- let warehouse = values.warehouse;
-
- if (!warehouse) {
- frappe.msgprint({
- title: __('Внимание'),
- indicator: 'orange',
- message: __('Пожалуйста, выберите склад по умолчанию')
- });
- return;
- }
-
- // Закрываем диалог
- d.hide();
- show_loading_dialog(__('Загрузка счетов-фактур...'));
-
- // Сначала получаем настройки авторизации
- frappe.call({
- method: 'invoice_az.api.get_default_asan_login',
- callback: function(login_response) {
- if (login_response.message && login_response.message.found) {
- // Получаем токен из настроек
- let main_token = login_response.message.main_token;
-
- if (!main_token) {
- hide_loading_dialog();
- frappe.msgprint({
- title: __('Ошибка авторизации'),
- indicator: 'red',
- message: __('Не найден токен авторизации. Пожалуйста, завершите процесс авторизации в настройках E-Taxes.')
- });
- return;
- }
-
- // Загружаем счета-фактуры с использованием полученного токена
- frappe.call({
- method: 'invoice_az.api.get_invoices',
- args: {
- 'token': main_token,
- 'filters': JSON.stringify({
- "creationDateFrom": fromDate,
- "creationDateTo": toDate
- })
- },
- callback: function(r) {
- // Закрываем диалог загрузки
- hide_loading_dialog();
-
- if (r.message && !r.message.error) {
- // Получаем данные о счетах-фактурах
- let invoices = r.message.data || r.message.invoices || [];
-
- if (invoices.length > 0) {
- // Передаем токен, frm и warehouse в следующий диалог
- show_invoice_selection_dialog(frm, invoices, main_token, warehouse);
- } else {
- frappe.msgprint({
- title: __('Информация'),
- indicator: 'blue',
- message: __('За указанный период не найдено счетов-фактур')
- });
- }
- } else if (r.message && r.message.error === 'unauthorized') {
- // Если ошибка авторизации, предлагаем авторизоваться
- frappe.msgprint({
- title: __('Ошибка авторизации'),
- indicator: 'red',
- message: __('Необходимо авторизоваться в системе E-Taxes. Токен устарел.')
- });
- } else {
- frappe.msgprint({
- title: __('Ошибка'),
- indicator: 'red',
- message: r.message ? r.message.message : __('Ошибка загрузки счетов-фактур')
- });
+// Глобальная переменная для хранения диалога загрузки
+var loading_dialog = null;
+
+// Функция для отображения диалога загрузки
+function show_loading_dialog(message) {
+ if (!loading_dialog) {
+ loading_dialog = new frappe.ui.Dialog({
+ title: __('Загрузка'),
+ fields: [
+ {
+ fieldname: 'status_html',
+ fieldtype: 'HTML',
+ options: `
+
+
+
+
+
${message || __('Пожалуйста, подождите...')}
+
${__('Операция может занять некоторое время')}
+
+
+
+
+ ----
+
+
Verification Code
+
+
+ `
}
+ ],
+ primary_action_label: __('Отмена'),
+ primary_action: function() {
+ hide_loading_dialog();
+ }
+ });
+ } else {
+ // Обновляем сообщение
+ $('#status_message h4').text(message || __('Пожалуйста, подождите...'));
+ }
+
+ loading_dialog.show();
+}
+
+// Функция для скрытия диалога загрузки
+function hide_loading_dialog() {
+ if (loading_dialog) {
+ loading_dialog.hide();
+ loading_dialog = null;
+ }
+}
+
+// Функция для обновления сообщения в диалоге загрузки
+function update_loading_message(message, submessage) {
+ if (loading_dialog) {
+ $('#status_message h4').text(message);
+ if (submessage !== undefined) {
+ $('#status_message p').text(submessage);
+ }
+ }
+}
+
+// Функция для отображения кода верификации
+function show_verification_code(code) {
+ if (loading_dialog && code) {
+ $('#verification_code').text(code);
+ $('#verification_code_container').show();
+ $('#verification_code_container').attr('style', 'display: block !important; margin-top: 15px;');
+ }
+}
+
+// Функция для установки статуса успеха
+function set_loading_success(message, submessage, callback, delay = 2000) {
+ if (loading_dialog) {
+ // Меняем анимацию на галочку
+ loading_dialog.$wrapper.find('.lds-dual-ring').html(`
+
+
+
+ `);
+
+ // Обновляем сообщения
+ $('#status_message h4').text(message);
+ if (submessage !== undefined) {
+ $('#status_message p').text(submessage);
+ }
+
+ // Скрываем код верификации
+ $('#verification_code_container').hide();
+
+ // Удаляем кнопки
+ loading_dialog.set_primary_action(null);
+ loading_dialog.set_secondary_action(null);
+
+ // Автоматически закрываем через указанную задержку
+ setTimeout(function() {
+ if (loading_dialog) {
+ loading_dialog.hide();
+ loading_dialog = null;
+ }
+ if (callback) callback();
+ }, delay);
+ } else if (callback) {
+ callback();
+ }
+}
+
+// Функция для установки статуса ошибки
+function set_loading_error(message, submessage, show_close_button = true) {
+ if (loading_dialog) {
+ // Меняем анимацию на крестик
+ loading_dialog.$wrapper.find('.lds-dual-ring').html(`
+
+
+
+ `);
+
+ // Обновляем сообщения
+ $('#status_message h4').text(message);
+ if (submessage !== undefined) {
+ $('#status_message p').text(submessage);
+ }
+
+ // Скрываем код верификации
+ $('#verification_code_container').hide();
+
+ // При необходимости добавляем кнопку закрытия
+ if (show_close_button) {
+ loading_dialog.set_primary_action(__('Close'), function() {
+ hide_loading_dialog();
+ });
+ }
+ }
+}
+
+// Функция для проверки токена и обработки аутентификации при необходимости
+function check_token_before_api_call(callback) {
+ // Проверка токена без отображения загрузки
+ frappe.call({
+ method: 'invoice_az.api.check_token_validity',
+ callback: function(r) {
+ if (r.message && r.message.valid) {
+ // Токен действителен, продолжаем с callback-функцией сразу
+ callback();
+ } else {
+ // Токен недействителен, спрашиваем пользователя о ре-аутентификации
+ frappe.confirm(
+ __('Your E-Taxes session has expired or authentication is required. Would you like to authenticate now?'),
+ function() {
+ // Если пользователь согласен, запускаем процесс аутентификации
+ start_authentication_process(callback);
+ },
+ function() {
+ // Если пользователь отказался
+ frappe.show_alert({
+ message: __('Authentication canceled. Operation cannot be completed.'),
+ indicator: 'red'
+ }, 5);
+ }
+ );
+ }
+ },
+ error: function() {
+ frappe.msgprint({
+ title: __('Error'),
+ indicator: 'red',
+ message: __('Failed to check authentication status')
});
}
});
+}
+
+// Функция для запуска полного процесса аутентификации
+function start_authentication_process(callback) {
+ show_loading_dialog(__('Starting Authentication'));
+ update_loading_message(__('Getting Asan Login settings...'), __('Please wait'));
- d.show();
+ frappe.call({
+ method: 'invoice_az.api.get_default_asan_login',
+ callback: function(r) {
+ if (r.message && r.message.found) {
+ var asan_login_name = r.message.name;
+ update_loading_message(
+ __('Sending authentication request...'),
+ __('This will send a request to your Asan Imza mobile app')
+ );
+
+ // Запускаем процесс аутентификации
+ frappe.call({
+ method: 'invoice_az.api.handle_authentication',
+ args: {
+ 'asan_login_name': asan_login_name
+ },
+ callback: function(r) {
+ if (r.message && r.message.success) {
+ const bearer_token = r.message.bearer_token;
+
+ // Отображаем код верификации, если он есть в ответе
+ if (r.message.verification_code) {
+ show_verification_code(r.message.verification_code);
+ }
+
+ // Изменяем сообщение для ожидания подтверждения
+ update_loading_message(
+ __('Waiting for confirmation on your phone'),
+ __('Please check your phone and confirm the authentication request')
+ );
+
+ // Опрашиваем статус аутентификации
+ poll_authentication_status(asan_login_name, bearer_token, function(success) {
+ if (success) {
+ // Если аутентификация успешна, продолжаем процесс
+ complete_authentication(asan_login_name, callback);
+ } else {
+ // Если аутентификация не удалась, показываем сообщение об ошибке
+ set_loading_error(
+ __('Authentication Failed'),
+ __('Could not authenticate with Asan Imza')
+ );
+ }
+ });
+ } else {
+ // Если запрос не удался, показываем сообщение об ошибке
+ set_loading_error(
+ __('Authentication Error'),
+ r.message ? r.message.message : __('Authentication request failed')
+ );
+ }
+ }
+ });
+ } else {
+ // Если настройки не найдены, показываем сообщение об ошибке
+ set_loading_error(
+ __('No Asan Login Settings'),
+ __('Please configure Asan Login settings first')
+ );
+ }
+ }
+ });
+}
+
+// Функция для опроса статуса аутентификации
+function poll_authentication_status(asan_login_name, bearer_token, callback) {
+ // Счетчик попыток и флаг для остановки опроса
+ let attempts = 0;
+ const maxAttempts = 20; // 20 попыток с интервалом 6 секунд
+ let stopPolling = false;
+
+ // Функция опроса
+ function pollStatus() {
+ // Если достигнуто максимальное количество попыток или установлен флаг остановки
+ if (attempts >= maxAttempts || stopPolling) {
+ if (attempts >= maxAttempts) {
+ // Показываем сообщение о таймауте
+ set_loading_error(
+ __('Authentication Timeout'),
+ __('The authentication request has timed out. Please try again.')
+ );
+ callback(false);
+ }
+ return;
+ }
+
+ attempts++;
+
+ // Обновляем счетчик попыток в сообщении
+ update_loading_message(
+ __('Waiting for confirmation on your phone'),
+ __('Please check your phone and confirm the authentication request') +
+ ' (' + attempts + '/' + maxAttempts + ')'
+ );
+
+ // Проверяем статус авторизации
+ frappe.call({
+ method: 'invoice_az.api.poll_auth_status',
+ args: {
+ 'asan_login_name': asan_login_name,
+ 'bearer_token': bearer_token
+ },
+ callback: function(r) {
+ if (r.message && r.message.success) {
+ if (r.message.authenticated) {
+ // Авторизация успешна
+ stopPolling = true;
+ set_loading_success(
+ __('Authentication Successful'),
+ __('You are now authenticated with Asan Imza'),
+ function() {
+ callback(true);
+ }
+ );
+ } else {
+ // Продолжаем опрос
+ setTimeout(pollStatus, 6000);
+ }
+ } else {
+ // Если ошибка, останавливаем опрос и показываем сообщение
+ stopPolling = true;
+ set_loading_error(
+ __('Authentication Error'),
+ r.message ? r.message.message : __('An unknown error occurred')
+ );
+ callback(false);
+ }
+ },
+ error: function(err) {
+ // В случае ошибки продолжаем опрос
+ console.error('Error during status check:', err);
+ setTimeout(pollStatus, 6000);
+ }
+ });
+ }
+
+ // Начинаем опрос
+ pollStatus();
+}
+
+// Функция для завершения аутентификации - получения сертификатов и выбора налогоплательщика
+function complete_authentication(asan_login_name, callback) {
+ show_loading_dialog(__('Completing Authentication'));
+ update_loading_message(__('Getting certificates...'), __('Please wait'));
+
+ // Получаем сертификаты
+ frappe.call({
+ method: 'invoice_az.api.get_auth_certificates',
+ args: {
+ 'asan_login_name': asan_login_name
+ },
+ callback: function(cert_r) {
+ if (cert_r.message && cert_r.message.success) {
+ update_loading_message(
+ __('Certificates retrieved'),
+ __('Selecting certificate and taxpayer')
+ );
+
+ // Получаем текущий документ для проверки выбранного сертификата
+ frappe.call({
+ method: 'frappe.client.get',
+ args: {
+ doctype: 'Asan Login',
+ name: asan_login_name
+ },
+ callback: function(r) {
+ if (r.message && r.message.selected_certificate_json) {
+ try {
+ const cert_data = JSON.parse(r.message.selected_certificate_json);
+ const cert_name = r.message.selected_certificate;
+
+ update_loading_message(
+ __('Selecting certificate'),
+ cert_name
+ );
+
+ // Выбираем сертификат
+ frappe.call({
+ method: 'invoice_az.api.select_certificate',
+ args: {
+ 'asan_login_name': asan_login_name,
+ 'certificate_data': cert_data,
+ 'certificate_name': cert_name
+ },
+ callback: function(select_r) {
+ if (select_r.message && select_r.message.success) {
+ update_loading_message(
+ __('Selecting taxpayer'),
+ __('Using certificate: ') + cert_name
+ );
+
+ // Выбираем налогоплательщика
+ frappe.call({
+ method: 'invoice_az.api.select_taxpayer',
+ args: {
+ 'asan_login_name': asan_login_name
+ },
+ callback: function(tax_r) {
+ if (tax_r.message && tax_r.message.success) {
+ set_loading_success(
+ __('Authentication Complete'),
+ __('You can now access E-Taxes services'),
+ callback
+ );
+ } else {
+ hide_loading_dialog();
+ show_certificate_selector(cert_r.message.certificates, asan_login_name, callback);
+ }
+ }
+ });
+ } else {
+ hide_loading_dialog();
+ show_certificate_selector(cert_r.message.certificates, asan_login_name, callback);
+ }
+ }
+ });
+ } catch (e) {
+ console.error('Error parsing certificate:', e);
+ hide_loading_dialog();
+ show_certificate_selector(cert_r.message.certificates, asan_login_name, callback);
+ }
+ } else {
+ hide_loading_dialog();
+ show_certificate_selector(cert_r.message.certificates, asan_login_name, callback);
+ }
+ }
+ });
+ } else {
+ set_loading_error(
+ __('Error'),
+ cert_r.message ? cert_r.message.message : __('Failed to get certificates')
+ );
+ }
+ }
+ });
+}
+
+// Функция для отображения селектора сертификатов
+function show_certificate_selector(certificates, asan_login_name, callback) {
+ if (!certificates || !certificates.length) {
+ frappe.msgprint(__('No certificates available'));
+ return;
+ }
+
+ // Создаем таблицу сертификатов
+ var cert_html = '';
+ cert_html += 'Type Name ID Position Status ';
+
+ certificates.forEach(function(cert, index) {
+ var name = '';
+ var id = '';
+
+ 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 || '';
+ }
+
+ cert_html += '' +
+ '' + (cert.taxpayerType || '') + ' ' +
+ '' + name + ' ' +
+ '' + id + ' ' +
+ '' + (cert.position || '') + ' ' +
+ '' + (cert.hasAccess ? 'Active' : 'Inactive') + (cert.liquidated ? ' (Liquidated)' : '') + ' ' +
+ 'Select ' +
+ ' ';
+ });
+
+ cert_html += '
';
+
+ var dialog = new frappe.ui.Dialog({
+ title: __('Select Certificate'),
+ fields: [{
+ fieldtype: 'HTML',
+ fieldname: 'certificates',
+ options: cert_html
+ }]
+ });
+
+ dialog.show();
+
+ // Обработчик нажатия на кнопку выбора сертификата
+ dialog.$wrapper.find('.select-cert').on('click', function() {
+ var index = $(this).data('index');
+ var cert = certificates[index];
+
+ // Формируем имя для отображения
+ 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}`;
+ }
+
+ dialog.hide();
+
+ // Показываем прогресс
+ show_loading_dialog(__('Selecting Certificate'));
+ update_loading_message(__('Processing your selection...'), certName);
+
+ // Отправляем запрос на выбор сертификата
+ frappe.call({
+ method: 'invoice_az.api.select_certificate',
+ args: {
+ 'asan_login_name': asan_login_name,
+ 'certificate_data': cert,
+ 'certificate_name': certName
+ },
+ callback: function(r) {
+ if (r.message && r.message.success) {
+ update_loading_message(
+ __('Certificate selected'),
+ __('Now selecting taxpayer information')
+ );
+
+ // Выбираем налогоплательщика
+ frappe.call({
+ method: 'invoice_az.api.select_taxpayer',
+ args: {
+ 'asan_login_name': asan_login_name
+ },
+ callback: function(r) {
+ if (r.message && r.message.success) {
+ set_loading_success(
+ __('Authentication Complete'),
+ __('You can now access E-Taxes services'),
+ callback
+ );
+ } else {
+ set_loading_error(
+ __('Error'),
+ r.message ? r.message.message : __('Failed to select taxpayer')
+ );
+ }
+ }
+ });
+ } else {
+ set_loading_error(
+ __('Error'),
+ r.message ? r.message.message : __('Failed to select certificate')
+ );
+ }
+ }
+ });
+ });
+}
+
+// Функция для отображения диалога импорта
+function show_etaxes_import_dialog(frm) {
+ // Сначала проверяем токен
+ check_token_before_api_call(function() {
+ // Создаем диалог для выбора периода и склада
+ var d = new frappe.ui.Dialog({
+ title: __('Импорт счета-фактуры из E-Taxes'),
+ fields: [
+ {
+ fieldname: 'date_range_section',
+ fieldtype: 'Section Break',
+ label: __('Период')
+ },
+ {
+ fieldname: 'creationDateFrom',
+ fieldtype: 'Date',
+ label: __('Дата с'),
+ default: moment().subtract(1, 'month').format('YYYY-MM-DD')
+ },
+ {
+ fieldname: 'creationDateTo',
+ fieldtype: 'Date',
+ label: __('Дата по'),
+ default: moment().format('YYYY-MM-DD')
+ },
+ {
+ fieldname: 'settings_section',
+ fieldtype: 'Section Break',
+ label: __('Настройки')
+ },
+ {
+ fieldname: 'warehouse',
+ fieldtype: 'Link',
+ label: __('Склад по умолчанию'),
+ options: 'Warehouse',
+ reqd: true,
+ get_query: function() {
+ return {
+ filters: {
+ 'is_group': 0,
+ 'disabled': 0
+ }
+ };
+ }
+ }
+ ],
+ primary_action_label: __('Поиск'),
+ primary_action: function() {
+ var values = d.get_values();
+
+ // Форматируем даты
+ let fromDate = values.creationDateFrom ? moment(values.creationDateFrom).format('DD-MM-YYYY 00:00') : '';
+ let toDate = values.creationDateTo ? moment(values.creationDateTo).format('DD-MM-YYYY 23:59') : '';
+ let warehouse = values.warehouse;
+
+ if (!warehouse) {
+ frappe.msgprint({
+ title: __('Внимание'),
+ indicator: 'orange',
+ message: __('Пожалуйста, выберите склад по умолчанию')
+ });
+ return;
+ }
+
+ // Закрываем диалог
+ d.hide();
+ show_loading_dialog(__('Загрузка счетов-фактур...'));
+
+ // Загружаем счета-фактуры
+ load_etaxes_invoices(frm, fromDate, toDate, warehouse);
+ }
+ });
+
+ d.show();
+ });
+}
+
+// Новая функция для загрузки инвойсов с обработкой ошибок авторизации
+function load_etaxes_invoices(frm, fromDate, toDate, warehouse) {
+ // Сначала получаем настройки авторизации
+ frappe.call({
+ method: 'invoice_az.api.get_default_asan_login',
+ callback: function(login_response) {
+ if (login_response.message && login_response.message.found) {
+ // Получаем токен из настроек
+ let main_token = login_response.message.main_token;
+
+ if (!main_token) {
+ hide_loading_dialog();
+ frappe.msgprint({
+ title: __('Ошибка авторизации'),
+ indicator: 'red',
+ message: __('Не найден токен авторизации. Пожалуйста, завершите процесс авторизации в настройках E-Taxes.')
+ });
+ return;
+ }
+
+ // Загружаем счета-фактуры с использованием полученного токена
+ frappe.call({
+ method: 'invoice_az.api.get_invoices',
+ args: {
+ 'token': main_token,
+ 'filters': JSON.stringify({
+ "creationDateFrom": fromDate,
+ "creationDateTo": toDate
+ })
+ },
+ callback: function(r) {
+ // Закрываем диалог загрузки
+ hide_loading_dialog();
+
+ if (r.message && !r.message.error) {
+ // Получаем данные о счетах-фактурах
+ let invoices = r.message.data || r.message.invoices || [];
+
+ if (invoices.length > 0) {
+ // Передаем токен, frm и warehouse в следующий диалог
+ show_invoice_selection_dialog(frm, invoices, main_token, warehouse);
+ } else {
+ frappe.msgprint({
+ title: __('Информация'),
+ indicator: 'blue',
+ message: __('За указанный период не найдено счетов-фактур')
+ });
+ }
+ } else if (r.message && r.message.error === 'unauthorized') {
+ // Если ошибка авторизации, предлагаем авторизоваться
+ frappe.confirm(
+ __('Your E-Taxes session has expired. Would you like to authenticate now?'),
+ function() {
+ // Запускаем процесс аутентификации и после успеха повторяем загрузку
+ start_authentication_process(function() {
+ // После успешной аутентификации повторяем загрузку
+ load_etaxes_invoices(frm, fromDate, toDate, warehouse);
+ });
+ },
+ function() {
+ // Если пользователь отказался, показываем сообщение
+ frappe.show_alert({
+ message: __('Authentication canceled. Operation cannot be completed.'),
+ indicator: 'red'
+ }, 5);
+ }
+ );
+ } else {
+ frappe.msgprint({
+ title: __('Ошибка'),
+ indicator: 'red',
+ message: r.message ? r.message.message : __('Ошибка загрузки счетов-фактур')
+ });
+ }
+ }
+ });
+ } else {
+ hide_loading_dialog();
+ frappe.msgprint({
+ title: __('Ошибка'),
+ indicator: 'red',
+ message: __('Не найдены настройки авторизации E-Taxes')
+ });
+ }
+ }
+ });
+}
+
+// Функция для отображения диалога импорта при работе из списка
+function show_etaxes_import_dialog_for_list() {
+ // Сначала проверяем токен
+ check_token_before_api_call(function() {
+ // Создаем диалог для выбора периода и склада
+ var d = new frappe.ui.Dialog({
+ title: __('Импорт счета-фактуры из E-Taxes'),
+ fields: [
+ {
+ fieldname: 'date_range_section',
+ fieldtype: 'Section Break',
+ label: __('Период')
+ },
+ {
+ fieldname: 'creationDateFrom',
+ fieldtype: 'Date',
+ label: __('Дата с'),
+ default: moment().subtract(1, 'month').format('YYYY-MM-DD')
+ },
+ {
+ fieldname: 'creationDateTo',
+ fieldtype: 'Date',
+ label: __('Дата по'),
+ default: moment().format('YYYY-MM-DD')
+ },
+ {
+ fieldname: 'settings_section',
+ fieldtype: 'Section Break',
+ label: __('Настройки')
+ },
+ {
+ fieldname: 'warehouse',
+ fieldtype: 'Link',
+ label: __('Склад по умолчанию'),
+ options: 'Warehouse',
+ reqd: true,
+ get_query: function() {
+ return {
+ filters: {
+ 'is_group': 0,
+ 'disabled': 0
+ }
+ };
+ }
+ }
+ ],
+ primary_action_label: __('Поиск'),
+ primary_action: function() {
+ var values = d.get_values();
+
+ // Форматируем даты
+ let fromDate = values.creationDateFrom ? moment(values.creationDateFrom).format('DD-MM-YYYY 00:00') : '';
+ let toDate = values.creationDateTo ? moment(values.creationDateTo).format('DD-MM-YYYY 23:59') : '';
+ let warehouse = values.warehouse;
+
+ if (!warehouse) {
+ frappe.msgprint({
+ title: __('Внимание'),
+ indicator: 'orange',
+ message: __('Пожалуйста, выберите склад по умолчанию')
+ });
+ return;
+ }
+
+ // Закрываем диалог
+ d.hide();
+ show_loading_dialog(__('Загрузка счетов-фактур...'));
+
+ // Загружаем счета-фактуры
+ load_etaxes_invoices(null, fromDate, toDate, warehouse);
+ }
+ });
+
+ d.show();
+ });
}
// Функция для отображения диалога с выбором счета-фактуры
@@ -354,13 +987,38 @@ function load_selected_invoices(frm, invoice_ids, token, warehouse) {
}
});
} else if (r.message && r.message.error === 'unauthorized') {
- frappe.msgprint({
- title: __('Ошибка авторизации'),
- indicator: 'red',
- message: __('Необходимо авторизоваться в системе E-Taxes. Токен устарел.')
- });
-
- // Останавливаем загрузку из-за ошибки авторизации
+ // Если ошибка авторизации, предлагаем авторизоваться заново
+ frappe.confirm(
+ __('Your E-Taxes session has expired. Would you like to authenticate now?'),
+ function() {
+ // Запускаем процесс аутентификации и после успеха повторяем загрузку
+ start_authentication_process(function() {
+ // После успешной аутентификации получаем новый токен и повторяем загрузку
+ frappe.call({
+ method: 'invoice_az.api.get_default_asan_login',
+ callback: function(login_r) {
+ if (login_r.message && login_r.message.found && login_r.message.main_token) {
+ // Используем новый токен для загрузки
+ load_selected_invoices(frm, invoice_ids, login_r.message.main_token, warehouse);
+ } else {
+ frappe.msgprint({
+ title: __('Ошибка'),
+ indicator: 'red',
+ message: __('Не удалось получить токен после авторизации')
+ });
+ }
+ }
+ });
+ });
+ },
+ function() {
+ // Если пользователь отказался, показываем сообщение
+ frappe.show_alert({
+ message: __('Authentication canceled. Operation cannot be completed.'),
+ indicator: 'red'
+ }, 5);
+ }
+ );
} else {
frappe.msgprint({
title: __('Ошибка'),
@@ -377,50 +1035,6 @@ function load_selected_invoices(frm, invoice_ids, token, warehouse) {
});
}
-// Функция для импорта одного инвойса
-function import_invoice_with_mappings(frm, invoice_data, warehouse) {
- // Получаем дату создания инвойса
- let creationDate = invoice_data.creationDate || invoice_data.date || frappe.datetime.nowdate();
- let scheduleDate = moment(creationDate).format('YYYY-MM-DD');
-
- frappe.call({
- method: 'invoice_az.api.import_invoice_with_mapping',
- args: {
- 'invoice_data': invoice_data,
- 'purchase_order_name': frm ? frm.doc.name : null,
- 'schedule_date': scheduleDate, // Передаем дату в API
- 'warehouse': warehouse // Передаем склад в API
- },
- callback: function(r) {
- if (r.message && r.message.success) {
- frappe.show_alert({
- message: __('Данные счета-фактуры импортированы успешно'),
- indicator: 'green'
- }, 5);
-
- // Перезагружаем документ, если создан новый
- if (r.message.purchase_order && (!frm || r.message.purchase_order !== frm.doc.name)) {
- frappe.set_route('Form', 'Purchase Order', r.message.purchase_order);
- } else if (frm) {
- frm.reload_doc();
- }
- } else if (r.message && r.message.unmatched_items) {
- // Если есть несопоставленные элементы, показываем диалог с ними
- show_unmatched_items_dialog(invoice_data, r.message.unmatched_items);
- } else if (r.message && r.message.unmatched_parties) {
- // Если есть несопоставленные контрагенты, показываем диалог с ними
- show_unmatched_parties_dialog(invoice_data, r.message.unmatched_parties);
- } else {
- frappe.msgprint({
- title: __('Ошибка'),
- indicator: 'red',
- message: r.message ? r.message.message : __('Ошибка импорта данных счета-фактуры')
- });
- }
- }
- });
-}
-
// Функция для отображения диалога с несопоставленными элементами
function show_unmatched_items_dialog(invoice_data, unmatched_items) {
// Создаем таблицу с несопоставленными товарами
@@ -526,278 +1140,3 @@ function show_unmatched_parties_dialog(invoice_data, unmatched_parties) {
d.show();
}
-
-// Функция для отображения диалога импорта при работе из списка
-function show_etaxes_import_dialog_for_list() {
- // Создаем диалог для выбора периода и склада
- var d = new frappe.ui.Dialog({
- title: __('Импорт счета-фактуры из E-Taxes'),
- fields: [
- {
- fieldname: 'date_range_section',
- fieldtype: 'Section Break',
- label: __('Период')
- },
- {
- fieldname: 'creationDateFrom',
- fieldtype: 'Date',
- label: __('Дата с'),
- default: moment().subtract(1, 'month').format('YYYY-MM-DD')
- },
- {
- fieldname: 'creationDateTo',
- fieldtype: 'Date',
- label: __('Дата по'),
- default: moment().format('YYYY-MM-DD')
- },
- {
- fieldname: 'settings_section',
- fieldtype: 'Section Break',
- label: __('Настройки')
- },
- {
- fieldname: 'warehouse',
- fieldtype: 'Link',
- label: __('Склад по умолчанию'),
- options: 'Warehouse',
- reqd: true,
- get_query: function() {
- return {
- filters: {
- 'is_group': 0,
- 'disabled': 0
- }
- };
- }
- }
- ],
- primary_action_label: __('Поиск'),
- primary_action: function() {
- var values = d.get_values();
-
- // Форматируем даты
- let fromDate = values.creationDateFrom ? moment(values.creationDateFrom).format('DD-MM-YYYY 00:00') : '';
- let toDate = values.creationDateTo ? moment(values.creationDateTo).format('DD-MM-YYYY 23:59') : '';
- let warehouse = values.warehouse;
-
- if (!warehouse) {
- frappe.msgprint({
- title: __('Внимание'),
- indicator: 'orange',
- message: __('Пожалуйста, выберите склад по умолчанию')
- });
- return;
- }
-
- // Закрываем диалог
- d.hide();
- show_loading_dialog(__('Загрузка счетов-фактур...'));
-
- // Сначала получаем настройки авторизации
- frappe.call({
- method: 'invoice_az.api.get_default_asan_login',
- callback: function(login_response) {
- if (login_response.message && login_response.message.found) {
- // Получаем токен из настроек
- let main_token = login_response.message.main_token;
-
- if (!main_token) {
- hide_loading_dialog();
- frappe.msgprint({
- title: __('Ошибка авторизации'),
- indicator: 'red',
- message: __('Не найден токен авторизации. Пожалуйста, завершите процесс авторизации в настройках E-Taxes.')
- });
- return;
- }
-
- // Загружаем счета-фактуры с использованием полученного токена
- frappe.call({
- method: 'invoice_az.api.get_invoices',
- args: {
- 'token': main_token,
- 'filters': JSON.stringify({
- "creationDateFrom": fromDate,
- "creationDateTo": toDate
- })
- },
- callback: function(r) {
- // Закрываем диалог загрузки
- hide_loading_dialog();
-
- if (r.message && !r.message.error) {
- // Получаем данные о счетах-фактурах
- let invoices = r.message.data || r.message.invoices || [];
-
- if (invoices.length > 0) {
- // Передаем токен и warehouse в следующий диалог
- show_invoice_selection_dialog_for_list(invoices, main_token, warehouse);
- } else {
- frappe.msgprint({
- title: __('Информация'),
- indicator: 'blue',
- message: __('За указанный период не найдено счетов-фактур')
- });
- }
- } else if (r.message && r.message.error === 'unauthorized') {
- // Если ошибка авторизации, предлагаем авторизоваться
- frappe.msgprint({
- title: __('Ошибка авторизации'),
- indicator: 'red',
- message: __('Необходимо авторизоваться в системе E-Taxes. Токен устарел.')
- });
- } else {
- frappe.msgprint({
- title: __('Ошибка'),
- indicator: 'red',
- message: r.message ? r.message.message : __('Ошибка загрузки счетов-фактур')
- });
- }
- }
- });
- } else {
- hide_loading_dialog();
- frappe.msgprint({
- title: __('Ошибка'),
- indicator: 'red',
- message: __('Не найдены настройки авторизации E-Taxes')
- });
- }
- }
- });
- }
- });
-
- d.show();
-}
-
-// Функция для отображения диалога с выбором счета-фактуры при работе из списка
-function show_invoice_selection_dialog_for_list(invoices, token, warehouse) {
- // Создаем таблицу с инвойсами
- var invoice_table = '';
-
- // Создаем диалог
- var d = new frappe.ui.Dialog({
- title: __('Выбор счета-фактуры'),
- fields: [
- {
- fieldname: 'invoices_html',
- fieldtype: 'HTML',
- options: invoice_table
- },
- {
- fieldname: 'warehouse_display',
- fieldtype: 'HTML',
- options: '' +
- __('Выбранный склад: ') + '' + warehouse + '
'
- }
- ],
- primary_action_label: __('Загрузить выбранные'),
- primary_action: function() {
- // Получаем выбранные инвойсы
- var selected_invoice_ids = [];
- d.$wrapper.find('.select-invoice:checked').each(function() {
- selected_invoice_ids.push($(this).data('id'));
- });
-
- if (selected_invoice_ids.length === 0) {
- frappe.msgprint({
- title: __('Внимание'),
- indicator: 'orange',
- message: __('Не выбрано ни одного счета-фактуры')
- });
- return;
- }
-
- // Закрываем диалог
- d.hide();
-
- // Загружаем выбранные инвойсы последовательно
- load_selected_invoices(null, selected_invoice_ids, token, warehouse);
- },
- secondary_action_label: __('Отмена'),
- secondary_action: function() {
- d.hide();
- }
- });
-
- d.show();
-
- // Добавляем обработчик для чекбокса "выбрать все"
- d.$wrapper.find('.select-all-invoices').on('change', function() {
- var is_checked = $(this).prop('checked');
- d.$wrapper.find('.select-invoice').prop('checked', is_checked);
- });
-}
-
-// Функция для отображения диалога загрузки
-function show_loading_dialog(message) {
- if (!window.loading_dialog) {
- window.loading_dialog = new frappe.ui.Dialog({
- title: __('Загрузка'),
- fields: [
- {
- fieldname: 'status_html',
- fieldtype: 'HTML',
- options: `
-
-
-
-
-
${message || __('Пожалуйста, подождите...')}
-
${__('Операция может занять некоторое время')}
-
-
- `
- }
- ],
- primary_action_label: __('Отмена'),
- primary_action: function() {
- window.loading_dialog.hide();
- }
- });
- } else {
- // Обновляем сообщение
- $('#status_message h4').text(message || __('Пожалуйста, подождите...'));
- }
-
- window.loading_dialog.show();
-}
-
-// Функция для скрытия диалога загрузки
-function hide_loading_dialog() {
- if (window.loading_dialog) {
- window.loading_dialog.hide();
- }
-}
\ No newline at end of file
diff --git a/invoice_az/invoice_az/doctype/asan_login/asan_login.js b/invoice_az/invoice_az/doctype/asan_login/asan_login.js
index 72b7e2f..b123ef3 100644
--- a/invoice_az/invoice_az/doctype/asan_login/asan_login.js
+++ b/invoice_az/invoice_az/doctype/asan_login/asan_login.js
@@ -35,6 +35,21 @@ frappe.ui.form.on('Asan Login', {
// Запускаем опрос статуса
startPollingWithTimeout(frm);
+
+ // Задержка, чтобы дать диалогу полностью создаться
+ setTimeout(function() {
+ // Если есть код верификации, отображаем его
+ if (r.message.verification_code) {
+ // Явно указываем текст кода
+ $('#verification_code').text(r.message.verification_code);
+
+ // Отображаем контейнер кода верификации
+ $('#verification_code_container').show();
+
+ // Принудительно устанавливаем стиль отображения
+ $('#verification_code_container').attr('style', 'display: block !important; margin-top: 15px;');
+ }
+ }, 500); // Задержка 500 мс для полной инициализации DOM
} else {
// Показываем сообщение об ошибке
frappe.msgprint({
@@ -222,6 +237,13 @@ function startPollingWithTimeout(frm, successCallback) {
Waiting for confirmation on your phone
Please check your phone and confirm the authentication request.
+
+
+
+ ----
+
+
Verification Code
+
`
}
@@ -257,6 +279,9 @@ function startPollingWithTimeout(frm, successCallback) {
`);
$('.lds-dual-ring').css('display', 'none');
+ // Скрываем код верификации при таймауте
+ $('#verification_code_container').hide();
+
// Обновляем поле auth_status и сохраняем
frm.set_value('auth_status', 'Failed');
@@ -273,7 +298,6 @@ function startPollingWithTimeout(frm, successCallback) {
}
attempts++;
- console.log(`Polling auth status, attempt ${attempts} of ${maxAttempts}`);
// Проверяем статус авторизации
frappe.call({
@@ -294,6 +318,13 @@ function startPollingWithTimeout(frm, successCallback) {
You are now authenticated. The page will reload shortly.
`);
$('.lds-dual-ring').css('display', 'none');
+ $('.lds-dual-ring').html(`
+
+
+
+ `);
+
+ // НЕ скрываем код верификации при успешной аутентификации
// Обновляем кнопку
status_dialog.set_primary_action(__('Continue'), function() {
@@ -332,6 +363,14 @@ function startPollingWithTimeout(frm, successCallback) {
${r.message ? r.message.message : 'An unknown error occurred.'}
`);
$('.lds-dual-ring').css('display', 'none');
+ $('.lds-dual-ring').html(`
+
+
+
+ `);
+
+ // Скрываем код верификации при ошибке
+ $('#verification_code_container').hide();
// Обновляем поле auth_status
frm.set_value('auth_status', 'Failed');
diff --git a/invoice_az/invoice_az/doctype/asan_login/asan_login.json b/invoice_az/invoice_az/doctype/asan_login/asan_login.json
index b0a9aea..0ee0e3d 100644
--- a/invoice_az/invoice_az/doctype/asan_login/asan_login.json
+++ b/invoice_az/invoice_az/doctype/asan_login/asan_login.json
@@ -11,6 +11,7 @@
"user_id",
"bearer_token",
"auth_status",
+ "verification_code",
"column_break_1",
"is_default",
"certificates_section",
@@ -45,9 +46,7 @@
{
"fieldname": "bearer_token",
"fieldtype": "Small Text",
- "hidden": 1,
- "label": "Bearer Token",
- "read_only": 1
+ "label": "Bearer Token"
},
{
"default": "Not Authenticated",
@@ -120,11 +119,16 @@
"label": "Taxpayer Response",
"options": "JSON",
"read_only": 1
+ },
+ {
+ "fieldname": "verification_code",
+ "fieldtype": "Data",
+ "label": "Verification Code"
}
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2025-05-12 13:30:47.341250",
+ "modified": "2025-05-13 14:13:35.366465",
"modified_by": "Administrator",
"module": "Invoice Az",
"name": "Asan Login",
diff --git a/test_invoice.git/HEAD b/test_invoice.git/HEAD
new file mode 100644
index 0000000..b870d82
--- /dev/null
+++ b/test_invoice.git/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/main
diff --git a/test_invoice.git/config b/test_invoice.git/config
new file mode 100644
index 0000000..d0fa58e
--- /dev/null
+++ b/test_invoice.git/config
@@ -0,0 +1,8 @@
+[core]
+ repositoryformatversion = 0
+ filemode = true
+ bare = true
+[remote "origin"]
+ url = https://github.com/Awey01/test_invoice.git
+ fetch = +refs/*:refs/*
+ mirror = true
diff --git a/test_invoice.git/description b/test_invoice.git/description
new file mode 100644
index 0000000..498b267
--- /dev/null
+++ b/test_invoice.git/description
@@ -0,0 +1 @@
+Unnamed repository; edit this file 'description' to name the repository.
diff --git a/test_invoice.git/hooks/applypatch-msg.sample b/test_invoice.git/hooks/applypatch-msg.sample
new file mode 100755
index 0000000..a5d7b84
--- /dev/null
+++ b/test_invoice.git/hooks/applypatch-msg.sample
@@ -0,0 +1,15 @@
+#!/bin/sh
+#
+# An example hook script to check the commit log message taken by
+# applypatch from an e-mail message.
+#
+# The hook should exit with non-zero status after issuing an
+# appropriate message if it wants to stop the commit. The hook is
+# allowed to edit the commit message file.
+#
+# To enable this hook, rename this file to "applypatch-msg".
+
+. git-sh-setup
+commitmsg="$(git rev-parse --git-path hooks/commit-msg)"
+test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"}
+:
diff --git a/test_invoice.git/hooks/commit-msg.sample b/test_invoice.git/hooks/commit-msg.sample
new file mode 100755
index 0000000..b58d118
--- /dev/null
+++ b/test_invoice.git/hooks/commit-msg.sample
@@ -0,0 +1,24 @@
+#!/bin/sh
+#
+# An example hook script to check the commit log message.
+# Called by "git commit" with one argument, the name of the file
+# that has the commit message. The hook should exit with non-zero
+# status after issuing an appropriate message if it wants to stop the
+# commit. The hook is allowed to edit the commit message file.
+#
+# To enable this hook, rename this file to "commit-msg".
+
+# Uncomment the below to add a Signed-off-by line to the message.
+# Doing this in a hook is a bad idea in general, but the prepare-commit-msg
+# hook is more suited to it.
+#
+# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
+# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"
+
+# This example catches duplicate Signed-off-by lines.
+
+test "" = "$(grep '^Signed-off-by: ' "$1" |
+ sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || {
+ echo >&2 Duplicate Signed-off-by lines.
+ exit 1
+}
diff --git a/test_invoice.git/hooks/fsmonitor-watchman.sample b/test_invoice.git/hooks/fsmonitor-watchman.sample
new file mode 100755
index 0000000..23e856f
--- /dev/null
+++ b/test_invoice.git/hooks/fsmonitor-watchman.sample
@@ -0,0 +1,174 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use IPC::Open2;
+
+# An example hook script to integrate Watchman
+# (https://facebook.github.io/watchman/) with git to speed up detecting
+# new and modified files.
+#
+# The hook is passed a version (currently 2) and last update token
+# formatted as a string and outputs to stdout a new update token and
+# all files that have been modified since the update token. Paths must
+# be relative to the root of the working tree and separated by a single NUL.
+#
+# To enable this hook, rename this file to "query-watchman" and set
+# 'git config core.fsmonitor .git/hooks/query-watchman'
+#
+my ($version, $last_update_token) = @ARGV;
+
+# Uncomment for debugging
+# print STDERR "$0 $version $last_update_token\n";
+
+# Check the hook interface version
+if ($version ne 2) {
+ die "Unsupported query-fsmonitor hook version '$version'.\n" .
+ "Falling back to scanning...\n";
+}
+
+my $git_work_tree = get_working_dir();
+
+my $retry = 1;
+
+my $json_pkg;
+eval {
+ require JSON::XS;
+ $json_pkg = "JSON::XS";
+ 1;
+} or do {
+ require JSON::PP;
+ $json_pkg = "JSON::PP";
+};
+
+launch_watchman();
+
+sub launch_watchman {
+ my $o = watchman_query();
+ if (is_work_tree_watched($o)) {
+ output_result($o->{clock}, @{$o->{files}});
+ }
+}
+
+sub output_result {
+ my ($clockid, @files) = @_;
+
+ # Uncomment for debugging watchman output
+ # open (my $fh, ">", ".git/watchman-output.out");
+ # binmode $fh, ":utf8";
+ # print $fh "$clockid\n@files\n";
+ # close $fh;
+
+ binmode STDOUT, ":utf8";
+ print $clockid;
+ print "\0";
+ local $, = "\0";
+ print @files;
+}
+
+sub watchman_clock {
+ my $response = qx/watchman clock "$git_work_tree"/;
+ die "Failed to get clock id on '$git_work_tree'.\n" .
+ "Falling back to scanning...\n" if $? != 0;
+
+ return $json_pkg->new->utf8->decode($response);
+}
+
+sub watchman_query {
+ my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty')
+ or die "open2() failed: $!\n" .
+ "Falling back to scanning...\n";
+
+ # In the query expression below we're asking for names of files that
+ # changed since $last_update_token but not from the .git folder.
+ #
+ # To accomplish this, we're using the "since" generator to use the
+ # recency index to select candidate nodes and "fields" to limit the
+ # output to file names only. Then we're using the "expression" term to
+ # further constrain the results.
+ my $last_update_line = "";
+ if (substr($last_update_token, 0, 1) eq "c") {
+ $last_update_token = "\"$last_update_token\"";
+ $last_update_line = qq[\n"since": $last_update_token,];
+ }
+ my $query = <<" END";
+ ["query", "$git_work_tree", {$last_update_line
+ "fields": ["name"],
+ "expression": ["not", ["dirname", ".git"]]
+ }]
+ END
+
+ # Uncomment for debugging the watchman query
+ # open (my $fh, ">", ".git/watchman-query.json");
+ # print $fh $query;
+ # close $fh;
+
+ print CHLD_IN $query;
+ close CHLD_IN;
+ my $response = do {local $/; };
+
+ # Uncomment for debugging the watch response
+ # open ($fh, ">", ".git/watchman-response.json");
+ # print $fh $response;
+ # close $fh;
+
+ die "Watchman: command returned no output.\n" .
+ "Falling back to scanning...\n" if $response eq "";
+ die "Watchman: command returned invalid output: $response\n" .
+ "Falling back to scanning...\n" unless $response =~ /^\{/;
+
+ return $json_pkg->new->utf8->decode($response);
+}
+
+sub is_work_tree_watched {
+ my ($output) = @_;
+ my $error = $output->{error};
+ if ($retry > 0 and $error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) {
+ $retry--;
+ my $response = qx/watchman watch "$git_work_tree"/;
+ die "Failed to make watchman watch '$git_work_tree'.\n" .
+ "Falling back to scanning...\n" if $? != 0;
+ $output = $json_pkg->new->utf8->decode($response);
+ $error = $output->{error};
+ die "Watchman: $error.\n" .
+ "Falling back to scanning...\n" if $error;
+
+ # Uncomment for debugging watchman output
+ # open (my $fh, ">", ".git/watchman-output.out");
+ # close $fh;
+
+ # Watchman will always return all files on the first query so
+ # return the fast "everything is dirty" flag to git and do the
+ # Watchman query just to get it over with now so we won't pay
+ # the cost in git to look up each individual file.
+ my $o = watchman_clock();
+ $error = $output->{error};
+
+ die "Watchman: $error.\n" .
+ "Falling back to scanning...\n" if $error;
+
+ output_result($o->{clock}, ("/"));
+ $last_update_token = $o->{clock};
+
+ eval { launch_watchman() };
+ return 0;
+ }
+
+ die "Watchman: $error.\n" .
+ "Falling back to scanning...\n" if $error;
+
+ return 1;
+}
+
+sub get_working_dir {
+ my $working_dir;
+ if ($^O =~ 'msys' || $^O =~ 'cygwin') {
+ $working_dir = Win32::GetCwd();
+ $working_dir =~ tr/\\/\//;
+ } else {
+ require Cwd;
+ $working_dir = Cwd::cwd();
+ }
+
+ return $working_dir;
+}
diff --git a/test_invoice.git/hooks/post-update.sample b/test_invoice.git/hooks/post-update.sample
new file mode 100755
index 0000000..ec17ec1
--- /dev/null
+++ b/test_invoice.git/hooks/post-update.sample
@@ -0,0 +1,8 @@
+#!/bin/sh
+#
+# An example hook script to prepare a packed repository for use over
+# dumb transports.
+#
+# To enable this hook, rename this file to "post-update".
+
+exec git update-server-info
diff --git a/test_invoice.git/hooks/pre-applypatch.sample b/test_invoice.git/hooks/pre-applypatch.sample
new file mode 100755
index 0000000..4142082
--- /dev/null
+++ b/test_invoice.git/hooks/pre-applypatch.sample
@@ -0,0 +1,14 @@
+#!/bin/sh
+#
+# An example hook script to verify what is about to be committed
+# by applypatch from an e-mail message.
+#
+# The hook should exit with non-zero status after issuing an
+# appropriate message if it wants to stop the commit.
+#
+# To enable this hook, rename this file to "pre-applypatch".
+
+. git-sh-setup
+precommit="$(git rev-parse --git-path hooks/pre-commit)"
+test -x "$precommit" && exec "$precommit" ${1+"$@"}
+:
diff --git a/test_invoice.git/hooks/pre-commit.sample b/test_invoice.git/hooks/pre-commit.sample
new file mode 100755
index 0000000..e144712
--- /dev/null
+++ b/test_invoice.git/hooks/pre-commit.sample
@@ -0,0 +1,49 @@
+#!/bin/sh
+#
+# An example hook script to verify what is about to be committed.
+# Called by "git commit" with no arguments. The hook should
+# exit with non-zero status after issuing an appropriate message if
+# it wants to stop the commit.
+#
+# To enable this hook, rename this file to "pre-commit".
+
+if git rev-parse --verify HEAD >/dev/null 2>&1
+then
+ against=HEAD
+else
+ # Initial commit: diff against an empty tree object
+ against=$(git hash-object -t tree /dev/null)
+fi
+
+# If you want to allow non-ASCII filenames set this variable to true.
+allownonascii=$(git config --type=bool hooks.allownonascii)
+
+# Redirect output to stderr.
+exec 1>&2
+
+# Cross platform projects tend to avoid non-ASCII filenames; prevent
+# them from being added to the repository. We exploit the fact that the
+# printable range starts at the space character and ends with tilde.
+if [ "$allownonascii" != "true" ] &&
+ # Note that the use of brackets around a tr range is ok here, (it's
+ # even required, for portability to Solaris 10's /usr/bin/tr), since
+ # the square bracket bytes happen to fall in the designated range.
+ test $(git diff --cached --name-only --diff-filter=A -z $against |
+ LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0
+then
+ cat <<\EOF
+Error: Attempt to add a non-ASCII file name.
+
+This can cause problems if you want to work with people on other platforms.
+
+To be portable it is advisable to rename the file.
+
+If you know what you are doing you can disable this check using:
+
+ git config hooks.allownonascii true
+EOF
+ exit 1
+fi
+
+# If there are whitespace errors, print the offending file names and fail.
+exec git diff-index --check --cached $against --
diff --git a/test_invoice.git/hooks/pre-merge-commit.sample b/test_invoice.git/hooks/pre-merge-commit.sample
new file mode 100755
index 0000000..399eab1
--- /dev/null
+++ b/test_invoice.git/hooks/pre-merge-commit.sample
@@ -0,0 +1,13 @@
+#!/bin/sh
+#
+# An example hook script to verify what is about to be committed.
+# Called by "git merge" with no arguments. The hook should
+# exit with non-zero status after issuing an appropriate message to
+# stderr if it wants to stop the merge commit.
+#
+# To enable this hook, rename this file to "pre-merge-commit".
+
+. git-sh-setup
+test -x "$GIT_DIR/hooks/pre-commit" &&
+ exec "$GIT_DIR/hooks/pre-commit"
+:
diff --git a/test_invoice.git/hooks/pre-push.sample b/test_invoice.git/hooks/pre-push.sample
new file mode 100755
index 0000000..4ce688d
--- /dev/null
+++ b/test_invoice.git/hooks/pre-push.sample
@@ -0,0 +1,53 @@
+#!/bin/sh
+
+# An example hook script to verify what is about to be pushed. Called by "git
+# push" after it has checked the remote status, but before anything has been
+# pushed. If this script exits with a non-zero status nothing will be pushed.
+#
+# This hook is called with the following parameters:
+#
+# $1 -- Name of the remote to which the push is being done
+# $2 -- URL to which the push is being done
+#
+# If pushing without using a named remote those arguments will be equal.
+#
+# Information about the commits which are being pushed is supplied as lines to
+# the standard input in the form:
+#
+#
+#
+# This sample shows how to prevent push of commits where the log message starts
+# with "WIP" (work in progress).
+
+remote="$1"
+url="$2"
+
+zero=$(git hash-object --stdin &2 "Found WIP commit in $local_ref, not pushing"
+ exit 1
+ fi
+ fi
+done
+
+exit 0
diff --git a/test_invoice.git/hooks/pre-rebase.sample b/test_invoice.git/hooks/pre-rebase.sample
new file mode 100755
index 0000000..6cbef5c
--- /dev/null
+++ b/test_invoice.git/hooks/pre-rebase.sample
@@ -0,0 +1,169 @@
+#!/bin/sh
+#
+# Copyright (c) 2006, 2008 Junio C Hamano
+#
+# The "pre-rebase" hook is run just before "git rebase" starts doing
+# its job, and can prevent the command from running by exiting with
+# non-zero status.
+#
+# The hook is called with the following parameters:
+#
+# $1 -- the upstream the series was forked from.
+# $2 -- the branch being rebased (or empty when rebasing the current branch).
+#
+# This sample shows how to prevent topic branches that are already
+# merged to 'next' branch from getting rebased, because allowing it
+# would result in rebasing already published history.
+
+publish=next
+basebranch="$1"
+if test "$#" = 2
+then
+ topic="refs/heads/$2"
+else
+ topic=`git symbolic-ref HEAD` ||
+ exit 0 ;# we do not interrupt rebasing detached HEAD
+fi
+
+case "$topic" in
+refs/heads/??/*)
+ ;;
+*)
+ exit 0 ;# we do not interrupt others.
+ ;;
+esac
+
+# Now we are dealing with a topic branch being rebased
+# on top of master. Is it OK to rebase it?
+
+# Does the topic really exist?
+git show-ref -q "$topic" || {
+ echo >&2 "No such branch $topic"
+ exit 1
+}
+
+# Is topic fully merged to master?
+not_in_master=`git rev-list --pretty=oneline ^master "$topic"`
+if test -z "$not_in_master"
+then
+ echo >&2 "$topic is fully merged to master; better remove it."
+ exit 1 ;# we could allow it, but there is no point.
+fi
+
+# Is topic ever merged to next? If so you should not be rebasing it.
+only_next_1=`git rev-list ^master "^$topic" ${publish} | sort`
+only_next_2=`git rev-list ^master ${publish} | sort`
+if test "$only_next_1" = "$only_next_2"
+then
+ not_in_topic=`git rev-list "^$topic" master`
+ if test -z "$not_in_topic"
+ then
+ echo >&2 "$topic is already up to date with master"
+ exit 1 ;# we could allow it, but there is no point.
+ else
+ exit 0
+ fi
+else
+ not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"`
+ /usr/bin/perl -e '
+ my $topic = $ARGV[0];
+ my $msg = "* $topic has commits already merged to public branch:\n";
+ my (%not_in_next) = map {
+ /^([0-9a-f]+) /;
+ ($1 => 1);
+ } split(/\n/, $ARGV[1]);
+ for my $elem (map {
+ /^([0-9a-f]+) (.*)$/;
+ [$1 => $2];
+ } split(/\n/, $ARGV[2])) {
+ if (!exists $not_in_next{$elem->[0]}) {
+ if ($msg) {
+ print STDERR $msg;
+ undef $msg;
+ }
+ print STDERR " $elem->[1]\n";
+ }
+ }
+ ' "$topic" "$not_in_next" "$not_in_master"
+ exit 1
+fi
+
+<<\DOC_END
+
+This sample hook safeguards topic branches that have been
+published from being rewound.
+
+The workflow assumed here is:
+
+ * Once a topic branch forks from "master", "master" is never
+ merged into it again (either directly or indirectly).
+
+ * Once a topic branch is fully cooked and merged into "master",
+ it is deleted. If you need to build on top of it to correct
+ earlier mistakes, a new topic branch is created by forking at
+ the tip of the "master". This is not strictly necessary, but
+ it makes it easier to keep your history simple.
+
+ * Whenever you need to test or publish your changes to topic
+ branches, merge them into "next" branch.
+
+The script, being an example, hardcodes the publish branch name
+to be "next", but it is trivial to make it configurable via
+$GIT_DIR/config mechanism.
+
+With this workflow, you would want to know:
+
+(1) ... if a topic branch has ever been merged to "next". Young
+ topic branches can have stupid mistakes you would rather
+ clean up before publishing, and things that have not been
+ merged into other branches can be easily rebased without
+ affecting other people. But once it is published, you would
+ not want to rewind it.
+
+(2) ... if a topic branch has been fully merged to "master".
+ Then you can delete it. More importantly, you should not
+ build on top of it -- other people may already want to
+ change things related to the topic as patches against your
+ "master", so if you need further changes, it is better to
+ fork the topic (perhaps with the same name) afresh from the
+ tip of "master".
+
+Let's look at this example:
+
+ o---o---o---o---o---o---o---o---o---o "next"
+ / / / /
+ / a---a---b A / /
+ / / / /
+ / / c---c---c---c B /
+ / / / \ /
+ / / / b---b C \ /
+ / / / / \ /
+ ---o---o---o---o---o---o---o---o---o---o---o "master"
+
+
+A, B and C are topic branches.
+
+ * A has one fix since it was merged up to "next".
+
+ * B has finished. It has been fully merged up to "master" and "next",
+ and is ready to be deleted.
+
+ * C has not merged to "next" at all.
+
+We would want to allow C to be rebased, refuse A, and encourage
+B to be deleted.
+
+To compute (1):
+
+ git rev-list ^master ^topic next
+ git rev-list ^master next
+
+ if these match, topic has not merged in next at all.
+
+To compute (2):
+
+ git rev-list master..topic
+
+ if this is empty, it is fully merged to "master".
+
+DOC_END
diff --git a/test_invoice.git/hooks/pre-receive.sample b/test_invoice.git/hooks/pre-receive.sample
new file mode 100755
index 0000000..a1fd29e
--- /dev/null
+++ b/test_invoice.git/hooks/pre-receive.sample
@@ -0,0 +1,24 @@
+#!/bin/sh
+#
+# An example hook script to make use of push options.
+# The example simply echoes all push options that start with 'echoback='
+# and rejects all pushes when the "reject" push option is used.
+#
+# To enable this hook, rename this file to "pre-receive".
+
+if test -n "$GIT_PUSH_OPTION_COUNT"
+then
+ i=0
+ while test "$i" -lt "$GIT_PUSH_OPTION_COUNT"
+ do
+ eval "value=\$GIT_PUSH_OPTION_$i"
+ case "$value" in
+ echoback=*)
+ echo "echo from the pre-receive-hook: ${value#*=}" >&2
+ ;;
+ reject)
+ exit 1
+ esac
+ i=$((i + 1))
+ done
+fi
diff --git a/test_invoice.git/hooks/prepare-commit-msg.sample b/test_invoice.git/hooks/prepare-commit-msg.sample
new file mode 100755
index 0000000..10fa14c
--- /dev/null
+++ b/test_invoice.git/hooks/prepare-commit-msg.sample
@@ -0,0 +1,42 @@
+#!/bin/sh
+#
+# An example hook script to prepare the commit log message.
+# Called by "git commit" with the name of the file that has the
+# commit message, followed by the description of the commit
+# message's source. The hook's purpose is to edit the commit
+# message file. If the hook fails with a non-zero status,
+# the commit is aborted.
+#
+# To enable this hook, rename this file to "prepare-commit-msg".
+
+# This hook includes three examples. The first one removes the
+# "# Please enter the commit message..." help message.
+#
+# The second includes the output of "git diff --name-status -r"
+# into the message, just before the "git status" output. It is
+# commented because it doesn't cope with --amend or with squashed
+# commits.
+#
+# The third example adds a Signed-off-by line to the message, that can
+# still be edited. This is rarely a good idea.
+
+COMMIT_MSG_FILE=$1
+COMMIT_SOURCE=$2
+SHA1=$3
+
+/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE"
+
+# case "$COMMIT_SOURCE,$SHA1" in
+# ,|template,)
+# /usr/bin/perl -i.bak -pe '
+# print "\n" . `git diff --cached --name-status -r`
+# if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;;
+# *) ;;
+# esac
+
+# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
+# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE"
+# if test -z "$COMMIT_SOURCE"
+# then
+# /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE"
+# fi
diff --git a/test_invoice.git/hooks/push-to-checkout.sample b/test_invoice.git/hooks/push-to-checkout.sample
new file mode 100755
index 0000000..af5a0c0
--- /dev/null
+++ b/test_invoice.git/hooks/push-to-checkout.sample
@@ -0,0 +1,78 @@
+#!/bin/sh
+
+# An example hook script to update a checked-out tree on a git push.
+#
+# This hook is invoked by git-receive-pack(1) when it reacts to git
+# push and updates reference(s) in its repository, and when the push
+# tries to update the branch that is currently checked out and the
+# receive.denyCurrentBranch configuration variable is set to
+# updateInstead.
+#
+# By default, such a push is refused if the working tree and the index
+# of the remote repository has any difference from the currently
+# checked out commit; when both the working tree and the index match
+# the current commit, they are updated to match the newly pushed tip
+# of the branch. This hook is to be used to override the default
+# behaviour; however the code below reimplements the default behaviour
+# as a starting point for convenient modification.
+#
+# The hook receives the commit with which the tip of the current
+# branch is going to be updated:
+commit=$1
+
+# It can exit with a non-zero status to refuse the push (when it does
+# so, it must not modify the index or the working tree).
+die () {
+ echo >&2 "$*"
+ exit 1
+}
+
+# Or it can make any necessary changes to the working tree and to the
+# index to bring them to the desired state when the tip of the current
+# branch is updated to the new commit, and exit with a zero status.
+#
+# For example, the hook can simply run git read-tree -u -m HEAD "$1"
+# in order to emulate git fetch that is run in the reverse direction
+# with git push, as the two-tree form of git read-tree -u -m is
+# essentially the same as git switch or git checkout that switches
+# branches while keeping the local changes in the working tree that do
+# not interfere with the difference between the branches.
+
+# The below is a more-or-less exact translation to shell of the C code
+# for the default behaviour for git's push-to-checkout hook defined in
+# the push_to_deploy() function in builtin/receive-pack.c.
+#
+# Note that the hook will be executed from the repository directory,
+# not from the working tree, so if you want to perform operations on
+# the working tree, you will have to adapt your code accordingly, e.g.
+# by adding "cd .." or using relative paths.
+
+if ! git update-index -q --ignore-submodules --refresh
+then
+ die "Up-to-date check failed"
+fi
+
+if ! git diff-files --quiet --ignore-submodules --
+then
+ die "Working directory has unstaged changes"
+fi
+
+# This is a rough translation of:
+#
+# head_has_history() ? "HEAD" : EMPTY_TREE_SHA1_HEX
+if git cat-file -e HEAD 2>/dev/null
+then
+ head=HEAD
+else
+ head=$(git hash-object -t tree --stdin &2
+ echo " (if you want, you could supply GIT_DIR then run" >&2
+ echo " $0 [ ] )" >&2
+ exit 1
+fi
+
+if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then
+ echo "usage: $0 [ ] " >&2
+ exit 1
+fi
+
+# --- Config
+allowunannotated=$(git config --type=bool hooks.allowunannotated)
+allowdeletebranch=$(git config --type=bool hooks.allowdeletebranch)
+denycreatebranch=$(git config --type=bool hooks.denycreatebranch)
+allowdeletetag=$(git config --type=bool hooks.allowdeletetag)
+allowmodifytag=$(git config --type=bool hooks.allowmodifytag)
+
+# check for no description
+projectdesc=$(sed -e '1q' "$GIT_DIR/description")
+case "$projectdesc" in
+"Unnamed repository"* | "")
+ echo "*** Project description file hasn't been set" >&2
+ exit 1
+ ;;
+esac
+
+# --- Check types
+# if $newrev is 0000...0000, it's a commit to delete a ref.
+zero=$(git hash-object --stdin &2
+ echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2
+ exit 1
+ fi
+ ;;
+ refs/tags/*,delete)
+ # delete tag
+ if [ "$allowdeletetag" != "true" ]; then
+ echo "*** Deleting a tag is not allowed in this repository" >&2
+ exit 1
+ fi
+ ;;
+ refs/tags/*,tag)
+ # annotated tag
+ if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1
+ then
+ echo "*** Tag '$refname' already exists." >&2
+ echo "*** Modifying a tag is not allowed in this repository." >&2
+ exit 1
+ fi
+ ;;
+ refs/heads/*,commit)
+ # branch
+ if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then
+ echo "*** Creating a branch is not allowed in this repository" >&2
+ exit 1
+ fi
+ ;;
+ refs/heads/*,delete)
+ # delete branch
+ if [ "$allowdeletebranch" != "true" ]; then
+ echo "*** Deleting a branch is not allowed in this repository" >&2
+ exit 1
+ fi
+ ;;
+ refs/remotes/*,commit)
+ # tracking branch
+ ;;
+ refs/remotes/*,delete)
+ # delete tracking branch
+ if [ "$allowdeletebranch" != "true" ]; then
+ echo "*** Deleting a tracking branch is not allowed in this repository" >&2
+ exit 1
+ fi
+ ;;
+ *)
+ # Anything else (is there anything else?)
+ echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2
+ exit 1
+ ;;
+esac
+
+# --- Finished
+exit 0
diff --git a/test_invoice.git/info/exclude b/test_invoice.git/info/exclude
new file mode 100644
index 0000000..a5196d1
--- /dev/null
+++ b/test_invoice.git/info/exclude
@@ -0,0 +1,6 @@
+# git ls-files --others --exclude-from=.git/info/exclude
+# Lines that start with '#' are comments.
+# For a project mostly in C, the following would be a good set of
+# exclude patterns (uncomment them if you want to use them):
+# *.[oa]
+# *~
diff --git a/test_invoice.git/objects/pack/pack-02a7e7e33bd9a362208ec9a6cbd2b910ef8a15c5.idx b/test_invoice.git/objects/pack/pack-02a7e7e33bd9a362208ec9a6cbd2b910ef8a15c5.idx
new file mode 100644
index 0000000000000000000000000000000000000000..34b0b6f1e325fadd54a94582e66294c8f6f55f92
GIT binary patch
literal 3900
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
P^)cVfz0r=#dJg{s?4e}V
literal 0
HcmV?d00001
diff --git a/test_invoice.git/objects/pack/pack-02a7e7e33bd9a362208ec9a6cbd2b910ef8a15c5.pack b/test_invoice.git/objects/pack/pack-02a7e7e33bd9a362208ec9a6cbd2b910ef8a15c5.pack
new file mode 100644
index 0000000000000000000000000000000000000000..40ed613f96a19d155996f8f6a2549ec0c42ddf43
GIT binary patch
literal 86481
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