first commit
This commit is contained in:
parent
3461bd7527
commit
07450e3ad4
File diff suppressed because it is too large
Load Diff
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,23 @@ app_description = "This application uploads and downloads invoices from e taxes
|
||||||
app_email = "info@jeyerp.az"
|
app_email = "info@jeyerp.az"
|
||||||
app_license = "unlicense"
|
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
|
# Apps
|
||||||
# ------------------
|
# ------------------
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
|
@ -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")
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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) {
|
||||||
|
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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) {
|
||||||
|
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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) {
|
||||||
|
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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) {
|
||||||
|
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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))
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
@ -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": []
|
||||||
|
}
|
||||||
|
|
@ -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
Loading…
Reference in New Issue