import json import os from datetime import datetime from flask import (Blueprint, render_template, redirect, url_for, flash, request, current_app, send_from_directory, abort, jsonify) from flask_login import login_required, current_user from app.extensions import db from app.models.paperwork import Paperwork, DOC_TYPES from app.models.document_template import DocumentTemplate from app.models.user import User from app.models.asset import Asset from app.models.assignment import Assignment from app.models.audit_log import AuditLog from app.services.pdf_service import generate_paperwork_pdf from app.services.template_service import ( build_context, render_template_to_docx, _template_path ) bp = Blueprint('paperwork', __name__, url_prefix='/paperwork') def _log(action, record_id, description, new=None): entry = AuditLog( table_name='paperwork', record_id=record_id, action=action, new_values=json.dumps(new) if new else None, performed_by_id=current_user.id, ip_address=request.remote_addr, description=description, ) db.session.add(entry) @bp.route('/') @login_required def index(): page = request.args.get('page', 1, type=int) q = request.args.get('q', '').strip() doc_type_filter = request.args.get('doc_type', '') query = Paperwork.query if doc_type_filter: query = query.filter_by(document_type=doc_type_filter) pagination = query.order_by(Paperwork.created_at.desc()).paginate( page=page, per_page=current_app.config['ITEMS_PER_PAGE'], error_out=False ) return render_template('paperwork/index.html', pagination=pagination, q=q, doc_type_filter=doc_type_filter, doc_types=DOC_TYPES) @bp.route('/new', methods=['GET', 'POST']) @login_required def create(): preselect_user_id = request.args.get('user_id', type=int) preselect_asset_id = request.args.get('asset_id', type=int) preselect_assignment_id = request.args.get('assignment_id', type=int) all_templates = DocumentTemplate.query.order_by(DocumentTemplate.name).all() if request.method == 'POST': user_id = request.form.get('user_id', type=int) asset_id = request.form.get('asset_id', type=int) or None assignment_id = request.form.get('assignment_id', type=int) or None doc_type = request.form.get('document_type', 'handover') title = request.form.get('title', '').strip() notes = request.form.get('notes', '').strip() or None template_id = request.form.get('template_id', type=int) or None user = User.query.get(user_id) if user_id else None if not user: flash('User is required.', 'danger') return render_template('paperwork/form.html', doc_types=DOC_TYPES, all_templates=all_templates, preselect_user_id=preselect_user_id, preselect_asset_id=preselect_asset_id) asset = Asset.query.get(asset_id) if asset_id else None assignment = Assignment.query.get(assignment_id) if assignment_id else None if not title: title = f'{dict(DOC_TYPES).get(doc_type, doc_type)} — {user.display_name}' doc = Paperwork( document_type=doc_type, title=title, user_id=user.id, asset_id=asset_id, assignment_id=assignment_id, template_id=template_id, notes=notes, created_by_id=current_user.id, ) db.session.add(doc) db.session.flush() # Build and store merge variables ctx = build_context(user, asset=asset, assignment=assignment, paperwork=doc, app=current_app._get_current_object()) # Allow form overrides for any variable (textarea fields named var_*) for k, v in request.form.items(): if k.startswith('var_'): ctx[k[4:]] = v ctx['admin_name'] = current_user.username doc.merge_vars = json.dumps(ctx) # Generate .docx from template if one is selected if template_id: tpl_obj = DocumentTemplate.query.get(template_id) if tpl_obj: tpl_file = _template_path(current_app._get_current_object(), tpl_obj.filename) if os.path.exists(tpl_file): out_name = f'doc_{doc.id}_{tpl_obj.id}.docx' try: render_template_to_docx(tpl_file, ctx, out_name) doc.docx_filename = out_name except Exception as exc: current_app.logger.error('docx render failed: %s', exc) flash(f'Word document generation failed: {exc}', 'warning') # Always generate PDF try: pdf_filename = generate_paperwork_pdf(doc, current_app._get_current_object()) doc.pdf_filename = pdf_filename except Exception as exc: current_app.logger.error(f'PDF generation failed: {exc}') flash(f'Document saved but PDF generation failed: {exc}', 'warning') _log('create', doc.id, f'Created paperwork "{title}" type={doc_type}', new={'type': doc_type, 'user_id': user_id, 'asset_id': asset_id, 'template_id': template_id}) db.session.commit() flash(f'Document "{title}" created.', 'success') return redirect(url_for('paperwork.detail', doc_id=doc.id)) return render_template('paperwork/form.html', doc_types=DOC_TYPES, all_templates=all_templates, preselect_user_id=preselect_user_id, preselect_asset_id=preselect_asset_id, preselect_assignment_id=preselect_assignment_id) @bp.route('/') @login_required def detail(doc_id): doc = Paperwork.query.get_or_404(doc_id) return render_template('paperwork/detail.html', doc=doc, merge_vars=doc.get_merge_vars()) @bp.route('//download') @login_required def download(doc_id): doc = Paperwork.query.get_or_404(doc_id) if not doc.pdf_filename: flash('No PDF available for this document.', 'warning') return redirect(url_for('paperwork.detail', doc_id=doc_id)) pdf_dir = os.path.join(current_app.root_path, '..', current_app.config['PDF_FOLDER']) return send_from_directory(pdf_dir, doc.pdf_filename, as_attachment=True) @bp.route('//download-docx') @login_required def download_docx(doc_id): doc = Paperwork.query.get_or_404(doc_id) if not doc.docx_filename: flash('No Word document available for this record.', 'warning') return redirect(url_for('paperwork.detail', doc_id=doc_id)) docx_dir = os.path.join(current_app.root_path, '..', current_app.config.get('DOCX_FOLDER', 'docx_output')) safe_name = (doc.title or f'document_{doc.id}') + '.docx' return send_from_directory(docx_dir, doc.docx_filename, as_attachment=True, download_name=safe_name) @bp.route('//regenerate', methods=['POST']) @login_required def regenerate(doc_id): doc = Paperwork.query.get_or_404(doc_id) app = current_app._get_current_object() # Regenerate .docx if template-based if doc.template_id and doc.template: tpl_file = _template_path(app, doc.template.filename) ctx = doc.get_merge_vars() out_name = doc.docx_filename or f'doc_{doc.id}_{doc.template_id}.docx' try: render_template_to_docx(tpl_file, ctx, out_name) doc.docx_filename = out_name except Exception as exc: flash(f'Word regeneration failed: {exc}', 'warning') try: pdf_filename = generate_paperwork_pdf(doc, app) doc.pdf_filename = pdf_filename db.session.commit() flash('Document regenerated.', 'success') except Exception as exc: flash(f'PDF generation failed: {exc}', 'danger') return redirect(url_for('paperwork.detail', doc_id=doc_id)) @bp.route('//sign', methods=['POST']) @login_required def sign(doc_id): """Record a signature on a document.""" doc = Paperwork.query.get_or_404(doc_id) signed_by_name = request.form.get('signed_by_name', '').strip() signature_data = request.form.get('signature_data', '').strip() # base64 PNG from canvas if not signed_by_name: flash('Signer name is required.', 'danger') return redirect(url_for('paperwork.detail', doc_id=doc_id)) doc.signed_at = datetime.utcnow() doc.signed_by_name = signed_by_name if signature_data: doc.signature_data = signature_data # Regenerate PDF to embed signature try: pdf_filename = generate_paperwork_pdf(doc, current_app._get_current_object()) doc.pdf_filename = pdf_filename except Exception as exc: current_app.logger.error('PDF re-gen after sign failed: %s', exc) _log('sign', doc.id, f'Document signed by {signed_by_name}') db.session.commit() flash(f'Document signed by {signed_by_name}.', 'success') return redirect(url_for('paperwork.detail', doc_id=doc_id)) @bp.route('//unsign', methods=['POST']) @login_required def unsign(doc_id): doc = Paperwork.query.get_or_404(doc_id) doc.signed_at = None doc.signed_by_name = None doc.signature_data = None db.session.commit() flash('Signature removed.', 'info') return redirect(url_for('paperwork.detail', doc_id=doc_id))