first commit

This commit is contained in:
Suherdy Yacob 2026-06-12 11:11:20 +07:00
commit 91fec1a5bf
8 changed files with 312 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*.pyc
__pycache__/

17
README.md Normal file
View File

@ -0,0 +1,17 @@
POS Reprint Receipt from Backend
This module allows users to reprint the bill/receipt for completed POS Orders directly from the backend Odoo form view.
Features
- Adds a Reprint Receipt button on the backend POS Order form view.
- The button is visible for orders that are already paid or posted (states Paid or Posted).
- Displays a QWeb HTML report styled as an 80mm thermal receipt, matching the layout of the checkout receipt.
- Shows company logo, config header/footer, cashier, order name, table/guest details, order line details, totals, payments, change, and taxes.
Usage
1. Navigate to Point of Sale -> Orders -> Orders.
2. Open any completed order (state Paid or Posted).
3. Click the Reprint Receipt button in the status header.
4. The print preview dialog of your browser will open with the receipt formatted for 80mm thermal printers.

2
__init__.py Normal file
View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from . import models

23
__manifest__.py Normal file
View File

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
{
'name': 'POS Reprint Receipt from Backend',
'version': '1.0.0',
'category': 'Point of Sale',
'summary': 'Add button to reprint POS order receipt/bill from the backend form view.',
'description': """
This module adds a "Reprint Receipt" button on the backend POS Order form view.
When clicked, it opens a QWeb HTML report styled as an 80mm thermal receipt, mimicking the layout of the checkout receipt.
""",
'author': 'Suherdy Yacob',
'depends': [
'point_of_sale',
],
'data': [
'report/pos_order_report.xml',
'views/pos_order_views.xml',
],
'installable': True,
'application': False,
'auto_install': False,
'license': 'LGPL-3',
}

2
models/__init__.py Normal file
View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from . import pos_order

36
models/pos_order.py Normal file
View File

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models
class PosOrder(models.Model):
_inherit = 'pos.order'
def get_receipt_tax_details(self):
self.ensure_one()
tax_map = {}
for line in self.lines:
price_unit = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
line_taxes = line.tax_ids_after_fiscal_position.compute_all(
price_unit,
self.currency_id,
line.qty,
product=line.product_id,
partner=self.partner_id
)
for tax in line_taxes.get('taxes', []):
tax_id = tax['id']
if tax_id not in tax_map:
tax_map[tax_id] = {
'name': tax['name'],
'amount': 0.0,
'base': 0.0,
}
tax_map[tax_id]['amount'] += tax['amount']
tax_map[tax_id]['base'] += tax['base']
return list(tax_map.values())
def get_order_name(self):
self.ensure_one()
name = self.floating_order_name or ""
if self.is_refund:
name += " (Refund)"
return name

211
report/pos_order_report.xml Normal file
View File

@ -0,0 +1,211 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Paper format: 80mm thermal receipt -->
<record id="paperformat_pos_reprint_receipt" model="report.paperformat">
<field name="name">POS Reprint Receipt (80mm)</field>
<field name="default" eval="False"/>
<field name="format">custom</field>
<field name="page_width">80</field>
<field name="page_height">297</field>
<field name="orientation">Portrait</field>
<field name="margin_top">3</field>
<field name="margin_bottom">3</field>
<field name="margin_left">3</field>
<field name="margin_right">3</field>
<field name="header_line" eval="False"/>
<field name="header_spacing">3</field>
<field name="dpi">96</field>
</record>
<!-- Report Action -->
<record id="action_report_pos_order_reprint" model="ir.actions.report">
<field name="name">Reprint Receipt</field>
<field name="model">pos.order</field>
<field name="report_type">qweb-html</field>
<field name="report_name">pos_reprint_receipt.report_pos_order_reprint</field>
<field name="report_file">pos_reprint_receipt.report_pos_order_reprint</field>
<field name="paperformat_id" ref="paperformat_pos_reprint_receipt"/>
<field name="binding_model_id" ref="point_of_sale.model_pos_order"/>
<field name="binding_type">report</field>
</record>
<!-- QWeb Template -->
<template id="report_pos_order_reprint">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="order">
<div style="font-family: 'Courier New', Courier, monospace;
font-size: 25px;
width: 100%;
max-width: 320px;
margin: 0 auto;
padding: 0px 4px;
page-break-after: always;
color: #000;">
<!-- ===== LOGO ===== -->
<t t-if="order.company_id.logo">
<div style="text-align: center; margin-bottom: 10px;">
<img t-att-src="image_data_uri(order.company_id.logo)" style="max-height: 80px; max-width: 100%; display: block; margin: 0 auto;"/>
</div>
</t>
<!-- ===== HEADER ===== -->
<div style="text-align: center; margin-bottom: 10px;">
<div t-if="order.config_id.receipt_header" style="white-space: pre-line; font-size: 21px; margin-bottom: 8px;">
<t t-esc="order.config_id.receipt_header"/>
</div>
<div style="font-size: 21px; font-weight: bold; text-align: center;">
<t t-if="order.company_id.country_id.code in ['AT', 'BE', 'BG', 'CY', 'CZ', 'DE', 'DK', 'EE', 'ES', 'FI', 'FR', 'GR', 'HR', 'HU', 'IE', 'IT', 'LT', 'LU', 'LV', 'MT', 'NL', 'PL', 'PT', 'RO', 'SE', 'SI', 'SK']">VAT </t>NO <t t-esc="order.pos_reference"/>
</div>
<div t-if="order.date_order" style="font-size: 21px;">
<t t-esc="order.date_order" t-options="{'widget': 'datetime'}"/>
</div>
<div style="margin-top: 4px; font-size: 19px; color: #666; font-weight: bold;">
(REPRINT)
</div>
</div>
<div style="border-top: 1px dashed #000; margin: 8px 0;"/>
<!-- ===== ORDER & CASHIER DETAILS ===== -->
<table style="width: 100%; border-collapse: collapse; margin-bottom: 8px; font-size: 21px;">
<tr t-if="order.cashier">
<td style="font-weight: bold; padding: 3px 0; width: 40%;">Served by</td>
<td style="text-align: right; padding: 3px 0; width: 60%;">
<t t-esc="order.cashier"/>
</td>
</tr>
<t t-set="order_name" t-value="order.get_order_name()"/>
<tr t-if="order_name">
<td style="font-weight: bold; padding: 3px 0;">Order</td>
<td style="text-align: right; padding: 3px 0;">
<t t-esc="order_name"/>
</td>
</tr>
<t t-if="'table_id' in order._fields and order.table_id">
<tr>
<td style="font-weight: bold; padding: 3px 0;">Table</td>
<td style="text-align: right; padding: 3px 0;">
<t t-esc="order.table_id.display_name"/>
<t t-if="order.customer_count"> (Guests: <t t-esc="order.customer_count"/>)</t>
</td>
</tr>
</t>
<tr t-if="order.partner_id">
<td style="font-weight: bold; padding: 3px 0; vertical-align: top;">Customer</td>
<td style="text-align: right; padding: 3px 0;">
<div><t t-esc="order.partner_id.name"/></div>
<div t-if="order.partner_id.vat" style="font-size: 19px;"><t t-esc="order.partner_id.vat"/></div>
</td>
</tr>
</table>
<div style="border-top: 1px dashed #000; margin: 8px 0;"/>
<!-- ===== ORDER LINES ===== -->
<div style="font-size: 25px; font-weight: bold; line-height: 1.3;">
<t t-foreach="order.lines" t-as="line">
<div style="margin-bottom: 8px;">
<div style="display: flex; justify-content: space-between; align-items: flex-start; width: 100%;">
<div style="padding-right: 10px; text-wrap: wrap; flex-grow: 1;">
<t t-esc="line.qty"/> x <t t-esc="line.product_id.name"/>
</div>
<div style="text-align: right; font-family: monospace; white-space: nowrap;">
<t t-esc="line.price_subtotal_incl" t-options="{'widget': 'monetary', 'display_currency': order.currency_id}"/>
</div>
</div>
<!-- Price unit details if qty != 1 or discount > 0 -->
<div t-if="line.qty != 1 or line.discount" style="font-size: 21px; font-weight: normal; margin-left: 20px; color: #555;">
<t t-esc="line.qty"/> x <t t-esc="line.price_unit" t-options="{'widget': 'monetary', 'display_currency': order.currency_id}"/>
<t t-if="line.discount">
(Discount: <t t-esc="line.discount"/>%)
</t>
</div>
<!-- Customer note -->
<div t-if="line.customer_note" style="font-size: 21px; font-weight: normal; margin-left: 20px; font-style: italic; color: #555; word-break: break-all;">
* <t t-esc="line.customer_note"/>
</div>
</div>
</t>
</div>
<div style="border-top: 1px dashed #000; margin: 8px 0;"/>
<!-- ===== TOTALS ===== -->
<table style="width: 100%; border-collapse: collapse; margin-bottom: 8px; font-size: 25px; font-weight: bold;">
<tr>
<td style="padding: 4px 0; width: 40%;">TOTAL</td>
<td style="text-align: right; padding: 4px 0; font-family: monospace; width: 60%;">
<t t-esc="order.amount_total" t-options="{'widget': 'monetary', 'display_currency': order.currency_id}"/>
</td>
</tr>
</table>
<div style="border-top: 1px solid #000; margin: 8px 0;"/>
<!-- ===== PAYMENTS & CHANGE ===== -->
<table style="width: 100%; border-collapse: collapse; margin-bottom: 8px; font-size: 21px;">
<t t-foreach="order.payment_ids" t-as="payment">
<tr>
<td style="padding: 4px 0; width: 50%;"><t t-esc="payment.payment_method_id.name"/></td>
<td style="text-align: right; padding: 4px 0; font-family: monospace; width: 50%;">
<t t-esc="payment.amount" t-options="{'widget': 'monetary', 'display_currency': order.currency_id}"/>
</td>
</tr>
</t>
<tr t-if="order.amount_return">
<td style="font-weight: bold; padding: 4px 0; width: 50%;">Change</td>
<td style="text-align: right; font-weight: bold; padding: 4px 0; font-family: monospace; width: 50%;">
<t t-esc="order.amount_return" t-options="{'widget': 'monetary', 'display_currency': order.currency_id}"/>
</td>
</tr>
</table>
<!-- ===== TAX DETAILS ===== -->
<t t-set="tax_details" t-value="order.get_receipt_tax_details()"/>
<t t-if="tax_details">
<div style="border-top: 1px dashed #000; margin: 8px 0;"/>
<table style="width: 100%; border-collapse: collapse; margin-bottom: 8px; font-size: 18px; color: #333;">
<thead>
<tr style="border-bottom: 1px solid #000;">
<th style="text-align: left; padding: 3px 0; width: 30%;">Tax Group</th>
<th style="text-align: right; padding: 3px 0; width: 35%;">Base</th>
<th style="text-align: right; padding: 3px 0; width: 35%;">Amount</th>
</tr>
</thead>
<tbody>
<t t-foreach="tax_details" t-as="tax">
<tr>
<td style="padding: 3px 0; width: 30%;"><t t-esc="tax['name']"/></td>
<td style="text-align: right; padding: 3px 0; font-family: monospace; width: 35%;">
<t t-esc="tax['base']" t-options="{'widget': 'monetary', 'display_currency': order.currency_id}"/>
</td>
<td style="text-align: right; padding: 3px 0; font-family: monospace; width: 35%;">
<t t-esc="tax['amount']" t-options="{'widget': 'monetary', 'display_currency': order.currency_id}"/>
</td>
</tr>
</t>
</tbody>
</table>
</t>
<div t-if="order.config_id.receipt_footer" style="border-top: 1px dashed #000; margin: 8px 0;"/>
<!-- ===== FOOTER ===== -->
<div t-if="order.config_id.receipt_footer" style="text-align: center; font-size: 21px; white-space: pre-line; margin-top: 8px;">
<t t-esc="order.config_id.receipt_footer"/>
</div>
<!-- Margin for paper feed -->
<div style="margin-top: 35px;">&#160;</div>
</div>
</t>
</t>
</template>
</odoo>

19
views/pos_order_views.xml Normal file
View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_pos_pos_form_reprint_receipt" model="ir.ui.view">
<field name="name">pos.order.form.reprint.receipt</field>
<field name="model">pos.order</field>
<field name="inherit_id" ref="point_of_sale.view_pos_pos_form"/>
<field name="arch" type="xml">
<xpath expr="//button[@name='action_pos_order_invoice']" position="after">
<button name="%(pos_reprint_receipt.action_report_pos_order_reprint)d"
string="Reprint Receipt"
type="action"
class="oe_highlight"
invisible="state not in ['paid', 'done']"/>
</xpath>
</field>
</record>
</odoo>