This project provides a unified Python integration client and test suite to verify connectivity and support programmatically creating draft expense reports in SAP Concur.
It supports two modes of interaction:
- API Integration (Direct): Uses SAP Concur REST APIs with OAuth 2.0 Client Credentials authentication (requires API permissions and administrative licensing).
- Browser Automation (Playwright): Automates a browser session to perform UI clicks (useful if your organization doesn't have Web Services API access or if direct API keys are unavailable).
- Client Web Services License: A valid license to enable API access.
- App Registration: A registered application in the SAP Concur App Center to obtain a
Client IDandClient Secret. - Scopes: Your application must have the
EXPRPT(Expense Report) scope enabled. - Target User Account: A valid SAP Concur Login ID.
- Login Credentials: Standard Concur username/password or SSO login.
- Playwright Setup: Playwright must be installed locally along with chromium binaries (handled automatically by
./ccworks setup).
brew tap pu-orfe/tap
brew install ccworksThis gives you a ccworks command anywhere on your PATH. The first time you
run a browser-based command (e.g. ccworks session login), ccworks will prompt to
download Playwright's chromium browser (~180 MB) into
~/Library/Caches/ms-playwright โ a one-time step.
Session state (login cookies, screenshots) is written to
~/Library/Application Support/ccworks (macOS) or
$XDG_STATE_HOME/ccworks (Linux). Override with CCWORKS_STATE_DIR=/some/path.
git clone https://github.com/pu-orfe/ccworks.git
cd ccworks
./ccworks setup # creates .venv, `pip install -e .`, installs chromiumccworks exposes a single set of commands, grouped by resource as
<group> <subcommand> โ the convention used by gh, kubectl, and modern
docker:
ccworks report list
ccworks report show "Q1 Travel" --deep
ccworks card show "Office Depot"
ccworks session statusThe two entry points accept identical arguments, provided the installed package is new enough to have this surface:
ccworks <group> <sub> |
./ccworks <group> <sub> |
|
|---|---|---|
| Source | Entry point from Homebrew / pip install |
The zsh launcher in a repo checkout |
| Behaviour | The CLI itself | Manages .venv, then forwards verbatim to the CLI |
| Tracks | The released version you installed | Your working tree, always current |
An installation predating the noun-verb surface rejects these commands with
invalid choice: 'report' and lists the old flat names instead. That is a stale
binary, not a bug โ check with ccworks --help, and either upgrade
(brew upgrade ccworks) or use ./ccworks from the checkout, which always
reflects your working tree.
The launcher owns only the checkout-only chores that cannot work from an
installed package โ setup, test-local, test-docker,
test-browser-smoke, test-reports-live, test-receipts-live. Everything else
it passes straight through, so the two forms behave identically.
Run ccworks for the grouped reference, or ccworks <group> <sub> --help for a
command's flags.
| Command | Scope / Notes |
|---|---|
./ccworks setup |
Create .venv, install the package editable, install chromium. |
./ccworks test-local |
Run mock unit tests locally using .venv. |
./ccworks test-docker |
Run mock unit tests in Docker (offline, no credentials needed). |
./ccworks test-browser-smoke |
Playwright browser CRUD smoke tests against the local mock server. |
./ccworks test-reports-live |
Playwright reports CRUD smoke test against your real Concur account. |
./ccworks test-receipts-live |
Playwright receipts smoke test against your real Concur account. |
| Group | Command | Scope / Notes |
|---|---|---|
| report | report list [--historical] [--view F] |
List draft reports, or historical ones with --historical. |
report show NAME [--deep] [--view F] |
Line-item details. --deep opens each transaction (slower, fully accurate). |
|
report create [--name N] [--purpose P] [--comment C] [--headed] |
Create a draft report. | |
report update NAME [--name --purpose --comment --justification] |
Update header fields. | |
report reconcile NAME [--rules PATH] [--submit] |
Reconcile transactions; review-only unless --submit. |
|
report submit NAME |
Submit for approval. | |
report delete NAME / report delete --all-drafts |
Delete one report, or every draft. | |
report apply-json PATH |
Apply an edited report show JSON back to Concur. |
|
| txn | txn update NAME IDX... [--type --purpose --comment --justification] |
Update one or more transactions by 1-based index. |
txn allocations NAME [--view F] |
List chartstring allocations (Dept, Fund, Program). | |
txn allocate NAME IDX --dept D --fund F [--prog P] |
Add a chartstring to a transaction. Adds; it does not replace โ a second allocation splits the expense by percentage. | |
txn unallocate NAME IDX |
Clear every allocation on a transaction, returning it to the report's default. Use before txn allocate to replace a chartstring rather than split it. |
|
txn attach-receipt NAME --merchant M --file PATH |
Attach a local receipt file to a transaction row. | |
| card | card list [--view F] |
List credit-card transactions. |
card show MERCHANT_OR_ID [--view F] |
Details for one card transaction. | |
| receipt | receipt delete --all |
Delete every available receipt. --all is required. |
| delegate | delegate add WHO [--can prepare submit approve] |
Add an expense delegate (default: prepare). |
delegate remove WHO |
Remove an expense delegate. | |
| session | session login |
Headed browser for manual SSO; saves session state. |
session status |
Check whether the saved session is still valid. | |
| api | api test |
Run the API client test suite (needs .env OAuth creds). |
| โ | nuke |
Delete ALL draft reports and all available receipts. |
Global flags work anywhere in the argument list: -V/--version,
-v/--verbose (logs to stderr), --output {json,text} (default json).
-V reports the version of the entry point you invoked, which is worth checking
first when a command is rejected as unrecognized โ ./ccworks tracks your
working tree while an installed ccworks tracks whatever release you installed:
$ ccworks --version
ccworks 0.3.3stdout is data, stderr is diagnostics. Query commands print JSON on stdout
while logs, spinners, and session warnings go to stderr, so 2>/dev/null is
safe when piping to jq.
A statement period is reconciled in the order below. The order matters: each step avoids a trap the next one would otherwise hit.
1. Capture the report and check the capture.
ccworks report list
ccworks report show "Statement Report 06/16 - 07/31" > report.jsonRead extraction.complete before trusting anything downstream. false means
rows were skipped or a detail pane never opened, and the reasons are listed in
extraction. Byte-identical line items are kept โ two shipments the same
day for the same amount are two expenses, not a duplicate.
Indices are 1-based and dense, and the same number addresses the same
expense in report show, txn update, txn allocate, and report apply-json.
They are positional, so re-capture before acting on numbers from an earlier run.
(txn allocations is the exception: it reports section_number, not the shared
index. Correlate on date + amount.)
2. Attach receipts. One apply-json pass, one browser session:
{ "report_name": "Statement Report 06/16 - 07/31",
"expenses": [
{ "index": 1, "vendor": "ANTHROPIC* CLAUDE TEAM", "amount": "$500.00",
"receipt_file_path": "/path/to/001.pdf" }
] }vendor and amount are the identity guard: apply-json refuses a row whose
amount and vendor do not match what you described, rather than writing to a
different expense. Omit a field to leave it alone; pass "" to clear it.
3. Correct expense types where they are wrong. Usually only a few rows.
Supply the full option text โ a bare prefix that matches more than one option is
refused rather than guessed. An expense typed Undefined cannot be saved at
all, so set its type in the same row as its text or neither will persist.
4. Fill Business Purpose and Comment for every transaction. Batch all of
them into one apply-json payload; --justification on txn update sets both
fields to the same text for a single row.
5. Allocate chartstrings last.
ccworks txn allocate "Statement Report 06/16 - 07/31" 6 \
--dept 25604 --fund A0002 --prog FC631
ccworks txn unallocate "Statement Report 06/16 - 07/31" 6 # clear, to replacetxn allocate adds; it does not replace. A second allocation splits the
expense by percentage rather than superseding the first, and both writes report
success. Clear first with txn unallocate to replace a chartstring.
Allocate last because editing any field on an allocated expense makes Concur ask "Update Other Items?" before it will commit โ ccworks answers it, but every later write then pays for the extra round trip.
Measured against a live 16-row statement, per transaction:
| Work | Cost |
|---|---|
| Business purpose, comment, receipt, save | ~10s |
| Allocation (clear then add, each verified) | ~103s |
| Full treatment | ~113s |
So a 16-row statement is roughly 30 minutes for everything, or about 3 minutes if you are only writing text and receipts. Allocation dominates because clearing and adding each verify against a fresh reload of the report.
That matters because Concur's session lasts about 60 minutes from login and is
not extended by use. One full pass fits; a teardown and a re-apply does
not. report apply-json estimates the payload against session status's
expires_in_minutes and refuses up front rather than dying partway, and stops
cleanly with remaining_indices if the session runs short mid-run. Pass
--ignore-session-budget to override.
6. Verify, then submit.
ccworks report show "Statement Report 06/16 - 07/31" --deep
ccworks report submit "Statement Report 06/16 - 07/31"--deep opens every transaction, so it is the only way to read business
purpose, comment and type back โ and it is slow. Submission is outward-facing
and not silently undoable.
report reconcile is review-only unless given --submit. Rules are keyed by
merchant substring, matched case-insensitively, first match wins:
{ "United Airlines": {
"expense_type": "Airfare",
"business_purpose": "INFORMS 2026 travel",
"allocation_code": "(25605) ORF-Technical Support",
"receipt_path": "/Users/you/receipts/united.pdf" } }Unmatched transactions are skipped with a warning on stderr โ check those rather
than assuming full coverage. Merchant matching cannot distinguish rows that
share a vendor, so prefer apply-json with explicit indices where a statement
repeats a merchant.
ccworks card list # corporate card transactions
ccworks card show "Office Depot"
ccworks delegate add "John Doe" --can prepare submit
ccworks report delete --all-drafts # destructiveThis project includes a project-local extension for the Pi coding agent (an open-source terminal-based AI coding assistant at pi.dev).
The extension is written in TypeScript and is saved at .pi/extensions/concur.ts. It registers custom tools that allow the Pi agent to interact directly with your SAP Concur session.
concur_list_reports(filter_view, is_old): Queries and lists active or historical expense reports.concur_report_details(report_name, filter_view): Fetches line-item details of a report.concur_list_card_transactions(filter_view): Lists card transactions from Available Expenses.concur_reconcile_report(report_name, rules, submit): Automatically reconciles transactions using JSON rules, and optionally submits the report (default:submitis false, leaving it in draft mode for review).concur_attach_receipt(report_name, merchant, receipt_path): Uploads and attaches a local receipt file to an expense.concur_create_report(name, purpose, comment): Creates a new draft expense report headlessly.concur_delete_report(report_name): Deletes a draft expense report by name.concur_card_transaction_details(merchant_or_id, filter_view): Fetches details of a specific credit card transaction by merchant or ID.concur_add_delegate(name_or_email, permissions): Adds a new expense delegate in settings with specified permissions.concur_remove_delegate(name_or_email): Removes an expense delegate from settings by name or email.concur_nuke_drafts_and_receipts(): Deletes all draft reports and available receipts inside Concur (intended for testing cleanup).concur_check_session(): Checks whether the currently saved browser session state is active and valid (returns true if authenticated, false if expired or missing).concur_update_transaction(report_name, transaction_index, type, purpose, comment): Updates fields (type, business purpose, comment) of a specific transaction inside an expense report.
If you use Pi within this repository, it will automatically discover the extension located in the .pi/extensions/ folder. You can also manually load it or reload your active session by running /reload inside the Pi terminal client.
Note:
concur_check_session()returns exit code 0 (authenticated) or 2 (invalid/expired session). It catches a non-zero exit and reportsfalseinstead of raising.
Receipt-to-Report Attachment(Already implemented โ seetxn attach-receipt/concur_attach_receipt).- Expense Itemization Automation:
- Description: Parse lodging/hotel folios or receipt text (using OCR/LLM) and programmatically itemize room rates, room taxes, parking, and meals.
- Value: Eliminates tedious manual breakdowns of hotel checkout bills.
- Approval workflows for Managers:
- Description: Scan pending approval reports, display total summaries, and click approve or send back to employees with custom comments.
- Value: Streamlines managers' review process via CLI/Slack commands.
- Export to ERP/Accounting Formats:
- Description: Export queried reports and transactions directly to CSV, JSON, or standard ERP formats (SAP, NetSuite, QuickBooks).
- Value: Syncs Concur expense data directly into business accounting books.
This project includes a fully automated GitHub Actions CI/CD Pipeline defined in .github/workflows/ci.yml. On every push and pull request to the main branch, it runs:
- Host-Based Unit Tests: Runs mock API tests directly on the runner.
- Containerized Unit Tests: Builds and executes mock unit tests inside a Docker container using
docker-compose. - End-to-End Browser Smoke & Regression Tests: Launches a stateful mock server and runs headless Playwright tests, including full CRUD and justification/classification regression suites.
Modern enterprise security often requires MFA or SSO login screens that standard automation cannot programmatically bypass. This project handles this using a Session State Preservation strategy:
- Run the manual session setup:
./ccworks session login
- A headed Chromium window will open. Enter your email/password, solve SSO if prompted, and complete the MFA authentication.
- Once logged in and redirected to the SAP Concur dashboard page, return to your terminal and press ENTER.
- Your authenticated session token, cookies, and local storage are saved into
concur_session.json. - Subsequent automated actions will load this file and run headlessly without requiring login or prompt parameters.
โโโ .env.example # Environment variables configuration template
โโโ .pi/
โ โโโ extensions/
โ โโโ concur.ts # Pi coding agent extension (13 tools)
โโโ Dockerfile # Docker container definition
โโโ docker-compose.yml # Service orchestration for testing
โโโ ccworks # Zsh shell helper script (CLI entry point)
โโโ requirements.txt # Third-party Python dependencies
โโโ src/
โ โโโ __init__.py
โ โโโ browser_client.py # Playwright Browser Automation Client
โ โโโ client.py # SAP Concur REST API integration (OAuth2)
โ โโโ cli.py # Argument parsing, signal handling, command routing
โโโ tests/
โ โโโ __init__.py
โ โโโ mock_concur_server.py # Stateful local mock SAP Concur Server
โ โโโ smoke_test_reports.py # Live reports CRUD smoke test (Playwright)
โ โโโ smoke_test_receipts.py # Live receipts list/delete smoke test
โ โโโ test_allocations_crud.py # Allocations read/write regression tests
โ โโโ test_browser_smoke.py # E2E local browser smoke tests against mock server
โ โโโ test_client.py # Unit tests using requests mocks
โ โโโ test_justification.py # Justification & classification regression tests
โ โโโ test_transaction_fields_crud.py # Transaction field (type/purpose/comment) CRUD tests
โโโ .github/
โโโ workflows/
โโโ ci.yml # GitHub Actions CI/CD workflow configuration