Added purchase invoice sending (act)
This commit is contained in:
parent
f1ae840d88
commit
4879dcc94c
326
CLAUDE.md
326
CLAUDE.md
|
|
@ -11,6 +11,332 @@ Invoice Az is a Frappe application that integrates with Azerbaijan's e-taxes.gov
|
|||
- Importing VAT Account operations and creating Journal Entries
|
||||
- Managing authentication via ASAN Login with automatic token renewal
|
||||
|
||||
## ⚠️ CRITICAL RULES FOR E-TAXES INTEGRATION ⚠️
|
||||
|
||||
**READ THIS BEFORE IMPLEMENTING ANY E-TAXES API CALLS!**
|
||||
|
||||
### 1. Authentication Pattern (MANDATORY)
|
||||
|
||||
**❌ WRONG - Direct API call:**
|
||||
```javascript
|
||||
function my_function(frm) {
|
||||
frappe.call({
|
||||
method: 'invoice_az.my_api.some_method',
|
||||
// ... this will fail with "Authentication expired"
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**✅ CORRECT - Always wrap with authentication check:**
|
||||
```javascript
|
||||
function my_function(frm) {
|
||||
ETaxes.auth.checkAndProcess(function() {
|
||||
// Your code here - only runs after auth is verified
|
||||
frappe.call({
|
||||
method: 'invoice_az.my_api.some_method',
|
||||
// ... works correctly
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Why?** `ETaxes.auth.checkAndProcess()` does:
|
||||
1. Checks if token is still valid
|
||||
2. If expired → prompts user to login via ASAN Imza
|
||||
3. Only after successful auth → executes your callback
|
||||
4. Prevents "Authentication expired" errors
|
||||
|
||||
### 2. ETaxes Module Inclusion (MANDATORY)
|
||||
|
||||
**⚠️ ETaxes is NOT a global module!** It must be defined in EACH JavaScript file that uses it.
|
||||
|
||||
**❌ WRONG - Assuming ETaxes exists:**
|
||||
```javascript
|
||||
// my_custom_form.js
|
||||
frappe.ui.form.on('My DocType', {
|
||||
refresh: function(frm) {
|
||||
ETaxes.auth.checkAndProcess(function() {
|
||||
// ERROR: ETaxes is not defined
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**✅ CORRECT - Include ETaxes module at the top of your JS file:**
|
||||
|
||||
Copy the entire ETaxes module from `sales_invoice.js` (lines 1-564) to the **beginning** of your JS file:
|
||||
```javascript
|
||||
// ======= COPY THIS ENTIRE BLOCK =======
|
||||
// From sales_invoice.js lines 1-564:
|
||||
const ETaxes = {
|
||||
PROGRESS_UPDATE_DELAY: 50,
|
||||
AUTH_POLL_INTERVAL: 6000,
|
||||
AUTH_MAX_ATTEMPTS: 20,
|
||||
// ... (full module definition)
|
||||
};
|
||||
ETaxes.utils = { /* ... */ };
|
||||
ETaxes.dialogs = { /* ... */ };
|
||||
ETaxes.auth = { /* ... */ };
|
||||
// ======= END COPY =======
|
||||
|
||||
// Now your code can use ETaxes:
|
||||
frappe.ui.form.on('My DocType', {
|
||||
refresh: function(frm) {
|
||||
ETaxes.auth.checkAndProcess(function() {
|
||||
// ✅ Works correctly
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**Files that need ETaxes module:**
|
||||
- ✅ `sales_invoice.js` - has it (564 lines)
|
||||
- ✅ `purchase_order.js` - has it
|
||||
- ✅ `supplier.js` - has it (after fix)
|
||||
- ❌ Any new JS file - **MUST include it!**
|
||||
|
||||
### 3. Backend API Pattern (MANDATORY)
|
||||
|
||||
**All backend functions that call E-Taxes API MUST follow this pattern:**
|
||||
|
||||
```python
|
||||
import frappe
|
||||
import requests
|
||||
from invoice_az.auth import record_etaxes_activity, get_default_asan_login
|
||||
|
||||
@frappe.whitelist()
|
||||
def my_etaxes_function(doc_name):
|
||||
"""My E-Taxes API function"""
|
||||
try:
|
||||
# Step 1: ALWAYS record activity (for token renewal)
|
||||
record_etaxes_activity()
|
||||
|
||||
# Step 2: Get document
|
||||
doc = frappe.get_doc("My DocType", doc_name)
|
||||
|
||||
# Step 3: Validate (business logic checks)
|
||||
validation = validate_my_document(doc)
|
||||
if not validation.get("valid"):
|
||||
return {
|
||||
"success": False,
|
||||
"message": validation.get("message")
|
||||
}
|
||||
|
||||
# Step 4: Get authentication token
|
||||
asan_login = get_default_asan_login()
|
||||
if not asan_login.get("found") or not asan_login.get("main_token"):
|
||||
return {
|
||||
"success": False,
|
||||
"message": "E-Taxes authentication not found. Please login via ASAN."
|
||||
}
|
||||
|
||||
token = asan_login.get("main_token")
|
||||
|
||||
# Step 5: Build headers with token
|
||||
headers = DEFAULT_HEADERS.copy()
|
||||
headers["x-authorization"] = f"Bearer {token}"
|
||||
|
||||
# Step 6: Make API request
|
||||
response = requests.post(
|
||||
"https://new.e-taxes.gov.az/api/...",
|
||||
data=json.dumps(payload),
|
||||
headers=headers,
|
||||
timeout=60
|
||||
)
|
||||
|
||||
# Step 7: Handle common errors
|
||||
if response.status_code == 401:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Authentication expired. Please login again."
|
||||
}
|
||||
|
||||
if response.status_code == 500:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "E-Taxes service error. Please try again later."
|
||||
}
|
||||
|
||||
if response.status_code == 404:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Resource not found."
|
||||
}
|
||||
|
||||
response.raise_for_status()
|
||||
|
||||
# Step 8: Process response
|
||||
result = response.json()
|
||||
|
||||
# Step 9: Return success
|
||||
return {
|
||||
"success": True,
|
||||
"data": result
|
||||
}
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
frappe.log_error(
|
||||
f"Network error: {str(e)}",
|
||||
"E-Taxes API Error"
|
||||
)
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"Network error: {str(e)}"
|
||||
}
|
||||
except Exception as e:
|
||||
frappe.log_error(
|
||||
f"Unexpected error: {str(e)}\n{frappe.get_traceback()}",
|
||||
"E-Taxes API Error"
|
||||
)
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"Error: {str(e)}"
|
||||
}
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
1. `@frappe.whitelist()` - ALWAYS required for frontend calls
|
||||
2. `record_etaxes_activity()` - ALWAYS first (enables token auto-renewal)
|
||||
3. `get_default_asan_login()` - Get token
|
||||
4. `headers["x-authorization"] = f"Bearer {token}"` - Required for auth
|
||||
5. Handle 401, 500, 404 errors explicitly
|
||||
6. ALWAYS return `{"success": bool, "message": str}` format
|
||||
7. ALWAYS use try-except with proper error logging
|
||||
|
||||
### 4. Constants and URLs
|
||||
|
||||
**Base URL:**
|
||||
```python
|
||||
BASE_URL = "https://new.e-taxes.gov.az"
|
||||
```
|
||||
|
||||
**Common endpoints:**
|
||||
- Auth: `/api/po/auth/public/v1/*`
|
||||
- Invoices: `/api/po/invoice/public/v2/invoice`
|
||||
- Sign: `/api/po/invoice/public/v1/invoice/sign/withAsanImza`
|
||||
- Serial: `/api/po/invoice/public/v1/generateSerialNumber/defaultInvoice`
|
||||
- Agricultural Serial: `/api/po/invoice/public/v1/act/generateSerialNumber/agriculturalProductsAct`
|
||||
- Agricultural Act: `/api/po/invoice/public/v1/act`
|
||||
- Remove Drafts: `/api/po/invoice/public/v1/common/removeDrafts`
|
||||
- VAT Operations: `/api/po/vatacc/public/v1/operation/find.outbox`
|
||||
- Taxpayer by FIN: `/api/po/profile/public/v1/taxpayer/findByFinAndPassport`
|
||||
|
||||
**Standard headers:**
|
||||
```python
|
||||
DEFAULT_HEADERS = {
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
"Content-Type": "application/json",
|
||||
"Cache-Control": "no-cache",
|
||||
"Pragma": "no-cache"
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Error Handling
|
||||
|
||||
**HTTP Status Codes:**
|
||||
- `200` - Success
|
||||
- `401` - Token expired → Show "Please login again"
|
||||
- `404` - Resource not found
|
||||
- `500` - E-Taxes server error → Show "Try again later"
|
||||
|
||||
**Always check in this order:**
|
||||
```python
|
||||
if response.status_code == 401:
|
||||
return {"success": False, "message": "Authentication expired. Please login again."}
|
||||
|
||||
if response.status_code == 500:
|
||||
return {"success": False, "message": "E-Taxes service error. Please try again later."}
|
||||
|
||||
if response.status_code == 404:
|
||||
return {"success": False, "message": "Not found"}
|
||||
|
||||
response.raise_for_status() # For other errors
|
||||
```
|
||||
|
||||
### 6. Frontend Button Pattern
|
||||
|
||||
**Always show buttons conditionally:**
|
||||
```javascript
|
||||
frappe.ui.form.on('My DocType', {
|
||||
refresh: function(frm) {
|
||||
// Only for submitted documents
|
||||
if (frm.doc.docstatus === 1) {
|
||||
add_etaxes_buttons(frm);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function add_etaxes_buttons(frm) {
|
||||
// Check status and show appropriate buttons
|
||||
if (frm.doc.etaxes_status === 'Sent and Signed') {
|
||||
frm.add_custom_button(__('View Details'), function() {
|
||||
// Show details
|
||||
}, __('E-Taxes'));
|
||||
} else if (frm.doc.etaxes_status === 'Created, not signed') {
|
||||
frm.add_custom_button(__('Sign with ASAN Imza'), function() {
|
||||
// Sign document
|
||||
}, __('E-Taxes'));
|
||||
} else {
|
||||
frm.add_custom_button(__('Send to E-Taxes'), function() {
|
||||
// Send to E-Taxes
|
||||
}, __('E-Taxes'));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Build Process
|
||||
|
||||
**After modifying JavaScript files:**
|
||||
```bash
|
||||
# Build assets
|
||||
cd /home/frappe/frappe-bench
|
||||
bench build --app invoice_az
|
||||
|
||||
# Clear cache
|
||||
bench --site site1 clear-cache
|
||||
```
|
||||
|
||||
**After modifying Python files:**
|
||||
```bash
|
||||
# Just clear cache (no build needed)
|
||||
bench --site site1 clear-cache
|
||||
```
|
||||
|
||||
### 8. Common Mistakes to Avoid
|
||||
|
||||
❌ **Don't do this:**
|
||||
1. Call E-Taxes API without `ETaxes.auth.checkAndProcess()`
|
||||
2. Forget to include ETaxes module in new JS files
|
||||
3. Skip `record_etaxes_activity()` in backend functions
|
||||
4. Forget to handle 401/500/404 errors
|
||||
5. Use `app_include_js` in hooks.py (doesn't work for doctype-specific code)
|
||||
6. Make API calls without Bearer token in headers
|
||||
|
||||
✅ **Always do this:**
|
||||
1. Wrap ALL E-Taxes operations in `ETaxes.auth.checkAndProcess()`
|
||||
2. Copy ETaxes module (564 lines) to every new JS file
|
||||
3. Call `record_etaxes_activity()` first in every API function
|
||||
4. Handle 401, 500, 404 explicitly
|
||||
5. Return consistent `{success, message}` format
|
||||
6. Add `x-authorization: Bearer {token}` header
|
||||
7. Build assets after JS changes
|
||||
|
||||
### 9. Example Files to Reference
|
||||
|
||||
**Backend (Python):**
|
||||
- ✅ `send_sales_api.py` - Perfect example of all patterns
|
||||
- ✅ `send_purchase_api.py` - Agricultural act example
|
||||
- ✅ `supplier_api.py` - Simple API call example
|
||||
- ✅ `auth.py` - Authentication implementation
|
||||
|
||||
**Frontend (JavaScript):**
|
||||
- ✅ `sales_invoice.js` - Complete ETaxes module (lines 1-564)
|
||||
- ✅ `purchase_invoice.js` - Button patterns
|
||||
- ✅ `supplier.js` - Minimal working example
|
||||
|
||||
**When in doubt:** Look at `send_sales_api.py` + `sales_invoice.js` - they contain all correct patterns!
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Linting and Code Quality
|
||||
|
|
|
|||
|
|
@ -1,26 +1,908 @@
|
|||
/**
|
||||
* Purchase Invoice Form and List Configuration
|
||||
* Optimized version using ETaxes common module patterns
|
||||
* Includes Agricultural Act sending functionality for Individual suppliers
|
||||
*/
|
||||
|
||||
// ======= ETAXES MODULE (REQUIRED FOR AUTHENTICATION) =======
|
||||
// This module MUST be included in every JS file that calls E-Taxes API
|
||||
const ETaxes = {
|
||||
// Константы
|
||||
PROGRESS_UPDATE_DELAY: 50,
|
||||
AUTH_POLL_INTERVAL: 6000,
|
||||
AUTH_MAX_ATTEMPTS: 20,
|
||||
|
||||
// Глобальные переменные
|
||||
loadingDialog: null,
|
||||
cancelLoading: false,
|
||||
loadingErrors: [],
|
||||
|
||||
// Кэш
|
||||
cache: {
|
||||
defaultLogin: null,
|
||||
cacheTime: null,
|
||||
cacheDuration: 300000 // 5 минут
|
||||
}
|
||||
};
|
||||
|
||||
// ======= БАЗОВЫЕ УТИЛИТЫ =======
|
||||
ETaxes.utils = {
|
||||
// Кэшированное получение настроек входа
|
||||
getDefaultLogin: function(callback, useCache = true) {
|
||||
const now = Date.now();
|
||||
|
||||
if (useCache && ETaxes.cache.defaultLogin && ETaxes.cache.cacheTime &&
|
||||
(now - ETaxes.cache.cacheTime) < ETaxes.cache.cacheDuration) {
|
||||
callback(ETaxes.cache.defaultLogin);
|
||||
return;
|
||||
}
|
||||
|
||||
frappe.call({
|
||||
method: 'invoice_az.auth.get_default_asan_login',
|
||||
callback: function(r) {
|
||||
if (r.message) {
|
||||
ETaxes.cache.defaultLogin = r.message;
|
||||
ETaxes.cache.cacheTime = now;
|
||||
}
|
||||
callback(r.message);
|
||||
},
|
||||
error: function() {
|
||||
callback({found: false, error: 'Network error'});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Проверка валидности токена
|
||||
checkTokenValidity: function(callback) {
|
||||
frappe.call({
|
||||
method: 'invoice_az.auth.check_token_validity',
|
||||
callback: function(r) {
|
||||
callback(r.message && r.message.valid);
|
||||
},
|
||||
error: function() {
|
||||
callback(false);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Форматирование валюты
|
||||
formatCurrency: function(amount) {
|
||||
return new Intl.NumberFormat('az-AZ', {
|
||||
style: 'currency',
|
||||
currency: 'AZN',
|
||||
minimumFractionDigits: 2
|
||||
}).format(amount || 0);
|
||||
},
|
||||
|
||||
// Очистка кэша
|
||||
clearCache: function() {
|
||||
ETaxes.cache.defaultLogin = null;
|
||||
ETaxes.cache.cacheTime = null;
|
||||
}
|
||||
};
|
||||
|
||||
// ======= ДИАЛОГИ ЗАГРУЗКИ =======
|
||||
ETaxes.dialogs = {
|
||||
// Показать диалог загрузки
|
||||
showLoading: function(title, message, submessage) {
|
||||
if (ETaxes.loadingDialog) {
|
||||
this.updateLoading(title, message, submessage);
|
||||
return;
|
||||
}
|
||||
|
||||
ETaxes.loadingDialog = new frappe.ui.Dialog({
|
||||
title: title,
|
||||
fields: [{
|
||||
fieldname: 'loading_html',
|
||||
fieldtype: 'HTML',
|
||||
options: this._getLoadingHTML(title, message, submessage)
|
||||
}],
|
||||
primary_action_label: __('Cancel'),
|
||||
primary_action: function() {
|
||||
ETaxes.cancelLoading = true;
|
||||
ETaxes.loadingDialog.$wrapper.find('.primary-action').prop('disabled', true);
|
||||
ETaxes.loadingDialog.$wrapper.find('.primary-action').html(__('Cancelling...'));
|
||||
$('#status_message p').text(__('Cancelling the operation.'));
|
||||
}
|
||||
});
|
||||
|
||||
ETaxes.cancelLoading = false;
|
||||
ETaxes.loadingDialog.show();
|
||||
ETaxes.loadingDialog.$wrapper.find('.modal-dialog').css('max-width', '450px');
|
||||
},
|
||||
|
||||
// Обновить сообщение в диалоге
|
||||
updateLoading: function(title, message, submessage) {
|
||||
if (!ETaxes.loadingDialog) return;
|
||||
|
||||
if (title) $('#loading_title').text(title);
|
||||
if (message) $('#loading_message').text(message);
|
||||
if (submessage !== undefined) {
|
||||
$('#loading_submessage').text(submessage);
|
||||
$('#loading_submessage').toggle(!!submessage);
|
||||
}
|
||||
},
|
||||
|
||||
// Показать код верификации
|
||||
showVerificationCode: function(code) {
|
||||
if (ETaxes.loadingDialog && code) {
|
||||
$('#verification_code').text(code);
|
||||
$('#verification_code_container').show();
|
||||
}
|
||||
},
|
||||
|
||||
// Установить статус успеха
|
||||
setSuccess: function(message, submessage, callback, delay = 2000) {
|
||||
if (!ETaxes.loadingDialog) {
|
||||
if (callback) callback();
|
||||
return;
|
||||
}
|
||||
|
||||
ETaxes.loadingDialog.$wrapper.find('.loading-animation').html(`
|
||||
<div style="width: 64px; height: 64px; display: flex; align-items: center; justify-content: center;">
|
||||
<i class="fa fa-check" style="font-size: 48px; color: #5cb85c;"></i>
|
||||
</div>
|
||||
`);
|
||||
|
||||
$('#loading_message').text(message);
|
||||
if (submessage !== undefined) {
|
||||
$('#loading_submessage').text(submessage);
|
||||
$('#loading_submessage').toggle(!!submessage);
|
||||
}
|
||||
|
||||
$('#verification_code_container').hide();
|
||||
ETaxes.loadingDialog.set_primary_action(null);
|
||||
ETaxes.loadingDialog.set_secondary_action(null);
|
||||
|
||||
setTimeout(() => {
|
||||
this.hide();
|
||||
if (callback) callback();
|
||||
}, delay);
|
||||
},
|
||||
|
||||
// Установить статус ошибки
|
||||
setError: function(message, submessage, showCloseButton = true) {
|
||||
if (!ETaxes.loadingDialog) return;
|
||||
|
||||
// Update dialog title
|
||||
ETaxes.loadingDialog.set_title(__('Error'));
|
||||
|
||||
// Change animation to error icon
|
||||
ETaxes.loadingDialog.$wrapper.find('.loading-animation').html(`
|
||||
<div style="width: 64px; height: 64px; display: flex; align-items: center; justify-content: center;">
|
||||
<i class="fa fa-times" style="font-size: 48px; color: #d9534f;"></i>
|
||||
</div>
|
||||
`);
|
||||
|
||||
// Clear all existing text elements
|
||||
ETaxes.loadingDialog.$wrapper.find('#loading_title').text('');
|
||||
ETaxes.loadingDialog.$wrapper.find('#loading_submessage').remove();
|
||||
|
||||
// Set error message
|
||||
const $msgElement = ETaxes.loadingDialog.$wrapper.find('#loading_message');
|
||||
if ($msgElement.length) {
|
||||
$msgElement.css({
|
||||
'color': '#d9534f',
|
||||
'font-weight': '500',
|
||||
'font-size': '14px',
|
||||
'margin-top': '15px'
|
||||
});
|
||||
|
||||
if (submessage) {
|
||||
$msgElement.html(submessage.replace(/\n/g, '<br>'));
|
||||
} else {
|
||||
$msgElement.text(message);
|
||||
}
|
||||
}
|
||||
|
||||
$('#verification_code_container').hide();
|
||||
|
||||
if (showCloseButton) {
|
||||
ETaxes.loadingDialog.set_primary_action(__('Close'), () => this.hide());
|
||||
}
|
||||
},
|
||||
|
||||
// Скрыть диалог
|
||||
hide: function() {
|
||||
if (ETaxes.loadingDialog) {
|
||||
try {
|
||||
ETaxes.loadingDialog.hide();
|
||||
} catch (e) {
|
||||
console.error("Error hiding loading dialog:", e);
|
||||
}
|
||||
ETaxes.loadingDialog = null;
|
||||
}
|
||||
ETaxes.cancelLoading = false;
|
||||
},
|
||||
|
||||
// Получить HTML для диалога загрузки
|
||||
_getLoadingHTML: function(title, message, submessage) {
|
||||
return `
|
||||
<div class="text-center">
|
||||
<div class="loading-animation" 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 id="loading_title">${title || __('Please wait...')}</h4>
|
||||
<p id="loading_message">${message || __('The operation may take some time')}</p>
|
||||
<p id="loading_submessage" style="${submessage ? '' : 'display:none'}">${submessage || ''}</p>
|
||||
</div>
|
||||
<div id="verification_code_container" style="display:none; margin-top: 15px;">
|
||||
<div style="background: #f8f9fa; padding: 10px; display: inline-block; border-radius: 5px; font-size: 24px; font-weight: bold; letter-spacing: 2px; color: #333; border: 1px solid #ddd;">
|
||||
<span id="verification_code">----</span>
|
||||
</div>
|
||||
<p style="margin-top: 5px; font-size: 12px; color: #666;">Verification Code</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
};
|
||||
|
||||
// ======= МОДУЛЬ АУТЕНТИФИКАЦИИ =======
|
||||
ETaxes.auth = {
|
||||
// Проверить токен и обработать аутентификацию при необходимости
|
||||
checkAndProcess: function(callback) {
|
||||
ETaxes.utils.checkTokenValidity(function(isValid) {
|
||||
if (isValid) {
|
||||
callback();
|
||||
} else {
|
||||
frappe.confirm(
|
||||
__('Your E-Taxes session has expired or authentication is required. Would you like to authenticate now?'),
|
||||
function() {
|
||||
ETaxes.auth.startProcess(callback);
|
||||
},
|
||||
function() {
|
||||
frappe.show_alert({
|
||||
message: __('Authentication canceled. Operation cannot be completed.'),
|
||||
indicator: 'red'
|
||||
}, 5);
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Начать процесс аутентификации
|
||||
startProcess: function(callback) {
|
||||
ETaxes.dialogs.showLoading(
|
||||
__('Starting Authentication'),
|
||||
__('Getting Asan Login settings...'),
|
||||
__('Please wait')
|
||||
);
|
||||
|
||||
ETaxes.utils.getDefaultLogin(function(loginData) {
|
||||
if (loginData && loginData.found) {
|
||||
const asanLoginName = loginData.name;
|
||||
ETaxes.dialogs.updateLoading(
|
||||
null,
|
||||
__('Sending authentication request...'),
|
||||
__('This will send a request to your Asan Imza mobile app')
|
||||
);
|
||||
|
||||
frappe.call({
|
||||
method: 'invoice_az.auth.handle_authentication',
|
||||
args: { 'asan_login_name': asanLoginName },
|
||||
callback: function(r) {
|
||||
if (r.message && r.message.success) {
|
||||
const bearerToken = r.message.bearer_token;
|
||||
|
||||
if (r.message.verification_code) {
|
||||
ETaxes.dialogs.showVerificationCode(r.message.verification_code);
|
||||
}
|
||||
|
||||
ETaxes.dialogs.updateLoading(
|
||||
null,
|
||||
__('Waiting for confirmation on your phone'),
|
||||
__('Please check your phone and confirm the authentication request')
|
||||
);
|
||||
|
||||
ETaxes.auth.pollStatus(asanLoginName, bearerToken, function(success) {
|
||||
if (success) {
|
||||
ETaxes.auth.complete(asanLoginName, callback);
|
||||
} else {
|
||||
ETaxes.dialogs.setError(
|
||||
__('Authentication Failed'),
|
||||
__('Could not authenticate with Asan Imza')
|
||||
);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ETaxes.dialogs.setError(
|
||||
__('Authentication Error'),
|
||||
r.message ? r.message.message : __('Authentication request failed')
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ETaxes.dialogs.setError(
|
||||
__('No Asan Login Settings'),
|
||||
__('Please configure Asan Login settings first')
|
||||
);
|
||||
}
|
||||
}, false); // Не использовать кэш для аутентификации
|
||||
},
|
||||
|
||||
// Опрос статуса аутентификации
|
||||
pollStatus: function(asanLoginName, bearerToken, callback) {
|
||||
let attempts = 0;
|
||||
let stopPolling = false;
|
||||
|
||||
function pollStatusStep() {
|
||||
if (attempts >= ETaxes.AUTH_MAX_ATTEMPTS || stopPolling) {
|
||||
if (attempts >= ETaxes.AUTH_MAX_ATTEMPTS) {
|
||||
ETaxes.dialogs.setError(
|
||||
__('Authentication Timeout'),
|
||||
__('The authentication request has timed out. Please try again.')
|
||||
);
|
||||
callback(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
attempts++;
|
||||
|
||||
ETaxes.dialogs.updateLoading(
|
||||
null,
|
||||
__('Waiting for confirmation on your phone'),
|
||||
__('Please check your phone and confirm the authentication request') +
|
||||
' (' + attempts + '/' + ETaxes.AUTH_MAX_ATTEMPTS + ')'
|
||||
);
|
||||
|
||||
frappe.call({
|
||||
method: 'invoice_az.auth.poll_auth_status',
|
||||
args: {
|
||||
'asan_login_name': asanLoginName,
|
||||
'bearer_token': bearerToken
|
||||
},
|
||||
callback: function(r) {
|
||||
if (r.message && r.message.success) {
|
||||
if (r.message.authenticated) {
|
||||
stopPolling = true;
|
||||
ETaxes.dialogs.setSuccess(
|
||||
__('Authentication Successful'),
|
||||
__('You are now authenticated with Asan Imza'),
|
||||
function() {
|
||||
callback(true);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
setTimeout(pollStatusStep, ETaxes.AUTH_POLL_INTERVAL);
|
||||
}
|
||||
} else {
|
||||
stopPolling = true;
|
||||
ETaxes.dialogs.setError(
|
||||
__('Authentication Error'),
|
||||
r.message ? r.message.message : __('An unknown error occurred')
|
||||
);
|
||||
callback(false);
|
||||
}
|
||||
},
|
||||
error: function(err) {
|
||||
console.error('Error during status check:', err);
|
||||
setTimeout(pollStatusStep, ETaxes.AUTH_POLL_INTERVAL);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pollStatusStep();
|
||||
},
|
||||
|
||||
// Завершение аутентификации
|
||||
complete: function(asanLoginName, callback) {
|
||||
ETaxes.dialogs.showLoading(
|
||||
__('Completing Authentication'),
|
||||
__('Getting certificates...'),
|
||||
__('Please wait')
|
||||
);
|
||||
|
||||
frappe.call({
|
||||
method: 'invoice_az.auth.get_auth_certificates',
|
||||
args: { 'asan_login_name': asanLoginName },
|
||||
callback: function(certR) {
|
||||
if (certR.message && certR.message.success) {
|
||||
ETaxes.dialogs.updateLoading(
|
||||
null,
|
||||
__('Certificates retrieved'),
|
||||
__('Selecting certificate and taxpayer')
|
||||
);
|
||||
|
||||
frappe.call({
|
||||
method: 'frappe.client.get',
|
||||
args: {
|
||||
doctype: 'Asan Login',
|
||||
name: asanLoginName
|
||||
},
|
||||
callback: function(r) {
|
||||
if (r.message && r.message.selected_certificate_json) {
|
||||
try {
|
||||
const certData = JSON.parse(r.message.selected_certificate_json);
|
||||
const certName = r.message.selected_certificate;
|
||||
|
||||
ETaxes.dialogs.updateLoading(
|
||||
null,
|
||||
__('Selecting certificate'),
|
||||
certName
|
||||
);
|
||||
|
||||
ETaxes.auth._selectCertificate(asanLoginName, certData, certName, callback);
|
||||
} catch (e) {
|
||||
console.error('Error parsing certificate:', e);
|
||||
ETaxes.dialogs.hide();
|
||||
ETaxes.auth._showCertificateSelector(certR.message.certificates, asanLoginName, callback);
|
||||
}
|
||||
} else {
|
||||
ETaxes.dialogs.hide();
|
||||
ETaxes.auth._showCertificateSelector(certR.message.certificates, asanLoginName, callback);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ETaxes.dialogs.setError(
|
||||
__('Error'),
|
||||
certR.message ? certR.message.message : __('Failed to get certificates')
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Выбор сертификата
|
||||
_selectCertificate: function(asanLoginName, certData, certName, callback) {
|
||||
frappe.call({
|
||||
method: 'invoice_az.auth.select_certificate',
|
||||
args: {
|
||||
'asan_login_name': asanLoginName,
|
||||
'certificate_data': certData,
|
||||
'certificate_name': certName
|
||||
},
|
||||
callback: function(r) {
|
||||
if (r.message && r.message.success) {
|
||||
ETaxes.dialogs.updateLoading(
|
||||
null,
|
||||
__('Selecting taxpayer'),
|
||||
__('Using certificate: ') + certName
|
||||
);
|
||||
|
||||
frappe.call({
|
||||
method: 'invoice_az.auth.select_taxpayer',
|
||||
args: { 'asan_login_name': asanLoginName },
|
||||
callback: function(r) {
|
||||
if (r.message && r.message.success) {
|
||||
ETaxes.utils.clearCache(); // Очищаем кэш после успешной аутентификации
|
||||
ETaxes.dialogs.setSuccess(
|
||||
__('Authentication Complete'),
|
||||
__('You can now access E-Taxes services'),
|
||||
callback
|
||||
);
|
||||
} else {
|
||||
ETaxes.dialogs.setError(
|
||||
__('Error'),
|
||||
r.message ? r.message.message : __('Failed to select taxpayer')
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ETaxes.dialogs.setError(
|
||||
__('Error'),
|
||||
r.message ? r.message.message : __('Failed to select certificate')
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Показать селектор сертификатов
|
||||
_showCertificateSelector: function(certificates, asanLoginName, callback) {
|
||||
if (!certificates || !certificates.length) {
|
||||
frappe.msgprint(__('No certificates available'));
|
||||
return;
|
||||
}
|
||||
|
||||
let certHtml = '<div style="max-height: 400px; overflow-y: auto;"><table class="table table-bordered">';
|
||||
certHtml += '<thead><tr><th>Type</th><th>Name</th><th>ID</th><th>Position</th><th>Status</th><th></th></tr></thead><tbody>';
|
||||
|
||||
certificates.forEach(function(cert, index) {
|
||||
let name = '';
|
||||
let 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 || '';
|
||||
}
|
||||
|
||||
certHtml += '<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>';
|
||||
});
|
||||
|
||||
certHtml += '</tbody></table></div>';
|
||||
|
||||
const dialog = new frappe.ui.Dialog({
|
||||
title: __('Select Certificate'),
|
||||
fields: [{
|
||||
fieldtype: 'HTML',
|
||||
fieldname: 'certificates',
|
||||
options: certHtml
|
||||
}]
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
|
||||
dialog.$wrapper.find('.select-cert').on('click', function() {
|
||||
const index = $(this).data('index');
|
||||
const cert = certificates[index];
|
||||
|
||||
let certName = '';
|
||||
if (cert.taxpayerType === 'individual' && cert.individualInfo) {
|
||||
certName = `${cert.individualInfo.name} (${cert.individualInfo.fin})`;
|
||||
} else if (cert.legalInfo) {
|
||||
certName = `${cert.legalInfo.name} (${cert.legalInfo.voen || cert.legalInfo.tin})`;
|
||||
} else {
|
||||
certName = `Certificate ${index + 1}`;
|
||||
}
|
||||
|
||||
dialog.hide();
|
||||
|
||||
ETaxes.dialogs.showLoading(
|
||||
__('Selecting Certificate'),
|
||||
__('Processing your selection...'),
|
||||
certName
|
||||
);
|
||||
|
||||
ETaxes.auth._selectCertificate(asanLoginName, cert, certName, callback);
|
||||
});
|
||||
}
|
||||
};
|
||||
// ======= END ETAXES MODULE =======
|
||||
|
||||
frappe.ui.form.on('Purchase Invoice', {
|
||||
refresh: function(frm) {
|
||||
// Check if is_taxes_doc flag is set
|
||||
if (frm.doc.is_taxes_doc) {
|
||||
// Make all fields read-only
|
||||
frm.set_read_only(true);
|
||||
|
||||
|
||||
// Add visual indicator
|
||||
frm.page.set_indicator(__('Tax Document'), 'blue');
|
||||
}
|
||||
|
||||
// Add E-Taxes buttons for Agricultural Acts (only for Individual suppliers)
|
||||
if (frm.doc.docstatus === 1) {
|
||||
add_agricultural_act_buttons(frm);
|
||||
}
|
||||
|
||||
// Add status indicator based on send status
|
||||
add_status_indicator(frm);
|
||||
},
|
||||
|
||||
|
||||
// Make is_taxes_doc field read-only when document loads
|
||||
onload: function(frm) {
|
||||
frm.set_df_property('is_taxes_doc', 'read_only', 1);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Add status indicator based on E-Taxes send status
|
||||
*/
|
||||
function add_status_indicator(frm) {
|
||||
if (!frm.doc.etaxes_purchase_send_status) return;
|
||||
|
||||
const status_colors = {
|
||||
'Sent and Signed': 'green',
|
||||
'Created, not signed': 'orange',
|
||||
'Failed': 'red',
|
||||
'Not Sent': 'gray'
|
||||
};
|
||||
|
||||
const color = status_colors[frm.doc.etaxes_purchase_send_status] || 'gray';
|
||||
|
||||
if (frm.doc.etaxes_purchase_send_status !== 'Not Sent') {
|
||||
frm.page.set_indicator(__(frm.doc.etaxes_purchase_send_status), color);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Agricultural Act buttons for Individual suppliers
|
||||
*/
|
||||
function add_agricultural_act_buttons(frm) {
|
||||
// Check if supplier is Individual type
|
||||
if (!frm.doc.supplier) return;
|
||||
|
||||
frappe.db.get_value('Supplier', frm.doc.supplier, 'supplier_type', function(r) {
|
||||
if (r && r.supplier_type === 'Individual') {
|
||||
// Show buttons based on status
|
||||
|
||||
// Status: Sent and Signed - show details only
|
||||
if (frm.doc.etaxes_purchase_send_status === 'Sent and Signed') {
|
||||
frm.add_custom_button(__('View E-Taxes Details'), function() {
|
||||
show_act_details(frm);
|
||||
}, __('E-Taxes Agricultural Act'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Status: Created, not signed - show sign button
|
||||
if (frm.doc.etaxes_purchase_send_status === 'Created, not signed') {
|
||||
frm.add_custom_button(__('Sign Act with ASAN Imza'), function() {
|
||||
sign_agricultural_act(frm);
|
||||
}, __('E-Taxes Agricultural Act'));
|
||||
|
||||
frm.add_custom_button(__('Cancel Act on E-Taxes'), function() {
|
||||
cancel_act_on_etaxes(frm);
|
||||
}, __('E-Taxes Agricultural Act'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Status: Not sent or Failed - show send button
|
||||
if (!frm.doc.etaxes_purchase_send_status ||
|
||||
frm.doc.etaxes_purchase_send_status === 'Not Sent' ||
|
||||
frm.doc.etaxes_purchase_send_status === 'Failed') {
|
||||
|
||||
frm.add_custom_button(__('Send Agricultural Act to E-Taxes'), function() {
|
||||
send_agricultural_act(frm);
|
||||
}, __('E-Taxes Agricultural Act'));
|
||||
|
||||
// Show cancel button if act exists
|
||||
if (frm.doc.etaxes_purchase_id) {
|
||||
frm.add_custom_button(__('Cancel Act on E-Taxes'), function() {
|
||||
cancel_act_on_etaxes(frm);
|
||||
}, __('E-Taxes Agricultural Act'));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send Agricultural Act to E-Taxes (Step 1: Create draft)
|
||||
*/
|
||||
function send_agricultural_act(frm) {
|
||||
frappe.confirm(
|
||||
__('Send Agricultural Act to E-Taxes?') + '<br><br>' +
|
||||
__('Purchase Invoice: {0}', [frm.doc.name]) + '<br>' +
|
||||
__('Supplier: {0}', [frm.doc.supplier_name]) + '<br><br>' +
|
||||
__('This will create a draft act. You will need to sign it with ASAN Imza after creation.'),
|
||||
function() {
|
||||
// ✅ CRITICAL: Always wrap E-Taxes API calls with authentication check
|
||||
ETaxes.auth.checkAndProcess(function() {
|
||||
// Show loading
|
||||
frappe.show_alert({
|
||||
message: __('Creating agricultural act...'),
|
||||
indicator: 'blue'
|
||||
}, 3);
|
||||
|
||||
frappe.call({
|
||||
method: 'invoice_az.send_purchase_api.send_purchase_invoice_to_etaxes',
|
||||
args: {
|
||||
purchase_invoice_name: frm.doc.name
|
||||
},
|
||||
callback: function(r) {
|
||||
// Log full response to console for debugging
|
||||
console.log('[PURCHASE_ACT] Full response from server:', r);
|
||||
|
||||
if (r.message && r.message.success) {
|
||||
// Success
|
||||
console.log('[PURCHASE_ACT] Success! Act created:', r.message);
|
||||
frappe.show_alert({
|
||||
message: __('Draft act created successfully!') + '<br>' +
|
||||
__('Serial: {0}', [r.message.serial_number]),
|
||||
indicator: 'green'
|
||||
}, 5);
|
||||
|
||||
// Reload document
|
||||
frm.reload_doc();
|
||||
} else {
|
||||
// Error
|
||||
console.error('[PURCHASE_ACT] Error creating act:', r.message);
|
||||
const errorMsg = r.message ? r.message.message : __('Failed to create agricultural act');
|
||||
frappe.msgprint({
|
||||
title: __('Error'),
|
||||
indicator: 'red',
|
||||
message: errorMsg
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function(r) {
|
||||
console.error('[PURCHASE_ACT] Communication error:', r);
|
||||
frappe.msgprint({
|
||||
title: __('Error'),
|
||||
indicator: 'red',
|
||||
message: __('Failed to communicate with E-Taxes service.')
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign Agricultural Act with ASAN Imza (Step 2)
|
||||
*/
|
||||
function sign_agricultural_act(frm) {
|
||||
frappe.confirm(
|
||||
__('Sign Agricultural Act with ASAN Imza?') + '<br><br>' +
|
||||
__('Purchase Invoice: {0}', [frm.doc.name]) + '<br>' +
|
||||
__('Serial Number: {0}', [frm.doc.etaxes_purchase_serial_number]) + '<br>' +
|
||||
__('E-Taxes Act ID: {0}', [frm.doc.etaxes_purchase_id]) + '<br><br>' +
|
||||
__('This will finalize the act and send it to E-Taxes.'),
|
||||
function() {
|
||||
// ✅ CRITICAL: Always wrap E-Taxes API calls with authentication check
|
||||
ETaxes.auth.checkAndProcess(function() {
|
||||
// Show loading
|
||||
frappe.show_alert({
|
||||
message: __('Signing agricultural act...'),
|
||||
indicator: 'blue'
|
||||
}, 3);
|
||||
|
||||
frappe.call({
|
||||
method: 'invoice_az.send_purchase_api.retry_signing_act',
|
||||
args: {
|
||||
purchase_invoice_name: frm.doc.name
|
||||
},
|
||||
callback: function(r) {
|
||||
if (r.message && r.message.success) {
|
||||
// Success
|
||||
frappe.show_alert({
|
||||
message: __('Act signed successfully!') + '<br>' +
|
||||
__('Verification Code: {0}', [r.message.verification_code]),
|
||||
indicator: 'green'
|
||||
}, 5);
|
||||
|
||||
// Reload document
|
||||
frm.reload_doc();
|
||||
} else {
|
||||
// Error
|
||||
const errorMsg = r.message ? r.message.message : __('Failed to sign agricultural act');
|
||||
frappe.msgprint({
|
||||
title: __('Error'),
|
||||
indicator: 'red',
|
||||
message: errorMsg
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function(r) {
|
||||
frappe.msgprint({
|
||||
title: __('Error'),
|
||||
indicator: 'red',
|
||||
message: __('Failed to communicate with E-Taxes service.')
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel/Remove draft act from E-Taxes
|
||||
*/
|
||||
function cancel_act_on_etaxes(frm) {
|
||||
frappe.confirm(
|
||||
__('Cancel this agricultural act on E-Taxes?') + '<br><br>' +
|
||||
__('Purchase Invoice: {0}', [frm.doc.name]) + '<br>' +
|
||||
__('E-Taxes Act ID: {0}', [frm.doc.etaxes_purchase_id]) + '<br>' +
|
||||
__('Status: {0}', [frm.doc.etaxes_purchase_send_status]) + '<br><br>' +
|
||||
__('<strong>Warning:</strong> This will permanently remove the act from E-Taxes system.'),
|
||||
function() {
|
||||
// ✅ CRITICAL: Always wrap E-Taxes API calls with authentication check
|
||||
ETaxes.auth.checkAndProcess(function() {
|
||||
// Show loading
|
||||
frappe.show_alert({
|
||||
message: __('Cancelling act on E-Taxes...'),
|
||||
indicator: 'orange'
|
||||
}, 3);
|
||||
|
||||
frappe.call({
|
||||
method: 'invoice_az.send_purchase_api.cancel_act_on_etaxes',
|
||||
args: {
|
||||
purchase_invoice_name: frm.doc.name
|
||||
},
|
||||
callback: function(r) {
|
||||
if (r.message && r.message.success) {
|
||||
// Success
|
||||
frappe.show_alert({
|
||||
message: __('Act successfully removed from E-Taxes'),
|
||||
indicator: 'green'
|
||||
}, 5);
|
||||
|
||||
// Reload document
|
||||
frm.reload_doc();
|
||||
} else {
|
||||
// Error
|
||||
const errorMsg = r.message ? r.message.message : __('Failed to cancel act on E-Taxes');
|
||||
frappe.msgprint({
|
||||
title: __('Error'),
|
||||
indicator: 'red',
|
||||
message: errorMsg
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function(r) {
|
||||
frappe.msgprint({
|
||||
title: __('Error'),
|
||||
indicator: 'red',
|
||||
message: __('Failed to communicate with E-Taxes service.')
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show Agricultural Act details dialog
|
||||
*/
|
||||
function show_act_details(frm) {
|
||||
const details = `
|
||||
<table class="table table-bordered">
|
||||
<tr>
|
||||
<th style="width: 40%">${__('Purchase Invoice')}</th>
|
||||
<td>${frm.doc.name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>${__('Supplier')}</th>
|
||||
<td>${frm.doc.supplier_name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>${__('Act Kind')}</th>
|
||||
<td>${frm.doc.act_kind || '-'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>${__('E-Taxes Act ID')}</th>
|
||||
<td>${frm.doc.etaxes_purchase_id || '-'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>${__('Serial Number')}</th>
|
||||
<td><strong>${frm.doc.etaxes_purchase_serial_number || '-'}</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>${__('Verification Code')}</th>
|
||||
<td><strong>${frm.doc.etaxes_purchase_verification_code || '-'}</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>${__('Status')}</th>
|
||||
<td><span class="indicator ${get_status_color(frm.doc.etaxes_purchase_send_status)}">${frm.doc.etaxes_purchase_send_status || '-'}</span></td>
|
||||
</tr>
|
||||
</table>
|
||||
`;
|
||||
|
||||
frappe.msgprint({
|
||||
title: __('E-Taxes Agricultural Act Details'),
|
||||
message: details,
|
||||
wide: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status indicator color
|
||||
*/
|
||||
function get_status_color(status) {
|
||||
const colors = {
|
||||
'Sent and Signed': 'green',
|
||||
'Created, not signed': 'orange',
|
||||
'Failed': 'red',
|
||||
'Not Sent': 'gray'
|
||||
};
|
||||
return colors[status] || 'gray';
|
||||
}
|
||||
|
||||
frappe.listview_settings['Purchase Invoice'] = {
|
||||
onload: function(listview) {
|
||||
// Add field as column (proper way)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,681 @@
|
|||
// ======= ОБЩИЕ УТИЛИТЫ И КОНСТАНТЫ =======
|
||||
const ETaxes = {
|
||||
// Константы
|
||||
PROGRESS_UPDATE_DELAY: 50,
|
||||
AUTH_POLL_INTERVAL: 6000,
|
||||
AUTH_MAX_ATTEMPTS: 20,
|
||||
|
||||
// Глобальные переменные
|
||||
loadingDialog: null,
|
||||
cancelLoading: false,
|
||||
loadingErrors: [],
|
||||
|
||||
// Кэш
|
||||
cache: {
|
||||
defaultLogin: null,
|
||||
cacheTime: null,
|
||||
cacheDuration: 300000 // 5 минут
|
||||
}
|
||||
};
|
||||
|
||||
// ======= БАЗОВЫЕ УТИЛИТЫ =======
|
||||
ETaxes.utils = {
|
||||
// Кэшированное получение настроек входа
|
||||
getDefaultLogin: function(callback, useCache = true) {
|
||||
const now = Date.now();
|
||||
|
||||
if (useCache && ETaxes.cache.defaultLogin && ETaxes.cache.cacheTime &&
|
||||
(now - ETaxes.cache.cacheTime) < ETaxes.cache.cacheDuration) {
|
||||
callback(ETaxes.cache.defaultLogin);
|
||||
return;
|
||||
}
|
||||
|
||||
frappe.call({
|
||||
method: 'invoice_az.auth.get_default_asan_login',
|
||||
callback: function(r) {
|
||||
if (r.message) {
|
||||
ETaxes.cache.defaultLogin = r.message;
|
||||
ETaxes.cache.cacheTime = now;
|
||||
}
|
||||
callback(r.message);
|
||||
},
|
||||
error: function() {
|
||||
callback({found: false, error: 'Network error'});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Проверка валидности токена
|
||||
checkTokenValidity: function(callback) {
|
||||
frappe.call({
|
||||
method: 'invoice_az.auth.check_token_validity',
|
||||
callback: function(r) {
|
||||
callback(r.message && r.message.valid);
|
||||
},
|
||||
error: function() {
|
||||
callback(false);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Форматирование валюты
|
||||
formatCurrency: function(amount) {
|
||||
return new Intl.NumberFormat('az-AZ', {
|
||||
style: 'currency',
|
||||
currency: 'AZN',
|
||||
minimumFractionDigits: 2
|
||||
}).format(amount || 0);
|
||||
},
|
||||
|
||||
// Очистка кэша
|
||||
clearCache: function() {
|
||||
ETaxes.cache.defaultLogin = null;
|
||||
ETaxes.cache.cacheTime = null;
|
||||
}
|
||||
};
|
||||
|
||||
// ======= ДИАЛОГИ ЗАГРУЗКИ =======
|
||||
ETaxes.dialogs = {
|
||||
// Показать диалог загрузки
|
||||
showLoading: function(title, message, submessage) {
|
||||
if (ETaxes.loadingDialog) {
|
||||
this.updateLoading(title, message, submessage);
|
||||
return;
|
||||
}
|
||||
|
||||
ETaxes.loadingDialog = new frappe.ui.Dialog({
|
||||
title: title,
|
||||
fields: [{
|
||||
fieldname: 'loading_html',
|
||||
fieldtype: 'HTML',
|
||||
options: this._getLoadingHTML(title, message, submessage)
|
||||
}],
|
||||
primary_action_label: __('Cancel'),
|
||||
primary_action: function() {
|
||||
ETaxes.cancelLoading = true;
|
||||
ETaxes.loadingDialog.$wrapper.find('.primary-action').prop('disabled', true);
|
||||
ETaxes.loadingDialog.$wrapper.find('.primary-action').html(__('Cancelling...'));
|
||||
$('#status_message p').text(__('Cancelling the operation.'));
|
||||
}
|
||||
});
|
||||
|
||||
ETaxes.cancelLoading = false;
|
||||
ETaxes.loadingDialog.show();
|
||||
ETaxes.loadingDialog.$wrapper.find('.modal-dialog').css('max-width', '450px');
|
||||
},
|
||||
|
||||
// Обновить сообщение в диалоге
|
||||
updateLoading: function(title, message, submessage) {
|
||||
if (!ETaxes.loadingDialog) return;
|
||||
|
||||
if (title) $('#loading_title').text(title);
|
||||
if (message) $('#loading_message').text(message);
|
||||
if (submessage !== undefined) {
|
||||
$('#loading_submessage').text(submessage);
|
||||
$('#loading_submessage').toggle(!!submessage);
|
||||
}
|
||||
},
|
||||
|
||||
// Показать код верификации
|
||||
showVerificationCode: function(code) {
|
||||
if (ETaxes.loadingDialog && code) {
|
||||
$('#verification_code').text(code);
|
||||
$('#verification_code_container').show();
|
||||
}
|
||||
},
|
||||
|
||||
// Установить статус успеха
|
||||
setSuccess: function(message, submessage, callback, delay = 2000) {
|
||||
if (!ETaxes.loadingDialog) {
|
||||
if (callback) callback();
|
||||
return;
|
||||
}
|
||||
|
||||
ETaxes.loadingDialog.$wrapper.find('.loading-animation').html(`
|
||||
<div style="width: 64px; height: 64px; display: flex; align-items: center; justify-content: center;">
|
||||
<i class="fa fa-check" style="font-size: 48px; color: #5cb85c;"></i>
|
||||
</div>
|
||||
`);
|
||||
|
||||
$('#loading_message').text(message);
|
||||
if (submessage !== undefined) {
|
||||
$('#loading_submessage').text(submessage);
|
||||
$('#loading_submessage').toggle(!!submessage);
|
||||
}
|
||||
|
||||
$('#verification_code_container').hide();
|
||||
ETaxes.loadingDialog.set_primary_action(null);
|
||||
ETaxes.loadingDialog.set_secondary_action(null);
|
||||
|
||||
setTimeout(() => {
|
||||
this.hide();
|
||||
if (callback) callback();
|
||||
}, delay);
|
||||
},
|
||||
|
||||
// Установить статус ошибки
|
||||
setError: function(message, submessage, showCloseButton = true) {
|
||||
if (!ETaxes.loadingDialog) return;
|
||||
|
||||
// Update dialog title
|
||||
ETaxes.loadingDialog.set_title(__('Error'));
|
||||
|
||||
// Change animation to error icon
|
||||
ETaxes.loadingDialog.$wrapper.find('.loading-animation').html(`
|
||||
<div style="width: 64px; height: 64px; display: flex; align-items: center; justify-content: center;">
|
||||
<i class="fa fa-times" style="font-size: 48px; color: #d9534f;"></i>
|
||||
</div>
|
||||
`);
|
||||
|
||||
// Clear all existing text elements
|
||||
ETaxes.loadingDialog.$wrapper.find('#loading_title').text('');
|
||||
ETaxes.loadingDialog.$wrapper.find('#loading_submessage').remove();
|
||||
|
||||
// Set error message
|
||||
const $msgElement = ETaxes.loadingDialog.$wrapper.find('#loading_message');
|
||||
if ($msgElement.length) {
|
||||
$msgElement.css({
|
||||
'color': '#d9534f',
|
||||
'font-weight': '500',
|
||||
'font-size': '14px',
|
||||
'margin-top': '15px'
|
||||
});
|
||||
|
||||
if (submessage) {
|
||||
$msgElement.html(submessage.replace(/\n/g, '<br>'));
|
||||
} else {
|
||||
$msgElement.text(message);
|
||||
}
|
||||
}
|
||||
|
||||
$('#verification_code_container').hide();
|
||||
|
||||
if (showCloseButton) {
|
||||
ETaxes.loadingDialog.set_primary_action(__('Close'), () => this.hide());
|
||||
}
|
||||
},
|
||||
|
||||
// Скрыть диалог
|
||||
hide: function() {
|
||||
if (ETaxes.loadingDialog) {
|
||||
try {
|
||||
ETaxes.loadingDialog.hide();
|
||||
} catch (e) {
|
||||
console.error("Error hiding loading dialog:", e);
|
||||
}
|
||||
ETaxes.loadingDialog = null;
|
||||
}
|
||||
ETaxes.cancelLoading = false;
|
||||
},
|
||||
|
||||
// Получить HTML для диалога загрузки
|
||||
_getLoadingHTML: function(title, message, submessage) {
|
||||
return `
|
||||
<div class="text-center">
|
||||
<div class="loading-animation" 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 id="loading_title">${title || __('Please wait...')}</h4>
|
||||
<p id="loading_message">${message || __('The operation may take some time')}</p>
|
||||
<p id="loading_submessage" style="${submessage ? '' : 'display:none'}">${submessage || ''}</p>
|
||||
</div>
|
||||
<div id="verification_code_container" style="display:none; margin-top: 15px;">
|
||||
<div style="background: #f8f9fa; padding: 10px; display: inline-block; border-radius: 5px; font-size: 24px; font-weight: bold; letter-spacing: 2px; color: #333; border: 1px solid #ddd;">
|
||||
<span id="verification_code">----</span>
|
||||
</div>
|
||||
<p style="margin-top: 5px; font-size: 12px; color: #666;">Verification Code</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
};
|
||||
|
||||
// ======= МОДУЛЬ АУТЕНТИФИКАЦИИ =======
|
||||
ETaxes.auth = {
|
||||
// Проверить токен и обработать аутентификацию при необходимости
|
||||
checkAndProcess: function(callback) {
|
||||
ETaxes.utils.checkTokenValidity(function(isValid) {
|
||||
if (isValid) {
|
||||
callback();
|
||||
} else {
|
||||
frappe.confirm(
|
||||
__('Your E-Taxes session has expired or authentication is required. Would you like to authenticate now?'),
|
||||
function() {
|
||||
ETaxes.auth.startProcess(callback);
|
||||
},
|
||||
function() {
|
||||
frappe.show_alert({
|
||||
message: __('Authentication canceled. Operation cannot be completed.'),
|
||||
indicator: 'red'
|
||||
}, 5);
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Начать процесс аутентификации
|
||||
startProcess: function(callback) {
|
||||
ETaxes.dialogs.showLoading(
|
||||
__('Starting Authentication'),
|
||||
__('Getting Asan Login settings...'),
|
||||
__('Please wait')
|
||||
);
|
||||
|
||||
ETaxes.utils.getDefaultLogin(function(loginData) {
|
||||
if (loginData && loginData.found) {
|
||||
const asanLoginName = loginData.name;
|
||||
ETaxes.dialogs.updateLoading(
|
||||
null,
|
||||
__('Sending authentication request...'),
|
||||
__('This will send a request to your Asan Imza mobile app')
|
||||
);
|
||||
|
||||
frappe.call({
|
||||
method: 'invoice_az.auth.handle_authentication',
|
||||
args: { 'asan_login_name': asanLoginName },
|
||||
callback: function(r) {
|
||||
if (r.message && r.message.success) {
|
||||
const bearerToken = r.message.bearer_token;
|
||||
|
||||
if (r.message.verification_code) {
|
||||
ETaxes.dialogs.showVerificationCode(r.message.verification_code);
|
||||
}
|
||||
|
||||
ETaxes.dialogs.updateLoading(
|
||||
null,
|
||||
__('Waiting for confirmation on your phone'),
|
||||
__('Please check your phone and confirm the authentication request')
|
||||
);
|
||||
|
||||
ETaxes.auth.pollStatus(asanLoginName, bearerToken, function(success) {
|
||||
if (success) {
|
||||
ETaxes.auth.complete(asanLoginName, callback);
|
||||
} else {
|
||||
ETaxes.dialogs.setError(
|
||||
__('Authentication Failed'),
|
||||
__('Could not authenticate with Asan Imza')
|
||||
);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ETaxes.dialogs.setError(
|
||||
__('Authentication Error'),
|
||||
r.message ? r.message.message : __('Authentication request failed')
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ETaxes.dialogs.setError(
|
||||
__('No Asan Login Settings'),
|
||||
__('Please configure Asan Login settings first')
|
||||
);
|
||||
}
|
||||
}, false); // Не использовать кэш для аутентификации
|
||||
},
|
||||
|
||||
// Опрос статуса аутентификации
|
||||
pollStatus: function(asanLoginName, bearerToken, callback) {
|
||||
let attempts = 0;
|
||||
let stopPolling = false;
|
||||
|
||||
function pollStatusStep() {
|
||||
if (attempts >= ETaxes.AUTH_MAX_ATTEMPTS || stopPolling) {
|
||||
if (attempts >= ETaxes.AUTH_MAX_ATTEMPTS) {
|
||||
ETaxes.dialogs.setError(
|
||||
__('Authentication Timeout'),
|
||||
__('The authentication request has timed out. Please try again.')
|
||||
);
|
||||
callback(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
attempts++;
|
||||
|
||||
ETaxes.dialogs.updateLoading(
|
||||
null,
|
||||
__('Waiting for confirmation on your phone'),
|
||||
__('Please check your phone and confirm the authentication request') +
|
||||
' (' + attempts + '/' + ETaxes.AUTH_MAX_ATTEMPTS + ')'
|
||||
);
|
||||
|
||||
frappe.call({
|
||||
method: 'invoice_az.auth.poll_auth_status',
|
||||
args: {
|
||||
'asan_login_name': asanLoginName,
|
||||
'bearer_token': bearerToken
|
||||
},
|
||||
callback: function(r) {
|
||||
if (r.message && r.message.success) {
|
||||
if (r.message.authenticated) {
|
||||
stopPolling = true;
|
||||
ETaxes.dialogs.setSuccess(
|
||||
__('Authentication Successful'),
|
||||
__('You are now authenticated with Asan Imza'),
|
||||
function() {
|
||||
callback(true);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
setTimeout(pollStatusStep, ETaxes.AUTH_POLL_INTERVAL);
|
||||
}
|
||||
} else {
|
||||
stopPolling = true;
|
||||
ETaxes.dialogs.setError(
|
||||
__('Authentication Error'),
|
||||
r.message ? r.message.message : __('An unknown error occurred')
|
||||
);
|
||||
callback(false);
|
||||
}
|
||||
},
|
||||
error: function(err) {
|
||||
console.error('Error during status check:', err);
|
||||
setTimeout(pollStatusStep, ETaxes.AUTH_POLL_INTERVAL);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pollStatusStep();
|
||||
},
|
||||
|
||||
// Завершение аутентификации
|
||||
complete: function(asanLoginName, callback) {
|
||||
ETaxes.dialogs.showLoading(
|
||||
__('Completing Authentication'),
|
||||
__('Getting certificates...'),
|
||||
__('Please wait')
|
||||
);
|
||||
|
||||
frappe.call({
|
||||
method: 'invoice_az.auth.get_auth_certificates',
|
||||
args: { 'asan_login_name': asanLoginName },
|
||||
callback: function(certR) {
|
||||
if (certR.message && certR.message.success) {
|
||||
ETaxes.dialogs.updateLoading(
|
||||
null,
|
||||
__('Certificates retrieved'),
|
||||
__('Selecting certificate and taxpayer')
|
||||
);
|
||||
|
||||
frappe.call({
|
||||
method: 'frappe.client.get',
|
||||
args: {
|
||||
doctype: 'Asan Login',
|
||||
name: asanLoginName
|
||||
},
|
||||
callback: function(r) {
|
||||
if (r.message && r.message.selected_certificate_json) {
|
||||
try {
|
||||
const certData = JSON.parse(r.message.selected_certificate_json);
|
||||
const certName = r.message.selected_certificate;
|
||||
|
||||
ETaxes.dialogs.updateLoading(
|
||||
null,
|
||||
__('Selecting certificate'),
|
||||
certName
|
||||
);
|
||||
|
||||
ETaxes.auth._selectCertificate(asanLoginName, certData, certName, callback);
|
||||
} catch (e) {
|
||||
console.error('Error parsing certificate:', e);
|
||||
ETaxes.dialogs.hide();
|
||||
ETaxes.auth._showCertificateSelector(certR.message.certificates, asanLoginName, callback);
|
||||
}
|
||||
} else {
|
||||
ETaxes.dialogs.hide();
|
||||
ETaxes.auth._showCertificateSelector(certR.message.certificates, asanLoginName, callback);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ETaxes.dialogs.setError(
|
||||
__('Error'),
|
||||
certR.message ? certR.message.message : __('Failed to get certificates')
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Выбор сертификата
|
||||
_selectCertificate: function(asanLoginName, certData, certName, callback) {
|
||||
frappe.call({
|
||||
method: 'invoice_az.auth.select_certificate',
|
||||
args: {
|
||||
'asan_login_name': asanLoginName,
|
||||
'certificate_data': certData,
|
||||
'certificate_name': certName
|
||||
},
|
||||
callback: function(r) {
|
||||
if (r.message && r.message.success) {
|
||||
ETaxes.dialogs.updateLoading(
|
||||
null,
|
||||
__('Selecting taxpayer'),
|
||||
__('Using certificate: ') + certName
|
||||
);
|
||||
|
||||
frappe.call({
|
||||
method: 'invoice_az.auth.select_taxpayer',
|
||||
args: { 'asan_login_name': asanLoginName },
|
||||
callback: function(r) {
|
||||
if (r.message && r.message.success) {
|
||||
ETaxes.utils.clearCache(); // Очищаем кэш после успешной аутентификации
|
||||
ETaxes.dialogs.setSuccess(
|
||||
__('Authentication Complete'),
|
||||
__('You can now access E-Taxes services'),
|
||||
callback
|
||||
);
|
||||
} else {
|
||||
ETaxes.dialogs.setError(
|
||||
__('Error'),
|
||||
r.message ? r.message.message : __('Failed to select taxpayer')
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
ETaxes.dialogs.setError(
|
||||
__('Error'),
|
||||
r.message ? r.message.message : __('Failed to select certificate')
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Показать селектор сертификатов
|
||||
_showCertificateSelector: function(certificates, asanLoginName, callback) {
|
||||
if (!certificates || !certificates.length) {
|
||||
frappe.msgprint(__('No certificates available'));
|
||||
return;
|
||||
}
|
||||
|
||||
let certHtml = '<div style="max-height: 400px; overflow-y: auto;"><table class="table table-bordered">';
|
||||
certHtml += '<thead><tr><th>Type</th><th>Name</th><th>ID</th><th>Position</th><th>Status</th><th></th></tr></thead><tbody>';
|
||||
|
||||
certificates.forEach(function(cert, index) {
|
||||
let name = '';
|
||||
let 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 || '';
|
||||
}
|
||||
|
||||
certHtml += '<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>';
|
||||
});
|
||||
|
||||
certHtml += '</tbody></table></div>';
|
||||
|
||||
const dialog = new frappe.ui.Dialog({
|
||||
title: __('Select Certificate'),
|
||||
fields: [{
|
||||
fieldtype: 'HTML',
|
||||
fieldname: 'certificates',
|
||||
options: certHtml
|
||||
}]
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
|
||||
dialog.$wrapper.find('.select-cert').on('click', function() {
|
||||
const index = $(this).data('index');
|
||||
const cert = certificates[index];
|
||||
|
||||
let certName = '';
|
||||
if (cert.taxpayerType === 'individual' && cert.individualInfo) {
|
||||
certName = `${cert.individualInfo.name} (${cert.individualInfo.fin})`;
|
||||
} else if (cert.legalInfo) {
|
||||
certName = `${cert.legalInfo.name} (${cert.legalInfo.voen || cert.legalInfo.tin})`;
|
||||
} else {
|
||||
certName = `Certificate ${index + 1}`;
|
||||
}
|
||||
|
||||
dialog.hide();
|
||||
|
||||
ETaxes.dialogs.showLoading(
|
||||
__('Selecting Certificate'),
|
||||
__('Processing your selection...'),
|
||||
certName
|
||||
);
|
||||
|
||||
ETaxes.auth._selectCertificate(asanLoginName, cert, certName, callback);
|
||||
});
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Supplier Form Configuration
|
||||
* E-Taxes integration for Individual suppliers
|
||||
*/
|
||||
|
||||
frappe.ui.form.on('Supplier', {
|
||||
refresh: function(frm) {
|
||||
// Add event handler for fetch button
|
||||
if (frm.doc.supplier_type === 'Individual' && frm.doc.fin && frm.doc.passport_serial_number) {
|
||||
setup_fetch_button_handler(frm);
|
||||
}
|
||||
},
|
||||
|
||||
// Re-setup button when supplier type changes
|
||||
supplier_type: function(frm) {
|
||||
if (frm.doc.supplier_type === 'Individual' && frm.doc.fin && frm.doc.passport_serial_number) {
|
||||
setup_fetch_button_handler(frm);
|
||||
}
|
||||
},
|
||||
|
||||
// Re-setup button when FIN changes
|
||||
fin: function(frm) {
|
||||
if (frm.doc.supplier_type === 'Individual' && frm.doc.fin && frm.doc.passport_serial_number) {
|
||||
setup_fetch_button_handler(frm);
|
||||
}
|
||||
},
|
||||
|
||||
// Re-setup button when Passport changes
|
||||
passport_serial_number: function(frm) {
|
||||
if (frm.doc.supplier_type === 'Individual' && frm.doc.fin && frm.doc.passport_serial_number) {
|
||||
setup_fetch_button_handler(frm);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Setup click handler for fetch button inside the section
|
||||
*/
|
||||
function setup_fetch_button_handler(frm) {
|
||||
// Wait for DOM to be ready
|
||||
setTimeout(function() {
|
||||
$('#fetch-from-etaxes-btn').off('click').on('click', function() {
|
||||
fetch_supplier_data_from_etaxes(frm);
|
||||
});
|
||||
}, 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and auto-fill supplier data from E-Taxes
|
||||
*/
|
||||
function fetch_supplier_data_from_etaxes(frm) {
|
||||
// Check authentication first (same pattern as Sales Invoice)
|
||||
ETaxes.auth.checkAndProcess(function() {
|
||||
execute_fetch_supplier_data(frm);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the actual fetch operation
|
||||
*/
|
||||
function execute_fetch_supplier_data(frm) {
|
||||
// Show confirmation dialog
|
||||
frappe.confirm(
|
||||
__('Fetch supplier data from E-Taxes?') + '<br><br>' +
|
||||
__('FIN: {0}', [frm.doc.fin]) + '<br>' +
|
||||
__('Passport: {0}', [frm.doc.passport_serial_number]) + '<br><br>' +
|
||||
__('This will automatically fill the following fields if they are empty:') + '<br>' +
|
||||
__('- Date of Birth') + '<br>' +
|
||||
__('- First Name') + '<br>' +
|
||||
__('- Last Name') + '<br>' +
|
||||
__('- Phone Number') + '<br>' +
|
||||
__('- Supplier Name (Full Name)'),
|
||||
function() {
|
||||
// Show loading
|
||||
frappe.show_alert({
|
||||
message: __('Fetching data from E-Taxes...'),
|
||||
indicator: 'blue'
|
||||
}, 5);
|
||||
|
||||
// Make API call
|
||||
frappe.call({
|
||||
method: 'invoice_az.supplier_api.update_supplier_from_etaxes',
|
||||
args: {
|
||||
supplier_name: frm.doc.name
|
||||
},
|
||||
callback: function(r) {
|
||||
if (r.message && r.message.success) {
|
||||
// Success
|
||||
frappe.show_alert({
|
||||
message: __('Supplier data fetched successfully!') + '<br>' +
|
||||
r.message.message,
|
||||
indicator: 'green'
|
||||
}, 7);
|
||||
|
||||
// Reload document to show updated fields
|
||||
frm.reload_doc();
|
||||
} else {
|
||||
// Error
|
||||
const errorMsg = r.message ? r.message.message : __('Failed to fetch supplier data from E-Taxes');
|
||||
frappe.msgprint({
|
||||
title: __('Error'),
|
||||
indicator: 'red',
|
||||
message: errorMsg
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function(r) {
|
||||
frappe.msgprint({
|
||||
title: __('Error'),
|
||||
indicator: 'red',
|
||||
message: __('Failed to communicate with E-Taxes service.')
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
@ -10,7 +10,8 @@ doctype_js = {
|
|||
"Purchase Invoice": "client/purchase_invoice.js",
|
||||
"Sales Order": "client/sales_order.js",
|
||||
"Sales Invoice": "client/sales_invoice.js",
|
||||
"Journal Entry": "client/journal_entry.js"
|
||||
"Journal Entry": "client/journal_entry.js",
|
||||
"Supplier": "client/supplier.js"
|
||||
}
|
||||
|
||||
doctype_list_js = {
|
||||
|
|
@ -32,6 +33,10 @@ doc_events = {
|
|||
"on_trash": "invoice_az.api.on_delete_purchase_order",
|
||||
"on_cancel": "invoice_az.api.on_delete_purchase_order"
|
||||
},
|
||||
"Purchase Invoice": {
|
||||
"on_trash": "invoice_az.send_purchase_api.on_delete_purchase_invoice",
|
||||
"on_cancel": "invoice_az.send_purchase_api.on_delete_purchase_invoice"
|
||||
},
|
||||
"Sales Order": {
|
||||
"on_trash": "invoice_az.sales_api.on_delete_sales_order",
|
||||
"on_cancel": "invoice_az.sales_api.on_delete_sales_order"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,129 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 0,
|
||||
"autoname": "format:ETPO-{####}",
|
||||
"creation": "2026-01-27 00:00:00.000000",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"purchase_invoice",
|
||||
"send_date",
|
||||
"column_break_1",
|
||||
"status",
|
||||
"etaxes_section",
|
||||
"etaxes_act_id",
|
||||
"serial_number",
|
||||
"column_break_2",
|
||||
"verification_code",
|
||||
"error_section",
|
||||
"error_message"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "purchase_invoice",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Purchase Invoice",
|
||||
"options": "Purchase Invoice",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "etaxes_act_id",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "E-Taxes Act ID",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "serial_number",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Serial Number",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "verification_code",
|
||||
"fieldtype": "Data",
|
||||
"label": "Verification Code",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "Now",
|
||||
"fieldname": "send_date",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Send Date",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Status",
|
||||
"options": "\nSent and Signed\nCreated, not signed\nFailed",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "error_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Error Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "error_message",
|
||||
"fieldtype": "Text",
|
||||
"label": "Error Message",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_1",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "etaxes_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "E-Taxes Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_2",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-27 00:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Invoice Az",
|
||||
"name": "E-Taxes Purchase Outbox",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# Copyright (c) 2026, Jey ERP and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class ETaxesPurchaseOutbox(Document):
|
||||
pass
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -68,7 +68,6 @@ def send_sales_invoice_to_etaxes(sales_invoice_name):
|
|||
token = asan_login.get("main_token")
|
||||
|
||||
# Step 1: Generate serial number
|
||||
frappe.logger().info(f"[SEND_SALES] Generating serial number for {sales_invoice_name}")
|
||||
serial_result = generate_serial_number(token)
|
||||
if not serial_result.get("success"):
|
||||
return {
|
||||
|
|
@ -77,14 +76,11 @@ def send_sales_invoice_to_etaxes(sales_invoice_name):
|
|||
}
|
||||
|
||||
serial_number = serial_result.get("serial_number")
|
||||
frappe.logger().info(f"[SEND_SALES] Serial number generated: {serial_number}")
|
||||
|
||||
# Step 2: Build invoice payload
|
||||
frappe.logger().info(f"[SEND_SALES] Building payload for {sales_invoice_name}")
|
||||
payload = build_invoice_payload(doc, serial_number)
|
||||
|
||||
# Step 3: Create invoice on E-Taxes
|
||||
frappe.logger().info(f"[SEND_SALES] Creating invoice on E-Taxes")
|
||||
create_result = create_invoice_on_etaxes(token, payload, serial_number)
|
||||
if not create_result.get("success"):
|
||||
return {
|
||||
|
|
@ -93,15 +89,12 @@ def send_sales_invoice_to_etaxes(sales_invoice_name):
|
|||
}
|
||||
|
||||
invoice_id = create_result.get("invoice_id")
|
||||
frappe.logger().info(f"[SEND_SALES] Invoice created with ID: {invoice_id}")
|
||||
|
||||
# Step 4: Set status as draft (signing will be done separately via "Sign Document" button)
|
||||
verification_code = None
|
||||
final_status = "Created, not signed"
|
||||
frappe.logger().info(f"[SEND_SALES] Draft invoice created successfully. User will need to sign separately.")
|
||||
|
||||
# Step 5: Create tracking record
|
||||
frappe.logger().info(f"[SEND_SALES] Creating E-Taxes Sales Outbox record")
|
||||
create_outbox_record(
|
||||
sales_invoice_name,
|
||||
invoice_id,
|
||||
|
|
@ -112,7 +105,6 @@ def send_sales_invoice_to_etaxes(sales_invoice_name):
|
|||
)
|
||||
|
||||
# Step 6: Update Sales Invoice
|
||||
frappe.logger().info(f"[SEND_SALES] Updating Sales Invoice {sales_invoice_name}")
|
||||
|
||||
# Use db.set_value to avoid triggering validation
|
||||
frappe.db.set_value("Sales Invoice", sales_invoice_name, {
|
||||
|
|
@ -398,8 +390,6 @@ def get_customer_objects(sales_invoice_name):
|
|||
"mainObjectOnly": "true"
|
||||
}
|
||||
|
||||
frappe.logger().info(f"[GET_CUSTOMER_OBJECTS] Fetching objects for customer {doc.customer} (TIN: {tax_id})")
|
||||
|
||||
# Make API request
|
||||
response = requests.get(url, headers=headers, params=params, timeout=30)
|
||||
|
||||
|
|
@ -419,7 +409,6 @@ def get_customer_objects(sales_invoice_name):
|
|||
|
||||
# Handle 404 Not Found (customer not found or no objects)
|
||||
if response.status_code == 404:
|
||||
frappe.logger().info(f"[GET_CUSTOMER_OBJECTS] No objects found for TIN: {tax_id}")
|
||||
return {
|
||||
"success": True,
|
||||
"objects": [],
|
||||
|
|
@ -435,13 +424,9 @@ def get_customer_objects(sales_invoice_name):
|
|||
result = response.json()
|
||||
objects = result.get("objects", [])
|
||||
|
||||
frappe.logger().info(f"[GET_CUSTOMER_OBJECTS] Found {len(objects)} objects for TIN: {tax_id}")
|
||||
|
||||
# Filter only active objects (status = "A")
|
||||
active_objects = [obj for obj in objects if obj.get("status") == "A"]
|
||||
|
||||
frappe.logger().info(f"[GET_CUSTOMER_OBJECTS] {len(active_objects)} active objects for TIN: {tax_id}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"objects": active_objects,
|
||||
|
|
@ -580,8 +565,6 @@ def create_invoice_on_etaxes(token, payload, serial_number):
|
|||
headers = DEFAULT_HEADERS.copy()
|
||||
headers["x-authorization"] = f"Bearer {token}"
|
||||
|
||||
frappe.logger().info(f"[CREATE_INVOICE] Sending payload for serial: {serial_number}")
|
||||
|
||||
response = requests.post(
|
||||
INVOICE_URL,
|
||||
data=json.dumps(payload),
|
||||
|
|
@ -601,6 +584,30 @@ def create_invoice_on_etaxes(token, payload, serial_number):
|
|||
"message": "E-Taxes service error. Please try again later."
|
||||
}
|
||||
|
||||
# Handle errors with E-Taxes message format
|
||||
if response.status_code != 200:
|
||||
try:
|
||||
error_data = response.json()
|
||||
# Try to get Azerbaijani message first
|
||||
if isinstance(error_data, dict) and "message" in error_data:
|
||||
if isinstance(error_data["message"], dict) and "az" in error_data["message"]:
|
||||
return {
|
||||
"success": False,
|
||||
"message": error_data["message"]["az"]
|
||||
}
|
||||
elif isinstance(error_data["message"], str):
|
||||
return {
|
||||
"success": False,
|
||||
"message": error_data["message"]
|
||||
}
|
||||
except:
|
||||
pass
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"E-Taxes error (status {response.status_code})"
|
||||
}
|
||||
|
||||
response.raise_for_status()
|
||||
|
||||
result = response.json()
|
||||
|
|
@ -612,8 +619,6 @@ def create_invoice_on_etaxes(token, payload, serial_number):
|
|||
"message": "No invoice ID returned from E-Taxes"
|
||||
}
|
||||
|
||||
frappe.logger().info(f"[CREATE_INVOICE] Created invoice ID: {invoice_id}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"invoice_id": invoice_id
|
||||
|
|
@ -905,8 +910,6 @@ def cancel_invoice_on_etaxes(sales_invoice_name):
|
|||
# Build payload - array with invoice ID (API expects array of IDs)
|
||||
payload = [invoice_id]
|
||||
|
||||
frappe.logger().info(f"[CANCEL_INVOICE] Removing draft invoice {invoice_id} from E-Taxes")
|
||||
|
||||
# Make DELETE request
|
||||
response = requests.delete(
|
||||
REMOVE_DRAFT_URL,
|
||||
|
|
@ -918,8 +921,6 @@ def cancel_invoice_on_etaxes(sales_invoice_name):
|
|||
# Check response
|
||||
if response.status_code == 204:
|
||||
# Success - invoice removed
|
||||
frappe.logger().info(f"[CANCEL_INVOICE] Successfully removed invoice {invoice_id} from E-Taxes")
|
||||
|
||||
# Clear E-Taxes fields in Sales Invoice
|
||||
frappe.db.set_value("Sales Invoice", sales_invoice_name, {
|
||||
"etaxes_invoice_id": None,
|
||||
|
|
@ -953,8 +954,6 @@ def cancel_invoice_on_etaxes(sales_invoice_name):
|
|||
|
||||
elif response.status_code == 404:
|
||||
# Invoice not found on E-Taxes - might be already deleted
|
||||
frappe.logger().info(f"[CANCEL_INVOICE] Invoice {invoice_id} not found on E-Taxes (404)")
|
||||
|
||||
# Clear fields anyway
|
||||
frappe.db.set_value("Sales Invoice", sales_invoice_name, {
|
||||
"etaxes_invoice_id": None,
|
||||
|
|
@ -971,17 +970,18 @@ def cancel_invoice_on_etaxes(sales_invoice_name):
|
|||
}
|
||||
|
||||
else:
|
||||
# Other error
|
||||
# Other error - try to get Azerbaijani message
|
||||
error_msg = f"E-Taxes returned status {response.status_code}"
|
||||
try:
|
||||
error_data = response.json()
|
||||
if error_data.get("message"):
|
||||
error_msg = error_data.get("message")
|
||||
if isinstance(error_data, dict) and "message" in error_data:
|
||||
if isinstance(error_data["message"], dict) and "az" in error_data["message"]:
|
||||
error_msg = error_data["message"]["az"]
|
||||
elif isinstance(error_data["message"], str):
|
||||
error_msg = error_data["message"]
|
||||
except:
|
||||
pass
|
||||
|
||||
frappe.logger().error(f"[CANCEL_INVOICE] Failed to remove invoice: {error_msg}")
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"Failed to remove invoice from E-Taxes: {error_msg}"
|
||||
|
|
@ -1015,11 +1015,8 @@ def on_cancel_sales_invoice(doc, method):
|
|||
try:
|
||||
# Check if invoice was sent to E-Taxes
|
||||
if doc.etaxes_invoice_id and doc.etaxes_send_status in ["Created, not signed", "Sent and Signed"]:
|
||||
# Log that invoice needs to be cancelled on E-Taxes
|
||||
frappe.logger().info(
|
||||
f"Sales Invoice {doc.name} cancelled. E-Taxes Invoice ID: {doc.etaxes_invoice_id}. "
|
||||
f"User should manually cancel on E-Taxes using the 'Cancel on E-Taxes' button."
|
||||
)
|
||||
# User should manually cancel on E-Taxes using the 'Cancel on E-Taxes' button
|
||||
pass
|
||||
except Exception as e:
|
||||
frappe.log_error(
|
||||
f"Error in on_cancel_sales_invoice hook: {str(e)}\n{frappe.get_traceback()}",
|
||||
|
|
@ -1030,8 +1027,6 @@ def on_cancel_sales_invoice(doc, method):
|
|||
def on_delete_sales_invoice(doc, method):
|
||||
"""Deletes related E-Taxes Sales Outbox record when Sales Invoice is deleted or cancelled"""
|
||||
try:
|
||||
frappe.logger().info(f"Checking for E-Taxes Sales Outbox records for Sales Invoice {doc.name}")
|
||||
|
||||
# Find all E-Taxes Sales Outbox records linked to this Sales Invoice
|
||||
outbox_records = frappe.get_all(
|
||||
"E-Taxes Sales Outbox",
|
||||
|
|
@ -1041,14 +1036,9 @@ def on_delete_sales_invoice(doc, method):
|
|||
|
||||
if outbox_records:
|
||||
for record in outbox_records:
|
||||
frappe.logger().info(f"Deleting E-Taxes Sales Outbox {record.name} due to Sales Invoice {doc.name} deletion/cancellation")
|
||||
frappe.delete_doc('E-Taxes Sales Outbox', record.name, ignore_permissions=True, force=True)
|
||||
frappe.logger().info(f"Successfully deleted E-Taxes Sales Outbox {record.name}")
|
||||
|
||||
frappe.db.commit()
|
||||
frappe.logger().info(f"Deleted {len(outbox_records)} E-Taxes Sales Outbox record(s) for Sales Invoice {doc.name}")
|
||||
else:
|
||||
frappe.logger().info(f"No E-Taxes Sales Outbox records found for Sales Invoice {doc.name}")
|
||||
|
||||
except Exception as e:
|
||||
frappe.log_error(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,242 @@
|
|||
import frappe
|
||||
import requests
|
||||
import json
|
||||
from invoice_az.auth import record_etaxes_activity, get_default_asan_login
|
||||
|
||||
# Constants
|
||||
BASE_URL = "https://new.e-taxes.gov.az"
|
||||
FIND_BY_FIN_URL = f"{BASE_URL}/api/po/profile/public/v1/taxpayer/findByFinAndPassport"
|
||||
|
||||
DEFAULT_HEADERS = {
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
"Content-Type": "application/json",
|
||||
"Cache-Control": "no-cache",
|
||||
"Pragma": "no-cache"
|
||||
}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def fetch_supplier_data_from_etaxes(supplier_name):
|
||||
"""
|
||||
Fetch Individual supplier data from E-Taxes by FIN and Passport Serial Number
|
||||
|
||||
Args:
|
||||
supplier_name: Name of Supplier document
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
"success": bool,
|
||||
"data": {
|
||||
"fin": str,
|
||||
"fullName": str,
|
||||
"phone": str,
|
||||
"dateOfBirth": str,
|
||||
"firstname": str,
|
||||
"lastname": str,
|
||||
"patronimyc": str
|
||||
},
|
||||
"message": str
|
||||
}
|
||||
"""
|
||||
try:
|
||||
# Record activity for token renewal
|
||||
record_etaxes_activity()
|
||||
|
||||
# Get Supplier document
|
||||
supplier = frappe.get_doc("Supplier", supplier_name)
|
||||
|
||||
# Validate supplier type
|
||||
if supplier.supplier_type != "Individual":
|
||||
return {
|
||||
"success": False,
|
||||
"message": "This function only works for Individual suppliers"
|
||||
}
|
||||
|
||||
# Check required fields
|
||||
if not supplier.fin:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "FIN is required. Please fill FIN field first."
|
||||
}
|
||||
|
||||
if not supplier.passport_serial_number:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Passport Serial Number is required. Please fill Passport Serial Number field first."
|
||||
}
|
||||
|
||||
# Get authentication token
|
||||
asan_login = get_default_asan_login()
|
||||
if not asan_login.get("found") or not asan_login.get("main_token"):
|
||||
return {
|
||||
"success": False,
|
||||
"message": "E-Taxes authentication not found. Please login via ASAN."
|
||||
}
|
||||
|
||||
token = asan_login.get("main_token")
|
||||
|
||||
# Build headers
|
||||
headers = DEFAULT_HEADERS.copy()
|
||||
headers["x-authorization"] = f"Bearer {token}"
|
||||
|
||||
# Build payload
|
||||
payload = {
|
||||
"fin": supplier.fin,
|
||||
"passportSerialNumber": supplier.passport_serial_number
|
||||
}
|
||||
|
||||
# Make API request
|
||||
response = requests.post(
|
||||
FIND_BY_FIN_URL,
|
||||
data=json.dumps(payload),
|
||||
headers=headers,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
# Handle 401 Unauthorized
|
||||
if response.status_code == 401:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Authentication expired. Please login again via ASAN."
|
||||
}
|
||||
|
||||
# Handle 500 Server Error
|
||||
if response.status_code == 500:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "E-Taxes service error. Please try again later."
|
||||
}
|
||||
|
||||
# Handle 404 Not Found (person not found)
|
||||
if response.status_code == 404:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Person not found in E-Taxes system. Please check FIN and Passport Serial Number."
|
||||
}
|
||||
|
||||
# Handle errors with E-Taxes message format
|
||||
if response.status_code != 200:
|
||||
try:
|
||||
error_data = response.json()
|
||||
# Try to get Azerbaijani message first
|
||||
if isinstance(error_data, dict) and "message" in error_data:
|
||||
if isinstance(error_data["message"], dict) and "az" in error_data["message"]:
|
||||
return {
|
||||
"success": False,
|
||||
"message": error_data["message"]["az"]
|
||||
}
|
||||
elif isinstance(error_data["message"], str):
|
||||
return {
|
||||
"success": False,
|
||||
"message": error_data["message"]
|
||||
}
|
||||
except:
|
||||
pass
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"E-Taxes error (status {response.status_code})"
|
||||
}
|
||||
|
||||
response.raise_for_status()
|
||||
|
||||
# Parse response
|
||||
result = response.json()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": result
|
||||
}
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
frappe.log_error(
|
||||
f"Error fetching supplier data from E-Taxes: {str(e)}",
|
||||
"Fetch Supplier Data Error"
|
||||
)
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"Network error: {str(e)}"
|
||||
}
|
||||
except Exception as e:
|
||||
frappe.log_error(
|
||||
f"Unexpected error fetching supplier data: {str(e)}\n{frappe.get_traceback()}",
|
||||
"Fetch Supplier Data Error"
|
||||
)
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"Error: {str(e)}"
|
||||
}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_supplier_from_etaxes(supplier_name):
|
||||
"""
|
||||
Fetch data from E-Taxes and update Supplier fields
|
||||
|
||||
Args:
|
||||
supplier_name: Name of Supplier document
|
||||
|
||||
Returns:
|
||||
dict: Success/error response with update details
|
||||
"""
|
||||
try:
|
||||
# Fetch data from E-Taxes
|
||||
fetch_result = fetch_supplier_data_from_etaxes(supplier_name)
|
||||
|
||||
if not fetch_result.get("success"):
|
||||
return fetch_result
|
||||
|
||||
data = fetch_result.get("data")
|
||||
|
||||
# Get Supplier document
|
||||
supplier = frappe.get_doc("Supplier", supplier_name)
|
||||
|
||||
# Update fields from E-Taxes response
|
||||
updated_fields = []
|
||||
|
||||
# Date of birth
|
||||
if data.get("dateOfBirth") and not supplier.date_of_birth:
|
||||
supplier.date_of_birth = data.get("dateOfBirth")
|
||||
updated_fields.append("Date of Birth")
|
||||
|
||||
# First name
|
||||
if data.get("firstname"):
|
||||
supplier.first_name_individual = data.get("firstname")
|
||||
updated_fields.append("First Name")
|
||||
|
||||
# Last name
|
||||
if data.get("lastname"):
|
||||
supplier.last_name_individual = data.get("lastname")
|
||||
updated_fields.append("Last Name")
|
||||
|
||||
# Phone number (optional)
|
||||
if data.get("phone") and not supplier.phone_number_individual:
|
||||
supplier.phone_number_individual = data.get("phone")
|
||||
updated_fields.append("Phone Number")
|
||||
|
||||
# Update supplier name if needed (use fullName from E-Taxes)
|
||||
if data.get("fullName") and supplier.supplier_name != data.get("fullName"):
|
||||
old_name = supplier.supplier_name
|
||||
supplier.supplier_name = data.get("fullName")
|
||||
updated_fields.append(f"Supplier Name (from '{old_name}' to '{data.get('fullName')}')")
|
||||
|
||||
# Save supplier
|
||||
supplier.save()
|
||||
frappe.db.commit()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Supplier data updated successfully. Updated fields: {', '.join(updated_fields)}",
|
||||
"updated_fields": updated_fields
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
frappe.log_error(
|
||||
f"Error updating supplier from E-Taxes: {str(e)}\n{frappe.get_traceback()}",
|
||||
"Update Supplier Error"
|
||||
)
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"Error: {str(e)}"
|
||||
}
|
||||
Loading…
Reference in New Issue