Skip to content

Panama Tax Reports — Taxable/Exempt Sales & Sales Taxes

Date: 2026-07-01

Problem / motivation

Clients migrating from Peachtree (Sage 50) rely on two AR tax reports we did not yet expose: Taxable / Exempt Sales ("Taxable and exempt sales per tax agency") and Sales Taxes ("Sales taxes with agencies, rates, and vendor information"). For Panama the only tax agency is the DGI (ITBMS/VAT), so these collapse into per-rate breakdowns of sales: how much of the period's sales was taxable vs exempt, and how much output ITBMS was collected at each rate. These close two rows on the Peachtree Report Parity tracker.

We already ship the ITBMS Liability report (output vs input tax, net payable). These two new reports are the sales-side companions: the ITBMS report answers "what do I owe the DGI?", while these answer "how is my taxable base composed?" and "what did I charge, per rate?".

In scope

  • taxable_exempt_sales_report(date_filter) — taxable base, exempt base, and output tax for the period, with a per-rate breakdown of the taxable side.
  • sales_taxes_report(date_filter) — per-rate output ITBMS collected on sales, enriched with the tax rate's display_name and dgi_code (the DGI is the single implicit agency).
  • Both exclude voided/canceled invoices via ~Invoice.is_voided_or_canceled.
  • Read-only GraphQL queries under Path.REPORTS; no CSV/PDF (matches the ITBMS Liability report).

Out of scope

  • Per-line drill-down / pagination (these are period summaries, like ITBMS Liability).
  • CSV/Excel export — deferred; the sibling tax report has none either.
  • Purchase-side taxable/exempt breakdown (input tax already lives in ITBMS Liability).
  • Withholding/retention agency reporting.

Data model changes

None. Both reports aggregate existing invoice_details (tax_rate, tax_amount, sub_total_amount) joined to invoices, and read tax_rates metadata. No migration, no new Path/Resource (reuse Path.REPORTS).

What's being implemented

  • Strawberry responsesapp/graphql/reports/strawberry/tax/taxable_exempt_sales_report_response.py, app/graphql/reports/strawberry/tax/sales_taxes_report_response.py.
  • Repositoryapp/graphql/reports/repositories/tax/tax_report_repository.py (sales_by_rate, tax_rate_metadata).
  • Serviceapp/graphql/reports/services/tax/tax_report_service.py.
  • Queriesapp/graphql/reports/queries/tax/tax_report_queries.py (TaxReportQueries, auto-mounted by class_discovery).
  • Teststests/graphql/reports/test_tax_reports.py.

GraphQL surface

taxableExemptSalesReport(dateFilter: DateRangeFilter!): TaxableExemptSalesReport!
salesTaxesReport(dateFilter: DateRangeFilter!): SalesTaxesReport!

type TaxableExemptSalesReport {
  startDate: Date!
  endDate: Date!
  taxableSales: Decimal!
  exemptSales: Decimal!
  totalSales: Decimal!
  taxCollected: Decimal!
  byRate: [TaxableExemptRateLine!]!
}
type TaxableExemptRateLine { rate: Decimal!, taxableBase: Decimal!, taxAmount: Decimal!, invoiceCount: Int! }

type SalesTaxesReport {
  startDate: Date!
  endDate: Date!
  totalTaxableBase: Decimal!
  totalTaxCollected: Decimal!
  byRate: [SalesTaxLine!]!
}
type SalesTaxLine { rate: Decimal!, displayName: String, dgiCode: String, taxableBase: Decimal!, taxCollected: Decimal!, invoiceCount: Int! }

Frontend contract

  • Two new report queries selectable in the Reports (AR) section, both taking a DateRangeFilter.
  • Taxable/Exempt Sales: headline tiles = taxableSales, exemptSales, taxCollected; table = byRate.
  • Sales Taxes: table over byRate (rate, displayName, dgiCode, base, collected, count); footer = totals.
  • exemptSales = sales where the line rate is 0; it is not part of byRate.

Future additions

  • CSV/Excel export once the tax-report family standardizes on it.
  • Retention/withholding certificate report (separate Panama feature).