An automatic receipt sorting, renaming, and aggregation tool using OCR (Surya).
When to Use
- Sort receipts, Organize receipts, receipt sorting
- I want to automatically organize receipt images in a folder by project
- I want to name a large number of receipt photos with date, amount, and purpose
- I want to batch rename and aggregate business trip expense receipts
Core Workflow
- Receive images / PDF / ZIP / videos from the input folder
- Extract date, amount, country, card, and purpose via OCR
- Apply quality correction and normalize to standard file names
- Sort by project
- Output a Markdown summary
Architecture
入力フォルダ(incoming/unassigned 等)
│
├─ JPEG/PNG/PDF/ZIP → OCR → 抽出 → リネーム → PJフォルダへ移動
├─ MP4/MOV → ffmpeg → JPG → OCR → ...
│
├─ カード不明 or テキスト不可 → 未分類フォルダ → 再仕分け可能
│
└─ 個人経費(D365対象外) → PJフォルダ/private/
private/Folder: Store personal expense receipts that are not submitted to D365. Move here when the user determines after OCR that it is a "personal expense".
Naming Convention
YYYY-MM-DD-国コード-金額[-カード]-分類.ext
例:
2026-02-09-jpn-14320-amex-shinkansen.jpeg
2026-02-11-us-272-amex-hotel.jpeg
2026-02-12-us-190.38-meal.jpeg ← カードなし
2026-02-09-us-6.80-amex-meal-starbucks.jpeg
For the country code list, classification list, D365 category mapping, and attachment procedure, refer to references/d365-expense-guide.md .
CLI Usage
# Dry-run(確認のみ、ファイル移動なし)
python receipt_sorter.py --project "202602_Domestic_Trip" --dry-run
# 本実行
python receipt_sorter.py --project "202602_Domestic_Trip" --input "incoming\unassigned"
# ログ指定
python receipt_sorter.py --project "202602_Domestic_Trip" --input "incoming\unassigned" --log "result.csv"
Options
| Options | Description | Default |
|---|---|---|
--project |
Output PJ folder name (required) | - |
--input |
Input folder | incoming/unassigned/ |
--dry-run |
Display results only without moving files | false |
--log |
Result CSV path | <PJフォルダ>/csv/<PJ名>_dryrun.csv or _result.csv |
Markdown Summary Output
After the actual run, <PJ名>_summary.md is automatically generated in the PJ folder.
Included content:
- Project total (mixed currencies, reference value)
- Total table by country code
- Total table by date x country code
- All line items table (date, country, amount, card, classification, Japanese summary, file name)
Intake Pattern
- Unclassified raw files should be consolidated not in the workspace root but in a dedicated intake folder
- Example:
incoming/unassigned/ - Files whose project is confirmed may be placed directly into the project folder without going through the intake folder
- For travel statements whose project is unknown, match read-only approval records, schedules, and reservation records by date, section, and purpose before moving. If there is no match, do not guess
--projectand leave it unclassified for confirmation - For system-issued transportation receipts, use the service date / boarding date, not the display date or purchase date. Only when both the departure point and arrival point can be confirmed, append
origin-to-destinationto the end of the classification to distinguish round trips - Limit the execution target to the dedicated intake, and perform the actual run after checking the dry-run results
- Handle dry-run copies and verification inputs in a temporary folder, and delete them after completion
Setup
Script Reference
- OCR sorter: references/receipt_sorter.py
- D365 read-only report checker: scripts/inspect-d365-expense.mjs
D365 Expense Integration
Attaching OCR-processed files to D365 Expense is an important use case of this skill, but the category table and attachment procedure are separated from the main body.
- Report creation / metadata editing
- Editing an Expense line's Category / Description / Country / tax group
- line-level receipt upload / replacement / verification
- read-only JSON verification of report / line / receipt card
- Avoiding stale display and active row misalignment in the D365 UI
For details, refer to references/d365-expense-guide.md .
Known Limitations
- OCR accuracy depends on the Surya model (misreads occur with handwritten or heavily distorted receipts)
- The maximum candidate amount is adopted, but individual line items cannot be extracted from receipts with multiple line items
- Amount concatenation misread: OCR may combine digits across line breaks and return a huge integer (e.g.,
14520+300→14520300). This is defended against with an upper-limit check (anything over 5 million is excluded), but it is not perfect - Only one intermediate frame is extracted from videos (optimal frame selection is not implemented)
- Japanese kanji store names are dropped during ASCII conversion when slugified (katakana is romanized)
- Windows PIL file lock:
Image.open()holds the handle andshutil.move()がWinError 32fails with it.with+img.copy()+ retries are already handled
Done Criteria
- Can be processed consistently from OCR extraction through renaming
- Can be sorted by project using standard file names
- Can explain how unclassified and private are handled
- A Markdown summary is output
- When uploading to D365, it can be attached according to the reference matching rules