8e6ee31596
ci / test (push) Successful in 6m18s
History-free snapshot of the private development repository at cbe45c5 (2026-09-09). Code, docs and public catalogue only; no plant or job knowledge layers, no client or job names (tools/scrub.py clean). The intro deck is left out of this snapshot because its slide images could not be checked for job drawings.
208 lines
8.9 KiB
Python
208 lines
8.9 KiB
Python
# -*- coding: utf-8 -*-
|
|
#
|
|
# Reconcile the issued ticket set against the whole job.
|
|
#
|
|
# Every other check on this panel judges one ticket at a time, and no amount of
|
|
# per-ticket checking can find a ticket that is not there. This button asks the
|
|
# question about the SET: every piece printed on something, everything printed
|
|
# about a piece that exists, and the counts reconciling mark by mark.
|
|
#
|
|
# It reads. It never writes, and it opens no transaction.
|
|
#
|
|
# THE SHAPE OF THE PAGE, AND WHOSE IT IS. Owner, 2026-08-30, now standing
|
|
# across this tab: the answer is the headline and is never folded; everything
|
|
# else follows it, collapsed; a section that carries a problem opens itself.
|
|
# The reader's contract is that if nothing below the headline is open, nothing
|
|
# needs him.
|
|
#
|
|
# Here the answer is the VERDICT IN NUMBERS -- pieces and tickets accounted
|
|
# for, and a count per finding -- not the findings themselves. On a job with
|
|
# four hundred uncovered pieces the lists are thousands of lines and the
|
|
# verdict is invisible inside them. So:
|
|
#
|
|
# * the headline is the totals and the finding counts, printed through
|
|
# pyRevit's own markdown and tables;
|
|
# * every non-empty finding is a section below it, OPEN, because there is no
|
|
# finding in this reconciliation that is merely interesting;
|
|
# * coverage by design, the page-set inventory, the categories swept, the
|
|
# checks that found nothing and the timings are folded;
|
|
# * and the two sections that say what this run could NOT see -- out of
|
|
# scope, and blind spots -- are separate, always, and carry their counts
|
|
# on their headers with the fold shut. A blind spot that is a REFUSAL
|
|
# (nothing reconciled) opens itself and is repeated in the headline,
|
|
# because a refusal reported quietly reads as a clean job.
|
|
#
|
|
# WHY THE HTML IS WRITTEN BY HAND AND LOOKS DATED. This output panel is a
|
|
# `System.Windows.Forms.WebBrowser` -- the IE engine, in IE7 document mode.
|
|
# `<details>` does not exist there and fails SILENTLY. The fold is an inline
|
|
# `onclick` plus `style.display`, which is IE5-era DOM; a section with a
|
|
# problem in it renders open as plain HTML and needs no script at all; and the
|
|
# whole report, nothing folded and nothing capped, is written to
|
|
# `last-report.txt` beside the model's other job data on every press, with the
|
|
# page saying where. A fold that will not open therefore costs a file open,
|
|
# never a lost sentence. All of that lives in `vibe.foldreport`.
|
|
|
|
# pylint: disable=import-error,invalid-name,broad-except
|
|
# `__title__` mirrors bundle.yaml's `title:` and is otherwise dead --
|
|
# pyRevit reads it only `if not self.meta`. It is kept in step so that
|
|
# trimming a bundle file cannot bring an old name back. One line, no
|
|
# `\n`: this button is a row in a `.pulldown`, and a row has no second
|
|
# line to put one on.
|
|
__title__ = 'Job Recon'
|
|
|
|
import os
|
|
import traceback
|
|
|
|
from pyrevit import revit, script
|
|
|
|
from vibe import compat, designspec, knowledge, jobrecon, jobreconscan
|
|
from vibe import foldreport as fold
|
|
from vibe import runlog
|
|
|
|
doc = revit.doc
|
|
output = script.get_output()
|
|
output.close_others()
|
|
|
|
REQUIRED = ('member.control_number', 'sheet.control_numbers')
|
|
|
|
# The cap on each list ON THE PAGE. The file written beside the model has no
|
|
# cap at all -- a page is read, a file is searched.
|
|
PAGE_LIMIT = 60
|
|
|
|
HOW_TO_MAP = """
|
|
This reconciliation needs roles the older checks never used. Add them to the
|
|
plant knowledge file (the layer that describes the office TEMPLATE), naming
|
|
whatever your title block and your pieces actually call these things:
|
|
|
|
parameters:
|
|
member:
|
|
control_number: <the piece's control number>
|
|
ticket_created: <optional -- the model's own 'a ticket exists' flag>
|
|
ticket_detailed: <optional>
|
|
sheet:
|
|
total_required: <the quantity the title block states>
|
|
sheet_count: <how many pages the ticket says it has>
|
|
# either list the control-number slots explicitly ...
|
|
control_numbers: [<slot 1>, <slot 2>, ...]
|
|
|
|
nomenclature:
|
|
# ... or, for numbered slots, match them with one expression:
|
|
control_slot_re: '^<PREFIX>_\\d+$'
|
|
|
|
Until then this button refuses rather than reporting a job it cannot see.
|
|
"""
|
|
|
|
|
|
def write_full(recon, full_path):
|
|
"""The whole report, unfolded, beside the model. Returns True if written.
|
|
|
|
Written BEFORE anything is printed, so the file exists even if the
|
|
printing throws. `as_text` first and encode second: under IronPython 2.7
|
|
the report is a byte string and a mark carrying a non-ASCII character
|
|
would make a bare `.encode('utf-8')` raise on the implicit ASCII decode.
|
|
"""
|
|
payload = compat.as_text(
|
|
'\n'.join(jobrecon.format_report(recon, full_path=full_path)))
|
|
handle = open(full_path, 'wb')
|
|
try:
|
|
handle.write(payload.encode('utf-8'))
|
|
finally:
|
|
handle.close()
|
|
return True
|
|
|
|
|
|
def main():
|
|
know = knowledge.load(doc)
|
|
problems = knowledge.config_problems(doc, know)
|
|
if problems:
|
|
for line in problems:
|
|
output.print_md('**' + line + '**')
|
|
runlog.record('Job Recon', doc_title=doc.Title, outcome='refused',
|
|
report_text=('\n'.join(problems) +
|
|
'\n\nJob Recon cannot run until the '
|
|
'configuration is fixed.'))
|
|
return
|
|
|
|
names = knowledge.param_names(know, 'member.control_number')
|
|
slots = (knowledge.param_names(know, 'sheet.control_numbers')
|
|
or knowledge.get(know, 'nomenclature.control_slot_re', None))
|
|
if not names or not slots:
|
|
output.print_md('## Job Recon cannot run')
|
|
output.print_md('Nothing was reconciled, and that is **not** a clean '
|
|
'result -- it is a refusal.')
|
|
print(HOW_TO_MAP)
|
|
runlog.record('Job Recon', doc_title=doc.Title, outcome='refused',
|
|
report_text=('Job Recon cannot run: the control-number '
|
|
'roles are not mapped.\n' + HOW_TO_MAP))
|
|
return
|
|
|
|
recon = jobreconscan.run(doc, know=know)
|
|
|
|
full_path = os.path.join(designspec.sub_dir(doc, 'jobrecon'),
|
|
'last-report.txt')
|
|
written = False
|
|
try:
|
|
written = write_full(recon, full_path)
|
|
except Exception as exc:
|
|
output.print_md('> The full report could not be written to disk ({0}). '
|
|
'Everything is still on this page; use the fold '
|
|
'headers.'.format(exc))
|
|
|
|
sections = jobrecon.render(recon, limit=PAGE_LIMIT,
|
|
full_path=full_path if written else None)
|
|
|
|
# ---- the answer, through pyRevit, big and first.
|
|
for block in sections[0].blocks:
|
|
if block[0] == fold.TABLE:
|
|
output.print_table(block[2], columns=block[1])
|
|
else:
|
|
output.print_md(block[1])
|
|
|
|
# ---- everything else, folded.
|
|
tail = sections[1:]
|
|
if not tail:
|
|
# The headline alone was the whole page -- recorded the same as a
|
|
# full page, with the file copy's path when one was written.
|
|
runlog.record('Job Recon', doc_title=doc.Title, outcome='ok',
|
|
report_path=full_path if written else None,
|
|
report_text='Job Recon ran -- nothing below the headline '
|
|
'to report.')
|
|
return
|
|
output.add_style(fold.STYLE)
|
|
output.inject_script(fold.SCRIPT)
|
|
opened = fold.sections_with_problems(tail)
|
|
output.print_md('---')
|
|
if opened:
|
|
output.print_md('**{0} of the {1} section(s) below opened themselves '
|
|
'— they have something in them that needs you.** '
|
|
'The rest are folded; click a header to open one.'
|
|
.format(len(opened), len(tail)))
|
|
else:
|
|
output.print_md('**Nothing below needs you.** Every section is folded; '
|
|
'click a header to open one.')
|
|
if written:
|
|
output.print_md('The whole report, nothing folded and nothing capped, '
|
|
'is in `{0}`.'.format(full_path))
|
|
output.print_html(fold.html(tail))
|
|
|
|
# The page is complete and the file copy exists. The verdict in numbers
|
|
# is the headline, which `sections[0]` carried as markdown -- this short
|
|
# summary says what reconciled, and points at the file for the rest.
|
|
runlog.record('Job Recon', doc_title=doc.Title, outcome='ok',
|
|
report_path=full_path if written else None,
|
|
report_text='Job Recon ran; full report at {0}'
|
|
.format(full_path if written else 'this page'))
|
|
|
|
|
|
try:
|
|
main()
|
|
except Exception as e:
|
|
# The deliberate exits above never reach here (script.exit() raises
|
|
# SystemExit, which Exception does not catch) -- this is the
|
|
# unexpected-exception path, and pyRevit shows its own dialog after
|
|
# the record is written.
|
|
runlog.record('Job Recon', doc_title=doc.Title,
|
|
outcome='error', error=str(e),
|
|
traceback_text=traceback.format_exc())
|
|
raise
|