diff --git a/README.md b/README.md index e0fda4e..d339763 100644 --- a/README.md +++ b/README.md @@ -232,6 +232,25 @@ checked rather than intended. On the SANS 1200 template, where quantities are derived (`=G35`, `=CEILING.MATH(...)`), writing a literal over that would destroy the engineer's model while looking like it worked. +## Tariffs carried inside a sum + +Candy sometimes prints a **word where a rate would go** — `INCL`, `Incl VAT`, +`PROV` — meaning that tariff is included in the sum above rather than priced +separately. Those lines have a quantity but no figure, and they used to arrive +in the consultant's file as **blank cells**, which read as a failure to pull +through. That is exactly what the engineer reported. + +They now carry the source's own wording into the rate cell, and are listed as a +note rather than a disagreement: + +``` +F15 = 'INCL' a) Office buildings +F17 = 'INCL' b) Survey Equipment +``` + +The amount column is left alone, because there is genuinely no figure for that +line — the money is in the sum above it. 20 of these on the Blueberry pair. + ## A person decides the rest The aligner leaves some lines unplaced because nothing scored high enough, and diff --git a/boq_engine/extract.py b/boq_engine/extract.py index b4097b8..97256a1 100644 --- a/boq_engine/extract.py +++ b/boq_engine/extract.py @@ -147,6 +147,11 @@ class Row: qty: float | None = None rate: float | None = None amount_txt: str = "" + # Candy prints a WORD in the rate/amount column when a tariff is carried + # inside a sum rather than priced separately - "INCL", "Incl VAT", "PROV". + # That is a real tariff decision, not a missing number, and it has to + # survive into the consultant's file instead of arriving as a blank. + tariff_note: str = "" top: float = 0.0 page: int = 0 @@ -196,9 +201,13 @@ def _page_rows(page, pno: int) -> list[Row]: continue if col == "rate" and r.rate is None: r.rate = _to_num(txt) + if r.rate is None and not txt.replace(".", "").isdigit(): + r.tariff_note = (r.tariff_note + " " + txt).strip() continue if col == "amount": r.amount_txt = (r.amount_txt + " " + txt).strip() + if _to_num(txt) is None and not txt.replace(".", "").isdigit(): + r.tariff_note = (r.tariff_note + " " + txt).strip() continue if UNIT_X_MIN <= x0 < UNIT_X_MAX: r.unit = (r.unit + txt).strip() @@ -348,6 +357,7 @@ def _merge_continuations(rows: list[Row]) -> list[Row]: @dataclass class Line: kind: str = "line" + tariff_note: str = "" number: str = "" desc: str = "" unit: str = "" @@ -505,6 +515,7 @@ def _build(content: list[Row]) -> Group: and t.depth(num) >= 3 line = Line( + tariff_note=r.tariff_note, number=num, desc=f"{sub}) {body}" if sub else body, unit=r.unit.strip(), diff --git a/boq_engine/template.py b/boq_engine/template.py index 29555e0..10dc01c 100644 --- a/boq_engine/template.py +++ b/boq_engine/template.py @@ -507,6 +507,7 @@ def source_lines(tree: dict, *, with_prices: bool = True) -> list[dict]: return out.append({ "number": node.get("number", "") or "", + "tariff_note": (node.get("tariff_note") or "").strip(), "desc": (node.get("desc") or "").strip(), "unit": (node.get("unit") or "").strip(), "qty": node.get("qty"), @@ -885,6 +886,23 @@ def _write_one(wb, name: str, lay: Layout, ln: dict, field="unit", template_value=cur, source_value=ln["unit"]) + # --- a tariff carried inside a sum ("INCL", "incl VAT") ----------------- + # Written as the literal text the source printed. A blank cell here reads + # as a failure to the consultant; the source's own wording is the honest + # answer, and the amount column is left alone because there is no figure. + note = (ln.get("tariff_note") or "").strip() + if note and ln["rate"] is None and c_rate and \ + ws.cell(r, c_rate).value in (None, ""): + if _writable(ws, r, c_rate, res, name, label, "rate"): + ws.cell(r, c_rate, note) + res.wrote(name, r, c_rate, note) + res.written += 1 + res.add("info", name, r, label, + f"carried in the sum - the source prints \"{note}\" rather " + f"than a rate, so that wording was kept. There is no amount " + f"for this line.", + source=note) + # --- quantity ------------------------------------------------------------ stop = False if c_qty and ln["qty"] is not None: