Repository navigation
Conversation
Add `add-attachments NUMBER -f FILE [-f FILE ...]` to every transaction type whose API accepts file uploads: invoices, bills, sales orders, purchase orders, journal entries, cash sales, direct expenses, customer deposits, customer refunds, customer prepayments, customer prepayment applications, vendor payments, and vendor refunds. Invoices also get `replace-attachments`, which swaps out every existing attachment. - Every file is checked against the API's 10 MiB limit, and for being empty, before anything is sent, so one bad file cannot leave the others half-uploaded. Folders are rejected. - The record argument is the record number. A prefixed internal ID such as IN-42 is rejected instead of being read as number 42, which would upload to a different record. - statistical-journals add-attachments now uses the same code path. It keeps accepting SJ-3, since that prefix wraps the record number, and gains the size check.
Rich colors its error panels under CI, and the escape codes split a wrapped message, so substring checks failed there but passed locally.
This was referenced Oct 30, 2026
The API rejects an Idempotency-Key on non-JSON bodies, so every upload failed with a 400. Uploads now go unkeyed and, since a retry could then attach the files twice, are only retried after a 429. The command now lists the record's attachments instead of reprinting the whole record, which never showed them.
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Closes DEV-12110
Summary
Thirteen transaction types accept
POST /{number}/attachments/in the public API, and invoices also acceptPOST /{number}/attachments/replace/. The CLI couldn't upload to any of them, except statistical journals, which had their own command. This PR adds the commands through the resource factory, so uploading a receipt or supporting document is one shell command:Changes
add-attachmentson invoices, bills, sales orders, purchase orders, journal entries, cash sales, direct expenses, customer deposits, customer refunds, customer prepayments, customer prepayment applications, vendor payments, and vendor refunds.replace-attachmentson invoices only, since that's the only resource with the route. The list matches the v2 API's routes, and a registration test fails if a resource gains a command the API doesn't have.has_attachments/has_replace_attachmentsflags. They raise at registration if replace is set without add, or attachments without record numbers.IN-42is the record's internal ID, but the upload route takes the record number. Stripping the prefix would upload to whichever record has number 42, so the command rejects prefixed IDs with an error that says what to pass. Statistical journals are the exception: theirSJ-prefix wraps the number, soSJ-3is still accepted. That's recorded as_NUMBER_PREFIXEDnext to the display-prefix table. Non-ASCII digits such as²are rejected too.statistical-journals add-attachmentsonmain, which has failed this way since 26961bf added the key to every write.statistical-journals add-attachmentsnow uses the shared helper. It gains the size check and rejects folders.3andSJ-3still work.Multiparttype alias moved toclient.pyand is used inDualEntryClient.post.Test plan
uv run pytest): 357 passed. The 136 skipped are integration tests that needX_API_KEY.uv run ruff check .anduv run ruff format --check .)dualentry <command>against a live test organization; see Artifacts.tests/test_attachments.pycovers: which resources get which commands, the exact multipart request for each resource, several files kept in order, a file exactly at the limit,SJ-handling, and rejection before any request for prefixed IDs, non-ASCII digits, empty, oversized, missing, folder, and no--file.Artifacts
Run against a test organization on production (
api.dualentry.com) with a real API key, using small dummy files.Found by running it: every upload failed
The client puts an
Idempotency-Keyon every write, and the API rejects that header on non-JSON bodies (core/idempotency.py: "only supported on JSON requests"). The unit tests mocked the client, so they never saw it. Fixed in 87cde03:TestMultipartUploadsclient tests cover this. Three of the four fail without the fix.Before (first push of this PR;
statistical-journals add-attachmentsonmainfails the same way)Also found: on success the command reprinted the whole invoice, and nothing in the CLI renders attachments, so you couldn't see what was uploaded. It now lists the record's attachments.
-o jsonstill prints the full record.After (this PR)
Invoice 3 had no attachments before this run (
"attachments": []frominvoices get 3 -o json).Live integration suite
Same result as
main: the 4 integration failures already onmain(429 without backoff; 403 onvat-rates/product-tax-codesget), and nothing new. One run showed an extra 403 oninvoiceslist; two reruns passed.Merge note
This PR and #39 both edit the import list of
statistical_journals.py, so whichever merges second needs a one-line conflict resolved.