feat: Company Data Loading + Tax Policy reference lists
Adds a "Data Loading" dialog on Company form that pulls 6 types of taxpayer reference data from E-Taxes into dedicated doctypes, plus in-form summary tables inside the Tax Policy tab. New DocTypes (all with company link, Accounts User read-only, System Manager full CRUD): - E-Taxes Object (taxpayer registered locations) - E-Taxes Cash Register (kassa aparatları with object FK) - E-Taxes POS Terminal (with auto-derived object from registration number) - E-Taxes Bank Account - E-Taxes Obligation Pact (sazişlər / oil fields) - E-Taxes Presented Certificate (with sazis_code FK to Obligation Pact) Backend (invoice_az/company_api.py): - 6 per-doctype loaders + shared _auth_headers / _etaxes_request helpers implementing CLAUDE.md §10 401-retry-once pattern - Cash Register / POS Terminal loaders walk the hasMore pagination - load_company_data_bulk enqueues a background job that runs selected loaders sequentially and streams progress over Socket.IO (company_data_loading_progress / _complete events) - get_company_reference_list exposes first N rows per doctype for the Tax Policy tab tables Frontend (invoice_az/client/company.js): - Full ETaxes auth module inlined (CLAUDE.md §2) so ASAN Imza re-auth works from Company form without relying on other forms' state - "Data Loading" button opens a dialog of checkboxes (DATA_LOADERS declarative array) — one bulk call, one floating show_progress bar - Six reference tables rendered inside Tax Policy tab sections, pattern copied from e-taxes_settings (card-section + Refresh + "View All" only when total > rows) CLAUDE.md: - Rewrote §3 backend template with 401 retry + explicit commit - Added §10–§16: 401 auto-refresh, two response shapes, db.commit() standard, log title convention, frontend unauthorized handling, field naming for reference vs fixture doctypes, Socket.IO background job pattern hooks.py: - Register client/company.js under doctype_js["Company"] .gitignore: - Exclude *.har (may contain Bearer tokens / PII) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
64961e4108
commit
f622a4ce2a
|
|
@ -53,3 +53,6 @@ jspm_packages/
|
|||
|
||||
# Aider AI Chat
|
||||
.aider*
|
||||
|
||||
# HAR captures (may contain Bearer tokens / PII)
|
||||
*.har
|
||||
|
|
|
|||
258
CLAUDE.md
258
CLAUDE.md
|
|
@ -110,7 +110,7 @@ frappe.ui.form.on('My DocType', {
|
|||
```python
|
||||
import frappe
|
||||
import requests
|
||||
from invoice_az.auth import record_etaxes_activity, get_default_asan_login
|
||||
from invoice_az.auth import record_etaxes_activity, get_default_asan_login, DEFAULT_HEADERS
|
||||
|
||||
@frappe.whitelist()
|
||||
def my_etaxes_function(doc_name):
|
||||
|
|
@ -134,7 +134,7 @@ def my_etaxes_function(doc_name):
|
|||
asan_login = get_default_asan_login()
|
||||
if not asan_login.get("found") or not asan_login.get("main_token"):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "unauthorized",
|
||||
"message": "E-Taxes authentication not found. Please login via ASAN."
|
||||
}
|
||||
|
||||
|
|
@ -147,22 +147,29 @@ def my_etaxes_function(doc_name):
|
|||
# Step 6: Make API request
|
||||
response = requests.post(
|
||||
"https://new.e-taxes.gov.az/api/...",
|
||||
data=json.dumps(payload),
|
||||
json=payload,
|
||||
headers=headers,
|
||||
timeout=60
|
||||
)
|
||||
|
||||
# Step 7: Handle common errors
|
||||
# Step 7: 401 — RETRY ONCE with a fresh token from DB, then fail
|
||||
if response.status_code == 401:
|
||||
fresh_login = get_default_asan_login()
|
||||
if fresh_login.get("found") and fresh_login.get("main_token"):
|
||||
headers["x-authorization"] = f"Bearer {fresh_login['main_token']}"
|
||||
response = requests.post(url, json=payload, headers=headers, timeout=60)
|
||||
if response.status_code == 401:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Authentication expired. Please login again."
|
||||
"error": "unauthorized",
|
||||
"message": "Authentication required. Please login again."
|
||||
}
|
||||
|
||||
# Step 8: Other error codes
|
||||
if response.status_code == 500:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "E-Taxes service error. Please try again later."
|
||||
"error": "server_error",
|
||||
"message": "Service temporarily unavailable. Please try again in a few minutes.",
|
||||
"status_code": 500
|
||||
}
|
||||
|
||||
if response.status_code == 404:
|
||||
|
|
@ -173,43 +180,40 @@ def my_etaxes_function(doc_name):
|
|||
|
||||
response.raise_for_status()
|
||||
|
||||
# Step 8: Process response
|
||||
# Step 9: Process response
|
||||
result = response.json()
|
||||
|
||||
# Step 9: Return success
|
||||
# Step 10: Persist any state changes explicitly
|
||||
frappe.db.commit()
|
||||
|
||||
# Step 11: 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)}"
|
||||
}
|
||||
frappe.log_error(f"Network error: {e}", "My Feature Error")
|
||||
return {"success": False, "message": f"Network error: {e}"}
|
||||
except Exception as e:
|
||||
frappe.log_error(
|
||||
f"Unexpected error: {str(e)}\n{frappe.get_traceback()}",
|
||||
"E-Taxes API Error"
|
||||
f"Unexpected error: {e}\n{frappe.get_traceback()}",
|
||||
"My Feature Error"
|
||||
)
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"Error: {str(e)}"
|
||||
}
|
||||
return {"success": False, "message": f"Error: {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
|
||||
1. `@frappe.whitelist()` — always bare, no `methods=[...]`, no `allow_guest`
|
||||
2. `record_etaxes_activity()` — ALWAYS first (resets inactivity timer, prevents token expiry mid-call)
|
||||
3. `get_default_asan_login()` — read token from DB (scheduler may have refreshed it)
|
||||
4. `headers["x-authorization"] = f"Bearer {token}"` — required for every call
|
||||
5. **401 MUST retry once** with a fresh token before giving up — see §10
|
||||
6. Return codes by use case — see §11
|
||||
7. `frappe.db.commit()` after any `frappe.db.set_value` / `.insert()` / `.save()` / `.delete_doc()` — see §12
|
||||
8. Use `ignore_permissions=True` when inserting E-Taxes internal/tracking records
|
||||
9. `frappe.log_error(msg, "Title Case Error")` — see §13 for title convention
|
||||
10. Always try-except around the whole body
|
||||
|
||||
### 4. Constants and URLs
|
||||
|
||||
|
|
@ -350,6 +354,198 @@ bench --site site1 clear-cache
|
|||
|
||||
**When in doubt:** Look at `send_sales_api.py` + `sales_invoice.js` - they contain all correct patterns!
|
||||
|
||||
### 10. 401 Auto-Refresh Pattern (MANDATORY)
|
||||
|
||||
**A bare 401 must never be shown to the user as "Authentication expired".** The scheduler renews the token every 4 minutes in the background, so an in-flight request that hits 401 is frequently fixable by re-reading the token from the DB and retrying once. Only the *second* 401 in a row means genuine re-auth is required.
|
||||
|
||||
**Reference implementation:** `invoice_az/sales_api.py:98-118`, `invoice_az/vat_api.py:~120-150`.
|
||||
|
||||
```python
|
||||
response = requests.get(url, headers=headers, timeout=60)
|
||||
|
||||
# 401 → refresh token from DB and retry ONCE
|
||||
if response.status_code == 401:
|
||||
fresh_login = get_default_asan_login()
|
||||
if fresh_login.get("found") and fresh_login.get("main_token"):
|
||||
headers["x-authorization"] = f"Bearer {fresh_login['main_token']}"
|
||||
response = requests.get(url, headers=headers, timeout=60)
|
||||
if response.status_code == 401:
|
||||
return {
|
||||
"error": "unauthorized",
|
||||
"message": "Authentication required. Please login again."
|
||||
}
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Retry **exactly once** — never loop, never escalate to `renew_token()` from inside a request handler.
|
||||
- On the second 401, return the `error: "unauthorized"` shape so the frontend can trigger re-auth (see §11 and §14).
|
||||
- Never return `{"success": False, "message": "Authentication expired..."}` — the frontend has no way to recognize that as an auth failure vs a business error.
|
||||
|
||||
### 11. Response Shapes (TWO canonical formats)
|
||||
|
||||
The app uses **two** response shapes — pick one per function based on use case. Never mix `success` and `error` in the same response.
|
||||
|
||||
**Format A — `{success, message}`** (user-triggered operations: send invoice, sign act, fetch supplier data):
|
||||
```python
|
||||
return {"success": True, "message": "Act signed", "document_id": "..."}
|
||||
return {"success": False, "message": "Item 3 has no product_group_code"}
|
||||
```
|
||||
|
||||
**Format B — `{error, message, status_code?}`** (data-fetch / lower-level calls where the frontend may want to branch on the error code):
|
||||
```python
|
||||
return {"error": "unauthorized", "message": "Authentication required. Please login again."}
|
||||
return {"error": "server_error", "message": "Service temporarily unavailable.", "status_code": 500}
|
||||
return {"error": "http_error", "message": f"HTTP error {code}: {msg}"}
|
||||
return {"error": "connection_error", "message": "..."}
|
||||
```
|
||||
|
||||
**When to use which:**
|
||||
- Format A: the frontend only needs "did it work, what to say to the user".
|
||||
- Format B: the frontend needs to **branch on the failure mode** (e.g. trigger re-auth on `unauthorized`, show a retry button on `server_error`). Data-loading functions (load objects, load invoices, fetch reference data) should use Format B.
|
||||
|
||||
**You MAY combine on "already authenticated check":** if `get_default_asan_login()` returns no token at all, return `{"error": "unauthorized", ...}` directly — the frontend reacts identically to a fresh 401.
|
||||
|
||||
### 12. `frappe.db.commit()` Standard
|
||||
|
||||
Frappe auto-commits at request end, but the E-Taxes code always commits **explicitly** after any state change so that:
|
||||
- tokens persist immediately (another worker may read them before the request ends),
|
||||
- partial progress in long-running loops isn't lost if the request later errors.
|
||||
|
||||
**Commit after:**
|
||||
- `frappe.db.set_value("Asan Login", ..., "last_activity_time", ...)` — see `auth.py:46, 57, 157, 173`
|
||||
- `doc.insert(ignore_permissions=True)` for E-Taxes tracking records
|
||||
- Any bulk loop that processes >1 record (commit after the loop, not after each iteration)
|
||||
- `frappe.delete_doc(..., force=True)` cleanups
|
||||
|
||||
**Do NOT commit after:** `frappe.get_doc()` reads, pure validations, dry-runs.
|
||||
|
||||
### 13. Logging Title Convention
|
||||
|
||||
`frappe.log_error(message, title)` — the **title** is `Title Case <Feature> Error` (or `Info`, `Success`, `Debug`):
|
||||
|
||||
| Good | Bad |
|
||||
|------|-----|
|
||||
| `"Token Renewal Error"` | `"token_renewal_error"` |
|
||||
| `"Sales Invoice Validation Error"` | `"sales_invoice_validation"` |
|
||||
| `"E-Taxes Object Load"` | `"object_load"` |
|
||||
| `"Sales Order Tax Info"` | `"info"` |
|
||||
| `"Signing Error"` | `"error signing"` |
|
||||
|
||||
Group related entries under the same title so the Error Log is grouped usefully. Put the full traceback in the **message**, not the title.
|
||||
|
||||
### 14. Frontend Auth Handling (MANDATORY pair with §10)
|
||||
|
||||
When a backend call returns Format B `{error: "unauthorized"}` the frontend must detect it and offer re-auth. Pure `{success: false}` branches MUST NOT be used for auth — the frontend has no way to distinguish.
|
||||
|
||||
**Canonical frontend handler** — see `client/e_taxes_customers_list.js:752-765`:
|
||||
|
||||
```javascript
|
||||
frappe.call({
|
||||
method: 'invoice_az.company_api.load_company_objects',
|
||||
args: { company: frm.doc.name },
|
||||
callback: function(r) {
|
||||
const msg = r.message || {};
|
||||
|
||||
if (msg.error === 'unauthorized') {
|
||||
frappe.confirm(
|
||||
__('Your E-Taxes session has expired. Would you like to authenticate now?'),
|
||||
function() {
|
||||
// Retry the same action after re-auth
|
||||
if (typeof ETaxes !== 'undefined' && ETaxes.auth) {
|
||||
ETaxes.auth.checkAndProcess(function() { load_objects(frm); });
|
||||
} else {
|
||||
frappe.msgprint(__('Please authenticate via Asan Login, then try again.'));
|
||||
}
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.error) {
|
||||
frappe.msgprint({ title: __('Error'), message: msg.message, indicator: 'red' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.success) {
|
||||
frappe.show_alert({ message: msg.message, indicator: 'green' }, 7);
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 15. Field Naming & DocType Structure
|
||||
|
||||
**E-Taxes reference doctypes (cached from API):** primary field uses the `etaxes_<entity>_name` pattern:
|
||||
- `E-Taxes Item` → `etaxes_item_name` (unique, primary, autoname: `field:etaxes_item_name`)
|
||||
- `E-Taxes Unit` → `etaxes_unit_name`
|
||||
- `E-Taxes Suppliers` → `etaxes_party_name`
|
||||
- `E-Taxes Customers` → `etaxes_party_name`
|
||||
|
||||
**Fixture-style doctypes (static/reference lookup data, not tied to invoices):** plain names — NO `etaxes_` prefix:
|
||||
- `E-Taxes Item Group` → `code`, `name_az`
|
||||
- `Classification code` → `classification_name`
|
||||
- `E-Taxes Object` → `object_code`, `object_name` (taxpayer-owned, fixture-shaped)
|
||||
|
||||
Rule of thumb: if the entity comes from an **invoice** and needs mapping back to an ERP doctype, use the `etaxes_` prefix. If it's a standalone reference/dictionary, use plain names.
|
||||
|
||||
**Standard field order:**
|
||||
1. Primary identifier (`unique=1`, `reqd=1`, `read_only=1`, `in_list_view=1`)
|
||||
2. Secondary identifier (code / tax_id)
|
||||
3. Status (`Select`, `in_list_view=1`, `in_standard_filter=1`)
|
||||
4. Source tracking (optional `source_invoice`)
|
||||
5. `Section Break` — grouped attribute sections (Activity, Classification, Address...)
|
||||
6. `Section Break` — mapping/link section at the end (link to ERP entity or Company)
|
||||
|
||||
**Standard permissions:**
|
||||
- System Manager: full CRUD + share/export/print/report/email
|
||||
- Accounts User: read-only + export/print/report/email/share (no create/write/delete)
|
||||
|
||||
### 16. Background Jobs with Socket.IO (for bulk imports)
|
||||
|
||||
Since the latest commit, bulk import operations use `frappe.enqueue` + `frappe.publish_realtime` instead of blocking requests. **Use this for any loop that processes more than ~50 records or calls E-Taxes more than ~10 times.** For small synchronous fetches (object list, supplier profile, serial number) stay synchronous.
|
||||
|
||||
**Reference implementation:** `invoice_az/sales_api.py:883-979`.
|
||||
|
||||
```python
|
||||
# Entry point (whitelisted)
|
||||
@frappe.whitelist()
|
||||
def import_bulk_foo(ids):
|
||||
frappe.enqueue(
|
||||
_process_bulk_foo,
|
||||
ids=ids,
|
||||
user=frappe.session.user,
|
||||
queue="default",
|
||||
timeout=1200,
|
||||
)
|
||||
return {"success": True, "enqueued": True, "total": len(ids)}
|
||||
|
||||
# Worker (not whitelisted, underscore-prefixed)
|
||||
def _process_bulk_foo(ids, user):
|
||||
frappe.set_user(user)
|
||||
record_etaxes_activity()
|
||||
|
||||
total = len(ids)
|
||||
for idx, id_ in enumerate(ids):
|
||||
try:
|
||||
process_one(id_)
|
||||
except Exception as e:
|
||||
frappe.log_error(f"Item {id_}: {e}", "Bulk Foo Error")
|
||||
|
||||
frappe.publish_realtime(
|
||||
"bulk_foo_progress",
|
||||
{"current": idx + 1, "total": total},
|
||||
user=user,
|
||||
)
|
||||
|
||||
frappe.publish_realtime(
|
||||
"bulk_foo_complete",
|
||||
{"total": total},
|
||||
user=user,
|
||||
)
|
||||
```
|
||||
|
||||
**Event naming convention:** `{feature}_progress` (periodic), `{feature}_complete` (final). Frontend subscribes via `frappe.realtime.on(event, handler)`.
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Linting and Code Quality
|
||||
|
|
|
|||
|
|
@ -0,0 +1,960 @@
|
|||
// ======= ОБЩИЕ УТИЛИТЫ И КОНСТАНТЫ =======
|
||||
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;
|
||||
|
||||
ETaxes.loadingDialog.set_title(__('Error'));
|
||||
|
||||
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>
|
||||
`);
|
||||
|
||||
ETaxes.loadingDialog.$wrapper.find('#loading_title').text('');
|
||||
ETaxes.loadingDialog.$wrapper.find('#loading_submessage').remove();
|
||||
|
||||
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 =======
|
||||
|
||||
|
||||
// ======= COMPANY FORM: DATA LOADING =======
|
||||
frappe.ui.form.on('Company', {
|
||||
refresh: function(frm) {
|
||||
if (frm.is_new()) return;
|
||||
frm.add_custom_button(__('Data Loading'), function() {
|
||||
show_data_loading_dialog(frm);
|
||||
}, __('E-Taxes'));
|
||||
render_all_reference_sections(frm);
|
||||
}
|
||||
});
|
||||
|
||||
// ======= REFERENCE LIST SECTIONS (Tax Policy tab) =======
|
||||
|
||||
const REFERENCE_SECTIONS = [
|
||||
{
|
||||
doctype: 'E-Taxes Object',
|
||||
html_fieldname: 'objects_list_html',
|
||||
list_route: '/app/e-taxes-object',
|
||||
columns: [
|
||||
{ label: 'Object Code', field: 'object_code', link_to_item: true },
|
||||
{ label: 'Name', field: 'object_name' },
|
||||
{ label: 'Status', field: 'object_status',
|
||||
indicator: { active: 'green', liquidated: 'red' } }
|
||||
]
|
||||
},
|
||||
{
|
||||
doctype: 'E-Taxes Cash Register',
|
||||
html_fieldname: 'cash_registers_list_html',
|
||||
list_route: '/app/e-taxes-cash-register',
|
||||
columns: [
|
||||
{ label: 'Serial Number', field: 'number', link_to_item: true },
|
||||
{ label: 'Model', field: 'model' },
|
||||
{ label: 'E-Cash Status', field: 'ecash_status',
|
||||
indicator: { ACTIVE: 'green', ELIMINATED: 'red' } },
|
||||
{ label: 'Object', field: 'object',
|
||||
link_to: (v) => '/app/e-taxes-object/' + encodeURIComponent(v) },
|
||||
{ label: 'Installed At', field: 'installed_at', format: 'date' }
|
||||
]
|
||||
},
|
||||
{
|
||||
doctype: 'E-Taxes POS Terminal',
|
||||
html_fieldname: 'pos_terminals_list_html',
|
||||
list_route: '/app/e-taxes-pos-terminal',
|
||||
columns: [
|
||||
{ label: 'Registration Number', field: 'registration_number', link_to_item: true },
|
||||
{ label: 'Serial Number', field: 'serial_number' },
|
||||
{ label: 'Status', field: 'status',
|
||||
indicator: { A: 'green', C: 'red' } },
|
||||
{ label: 'Registration Date', field: 'registration_date', format: 'date' }
|
||||
]
|
||||
},
|
||||
{
|
||||
doctype: 'E-Taxes Bank Account',
|
||||
html_fieldname: 'bank_accounts_list_html',
|
||||
list_route: '/app/e-taxes-bank-account',
|
||||
columns: [
|
||||
{ label: 'Account Number', field: 'number', link_to_item: true },
|
||||
{ label: 'Currency', field: 'currency' },
|
||||
{ label: 'Status', field: 'status',
|
||||
indicator: { A: 'green', C: 'red' } },
|
||||
{ label: 'Bank Name', field: 'bank_name' }
|
||||
]
|
||||
},
|
||||
{
|
||||
doctype: 'E-Taxes Obligation Pact',
|
||||
html_fieldname: 'obligation_pacts_list_html',
|
||||
list_route: '/app/e-taxes-obligation-pact',
|
||||
columns: [
|
||||
{ label: 'Code', field: 'code', link_to_item: true },
|
||||
{ label: 'Name', field: 'name_az' },
|
||||
{ label: 'Validity', field: 'validity_date', format: 'date' },
|
||||
{ label: 'Expire', field: 'expire_date', format: 'date' }
|
||||
]
|
||||
},
|
||||
{
|
||||
doctype: 'E-Taxes Presented Certificate',
|
||||
html_fieldname: 'presented_certificates_list_html',
|
||||
list_route: '/app/e-taxes-presented-certificate',
|
||||
columns: [
|
||||
{ label: 'Cert No', field: 'cert_no', link_to_item: true },
|
||||
{ label: 'Taxpayer', field: 'taxpayer_full_name' },
|
||||
{ label: 'Category', field: 'cert_category',
|
||||
indicator: { ACCEPTED: 'green', PRESENTED: 'blue', REJECTED: 'red', PENDING: 'orange' } }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
function render_all_reference_sections(frm) {
|
||||
// Defer to next tick so HTML fields are mounted in DOM.
|
||||
setTimeout(function() {
|
||||
REFERENCE_SECTIONS.forEach(function(section) {
|
||||
load_reference_section(frm, section);
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function section_container(frm, section) {
|
||||
const field = frm.get_field(section.html_fieldname);
|
||||
return (field && field.$wrapper) ? field.$wrapper : null;
|
||||
}
|
||||
|
||||
function load_reference_section(frm, section) {
|
||||
const $c = section_container(frm, section);
|
||||
if (!$c) return;
|
||||
|
||||
frappe.call({
|
||||
method: 'invoice_az.company_api.get_company_reference_list',
|
||||
args: { doctype: section.doctype, company: frm.doc.name, limit: 50 },
|
||||
callback: function(r) {
|
||||
if (r.message && r.message.success) {
|
||||
render_reference_list(frm, section, r.message.data, r.message.total_count);
|
||||
} else {
|
||||
const error_msg = r.message ? r.message.message : 'Unknown error';
|
||||
$c.html(`<div class="form-section card-section">
|
||||
<div class="section-body">
|
||||
<div class="alert alert-warning">
|
||||
<p>Error loading ${frappe.utils.escape_html(section.doctype)}: ${frappe.utils.escape_html(error_msg)}</p>
|
||||
<button class="btn btn-sm btn-default retry-section-btn">Try Again</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`);
|
||||
$c.off('click', '.retry-section-btn').on('click', '.retry-section-btn', function() {
|
||||
load_reference_section(frm, section);
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
$c.html(`<div class="form-section card-section">
|
||||
<div class="section-body">
|
||||
<div class="alert alert-danger">
|
||||
<p>Network error loading ${frappe.utils.escape_html(section.doctype)}: ${frappe.utils.escape_html(error || '')}</p>
|
||||
<button class="btn btn-sm btn-default retry-section-btn">Try Again</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`);
|
||||
$c.off('click', '.retry-section-btn').on('click', '.retry-section-btn', function() {
|
||||
load_reference_section(frm, section);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function render_reference_list(frm, section, rows, total_count) {
|
||||
const $c = section_container(frm, section);
|
||||
if (!$c) return;
|
||||
|
||||
const esc = frappe.utils.escape_html;
|
||||
|
||||
let html = `<div class="frappe-control" style="margin-bottom: 15px;">
|
||||
<div class="clearfix">
|
||||
<div class="pull-left">
|
||||
<h6 class="text-muted">${esc(section.doctype)} (${total_count} total)</h6>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<button class="btn btn-xs btn-default refresh-section-btn">
|
||||
<i class="octicon octicon-sync"></i> Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
if (rows && rows.length > 0) {
|
||||
html += '<div class="form-section card-section">';
|
||||
html += '<div class="section-body">';
|
||||
html += '<table class="table table-bordered">';
|
||||
html += '<thead><tr>';
|
||||
section.columns.forEach(function(col) { html += '<th>' + esc(__(col.label)) + '</th>'; });
|
||||
html += '</tr></thead>';
|
||||
html += '<tbody>';
|
||||
|
||||
rows.forEach(function(row) {
|
||||
html += '<tr>';
|
||||
section.columns.forEach(function(col) {
|
||||
const val = row[col.field];
|
||||
html += '<td>' + render_cell(row, col, val, section) + '</td>';
|
||||
});
|
||||
html += '</tr>';
|
||||
});
|
||||
|
||||
html += '</tbody></table>';
|
||||
html += '</div>'; // section-body
|
||||
html += '</div>'; // form-section
|
||||
|
||||
if (total_count > rows.length) {
|
||||
html += `<div class="text-center" style="margin-top: 10px;">
|
||||
<p class="text-muted small">Showing ${rows.length} of ${total_count}</p>
|
||||
<a href="${section.list_route}" class="btn btn-default btn-xs">View All ${esc(section.doctype)}</a>
|
||||
</div>`;
|
||||
}
|
||||
} else {
|
||||
html += '<div class="form-section card-section">';
|
||||
html += '<div class="section-body">';
|
||||
html += '<div class="empty-state">';
|
||||
html += '<p class="text-muted">No records found. Use "Data Loading" to import from E-Taxes.</p>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
$c.html(html);
|
||||
|
||||
$c.off('click', '.refresh-section-btn').on('click', '.refresh-section-btn', function() {
|
||||
$c.html('<div class="text-center" style="padding: 20px;"><i class="fa fa-spinner fa-spin"></i> Loading...</div>');
|
||||
load_reference_section(frm, section);
|
||||
});
|
||||
}
|
||||
|
||||
function render_cell(row, col, val, section) {
|
||||
const esc = frappe.utils.escape_html;
|
||||
if (val == null || val === '') return '-';
|
||||
|
||||
if (col.indicator) {
|
||||
const color = col.indicator[val] || 'gray';
|
||||
return '<span class="indicator-pill ' + color + '">' + esc(String(val)) + '</span>';
|
||||
}
|
||||
if (col.format === 'date') {
|
||||
return esc(frappe.datetime.str_to_user(val));
|
||||
}
|
||||
if (col.link_to_item) {
|
||||
return '<a href="' + section.list_route + '/' + encodeURIComponent(row.name) + '">' +
|
||||
esc(String(val)) + '</a>';
|
||||
}
|
||||
if (typeof col.link_to === 'function') {
|
||||
return '<a href="' + col.link_to(val) + '">' + esc(String(val)) + '</a>';
|
||||
}
|
||||
return esc(String(val));
|
||||
}
|
||||
|
||||
// Ordered list of data types loadable from the Company → Data Loading dialog.
|
||||
// Adding a new loader = append an entry here.
|
||||
const DATA_LOADERS = [
|
||||
{
|
||||
key: 'load_objects',
|
||||
label: __('Load Objects'),
|
||||
description: __('Load company objects (taxpayer registered locations) from E-Taxes'),
|
||||
default: 1,
|
||||
method: 'invoice_az.company_api.load_company_objects',
|
||||
freezeMessage: __('Loading objects from E-Taxes...')
|
||||
},
|
||||
{
|
||||
key: 'load_cash_registers',
|
||||
label: __('Load Cash Registers'),
|
||||
description: __('Load cash registers (kassa aparatları) from E-Taxes. Uses pagination.'),
|
||||
default: 1,
|
||||
method: 'invoice_az.company_api.load_company_cash_registers',
|
||||
freezeMessage: __('Loading cash registers from E-Taxes...')
|
||||
},
|
||||
{
|
||||
key: 'load_pos_terminals',
|
||||
label: __('Load POS Terminals'),
|
||||
description: __('Load POS terminals from E-Taxes. Uses pagination.'),
|
||||
default: 1,
|
||||
method: 'invoice_az.company_api.load_company_pos_terminals',
|
||||
freezeMessage: __('Loading POS terminals from E-Taxes...')
|
||||
},
|
||||
{
|
||||
key: 'load_bank_accounts',
|
||||
label: __('Load Bank Accounts'),
|
||||
description: __('Load bank accounts (bank hesabları) from E-Taxes'),
|
||||
default: 1,
|
||||
method: 'invoice_az.company_api.load_company_bank_accounts',
|
||||
freezeMessage: __('Loading bank accounts from E-Taxes...')
|
||||
},
|
||||
{
|
||||
key: 'load_obligation_pacts',
|
||||
label: __('Load Obligation Pacts (Sazişlər)'),
|
||||
description: __('Load sub-contractor obligation pacts (oil fields / sazişlər) from E-Taxes'),
|
||||
default: 1,
|
||||
method: 'invoice_az.company_api.load_company_obligation_pacts',
|
||||
freezeMessage: __('Loading obligation pacts from E-Taxes...')
|
||||
},
|
||||
{
|
||||
key: 'load_presented_certificates',
|
||||
label: __('Load Presented Certificates'),
|
||||
description: __('Load presented/accepted e-certificates from E-Taxes. Requires obligation pacts for links to resolve.'),
|
||||
default: 1,
|
||||
method: 'invoice_az.company_api.load_company_presented_certificates',
|
||||
freezeMessage: __('Loading presented certificates from E-Taxes...')
|
||||
}
|
||||
];
|
||||
|
||||
function show_data_loading_dialog(frm) {
|
||||
const fields = [{
|
||||
fieldname: 'data_types_section',
|
||||
fieldtype: 'Section Break',
|
||||
label: __('Data Types to Load')
|
||||
}];
|
||||
|
||||
DATA_LOADERS.forEach(function(loader) {
|
||||
fields.push({
|
||||
fieldname: loader.key,
|
||||
fieldtype: 'Check',
|
||||
label: loader.label,
|
||||
default: loader.default,
|
||||
description: loader.description
|
||||
});
|
||||
});
|
||||
|
||||
const d = new frappe.ui.Dialog({
|
||||
title: __('Load Company Reference Data from E-Taxes'),
|
||||
fields: fields,
|
||||
size: 'small',
|
||||
primary_action_label: __('Load'),
|
||||
primary_action: function() {
|
||||
const values = d.get_values();
|
||||
const selected = DATA_LOADERS.filter(function(l) { return values[l.key]; });
|
||||
if (!selected.length) {
|
||||
frappe.msgprint(__('Please select at least one data type to load.'));
|
||||
return;
|
||||
}
|
||||
d.hide();
|
||||
ETaxes.auth.checkAndProcess(function() {
|
||||
run_data_loading(frm, selected);
|
||||
});
|
||||
}
|
||||
});
|
||||
d.show();
|
||||
}
|
||||
|
||||
const PROGRESS_EVENT = 'company_data_loading_progress';
|
||||
const COMPLETE_EVENT = 'company_data_loading_complete';
|
||||
|
||||
function run_data_loading(frm, loaders) {
|
||||
const keys = loaders.map(function(l) { return l.key; });
|
||||
|
||||
// Handlers are closed over per-run so we can detach cleanly at end
|
||||
let progress_handler = null;
|
||||
let complete_handler = null;
|
||||
|
||||
const detach = function() {
|
||||
if (progress_handler) frappe.realtime.off(PROGRESS_EVENT, progress_handler);
|
||||
if (complete_handler) frappe.realtime.off(COMPLETE_EVENT, complete_handler);
|
||||
progress_handler = null;
|
||||
complete_handler = null;
|
||||
};
|
||||
|
||||
progress_handler = function(data) {
|
||||
if (!data || !data.total) return;
|
||||
const pct = Math.round((data.current / data.total) * 100);
|
||||
const desc = data.message || '';
|
||||
frappe.show_progress(__('Loading E-Taxes Data'), pct, 100, desc);
|
||||
};
|
||||
|
||||
complete_handler = function(data) {
|
||||
detach();
|
||||
frappe.hide_progress();
|
||||
|
||||
if (data && data.error === 'unauthorized') {
|
||||
frappe.confirm(
|
||||
__('Your E-Taxes session has expired. Would you like to authenticate now?'),
|
||||
function() {
|
||||
ETaxes.auth.checkAndProcess(function() {
|
||||
run_data_loading(frm, loaders);
|
||||
});
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = [];
|
||||
(data.summary || []).forEach(function(item) {
|
||||
const res = item.result || {};
|
||||
if (res.success) {
|
||||
lines.push('<b>' + item.label + ':</b> ' + (res.message || __('OK')));
|
||||
} else {
|
||||
lines.push('<b style="color:#d9534f">' + item.label + ':</b> ' +
|
||||
(res.message || __('Unknown error')));
|
||||
}
|
||||
});
|
||||
|
||||
frappe.msgprint({
|
||||
title: __('Data Loading Complete'),
|
||||
message: lines.join('<br>') || __('Nothing was loaded.'),
|
||||
indicator: 'green'
|
||||
});
|
||||
};
|
||||
|
||||
frappe.realtime.on(PROGRESS_EVENT, progress_handler);
|
||||
frappe.realtime.on(COMPLETE_EVENT, complete_handler);
|
||||
|
||||
frappe.show_progress(__('Loading E-Taxes Data'), 0, 100, __('Starting...'));
|
||||
|
||||
frappe.call({
|
||||
method: 'invoice_az.company_api.load_company_data_bulk',
|
||||
args: { company: frm.doc.name, selections: JSON.stringify(keys) },
|
||||
callback: function(r) {
|
||||
const msg = r.message || {};
|
||||
if (!msg.enqueued) {
|
||||
detach();
|
||||
frappe.hide_progress();
|
||||
frappe.msgprint({
|
||||
title: __('Failed to Start'),
|
||||
message: msg.message || __('Could not enqueue the loading job.'),
|
||||
indicator: 'red'
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
detach();
|
||||
frappe.hide_progress();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,762 @@
|
|||
# ======= COMPANY E-TAXES DATA LOADING =======
|
||||
|
||||
import frappe
|
||||
import requests
|
||||
from frappe.utils import getdate
|
||||
|
||||
from invoice_az.auth import DEFAULT_HEADERS, get_default_asan_login, record_etaxes_activity
|
||||
|
||||
BASE_URL = "https://new.e-taxes.gov.az"
|
||||
OBJECT_LIST_URL = f"{BASE_URL}/api/po/profile/public/v1/object/list"
|
||||
CASH_REGISTER_LIST_URL = f"{BASE_URL}/api/po/profile/public/v2/profile/cashRegister/list"
|
||||
POS_TERMINAL_LIST_URL = f"{BASE_URL}/api/po/profile/public/v2/profile/posTerminal/list"
|
||||
BANK_ACCOUNT_LIST_URL = f"{BASE_URL}/api/po/profile/public/v1/profile/bankAccount/list"
|
||||
OBL_PACT_LIST_URL = f"{BASE_URL}/api/po/edi-legal/public/v1/application/sub-contractor/obl-pact-list"
|
||||
PRESENTED_CERT_LIST_URL = f"{BASE_URL}/api/po/edi-legal/public/v1/application/contractor/presented-accepted-e-certificates"
|
||||
|
||||
PAGE_SIZE = 200
|
||||
MAX_PAGES = 50 # safety cap (≤ 10 000 records per import)
|
||||
|
||||
|
||||
# ======= HELPERS =======
|
||||
|
||||
def _get(d, *path):
|
||||
cur = d
|
||||
for key in path:
|
||||
if not isinstance(cur, dict):
|
||||
return None
|
||||
cur = cur.get(key)
|
||||
return cur
|
||||
|
||||
|
||||
def _safe_date(value):
|
||||
if not value:
|
||||
return None
|
||||
try:
|
||||
return getdate(value)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _unauthorized():
|
||||
return {
|
||||
"error": "unauthorized",
|
||||
"message": "Authentication required. Please login again.",
|
||||
}
|
||||
|
||||
|
||||
def _server_error():
|
||||
return {
|
||||
"error": "server_error",
|
||||
"message": "Service temporarily unavailable. Please try again in a few minutes.",
|
||||
"status_code": 500,
|
||||
}
|
||||
|
||||
|
||||
def _auth_headers():
|
||||
"""Return (headers, error_response_or_None). If err is truthy, bail out."""
|
||||
asan_login = get_default_asan_login()
|
||||
if not asan_login.get("found") or not asan_login.get("main_token"):
|
||||
return None, _unauthorized()
|
||||
headers = DEFAULT_HEADERS.copy()
|
||||
headers["x-authorization"] = f"Bearer {asan_login['main_token']}"
|
||||
return headers, None
|
||||
|
||||
|
||||
def _etaxes_request(method, url, headers, **kwargs):
|
||||
"""Make an E-Taxes request with the §10 401-retry-once pattern.
|
||||
|
||||
Returns (response, error_dict) — if error_dict is truthy, caller must return it.
|
||||
"""
|
||||
try:
|
||||
response = requests.request(method, url, headers=headers, timeout=60, **kwargs)
|
||||
|
||||
if response.status_code == 401:
|
||||
fresh_login = get_default_asan_login()
|
||||
if fresh_login.get("found") and fresh_login.get("main_token"):
|
||||
headers["x-authorization"] = f"Bearer {fresh_login['main_token']}"
|
||||
response = requests.request(method, url, headers=headers, timeout=60, **kwargs)
|
||||
if response.status_code == 401:
|
||||
return None, _unauthorized()
|
||||
|
||||
if response.status_code == 500:
|
||||
return None, _server_error()
|
||||
|
||||
if response.status_code == 404:
|
||||
return None, {"success": False, "message": "Resource not found."}
|
||||
|
||||
response.raise_for_status()
|
||||
return response, None
|
||||
|
||||
except requests.exceptions.HTTPError as e:
|
||||
code = e.response.status_code if e.response is not None else None
|
||||
if code == 401:
|
||||
return None, _unauthorized()
|
||||
if code == 500:
|
||||
return None, _server_error()
|
||||
frappe.log_error(f"HTTP error {code}: {e}", "E-Taxes Company Load")
|
||||
return None, {"error": "http_error", "message": f"HTTP error {code}: {e}"}
|
||||
except requests.exceptions.RequestException as e:
|
||||
frappe.log_error(f"Network error: {e}", "E-Taxes Company Load")
|
||||
return None, {"error": "connection_error", "message": f"Network error: {e}"}
|
||||
|
||||
|
||||
# ======= OBJECTS =======
|
||||
|
||||
@frappe.whitelist()
|
||||
def load_company_objects(company):
|
||||
"""Load taxpayer objects (registered locations) from E-Taxes into E-Taxes Object doctype."""
|
||||
try:
|
||||
record_etaxes_activity()
|
||||
|
||||
if not company or not frappe.db.exists("Company", company):
|
||||
return {"success": False, "message": f"Company '{company}' not found."}
|
||||
|
||||
headers, err = _auth_headers()
|
||||
if err:
|
||||
return err
|
||||
|
||||
response, err = _etaxes_request("GET", OBJECT_LIST_URL, headers)
|
||||
if err:
|
||||
return err
|
||||
|
||||
data = response.json() or {}
|
||||
objects = data.get("objectInfoShortList") or []
|
||||
|
||||
created, updated = 0, 0
|
||||
for obj in objects:
|
||||
code = obj.get("objectCode")
|
||||
if not code:
|
||||
continue
|
||||
|
||||
values = {
|
||||
"object_name": (obj.get("objectName") or "").strip(),
|
||||
"object_status": obj.get("status") or "active",
|
||||
"address": obj.get("address"),
|
||||
"main_activity_code": _get(obj, "mainActivity", "code"),
|
||||
"main_activity_name": _get(obj, "mainActivity", "name", "az"),
|
||||
"main_class_code": _get(obj, "mainClass", "code"),
|
||||
"main_class_name": _get(obj, "mainClass", "name", "az"),
|
||||
"middle_class_code": _get(obj, "middleClass", "code"),
|
||||
"middle_class_name": _get(obj, "middleClass", "name", "az"),
|
||||
"sub_class_code": _get(obj, "subClass", "code"),
|
||||
"sub_class_name": _get(obj, "subClass", "name", "az"),
|
||||
"company": company,
|
||||
}
|
||||
|
||||
if frappe.db.exists("E-Taxes Object", code):
|
||||
frappe.db.set_value("E-Taxes Object", code, values, update_modified=False)
|
||||
updated += 1
|
||||
else:
|
||||
doc = frappe.get_doc({
|
||||
"doctype": "E-Taxes Object",
|
||||
"object_code": code,
|
||||
**values,
|
||||
})
|
||||
doc.insert(ignore_permissions=True)
|
||||
created += 1
|
||||
|
||||
frappe.db.commit()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Objects loaded: {created} created, {updated} updated",
|
||||
"created": created,
|
||||
"updated": updated,
|
||||
"total": len(objects),
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
frappe.log_error(
|
||||
f"Unexpected error: {e}\n{frappe.get_traceback()}",
|
||||
"E-Taxes Object Load",
|
||||
)
|
||||
return {"success": False, "message": f"Error: {e}"}
|
||||
|
||||
|
||||
# ======= CASH REGISTERS =======
|
||||
|
||||
@frappe.whitelist()
|
||||
def load_company_cash_registers(company):
|
||||
"""Load cash registers from E-Taxes into E-Taxes Cash Register doctype.
|
||||
|
||||
Uses hasMore pagination (CLAUDE.md §10 401-retry-once applies per page).
|
||||
"""
|
||||
try:
|
||||
record_etaxes_activity()
|
||||
|
||||
if not company or not frappe.db.exists("Company", company):
|
||||
return {"success": False, "message": f"Company '{company}' not found."}
|
||||
|
||||
headers, err = _auth_headers()
|
||||
if err:
|
||||
return err
|
||||
|
||||
created, updated, total_fetched = 0, 0, 0
|
||||
offset = 0
|
||||
|
||||
for page in range(MAX_PAGES):
|
||||
payload = {"maxCount": PAGE_SIZE, "offset": offset}
|
||||
response, err = _etaxes_request(
|
||||
"POST", CASH_REGISTER_LIST_URL, headers, json=payload
|
||||
)
|
||||
if err:
|
||||
return err
|
||||
|
||||
data = response.json() or {}
|
||||
registers = data.get("cashRegisters") or []
|
||||
has_more = bool(data.get("hasMore"))
|
||||
|
||||
if not registers:
|
||||
break
|
||||
|
||||
for reg in registers:
|
||||
token = reg.get("token")
|
||||
if not token:
|
||||
continue
|
||||
|
||||
values = {
|
||||
"number": reg.get("number"),
|
||||
"model": reg.get("model"),
|
||||
"ecash_status": reg.get("ecashStatus"),
|
||||
"status_code": reg.get("status"),
|
||||
"object": reg.get("object") if frappe.db.exists("E-Taxes Object", reg.get("object")) else None,
|
||||
"certificate_type": reg.get("certificateType"),
|
||||
"installed_at": _safe_date(reg.get("installedAt")),
|
||||
"certificate_from": _safe_date(reg.get("certificateFrom")),
|
||||
"certificate_to": _safe_date(reg.get("certificateTo")),
|
||||
"replacement_date": _safe_date(reg.get("replacementDate")),
|
||||
"operator_tin": reg.get("operatorTin"),
|
||||
"operator_name": reg.get("operatorName"),
|
||||
"company": company,
|
||||
}
|
||||
|
||||
if frappe.db.exists("E-Taxes Cash Register", token):
|
||||
frappe.db.set_value(
|
||||
"E-Taxes Cash Register", token, values, update_modified=False
|
||||
)
|
||||
updated += 1
|
||||
else:
|
||||
doc = frappe.get_doc({
|
||||
"doctype": "E-Taxes Cash Register",
|
||||
"token": token,
|
||||
**values,
|
||||
})
|
||||
doc.insert(ignore_permissions=True)
|
||||
created += 1
|
||||
|
||||
total_fetched += len(registers)
|
||||
offset += PAGE_SIZE
|
||||
|
||||
if not has_more:
|
||||
break
|
||||
else:
|
||||
frappe.log_error(
|
||||
f"Cash register pagination hit safety cap of {MAX_PAGES} pages "
|
||||
f"for company {company}",
|
||||
"E-Taxes Cash Register Load",
|
||||
)
|
||||
|
||||
frappe.db.commit()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Cash registers loaded: {created} created, {updated} updated",
|
||||
"created": created,
|
||||
"updated": updated,
|
||||
"total": total_fetched,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
frappe.log_error(
|
||||
f"Unexpected error: {e}\n{frappe.get_traceback()}",
|
||||
"E-Taxes Cash Register Load",
|
||||
)
|
||||
return {"success": False, "message": f"Error: {e}"}
|
||||
|
||||
|
||||
# ======= POS TERMINALS =======
|
||||
|
||||
def _object_from_registration_number(registration_number):
|
||||
"""Registration numbers look like "4800111741-84001P02" — strip the trailing Pxx."""
|
||||
if not registration_number:
|
||||
return None
|
||||
code = registration_number.rsplit("P", 1)[0]
|
||||
if frappe.db.exists("E-Taxes Object", code):
|
||||
return code
|
||||
return None
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def load_company_pos_terminals(company):
|
||||
"""Load POS terminals from E-Taxes into E-Taxes POS Terminal doctype.
|
||||
|
||||
Uses hasMore pagination (CLAUDE.md §10 401-retry-once applies per page).
|
||||
"""
|
||||
try:
|
||||
record_etaxes_activity()
|
||||
|
||||
if not company or not frappe.db.exists("Company", company):
|
||||
return {"success": False, "message": f"Company '{company}' not found."}
|
||||
|
||||
headers, err = _auth_headers()
|
||||
if err:
|
||||
return err
|
||||
|
||||
created, updated, total_fetched = 0, 0, 0
|
||||
offset = 0
|
||||
|
||||
for page in range(MAX_PAGES):
|
||||
payload = {"maxCount": PAGE_SIZE, "offset": offset}
|
||||
response, err = _etaxes_request(
|
||||
"POST", POS_TERMINAL_LIST_URL, headers, json=payload
|
||||
)
|
||||
if err:
|
||||
return err
|
||||
|
||||
data = response.json() or {}
|
||||
terminals = data.get("posTerminals") or []
|
||||
has_more = bool(data.get("hasMore"))
|
||||
|
||||
if not terminals:
|
||||
break
|
||||
|
||||
for term in terminals:
|
||||
registration_number = term.get("registrationNumber")
|
||||
if not registration_number:
|
||||
continue
|
||||
|
||||
values = {
|
||||
"serial_number": term.get("serialNumber"),
|
||||
"status": term.get("status"),
|
||||
"registration_date": _safe_date(term.get("registrationDate")),
|
||||
"bank_name": term.get("bankName"),
|
||||
"object": _object_from_registration_number(registration_number),
|
||||
"company": company,
|
||||
}
|
||||
|
||||
if frappe.db.exists("E-Taxes POS Terminal", registration_number):
|
||||
frappe.db.set_value(
|
||||
"E-Taxes POS Terminal", registration_number, values,
|
||||
update_modified=False,
|
||||
)
|
||||
updated += 1
|
||||
else:
|
||||
doc = frappe.get_doc({
|
||||
"doctype": "E-Taxes POS Terminal",
|
||||
"registration_number": registration_number,
|
||||
**values,
|
||||
})
|
||||
doc.insert(ignore_permissions=True)
|
||||
created += 1
|
||||
|
||||
total_fetched += len(terminals)
|
||||
offset += PAGE_SIZE
|
||||
|
||||
if not has_more:
|
||||
break
|
||||
else:
|
||||
frappe.log_error(
|
||||
f"POS terminal pagination hit safety cap of {MAX_PAGES} pages "
|
||||
f"for company {company}",
|
||||
"E-Taxes POS Terminal Load",
|
||||
)
|
||||
|
||||
frappe.db.commit()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"POS terminals loaded: {created} created, {updated} updated",
|
||||
"created": created,
|
||||
"updated": updated,
|
||||
"total": total_fetched,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
frappe.log_error(
|
||||
f"Unexpected error: {e}\n{frappe.get_traceback()}",
|
||||
"E-Taxes POS Terminal Load",
|
||||
)
|
||||
return {"success": False, "message": f"Error: {e}"}
|
||||
|
||||
|
||||
# ======= COMPACT DATE/DATETIME PARSERS =======
|
||||
|
||||
def _parse_compact_date(value):
|
||||
"""Parse "yyyyMMdd" → Date string "yyyy-MM-dd". Also accepts standard Date."""
|
||||
if not value:
|
||||
return None
|
||||
s = str(value).strip()
|
||||
if len(s) == 8 and s.isdigit():
|
||||
s = f"{s[0:4]}-{s[4:6]}-{s[6:8]}"
|
||||
return _safe_date(s)
|
||||
|
||||
|
||||
def _parse_compact_datetime(value):
|
||||
"""Parse "yyyyMMddHHmmss" → Datetime string. Also accepts standard Datetime."""
|
||||
if not value:
|
||||
return None
|
||||
s = str(value).strip()
|
||||
if len(s) == 14 and s.isdigit():
|
||||
return f"{s[0:4]}-{s[4:6]}-{s[6:8]} {s[8:10]}:{s[10:12]}:{s[12:14]}"
|
||||
return s
|
||||
|
||||
|
||||
def _upsert(doctype, primary_key_value, values, primary_field):
|
||||
"""Upsert by primary field. Returns (created, updated) deltas as (0,1) or (1,0)."""
|
||||
if frappe.db.exists(doctype, primary_key_value):
|
||||
frappe.db.set_value(doctype, primary_key_value, values, update_modified=False)
|
||||
return 0, 1
|
||||
doc = frappe.get_doc({
|
||||
"doctype": doctype,
|
||||
primary_field: primary_key_value,
|
||||
**values,
|
||||
})
|
||||
doc.insert(ignore_permissions=True)
|
||||
return 1, 0
|
||||
|
||||
|
||||
def _summarize(created, updated, total, label):
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"{label}: {created} created, {updated} updated",
|
||||
"created": created,
|
||||
"updated": updated,
|
||||
"total": total,
|
||||
}
|
||||
|
||||
|
||||
# ======= BANK ACCOUNTS =======
|
||||
|
||||
@frappe.whitelist()
|
||||
def load_company_bank_accounts(company):
|
||||
"""Load bank accounts from E-Taxes into E-Taxes Bank Account doctype."""
|
||||
try:
|
||||
record_etaxes_activity()
|
||||
|
||||
if not company or not frappe.db.exists("Company", company):
|
||||
return {"success": False, "message": f"Company '{company}' not found."}
|
||||
|
||||
headers, err = _auth_headers()
|
||||
if err:
|
||||
return err
|
||||
|
||||
response, err = _etaxes_request("GET", BANK_ACCOUNT_LIST_URL, headers)
|
||||
if err:
|
||||
return err
|
||||
|
||||
data = response.json() or {}
|
||||
accounts = data.get("bankAccounts") or []
|
||||
|
||||
created, updated = 0, 0
|
||||
for acc in accounts:
|
||||
duplicate_number = acc.get("duplicateNumber")
|
||||
if not duplicate_number:
|
||||
continue
|
||||
values = {
|
||||
"number": acc.get("number"),
|
||||
"currency": acc.get("currency"),
|
||||
"status": acc.get("status"),
|
||||
"account_type": acc.get("type"),
|
||||
"registration_date": _safe_date(acc.get("registrationDate")),
|
||||
"duplicate_date": _safe_date(acc.get("duplicateDate")),
|
||||
"bank_name": acc.get("bankName"),
|
||||
"bank_tin": acc.get("bankTin"),
|
||||
"bank_code": acc.get("bankCode"),
|
||||
"main_bank_code": acc.get("mainBankCode"),
|
||||
"settlement_account": acc.get("settlementAccount"),
|
||||
"company": company,
|
||||
}
|
||||
c, u = _upsert("E-Taxes Bank Account", duplicate_number, values, "duplicate_number")
|
||||
created += c
|
||||
updated += u
|
||||
|
||||
frappe.db.commit()
|
||||
return _summarize(created, updated, len(accounts), "Bank accounts loaded")
|
||||
|
||||
except Exception as e:
|
||||
frappe.log_error(
|
||||
f"Unexpected error: {e}\n{frappe.get_traceback()}",
|
||||
"E-Taxes Bank Account Load",
|
||||
)
|
||||
return {"success": False, "message": f"Error: {e}"}
|
||||
|
||||
|
||||
# ======= OBLIGATION PACTS (sub-contractor obl-pact-list) =======
|
||||
|
||||
@frappe.whitelist()
|
||||
def load_company_obligation_pacts(company):
|
||||
"""Load obligation pacts (sazişlər — oil fields/agreements) from E-Taxes."""
|
||||
try:
|
||||
record_etaxes_activity()
|
||||
|
||||
if not company or not frappe.db.exists("Company", company):
|
||||
return {"success": False, "message": f"Company '{company}' not found."}
|
||||
|
||||
headers, err = _auth_headers()
|
||||
if err:
|
||||
return err
|
||||
|
||||
response, err = _etaxes_request("GET", OBL_PACT_LIST_URL, headers)
|
||||
if err:
|
||||
return err
|
||||
|
||||
data = response.json() or {}
|
||||
pacts = data.get("oblPacts") or []
|
||||
|
||||
created, updated = 0, 0
|
||||
for item in pacts:
|
||||
pact = item.get("oblPact") or {}
|
||||
code = pact.get("code")
|
||||
if not code:
|
||||
continue
|
||||
values = {
|
||||
"name_az": _get(pact, "name", "az"),
|
||||
"validity_date": _safe_date(item.get("validityDate")),
|
||||
"expire_date": _safe_date(item.get("expireDate")),
|
||||
"company": company,
|
||||
}
|
||||
c, u = _upsert("E-Taxes Obligation Pact", code, values, "code")
|
||||
created += c
|
||||
updated += u
|
||||
|
||||
frappe.db.commit()
|
||||
return _summarize(created, updated, len(pacts), "Obligation pacts loaded")
|
||||
|
||||
except Exception as e:
|
||||
frappe.log_error(
|
||||
f"Unexpected error: {e}\n{frappe.get_traceback()}",
|
||||
"E-Taxes Obligation Pact Load",
|
||||
)
|
||||
return {"success": False, "message": f"Error: {e}"}
|
||||
|
||||
|
||||
# ======= PRESENTED / ACCEPTED E-CERTIFICATES =======
|
||||
|
||||
def _map_certificate(cert, company):
|
||||
sazis_code = cert.get("sazisKodu")
|
||||
return {
|
||||
"cert_no": cert.get("certNo"),
|
||||
"cert_category": cert.get("certCategory"),
|
||||
"cert_state": cert.get("certState"),
|
||||
"operation_date": _parse_compact_datetime(cert.get("operationDate")),
|
||||
"taxpayer_full_name": cert.get("taxPayerFullName"),
|
||||
"voen": cert.get("voen"),
|
||||
"sazis_name": cert.get("sazisName"),
|
||||
"sazis_code": sazis_code if sazis_code and frappe.db.exists("E-Taxes Obligation Pact", sazis_code) else None,
|
||||
"contract_start_date": _parse_compact_date(cert.get("contractStartDate")),
|
||||
"contract_end_date": _parse_compact_date(cert.get("contractEndDate")),
|
||||
"cert_start_date": _parse_compact_date(cert.get("certStartDate")),
|
||||
"cert_end_date": _parse_compact_date(cert.get("certEndDate")),
|
||||
"fk_doc_oid": cert.get("fkDocOid"),
|
||||
"company": company,
|
||||
}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def load_company_presented_certificates(company):
|
||||
"""Load presented/accepted e-certificates from E-Taxes."""
|
||||
try:
|
||||
record_etaxes_activity()
|
||||
|
||||
if not company or not frappe.db.exists("Company", company):
|
||||
return {"success": False, "message": f"Company '{company}' not found."}
|
||||
|
||||
headers, err = _auth_headers()
|
||||
if err:
|
||||
return err
|
||||
|
||||
response, err = _etaxes_request("GET", PRESENTED_CERT_LIST_URL, headers)
|
||||
if err:
|
||||
return err
|
||||
|
||||
data = response.json() or {}
|
||||
certs = data.get("certificateList") or []
|
||||
|
||||
created, updated = 0, 0
|
||||
for cert in certs:
|
||||
oid = cert.get("oid")
|
||||
if not oid:
|
||||
continue
|
||||
values = _map_certificate(cert, company)
|
||||
c, u = _upsert("E-Taxes Presented Certificate", oid, values, "oid")
|
||||
created += c
|
||||
updated += u
|
||||
|
||||
frappe.db.commit()
|
||||
return _summarize(created, updated, len(certs), "Presented certificates loaded")
|
||||
|
||||
except Exception as e:
|
||||
frappe.log_error(
|
||||
f"Unexpected error: {e}\n{frappe.get_traceback()}",
|
||||
"E-Taxes Presented Certificate Load",
|
||||
)
|
||||
return {"success": False, "message": f"Error: {e}"}
|
||||
|
||||
|
||||
|
||||
|
||||
# ======= BULK ORCHESTRATOR (Socket.IO progress) =======
|
||||
|
||||
PROGRESS_EVENT = "company_data_loading_progress"
|
||||
COMPLETE_EVENT = "company_data_loading_complete"
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def load_company_data_bulk(company, selections):
|
||||
"""Enqueue a background job that runs the selected loaders sequentially
|
||||
and broadcasts progress/summary over Socket.IO.
|
||||
|
||||
`selections` is either a JSON string or a list of loader keys:
|
||||
["load_objects", "load_cash_registers", ...]
|
||||
"""
|
||||
import json as _json
|
||||
|
||||
if isinstance(selections, str):
|
||||
try:
|
||||
selections = _json.loads(selections)
|
||||
except ValueError:
|
||||
selections = [s.strip() for s in selections.split(",") if s.strip()]
|
||||
if not isinstance(selections, list):
|
||||
selections = []
|
||||
|
||||
if not company or not frappe.db.exists("Company", company):
|
||||
return {"success": False, "message": f"Company '{company}' not found."}
|
||||
if not selections:
|
||||
return {"success": False, "message": "No loaders selected."}
|
||||
|
||||
frappe.enqueue(
|
||||
_process_company_data_bulk,
|
||||
company=company,
|
||||
selections=selections,
|
||||
user=frappe.session.user,
|
||||
queue="default",
|
||||
timeout=1800,
|
||||
)
|
||||
return {"success": True, "enqueued": True, "total": len(selections)}
|
||||
|
||||
|
||||
def _process_company_data_bulk(company, selections, user):
|
||||
"""Background worker — runs each loader in turn, publishing realtime updates."""
|
||||
frappe.set_user(user)
|
||||
record_etaxes_activity()
|
||||
|
||||
loaders = {
|
||||
"load_objects": (load_company_objects, "Objects"),
|
||||
"load_cash_registers": (load_company_cash_registers, "Cash Registers"),
|
||||
"load_pos_terminals": (load_company_pos_terminals, "POS Terminals"),
|
||||
"load_bank_accounts": (load_company_bank_accounts, "Bank Accounts"),
|
||||
"load_obligation_pacts": (load_company_obligation_pacts, "Obligation Pacts"),
|
||||
"load_presented_certificates": (load_company_presented_certificates, "Presented Certificates"),
|
||||
}
|
||||
|
||||
total = len(selections)
|
||||
summary = []
|
||||
|
||||
frappe.publish_realtime(
|
||||
PROGRESS_EVENT,
|
||||
{"current": 0, "total": total, "label": "", "message": "Starting..."},
|
||||
user=user,
|
||||
)
|
||||
|
||||
for idx, key in enumerate(selections):
|
||||
entry = loaders.get(key)
|
||||
if not entry:
|
||||
summary.append({"key": key, "label": key, "result": {
|
||||
"success": False, "message": f"Unknown loader: {key}"
|
||||
}})
|
||||
continue
|
||||
loader_func, label = entry
|
||||
|
||||
frappe.publish_realtime(
|
||||
PROGRESS_EVENT,
|
||||
{"current": idx, "total": total, "label": label,
|
||||
"message": f"Loading {label}..."},
|
||||
user=user,
|
||||
)
|
||||
|
||||
try:
|
||||
result = loader_func(company)
|
||||
except Exception as e:
|
||||
frappe.log_error(
|
||||
f"Bulk loader '{key}' crashed: {e}\n{frappe.get_traceback()}",
|
||||
"Company Data Bulk Load",
|
||||
)
|
||||
result = {"success": False, "message": f"{label}: {e}"}
|
||||
|
||||
summary.append({"key": key, "label": label, "result": result})
|
||||
|
||||
# Abort early on auth failure — frontend will re-auth and retry
|
||||
if isinstance(result, dict) and result.get("error") == "unauthorized":
|
||||
frappe.publish_realtime(
|
||||
COMPLETE_EVENT,
|
||||
{"summary": summary, "total": total, "completed": idx,
|
||||
"error": "unauthorized",
|
||||
"message": "Authentication required. Please login again."},
|
||||
user=user,
|
||||
)
|
||||
return
|
||||
|
||||
frappe.publish_realtime(
|
||||
PROGRESS_EVENT,
|
||||
{"current": total, "total": total, "label": "", "message": "Done"},
|
||||
user=user,
|
||||
)
|
||||
frappe.publish_realtime(
|
||||
COMPLETE_EVENT,
|
||||
{"summary": summary, "total": total, "completed": total},
|
||||
user=user,
|
||||
)
|
||||
|
||||
|
||||
# ======= REFERENCE LIST (for Tax Policy tab sections) =======
|
||||
|
||||
_REFERENCE_LIST_FIELDS = {
|
||||
"E-Taxes Object": ["name", "object_code", "object_name", "object_status"],
|
||||
"E-Taxes Cash Register": ["name", "number", "model", "ecash_status", "object", "installed_at"],
|
||||
"E-Taxes POS Terminal": ["name", "registration_number", "serial_number", "status", "registration_date"],
|
||||
"E-Taxes Bank Account": ["name", "number", "currency", "status", "bank_name"],
|
||||
"E-Taxes Obligation Pact": ["name", "code", "name_az", "validity_date", "expire_date"],
|
||||
"E-Taxes Presented Certificate": ["name", "cert_no", "taxpayer_full_name", "cert_category"],
|
||||
}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_company_reference_list(doctype, company, limit=50):
|
||||
"""Return first `limit` records of an E-Taxes reference doctype, filtered by company.
|
||||
|
||||
Only doctypes in the allow-list above are accessible via this endpoint.
|
||||
"""
|
||||
try:
|
||||
if doctype not in _REFERENCE_LIST_FIELDS:
|
||||
return {"success": False, "message": f"Doctype '{doctype}' not allowed."}
|
||||
|
||||
if not company or not frappe.db.exists("Company", company):
|
||||
return {"success": False, "message": f"Company '{company}' not found."}
|
||||
|
||||
try:
|
||||
limit = int(limit)
|
||||
except (TypeError, ValueError):
|
||||
limit = 50
|
||||
limit = max(1, min(limit, 200))
|
||||
|
||||
fields = _REFERENCE_LIST_FIELDS[doctype]
|
||||
filters = {"company": company}
|
||||
|
||||
data = frappe.get_all(
|
||||
doctype,
|
||||
filters=filters,
|
||||
fields=fields,
|
||||
limit=limit,
|
||||
order_by="modified desc",
|
||||
)
|
||||
total_count = frappe.db.count(doctype, filters=filters)
|
||||
|
||||
return {"success": True, "data": data, "total_count": total_count}
|
||||
|
||||
except Exception as e:
|
||||
frappe.log_error(
|
||||
f"Reference list load failed for {doctype}: {e}\n{frappe.get_traceback()}",
|
||||
"Company Reference List",
|
||||
)
|
||||
return {"success": False, "message": f"Error: {e}"}
|
||||
|
|
@ -13,7 +13,8 @@ doctype_js = {
|
|||
"Journal Entry": "client/journal_entry.js",
|
||||
"Supplier": "client/supplier.js",
|
||||
"Amas Employees": "client/amas_employees.js",
|
||||
"Employee": "client/employee.js"
|
||||
"Employee": "client/employee.js",
|
||||
"Company": "client/company.js"
|
||||
}
|
||||
|
||||
doctype_list_js = {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,103 @@
|
|||
{
|
||||
"actions": [],
|
||||
"autoname": "field:duplicate_number",
|
||||
"creation": "2026-04-21 12:00:00",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"duplicate_number",
|
||||
"number",
|
||||
"currency",
|
||||
"status",
|
||||
"account_type",
|
||||
"dates_section",
|
||||
"registration_date",
|
||||
"duplicate_date",
|
||||
"bank_section",
|
||||
"bank_name",
|
||||
"bank_tin",
|
||||
"bank_code",
|
||||
"main_bank_code",
|
||||
"settlement_account",
|
||||
"company_section",
|
||||
"company"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "duplicate_number",
|
||||
"fieldtype": "Data",
|
||||
"label": "Duplicate Number",
|
||||
"length": 40,
|
||||
"read_only": 1,
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "number",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Account Number",
|
||||
"length": 40,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Currency",
|
||||
"length": 5,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Status",
|
||||
"length": 5,
|
||||
"read_only": 1,
|
||||
"description": "A = Active, C = Closed"
|
||||
},
|
||||
{
|
||||
"fieldname": "account_type",
|
||||
"fieldtype": "Data",
|
||||
"label": "Type",
|
||||
"length": 5,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "dates_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Dates"
|
||||
},
|
||||
{"fieldname": "registration_date", "fieldtype": "Date", "label": "Registration Date", "read_only": 1},
|
||||
{"fieldname": "duplicate_date", "fieldtype": "Date", "label": "Duplicate Date", "read_only": 1},
|
||||
{"fieldname": "bank_section", "fieldtype": "Section Break", "label": "Bank"},
|
||||
{"fieldname": "bank_name", "fieldtype": "Data", "in_list_view": 1, "label": "Bank Name", "length": 500, "read_only": 1},
|
||||
{"fieldname": "bank_tin", "fieldtype": "Data", "label": "Bank TIN", "length": 20, "read_only": 1},
|
||||
{"fieldname": "bank_code", "fieldtype": "Data", "label": "Bank Code", "length": 20, "read_only": 1},
|
||||
{"fieldname": "main_bank_code", "fieldtype": "Data", "label": "Main Bank Code", "length": 20, "read_only": 1},
|
||||
{"fieldname": "settlement_account", "fieldtype": "Data", "label": "Settlement Account", "length": 40, "read_only": 1},
|
||||
{"fieldname": "company_section", "fieldtype": "Section Break", "label": "Link"},
|
||||
{"fieldname": "company", "fieldtype": "Link", "in_standard_filter": 1, "label": "Company", "options": "Company", "read_only": 1}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-04-21 12:00:00",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Invoice Az",
|
||||
"name": "E-Taxes Bank Account",
|
||||
"naming_rule": "By fieldname",
|
||||
"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,
|
||||
"title_field": "number"
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# Copyright (c) 2025, Jey ERP and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class ETaxesBankAccount(Document):
|
||||
pass
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# Copyright (c) 2025, Jey ERP and Contributors
|
||||
# See license.txt
|
||||
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||
|
||||
|
||||
EXTRA_TEST_RECORD_DEPENDENCIES = []
|
||||
IGNORE_TEST_RECORD_DEPENDENCIES = []
|
||||
|
||||
|
||||
class UnitTestETaxesBankAccount(UnitTestCase):
|
||||
pass
|
||||
|
||||
|
||||
class IntegrationTestETaxesBankAccount(IntegrationTestCase):
|
||||
pass
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
{
|
||||
"actions": [],
|
||||
"autoname": "field:token",
|
||||
"creation": "2026-04-21 12:00:00",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"token",
|
||||
"number",
|
||||
"model",
|
||||
"ecash_status",
|
||||
"status_code",
|
||||
"device_section",
|
||||
"object",
|
||||
"certificate_type",
|
||||
"dates_section",
|
||||
"installed_at",
|
||||
"certificate_from",
|
||||
"certificate_to",
|
||||
"replacement_date",
|
||||
"operator_section",
|
||||
"operator_tin",
|
||||
"operator_name",
|
||||
"company_section",
|
||||
"company"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "token",
|
||||
"fieldtype": "Data",
|
||||
"label": "Token",
|
||||
"length": 140,
|
||||
"read_only": 1,
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "number",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Serial Number",
|
||||
"length": 140,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "model",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Model",
|
||||
"length": 140,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "ecash_status",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "E-Cash Status",
|
||||
"options": "\nACTIVE\nELIMINATED",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "status_code",
|
||||
"fieldtype": "Data",
|
||||
"label": "Status Code",
|
||||
"length": 10,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "device_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Device"
|
||||
},
|
||||
{
|
||||
"fieldname": "object",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Object",
|
||||
"options": "E-Taxes Object",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "certificate_type",
|
||||
"fieldtype": "Data",
|
||||
"label": "Certificate Type",
|
||||
"length": 140,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "dates_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Dates"
|
||||
},
|
||||
{
|
||||
"fieldname": "installed_at",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Installed At",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "certificate_from",
|
||||
"fieldtype": "Date",
|
||||
"label": "Certificate From",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "certificate_to",
|
||||
"fieldtype": "Date",
|
||||
"label": "Certificate To",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "replacement_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Replacement Date",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "operator_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Operator"
|
||||
},
|
||||
{
|
||||
"fieldname": "operator_tin",
|
||||
"fieldtype": "Data",
|
||||
"label": "Operator TIN",
|
||||
"length": 20,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "operator_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Operator Name",
|
||||
"length": 500,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "company_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Link"
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-04-21 12:00:00",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Invoice Az",
|
||||
"name": "E-Taxes Cash Register",
|
||||
"naming_rule": "By fieldname",
|
||||
"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,
|
||||
"title_field": "number"
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# Copyright (c) 2025, Jey ERP and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class ETaxesCashRegister(Document):
|
||||
pass
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# Copyright (c) 2025, Jey ERP and Contributors
|
||||
# See license.txt
|
||||
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||
|
||||
|
||||
EXTRA_TEST_RECORD_DEPENDENCIES = []
|
||||
IGNORE_TEST_RECORD_DEPENDENCIES = []
|
||||
|
||||
|
||||
class UnitTestETaxesCashRegister(UnitTestCase):
|
||||
pass
|
||||
|
||||
|
||||
class IntegrationTestETaxesCashRegister(IntegrationTestCase):
|
||||
pass
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
{
|
||||
"actions": [],
|
||||
"autoname": "field:object_code",
|
||||
"creation": "2026-04-21 12:00:00",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"object_code",
|
||||
"object_name",
|
||||
"object_status",
|
||||
"address",
|
||||
"activity_section",
|
||||
"main_activity_code",
|
||||
"main_activity_name",
|
||||
"classification_section",
|
||||
"main_class_code",
|
||||
"main_class_name",
|
||||
"middle_class_code",
|
||||
"middle_class_name",
|
||||
"sub_class_code",
|
||||
"sub_class_name",
|
||||
"company_section",
|
||||
"company"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "object_code",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Object Code",
|
||||
"length": 140,
|
||||
"read_only": 1,
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "object_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Object Name",
|
||||
"length": 500,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "active",
|
||||
"fieldname": "object_status",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Status",
|
||||
"options": "active\nliquidated",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "address",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Address",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "activity_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Activity"
|
||||
},
|
||||
{
|
||||
"fieldname": "main_activity_code",
|
||||
"fieldtype": "Data",
|
||||
"label": "Main Activity Code",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "main_activity_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Main Activity Name",
|
||||
"length": 500,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "classification_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Classification"
|
||||
},
|
||||
{
|
||||
"fieldname": "main_class_code",
|
||||
"fieldtype": "Data",
|
||||
"label": "Main Class Code",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "main_class_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Main Class Name",
|
||||
"length": 500,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "middle_class_code",
|
||||
"fieldtype": "Data",
|
||||
"label": "Middle Class Code",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "middle_class_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Middle Class Name",
|
||||
"length": 500,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "sub_class_code",
|
||||
"fieldtype": "Data",
|
||||
"label": "Sub Class Code",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "sub_class_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Sub Class Name",
|
||||
"length": 500,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "company_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Link"
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-04-21 12:00:00",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Invoice Az",
|
||||
"name": "E-Taxes Object",
|
||||
"naming_rule": "By fieldname",
|
||||
"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,8 @@
|
|||
# Copyright (c) 2025, Jey ERP and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class ETaxesObject(Document):
|
||||
pass
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# Copyright (c) 2025, Jey ERP and Contributors
|
||||
# See license.txt
|
||||
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||
|
||||
|
||||
EXTRA_TEST_RECORD_DEPENDENCIES = []
|
||||
IGNORE_TEST_RECORD_DEPENDENCIES = []
|
||||
|
||||
|
||||
class UnitTestETaxesObject(UnitTestCase):
|
||||
pass
|
||||
|
||||
|
||||
class IntegrationTestETaxesObject(IntegrationTestCase):
|
||||
pass
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
{
|
||||
"actions": [],
|
||||
"autoname": "field:code",
|
||||
"creation": "2026-04-21 12:00:00",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"code",
|
||||
"name_az",
|
||||
"validity_date",
|
||||
"expire_date",
|
||||
"company_section",
|
||||
"company"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "code",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Code",
|
||||
"length": 40,
|
||||
"read_only": 1,
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "name_az",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Name (AZ)",
|
||||
"length": 500,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "validity_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Validity Date",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "expire_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Expire Date",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "company_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Link"
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-04-21 12:00:00",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Invoice Az",
|
||||
"name": "E-Taxes Obligation Pact",
|
||||
"naming_rule": "By fieldname",
|
||||
"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,
|
||||
"title_field": "name_az"
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# Copyright (c) 2025, Jey ERP and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class ETaxesObligationPact(Document):
|
||||
pass
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# Copyright (c) 2025, Jey ERP and Contributors
|
||||
# See license.txt
|
||||
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||
|
||||
|
||||
EXTRA_TEST_RECORD_DEPENDENCIES = []
|
||||
IGNORE_TEST_RECORD_DEPENDENCIES = []
|
||||
|
||||
|
||||
class UnitTestETaxesObligationPact(UnitTestCase):
|
||||
pass
|
||||
|
||||
|
||||
class IntegrationTestETaxesObligationPact(IntegrationTestCase):
|
||||
pass
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
{
|
||||
"actions": [],
|
||||
"autoname": "field:registration_number",
|
||||
"creation": "2026-04-21 12:00:00",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"registration_number",
|
||||
"serial_number",
|
||||
"status",
|
||||
"registration_date",
|
||||
"device_section",
|
||||
"object",
|
||||
"bank_name",
|
||||
"company_section",
|
||||
"company"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "registration_number",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Registration Number",
|
||||
"length": 140,
|
||||
"read_only": 1,
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "serial_number",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Serial Number",
|
||||
"length": 140,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Status",
|
||||
"options": "\nA\nC",
|
||||
"read_only": 1,
|
||||
"description": "A = Active, C = Closed"
|
||||
},
|
||||
{
|
||||
"fieldname": "registration_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Registration Date",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "device_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Device"
|
||||
},
|
||||
{
|
||||
"fieldname": "object",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Object",
|
||||
"options": "E-Taxes Object",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "bank_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Bank Name",
|
||||
"length": 500,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "company_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Link"
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-04-21 12:00:00",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Invoice Az",
|
||||
"name": "E-Taxes POS Terminal",
|
||||
"naming_rule": "By fieldname",
|
||||
"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,
|
||||
"title_field": "serial_number"
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# Copyright (c) 2025, Jey ERP and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class ETaxesPOSTerminal(Document):
|
||||
pass
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# Copyright (c) 2025, Jey ERP and Contributors
|
||||
# See license.txt
|
||||
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||
|
||||
|
||||
EXTRA_TEST_RECORD_DEPENDENCIES = []
|
||||
IGNORE_TEST_RECORD_DEPENDENCIES = []
|
||||
|
||||
|
||||
class UnitTestETaxesPOSTerminal(UnitTestCase):
|
||||
pass
|
||||
|
||||
|
||||
class IntegrationTestETaxesPOSTerminal(IntegrationTestCase):
|
||||
pass
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
{
|
||||
"actions": [],
|
||||
"autoname": "field:oid",
|
||||
"creation": "2026-04-21 12:00:00",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"oid",
|
||||
"cert_no",
|
||||
"cert_category",
|
||||
"cert_state",
|
||||
"operation_date",
|
||||
"taxpayer_section",
|
||||
"taxpayer_full_name",
|
||||
"voen",
|
||||
"sazis_section",
|
||||
"sazis_name",
|
||||
"sazis_code",
|
||||
"contract_section",
|
||||
"contract_start_date",
|
||||
"contract_end_date",
|
||||
"cert_start_date",
|
||||
"cert_end_date",
|
||||
"misc_section",
|
||||
"fk_doc_oid",
|
||||
"company_section",
|
||||
"company"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "oid",
|
||||
"fieldtype": "Data",
|
||||
"label": "OID",
|
||||
"length": 40,
|
||||
"read_only": 1,
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "cert_no",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Certificate No",
|
||||
"length": 40,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "cert_category",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Category",
|
||||
"options": "\nACCEPTED\nPRESENTED\nREJECTED\nPENDING",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "cert_state",
|
||||
"fieldtype": "Data",
|
||||
"label": "State",
|
||||
"length": 40,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "operation_date",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Operation Date",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "taxpayer_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Taxpayer"
|
||||
},
|
||||
{
|
||||
"fieldname": "taxpayer_full_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Taxpayer Full Name",
|
||||
"length": 500,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "voen",
|
||||
"fieldtype": "Data",
|
||||
"label": "VOEN (Tax ID)",
|
||||
"length": 20,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "sazis_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Agreement (Sazış)"
|
||||
},
|
||||
{
|
||||
"fieldname": "sazis_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Agreement Name",
|
||||
"length": 500,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "sazis_code",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Agreement Code",
|
||||
"options": "E-Taxes Obligation Pact",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "contract_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Validity"
|
||||
},
|
||||
{"fieldname": "contract_start_date", "fieldtype": "Date", "label": "Contract Start", "read_only": 1},
|
||||
{"fieldname": "contract_end_date", "fieldtype": "Date", "label": "Contract End", "read_only": 1},
|
||||
{"fieldname": "cert_start_date", "fieldtype": "Date", "label": "Certificate Start", "read_only": 1},
|
||||
{"fieldname": "cert_end_date", "fieldtype": "Date", "label": "Certificate End", "read_only": 1},
|
||||
{"fieldname": "misc_section", "fieldtype": "Section Break", "label": "Misc"},
|
||||
{"fieldname": "fk_doc_oid", "fieldtype": "Data", "label": "FK Doc OID", "length": 40, "read_only": 1},
|
||||
{"fieldname": "company_section", "fieldtype": "Section Break", "label": "Link"},
|
||||
{"fieldname": "company", "fieldtype": "Link", "in_standard_filter": 1, "label": "Company", "options": "Company", "read_only": 1}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-04-21 12:00:00",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Invoice Az",
|
||||
"name": "E-Taxes Presented Certificate",
|
||||
"naming_rule": "By fieldname",
|
||||
"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,
|
||||
"title_field": "cert_no"
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# Copyright (c) 2025, Jey ERP and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class ETaxesPresentedCertificate(Document):
|
||||
pass
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# Copyright (c) 2025, Jey ERP and Contributors
|
||||
# See license.txt
|
||||
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||
|
||||
|
||||
EXTRA_TEST_RECORD_DEPENDENCIES = []
|
||||
IGNORE_TEST_RECORD_DEPENDENCIES = []
|
||||
|
||||
|
||||
class UnitTestETaxesPresentedCertificate(UnitTestCase):
|
||||
pass
|
||||
|
||||
|
||||
class IntegrationTestETaxesPresentedCertificate(IntegrationTestCase):
|
||||
pass
|
||||
Loading…
Reference in New Issue