Skip to content

Sales Report By Document

sales-report-by-document is the query behind the Sales Report applet’s SR By Document screen — one row per sales document line, the level of detail the applet’s advance search filters down to before the grid renders it.

Path: POST /core2/tnt/dm/erp/reports/sales/sales-report-by-document/etl-ep Request body: SalesReportByItemCodeInputDto as JSON Response envelope: StreamingPagingResponse — see the family page

See Applet Reports API for authentication and the shared envelope shapes, and Data API for hosts and headers common to every etl-ep call.


Targets: when you must send one, and what happens to it

What an access key may see is decided by the permission on the user behind it. If that permission is restricted to particular branches or companies, the route requires at least one of branch_guids, company_guids or location_guids; leave all three empty and it answers 400 with “Need to provide at least one target guid”. A key whose permission is not restricted that way needs none of them and gets every branch.

On a restricted key, the branch_guids and company_guids you send are checked against the ones the permission allows: permitted values are kept, the others are dropped without a message, and if neither list keeps a single value the call is refused as not authorised. So a report that comes back with fewer branches than you asked for is a permission question, not a data question. location_guids is not checked against the permission at all, so on a restricted key it cannot stand alone — a request with only location_guids is refused; send a branch or company with it.


Request fields

Defaults are the DTO’s no-arg constructor: anything you omit takes that value. An empty set or an empty optional means no filter — with one exception, server_doc_type, below.

The date window. date_from is inclusive. date_to is turned into “before the start of the next day”: the query adds one day to whatever you send and compares date_txn < that. Send date_to as midnight at the start of the last day you want (2026-08-31T00:00:00+08:00 for August). A timestamp such as 2026-08-31T23:59:59+08:00 reaches almost to the end of 1 September, so a month-end pull taken that way double-counts the first day of the next month. Both dates default to now, so a request that omits them asks for roughly the next 24 hours and comes back near-empty, without an error.

What every row already has to satisfy, whatever you send: the document is FINAL (drafts and unposted documents never appear), the header carries no forex-document link (forex_doc_hdr_guid is empty), and the line is an item line (PNS) that is not deleted.

Filters on the document header — each one is matched against the document as it was saved:

  • branch_guids, company_guids, location_guids — the header’s branch, company and store. See Targets above for how a restricted key changes the first two.
  • customer_guids — the header’s customer; customer_category_guids — customers carrying any of these customer labels today (a customer relabelled since the sale moves with the label).
  • salesman_guids — the header’s sales person (fed by the employee dropdown); created_by_guids — the user who created the document; shipping_recipient_entity_hdr_guids — the header’s ship-to entity.
  • sales_lead_label_guids — the header’s sales lead. Each value must be a GUID string; anything else fails the call rather than being ignored.
  • server_doc_1 — document numbers, exact match, one or more; doc_remarks — a contains-match on the header remarks, case-insensitive.

Filters on the item, as the item master says today — not as it was when the line was sold:

  • keyword — case-insensitive contains-match across item code, name, description, scan code, the two alternate codes and remarks; spaces in your keyword act as wildcards (usb c matches USB-C).
  • item_guids, item_type, item_status (ACTIVE, INACTIVE, DELETED) and item_category1_guids … item_category20_guids. An item deactivated last week drops out of item_status: ["ACTIVE"] for every past month too.
FieldTypeDefaultWhat to know
server_doc_typeSet<String>INTERNAL_SALES_CASHBILL, INTERNAL_SALES_INVOICE, INTERNAL_SALES_ORDERThe only set that is not empty by default — see the warning below.
pricing_scheme_hdr_guidOptional<UUID>emptyIgnored on this route. The DTO carries it for sibling reports; this query never applies it, so sending it does not narrow anything.
report_period, group_by, optional, timeZonevariousDAILY, empty, empty, Asia/Kuala_LumpurIgnored on this route. They drive the daily/weekly/monthly summaries on the same controller; this query neither groups nor converts time zones, so dates compare in the offset you send.

The applet’s own advance search sends branches, dates, keyword, customer, customer category, salesman, ship-to, sales lead, item type, item status, categories and document numbers; it never sends company_guids, location_guids, item_guids, created_by_guids or doc_remarks, so those narrow the rows in ways this screen has no control for. Of the five, only company_guids is sent by any screen — the Financial Report applet’s gross-profit journal view sends it to this route’s back-office twin for one company’s month — and no screen sends the other four, so there is no screen output to check such a slice against.

server_doc_type has a non-empty default. Unlike every other Set field on this DTO, omitting server_doc_type does not mean “no filter” — it means “cashbills, invoices and orders only,” because that’s what the no-arg constructor fills in. If you need sales returns, credit notes, or other document types the applet’s own default doesn’t show, you must send server_doc_type explicitly.

Company is implied by which branches you pick — don’t send company_guids unless you specifically want to filter branches out that way; the applet itself never does.


Cost basis: replicate the applet’s fallback yourself

The response carries every cost-type amount regardless of what the applet’s “Calculate Base On” dropdown shows, because that control is a client-side display choice, not something sent to the backend at all. To reproduce the number the applet displays as “Cost Amt” for a line, apply this fallback in your own frontend:

cost_amt = cost_ma_amount
  ?? (nonzero) cost_fifo_amount
  ?? (nonzero) cost_wa_amount
  ?? (nonzero) cost_replacement_amount

i.e. use cost_ma_amount if it is present and non-zero; otherwise fall through the list in that order. This was reverse-engineered from the applet’s own line-construction code and confirmed by matching totals against a live export — it is not documented anywhere in the backend.


Response fields

The response is one flat object per sales document line (not per document — a multi-line invoice produces multiple rows sharing the same server_doc_1). The query joins in the full item-category tree, pricing, and customer/salesman detail, so a single row carries well over a hundred fields; below are the ones you’re likely to actually need. Field names only — no data from the verification capture is reproduced here (see this page’s sources: map).

Every quantity and amount is already signed: the query multiplies it by the line’s sign, so if you widen server_doc_type to include returns, return lines arrive negative and a plain sum nets them off.

FieldWhere it comes from, and what to watch
line_guidThe document line. Unique per row; use it as your key when you re-pull a period.
server_doc_1, date_txnThe document number and transaction date from the header. Several rows share a server_doc_1, one per line. date_txn is the column the date window tests — not the creation or posting time.
doc_ccy, base_doc_ccy, base_doc_xrateEvery amount on the row is in doc_ccy and is not converted. Do not total rows across currencies without converting them first.
item_code, item_descr, type, uomRead from the item master today, not stored on the line. Recode or retype an item and its past sales come back under the new code. qty_sold is the line’s base quantity, not the quantity in whatever unit the line was keyed in.
item_name, serial_noStored on the line when it was sold — so item_name can disagree with item_code after a rename. serial_no is {"serialNumbers": [...]} for serial-tracked items.
fi_item_guidThe item’s GUID — the stable key to join on, where codes can change.
category1_code … category20_code, category1_name … category20_nameThe item’s category labels today, paired code/name per level. Levels a tenant does not use come back null.
branch_code, branch_guid, location_code, loc_guidThe branch and store on the document header; codes and names are read from the branch and location masters today.
customer_code, customer_name, customer_phoneRead from the customer record today, not from the document — a customer renamed since the sale shows the new name on old rows. Personal data: handle accordingly.
salesman_codeThe employee code of the header’s sales person, read from the employee record today; empty when the document had no sales person.
qty_sold, amount_std, amount_discount, amount_net, amount_txnSigned line values — see the generic document for what each amount means on a line.
cost_ma_amount, cost_fifo_amount, cost_wa_amount, cost_replacement_amount, cost_lifo_amountCost basis by method. See Cost basis above for which one the applet actually shows.
pricing_scheme_codeThe pricing scheme the line was priced under, through the line’s own pricing-scheme link; empty when the line used none.

Filter dropdowns

Populate the applet-equivalent filters from these. See Applet Reports API → dropdowns.

POST /core2/tnt/dm/erp/drop-down/company/etl-ep
POST /core2/tnt/dm/erp/drop-down/branch/etl-ep
POST /core2/tnt/dm/erp/drop-down/location/etl-ep
POST /core2/tnt/dm/erp/drop-down/customer/etl-ep
POST /core2/tnt/dm/erp/drop-down/employee/etl-ep

Body for each: {"keyword": "", "filters": {}, "limit": 50} (DropDownInputDto — keyword, filters, filterLogical, limit default 50, orderBy, order default "ASC"). Response: {"guid": "...", "code": "...", "name": "..."} per row, in the standard StreamingPagingResponse envelope.


Example

curl -X POST "https://api-etl.akaun.com/core2/tnt/dm/erp/reports/sales/sales-report-by-document/etl-ep" \
  -H "AccessId: YOUR_ACCESS_ID" \
  -H "AccessKey: YOUR_ACCESS_KEY" \
  -H "tenantCode: YOUR_TENANT_CODE" \
  -H "Content-Type: application/json" \
  -d '{
    "branch_guids": ["3f2a7c10-9b4e-4d61-8a22-1c5e7f9d0b33"],
    "date_from": "2026-08-01T00:00:00+08:00",
    "date_to": "2026-08-31T00:00:00+08:00",
    "item_status": ["ACTIVE"]
  }'

date_to is midnight at the start of 31 August because the route adds a day to it: this asks for the whole of August and nothing of September. server_doc_type was left unset, so the response is cashbills, invoices and orders only — send it explicitly to widen that.


Related

Last updated on