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 AI Chat
|
||||||
.aider*
|
.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
|
```python
|
||||||
import frappe
|
import frappe
|
||||||
import requests
|
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()
|
@frappe.whitelist()
|
||||||
def my_etaxes_function(doc_name):
|
def my_etaxes_function(doc_name):
|
||||||
|
|
@ -134,7 +134,7 @@ def my_etaxes_function(doc_name):
|
||||||
asan_login = get_default_asan_login()
|
asan_login = get_default_asan_login()
|
||||||
if not asan_login.get("found") or not asan_login.get("main_token"):
|
if not asan_login.get("found") or not asan_login.get("main_token"):
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"error": "unauthorized",
|
||||||
"message": "E-Taxes authentication not found. Please login via ASAN."
|
"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
|
# Step 6: Make API request
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
"https://new.e-taxes.gov.az/api/...",
|
"https://new.e-taxes.gov.az/api/...",
|
||||||
data=json.dumps(payload),
|
json=payload,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
timeout=60
|
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:
|
if response.status_code == 401:
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"error": "unauthorized",
|
||||||
"message": "Authentication expired. Please login again."
|
"message": "Authentication required. Please login again."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Step 8: Other error codes
|
||||||
if response.status_code == 500:
|
if response.status_code == 500:
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"error": "server_error",
|
||||||
"message": "E-Taxes service error. Please try again later."
|
"message": "Service temporarily unavailable. Please try again in a few minutes.",
|
||||||
|
"status_code": 500
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.status_code == 404:
|
if response.status_code == 404:
|
||||||
|
|
@ -173,43 +180,40 @@ def my_etaxes_function(doc_name):
|
||||||
|
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
# Step 8: Process response
|
# Step 9: Process response
|
||||||
result = response.json()
|
result = response.json()
|
||||||
|
|
||||||
# Step 9: Return success
|
# Step 10: Persist any state changes explicitly
|
||||||
|
frappe.db.commit()
|
||||||
|
|
||||||
|
# Step 11: Return success
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"data": result
|
"data": result
|
||||||
}
|
}
|
||||||
|
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
frappe.log_error(
|
frappe.log_error(f"Network error: {e}", "My Feature Error")
|
||||||
f"Network error: {str(e)}",
|
return {"success": False, "message": f"Network error: {e}"}
|
||||||
"E-Taxes API Error"
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"message": f"Network error: {str(e)}"
|
|
||||||
}
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
frappe.log_error(
|
frappe.log_error(
|
||||||
f"Unexpected error: {str(e)}\n{frappe.get_traceback()}",
|
f"Unexpected error: {e}\n{frappe.get_traceback()}",
|
||||||
"E-Taxes API Error"
|
"My Feature Error"
|
||||||
)
|
)
|
||||||
return {
|
return {"success": False, "message": f"Error: {e}"}
|
||||||
"success": False,
|
|
||||||
"message": f"Error: {str(e)}"
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Key points:**
|
**Key points:**
|
||||||
1. `@frappe.whitelist()` - ALWAYS required for frontend calls
|
1. `@frappe.whitelist()` — always bare, no `methods=[...]`, no `allow_guest`
|
||||||
2. `record_etaxes_activity()` - ALWAYS first (enables token auto-renewal)
|
2. `record_etaxes_activity()` — ALWAYS first (resets inactivity timer, prevents token expiry mid-call)
|
||||||
3. `get_default_asan_login()` - Get token
|
3. `get_default_asan_login()` — read token from DB (scheduler may have refreshed it)
|
||||||
4. `headers["x-authorization"] = f"Bearer {token}"` - Required for auth
|
4. `headers["x-authorization"] = f"Bearer {token}"` — required for every call
|
||||||
5. Handle 401, 500, 404 errors explicitly
|
5. **401 MUST retry once** with a fresh token before giving up — see §10
|
||||||
6. ALWAYS return `{"success": bool, "message": str}` format
|
6. Return codes by use case — see §11
|
||||||
7. ALWAYS use try-except with proper error logging
|
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
|
### 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!
|
**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
|
## Development Commands
|
||||||
|
|
||||||
### Linting and Code Quality
|
### 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",
|
"Journal Entry": "client/journal_entry.js",
|
||||||
"Supplier": "client/supplier.js",
|
"Supplier": "client/supplier.js",
|
||||||
"Amas Employees": "client/amas_employees.js",
|
"Amas Employees": "client/amas_employees.js",
|
||||||
"Employee": "client/employee.js"
|
"Employee": "client/employee.js",
|
||||||
|
"Company": "client/company.js"
|
||||||
}
|
}
|
||||||
|
|
||||||
doctype_list_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