PhoenixKitCatalogue.Import.Pro100TemplateParser (PhoenixKitCatalogue v0.19.1)

Copy Markdown View Source

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

Shape: ArrayOfTableTableItems/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.

Summary

Functions

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

Types

line()

@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()

@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()
}

Functions

parse(xml)

@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.