Initial commit – AufmaßCreater v2.35

This commit is contained in:
2026-06-10 11:03:43 +02:00
commit 84c933ea9c
2823 changed files with 490495 additions and 0 deletions
View File
+415
View File
@@ -0,0 +1,415 @@
import os
from flask import Blueprint, render_template, redirect, url_for, request, flash
from flask_login import login_required, current_user
from app.extensions import db
from app.models.company import Company
from app.models.user import User
from app.models.license import License, LicenseModule
from app.models.module import Module
from app.models.company_module import CompanyModule
from app.models.user_module import UserModulePermission
from app.services.license_service import get_aktive_module
from app.models.project import Project
from app.models.contract import Contract
from app.models.lv import LVPosition
from app.models.project_access import ProjectAccess
admin_bp = Blueprint('admin', __name__)
def _get_sichtbare_projekte():
if current_user.is_firmadmin():
return Project.query.filter_by(company_id=current_user.company_id)
zugriff_ids = db.session.query(ProjectAccess.project_id).filter_by(
user_id=current_user.id
).subquery()
return Project.query.filter(Project.id.in_(zugriff_ids))
@admin_bp.route('/')
@login_required
def dashboard():
if current_user.is_superadmin():
return redirect(url_for('superadmin.dashboard'))
modules = get_aktive_module(current_user.company_id, user=current_user)
projekte_query = _get_sichtbare_projekte()
projekte_anzahl = projekte_query.filter(Project.status == 'aktiv').count()
letztes_projekt = projekte_query.order_by(Project.erstellt_am.desc()).first()
contracts = Contract.query.filter_by(
company_id=current_user.company_id
).order_by(Contract.name).all()
lv_names = [r[0] for r in db.session.query(LVPosition.lv_name).filter_by(
company_id=current_user.company_id
).distinct().order_by(LVPosition.lv_name).all()]
# Project sums
projekte = projekte_query.order_by(Project.sm_nr).all()
from app.models.position import Position
from app.models.aufmass import Aufmass
projekte_mit_summe = []
gesamt_summe = 0.0
for p in projekte:
summe = db.session.query(db.func.coalesce(db.func.sum(Position.gesamtpreis), 0)).join(
Aufmass, Position.aufmass_id == Aufmass.id
).filter(Aufmass.project_id == p.id).scalar() or 0.0
projekte_mit_summe.append((p, summe))
gesamt_summe += summe
# Employee count
mitarbeiter_anzahl = User.query.filter_by(company_id=current_user.company_id).count()
# License info
licenses = License.query.filter_by(company_id=current_user.company_id, aktiv=True).all()
license_max_ma = sum(l.max_mitarbeiter for l in licenses)
license_module_count = 0
license_module_used = 0
for lic in licenses:
lms = LicenseModule.query.filter_by(license_id=lic.id, aktiv=True).all()
license_module_count += len(lms)
license_count = len(licenses)
# Profiles
from app.models.view_profile import ViewProfile
profile_list = ViewProfile.query.filter_by(
user_id=current_user.id, view_type='dashboard'
).order_by(ViewProfile.is_default.desc(), ViewProfile.name).all()
active_profile = next((p for p in profile_list if p.is_default), profile_list[0] if profile_list else None)
return render_template('admin/dashboard.html', modules=modules, titel='Dashboard',
projekte_anzahl=projekte_anzahl, letztes_projekt=letztes_projekt,
contracts=contracts, lv_names=lv_names,
projekte_mit_summe=projekte_mit_summe, gesamt_summe=gesamt_summe,
mitarbeiter_anzahl=mitarbeiter_anzahl,
license_count=license_count, license_max_ma=license_max_ma,
license_module_count=license_module_count,
license_module_used=license_module_used,
profile_list=profile_list, active_profile=active_profile)
@admin_bp.route('/profil', methods=['GET', 'POST'])
@login_required
def profil():
if request.method == 'POST':
current_user.vorname = request.form.get('vorname', '').strip()
current_user.nachname = request.form.get('nachname', '').strip()
if request.form.get('password'):
current_user.set_password(request.form['password'])
db.session.commit()
flash('Profil aktualisiert.', 'success')
return render_template('admin/profil.html', titel='Profil')
@admin_bp.route('/firma')
@login_required
def firma():
if not current_user.is_firmadmin():
flash('Keine Berechtigung.', 'danger')
return redirect(url_for('admin.dashboard'))
company = Company.query.get(current_user.company_id)
users = User.query.filter_by(company_id=current_user.company_id).all()
licenses = License.query.filter_by(company_id=current_user.company_id).all()
modules = get_aktive_module(company.id, user=current_user)
return render_template('admin/firma.html', company=company, users=users, licenses=licenses,
modules=modules, titel='Firma')
@admin_bp.route('/mitarbeiter/neu', methods=['POST'])
@login_required
def mitarbeiter_neu():
if not current_user.is_firmadmin():
flash('Keine Berechtigung.', 'danger')
return redirect(url_for('admin.firma'))
email = request.form.get('email', '').strip()
if User.query.filter_by(email=email).first():
flash('E-Mail existiert bereits.', 'danger')
return redirect(url_for('admin.firma'))
rolle = request.form.get('rolle', 'mitarbeiter')
if rolle not in ('mitarbeiter', 'firmadmin'):
rolle = 'mitarbeiter'
user = User(
company_id=current_user.company_id,
email=email,
vorname=request.form.get('vorname', '').strip(),
nachname=request.form.get('nachname', '').strip(),
rolle=rolle,
darf_projekte_anlegen=bool(request.form.get('darf_projekte_anlegen')),
darf_lv_verwalten=bool(request.form.get('darf_lv_verwalten')),
darf_preise_sehen=bool(request.form.get('darf_preise_sehen')),
darf_aufmass_verwalten=bool(request.form.get('darf_aufmass_verwalten')),
darf_evergabe_nutzen=bool(request.form.get('darf_evergabe_nutzen')),
darf_kopfdaten_holen=bool(request.form.get('darf_kopfdaten_holen')),
darf_aufmass_uebertragen=bool(request.form.get('darf_aufmass_uebertragen')),
)
user.set_password(request.form.get('password', '123456'))
db.session.add(user)
db.session.commit()
flash(f'Mitarbeiter {user.full_name} angelegt.', 'success')
return redirect(url_for('admin.firma'))
@admin_bp.route('/projekt/schnellanlage', methods=['POST'])
@login_required
def projekt_schnellanlage():
if not current_user.is_firmadmin() and not current_user.is_superadmin() and not current_user.darf_projekte_anlegen:
flash('Keine Berechtigung.', 'danger')
return redirect(url_for('admin.dashboard'))
contract_id = request.form.get('contract_id', type=int)
contract = Contract.query.get(contract_id) if contract_id else None
from datetime import datetime as dt
project = Project(
company_id=current_user.company_id,
contract_id=contract_id,
sm_nr=request.form.get('sm_nr', '').strip(),
bezeichnung=request.form.get('bezeichnung', '').strip(),
vertrag=contract.name if contract else '',
lv_name=request.form.get('lv_name', '').strip(),
status='aktiv', erstellt_von=current_user.id,
)
db.session.add(project)
db.session.commit()
flash(f'Projekt {project.sm_nr} angelegt.', 'success')
return redirect(url_for('aufmass.aufmass_list', project_id=project.id))
@admin_bp.route('/mitarbeiter/<int:user_id>/toggle')
@login_required
def mitarbeiter_toggle(user_id):
if not current_user.is_firmadmin():
flash('Keine Berechtigung.', 'danger')
return redirect(url_for('admin.firma'))
user = User.query.get_or_404(user_id)
if user.company_id != current_user.company_id:
flash('Zugriff verweigert.', 'danger')
return redirect(url_for('admin.firma'))
user.aktiv = not user.aktiv
db.session.commit()
flash(f'Benutzer {"aktiviert" if user.aktiv else "deaktiviert"}.', 'success')
return redirect(url_for('admin.firma'))
@admin_bp.route('/mitarbeiter/<int:user_id>/rechte', methods=['GET', 'POST'])
@login_required
def mitarbeiter_rechte(user_id):
if not current_user.is_firmadmin() and not current_user.is_superadmin():
flash('Keine Berechtigung.', 'danger')
return redirect(url_for('admin.dashboard'))
user = User.query.get_or_404(user_id)
if user.company_id != current_user.company_id and not current_user.is_superadmin():
flash('Zugriff verweigert.', 'danger')
return redirect(url_for('admin.firma'))
if request.method == 'POST':
user.darf_projekte_anlegen = bool(request.form.get('darf_projekte_anlegen'))
user.darf_lv_verwalten = bool(request.form.get('darf_lv_verwalten'))
user.darf_preise_sehen = bool(request.form.get('darf_preise_sehen'))
user.darf_aufmass_verwalten = bool(request.form.get('darf_aufmass_verwalten'))
user.darf_evergabe_nutzen = bool(request.form.get('darf_evergabe_nutzen'))
user.darf_kopfdaten_holen = bool(request.form.get('darf_kopfdaten_holen'))
user.darf_aufmass_uebertragen = bool(request.form.get('darf_aufmass_uebertragen'))
db.session.commit()
flash('Rechte aktualisiert.', 'success')
return redirect(url_for('admin.firma'))
projekte = Project.query.filter_by(company_id=current_user.company_id).order_by(Project.sm_nr).all()
zugriffe = {pa.project_id: pa for pa in ProjectAccess.query.filter_by(user_id=user.id).all()}
modules = get_aktive_module(current_user.company_id, user=current_user)
user_modules = {um.module_id for um in UserModulePermission.query.filter_by(user_id=user.id, aktiv=True).all()}
return render_template('admin/rechte.html', user=user, projekte=projekte, zugriffe=zugriffe,
modules=modules, user_modules=user_modules, titel='Rechte')
@admin_bp.route('/mitarbeiter/<int:user_id>/projekt-zugriff', methods=['POST'])
@login_required
def mitarbeiter_projekt_zugriff(user_id):
if not current_user.is_firmadmin():
return 'Keine Berechtigung', 403
user = User.query.get_or_404(user_id)
if user.company_id != current_user.company_id:
return 'Zugriff verweigert', 403
project_id = request.form.get('project_id', type=int)
zugriff = request.form.get('zugriff', '')
if not project_id or zugriff not in ('lesen', 'schreiben', ''):
return 'Ungültige Daten', 400
existing = ProjectAccess.query.filter_by(user_id=user.id, project_id=project_id).first()
if zugriff:
if existing:
existing.zugriff = zugriff
else:
pa = ProjectAccess(user_id=user.id, project_id=project_id, zugriff=zugriff)
db.session.add(pa)
elif existing:
db.session.delete(existing)
db.session.commit()
return redirect(url_for('admin.mitarbeiter_rechte', user_id=user.id))
@admin_bp.route('/mitarbeiter/<int:user_id>/bearbeiten', methods=['GET', 'POST'])
@login_required
def mitarbeiter_bearbeiten(user_id):
if not current_user.is_firmadmin() and not current_user.is_superadmin():
flash('Keine Berechtigung.', 'danger')
return redirect(url_for('admin.firma'))
user = User.query.get_or_404(user_id)
if user.company_id != current_user.company_id and not current_user.is_superadmin():
flash('Zugriff verweigert.', 'danger')
return redirect(url_for('admin.firma'))
if request.method == 'POST':
email = request.form.get('email', '').strip()
if email and email != user.email:
if User.query.filter_by(email=email).first():
flash('E-Mail existiert bereits.', 'danger')
return redirect(url_for('admin.mitarbeiter_bearbeiten', user_id=user.id))
user.email = email
user.vorname = request.form.get('vorname', '').strip()
user.nachname = request.form.get('nachname', '').strip()
rolle = request.form.get('rolle', '')
if rolle in ('mitarbeiter', 'firmadmin'):
user.rolle = rolle
if request.form.get('password'):
user.set_password(request.form['password'])
user.darf_projekte_anlegen = bool(request.form.get('darf_projekte_anlegen'))
user.darf_lv_verwalten = bool(request.form.get('darf_lv_verwalten'))
user.darf_preise_sehen = bool(request.form.get('darf_preise_sehen'))
user.darf_aufmass_verwalten = bool(request.form.get('darf_aufmass_verwalten'))
user.darf_evergabe_nutzen = bool(request.form.get('darf_evergabe_nutzen'))
user.darf_kopfdaten_holen = bool(request.form.get('darf_kopfdaten_holen'))
user.darf_aufmass_uebertragen = bool(request.form.get('darf_aufmass_uebertragen'))
db.session.commit()
flash(f'Benutzer {user.full_name} aktualisiert.', 'success')
if current_user.is_superadmin():
return redirect(url_for('superadmin.firma_detail', company_id=user.company_id))
return redirect(url_for('admin.firma'))
company = Company.query.get(user.company_id)
return render_template('admin/mitarbeiter_bearbeiten.html', user=user, company=company, titel='Benutzer bearbeiten')
@admin_bp.route('/mitarbeiter/<int:user_id>/loeschen', methods=['POST'])
@login_required
def mitarbeiter_loeschen(user_id):
if not current_user.is_firmadmin() and not current_user.is_superadmin():
flash('Keine Berechtigung.', 'danger')
return redirect(url_for('admin.firma'))
user = User.query.get_or_404(user_id)
if user.company_id != current_user.company_id and not current_user.is_superadmin():
flash('Zugriff verweigert.', 'danger')
return redirect(url_for('admin.firma'))
if user.id == current_user.id:
flash('Sie können sich nicht selbst löschen.', 'danger')
return redirect(url_for('admin.firma'))
email = user.email
ProjectAccess.query.filter_by(user_id=user.id).delete()
UserModulePermission.query.filter_by(user_id=user.id).delete()
db.session.delete(user)
db.session.commit()
flash(f'Benutzer {email} gelöscht.', 'success')
return redirect(url_for('admin.firma'))
@admin_bp.route('/mitarbeiter/<int:user_id>/module/<int:module_id>/toggle')
@login_required
def mitarbeiter_module_toggle(user_id, module_id):
if not current_user.is_firmadmin():
flash('Keine Berechtigung.', 'danger')
return redirect(url_for('admin.firma'))
user = User.query.get_or_404(user_id)
if user.company_id != current_user.company_id:
flash('Zugriff verweigert.', 'danger')
return redirect(url_for('admin.firma'))
if user.is_firmadmin():
flash('Firmadmin hat automatisch alle Module.', 'info')
return redirect(url_for('admin.mitarbeiter_rechte', user_id=user.id))
um = UserModulePermission.query.filter_by(user_id=user.id, module_id=module_id).first()
if um:
um.aktiv = not um.aktiv
else:
um = UserModulePermission(user_id=user.id, module_id=module_id, aktiv=True)
db.session.add(um)
db.session.commit()
return redirect(url_for('admin.mitarbeiter_rechte', user_id=user.id))
@admin_bp.route('/avatar/upload', methods=['POST'])
@login_required
def avatar_upload():
file = request.files.get('avatar')
if not file:
flash('Keine Datei ausgewählt.', 'danger')
return redirect(url_for('admin.profil'))
import os
from flask import current_app
ext = os.path.splitext(file.filename)[1].lower()
if ext not in ('.png', '.jpg', '.jpeg', '.gif'):
flash('Nur PNG, JPG, GIF erlaubt.', 'danger')
return redirect(url_for('admin.profil'))
upload_dir = os.path.join(current_app.root_path, 'static', 'avatars')
os.makedirs(upload_dir, exist_ok=True)
filename = f'avatar_{current_user.id}{ext}'
file.save(os.path.join(upload_dir, filename))
for old_ext in ('.png', '.jpg', '.jpeg', '.gif'):
old_path = os.path.join(upload_dir, f'avatar_{current_user.id}{old_ext}')
if old_ext != ext and os.path.exists(old_path):
os.remove(old_path)
current_user.profile_image = filename
db.session.commit()
flash('Profilbild aktualisiert.', 'success')
return redirect(url_for('admin.profil'))
@admin_bp.route('/avatar/entfernen', methods=['POST'])
@login_required
def avatar_entfernen():
import os
from flask import current_app
if current_user.profile_image:
path = os.path.join(current_app.root_path, 'static', 'avatars', current_user.profile_image)
if os.path.exists(path):
os.remove(path)
current_user.profile_image = None
db.session.commit()
flash('Profilbild entfernt.', 'success')
return redirect(url_for('admin.profil'))
@admin_bp.route('/firma/logo')
@login_required
def firma_logo():
if not current_user.is_firmadmin():
return '', 403
company = Company.query.get(current_user.company_id)
if not company or not company.logo or not os.path.exists(company.logo):
return '', 404
from flask import send_file
return send_file(company.logo)
@admin_bp.route('/firma/logo-upload', methods=['POST'])
@login_required
def firma_logo_upload():
if not current_user.is_firmadmin():
flash('Keine Berechtigung.', 'danger')
return redirect(url_for('admin.firma'))
company = Company.query.get(current_user.company_id)
if request.form.get('delete'):
if company.logo and os.path.exists(company.logo):
os.remove(company.logo)
company.logo = None
db.session.commit()
flash('Firmenlogo entfernt.', 'success')
return redirect(url_for('admin.firma'))
file = request.files.get('logo')
if not file or file.filename == '':
flash('Keine Datei ausgewählt.', 'danger')
return redirect(url_for('admin.firma'))
ext = os.path.splitext(file.filename)[1].lower()
if ext not in ('.png', '.jpg', '.jpeg', '.gif', '.webp'):
flash('Nur Bildformate: PNG, JPG, GIF, WebP.', 'danger')
return redirect(url_for('admin.firma'))
from flask import current_app
upload_dir = os.path.join(current_app.root_path, '..', 'data', 'uploads', 'logos')
os.makedirs(upload_dir, exist_ok=True)
filename = f'logo_{company.id}{ext}'
filepath = os.path.join(upload_dir, filename)
file.save(filepath)
company.logo = filepath
db.session.commit()
flash('Firmenlogo erfolgreich hochgeladen.', 'success')
return redirect(url_for('admin.firma'))
@admin_bp.route('/firma/evergabe', methods=['POST'])
@login_required
def firma_evergabe_save():
if not current_user.is_firmadmin():
flash('Keine Berechtigung.', 'danger')
return redirect(url_for('admin.firma'))
company = Company.query.get(current_user.company_id)
if not company.evergabe_aktiviert:
flash('E-Vergabe ist für Ihre Firma nicht freigeschaltet.', 'danger')
return redirect(url_for('admin.firma'))
company.evergabe_benutzer = request.form.get('evergabe_benutzer', '').strip()
company.evergabe_passwort = request.form.get('evergabe_passwort', '').strip()
company.evergabe_name = request.form.get('evergabe_name', '').strip()
db.session.commit()
flash('E-Vergabe Logindaten gespeichert.', 'success')
return redirect(url_for('admin.firma'))
File diff suppressed because it is too large Load Diff
+88
View File
@@ -0,0 +1,88 @@
from flask import Blueprint, render_template, redirect, url_for, request, flash, current_app
from flask_login import login_user, logout_user, login_required, current_user
from app.extensions import db
from app.models.user import User
from app.models.company import Company
from app.models.settings import Settings
auth_bp = Blueprint('auth', __name__)
@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
if current_user.is_superadmin():
return redirect(url_for('superadmin.dashboard'))
return redirect(url_for('admin.dashboard'))
if request.method == 'POST':
email = request.form.get('email', '').strip()
password = request.form.get('password', '')
user = User.query.filter_by(email=email).first()
if user and user.check_password(password) and user.aktiv:
company = Company.query.get(user.company_id)
if (user.is_superadmin() or (company and company.aktiv)):
login_user(user)
user.letzter_login = __import__('datetime').datetime.utcnow()
db.session.commit()
if user.is_superadmin():
return redirect(url_for('superadmin.dashboard'))
return redirect(url_for('admin.dashboard'))
flash('Ungültige Anmeldedaten oder Konto deaktiviert.', 'danger')
reg_enabled = Settings.get('registration_enabled', 'false') == 'true'
return render_template('auth/login.html', registration_enabled=reg_enabled)
@auth_bp.route('/register', methods=['GET', 'POST'])
def register():
if Settings.get('registration_enabled', 'false') != 'true':
flash('Registrierung ist derzeit deaktiviert.', 'warning')
return redirect(url_for('auth.login'))
if request.method == 'POST':
firmenname = request.form.get('firmenname', '').strip()
email = request.form.get('email', '').strip()
password = request.form.get('password', '')
vorname = request.form.get('vorname', '').strip()
nachname = request.form.get('nachname', '').strip()
if not firmenname or not email or not password:
flash('Bitte alle Pflichtfelder ausfüllen.', 'danger')
return render_template('auth/register.html')
if User.query.filter_by(email=email).first():
flash('E-Mail bereits registriert.', 'danger')
return render_template('auth/register.html')
slug = firmenname.lower().replace(' ', '-').replace('ä', 'ae').replace('ö', 'oe').replace('ü', 'ue')[:100]
base_slug = slug
counter = 1
while Company.query.filter_by(slug=slug).first():
slug = f"{base_slug}-{counter}"
counter += 1
company = Company(name=firmenname, slug=slug)
db.session.add(company)
db.session.flush()
user = User(
company_id=company.id,
email=email,
vorname=vorname,
nachname=nachname,
rolle='firmadmin',
darf_projekte_anlegen=True,
darf_lv_verwalten=True,
darf_preise_sehen=True,
darf_aufmass_verwalten=True,
)
user.set_password(password)
db.session.add(user)
db.session.commit()
flash('Registrierung erfolgreich! Sie können sich jetzt anmelden.', 'success')
return redirect(url_for('auth.login'))
return render_template('auth/register.html')
@auth_bp.route('/logout')
@login_required
def logout():
logout_user()
return redirect(url_for('auth.login'))
+128
View File
@@ -0,0 +1,128 @@
from flask import Blueprint, render_template, request, flash, redirect, url_for, jsonify
from flask_login import login_required, current_user
from app.extensions import db
from app.models.contract import Contract
from app.models.lv import LVPosition
from datetime import datetime
contracts_bp = Blueprint('contracts', __name__)
def _vertrag_berechtigt():
if current_user.is_superadmin():
return True
if current_user.is_firmadmin() or current_user.darf_lv_verwalten:
return True
return False
@contracts_bp.route('/')
@login_required
def index():
if current_user.is_superadmin():
contracts = Contract.query.order_by(Contract.name).all()
else:
contracts = Contract.query.filter_by(
company_id=current_user.company_id
).order_by(Contract.name).all()
return render_template('contracts/index.html', contracts=contracts, titel='Verträge')
@contracts_bp.route('/neu', methods=['GET', 'POST'])
@login_required
def neu():
if not _vertrag_berechtigt():
flash('Keine Berechtigung.', 'danger')
return redirect(url_for('contracts.index'))
if request.method == 'POST':
c = Contract(
company_id=current_user.company_id,
name=request.form.get('name', '').strip(),
belegnummer=request.form.get('belegnummer', '').strip(),
beleg_datum=_parse_date(request.form.get('beleg_datum')),
laufzeit_start=_parse_date(request.form.get('laufzeit_start')),
laufzeit_ende=_parse_date(request.form.get('laufzeit_ende')),
status=request.form.get('status', 'NEU'),
)
db.session.add(c)
db.session.commit()
flash(f'Vertrag "{c.name}" angelegt.', 'success')
return redirect(url_for('contracts.index'))
return render_template('contracts/neu.html', titel='Neuer Vertrag')
@contracts_bp.route('/<int:contract_id>')
@login_required
def detail(contract_id):
c = Contract.query.get_or_404(contract_id)
if c.company_id != current_user.company_id and not current_user.is_superadmin():
return 'Zugriff verweigert', 403
lv_names = db.session.query(LVPosition.lv_name).filter_by(
company_id=current_user.company_id, contract_id=contract_id
).distinct().order_by(LVPosition.lv_name).all()
lv_names = [r[0] for r in lv_names]
return render_template('contracts/detail.html', contract=c, lv_names=lv_names, titel=c.name)
@contracts_bp.route('/<int:contract_id>/status', methods=['POST'])
@login_required
def status_set(contract_id):
c = Contract.query.get_or_404(contract_id)
if c.company_id != current_user.company_id and not current_user.is_superadmin():
return 'Zugriff verweigert', 403
if not _vertrag_berechtigt():
flash('Keine Berechtigung.', 'danger')
return redirect(url_for('contracts.index'))
c.status = request.form.get('status', c.status)
db.session.commit()
flash('Status aktualisiert.', 'success')
return redirect(url_for('contracts.detail', contract_id=contract_id))
@contracts_bp.route('/<int:contract_id>/update', methods=['POST'])
@login_required
def detail_update(contract_id):
c = Contract.query.get_or_404(contract_id)
if c.company_id != current_user.company_id and not current_user.is_superadmin():
return 'Zugriff verweigert', 403
if not _vertrag_berechtigt():
flash('Keine Berechtigung.', 'danger')
return redirect(url_for('contracts.index'))
c.belegnummer = request.form.get('belegnummer', '').strip()
c.beleg_datum = _parse_date(request.form.get('beleg_datum'))
c.laufzeit_start = _parse_date(request.form.get('laufzeit_start'))
c.laufzeit_ende = _parse_date(request.form.get('laufzeit_ende'))
c.status = request.form.get('status', c.status)
db.session.commit()
if request.headers.get('X-Requested-With') == 'XMLHttpRequest' or request.accept_mimetypes.best == 'application/json':
return jsonify({'ok': True})
flash('Vertrag aktualisiert.', 'success')
return redirect(url_for('contracts.detail', contract_id=contract_id))
@contracts_bp.route('/<int:contract_id>/loeschen', methods=['POST'])
@login_required
def delete(contract_id):
c = Contract.query.get_or_404(contract_id)
if c.company_id != current_user.company_id and not current_user.is_superadmin():
return 'Zugriff verweigert', 403
if not _vertrag_berechtigt():
flash('Keine Berechtigung.', 'danger')
return redirect(url_for('contracts.index'))
db.session.delete(c)
db.session.commit()
flash('Vertrag gelöscht.', 'success')
return redirect(url_for('contracts.index'))
@contracts_bp.route('/api/lv-names')
@login_required
def api_lv_names():
contract_id = request.args.get('contract_id', type=int)
q = db.session.query(LVPosition.lv_name).filter_by(company_id=current_user.company_id)
if contract_id:
q = q.filter_by(contract_id=contract_id)
names = [r[0] for r in q.distinct().order_by(LVPosition.lv_name).all()]
return jsonify(names)
def _parse_date(s):
if not s:
return None
for fmt in ('%Y-%m-%d', '%d.%m.%Y', '%Y.%m.%d'):
try:
return datetime.strptime(s.strip(), fmt).date()
except ValueError:
continue
return None
+414
View File
@@ -0,0 +1,414 @@
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify
from flask_login import login_required, current_user
from app.extensions import db
from app.models.custom_module import CustomModule, CustomModuleAssignment
from app.models.project import Project
from app.models.aufmass import Aufmass
from app.models.position import Position
from app.models.user import User
from app.models.company import Company
from app.services.custom_module_renderer import render_form
from app.services.custom_module_executor import execute_rules
import json
custom_modules_bp = Blueprint('custom_modules', __name__, url_prefix='/custom-modules')
def _can_manage(user, module=None):
if user.is_superadmin():
return True
if user.is_firmadmin():
if module is None:
return True
if module.is_template:
return False
return module.company_id == user.company_id
return False
def _can_use(user, module):
if user.is_superadmin():
return True
if user.is_firmadmin() and module.company_id == user.company_id:
return True
if not module.is_active:
return False
return CustomModuleAssignment.query.filter_by(
module_id=module.id, user_id=user.id
).first() is not None
@custom_modules_bp.route('/')
@login_required
def index():
if not _can_manage(current_user):
flash('Keine Berechtigung.', 'danger')
return redirect(url_for('admin.dashboard'))
if current_user.is_superadmin():
templates = CustomModule.query.filter_by(is_template=True).order_by(CustomModule.sort_index).all()
company_modules = CustomModule.query.filter_by(is_template=False).order_by(CustomModule.company_id, CustomModule.sort_index).all()
companies = Company.query.order_by(Company.name).all()
else:
templates = CustomModule.query.filter_by(is_template=True, is_active=True).order_by(CustomModule.sort_index).all()
company_modules = CustomModule.query.filter_by(
company_id=current_user.company_id, is_template=False
).order_by(CustomModule.sort_index).all()
companies = []
return render_template('custom_modules/index.html',
templates=templates,
company_modules=company_modules,
companies=companies)
@custom_modules_bp.route('/neu', methods=['GET', 'POST'])
@login_required
def neu():
if not _can_manage(current_user):
flash('Keine Berechtigung.', 'danger')
return redirect(url_for('admin.dashboard'))
if request.method == 'POST':
name = request.form.get('name', '').strip()
if not name:
flash('Name ist erforderlich.', 'danger')
return render_template('custom_modules/edit.html', module=None)
module = CustomModule(
company_id=current_user.company_id if not current_user.is_superadmin() else None,
name=name,
description=request.form.get('description', ''),
kategorie=request.form.get('kategorie', 'allgemein'),
icon=request.form.get('icon', '🔧'),
is_template=bool(request.form.get('is_template')) if current_user.is_superadmin() else False,
form_json='[]',
rules_json='[]',
created_by=current_user.id,
)
db.session.add(module)
db.session.commit()
flash(f'Modul "{name}" erstellt.', 'success')
return redirect(url_for('custom_modules.edit', module_id=module.id))
return render_template('custom_modules/edit.html', module=None,
is_superadmin=current_user.is_superadmin())
@custom_modules_bp.route('/<int:module_id>/bearbeiten', methods=['GET', 'POST'])
@login_required
def edit(module_id):
module = CustomModule.query.get_or_404(module_id)
if not _can_manage(current_user, module):
flash('Keine Berechtigung.', 'danger')
return redirect(url_for('admin.dashboard'))
if request.method == 'POST':
module.name = request.form.get('name', module.name).strip()
module.description = request.form.get('description', '')
module.kategorie = request.form.get('kategorie', 'allgemein')
module.icon = request.form.get('icon', '🔧')
module.is_active = bool(request.form.get('is_active', True))
if current_user.is_superadmin():
module.is_template = bool(request.form.get('is_template', module.is_template))
db.session.commit()
flash('Modul aktualisiert.', 'success')
return redirect(url_for('custom_modules.index'))
users = []
if module.company_id:
users = User.query.filter_by(company_id=module.company_id, aktiv=True).order_by(User.vorname).all()
assignments = {a.user_id: a for a in module.assignments}
return render_template('custom_modules/edit.html', module=module,
users=users, assignments=assignments,
is_superadmin=current_user.is_superadmin())
@custom_modules_bp.route('/<int:module_id>/builder')
@login_required
def builder(module_id):
module = CustomModule.query.get_or_404(module_id)
if not _can_manage(current_user, module):
flash('Keine Berechtigung.', 'danger')
return redirect(url_for('admin.dashboard'))
return render_template('custom_modules/builder.html', module=module)
@custom_modules_bp.route('/<int:module_id>/loeschen', methods=['POST'])
@login_required
def loeschen(module_id):
module = CustomModule.query.get_or_404(module_id)
if not _can_manage(current_user, module):
flash('Keine Berechtigung.', 'danger')
return redirect(url_for('admin.dashboard'))
name = module.name
db.session.delete(module)
db.session.commit()
flash(f'Modul "{name}" gelöscht.', 'success')
return redirect(url_for('custom_modules.index'))
@custom_modules_bp.route('/<int:module_id>/importieren', methods=['POST'])
@login_required
def importieren(module_id):
template = CustomModule.query.get_or_404(module_id)
if not template.is_template:
flash('Nur Vorlagen können importiert werden.', 'danger')
return redirect(url_for('custom_modules.index'))
if not current_user.is_firmadmin():
flash('Keine Berechtigung.', 'danger')
return redirect(url_for('custom_modules.index'))
existing = CustomModule.query.filter_by(
company_id=current_user.company_id,
original_template_id=template.id
).first()
if existing:
flash(f'Vorlage "{template.name}" bereits als "{existing.name}" importiert.', 'warning')
return redirect(url_for('custom_modules.index'))
copy = CustomModule(
company_id=current_user.company_id,
original_template_id=template.id,
name=template.name,
description=template.description,
kategorie=template.kategorie,
icon=template.icon,
form_json=template.form_json,
rules_json=template.rules_json,
is_template=False,
created_by=current_user.id,
)
db.session.add(copy)
db.session.commit()
flash(f'Vorlage "{template.name}" importiert jetzt kannst du sie anpassen.', 'success')
return redirect(url_for('custom_modules.edit', module_id=copy.id))
@custom_modules_bp.route('/<int:module_id>/als-vorlage', methods=['POST'])
@login_required
def als_vorlage(module_id):
module = CustomModule.query.get_or_404(module_id)
if not current_user.is_superadmin():
flash('Keine Berechtigung.', 'danger')
return redirect(url_for('admin.dashboard'))
template = CustomModule(
company_id=None,
original_template_id=None,
name=module.name,
description=module.description,
kategorie=module.kategorie,
icon=module.icon,
form_json=module.form_json,
rules_json=module.rules_json,
is_template=True,
created_by=current_user.id,
)
db.session.add(template)
db.session.commit()
flash(f'Modul "{module.name}" als globale Vorlage gespeichert.', 'success')
return redirect(url_for('custom_modules.index'))
@custom_modules_bp.route('/<int:module_id>/user/<int:user_id>/toggle', methods=['POST'])
@login_required
def user_toggle(module_id, user_id):
module = CustomModule.query.get_or_404(module_id)
if not _can_manage(current_user, module):
return jsonify({'error': 'Keine Berechtigung'}), 403
user = User.query.get_or_404(user_id)
assignment = CustomModuleAssignment.query.filter_by(
module_id=module.id, user_id=user.id
).first()
data = request.get_json(silent=True) or {}
active = data.get('active', False)
can_edit = data.get('can_edit', False)
if active:
if not assignment:
assignment = CustomModuleAssignment(
module_id=module.id, user_id=user.id, can_edit=can_edit
)
db.session.add(assignment)
db.session.commit()
return jsonify({'message': f'{user.full_name} hat jetzt Zugriff.', 'active': True})
return jsonify({'active': True})
else:
if assignment:
db.session.delete(assignment)
db.session.commit()
return jsonify({'message': f'Zugriff für {user.full_name} entzogen.', 'active': False})
return jsonify({'active': False})
@custom_modules_bp.route('/<int:module_id>/api/form-json', methods=['GET', 'PUT'])
@login_required
def api_form_json(module_id):
module = CustomModule.query.get_or_404(module_id)
if not _can_manage(current_user, module):
return jsonify({'error': 'Keine Berechtigung'}), 403
if request.method == 'PUT':
try:
data = request.get_json(force=True)
module.form_json = json.dumps(data, ensure_ascii=False)
db.session.commit()
return jsonify({'ok': True})
except Exception as e:
return jsonify({'error': str(e)}), 400
return jsonify(json.loads(module.form_json or '[]'))
@custom_modules_bp.route('/<int:module_id>/api/rules-json', methods=['GET', 'PUT'])
@login_required
def api_rules_json(module_id):
module = CustomModule.query.get_or_404(module_id)
if not _can_manage(current_user, module):
return jsonify({'error': 'Keine Berechtigung'}), 403
if request.method == 'PUT':
try:
data = request.get_json(force=True)
module.rules_json = json.dumps(data, ensure_ascii=False)
db.session.commit()
return jsonify({'ok': True})
except Exception as e:
return jsonify({'error': str(e)}), 400
return jsonify(json.loads(module.rules_json or '[]'))
@custom_modules_bp.route('/<int:module_id>/api/preview')
@login_required
def api_preview(module_id):
module = CustomModule.query.get_or_404(module_id)
if not _can_manage(current_user, module):
return '<div class="notification is-danger">Keine Berechtigung.</div>'
html = render_form(module.form_json, module_id)
return html
@custom_modules_bp.route('/api/available')
@login_required
def api_available():
modules = CustomModule.query.filter(
CustomModule.is_active == True,
CustomModule.is_template == False,
CustomModule.company_id == current_user.company_id
).order_by(CustomModule.sort_index).all()
result = []
for m in modules:
if current_user.is_firmadmin() or current_user.is_superadmin():
result.append({'id': m.id, 'name': m.name, 'icon': m.icon, 'kategorie': m.kategorie})
elif _can_use(current_user, m):
result.append({'id': m.id, 'name': m.name, 'icon': m.icon, 'kategorie': m.kategorie})
return jsonify(result)
@custom_modules_bp.route('/<int:module_id>/formular')
@login_required
def formular(module_id):
module = CustomModule.query.get_or_404(module_id)
if not _can_use(current_user, module):
return '<div class="notification is-danger">Keine Berechtigung für dieses Modul.</div>'
aufmass_id = request.args.get('aufmass_id')
html = render_form(module.form_json, module_id, aufmass_id=aufmass_id)
return html
@custom_modules_bp.route('/<int:module_id>/berechnen', methods=['POST'])
@login_required
def berechnen(module_id):
module = CustomModule.query.get_or_404(module_id)
if not _can_use(current_user, module):
return '<div class="notification is-danger">Keine Berechtigung.</div>'
aufmass_id = request.args.get('aufmass_id')
if not aufmass_id:
return '<div class="notification is-danger">Kein Aufmaß ausgewählt.<br><button class="button is-small is-light mt-2" onclick="closeModulModal()">Schließen</button></div>'
aufmass = Aufmass.query.get_or_404(aufmass_id)
project = Project.query.get(aufmass.project_id)
if not project:
return '<div class="notification is-danger">Projekt nicht gefunden.<br><button class="button is-small is-light mt-2" onclick="closeModulModal()">Schließen</button></div>'
project_id = project.id
company_id = project.company_id
form_data = {k: v for k, v in request.form.items()}
rules = json.loads(module.rules_json) if isinstance(module.rules_json, str) else module.rules_json
positions_data = execute_rules(form_data, module.rules_json, company_id)
if not positions_data:
return '''
<div class="notification is-warning" style="margin-top:10px">
Keine Bedingungen erfüllt es wurden keine Positionen erzeugt.
<button class="delete" onclick="closeModulModal()"></button>
</div>
<button class="button is-small is-light mt-2" onclick="closeModulModal()">Schließen</button>'''
created = 0
for pd in positions_data:
pos = Position(
aufmass_id=aufmass.id,
project_id=project_id,
pos_nr=pd.get('pos_nr', ''),
kurztext=pd.get('kurztext', ''),
langtext=pd.get('langtext', ''),
einheit=pd.get('einheit', 'ST'),
menge=pd.get('menge', 0),
faktor=pd.get('faktor', 1),
laenge=pd.get('laenge', 0),
breite=pd.get('breite', 0),
tiefe=pd.get('tiefe', 0),
einzelpreis=pd.get('einzelpreis', 0),
bemerkung=pd.get('bemerkung', ''),
sort_index=pd.get('sort_index', 0),
)
db.session.add(pos)
created += 1
db.session.commit()
return f'''<div class="notification is-success">
<strong>{created} Positionen</strong> ins Aufmaß übernommen.
<span class="is-pulled-right tag is-light is-info">Seite wird neu geladen...</span>
</div>
<script>
setTimeout(function() {{
var m = document.getElementById('modul-modal');
if (m) m.classList.remove('is-active');
location.reload();
}}, 1200);
</script>'''
@custom_modules_bp.route('/api/sort-batch', methods=['POST'])
@login_required
def sort_batch():
if not _can_manage(current_user):
return jsonify({'error': 'Keine Berechtigung'}), 403
data = request.get_json(force=True)
if not isinstance(data, list):
return jsonify({'error': 'Erwarte Liste von {id, sort_index}'}), 400
for item in data:
module = CustomModule.query.get(item.get('id'))
if not module:
continue
if not _can_manage(current_user, module):
continue
module.sort_index = item.get('sort_index', 0)
db.session.commit()
return jsonify({'ok': True, 'updated': len(data)})
@custom_modules_bp.route('/<int:module_id>/sort', methods=['POST'])
@login_required
def sort(module_id):
module = CustomModule.query.get_or_404(module_id)
if not _can_manage(current_user, module):
return jsonify({'error': 'Keine Berechtigung'}), 403
data = request.get_json(force=True)
sort_index = data.get('sort_index', 0)
module.sort_index = sort_index
db.session.commit()
return jsonify({'ok': True})
+433
View File
@@ -0,0 +1,433 @@
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('/<int:project_id>/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('/<int:project_id>/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('/<int:project_id>/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('/<int:project_id>/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('/<int:project_id>/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('/<int:project_id>/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)
+236
View File
@@ -0,0 +1,236 @@
from flask import Blueprint, render_template, request, jsonify, flash, redirect, url_for
from flask_login import login_required, current_user
from app.extensions import db
from app.models.lv import LVPosition
from app.models.contract import Contract
from app.models.view_profile import ViewProfile
import json
lv_bp = Blueprint('lv', __name__)
def _lv_berechtigt():
if current_user.is_superadmin():
return True
if current_user.is_firmadmin() or current_user.darf_lv_verwalten:
return True
return False
@lv_bp.route('/')
@login_required
def index():
if not _lv_berechtigt():
flash('Keine Berechtigung für LV-Verwaltung.', 'danger')
return redirect(url_for('admin.dashboard'))
lv_names = db.session.query(LVPosition.lv_name).filter_by(
company_id=current_user.company_id
).distinct().order_by(LVPosition.lv_name).all()
lv_names = [r[0] for r in lv_names]
selected_lv = request.args.get('lv', lv_names[0] if lv_names else '')
search = request.args.get('q', '').strip()
sort_col = request.args.get('sort', '')
sort_dir = request.args.get('dir', 'asc')
base = LVPosition.query.filter_by(
company_id=current_user.company_id, lv_name=selected_lv
)
if search:
like = f'%{search}%'
base = base.filter(
db.or_(LVPosition.pos_nr.like(like), LVPosition.kurztext.like(like))
)
sort_map = {
'pos_nr': LVPosition.pos_nr,
'text': LVPosition.kurztext,
'einheit': LVPosition.einheit,
'ep': LVPosition.einzelpreis,
}
if sort_col in sort_map:
col = sort_map[sort_col]
order = col.asc() if sort_dir == 'asc' else col.desc()
positionen = base.order_by(LVPosition.favorite.desc(), order).all()
else:
positionen = base.order_by(LVPosition.favorite.desc(), LVPosition.order_index).all()
contracts = Contract.query.filter_by(company_id=current_user.company_id).order_by(Contract.name).all()
view_id = request.args.get('view_id', type=int)
view_config = ViewProfile.get_default_config()
if view_id:
vp = ViewProfile.query.get(view_id)
if vp and vp.user_id == current_user.id:
view_config = vp.get_config()
else:
default_vp = ViewProfile.query.filter_by(
user_id=current_user.id, view_type='lv', is_default=True
).first()
if default_vp:
view_config = default_vp.get_config()
preise_sichtbar = current_user.is_firmadmin() or current_user.darf_preise_sehen
return render_template('lv/index.html',
lv_names=lv_names, selected_lv=selected_lv,
positionen=positionen, search=search,
contracts=contracts,
view_config=view_config,
view_config_json=json.dumps(view_config),
preise_sichtbar=preise_sichtbar,
titel='Leistungsverzeichnis')
@lv_bp.route('/neu', methods=['POST'])
@login_required
def neu_lv():
if not _lv_berechtigt():
flash('Keine Berechtigung.', 'danger')
return redirect(url_for('lv.index'))
lv_name = request.form.get('lv_name', '').strip()
if not lv_name:
flash('LV-Name erforderlich.', 'danger')
return redirect(url_for('lv.index'))
return redirect(url_for('lv.index', lv=lv_name))
@lv_bp.route('/position/neu', methods=['POST'])
@login_required
def position_neu():
if not _lv_berechtigt():
flash('Keine Berechtigung.', 'danger')
return redirect(url_for('lv.index'))
lv_name = request.form.get('lv_name', '')
if not lv_name:
flash('Bitte zuerst ein LV auswählen.', 'danger')
return redirect(url_for('lv.index'))
max_order = db.session.query(db.func.max(LVPosition.order_index)).filter_by(
company_id=current_user.company_id, lv_name=lv_name
).scalar() or 0
einzelpreis = float(request.form.get('einzelpreis', 0))
pos = LVPosition(
company_id=current_user.company_id, lv_name=lv_name,
pos_nr=request.form.get('pos_nr', ''), order_index=max_order + 1,
kurztext=request.form.get('kurztext', ''),
langtext=request.form.get('langtext', ''),
einheit=request.form.get('einheit', 'ST'),
einzelpreis=einzelpreis if current_user.is_firmadmin() or current_user.darf_preise_sehen else 0,
gruppe=request.form.get('gruppe', ''),
)
db.session.add(pos)
db.session.commit()
return redirect(url_for('lv.index', lv=lv_name))
@lv_bp.route('/position/<int:pos_id>/bearbeiten', methods=['POST'])
@login_required
def position_bearbeiten(pos_id):
if not _lv_berechtigt():
return 'Keine Berechtigung', 403
pos = LVPosition.query.get_or_404(pos_id)
if pos.company_id != current_user.company_id:
return 'Zugriff verweigert', 403
pos.pos_nr = request.form.get('pos_nr', pos.pos_nr)
pos.kurztext = request.form.get('kurztext', pos.kurztext)
pos.langtext = request.form.get('langtext', pos.langtext)
pos.einheit = request.form.get('einheit', pos.einheit)
if current_user.is_firmadmin() or current_user.darf_preise_sehen:
pos.einzelpreis = float(request.form.get('einzelpreis', pos.einzelpreis))
pos.gruppe = request.form.get('gruppe', pos.gruppe)
db.session.commit()
return redirect(url_for('lv.index', lv=pos.lv_name))
@lv_bp.route('/position/<int:pos_id>/loeschen', methods=['POST'])
@login_required
def position_loeschen(pos_id):
if not _lv_berechtigt():
return 'Keine Berechtigung', 403
pos = LVPosition.query.get_or_404(pos_id)
if pos.company_id != current_user.company_id:
return 'Zugriff verweigert', 403
lv_name = pos.lv_name
db.session.delete(pos)
db.session.commit()
return redirect(url_for('lv.index', lv=lv_name))
@lv_bp.route('/positionen/reihenfolge', methods=['POST'])
@login_required
def positionen_reihenfolge():
if not _lv_berechtigt():
return jsonify({'error': 'Keine Berechtigung'}), 403
data = request.get_json()
if not data or 'reihenfolge' not in data:
return jsonify({'error': 'Keine Daten'}), 400
for item in data['reihenfolge']:
pos = LVPosition.query.get(item['id'])
if pos and pos.company_id == current_user.company_id:
pos.order_index = item['order_index']
db.session.commit()
return jsonify({'status': 'ok'})
@lv_bp.route('/position/<int:pos_id>/favorite', methods=['POST'])
@login_required
def position_favorite(pos_id):
pos = LVPosition.query.get_or_404(pos_id)
if pos.company_id != current_user.company_id:
return 'Zugriff verweigert', 403
data = request.get_json() or {}
pos.favorite = data.get('favorite', not pos.favorite)
db.session.commit()
return jsonify({'favorite': pos.favorite})
@lv_bp.route('/position/<int:pos_id>/langtext')
@login_required
def position_langtext(pos_id):
pos = LVPosition.query.get_or_404(pos_id)
if pos.company_id != current_user.company_id:
return 'Zugriff verweigert', 403
preise_sichtbar = current_user.is_firmadmin() or current_user.darf_preise_sehen
preis_text = f'{pos.einzelpreis:.2f}' if preise_sichtbar else 'versteckt'
return f'''<div class="box">
<p class="has-text-weight-bold mb-2">{pos.pos_nr} {pos.kurztext or ''}</p>
<p class="is-size-6"><strong>Einheit:</strong> {pos.einheit} | <strong>EP:</strong> {preis_text} | <strong>Favorit:</strong> {'' if pos.favorite else ''}</p>
<hr>
<div style="white-space:pre-wrap;font-size:0.9rem">{pos.langtext or 'Kein Langtext vorhanden.'}</div>
</div>'''
@lv_bp.route('/import/txt', methods=['POST'])
@login_required
def import_txt():
if not _lv_berechtigt():
flash('Keine Berechtigung.', 'danger')
return redirect(url_for('lv.index'))
lv_name = request.form.get('lv_name', '').strip()
if not lv_name:
flash('LV-Name erforderlich.', 'danger')
return redirect(url_for('lv.index'))
file = request.files.get('datei')
if not file:
flash('Keine Datei ausgewählt.', 'danger')
return redirect(url_for('lv.index'))
content = file.read().decode('utf-8', errors='ignore')
max_order = db.session.query(db.func.max(LVPosition.order_index)).filter_by(
company_id=current_user.company_id, lv_name=lv_name
).scalar() or 0
zeilen = content.split('\n')
count = 0
preise_sichtbar = current_user.is_firmadmin() or current_user.darf_preise_sehen
for zeile in zeilen:
zeile = zeile.strip()
if not zeile or zeile.startswith('#'):
continue
parts = [p.strip() for p in zeile.split('|')]
if len(parts) >= 2:
pos_nr = parts[0]
if not LVPosition.query.filter_by(
company_id=current_user.company_id, lv_name=lv_name, pos_nr=pos_nr
).first():
max_order += 1
einzelpreis = float(parts[4]) if len(parts) > 4 and parts[4] else 0
pos = LVPosition(
company_id=current_user.company_id, lv_name=lv_name,
pos_nr=pos_nr, order_index=max_order,
kurztext=parts[1] if len(parts) > 1 else '',
langtext=parts[2] if len(parts) > 2 else '',
einheit=parts[3] if len(parts) > 3 else 'ST',
einzelpreis=einzelpreis if preise_sichtbar else 0,
)
db.session.add(pos)
count += 1
db.session.commit()
flash(f'{count} Positionen importiert.', 'success')
return redirect(url_for('lv.index', lv=lv_name))
+121
View File
@@ -0,0 +1,121 @@
from flask import Blueprint, render_template, request, jsonify, redirect, url_for, flash
from flask_login import login_required, current_user
from app.extensions import db
from app.models.project import Project
from app.models.aufmass import Aufmass
from app.models.position import Position
from app.models.lv import LVPosition
from app.services.license_service import check_user_module_access
modules_bp = Blueprint('modules', __name__)
@modules_bp.route('/<module_name>/formular')
@login_required
def formular(module_name):
if not check_user_module_access(current_user, module_name):
return 'Modul nicht freigeschaltet', 403
try:
mod = __import__(f'app.modules.{module_name}', fromlist=['get_formular_html'])
html = mod.get_formular_html()
aufmass_id = request.args.get('aufmass_id')
if aufmass_id and '<form' in html:
html = html.replace('</form>', f'<input type="hidden" name="aufmass_id" value="{aufmass_id}"></form>', 1)
return html
except (ImportError, AttributeError) as e:
return f'Modul {module_name} nicht gefunden: {e}', 404
@modules_bp.route('/<module_name>/berechnen', methods=['POST'])
@login_required
def berechnen(module_name):
if not check_user_module_access(current_user, module_name):
return jsonify({'error': 'Modul nicht freigeschaltet'}), 403
try:
mod = __import__(f'app.modules.{module_name}', fromlist=['berechne'])
form_data = request.form.to_dict()
aufmass_id = form_data.pop('aufmass_id', None)
sm_nr = form_data.pop('sm_nr', f'{module_name}_auto')
projekt_name = form_data.pop('projekt_name', f'{module_name} Berechnung')
result = mod.berechne(form_data)
# Use existing aufmass if aufmass_id provided
if aufmass_id:
aufmass = Aufmass.query.get(int(aufmass_id))
if not aufmass:
return '<div class="notification is-danger">Aufmaß nicht gefunden.<br><button class="button is-small is-light mt-2" onclick="closeModulModal()">Schließen</button></div>'
project = Project.query.get(aufmass.project_id)
if not project or project.company_id != current_user.company_id:
return '<div class="notification is-danger">Kein Zugriff.<br><button class="button is-small is-light mt-2" onclick="closeModulModal()">Schließen</button></div>'
locked, holder_id = aufmass.is_locked()
if locked and holder_id != current_user.id:
from app.models.user import User
holder = User.query.get(holder_id)
name = holder.full_name if holder else 'Unbekannt'
return f'<div class="notification is-danger">🔒 {name} bearbeitet dieses Aufmaß gerade.<br><button class="button is-small is-light mt-2" onclick="closeModulModal()">Schließen</button></div>'
else:
project = Project.query.filter_by(company_id=current_user.company_id, sm_nr=sm_nr).first()
if not project:
project = Project(
company_id=current_user.company_id,
sm_nr=sm_nr,
bezeichnung=projekt_name,
status='aktiv',
erstellt_von=current_user.id,
)
db.session.add(project)
db.session.flush()
aufmass = Aufmass(project_id=project.id, name='Standard', typ='', sortierung=0, erstellt_von=current_user.id)
db.session.add(aufmass)
db.session.flush()
else:
aufmass = Aufmass.query.filter_by(project_id=project.id).order_by(Aufmass.sortierung).first()
if not aufmass:
aufmass = Aufmass(project_id=project.id, name='Standard', typ='', sortierung=0, erstellt_von=current_user.id)
db.session.add(aufmass)
db.session.flush()
max_sort = db.session.query(db.func.max(Position.sortierung)).filter_by(
aufmass_id=aufmass.id
).scalar() or 0
count = 0
for pos_data in result:
max_sort += 1
explicit_menge = pos_data.get('menge')
pos = Position(
project_id=project.id, aufmass_id=aufmass.id,
pos_nr=pos_data.get('pos_nr', ''),
sortierung=max_sort,
kurztext=pos_data.get('kurztext', ''),
einheit=pos_data.get('einheit', 'ST'),
einzelpreis=float(pos_data.get('einzelpreis', 0)),
menge=float(explicit_menge) if explicit_menge is not None else 0,
faktor=float(pos_data.get('faktor', 1)),
laenge=float(pos_data.get('laenge', 0)),
breite=float(pos_data.get('breite', 0)),
tiefe=float(pos_data.get('tiefe', 0)),
bemerkung=pos_data.get('bemerkung', ''),
gesamtpreis=0,
)
pos.berechne_menge()
if explicit_menge is not None:
pos.menge = float(explicit_menge)
pos.menge_hinten = pos.faktor * pos.menge
pos.gesamtpreis = pos.menge_hinten * pos.einzelpreis
db.session.add(pos)
count += 1
db.session.commit()
return f'''<div class="notification is-success">
<strong>{count} Positionen</strong> berechnet und ins Aufmaß übernommen.
<span class="is-pulled-right tag is-light is-info">Seite wird neu geladen...</span>
</div>
<script>
setTimeout(function() {{
var m = document.getElementById('modul-modal');
if (m) m.classList.remove('is-active');
location.reload();
}}, 1200);
</script>'''
except (ImportError, AttributeError) as e:
return f'<div class="notification is-danger">Modul-Fehler: {e}<br><button class="button is-small is-light mt-2" onclick="closeModulModal()">Schließen</button></div>'
except Exception as e:
return f'<div class="notification is-danger">Fehler: {e}<br><button class="button is-small is-light mt-2" onclick="closeModulModal()">Schließen</button></div>'
+376
View File
@@ -0,0 +1,376 @@
from flask import Blueprint, render_template, request, flash, redirect, url_for, jsonify, current_app
from flask_login import login_required, current_user
from app.extensions import db
from app.models.company import Company
from app.models.user import User
from app.models.project import Project
from app.models.contract import Contract
from app.models.module import Module
from app.models.company_module import CompanyModule
from app.models.user_module import UserModulePermission
from app.models.project_access import ProjectAccess
from app.models.license import License, LicenseModule
from app.models.aufmass import Aufmass
from app.models.position import Position
from app.models.settings import Settings
superadmin_bp = Blueprint('superadmin', __name__)
def _require_superadmin():
if not current_user.is_superadmin():
flash('Nur für Superadmins.', 'danger')
return False
return True
@superadmin_bp.route('/')
@login_required
def dashboard():
if not _require_superadmin():
return redirect(url_for('admin.dashboard'))
firmsen = Company.query.order_by(Company.name).all()
gesamt_user = User.query.count()
gesamt_projekte = Project.query.count()
all_licenses = License.query.all()
gesamt_lizenzen = len(all_licenses)
gesamt_max_mitarbeiter = sum((l.max_mitarbeiter or 0) for l in all_licenses if not l.unlimited_users)
# Unlimited companies count as "max" being their actual user count
for l in all_licenses:
if l.unlimited_users:
gesamt_max_mitarbeiter += l.used_users
gesamt_max_module_slots = sum((l.max_module_slots or 0) for l in all_licenses if not l.unlimited_modules)
for l in all_licenses:
if l.unlimited_modules:
gesamt_max_module_slots += l.used_module_slots
gesamt_module_anzahl = Module.query.count()
belegte_module_slots = db.session.query(UserModulePermission.id)\
.filter(UserModulePermission.aktiv==True).count()
for f in firmsen:
f._user_count = User.query.filter_by(company_id=f.id).count()
lic = License.query.filter_by(company_id=f.id, aktiv=True).first()
if lic:
f._license_slots = '\u221e' if lic.unlimited_users else f'{f._user_count} / {lic.max_mitarbeiter}'
f._module_slots = '\u221e' if lic.unlimited_modules else f'{lic.used_module_slots} / {lic.max_module_slots}'
f._licensed_modules = LicenseModule.query.filter_by(license_id=lic.id, aktiv=True).count()
else:
f._license_slots = ''
f._module_slots = ''
f._licensed_modules = 0
reg_enabled = Settings.get('registration_enabled', 'false') == 'true'
return render_template('superadmin/dashboard.html', titel='Superadmin',
firmsen=firmsen, gesamt_user=gesamt_user,
gesamt_projekte=gesamt_projekte,
gesamt_lizenzen=gesamt_lizenzen,
gesamt_max_mitarbeiter=gesamt_max_mitarbeiter,
gesamt_max_module_slots=gesamt_max_module_slots,
gesamt_module_anzahl=gesamt_module_anzahl,
belegte_module_slots=belegte_module_slots,
registration_enabled=reg_enabled)
@superadmin_bp.route('/firma/neu', methods=['GET', 'POST'])
@login_required
def firma_create():
if not _require_superadmin():
return redirect(url_for('admin.dashboard'))
if request.method == 'POST':
name = request.form.get('name', '').strip()
if not name:
flash('Firmenname ist erforderlich.', 'danger')
return render_template('superadmin/firma_form.html', titel='Neue Firma', company=None)
slug = name.lower().replace(' ', '-').replace('ä','ae').replace('ö','oe').replace('ü','ue')[:100]
base_slug = slug
counter = 1
while Company.query.filter_by(slug=slug).first():
slug = f"{base_slug}-{counter}"
counter += 1
company = Company(
name=name, slug=slug,
strasse=request.form.get('strasse', '').strip(),
house_number=request.form.get('house_number', '').strip(),
plz=request.form.get('plz', '').strip(),
ort=request.form.get('ort', '').strip(),
telefon=request.form.get('telefon', '').strip(),
email=request.form.get('email', '').strip(),
aktiv=True
)
db.session.add(company)
db.session.flush()
# Optional: Firmadmin-User direkt anlegen
admin_email = request.form.get('admin_email', '').strip()
admin_password = request.form.get('admin_password', '').strip()
if admin_email and admin_password:
if User.query.filter_by(email=admin_email).first():
flash('E-Mail existiert bereits. Firma wurde trotzdem angelegt.', 'warning')
else:
user = User(
company_id=company.id, email=admin_email,
vorname=request.form.get('admin_vorname', '').strip(),
nachname=request.form.get('admin_nachname', '').strip(),
rolle='firmadmin',
darf_projekte_anlegen=True, darf_lv_verwalten=True,
darf_preise_sehen=True, darf_aufmass_verwalten=True,
)
user.set_password(admin_password)
db.session.add(user)
db.session.commit()
flash(f'Firma "{name}" angelegt.', 'success')
return redirect(url_for('superadmin.firma_detail', company_id=company.id))
return render_template('superadmin/firma_form.html', titel='Neue Firma', company=None)
@superadmin_bp.route('/firma/<int:company_id>/edit', methods=['GET', 'POST'])
@login_required
def firma_edit(company_id):
if not _require_superadmin():
return redirect(url_for('admin.dashboard'))
company = Company.query.get_or_404(company_id)
if request.method == 'POST':
company.name = request.form.get('name', company.name).strip()
company.strasse = request.form.get('strasse', '').strip()
company.house_number = request.form.get('house_number', '').strip()
company.plz = request.form.get('plz', '').strip()
company.ort = request.form.get('ort', '').strip()
company.telefon = request.form.get('telefon', '').strip()
company.email = request.form.get('email', '').strip()
db.session.commit()
flash('Firmendaten aktualisiert.', 'success')
return redirect(url_for('superadmin.firma_detail', company_id=company.id))
return render_template('superadmin/firma_form.html', titel=f'Firma bearbeiten: {company.name}', company=company)
@superadmin_bp.route('/firma/<int:company_id>/user/neu', methods=['POST'])
@login_required
def firma_user_create(company_id):
if not _require_superadmin():
return redirect(url_for('admin.dashboard'))
company = Company.query.get_or_404(company_id)
email = request.form.get('email', '').strip()
if not email:
flash('E-Mail ist erforderlich.', 'danger')
return redirect(url_for('superadmin.firma_detail', company_id=company.id))
if User.query.filter_by(email=email).first():
flash('E-Mail existiert bereits.', 'danger')
return redirect(url_for('superadmin.firma_detail', company_id=company.id))
password = request.form.get('password', '').strip()
if not password:
flash('Passwort ist erforderlich.', 'danger')
return redirect(url_for('superadmin.firma_detail', company_id=company.id))
rolle = request.form.get('rolle', 'mitarbeiter')
if rolle not in ('mitarbeiter', 'firmadmin'):
rolle = 'mitarbeiter'
user = User(
company_id=company.id, email=email,
vorname=request.form.get('vorname', '').strip(),
nachname=request.form.get('nachname', '').strip(),
rolle=rolle,
darf_projekte_anlegen=bool(request.form.get('darf_projekte_anlegen')),
darf_lv_verwalten=bool(request.form.get('darf_lv_verwalten')),
darf_preise_sehen=bool(request.form.get('darf_preise_sehen')),
darf_aufmass_verwalten=bool(request.form.get('darf_aufmass_verwalten')),
darf_evergabe_nutzen=bool(request.form.get('darf_evergabe_nutzen')),
darf_kopfdaten_holen=bool(request.form.get('darf_kopfdaten_holen')),
darf_aufmass_uebertragen=bool(request.form.get('darf_aufmass_uebertragen')),
)
user.set_password(password)
db.session.add(user)
db.session.commit()
flash(f'Benutzer {user.full_name} angelegt.', 'success')
return redirect(url_for('superadmin.firma_detail', company_id=company.id))
@superadmin_bp.route('/firma/<int:company_id>')
@login_required
def firma_detail(company_id):
if not _require_superadmin():
return redirect(url_for('admin.dashboard'))
company = Company.query.get_or_404(company_id)
users = User.query.filter_by(company_id=company.id).order_by(User.email).all()
projekte = Project.query.filter_by(company_id=company.id).order_by(Project.erstellt_am.desc()).all()
licenses = License.query.filter_by(company_id=company.id).order_by(License.erstellt_am.desc()).all()
contracts = Contract.query.filter_by(company_id=company.id).order_by(Contract.name).all()
modules = Module.query.order_by(Module.sortierung).all()
company_modules = {cm.module_id for cm in CompanyModule.query.filter_by(company_id=company.id, aktiv=True).all()}
all_modules = Module.query.order_by(Module.sortierung).all()
return render_template('superadmin/firma_detail.html', company=company,
users=users, projekte=projekte, licenses=licenses,
contracts=contracts, modules=modules,
company_modules=company_modules, all_modules=all_modules,
titel=f'Firma: {company.name}')
@superadmin_bp.route('/firma/<int:company_id>/toggle')
@login_required
def firma_toggle(company_id):
if not _require_superadmin():
return redirect(url_for('admin.dashboard'))
company = Company.query.get_or_404(company_id)
company.aktiv = not company.aktiv
db.session.commit()
flash(f'Firma {"aktiviert" if company.aktiv else "deaktiviert"}.', 'success')
return redirect(url_for('superadmin.dashboard'))
@superadmin_bp.route('/user/<int:user_id>/toggle')
@login_required
def user_toggle(user_id):
if not _require_superadmin():
return redirect(url_for('admin.dashboard'))
user = User.query.get_or_404(user_id)
user.aktiv = not user.aktiv
db.session.commit()
flash(f'User {"aktiviert" if user.aktiv else "deaktiviert"}.', 'success')
return redirect(url_for('superadmin.firma_detail', company_id=user.company_id))
@superadmin_bp.route('/user/<int:user_id>/make-superadmin')
@login_required
def user_make_superadmin(user_id):
if not _require_superadmin():
return redirect(url_for('admin.dashboard'))
user = User.query.get_or_404(user_id)
if user.is_superadmin():
user.rolle = 'firmadmin'
else:
user.rolle = 'superadmin'
db.session.commit()
flash(f'User {user.email} ist jetzt {user.rolle}.', 'success')
return redirect(url_for('superadmin.firma_detail', company_id=user.company_id))
@superadmin_bp.route('/user/<int:user_id>/loeschen', methods=['POST'])
@login_required
def user_loeschen(user_id):
if not _require_superadmin():
return redirect(url_for('admin.dashboard'))
user = User.query.get_or_404(user_id)
if user.id == current_user.id:
flash('Sie können sich nicht selbst löschen.', 'danger')
return redirect(url_for('superadmin.firma_detail', company_id=user.company_id))
email = user.email
ProjectAccess.query.filter_by(user_id=user.id).delete()
UserModulePermission.query.filter_by(user_id=user.id).delete()
db.session.delete(user)
db.session.commit()
flash(f'Benutzer {email} gelöscht.', 'success')
return redirect(url_for('superadmin.firma_detail', company_id=user.company_id))
@superadmin_bp.route('/company/<int:company_id>/module/<int:module_id>/toggle')
@login_required
def company_module_toggle(company_id, module_id):
if not _require_superadmin():
return redirect(url_for('admin.dashboard'))
cm = CompanyModule.query.filter_by(company_id=company_id, module_id=module_id).first()
if cm:
cm.aktiv = not cm.aktiv
else:
cm = CompanyModule(company_id=company_id, module_id=module_id, aktiv=True)
db.session.add(cm)
db.session.commit()
return redirect(url_for('superadmin.firma_detail', company_id=company_id))
@superadmin_bp.route('/firma/<int:company_id>/license/create', methods=['POST'])
@login_required
def license_create(company_id):
if not _require_superadmin():
return redirect(url_for('admin.dashboard'))
company = Company.query.get_or_404(company_id)
max_mitarbeiter = request.form.get('max_mitarbeiter', type=int) or 5
max_module_slots = request.form.get('max_module_slots', type=int) or 5
unlimited_users = bool(request.form.get('unlimited_users'))
unlimited_modules = bool(request.form.get('unlimited_modules'))
uid = License.generate_uid(company.name)
lic = License(company_id=company_id, uid=uid,
max_mitarbeiter=max_mitarbeiter,
max_module_slots=max_module_slots,
unlimited_users=unlimited_users,
unlimited_modules=unlimited_modules)
db.session.add(lic)
db.session.flush()
# Assign selected modules
mod_ids = request.form.getlist('modules')
for mid in mod_ids:
db.session.add(LicenseModule(license_id=lic.id, module_id=int(mid), aktiv=True))
db.session.commit()
flash('Lizenz angelegt.', 'success')
return redirect(url_for('superadmin.firma_detail', company_id=company_id))
@superadmin_bp.route('/license/<int:license_id>/edit', methods=['POST'])
@login_required
def license_edit(license_id):
if not _require_superadmin():
return redirect(url_for('admin.dashboard'))
lic = License.query.get_or_404(license_id)
lic.max_mitarbeiter = request.form.get('max_mitarbeiter', type=int) or lic.max_mitarbeiter
lic.max_module_slots = request.form.get('max_module_slots', type=int) or lic.max_module_slots
lic.unlimited_users = bool(request.form.get('unlimited_users'))
lic.unlimited_modules = bool(request.form.get('unlimited_modules'))
# Update module assignments
new_mod_ids = set(int(x) for x in request.form.getlist('modules'))
for lm in lic.modules.filter_by(aktiv=True).all():
if lm.module_id not in new_mod_ids:
lm.aktiv = False
existing_ids = {lm.module_id for lm in lic.modules.filter_by(aktiv=True).all()}
for mid in new_mod_ids:
if mid not in existing_ids:
db.session.add(LicenseModule(license_id=lic.id, module_id=mid, aktiv=True))
db.session.commit()
flash('Lizenz aktualisiert.', 'success')
return redirect(url_for('superadmin.firma_detail', company_id=lic.company_id))
@superadmin_bp.route('/license/<int:license_id>/delete', methods=['POST'])
@login_required
def license_delete(license_id):
if not _require_superadmin():
return redirect(url_for('admin.dashboard'))
lic = License.query.get_or_404(license_id)
company_id = lic.company_id
db.session.delete(lic)
db.session.commit()
flash('Lizenz gelöscht.', 'success')
return redirect(url_for('superadmin.firma_detail', company_id=company_id))
@superadmin_bp.route('/license/<int:license_id>/module/<int:module_id>/toggle')
@login_required
def license_module_toggle(license_id, module_id):
if not _require_superadmin():
return redirect(url_for('admin.dashboard'))
lm = LicenseModule.query.filter_by(license_id=license_id, module_id=module_id).first()
if lm:
lm.aktiv = not lm.aktiv
else:
lm = LicenseModule(license_id=license_id, module_id=module_id, aktiv=True)
db.session.add(lm)
db.session.commit()
lic = License.query.get(license_id)
return redirect(url_for('superadmin.firma_detail', company_id=lic.company_id))
@superadmin_bp.route('/firma/<int:company_id>/evergabe/toggle')
@login_required
def firma_evergabe_toggle(company_id):
if not _require_superadmin():
return redirect(url_for('admin.dashboard'))
company = Company.query.get_or_404(company_id)
company.evergabe_aktiviert = not company.evergabe_aktiviert
db.session.commit()
flash(f'E-Vergabe Addon {"freigeschaltet" if company.evergabe_aktiviert else "deaktiviert"}.', 'success')
return redirect(url_for('superadmin.firma_detail', company_id=company_id))
@superadmin_bp.route('/registration/toggle')
@login_required
def registration_toggle():
if not _require_superadmin():
if request.headers.get('HX-Request'):
return jsonify({'error': 'Zugriff verweigert'}), 403
return redirect(url_for('admin.dashboard'))
current = Settings.get('registration_enabled', 'false') == 'true'
Settings.set('registration_enabled', 'false' if current else 'true')
current_app.config['REGISTRATION_ENABLED'] = not current
if request.headers.get('HX-Request'):
return jsonify({'ok': True, 'registration_enabled': not current})
flash(f'Registrierung {"aktiviert" if not current else "deaktiviert"}.', 'success')
return redirect(url_for('superadmin.dashboard'))
@superadmin_bp.route('/registration/status')
@login_required
def registration_status():
if not _require_superadmin():
return jsonify({'error': 'Zugriff verweigert'}), 403
enabled = Settings.get('registration_enabled', 'false') == 'true'
return jsonify({'registration_enabled': enabled})
+91
View File
@@ -0,0 +1,91 @@
from flask import Blueprint, request, jsonify
from flask_login import login_required, current_user
from app.extensions import db
from app.models.view_profile import ViewProfile
views_bp = Blueprint('views', __name__)
@views_bp.route('/api/profiles', methods=['GET'])
@login_required
def list_profiles():
view_type = request.args.get('view_type', 'lv')
profiles = ViewProfile.query.filter_by(
user_id=current_user.id, view_type=view_type
).order_by(ViewProfile.is_default.desc(), ViewProfile.name).all()
return jsonify([{
'id': p.id, 'name': p.name, 'is_default': p.is_default,
'config': p.get_config()
} for p in profiles])
@views_bp.route('/api/profiles', methods=['POST'])
@login_required
def save_profile():
data = request.get_json() or {}
name = data.get('name', '').strip()
view_type = data.get('view_type', 'lv')
config = data.get('config', {})
is_default = data.get('is_default', False)
if not name:
return jsonify({'error': 'Name erforderlich'}), 400
existing = ViewProfile.query.filter_by(
user_id=current_user.id, name=name, view_type=view_type
).first()
if existing:
existing.set_config(config)
existing.is_default = is_default or existing.is_default
else:
existing = ViewProfile(
user_id=current_user.id, name=name,
view_type=view_type, is_default=is_default
)
existing.set_config(config)
db.session.add(existing)
if is_default:
ViewProfile.query.filter_by(
user_id=current_user.id, view_type=view_type
).filter(ViewProfile.id != existing.id).update({'is_default': False})
db.session.commit()
return jsonify({'id': existing.id, 'name': existing.name})
@views_bp.route('/api/profiles/<int:profile_id>', methods=['GET'])
@login_required
def get_profile(profile_id):
p = ViewProfile.query.get_or_404(profile_id)
if p.user_id != current_user.id:
return jsonify({'error': 'Zugriff verweigert'}), 403
return jsonify({
'id': p.id, 'name': p.name, 'view_type': p.view_type,
'is_default': p.is_default, 'config_json': p.get_config()
})
@views_bp.route('/api/profiles/<int:profile_id>', methods=['PUT'])
@login_required
def update_profile(profile_id):
p = ViewProfile.query.get_or_404(profile_id)
if p.user_id != current_user.id:
return jsonify({'error': 'Zugriff verweigert'}), 403
data = request.get_json() or {}
if 'name' in data and data['name']:
p.name = data['name'].strip()
if 'config_json' in data:
p.set_config(data['config_json'])
if 'is_default' in data:
p.is_default = data['is_default']
db.session.commit()
return jsonify({'id': p.id, 'name': p.name})
@views_bp.route('/api/profiles/<int:profile_id>', methods=['DELETE'])
@login_required
def delete_profile(profile_id):
p = ViewProfile.query.get_or_404(profile_id)
if p.user_id != current_user.id:
return jsonify({'error': 'Zugriff verweigert'}), 403
if p.is_default:
return jsonify({'error': 'Standard kann nicht gelöscht werden'}), 400
db.session.delete(p)
db.session.commit()
return jsonify({'status': 'ok'})