# `PhoenixKitCatalogue.Import.Pro100TemplateParser`
[🔗](https://github.com/BeamLabEU/phoenix_kit_catalogue/blob/v0.19.1/lib/phoenix_kit_catalogue/import/pro100_template_parser.ex#L1)

Parses a PRO100 `configTables` export — the estimate/price template PRO100 uses
to quote a project. XML, despite the extension the exporter gives it.

Shape: `ArrayOfTable` → `Table` → `Items/TableItem`. Three things about that
structure are easy to get wrong and each was measured against the real export
(`Pro100doc_configTables_2026-07-15_192106`, 10 tables / 708 rows):

  * **Only `Table/Items/TableItem` are rows.** The same file serialises 584 of
    them twice more, under `TableItem/SumTables/Table` and
    `CalculatedItem/SumTables/Table` — snapshots of the summed tables, not
    products. A `//TableItem` sweep yields 1876 rows instead of 708.

  * **…but the nested `SumTables/Table/Name` list is still needed.** It is where
    a computed row's rule targets come from, and it lives inside the branch
    whose *rows* are ignored.

  * **A row's table comes from the XML nesting, never from `TableId`.**
    `TableId` is `0` on 707 of 708 rows; only the computed row carries a real
    one. (`TableGuid` is a valid alternative — it equals the parent table's
    `TemplateGuid` on every row — but nesting is what we already have.)

Sections are implicit: a row whose `Name` ends with `:` is a header for the rows
after it in `SortOrder` order. 44 lines precede any header in their table and
belong to no section.

Prices in the file are **gross**; converting them is the caller's business —
this module reports what the file says.

# `line`

```elixir
@type line() :: %{
  name: String.t(),
  section: String.t() | nil,
  gross_price: Decimal.t() | nil,
  template_guid: String.t(),
  sort_order: integer(),
  specification: String.t() | nil,
  percentage: Decimal.t() | nil,
  sum_tables: [String.t()]
}
```

# `table`

```elixir
@type table() :: %{
  name: String.t(),
  template_guid: String.t(),
  id: String.t(),
  sort_order: integer(),
  sections: [String.t()],
  lines: [line()],
  blank_count: non_neg_integer()
}
```

# `parse`

```elixir
@spec parse(binary()) :: {:ok, [table()]} | {:error, term()}
```

Parses the export into `{:ok, [table()]}`.

Blank-named rows are counted (`:blank_count`) and dropped; header rows become
`:sections` and set the `:section` of the lines that follow them.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
