9.0 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
What this app does
jey_wizard replaces Frappe's stock setup wizard with a fixed flow tailored for Azerbaijani tenants. The flow is hard-wired to: language → asan → employees → company → confirm. Steps past Asan Imza authentication assume the company is Azerbaijani (country/currency/timezone/CoA are hardcoded in build_setup_args) and that the installer wants e-taxes + ƏMAS data pulled and materialised as part of setup.
Depends on invoice_az, declared in hooks.py:required_apps. The wizard does not re-implement e-taxes or ƏMAS integration — it thinly wraps invoice_az.auth.*, invoice_az.amas_api.* and invoice_az.company_api.*. Read those modules first before touching the flow.
Architecture
Stock wizard override (one big trick)
hooks.py:setup_wizard_requires loads jey_wizard/public/js/jey_setup.js inside /app/setup-wizard. At the top of that file we overwrite frappe.setup.SetupWizard (the class) and wipe frappe.setup.slides / slides_settings / events so ERPNext's default before_load/after_load hooks don't mutate a wizard that is no longer slide-based. Frappe's page handler then calls new frappe.setup.SetupWizard(wizard_settings) and gets our class instead. There is no slide system — we drive navigation manually with a current_step index and per-step sub-state machines (asan_state, amas_state).
On finalize we call the unmodified frappe.desk.page.setup_wizard.setup_wizard.setup_complete with a full args dict (see build_setup_args in jey_setup.js). Staying on that codepath keeps typing_validations / sanitize_input / setup_complete hooks working.
Two authentication systems, two phone taps
e-taxes and ƏMAS are separate auth flows — this is not a limitation of our code, it's how the Azerbaijani services work:
- e-taxes (Asan Imza direct):
asanImza/start→ SMS → user tap →bearer_token→chooseTaxpayerconsumes bearer, returnsmain_token. Thebearer_tokenis single-use per chooseTaxpayer call. Switching taxpayer requires another phone tap (auth.py:526returns 401 otherwise).invoice_az.client.etaxes_common.jsfollows the same pattern. - ƏMAS (via MyGovID):
asanSignLogin→ SMS → user tap →mygovid_token(JWT) → OAuth code exchange → cookie session. AMAS requires a fresh CSRF token before every request;invoice_az.amas_api.make_amas_requesthandles that.
The UI warns about the second tap on the employees step intro. Do not try to unify the two — there is no e-taxes MyGovID OAuth path in the current code.
Cache doctype: Jey Wizard Etaxes Cache
Single doctype that carries state between wizard steps and setup_wizard_complete:
objects_json/cash_registers_json/pos_terminals_json/bank_accounts_json/presented_certs_json— written byetaxes.py:fetch_all_etaxesduring the Asan step, not read back on finalize (see next point).amas_selected_employees_json— written byamas.py:cache_selected_employees, read byetaxes.py:_materialize_amas_employeesin thesetup_wizard_completehook.last_summary/last_error/fetched_at— displayed on the Asan done screen.
Schema changes to this doctype require bench migrate.
Two-phase e-taxes materialisation
During Asan step we fetch the 6 e-taxes lists into the cache doctype. On setup_wizard_complete (etaxes.py:materialize_after_setup) we call invoice_az.company_api.load_company_* — these re-fetch from e-taxes and write to real DocTypes (E-Taxes Object, E-Taxes Cash Register, …) linked to the freshly-created Company. The mid-wizard cache exists so future wizard steps can consume the data before Company exists; the loaders are invoice_az's authoritative path. Don't try to read from the cache on finalize.
AMAS employees go through invoice_az.amas_api.import_bulk_employees which frappe.enqueues a background job with realtime progress (events amas_import_progress / amas_import_complete). The wizard fires-and-forgets — the user lands in /app while employees continue importing.
"Wrong company? Change" rollback
Clicking rollback on asan/done calls api.py:reset_company_selection which wipes bearer_token, main_token, certificates_json, selected_certificate*, verification_code, sets auth_status="Not Authenticated", and empties the cache doctype. The JS then calls trigger_asan_authentication again, which sends a fresh SMS. Re-using the consumed bearer_token is what caused the historical 401 "Authentication required" bug — do not skip the backend reset.
Common commands
From any bench site
bench --site <site> migrate # pick up schema changes to Jey Wizard Etaxes Cache
bench build --app jey_wizard # rebuild /assets/jey_wizard/js/jey_setup.js
bench restart # reload Python changes
Pre-commit (from apps/jey_wizard)
pre-commit install
pre-commit run --all-files
Configured tools: ruff, eslint, prettier, pyupgrade.
Version badge
jey_wizard/__init__.py:__version__ and JEY_WIZARD_VERSION in jey_setup.js are kept in sync and bumped on every commit that changes wizard code. The badge in the top-right of the wizard UI reads this string — it's how you tell at a glance which revision is actually loaded on a given site.
Translations
Translations live in gettext PO files under jey_wizard/locale/ (main.pot is the extracted template; az.po / ru.po carry the strings). The legacy translations/*.csv are gone — Frappe v16 still reads CSV, but PO/MO is the modern path and overrides it.
Source of truth is the az_locale app, not this repo. az_locale/locale/translations_az/jey_wizard/az.po and translations_ru/jey_wizard/ru.po are the masters. On every bench migrate, az_locale's after_migrate → setup_locale() copies those over jey_wizard/locale/*.po and force-compiles the runtime .mo into sites/assets/locale/<lang>/LC_MESSAGES/jey_wizard.mo. So: edit the PO in az_locale (or here, then mirror to az_locale), never hand-edit the .mo.
jey_wizard/locale/*.po (az.po/ru.po) are git-ignored, not committed. Because setup_locale() overwrites them on every migrate, tracking them dirtied the working tree and made git pull abort ("local changes would be overwritten by merge"). They're now untracked via .gitignore (jey_wizard/locale/*.po); only main.pot stays tracked. The on-disk copies still exist (regenerated each migrate) — just don't try to commit them, and don't rely on their git history. Same treatment is applied across the other az_locale-managed apps (invoice_az, taxes_az, kapital_bank, jey_theme, jey_erp).
Workflow to add/refresh strings:
bench generate-pot-file --app jey_wizard— re-extract__()(JS) +_()(Python) + DocType labels intolocale/main.pot.bench update-po-files --app jey_wizard— fold new msgids intoaz.po/ru.po(empty msgstr = falls back to the English source; fine for proper nouns likeIBAN,ƏMAS,Stores).- Fill the empty
msgstr, copy both PO intoaz_locale(the masters), then compile + activate. Options:bench migrate/az_locale.locale.setup_locale.setup_localerecompiles all apps — but that also re-copies every app's masters, clobbering any uncommitted PO edits in other apps. To recompile just this app, runbench --site <site> execute frappe.gettext.translate.compile_translations --kwargs "{'target_app': 'jey_wizard', 'force': True}"(kwargs is eval'd as Python — useTrue, nottrue). Either way,bench clear-cacheto drop themerged_translationsRedis cache.
msgstrreferenced only via a Python constant (e.g._(SOME_VAR)) is not extracted bygenerate-pot-file— add that msgid to the PO by hand. And server-side_()renders in the session user's language, so anazuser sees Azerbaijani even when the site default isen.
Switching language on step 1 calls frappe.desk.page.setup_wizard.setup_wizard.load_messages → get_messages_for_boot() → get_all_translations(lang), which merges every app's CSV and MO, so the new PO strings render after compile. The constructor does this automatically on boot if frappe.boot.lang differs from the default (az).
Gotchas
- Admin-only.
_only_admin()inapi.py/etaxes.py/amas.pythrows unlessfrappe.session.user == "Administrator". The wizard is not designed for any other user. - Phone input. UI collects a 2-digit carrier prefix (values
50/51/10/55/60/70/77/99) + 7-digit local number. On submit we assemble+994{prefix}{local}. Don't strip the+— MyGovID accepts it; invoice_az stores it verbatim. - Back navigation. Within a sub-state machine, Back rewinds to intro/input first; only Back from intro/done crosses a wizard-step boundary. See
prev()injey_setup.js. - Placement of JS.
public/js/jey_setup.jsis served raw viasetup_wizard_requires. There is no bundler;bench buildjust symlinkspublic/tosites/assets/jey_wizard/.