first commit

This commit is contained in:
Ali 2025-05-06 17:27:43 +04:00
parent 3461bd7527
commit 07450e3ad4
41 changed files with 7086 additions and 0 deletions

1519
invoice_az/api.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,124 @@
frappe.listview_settings['E-Taxes Item'] = {
add_fields: ['status', 'mapped_item'],
get_indicator: function(doc) {
if (doc.status === 'Mapped') {
return [__('Mapped'), 'green', 'status,=,Mapped'];
} else if (doc.status === 'Processing') {
return [__('Processing'), 'orange', 'status,=,Processing'];
} else {
return [__('New'), 'red', 'status,=,New'];
}
},
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();
});
}
};
// Function to display the invoice filter dialog
function show_invoice_filter_dialog() {
// Get previous year for start date and current date for end date
const prevYear = moment().subtract(1, 'year').year();
const startDate = prevYear + "-01-01"; // January 1st of previous year
const endDate = moment().format('YYYY-MM-DD'); // Today
// Create simple dialog for selecting period
var d = new frappe.ui.Dialog({
title: __('Invoice Filters'),
fields: [
{
fieldname: 'date_range_section',
fieldtype: 'Section Break',
label: __('Date Range')
},
{
fieldname: 'creationDateFrom',
fieldtype: 'Date',
label: __('From Date'),
default: startDate
},
{
fieldname: 'creationDateTo',
fieldtype: 'Date',
label: __('To Date'),
default: endDate
}
],
primary_action_label: __('Load Items'),
primary_action: function() {
var values = d.get_values();
// 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: `
<div class="text-center">
<div class="lds-dual-ring" style="display: inline-block; width: 64px; height: 64px;">
<div style="width: 64px; height: 64px; border: 6px solid #4e73df; border-radius: 50%; border-color: #4e73df transparent #4e73df transparent; animation: lds-dual-ring 1.2s linear infinite;"></div>
</div>
<style>
@keyframes lds-dual-ring {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
<div class="mt-3" id="status_message">
<h4>Loading items from E-Taxes</h4>
<p>Please wait, this process may take several minutes.</p>
</div>
</div>
`
}
]
});
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');
// Load items
frappe.call({
method: 'invoice_az.api.load_items_from_invoices',
args: {
'date_from': fromDate,
'date_to': toDate
},
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', [r.message.created_count]),
indicator: 'green'
}, 5);
// Refresh list
cur_list.refresh();
} else {
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: r.message ? r.message.message : __('An error occurred while loading items')
});
}
}
});
}
});
d.show();
}

View File

@ -0,0 +1,240 @@
frappe.listview_settings['E-Taxes Parties'] = {
add_fields: ['status', 'mapped_party', 'erp_party_type'],
get_indicator: function(doc) {
if (doc.status === 'Сопоставлен') {
return [__('Сопоставлен'), 'green', 'status,=,Сопоставлен'];
} else if (doc.status === 'В обработке') {
return [__('В обработке'), 'orange', 'status,=,В обработке'];
} else {
return [__('Новый'), 'red', 'status,=,Новый'];
}
},
onload: function(listview) {
// Добавляем кнопку для загрузки контрагентов из E-Taxes
listview.page.add_menu_item(__('Загрузить из E-Taxes'), function() {
show_invoice_filter_dialog();
});
}
};
// Функция для отображения диалога выбора периода инвойсов
function show_invoice_filter_dialog() {
// Использовать значения дат, охватывающие продолжительный период
const startDate = "01-01-2020 00:00";
const endDate = moment().add(1, 'year').format('DD-MM-YYYY HH:mm');
// Создаем диалог с полями для фильтрации
var d = new frappe.ui.Dialog({
title: __('Фильтры инвойсов'),
fields: [
{
fieldname: 'date_range_section',
fieldtype: 'Section Break',
label: __('Период')
},
{
fieldname: 'creationDateFrom',
fieldtype: 'Data',
label: __('С даты'),
default: startDate
},
{
fieldname: 'creationDateTo',
fieldtype: 'Data',
label: __('По дату'),
default: endDate
},
{
fieldname: 'invoice_details_section',
fieldtype: 'Section Break',
label: __('Детали инвойса')
},
{
fieldname: 'serialNumber',
fieldtype: 'Data',
label: __('Серийный номер')
},
{
fieldname: 'party_section',
fieldtype: 'Section Break',
label: __('Отправитель/Получатель')
},
{
fieldname: 'senderTin',
fieldtype: 'Data',
label: __('ИНН отправителя')
},
{
fieldname: 'senderName',
fieldtype: 'Data',
label: __('Имя отправителя')
},
{
fieldname: 'receiverTin',
fieldtype: 'Data',
label: __('ИНН получателя')
},
{
fieldname: 'receiverName',
fieldtype: 'Data',
label: __('Имя получателя')
},
{
fieldname: 'amount_section',
fieldtype: 'Section Break',
label: __('Сумма')
},
{
fieldname: 'amountFrom',
fieldtype: 'Float',
label: __('Сумма от'),
precision: 2
},
{
fieldname: 'amountTo',
fieldtype: 'Float',
label: __('Сумма до'),
precision: 2
},
{
fieldname: 'limit_section',
fieldtype: 'Section Break',
label: __('Лимит')
},
{
fieldname: 'maxCount',
fieldtype: 'Int',
label: __('Макс. кол-во записей'),
default: 200
}
],
primary_action_label: __('Получить инвойсы'),
primary_action: function() {
var values = d.get_values();
// Дополняем значения фильтров полными параметрами
const fullPayload = {
"sortBy": "creationDate",
"sortAsc": true,
"statuses": ["approved", "onApproval", "updateApproval", "updateRequested",
"cancelRequested", "approvedBySystem", "onApprovalEdited",
"deactivated", "cancelationRefused", "correctionRefused"],
"types": ["current", "corrected"],
"kinds": ["defaultInvoice", "agent", "resale", "recycling", "taxCodex163",
"taxCodex177_5", "returnInvoice", "returnByAgent", "returnRecycled",
"exportNoteInvoice", "exciseGoodsTransfer", "advanceInvoice"],
"offset": 0,
"actionOwner": null,
// Заполняем значениями из диалога
"serialNumber": values.serialNumber || null,
"senderTin": values.senderTin || null,
"senderName": values.senderName || null,
"productName": null,
"productCode": null,
"receiverTin": values.receiverTin || null,
"receiverName": values.receiverName || null,
"creationDateFrom": values.creationDateFrom || startDate,
"creationDateTo": values.creationDateTo || endDate,
"amountFrom": values.amountFrom || null,
"amountTo": values.amountTo || null,
"maxCount": values.maxCount || 200
};
// Закрываем диалог
d.hide();
// Получаем настройки Asan Login
frappe.call({
method: 'invoice_az.api.get_default_asan_login',
callback: function(r) {
if (r.message && r.message.found) {
if (r.message.auth_status === 'Fully Authenticated') {
// Показываем индикатор загрузки
frappe.show_alert({
message: __('Получение инвойсов из E-Taxes, пожалуйста, подождите...'),
indicator: 'blue'
}, 10);
// Получаем инвойсы
frappe.call({
method: 'invoice_az.api.get_invoices',
args: {
'token': r.message.main_token,
'filters': JSON.stringify(fullPayload)
},
callback: function(r2) {
if (r2.message) {
if (r2.message.error === "unauthorized") {
frappe.msgprint({
title: __('Ошибка аутентификации'),
indicator: 'red',
message: __('Необходимо выполнить повторную аутентификацию в Asan Login')
});
frappe.set_route('List', 'Asan Login');
return;
}
let invoicesData = null;
if (r2.message.data && r2.message.data.length) {
invoicesData = r2.message.data;
} else if (r2.message.invoices && r2.message.invoices.length) {
invoicesData = r2.message.invoices;
}
if (invoicesData && invoicesData.length) {
// Извлекаем контрагентов из инвойсов
frappe.call({
method: 'invoice_az.e_taxes_settings.extract_parties_from_invoices',
args: {
'invoices': invoicesData
},
callback: function(r3) {
if (r3.message && r3.message.success) {
frappe.show_alert({
message: __('Извлечено и создано {0} уникальных контрагентов', [r3.message.parties_created]),
indicator: 'green'
}, 5);
// Обновляем список
cur_list.refresh();
} else {
frappe.msgprint({
title: __('Ошибка'),
indicator: 'red',
message: r3.message ? r3.message.message : __('Произошла ошибка при извлечении контрагентов из инвойсов')
});
}
}
});
} else {
frappe.msgprint(__('Инвойсы не найдены по заданным фильтрам'));
}
}
},
error: function(err) {
console.error('Error fetching invoices:', err);
frappe.msgprint(__('Ошибка при получении инвойсов. См. консоль для деталей.'));
}
});
} else {
frappe.msgprint({
title: __('Требуется аутентификация'),
indicator: 'orange',
message: __('Необходимо выполнить полную аутентификацию в Asan Login перед получением инвойсов')
});
frappe.set_route('List', 'Asan Login');
}
} else {
frappe.msgprint(__('Настройки Asan Login не найдены'));
frappe.set_route('List', 'Asan Login');
}
}
});
}
});
d.show();
}

View File

@ -5,6 +5,23 @@ app_description = "This application uploads and downloads invoices from e taxes
app_email = "info@jeyerp.az"
app_license = "unlicense"
doctype_js = {
"Purchase Order": "public/js/purchase_order.js"
}
page_js = {
"purchase-order": "public/js/purchase_order.js"
}
app_include_js = [
"/assets/invoice_az/js/purchase_order.js"
]
doctype_list_js = {
"E-Taxes Item": "client/e_taxes_items_list.js",
"E-Taxes Parties": "client/e_taxes_parties_list.js"
}
# Apps
# ------------------

View File

@ -0,0 +1,533 @@
frappe.ui.form.on('Asan Login', {
refresh: function(frm) {
// Остановить любые существующие интервалы
if (window.authStatusInterval) {
clearInterval(window.authStatusInterval);
window.authStatusInterval = null;
}
// Кнопка для получения токена
frm.add_custom_button(__('Login with Asan Imza'), function() {
// Проверяем, что поля phone и user_id заполнены
if (!frm.doc.phone || !frm.doc.user_id) {
frappe.msgprint(__('Please fill in Phone and User ID fields'));
return;
}
// Показываем сообщение о том, что запрос отправлен
frappe.show_alert({
message: __('Authentication request sent. Please confirm on your phone.'),
indicator: 'blue'
}, 5);
// Отправляем запрос для начала процесса аутентификации
frappe.call({
method: 'invoice_az.api.handle_authentication',
args: {
'asan_login_name': frm.doc.name
},
callback: function(r) {
if (r.message && r.message.success) {
// Сохраняем токен в документе и запускаем опрос статуса
if (r.message.bearer_token) {
frm.set_value('bearer_token', r.message.bearer_token);
}
// Запускаем опрос статуса
startPollingWithTimeout(frm);
} else {
// Показываем сообщение об ошибке
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: r.message ? r.message.message : __('Authentication request failed.')
});
}
}
});
}, __('Login'));
// Кнопка для получения сертификатов (доступна только если авторизация успешна)
if (frm.doc.auth_status === 'Authenticated' || frm.doc.auth_status === 'Fully Authenticated') {
frm.add_custom_button(__('Get Certificates'), function() {
frappe.call({
method: 'invoice_az.api.get_auth_certificates',
args: {
'asan_login_name': frm.doc.name
},
callback: function(r) {
if (r.message && r.message.success) {
// Обновляем форму после успешного получения сертификатов
frm.reload_doc();
// Показываем сообщение об успешном получении сертификатов
frappe.msgprint({
title: __('Success'),
indicator: 'green',
message: __('Certificates retrieved successfully')
});
// Отображаем список сертификатов в диалоговом окне
if (r.message.certificates) {
showCertificatesDialog(r.message.certificates, frm);
}
} else if (r.message && r.message.unauthorized) {
// Если ошибка авторизации, предлагаем авторизоваться заново
showAuthDialog(frm);
} else {
// Другие ошибки
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: r.message ? r.message.message : __('Failed to get certificates')
});
}
}
});
}, __('Actions'));
}
// Кнопка для выбора налогоплательщика (доступна если авторизация успешна и сертификат выбран)
if ((frm.doc.auth_status === 'Authenticated' || frm.doc.auth_status === 'Fully Authenticated') &&
frm.doc.selected_certificate) {
frm.add_custom_button(__('Choose Taxpayer'), function() {
frappe.call({
method: 'invoice_az.api.select_taxpayer',
args: {
'asan_login_name': frm.doc.name
},
callback: function(r) {
if (r.message && r.message.success) {
// Обновляем форму после успешного выбора налогоплательщика
frm.reload_doc();
// Показываем сообщение об успешном выборе
frappe.msgprint({
title: __('Success'),
indicator: 'green',
message: __('Taxpayer selected successfully')
});
} else if (r.message && r.message.unauthorized) {
// Если ошибка авторизации, предлагаем авторизоваться заново
showAuthDialog(frm);
} else {
// Другие ошибки
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: r.message ? r.message.message : __('Failed to select taxpayer')
});
}
}
});
}, __('Actions'));
}
// Обработчик изменения выбранного сертификата
if (frm.doc.certificates_json) {
try {
const certificates = JSON.parse(frm.doc.certificates_json);
frm.add_custom_button(__('Select Certificate'), function() {
showCertificatesDialog(certificates, frm);
}, __('Actions'));
} catch (e) {
console.error('Error parsing certificates JSON:', e);
}
}
},
// Обработчик изменения выбранного сертификата (введенного вручную)
selected_certificate: function(frm) {
// Только если поле не пустое и сертификаты доступны
if (frm.doc.selected_certificate && frm.doc.certificates_json) {
try {
// Получаем данные сертификатов
const certificates = JSON.parse(frm.doc.certificates_json);
if (!Array.isArray(certificates) || certificates.length === 0) return;
// Ищем сертификат по имени или его части
const searchStr = frm.doc.selected_certificate.toLowerCase();
// Сначала пытаемся найти точное совпадение по имени и идентификатору
let foundCert = null;
let foundIndex = -1;
for (let i = 0; i < certificates.length; i++) {
const cert = certificates[i];
let certName = '';
let certId = '';
if (cert.taxpayerType === 'individual' && cert.individualInfo) {
certName = cert.individualInfo.name || '';
certId = cert.individualInfo.fin || '';
} else if (cert.legalInfo) {
certName = cert.legalInfo.name || '';
certId = cert.legalInfo.voen || cert.legalInfo.tin || '';
}
const fullName = `${certName} (${certId})`.toLowerCase();
// Проверяем точное совпадение или содержание строки поиска
if (fullName === searchStr || fullName.includes(searchStr)) {
foundCert = cert;
foundIndex = i;
break;
}
}
// Если нашли совпадение, устанавливаем данные сертификата
if (foundCert) {
frm.set_value('selected_certificate_index', foundIndex);
frm.set_value('selected_certificate_json', JSON.stringify(foundCert, null, 2));
frappe.show_alert({
message: __('Certificate found and selected'),
indicator: 'green'
}, 3);
}
} catch (e) {
console.error('Error processing certificate selection:', e);
}
}
}
});
// Функция для запуска опроса через setTimeout вместо setInterval
function startPollingWithTimeout(frm) {
console.log('startPollingWithTimeout function has been called');
// Отображаем индикатор загрузки
frm.page.set_indicator(__('Waiting for confirmation'), 'orange');
// Создаем всплывающее окно с информацией
const status_dialog = new frappe.ui.Dialog({
title: __('Authentication Status'),
fields: [
{
fieldname: 'status_html',
fieldtype: 'HTML',
options: `
<div class="text-center">
<div class="lds-dual-ring" style="display: inline-block; width: 64px; height: 64px;">
<div style="width: 64px; height: 64px; border: 6px solid #4e73df; border-radius: 50%; border-color: #4e73df transparent #4e73df transparent; animation: lds-dual-ring 1.2s linear infinite;"></div>
</div>
<style>
@keyframes lds-dual-ring {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
<div class="mt-3" id="status_message">
<h4>Waiting for confirmation on your phone</h4>
<p>Please check your phone and confirm the authentication request.</p>
</div>
</div>
`
}
],
primary_action_label: __('Cancel'),
primary_action: function() {
// Остановить опрос
window.stopPolling = true;
status_dialog.hide();
frm.page.clear_indicator();
frm.reload_doc();
}
});
status_dialog.show();
// Счетчик попыток и флаг для остановки опроса
let attempts = 0;
const maxAttempts = 20; // 2 минут (20 попыток с интервалом 5 секунд)
window.stopPolling = false;
// Функция опроса
function pollAuthStatus() {
// Если достигнуто максимальное количество попыток или установлен флаг остановки
if (attempts >= maxAttempts || window.stopPolling) {
console.log('Polling stopped: ' + (attempts >= maxAttempts ? 'max attempts reached' : 'manually cancelled'));
if (attempts >= maxAttempts) {
// Обновляем информацию в диалоговом окне по таймауту
$('#status_message').html(`
<h4 style="color: red;">Authentication Timeout</h4>
<p>The authentication request has timed out. Please try again.</p>
`);
$('.lds-dual-ring').css('display', 'none');
// Обновляем поле auth_status
frm.set_value('auth_status', 'Failed');
frm.save();
// Закрываем диалог через 3 секунды
setTimeout(function() {
status_dialog.hide();
frm.reload_doc();
}, 3000);
}
return;
}
attempts++;
console.log(`Polling auth status, attempt ${attempts} of ${maxAttempts}`);
// Проверяем статус авторизации
frappe.call({
method: 'invoice_az.api.poll_auth_status',
args: {
'asan_login_name': frm.doc.name,
'bearer_token': frm.doc.bearer_token
},
callback: function(r) {
if (r.message && r.message.success) {
if (r.message.authenticated) {
// Авторизация успешна
window.stopPolling = true;
// Обновляем информацию в диалоговом окне
$('#status_message').html(`
<h4 style="color: green;">Authentication Successful!</h4>
<p>You are now authenticated. The page will reload shortly.</p>
`);
$('.lds-dual-ring').css('display', 'none');
// Закрываем диалоговое окно через 2 секунды
setTimeout(function() {
status_dialog.hide();
frm.reload_doc();
}, 2000);
} else {
// Продолжаем опрос
setTimeout(pollAuthStatus, 5000);
}
} else {
// Если ошибка, останавливаем опрос и показываем сообщение
window.stopPolling = true;
// Обновляем информацию в диалоговом окне
$('#status_message').html(`
<h4 style="color: red;">Authentication Error</h4>
<p>${r.message ? r.message.message : 'An unknown error occurred.'}</p>
`);
$('.lds-dual-ring').css('display', 'none');
// Обновляем поле auth_status
frm.set_value('auth_status', 'Failed');
frm.save();
// Закрываем диалог через 3 секунды
setTimeout(function() {
status_dialog.hide();
frm.reload_doc();
}, 3000);
}
},
error: function(err) {
// В случае ошибки продолжаем опрос
console.error('Error during status check:', err);
setTimeout(pollAuthStatus, 5000);
}
});
}
// Начинаем опрос
console.log('Starting first polling attempt');
pollAuthStatus();
}
// Функция для отображения сертификатов в диалоговом окне
function showCertificatesDialog(certificates, frm) {
if (!certificates || !certificates.length) {
frappe.msgprint(__('No certificates data to display'));
return;
}
// Создаем таблицу с сертификатами
var cert_table = '<div style="max-height: 500px; overflow-y: auto;"><table class="table table-bordered">';
cert_table += '<thead><tr>' +
'<th>Type</th>' +
'<th>Name</th>' +
'<th>ID</th>' +
'<th>Position</th>' +
'<th>Status</th>' +
'<th>Action</th>' +
'</tr></thead><tbody>';
certificates.forEach(function(cert, index) {
var name = '';
var id = '';
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_table += '<tr>' +
'<td>' + (cert.taxpayerType || '') + '</td>' +
'<td>' + name + '</td>' +
'<td>' + id + '</td>' +
'<td>' + (cert.position || '') + '</td>' +
'<td>' + (cert.hasAccess ? 'Active' : 'Inactive') +
(cert.liquidated ? ' (Liquidated)' : '') + '</td>' +
'<td><button class="btn btn-xs btn-primary select-cert" data-index="' + index + '">Select</button></td>' +
'</tr>';
});
cert_table += '</tbody></table></div>';
// Отображаем диалоговое окно с таблицей
var d = new frappe.ui.Dialog({
title: __('Certificates List'),
fields: [{
fieldtype: 'HTML',
fieldname: 'certificate_list',
options: cert_table
}],
primary_action_label: __('Close'),
primary_action: function() {
d.hide();
}
});
d.show();
// Добавляем обработчик для кнопки выбора сертификата
d.$wrapper.find('.select-cert').on('click', function() {
var index = $(this).data('index');
var cert = certificates[index];
// Получаем имя для отображения
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}`;
}
// Отправляем запрос на выбор сертификата
frappe.call({
method: 'invoice_az.api.select_certificate',
args: {
'asan_login_name': frm.doc.name,
'certificate_data': cert,
'certificate_name': certName
},
callback: function(r) {
if (r.message && r.message.success) {
frappe.show_alert({
message: __('Certificate selected. Choosing taxpayer automatically...'),
indicator: 'blue'
}, 3);
d.hide();
// После успешного выбора сертификата автоматически вызываем выбор налогоплательщика
frappe.call({
method: 'invoice_az.api.select_taxpayer',
args: {
'asan_login_name': frm.doc.name
},
callback: function(r2) {
if (r2.message && r2.message.success) {
frappe.show_alert({
message: __('Taxpayer selected automatically'),
indicator: 'green'
}, 5);
// Перезагружаем страницу для обновления данных
frm.reload_doc();
} else if (r2.message && r2.message.unauthorized) {
// Если ошибка авторизации, предлагаем авторизоваться заново
showAuthDialog(frm);
} else {
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: r2.message ? r2.message.message : __('Failed to select taxpayer automatically')
});
// Все равно перезагружаем страницу, чтобы отобразить выбранный сертификат
frm.reload_doc();
}
}
});
} else if (r.message && r.message.unauthorized) {
d.hide();
// Если ошибка авторизации, предлагаем авторизоваться заново
showAuthDialog(frm);
} else {
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: r.message ? r.message.message : __('Failed to select certificate')
});
}
}
});
});
}
// Функция для отображения диалога повторной аутентификации
function showAuthDialog(frm) {
var d = new frappe.ui.Dialog({
title: __('Authentication Required'),
fields: [
{
fieldname: 'message',
fieldtype: 'HTML',
options: `
<div class="text-center">
<p>Your session has expired or authentication is required.</p>
<p>Would you like to authenticate again?</p>
</div>
`
}
],
primary_action_label: __('Authenticate'),
primary_action: function() {
d.hide();
// Вызываем процесс аутентификации
frappe.call({
method: 'invoice_az.api.handle_authentication',
args: {
'asan_login_name': frm.doc.name
},
callback: function(r) {
if (r.message && r.message.success) {
// Сохраняем токен в документе и запускаем опрос статуса
if (r.message.bearer_token) {
frm.set_value('bearer_token', r.message.bearer_token);
frm.save();
}
// Запускаем опрос статуса
startPollingWithTimeout(frm);
} else {
// Показываем сообщение об ошибке
frappe.msgprint({
title: __('Error'),
indicator: 'red',
message: r.message ? r.message.message : __('Authentication request failed.')
});
}
}
});
},
secondary_action_label: __('Cancel'),
secondary_action: function() {
d.hide();
}
});
d.show();
}

View File

@ -0,0 +1,139 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "hash",
"creation": "2025-04-28 10:00:00.000000",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"authentication_section",
"phone",
"user_id",
"bearer_token",
"auth_status",
"column_break_1",
"is_default",
"certificates_section",
"certificates_json",
"selected_certificate",
"selected_certificate_index",
"selected_certificate_json",
"taxpayer_section",
"main_token",
"choose_taxpayer_response"
],
"fields": [
{
"fieldname": "authentication_section",
"fieldtype": "Section Break",
"label": "Authentication"
},
{
"fieldname": "phone",
"fieldtype": "Data",
"label": "Phone",
"reqd": 1
},
{
"fieldname": "user_id",
"fieldtype": "Data",
"label": "ID",
"reqd": 1
},
{
"fieldname": "bearer_token",
"fieldtype": "Small Text",
"label": "Bearer Token",
"read_only": 1
},
{
"fieldname": "auth_status",
"fieldtype": "Select",
"label": "Auth Status",
"options": "Not Authenticated\nWaiting for confirmation\nAuthenticated\nFully Authenticated\nFailed",
"default": "Not Authenticated",
"read_only": 1
},
{
"fieldname": "column_break_1",
"fieldtype": "Column Break"
},
{
"fieldname": "is_default",
"fieldtype": "Check",
"label": "Is Default"
},
{
"fieldname": "certificates_section",
"fieldtype": "Section Break",
"label": "Certificates"
},
{
"fieldname": "certificates_json",
"fieldtype": "Code",
"label": "Certificates JSON",
"options": "JSON",
"read_only": 1
},
{
"fieldname": "selected_certificate",
"fieldtype": "Data",
"label": "Selected Certificate"
},
{
"fieldname": "selected_certificate_index",
"fieldtype": "Data",
"hidden": 1,
"label": "Selected Certificate Index"
},
{
"fieldname": "selected_certificate_json",
"fieldtype": "Code",
"label": "Selected Certificate Details",
"options": "JSON",
"read_only": 1
},
{
"fieldname": "taxpayer_section",
"fieldtype": "Section Break",
"label": "Taxpayer Information"
},
{
"fieldname": "main_token",
"fieldtype": "Small Text",
"label": "Main Token",
"description": "Primary token for all API operations",
"read_only": 1
},
{
"fieldname": "choose_taxpayer_response",
"fieldtype": "Code",
"label": "Taxpayer Response",
"options": "JSON",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-04-28 10:00:00.000000",
"modified_by": "Administrator",
"module": "Invoice Az",
"name": "Asan Login",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC"
}

View File

@ -0,0 +1,29 @@
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
class AsanLogin(Document):
def before_save(self):
# Если установлен флаг is_default, сбрасываем его у других документов
if self.is_default:
existing_defaults = frappe.get_all(
"Asan Login",
filters={
"is_default": 1,
"name": ["!=", self.name]
}
)
for default in existing_defaults:
doc = frappe.get_doc("Asan Login", default.name)
doc.is_default = 0
doc.flags.ignore_permissions = True
doc.save()
def validate(self):
# Проверяем обязательные поля
if not self.phone:
frappe.throw("Phone is required")
if not self.user_id:
frappe.throw("User ID is required")

View File

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

View File

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

View File

@ -0,0 +1,123 @@
{
"actions": [],
"allow_rename": 0,
"autoname": "autoincrement",
"creation": "2025-05-01 12:00:00.000000",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"etaxes_item_name",
"etaxes_item_code",
"etaxes_unit",
"etaxes_price",
"source_invoice",
"status",
"mapped_item",
"item_creation_section",
"item_group",
"uom",
"item_tax_template"
],
"fields": [
{
"fieldname": "etaxes_item_name",
"fieldtype": "Data",
"label": "Item Name",
"reqd": 1
},
{
"fieldname": "etaxes_item_code",
"fieldtype": "Data",
"label": "Item Code"
},
{
"fieldname": "etaxes_unit",
"fieldtype": "Data",
"label": "Unit of Measure"
},
{
"fieldname": "etaxes_price",
"fieldtype": "Currency",
"label": "Price",
"options": "AZN"
},
{
"fieldname": "source_invoice",
"fieldtype": "Data",
"label": "Source",
"description": "Source invoice number"
},
{
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"options": "New\nMapped\nProcessing",
"default": "New"
},
{
"fieldname": "mapped_item",
"fieldtype": "Link",
"label": "Mapped Item",
"options": "Item"
},
{
"fieldname": "item_creation_section",
"fieldtype": "Section Break",
"label": "Item Creation Parameters"
},
{
"fieldname": "item_group",
"fieldtype": "Link",
"label": "Item Group",
"options": "Item Group"
},
{
"fieldname": "uom",
"fieldtype": "Link",
"label": "Unit of Measure",
"options": "UOM"
},
{
"fieldname": "item_tax_template",
"fieldtype": "Link",
"label": "Tax Template",
"options": "Item Tax Template"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-05-01 12:00:00.000000",
"modified_by": "Administrator",
"module": "Invoice Az",
"name": "E-Taxes Item",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 0,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,52 @@
{
"actions": [],
"allow_rename": 0,
"creation": "2025-05-01 12:00:00.000000",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"etaxes_item_name",
"etaxes_item_code",
"erp_item",
"mapping_type"
],
"fields": [
{
"fieldname": "etaxes_item_name",
"fieldtype": "Data",
"label": "E-Taxes Item Name",
"reqd": 1
},
{
"fieldname": "etaxes_item_code",
"fieldtype": "Data",
"label": "E-Taxes Item Code"
},
{
"fieldname": "erp_item",
"fieldtype": "Link",
"label": "System Item",
"options": "Item",
"reqd": 1
},
{
"fieldname": "mapping_type",
"fieldtype": "Select",
"label": "Mapping Type",
"options": "Manual\nAutomatic",
"default": "Manual"
}
],
"index_web_pages_for_search": 0,
"istable": 1,
"links": [],
"modified": "2025-05-01 12:00:00.000000",
"modified_by": "Administrator",
"module": "Invoice Az",
"name": "E-Taxes Item Mapping",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,134 @@
{
"actions": [],
"allow_rename": 0,
"autoname": "autoincrement",
"creation": "2025-05-01 12:00:00.000000",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"etaxes_party_name",
"etaxes_tax_id",
"etaxes_party_type",
"etaxes_address",
"source_invoice",
"status",
"erp_party_type",
"mapped_party",
"party_creation_section",
"party_group",
"party_group_type",
"payment_terms"
],
"fields": [
{
"fieldname": "etaxes_party_name",
"fieldtype": "Data",
"label": "Party Name",
"reqd": 1
},
{
"fieldname": "etaxes_tax_id",
"fieldtype": "Data",
"label": "Tax ID"
},
{
"fieldname": "etaxes_party_type",
"fieldtype": "Select",
"label": "E-Taxes Type",
"options": "Sender\nReceiver",
"default": "Sender"
},
{
"fieldname": "etaxes_address",
"fieldtype": "Data",
"label": "Address"
},
{
"fieldname": "source_invoice",
"fieldtype": "Data",
"label": "Source",
"description": "Source invoice number"
},
{
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"options": "New\nMapped\nProcessing",
"default": "New"
},
{
"fieldname": "erp_party_type",
"fieldtype": "Select",
"label": "System Party Type",
"options": "Supplier\nCustomer",
"default": "Supplier"
},
{
"fieldname": "mapped_party",
"fieldtype": "Dynamic Link",
"label": "Mapped Party",
"options": "erp_party_type"
},
{
"fieldname": "party_creation_section",
"fieldtype": "Section Break",
"label": "Party Creation Parameters"
},
{
"fieldname": "party_group",
"fieldtype": "Dynamic Link",
"label": "Party Group",
"options": "party_group_type"
},
{
"fieldname": "party_group_type",
"fieldtype": "Select",
"label": "Party Group Type",
"options": "Supplier Group\nCustomer Group",
"default": "Supplier Group",
"hidden": 1
},
{
"fieldname": "payment_terms",
"fieldtype": "Link",
"label": "Payment Terms",
"options": "Payment Terms Template"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-05-01 12:00:00.000000",
"modified_by": "Administrator",
"module": "Invoice Az",
"name": "E-Taxes Parties",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 0,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,60 @@
{
"actions": [],
"allow_rename": 0,
"creation": "2025-05-01 12:00:00.000000",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"etaxes_party_name",
"etaxes_tax_id",
"party_type",
"erp_party",
"mapping_type"
],
"fields": [
{
"fieldname": "etaxes_party_name",
"fieldtype": "Data",
"label": "E-Taxes Party Name",
"reqd": 1
},
{
"fieldname": "etaxes_tax_id",
"fieldtype": "Data",
"label": "Tax ID"
},
{
"fieldname": "party_type",
"fieldtype": "Select",
"label": "Party Type",
"options": "Supplier\nCustomer",
"reqd": 1
},
{
"fieldname": "erp_party",
"fieldtype": "Dynamic Link",
"label": "System Party",
"options": "party_type",
"reqd": 1
},
{
"fieldname": "mapping_type",
"fieldtype": "Select",
"label": "Mapping Type",
"options": "Manual\nAutomatic",
"default": "Manual"
}
],
"index_web_pages_for_search": 0,
"istable": 1,
"links": [],
"modified": "2025-05-01 12:00:00.000000",
"modified_by": "Administrator",
"module": "Invoice Az",
"name": "E-Taxes Party Mapping",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

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

View File

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

View File

@ -0,0 +1,99 @@
frappe.ui.form.on('E-Taxes Settings', {
refresh: function(frm) {
// Добавляем кнопку для автоматического сопоставления
frm.add_custom_button(__('Сопоставить по похожему названию'), function() {
frappe.confirm(
__('Это действие попытается автоматически сопоставить все несопоставленные товары и контрагенты на основе схожести названий. Продолжить?'),
function() {
// Запускаем процесс автоматического сопоставления
frappe.call({
method: 'invoice_az.invoice_az.doctype.e_taxes_settings.e_taxes_settings.auto_match_by_name',
args: {
'settings_name': frm.doc.name
},
callback: function(r) {
if (r.message && r.message.success) {
frappe.msgprint({
title: __('Сопоставление выполнено'),
indicator: 'green',
message: __('Автоматически сопоставлено {0} товаров и {1} контрагентов.', [r.message.items_matched, r.message.parties_matched])
});
frm.reload_doc();
} else {
frappe.msgprint({
title: __('Ошибка'),
indicator: 'red',
message: r.message ? r.message.message : __('Произошла ошибка при автоматическом сопоставлении.')
});
}
}
});
}
);
}, __('Действия'));
// Добавляем кнопку для импорта товаров из CSV
frm.add_custom_button(__('Импорт сопоставлений из CSV'), function() {
// Логика импорта будет добавлена позже
frappe.msgprint(__('Функция в разработке'));
}, __('Действия'));
// Кнопка для установки настроек по умолчанию
if (!frm.doc.is_default) {
frm.add_custom_button(__('Установить по умолчанию'), function() {
frappe.call({
method: 'frappe.client.set_value',
args: {
doctype: 'E-Taxes Settings',
name: frm.doc.name,
fieldname: 'is_default',
value: 1
},
callback: function(r) {
if (r.message) {
frappe.msgprint({
title: __('Успешно'),
indicator: 'green',
message: __('Настройки установлены по умолчанию.')
});
frm.reload_doc();
}
}
});
}, __('Действия'));
}
},
// При изменении флага is_default, сбрасываем этот флаг у других настроек
is_default: function(frm) {
if (frm.doc.is_default) {
frappe.call({
method: 'frappe.client.get_list',
args: {
doctype: 'E-Taxes Settings',
filters: {
'is_default': 1,
'name': ['!=', frm.doc.name]
},
fields: ['name']
},
callback: function(r) {
if (r.message && r.message.length) {
// Сбрасываем флаг у других настроек
r.message.forEach(function(settings) {
frappe.call({
method: 'frappe.client.set_value',
args: {
doctype: 'E-Taxes Settings',
name: settings.name,
fieldname: 'is_default',
value: 0
}
});
});
}
}
});
}
}
});

View File

@ -0,0 +1,162 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:settings_name",
"creation": "2025-05-01 12:00:00.000000",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"settings_name",
"is_default",
"is_active",
"general_section",
"similarity_threshold",
"consider_azeri_chars",
"default_item_settings_section",
"default_item_group",
"default_uom",
"default_item_tax_template",
"default_party_settings_section",
"default_supplier_group",
"default_customer_group",
"default_payment_terms",
"item_mappings_section",
"item_mappings",
"party_mappings_section",
"party_mappings"
],
"fields": [
{
"fieldname": "settings_name",
"fieldtype": "Data",
"label": "Settings Name",
"reqd": 1,
"unique": 1
},
{
"fieldname": "is_default",
"fieldtype": "Check",
"label": "Is Default",
"default": 0
},
{
"fieldname": "is_active",
"fieldtype": "Check",
"label": "Is Active",
"default": 1
},
{
"fieldname": "general_section",
"fieldtype": "Section Break",
"label": "General Settings"
},
{
"fieldname": "similarity_threshold",
"fieldtype": "Percent",
"label": "Similarity Threshold (%)",
"default": 80,
"description": "Minimum similarity percentage for automatic mapping"
},
{
"fieldname": "consider_azeri_chars",
"fieldtype": "Check",
"label": "Consider Azerbaijani Characters",
"default": 1,
"description": "Consider replacement of Azerbaijani letters with Latin equivalents when matching"
},
{
"fieldname": "default_item_settings_section",
"fieldtype": "Section Break",
"label": "Default Item Settings"
},
{
"fieldname": "default_item_group",
"fieldtype": "Link",
"label": "Default Item Group",
"options": "Item Group",
"reqd": 1
},
{
"fieldname": "default_uom",
"fieldtype": "Link",
"label": "Default Unit of Measure",
"options": "UOM",
"reqd": 1
},
{
"fieldname": "default_item_tax_template",
"fieldtype": "Link",
"label": "Default Item Tax Template",
"options": "Item Tax Template"
},
{
"fieldname": "default_party_settings_section",
"fieldtype": "Section Break",
"label": "Default Business Partner Settings"
},
{
"fieldname": "default_supplier_group",
"fieldtype": "Link",
"label": "Default Supplier Group",
"options": "Supplier Group"
},
{
"fieldname": "default_customer_group",
"fieldtype": "Link",
"label": "Default Customer Group",
"options": "Customer Group"
},
{
"fieldname": "default_payment_terms",
"fieldtype": "Link",
"label": "Default Payment Terms",
"options": "Payment Terms Template"
},
{
"fieldname": "item_mappings_section",
"fieldtype": "Section Break",
"label": "Item Mappings"
},
{
"fieldname": "item_mappings",
"fieldtype": "Table",
"label": "Item Mappings",
"options": "E-Taxes Item Mapping"
},
{
"fieldname": "party_mappings_section",
"fieldtype": "Section Break",
"label": "Business Partner Mappings"
},
{
"fieldname": "party_mappings",
"fieldtype": "Table",
"label": "Business Partner Mappings",
"options": "E-Taxes Party Mapping"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-05-01 12:00:00.000000",
"modified_by": "Administrator",
"module": "Invoice Az",
"name": "E-Taxes Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,805 @@
import frappe
import json
import re
import difflib
from frappe.utils import now, cstr, cint, flt, get_datetime
from frappe.model.document import Document
class ETaxesSettings(Document):
pass
# Функция для получения настроек E-Taxes по умолчанию
@frappe.whitelist()
def get_default_settings():
"""Получение настроек E-Taxes по умолчанию"""
try:
# Получаем настройки E-Taxes
settings_list = frappe.get_all('E-Taxes Settings',
filters={'is_default': 1, 'is_active': 1},
limit=1)
if settings_list:
settings = frappe.get_doc('E-Taxes Settings', settings_list[0].name)
return {
'found': True,
'name': settings.name,
'similarity_threshold': settings.similarity_threshold,
'consider_azeri_chars': settings.consider_azeri_chars
}
else:
# Если настройки не найдены, пробуем взять любые доступные
all_settings = frappe.get_all('E-Taxes Settings',
filters={'is_active': 1},
limit=1)
if all_settings:
settings = frappe.get_doc('E-Taxes Settings', all_settings[0].name)
return {
'found': True,
'name': settings.name,
'similarity_threshold': settings.similarity_threshold,
'consider_azeri_chars': settings.consider_azeri_chars
}
else:
return {
'found': False,
'message': 'No E-Taxes Settings found'
}
except Exception as e:
frappe.log_error(frappe.get_traceback(), 'Error getting E-Taxes Settings')
return {
'found': False,
'message': 'Error: {0}'.format(str(e))
}
# Функция нормализации азербайджанских символов
def normalize_azeri_chars(text):
if not text:
return text
# Азербайджанские буквы и их аналоги
azeri_chars = {
'ə': 'e', # также может быть 'a'
'ü': 'u',
'ö': 'o',
'ğ': 'g',
'ı': 'i',
'ç': 'c',
'ş': 's' # также может быть 'sh'
}
# Нормализуем текст
normalized = ''
for char in text:
if char.lower() in azeri_chars:
normalized += azeri_chars[char.lower()] if char.islower() else azeri_chars[char.lower()].upper()
else:
normalized += char
return normalized
# Функция для расчета схожести строк с учетом азербайджанских символов
def calculate_similarity(str1, str2, consider_azeri=True):
"""Расчет процента схожести двух строк"""
if not str1 or not str2:
return 0
# Нормализуем строки (приводим к нижнему регистру)
str1 = str1.lower()
str2 = str2.lower()
# Если нужно учитывать азербайджанские символы
if consider_azeri:
str1_normalized = normalize_azeri_chars(str1)
str2_normalized = normalize_azeri_chars(str2)
# Рассчитываем схожесть между оригинальными и нормализованными строками
sim1 = difflib.SequenceMatcher(None, str1, str2).ratio()
sim2 = difflib.SequenceMatcher(None, str1_normalized, str2).ratio()
sim3 = difflib.SequenceMatcher(None, str1, str2_normalized).ratio()
sim4 = difflib.SequenceMatcher(None, str1_normalized, str2_normalized).ratio()
# Берем максимальную схожесть
similarity = max(sim1, sim2, sim3, sim4)
else:
# Рассчитываем схожесть только между оригинальными строками
similarity = difflib.SequenceMatcher(None, str1, str2).ratio()
return similarity * 100 # Возвращаем в процентах
# Функция для поиска соответствий товаров
@frappe.whitelist()
def find_matching_items(invoice_items, settings_name):
"""Поиск соответствий для товаров из инвойса"""
try:
if not invoice_items or not settings_name:
return {
'success': False,
'message': 'Missing required parameters'
}
# Получаем настройки
settings = frappe.get_doc('E-Taxes Settings', settings_name)
# Если параметр invoice_items передан как строка, преобразуем в объект
if isinstance(invoice_items, str):
invoice_items = json.loads(invoice_items)
# Получаем список всех товаров в системе
all_items = frappe.get_all('Item', fields=['name', 'item_name', 'item_code'])
# Получаем существующие сопоставления из настроек
item_mappings = {(m.etaxes_item_name, m.etaxes_item_code): m.erp_item for m in settings.item_mappings}
# Результаты сопоставления
matched_items = []
unmatched_items = []
suggested_matches = []
# Проходим по каждому товару из инвойса
for item in invoice_items:
item_name = item.get('productName') or item.get('etaxes_item_name')
item_code = item.get('itemId') or item.get('etaxes_item_code')
if not item_name:
continue
# Проверяем, есть ли прямое соответствие в сопоставлениях
if (item_name, item_code) in item_mappings:
matched_items.append({
'etaxes_item_name': item_name,
'etaxes_item_code': item_code,
'erp_item': item_mappings[(item_name, item_code)],
'mapping_type': 'manual'
})
continue
# Проверяем, есть ли товар с таким же именем или кодом в системе
direct_match = False
for erp_item in all_items:
if (erp_item.item_name == item_name or
(item_code and erp_item.item_code == item_code)):
matched_items.append({
'etaxes_item_name': item_name,
'etaxes_item_code': item_code,
'erp_item': erp_item.name,
'mapping_type': 'auto'
})
direct_match = True
break
if direct_match:
continue
# Ищем похожие товары
similar_items = []
for erp_item in all_items:
similarity = calculate_similarity(
item_name,
erp_item.item_name,
settings.consider_azeri_chars
)
if similarity >= settings.similarity_threshold:
similar_items.append({
'item': erp_item.name,
'similarity': similarity / 100 # Переводим обратно в дробь
})
# Сортируем похожие товары по схожести
similar_items.sort(key=lambda x: x['similarity'], reverse=True)
# Если найдены похожие товары, добавляем их в предложения
if similar_items:
suggested_matches.append({
'etaxes_item_name': item_name,
'etaxes_item_code': item_code,
'similar_items': similar_items[:5] # Берем топ-5 похожих товаров
})
else:
# Если не найдены похожие товары, добавляем в несопоставленные
unmatched_items.append({
'etaxes_item_name': item_name,
'etaxes_item_code': item_code
})
return {
'success': True,
'result': {
'matched_items': matched_items,
'unmatched_items': unmatched_items,
'suggested_matches': suggested_matches
}
}
except Exception as e:
frappe.log_error(frappe.get_traceback(), 'Error finding matching items')
return {
'success': False,
'message': 'Error: {0}'.format(str(e))
}
# Функция для поиска соответствий контрагентов
@frappe.whitelist()
def find_matching_parties(invoice_data, settings_name):
"""Поиск соответствий для контрагентов из инвойса"""
try:
if not invoice_data or not settings_name:
return {
'success': False,
'message': 'Missing required parameters'
}
# Получаем настройки
settings = frappe.get_doc('E-Taxes Settings', settings_name)
# Если параметр invoice_data передан как строка, преобразуем в объект
if isinstance(invoice_data, str):
invoice_data = json.loads(invoice_data)
# Получаем данные отправителя и получателя из инвойса
sender_name = invoice_data.get('senderName') or (invoice_data.get('sender', {}) or {}).get('name')
sender_tin = invoice_data.get('senderTin') or (invoice_data.get('sender', {}) or {}).get('tin')
receiver_name = invoice_data.get('receiverName') or (invoice_data.get('receiver', {}) or {}).get('name')
receiver_tin = invoice_data.get('receiverTin') or (invoice_data.get('receiver', {}) or {}).get('tin')
# Результаты сопоставления
result = {
'sender': {
'etaxes_party_name': sender_name,
'etaxes_tax_id': sender_tin,
'matched': False
},
'receiver': {
'etaxes_party_name': receiver_name,
'etaxes_tax_id': receiver_tin,
'matched': False
}
}
# Проверяем соответствие отправителя
if sender_name and sender_tin:
# Проверяем существующие сопоставления
for mapping in settings.party_mappings:
if (mapping.etaxes_party_name == sender_name or
(sender_tin and mapping.etaxes_tax_id == sender_tin)):
result['sender'].update({
'matched': True,
'party_type': mapping.party_type,
'party': mapping.erp_party
})
break
# Если не найдено в сопоставлениях, ищем напрямую
if not result['sender']['matched']:
# Поиск среди поставщиков
suppliers = frappe.get_all('Supplier',
fields=['name', 'tax_id'],
filters=[
['name', '=', sender_name],
['tax_id', '=', sender_tin]
])
if suppliers:
result['sender'].update({
'matched': True,
'party_type': 'Supplier',
'party': suppliers[0].name
})
else:
# Поиск среди клиентов
customers = frappe.get_all('Customer',
fields=['name', 'tax_id'],
filters=[
['name', '=', sender_name],
['tax_id', '=', sender_tin]
])
if customers:
result['sender'].update({
'matched': True,
'party_type': 'Customer',
'party': customers[0].name
})
# Проверяем соответствие получателя
if receiver_name and receiver_tin:
# Проверяем существующие сопоставления
for mapping in settings.party_mappings:
if (mapping.etaxes_party_name == receiver_name or
(receiver_tin and mapping.etaxes_tax_id == receiver_tin)):
result['receiver'].update({
'matched': True,
'party_type': mapping.party_type,
'party': mapping.erp_party
})
break
# Если не найдено в сопоставлениях, ищем напрямую
if not result['receiver']['matched']:
# Поиск среди поставщиков
suppliers = frappe.get_all('Supplier',
fields=['name', 'tax_id'],
filters=[
['name', '=', receiver_name],
['tax_id', '=', receiver_tin]
])
if suppliers:
result['receiver'].update({
'matched': True,
'party_type': 'Supplier',
'party': suppliers[0].name
})
else:
# Поиск среди клиентов
customers = frappe.get_all('Customer',
fields=['name', 'tax_id'],
filters=[
['name', '=', receiver_name],
['tax_id', '=', receiver_tin]
])
if customers:
result['receiver'].update({
'matched': True,
'party_type': 'Customer',
'party': customers[0].name
})
return {
'success': True,
'result': result
}
except Exception as e:
frappe.log_error(frappe.get_traceback(), 'Error finding matching parties')
return {
'success': False,
'message': 'Error: {0}'.format(str(e))
}
# Функция для добавления сопоставления товара
@frappe.whitelist()
def add_item_mapping(settings_name, etaxes_item_name, etaxes_item_code, erp_item):
"""Добавление сопоставления товара в настройки"""
try:
if not settings_name or not etaxes_item_name or not erp_item:
return {
'success': False,
'message': 'Missing required parameters'
}
# Получаем документ настроек
settings = frappe.get_doc('E-Taxes Settings', settings_name)
# Проверяем, существует ли уже такое сопоставление
for mapping in settings.item_mappings:
if mapping.etaxes_item_name == etaxes_item_name and mapping.etaxes_item_code == etaxes_item_code:
# Обновляем существующее сопоставление
mapping.erp_item = erp_item
settings.save()
return {
'success': True,
'message': 'Item mapping updated successfully'
}
# Если сопоставление не найдено, создаем новое
mapping = settings.append('item_mappings', {
'etaxes_item_name': etaxes_item_name,
'etaxes_item_code': etaxes_item_code,
'erp_item': erp_item,
'mapping_type': 'Ручное'
})
settings.save()
# Обновляем статус в E-Taxes Item, если такие записи существуют
items = frappe.get_all('E-Taxes Item',
filters={
'etaxes_item_name': etaxes_item_name,
'etaxes_item_code': etaxes_item_code
})
for item in items:
frappe.db.set_value('E-Taxes Item', item.name, {
'status': 'Сопоставлен',
'mapped_item': erp_item
})
return {
'success': True,
'message': 'Item mapping added successfully'
}
except Exception as e:
frappe.log_error(frappe.get_traceback(), 'Error adding item mapping')
return {
'success': False,
'message': 'Error: {0}'.format(str(e))
}
# Функция для добавления сопоставления контрагента
@frappe.whitelist()
def add_party_mapping(settings_name, etaxes_party_name, etaxes_tax_id, party_type, erp_party):
"""Добавление сопоставления контрагента в настройки"""
try:
if not settings_name or not etaxes_party_name or not party_type or not erp_party:
return {
'success': False,
'message': 'Missing required parameters'
}
# Получаем документ настроек
settings = frappe.get_doc('E-Taxes Settings', settings_name)
# Проверяем, существует ли уже такое сопоставление
for mapping in settings.party_mappings:
if mapping.etaxes_party_name == etaxes_party_name and mapping.etaxes_tax_id == etaxes_tax_id:
# Обновляем существующее сопоставление
mapping.party_type = party_type
mapping.erp_party = erp_party
settings.save()
return {
'success': True,
'message': 'Party mapping updated successfully'
}
# Если сопоставление не найдено, создаем новое
mapping = settings.append('party_mappings', {
'etaxes_party_name': etaxes_party_name,
'etaxes_tax_id': etaxes_tax_id,
'party_type': party_type,
'erp_party': erp_party,
'mapping_type': 'Ручное'
})
settings.save()
# Обновляем статус в E-Taxes Parties, если такие записи существуют
parties = frappe.get_all('E-Taxes Parties',
filters={
'etaxes_party_name': etaxes_party_name,
'etaxes_tax_id': etaxes_tax_id
})
for party in parties:
frappe.db.set_value('E-Taxes Parties', party.name, {
'status': 'Сопоставлен',
'erp_party_type': party_type,
'mapped_party': erp_party
})
return {
'success': True,
'message': 'Party mapping added successfully'
}
except Exception as e:
frappe.log_error(frappe.get_traceback(), 'Error adding party mapping')
return {
'success': False,
'message': 'Error: {0}'.format(str(e))
}
# Функция для автоматического сопоставления по похожим названиям
@frappe.whitelist()
def auto_match_by_name(settings_name):
"""Автоматическое сопоставление товаров и контрагентов по похожим названиям"""
try:
if not settings_name:
return {
'success': False,
'message': 'Missing required parameter'
}
# Получаем настройки
settings = frappe.get_doc('E-Taxes Settings', settings_name)
# Получаем все несопоставленные товары
unmatched_items = frappe.get_all('E-Taxes Item',
filters={'status': 'Новый'},
fields=['name', 'etaxes_item_name', 'etaxes_item_code'])
# Получаем все товары в системе
all_items = frappe.get_all('Item', fields=['name', 'item_name', 'item_code'])
# Счетчики для результата
items_matched = 0
# Проходим по каждому несопоставленному товару
for item in unmatched_items:
best_match = None
best_similarity = 0
# Ищем наиболее похожий товар
for erp_item in all_items:
similarity = calculate_similarity(
item.etaxes_item_name,
erp_item.item_name,
settings.consider_azeri_chars
)
if similarity >= settings.similarity_threshold and similarity > best_similarity:
best_match = erp_item.name
best_similarity = similarity
# Если найден подходящий товар, создаем сопоставление
if best_match:
# Добавляем сопоставление в настройки
mapping = settings.append('item_mappings', {
'etaxes_item_name': item.etaxes_item_name,
'etaxes_item_code': item.etaxes_item_code,
'erp_item': best_match,
'mapping_type': 'Автоматическое'
})
# Обновляем статус товара
frappe.db.set_value('E-Taxes Item', item.name, {
'status': 'Сопоставлен',
'mapped_item': best_match
})
items_matched += 1
# Получаем все несопоставленные контрагенты
unmatched_parties = frappe.get_all('E-Taxes Parties',
filters={'status': 'Новый'},
fields=['name', 'etaxes_party_name', 'etaxes_tax_id', 'etaxes_party_type'])
# Получаем всех поставщиков и клиентов в системе
all_suppliers = frappe.get_all('Supplier', fields=['name', 'tax_id'])
all_customers = frappe.get_all('Customer', fields=['name', 'tax_id'])
# Счетчик для результата
parties_matched = 0
# Проходим по каждому несопоставленному контрагенту
for party in unmatched_parties:
best_match = None
best_type = None
best_similarity = 0
# Пытаемся найти по налоговому номеру
if party.etaxes_tax_id:
# Ищем среди поставщиков
for supplier in all_suppliers:
if supplier.tax_id and supplier.tax_id == party.etaxes_tax_id:
best_match = supplier.name
best_type = 'Supplier'
break
# Если не нашли среди поставщиков, ищем среди клиентов
if not best_match:
for customer in all_customers:
if customer.tax_id and customer.tax_id == party.etaxes_tax_id:
best_match = customer.name
best_type = 'Customer'
break
# Если не нашли по налоговому номеру, ищем по названию
if not best_match:
# Ищем среди поставщиков
for supplier in all_suppliers:
similarity = calculate_similarity(
party.etaxes_party_name,
supplier.name,
settings.consider_azeri_chars
)
if similarity >= settings.similarity_threshold and similarity > best_similarity:
best_match = supplier.name
best_type = 'Supplier'
best_similarity = similarity
# Ищем среди клиентов
for customer in all_customers:
similarity = calculate_similarity(
party.etaxes_party_name,
customer.name,
settings.consider_azeri_chars
)
if similarity >= settings.similarity_threshold and similarity > best_similarity:
best_match = customer.name
best_type = 'Customer'
best_similarity = similarity
# Если найден подходящий контрагент, создаем сопоставление
if best_match and best_type:
# Добавляем сопоставление в настройки
mapping = settings.append('party_mappings', {
'etaxes_party_name': party.etaxes_party_name,
'etaxes_tax_id': party.etaxes_tax_id,
'party_type': best_type,
'erp_party': best_match,
'mapping_type': 'Автоматическое'
})
# Обновляем статус контрагента
frappe.db.set_value('E-Taxes Parties', party.name, {
'status': 'Сопоставлен',
'erp_party_type': best_type,
'mapped_party': best_match
})
parties_matched += 1
# Сохраняем настройки
settings.save()
return {
'success': True,
'items_matched': items_matched,
'parties_matched': parties_matched,
'message': 'Auto-matched {0} items and {1} parties'.format(items_matched, parties_matched)
}
except Exception as e:
frappe.log_error(frappe.get_traceback(), 'Error in auto-matching by name')
return {
'success': False,
'message': 'Error: {0}'.format(str(e))
}
# Функция для извлечения товаров из инвойсов
@frappe.whitelist()
def extract_items_from_invoices(invoices, source_doc=None):
"""Извлечение уникальных товаров из списка инвойсов"""
try:
if not invoices:
return {
'success': False,
'message': 'No invoices provided'
}
# Если передано как строка, преобразуем в объект
if isinstance(invoices, str):
invoices = json.loads(invoices)
# Хеш-таблица для хранения уникальных товаров
unique_items = {}
# Проходим по каждому инвойсу
for invoice in invoices:
# Получаем список товаров из инвойса
items = invoice.get('items', [])
# Проходим по каждому товару
for item in items:
# Создаем ключ для уникальности
item_name = item.get('productName', '')
item_code = item.get('itemId', '')
key = f"{item_name}|{item_code}"
# Если товар еще не добавлен, добавляем его
if key not in unique_items:
unique_items[key] = {
'etaxes_item_name': item_name,
'etaxes_item_code': item_code,
'etaxes_unit': item.get('unit', ''),
'etaxes_price': item.get('pricePerUnit', 0),
'source_invoice': invoice.get('serialNumber', '')
}
# Создаем записи в E-Taxes Item
items_created = 0
for key, item_data in unique_items.items():
# Проверяем, существует ли уже такой товар
existing_items = frappe.get_all('E-Taxes Item',
filters={
'etaxes_item_name': item_data['etaxes_item_name'],
'etaxes_item_code': item_data['etaxes_item_code']
})
if not existing_items:
# Создаем новый товар
item = frappe.new_doc('E-Taxes Item')
item.update(item_data)
item.insert()
items_created += 1
return {
'success': True,
'items_created': items_created,
'message': 'Extracted and created {0} unique items'.format(items_created)
}
except Exception as e:
frappe.log_error(frappe.get_traceback(), 'Error extracting items from invoices')
return {
'success': False,
'message': 'Error: {0}'.format(str(e))
}
# Функция для извлечения контрагентов из инвойсов
@frappe.whitelist()
def extract_parties_from_invoices(invoices, source_doc=None):
"""Извлечение уникальных контрагентов из списка инвойсов"""
try:
if not invoices:
return {
'success': False,
'message': 'No invoices provided'
}
# Если передано как строка, преобразуем в объект
if isinstance(invoices, str):
invoices = json.loads(invoices)
# Хеш-таблицы для хранения уникальных контрагентов
unique_senders = {}
unique_receivers = {}
# Проходим по каждому инвойсу
for invoice in invoices:
# Получаем данные отправителя
sender_name = invoice.get('senderName') or (invoice.get('sender', {}) or {}).get('name')
sender_tin = invoice.get('senderTin') or (invoice.get('sender', {}) or {}).get('tin')
if sender_name and sender_tin:
key = f"{sender_name}|{sender_tin}"
# Если отправитель еще не добавлен, добавляем его
if key not in unique_senders:
unique_senders[key] = {
'etaxes_party_name': sender_name,
'etaxes_tax_id': sender_tin,
'etaxes_party_type': 'Отправитель',
'etaxes_address': (invoice.get('sender', {}) or {}).get('address', ''),
'source_invoice': invoice.get('serialNumber', '')
}
# Получаем данные получателя
receiver_name = invoice.get('receiverName') or (invoice.get('receiver', {}) or {}).get('name')
receiver_tin = invoice.get('receiverTin') or (invoice.get('receiver', {}) or {}).get('tin')
if receiver_name and receiver_tin:
key = f"{receiver_name}|{receiver_tin}"
# Если получатель еще не добавлен, добавляем его
if key not in unique_receivers:
unique_receivers[key] = {
'etaxes_party_name': receiver_name,
'etaxes_tax_id': receiver_tin,
'etaxes_party_type': 'Получатель',
'etaxes_address': (invoice.get('receiver', {}) or {}).get('address', ''),
'source_invoice': invoice.get('serialNumber', '')
}
# Создаем записи в E-Taxes Parties
parties_created = 0
# Создаем отправителей
for key, party_data in unique_senders.items():
# Проверяем, существует ли уже такой контрагент
existing_parties = frappe.get_all('E-Taxes Parties',
filters={
'etaxes_party_name': party_data['etaxes_party_name'],
'etaxes_tax_id': party_data['etaxes_tax_id']
})
if not existing_parties:
# Создаем нового контрагента
party = frappe.new_doc('E-Taxes Parties')
party.update(party_data)
party.insert()
parties_created += 1
# Создаем получателей
for key, party_data in unique_receivers.items():
# Проверяем, существует ли уже такой контрагент
existing_parties = frappe.get_all('E-Taxes Parties',
filters={
'etaxes_party_name': party_data['etaxes_party_name'],
'etaxes_tax_id': party_data['etaxes_tax_id']
})
if not existing_parties:
# Создаем нового контрагента
party = frappe.new_doc('E-Taxes Parties')
party.update(party_data)
party.insert()
parties_created += 1
return {
'success': True,
'parties_created': parties_created,
'message': 'Extracted and created {0} unique parties'.format(parties_created)
}
except Exception as e:
frappe.log_error(frappe.get_traceback(), 'Error extracting parties from invoices')
return {
'success': False,
'message': 'Error: {0}'.format(str(e))
}

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff