Skip to content

Commit cc70a54

Browse files
authored
A RECEIPT GENERATOR WEB APP
A web app which generates receipt. Made with the help of Chat-GPT.
1 parent 23c6d64 commit cc70a54

1 file changed

Lines changed: 244 additions & 0 deletions

File tree

app.py

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
from shiny import App, ui, render, reactive
2+
import pandas as pd
3+
import datetime
4+
from reportlab.lib.pagesizes import letter
5+
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer
6+
from reportlab.lib import colors
7+
from reportlab.lib.styles import getSampleStyleSheet
8+
import tempfile, os
9+
10+
# Reactive storage
11+
items = reactive.Value(pd.DataFrame(columns=["Product_Code", "Qty", "Category", "Price"]))
12+
edit_index = reactive.Value(None)
13+
14+
app_ui = ui.page_sidebar(
15+
ui.sidebar(
16+
ui.input_text("code", "Product_Code", ""),
17+
ui.input_select(
18+
"desc", "Category",
19+
{"Shirt": "Shirt", "Pants": "Pants", "T-Shirt": "T-Shirt", "Trouser": "Trouser"},
20+
),
21+
ui.input_numeric("qty", "Quantity", 1, min=1),
22+
ui.input_numeric("price", "Price", 0.0, min=0.0, step=0.01),
23+
ui.input_slider("tax_rate", "Tax (%)", 0, 20, 8, step=1),
24+
ui.layout_columns(
25+
ui.input_action_button("add", "Add Item", class_="btn btn-primary btn-sm"),
26+
ui.input_action_button("update", "Update Item", class_="btn btn-success btn-sm"),
27+
ui.input_action_button("clear", "Clear All", class_="btn btn-danger btn-sm"),
28+
),
29+
ui.download_button("download_pdf", "Download Receipt (PDF)", class_="btn btn-secondary btn-sm"),
30+
open="always",
31+
),
32+
ui.layout_columns(
33+
ui.card(
34+
ui.card_header("Items Added"),
35+
ui.output_ui("items_table"),
36+
),
37+
ui.card(
38+
ui.card_header("Generated Receipt"),
39+
ui.output_ui("receipt"),
40+
),
41+
),
42+
)
43+
44+
def server(input, output, session):
45+
# Same Add / Update / Clear / Edit / Delete code as before ----------------
46+
@reactive.effect
47+
@reactive.event(input.add)
48+
def _add():
49+
df = items.get().copy()
50+
df = pd.concat(
51+
[df, pd.DataFrame([{
52+
"Product_Code": input.code(),
53+
"Qty": input.qty(),
54+
"Category": input.desc(),
55+
"Price": input.price(),
56+
}])],
57+
ignore_index=True,
58+
)
59+
items.set(df)
60+
61+
@reactive.effect
62+
@reactive.event(input.update)
63+
def _update():
64+
idx = edit_index.get()
65+
if idx is not None:
66+
df = items.get().copy()
67+
if 0 <= idx < len(df):
68+
df.loc[idx] = {
69+
"Product_Code": input.code(),
70+
"Qty": input.qty(),
71+
"Category": input.desc(),
72+
"Price": input.price(),
73+
}
74+
items.set(df)
75+
edit_index.set(None)
76+
77+
@reactive.effect
78+
@reactive.event(input.clear)
79+
def _clear():
80+
items.set(pd.DataFrame(columns=["Product_Code", "Qty", "Category", "Price"]))
81+
82+
@reactive.effect
83+
@reactive.event(input.remove_index)
84+
def _remove():
85+
idx = int(input.remove_index())
86+
df = items.get().copy()
87+
if 0 <= idx < len(df):
88+
items.set(df.drop(idx).reset_index(drop=True))
89+
90+
@reactive.effect
91+
@reactive.event(input.edit_index)
92+
def _edit():
93+
idx = int(input.edit_index())
94+
df = items.get().copy()
95+
if 0 <= idx < len(df):
96+
row = df.iloc[idx]
97+
session.send_input_message("code", {"value": str(row["Product_Code"])})
98+
session.send_input_message("desc", {"value": str(row["Category"])})
99+
session.send_input_message("qty", {"value": float(row["Qty"])})
100+
session.send_input_message("price", {"value": float(row["Price"])})
101+
edit_index.set(idx)
102+
103+
@output
104+
@render.ui
105+
def items_table():
106+
df = items.get()
107+
if df.empty:
108+
return ui.p("No items yet.")
109+
110+
rows_html = []
111+
for i, r in df.iterrows():
112+
rows_html.append(
113+
f"""
114+
<tr>
115+
<td style="white-space:nowrap;">
116+
<button class="btn btn-sm btn-warning"
117+
onclick="Shiny.setInputValue('edit_index', {i}, {{priority:'event'}})">✏️ Edit</button>
118+
<button class="btn btn-sm btn-danger"
119+
onclick="Shiny.setInputValue('remove_index', {i}, {{priority:'event'}})">❎ Delete</button>
120+
</td>
121+
<td>{r['Product_Code']}</td>
122+
<td>{r['Qty']}</td>
123+
<td>{r['Category']}</td>
124+
<td>{r['Price']:.2f}</td>
125+
</tr>
126+
"""
127+
)
128+
129+
return ui.HTML(
130+
"""
131+
<table class="table table-sm" style="font-size:13px; border-collapse:collapse;">
132+
<thead>
133+
<tr><th style="width:140px;">Actions</th><th>Product_Code</th><th>Qty</th><th>Item</th><th>Price</th></tr>
134+
</thead>
135+
<tbody>
136+
""" + "\n".join(rows_html) + """
137+
</tbody>
138+
</table>
139+
"""
140+
)
141+
142+
@output
143+
@render.ui
144+
def receipt():
145+
df = items.get()
146+
if df.empty:
147+
return ui.p("No items yet.")
148+
149+
subtotal = float((df["Qty"] * df["Price"]).sum())
150+
tax_rate = input.tax_rate() / 100.0
151+
tax = round(subtotal * tax_rate, 2)
152+
total = round(subtotal + tax, 2)
153+
now = datetime.datetime.now()
154+
155+
lines = "".join(
156+
f"<tr><td>{row.Product_Code}</td><td>{row.Qty}</td><td>{row.Category}</td><td>{row.Price:.2f}</td></tr>"
157+
for row in df.itertuples()
158+
)
159+
160+
return ui.HTML(f"""
161+
<div style="border:1px solid #000; padding:10px; width:280px; font-family:monospace; font-size:13px">
162+
<h4 style="text-align:center; margin:0;">RECEIPT 🧾</h4>
163+
<p style="margin:2px 0;">Nr.: 69 </p>
164+
<hr style="margin:4px 0;">
165+
<table style="width:100%; font-size:12px; border-collapse:collapse;">
166+
<tr><th>Product_Code</th><th>Qty</th><th>Item</th><th>Price</th></tr>
167+
{lines}
168+
</table>
169+
<hr style="margin:4px 0;">
170+
<p style="margin:2px 0;">Subtotal: {subtotal:.2f}</p>
171+
<p style="margin:2px 0;">Tax ({input.tax_rate()}%): {tax:.2f}</p>
172+
<p style="margin:2px 0;"><b>Total: {total:.2f}</b></p>
173+
<hr style="margin:4px 0;">
174+
<p style="margin:2px 0;">Date: {now.strftime('%d/%m/%Y')}</p>
175+
<p style="margin:2px 0;">Time: {now.strftime('%I:%M %p')}</p>
176+
</div>
177+
""")
178+
179+
# PDF Download (receipt-style narrow width)
180+
@render.download(filename=lambda: f"receipt_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf")
181+
def download_pdf():
182+
df = items.get()
183+
if df.empty:
184+
yield b""
185+
return
186+
187+
subtotal = float((df["Qty"] * df["Price"]).sum())
188+
tax_rate = input.tax_rate() / 100.0
189+
tax = round(subtotal * tax_rate, 2)
190+
total = round(subtotal + tax, 2)
191+
now = datetime.datetime.now()
192+
193+
from io import BytesIO
194+
buffer = BytesIO()
195+
196+
# --- custom page size like thermal roll ---
197+
RECEIPT_WIDTH = 220 # ~80mm
198+
RECEIPT_HEIGHT = 600
199+
receipt_page = (RECEIPT_WIDTH, RECEIPT_HEIGHT)
200+
201+
doc = SimpleDocTemplate(
202+
buffer,
203+
pagesize=receipt_page,
204+
leftMargin=10, rightMargin=10, topMargin=10, bottomMargin=10
205+
)
206+
styles = getSampleStyleSheet()
207+
elements = []
208+
209+
# ---- Shop name / Header ----
210+
elements.append(Paragraph("<b>PARZi GLOBAL</b>", styles["Title"]))
211+
elements.append(Paragraph(f"Nr.: {now.strftime('%Y%m%d%H%M%S')}", styles["Normal"]))
212+
elements.append(Spacer(1, 8))
213+
214+
# Items Table
215+
data = [["Product_Code", "Qty", "Item", "Price"]]
216+
for row in df.itertuples():
217+
data.append([row.Product_Code, row.Qty, row.Category, f"{row.Price:.2f}"])
218+
table = Table(data, colWidths=[40, 30, 70, 50])
219+
table.setStyle(TableStyle([
220+
("GRID", (0, 0), (-1, -1), 0.5, colors.black),
221+
("FONTNAME", (0, 0), (-1, -1), "Courier"),
222+
("FONTSIZE", (0, 0), (-1, -1), 8),
223+
("ALIGN", (1, 1), (-1, -1), "CENTER"),
224+
("BACKGROUND", (0, 0), (-1, 0), colors.lightgrey),
225+
]))
226+
elements.append(table)
227+
elements.append(Spacer(1, 8))
228+
229+
# Totals
230+
elements.append(Paragraph(f"Subtotal: {subtotal:.2f}", styles["Normal"]))
231+
elements.append(Paragraph(f"Tax ({input.tax_rate()}%): {tax:.2f}", styles["Normal"]))
232+
elements.append(Paragraph(f"<b>Total: {total:.2f}</b>", styles["Normal"]))
233+
elements.append(Spacer(1, 8))
234+
235+
# Date/Time
236+
elements.append(Paragraph(f"Date: {now.strftime('%d/%m/%Y')}", styles["Normal"]))
237+
elements.append(Paragraph(f"Time: {now.strftime('%I:%M %p')}", styles["Normal"]))
238+
239+
doc.build(elements)
240+
241+
buffer.seek(0)
242+
yield buffer.read()
243+
244+
app = App(app_ui, server)

0 commit comments

Comments
 (0)