Conference expense reimbursement
Produce two spreadsheets from the same rows, with the same totals, and save every receipt they
cite in <conference folder>/החזרים/קבלות/:
- SENT file
<conference folder>/החזרים/<full name> הוצאות.xlsx— the only file that goes to finance. Column Bסעיף= budget-form category; receipts sheet; bank block; rates. No budget-vs-actual comparison. - INTERNAL file
<conference folder>/החזרים/<full name> הוצאות - פנימי.xlsx— the user's local copy, never attached or sent. Column Bהשירות= what each item was (e.g. "קפה", "רכבת שדה–עיר"), aסעיףcategory column afterהערות, and the budget comparison table.
Both are Hebrew and right-to-left; talk to the user in English. The layout comes from the
bundled assets/claim-template.xlsx (labels only); the user never supplies a spreadsheet.
Profile (full name and bank details). Read ~/.config/conference-reimbursement/profile.json
({"full_name": "...", "bank": {"בנק": "...", "סניף": "...", "מספר חשבון": "..."}}). If it is
missing or incomplete, ask for the missing fields with the AskUserQuestion tool (the user types
the values as their own answer), then save the file with permissions 600. Keep it only there —
outside every repository and project folder. Never commit it, never print the bank details in
chat or in the report (say "bank details: from profile"), and copy them only into the scratchpad
spec's bank key.
Ask every question to the user through the AskUserQuestion tool — never in prose — one decision per question, recommended option first.
Pick the mode
- Mode A — full claim. The approved budget form PDF, the bank details (profile), the receipts folder and mailbox access are all available. Run steps 1–7.
- Mode B — partial inputs. One or more of those is missing. Run the same steps and leave the
missing part out, never invent it:
- user declines to give bank details → omit
bank; the script writes empty labelled cells and warns "bank details missing — fill by hand"; report it; - no mailbox access → use only the files the user puts in
קבלות/; report that email was not searched; at step 7 print the email body instead of drafting it; - no budget form → omit
budget; the INTERNAL file is built without the budget table and there is no comparison with approved amounts.
- user declines to give bank details → omit
List every Mode B gap in the report under "to complete by hand".
1. Gather inputs (read-only)
- Approved budget. Open the travel budget form PDF in the conference folder
(
*טופס נסיעות*). Read it yourself; forms differ by institution. Record each approved line in USD (per diem אש״ל, flight טיסה, hotel, insurance, registration, other) and the trip dates. - Profile. Load the full name and bank details as described above (ask only the first time).
- Physical receipts. List
קבלות/and read every image (convert HEIC first, step 3). Record date, provider, amount, currency, card last digits and number of diners. - Email receipts. Search the mailbox over booking date → return date plus about two weeks,
and the user's travel label or folder. Run the per-category queries together in one turn —
read
references/email-sources.mdfirst. Look beyond what the user listed: finding unnoticed expenses is part of the job; propose each one found. - Exclude other trips. Open every finance transfer and every flight booking in the window and match conference, route and dates. Record each exclusion for the report.
2. Ask the policy decisions
Ask only those that apply, one decision per question, recommended option first. These defaults come from one past claim; they are suggestions, not institutional rules.
- Shared hotel booking → own room only (Recommended) / whole booking.
- Shared meals → full bill / own share only. Flag whichever is chosen for the clerk.
- Exchange rate → Bank of Israel representative rate of the return day, one rate per currency (Recommended) / the rate of each receipt's date.
- Shared items (a poster invoice covering two people) → claim the user's share, with a note.
- Add-ons bought with the flight (seat, bag, cover) → inside the flight line (Recommended).
3. Save receipts as PDFs
- Email attachments. If the mail connector returns message text but not attachments, first
re-check whether it can now fetch them; if not, download through browser automation (open the
message, click the attachment's download icon, then move the file from
~/Downloads). Without browser automation, ask the user to download them or drop them intoקבלות/. - Emails with no attachment (order confirmations) → print to PDF.
- Name each file
<what> <provider> <document number>.pdf, in Hebrew. - HEIC photos → add a PDF copy next to the original, never modify the original. macOS:
sips -s format pdf IN.heic --out IN.pdf. Elsewhere use Pillow with pillow-heif if installed, otherwise ask the user to export the photo as PDF or JPEG. For the spreadsheet's receipts sheet, also convert every photo and screenshot to a JPEG named by its receipt base name, in the scratchpad (never over the original):sips -s format jpeg -s formatOptions 70 -Z 1400 IN --out "<receipt base name>.jpg". - Read every saved PDF. Take the amount from the document, not the email text. If they differ, use the document and flag the difference.
4. Build the spreadsheet
-
Fetch the Bank of Israel representative rates (boi.org.il) for the chosen date for every foreign currency used. Put the date in
rates.label:שער יציג בנק ישראל, <dd.mm.yyyy>. Never reuse a rate from memory or from an earlier claim. -
Write the spec JSON in the scratchpad. Schema, layout and Mode B behaviour:
references/spec-format.md; synthetic worked example:references/example-spec.json. Setbankfrom the profile. Leavetemplateout: the bundled template is the default. -
Row rules:
- one row per receipt, sorted by date;
- the amount in exactly one key: its original currency (
usd,eur, or an extra code that is also inrates) orils; receipt= file base name(s) inקבלות/, several joined with+;noteexplains anything a clerk could question (shared bill, half invoice, item split out of a hotel invoice);line= a budget-line name spelled exactly as inbudget; column B of the SENT file shows it (category only, never "coffee" or "dinner" — clerks see approval categories), and the INTERNAL file'sסעיףcolumn and budget table use it;service= a short Hebrew description of the item ("קפה", "ארוחת ערב", "רכבת שדה–עיר"); shown only in column B of the INTERNAL file (omitted → the category is shown);- meals, including the conference dinner → the per-diem line;
- restaurant charges on a hotel invoice → their own meal row, hotel row reduced.
receipt_images= the JPEGs from step 3 (this trip's photos and screenshots only). They go on sheetקבלות, 5 per row, each with a caption cell holding its file base name. Emailed PDFs are not embedded; they go in the zip at step 7.
-
Run:
python3 -I "${CLAUDE_PLUGIN_ROOT}/skills/conference-reimbursement/scripts/build_claim.py" spec.jsonOne run writes both files. It prints JSON with
output(SENT),output_internal,total_ILS,total_USDandwarnings. Fix every warning about budget lines; carry the Mode B warnings into the report.output_internaldefaults to the SENT path with- פנימיbefore.xlsx; set it in the spec only to put the local copy elsewhere. -
The script refuses to overwrite either output unless
"overwrite": true. Never set it on a file the user edited by hand; write a new name or ask.
5. Verify
Done means run and checked. Read references/pitfalls.md first.
-
Run the checks:
python3 -I "${CLAUDE_PLUGIN_ROOT}/skills/conference-reimbursement/scripts/check_claim.py" spec.jsontotal/total_internal: the script's Python total equals theסה״כ בש״חcell of each file after LibreOffice recalculation (to 0.01 ₪), so both files carry the same total;sent_no_budget: the SENT file holds no budget comparison table;receipts: every cited file exists inקבלות/;budget: every row's line is a budget line and the INTERNAL file's budget table actual total equalstotal_ILS / USD rate.
Report a
not_runcheck as not run, never as passed. -
Independent audit: start a subagent with only the SENT spreadsheet and the receipts folder — not your conclusions. Ask it to check every row's amount, currency, date and file against the receipt and to list anything a finance clerk might question. Fix real errors; report the rest.
6. Report
In this order:
- Both paths (SENT, to attach; INTERNAL, local only), row count, total ₪ and ≈ USD versus the approved total.
- Receipts added (and which were found by searching beyond the user's list).
- Rates used, with source and date.
- Over-budget lines, as a table: line, approved, actual, difference.
- Discrepancies between sources (email versus document).
- Exclusions (items belonging to other trips).
- Clerk flags (from
references/pitfalls.md). - To complete by hand (Mode B gaps, script warnings).
- Decisions made without asking.
7. Draft the email to the finance contact
Read references/email-draft.md first; follow it in order. Use and attach only the SENT file
(output, <full name> הוצאות.xlsx); never attach, upload or quote the INTERNAL - פנימי file
— email_summary.py refuses it. In short:
- Read the user's previous claim thread with the finance contact (Sent mail, about two years back, replies included) and copy its style and subject pattern.
- Ask: bank details in or out; budget or rate lines (default: neither); BCC a co-traveller (default: no; never invent an address).
scripts/zip_ereceipts.py spec.json→קבלות אלקטרוניות.zipof the e-receipt PDFs (UTF-8 names; never the macOSzipcommand). Photos and screenshots are in sheetקבלותinstead.scripts/email_summary.py spec.json …→ the RTL HTML body ("היי ," / "מה שלומך? :)" … "תודה רבה!"), the summary table with a קבלה column saying where each receipt is, and the bank details read from the spreadsheet.- Check its
total_ILSequals the build's; mention anyrounding_diff. - Gmail
create_draftwithhtmlBodyfirst. Then attach the SENT spreadsheet and the zip through Claude in Chrome (file_uploadon the draft's compose file input) and confirm both chips — the connector takes attachments only as base64 in the call, andupdate_draftdrops them. Never send. Give the user the draft link.
Formulas
- Row:
ILS_i = amount_i × rate[cur_i], or the ₪ amount itself. - Total:
T = Σ ILS_i; in dollarsT / rate[USD]. - Per budget line ℓ (INTERNAL file):
actual_ℓ = Σ_{line_i = ℓ} ILS_i / rate[USD];diff_ℓ = approved_ℓ − actual_ℓ, negative means over budget.Σ_ℓ actual_ℓ = T / rate[USD]when every row's line is in the budget.
Example: rates USD 4.00, EUR 4.50; registration $500, hotel €600, taxi ₪200 → 2,000 + 2,700 + 200 = 4,900 ₪ = $1,225.
Judgment calls
- Hotel's own invoice beats the booking-site confirmation (it is the tax document).
- Bank details come from the local profile into the spec's
bankkey; the script writes them by label. Ask for them only through AskUserQuestion, never in prose, and never echo them. - An earlier partial reimbursement that belongs to this trip → mark those rows as already claimed.
- The user's institution's rules decide policy questions; ask rather than assert a rule.
References
references/email-sources.md— per-category mailbox queries and what to check (step 1.4).references/spec-format.md— spec keys, both files' layouts, script output, Mode B behaviour (step 4).references/example-spec.json— synthetic complete spec (step 4).references/pitfalls.md— failures seen before and the clerk-flag checklist (steps 5–6).references/email-draft.md— the email draft: previous thread, wording, attachments (step 7).