Live Demo: Try this and other plugins live — no installation, no risk: demo.markus-michalski.net
This documentation always describes the latest published version.
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).
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:
| 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 |
| 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) |
Format: {prefix}{year}-{number} with a five-digit number with leading zeros, for example RE-2026-00001.
mmd_pdfinvoice_seq per combination of shop and year.UPDATE using LAST_INSERT_ID(), so parallel requests cannot receive the same number. The method is MySQL-specific.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.
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.
| 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).
| 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.
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.