first commit
This commit is contained in:
commit
91fec1a5bf
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
*.pyc
|
||||||
|
__pycache__/
|
||||||
17
README.md
Normal file
17
README.md
Normal 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
2
__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from . import models
|
||||||
23
__manifest__.py
Normal file
23
__manifest__.py
Normal 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
2
models/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from . import pos_order
|
||||||
36
models/pos_order.py
Normal file
36
models/pos_order.py
Normal 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
211
report/pos_order_report.xml
Normal 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;"> </div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
19
views/pos_order_views.xml
Normal file
19
views/pos_order_views.xml
Normal 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>
|
||||||
Loading…
Reference in New Issue
Block a user