Base Import From Email
On this page
Three pieces that together let an inbound email drive base_import_pdf_by_template through the EDI framework's exchange-record pipeline instead of only a manually-uploaded
PDF — for as many different senders and target document types as you configure, not just
one:
Two
extraction_modeoptions onbase.import.pdf.template—plaintextandhtml— alongside thepypdfmode that ships withbase_import_pdf_by_template. A template using either mode is matched and processed exactly like a PDF-based one; only the first step (turning the source file into text) is different.A
mail.aliasentry point ontoedi.exchange.record(fromedi_core_oca), so an inbound email creates a tracked, retryable EDI exchange record instead of a bespokemail.threadhook.A generic EDI input processor (
edi.input.process.template) that turns an exchange record into an Odoo record using whicheverbase.import.pdf.templateits exchange type points at. Add a newedi.exchange.type+base.import.pdf.templatepair for every new sender/document combination; no new code.
Why
base_import_pdf_by_template's regex-based field/table matching
(_get_field_header_values, _get_table_info) already works on a plain text string — pypdf is only there to get from a PDF binary to that string. Some sources never
produce a PDF at all. Parts-supplier order confirmation emails, for example, only exist
as an email body (plain text and HTML alternative parts) — no PDF attachment. This
module lets a template be built directly against that text, without inventing a separate
import pipeline per document model.
The module is deliberately generic: it only adds extraction modes to the existing
framework. It doesn't know about email, purchase orders, or any other specific model —
any module that materializes a plain text or HTML body as an ir.attachment (mimetype text/plain or text/html) can hand it to wizard.base.import.pdf.upload and get the
same auto-detect + template-line processing that PDF attachments get today.
What it adds
extraction_mode = "plaintext": reads the source bytes back as UTF-8 text, no transformation.extraction_mode = "html": same, then runs it through Odoo'shtml2plaintextto strip markup before the template's patterns see it.
Both live in wizards/wizard_base_import_pdf_mixin.py, following the same _pdf_text_extraction_<mode> naming the base module uses for pypdf, so simple_pdf_text_extraction()'s dynamic dispatch and _parse_pdf_grouped()'s
try-every-mode auto-detection keep working unmodified.
This module also depends on base_import_pdf_by_template_engine (see that module's
README), even though nothing here calls the seams it adds - a plaintext/html template's lines are always plain regex against flat text, matched exactly like a pypdf template's. The dependency exists so the engine's two _get_table_info_data() bug fixes (an IndexError when no "lines" child line has a pattern yet, and a "lines"
column with fewer matches than its siblings silently shifting every later column of that
row instead of leaving a blank cell) always apply here too. That second bug is not
theoretical for this module: a parts-supplier order confirmation has a repeating item
table (see tests/data/parts_order_confirmation.txt), and any "lines" template built
against it - or a future sender with similar structure - would hit exactly that path the
moment one item's column doesn't match as many times as another's.
Routing email into an exchange record
edi.exchange.record already inherits mail.thread (from edi_core_oca), so a mail.alias can point directly at it — models/edi_exchange_record.py overrides message_new() to make that alias useful: it stores the raw message body as the
exchange's exchange_file and hands off to edi.backend.create_record(), which
resolves the right edi.exchange.type and (per that type's quick_exec setting) can
trigger processing immediately.
The alias only needs two defaults, resolved via its alias_defaults:
backend_id— which EDI backend the message belongs to.type_code— whichedi.exchange.type.codeto file it under.
Both are required; a message routed here without them raises rather than creating a
record type_id can't actually be set on. The body is kept as raw HTML (no conversion
at intake) — interpreting it is the extraction template's job, using the html mode
added above.
The record is created with edi_exchange_state = "input_received" straight away: the
whole document already arrived in the email, so there is no separate "receive"
round-trip to wait for. Skipping this would leave a quick_exec type stuck at "new" forever — the receive/process cron domains only look for "input_pending" and "input_received" respectively (see edi_backend._input_pending_records_domain/_input_pending_process_records_domain),
and plain create() on edi.exchange.record does not infer this transition on its own.
Processing an exchange record generically
models/edi_input_process_template.py adds edi.input.process.template, an edi.oca.handler.process implementation that:
Reads
exchange_record.type_id.import_template_id(a new field this module adds toedi.exchange.type).Extracts text via that template's own
extraction_mode(reusingsimple_pdf_text_extraction()— the same dispatch the PDF wizard uses).Runs the template's field/table matching and creates the record with the same
Form()-based enginewizard.base.import.pdf.uploaduses for a manual upload.Links the result back to the exchange record via
_set_related_record().
To use it for a new sender or document type: build a base.import.pdf.template for that
model as usual, create an edi.exchange.type with Processor set to "EDI Input
Process: Import by Template" and Import Template set to that template, and point a mail.alias (or any other trigger) at it with the matching backend_id/type_code.
Nothing here is written against one particular supplier, purchase orders, or any other
specific document — that's the point: the same processor model serves every exchange
type configured this way.
A document whose creation needs more than the template engine gives you out of the box —
fuzzy partner/product matching, safe updates to an existing record instead of always
creating new, dedup — should get its own dedicated processor (e.g. one built on purchase_order_import) registered as that exchange type's Processor instead of this
generic one. Both kinds of processor can coexist across different exchange types on the
same backend.
alias_defaults isn't a per-field form
backend_id and type_code above are not fields you'll find on edi.exchange.record (backend_id happens to also be a real column, but that's incidental) - they're just
keys in the mail.alias's alias_defaults text field, which Odoo core only
validates with ast.literal_eval (no check against real model fields). Set it directly
as a Python dict literal, e.g. {'backend_id': 3, 'type_code': 'parts_supplier_po_confirmation'} - our message_new() override is the only thing that ever reads type_code.
quick_exec is hidden for input types in the stock view
edi_core_oca's form only shows quick_exec when exchange_file_auto_generate is set,
and that field is itself only shown for direction = "output" - so on an input exchange
type quick_exec is permanently hidden in the stock view, even though _quick_exec_enabled() never checks exchange_file_auto_generate for inputs at all. views/edi_exchange_type_views.xml overrides that field's invisible condition so it
shows for inputs too, without touching edi_core_oca itself.
Reference fixture
tests/data/parts_order_confirmation.txt is the plain-text body of a parts-supplier
order confirmation with the sender, customer, store, and order details replaced by
fictional values (Order # ORD-00000TESTA), and parts_order_confirmation.html is a
trimmed-down HTML document reproducing the same structure. Both are used only to prove
the extraction methods round-trip real-world content correctly; this module does not
itself define a template or know what any one supplier's order looks like — that belongs
in whatever module actually imports that supplier's confirmations.
Tests
python odoo/odoo-bin -c odoo.conf -d <testdb> -i import_from_email \
--test-enable --test-tags /import_from_email --stop-after-init