Sales Ledger Summary
Billing → Reports (/billing/BillingReportsPage) includes the Sales Ledger Summary dashboard (code sales-ledger-summary). It is allowed for the roles owner, admin, and accountant.
The dashboard answers two questions, in that order on the page:
- Daily — certified sales by location and issue date, with annulled documents beside that day's certified total.
- Period — the same documents rolled into goods, services, exempt, IVA, and a period total.
Voided and cancelled amounts are their own rows. They do not change Monto total or Total del período.
Where the numbers come from
Classification is the SQL function fel_sales_ledger_document(business_id, start_date, end_date). The Metabase cards only aggregate it:
| Card | File |
|---|---|
| Daily | scripts/metabase/card-definitions/flowpos-sales-sales-ledger-summary-daily.sql |
| Period | scripts/metabase/card-definitions/flowpos-sales-sales-ledger-summary-period.sql |
Both cards take business_id, start_date, and end_date. They do not read sale or order_bill themselves.
Sales Book (flowpos-sales-sales-book) is a sibling report. It keeps its own SQL. Pointing Sales Book at fel_sales_ledger_document would change that book; this dashboard does not do that.
The layout script writes numeric Metabase ids onto the dashboard row. It does not contain the classification. Re-running it is safe:
SSL_DISABLE=true tsx src/metabase/layout-sales-ledger-summary.ts --env local
SSL_DISABLE=true tsx src/metabase/layout-sales-ledger-summary.ts --env staging
SSL_DISABLE=true tsx src/metabase/layout-sales-ledger-summary.ts --env production --yes
Run it from packages/backend/scripts. The dashboard row must already exist. A missing Billing Reports form (path /billing/BillingReportsPage) fails the schema change that registers the dashboard.
What counts as a document
Each returned row has kind, location_name, emission_date, goods, services, exempt, iva_goods, iva_services, document_total, and annulled_amount. A blank location name is stored as Sin ubicación. The location timezone defaults to America/Guatemala.
The window is location-local calendar days: from the start of start_date's local day through the end of end_date (exclusive of the next midnight).
Certified retail (kind = certified)
A sale row (a null type counts as sale) with fel_status = certified, a non-empty fel_authorization, and a status outside draft, cancelled, voided, and pending_approval.
Issue date is fel_date_dte when it parses, otherwise sale_date. Accepted fel_date_dte shapes are an ISO prefix (YYYY-MM-DD…) and DD/MM/YYYY HH24:MI.
Line split, from sale_detail.items:
| Bucket | Rule |
|---|---|
| Goods | itemType is not service, and the line's IVA is greater than 0. Amount is taxableAmount |
| Services | itemType is service, and IVA is greater than 0. Amount is taxableAmount |
| Exempt | IVA is 0. Goods and services are added together. Amount is amount |
| IVA | Sum of tax lines whose shortCode, code, or shortName is IVA. A non-empty taxes array with no IVA line contributes 0. A missing taxes array falls back to the line taxAmount |
document_total is the header total_amount. It is not recomputed from the buckets, so a header that disagrees with its lines still shows the header as Monto total. annulled_amount is 0 on certified rows.
Credit notes and other non-sale types are absent.
Certified restaurant (kind = certified)
An order_bill with fel_status = certified, a non-empty fel_authorization, and a status other than cancelled. Issue date is fel_date_dte, otherwise created_at. document_total is order_bill.total.
Taxed vs exempt follows item weight (unit_price_snapshot * split_quantity). Tips are added to exempt. Every restaurant line is classified as goods: is_service is fixed false, so the services bucket and services IVA are 0 for bills.
Annulled
| Source | kind | Amount |
|---|---|---|
Retail sale with status voided or cancelled (type sale, null counts as sale) | the status string | total_amount in annulled_amount |
Restaurant bill with status cancelled | cancelled | total in annulled_amount |
These rows do not require a certified DTE. Goods, services, exempt, IVA, and document_total are 0. A cancelled sale that never received an authorization still appears here when its status is cancelled or voided.
How the cards present it
Daily groups by location and issue date. Certified columns (registros, bienes, servicios, exentas, IVA, monto total) count kind = certified only. Registros anulados and Monto anulado count voided and cancelled. Each location ends with a Total row.
Period emits five concepts per location, in this order:
| Concepto | Valor neto | IVA débito | Valor total |
|---|---|---|---|
| Venta de bienes | certified goods | certified IVA on goods | goods + that IVA |
| Prestación de servicios | certified services | certified IVA on services | services + that IVA |
| Ventas exentas | certified exempt | 0 | exempt |
| Ventas anuladas o canceladas | annulled amount | empty | empty |
| Total del período | goods + services + exempt | both IVA columns | sum of certified document_total |
Total del período valor total is the sum of certified headers. It is not goods + services + exempt + IVA, and it does not subtract annulled amounts.