Add FIFO/Moving Average picker; add CLAUDE.md

- Company step gains a "Stock Valuation" select (FIFO default, Moving
  Average option). Value is passed through build_setup_args and applied
  to Company.valuation_method in materialize_after_setup — Company-level
  wins over Stock Settings in get_valuation_method, so inventory posts
  use the choice immediately.
- Cannot be changed after first stock entry (helper text + ERPNext
  validation), so this is the only window to set it cleanly.
- CLAUDE.md added to document the architecture for future sessions
  (override trick, two separate auth flows, cache doctype, rollback
  mechanics).
This commit is contained in:
Ali 2026-04-24 13:01:18 +00:00
parent db43231927
commit 3137fc3caf
6 changed files with 113 additions and 2 deletions

80
CLAUDE.md Normal file
View File

@ -0,0 +1,80 @@
# 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``chooseTaxpayer` consumes bearer, returns `main_token`. The `bearer_token` is **single-use per chooseTaxpayer call**. Switching taxpayer requires another phone tap (`auth.py:526` returns 401 otherwise). `invoice_az.client.etaxes_common.js` follows 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_request` handles 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` / `obligation_pacts_json` / `presented_certs_json` — written by `etaxes.py:fetch_all_etaxes` during the Asan step, **not read back** on finalize (see next point).
- `amas_selected_employees_json` — written by `amas.py:cache_selected_employees`, read by `etaxes.py:_materialize_amas_employees` in the `setup_wizard_complete` hook.
- `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.enqueue`s 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
```bash
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`)
```bash
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/ru.csv` and `translations/az.csv` (plain two-column CSV, quote fields with commas). Switching language on step 1 calls `frappe.desk.page.setup_wizard.setup_wizard.load_messages`, which repopulates `frappe._messages` so subsequent `__()` calls render in the new language. The constructor does this automatically on boot if `frappe.boot.lang` differs from the default (`az`).
## Gotchas
- **Admin-only.** `_only_admin()` in `api.py` / `etaxes.py` / `amas.py` throws unless `frappe.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()` in `jey_setup.js`.
- **Placement of JS.** `public/js/jey_setup.js` is served raw via `setup_wizard_requires`. There is no bundler; `bench build` just symlinks `public/` to `sites/assets/jey_wizard/`.

View File

@ -1 +1 @@
__version__ = "0.1.2"
__version__ = "0.1.3"

View File

@ -168,6 +168,14 @@ def materialize_after_setup(args):
)
return
# Apply inventory valuation method before any stock loaders touch the Company.
# Company-level wins over Stock Settings in erpnext.stock.utils.get_valuation_method.
chosen_vm = (args or {}).get("valuation_method") or "FIFO"
if chosen_vm in ("FIFO", "Moving Average"):
frappe.db.set_value(
"Company", company_name, "valuation_method", chosen_vm, update_modified=False
)
from invoice_az import company_api
loaders = [

View File

@ -10,7 +10,7 @@ frappe.provide("jey_wizard");
// Bump this string in every commit that changes wizard code. Displayed in the badge so
// we can tell at a glance which version is actually running on a given machine. Kept in
// sync with __version__ in jey_wizard/__init__.py.
const JEY_WIZARD_VERSION = "0.1.2";
const JEY_WIZARD_VERSION = "0.1.3";
// Wipe Frappe + ERPNext default slides so their `before_load`/`after_load` listeners
// don't try to mutate a wizard that isn't slide-based anymore.
@ -50,6 +50,7 @@ frappe.setup.SetupWizard = class JeySetupWizard {
company_name: "",
fy_start_date: `${year}-01-01`,
fy_end_date: `${year}-12-31`,
valuation_method: "FIFO",
// AMAS
amas_skipped: false,
amas_selected_employees: [],
@ -402,10 +403,20 @@ frappe.setup.SetupWizard = class JeySetupWizard {
</div>
</div>
<div style="color:#888;font-size:12px;margin-top:6px">${__("Default: current calendar year (Jan 1 Dec 31).")}</div>
<h4 style="margin-top:20px;margin-bottom:10px">${__("Stock Valuation")}</h4>
<div style="max-width:280px">
<label style="display:block;margin-bottom:4px">${__("Valuation method")}</label>
<select class="jey-valuation-method form-control">
<option value="FIFO">FIFO</option>
<option value="Moving Average">${__("Moving Average")}</option>
</select>
<div style="color:#888;font-size:12px;margin-top:6px">${__("Cannot be changed after the first stock entry.")}</div>
</div>
`);
$body.find(".jey-company-name").val(this.data.company_name);
$body.find(".jey-fy-start").val(this.data.fy_start_date);
$body.find(".jey-fy-end").val(this.data.fy_end_date);
$body.find(".jey-valuation-method").val(this.data.valuation_method || "FIFO");
}
render_confirm($body) {
@ -422,6 +433,7 @@ frappe.setup.SetupWizard = class JeySetupWizard {
? `<li><b>${__("VÖEN")}:</b> ${frappe.utils.escape_html(this.data.asan_voen)}</li>`
: ""}
<li><b>${__("Fiscal Year")}:</b> ${frappe.utils.escape_html(this.data.fy_start_date)} ${frappe.utils.escape_html(this.data.fy_end_date)}</li>
<li><b>${__("Valuation method")}:</b> ${this.data.valuation_method === "Moving Average" ? __("Moving Average") : "FIFO"}</li>
<li><b>${__("ƏMAS employees")}:</b> ${empRow}</li>
</ul>
`);
@ -493,6 +505,8 @@ frappe.setup.SetupWizard = class JeySetupWizard {
}
this.data.fy_start_date = fyStart;
this.data.fy_end_date = fyEnd;
const vm = (this.$wrap.find(".jey-valuation-method").val() || "FIFO").trim();
this.data.valuation_method = vm === "Moving Average" ? "Moving Average" : "FIFO";
}
return true;
}
@ -1051,6 +1065,7 @@ frappe.setup.SetupWizard = class JeySetupWizard {
chart_of_accounts: "Azerbaijan Republic",
fy_start_date: this.data.fy_start_date,
fy_end_date: this.data.fy_end_date,
valuation_method: this.data.valuation_method || "FIFO",
setup_demo: 0,
};
}

View File

@ -81,6 +81,10 @@ Employees selected,İşçilər seçildi
Company name,Şirkət adı
VÖEN {0} (from Asan),VÖEN {0} (Asan-dan)
Fiscal Year,Maliyyə ili
Stock Valuation,Ehtiyatların qiymətləndirilməsi
Valuation method,Qiymətləndirmə metodu
Moving Average,Hərəkətli orta
Cannot be changed after the first stock entry.,İlk anbar əməliyyatından sonra dəyişdirmək mümkün deyil.
Start date,Başlama tarixi
End date,Bitmə tarixi
"Default: current calendar year (Jan 1 Dec 31).","Standart: cari təqvim ili (1 yanvar — 31 dekabr)."

1 Step {0} of {1} Addım {0} / {1}
81 Company name Şirkət adı
82 VÖEN {0} (from Asan) VÖEN {0} (Asan-dan)
83 Fiscal Year Maliyyə ili
84 Stock Valuation Ehtiyatların qiymətləndirilməsi
85 Valuation method Qiymətləndirmə metodu
86 Moving Average Hərəkətli orta
87 Cannot be changed after the first stock entry. İlk anbar əməliyyatından sonra dəyişdirmək mümkün deyil.
88 Start date Başlama tarixi
89 End date Bitmə tarixi
90 Default: current calendar year (Jan 1 – Dec 31). Standart: cari təqvim ili (1 yanvar — 31 dekabr).

View File

@ -81,6 +81,10 @@ Employees selected,Сотрудники выбраны
Company name,Название компании
VÖEN {0} (from Asan),ВÖЕН {0} (из Asan)
Fiscal Year,Финансовый год
Stock Valuation,Оценка запасов
Valuation method,Метод оценки
Moving Average,Скользящая средняя
Cannot be changed after the first stock entry.,Не может быть изменён после первой складской операции.
Start date,Дата начала
End date,Дата окончания
"Default: current calendar year (Jan 1 Dec 31).","По умолчанию: текущий календарный год (1 января — 31 декабря)."

1 Step {0} of {1} Шаг {0} из {1}
81 Company name Название компании
82 VÖEN {0} (from Asan) ВÖЕН {0} (из Asan)
83 Fiscal Year Финансовый год
84 Stock Valuation Оценка запасов
85 Valuation method Метод оценки
86 Moving Average Скользящая средняя
87 Cannot be changed after the first stock entry. Не может быть изменён после первой складской операции.
88 Start date Дата начала
89 End date Дата окончания
90 Default: current calendar year (Jan 1 – Dec 31). По умолчанию: текущий календарный год (1 января — 31 декабря).