Initial commit – AufmaßCreater v2.35
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
@@ -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'))
|
||||
@@ -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
|
||||
@@ -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})
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
@@ -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>'
|
||||
@@ -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})
|
||||
@@ -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'})
|
||||
Reference in New Issue
Block a user