# Common Recipes > This file is imported into `CLAUDE.md` via `@reference/recipes.md`, so everything > here loads into context exactly as if it were inline. It was split out only to keep > `CLAUDE.md` under its size limit. **Add new recipes here, not in `CLAUDE.md`.** Living reference for non-obvious patterns — extend this section whenever a pattern would otherwise have to be rediscovered by reading code or trial and error. If you're about to write custom plumbing that another file already solves, look here first. > Note: recipe paths that read `components/…`, `pages/…`, `composables/…`, > `assets/…` live under `app/` (Nuxt 4 srcDir). The `~/` and `@/` aliases > still resolve them unchanged; only bare relative/OS paths gained the `app/` prefix. ### Get the c_bpartner linked to an ad_org Every organization has a linked business partner (`ad_org.C_BPartner_ID`). Use the dedicated endpoint — do NOT re-resolve via `models/ad_org/{id}` plus a second filter call. ```typescript // Returns the full c_bpartner record (all fields, no $select) const bpartner = await $fetch(`/api/admin/organizations/${orgId}/bpartner`, { headers: useRequestHeaders(['cookie']) }) ``` Server route: `server/api/admin/organizations/[id]/bpartner.get.ts` ### Hourly rate for billing (tasks / requests) Field on `c_bpartner`: **`fulfillment_service_hourly_rate`** (number, EUR). Standard fallback chain used across the app: 1. request's own `C_BPartner_ID` 2. the org's linked c_bpartner (recipe above) 3. `33` EUR hard fallback Reference implementations: - Desktop: `components/requests/RequestForm.vue` → `fetchHourlyRate()` - Mobile: `pages/mobile/tasks.vue` → `resolveOrgBpartner()` + `extractHourlyRate()` When reading from a c_bpartner record, check multiple case variants (`fulfillment_service_hourly_rate`, `Fulfillment_Service_Hourly_Rate`) because iDempiere's OData response casing varies by model. ### Appending time-billing notes to a request When billing time (either via desktop "Zeit abrechnen" or mobile task stop), append a `[Zeitabrechnung]` block alongside `[START:]` / `[END:]` / `[DURATION:]` so how the amount was calculated is recorded on the request itself: ``` [Zeitabrechnung] Zeit erfasst: 1h 15min Stundensatz: 45,00 €/h Berechneter Betrag: 56,25 € ``` Desktop writes this to a chat `request-update` entry; mobile appends it to the request's `summary` field (no chat on mobile). ### iDempiere boolean flags with a DB-side default When a `C_*` column has a DB default (e.g. `C_Order.isFulfillmentOrder` defaults to `true`), do NOT send the field on every create call. Send it only when the user picks the non-default value — saves payload bytes and keeps the DB default authoritative for any flow that doesn't know about the field yet. Pattern in `*/store.post.ts` (or any creator route): ```typescript // Only send when explicitly false — DB column defaults to true. if (body.isFulfillmentOrder === false) { newObjValue = { ...newObjValue, isFulfillmentOrder: false } } ``` For background creators that should set the non-default value (e.g. `server/api/fulfillment/generate-orders.post.ts` creates bookkeeping orders that must NOT be treated as fulfilment), inline the override directly on the payload: ```typescript orderPayload = { AD_Org_ID: organizationId, IsSOTrx: true, isFulfillmentOrder: false, // … } ``` Key always lowercase-first on writes (`isFulfillmentOrder`, not `IsFulfillmentOrder`) — see the existing memory note. On reads, accept both casings because iDempiere's OData response varies by model: ```typescript isFulfillmentOrder: res?.isFulfillmentOrder ?? res?.IsFulfillmentOrder ?? true, ``` ### Partner reminder notes — auto-popup on order open `C_BPartner.PartnerInvoiceReminderNotes` is a per-partner HTML field (``) that staff use as a manual checklist when finalising invoice-type orders. The reminder modal auto-opens on `pages/sales/orders/[id]/{view,edit}.vue` when ALL of: - `getMenuType() !== 'c'` (non-limited role) - `form.value.isFulfillmentOrder === false` - `docStatusId` ∉ {`CO`, `VO`, `CL`} - partner has non-empty notes Cached on the page in `reminderNotesContent` so the manual "🔔 Erinnerungen" button can re-open the modal without another partner fetch. Components: - `components/sales/ReminderNotesModal.vue` — bound to `useReminderNotes()` / `useReminderNotesParam()` (see `composables/states.ts`) - `components/partners/PartnerReminderNotesEditor.vue` — list editor with `+` / reorder / remove. Editor parses both HTML (`
  • `) and legacy newline-separated plain text; always writes back as ``. The editor is dropped into the customer-tab section of `PartnerForm.vue`. Persisted via `addIfChanged('partnerInvoiceReminderNotes', 'partnerInvoiceReminderNotes')` in `server/api/partners/partners/update.put.ts` and read tolerantly: `res?.partnerInvoiceReminderNotes || res?.PartnerInvoiceReminderNotes || ''`. ### Adding a new editable field to the Business Partner edit form A new `C_BPartner` column becomes editable on `/partners/partners/[id]/edit` only after it is wired through **four** places (the edit page does NOT pass the form through generically — it explicitly maps every field three times). Miss one and the field silently won't load or won't save. 1. **`components/partners/PartnerForm.vue`** — the shared form UI. - Add the key(s) to the `form = ref({...})` init (`fooBar: props.modelValue.fooBar ?? …`). For an FK add both `fooBar` (identifier string) and `fooBarId`. - Render the control in the right tab. Booleans → an `ff-toggle-card` in the fulfillment tab's *Feature Flags* section; FKs → a `SelectBox` mirroring the `models/ad_org` select (`@click="setOptions('models/', '')"`, `@change="(val, opt) => {form.foo = opt.Name; save()}"`). Every control calls `save()`. 2. **`pages/partners/partners/[id]/edit.vue`** — three edits: - add the key(s) to its own `form = ref({...})`; - map them in `retrieve()` (`res?.Foo_ID?.identifier`/`.id`, or for a Yes/No flag parse tolerantly: `(res?.IsFoo ?? res?.isFoo) === true || … === 'Y'`); - add them to the `fieldMapping` in `getChangedFields()` (FK → list the `…Id` key; this is what actually gets sent on save). 3. **`server/api/partners/partners/update.put.ts`** — `addIfChanged('fooBar','fooBar')` for scalars/booleans (**lowercase-first key** per the iDempiere write-casing rule, even when the DB column is PascalCase), or `addRefIfChanged('fooBarId','Foo_ID','FooTable')` for FKs (sending `''` clears it to `0`). 4. **`assets/langs/shared.js`** — add label/tooltip keys to en-US / de-DE / es-ES. `store.post.ts` is OPTIONAL for parity (create-path is robust), but **`create.vue` is deliberately NOT wired** for detailed fulfillment fields — it sends only a subset at creation, so those fields are edit-only (the tab still renders on create, just won't persist). The custom read-only `[id]/view.vue` does not use `PartnerForm` and is intentionally left out. Example (this is how `Accounting_AD_Org_ID`, an `ad_org` FK in the *Accounting Assignment* sub-head of the LogShip-account section, and `IsEditProductImage`, a *Feature Flags* toggle that lets a limited-role customer manage product images — read by `pages/materials/products/[id]/edit.vue` to expose the Gallery tab — were added). ### Adding a new `FulfillTypeAccounting` to the fulfillment invoice generator `server/api/fulfillment/generate-orders.post.ts` fetches all `cust_fulfillmentfeeline` records with `A_Processed eq false` and routes each line through a per-type `if / else if` chain. **A line with an unhandled `FulfillTypeAccounting` value is silently skipped but still marked `a_processed='Y'` at the end** (`batch-process-lines` sweeps every fetched line ID regardless), so missing a branch = silent data loss on the invoice. Always extend the chain when a new type appears in the table. Currently handled types (in branch order): `fulfillment`, `return`, `spacerentqm3`, `spacerentflat`, `parcel`, `subscription`, `shippingfee`, `request`. Two billing shapes are in use: - **Aggregated** (default — `fulfillment`/`return`/`parcel`/etc.): bucket lines by `M_Product_ID` into a keyed object, sum `LineTotalAmt`, emit one order line per product with a summary description like `${t.parcel}: ${qty} ${t.parcels}`. - **Per-entry** (`request`): push each fee line into a flat array, emit one order line per fee line, copy `line.Description` verbatim onto the order line so the ticket reference stays on the invoice. Adding a type touches **six** places in `generate-orders.post.ts`: 1. Bucket declaration near `const fulfillmentLines: any = {}` (keyed object for aggregated, `any[] = []` for per-entry). 2. New `else if (line.FulfillTypeAccounting === 'X')` branch in the grouping loop (around the existing `shippingfee` branch). 3. `orderLines.push({...})` loop — the actual iDempiere `c_orderline` payload. 4. `previewOrderLines.push({...})` loop (used for the preview modal). 5. `responseOrderLines.push({...})` loop (post-create API response). 6. `reportOrderLines.push({...})` loop (PDF report generation). If you add a translation label for the description, also update `t.X` / `t.Xs` (singular/plural) across all four language blocks in `assets/langs/shared.js`. Mirror the new branch in `server/api/fulfillment/test-grouping.post.ts` (diagnostic endpoint — same grouping, no order creation). `components/fulfillment/FulfillmentGenerateOrdersModal.vue` already falls back to a 4-column detail layout (Fee Line ID + Date + Qty + Unit Price + Amount) for any `line.type` it doesn't have an explicit branch for, so a per-entry type needs no UI change unless it has order/shipment links worth surfacing. ### Nuxt 4 + Nuxt UI v4 conventions (post-migration) The app was migrated from Nuxt 3 (flat layout) to **Nuxt 4** with the idiomatic `app/` srcDir and **Nuxt UI v4**. Key things that bite if forgotten: - **srcDir is `app/`.** `~`/`@` → `app/`; `~~`/`@@` → repo root. `server/`, `public/`, `data/`, `scripts/` and configs stay at root. When importing a root-level file from inside `app/`, use `~~/…` (e.g. `~~/node_modules/...`) — though for node_modules CSS prefer **bare specifiers** (`'bulma/css/…'`). - **Reference public assets as `/assets/…`, never `/public/assets/…`.** `public/` is served at the URL root; the `/public/` prefix 404s at runtime AND breaks the production build (Rollup tries to resolve it as a module). - **Custom router config** lives at `app/router.options.ts` (Nuxt-conventional). Its dynamic page imports use the `~/pages/...` alias (not relative `../pages`). `app/forms/*.ts` and `app/windows/index.vue` are custom helpers it imports. - **Nuxt UI v4:** single `@nuxt/ui` module (no `@nuxt/ui-pro`); Pro components (e.g. `UFooter`) need the `NUXT_UI_PRO_LICENSE` env (set in `.env*`). `` now provides the tooltip context — there is no ``. Theme is `app.config.ts` → `ui: { colors: { primary, neutral } }` (the old `gray` key is now `neutral`; `'cool'`/other UIv2 aliases are invalid — use a Tailwind color like `'slate'`). On components, `color="gray"` → `color="neutral"`. - **Fonts:** `@nuxt/fonts` (bundled with UI v4) rejects `provider: 'none'`. Fonts are self-hosted via `app/assets/css/fonts.css`, so use `provider: 'local'`. ### Dev/build need an elevated Node heap (large app) This codebase is large enough that under Nuxt 4 + Vite 7 the default Node heap is exhausted, killing the dev `vite-node` SSR worker — surfacing as **`IPC connection closed`** or a stale-socket **`connect ENOENT …nuxt-vite-node-*.sock`** with every route returning 500 (no V8 OOM trace because the OS kills the worker). The production build OOMs similarly. - **Dev:** the `dev` script sets `TMPDIR=/tmp NODE_OPTIONS=--max-old-space-size=8192 nuxt dev`. (Append host flag as `npm run dev -- --host`.) The `TMPDIR=/tmp` prefix is the macOS socket-length fix below — keep it. - **Build/deploy:** do NOT hardcode `NODE_OPTIONS` in the `build` script — `ci/deploy-production.sh` already `export`s `NODE_OPTIONS="--max-old-space-size=12288"` before `npm run build`; an inline value would override and lower it. - If a fresh `nuxt dev` misbehaves right after a `nuxt build`, do a clean restart: kill node, `rm -rf .nuxt .output node_modules/.cache`, then `npm run dev`. **Two distinct causes produce the same `connect ENOENT …nuxt-vite-node-*.sock`:** 1. **OOM** — the SSR worker is killed; fixed by the `--max-old-space-size` heap above. 2. **macOS 104-char unix-socket limit (the subtle one).** macOS caps socket paths at 104 bytes. The default macOS `$TMPDIR` (`/var/folders//T/`, ~49 chars) pushes the vite-node socket path to ~110 — over the limit. A recent `@nuxt/vite-builder` security step `chmodSync`s the socket right after `listen()`; because the kernel truncated the over-long name at bind, the chmod targets a path that doesn't exist → it logs **`Failed to restrict vite-node socket permissions; closing`** and closes the server, so every request then ENOENTs. **Tell-tale:** the `chmod ENOENT …` line *above* the ENOENT spam, and a socket path under `/var/folders/…/T/`. Clean restarts don't help (the path is long every boot). **Fix:** shorten `$TMPDIR` — the `dev` script forces `TMPDIR=/tmp` (socket path drops to ~66 chars). Must live *inside* the npm script (not a shell prefix), because IDE runners (PhpStorm) launch with their own launchd `$TMPDIR`. **Only one `nuxt dev` per project directory.** Nuxt's dev-lock refuses a second `nuxt dev` against the same repo (`Another Nuxt dev server is already running: PID …` — the second process exits immediately) — so a stray/IDE-launched instance on a different port (e.g. `nuxt dev --port 3010 --host` from a PhpStorm run config) silently coexists with a terminal-launched one only until something breaks the lock. Running `rm -rf .nuxt` while BOTH are alive (e.g. the terminal one just exited and its lock file lingered) lets a second `npm run dev` start clean and acquire a new lock — leaving two servers writing into the same `.nuxt`/Vite cache concurrently, which corrupts shared build state and makes the older one start 503ing. **Symptom:** a page that works in one tab but looks broken/unstyled in another, even after "fixing" and restarting — first check `ps aux | grep "nuxt dev"` for more than one matching this repo's path before assuming a code/CSS problem. Fix: kill every one of them, `rm -rf .nuxt node_modules/.cache`, start exactly one. ### Non-blocking list pages — `lazy: true` + AgGrid `:loading` In Nuxt 4 a top-level `await useFetch(...)` in `