Spaces:
Sleeping
Sleeping
Upload 6 files
Browse files- app.py +609 -0
- assets/inephos.png +0 -0
- assets/srestham.png +0 -0
- inephos.html +268 -0
- requirements.txt +6 -0
- 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> 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 | PAN : AAKCA7063N | 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. & 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> 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 | PAN : AAKCA7063N | 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. & 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>
|