From 89be88566f4831a44224a1a6ea55f74d4904454d Mon Sep 17 00:00:00 2001 From: Javier Date: Sun, 1 Feb 2026 01:35:02 -0600 Subject: [PATCH] Excel Template working better, still not finished. --- blueprints/cons_sheets.py | 214 ++++++-------------- migrations.py | 14 ++ templates/cons_sheets/process_template.html | 11 +- 3 files changed, 90 insertions(+), 149 deletions(-) diff --git a/blueprints/cons_sheets.py b/blueprints/cons_sheets.py index 963a0a1..548390f 100644 --- a/blueprints/cons_sheets.py +++ b/blueprints/cons_sheets.py @@ -284,19 +284,23 @@ def update_template_settings(process_id): rows_per_page = request.form.get('rows_per_page', 30) detail_start_row = request.form.get('detail_start_row', 10) + detail_end_row = request.form.get('detail_end_row') # <--- Get the new value try: rows_per_page = int(rows_per_page) detail_start_row = int(detail_start_row) + # Handle empty string for end row (it's optional-ish, but needed for this specific strategy) + detail_end_row = int(detail_end_row) if detail_end_row and detail_end_row.strip() else None except ValueError: flash('Invalid number values', 'danger') return redirect(url_for('cons_sheets.process_template', process_id=process_id)) + # Update query to include the new column execute_db(''' UPDATE cons_processes - SET rows_per_page = ?, detail_start_row = ? + SET rows_per_page = ?, detail_start_row = ?, detail_end_row = ? WHERE id = ? - ''', [rows_per_page, detail_start_row, process_id]) + ''', [rows_per_page, detail_start_row, detail_end_row, process_id]) flash('Settings updated successfully!', 'success') return redirect(url_for('cons_sheets.process_template', process_id=process_id)) @@ -909,18 +913,17 @@ def archive_session(session_id): @cons_sheets_bp.route('/cons-sheets/session//export') @login_required def export_session(session_id): - """Export session to Excel using the process template""" + """Export session to Excel using the One Giant Template strategy""" from flask import Response from io import BytesIO import openpyxl - from openpyxl.utils import get_column_letter, column_index_from_string - from copy import copy from datetime import datetime - # Get session with process info + # Get session with process info AND the new detail_end_row sess = query_db(''' SELECT cs.*, cp.process_name, cp.process_key, cp.id as process_id, - cp.template_file, cp.template_filename, cp.rows_per_page, cp.detail_start_row + cp.template_file, cp.template_filename, cp.rows_per_page, + cp.detail_start_row, cp.detail_end_row FROM cons_sessions cs JOIN cons_processes cp ON cs.process_id = cp.id WHERE cs.id = ? @@ -951,7 +954,7 @@ def export_session(session_id): ''', [sess['process_id']]) # Get all scanned details - table_name = get_detail_table_name(sess['process_key']) + table_name = f'cons_proc_{sess["process_key"]}_details' scans = query_db(f''' SELECT * FROM {table_name} WHERE session_id = ? AND is_deleted = 0 @@ -961,158 +964,75 @@ def export_session(session_id): # Load the template template_bytes = BytesIO(sess['template_file']) wb = openpyxl.load_workbook(template_bytes) - ws = wb.active + ws = wb.active # We only work on the first sheet now - rows_per_page = sess['rows_per_page'] or 30 detail_start_row = sess['detail_start_row'] or 11 + detail_end_row = sess['detail_end_row'] # This is our new target - # Calculate how many pages we need - total_scans = len(scans) if scans else 0 - num_pages = max(1, (total_scans + rows_per_page - 1) // rows_per_page) if total_scans > 0 else 1 + # --- STEP 1: Fill Header --- + for field in header_fields: + if field['excel_cell'] and field['field_value']: + try: + ws[field['excel_cell']] = field['field_value'] + except: + pass - # Helper function to fill header values on a sheet - def fill_header(worksheet, header_fields): - for field in header_fields: - if field['excel_cell'] and field['field_value']: + # --- STEP 2: Fill ALL Details --- + # We just write them all sequentially, relying on the template being "Giant" + for i, scan in enumerate(scans): + row_num = detail_start_row + i + for field in detail_fields: + if field['excel_cell']: try: - worksheet[field['excel_cell']] = field['field_value'] - except: - pass # Skip invalid cell references - - # Helper function to clear detail rows on a sheet - def clear_details(worksheet, detail_fields, start_row, num_rows): - for i in range(num_rows): - row_num = start_row + i - for field in detail_fields: - if field['excel_cell']: - try: - col_letter = field['excel_cell'].upper().strip() - cell_ref = f"{col_letter}{row_num}" - worksheet[cell_ref] = None - except: - pass - - # Helper function to fill detail rows on a sheet - def fill_details(worksheet, scans_subset, detail_fields, start_row): - for i, scan in enumerate(scans_subset): - row_num = start_row + i - for field in detail_fields: - if field['excel_cell']: - try: - col_letter = field['excel_cell'].upper().strip() - cell_ref = f"{col_letter}{row_num}" - value = scan[field['field_name']] - # Convert to appropriate type - if field['field_type'] == 'REAL' and value: - value = float(value) - elif field['field_type'] == 'INTEGER' and value: - value = int(value) - worksheet[cell_ref] = value - except Exception as e: - print(f"Error filling cell: {e}") - - # Fill the first page - fill_header(ws, header_fields) - first_page_scans = scans[:rows_per_page] if scans else [] - fill_details(ws, first_page_scans, detail_fields, detail_start_row) - - # Create additional pages if needed - for page_num in range(2, num_pages + 1): - # Copy the worksheet within the same workbook - new_ws = wb.copy_worksheet(ws) - new_ws.title = f"Page {page_num}" + col_letter = field['excel_cell'].upper().strip() + cell_ref = f"{col_letter}{row_num}" + value = scan[field['field_name']] + + # Convert types + if field['field_type'] == 'REAL' and value: + value = float(value) + elif field['field_type'] == 'INTEGER' and value: + value = int(value) + + ws[cell_ref] = value + except Exception as e: + print(f"Error filling cell: {e}") + +# --- STEP 3: Delete Unused Rows & Fix Print Area --- + if detail_end_row: + first_empty_row = detail_start_row + len(scans) - # Clear detail rows (they have Page 1 data) - clear_details(new_ws, detail_fields, detail_start_row, rows_per_page) - - # Fill details for this page - start_idx = (page_num - 1) * rows_per_page - end_idx = start_idx + rows_per_page - page_scans = scans[start_idx:end_idx] - fill_details(new_ws, page_scans, detail_fields, detail_start_row) - - # Rename first sheet if we have multiple pages - if num_pages > 1: - ws.title = "Page 1" - - # Save to BytesIO + # Only delete if we actually have empty rows to remove + if first_empty_row <= detail_end_row: + rows_to_delete = detail_end_row - first_empty_row + 1 + ws.delete_rows(first_empty_row, amount=rows_to_delete) + + # --- FIX 1: Clear Breaks --- + ws.row_breaks.brk = [] + ws.col_breaks.brk = [] + + # --- FIX 2: Explicitly Set Print Area --- + # The "Total" line (and footer) has now moved UP to 'first_empty_row'. + # We want to print everything from A1 down to that Total line. + # (If your footer is taller than 1 row, increase the +0 below) + footer_height = 0 + final_print_row = first_empty_row + footer_height + + # Force the print area to cut off the "Zombie Pages" + ws.print_area = f"A1:K{final_print_row}" + + # Reset scaling + if ws.sheet_properties.pageSetUpPr: + ws.sheet_properties.pageSetUpPr.fitToPage = False + + # --- Save & Export --- output = BytesIO() wb.save(output) output.seek(0) - # Generate filename timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') base_filename = f"{sess['process_key']}_{session_id}_{timestamp}" - # Check if PDF export is requested - export_format = request.args.get('format', 'xlsx') - print(f"DEBUG: Export format requested: {export_format}") - - if export_format == 'pdf': - # Use win32com to convert to PDF (requires Excel installed) - try: - import tempfile - import pythoncom - import win32com.client as win32 - print("DEBUG: pywin32 imported successfully") - - # Save Excel to temp file - temp_xlsx = tempfile.NamedTemporaryFile(suffix='.xlsx', delete=False) - temp_xlsx.write(output.getvalue()) - temp_xlsx.close() - print(f"DEBUG: Temp Excel saved to: {temp_xlsx.name}") - - temp_pdf = temp_xlsx.name.replace('.xlsx', '.pdf') - - # Initialize COM for this thread - pythoncom.CoInitialize() - print("DEBUG: COM initialized") - - try: - excel = win32.Dispatch('Excel.Application') - excel.Visible = False - excel.DisplayAlerts = False - print("DEBUG: Excel application started") - - workbook = excel.Workbooks.Open(temp_xlsx.name) - print("DEBUG: Workbook opened") - - workbook.ExportAsFixedFormat(0, temp_pdf) # 0 = PDF format - print(f"DEBUG: Exported to PDF: {temp_pdf}") - - workbook.Close(False) - excel.Quit() - print("DEBUG: Excel closed") - finally: - pythoncom.CoUninitialize() - - # Read the PDF - with open(temp_pdf, 'rb') as f: - pdf_data = f.read() - print(f"DEBUG: PDF read, size: {len(pdf_data)} bytes") - - # Clean up temp files - import os - os.unlink(temp_xlsx.name) - os.unlink(temp_pdf) - print("DEBUG: Temp files cleaned up") - - return Response( - pdf_data, - mimetype='application/pdf', - headers={'Content-Disposition': f'attachment; filename={base_filename}.pdf'} - ) - except ImportError as e: - print(f"ERROR: Import failed - {e}") - # Fall back to Excel export - except Exception as e: - print(f"ERROR: PDF export failed - {e}") - import traceback - traceback.print_exc() - # Fall back to Excel export - - # Default: return Excel file - print("DEBUG: Returning Excel file") return Response( output.getvalue(), mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', diff --git a/migrations.py b/migrations.py index 00ad63b..82ead5e 100644 --- a/migrations.py +++ b/migrations.py @@ -210,6 +210,19 @@ def migration_006_add_is_deleted_to_locationcounts(): conn.commit() conn.close() + +def migration_007_add_detail_end_row(): + """Add detail_end_row column to cons_processes table""" + conn = get_db() + + if table_exists('cons_processes'): + if not column_exists('cons_processes', 'detail_end_row'): + conn.execute('ALTER TABLE cons_processes ADD COLUMN detail_end_row INTEGER') + print(" Added detail_end_row column to cons_processes") + + conn.commit() + conn.close() + # List of all migrations in order MIGRATIONS = [ (1, 'add_modules_tables', migration_001_add_modules_tables), @@ -218,6 +231,7 @@ MIGRATIONS = [ (4, 'assign_modules_to_admins', migration_004_assign_modules_to_admins), (5, 'add_cons_process_fields_duplicate_key', migration_005_add_cons_process_fields_duplicate_key), (6, 'add_is_deleted_to_locationcounts', migration_006_add_is_deleted_to_locationcounts), + (7, 'add_detail_end_row', migration_007_add_detail_end_row), ] diff --git a/templates/cons_sheets/process_template.html b/templates/cons_sheets/process_template.html index f1ee667..89c8b4a 100644 --- a/templates/cons_sheets/process_template.html +++ b/templates/cons_sheets/process_template.html @@ -52,17 +52,24 @@
+ value="{{ process.rows_per_page or 30 }}" min="1" max="5000" class="form-input">

Max detail rows before starting a new page

+ value="{{ process.detail_start_row or 10 }}" min="1" max="5000" class="form-input">

Excel row number where detail data begins

+
+ + +

The row number where your pre-made blank lines end. Unused rows up to this point will be deleted.

+
+