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:
Ali 2026-04-21 13:59:29 +00:00
parent 64961e4108
commit f622a4ce2a
29 changed files with 2917 additions and 34 deletions

3
.gitignore vendored
View File

@ -53,3 +53,6 @@ jspm_packages/
# Aider AI Chat # Aider AI Chat
.aider* .aider*
# HAR captures (may contain Bearer tokens / PII)
*.har

262
CLAUDE.md
View File

@ -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: if response.status_code == 401:
return { fresh_login = get_default_asan_login()
"success": False, if fresh_login.get("found") and fresh_login.get("main_token"):
"message": "Authentication expired. Please login again." headers["x-authorization"] = f"Bearer {fresh_login['main_token']}"
} response = requests.post(url, json=payload, headers=headers, timeout=60)
if response.status_code == 401:
return {
"error": "unauthorized",
"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

View File

@ -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();
}
});
}

762
invoice_az/company_api.py Normal file
View File

@ -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}"}

View File

@ -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 = {

View File

@ -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"
}

View File

@ -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

View File

@ -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

View File

@ -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"
}

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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"
}

View File

@ -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

View File

@ -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

View File

@ -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"
}

View File

@ -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

View File

@ -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

View File

@ -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"
}

View File

@ -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

View File

@ -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