Spaces:
Sleeping
Sleeping
| # HTML-based Invoice Generator | |
| import io | |
| import os | |
| import zipfile | |
| import tempfile | |
| import shutil | |
| from typing import Dict, List | |
| import base64 | |
| import subprocess | |
| import sys | |
| import pandas as pd | |
| import streamlit as st | |
| from num2words import num2words | |
| # Try to import Playwright for PDF conversion | |
| PLAYWRIGHT_AVAILABLE = False | |
| try: | |
| from playwright.sync_api import sync_playwright | |
| PLAYWRIGHT_AVAILABLE = True | |
| except ImportError: | |
| pass | |
| # Install Playwright browsers at runtime (disabled by default for hosted envs) | |
| _playwright_flag = os.path.join(tempfile.gettempdir(), 'playwright_installed') | |
| _allow_runtime_install = os.environ.get('PLAYWRIGHT_RUNTIME_INSTALL') == '1' | |
| if PLAYWRIGHT_AVAILABLE and _allow_runtime_install and not os.path.exists(_playwright_flag): | |
| try: | |
| cmd = [sys.executable, "-m", "playwright", "install", "chromium"] | |
| subprocess.run(cmd, check=True) | |
| with open(_playwright_flag, 'w') as f: | |
| f.write('installed') | |
| print("β Playwright browsers installed!") | |
| except Exception as e: | |
| print(f"β οΈ Failed to install Playwright browsers: {e}") | |
| else: | |
| if not _allow_runtime_install: | |
| print("βΉοΈ Skipping runtime Playwright install (build-time install expected).") | |
| # Try to import PyPDF for PDF page extraction | |
| PYPDF_AVAILABLE = False | |
| try: | |
| from pypdf import PdfReader, PdfWriter | |
| PYPDF_AVAILABLE = True | |
| except ImportError: | |
| pass | |
| # ------------------------- | |
| # Utility helpers | |
| # ------------------------- | |
| def safe_str(x): | |
| if pd.isna(x): | |
| return "" | |
| return str(x) | |
| def format_amount(x): | |
| """Format amount as Indian currency format""" | |
| try: | |
| num = float(x) | |
| if num == 0: | |
| return "0.00" | |
| return f"{num:,.2f}" | |
| except Exception: | |
| return "0.00" | |
| def to_words(value): | |
| """Convert number to words""" | |
| try: | |
| num = float(value) | |
| if num == 0: | |
| return "Zero Rupees Only" | |
| if num.is_integer(): | |
| num = int(num) | |
| return num2words(num, to="cardinal", lang="en").title() + " Only" | |
| except Exception: | |
| return "Zero Rupees Only" | |
| # ------------------------- | |
| # HTML Template Processing | |
| # ------------------------- | |
| def fill_html_template(template_path: str, invoice_data: dict, output_path: str): | |
| """Fill HTML template with invoice data and save""" | |
| # Read the HTML template | |
| with open(template_path, 'r', encoding='utf-8') as f: | |
| html_content = f.read() | |
| # Prepare data for injection | |
| data = { | |
| 'invoice_no': invoice_data.get('Invoice No', ''), | |
| 'invoice_date': invoice_data.get('Invoice Date', ''), | |
| 'customer_name': invoice_data.get('Customer Name', ''), | |
| 'buyer_gstin': invoice_data.get('Customer GSTIN', ''), | |
| 'buyer_pan': invoice_data.get('Customer PAN', ''), | |
| 'buyer_cin': invoice_data.get('Customer CIN', ''), | |
| 'po_no': invoice_data.get('PO No', ''), | |
| 'buyer_state': invoice_data.get('Customer State', ''), | |
| } | |
| # Handle customer address (split into multiple lines) | |
| address = invoice_data.get('Customer Address', '') | |
| if address: | |
| address_parts = [p.strip() for p in address.split(',')] | |
| data['customer_address_1'] = address_parts[0] if len(address_parts) > 0 else '' | |
| data['customer_address_2'] = address_parts[1] if len(address_parts) > 1 else '' | |
| data['customer_address_3'] = ', '.join(address_parts[2:]) if len(address_parts) > 2 else '' | |
| else: | |
| data['customer_address_1'] = data['customer_address_2'] = data['customer_address_3'] = '' | |
| # Calculate totals dynamically | |
| items = invoice_data.get('Items', []) | |
| total_taxable = 0 | |
| total_cgst = 0 | |
| total_sgst = 0 | |
| total_igst = 0 | |
| for item in items: | |
| taxable_value = float(item.get('Taxable Value', 0)) | |
| cgst_percentage = float(item.get('CGST %', 0)) | |
| sgst_percentage = float(item.get('SGST %', 0)) | |
| igst_percentage = float(item.get('IGST %', 0)) | |
| # Calculate tax amounts dynamically | |
| cgst_amount = round((taxable_value * cgst_percentage / 100), 2) | |
| sgst_amount = round((taxable_value * sgst_percentage / 100), 2) | |
| igst_amount = round((taxable_value * igst_percentage / 100), 2) | |
| # Add to totals | |
| total_taxable += taxable_value | |
| total_cgst += cgst_amount | |
| total_sgst += sgst_amount | |
| total_igst += igst_amount | |
| # Calculate grand total | |
| grand_total = total_taxable + total_cgst + total_sgst + total_igst | |
| # Add totals to data | |
| data.update({ | |
| 'total_taxable': format_amount(total_taxable), | |
| 'total_cgst': format_amount(total_cgst), | |
| 'total_sgst': format_amount(total_sgst), | |
| 'total_igst': format_amount(total_igst), | |
| 'grand_total': format_amount(grand_total), | |
| 'rounding': format_amount(0), # Can be calculated if needed | |
| 'net_amount': format_amount(grand_total), | |
| 'words_total': to_words(grand_total), | |
| 'words_cgst': to_words(total_cgst), | |
| 'words_sgst': to_words(total_sgst), | |
| 'words_igst': to_words(total_igst), | |
| }) | |
| # Prepare items data for JavaScript injection | |
| items_js = [] | |
| for i, item in enumerate(items): | |
| taxable_value = float(item.get('Taxable Value', 0)) | |
| cgst_percentage = float(item.get('CGST %', 0)) | |
| sgst_percentage = float(item.get('SGST %', 0)) | |
| igst_percentage = float(item.get('IGST %', 0)) | |
| # Calculate tax amounts dynamically from percentages | |
| cgst_amount = round((taxable_value * cgst_percentage / 100), 2) | |
| sgst_amount = round((taxable_value * sgst_percentage / 100), 2) | |
| igst_amount = round((taxable_value * igst_percentage / 100), 2) | |
| # Calculate total amount | |
| total_amount = taxable_value + cgst_amount + sgst_amount + igst_amount | |
| items_js.append({ | |
| 'sr': i + 1, | |
| 'desc': item.get('Service Details', ''), | |
| 'hsn': item.get('HSN', ''), | |
| 'tax': taxable_value, | |
| 'cgst_p': cgst_percentage, | |
| 'cgst_a': cgst_amount, # Dynamically calculated | |
| 'sgst_p': sgst_percentage, | |
| 'sgst_a': sgst_amount, # Dynamically calculated | |
| 'igst_p': igst_percentage, | |
| 'igst_a': igst_amount, # Dynamically calculated | |
| 'total': round(total_amount, 2), # Dynamically calculated | |
| }) | |
| # Create JavaScript injection code | |
| js_injection = f""" | |
| <script> | |
| // Data injection | |
| const invoiceData = {data}; | |
| const itemsData = {items_js}; | |
| // Inject data into template | |
| inject({{ ...invoiceData, items: itemsData }}); | |
| </script> | |
| """ | |
| # Insert the injection script before the closing body tag | |
| html_content = html_content.replace('</body>', f'{js_injection}\n</body>') | |
| # Handle logo images (convert to base64 if exists) | |
| # Check for both company logos | |
| logo_paths = ['assets/inephos.png', 'assets/srestham.png'] | |
| for logo_path in logo_paths: | |
| if os.path.exists(logo_path): | |
| with open(logo_path, 'rb') as img_file: | |
| img_b64 = base64.b64encode(img_file.read()).decode() | |
| img_src = f"data:image/png;base64,{img_b64}" | |
| html_content = html_content.replace(f'src="{logo_path}"', f'src="{img_src}"') | |
| # Also handle any remaining generic image references | |
| if os.path.exists('image.png'): | |
| with open('image.png', 'rb') as img_file: | |
| img_b64 = base64.b64encode(img_file.read()).decode() | |
| img_src = f"data:image/png;base64,{img_b64}" | |
| html_content = html_content.replace('src="image.png"', f'src="{img_src}"') | |
| # Write the filled HTML | |
| with open(output_path, 'w', encoding='utf-8') as f: | |
| f.write(html_content) | |
| print(f"β Generated HTML invoice: {output_path}") | |
| return output_path | |
| def html_to_pdf_playwright(html_path: str, pdf_path: str) -> bool: | |
| """Convert HTML to PDF using Playwright with Windows compatibility""" | |
| import asyncio | |
| import sys | |
| import traceback | |
| async def create_pdf(): | |
| from playwright.async_api import async_playwright | |
| async with async_playwright() as p: | |
| browser = await p.chromium.launch(headless=True) | |
| page = await browser.new_page() | |
| # Load the HTML file | |
| file_url = f"file:///{os.path.abspath(html_path).replace(os.sep, '/')}" | |
| await page.goto(file_url, wait_until='networkidle') | |
| # Generate PDF with proper settings | |
| await page.pdf( | |
| path=pdf_path, | |
| format='A4', | |
| margin={ | |
| 'top': '16mm', | |
| 'bottom': '16mm', | |
| 'left': '10mm', | |
| 'right': '10mm' | |
| }, | |
| print_background=True, | |
| prefer_css_page_size=True | |
| ) | |
| await browser.close() | |
| def _try_generate() -> bool: | |
| try: | |
| # Set event loop policy for Windows compatibility | |
| if sys.platform == "win32": | |
| asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) | |
| asyncio.run(create_pdf()) | |
| return True | |
| except Exception as ex: | |
| raise ex | |
| def _install_playwright_browser_once() -> bool: | |
| flag = os.path.join(tempfile.gettempdir(), 'playwright_browser_installed') | |
| if os.path.exists(flag): | |
| return True | |
| try: | |
| subprocess.run([sys.executable, "-m", "playwright", "install", "chromium"], check=True) | |
| with open(flag, 'w') as f: | |
| f.write('installed') | |
| print("β Playwright Chromium downloaded at runtime.") | |
| return True | |
| except Exception as ex: | |
| print(f"β Runtime Playwright browser install failed: {ex}") | |
| return False | |
| try: | |
| # Set event loop policy for Windows compatibility | |
| if sys.platform == "win32": | |
| asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) | |
| # First attempt | |
| if _try_generate(): | |
| return True | |
| except Exception as e: | |
| print(f"Playwright conversion failed: {e}") | |
| err_text = f"{e}\n{traceback.format_exc()}" | |
| # If browsers are not downloaded, try to fetch them once and retry | |
| if "Executable doesn't exist" in err_text or "playwright install" in err_text: | |
| if _install_playwright_browser_once(): | |
| try: | |
| if _try_generate(): | |
| return True | |
| except Exception as e2: | |
| print(f"Retry after browser install failed: {e2}") | |
| # Additional debugging for Windows issues | |
| if "NotImplementedError" in str(e): | |
| print("Windows asyncio compatibility issue detected. Trying alternative approach...") | |
| try: | |
| # Alternative: Use subprocess to call playwright directly | |
| import subprocess | |
| import json | |
| # Create a simple script to run playwright | |
| script_content = f''' | |
| import asyncio | |
| import sys | |
| from playwright.async_api import async_playwright | |
| async def main(): | |
| async with async_playwright() as p: | |
| browser = await p.chromium.launch(headless=True) | |
| page = await browser.new_page() | |
| await page.goto("file:///{os.path.abspath(html_path).replace(os.sep, '/')}", wait_until='networkidle') | |
| await page.pdf( | |
| path="{pdf_path.replace(os.sep, '/')}", | |
| format='A4', | |
| margin={{"top": "16mm", "bottom": "16mm", "left": "10mm", "right": "10mm"}}, | |
| print_background=True, | |
| prefer_css_page_size=True | |
| ) | |
| await browser.close() | |
| if __name__ == "__main__": | |
| asyncio.run(main()) | |
| ''' | |
| # Write temporary script | |
| script_path = os.path.join(os.path.dirname(pdf_path), 'temp_pdf_script.py') | |
| with open(script_path, 'w', encoding='utf-8') as f: | |
| f.write(script_content) | |
| # Run script in subprocess | |
| result = subprocess.run([sys.executable, script_path], | |
| capture_output=True, text=True, timeout=30) | |
| # Clean up | |
| if os.path.exists(script_path): | |
| os.remove(script_path) | |
| if result.returncode == 0 and os.path.exists(pdf_path): | |
| return True | |
| else: | |
| print(f"Subprocess approach failed: {result.stderr}") | |
| except Exception as e2: | |
| print(f"Alternative approach also failed: {e2}") | |
| return False | |
| def html_to_pdf(html_path: str, pdf_path: str) -> bool: | |
| """Convert HTML to PDF using Playwright (mandatory)""" | |
| if PLAYWRIGHT_AVAILABLE: | |
| return html_to_pdf_playwright(html_path, pdf_path) | |
| else: | |
| print("β Playwright not available. PDF generation is mandatory!") | |
| print("Please install: pip install playwright && playwright install chromium") | |
| return False | |
| def extract_first_page_pdf(input_pdf_path: str, output_pdf_path: str) -> bool: | |
| """Extract only the first page from a PDF file""" | |
| if not PYPDF_AVAILABLE: | |
| print("β οΈ PyPDF not available. Keeping original PDF.") | |
| # Copy the original file if PyPDF is not available | |
| try: | |
| shutil.copy2(input_pdf_path, output_pdf_path) | |
| return True | |
| except Exception as e: | |
| print(f"β Failed to copy PDF: {e}") | |
| return False | |
| try: | |
| # Read the input PDF | |
| reader = PdfReader(input_pdf_path) | |
| # Create a new PDF writer | |
| writer = PdfWriter() | |
| # Add only the first page (if it exists) | |
| if len(reader.pages) > 0: | |
| writer.add_page(reader.pages[0]) | |
| # Write the first page to the output file | |
| with open(output_pdf_path, 'wb') as output_file: | |
| writer.write(output_file) | |
| print(f"β Extracted first page: {output_pdf_path}") | |
| return True | |
| else: | |
| print(f"β οΈ No pages found in PDF: {input_pdf_path}") | |
| return False | |
| except Exception as e: | |
| print(f"β Failed to extract first page from {input_pdf_path}: {e}") | |
| # Fallback: copy the original file | |
| try: | |
| shutil.copy2(input_pdf_path, output_pdf_path) | |
| return True | |
| except Exception as e2: | |
| print(f"β Failed to copy PDF as fallback: {e2}") | |
| return False | |
| def process_invoices_html(data_file, template_path: str, company_name: str): | |
| """Process invoices using HTML template approach""" | |
| # Read customer data | |
| df = pd.read_excel(data_file, engine="openpyxl") | |
| df.columns = [str(c).strip().lower() for c in df.columns] | |
| # Create temporary directory for processing | |
| with tempfile.TemporaryDirectory() as temp_dir: | |
| invoices = [] | |
| html_files = [] | |
| pdf_files = [] | |
| # Process each row as a separate invoice | |
| for i, r in df.iterrows(): | |
| # Create invoice data structure | |
| invoice_data = { | |
| 'Invoice No': safe_str(r.get("invoice no", f"INV/{i+1}")), | |
| 'Invoice Date': safe_str(r.get("invoice date", "")), | |
| 'Customer Name': safe_str(r.get("customer name", "")), | |
| 'Customer Address': safe_str(r.get("customer address", "")), | |
| 'Customer GSTIN': safe_str(r.get("customer_gstin", "")), | |
| 'Customer PAN': safe_str(r.get("customer_pan", "")), | |
| 'Customer CIN': safe_str(r.get("customer_cin", "")), | |
| 'Customer State': safe_str(r.get("customer_state", "")), | |
| 'PO No': safe_str(r.get("po_no", "")), | |
| 'Total Amount': r.get("total amount", 0) or 0, | |
| 'Items': [{ | |
| 'Service Details': safe_str(r.get("service details", "")), | |
| 'HSN': safe_str(r.get("hsn", "")), | |
| 'Taxable Value': r.get("taxable value", 0) or 0, | |
| 'CGST %': r.get("cgst %", 0) or 0, | |
| 'SGST %': r.get("sgst %", 0) or 0, | |
| 'IGST %': r.get("igst %", 0) or 0, | |
| # Tax amounts and total will be calculated dynamically | |
| }] | |
| } | |
| # Generate HTML file | |
| html_filename = f"{invoice_data['Invoice No']}_{company_name}.html" | |
| html_path = os.path.join(temp_dir, html_filename) | |
| # Fill template | |
| fill_html_template(template_path, invoice_data, html_path) | |
| html_files.append((html_filename, html_path)) | |
| # Convert to PDF (mandatory with Playwright) | |
| pdf_filename = f"{invoice_data['Invoice No']}_{company_name}.pdf" | |
| pdf_path_temp = os.path.join(temp_dir, f"temp_{pdf_filename}") | |
| pdf_path_final = os.path.join(temp_dir, pdf_filename) | |
| if html_to_pdf(html_path, pdf_path_temp): | |
| # Extract only first page from the generated PDF | |
| if extract_first_page_pdf(pdf_path_temp, pdf_path_final): | |
| pdf_files.append((pdf_filename, pdf_path_final)) | |
| # Clean up temporary PDF | |
| if os.path.exists(pdf_path_temp): | |
| os.remove(pdf_path_temp) | |
| else: | |
| st.error(f"β Failed to extract first page for {invoice_data['Invoice No']}") | |
| print(f"β First page extraction failed for {invoice_data['Invoice No']}") | |
| else: | |
| st.error(f"β Failed to generate PDF for {invoice_data['Invoice No']}") | |
| print(f"β PDF generation failed for {invoice_data['Invoice No']}") | |
| invoices.append(invoice_data) | |
| # Create ZIP file with results | |
| zip_buffer = io.BytesIO() | |
| with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf: | |
| # Add HTML files | |
| for filename, filepath in html_files: | |
| zf.write(filepath, filename) | |
| # Add PDF files if available | |
| for filename, filepath in pdf_files: | |
| if os.path.exists(filepath): | |
| zf.write(filepath, filename) | |
| zip_buffer.seek(0) | |
| return zip_buffer, len(invoices) | |
| # ------------------------- | |
| # Streamlit UI | |
| # ------------------------- | |
| st.set_page_config(page_title="HTML-Based Invoice Generator", layout="wide") | |
| st.title("π HTML-Based Invoice Generator β Inephos & Srestham") | |
| st.markdown(""" | |
| Upload your customer data Excel file. The system will: | |
| 1. π Generate individual HTML invoices using the template | |
| 2. π¨ Apply beautiful styling and formatting | |
| 3. π Convert to PDF (if weasyprint/playwright is available) | |
| 4. π¦ Package everything in a downloadable ZIP | |
| **Benefits of HTML approach:** | |
| - β¨ Perfect visual control and styling | |
| - β‘ Faster processing than Excel manipulation | |
| - π― Production-ready professional output | |
| - π§ Easy to customize and maintain | |
| """) | |
| # Upload section | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.subheader("π Customer Data") | |
| data_file = st.file_uploader("Upload customer Excel (.xlsx/.xlsm)", type=["xlsx", "xlsm"]) | |
| if data_file: | |
| st.success("β Customer data uploaded") | |
| # Preview data | |
| try: | |
| preview_df = pd.read_excel(data_file, engine="openpyxl").head(3) | |
| st.write("**Data Preview:**") | |
| st.dataframe(preview_df) | |
| except Exception as e: | |
| st.error(f"Error reading file: {e}") | |
| with col2: | |
| st.subheader("π¨ Template Selection") | |
| # Check available templates | |
| inephos_available = os.path.exists('inephos.html') | |
| srestham_available = os.path.exists('srestham.html') | |
| if inephos_available and srestham_available: | |
| st.success("β Both templates found") | |
| # Radio button for template selection | |
| selected_template = st.radio( | |
| "Select Company Template:", | |
| options=["Inephos", "Srestham"], | |
| horizontal=True, | |
| help="Choose which company's invoice template to use" | |
| ) | |
| template_file = "inephos.html" if selected_template == "Inephos" else "srestham.html" | |
| st.info(f"Using: {template_file}") | |
| elif inephos_available: | |
| st.warning("β οΈ Only Inephos template found") | |
| selected_template = "Inephos" | |
| template_file = "inephos.html" | |
| elif srestham_available: | |
| st.warning("β οΈ Only Srestham template found") | |
| selected_template = "Srestham" | |
| template_file = "srestham.html" | |
| else: | |
| st.error("β No templates found") | |
| st.write("Make sure inephos.html and/or srestham.html are in the same directory") | |
| selected_template = None | |
| template_file = None | |
| # Processing section | |
| if template_file: | |
| st.markdown("---") | |
| st.subheader("π Generate Invoices") | |
| col3, col4 = st.columns(2) | |
| with col3: | |
| # Enable Inephos button only when Inephos is selected | |
| inephos_enabled = (selected_template == "Inephos") | |
| if st.button("π’ Generate Inephos Invoices", | |
| use_container_width=True, | |
| disabled=not inephos_enabled, | |
| help="Select Inephos template to enable this button"): | |
| if not data_file: | |
| st.error("Please upload customer data file.") | |
| else: | |
| with st.spinner("Generating Inephos invoices..."): | |
| try: | |
| zip_buffer, count = process_invoices_html(data_file, "inephos.html", "inephos") | |
| st.success(f"β Generated {count} Inephos invoices successfully!") | |
| file_types = "HTML files and PDFs" | |
| st.download_button( | |
| label=f"π₯ Download ZIP ({file_types})", | |
| data=zip_buffer, | |
| file_name="inephos_invoices.zip", | |
| mime="application/zip" | |
| ) | |
| except Exception as e: | |
| st.error(f"Error generating invoices: {e}") | |
| st.exception(e) | |
| with col4: | |
| # Enable Srestham button only when Srestham is selected | |
| srestham_enabled = (selected_template == "Srestham") | |
| if st.button("π’ Generate Srestham Invoices", | |
| use_container_width=True, | |
| disabled=not srestham_enabled, | |
| help="Select Srestham template to enable this button"): | |
| if not data_file: | |
| st.error("Please upload customer data file.") | |
| else: | |
| with st.spinner("Generating Srestham invoices..."): | |
| try: | |
| zip_buffer, count = process_invoices_html(data_file, "srestham.html", "srestham") | |
| st.success(f"β Generated {count} Srestham invoices successfully!") | |
| file_types = "HTML files and PDFs" | |
| st.download_button( | |
| label=f"π₯ Download ZIP ({file_types})", | |
| data=zip_buffer, | |
| file_name="srestham_invoices.zip", | |
| mime="application/zip" | |
| ) | |
| except Exception as e: | |
| st.error(f"Error generating invoices: {e}") | |
| st.exception(e) | |
| # Information section | |
| st.markdown("---") | |
| st.subheader("βΉοΈ Setup & Dependencies") | |
| col5, col6 = st.columns(2) | |
| with col5: | |
| st.markdown(""" | |
| **Installation for PDF conversion:** | |
| ```bash | |
| # Playwright (mandatory for PDF generation) | |
| pip install playwright | |
| playwright install chromium | |
| # PyPDF (recommended for first-page extraction) | |
| pip install pypdf | |
| ``` | |
| """) | |
| with col6: | |
| st.markdown(f""" | |
| **Current Status:** | |
| - Playwright: {'β Available' if PLAYWRIGHT_AVAILABLE else 'β Not installed (REQUIRED)'} | |
| - PyPDF: {'β Available' if PYPDF_AVAILABLE else 'β οΈ Not installed (recommended)'} | |
| - Inephos Template: {'β Found' if os.path.exists('inephos.html') else 'β Missing'} | |
| - Srestham Template: {'β Found' if os.path.exists('srestham.html') else 'β Missing'} | |
| **Output:** HTML invoices + First-page PDFs (mandatory) | |
| """) | |
| if not PLAYWRIGHT_AVAILABLE: | |
| st.error("π¨ **Playwright is required for PDF generation! Install it to continue.**") | |
| st.code("pip install playwright && playwright install chromium") | |