From 3137fc3cafe9362e503564aed315dd382f78beee Mon Sep 17 00:00:00 2001 From: Ali <010109ali@gmail.com> Date: Fri, 24 Apr 2026 13:01:18 +0000 Subject: [PATCH] Add FIFO/Moving Average picker; add CLAUDE.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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). --- CLAUDE.md | 80 +++++++++++++++++++++++++++++++ jey_wizard/__init__.py | 2 +- jey_wizard/etaxes.py | 8 ++++ jey_wizard/public/js/jey_setup.js | 17 ++++++- jey_wizard/translations/az.csv | 4 ++ jey_wizard/translations/ru.csv | 4 ++ 6 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..a2cdca2 --- /dev/null +++ b/CLAUDE.md @@ -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 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/`. diff --git a/jey_wizard/__init__.py b/jey_wizard/__init__.py index b3f4756..ae73625 100644 --- a/jey_wizard/__init__.py +++ b/jey_wizard/__init__.py @@ -1 +1 @@ -__version__ = "0.1.2" +__version__ = "0.1.3" diff --git a/jey_wizard/etaxes.py b/jey_wizard/etaxes.py index 475e785..11231eb 100644 --- a/jey_wizard/etaxes.py +++ b/jey_wizard/etaxes.py @@ -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 = [ diff --git a/jey_wizard/public/js/jey_setup.js b/jey_wizard/public/js/jey_setup.js index c8c113c..17c12a8 100644 --- a/jey_wizard/public/js/jey_setup.js +++ b/jey_wizard/public/js/jey_setup.js @@ -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 {
${__("Default: current calendar year (Jan 1 – Dec 31).")}
+

${__("Stock Valuation")}

+
+ + +
${__("Cannot be changed after the first stock entry.")}
+
`); $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 { ? `
  • ${__("VÖEN")}: ${frappe.utils.escape_html(this.data.asan_voen)}
  • ` : ""}
  • ${__("Fiscal Year")}: ${frappe.utils.escape_html(this.data.fy_start_date)} — ${frappe.utils.escape_html(this.data.fy_end_date)}
  • +
  • ${__("Valuation method")}: ${this.data.valuation_method === "Moving Average" ? __("Moving Average") : "FIFO"}
  • ${__("ƏMAS employees")}: ${empRow}
  • `); @@ -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, }; } diff --git a/jey_wizard/translations/az.csv b/jey_wizard/translations/az.csv index bbf6ec4..113a4b1 100644 --- a/jey_wizard/translations/az.csv +++ b/jey_wizard/translations/az.csv @@ -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)." diff --git a/jey_wizard/translations/ru.csv b/jey_wizard/translations/ru.csv index ea96013..9525912 100644 --- a/jey_wizard/translations/ru.csv +++ b/jey_wizard/translations/ru.csv @@ -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 декабря)."