Direct Thermal Printing & Compliance Archiving for Odoo POS
No IoT Box. No Middleware. Just Print.
Siso SmartPOS is a direct-to-printer receipt module for Odoo Point of Sale. It replaces Odoo's default IoT Box–dependent printing with a browser-native approach: raw ESC/POS data is generated in the browser and sent directly to a thermal printer over USB, serial, or TCP — no additional hardware, no proxy service (except for network printers), no cloud dependency.
The module has three major components:
Part 1 of this document covers the day-to-day features available to cashiers and operators. Part 2 covers installation, configuration, and the administrative tools. Part 3 is a full reference for the receipt template language.
WebUSB and WebSerial are only available in Chrome and Edge. Firefox does not support these APIs. This restriction applies to the workstation physically connected to the printer; the Odoo server itself has no such constraint.
Day-to-day features for cashiers and POS operators.
When SmartPOS is installed and configured, receipt printing works as it always has in Odoo POS — the operator completes a sale, and the receipt is printed automatically if the POS is configured to auto-print, or manually via the Print Receipt button on the receipt screen.
Behind the scenes, SmartPOS intercepts the print job before Odoo's default handler, generates ESC/POS data from the active receipt template, and sends it directly to the configured printer. The customer sees no difference; the operator sees a faster, more reliable print path.
Opening any past order from the Ticket Screen and clicking Print Receipt sends a reprint. SmartPOS tracks reprints as "copies" for compliance purposes — the copy counter increments and the reprint is recorded in the session's X/Z-report data.
When a return (refund) order is finalised, it is automatically archived as a return receipt — a distinct document type from a standard sales receipt. The return count and amount are reported separately on X and Z reports.
The first time a print job is sent in a new browser session, Chrome or Edge will display a device-picker dialog. Select your printer and click Connect. The browser remembers the choice for all subsequent prints within the same browser session.
A draft receipt — also called a pro-forma — is a non-finalised printout of the current order before payment. It is useful for showing a customer the total before they pay, printing a kitchen order, or obtaining a signature on a quote.
Each draft print increments the pro-forma counter in the session statistics. The printed document is archived as a proforma type in the Document Archive and counts toward the session's pro-forma total in X/Z reports.
A pro-forma is not a receipt. It does not finalize the order, does not affect inventory, and does not generate a sequential receipt number. If you need to provide a legally valid receipt, the order must be completed through the payment flow.
The X-Report is a non-closing snapshot of the current session's turnover. It shows accumulated totals, VAT breakdown, payment method summary, and correction counts — identical in structure to a Z-Report, but printing it does not close the session or reset any counters.
X-Reports use the X-Report Body template section, sharing the same Header and Footer as sales receipts. The body template is separately configurable so reports can be formatted differently from customer receipts.
The Z-Report is triggered automatically when you close the register via the Close button and complete the Closing Register routine. It is identical to an X-Report in content, but it generates a sequential Z-number and is the official end-of-day record. See Appendix B for compliance details.
SmartPOS integrates with Odoo's built-in receipt email functionality. On the receipt screen, enter the customer's email address in the send-by-email field and click the send button.
When a customer is attached to the order, their email address is pre-filled automatically. SmartPOS generates the receipt as an HTML document (using the same template as the printed receipt) and sends it via Odoo's standard mail server.
Email delivery requires an outgoing mail server to be configured in Settings → Technical → Email → Outgoing Mail Servers. If no mail server is configured, the email action will silently fail.
The receipt screen includes a customer search field — labelled Kundesøk by default — that lets the operator find and attach a customer to the current order without leaving the receipt screen.
The fields searched and the columns shown in the multi-match popup are configurable in POS Settings → Siso SmartPOS:
| Setting | Default | Description |
|---|---|---|
| Omni Search Fields | name,phone,mobile,email | Comma-separated list of res.partner fields to search against. |
| Omni Return Fields | name,phone,email,barcode | Fields displayed in the multi-match popup and stored on the result. |
Installation, configuration, and management for system administrators.
siso_smartpos directory and place it in your Odoo addons path (e.g., /opt/odoo/addons).
The Siso SmartPOS Printer section is only visible when Developer Mode is active. Enable it by appending ?debug=1 to the URL, or via Settings → Activate Developer Mode.
siso_smartpos directory before proceeding.
siso_smartpos, click the three-dot menu, and select Upgrade.
DELETE FROM ir_attachment WHERE url LIKE '/web/assets/%';Odoo aggressively caches JavaScript bundles. If you see old behaviour after an upgrade, clearing the ir_attachment rows as described above is almost always the fix. A hard reload (Ctrl+Shift+R) in the browser is also required after the cache is cleared.
SmartPOS supports three connection types. All are configured in Point of Sale → Configuration → Settings under the Siso SmartPOS Printer section.
Uses the browser's WebUSB API to send ESC/POS data directly over a USB connection. This is the fastest and most reliable connection type for a dedicated POS terminal.
| Field | Description | Example |
|---|---|---|
| USB Vendor ID | Hexadecimal vendor ID of the printer. | 0x04b8 (Epson) |
| USB Product ID | Hexadecimal product ID of the printer. | 0x0e15 |
How to find your IDs:
lsusb. Each line shows ID vendor:product.VID_XXXX&PID_XXXX.Linux binds USB printers to the usblp kernel module, which blocks WebUSB. You must unbind the driver and create a udev rule. See Chapter 9 for the full procedure.
Uses the browser's WebSerial API. Suitable for older printers connected via RS-232 or USB-to-serial adapters.
| Field | Description | Example |
|---|---|---|
| Serial Port | System device name. | /dev/ttyUSB0 · COM3 |
| Baud Rate | Must match the printer's hardware setting. | 9600 · 115200 |
The baud rate is set in the printer's hardware configuration (DIP switches or service menu). Check your printer's manual; 9600 is the most common default.
Sends ESC/POS data over TCP to a network-connected printer. Requires the Siso Local Bridge — a small service that runs on the POS workstation and relays print jobs from the browser to the printer.
| Field | Description | Example |
|---|---|---|
| Printer IP Address | Static IP of the network printer. | 192.168.1.200 |
| Printer Port | TCP port the printer listens on. | 9100 |
Browsers block direct TCP connections to LAN devices for security reasons. The Siso Local Bridge listens on localhost:19100 and forwards raw ESC/POS data to the printer's IP and port. It must be running on the workstation before printing.
Set Printer Width (chars) to the number of characters that fit on one line at normal font size. This drives all column layout calculations.
| Paper Width | Typical Characters |
|---|---|
| 57 mm | 32 |
| 80 mm | 42 – 48 |
Run a self-test print from the printer itself — it typically prints a full-width line of characters you can count directly.
WebUSB requires the operating system to grant the browser access to the USB device. Each OS handles this differently.
Linux automatically binds USB printers to the usblp kernel module, which blocks WebUSB. You must unbind the driver and prevent it from re-attaching.
lsusb
# Example: Bus 005 Device 003: ID 1504:003d Bixolon SRP-350plusIII
1504 and 003d with your IDs):
sudo tee /etc/udev/rules.d/99-usb-printer.rules << 'EOF'
SUBSYSTEM=="usb", ATTRS{idVendor}=="1504", ATTRS{idProduct}=="003d", MODE="0666"
EOF
sudo tee /etc/modprobe.d/blacklist-usblp.conf << 'EOF'
blacklist usblp
EOF
# Find interface name — look for DRIVER=usblp
grep -r usblp /sys/bus/usb/drivers/usblp/*/uevent 2>/dev/null
# Unbind it (replace 5-1:1.0 with your interface name)
echo -n "5-1:1.0" | sudo tee /sys/bus/usb/drivers/usblp/unbind
sudo udevadm control --reload-rules
sudo udevadm trigger --attr-match=idVendor=1504
macOS does not need driver replacement. Chrome and Edge can access USB printers through WebUSB directly. If the printer does not appear in the device picker, try unplugging and replugging. On macOS Ventura and later, allow USB accessories access in System Settings → Privacy & Security → USB Accessories.
Windows assigns the usbprint.sys driver to USB printers, which blocks WebUSB. Replace it with the generic WinUSB driver using the free tool Zadig.
After replacing the driver, the printer will no longer appear in the Windows print dialog. This is expected — SmartPOS sends raw ESC/POS data directly, bypassing the Windows print spooler entirely. To revert, uninstall the WinUSB device in Device Manager and let Windows reinstall the original driver.
The Receipt Designer is accessed from Point of Sale → Configuration → Receipt Templates. Each template consists of five separate sections printed in order:
| Section | Printed for | Typical content |
|---|---|---|
| Header | All document types | Company logo, name, address, VAT number |
| Body | Sales, return, training, and pro-forma receipts | Order lines, totals, VAT table, payments |
| X-Report Body | X-Reports | Session totals, VAT breakdown, payment summary, counters |
| Z-Report Body | Z-Reports | Same as X-Report Body, plus Z-number and lifetime totals |
| Footer | All document types | QR code, website, thank-you message, paper cut |
Go to POS Settings for the target register and set the Receipt Template field. If no template is assigned, SmartPOS falls back to built-in default templates that satisfy basic compliance requirements.
Two compliance flags are available on each template:
SmartPOS ships with a special field dump template that prints every available context variable and its current value. Assign it temporarily to a register and print a test receipt to see exactly what data is available for your layout — useful when building a new template from scratch.
SmartPOS automatically archives every document it prints. Six document types are supported:
| Type | When created |
|---|---|
| Sales Receipt | A completed sale is pushed to the server. |
| Return Receipt | A completed order with a negative total is pushed. |
| Training Receipt | A sale completed while the POS is in training mode. |
| Pro-forma / Draft | The Print Draft button is used before payment. |
| X-Report | The X-Report button is clicked in the navbar. |
| Z-Report | The register is closed via the Closing Register routine. |
Each archive record stores three payloads:
Navigate to Point of Sale → Reporting. Each document type has its own menu entry. The SmartPOS: Full Archive view (under Configuration) shows all types in a single filterable list.
Enable Prevent Archive Deletion on the receipt template to protect records from being deleted through the Odoo interface. This is strongly recommended for regulated businesses.
Complete reference for the SmartPOS XML template language.
Receipt templates are written in a simple XML-based tag language. Each tag maps to one or more ESC/POS commands when printing, and to equivalent HTML for the preview and archive. Tags can be nested freely unless otherwise stated.
Dynamic values are inserted with curly-brace placeholders:
{variable} ← plain value
{variable:.2f} ← numeric format (2 decimal places)
{variable:dd.MM.yyyy} ← date format (Luxon tokens)
Variable names are case-sensitive dotted paths (e.g., head.company.name, line.price_total_incl). Unknown paths render as an empty string — they never cause an error.
<!-- HEADER -->
<center><logo max-width="80"/></center>
<feed lines="1"/>
<center><x2>{head.company.name}</x2></center>
<center>{head.company.address.line1}</center>
<center>{head.company.address.postal_code} {head.company.address.city}</center>
<center>Org.nr: {head.company.vat_id}</center>
<hr/>
<center>{doc.type_label}</center>
<hr/>
<!-- BODY -->
<row><left>{order.name}</left><right>{head.date:dd.MM.yyyy HH:mm}</right></row>
<hr/>
<lines>
<table><tr>
<td wrap="1">{line.name}</td>
<td w="5" align="right">{line.qty}</td>
<td w="8" align="right">{line.price_total_incl:.2f}</td>
</tr></table>
</lines>
<hr/>
<row bold="1"><left>TOTAL</left><right>{order.amount_total:.2f}</right></row>
<!-- FOOTER -->
<feed lines="1"/>
<center><qr size="4"/></center>
<feed lines="3"/><cut/>
SmartPOS ships with default templates for all five sections. Assign the built-in field dump template to your register and print a test receipt to see every available variable and its current value before designing your own layout.
The following variables are available in all template sections unless noted otherwise.
doc.*)| Variable | Description |
|---|---|
{doc.type} | Renderer type key: final · training · proforma · x_report · z_report |
{doc.type_label} | Localised label printed on the document (e.g., SALGSKVITTERING) |
{doc.number} | Order or document number |
{doc.is_copy} | true when printing a reprint |
{doc.training_mode} | true when the POS is in training mode |
{doc.currency} | ISO 4217 currency code (e.g., NOK) |
{doc.currency_symbol} | Currency symbol (e.g., kr) |
head.company.*)| Variable | Description |
|---|---|
{head.company.name} | Full legal company name |
{head.company.address.line1} | Street address |
{head.company.address.postal_code} | Postal / ZIP code |
{head.company.address.city} | City |
{head.company.address.country} | ISO 3166-1 country code |
{head.company.phone} | Phone number |
{head.company.vat_id} | VAT number incl. prefix (NO123456789MVA) |
{head.company.website} | Website URL |
head.pos.*)| Variable | Description |
|---|---|
{head.pos.id} | POS unit identifier (required in Norway by kassasystemforskriften § 2-8-2) |
{head.pos.name} | Human-readable POS name |
{head.pos.software_name} | Software name string |
| Variable | Description |
|---|---|
{head.date} | Transaction date (Luxon DateTime — use with a format specifier) |
{head.time} | Transaction time |
{head.datetime.end} | Full timestamp when the transaction was finalised |
order.*)| Variable | Description |
|---|---|
{order.name} | Order / receipt reference number |
{order.date_order} | Order date/time (Luxon DateTime) |
{order.amount_untaxed} | Order total excl. tax |
{order.amount_total} | Order total incl. tax |
customer.*)Only populated when a customer is attached to the order.
| Variable | Description |
|---|---|
{customer.name} | Customer or company name |
{customer.id} | Internal CRM identifier |
{customer.vat_id} | Customer VAT number |
<lines> loops)| Variable | Description |
|---|---|
{line.name} | Product name |
{line.qty} | Quantity sold |
{line.unit.name} | Unit of measure name (e.g., kg, stk) |
{line.price_unit_incl} | Unit price incl. tax |
{line.price_unit_excl} | Unit price excl. tax |
{line.price_total_incl} | Line total incl. tax |
{line.price_total_excl} | Line total excl. tax |
{line.discount} | Discount percentage (0–100) |
{line.discount_amount_incl} | Discount amount incl. tax |
{line.has_discount} | true if any discount is applied |
{line.tax_name} | Tax name string (e.g., MVA 25%) |
{line.tax_rate} | Tax rate as a number |
{line.amount_tax} | VAT amount for this line |
<taxes> loops)| Variable | Description |
|---|---|
{tax.name} | Tax name |
{tax.rate} | Tax rate (%) |
{tax.base} | Taxable base (excl. tax) |
{tax.amount} | Tax amount |
{tax.total} | Gross total (base + amount) |
<payments> loops)| Variable | Description |
|---|---|
{payment.method} | Payment method name |
{payment.amount} | Amount paid by this method |
z.*) — X-Report and Z-Report Body only| Variable | Description |
|---|---|
{z.type} | x_report or z_report |
{z.number} | Sequential Z-number (never resets) |
{z.period.end} | End of the reporting period |
{z.counts.sales_receipts} | Sales receipts issued this period |
{z.counts.correction_posts} | Correction / refund posts |
{z.returns.count} | Number of return receipts |
{z.returns.amount} | Total returned amount |
{z.lifetime.gross_total} | Accumulated gross turnover since commissioning |
{z.lifetime.net_total} | Accumulated net turnover |
{z.lifetime.vat_total} | Accumulated VAT |
{z.lifetime.sales_receipts_count} | Total receipts ever printed |
Short-form aliases like {company.name}, {order.name}, and {user.name} remain supported for backward compatibility. New templates should use the full head.* / doc.* paths.
Alignment tags reset to left after the closing tag. They map to ESC a commands.
| Tag | ESC/POS | Description |
|---|---|---|
<center>…</center> | ESC a 1 | Centre-align |
<right>…</right> | ESC a 2 | Right-align |
<left>…</left> | ESC a 0 | Left-align (default) |
All modifiers nest freely and reset on their closing tag.
| Tag | ESC/POS | Description |
|---|---|---|
<b>…</b> | ESC E | Bold. Aliases: <bold>, <strong> |
<u>…</u> | ESC - | Underline. Alias: <underline> |
<r>…</r> | GS B | Reverse video (white on black). Alias: <reverse> |
<red>…</red> | ESC r 1 | Red text (dual-colour printers only) |
| Tag | ESC/POS | Description |
|---|---|---|
<fonta>…</fonta> | ESC M 0 | Font A — standard size (default) |
<fontb>…</fontb> | ESC M 1 | Font B — smaller/condensed; fits more per line |
Size is set with GS !. Width and height are scaled independently; normal size is restored on the closing tag.
| Tag | Width | Height | Typical use |
|---|---|---|---|
<x1>…</x1> | 1× | 1× | Normal (same as default) |
<x2>…</x2> | 2× | 1× | Wide subheadings |
<x3>…</x3> | 3× | 1× | Large labels |
<x4>…</x4> | 4× | 1× | Banner / section headers |
<x5>…</x5> | 1× | 2× | Tall text |
<x6>…</x6> | 2× | 2× | Large prominent text |
Wide characters consume more physical space. A <x2> character is twice as wide as normal, so a 42-char printer effectively fits only 21 <x2> characters per line. Account for this when using size tags inside column layouts.
| Tag | Description |
|---|---|
<br/> | Line break — moves to the next line, no blank line added. |
<feed lines="1"/> | One blank line. |
<feed lines="N"/> | N blank lines. |
<hr/> | Full-width rule of - characters. |
<hr char="="/> | Full-width rule using a custom character. |
<cut/> | Paper cut (partial cut, GS V 41 3). |
<br/> vs <feed><br/> is a line break — it ends the current line without adding a blank line. Use it to move to the next print line within flowing text. <feed lines="1"/> adds a visible blank line (an empty line of paper). Use feed to create vertical space between sections.
<row>)Left-aligns the left side, right-aligns the right side, and fills the gap with spaces to exactly fill one line.
<row><left>Subtotal</left><right>{order.amount_untaxed:.2f}</right></row>
<row bold="1"><left>TOTAL</left><right>{order.amount_total:.2f}</right></row>
The optional bold="1" attribute makes the entire row bold.
<table><tr><td>)The preferred approach for new templates. Supports word-wrap and flexible column widths.
| Element | Attribute | Description |
|---|---|---|
<table> | width="N" | Override printer width for this table (useful inside <fontb>). |
<tr> | bold="1" | Makes the entire row bold. |
<td> | w="N" | Fixed column width in characters. |
align="left|center|right" | Cell alignment (default: left). | |
wrap="1" | Enable word-wrap for long content; continuation lines are left-aligned. |
Columns without a w attribute share the remaining width equally (flex behaviour).
<!-- 4-column line items table -->
<table>
<tr bold="1">
<td>Varenavn</td>
<td w="5" align="right">Ant.</td>
<td w="7" align="right">Pris</td>
<td w="7" align="right">Sum</td>
</tr>
</table>
<lines>
<table>
<tr>
<td wrap="1">{line.name}</td>
<td w="5" align="right">{line.qty}</td>
<td w="7" align="right">{line.price_unit_incl:.2f}</td>
<td w="7" align="right">{line.price_total_incl:.2f}</td>
</tr>
</table>
</lines>
Formatting tags (<b>, <x2>, etc.) inside <td> are stripped — only their text content is used. Apply row-level bold via bold="1" on <tr>, or wrap the entire <table> in a formatting tag.
<dot><col>)An older but fully supported column layout. Each <col> is allocated a fixed number of character positions; content is padded or truncated to fit.
| Attribute | Example | Description |
|---|---|---|
w="N" | w="12" | Fixed width of exactly N characters. |
w="N%" | w="50%" | Width as a percentage of the line width. |
| (none) | Flex: shares remaining width with other unsized columns. | |
align | align="right" | Default alignment when no child tag is present. |
Both <table><tr><td> and <dot><col> can be mixed in the same template.
Loop tags iterate over a list from the rendering context. The content between the opening and closing tag is repeated once per item, with the item's fields available as short-form variables.
<lines> — Order LinesIterates once per order line. All {line.*} variables are available inside.
<lines>
<row><left>{line.name}</left><right>{line.price_total_incl:.2f}</right></row>
</lines>
<taxes> — Tax BreakdownIterates once per tax rate applied to the order. Satisfies the Norwegian requirement for per-rate VAT disclosure.
<taxes>
<row><left>MVA {tax.rate:.0f}%</left><right>{tax.amount:.2f}</right></row>
</taxes>
<payments> — Payment LinesIterates once per payment line on the order (e.g., cash, card, Vipps).
<payments>
<row><left>{payment.method}</left><right>{payment.amount:.2f}</right></row>
</payments>
<categories> and <products> — Report LoopsAvailable in X/Z-Report Body templates. Iterate over POS category sales and product sales for the session, respectively. Use {category.name}, {category.amount}, {product.name}, {product.qty}, {product.amount} inside these loops.
<logo max-width="80"/>
Fetches the company logo, scales it to the specified percentage of the printer dot width, and rasterises it to 1-bit for GS v 0 printing.
| Attribute | Default | Description |
|---|---|---|
max-width | 100 | Maximum width as a percentage of the print area (0–100). |
<qr size="4" error="M" data="{order.name}"/>
Prints a QR code via GS ( k (Model 2).
| Attribute | Default | Description |
|---|---|---|
size | 4 | Module size in dots per cell (1–16). |
error | M | Error correction: L (7%), M (15%), Q (25%), H (30%). |
data | (order name) | Data string to encode; supports {placeholders}. |
<barcode height="50" text="1" data="{order.name}"/>
Prints a CODE128 barcode via GS k 73.
| Attribute | Default | Description |
|---|---|---|
height | 50 | Bar height in dots. |
text | 1 | 1 = print human-readable text below the bars; 0 = no text. |
data | (order name) | Data string to encode; supports {placeholders}. |
Inline formatting for numbers and dates in template placeholders.
Append a colon and a format string after any variable to control how its value is rendered: {variable:format}. If no format is given, the raw string representation is used.
Numeric format strings follow Python's format specification:
| Syntax | Example | Output (input 1250.5) |
|---|---|---|
.2f | {order.amount_total:.2f} | 1250.50 |
.0f | {tax.rate:.0f} | 25 |
,.2f | {order.amount_total:,.2f} | 1,250.50 |
+.2f | {line.discount:+.2f} | +12.50 |
Date and datetime variables use Luxon format tokens:
| Syntax | Example | Output |
|---|---|---|
dd.MM.yyyy | {head.date:dd.MM.yyyy} | 16.05.2026 |
dd.MM.yyyy HH:mm | {order.date_order:dd.MM.yyyy HH:mm} | 16.05.2026 14:32 |
HH:mm | {head.time:HH:mm} | 14:32 |
Full Luxon token reference: moment.github.io/luxon.
Norwegian kassasystemloven and general regulatory context.
Norway's kassasystemforskriften (the Cash Register Regulation) mandates specific capabilities and data requirements for all electronic cash register systems used in business. The key sections relevant to SmartPOS are:
X and Z reports must include the following data, which SmartPOS populates automatically:
| Requirement | Template variable | Status |
|---|---|---|
| Sequential Z-number (never resets) | {z.number} | Implemented |
| POS unit identifier | {head.pos.id} | Implemented |
| Operator ID | {head.user.id} | Implemented |
| Count of sales receipts issued | {z.counts.sales_receipts} | Implemented |
| Count of correction/refund posts | {z.counts.correction_posts} | Implemented |
| Lifetime gross turnover (never resets) | {z.lifetime.gross_total} | Implemented |
| Lifetime receipt count (never resets) | {z.lifetime.sales_receipts_count} | Implemented |
| VAT breakdown by rate | <taxes> loop | Implemented |
| Payment method breakdown | <payments> loop | Implemented |
All documents generated by the cash register must be stored and retrievable for a minimum period. SmartPOS satisfies this requirement through its automatic Document Archive — every document type is archived with full content at the moment of printing.
Enable Prevent Archive Deletion on your receipt template to ensure no archived records can be deleted through the Odoo user interface. This is required for full § 11 compliance.
When the POS is in training mode, receipts are printed with a ØVELSE (training) designation and archived as training_receipt — a distinct type that does not count toward official sales totals or lifetime counters.
SmartPOS's compliance engine is designed to be jurisdiction-neutral. The field catalog (field_catalog.md in the module source) documents planned fields for Germany (TSE fiscal signatures), France (CGI Art. 88 hashing), Italy (RT device integration), and other regulated markets. Contact Shinshout.com for current availability.
All supported template tags at a glance.
| Tag | Category | Description |
|---|---|---|
<center> | Alignment | Centre-align text |
<right> | Alignment | Right-align text |
<left> | Alignment | Left-align text (default) |
<b> / <bold> / <strong> | Modifier | Bold |
<u> / <underline> | Modifier | Underline |
<r> / <reverse> | Modifier | Reverse video |
<red> | Modifier | Red text (dual-colour only) |
<fonta> | Font | Font A (standard, default) |
<fontb> | Font | Font B (condensed) |
<x1> … <x6> | Size | Character size multipliers (width × height) |
<br/> | Whitespace | Line break (no blank line) |
<feed lines="N"/> | Whitespace | N blank lines |
<hr/> | Rule | Full-width horizontal rule (-) |
<hr char="X"/> | Rule | Full-width rule with custom character |
<cut/> | Control | Paper cut |
<row> | Layout | Two-column left/right row |
<dot><col> | Layout | Multi-column dot layout |
<table><tr><td> | Layout | Multi-column table with wrap support |
<logo/> | Graphic | Company logo (rasterised) |
<qr/> | Graphic | QR code (GS ( k Model 2) |
<barcode/> | Graphic | CODE128 barcode |
<lines> | Loop | Iterate order lines → {line.*} |
<taxes> | Loop | Iterate tax rates → {tax.*} |
<payments> | Loop | Iterate payment lines → {payment.*} |
<categories> | Loop (reports) | Iterate category sales → {category.*} |
<products> | Loop (reports) | Iterate product sales → {product.*} |
<esc> | Legacy | Compound size/bold/underline (backward compat.) |