PDF Invoices: Technical Details
Technical details of the PDF Invoices module for OXID eShop 7.x: invoice creation flow, components, number assignment, storage, database tables and snapshot layout.
PDF Invoices: Technical Details
Live Demo: Try this and other plugins live — no installation, no risk: demo.markus-michalski.net
This documentation always describes the latest published version.
Invoice creation flow
The invoice is not triggered by an order event. It is created either when the order email is sent or manually in the admin area. Both paths call the same service (InvoiceService.createForOrder).
graph TD
A["Mail path: order email, missing invoice is created automatically"] --> E["InvoiceService.createForOrder"]
M["Admin path: Create Invoice (InvoiceController.createInvoice)"] --> E
E --> N["Assign invoice number"]
N --> S["Build snapshot (ExtendedOrder.createInvoiceSnapshot)"]
S --> P["Render PDF (Twig template and Dompdf)"]
P --> F["Store PDF and compute SHA-256"]
F --> R["Save record in mmd_pdfinvoice"]
The order inside createForOrder is: check for an existing invoice, assign the number, build the snapshot, create the PDF, store the file, compute the hash, save the record. The number is therefore fixed before the PDF exists.
The admin path ends with the stored PDF. It attaches nothing to an order email and sets no sent status. Attaching and setting the sent status belong only to the mail path in ExtendedEmail.send:
graph TD
A["Order email is sent (ExtendedEmail.send)"] --> B{"Attachment mode matches recipient?"}
B -->|"No"| X["Email without invoice"]
B -->|"Yes"| C{"Invoice exists for the order?"}
C -->|"Yes"| G["Read PDF from storage and verify hash"]
C -->|"No"| D{"Auto-generate enabled?"}
D -->|"No"| X
D -->|"Yes"| E["InvoiceService.createForOrder (see above)"]
E --> G
G --> H["Attach PDF to the email and set sent status"]
Components
| Class | Purpose |
|---|---|
ExtendedEmail |
Extends Email, remembers order emails and attaches the PDF in send() |
ExtendedOrder |
Extends Order, provides the invoice and builds the snapshot |
InvoiceService |
Orchestrates creation, loading, hash verification and regeneration |
InvoiceNumberGenerator |
Assigns invoice numbers through the table mmd_pdfinvoice_seq |
PdfGenerator |
Prepares the template variables, renders the Twig template and creates the PDF with Dompdf |
InvoiceStorage |
Writes, reads and deletes the PDF files |
InvoiceRepository |
Reads and writes the table mmd_pdfinvoice |
InvoiceConfiguration |
Reads the module settings |
DebugLogger |
Writes debug messages only when debug logging is enabled |
InvoiceController |
Admin controller mmd_pdfinvoice_admin with the actions createInvoice, downloadPdf, sendInvoiceEmail, regeneratePdf |
ModuleEvents |
Creates tables and directory on activation |
Module data
| Property | Value |
|---|---|
| Module ID | mmd_pdfinvoice |
| Composer package | mmd/oxid7-pdf-invoice |
| Install directory | source/modules/mmd/pdf-invoice |
| Admin controller | mmd_pdfinvoice_admin |
| Admin tab | tbclorder_pdfinvoice (Orders > Order) |
| Templates | @mmd_pdfinvoice/pdf/invoice, @mmd_pdfinvoice/admin/invoice_main |
| Events | onActivate (create tables and directory), onDeactivate (deletes nothing) |
Invoice numbering
Format: {prefix}{year}-{number} with a five-digit number with leading zeros, for example RE-2026-00001.
- The year is the year of creation. There is one row in
mmd_pdfinvoice_seqper combination of shop and year. - With every new year the counter automatically starts at 1 again. There is no setting for a counter that continues across years.
- The counter row is created with the first invoice of the year. The increment is atomic through a single
UPDATEusingLAST_INSERT_ID(), so parallel requests cannot receive the same number. The method is MySQL-specific. - The prefix is read from the current setting when the number is generated.
The admin area shows the setting "Reset Numbering Yearly" (
mmdPdfInvoiceResetYearly). It has no effect in the code. Numbering always runs per shop and year, whether the option is switched on or off.
PDF file storage
Path: {storage path}/{shop ID}/{year}/{month}/{invoice number}.pdf, relative to the shop directory. In the invoice number every character except letters, digits, - and _ is replaced by _. Year and month come from the invoice date.
On reading, the module compares the file's SHA-256 hash with the value stored in PDF_HASH. If it differs, the file is not delivered until the PDF has been regenerated.
Dompdf keeps its font cache in source/tmp/dompdf. The PDF is created in A4 portrait with the default font DejaVu Sans.
Database
mmd_pdfinvoice
| Column | Description |
|---|---|
OXID |
Primary key |
OXORDERID |
Order ID |
OXSHOPID |
Shop ID |
INVOICE_NUMBER |
Formatted invoice number |
INVOICE_DATE |
Invoice date |
PDF_PATH |
Relative path of the PDF file |
PDF_HASH |
SHA-256 hash of the PDF file |
SENT_TO_CUSTOMER |
Flag: sent to customer |
SENT_AT_CUSTOMER |
Time of sending to the customer |
SENT_TO_OWNER |
Flag: sent to shop owner |
SENT_AT_OWNER |
Time of sending to the shop owner |
SNAPSHOT_DATA |
JSON snapshot of the order data |
CREATED_AT |
Time of creation |
OXTIMESTAMP |
Timestamp of the last change (automatic) |
Unique keys: OXORDERID + OXSHOPID (one invoice per order and shop) and INVOICE_NUMBER + OXSHOPID (one invoice number per shop).
mmd_pdfinvoice_seq
| Column | Description |
|---|---|
OXSHOPID |
Shop ID (part of the primary key) |
YEAR |
Year (part of the primary key) |
CURRENT_NUMBER |
Last assigned number |
PREFIX |
Prefix at the time the row was created |
The PREFIX column is filled when the row is created. The number itself is always built with the current setting.
Data snapshot
When the invoice is created, the module stores the snapshot in SNAPSHOT_DATA. It is built from the order and contains:
| Key | Content |
|---|---|
version |
Format version of the snapshot, currently the value 1.0 |
created_at |
Time of creation |
order |
Order number, order date, shipping date, tracking code, currency, rate, order remark |
billing |
Billing address including phone, fax, email and VAT ID |
delivery |
Delivery address if the order has a delivery last name stored, otherwise null (no comparison with the billing address) |
items |
Items with article number, title, quantity, prices, VAT and weight |
totals |
Net, gross and total sums, shipping, payment, wrapping and gift card costs, discount, VAT breakdown |
payment |
Payment ID, name and, for direct debit, the bank details |
The shop master data and the logo are not part of the snapshot. When a PDF is regenerated they are read as they are now.
Behavior on errors
- Errors while attaching to an order email are logged, the order email is still sent.
- If PDF creation fails during manual creation, the admin area shows an error message and the cause is in the OXID log.
- Tables and storage remain when the module is deactivated.