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:
parent
db43231927
commit
3137fc3caf
|
|
@ -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/`.
|
||||
|
|
@ -1 +1 @@
|
|||
__version__ = "0.1.2"
|
||||
__version__ = "0.1.3"
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)."
|
||||
|
|
|
|||
|
|
|
@ -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 декабря)."
|
||||
|
|
|
|||
|
Loading…
Reference in New Issue