Excel Template working better, still not finished.

This commit is contained in:
Javier
2026-02-01 01:35:02 -06:00
parent 1359e036d5
commit 89be88566f
3 changed files with 90 additions and 149 deletions

View File

@@ -284,19 +284,23 @@ def update_template_settings(process_id):
rows_per_page = request.form.get('rows_per_page', 30) rows_per_page = request.form.get('rows_per_page', 30)
detail_start_row = request.form.get('detail_start_row', 10) detail_start_row = request.form.get('detail_start_row', 10)
detail_end_row = request.form.get('detail_end_row') # <--- Get the new value
try: try:
rows_per_page = int(rows_per_page) rows_per_page = int(rows_per_page)
detail_start_row = int(detail_start_row) 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: except ValueError:
flash('Invalid number values', 'danger') flash('Invalid number values', 'danger')
return redirect(url_for('cons_sheets.process_template', process_id=process_id)) return redirect(url_for('cons_sheets.process_template', process_id=process_id))
# Update query to include the new column
execute_db(''' execute_db('''
UPDATE cons_processes UPDATE cons_processes
SET rows_per_page = ?, detail_start_row = ? SET rows_per_page = ?, detail_start_row = ?, detail_end_row = ?
WHERE id = ? 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') flash('Settings updated successfully!', 'success')
return redirect(url_for('cons_sheets.process_template', process_id=process_id)) 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/<int:session_id>/export') @cons_sheets_bp.route('/cons-sheets/session/<int:session_id>/export')
@login_required @login_required
def export_session(session_id): 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 flask import Response
from io import BytesIO from io import BytesIO
import openpyxl import openpyxl
from openpyxl.utils import get_column_letter, column_index_from_string
from copy import copy
from datetime import datetime from datetime import datetime
# Get session with process info # Get session with process info AND the new detail_end_row
sess = query_db(''' sess = query_db('''
SELECT cs.*, cp.process_name, cp.process_key, cp.id as process_id, 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 FROM cons_sessions cs
JOIN cons_processes cp ON cs.process_id = cp.id JOIN cons_processes cp ON cs.process_id = cp.id
WHERE cs.id = ? WHERE cs.id = ?
@@ -951,7 +954,7 @@ def export_session(session_id):
''', [sess['process_id']]) ''', [sess['process_id']])
# Get all scanned details # 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''' scans = query_db(f'''
SELECT * FROM {table_name} SELECT * FROM {table_name}
WHERE session_id = ? AND is_deleted = 0 WHERE session_id = ? AND is_deleted = 0
@@ -961,158 +964,75 @@ def export_session(session_id):
# Load the template # Load the template
template_bytes = BytesIO(sess['template_file']) template_bytes = BytesIO(sess['template_file'])
wb = openpyxl.load_workbook(template_bytes) 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_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 # --- STEP 1: Fill Header ---
total_scans = len(scans) if scans else 0 for field in header_fields:
num_pages = max(1, (total_scans + rows_per_page - 1) // rows_per_page) if total_scans > 0 else 1 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 # --- STEP 2: Fill ALL Details ---
def fill_header(worksheet, header_fields): # We just write them all sequentially, relying on the template being "Giant"
for field in header_fields: for i, scan in enumerate(scans):
if field['excel_cell'] and field['field_value']: row_num = detail_start_row + i
for field in detail_fields:
if field['excel_cell']:
try: try:
worksheet[field['excel_cell']] = field['field_value'] col_letter = field['excel_cell'].upper().strip()
except: cell_ref = f"{col_letter}{row_num}"
pass # Skip invalid cell references value = scan[field['field_name']]
# Helper function to clear detail rows on a sheet # Convert types
def clear_details(worksheet, detail_fields, start_row, num_rows): if field['field_type'] == 'REAL' and value:
for i in range(num_rows): value = float(value)
row_num = start_row + i elif field['field_type'] == 'INTEGER' and value:
for field in detail_fields: value = int(value)
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 ws[cell_ref] = value
def fill_details(worksheet, scans_subset, detail_fields, start_row): except Exception as e:
for i, scan in enumerate(scans_subset): print(f"Error filling cell: {e}")
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 # --- STEP 3: Delete Unused Rows & Fix Print Area ---
fill_header(ws, header_fields) if detail_end_row:
first_page_scans = scans[:rows_per_page] if scans else [] first_empty_row = detail_start_row + len(scans)
fill_details(ws, first_page_scans, detail_fields, detail_start_row)
# Create additional pages if needed # Only delete if we actually have empty rows to remove
for page_num in range(2, num_pages + 1): if first_empty_row <= detail_end_row:
# Copy the worksheet within the same workbook rows_to_delete = detail_end_row - first_empty_row + 1
new_ws = wb.copy_worksheet(ws) ws.delete_rows(first_empty_row, amount=rows_to_delete)
new_ws.title = f"Page {page_num}"
# Clear detail rows (they have Page 1 data) # --- FIX 1: Clear Breaks ---
clear_details(new_ws, detail_fields, detail_start_row, rows_per_page) ws.row_breaks.brk = []
ws.col_breaks.brk = []
# Fill details for this page # --- FIX 2: Explicitly Set Print Area ---
start_idx = (page_num - 1) * rows_per_page # The "Total" line (and footer) has now moved UP to 'first_empty_row'.
end_idx = start_idx + rows_per_page # We want to print everything from A1 down to that Total line.
page_scans = scans[start_idx:end_idx] # (If your footer is taller than 1 row, increase the +0 below)
fill_details(new_ws, page_scans, detail_fields, detail_start_row) footer_height = 0
final_print_row = first_empty_row + footer_height
# Rename first sheet if we have multiple pages # Force the print area to cut off the "Zombie Pages"
if num_pages > 1: ws.print_area = f"A1:K{final_print_row}"
ws.title = "Page 1"
# Save to BytesIO # Reset scaling
if ws.sheet_properties.pageSetUpPr:
ws.sheet_properties.pageSetUpPr.fitToPage = False
# --- Save & Export ---
output = BytesIO() output = BytesIO()
wb.save(output) wb.save(output)
output.seek(0) output.seek(0)
# Generate filename
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
base_filename = f"{sess['process_key']}_{session_id}_{timestamp}" 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( return Response(
output.getvalue(), output.getvalue(),
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',

View File

@@ -210,6 +210,19 @@ def migration_006_add_is_deleted_to_locationcounts():
conn.commit() conn.commit()
conn.close() 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 # List of all migrations in order
MIGRATIONS = [ MIGRATIONS = [
(1, 'add_modules_tables', migration_001_add_modules_tables), (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), (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), (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), (6, 'add_is_deleted_to_locationcounts', migration_006_add_is_deleted_to_locationcounts),
(7, 'add_detail_end_row', migration_007_add_detail_end_row),
] ]

View File

@@ -52,17 +52,24 @@
<div class="form-group"> <div class="form-group">
<label for="rows_per_page" class="form-label">Rows Per Page</label> <label for="rows_per_page" class="form-label">Rows Per Page</label>
<input type="number" id="rows_per_page" name="rows_per_page" <input type="number" id="rows_per_page" name="rows_per_page"
value="{{ process.rows_per_page or 30 }}" min="1" max="500" class="form-input"> value="{{ process.rows_per_page or 30 }}" min="1" max="5000" class="form-input">
<p class="form-hint">Max detail rows before starting a new page</p> <p class="form-hint">Max detail rows before starting a new page</p>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="detail_start_row" class="form-label">Detail Start Row</label> <label for="detail_start_row" class="form-label">Detail Start Row</label>
<input type="number" id="detail_start_row" name="detail_start_row" <input type="number" id="detail_start_row" name="detail_start_row"
value="{{ process.detail_start_row or 10 }}" min="1" max="500" class="form-input"> value="{{ process.detail_start_row or 10 }}" min="1" max="5000" class="form-input">
<p class="form-hint">Excel row number where detail data begins</p> <p class="form-hint">Excel row number where detail data begins</p>
</div> </div>
<div class="form-group">
<label for="detail_end_row" class="form-label">Detail End Row (Footer Start)</label>
<input type="number" id="detail_end_row" name="detail_end_row"
value="{{ process.detail_end_row or '' }}" min="1" class="form-input">
<p class="form-hint">The row number where your pre-made blank lines end. Unused rows up to this point will be deleted.</p>
</div>
<button type="submit" class="btn btn-primary">Save Settings</button> <button type="submit" class="btn btn-primary">Save Settings</button>
</form> </form>
</div> </div>