Excel Template working better, still not finished.
This commit is contained in:
@@ -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:
|
ws[cell_ref] = value
|
||||||
col_letter = field['excel_cell'].upper().strip()
|
except Exception as e:
|
||||||
cell_ref = f"{col_letter}{row_num}"
|
print(f"Error filling cell: {e}")
|
||||||
worksheet[cell_ref] = None
|
|
||||||
except:
|
# --- STEP 3: Delete Unused Rows & Fix Print Area ---
|
||||||
pass
|
if detail_end_row:
|
||||||
|
first_empty_row = detail_start_row + len(scans)
|
||||||
# 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}"
|
|
||||||
|
|
||||||
# Clear detail rows (they have Page 1 data)
|
# Only delete if we actually have empty rows to remove
|
||||||
clear_details(new_ws, detail_fields, detail_start_row, rows_per_page)
|
if first_empty_row <= detail_end_row:
|
||||||
|
rows_to_delete = detail_end_row - first_empty_row + 1
|
||||||
# Fill details for this page
|
ws.delete_rows(first_empty_row, amount=rows_to_delete)
|
||||||
start_idx = (page_num - 1) * rows_per_page
|
|
||||||
end_idx = start_idx + rows_per_page
|
# --- FIX 1: Clear Breaks ---
|
||||||
page_scans = scans[start_idx:end_idx]
|
ws.row_breaks.brk = []
|
||||||
fill_details(new_ws, page_scans, detail_fields, detail_start_row)
|
ws.col_breaks.brk = []
|
||||||
|
|
||||||
# Rename first sheet if we have multiple pages
|
# --- FIX 2: Explicitly Set Print Area ---
|
||||||
if num_pages > 1:
|
# The "Total" line (and footer) has now moved UP to 'first_empty_row'.
|
||||||
ws.title = "Page 1"
|
# We want to print everything from A1 down to that Total line.
|
||||||
|
# (If your footer is taller than 1 row, increase the +0 below)
|
||||||
# Save to BytesIO
|
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()
|
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',
|
||||||
|
|||||||
@@ -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),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user