from flask import Blueprint, render_template, send_file, flash, redirect, url_for, request, jsonify, Response from flask_login import login_required, current_user from app.extensions import db from app.models.project import Project from app.models.position import Position from app.services.export_service import export_project_to_excel from app.services.export_pdf_service import export_project_to_pdf from app.services.export_x31_service import export_to_x31, convert_to_california import os, tempfile, io, zipfile, json export_bp = Blueprint('export', __name__) def _build_filename(project, suffix): parts = [project.bezeichnung or '', project.baustelle or '', project.bauabschnitt or '', project.sm_nr or '', project.abruf_nr or ''] name = ' - '.join(p for p in parts if p) return f'Aufmass {name}{suffix}' def _get_aufmass_and_positions(project_id, aufmass_id=None, visible_ids=None): project = Project.query.get_or_404(project_id) if not current_user.hat_zugriff(project, 'lesen'): return None, None, None from app.models.aufmass import Aufmass if aufmass_id: aufmass = Aufmass.query.get_or_404(aufmass_id) else: aufmass = Aufmass.query.filter_by(project_id=project_id).order_by(Aufmass.sortierung).first() if not aufmass: return None, None, None pos_query = Position.query.filter_by(aufmass_id=aufmass.id) if visible_ids: ids = [int(x) for x in visible_ids.split(',') if x.isdigit()] if ids: pos_query = pos_query.filter(Position.id.in_(ids)) positionen = pos_query.order_by(Position.sortierung).all() return project, aufmass, positionen @export_bp.route('//excel') @login_required def excel(project_id): project = Project.query.get_or_404(project_id) if not current_user.hat_zugriff(project, 'lesen'): flash('Zugriff verweigert.', 'danger') return redirect(url_for('aufmass.index')) from app.models.aufmass import Aufmass aufmass_id = request.args.get('aufmass_id', type=int) if aufmass_id: aufmass = Aufmass.query.get_or_404(aufmass_id) else: aufmass = Aufmass.query.filter_by(project_id=project_id).order_by(Aufmass.sortierung).first() if not aufmass: flash('Kein Aufmaß vorhanden.', 'danger') return redirect(url_for('aufmass.aufmass_list', project_id=project_id)) pos_query = Position.query.filter_by(aufmass_id=aufmass.id) visible_ids = request.args.get('visible_ids') if visible_ids: ids = [int(x) for x in visible_ids.split(',') if x.isdigit()] if ids: pos_query = pos_query.filter(Position.id.in_(ids)) pos = pos_query.order_by(Position.sortierung).all() fd, path = tempfile.mkstemp(suffix='.xlsx') os.close(fd) try: company = current_user.company if not current_user.is_superadmin() else None export_project_to_excel(project, aufmass, pos, path, company=company) return send_file(path, as_attachment=True, download_name=_build_filename(project, '.xlsx')) except Exception as e: flash(f'Fehler beim Export: {e}', 'danger') return redirect(url_for('aufmass.aufmass_list', project_id=project_id)) @export_bp.route('//pdf') @login_required def pdf(project_id): project = Project.query.get_or_404(project_id) if not current_user.hat_zugriff(project, 'lesen'): flash('Zugriff verweigert.', 'danger') return redirect(url_for('aufmass.index')) from app.models.aufmass import Aufmass aufmass_id = request.args.get('aufmass_id', type=int) if aufmass_id: aufmass = Aufmass.query.get_or_404(aufmass_id) else: aufmass = Aufmass.query.filter_by(project_id=project_id).order_by(Aufmass.sortierung).first() if not aufmass: flash('Kein Aufmaß vorhanden.', 'danger') return redirect(url_for('aufmass.aufmass_list', project_id=project_id)) pos_query = Position.query.filter_by(aufmass_id=aufmass.id) visible_ids = request.args.get('visible_ids') if visible_ids: ids = [int(x) for x in visible_ids.split(',') if x.isdigit()] if ids: pos_query = pos_query.filter(Position.id.in_(ids)) positionen = pos_query.order_by(Position.sortierung).all() company = current_user.company if not current_user.is_superadmin() else None if request.args.get('html') == '1': from app.services.export_pdf_service import _render_pdf_html html = _render_pdf_html(project, aufmass, positionen, company=company) return Response(html, mimetype='text/html') fd, path = tempfile.mkstemp(suffix='.pdf') os.close(fd) try: export_project_to_pdf(project, aufmass, positionen, path, company=company) return send_file(path, as_attachment=True, download_name=_build_filename(project, '.pdf')) except Exception as e: flash(f'Fehler beim PDF-Export: {e}', 'danger') return redirect(url_for('aufmass.aufmass_list', project_id=project_id)) @export_bp.route('//txt', methods=['GET','POST']) @login_required def txt(project_id): project = Project.query.get_or_404(project_id) if not current_user.hat_zugriff(project, 'lesen'): flash('Zugriff verweigert.', 'danger') return redirect(url_for('aufmass.index')) from app.models.aufmass import Aufmass aufmass_id = request.args.get('aufmass_id', type=int) if aufmass_id: aufmass = Aufmass.query.get_or_404(aufmass_id) else: aufmass = Aufmass.query.filter_by(project_id=project_id).order_by(Aufmass.sortierung).first() if not aufmass: flash('Kein Aufmaß vorhanden.', 'danger') return redirect(url_for('aufmass.aufmass_list', project_id=project_id)) pos_query = Position.query.filter_by(aufmass_id=aufmass.id) visible_ids = request.args.get('visible_ids') if visible_ids: ids = [int(x) for x in visible_ids.split(',') if x.isdigit()] if ids: pos_query = pos_query.filter(Position.id.in_(ids)) positionen = pos_query.order_by(Position.sortierung).all() def _d(v): if v is None: return '' if hasattr(v, 'strftime'): return v.strftime('%d.%m.%Y') s = str(v)[:10] for fmt in ('%Y-%m-%d', '%d.%m.%Y'): try: from datetime import datetime return datetime.strptime(s, fmt).strftime('%d.%m.%Y') except ValueError: continue return s typ = aufmass.typ if aufmass else '' ist_teil = typ and 'Teilaufma' in typ ap_vorname = project.ansprechpartner_vorname or '' ap_nachname = project.ansprechpartner_nachname or '' ap_name = f'{ap_vorname} {ap_nachname}'.strip() lines = ['[Kopfdaten]'] lines.append(f'Teilaufma={"X" if ist_teil else ""}') lines.append(f'Schlussaufma={"X" if not ist_teil else ""}') lines.append(f'Datum={_d(project.datum)}') lines.append(f'Baustelle={project.baustelle or ""}') lines.append(f'AbrufNr={project.abruf_nr or ""}') lines.append(f'SMNr={project.sm_nr or ""}') lines.append(f'Vertrag={project.lv_name or ""}') lines.append(f'StartZ={_d(project.datum_start)}') lines.append(f'EndZ={_d(project.datum_ende)}') lines.append(f'AspaN={ap_name}') lines.append(f'AspaTel={project.ansprechpartner_tel or ""}') lines.append(f'Bauabschnitt={project.bauabschnitt or ""}') lines.append(f'Kolone=') lines.append('[Aufmaßdaten]') def g(v, is_money=False): if v is None or v == 0 or v == '': return '' try: n = float(str(v).replace(',','.')) except (ValueError, TypeError): return str(v) if is_money: return f'{n:.2f}'.replace('.',',') s = f'{n:.2f}'.replace('.',',') s = s.rstrip('0').rstrip(',') return s for p in positionen: rows = [ p.abschnitt or '', g(p.pos_nr) if p.pos_nr else '', g(p.faktor), g(p.laenge), g(p.breite), g(p.tiefe), g(p.menge), ] if p.einheit in ('ST', 'LE', 'STD', 'h', 'Psch'): rows[6] = g(p.faktor * 1) if p.faktor else '' rows += [ p.einheit or '', p.kurztext or '', p.bemerkung or '', g(p.menge_hinten), g(p.einzelpreis, is_money=True), g(p.gesamtpreis, is_money=True), ] lines.append('|' + '|'.join(rows)) buf = io.BytesIO() buf.write('\n'.join(lines).encode('utf-8')) buf.seek(0) return send_file(buf, as_attachment=True, download_name=_build_filename(project, '.txt'), mimetype='text/plain; charset=utf-8') @export_bp.route('//x31') @login_required def x31(project_id): project, aufmass, positionen = _get_aufmass_and_positions( project_id, request.args.get('aufmass_id', type=int), request.args.get('visible_ids') ) if not project: flash('Zugriff verweigert oder kein Aufmaß vorhanden.', 'danger') return redirect(url_for('aufmass.index')) xml_bytes = export_to_x31(project, aufmass, positionen) if xml_bytes is None: flash('Keine gültigen Positionen für X31-Export.', 'danger') return redirect(url_for('aufmass.aufmass_list', project_id=project_id)) buf = io.BytesIO(xml_bytes) buf.seek(0) return send_file(buf, as_attachment=True, download_name=_build_filename(project, '.x31'), mimetype='application/xml; charset=utf-8') @export_bp.route('//x31_california') @login_required def x31_california(project_id): project, aufmass, positionen = _get_aufmass_and_positions( project_id, request.args.get('aufmass_id', type=int), request.args.get('visible_ids') ) if not project: flash('Zugriff verweigert oder kein Aufmaß vorhanden.', 'danger') return redirect(url_for('aufmass.index')) xml_bytes = export_to_x31(project, aufmass, positionen) if xml_bytes is None: flash('Keine gültigen Positionen für X31 California-Export.', 'danger') return redirect(url_for('aufmass.aufmass_list', project_id=project_id)) ap_vorname = project.ansprechpartner_vorname or '' ap_nachname = project.ansprechpartner_nachname or '' owner_name = f'{ap_vorname} {ap_nachname}'.strip() s_lv_name = (project.lv_name or project.baustelle or '') cal_bytes = convert_to_california( xml_bytes, ref_prj_name=project.baustelle or '', ref_prj_id=s_lv_name[:20], owner_name=owner_name ) buf = io.BytesIO(cal_bytes) buf.seek(0) return send_file(buf, as_attachment=True, download_name=_build_filename(project, '.x31ca'), mimetype='application/xml; charset=utf-8') @export_bp.route('//zip_download', methods=['POST']) @login_required def zip_download(project_id): project = Project.query.get_or_404(project_id) if not current_user.hat_zugriff(project, 'lesen'): return jsonify({'error': 'Zugriff verweigert'}), 403 try: exports = json.loads(request.form.get('exports', '[]')) base_params = request.form.get('base_params', '') zip_name = request.form.get('zip_name', 'export.zip') except: return jsonify({'error': 'Ungültige Anfrage'}), 400 if not exports: return jsonify({'error': 'Keine Exporte ausgewählt'}), 400 from app.models.aufmass import Aufmass aufmass_id = request.args.get('aufmass_id') or (base_params.split('aufmass_id=')[1].split('&')[0] if 'aufmass_id=' in base_params else None) visible_ids = base_params.split('visible_ids=')[1].split('&')[0] if 'visible_ids=' in base_params else None aufmass = Aufmass.query.get(aufmass_id) if aufmass_id else Aufmass.query.filter_by(project_id=project_id).order_by(Aufmass.sortierung).first() if not aufmass: return jsonify({'error': 'Kein Aufmaß gefunden'}), 400 pos_query = Position.query.filter_by(aufmass_id=aufmass.id) if visible_ids: ids = [int(x) for x in visible_ids.split(',') if x.isdigit()] if ids: pos_query = pos_query.filter(Position.id.in_(ids)) positionen = pos_query.order_by(Position.sortierung).all() company = current_user.company if not current_user.is_superadmin() else None temp_files = [] fd, zip_path = tempfile.mkstemp(suffix='.zip') os.close(fd) temp_files.append(zip_path) try: with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf: for exp in exports: exp_id = exp['id'] suffix = exp['suffix'] if exp_id == 'excel': fd, path = tempfile.mkstemp(suffix='.xlsx') os.close(fd) temp_files.append(path) export_project_to_excel(project, aufmass, positionen, path, company=company) zf.write(path, f'Aufmass - Excel{suffix}') elif exp_id == 'pdf': fd, path = tempfile.mkstemp(suffix='.pdf') os.close(fd) temp_files.append(path) export_project_to_pdf(project, aufmass, positionen, path, company=company) zf.write(path, f'Aufmass - PDF{suffix}') elif exp_id == 'txt': buf = io.BytesIO() buf.write(_generate_txt_content(project, aufmass, positionen).encode('utf-8')) buf.seek(0) zf.writestr(f'Aufmass - TXT{suffix}', buf.getvalue()) elif exp_id == 'x31': xml_bytes = export_to_x31(project, aufmass, positionen) if xml_bytes: zf.writestr(f'Aufmass - X31{suffix}', xml_bytes) elif exp_id == 'x31ca': xml_bytes = export_to_x31(project, aufmass, positionen) if xml_bytes: ap_vorname = project.ansprechpartner_vorname or '' ap_nachname = project.ansprechpartner_nachname or '' owner_name = f'{ap_vorname} {ap_nachname}'.strip() s_lv_name = (project.lv_name or project.baustelle or '') cal_bytes = convert_to_california( xml_bytes, ref_prj_name=project.baustelle or '', ref_prj_id=s_lv_name[:20], owner_name=owner_name ) zf.writestr(f'Aufmass - X31 California{suffix}', cal_bytes) return send_file(zip_path, as_attachment=True, download_name=zip_name, mimetype='application/zip') finally: for path in temp_files: try: os.unlink(path) except: pass def _generate_txt_content(project, aufmass, positionen): def _d(v): if v is None: return '' if hasattr(v, 'strftime'): return v.strftime('%d.%m.%Y') s = str(v)[:10] for fmt in ('%Y-%m-%d', '%d.%m.%Y'): try: from datetime import datetime return datetime.strptime(s, fmt).strftime('%d.%m.%Y') except ValueError: continue return s typ = aufmass.typ if aufmass else '' ist_teil = typ and 'Teilaufma' in typ ap_vorname = project.ansprechpartner_vorname or '' ap_nachname = project.ansprechpartner_nachname or '' ap_name = f'{ap_vorname} {ap_nachname}'.strip() lines = ['[Kopfdaten]'] lines.append(f'Teilaufma={"X" if ist_teil else ""}') lines.append(f'Schlussaufma={"X" if not ist_teil else ""}') lines.append(f'Datum={_d(project.datum)}') lines.append(f'Baustelle={project.baustelle or ""}') lines.append(f'AbrufNr={project.abruf_nr or ""}') lines.append(f'SMNr={project.sm_nr or ""}') lines.append(f'Vertrag={project.lv_name or ""}') lines.append(f'StartZ={_d(project.datum_start)}') lines.append(f'EndZ={_d(project.datum_ende)}') lines.append(f'AspaN={ap_name}') lines.append(f'AspaTel={project.ansprechpartner_tel or ""}') lines.append(f'Bauabschnitt={project.bauabschnitt or ""}') lines.append(f'Kolone=') lines.append('[Aufmaßdaten]') def g(v, is_money=False): if v is None or v == 0 or v == '': return '' try: n = float(str(v).replace(',','.')) except (ValueError, TypeError): return str(v) if is_money: return f'{n:.2f}'.replace('.',',') s = f'{n:.2f}'.replace('.',',') s = s.rstrip('0').rstrip(',') return s for p in positionen: rows = [ p.abschnitt or '', g(p.pos_nr) if p.pos_nr else '', g(p.faktor), g(p.laenge), g(p.breite), g(p.tiefe), g(p.menge), ] if p.einheit in ('ST', 'LE', 'STD', 'h', 'Psch'): rows[6] = g(p.faktor * 1) if p.faktor else '' rows += [ p.einheit or '', p.kurztext or '', p.bemerkung or '', g(p.menge_hinten), g(p.einzelpreis, is_money=True), g(p.gesamtpreis, is_money=True), ] lines.append('|' + '|'.join(rows)) return '\n'.join(lines)