Skip to main content
iVentureTeam

attach_document

The Attach Receipt button on Odoo expenses is a reusable view widget: attach_document. It saves the record before it ever opens the file picker, and it can hand the uploaded files to any model method you name.

Siddharth JambukiyaSiddharth JambukiyaOdoo Techno-Functional Consultant
August 26, 2026Updated August 26, 20265 min read
Technical nameattach_document
Viewsform (view widget, element, typically in the header)
Moduleweb, present in every Odoo database
Used in core2 occurrences in hr_expense: the Attach Receipt button, once highlighted and once plain
VersionsOdoo 20.0, Odoo 19.0, Odoo 18.0, Odoo 17.0, Odoo 16.0
No-code setupNo. The button is added in view XML; Studio has no upload button component
Alternativesmany2many_binary, binary, account_file_uploader

What the attach document widget does

Some records are built around an incoming file: an expense around its receipt, a claim around its evidence. attach_document is the small view widget behind that pattern, a button that opens the native file picker, uploads the chosen files as attachments of the current record, and optionally hands them to a Python method for processing.

Because it is a view widget, it appears as a <widget> element and stores nothing itself. Its three attributes cover the label, the styling and the follow-up call, and everything else, the save-first rule, the multi-file upload, the size check, is fixed behavior read from the source.

What this means for your team

The widget's value is workflow compression. On expenses, one click covers what used to be four steps: save the draft, open the chatter, upload the receipt, then trigger digitization. The action attribute is the hinge; core points it at the expense method that runs OCR over the new attachments and fills amounts and dates from the receipt, and the automatic reload means the user watches the form populate itself.

The same pattern fits any document-first process you build: proof-of-delivery on field service tasks, signed contracts on subscriptions, certificates on quality checks. Wherever a scanned file should immediately drive record data, this button plus one model method is the shortest path.

Supported options in Odoo 19

A view widget takes attributes on the element, not an options dictionary. All three verified in attach_document.js, Odoo 19.0 web module.

OptionTypeWhat it does
stringstring (attribute)The button label. Required by the component's props; omitting it fails props validation.
actionstring (attribute)Name of a method on the current model, called after upload with attachment_ids as a keyword argument; the record reloads afterward. Omit it and files simply become attachments.
highlightboolean (attribute)Renders the button as btn-primary instead of btn-secondary. Core uses it to spotlight Attach Receipt until a receipt exists.(default: false)

The save is not optional. beforeOpen returns record.save() and the file picker only opens when that succeeds. On a new record with unfilled required fields the button appears to do nothing except surface the validation errors, which is the number one support question about this widget.

Working examples

The expense pattern, verbatim from core

<widget name="attach_document" string="Attach Receipt"
        action="attach_document" highlight="1"
        invisible="nb_attachment >= 1"/>

Highlighted while the expense has no receipt yet; a second, non-highlighted copy shows once attachments exist. The action value is a method name on the current model.

A minimal upload button, no follow-up

<widget name="attach_document" string="Upload Evidence"/>

Files land in the record's attachments and nothing else happens.

The Python side of an action

def attach_document(self, attachment_ids=None):
    # called once per click, after upload; record reloads afterward
    for attachment in self.env['ir.attachment'].browse(attachment_ids):
        ...

What happens between click and reload

The upload path is worth reading once because it explains every edge case. The widget builds its own hidden file input with multiple enabled and accept="*", so there is no way in 19 to restrict file types from XML. On change, it loops the selection through checkFileSize, and a single file over the server limit aborts the entire batch with a notification, nothing partial uploads. The files then post to /web/binary/upload_attachment with the record's model and id, which is why the record must exist first and why beforeOpen forces the save.

Only after a successful upload does the optional action run, as an ORM call on the current record with attachment_ids passed as a keyword argument, followed by record.load(). Errors from the action surface as normal Odoo dialogs, and the attachments remain, they were created before the action ran. If your method should be transactional with the upload, it has to clean up the attachments itself.

Version compatibility

VersionStatusNotes
Odoo 20.0VerifiedNot released. Adds the accepted_file_extensions option; see below.
Odoo 19.0VerifiedVerified against the shipped source.
Odoo 18.0VerifiedByte-identical behavior to 19. No XML changes needed.
Odoo 17.0VerifiedSame attributes and flow. No XML changes needed.
Odoo 16.0VerifiedSame widget in the older class-property style; a legacy twin file still shipped alongside.

Upgrade note. Views using this widget carry from 16 through 19 unchanged. When moving to 20 you can start declaring accepted_file_extensions in the options dictionary to filter the picker instead of validating server side.

What is changing in Odoo 20

Odoo 20 is expected at Odoo Experience in Brussels, 24 to 26 September 2026. We read the widget's file on the public development branch at the time of writing; the branch is unstable and this page will be re-verified after release.

The development branch gives the widget its first real option: accepted_file_extensions, read from the options dictionary and applied to the hidden input's accept attribute, so the file picker itself filters types. The rest is the Owl typed-props migration, invisible from XML.

Common problems and fixes

SymptomCause and fix
Clicking the button does nothingThe pre-upload record.save() failed, usually on required fields or a validation error. Fill the form so it saves cleanly; the file picker opens only after a successful save.
Selecting several files uploads none of themOne file exceeds the size limit and the batch check aborts everything before upload. Remove or shrink the oversized file, or raise the server's upload limit.
The action method never runsThe action attribute is missing, misspelled, or names a method that does not exist on the model. Match the attribute to an existing method that accepts an attachment_ids keyword.
The action failed but attachments were still createdAttachments are created before the action runs; the two are not one transaction. Handle cleanup inside the method if it must be all-or-nothing.
Need to restrict uploads to PDFs or imagesOdoo 19 hardcodes accept="*" on the file input. Validate in the action method in 19; from Odoo 20 use the accepted_file_extensions option.

Attach document widget vs the alternatives

WidgetBest forKey difference
attach_documentA header button that uploads files to the record and optionally processes themSaves the record first, then uploads to ir.attachment and calls your model method
many2many_binaryA field holding a set of attachments, visible as a listField widget on a many2many to ir.attachment; files are the field's value
binaryOne file stored in one binary fieldSingle file bound to a field, with download and replace controls
account_file_uploaderCreating invoices or bills from uploaded documentsAccounting-specific view widget that creates new records from the files

Choose by where the files should live and what should happen next: this widget for record attachments plus an optional processing method, the binary field widgets when the file is the value of a specific field.

Frequently asked questions

Why must the record be saved before uploading?+
The upload route links files to a model and record id, so the record has to exist in the database. The widget enforces this itself: beforeOpen runs record.save() and only opens the picker when the save succeeds.
What exactly does the action attribute receive?+
The method is called on the current record with one keyword argument, attachment_ids, the list of newly created ir.attachment ids from this click. After it returns, the widget reloads the record so computed fields refresh.
Can users select multiple files at once?+
Yes, the hidden input sets multiple. All files are size-checked first and uploaded together; a single oversized file cancels the whole selection.
Can I limit the file types in the picker?+
Not in Odoo 19; the input is hardcoded to accept="*", so filtering must happen in your action method. The development branch adds an accepted_file_extensions option for exactly this.
Where do the uploaded files end up?+
As standard ir.attachment records with res_model and res_id pointing at the current record, so they appear in the chatter's attachment list like any manually added file.
How does Odoo use this widget for expense OCR?+
The expense form points action at a method that queues the new receipt attachments for digitization, and the post-action reload is what makes the scanned totals appear on the form moments later.

Want documents to drive your Odoo records, not just sit on them?

Receipt OCR, proof-of-delivery capture, contract intake: we wire upload buttons like attach_document to Python methods that extract data and push your workflow forward automatically. That is standard scope in our Odoo customization work.

Discuss your document workflow

How this page was produced

Attributes, the save-first rule and the upload flow were read from attach_document.js and its template in the Odoo 19.0 web module, compared against 16.0 through 18.0 and the development branch, and the two core usages were verified in hr_expense view XML. Spotted an error? Tell us and we will correct the page.