237 lines
9.5 KiB
Python
237 lines
9.5 KiB
Python
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))
|