invoicing app 1

# Complete Flask Invoicing Web Application # Based on comprehensive market research for beginner-friendly design import os import re import smtplib from datetime import datetime, timedelta from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.mime.application import MIMEApplication from functools import wraps import secrets import logging from io import BytesIO import base64 from flask import Flask, render_template, request, redirect, url_for, flash, jsonify, session, send_file, abort from flask_sqlalchemy import SQLAlchemy from flask_bcrypt import Bcrypt from werkzeug.security import generate_password_hash, check_password_hash from reportlab.lib.pagesizes import letter, A4 from reportlab.pdfgen import canvas from reportlab.lib.units import inch from reportlab.lib import colors from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.lib.enums import TA_CENTER, TA_RIGHT, TA_LEFT # Initialize Flask app with security configurations app = Flask(__name__) app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', secrets.token_hex(32)) app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL', 'sqlite:///invoicing.db') app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # Email configuration for automated invoice sending app.config['MAIL_SERVER'] = os.environ.get('MAIL_SERVER', 'smtp.gmail.com') app.config['MAIL_PORT'] = int(os.environ.get('MAIL_PORT', 587)) app.config['MAIL_USE_TLS'] = os.environ.get('MAIL_USE_TLS', 'true').lower() in ['true', 'on', '1'] app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME') app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD') # Initialize extensions db = SQLAlchemy(app) bcrypt = Bcrypt(app) # Configure logging for better error tracking logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # Database Models with relationships for comprehensive invoicing class User(db.Model): """User authentication model with secure password hashing""" id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), unique=True, nullable=False) email = db.Column(db.String(120), unique=True, nullable=False) password_hash = db.Column(db.String(128), nullable=False) company_name = db.Column(db.String(200), nullable=True) company_address = db.Column(db.Text, nullable=True) created_at = db.Column(db.DateTime, default=datetime.utcnow) # Relationships invoices = db.relationship('Invoice', backref='user', lazy=True, cascade='all, delete-orphan') clients = db.relationship('Client', backref='user', lazy=True, cascade='all, delete-orphan') def set_password(self, password): """Secure password hashing using bcrypt""" self.password_hash = bcrypt.generate_password_hash(password).decode('utf-8') def check_password(self, password): """Verify password against stored hash""" return bcrypt.check_password_hash(self.password_hash, password) class Client(db.Model): """Client management model for CRM integration""" id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) name = db.Column(db.String(200), nullable=False) email = db.Column(db.String(120), nullable=False) phone = db.Column(db.String(20), nullable=True) address = db.Column(db.Text, nullable=True) company = db.Column(db.String(200), nullable=True) vat_number = db.Column(db.String(50), nullable=True) # EU compliance requirement created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) # Relationships invoices = db.relationship('Invoice', backref='client', lazy=True) class Invoice(db.Model): """Core invoice model with sequential numbering and status tracking""" id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) client_id = db.Column(db.Integer, db.ForeignKey('client.id'), nullable=False) # Sequential numbering for EU compliance invoice_number = db.Column(db.String(50), unique=True, nullable=False) # Invoice details issue_date = db.Column(db.Date, default=datetime.utcnow().date) due_date = db.Column(db.Date, nullable=False) # Financial calculations subtotal = db.Column(db.Numeric(10, 2), default=0.00) tax_rate = db.Column(db.Numeric(5, 2), default=0.00) tax_amount = db.Column(db.Numeric(10, 2), default=0.00) discount_rate = db.Column(db.Numeric(5, 2), default=0.00) discount_amount = db.Column(db.Numeric(10, 2), default=0.00) total_amount = db.Column(db.Numeric(10, 2), default=0.00) # Status tracking for payment management status = db.Column(db.String(20), default='draft') # draft, sent, paid, overdue payment_date = db.Column(db.Date, nullable=True) payment_method = db.Column(db.String(50), nullable=True) # Notes and terms notes = db.Column(db.Text, nullable=True) payment_terms = db.Column(db.String(200), default='Net 30') # Timestamps created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) sent_at = db.Column(db.DateTime, nullable=True) # Relationships items = db.relationship('InvoiceItem', backref='invoice', lazy=True, cascade='all, delete-orphan') class InvoiceItem(db.Model): """Invoice line items with flexible pricing structure""" id = db.Column(db.Integer, primary_key=True) invoice_id = db.Column(db.Integer, db.ForeignKey('invoice.id'), nullable=False) description = db.Column(db.String(500), nullable=False) quantity = db.Column(db.Numeric(10, 2), default=1.00) unit_price = db.Column(db.Numeric(10, 2), nullable=False) line_total = db.Column(db.Numeric(10, 2), nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow) # Authentication decorator for secure access control def login_required(f): @wraps(f) def decorated_function(*args, **kwargs): if 'user_id' not in session: flash('Please log in to access this page.', 'warning') return redirect(url_for('login')) return f(*args, **kwargs) return decorated_function # Utility Functions def generate_invoice_number(): """Generate sequential invoice number with year prefix (EU compliance)""" year = datetime.now().year last_invoice = Invoice.query.filter( Invoice.invoice_number.like(f'{year}-%') ).order_by(Invoice.invoice_number.desc()).first() if last_invoice: try: last_num = int(last_invoice.invoice_number.split('-')[1]) next_num = last_num + 1 except (IndexError, ValueError): next_num = 1 else: next_num = 1 return f"{year}-{next_num:04d}" def calculate_invoice_totals(items_data): """Calculate invoice totals with tax and discount logic""" subtotal = sum(float(item['quantity']) * float(item['unit_price']) for item in items_data) return subtotal def send_invoice_email(invoice, pdf_data): """Automated email sending with PDF attachment""" try: msg = MIMEMultipart() msg['From'] = app.config['MAIL_USERNAME'] msg['To'] = invoice.client.email msg['Subject'] = f'Invoice {invoice.invoice_number} from {invoice.user.company_name or invoice.user.username}' # Email body with professional template body = f""" Dear {invoice.client.name}, Please find attached your invoice #{invoice.invoice_number} for ${invoice.total_amount}. Invoice Details: - Issue Date: {invoice.issue_date.strftime('%B %d, %Y')} - Due Date: {invoice.due_date.strftime('%B %d, %Y')} - Amount: ${invoice.total_amount} - Payment Terms: {invoice.payment_terms} {invoice.notes or ''} Thank you for your business! Best regards, {invoice.user.company_name or invoice.user.username} """ msg.attach(MIMEText(body, 'plain')) # Attach PDF pdf_attachment = MIMEApplication(pdf_data, _subtype='pdf') pdf_attachment.add_header('Content-Disposition', 'attachment', filename=f'invoice_{invoice.invoice_number}.pdf') msg.attach(pdf_attachment) # Send email with smtplib.SMTP(app.config['MAIL_SERVER'], app.config['MAIL_PORT']) as server: server.starttls() server.login(app.config['MAIL_USERNAME'], app.config['MAIL_PASSWORD']) server.send_message(msg) return True except Exception as e: logger.error(f"Email sending failed: {str(e)}") return False def generate_invoice_pdf(invoice): """Generate professional PDF invoice with branding""" buffer = BytesIO() doc = SimpleDocTemplate(buffer, pagesize=A4) elements = [] styles = getSampleStyleSheet() # Custom styles for professional appearance title_style = ParagraphStyle( 'CustomTitle', parent=styles['Heading1'], fontSize=24, textColor=colors.HexColor('#2c3e50'), spaceAfter=30, alignment=TA_CENTER ) header_style = ParagraphStyle( 'CustomHeader', parent=styles['Normal'], fontSize=12, textColor=colors.HexColor('#34495e'), spaceAfter=12 ) # Invoice header elements.append(Paragraph("INVOICE", title_style)) elements.append(Spacer(1, 20)) # Company and client info table info_data = [ ['From:', 'To:'], [f"{invoice.user.company_name or invoice.user.username}", invoice.client.name], [invoice.user.company_address or 'Address not set', f"{invoice.client.company or ''}"], ['', invoice.client.address or ''], ['', f"Email: {invoice.client.email}"], ] info_table = Table(info_data, colWidths=[3*inch, 3*inch]) info_table.setStyle(TableStyle([ ('ALIGN', (0, 0), (-1, -1), 'LEFT'), ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), ('FONTSIZE', (0, 0), (-1, -1), 10), ('BOTTOMPADDING', (0, 0), (-1, -1), 8), ])) elements.append(info_table) elements.append(Spacer(1, 20)) # Invoice details details_data = [ ['Invoice Number:', invoice.invoice_number], ['Issue Date:', invoice.issue_date.strftime('%B %d, %Y')], ['Due Date:', invoice.due_date.strftime('%B %d, %Y')], ['Payment Terms:', invoice.payment_terms], ] details_table = Table(details_data, colWidths=[2*inch, 2*inch]) details_table.setStyle(TableStyle([ ('ALIGN', (0, 0), (-1, -1), 'LEFT'), ('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'), ('FONTSIZE', (0, 0), (-1, -1), 10), ('BOTTOMPADDING', (0, 0), (-1, -1), 6), ])) elements.append(details_table) elements.append(Spacer(1, 30)) # Invoice items table items_data = [['Description', 'Quantity', 'Unit Price', 'Total']] for item in invoice.items: items_data.append([ item.description, str(item.quantity), f"${item.unit_price:.2f}", f"${item.line_total:.2f}" ]) items_table = Table(items_data, colWidths=[3*inch, 1*inch, 1.5*inch, 1.5*inch]) items_table.setStyle(TableStyle([ ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#3498db')), ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), ('ALIGN', (0, 0), (-1, -1), 'CENTER'), ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), ('FONTSIZE', (0, 0), (-1, -1), 10), ('BOTTOMPADDING', (0, 0), (-1, -1), 8), ('BACKGROUND', (0, 1), (-1, -1), colors.beige), ('GRID', (0, 0), (-1, -1), 1, colors.black) ])) elements.append(items_table) elements.append(Spacer(1, 30)) # Totals table totals_data = [ ['Subtotal:', f"${invoice.subtotal:.2f}"], [f'Discount ({invoice.discount_rate}%):', f"-${invoice.discount_amount:.2f}"], [f'Tax ({invoice.tax_rate}%):', f"${invoice.tax_amount:.2f}"], ['Total Amount:', f"${invoice.total_amount:.2f}"] ] totals_table = Table(totals_data, colWidths=[2*inch, 1.5*inch], hAlign='RIGHT') totals_table.setStyle(TableStyle([ ('ALIGN', (0, 0), (-1, -1), 'RIGHT'), ('FONTNAME', (0, -1), (-1, -1), 'Helvetica-Bold'), ('FONTSIZE', (0, 0), (-1, -1), 10), ('BACKGROUND', (0, -1), (-1, -1), colors.HexColor('#e8f4fd')), ('BOTTOMPADDING', (0, 0), (-1, -1), 6), ])) elements.append(totals_table) # Notes section if invoice.notes: elements.append(Spacer(1, 30)) elements.append(Paragraph("Notes:", header_style)) elements.append(Paragraph(invoice.notes, styles['Normal'])) doc.build(elements) buffer.seek(0) return buffer.getvalue() # Routes @app.route('/') @login_required def dashboard(): """Modern dashboard with key metrics and analytics""" user = User.query.get(session['user_id']) # Calculate dashboard metrics total_invoices = Invoice.query.filter_by(user_id=user.id).count() paid_invoices = Invoice.query.filter_by(user_id=user.id, status='paid').count() pending_invoices = Invoice.query.filter_by(user_id=user.id, status='sent').count() overdue_invoices = Invoice.query.filter( Invoice.user_id == user.id, Invoice.status == 'sent', Invoice.due_date < datetime.utcnow().date() ).count() total_revenue = db.session.query(db.func.sum(Invoice.total_amount)).filter( Invoice.user_id == user.id, Invoice.status == 'paid' ).scalar() or 0 pending_amount = db.session.query(db.func.sum(Invoice.total_amount)).filter( Invoice.user_id == user.id, Invoice.status == 'sent' ).scalar() or 0 # Recent invoices for quick access recent_invoices = Invoice.query.filter_by(user_id=user.id).order_by( Invoice.created_at.desc() ).limit(5).all() # Monthly revenue data for charts monthly_data = db.session.query( db.func.strftime('%Y-%m', Invoice.issue_date).label('month'), db.func.sum(Invoice.total_amount).label('revenue') ).filter( Invoice.user_id == user.id, Invoice.status == 'paid' ).group_by('month').order_by('month').limit(12).all() return render_template('dashboard.html', user=user, total_invoices=total_invoices, paid_invoices=paid_invoices, pending_invoices=pending_invoices, overdue_invoices=overdue_invoices, total_revenue=float(total_revenue), pending_amount=float(pending_amount), recent_invoices=recent_invoices, monthly_data=monthly_data) @app.route('/login', methods=['GET', 'POST']) def login(): """Secure login with bcrypt password verification""" if request.method == 'POST': username = request.form['username'] password = request.form['password'] user = User.query.filter_by(username=username).first() if user and user.check_password(password): session['user_id'] = user.id session['username'] = user.username flash('Login successful!', 'success') return redirect(url_for('dashboard')) else: flash('Invalid username or password.', 'danger') return render_template('auth/login.html') @app.route('/register', methods=['GET', 'POST']) def register(): """User registration with input validation""" if request.method == 'POST': username = request.form['username'].strip() email = request.form['email'].strip().lower() password = request.form['password'] company_name = request.form.get('company_name', '').strip() company_address = request.form.get('company_address', '').strip() # Input validation if len(username) < 3: flash('Username must be at least 3 characters long.', 'danger') return render_template('auth/register.html') if len(password) < 6: flash('Password must be at least 6 characters long.', 'danger') return render_template('auth/register.html') # Email format validation if not re.match(r'^[^@]+@[^@]+\.[^@]+$', email): flash('Please enter a valid email address.', 'danger') return render_template('auth/register.html') # Check if user already exists if User.query.filter_by(username=username).first(): flash('Username already exists.', 'danger') return render_template('auth/register.html') if User.query.filter_by(email=email).first(): flash('Email already registered.', 'danger') return render_template('auth/register.html') # Create new user user = User( username=username, email=email, company_name=company_name, company_address=company_address ) user.set_password(password) db.session.add(user) db.session.commit() flash('Registration successful! Please log in.', 'success') return redirect(url_for('login')) return render_template('auth/register.html') @app.route('/logout') def logout(): """Secure session cleanup""" session.clear() flash('You have been logged out successfully.', 'info') return redirect(url_for('login')) @app.route('/clients') @login_required def clients(): """Client management with search functionality""" search_query = request.args.get('search', '') page = request.args.get('page', 1, type=int) query = Client.query.filter_by(user_id=session['user_id']) if search_query: query = query.filter( db.or_( Client.name.contains(search_query), Client.email.contains(search_query), Client.company.contains(search_query) ) ) clients = query.order_by(Client.created_at.desc()).paginate( page=page, per_page=10, error_out=False ) return render_template('clients/list.html', clients=clients, search_query=search_query) @app.route('/clients/add', methods=['GET', 'POST']) @login_required def add_client(): """Add new client with validation""" if request.method == 'POST': name = request.form['name'].strip() email = request.form['email'].strip().lower() phone = request.form.get('phone', '').strip() address = request.form.get('address', '').strip() company = request.form.get('company', '').strip() vat_number = request.form.get('vat_number', '').strip() # Validation if not name or not email: flash('Name and email are required.', 'danger') return render_template('clients/form.html') if not re.match(r'^[^@]+@[^@]+\.[^@]+$', email): flash('Please enter a valid email address.', 'danger') return render_template('clients/form.html') # Check for duplicate email within user's clients existing_client = Client.query.filter_by( user_id=session['user_id'], email=email ).first() if existing_client: flash('A client with this email already exists.', 'danger') return render_template('clients/form.html') client = Client( user_id=session['user_id'], name=name, email=email, phone=phone, address=address, company=company, vat_number=vat_number ) db.session.add(client) db.session.commit() flash(f'Client {name} added successfully!', 'success') return redirect(url_for('clients')) return render_template('clients/form.html') @app.route('/clients//edit', methods=['GET', 'POST']) @login_required def edit_client(client_id): """Edit existing client""" client = Client.query.filter_by(id=client_id, user_id=session['user_id']).first_or_404() if request.method == 'POST': client.name = request.form['name'].strip() client.email = request.form['email'].strip().lower() client.phone = request.form.get('phone', '').strip() client.address = request.form.get('address', '').strip() client.company = request.form.get('company', '').strip() client.vat_number = request.form.get('vat_number', '').strip() client.updated_at = datetime.utcnow() db.session.commit() flash(f'Client {client.name} updated successfully!', 'success') return redirect(url_for('clients')) return render_template('clients/form.html', client=client) @app.route('/clients//delete', methods=['POST']) @login_required def delete_client(client_id): """Delete client with confirmation""" client = Client.query.filter_by(id=client_id, user_id=session['user_id']).first_or_404() # Check if client has invoices if client.invoices: flash(f'Cannot delete client {client.name}. They have existing invoices.', 'danger') return redirect(url_for('clients')) db.session.delete(client) db.session.commit() flash(f'Client {client.name} deleted successfully!', 'success') return redirect(url_for('clients')) @app.route('/invoices') @login_required def invoices(): """Invoice list with advanced filtering""" status_filter = request.args.get('status', '') search_query = request.args.get('search', '') page = request.args.get('page', 1, type=int) query = Invoice.query.filter_by(user_id=session['user_id']) if status_filter: query = query.filter(Invoice.status == status_filter) if search_query: query = query.join(Client).filter( db.or_( Invoice.invoice_number.contains(search_query), Client.name.contains(search_query), Client.company.contains(search_query) ) ) invoices = query.order_by(Invoice.created_at.desc()).paginate( page=page, per_page=15, error_out=False ) return render_template('invoices/list.html', invoices=invoices, status_filter=status_filter, search_query=search_query) @app.route('/invoices/create', methods=['GET', 'POST']) @login_required def create_invoice(): """Create new invoice with automated calculations""" if request.method == 'POST': try: # Basic invoice data client_id = int(request.form['client_id']) due_date = datetime.strptime(request.form['due_date'], '%Y-%m-%d').date() tax_rate = float(request.form.get('tax_rate', 0)) discount_rate = float(request.form.get('discount_rate', 0)) notes = request.form.get('notes', '').strip() payment_terms = request.form.get('payment_terms', 'Net 30').strip() # Validate client ownership client = Client.query.filter_by(id=client_id, user_id=session['user_id']).first() if not client: flash('Invalid client selected.', 'danger') return redirect(url_for('create_invoice')) # Process invoice items items_data = [] item_count = int(request.form.get('item_count', 0)) for i in range(item_count): description = request.form.get(f'item_description_{i}', '').strip() quantity = float(request.form.get(f'item_quantity_{i}', 0)) unit_price = float(request.form.get(f'item_price_{i}', 0)) if description and quantity > 0 and unit_price > 0: line_total = quantity * unit_price items_data.append({ 'description': description, 'quantity': quantity, 'unit_price': unit_price, 'line_total': line_total }) if not items_data: flash('Please add at least one invoice item.', 'danger') return redirect(url_for('create_invoice')) # Calculate totals subtotal = sum(item['line_total'] for item in items_data) discount_amount = subtotal * (discount_rate / 100) subtotal_after_discount = subtotal - discount_amount tax_amount = subtotal_after_discount * (tax_rate / 100) total_amount = subtotal_after_discount + tax_amount # Create invoice invoice = Invoice( user_id=session['user_id'], client_id=client_id, invoice_number=generate_invoice_number(), due_date=due_date, subtotal=subtotal, tax_rate=tax_rate, tax_amount=tax_amount, discount_rate=discount_rate, discount_amount=discount_amount, total_amount=total_amount, notes=notes, payment_terms=payment_terms ) db.session.add(invoice) db.session.flush() # Get invoice ID # Add invoice items for item_data in items_data: item = InvoiceItem( invoice_id=invoice.id, description=item_data['description'], quantity=item_data['quantity'], unit_price=item_data['unit_price'], line_total=item_data['line_total'] ) db.session.add(item) db.session.commit() flash(f'Invoice {invoice.invoice_number} created successfully!', 'success') return redirect(url_for('view_invoice', invoice_id=invoice.id)) except Exception as e: db.session.rollback() logger.error(f"Invoice creation error: {str(e)}") flash('An error occurred while creating the invoice. Please try again.', 'danger') return redirect(url_for('create_invoice')) # Get clients for dropdown clients = Client.query.filter_by(user_id=session['user_id']).order_by(Client.name).all() return render_template('invoices/form.html', clients=clients) @app.route('/invoices/') @login_required def view_invoice(invoice_id): """View invoice details with action buttons""" invoice = Invoice.query.filter_by(id=invoice_id, user_id=session['user_id']).first_or_404() return render_template('invoices/view.html', invoice=invoice) @app.route('/invoices//edit', methods=['GET', 'POST']) @login_required def edit_invoice(invoice_id): """Edit existing invoice (only if not paid)""" invoice = Invoice.query.filter_by(id=invoice_id, user_id=session['user_id']).first_or_404() if invoice.status == 'paid': flash('Cannot edit a paid invoice.', 'warning') return redirect(url_for('view_invoice', invoice_id=invoice.id)) if request.method == 'POST': try: # Update invoice details invoice.client_id = int(request.form['client_id']) invoice.due_date = datetime.strptime(request.form['due_date'], '%Y-%m-%d').date() invoice.tax_rate = float(request.form.get('tax_rate', 0)) invoice.discount_rate = float(request.form.get('discount_rate', 0)) invoice.notes = request.form.get('notes', '').strip() invoice.payment_terms = request.form.get('payment_terms', 'Net 30').strip() # Clear existing items InvoiceItem.query.filter_by(invoice_id=invoice.id).delete() # Process updated items items_data = [] item_count = int(request.form.get('item_count', 0)) for i in range(item_count): description = request.form.get(f'item_description_{i}', '').strip() quantity = float(request.form.get(f'item_quantity_{i}', 0)) unit_price = float(request.form.get(f'item_price_{i}', 0)) if description and quantity > 0 and unit_price > 0: line_total = quantity * unit_price item = InvoiceItem( invoice_id=invoice.id, description=description, quantity=quantity, unit_price=unit_price, line_total=line_total ) db.session.add(item) items_data.append(item) # Recalculate totals subtotal = sum(item.line_total for item in items_data) discount_amount = subtotal * (invoice.discount_rate / 100) subtotal_after_discount = subtotal - discount_amount tax_amount = subtotal_after_discount * (invoice.tax_rate / 100) total_amount = subtotal_after_discount + tax_amount invoice.subtotal = subtotal invoice.discount_amount = discount_amount invoice.tax_amount = tax_amount invoice.total_amount = total_amount invoice.updated_at = datetime.utcnow() db.session.commit() flash(f'Invoice {invoice.invoice_number} updated successfully!', 'success') return redirect(url_for('view_invoice', invoice_id=invoice.id)) except Exception as e: db.session.rollback() logger.error(f"Invoice update error: {str(e)}") flash('An error occurred while updating the invoice.', 'danger') clients = Client.query.filter_by(user_id=session['user_id']).order_by(Client.name).all() return render_template('invoices/form.html', invoice=invoice, clients=clients) @app.route('/invoices//delete', methods=['POST']) @login_required def delete_invoice(invoice_id): """Delete invoice with confirmation""" invoice = Invoice.query.filter_by(id=invoice_id, user_id=session['user_id']).first_or_404() if invoice.status == 'paid': flash('Cannot delete a paid invoice.', 'warning') return redirect(url_for('invoices')) db.session.delete(invoice) db.session.commit() flash(f'Invoice {invoice.invoice_number} deleted successfully!', 'success') return redirect(url_for('invoices')) @app.route('/invoices//send', methods=['POST']) @login_required def send_invoice(invoice_id): """Send invoice via email with PDF attachment""" invoice = Invoice.query.filter_by(id=invoice_id, user_id=session['user_id']).first_or_404() try: # Generate PDF pdf_data = generate_invoice_pdf(invoice) # Send email if send_invoice_email(invoice, pdf_data): invoice.status = 'sent' invoice.sent_at = datetime.utcnow() db.session.commit() flash(f'Invoice {invoice.invoice_number} sent successfully to {invoice.client.email}!', 'success') else: flash('Failed to send invoice. Please check email configuration.', 'danger') except Exception as e: logger.error(f"Invoice sending error: {str(e)}") flash('An error occurred while sending the invoice.', 'danger') return redirect(url_for('view_invoice', invoice_id=invoice.id)) @app.route('/invoices//pdf') @login_required def download_pdf(invoice_id): """Download invoice as PDF""" invoice = Invoice.query.filter_by(id=invoice_id, user_id=session['user_id']).first_or_404() try: pdf_data = generate_invoice_pdf(invoice) return send_file( BytesIO(pdf_data), as_attachment=True, download_name=f'invoice_{invoice.invoice_number}.pdf', mimetype='application/pdf' ) except Exception as e: logger.error(f"PDF generation error: {str(e)}") flash('An error occurred while generating the PDF.', 'danger') return redirect(url_for('view_invoice', invoice_id=invoice.id)) @app.route('/invoices//mark_paid', methods=['POST']) @login_required def mark_paid(invoice_id): """Mark invoice as paid with payment details""" invoice = Invoice.query.filter_by(id=invoice_id, user_id=session['user_id']).first_or_404() payment_method = request.form.get('payment_method', 'Cash') payment_date = request.form.get('payment_date') if payment_date: try: invoice.payment_date = datetime.strptime(payment_date, '%Y-%m-%d').date() except ValueError: invoice.payment_date = datetime.utcnow().date() else: invoice.payment_date = datetime.utcnow().date() invoice.payment_method = payment_method invoice.status = 'paid' invoice.updated_at = datetime.utcnow() db.session.commit() flash(f'Invoice {invoice.invoice_number} marked as paid!', 'success') return redirect(url_for('view_invoice', invoice_id=invoice.id)) @app.route('/invoices//mark_unpaid', methods=['POST']) @login_required def mark_unpaid(invoice_id): """Mark invoice as unpaid (revert payment)""" invoice = Invoice.query.filter_by(id=invoice_id, user_id=session['user_id']).first_or_404() invoice.status = 'sent' if invoice.sent_at else 'draft' invoice.payment_date = None invoice.payment_method = None invoice.updated_at = datetime.utcnow() db.session.commit() flash(f'Invoice {invoice.invoice_number} marked as unpaid!', 'success') return redirect(url_for('view_invoice', invoice_id=invoice.id)) @app.route('/reports') @login_required def reports(): """Financial reports and analytics""" # Date range filters start_date = request.args.get('start_date') end_date = request.args.get('end_date') if not start_date: start_date = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d') if not end_date: end_date = datetime.now().strftime('%Y-%m-%d') # Base query query = Invoice.query.filter_by(user_id=session['user_id']) if start_date: query = query.filter(Invoice.issue_date >= datetime.strptime(start_date, '%Y-%m-%d').date()) if end_date: query = query.filter(Invoice.issue_date <= datetime.strptime(end_date, '%Y-%m-%d').date()) # Revenue by status revenue_by_status = db.session.query( Invoice.status, db.func.count(Invoice.id).label('count'), db.func.sum(Invoice.total_amount).label('total') ).filter( Invoice.user_id == session['user_id'], Invoice.issue_date >= datetime.strptime(start_date, '%Y-%m-%d').date(), Invoice.issue_date <= datetime.strptime(end_date, '%Y-%m-%d').date() ).group_by(Invoice.status).all() # Top clients top_clients = db.session.query( Client.name, db.func.count(Invoice.id).label('invoice_count'), db.func.sum(Invoice.total_amount).label('total_revenue') ).join(Invoice).filter( Invoice.user_id == session['user_id'], Invoice.issue_date >= datetime.strptime(start_date, '%Y-%m-%d').date(), Invoice.issue_date <= datetime.strptime(end_date, '%Y-%m-%d').date() ).group_by(Client.id, Client.name).order_by( db.func.sum(Invoice.total_amount).desc() ).limit(10).all() # Monthly revenue trend monthly_revenue = db.session.query( db.func.strftime('%Y-%m', Invoice.issue_date).label('month'), db.func.sum(Invoice.total_amount).label('revenue'), db.func.count(Invoice.id).label('invoice_count') ).filter( Invoice.user_id == session['user_id'], Invoice.status == 'paid', Invoice.issue_date >= datetime.strptime(start_date, '%Y-%m-%d').date(), Invoice.issue_date <= datetime.strptime(end_date, '%Y-%m-%d').date() ).group_by('month').order_by('month').all() return render_template('reports/dashboard.html', start_date=start_date, end_date=end_date, revenue_by_status=revenue_by_status, top_clients=top_clients, monthly_revenue=monthly_revenue) @app.route('/api/clients/search') @login_required def api_client_search(): """API endpoint for client autocomplete""" query = request.args.get('q', '') clients = Client.query.filter( Client.user_id == session['user_id'], Client.name.contains(query) ).limit(10).all() return jsonify([{ 'id': client.id, 'name': client.name, 'email': client.email, 'company': client.company } for client in clients]) @app.route('/settings', methods=['GET', 'POST']) @login_required def settings(): """User settings and preferences""" user = User.query.get(session['user_id']) if request.method == 'POST': action = request.form.get('action') if action == 'update_profile': user.company_name = request.form.get('company_name', '').strip() user.company_address = request.form.get('company_address', '').strip() user.email = request.form.get('email', '').strip().lower() db.session.commit() flash('Profile updated successfully!', 'success') elif action == 'change_password': current_password = request.form.get('current_password') new_password = request.form.get('new_password') confirm_password = request.form.get('confirm_password') if not user.check_password(current_password): flash('Current password is incorrect.', 'danger') elif new_password != confirm_password: flash('New passwords do not match.', 'danger') elif len(new_password) < 6: flash('New password must be at least 6 characters long.', 'danger') else: user.set_password(new_password) db.session.commit() flash('Password changed successfully!', 'success') return render_template('settings.html', user=user) # Error Handlers @app.errorhandler(404) def not_found(error): """Custom 404 error page""" return render_template('errors/404.html'), 404 @app.errorhandler(500) def internal_error(error): """Custom 500 error page""" db.session.rollback() logger.error(f"Internal error: {str(error)}") return render_template('errors/500.html'), 500 @app.errorhandler(403) def forbidden(error): """Custom 403 error page""" return render_template('errors/403.html'), 403 # Database initialization and sample data def create_sample_data(): """Create sample data for testing""" try: # Create sample user if not User.query.filter_by(username='demo').first(): demo_user = User( username='demo', email='demo@invoicing.com', company_name='Demo Company Ltd.', company_address='123 Business Street\nDemo City, DC 12345\nUnited States' ) demo_user.set_password('demo123') db.session.add(demo_user) db.session.flush() # Create sample clients clients_data = [ { 'name': 'John Smith', 'email': 'john@example.com', 'company': 'Smith & Associates', 'phone': '+1-555-0123', 'address': '456 Client Avenue\nClient City, CC 67890', 'vat_number': 'VAT123456789' }, { 'name': 'Sarah Johnson', 'email': 'sarah@techcorp.com', 'company': 'TechCorp Solutions', 'phone': '+1-555-0456', 'address': '789 Technology Blvd\nTech City, TC 13579', 'vat_number': 'VAT987654321' }, { 'name': 'Mike Wilson', 'email': 'mike@freelance.com', 'company': 'Wilson Freelance', 'phone': '+1-555-0789', 'address': '321 Freelancer Lane\nWork City, WC 24680' } ] sample_clients = [] for client_data in clients_data: client = Client(user_id=demo_user.id, **client_data) db.session.add(client) sample_clients.append(client) db.session.flush() # Create sample invoices for i, client in enumerate(sample_clients): # Create a paid invoice paid_invoice = Invoice( user_id=demo_user.id, client_id=client.id, invoice_number=f"2025-{str(i+1).zfill(4)}", issue_date=(datetime.now() - timedelta(days=30-i*5)).date(), due_date=(datetime.now() - timedelta(days=i*5)).date(), subtotal=1000.00, tax_rate=10.0, tax_amount=100.00, discount_rate=0.0, discount_amount=0.00, total_amount=1100.00, status='paid', payment_date=(datetime.now() - timedelta(days=i*3)).date(), payment_method='Bank Transfer', notes=f'Thank you for your business, {client.name}!', payment_terms='Net 30' ) db.session.add(paid_invoice) db.session.flush() # Add items to paid invoice items = [ {'description': 'Web Development Services', 'quantity': 40, 'unit_price': 75.00}, {'description': 'Project Management', 'quantity': 10, 'unit_price': 50.00} ] for item_data in items: item = InvoiceItem( invoice_id=paid_invoice.id, description=item_data['description'], quantity=item_data['quantity'], unit_price=item_data['unit_price'], line_total=item_data['quantity'] * item_data['unit_price'] ) db.session.add(item) # Create a pending invoice if i < 2: # Only for first two clients pending_invoice = Invoice( user_id=demo_user.id, client_id=client.id, invoice_number=f"2025-{str(i+10).zfill(4)}", issue_date=datetime.now().date(), due_date=(datetime.now() + timedelta(days=30)).date(), subtotal=750.00, tax_rate=10.0, tax_amount=75.00, discount_rate=5.0, discount_amount=37.50, total_amount=787.50, status='sent', sent_at=datetime.now(), notes='Please review and approve the attached invoice.', payment_terms='Net 30' ) db.session.add(pending_invoice) db.session.flush() # Add items to pending invoice pending_items = [ {'description': 'UI/UX Design Services', 'quantity': 15, 'unit_price': 80.00}, {'description': 'Code Review & Testing', 'quantity': 5, 'unit_price': 60.00} ] for item_data in pending_items: item = InvoiceItem( invoice_id=pending_invoice.id, description=item_data['description'], quantity=item_data['quantity'], unit_price=item_data['unit_price'], line_total=item_data['quantity'] * item_data['unit_price'] ) db.session.add(item) db.session.commit() logger.info("Sample data created successfully") except Exception as e: db.session.rollback() logger.error(f"Error creating sample data: {str(e)}") # Application initialization def init_app(): """Initialize the application with database and sample data""" with app.app_context(): db.create_all() create_sample_data() # Run application if __name__ == '__main__': init_app() app.run(debug=True, host='0.0.0.0', port=5000)

Comments

Popular posts from this blog

How to Leverage AI for Better Graphic Design

"The Rise of the Global Brain in 2025: A Vision Inspired by Arthur C. Clarke" How does it affect you?

Attendance sheet version 7