arittrabag commited on
Commit
3cfd7a0
·
verified ·
1 Parent(s): d69c3ff

Upload 6 files

Browse files
Files changed (6) hide show
  1. app.py +609 -0
  2. assets/inephos.png +0 -0
  3. assets/srestham.png +0 -0
  4. inephos.html +268 -0
  5. requirements.txt +6 -0
  6. srestham.html +268 -0
app.py ADDED
@@ -0,0 +1,609 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # HTML-based Invoice Generator
2
+ import io
3
+ import os
4
+ import zipfile
5
+ import tempfile
6
+ import shutil
7
+ from typing import Dict, List
8
+ import base64
9
+
10
+ import pandas as pd
11
+ import streamlit as st
12
+ from num2words import num2words
13
+
14
+ # Try to import Playwright for PDF conversion
15
+ PLAYWRIGHT_AVAILABLE = False
16
+
17
+ try:
18
+ from playwright.sync_api import sync_playwright
19
+ PLAYWRIGHT_AVAILABLE = True
20
+ except ImportError:
21
+ pass
22
+
23
+ # Try to import PyPDF for PDF page extraction
24
+ PYPDF_AVAILABLE = False
25
+
26
+ try:
27
+ from pypdf import PdfReader, PdfWriter
28
+ PYPDF_AVAILABLE = True
29
+ except ImportError:
30
+ pass
31
+
32
+ # -------------------------
33
+ # Utility helpers
34
+ # -------------------------
35
+
36
+ def safe_str(x):
37
+ if pd.isna(x):
38
+ return ""
39
+ return str(x)
40
+
41
+ def format_amount(x):
42
+ """Format amount as Indian currency format"""
43
+ try:
44
+ num = float(x)
45
+ if num == 0:
46
+ return "0.00"
47
+ return f"{num:,.2f}"
48
+ except Exception:
49
+ return "0.00"
50
+
51
+ def to_words(value):
52
+ """Convert number to words"""
53
+ try:
54
+ num = float(value)
55
+ if num == 0:
56
+ return "Zero Rupees Only"
57
+ if num.is_integer():
58
+ num = int(num)
59
+ return num2words(num, to="cardinal", lang="en").title() + " Only"
60
+ except Exception:
61
+ return "Zero Rupees Only"
62
+
63
+ # -------------------------
64
+ # HTML Template Processing
65
+ # -------------------------
66
+
67
+ def fill_html_template(template_path: str, invoice_data: dict, output_path: str):
68
+ """Fill HTML template with invoice data and save"""
69
+
70
+ # Read the HTML template
71
+ with open(template_path, 'r', encoding='utf-8') as f:
72
+ html_content = f.read()
73
+
74
+ # Prepare data for injection
75
+ data = {
76
+ 'invoice_no': invoice_data.get('Invoice No', ''),
77
+ 'invoice_date': invoice_data.get('Invoice Date', ''),
78
+ 'customer_name': invoice_data.get('Customer Name', ''),
79
+ 'buyer_gstin': invoice_data.get('Customer GSTIN', ''),
80
+ 'buyer_pan': invoice_data.get('Customer PAN', ''),
81
+ 'buyer_cin': invoice_data.get('Customer CIN', ''),
82
+ 'po_no': invoice_data.get('PO No', ''),
83
+ 'buyer_state': invoice_data.get('Customer State', ''),
84
+ }
85
+
86
+ # Handle customer address (split into multiple lines)
87
+ address = invoice_data.get('Customer Address', '')
88
+ if address:
89
+ address_parts = [p.strip() for p in address.split(',')]
90
+ data['customer_address_1'] = address_parts[0] if len(address_parts) > 0 else ''
91
+ data['customer_address_2'] = address_parts[1] if len(address_parts) > 1 else ''
92
+ data['customer_address_3'] = ', '.join(address_parts[2:]) if len(address_parts) > 2 else ''
93
+ else:
94
+ data['customer_address_1'] = data['customer_address_2'] = data['customer_address_3'] = ''
95
+
96
+ # Calculate totals dynamically
97
+ items = invoice_data.get('Items', [])
98
+ total_taxable = 0
99
+ total_cgst = 0
100
+ total_sgst = 0
101
+ total_igst = 0
102
+
103
+ for item in items:
104
+ taxable_value = float(item.get('Taxable Value', 0))
105
+ cgst_percentage = float(item.get('CGST %', 0))
106
+ sgst_percentage = float(item.get('SGST %', 0))
107
+ igst_percentage = float(item.get('IGST %', 0))
108
+
109
+ # Calculate tax amounts dynamically
110
+ cgst_amount = round((taxable_value * cgst_percentage / 100), 2)
111
+ sgst_amount = round((taxable_value * sgst_percentage / 100), 2)
112
+ igst_amount = round((taxable_value * igst_percentage / 100), 2)
113
+
114
+ # Add to totals
115
+ total_taxable += taxable_value
116
+ total_cgst += cgst_amount
117
+ total_sgst += sgst_amount
118
+ total_igst += igst_amount
119
+
120
+ # Calculate grand total
121
+ grand_total = total_taxable + total_cgst + total_sgst + total_igst
122
+
123
+ # Add totals to data
124
+ data.update({
125
+ 'total_taxable': format_amount(total_taxable),
126
+ 'total_cgst': format_amount(total_cgst),
127
+ 'total_sgst': format_amount(total_sgst),
128
+ 'total_igst': format_amount(total_igst),
129
+ 'grand_total': format_amount(grand_total),
130
+ 'rounding': format_amount(0), # Can be calculated if needed
131
+ 'net_amount': format_amount(grand_total),
132
+ 'words_total': to_words(grand_total),
133
+ 'words_cgst': to_words(total_cgst),
134
+ 'words_sgst': to_words(total_sgst),
135
+ 'words_igst': to_words(total_igst),
136
+ })
137
+
138
+ # Prepare items data for JavaScript injection
139
+ items_js = []
140
+ for i, item in enumerate(items):
141
+ taxable_value = float(item.get('Taxable Value', 0))
142
+ cgst_percentage = float(item.get('CGST %', 0))
143
+ sgst_percentage = float(item.get('SGST %', 0))
144
+ igst_percentage = float(item.get('IGST %', 0))
145
+
146
+ # Calculate tax amounts dynamically from percentages
147
+ cgst_amount = round((taxable_value * cgst_percentage / 100), 2)
148
+ sgst_amount = round((taxable_value * sgst_percentage / 100), 2)
149
+ igst_amount = round((taxable_value * igst_percentage / 100), 2)
150
+
151
+ # Calculate total amount
152
+ total_amount = taxable_value + cgst_amount + sgst_amount + igst_amount
153
+
154
+ items_js.append({
155
+ 'sr': i + 1,
156
+ 'desc': item.get('Service Details', ''),
157
+ 'hsn': item.get('HSN', ''),
158
+ 'tax': taxable_value,
159
+ 'cgst_p': cgst_percentage,
160
+ 'cgst_a': cgst_amount, # Dynamically calculated
161
+ 'sgst_p': sgst_percentage,
162
+ 'sgst_a': sgst_amount, # Dynamically calculated
163
+ 'igst_p': igst_percentage,
164
+ 'igst_a': igst_amount, # Dynamically calculated
165
+ 'total': round(total_amount, 2), # Dynamically calculated
166
+ })
167
+
168
+ # Create JavaScript injection code
169
+ js_injection = f"""
170
+ <script>
171
+ // Data injection
172
+ const invoiceData = {data};
173
+ const itemsData = {items_js};
174
+
175
+ // Inject data into template
176
+ inject({{ ...invoiceData, items: itemsData }});
177
+ </script>
178
+ """
179
+
180
+ # Insert the injection script before the closing body tag
181
+ html_content = html_content.replace('</body>', f'{js_injection}\n</body>')
182
+
183
+ # Handle logo images (convert to base64 if exists)
184
+ # Check for both company logos
185
+ logo_paths = ['assets/inephos.png', 'assets/srestham.png']
186
+ for logo_path in logo_paths:
187
+ if os.path.exists(logo_path):
188
+ with open(logo_path, 'rb') as img_file:
189
+ img_b64 = base64.b64encode(img_file.read()).decode()
190
+ img_src = f"data:image/png;base64,{img_b64}"
191
+ html_content = html_content.replace(f'src="{logo_path}"', f'src="{img_src}"')
192
+
193
+ # Also handle any remaining generic image references
194
+ if os.path.exists('image.png'):
195
+ with open('image.png', 'rb') as img_file:
196
+ img_b64 = base64.b64encode(img_file.read()).decode()
197
+ img_src = f"data:image/png;base64,{img_b64}"
198
+ html_content = html_content.replace('src="image.png"', f'src="{img_src}"')
199
+
200
+ # Write the filled HTML
201
+ with open(output_path, 'w', encoding='utf-8') as f:
202
+ f.write(html_content)
203
+
204
+ print(f"✅ Generated HTML invoice: {output_path}")
205
+ return output_path
206
+
207
+
208
+ def html_to_pdf_playwright(html_path: str, pdf_path: str) -> bool:
209
+ """Convert HTML to PDF using Playwright with Windows compatibility"""
210
+ import asyncio
211
+ import sys
212
+
213
+ async def create_pdf():
214
+ from playwright.async_api import async_playwright
215
+
216
+ async with async_playwright() as p:
217
+ browser = await p.chromium.launch(headless=True)
218
+ page = await browser.new_page()
219
+
220
+ # Load the HTML file
221
+ file_url = f"file:///{os.path.abspath(html_path).replace(os.sep, '/')}"
222
+ await page.goto(file_url, wait_until='networkidle')
223
+
224
+ # Generate PDF with proper settings
225
+ await page.pdf(
226
+ path=pdf_path,
227
+ format='A4',
228
+ margin={
229
+ 'top': '16mm',
230
+ 'bottom': '16mm',
231
+ 'left': '10mm',
232
+ 'right': '10mm'
233
+ },
234
+ print_background=True,
235
+ prefer_css_page_size=True
236
+ )
237
+
238
+ await browser.close()
239
+
240
+ try:
241
+ # Set event loop policy for Windows compatibility
242
+ if sys.platform == "win32":
243
+ asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
244
+
245
+ # Run the async function
246
+ asyncio.run(create_pdf())
247
+ return True
248
+
249
+ except Exception as e:
250
+ print(f"Playwright conversion failed: {e}")
251
+ # Additional debugging for Windows issues
252
+ if "NotImplementedError" in str(e):
253
+ print("Windows asyncio compatibility issue detected. Trying alternative approach...")
254
+ try:
255
+ # Alternative: Use subprocess to call playwright directly
256
+ import subprocess
257
+ import json
258
+
259
+ # Create a simple script to run playwright
260
+ script_content = f'''
261
+ import asyncio
262
+ import sys
263
+ from playwright.async_api import async_playwright
264
+
265
+ async def main():
266
+ async with async_playwright() as p:
267
+ browser = await p.chromium.launch(headless=True)
268
+ page = await browser.new_page()
269
+ await page.goto("file:///{os.path.abspath(html_path).replace(os.sep, '/')}", wait_until='networkidle')
270
+ await page.pdf(
271
+ path="{pdf_path.replace(os.sep, '/')}",
272
+ format='A4',
273
+ margin={{"top": "16mm", "bottom": "16mm", "left": "10mm", "right": "10mm"}},
274
+ print_background=True,
275
+ prefer_css_page_size=True
276
+ )
277
+ await browser.close()
278
+
279
+ if __name__ == "__main__":
280
+ asyncio.run(main())
281
+ '''
282
+
283
+ # Write temporary script
284
+ script_path = os.path.join(os.path.dirname(pdf_path), 'temp_pdf_script.py')
285
+ with open(script_path, 'w', encoding='utf-8') as f:
286
+ f.write(script_content)
287
+
288
+ # Run script in subprocess
289
+ result = subprocess.run([sys.executable, script_path],
290
+ capture_output=True, text=True, timeout=30)
291
+
292
+ # Clean up
293
+ if os.path.exists(script_path):
294
+ os.remove(script_path)
295
+
296
+ if result.returncode == 0 and os.path.exists(pdf_path):
297
+ return True
298
+ else:
299
+ print(f"Subprocess approach failed: {result.stderr}")
300
+
301
+ except Exception as e2:
302
+ print(f"Alternative approach also failed: {e2}")
303
+
304
+ return False
305
+
306
+
307
+ def html_to_pdf(html_path: str, pdf_path: str) -> bool:
308
+ """Convert HTML to PDF using Playwright (mandatory)"""
309
+ if PLAYWRIGHT_AVAILABLE:
310
+ return html_to_pdf_playwright(html_path, pdf_path)
311
+ else:
312
+ print("❌ Playwright not available. PDF generation is mandatory!")
313
+ print("Please install: pip install playwright && playwright install chromium")
314
+ return False
315
+
316
+ def extract_first_page_pdf(input_pdf_path: str, output_pdf_path: str) -> bool:
317
+ """Extract only the first page from a PDF file"""
318
+ if not PYPDF_AVAILABLE:
319
+ print("⚠️ PyPDF not available. Keeping original PDF.")
320
+ # Copy the original file if PyPDF is not available
321
+ try:
322
+ shutil.copy2(input_pdf_path, output_pdf_path)
323
+ return True
324
+ except Exception as e:
325
+ print(f"❌ Failed to copy PDF: {e}")
326
+ return False
327
+
328
+ try:
329
+ # Read the input PDF
330
+ reader = PdfReader(input_pdf_path)
331
+
332
+ # Create a new PDF writer
333
+ writer = PdfWriter()
334
+
335
+ # Add only the first page (if it exists)
336
+ if len(reader.pages) > 0:
337
+ writer.add_page(reader.pages[0])
338
+
339
+ # Write the first page to the output file
340
+ with open(output_pdf_path, 'wb') as output_file:
341
+ writer.write(output_file)
342
+
343
+ print(f"✅ Extracted first page: {output_pdf_path}")
344
+ return True
345
+ else:
346
+ print(f"⚠️ No pages found in PDF: {input_pdf_path}")
347
+ return False
348
+
349
+ except Exception as e:
350
+ print(f"❌ Failed to extract first page from {input_pdf_path}: {e}")
351
+ # Fallback: copy the original file
352
+ try:
353
+ shutil.copy2(input_pdf_path, output_pdf_path)
354
+ return True
355
+ except Exception as e2:
356
+ print(f"❌ Failed to copy PDF as fallback: {e2}")
357
+ return False
358
+
359
+ def process_invoices_html(data_file, template_path: str, company_name: str):
360
+ """Process invoices using HTML template approach"""
361
+
362
+ # Read customer data
363
+ df = pd.read_excel(data_file, engine="openpyxl")
364
+ df.columns = [str(c).strip().lower() for c in df.columns]
365
+
366
+ # Create temporary directory for processing
367
+ with tempfile.TemporaryDirectory() as temp_dir:
368
+ invoices = []
369
+ html_files = []
370
+ pdf_files = []
371
+
372
+ # Process each row as a separate invoice
373
+ for i, r in df.iterrows():
374
+ # Create invoice data structure
375
+ invoice_data = {
376
+ 'Invoice No': safe_str(r.get("invoice no", f"INV/{i+1}")),
377
+ 'Invoice Date': safe_str(r.get("invoice date", "")),
378
+ 'Customer Name': safe_str(r.get("customer name", "")),
379
+ 'Customer Address': safe_str(r.get("customer address", "")),
380
+ 'Customer GSTIN': safe_str(r.get("customer_gstin", "")),
381
+ 'Customer PAN': safe_str(r.get("customer_pan", "")),
382
+ 'Customer CIN': safe_str(r.get("customer_cin", "")),
383
+ 'Customer State': safe_str(r.get("customer_state", "")),
384
+ 'PO No': safe_str(r.get("po_no", "")),
385
+ 'Total Amount': r.get("total amount", 0) or 0,
386
+ 'Items': [{
387
+ 'Service Details': safe_str(r.get("service details", "")),
388
+ 'HSN': safe_str(r.get("hsn", "")),
389
+ 'Taxable Value': r.get("taxable value", 0) or 0,
390
+ 'CGST %': r.get("cgst %", 0) or 0,
391
+ 'SGST %': r.get("sgst %", 0) or 0,
392
+ 'IGST %': r.get("igst %", 0) or 0,
393
+ # Tax amounts and total will be calculated dynamically
394
+ }]
395
+ }
396
+
397
+ # Generate HTML file
398
+ html_filename = f"{invoice_data['Invoice No']}_{company_name}.html"
399
+ html_path = os.path.join(temp_dir, html_filename)
400
+
401
+ # Fill template
402
+ fill_html_template(template_path, invoice_data, html_path)
403
+ html_files.append((html_filename, html_path))
404
+
405
+ # Convert to PDF (mandatory with Playwright)
406
+ pdf_filename = f"{invoice_data['Invoice No']}_{company_name}.pdf"
407
+ pdf_path_temp = os.path.join(temp_dir, f"temp_{pdf_filename}")
408
+ pdf_path_final = os.path.join(temp_dir, pdf_filename)
409
+
410
+ if html_to_pdf(html_path, pdf_path_temp):
411
+ # Extract only first page from the generated PDF
412
+ if extract_first_page_pdf(pdf_path_temp, pdf_path_final):
413
+ pdf_files.append((pdf_filename, pdf_path_final))
414
+ # Clean up temporary PDF
415
+ if os.path.exists(pdf_path_temp):
416
+ os.remove(pdf_path_temp)
417
+ else:
418
+ st.error(f"❌ Failed to extract first page for {invoice_data['Invoice No']}")
419
+ print(f"❌ First page extraction failed for {invoice_data['Invoice No']}")
420
+ else:
421
+ st.error(f"❌ Failed to generate PDF for {invoice_data['Invoice No']}")
422
+ print(f"❌ PDF generation failed for {invoice_data['Invoice No']}")
423
+
424
+ invoices.append(invoice_data)
425
+
426
+ # Create ZIP file with results
427
+ zip_buffer = io.BytesIO()
428
+ with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf:
429
+ # Add HTML files
430
+ for filename, filepath in html_files:
431
+ zf.write(filepath, filename)
432
+
433
+ # Add PDF files if available
434
+ for filename, filepath in pdf_files:
435
+ if os.path.exists(filepath):
436
+ zf.write(filepath, filename)
437
+
438
+ zip_buffer.seek(0)
439
+ return zip_buffer, len(invoices)
440
+
441
+ # -------------------------
442
+ # Streamlit UI
443
+ # -------------------------
444
+ st.set_page_config(page_title="HTML-Based Invoice Generator", layout="wide")
445
+ st.title("🌐 HTML-Based Invoice Generator — Inephos & Srestham")
446
+
447
+ st.markdown("""
448
+ Upload your customer data Excel file. The system will:
449
+ 1. 📄 Generate individual HTML invoices using the template
450
+ 2. 🎨 Apply beautiful styling and formatting
451
+ 3. 📁 Convert to PDF (if weasyprint/playwright is available)
452
+ 4. 📦 Package everything in a downloadable ZIP
453
+
454
+ **Benefits of HTML approach:**
455
+ - ✨ Perfect visual control and styling
456
+ - ⚡ Faster processing than Excel manipulation
457
+ - 🎯 Production-ready professional output
458
+ - 🔧 Easy to customize and maintain
459
+ """)
460
+
461
+ # Upload section
462
+ col1, col2 = st.columns(2)
463
+
464
+ with col1:
465
+ st.subheader("📊 Customer Data")
466
+ data_file = st.file_uploader("Upload customer Excel (.xlsx/.xlsm)", type=["xlsx", "xlsm"])
467
+
468
+ if data_file:
469
+ st.success("✅ Customer data uploaded")
470
+
471
+ # Preview data
472
+ try:
473
+ preview_df = pd.read_excel(data_file, engine="openpyxl").head(3)
474
+ st.write("**Data Preview:**")
475
+ st.dataframe(preview_df)
476
+ except Exception as e:
477
+ st.error(f"Error reading file: {e}")
478
+
479
+ with col2:
480
+ st.subheader("🎨 Template Selection")
481
+
482
+ # Check available templates
483
+ inephos_available = os.path.exists('inephos.html')
484
+ srestham_available = os.path.exists('srestham.html')
485
+
486
+ if inephos_available and srestham_available:
487
+ st.success("✅ Both templates found")
488
+
489
+ # Radio button for template selection
490
+ selected_template = st.radio(
491
+ "Select Company Template:",
492
+ options=["Inephos", "Srestham"],
493
+ horizontal=True,
494
+ help="Choose which company's invoice template to use"
495
+ )
496
+
497
+ template_file = "inephos.html" if selected_template == "Inephos" else "srestham.html"
498
+ st.info(f"Using: {template_file}")
499
+
500
+ elif inephos_available:
501
+ st.warning("⚠️ Only Inephos template found")
502
+ selected_template = "Inephos"
503
+ template_file = "inephos.html"
504
+
505
+ elif srestham_available:
506
+ st.warning("⚠️ Only Srestham template found")
507
+ selected_template = "Srestham"
508
+ template_file = "srestham.html"
509
+
510
+ else:
511
+ st.error("❌ No templates found")
512
+ st.write("Make sure inephos.html and/or srestham.html are in the same directory")
513
+ selected_template = None
514
+ template_file = None
515
+
516
+ # Processing section
517
+ if template_file:
518
+ st.markdown("---")
519
+ st.subheader("🚀 Generate Invoices")
520
+
521
+ col3, col4 = st.columns(2)
522
+
523
+ with col3:
524
+ # Enable Inephos button only when Inephos is selected
525
+ inephos_enabled = (selected_template == "Inephos")
526
+ if st.button("🏢 Generate Inephos Invoices",
527
+ use_container_width=True,
528
+ disabled=not inephos_enabled,
529
+ help="Select Inephos template to enable this button"):
530
+ if not data_file:
531
+ st.error("Please upload customer data file.")
532
+ else:
533
+ with st.spinner("Generating Inephos invoices..."):
534
+ try:
535
+ zip_buffer, count = process_invoices_html(data_file, "inephos.html", "inephos")
536
+ st.success(f"✅ Generated {count} Inephos invoices successfully!")
537
+
538
+ file_types = "HTML files and PDFs"
539
+
540
+ st.download_button(
541
+ label=f"📥 Download ZIP ({file_types})",
542
+ data=zip_buffer,
543
+ file_name="inephos_invoices.zip",
544
+ mime="application/zip"
545
+ )
546
+ except Exception as e:
547
+ st.error(f"Error generating invoices: {e}")
548
+ st.exception(e)
549
+
550
+ with col4:
551
+ # Enable Srestham button only when Srestham is selected
552
+ srestham_enabled = (selected_template == "Srestham")
553
+ if st.button("🏢 Generate Srestham Invoices",
554
+ use_container_width=True,
555
+ disabled=not srestham_enabled,
556
+ help="Select Srestham template to enable this button"):
557
+ if not data_file:
558
+ st.error("Please upload customer data file.")
559
+ else:
560
+ with st.spinner("Generating Srestham invoices..."):
561
+ try:
562
+ zip_buffer, count = process_invoices_html(data_file, "srestham.html", "srestham")
563
+ st.success(f"✅ Generated {count} Srestham invoices successfully!")
564
+
565
+ file_types = "HTML files and PDFs"
566
+
567
+ st.download_button(
568
+ label=f"📥 Download ZIP ({file_types})",
569
+ data=zip_buffer,
570
+ file_name="srestham_invoices.zip",
571
+ mime="application/zip"
572
+ )
573
+ except Exception as e:
574
+ st.error(f"Error generating invoices: {e}")
575
+ st.exception(e)
576
+
577
+ # Information section
578
+ st.markdown("---")
579
+ st.subheader("ℹ️ Setup & Dependencies")
580
+
581
+ col5, col6 = st.columns(2)
582
+
583
+ with col5:
584
+ st.markdown("""
585
+ **Installation for PDF conversion:**
586
+ ```bash
587
+ # Playwright (mandatory for PDF generation)
588
+ pip install playwright
589
+ playwright install chromium
590
+
591
+ # PyPDF (recommended for first-page extraction)
592
+ pip install pypdf
593
+ ```
594
+ """)
595
+
596
+ with col6:
597
+ st.markdown(f"""
598
+ **Current Status:**
599
+ - Playwright: {'✅ Available' if PLAYWRIGHT_AVAILABLE else '❌ Not installed (REQUIRED)'}
600
+ - PyPDF: {'✅ Available' if PYPDF_AVAILABLE else '⚠️ Not installed (recommended)'}
601
+ - Inephos Template: {'✅ Found' if os.path.exists('inephos.html') else '❌ Missing'}
602
+ - Srestham Template: {'✅ Found' if os.path.exists('srestham.html') else '❌ Missing'}
603
+
604
+ **Output:** HTML invoices + First-page PDFs (mandatory)
605
+ """)
606
+
607
+ if not PLAYWRIGHT_AVAILABLE:
608
+ st.error("🚨 **Playwright is required for PDF generation! Install it to continue.**")
609
+ st.code("pip install playwright && playwright install chromium")
assets/inephos.png ADDED
assets/srestham.png ADDED
inephos.html ADDED
@@ -0,0 +1,268 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>Inephos – Tax Invoice</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ <style>
8
+ /* ===== Page & print ===== */
9
+ @page { size: A4; margin: 16mm 10mm; }
10
+ html, body { height: 100%; }
11
+ body {
12
+ font-family: "Segoe UI", Roboto, system-ui, -apple-system, Arial, sans-serif;
13
+ margin: 0; color: #111; background:#fff;
14
+ }
15
+ .invoice-wrap { width: 210mm; max-width: 100%; margin: 0 auto; }
16
+ h1.title { text-align:center; font-size: 18px; margin: 6px 0 10px; letter-spacing: .4px; }
17
+
18
+ /* ===== Frame ===== */
19
+ .frame { border: 1px solid #888; border-radius: 2px; padding: 8px 8px 10px; }
20
+ .row { display: grid; grid-gap: 0; }
21
+ .muted { color:#444; }
22
+ .small { font-size: 11px; }
23
+ .right { text-align:right; }
24
+ .bold { font-weight:600; }
25
+ .mb8 { margin-bottom: 8px; }
26
+ .mb4 { margin-bottom: 4px; }
27
+ .mt4 { margin-top: 4px; }
28
+
29
+ /* ===== Header block (logo + seller) ===== */
30
+ .hdr {
31
+ grid-template-columns: 1fr 1.2fr;
32
+ align-items: start;
33
+ }
34
+ .logo-wrap { display:flex; align-items:flex-start; gap:8px; }
35
+ .logo { height: 100px; border: none; }
36
+ .tagline { font-size: 12px; margin-top: 6px; }
37
+ .seller h3 { margin: 0 0 4px; font-size: 14px; }
38
+ .seller p { margin: 0; line-height: 1.25; font-size: 11.5px; }
39
+
40
+ .statbar { background:#e9edf3; border:1px solid #bfc7d1; border-left:0; border-right:0; padding:4px 6px; font-size: 11.5px; }
41
+
42
+ /* ===== Meta (Invoice No / Date) ===== */
43
+ .meta { grid-template-columns: 140px 1fr 140px 1fr; border:1px solid #999; border-top:0; }
44
+ .meta > div { padding:4px 6px; border-right:1px solid #999; }
45
+ .meta > div:nth-child(4n) { border-right:0; }
46
+ .lbl { background:#f5f5f5; font-weight:600; }
47
+
48
+ /* ===== Customer details ===== */
49
+ .cust-hdr { border:1px solid #999; border-top:0; padding:4px 6px; font-weight:600; }
50
+ .cust { display:grid; grid-template-columns: 1fr 140px 1fr; border:1px solid #999; border-top:0; }
51
+ .cust > div { padding:6px; }
52
+ .cust .grid { display:grid; grid-template-columns: 70px 1fr; }
53
+ .cust .grid > div { padding:2px 0; border-bottom:1px solid #eee; }
54
+ .cust .grid > div:nth-child(2n) { border-left:1px solid #eee; padding-left:8px; }
55
+
56
+ /* ===== Items table ===== */
57
+ table.items { width:100%; border-collapse:collapse; margin-top: 6px; }
58
+ table.items th, table.items td { border:1px solid #999; padding:6px 6px; font-size: 12px; vertical-align: top; }
59
+ table.items th { background:#f5f5f5; font-weight:600; }
60
+ table.items tfoot td { border:1px solid #999; padding:6px 6px; font-size: 12px; }
61
+ table.items tfoot td.label { background:#f5f5f5; font-weight:600; text-align:center; }
62
+ .num { text-align:right; white-space:nowrap; }
63
+ .pct { text-align:center; }
64
+ .w-sr { width:40px; }
65
+ .w-desc { width:auto; }
66
+ .w-hsn { width:90px; }
67
+ .w-taxval { width:95px; }
68
+ .w-pct { width:45px; }
69
+ .w-amt { width:95px; }
70
+ .w-total { width:110px; }
71
+
72
+ /* ===== Totals ===== */
73
+ .totals { width:100%; border-collapse:collapse; margin-top: 6px; }
74
+ .totals td { border:1px solid #999; padding:6px; font-size:12px; }
75
+ .totals td.label { background:#f5f5f5; font-weight:600; text-align:center; }
76
+
77
+ /* ===== Words & bank ===== */
78
+ .words, .bank { border:1px solid #999; border-top:0; padding:6px; font-size:12px; }
79
+ .bank { display:grid; grid-template-columns: 1fr 200px; }
80
+ .bank .rightbox { border-left:1px solid #999; padding-left:6px; display:flex; flex-direction:column; justify-content:space-between; }
81
+ .bank .sign { text-align:right; padding-top:40px; }
82
+
83
+ /* ===== Print tweaks ===== */
84
+ @media print { .invoice-wrap { width:auto; } }
85
+ </style>
86
+ </head>
87
+ <body>
88
+ <div class="invoice-wrap">
89
+ <h1 class="title">TAX INVOICE</h1>
90
+
91
+ <div class="frame">
92
+ <!-- Header: logo left, seller right -->
93
+ <div class="row hdr mb8">
94
+ <div>
95
+ <div class="logo-wrap">
96
+ <img class="logo" src="assets/inephos.png" alt="Inephos logo" />
97
+ </div>
98
+ </div>
99
+ <div class="seller">
100
+ <h3>Inephos</h3>
101
+ <p>(a unit of A1 Future Technologies Pvt. Ltd.)</p>
102
+ <p>64/89, Khudiram Bose Sarani, Dutta Bagan, Milk Colony, Kolkata - 700037, India</p>
103
+ <p>Phone : +91-9007797979, +91-9831066996</p>
104
+ <p>Website : <span>www.inephos.com</span> &nbsp; E‑Mail : <span>help@inephos.com</span></p>
105
+ <p>State : West Bengal, Code: 19</p>
106
+ </div>
107
+ </div>
108
+
109
+ <div class="statbar small">
110
+ GSTIN: 19AAKCA7063N1Z1 &nbsp; | &nbsp; PAN : AAKCA7063N &nbsp; | &nbsp; CIN : U72200WB2012PTC183279
111
+ </div>
112
+
113
+ <!-- Invoice meta -->
114
+ <div class="row meta">
115
+ <div class="lbl">Invoice No.</div>
116
+ <div data-key="invoice_no"></div>
117
+ <div class="lbl">Date</div>
118
+ <div data-key="invoice_date"></div>
119
+ </div>
120
+
121
+ <!-- Customer -->
122
+ <div class="cust-hdr">Customer Details</div>
123
+ <div class="cust">
124
+ <div>
125
+ <div class="bold" data-key="customer_name"></div>
126
+ <div data-key="customer_address_1"></div>
127
+ <div data-key="customer_address_2"></div>
128
+ <div data-key="customer_address_3"></div>
129
+ </div>
130
+ <div></div>
131
+ <div class="grid small">
132
+ <div class="bold right">GSTIN :</div><div data-key="buyer_gstin"></div>
133
+ <div class="bold right">PAN :</div><div data-key="buyer_pan"></div>
134
+ <div class="bold right">CIN :</div><div data-key="buyer_cin"></div>
135
+ <div class="bold right">PO No. :</div><div data-key="po_no"></div>
136
+ <div class="bold right">State :</div><div data-key="buyer_state"></div>
137
+ </div>
138
+ </div>
139
+
140
+ <!-- Items table -->
141
+ <table class="items">
142
+ <thead>
143
+ <tr>
144
+ <th class="w-sr">Sr.<br/>No.</th>
145
+ <th class="w-desc">Service Details</th>
146
+ <th class="w-hsn">HSN</th>
147
+ <th class="w-taxval">Taxable<br/>Value</th>
148
+ <th class="w-pct">CGST<br/>%</th>
149
+ <th class="w-amt">CGST<br/>Amt</th>
150
+ <th class="w-pct">SGST/UGST<br/>%</th>
151
+ <th class="w-amt">SGST/UGST<br/>Amt</th>
152
+ <th class="w-pct">IGST<br/>%</th>
153
+ <th class="w-amt">IGST<br/>Amt</th>
154
+ <th class="w-total">Total</th>
155
+ </tr>
156
+ </thead>
157
+ <tbody data-key="items_tbody">
158
+ <!-- rows will be injected -->
159
+ </tbody>
160
+ <tfoot>
161
+ <tr>
162
+ <td colspan="3" class="label">TOTAL</td>
163
+ <td class="num" data-key="total_taxable"></td>
164
+ <td></td>
165
+ <td class="num" data-key="total_cgst"></td>
166
+ <td></td>
167
+ <td class="num" data-key="total_sgst"></td>
168
+ <td></td>
169
+ <td class="num" data-key="total_igst"></td>
170
+ <td class="num" data-key="grand_total"></td>
171
+ </tr>
172
+ </tfoot>
173
+ </table>
174
+
175
+ <!-- Totals strip moved into items table as tfoot for perfect alignment -->
176
+
177
+ <table class="totals">
178
+ <tr>
179
+ <td class="label" colspan="9" style="text-align:right">Rounding Off</td>
180
+ <td class="num" data-key="rounding"></td>
181
+ </tr>
182
+ <tr>
183
+ <td class="label" colspan="9" style="text-align:right">Net Amount</td>
184
+ <td class="num" data-key="net_amount"></td>
185
+ </tr>
186
+ </table>
187
+
188
+ <div class="words">
189
+ <div><span class="bold">Total Invoice Value (In Words):</span> <span data-key="words_total"></span></div>
190
+ <div><span class="bold">Total CGST Value (In Words):</span> <span data-key="words_cgst"></span></div>
191
+ <div><span class="bold">Total SGST Value (In Words):</span> <span data-key="words_sgst"></span></div>
192
+ <div><span class="bold">Total IGST Value (In Words):</span> <span data-key="words_igst"></span></div>
193
+ </div>
194
+
195
+ <div class="bank">
196
+ <div>
197
+ <div class="bold">Bank Details</div>
198
+ <div>Account Name: A1 Future Technologies Private Limited</div>
199
+ <div>Bank Name: HDFC Bank Ltd. - Bank Branch: Shyam Bazar</div>
200
+ <div>Account No.: 01742320002223 - Swift Code : HDFCINBBCAL </div>
201
+ <div>RTGS / NEFT IFSC : HDFC0000174 - MICR Code: 700240011</div>
202
+ </div>
203
+ <div class="rightbox">
204
+ <div class="right">E. &amp; O. E.</div>
205
+ <div class="sign">For Inephos</div>
206
+ </div>
207
+ </div>
208
+ </div>
209
+ </div>
210
+
211
+ <script>
212
+ // --- Minimal injection helpers (optional) ---
213
+ function fmtMoney(n){
214
+ if(n === null || n === undefined || isNaN(n)) return '';
215
+ return Number(n).toLocaleString('en-IN', {minimumFractionDigits:2, maximumFractionDigits:2});
216
+ }
217
+
218
+ function inject(data){
219
+ // Simple fields
220
+ const map = {
221
+ invoice_no: 'INV/00001/25-26',
222
+ invoice_date: 'April 1, 2025',
223
+ customer_name: 'Binay Sharma',
224
+ customer_address_1: "H. No. 8-54/22, Happy's Home",
225
+ customer_address_2: 'Sai Harsha Nagar Colony, Hydershakote,',
226
+ customer_address_3: '500091 Hyderabad TG',
227
+ buyer_gstin: '', buyer_pan: '', buyer_cin: '',
228
+ po_no: '', buyer_state: 'Telangana, Code: 36',
229
+ total_taxable: fmtMoney(9150), total_cgst: fmtMoney(0), total_sgst: fmtMoney(0), total_igst: fmtMoney(1648),
230
+ grand_total: fmtMoney(10798), rounding: fmtMoney(0), net_amount: fmtMoney(10798),
231
+ words_total: 'Rupees Ten Thousand Seven Hundred Ninety Eight Only',
232
+ words_cgst: 'No Rupees Only', words_sgst: 'No Rupees Only',
233
+ words_igst: 'Rupees One Thousand Six Hundred Forty Eight Only'
234
+ };
235
+ Object.assign(map, data||{});
236
+ document.querySelectorAll('[data-key]').forEach(el=>{
237
+ const k = el.getAttribute('data-key');
238
+ if(k === 'items_tbody') return;
239
+ if(map[k] !== undefined) el.textContent = map[k];
240
+ });
241
+
242
+ // Items
243
+ const rows = (data && data.items) || [
244
+ { sr:1, desc:'Inephos Framed Canvas Painting - Seven Running White Horses 47 X 29', hsn:'491199', tax:4575, cgst_p:0, cgst_a:0, sgst_p:0, sgst_a:0, igst_p:18, igst_a:824, total:5399 },
245
+ { sr:2, desc:'Inephos Framed Canvas Painting - Divine Radha Krishna 47 X 29', hsn:'491199', tax:4575, cgst_p:0, cgst_a:0, sgst_p:0, sgst_a:0, igst_p:18, igst_a:824, total:5399 },
246
+ ];
247
+ const tbody = document.querySelector('[data-key="items_tbody"]');
248
+ tbody.innerHTML = rows.map(r => `
249
+ <tr>
250
+ <td class="w-sr num">${r.sr}</td>
251
+ <td class="w-desc">${r.desc || ''}</td>
252
+ <td class="w-hsn">${r.hsn || ''}</td>
253
+ <td class="w-taxval num">${fmtMoney(r.tax)}</td>
254
+ <td class="w-pct pct">${r.cgst_p||0}</td>
255
+ <td class="w-amt num">${fmtMoney(r.cgst_a||0)}</td>
256
+ <td class="w-pct pct">${r.sgst_p||0}</td>
257
+ <td class="w-amt num">${fmtMoney(r.sgst_a||0)}</td>
258
+ <td class="w-pct pct">${r.igst_p||0}</td>
259
+ <td class="w-amt num">${fmtMoney(r.igst_a||0)}</td>
260
+ <td class="w-total num">${fmtMoney(r.total||0)}</td>
261
+ </tr>`).join('');
262
+ }
263
+
264
+ // Example: inject with defaults
265
+ inject();
266
+ </script>
267
+ </body>
268
+ </html>
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ streamlit
2
+ pandas
3
+ num2words
4
+ openpyxl
5
+ playwright
6
+ pypdf
srestham.html ADDED
@@ -0,0 +1,268 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>Srestham – Tax Invoice</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ <style>
8
+ /* ===== Page & print ===== */
9
+ @page { size: A4; margin: 16mm 10mm; }
10
+ html, body { height: 100%; }
11
+ body {
12
+ font-family: "Segoe UI", Roboto, system-ui, -apple-system, Arial, sans-serif;
13
+ margin: 0; color: #111; background:#fff;
14
+ }
15
+ .invoice-wrap { width: 210mm; max-width: 100%; margin: 0 auto; }
16
+ h1.title { text-align:center; font-size: 18px; margin: 6px 0 10px; letter-spacing: .4px; }
17
+
18
+ /* ===== Frame ===== */
19
+ .frame { border: 1px solid #888; border-radius: 2px; padding: 8px 8px 10px; }
20
+ .row { display: grid; grid-gap: 0; }
21
+ .muted { color:#444; }
22
+ .small { font-size: 11px; }
23
+ .right { text-align:right; }
24
+ .bold { font-weight:600; }
25
+ .mb8 { margin-bottom: 8px; }
26
+ .mb4 { margin-bottom: 4px; }
27
+ .mt4 { margin-top: 4px; }
28
+
29
+ /* ===== Header block (logo + seller) ===== */
30
+ .hdr {
31
+ grid-template-columns: 1fr 1.2fr;
32
+ align-items: start;
33
+ }
34
+ .logo-wrap { display:flex; align-items:flex-start; gap:8px; }
35
+ .logo { height: 100px; border: none; }
36
+ .tagline { font-size: 12px; margin-top: 6px; }
37
+ .seller h3 { margin: 0 0 4px; font-size: 14px; }
38
+ .seller p { margin: 0; line-height: 1.25; font-size: 11.5px; }
39
+
40
+ .statbar { background:#e9edf3; border:1px solid #bfc7d1; border-left:0; border-right:0; padding:4px 6px; font-size: 11.5px; }
41
+
42
+ /* ===== Meta (Invoice No / Date) ===== */
43
+ .meta { grid-template-columns: 140px 1fr 140px 1fr; border:1px solid #999; border-top:0; }
44
+ .meta > div { padding:4px 6px; border-right:1px solid #999; }
45
+ .meta > div:nth-child(4n) { border-right:0; }
46
+ .lbl { background:#f5f5f5; font-weight:600; }
47
+
48
+ /* ===== Customer details ===== */
49
+ .cust-hdr { border:1px solid #999; border-top:0; padding:4px 6px; font-weight:600; }
50
+ .cust { display:grid; grid-template-columns: 1fr 140px 1fr; border:1px solid #999; border-top:0; }
51
+ .cust > div { padding:6px; }
52
+ .cust .grid { display:grid; grid-template-columns: 70px 1fr; }
53
+ .cust .grid > div { padding:2px 0; border-bottom:1px solid #eee; }
54
+ .cust .grid > div:nth-child(2n) { border-left:1px solid #eee; padding-left:8px; }
55
+
56
+ /* ===== Items table ===== */
57
+ table.items { width:100%; border-collapse:collapse; margin-top: 6px; }
58
+ table.items th, table.items td { border:1px solid #999; padding:6px 6px; font-size: 12px; vertical-align: top; }
59
+ table.items th { background:#f5f5f5; font-weight:600; }
60
+ table.items tfoot td { border:1px solid #999; padding:6px 6px; font-size: 12px; }
61
+ table.items tfoot td.label { background:#f5f5f5; font-weight:600; text-align:center; }
62
+ .num { text-align:right; white-space:nowrap; }
63
+ .pct { text-align:center; }
64
+ .w-sr { width:40px; }
65
+ .w-desc { width:auto; }
66
+ .w-hsn { width:90px; }
67
+ .w-taxval { width:95px; }
68
+ .w-pct { width:45px; }
69
+ .w-amt { width:95px; }
70
+ .w-total { width:110px; }
71
+
72
+ /* ===== Totals ===== */
73
+ .totals { width:100%; border-collapse:collapse; margin-top: 6px; }
74
+ .totals td { border:1px solid #999; padding:6px; font-size:12px; }
75
+ .totals td.label { background:#f5f5f5; font-weight:600; text-align:center; }
76
+
77
+ /* ===== Words & bank ===== */
78
+ .words, .bank { border:1px solid #999; border-top:0; padding:6px; font-size:12px; }
79
+ .bank { display:grid; grid-template-columns: 1fr 200px; }
80
+ .bank .rightbox { border-left:1px solid #999; padding-left:6px; display:flex; flex-direction:column; justify-content:space-between; }
81
+ .bank .sign { text-align:right; padding-top:40px; }
82
+
83
+ /* ===== Print tweaks ===== */
84
+ @media print { .invoice-wrap { width:auto; } }
85
+ </style>
86
+ </head>
87
+ <body>
88
+ <div class="invoice-wrap">
89
+ <h1 class="title">TAX INVOICE</h1>
90
+
91
+ <div class="frame">
92
+ <!-- Header: logo left, seller right -->
93
+ <div class="row hdr mb8">
94
+ <div>
95
+ <div class="logo-wrap">
96
+ <img class="logo" src="assets/srestham.png" alt="Srestham logo" />
97
+ </div>
98
+ </div>
99
+ <div class="seller">
100
+ <h3>Srestham</h3>
101
+ <p>(a unit of A1 Future Technologies Pvt. Ltd.)</p>
102
+ <p>64/89, Khudiram Bose Sarani, Dutta Bagan, Milk Colony, Kolkata - 700037, India</p>
103
+ <p>Phone : +91-9007797979, +91-9831066996</p>
104
+ <p>Website : <span>www.srestham.com</span> &nbsp; E‑Mail : <span>info@srestham.com</span></p>
105
+ <p>State : West Bengal, Code: 19</p>
106
+ </div>
107
+ </div>
108
+
109
+ <div class="statbar small">
110
+ GSTIN: 19AAKCA7063N1Z1 &nbsp; | &nbsp; PAN : AAKCA7063N &nbsp; | &nbsp; CIN : U72200WB2012PTC183279
111
+ </div>
112
+
113
+ <!-- Invoice meta -->
114
+ <div class="row meta">
115
+ <div class="lbl">Invoice No.</div>
116
+ <div data-key="invoice_no"></div>
117
+ <div class="lbl">Date</div>
118
+ <div data-key="invoice_date"></div>
119
+ </div>
120
+
121
+ <!-- Customer -->
122
+ <div class="cust-hdr">Customer Details</div>
123
+ <div class="cust">
124
+ <div>
125
+ <div class="bold" data-key="customer_name"></div>
126
+ <div data-key="customer_address_1"></div>
127
+ <div data-key="customer_address_2"></div>
128
+ <div data-key="customer_address_3"></div>
129
+ </div>
130
+ <div></div>
131
+ <div class="grid small">
132
+ <div class="bold right">GSTIN :</div><div data-key="buyer_gstin"></div>
133
+ <div class="bold right">PAN :</div><div data-key="buyer_pan"></div>
134
+ <div class="bold right">CIN :</div><div data-key="buyer_cin"></div>
135
+ <div class="bold right">PO No. :</div><div data-key="po_no"></div>
136
+ <div class="bold right">State :</div><div data-key="buyer_state"></div>
137
+ </div>
138
+ </div>
139
+
140
+ <!-- Items table -->
141
+ <table class="items">
142
+ <thead>
143
+ <tr>
144
+ <th class="w-sr">Sr.<br/>No.</th>
145
+ <th class="w-desc">Service Details</th>
146
+ <th class="w-hsn">HSN</th>
147
+ <th class="w-taxval">Taxable<br/>Value</th>
148
+ <th class="w-pct">CGST<br/>%</th>
149
+ <th class="w-amt">CGST<br/>Amt</th>
150
+ <th class="w-pct">SGST/UGST<br/>%</th>
151
+ <th class="w-amt">SGST/UGST<br/>Amt</th>
152
+ <th class="w-pct">IGST<br/>%</th>
153
+ <th class="w-amt">IGST<br/>Amt</th>
154
+ <th class="w-total">Total</th>
155
+ </tr>
156
+ </thead>
157
+ <tbody data-key="items_tbody">
158
+ <!-- rows will be injected -->
159
+ </tbody>
160
+ <tfoot>
161
+ <tr>
162
+ <td colspan="3" class="label">TOTAL</td>
163
+ <td class="num" data-key="total_taxable"></td>
164
+ <td></td>
165
+ <td class="num" data-key="total_cgst"></td>
166
+ <td></td>
167
+ <td class="num" data-key="total_sgst"></td>
168
+ <td></td>
169
+ <td class="num" data-key="total_igst"></td>
170
+ <td class="num" data-key="grand_total"></td>
171
+ </tr>
172
+ </tfoot>
173
+ </table>
174
+
175
+ <!-- Totals strip moved into items table as tfoot for perfect alignment -->
176
+
177
+ <table class="totals">
178
+ <tr>
179
+ <td class="label" colspan="9" style="text-align:right">Rounding Off</td>
180
+ <td class="num" data-key="rounding"></td>
181
+ </tr>
182
+ <tr>
183
+ <td class="label" colspan="9" style="text-align:right">Net Amount</td>
184
+ <td class="num" data-key="net_amount"></td>
185
+ </tr>
186
+ </table>
187
+
188
+ <div class="words">
189
+ <div><span class="bold">Total Invoice Value (In Words):</span> <span data-key="words_total"></span></div>
190
+ <div><span class="bold">Total CGST Value (In Words):</span> <span data-key="words_cgst"></span></div>
191
+ <div><span class="bold">Total SGST Value (In Words):</span> <span data-key="words_sgst"></span></div>
192
+ <div><span class="bold">Total IGST Value (In Words):</span> <span data-key="words_igst"></span></div>
193
+ </div>
194
+
195
+ <div class="bank">
196
+ <div>
197
+ <div class="bold">Bank Details</div>
198
+ <div>Account Name: A1 Future Technologies Private Limited</div>
199
+ <div>Bank Name: HDFC Bank Ltd. - Bank Branch: Shyam Bazar</div>
200
+ <div>Account No.: 01742320002223 - Swift Code : HDFCINBBCAL </div>
201
+ <div>RTGS / NEFT IFSC : HDFC0000174 - MICR Code: 700240011</div>
202
+ </div>
203
+ <div class="rightbox">
204
+ <div class="right">E. &amp; O. E.</div>
205
+ <div class="sign">For Srestham</div>
206
+ </div>
207
+ </div>
208
+ </div>
209
+ </div>
210
+
211
+ <script>
212
+ // --- Minimal injection helpers (optional) ---
213
+ function fmtMoney(n){
214
+ if(n === null || n === undefined || isNaN(n)) return '';
215
+ return Number(n).toLocaleString('en-IN', {minimumFractionDigits:2, maximumFractionDigits:2});
216
+ }
217
+
218
+ function inject(data){
219
+ // Simple fields
220
+ const map = {
221
+ invoice_no: 'INV/00001/25-26',
222
+ invoice_date: 'April 1, 2025',
223
+ customer_name: 'Binay Sharma',
224
+ customer_address_1: "H. No. 8-54/22, Happy's Home",
225
+ customer_address_2: 'Sai Harsha Nagar Colony, Hydershakote,',
226
+ customer_address_3: '500091 Hyderabad TG',
227
+ buyer_gstin: '', buyer_pan: '', buyer_cin: '',
228
+ po_no: '', buyer_state: 'Telangana, Code: 36',
229
+ total_taxable: fmtMoney(9150), total_cgst: fmtMoney(0), total_sgst: fmtMoney(0), total_igst: fmtMoney(1648),
230
+ grand_total: fmtMoney(10798), rounding: fmtMoney(0), net_amount: fmtMoney(10798),
231
+ words_total: 'Rupees Ten Thousand Seven Hundred Ninety Eight Only',
232
+ words_cgst: 'No Rupees Only', words_sgst: 'No Rupees Only',
233
+ words_igst: 'Rupees One Thousand Six Hundred Forty Eight Only'
234
+ };
235
+ Object.assign(map, data||{});
236
+ document.querySelectorAll('[data-key]').forEach(el=>{
237
+ const k = el.getAttribute('data-key');
238
+ if(k === 'items_tbody') return;
239
+ if(map[k] !== undefined) el.textContent = map[k];
240
+ });
241
+
242
+ // Items
243
+ const rows = (data && data.items) || [
244
+ { sr:1, desc:'Inephos Framed Canvas Painting - Seven Running White Horses 47 X 29', hsn:'491199', tax:4575, cgst_p:0, cgst_a:0, sgst_p:0, sgst_a:0, igst_p:18, igst_a:824, total:5399 },
245
+ { sr:2, desc:'Inephos Framed Canvas Painting - Divine Radha Krishna 47 X 29', hsn:'491199', tax:4575, cgst_p:0, cgst_a:0, sgst_p:0, sgst_a:0, igst_p:18, igst_a:824, total:5399 },
246
+ ];
247
+ const tbody = document.querySelector('[data-key="items_tbody"]');
248
+ tbody.innerHTML = rows.map(r => `
249
+ <tr>
250
+ <td class="w-sr num">${r.sr}</td>
251
+ <td class="w-desc">${r.desc || ''}</td>
252
+ <td class="w-hsn">${r.hsn || ''}</td>
253
+ <td class="w-taxval num">${fmtMoney(r.tax)}</td>
254
+ <td class="w-pct pct">${r.cgst_p||0}</td>
255
+ <td class="w-amt num">${fmtMoney(r.cgst_a||0)}</td>
256
+ <td class="w-pct pct">${r.sgst_p||0}</td>
257
+ <td class="w-amt num">${fmtMoney(r.sgst_a||0)}</td>
258
+ <td class="w-pct pct">${r.igst_p||0}</td>
259
+ <td class="w-amt num">${fmtMoney(r.igst_a||0)}</td>
260
+ <td class="w-total num">${fmtMoney(r.total||0)}</td>
261
+ </tr>`).join('');
262
+ }
263
+
264
+ // Example: inject with defaults
265
+ inject();
266
+ </script>
267
+ </body>
268
+ </html>