---
title: Product report
order: 3
updated: 2026-07-12
---

# Product report

The *Product* tab on the dashboard analyses your business at the **product line level** — not per document like the [Sales report](/docs/reports/sales-report), but per product code and product head. It answers: how much of each code and head do you *quote*, *win*, *dispatch* and *buy*, who buys it, in which states, under which makes, and what does it earn you after cost. Every figure is scoped to the *From*/*To* date range at the top of the dashboard, and to whichever product head or code you have selected.

Unlike the Sales report, the Product report is **never cached** — each time you open the tab, change the range, or switch a selector, the figures are recomputed live from the aggregation pipeline. There is no "Updated …" label and no refresh button; the numbers are always current.

## Open the Product report

1. Open the *Dashboard* from the sidebar. See [The dashboard](/docs/getting-started/dashboard) for the shared shell.
2. Click the *Product* tab in the tab strip (package icon).
3. Set the reporting window with the *From* and *To* date pickers in the dashboard header — they are shared by every tab.
4. Optionally narrow the report with the two selectors in the tab toolbar (see [The two selectors](#the-two-selectors)).

The tab appears only if you hold the *Product Report (detailed)* permission (`dashboard.productReport`), which itself depends on *Dashboard access* (`dashboard.view`). Both are re-checked on the server for every data request and every PDF export — a user who forges the request without them gets a `403 Forbidden`, never data. See [Users and permissions](/docs/settings/users-and-permissions).

### The date range

The window comes from the two date pickers in the dashboard header, shared across all tabs.

- *From* is taken as the **start of that day** (`00:00:00.000`); *To* as the **end of that day** (`23:59:59.999`) in your local time zone.
- If you pick a *From* later than the current *To*, the *To* snaps forward to the same day (and vice-versa), so the range is never inverted.
- On first load the range defaults to your saved dashboard-range preference, falling back to **the current calendar month** for users who have never set one.

The server independently re-validates the window: both timestamps must be positive numbers, `start` cannot exceed `end`, and the span cannot exceed **3 years** (`366 × 3` days). A range that fails any of these is rejected with `Invalid period. Expected { start: <ms>, end: <ms> }.` — nothing is computed.

## The two selectors

At the left of the tab toolbar are two optional, searchable selectors that decide the report's **scope**.

### Product head

- Labelled *Product head*, placeholder `All product heads (optional)`.
- Type to search your product heads (see [Product codes & heads](/docs/records/product-codes)). Picking one focuses the whole report on that head and its codes.
- Leave it empty for the firm-wide (global) view.
- Changing the head **clears any product code** you had chosen and resets the code picker.

### Product code

- Labelled *Product code (optional)*, placeholder `All codes (optional)` once a head is chosen, or `Pick a head first` while it is **disabled**.
- The code picker only searches codes **within the selected head** — you cannot choose a code until a head is set. This mirrors the catalog, where every code belongs to exactly one head.
- A small *✕ Clear* link appears above the picker once a code is selected; use it to drop back to the whole head.

### The three scopes

The two selectors produce exactly three views, and the pipeline runs in the matching mode:

| Selection | Scope (`mode`) | View |
|---|---|---|
| Nothing selected | `global` | **Firm-wide** — product & head leaderboards, trend, top suppliers |
| Head only | `head` | **One head** — its code breakdown, customers, geography, make split, procurement |
| Head + code | `code` | **One code** — a deep dive with price statistics and rate history |

Every id you send is re-validated server-side before any aggregation runs: an invalid `productCodeId` or `productHeadId` is rejected `400`; a head or code that does not exist gives `404`; and a code that does **not** belong to the given head is rejected with `Product code does not belong to the given product head.` This is deliberate — a forged code id can never make the pipeline crash or leak another head's data.

## What the numbers mean

A few definitions apply to every view. They match the [Sales report](/docs/reports/sales-report) and Customer report so figures reconcile across the dashboard.

- **Queried** — enquiry lines whose enquiry date (`dateOfEnquiry`) falls in the window. This is *demand*: what customers asked you to quote.
- **Ordered** — lines on **won orders only** (`PO arrived`), whose PO date falls in the window. A sales order that has not yet received its customer PO does **not** count here. See [Orders](/docs/enquiry-bank/orders).
- **Dispatched** — lines actually shipped on **approved, non-return** deliveries whose delivery date falls in the window. Returns are excluded. The dispatched *value* is joined from the parent order's line price. See [Delivery receipt](/docs/enquiry-bank/delivery-receipt).
- **Purchased / procurement** — approved stock arrivals (excluding return-to-supplier reverse entries) whose invoice date falls in the window. See [Add a stock arrival](/docs/inventory/add-stock-arrival).

Two money conventions run throughout:

- Every value is **net of discount** — each line's `discountInPercentage` is subtracted before the value is computed.
- **Value** figures are **inclusive of tax** (used for the ₹ displays). **Revenue** figures are **ex-tax** and are used only for profit and average sell price, because tax is pass-through and would distort margin.

## KPI strip (Key figures)

Every view shows a strip of eight tiles for the selected scope. Hover a tile for its one-line definition.

- **Ordered value** — total incl-tax value of won orders (`PO arrived`) in the scope, net of discount.
- **Ordered qty** — total quantity ordered across those won orders.
- **Dispatched qty** — quantity actually dispatched via approved deliveries (returns excluded).
- **Queried value** — incl-tax value of enquiry lines raised in the period.
- **Win rate** — `ordered ₹ ÷ (ordered ₹ + lost/regret queried ₹) × 100`. The denominator only counts queried value from enquiries that ended `lost` or `regret`, so the figure reflects how much of the *decided* demand you converted. Shown as a whole-number percentage; `0%` when there is no decided demand.
- **Avg sell price** — `ordered ex-tax revenue ÷ ordered qty` — the effective unit rate you sold at. `0` when nothing was ordered.
- **Est. profit** — estimated profit across the scope; see [How profit is estimated](#how-profit-is-estimated).
- **Eighth tile — scope dependent:**
  - *Distinct codes* (global view) — number of distinct product codes that transacted in the period.
  - *Margin* (head & code views) — `est. profit ÷ ordered ex-tax revenue × 100`, whole-number percent.

Just under the scope title, a small line also reports the scope's **purchasing** for the period: `Purchase ₹… · N units in` — the total approved-arrival value and quantity that flowed in.

> The one-click PDF carries a slightly wider KPI grid than the on-screen strip: it always prints *Ordered ₹, Ordered qty, Dispatched ₹, Dispatched qty, Queried ₹, Win rate, Avg sell price, Purchase ₹, Est. profit ₹, Margin, Distinct codes* and *Customers* together, regardless of scope.

## Global view (firm-wide)

With no head selected you get firm-wide product leaderboards.

### Leaderboards — ordered vs queried

Two grouped bar charts side by side:

- *Top codes — ordered vs queried* — value ₹ per product code, ordered (green) against queried (indigo).
- *Top heads — ordered vs queried* — the same, aggregated per product head.

Both are drawn from the top-ranked rows (the pipeline ranks up to `60` codes and `50` heads internally, then the top `10` by value are charted and tabled).

### Top product codes tables

- *Top product codes — sold* — ranked by ordered value. Columns: *Product code*, *Head*, *Ordered ₹*, *Queried ₹* (top 10).
- *Top product codes — queried* — the same columns, ranked by queried value instead.

### Trend

*Monthly trend* — a dense month-by-month chart across the whole window (no month is skipped, capped at 36 months): ordered ₹ and queried ₹ as bars, dispatched ₹ as a line. Each metric is bucketed by its own date — ordered by PO date, queried by enquiry date, dispatched by delivery date.

### Procurement — suppliers

*Suppliers* — where these codes are purchased from, ranked by purchase value (top 12). Columns: *Supplier*, *Qty*, *Avg rate ₹*, *Purchase ₹*. A blank supplier reads as `Unspecified`.

## Head view (one product head)

Selecting a head shows everything about it and its codes.

### Codes in this head — ordered vs queried

A grouped bar chart of ordered ₹ vs queried ₹ for the codes in the head (up to the first 14 codes by value).

### Mix — donuts

Two donuts showing each code's share of the head:

- *Ordered value by code* — share of ordered ₹ (top 10 slices).
- *Ordered qty by code* — share of ordered quantity (top 10 slices).

### Code breakdown

A table of every code that transacted in the head, with its estimated profit. Columns: *Product code*, *Ordered ₹*, *Disp qty*, *Queried ₹*, *Est. profit ₹*, *Margin*. Sorted by ordered value (then queried value), up to `60` rows.

### Customers & geography

- *Top customers* — who buys this head, ranked by ordered value (top 15). Columns: *Customer*, *State*, *Orders*, *Enquiries*, *Ordered ₹*. *Orders* and *Enquiries* are the count of distinct order and enquiry documents from that customer in the scope.
- *By make* — a donut of ordered-value share by make/manufacturer (top 12). A line with no make recorded is grouped as `Unspecified`. See [Manufacturers](/docs/records/manufacturers).
- *By state* — a table of ordered value and quantity by the **customer's** state (top 12), built from ordered lines only. A customer with no state on file shows as `—`.

### Trend

The same *Monthly trend* chart as the global view, scoped to this head.

### Procurement — suppliers

*Suppliers* — where the head's codes are purchased from (same columns as the global suppliers table).

## Code view (one product code)

Selecting a code within a head gives a deep dive. It drops the per-code breakdown (there is only one code) and adds price and rate detail.

### Price statistics

Four tiles computed from the unit selling price (`pricePerUnit`) of that code across won-order lines in the window, ignoring zero-priced lines:

- *Lowest price* — the minimum unit price sold at.
- *Average price* — the mean unit price.
- *Highest price* — the maximum unit price.
- *Latest price* — the most recent unit price (the price on the latest-dated order line).

If nothing was ordered in the window this block is hidden.

### Customers & geography

The same *Top customers*, *By make* and *By state* blocks as the head view, all scoped to this single code.

### Trend

The *Monthly trend* chart for this code.

### Procurement — rate history

For a single code the procurement block expands to three tables:

- *Suppliers* — who you buy this code from, ranked by purchase value (top 12): *Supplier*, *Qty*, *Avg rate ₹*, *Purchase ₹*.
- *Highest purchase rates* — the 5 approved arrivals with the highest unit rate: *Supplier*, *Rate ₹*, *Qty*, *Date*. (The PDF adds the *Invoice* number.)
- *Latest arrivals* — the 5 most recent approved arrivals by invoice date, same columns.

Both rate tables exclude return-to-supplier reverse entries, so a return can never corrupt the rate samples.

## How profit is estimated

Profit in this report is an **estimate**, not booked accounting profit — the on-screen footnote says so explicitly.

> Est. profit (per code) = ordered ex-tax revenue − (ordered quantity × average purchase rate)

- The **average purchase rate** for a code is the mean `pricePerUnit` of that code's approved stock arrivals in the window.
- **Tax is excluded** from both revenue and cost, because it is pass-through and would distort margin.
- A code with **no purchase history in the window** is treated as **zero cost**, so its estimated profit equals its full ex-tax revenue. Read those rows with that caveat — a high margin may just mean you did not buy that code in the period.
- The scope-level *Est. profit* KPI is the **sum of per-code estimates**, so each code is costed at its own average rate rather than a blended firm-wide rate. *Margin* is that summed profit over the scope's ordered ex-tax revenue.

## Print the report

The *Print product report* button (top-right of the tab) generates a one-click PDF of the **current view** — global, head, or code — for the current date range and selectors.

- The PDF is branded with your firm name and logo (a `PNG`/`JPEG` logo is embedded; other formats are skipped) and opens with the period and scope title.
- Sections vary by mode, matching the on-screen layout: KPI grid, then the leaderboards & trend (global) or the breakdown / customers / suppliers / price stats / rate history (head & code), plus a *By make* donut where data exists.
- Every PDF prints a **"Printed by"** block — the user's name, username and user ID — plus a generated-at timestamp, a faint trace watermark, and a confidential footer. This is an audit trail, so a printed report can always be traced to who generated it.
- The file downloads as `product-report-<scope>-<start-date>.pdf`.
- The PDF is **always live** — it is rebuilt from the pipeline at export time and never served from a cache. Export access is re-checked against the same two permissions as the tab.

If generation fails, an inline error appears next to the profit footnote rather than a broken download.

## Common questions

### Why do my ordered figures look lower than my sales orders?

*Ordered* counts **won orders only** — orders that have received the customer's PO (`PO arrived`). A sales order still awaiting its PO is genuine pipeline but is not yet "ordered" here. It will appear once the PO is recorded. See [Orders](/docs/enquiry-bank/orders).

### A product code shows huge margin — is that real?

Probably not on its own. If you did not **purchase** that code inside the selected window, the report has no cost for it and treats its cost as zero, so its estimated profit equals its full ex-tax revenue. Widen the range to include the relevant arrivals, or read that row as revenue-only. See [How profit is estimated](#how-profit-is-estimated).

### Why can't I pick a product code?

The code picker stays disabled until you choose a *Product head* — every code belongs to a head, and the report needs the head to scope the code correctly. Pick a head first, then the code picker searches only that head's codes.

### The report is slower than the Sales tab — why?

The Product report is **not cached**; it recomputes on every open, range change and selector change, because it fans out across enquiries, orders, deliveries and stock arrivals. The [Sales report](/docs/reports/sales-report) caches its heavy figures for standard presets, which is why it can feel instant.

### Dispatched value is missing on some deliveries — why?

Dispatched value is joined from the parent order's line price. If a delivery line cannot be matched back to its order line (for example the order line was removed), that line contributes quantity but **zero value**. The quantity still shows under *Dispatched qty*.

### Do returns and reverse entries count?

No. Delivery **returns** are excluded from dispatched figures, and **reverse (return-to-supplier)** stock arrivals are excluded from all procurement figures and rate samples. Both would otherwise be negative movements that corrupt the rates and totals.

### Who can see this tab and export the PDF?

Only users with *Product Report (detailed)* (`dashboard.productReport`), which requires *Dashboard access* (`dashboard.view`). Both are enforced again on the server for the data call and the PDF, so hiding the tab is not the only safeguard. Grant them under [Users and permissions](/docs/settings/users-and-permissions).

### How is Win rate different from the Sales report's Conversion %?

*Win rate* here is value-weighted at the product level: ordered ₹ against ordered ₹ plus the queried ₹ of enquiries that ended `lost`/`regret`. The Sales report's *Conversion %* is a document-count ratio across the whole funnel. They answer related but different questions, so they will not match.
