43 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Overview
Invoice Az is a Frappe application that integrates with Azerbaijan government systems:
E-Taxes Integration (e-taxes.gov.az):
- Downloading and importing purchase invoices from e-taxes
- Downloading and importing sales invoices from e-taxes
- Sending sales invoices to e-taxes with ASAN Imza signing
- Sending purchase acts to e-taxes for Individual suppliers
- Importing VAT Account operations and creating Journal Entries
- Managing authentication via ASAN Login with automatic token renewal
AMAS Integration (e-social.gov.az):
- Importing employees from AMAS (Azerbaijan Employment Management System)
- MyGovID/ASAN authentication for accessing AMAS
- Automatic employee synchronization with ERPNext
⚠️ CRITICAL RULES FOR E-TAXES INTEGRATION ⚠️
READ THIS BEFORE IMPLEMENTING ANY E-TAXES API CALLS!
1. Authentication Pattern (MANDATORY)
❌ WRONG - Direct API call:
function my_function(frm) {
frappe.call({
method: 'invoice_az.my_api.some_method',
// ... this will fail with "Authentication expired"
});
}
✅ CORRECT - Always wrap with authentication check:
function my_function(frm) {
ETaxes.auth.checkAndProcess(function() {
// Your code here - only runs after auth is verified
frappe.call({
method: 'invoice_az.my_api.some_method',
// ... works correctly
});
});
}
Why? ETaxes.auth.checkAndProcess() does:
- Checks if token is still valid
- If expired → prompts user to login via ASAN Imza
- Only after successful auth → executes your callback
- Prevents "Authentication expired" errors
2. ETaxes Module Inclusion (MANDATORY)
⚠️ ETaxes is NOT a global module! It must be defined in EACH JavaScript file that uses it.
❌ WRONG - Assuming ETaxes exists:
// my_custom_form.js
frappe.ui.form.on('My DocType', {
refresh: function(frm) {
ETaxes.auth.checkAndProcess(function() {
// ERROR: ETaxes is not defined
});
}
});
✅ CORRECT - Include ETaxes module at the top of your JS file:
Copy the entire ETaxes module from sales_invoice.js (lines 1-564) to the beginning of your JS file:
// ======= COPY THIS ENTIRE BLOCK =======
// From sales_invoice.js lines 1-564:
const ETaxes = {
PROGRESS_UPDATE_DELAY: 50,
AUTH_POLL_INTERVAL: 6000,
AUTH_MAX_ATTEMPTS: 20,
// ... (full module definition)
};
ETaxes.utils = { /* ... */ };
ETaxes.dialogs = { /* ... */ };
ETaxes.auth = { /* ... */ };
// ======= END COPY =======
// Now your code can use ETaxes:
frappe.ui.form.on('My DocType', {
refresh: function(frm) {
ETaxes.auth.checkAndProcess(function() {
// ✅ Works correctly
});
}
});
Files that need ETaxes module:
- ✅
sales_invoice.js- has it (564 lines) - ✅
purchase_order.js- has it - ✅
supplier.js- has it (after fix) - ❌ Any new JS file - MUST include it!
3. Backend API Pattern (MANDATORY)
All backend functions that call E-Taxes API MUST follow this pattern:
import frappe
import requests
from invoice_az.auth import record_etaxes_activity, get_default_asan_login, DEFAULT_HEADERS
@frappe.whitelist()
def my_etaxes_function(doc_name):
"""My E-Taxes API function"""
try:
# Step 1: ALWAYS record activity (for token renewal)
record_etaxes_activity()
# Step 2: Get document
doc = frappe.get_doc("My DocType", doc_name)
# Step 3: Validate (business logic checks)
validation = validate_my_document(doc)
if not validation.get("valid"):
return {
"success": False,
"message": validation.get("message")
}
# Step 4: Get authentication token
asan_login = get_default_asan_login()
if not asan_login.get("found") or not asan_login.get("main_token"):
return {
"error": "unauthorized",
"message": "E-Taxes authentication not found. Please login via ASAN."
}
token = asan_login.get("main_token")
# Step 5: Build headers with token
headers = DEFAULT_HEADERS.copy()
headers["x-authorization"] = f"Bearer {token}"
# Step 6: Make API request
response = requests.post(
"https://new.e-taxes.gov.az/api/...",
json=payload,
headers=headers,
timeout=60
)
# Step 7: 401 — RETRY ONCE with a fresh token from DB, then fail
if response.status_code == 401:
fresh_login = get_default_asan_login()
if fresh_login.get("found") and fresh_login.get("main_token"):
headers["x-authorization"] = f"Bearer {fresh_login['main_token']}"
response = requests.post(url, json=payload, headers=headers, timeout=60)
if response.status_code == 401:
return {
"error": "unauthorized",
"message": "Authentication required. Please login again."
}
# Step 8: Other error codes
if response.status_code == 500:
return {
"error": "server_error",
"message": "Service temporarily unavailable. Please try again in a few minutes.",
"status_code": 500
}
if response.status_code == 404:
return {
"success": False,
"message": "Resource not found."
}
response.raise_for_status()
# Step 9: Process response
result = response.json()
# Step 10: Persist any state changes explicitly
frappe.db.commit()
# Step 11: Return success
return {
"success": True,
"data": result
}
except requests.exceptions.RequestException as e:
frappe.log_error(f"Network error: {e}", "My Feature Error")
return {"success": False, "message": f"Network error: {e}"}
except Exception as e:
frappe.log_error(
f"Unexpected error: {e}\n{frappe.get_traceback()}",
"My Feature Error"
)
return {"success": False, "message": f"Error: {e}"}
Key points:
@frappe.whitelist()— always bare, nomethods=[...], noallow_guestrecord_etaxes_activity()— ALWAYS first (resets inactivity timer, prevents token expiry mid-call)get_default_asan_login()— read token from DB (scheduler may have refreshed it)headers["x-authorization"] = f"Bearer {token}"— required for every call- 401 MUST retry once with a fresh token before giving up — see §10
- Return codes by use case — see §11
frappe.db.commit()after anyfrappe.db.set_value/.insert()/.save()/.delete_doc()— see §12- Use
ignore_permissions=Truewhen inserting E-Taxes internal/tracking records frappe.log_error(msg, "Title Case Error")— see §13 for title convention- Always try-except around the whole body
4. Constants and URLs
Base URL:
BASE_URL = "https://new.e-taxes.gov.az"
Common endpoints:
- Auth:
/api/po/auth/public/v1/* - Invoices:
/api/po/invoice/public/v2/invoice - Sign:
/api/po/invoice/public/v1/invoice/sign/withAsanImza - Serial:
/api/po/invoice/public/v1/generateSerialNumber/defaultInvoice - Agricultural Serial:
/api/po/invoice/public/v1/act/generateSerialNumber/agriculturalProductsAct - Metal Scrap Serial:
/api/po/invoice/public/v1/act/generateSerialNumber/metalProductsAct - Tire Disposal Serial:
/api/po/invoice/public/v1/act/generateSerialNumber/tireProductsForDisposalAct - Plastic Disposal Serial:
/api/po/invoice/public/v1/act/generateSerialNumber/plasticProductsForDisposalAct - Rawhide Supply Serial:
/api/po/invoice/public/v1/act/generateSerialNumber/rawhideSupply - Other Product Receipt Serial:
/api/po/invoice/public/v1/act/generateSerialNumber/otherProductReceiptAct - All Acts:
/api/po/invoice/public/v1/act - Remove Drafts:
/api/po/invoice/public/v1/common/removeDrafts - VAT Operations:
/api/po/vatacc/public/v1/operation/find.outbox - Taxpayer by FIN:
/api/po/profile/public/v1/taxpayer/findByFinAndPassport
Standard headers:
DEFAULT_HEADERS = {
"Accept": "application/json, text/plain, */*",
"Accept-Language": "en-US,en;q=0.9",
"Content-Type": "application/json",
"Cache-Control": "no-cache",
"Pragma": "no-cache"
}
5. Error Handling
HTTP Status Codes:
200- Success401- Token expired → Show "Please login again"404- Resource not found500- E-Taxes server error → Show "Try again later"
Always check in this order:
if response.status_code == 401:
return {"success": False, "message": "Authentication expired. Please login again."}
if response.status_code == 500:
return {"success": False, "message": "E-Taxes service error. Please try again later."}
if response.status_code == 404:
return {"success": False, "message": "Not found"}
response.raise_for_status() # For other errors
6. Frontend Button Pattern
Always show buttons conditionally:
frappe.ui.form.on('My DocType', {
refresh: function(frm) {
// Only for submitted documents
if (frm.doc.docstatus === 1) {
add_etaxes_buttons(frm);
}
}
});
function add_etaxes_buttons(frm) {
// Check status and show appropriate buttons
if (frm.doc.etaxes_status === 'Sent and Signed') {
frm.add_custom_button(__('View Details'), function() {
// Show details
}, __('E-Taxes'));
} else if (frm.doc.etaxes_status === 'Created, not signed') {
frm.add_custom_button(__('Sign with ASAN Imza'), function() {
// Sign document
}, __('E-Taxes'));
} else {
frm.add_custom_button(__('Send to E-Taxes'), function() {
// Send to E-Taxes
}, __('E-Taxes'));
}
}
7. Build Process
After modifying JavaScript files:
# Build assets
cd /home/frappe/frappe-bench
bench build --app invoice_az
# Clear cache
bench --site site1 clear-cache
After modifying Python files:
# Just clear cache (no build needed)
bench --site site1 clear-cache
8. Common Mistakes to Avoid
❌ Don't do this:
- Call E-Taxes API without
ETaxes.auth.checkAndProcess() - Forget to include ETaxes module in new JS files
- Skip
record_etaxes_activity()in backend functions - Forget to handle 401/500/404 errors
- Use
app_include_jsin hooks.py (doesn't work for doctype-specific code) - Make API calls without Bearer token in headers
✅ Always do this:
- Wrap ALL E-Taxes operations in
ETaxes.auth.checkAndProcess() - Copy ETaxes module (564 lines) to every new JS file
- Call
record_etaxes_activity()first in every API function - Handle 401, 500, 404 explicitly
- Return consistent
{success, message}format - Add
x-authorization: Bearer {token}header - Build assets after JS changes
9. Example Files to Reference
Backend (Python):
- ✅
send_sales_api.py- Perfect example of all patterns - ✅
send_purchase_api.py- Agricultural act example - ✅
supplier_api.py- Simple API call example - ✅
auth.py- Authentication implementation
Frontend (JavaScript):
- ✅
sales_invoice.js- Complete ETaxes module (lines 1-564) - ✅
purchase_invoice.js- Button patterns - ✅
supplier.js- Minimal working example
When in doubt: Look at send_sales_api.py + sales_invoice.js - they contain all correct patterns!
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.
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):
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):
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 onserver_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", ...)— seeauth.py:46, 57, 157, 173doc.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:
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_nameE-Taxes Suppliers→etaxes_party_nameE-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_azClassification code→classification_nameE-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:
- Primary identifier (
unique=1,reqd=1,read_only=1,in_list_view=1) - Secondary identifier (code / tax_id)
- Status (
Select,in_list_view=1,in_standard_filter=1) - Source tracking (optional
source_invoice) Section Break— grouped attribute sections (Activity, Classification, Address...)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.
# Entry point (whitelisted)
@frappe.whitelist()
def import_bulk_foo(ids):
frappe.enqueue(
_process_bulk_foo,
ids=ids,
user=frappe.session.user,
queue="default",
timeout=1200,
)
return {"success": True, "enqueued": True, "total": len(ids)}
# Worker (not whitelisted, underscore-prefixed)
def _process_bulk_foo(ids, user):
frappe.set_user(user)
record_etaxes_activity()
total = len(ids)
for idx, id_ in enumerate(ids):
try:
process_one(id_)
except Exception as e:
frappe.log_error(f"Item {id_}: {e}", "Bulk Foo Error")
frappe.publish_realtime(
"bulk_foo_progress",
{"current": idx + 1, "total": total},
user=user,
)
frappe.publish_realtime(
"bulk_foo_complete",
{"total": total},
user=user,
)
Event naming convention: {feature}_progress (periodic), {feature}_complete (final). Frontend subscribes via frappe.realtime.on(event, handler).
Development Commands
Linting and Code Quality
# Run ruff for Python linting
ruff check invoice_az/
# Run ruff format
ruff format invoice_az/
# Run pre-commit hooks (includes ruff, eslint, prettier, pyupgrade)
pre-commit run --all-files
Testing
# Run unit tests for a specific doctype
bench run-tests --app invoice_az --doctype "E-Taxes Item"
# Run all tests for the app
bench run-tests --app invoice_az
Development
# Clear cache after making changes
bench clear-cache
# Restart workers after API changes
bench restart
# Watch logs
bench --site [site-name] console
Architecture
Core Modules
-
Authentication Module (
invoice_az/auth.py)- ASAN Login integration for Azerbaijan government authentication
- Token management with automatic renewal every 4 minutes via cron job
- Activity tracking to prevent unnecessary token renewals
- Retry logic with exponential backoff for failed requests
- Token validity checking and automatic refresh on 401 errors
- Full authentication flow: start auth → poll status → get certificates → select taxpayer
- Key functions:
renew_token(),get_auth_token(),check_auth_status(),get_certificates(),choose_taxpayer(),check_token_validity(),handle_unauthorized_request(),handle_authentication(),poll_auth_status(),select_certificate(),select_taxpayer()
-
Purchase API (
invoice_az/api.py)- Download and import purchase invoices from e-taxes
- Party, item, and unit mapping system
- Separate Customer and Supplier mapping systems
- Create Purchase Orders and Purchase Invoices
- E-Taxes Purchase tracking records
- Reference data management (sync items, units, parties from e-taxes)
- Batch invoice processing for loading reference data
- Auto-matching for items, units, customers, suppliers by name similarity
-
Sales Import API (
invoice_az/sales_api.py)- Download and import sales invoices from e-taxes outbox
- Customer mapping system (separate from suppliers)
- Create Sales Orders and Sales Invoices
- E-Taxes Sales tracking records
- Azerbaijani character normalization for matching
-
Sales Send API (
invoice_az/send_sales_api.py)- Send Sales Invoices to e-taxes system
- Two-step workflow: create draft, then sign with ASAN Imza
- Generate serial numbers from e-taxes
- Build invoice payload with product codes and VAT fields
- E-Taxes Sales Outbox tracking
- Cancel/remove draft invoices functionality
- Retry signing for failed/pending invoices
- Get customer objects by TIN for delivery address selection
-
VAT Operations API (
invoice_az/vat_api.py)- Import VAT Account operations from e-taxes
- Import all operations (both income and expense types)
- Create Journal Entries with configurable account mappings
- VAT Account Mappings by operation type and expense/income type (Sub uçot hesabı → Sub uçot hesabı, Sub uçot hesabı → Büdcə, etc.)
- Classification Code support - allows specific account mappings per tax code with priority:
- Priority 1: Mapping with matching operation_type + expense_income + classification_code
- Priority 2: Default mapping with matching operation_type + expense_income (empty classification_code)
- Required mapping configuration - operation fails with error if mapping not found
- Smart party assignment: adds Customer/Supplier only for Receivable/Payable accounts
- Customer lookup by TIN
- E-Taxes VAT Operations tracking
- Key functions:
get_accounts_for_operation_type(),find_account_by_number(),create_journal_entry_from_vat_operation(),map_operation_type()
-
VAT Operations Hooks (
invoice_az/vat_operations.py)- Delete related E-Taxes VAT Operations when Journal Entry is deleted/cancelled
-
Purchase Send API (
invoice_az/send_purchase_api.py)- Send Purchase Invoices (acts) to e-taxes for Individual suppliers
- Two-step workflow: create draft act, then sign with ASAN Imza
- Support for 6 act types: Agricultural Products, Metal Scrap Reception, Tire Disposal, Plastic Disposal, Rawhide Supply, Other Product Receipt
- Generate serial numbers from e-taxes for each act type
- Build act payload with product group codes and tax types
- E-Taxes Purchase Outbox tracking
- Cancel/remove draft acts functionality
- Retry signing for failed/pending acts
- Tax type validation (some acts allow only "Taxable" items)
-
Supplier API (
invoice_az/supplier_api.py)- Fetch Individual supplier data from E-Taxes by FIN and Passport Serial Number
- Auto-populate supplier fields (full name, phone, date of birth, first name, last name, patronymic)
- Validate supplier type (Individual only)
- Used for completing supplier information before creating purchase acts
-
AMAS Integration API (
invoice_az/amas_api.py)- Connect to AMAS (e-social.gov.az) using MyGovID JWT token
- OAuth flow: JWT token → authorization code → AMAS session
- Fetch employee lists from AMAS with pagination
- Execute AMAS reports (Magusbi reporting system)
- Multi-account support (switch between different company accounts)
- Session management with cookie handling
- Import employees into ERPNext Employee doctype
- Key functions:
connect_amas(),get_employees_report(),import_employees_from_amas(),get_connected_asan_logins()
Document Integration (hooks.py)
Extended Doctypes:
- Purchase Order - import from e-taxes, delete hooks
- Purchase Invoice - import from e-taxes, send acts to e-taxes, delete hooks
- Sales Order - import from e-taxes, delete hooks
- Sales Invoice - send to e-taxes, delete hooks
- Journal Entry - import VAT operations, delete hooks, before_save hook for title
- Supplier - fetch data from e-taxes by FIN/passport
- Employee - import from AMAS, delete hooks
- E-Taxes Settings - on_update hook to sync mapped statuses
Client-side Scripts (invoice_az/client/):
purchase_order.js- E-taxes import buttons and dialogspurchase_invoice.js- E-taxes import functionality, send acts to e-taxes, sign actssales_order.js- E-taxes import buttons and dialogssales_invoice.js- Send to e-taxes, sign document, retry signing, cancel buttonsjournal_entry.js- VAT operations import functionality with operation type displaysupplier.js- Fetch supplier data from e-taxes by FIN/passportemployee.js- Import employees from AMASamas_employees.js- AMAS Employees list functionalitye_taxes_items_list.js- Sync items from e-taxese_taxes_suppliers_list.js- Sync suppliers from e-taxese_taxes_customers_list.js- Sync customers from e-taxese_taxes_unit_list.js- Sync units from e-taxesetaxes_common.js- Shared utilities
Database Schema
Authentication:
- Asan Login - Stores ASAN credentials, tokens, certificates, auth status, MyGovID token, AMAS session
Settings:
- E-Taxes Settings - Global settings, status mappings, item/party/customer/supplier/VAT account mappings
E-Taxes Entities (cached from e-taxes):
- E-Taxes Item - Products/services from e-taxes with EQM codes
- E-Taxes Parties - All parties (legacy, for purchases)
- E-Taxes Suppliers - Supplier companies for purchases
- E-Taxes Customers - Customer companies for sales
- E-Taxes Unit - Units of measurement
- E-Taxes Item Group - Product group codes (fixtures)
- Classification code - Tax classification codes from e-taxes (code and description)
AMAS Entities (cached from AMAS):
- AMAS Employees - Employee records from AMAS (e-social.gov.az) with FIN, name, position, status
Mapping Tables:
- E-Taxes Item Mapping - Links ERPNext Items to e-taxes items
- E-Taxes Party Mapping - Links ERPNext parties to e-taxes parties (legacy)
- E-Taxes Supplier Mappings - Links ERPNext Suppliers to e-taxes suppliers
- E-Taxes Customer Mappings - Links ERPNext Customers to e-taxes customers
- E-Taxes Unit Mapping - Links ERPNext UOM to e-taxes units
- E-Taxes VAT Account Mapping - Maps VAT operation types to Chart of Accounts (debit/credit), with expense/income type and optional classification code filtering
Tracking Records:
- E-Taxes Purchase - Tracks imported purchase documents
- E-Taxes Sales - Tracks imported sales documents (from e-taxes)
- E-Taxes Sales Outbox - Tracks sent sales invoices (to e-taxes)
- E-Taxes Purchase Outbox - Tracks sent purchase acts (to e-taxes)
- E-Taxes VAT Operations - Tracks imported VAT operations
Key Features
E-Taxes Integration:
- Token Renewal: Automatic token renewal via scheduler (every 4 minutes) with activity check
- Token Validity Check: Verify token validity before API calls, auto-refresh on 401 errors
- Activity Tracking: Records user activity to optimize token renewals
- Entity Mapping: Maps ERPNext items, parties, suppliers, customers, and units to e-taxes equivalents
- Auto-Matching: Automatic matching of items, units, customers, suppliers by name similarity
- Bulk Operations: Support for syncing multiple entities at once
- Batch Processing: Load reference data from invoices in batches
- Error Handling: Comprehensive error logging and user-friendly error messages
- Azerbaijani Support: Character normalization for Ə, Ü, Ö, Ğ, İ, Ç, Ş characters
- Two-step Signing: Create draft invoice/act, then sign with ASAN Imza
- Retry Signing: Retry signing for failed/pending invoices/acts
- VAT Import: Import VAT account operations as Journal Entries with configurable account mappings
- VAT Account Mappings: Configure debit/credit accounts by operation type and expense/income type (Sub uçot hesabı → Sub uçot hesabı, Sub uçot hesabı → Büdcə, etc.)
- Smart Party Assignment: Automatically adds party (Customer/Supplier) only for Receivable/Payable accounts
- Purchase Acts: Send 6 types of purchase acts to e-taxes for Individual suppliers
- Supplier Data Fetching: Auto-populate Individual supplier data from e-taxes by FIN/passport
AMAS Integration:
- MyGovID OAuth: Automatic OAuth flow using existing MyGovID token
- Employee Import: Bulk import employees from AMAS to ERPNext
- Multi-Account Support: Switch between different company accounts in AMAS
- Session Management: Cookie-based session handling with automatic renewal
- Duplicate Detection: Prevents duplicate employee imports by FIN
- Status Tracking: Import employment status (Active/Inactive/Terminated)
API Endpoints
E-Taxes (https://new.e-taxes.gov.az):
/api/po/auth/public/v1/*- Authentication endpoints/api/po/invoice/public/v2/invoice/find.inbox- Purchase invoices/api/po/invoice/public/v2/invoice/find.outbox- Sales invoices (sent)/api/po/invoice/public/v2/invoice- Create invoice/api/po/invoice/public/v1/invoice/sign/withAsanImza- Sign invoice/api/po/invoice/public/v1/generateSerialNumber/defaultInvoice- Generate serial/api/po/invoice/public/v1/act- Create/manage acts/api/po/invoice/public/v1/act/generateSerialNumber/*- Generate act serials/api/po/invoice/public/v1/common/removeDrafts- Remove draft invoices/api/po/vatacc/public/v1/operation/find.outbox- VAT operations/api/po/profile/public/v1/taxpayer/findByFinAndPassport- Find taxpayer by FIN/api/po/profile/public/v1/taxpayer/{tin}/object/find- Customer objects/api/po/dictionary/public/v1/productGroups/find- Product groups dictionary
AMAS (https://eroom.e-social.gov.az):
/ssoAsan/v2- ASAN SSO callback/service/aas.changeAccount- Switch company accounts/service/magusbi.executeReport- Execute employee reports/service/aas.domains- Get available domains/request/accounts- Get available accounts
MyGovID (https://api.mygovid.gov.az):
/ssoauth/oauth2/auth/codes- Get authorization codes for OAuth
Fixtures
- E-Taxes Item Group - Pre-populated product group codes (for purchase acts)
- Classification code - Pre-populated tax classification codes from e-taxes (for VAT operations)
Install Hooks
after_install- Callssetup_token_renewal()andinstall_custom_fields()to configure scheduler and add custom fieldsafter_migrate- Callssetup_token_renewal(),create_journal_entry_custom_fields(), andcreate_supplier_custom_fields()to ensure scheduler and custom fields are up to date
Custom Fields Added:
- Purchase Invoice:
purchase_type,act_kind,product_group_code,tax_type,etaxes_status,etaxes_document_id,serial_number,seller_tin,buyer_tin - Sales Invoice:
customer_object_name,etaxes_status,etaxes_document_id,serial_number,seller_tin,buyer_tin - Journal Entry:
etaxes_vat_operation,customer_name_from_vat(for title display) - Supplier:
fin,passport_serial_number,date_of_birth,first_name,last_name,patronymic(for Individual suppliers and acts)
Security Considerations
- All API endpoints are whitelisted with
@frappe.whitelist() - Token storage uses Frappe's password field type
- Activity tracking prevents unnecessary API calls
- Retry logic includes exponential backoff to prevent API flooding
- Automatic token refresh on 401 errors
Important Files
Backend (Python):
invoice_az/hooks.py- App configuration, event hooks, scheduler setup, and fixtures (~310 lines)invoice_az/auth.py- Authentication and token management (~970 lines)invoice_az/api.py- Purchase invoice API and reference data management (~4460 lines)invoice_az/sales_api.py- Sales invoice import API (~890 lines)invoice_az/send_sales_api.py- Sales invoice send API (~1060 lines)invoice_az/send_purchase_api.py- Purchase acts send API (~800 lines)invoice_az/supplier_api.py- Supplier data fetching API (~150 lines)invoice_az/vat_api.py- VAT operations import API (~620 lines)invoice_az/vat_operations.py- VAT operations hooks (~40 lines)invoice_az/amas_api.py- AMAS integration API (~500 lines)invoice_az/install.py- Custom field installation
Frontend (JavaScript):
invoice_az/client/*.js- Client-side functionality for UI enhancementsinvoice_az/client/sales_invoice.js- Send sales invoices to e-taxesinvoice_az/client/purchase_invoice.js- Send purchase acts to e-taxesinvoice_az/client/supplier.js- Fetch supplier data from e-taxesinvoice_az/client/employee.js- Import employees from AMAS
Doctypes:
invoice_az/invoice_az/doctype/*/- Custom doctype definitions
Fixtures:
invoice_az/fixtures/e_taxes_item_group.json- Item group fixturesinvoice_az/fixtures/classification_code.json- Classification code fixtures
Workflow: Importing Purchases
- User opens Purchase Order/Invoice list
- Clicks "Import from E-Taxes" button
- System fetches invoices from e-taxes inbox
- User selects invoice to import
- System validates mappings (items, suppliers, units)
- Creates Purchase Order with items
- Creates E-Taxes Purchase tracking record
Workflow: Importing Sales (from E-Taxes)
- User opens Sales Order list
- Clicks "Import from E-Taxes" button
- System fetches invoices from e-taxes outbox
- User selects invoice to import
- System validates mappings (items, customers, units)
- Creates Sales Order and Sales Invoice
- Creates E-Taxes Sales tracking record
Workflow: Sending Sales Invoice to E-Taxes
- User creates and submits Sales Invoice
- Fills customer_object_name field (required for delivery address)
- Clicks "Send to E-Taxes" button
- System generates serial number
- Creates draft invoice on e-taxes
- User clicks "Sign Document" button
- System signs with ASAN Imza
- E-Taxes Sales Outbox record updated
Retry/Cancel:
- If signing fails, user can click "Retry Signing" button
- User can cancel draft invoice using "Cancel on E-Taxes" button
Workflow: Sending Purchase Acts to E-Taxes
Supported act types for Purchase Invoices with Individual suppliers:
-
Agricultural Products Act - For agricultural goods (kind: "agriculturalProductsAct")
- Tax types: "Tax Free" (0%) or "Taxable" (5%)
- Endpoint:
/api/po/invoice/public/v1/act/generateSerialNumber/agriculturalProductsAct
-
Metal Scrap Reception Act - For ferrous/non-ferrous metal scrap (kind: "metalProductsAct")
- Tax types: Only "Taxable" (5%) - tax-free items not allowed
- Validation: All items must have tax_type = "Taxable"
- Endpoint:
/api/po/invoice/public/v1/act/generateSerialNumber/metalProductsAct
-
Tire Products For Disposal Act - For tire disposal operations (kind: "tireProductsForDisposalAct")
- Tax types: Only "Taxable" (5%) - tax-free items not allowed
- Validation: All items must have tax_type = "Taxable"
- Endpoint:
/api/po/invoice/public/v1/act/generateSerialNumber/tireProductsForDisposalAct
-
Plastic Products For Disposal Act - For plastic disposal operations (kind: "plasticProductsForDisposalAct")
- Tax types: Only "Taxable" (5%) - tax-free items not allowed
- Validation: All items must have tax_type = "Taxable"
- Endpoint:
/api/po/invoice/public/v1/act/generateSerialNumber/plasticProductsForDisposalAct
-
Rawhide Supply - For rawhide supply operations (kind: "rawhideSupply")
- Tax types: Only "Taxable" (5%) - tax-free items not allowed
- Validation: All items must have tax_type = "Taxable"
- Endpoint:
/api/po/invoice/public/v1/act/generateSerialNumber/rawhideSupply
-
Other Product Receipt Act - For other product receipts (kind: "otherProductReceiptAct")
- Tax types: "Tax Free" (0%) or "Taxable" (5%) - both allowed
- Validation: No tax type restrictions (same as Agricultural)
- Endpoint:
/api/po/invoice/public/v1/act/generateSerialNumber/otherProductReceiptAct
Workflow:
- User creates and submits Purchase Invoice
- Selects Individual supplier (with FIN, passport, DOB)
- Checks
purchase_typecheckbox to enable act fields - Selects
act_kind(one of 6 available types) - Adds items with product_group_code and tax_type
- Clicks "Send Act to E-Taxes" button (button text changes based on act_kind)
- System generates serial number from appropriate endpoint
- Creates draft act on e-taxes
- User clicks "Sign Act with ASAN Imza" button
- System signs with ASAN Imza
- E-Taxes Purchase Outbox record updated
Retry/Cancel:
- If signing fails, user can click "Retry Signing" button
- User can cancel draft act using "Cancel Act on E-Taxes" button
Workflow: Importing VAT Operations
- User opens Journal Entry list
- Clicks "Import VAT from E-Taxes" button
- Selects date range
- System fetches all VAT operations (both income and expense types)
- System shows operation selection dialog with columns:
- Date, TIN, Name, Operation Type, Classification Code, Amount
- Creates Journal Entries using account mappings:
- API returns technical operation types (SUB_TO_SUB, etc.) which are mapped to Azerbaijani names
- API returns taxCodeInfo with code (e.g., "114117") and description
- Looks up mapping in E-Taxes Settings with priority:
- First: operation_type + classification_code (if taxCodeInfo.code exists)
- Second: operation_type + empty classification_code (default mapping)
- If mapping not found, operation fails with error (shown in error log)
- Automatically adds party (Customer) only if account type is Receivable/Payable
- Supports different debit/credit accounts for different operation types and classification codes
- Creates E-Taxes VAT Operations tracking records
VAT Account Mapping Configuration:
- Configure in E-Taxes Settings > VAT Account Mappings tab
- Each mapping specifies: operation type, expense/income type (required), classification code (optional), debit account, credit account
- Expense/Income field: Select "Expense" or "Income" to distinguish between expense and income operations
- Classification Code field links to Classification code doctype
- Uniqueness: Each combination of operation_type + expense_income + classification_code must be unique
- Priority logic:
- System determines expense/income type from API data (income > 0 → Income, expense > 0 → Expense)
- If classification_code is set: mapping applies only to operations with matching taxCodeInfo.code
- If classification_code is empty: mapping is default for this operation type
- System searches with priority: specific classification_code first, then default (empty)
- All searches include expense_income filter
- Supported operation types:
- Sub uçot hesabı → Sub uçot hesabı (SUB_TO_SUB in API)
- Naməlum → digər Sub hesab (UNKNOWN_TO_OTHER_SUB in API)
- Cari → Sub uçot hesabı (CURRENT_TO_SUB in API)
- Cari → Naməlum (CURRENT_TO_UNKNOWN in API)
- Sub uçot hesabı → İdxal (SUB_TO_IMPORT in API)
- Naməlum → İdxal (UNKNOWN_TO_IMPORT in API)
- Sub uçot hesabı → Büdcə (SUB_TO_BUDGET and AUTO in API)
- Naməlum → Büdcə (UNKNOWN_TO_BUDGET in API)
Note: AUTO operation types from API are automatically mapped to "Sub uçot hesabı → Büdcə" (SUB_TO_BUDGET).
Workflow: Importing Employees from AMAS
AMAS (e-social.gov.az) is Azerbaijan's Employment Management System maintained by the Ministry of Labour and Social Protection.
Prerequisites:
- User must have MyGovID authentication (same as ASAN Login)
- AMAS connection must be established in Asan Login
Workflow:
- User opens Asan Login
- Clicks "Connect to AMAS" button (uses existing MyGovID token)
- System performs OAuth flow:
- Gets authorization code from MyGovID API
- Exchanges code for AMAS session
- Stores AMAS cookies in Asan Login
- User opens Employee list
- Clicks "Load from AMAS" button
- System fetches employee list from AMAS (up to 1000 records)
- System displays employee selection dialog with:
- FIN, Full Name, Position, Employment Status
- User selects employees to import
- System creates/updates ERPNext Employee records
- Creates AMAS Employees tracking records
Key Features:
- Automatic MyGovID authentication reuse
- Multi-account support (can switch between company accounts in AMAS)
- Pagination support for large employee lists
- Duplicate detection (checks existing employees by FIN)
- Status tracking (Active/Inactive/Terminated)
AMAS API Endpoints:
- Base URL:
https://eroom.e-social.gov.az - SSO Callback:
/ssoAsan/v2 - Execute Report:
/service/magusbi.executeReport - Change Account:
/service/aas.changeAccount - Get Accounts:
/request/accounts
MyGovID OAuth:
- API URL:
https://api.mygovid.gov.az - Auth Codes:
/ssoauth/oauth2/auth/codes - Client ID:
64942e33ec8d49059333a1e0ebad7fe2
Workflow: Fetching Supplier Data from E-Taxes
For Individual suppliers, the system can auto-populate supplier information from E-Taxes.
Prerequisites:
- Supplier must be type "Individual"
- FIN field must be filled
- Passport Serial Number field must be filled
Workflow:
- User creates new Supplier (type: Individual)
- Fills FIN and Passport Serial Number
- Clicks "Fetch Data from E-Taxes" button
- System calls
/api/po/profile/public/v1/taxpayer/findByFinAndPassport - System auto-populates fields:
- Full Name
- Phone
- Date of Birth
- First Name
- Last Name
- Patronymic
- User can now use this supplier for Purchase Invoices with acts