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