---
title: Assembly Cost
order: 10
updated: 2026-07-12
---

# Assembly Cost

An assembly cost is a recipe (a bill-of-materials) for producing a finished good: which components go into it, how much of each, and how many units one batch produces. Once a recipe exists, the app can compute an *assumed* production cost for that product by pulling the real purchase prices of its raw materials from your approved stock arrivals — and it does the same recursively for any component that is itself an assembly, so you can compare "make it" against "buy it" at every level.

Recipes are keyed by *product code + make*: there can be exactly one recipe per code-and-make combination. A recipe stores nothing about stock, quantity on hand, or money — it is purely the composition. The prices are looked up live every time you open the cost breakdown.

## Who can use it

Two permissions govern the feature, and the server re-checks the matching one on **every** request — the buttons being hidden in the UI is never the only guard.

- *View Assembly Costs* (`inventory.viewAssemblyCost`) — read-only access to the *Assembly Cost* page and to browsing recipes. Depends on *View Inventory* (`inventory.viewInventory`). This permission also puts the *Assembly Cost* entry in the *Stock* screen's action menu.
- *Manage Assembly Costs* (`inventory.manageAssemblyCost`) — create, edit, and delete recipes. Depends on *View Assembly Costs*.

A firm administrator always has both, regardless of the toggles. See [Users and permissions](/docs/settings/users-and-permissions) for how to grant these. Without *View Assembly Costs* the list, view-codes, and view-one endpoints all return a `403` and the page never loads; without *Manage Assembly Costs* the add, edit, and delete endpoints return `403` even if a forged request reaches them.

## Open the Assembly Cost page

There is **no sidebar link** for Assembly Cost. It is reached only from the *Stock* screen:

1. Open *Inventory* in the sidebar and click *Stock*.
2. Click the *⋮* (more) button at the top right and pick *Assembly Cost* (*"Define cost compositions for products"*).

The menu entry appears only if you hold *View Assembly Costs*. See [View current stock](/docs/inventory/stock-overview) for the rest of the *Stock* screen.

The page lands on `/inventory/assembly-cost` and lists every product head that has at least one recipe.

## Browse recipes

The main table groups recipes by product head. Each row shows:

- *#* — a running row number.
- *Head Name* — the product head.
- *Codes* — how many distinct product codes under that head have a recipe. A code with recipes for two different makes still counts as one code here.
- *View* — opens the per-head modal (see below).

The heading carries a live count badge; a trailing `+` means more rows can still be loaded. Rows load `20` at a time as you scroll (infinite scroll). The list only shows heads that actually have recipes — a head with none simply isn't listed.

### Search

The search box filters by **product head name** (case-insensitive substring, debounced ~`400 ms`). Behind the scenes the server finds heads whose name matches, then finds recipes under those heads' codes, so typing a head name that has no recipes returns nothing. Search results are not paginated the same way — up to `50` matching heads come back at once.

### The per-head modal

Clicking *View* opens a *{Head} — Assembly Costs* modal listing every recipe under that head. Its columns:

- *Code* — the finished-good product code.
- *Make* — the make the recipe is for (or `—` if the make was removed).
- *Components* — how many component lines the recipe has.
- *Output Qty* — how many units one batch produces.
- *Unit* — the output unit (its short form, or `—` if removed).

Rows are sorted by code, then make. The modal has its own *Search codes…* box (filters by product code name) and its own `20`-at-a-time scroll. *Edit* and *Delete* buttons appear on each row **only if you hold *Manage Assembly Costs*** — a view-only user sees the list but no action buttons.

## Add a recipe

Click *Add Assembly Cost* at the top right of the page (shown only to managers) to open the *Add Assembly Cost* modal.

### Finished Good

- *Product Head* — pick the head. This is a **filter only** and is not saved on the recipe; it narrows the *Product Code*, *Make*, and *Unit* choices below. Changing it clears the code, make, and unit.
- *Product Code* — the finished-good code under the chosen head. Disabled until a head is picked.
- *Make* — required. The dropdown lists the makes allowed on the head (from the head's *allowed makes* list). If the head has exactly one make it is auto-selected. If the head has **no** makes configured the dropdown stays empty and the server will reject the save with a message telling you to add makes in Records first — see [Manufacturers](/docs/records/manufacturers).
- *Output Qty* — how many units the batch yields (for example `7`). Accepts decimals up to `4` places; must be greater than `0`.
- *Unit* — the output unit. The choices are the units allowed on the head, further narrowed to the units allowed on the chosen code; if only one qualifies it is auto-selected. See [Product codes](/docs/records/product-codes) for where a code's allowed units come from.

### Components

Add one or more component lines with *Add Component*. Each line is the same cascade as the finished good:

- *Head* → *Code* → *Make* → *Qty* → *Unit*, plus an *✕* to remove the line.
- *Code* is disabled until that line's *Head* is chosen; *Make* is filtered to the line-head's allowed makes (auto-selected when only one); *Unit* is filtered to the line-code's allowed units.
- *Qty* accepts up to `4` decimal places and must be greater than `0`.

You cannot remove the last remaining component row — at least one component is always required.

### What the save enforces

The client blocks obvious mistakes, and the server independently re-validates every field in three layers (shape → existence → semantics), so a forged or stale payload can't slip a bad recipe in:

- Product code, make, output quantity, and output unit are all required for the finished good; output quantity must be `> 0`.
- At least one component is required; each component needs a code, make, quantity `> 0`, and unit.
- Every id must be a real, existing product code / make / unit (a non-existent id is rejected with a `404`, not silently accepted).
- **Semantic rules** — for the finished good *and* every component: the make must be one of the makes allowed on that code's head, and the unit (if the code restricts units) must be one of the code's allowed units.
- **No self-reference** — a component cannot be the same code *and* make as the finished good.
- **No duplicate components** — the same code-and-make pair cannot appear twice in the component list.
- **One recipe per code + make** — a unique index enforces this. Trying to add a second recipe for a code-and-make that already has one fails with a conflict (`409`) "already exists for this product code and make combination".

Click *Create* to save. On success the page reloads.

## Edit a recipe

From the per-head modal, click *Edit* on a code row (managers only).

The finished good's *Product Head*, *Product Code*, and *Make* are shown but **cannot be changed** — they are the recipe's identity. You can change:

- *Output Qty* and *Unit* (the output unit is re-validated against the code's allowed units).
- The full component list — add, remove, or re-pick any component line.

The same self-reference, duplicate-component, existence, and semantic checks run on save. To change the finished good's code or make, delete the recipe and create a new one.

## Delete a recipe

From the per-head modal, click *Delete* on a code row (managers only). A confirmation dialog (*Delete Assembly Cost*) names the code and make and warns the action cannot be undone.

Deletion is a **hard delete** — the record is removed, not soft-hidden. This is safe because nothing else in the app stores a reference to a recipe. Deleting frees the code-and-make slot immediately, so you can create a fresh recipe for the same combination right away. If you delete the last recipe under a head, the modal closes and that head drops off the main table.

## The Assembly Cost tab (assumed cost)

Once a recipe exists for a product code, an *Assm Cost* tab appears in the *Product Pricing Analytics* modal — the modal you open from a product row while adding or editing an enquiry (see [Pricing analytics](/docs/enquiry-bank/add-enquiry#pricing-analytics)). The tab is shown only when the code actually has a recipe.

This is where the recipe turns into money: the app walks the recipe tree and prices every raw material from your approved stock arrivals.

### How the cost is computed

The engine walks the recipe recursively, up to a depth of `10` levels:

- If a component **has its own recipe** (a sub-assembly), it recurses into that recipe and computes a *made* cost from its children.
- If a component is a **raw material** with no recipe, it takes the component's direct purchase price from arrivals.
- If the walk hits a **cycle** (A needs B, B needs A) the repeated node is capped as a leaf and flagged, so the calculation can never loop forever.

Prices are drawn only from **approved** stock arrivals; reverse (return-to-supplier) entries are excluded because they aren't purchase samples. The rate used is the *net* rate — the arrival's per-unit price with the linked purchase order's line discount subtracted (a direct, non-PO arrival has no discount, so net equals gross). See [Add a stock arrival](/docs/inventory/add-stock-arrival).

The prices shown are also inflated by the firm's *Purchase Price Markup* percent, the same setting that inflates the *Cost* tab — see [Firm customisation](/docs/settings/firm-customisation). At the default `0%` you see the real paid rates.

### Price modes

A *Price Mode* dropdown offers three lenses, all computed in a single request so switching is instant:

- *Highest* — the highest purchase rate ever recorded for each component.
- *Latest* — the most recent arrival's rate (by invoice date, then entry time).
- *Lowest* — the lowest purchase rate ever recorded.

### Make selector

If the code has recipes for more than one make, a *Make* dropdown lets you switch between them; changing it re-fetches the tree for that make. If you open the tab without a make in mind, the first available recipe's make is used.

### Reading the breakdown tree

The tree has three columns — *Component*, *Qty*, and *Unit Cost*:

- **Branch rows** are sub-assemblies (components that have their own recipe). They start **expanded**; click to collapse. Collapsed, the row shows that part's direct *buy* price; expanded, it shows the *made* cost rolled up from its children. That lets you weigh make-vs-buy on any node.
- **Leaf rows** are raw materials, showing their purchase price for the chosen mode.
- **Cycle rows** carry a `cycle` badge and are treated as leaves.
- A price of `Nil` means no arrival has been recorded for that component yet, so there is no rate to reference — the overall cost is partial until arrivals are logged. (The server also tracks how many components are missing prices.)

The tree lists the finished good's components; the finished good's own per-unit and total batch cost are computed (unit cost = batch cost ÷ output quantity, rolled up bottom-up) but this tab renders the component breakdown rather than a separate headline total.

## Common questions

### Why isn't Assembly Cost in my sidebar?

It never is. Open it from the *Stock* screen's *⋮* menu — the *Assembly Cost* entry there. If you don't see that entry, you're missing the *View Assembly Costs* permission.

### Why can't I add, edit, or delete a recipe?

Those actions need *Manage Assembly Costs* (which itself depends on *View Assembly Costs*). A view-only user can browse recipes and see the *Assembly Cost* tab but gets no *Add*, *Edit*, or *Delete* buttons, and the server rejects those calls anyway.

### It says the head has no makes defined — what now?

A recipe's make must be one of the makes allowed on the finished good's product head. If the head has none, add makes in [Manufacturers](/docs/records/manufacturers) and attach them to the head under [Products](/docs/records/products), then come back.

### Why does it say a recipe already exists?

Because there can be only one recipe per product code + make. Edit the existing one, or delete it and create a new recipe for that combination.

### Why can't I change the code or make when editing?

They are the recipe's identity — a recipe is uniquely a code-and-make pairing. To change either, delete the recipe and create a new one; the slot frees up immediately.

### The cost shows Nil or looks incomplete — why?

A component priced `Nil` has no approved stock arrival on record yet, so there's no purchase rate to use. Record an arrival for that component (or the sub-assembly's raw materials) and reopen the tab. Only approved, non-reverse arrivals count. See [Add a stock arrival](/docs/inventory/add-stock-arrival).

### Do the assumed-cost prices match what we actually paid?

They start from real net arrival rates (gross price minus the PO line discount), then apply the firm's *Purchase Price Markup* percent. At the default `0%` markup they equal the real paid rates; a non-zero markup shows them inflated. See [Firm customisation](/docs/settings/firm-customisation).

### Can a recipe reference another assembled product?

Yes — that's the point of the tree. A component that has its own recipe becomes an expandable branch, priced from *its* components, and so on up to `10` levels deep. A circular reference is detected and shown as a `cycle` leaf rather than looping.
