Initial commit – AufmaßCreater v2.35

This commit is contained in:
2026-06-10 11:03:43 +02:00
commit 84c933ea9c
2823 changed files with 490495 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
import sys
import os
# Pfad zum Projekt-Root hinzufügen
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from app import create_app
from app.extensions import db
from app.models.company import Company
def add_house_number_column():
app = create_app()
with app.app_context():
# Prüfen, ob die Spalte bereits existiert
inspector = db.inspect(db.engine)
columns = [c['name'] for c in inspector.get_columns('companies')]
if 'house_number' not in columns:
print("Spalte 'house_number' wird hinzugefügt...")
db.session.execute(db.text("ALTER TABLE companies ADD COLUMN house_number VARCHAR(20)"))
db.session.commit()
print("Spalte erfolgreich hinzugefügt.")
else:
print("Spalte 'house_number' existiert bereits.")
if __name__ == '__main__':
add_house_number_column()
@@ -0,0 +1,39 @@
param(
[string]$DumpFile = "$PSScriptRoot\aufmassweb_export.dump",
[string]$PgBin = "C:\Program Files\PostgreSQL\16\bin",
[string]$DbHost = "localhost",
[string]$DbPort = "5432",
[string]$DbUser = "aufmass",
[string]$DbName = "aufmassweb"
)
$pg_dump = Join-Path $PgBin "pg_dump.exe"
if (-not (Test-Path $pg_dump)) {
Write-Error "pg_dump nicht gefunden unter: $pg_dump"
exit 1
}
Write-Host "=== AufmaßWeb Datenexport vom Laptop ===" -ForegroundColor Cyan
Write-Host "Ziel: $DumpFile"
Write-Host "DB: $DbHost:$DbPort/$DbName"
Write-Host "User: $DbUser"
Write-Host ""
$env:PGPASSWORD = Read-Host "Passwort für PostgreSQL-User '$DbUser'" -AsSecureString
$BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($env:PGPASSWORD)
$env:PGPASSWORD = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)
Write-Host "Exportiere Datenbank (custom-format) ..."
& $pg_dump -h $DbHost -p $DbPort -U $DbUser -d $DbName -F c -f $DumpFile --verbose
if ($LASTEXITCODE -eq 0) {
$size = (Get-Item $DumpFile).Length / 1MB
Write-Host "Export erfolgreich: $DumpFile ({0:N1} MB)" -f $size -ForegroundColor Green
Write-Host "Übertrage die Datei auf den Ziel-PC und führe dort import_from_dump.ps1 aus." -ForegroundColor Yellow
} else {
Write-Error "Export fehlgeschlagen (Exit-Code: $LASTEXITCODE)"
Remove-Variable PGPASSWORD -ErrorAction SilentlyContinue
exit 1
}
Remove-Variable PGPASSWORD -ErrorAction SilentlyContinue
+75
View File
@@ -0,0 +1,75 @@
param(
[Parameter(Mandatory=$true)]
[string]$DumpFile,
[string]$DockerBin = "C:\Program Files\Docker\Docker\resources\bin",
[string]$DbContainer = "aufmass_web-db-1",
[string]$WebContainer = "aufmass_web-web-1",
[string]$DbUser = "aufmass",
[string]$DbName = "aufmassweb"
)
Write-Host "=== AufmaßWeb Datenimport (Docker PostgreSQL) ===" -ForegroundColor Cyan
Write-Host "Dump: $DumpFile"
Write-Host "Ziel: Container $DbContainer, DB $DbName"
Write-Host ""
if (-not (Test-Path $DumpFile)) {
Write-Error "Dump-Datei nicht gefunden: $DumpFile"
exit 1
}
$dockerd = Join-Path $DockerBin "docker.exe"
if (-not (Test-Path $dockerd)) {
Write-Error "Docker nicht gefunden unter: $dockerd"
exit 1
}
# 1. Prüfen ob Container läuft
Write-Host "[1/5] Prüfe Docker-Container ..." -NoNewline
$running = & $dockerd ps --filter "name=$DbContainer" --filter "status=running" --format "{{.Names}}" 2>$null
if (-not $running) {
Write-Host " FEHLER" -ForegroundColor Red
Write-Error "Container $DbContainer läuft nicht. Starte zuerst 'docker compose up -d'"
exit 1
}
Write-Host " OK ($running)" -ForegroundColor Green
# 2. Dump in Container kopieren
Write-Host "[2/5] Kopiere Dump in Container ..." -NoNewline
$remotePath = "/tmp/$(Split-Path $DumpFile -Leaf)"
& $dockerd cp $DumpFile "${DbContainer}:${remotePath}"
Write-Host " OK → $remotePath" -ForegroundColor Green
# 3. Datenbank droppen + neu anlegen (verbinde mit 'postgres' DB, nicht mit der Zieldatenbank)
Write-Host "[3/5] Lösche alte Datenbank ..." -NoNewline
& $dockerd exec -i $DbContainer psql -U $DbUser -d postgres -c "DROP DATABASE IF EXISTS \"$DbName\";" 2>$null
Write-Host " OK" -ForegroundColor Green
Write-Host "[4/5] Erstelle neue Datenbank ..." -NoNewline
& $dockerd exec -i $DbContainer psql -U $DbUser -d postgres -c "CREATE DATABASE \"$DbName\";" 2>$null
Write-Host " OK" -ForegroundColor Green
# 4. Restore
Write-Host "[5/5] Stelle Daten aus Dump wieder her ..." -ForegroundColor Yellow
& $dockerd exec -i $DbContainer pg_restore -U $DbUser -d $DbName --verbose --exit-on-error $remotePath
if ($LASTEXITCODE -ne 0) {
Write-Error "pg_restore fehlgeschlagen (Exit-Code: $LASTEXITCODE)"
exit 1
}
Write-Host "Datenbank-Restore erfolgreich!" -ForegroundColor Green
# 5. Web-Container neuladen
Write-Host "Starte Web-Container neu ..." -NoNewline
& $dockerd restart $WebContainer 2>$null
Write-Host " OK" -ForegroundColor Green
# Aufräumen
& $dockerd exec $DbContainer rm -f $remotePath 2>$null
$size = (Get-Item $DumpFile).Length / 1MB
Write-Host ""
Write-Host "=== Fertig! ===" -ForegroundColor Cyan
Write-Host "Dump: $DumpFile ({0:N1} MB)" -f $size
Write-Host "App: http://localhost:5000" -ForegroundColor Green
Write-Host ""
Write-Host "Tipp: Du kannst den Dump archivieren oder löschen." -ForegroundColor Gray
+30
View File
@@ -0,0 +1,30 @@
"""Add rsa, abschnitt columns to lv_positionen + abschnitt to positionen."""
import sqlite3, os
db_path = os.path.normpath(os.path.join(os.path.dirname(__file__), '..', 'data', 'aufmass.db'))
if not os.path.exists(db_path):
print("No DB found, skipping.")
exit(0)
conn = sqlite3.connect(db_path)
c = conn.cursor()
c.execute("PRAGMA table_info(lv_positionen)")
cols = [r[1] for r in c.fetchall()]
if 'rsa' not in cols:
c.execute("ALTER TABLE lv_positionen ADD COLUMN rsa VARCHAR(20)")
print("Added rsa to lv_positionen")
if 'abschnitt' not in cols:
c.execute("ALTER TABLE lv_positionen ADD COLUMN abschnitt VARCHAR(100)")
print("Added abschnitt to lv_positionen")
c.execute("PRAGMA table_info(positionen)")
pcols = [r[1] for r in c.fetchall()]
if 'abschnitt' not in pcols:
c.execute("ALTER TABLE positionen ADD COLUMN abschnitt VARCHAR(100)")
print("Added abschnitt to positionen")
conn.commit()
conn.close()
print("Migration done.")
+26
View File
@@ -0,0 +1,26 @@
"""Add profile_image column to users table."""
import sqlite3
import os
db_path = os.path.join(os.path.dirname(__file__), '..', 'data', 'aufmass.db')
db_path = os.path.normpath(db_path)
if not os.path.exists(db_path):
print(f"DB not found at {db_path}")
exit(0)
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# Check if column already exists
cursor.execute("PRAGMA table_info(users)")
cols = [row[1] for row in cursor.fetchall()]
if 'profile_image' not in cols:
cursor.execute("ALTER TABLE users ADD COLUMN profile_image VARCHAR(255)")
print("Added profile_image column to users table.")
else:
print("Column profile_image already exists.")
conn.commit()
conn.close()
print("Done.")
+16
View File
@@ -0,0 +1,16 @@
"""Add formel_typ, formel columns to positionen."""
import sqlite3, os
db_path = os.path.normpath(os.path.join(os.path.dirname(__file__), '..', 'data', 'aufmass.db'))
if not os.path.exists(db_path):
print("No DB found, skipping."); exit(0)
conn = sqlite3.connect(db_path); c = conn.cursor()
c.execute("PRAGMA table_info(positionen)")
cols = [r[1] for r in c.fetchall()]
for col, typ in [('formel_typ','VARCHAR(10)'), ('formel','VARCHAR(300)')]:
if col not in cols:
c.execute(f"ALTER TABLE positionen ADD COLUMN {col} {typ}")
print(f"Added {col} to positionen")
conn.commit(); conn.close()
print("Migration done.")
@@ -0,0 +1,109 @@
import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..'))
from app import create_app
from sqlalchemy import text
import sqlite3
SQLITE_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'data', 'aufmass.db')
# Tables in FK-safe order (parents first)
TABLE_ORDER = [
'companies',
'users',
'licenses',
'contracts',
'modules',
'aufmass_typen',
'projekte',
'custom_modules',
'company_modules',
'license_modules',
'aufmass',
'lv_positionen',
'positionen',
'project_access',
'custom_module_assignments',
'user_module_permissions',
'view_profiles',
]
def main():
print("=" * 60)
print("SQLite -> PostgreSQL Migration v2")
print("=" * 60)
if not os.path.exists(SQLITE_PATH):
print(f"SQLite DB nicht gefunden: {SQLITE_PATH}")
return
# Read all SQLite data
print("\n1. Lese SQLite-Daten...")
conn = sqlite3.connect(SQLITE_PATH)
conn.row_factory = sqlite3.Row
c = conn.cursor()
sqlite_data = {}
for table_name in TABLE_ORDER:
c.execute(f'SELECT * FROM "{table_name}"')
rows = [dict(r) for r in c.fetchall()]
sqlite_data[table_name] = rows
print(f" {table_name}: {len(rows)} Zeilen")
conn.close()
# Create Flask app -> creates PG tables + seed defaults
print("\n2. Starte Flask-App mit PostgreSQL...")
app = create_app()
with app.app_context():
from app import db
# Delete all existing data in reverse FK order
print(" Entferne vorhandene Daten...")
with db.engine.connect() as c:
for table_name in reversed(TABLE_ORDER):
c.execute(text(f'DELETE FROM "{table_name}"'))
c.commit()
# Import in FK-safe order
print("\n3. Importiere Daten in FK-Reihenfolge...")
meta = __import__('sqlalchemy', fromlist=['MetaData']).MetaData()
meta.reflect(bind=db.engine)
total_ok = 0
total_fail = 0
for table_name in TABLE_ORDER:
rows = sqlite_data.get(table_name, [])
if not rows:
print(f" -- {table_name}: keine Daten")
continue
table = __import__('sqlalchemy', fromlist=['Table']).Table(table_name, meta, autoload_with=db.engine)
pg_cols = {c.name for c in table.columns}
ok = 0
fail = 0
with db.engine.connect() as pg_conn:
for row in rows:
filtered = {k: v for k, v in row.items() if k in pg_cols}
try:
pg_conn.execute(table.insert().values(**filtered))
ok += 1
except Exception as e:
fail += 1
if fail <= 2:
print(f" Fehler {table_name} ID={row.get('id','?')}: {e}")
pg_conn.commit()
total_ok += ok
total_fail += fail
status = "+" if ok else " "
print(f" {status} {table_name}: {ok}/{len(rows)} OK" + (f" ({fail} Fehler)" if fail else ""))
print(f"\nErgebnis: {total_ok} Zeilen importiert" + (f", {total_fail} Fehler" if total_fail else ""))
print("\nFertig! Starte Flask-App neu -> _seed_defaults() ergaenzt Seed-Daten via Upsert.")
print("PostgreSQL ist bereit fuer AufmassWeb.")
if __name__ == '__main__':
main()
+99
View File
@@ -0,0 +1,99 @@
"""
Migration: v2 Neue Struktur (Aufmaß-Entity + Rechtesystem)
- users: neue Spalten (darf_*) + rolle admin -> firmadmin
- neu: aufmass Tabelle + project_access Tabelle
- positionen: neue Spalte aufmass_id
"""
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from app import create_app
from app.extensions import db
from app.models.user import User
from app.models.project import Project
from app.models.position import Position
from app.models.aufmass import Aufmass
from app.models.project_access import ProjectAccess
from sqlalchemy import inspect, text
app = create_app()
def _spalte_existiert(table, column):
insp = inspect(db.engine)
cols = [c['name'] for c in insp.get_columns(table)]
return column in cols
def _tabelle_existiert(table):
insp = inspect(db.engine)
return table in insp.get_table_names()
with app.app_context():
print("=== Migration v2 starten ===")
# 1. User-Spalten hinzufügen
if not _spalte_existiert('users', 'darf_projekte_anlegen'):
with db.engine.connect() as conn:
conn.execute(text("ALTER TABLE users ADD COLUMN darf_projekte_anlegen BOOLEAN DEFAULT 0"))
conn.execute(text("ALTER TABLE users ADD COLUMN darf_lv_verwalten BOOLEAN DEFAULT 0"))
conn.execute(text("ALTER TABLE users ADD COLUMN darf_preise_sehen BOOLEAN DEFAULT 0"))
conn.execute(text("ALTER TABLE users ADD COLUMN darf_aufmass_verwalten BOOLEAN DEFAULT 0"))
conn.commit()
print(" -> User-Spalten hinzugefügt")
else:
print(" -> User-Spalten bereits vorhanden")
# 2. Rolle migrieren: admin -> firmadmin, mitarbeiter bleibt
count = User.query.filter_by(rolle='admin').update({'rolle': 'firmadmin'})
db.session.commit()
print(f" -> {count} User von 'admin' -> 'firmadmin' migriert")
# 3. firmadmin bekommt automatisch alle Rechte
for u in User.query.filter_by(rolle='firmadmin').all():
u.darf_projekte_anlegen = True
u.darf_lv_verwalten = True
u.darf_preise_sehen = True
u.darf_aufmass_verwalten = True
db.session.commit()
print(" -> firmadmin-Rechte gesetzt")
# 4. Aufmass-Tabelle anlegen (wenn nicht vorhanden, wird von db.create_all() erstellt)
db.create_all()
print(" -> Tabellen erstellt/aktualisiert")
# 5. aufmass_id zu positionen hinzufügen
if not _spalte_existiert('positionen', 'aufmass_id'):
with db.engine.connect() as conn:
conn.execute(text("ALTER TABLE positionen ADD COLUMN aufmass_id INTEGER REFERENCES aufmass(id)"))
conn.commit()
print(" -> aufmass_id zu positionen hinzugefügt")
else:
print(" -> aufmass_id bereits vorhanden")
# 6. Für jedes Projekt ohne Aufmaß ein Standard-Aufmaß anlegen
projekte = Project.query.all()
for p in projekte:
bestand = Aufmass.query.filter_by(project_id=p.id).first()
if not bestand:
a = Aufmass(project_id=p.id, name='Standard', typ='', sortierung=0)
db.session.add(a)
db.session.flush()
# Bestehende Positionen an das Standard-Aufmaß hängen
Position.query.filter_by(project_id=p.id, aufmass_id=None).update({'aufmass_id': a.id})
print(f" -> Standard-Aufmaß für Projekt {p.id} ({p.sm_nr}) angelegt")
db.session.commit()
# 7. Noch verwaiste Positionen ohne aufmass_id
rest = Position.query.filter_by(aufmass_id=None).count()
if rest:
print(f" -> WARNUNG: {rest} Positionen ohne aufmass_id!")
# Für jedes Projekt ohne aufmass_id die erste gefundene Position suchen
orphans = Position.query.filter_by(aufmass_id=None).all()
for pos in orphans:
a = Aufmass.query.filter_by(project_id=pos.project_id).first()
if a:
pos.aufmass_id = a.id
db.session.commit()
print(f" -> {len(orphans)} verwaiste Positionen zugeordnet")
print("=== Migration v2 abgeschlossen ===")
+84
View File
@@ -0,0 +1,84 @@
import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from app import create_app
from app.extensions import db
from app.models.company import Company
from app.models.user import User
from app.models.contract import Contract
from app.models.lv import LVPosition
from datetime import date
app = create_app()
def run():
with app.app_context():
# Create KPT company
company = Company.query.filter_by(slug='kpt-consulting').first()
if not company:
company = Company(
name='KPT-Consulting', slug='kpt-consulting',
strasse='Musterstr. 1', plz='88045', ort='Friedrichshafen',
telefon='+49 7541 123456', email='info@kpt-consulting.de'
)
db.session.add(company)
db.session.flush()
print(f'Firma: {company.name} (ID {company.id})')
else:
print(f'Firma: {company.name} (ID {company.id})')
# Superadmin (ohne company_id)
sa = User.query.filter_by(email='super@admin.de').first()
if not sa:
sa = User(
company_id=None, email='super@admin.de', rolle='superadmin',
vorname='Super', nachname='Admin',
darf_projekte_anlegen=True, darf_lv_verwalten=True,
darf_preise_sehen=True, darf_aufmass_verwalten=True,
darf_evergabe_nutzen=True, darf_kopfdaten_holen=True,
darf_aufmass_uebertragen=True,
)
sa.set_password('admin123')
db.session.add(sa)
print('Superadmin: super@admin.de / admin123')
else:
print('Superadmin existiert bereits')
# Firmadmin
fa = User.query.filter_by(email='firmadmin@kpt.de').first()
if not fa:
fa = User(
company_id=company.id, email='firmadmin@kpt.de',
vorname='Firmen', nachname='Admin', rolle='firmadmin',
darf_projekte_anlegen=True, darf_lv_verwalten=True,
darf_preise_sehen=True, darf_aufmass_verwalten=True,
darf_evergabe_nutzen=True, darf_kopfdaten_holen=True,
darf_aufmass_uebertragen=True,
)
fa.set_password('firma123')
db.session.add(fa)
print('Firmadmin: firmadmin@kpt.de / firma123')
else:
print('Firmadmin existiert bereits')
# Florian Kramer
fk = User.query.filter_by(email='fk@kpt-consulting.de').first()
if not fk:
fk = User(
company_id=company.id, email='fk@kpt-consulting.de',
vorname='Florian', nachname='Kramer', rolle='firmadmin',
darf_projekte_anlegen=True, darf_lv_verwalten=True,
darf_preise_sehen=True, darf_aufmass_verwalten=True,
darf_evergabe_nutzen=True, darf_kopfdaten_holen=True,
darf_aufmass_uebertragen=True,
)
fk.set_password('kpt2024')
db.session.add(fk)
print('Florian Kramer: fk@kpt-consulting.de / kpt2024')
else:
print('Florian Kramer existiert bereits')
db.session.commit()
print('\nSeed abgeschlossen.')
if __name__ == '__main__':
run()
+73
View File
@@ -0,0 +1,73 @@
import sys, os, re
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from app import create_app
from app.extensions import db
from app.models.company import Company
from app.models.user import User
app = create_app()
def slugify(name):
s = name.lower().strip()
s = re.sub(r'[^a-z0-9\s-]', '', s)
s = re.sub(r'[\s-]+', '-', s)
return s
def run():
with app.app_context():
slug = slugify('Dibran Dautaj Tief und Kabelbau')
company = Company.query.filter_by(slug=slug).first()
if not company:
company = Company(
name='Dibran Dautaj Tief und Kabelbau',
slug=slug,
strasse='Alemannenring',
house_number='25',
plz='88326',
ort='Aulendorf',
aktiv=True,
evergabe_aktiviert=False,
)
db.session.add(company)
db.session.flush()
print(f'Firma angelegt: {company.name} (ID {company.id})')
else:
print(f'Firma existiert bereits: {company.name} (ID {company.id})')
fa = User.query.filter_by(email='fk@dd-kabelbau.de').first()
if not fa:
fa = User(
company_id=company.id, email='fk@dd-kabelbau.de',
vorname='Florian', nachname='Kramer', rolle='firmadmin',
darf_projekte_anlegen=True, darf_lv_verwalten=True,
darf_preise_sehen=True, darf_aufmass_verwalten=True,
darf_evergabe_nutzen=True, darf_kopfdaten_holen=True,
darf_aufmass_uebertragen=True,
)
fa.set_password('Tami1234!')
db.session.add(fa)
print('Firmadmin: fk@dd-kabelbau.de / Tami1234!')
else:
print('Firmadmin fk@dd-kabelbau.de existiert bereits')
usr = User.query.filter_by(email='rs@dd-kabelbau.de').first()
if not usr:
usr = User(
company_id=company.id, email='rs@dd-kabelbau.de',
vorname='Robert', nachname='Schöndienst', rolle='mitarbeiter',
darf_projekte_anlegen=False, darf_lv_verwalten=False,
darf_preise_sehen=True, darf_aufmass_verwalten=True,
darf_evergabe_nutzen=False, darf_kopfdaten_holen=False,
darf_aufmass_uebertragen=False,
)
usr.set_password('Robert21071954')
db.session.add(usr)
print('User: rs@dd-kabelbau.de / Robert21071954')
else:
print('User rs@dd-kabelbau.de existiert bereits')
db.session.commit()
print('\nSeed abgeschlossen.')
if __name__ == '__main__':
run()
+131
View File
@@ -0,0 +1,131 @@
"""
Seed-Script: Legt Firma KPT-Consulting + User Florian an
und importiert das LV-KPT-Proj+Doku-2024ff inkl. Langtexte.
"""
import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from app import create_app
from app.extensions import db
from app.models.company import Company
from app.models.user import User
from app.models.lv import LVPosition
from app.models.contract import Contract
from datetime import date
app = create_app()
LV_NAME = 'LV-KPT-Proj+Doku-2024ff'
FLATFILE = os.path.join('..', 'daten', 'LV', f'{LV_NAME}.txt')
LANGFOLDER = os.path.join('..', 'daten', 'LV', LV_NAME)
def run():
with app.app_context():
# 1. Firma anlegen
company = Company.query.filter_by(slug='kpt-consulting').first()
if not company:
company = Company(
name='KPT-Consulting', slug='kpt-consulting',
strasse='Musterstr. 1', plz='88045', ort='Friedrichshafen',
telefon='+49 7541 123456', email='info@kpt-consulting.de'
)
db.session.add(company)
db.session.flush()
print(f'Firma angelegt: {company.name} (ID {company.id})')
else:
print(f'Firma existiert bereits: {company.name} (ID {company.id})')
# 2. User Florian Kramer anlegen
user = User.query.filter_by(email='fk@kpt-consulting.de').first()
if not user:
user = User(
company_id=company.id, email='fk@kpt-consulting.de',
vorname='Florian', nachname='Kramer', rolle='firmadmin',
darf_projekte_anlegen=True, darf_lv_verwalten=True,
darf_preise_sehen=True, darf_aufmass_verwalten=True,
)
user.set_password('kpt2024')
db.session.add(user)
db.session.flush()
print(f'User angelegt: {user.full_name} ({user.email})')
else:
print(f'User existiert bereits: {user.full_name}')
# 3. Vertrag anlegen
contract = Contract.query.filter_by(company_id=company.id, belegnummer='4650014601').first()
if not contract:
contract = Contract(
company_id=company.id,
name='SW Proj+Doku 2024ff',
belegnummer='4650014601',
beleg_datum=date(2025, 12, 16),
laufzeit_start=date(2024, 6, 1),
laufzeit_ende=date(2026, 5, 31),
status='Angenommen',
)
db.session.add(contract)
db.session.flush()
print(f'Vertrag angelegt: {contract.name}')
else:
print(f'Vertrag existiert bereits: {contract.name}')
# 4. LV-Positionen importieren
if not os.path.exists(FLATFILE):
print(f'FEHLER: Flatfile nicht gefunden: {FLATFILE}')
return
existing = LVPosition.query.filter_by(company_id=company.id, lv_name=LV_NAME).count()
if existing > 0:
print(f'LV {LV_NAME} bereits importiert ({existing} Positionen). Überspringe.')
return
# Encoding automatisch erkennen (UTF-16-BE/LE oder UTF-8)
with open(FLATFILE, 'rb') as f:
raw = f.read()
if raw[:2] in (b'\xff\xfe', b'\xfe\xff'):
enc = 'utf-16'
else:
enc = 'utf-8'
text = raw.decode(enc, errors='replace').strip('\ufeff').replace('\r\n', '\n').replace('\r', '\n')
lines = text.split('\n')
count = 0
for idx, line in enumerate(lines):
line = line.strip()
if not line or line.startswith('#'):
continue
parts = [p.strip() for p in line.split('|')]
if len(parts) < 2:
continue
pos_nr = parts[0]
kurztext = parts[1] if len(parts) > 1 else ''
einheit = parts[2] if len(parts) > 2 else 'ST'
preis_str = parts[3] if len(parts) > 3 else '0'
einzelpreis = float(preis_str.replace(',', '.')) if preis_str else 0.0
# Langtext aus Einzeldatei lesen
langtext = ''
txt_path = os.path.join(LANGFOLDER, f'{pos_nr}.txt')
if os.path.exists(txt_path):
with open(txt_path, 'rb') as tf:
raw_txt = tf.read()
if raw_txt[:2] in (b'\xff\xfe', b'\xfe\xff'):
enc_txt = 'utf-16'
else:
enc_txt = 'utf-8'
langtext = raw_txt.decode(enc_txt, errors='replace').strip()
pos = LVPosition(
company_id=company.id, contract_id=contract.id, lv_name=LV_NAME,
pos_nr=pos_nr, order_index=idx + 1,
kurztext=kurztext, langtext=langtext,
einheit=einheit, einzelpreis=einzelpreis,
)
db.session.add(pos)
count += 1
db.session.commit()
print(f'{count} LV-Positionen importiert (mit Langtext aus {LANGFOLDER})')
if __name__ == '__main__':
run()
@@ -0,0 +1,290 @@
"""
Seed-Script: Erzeugt CustomModule-Vorlage "SAS Meckenbeuren"
mit vollständigem Formular- und Regelwerk.
"""
import sys, os, json
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from app import create_app
from app.extensions import db
from app.models.custom_module import CustomModule
app = create_app()
FORM_JSON = [
# ── HA (Hausanschluss) ──
{"type":"group_start","title":"Hausanschluss","collapsible":False},
{"type":"checkbox","name":"ha_herstellen","label":"HA Herstellen"},
{"type":"text","name":"scan_name_ha","label":"Scan Name","placeholder":"Scan Name"},
{"type":"number","name":"trassenmeter","label":"Trassenmeter","inputmode":"decimal","step":"0.1"},
{"type":"number","name":"kabelmeter_ha","label":"Kabelsichern (m)","inputmode":"decimal","step":"0.1"},
{"type":"number","name":"anz_qkr","label":"Anzahl Q-Kabel/Rohre","inputmode":"numeric","step":"1"},
{"type":"number","name":"anz_qst","label":"Anzahl Querungsstellen","inputmode":"numeric","step":"1"},
{"type":"number","name":"strqm","label":"Straßenquerung (m)","inputmode":"decimal","step":"0.1"},
{"type":"number","name":"anz_suchgrube_ha","label":"Anzahl Suchgrube","inputmode":"numeric","step":"1"},
{"type":"checkbox","name":"einzug_10er","label":"Einzug 10er Pipes"},
{"type":"group_end"},
# ── TB (Tiefbau) ──
{"type":"group_start","title":"Tiefbau","collapsible":False},
{"type":"text","name":"scan_name_tb","label":"Scan Name","placeholder":"Scan Name"},
{"type":"number","name":"tb_laenge","label":"Länge (m)","inputmode":"decimal","step":"0.1"},
{"type":"number","name":"tb_tiefe","label":"Tiefe (m)","inputmode":"decimal","step":"0.1"},
{"type":"checkbox","name":"tb_unbefestigt","label":"unbefestigt (Wiese, Kies)"},
{"type":"checkbox","name":"tb_befestigt","label":"befestigt (Pflaster, Asphalt)"},
{"type":"number","name":"tb_anz_12x10","label":"Anzahl Rohre 12×10","inputmode":"numeric","step":"1"},
{"type":"number","name":"tb_anz_4x20","label":"Anzahl Rohre 4×20","inputmode":"numeric","step":"1"},
{"type":"number","name":"tb_anz_qk","label":"Anzahl Q-Kabel/Rohre","inputmode":"numeric","step":"1"},
{"type":"number","name":"tb_anz_qs","label":"Anzahl Querungsst.","inputmode":"numeric","step":"1"},
{"type":"number","name":"tb_kabelmeter","label":"Kabelsichern (m)","inputmode":"decimal","step":"0.1"},
{"type":"number","name":"tb_anz_suchgrube","label":"Anzahl Suchgrube","inputmode":"numeric","step":"1"},
{"type":"checkbox","name":"tb_kg2","label":"Kabelgraben 2"},
{"type":"checkbox","name":"tb_kg4","label":"Kabelgraben 4"},
{"type":"checkbox","name":"tb_kg6","label":"Kabelgraben 6"},
{"type":"group_end"},
]
RULES_JSON = [
# ════════════════════════════════════════
# HA (Hausanschluss)
# ════════════════════════════════════════
# HA Basis
{"name":"HA Basispositionen","conditions":{"operator":"and","items":[
{"field":"ha_herstellen","operator":"is_checked"}]},
"actions":[
{"pos_nr":"01.06.0001","columns":{"menge":{"type":"fixed","value":"1"}}},
{"pos_nr":"01.06.0003","columns":{"menge":{"type":"fixed","value":"1"}}},
{"pos_nr":"01.06.0007","columns":{"menge":{"type":"fixed","value":"1"}}}]},
# HA Q-Kabel/Querung
{"name":"HA Q-Kabel / Querung","conditions":{"operator":"and","items":[
{"field":"ha_herstellen","operator":"is_checked"},
{"field":"anz_qkr","operator":"gt","value":"0"}]},
"actions":[
{"pos_nr":"01.03.0019","columns":{"faktor":{"type":"field","value":"anz_qst"},"laenge":{"type":"fixed","value":"1"},"menge":{"type":"fixed","value":"1"},"bemerkung":{"type":"fixed","value":"Siehe Bild: "}}},
{"pos_nr":"01.03.0020","columns":{"faktor":{"type":"field","value":"anz_qkr"},"laenge":{"type":"fixed","value":"0.5"},"menge":{"type":"fixed","value":"0.5"},"bemerkung":{"type":"fixed","value":"Siehe Bild: "}}}]},
# HA Trassenmeter
{"name":"HA Trassenmeter","conditions":{"operator":"and","items":[
{"field":"ha_herstellen","operator":"is_checked"},
{"field":"trassenmeter","operator":"gt","value":"0"}]},
"actions":[
{"pos_nr":"01.06.0004","columns":{"laenge":{"type":"field","value":"trassenmeter"}}}]},
# HA Einzug 10er
{"name":"HA Einzug 10er Pipes","conditions":{"operator":"and","items":[
{"field":"ha_herstellen","operator":"is_checked"},
{"field":"trassenmeter","operator":"gt","value":"0"},
{"field":"einzug_10er","operator":"is_checked"}]},
"actions":[
{"pos_nr":"01.04.0003","columns":{"laenge":{"type":"field","value":"trassenmeter"},"menge":{"type":"field","value":"trassenmeter"}}},
{"pos_nr":"01.06.0006","columns":{"laenge":{"type":"field","value":"trassenmeter"},"menge":{"type":"field","value":"trassenmeter"}}}]},
# HA ohne Einzug
{"name":"HA ohne Einzug","conditions":{"operator":"and","items":[
{"field":"ha_herstellen","operator":"is_checked"},
{"field":"trassenmeter","operator":"gt","value":"0"},
{"field":"einzug_10er","operator":"is_empty"}]},
"actions":[
{"pos_nr":"01.06.0006","columns":{"laenge":{"type":"formula","value":"[trassenmeter]+1+[strqm]"}}}]},
# HA Kabelsichern
{"name":"HA Kabelsichern","conditions":{"operator":"and","items":[
{"field":"ha_herstellen","operator":"is_checked"},
{"field":"kabelmeter_ha","operator":"gt","value":"0"}]},
"actions":[
{"pos_nr":"01.03.0019","columns":{"laenge":{"type":"field","value":"kabelmeter_ha"},"menge":{"type":"field","value":"kabelmeter_ha"},"bemerkung":{"type":"fixed","value":"Siehe Bild: "}}},
{"pos_nr":"01.03.0020","columns":{"laenge":{"type":"field","value":"kabelmeter_ha"},"menge":{"type":"field","value":"kabelmeter_ha"},"bemerkung":{"type":"fixed","value":"Siehe Bild: "}}}]},
# HA Straßenquerung
{"name":"HA Straßenquerung","conditions":{"operator":"and","items":[
{"field":"ha_herstellen","operator":"is_checked"},
{"field":"strqm","operator":"gt","value":"0"}]},
"actions":[
{"pos_nr":"01.03.0008","columns":{"laenge":{"type":"field","value":"strqm"},"menge":{"type":"field","value":"strqm"},"bemerkung":{"type":"fixed","value":"Öffentlicherbereich "}}}]},
# HA Suchgrube
{"name":"HA Suchgrube","conditions":{"operator":"and","items":[
{"field":"ha_herstellen","operator":"is_checked"},
{"field":"anz_suchgrube_ha","operator":"gt","value":"0"}]},
"actions":[
{"pos_nr":"01.03.0018","columns":{"menge":{"type":"field","value":"anz_suchgrube_ha"},"bemerkung":{"type":"fixed","value":"Öffentlicherbereich "}}}]},
# ════════════════════════════════════════
# TB (Tiefbau)
# ════════════════════════════════════════
# -- Tiefe ≤ 0,65 --
{"name":"TB 0,6m unbefestigt","conditions":{"operator":"and","items":[
{"field":"tb_laenge","operator":"gt","value":"0"},
{"field":"tb_tiefe","operator":"gt","value":"0"},
{"field":"tb_tiefe","operator":"lte","value":"0.65"},
{"field":"tb_unbefestigt","operator":"is_checked"}]},
"actions":[
{"pos_nr":"01.03.0001","columns":{"laenge":{"type":"field","value":"tb_laenge"}}}]},
{"name":"TB 0,6m KG 2","conditions":{"operator":"and","items":[
{"field":"tb_laenge","operator":"gt","value":"0"},
{"field":"tb_tiefe","operator":"gt","value":"0"},
{"field":"tb_tiefe","operator":"lte","value":"0.65"},
{"field":"tb_befestigt","operator":"is_checked"},
{"field":"tb_kg2","operator":"is_checked"}]},
"actions":[
{"pos_nr":"01.03.0004","columns":{"laenge":{"type":"field","value":"tb_laenge"}}}]},
{"name":"TB 0,6m KG 4","conditions":{"operator":"and","items":[
{"field":"tb_laenge","operator":"gt","value":"0"},
{"field":"tb_tiefe","operator":"gt","value":"0"},
{"field":"tb_tiefe","operator":"lte","value":"0.65"},
{"field":"tb_befestigt","operator":"is_checked"},
{"field":"tb_kg4","operator":"is_checked"}]},
"actions":[
{"pos_nr":"01.03.0005","columns":{"laenge":{"type":"field","value":"tb_laenge"}}}]},
{"name":"TB 0,6m KG 6","conditions":{"operator":"and","items":[
{"field":"tb_laenge","operator":"gt","value":"0"},
{"field":"tb_tiefe","operator":"gt","value":"0"},
{"field":"tb_tiefe","operator":"lte","value":"0.65"},
{"field":"tb_befestigt","operator":"is_checked"},
{"field":"tb_kg6","operator":"is_checked"}]},
"actions":[
{"pos_nr":"01.03.0006","columns":{"laenge":{"type":"field","value":"tb_laenge"}}}]},
# -- Tiefe ≤ 0,9 --
{"name":"TB 0,8m unbefestigt","conditions":{"operator":"and","items":[
{"field":"tb_laenge","operator":"gt","value":"0"},
{"field":"tb_tiefe","operator":"gt","value":"0.65"},
{"field":"tb_tiefe","operator":"lte","value":"0.9"},
{"field":"tb_unbefestigt","operator":"is_checked"}]},
"actions":[
{"pos_nr":"01.03.0002","columns":{"laenge":{"type":"field","value":"tb_laenge"}}}]},
{"name":"TB 0,8m KG 2","conditions":{"operator":"and","items":[
{"field":"tb_laenge","operator":"gt","value":"0"},
{"field":"tb_tiefe","operator":"gt","value":"0.65"},
{"field":"tb_tiefe","operator":"lte","value":"0.9"},
{"field":"tb_befestigt","operator":"is_checked"},
{"field":"tb_kg2","operator":"is_checked"}]},
"actions":[
{"pos_nr":"01.03.0008","columns":{"laenge":{"type":"field","value":"tb_laenge"}}}]},
{"name":"TB 0,8m KG 4","conditions":{"operator":"and","items":[
{"field":"tb_laenge","operator":"gt","value":"0"},
{"field":"tb_tiefe","operator":"gt","value":"0.65"},
{"field":"tb_tiefe","operator":"lte","value":"0.9"},
{"field":"tb_befestigt","operator":"is_checked"},
{"field":"tb_kg4","operator":"is_checked"}]},
"actions":[
{"pos_nr":"01.03.0009","columns":{"laenge":{"type":"field","value":"tb_laenge"}}}]},
{"name":"TB 0,8m KG 6","conditions":{"operator":"and","items":[
{"field":"tb_laenge","operator":"gt","value":"0"},
{"field":"tb_tiefe","operator":"gt","value":"0.65"},
{"field":"tb_tiefe","operator":"lte","value":"0.9"},
{"field":"tb_befestigt","operator":"is_checked"},
{"field":"tb_kg6","operator":"is_checked"}]},
"actions":[
{"pos_nr":"01.03.0010","columns":{"laenge":{"type":"field","value":"tb_laenge"}}}]},
# -- Tiefe > 0,9 --
{"name":"TB 1,2m unbefestigt","conditions":{"operator":"and","items":[
{"field":"tb_laenge","operator":"gt","value":"0"},
{"field":"tb_tiefe","operator":"gt","value":"0.9"},
{"field":"tb_unbefestigt","operator":"is_checked"}]},
"actions":[
{"pos_nr":"01.03.0003","columns":{"laenge":{"type":"field","value":"tb_laenge"}}}]},
{"name":"TB 1,2m KG 2","conditions":{"operator":"and","items":[
{"field":"tb_laenge","operator":"gt","value":"0"},
{"field":"tb_tiefe","operator":"gt","value":"0.9"},
{"field":"tb_befestigt","operator":"is_checked"},
{"field":"tb_kg2","operator":"is_checked"}]},
"actions":[
{"pos_nr":"01.03.0012","columns":{"laenge":{"type":"field","value":"tb_laenge"}}}]},
{"name":"TB 1,2m KG 4","conditions":{"operator":"and","items":[
{"field":"tb_laenge","operator":"gt","value":"0"},
{"field":"tb_tiefe","operator":"gt","value":"0.9"},
{"field":"tb_befestigt","operator":"is_checked"},
{"field":"tb_kg4","operator":"is_checked"}]},
"actions":[
{"pos_nr":"01.03.0013","columns":{"laenge":{"type":"field","value":"tb_laenge"}}}]},
{"name":"TB 1,2m KG 6","conditions":{"operator":"and","items":[
{"field":"tb_laenge","operator":"gt","value":"0"},
{"field":"tb_tiefe","operator":"gt","value":"0.9"},
{"field":"tb_befestigt","operator":"is_checked"},
{"field":"tb_kg6","operator":"is_checked"}]},
"actions":[
{"pos_nr":"01.03.0014","columns":{"laenge":{"type":"field","value":"tb_laenge"}}}]},
# TB Rohre
{"name":"TB Rohre 4×20","conditions":{"operator":"and","items":[
{"field":"tb_laenge","operator":"gt","value":"0"},
{"field":"tb_tiefe","operator":"gt","value":"0"},
{"field":"tb_anz_4x20","operator":"gt","value":"0"}]},
"actions":[
{"pos_nr":"01.04.0001","columns":{"faktor":{"type":"field","value":"tb_anz_4x20"},"laenge":{"type":"field","value":"tb_laenge"},"menge":{"type":"field","value":"tb_laenge"},"bemerkung":{"type":"fixed","value":"4x20 Rohre"}}}]},
{"name":"TB Rohre 12×10","conditions":{"operator":"and","items":[
{"field":"tb_laenge","operator":"gt","value":"0"},
{"field":"tb_tiefe","operator":"gt","value":"0"},
{"field":"tb_anz_12x10","operator":"gt","value":"0"}]},
"actions":[
{"pos_nr":"01.04.0002","columns":{"faktor":{"type":"field","value":"tb_anz_12x10"},"laenge":{"type":"field","value":"tb_laenge"},"menge":{"type":"field","value":"tb_laenge"},"bemerkung":{"type":"fixed","value":"12x10 Rohre"}}}]},
# TB Querung
{"name":"TB Querung","conditions":{"operator":"and","items":[
{"field":"tb_anz_qs","operator":"gt","value":"0"}]},
"actions":[
{"pos_nr":"01.03.0019","columns":{"faktor":{"type":"field","value":"tb_anz_qs"},"laenge":{"type":"fixed","value":"1"},"menge":{"type":"fixed","value":"1"},"bemerkung":{"type":"fixed","value":"Siehe Bild: "}}},
{"pos_nr":"01.03.0020","columns":{"faktor":{"type":"field","value":"tb_anz_qk"},"laenge":{"type":"fixed","value":"0.5"},"menge":{"type":"fixed","value":"0.5"},"bemerkung":{"type":"fixed","value":"Siehe Bild: "}}}]},
# TB Kabelsichern
{"name":"TB Kabelsichern","conditions":{"operator":"and","items":[
{"field":"tb_kabelmeter","operator":"gt","value":"0"}]},
"actions":[
{"pos_nr":"01.03.0019","columns":{"laenge":{"type":"field","value":"tb_kabelmeter"},"menge":{"type":"field","value":"tb_kabelmeter"},"bemerkung":{"type":"fixed","value":"Siehe Bild: "}}},
{"pos_nr":"01.03.0020","columns":{"laenge":{"type":"field","value":"tb_kabelmeter"},"menge":{"type":"field","value":"tb_kabelmeter"},"bemerkung":{"type":"fixed","value":"Siehe Bild: "}}}]},
# TB Suchgrube
{"name":"TB Suchgrube","conditions":{"operator":"and","items":[
{"field":"tb_anz_suchgrube","operator":"gt","value":"0"}]},
"actions":[
{"pos_nr":"01.03.0018","columns":{"menge":{"type":"field","value":"tb_anz_suchgrube"},"bemerkung":{"type":"fixed","value":"Öffentlicherbereich "}}}]},
]
def run():
with app.app_context():
name = 'SAS Meckenbeuren'
existing = CustomModule.query.filter_by(name=name, is_template=True).first()
if existing:
existing.form_json = json.dumps(FORM_JSON, ensure_ascii=False)
existing.rules_json = json.dumps(RULES_JSON, ensure_ascii=False)
existing.description = 'SAS Meckenbeuren Hausanschluss + Tiefbau (portiert vom Legacy-Modul)'
db.session.commit()
print(f'OK - Vorlage "{name}" aktualisiert (ID {existing.id})')
print(f' Formular-Felder: {len(FORM_JSON)}')
print(f' Regeln: {len(RULES_JSON)}')
return
mod = CustomModule(
name=name,
description='SAS Meckenbeuren Hausanschluss + Tiefbau (portiert vom Legacy-Modul)',
kategorie='Spezial',
icon='📍',
form_json=json.dumps(FORM_JSON, ensure_ascii=False),
rules_json=json.dumps(RULES_JSON, ensure_ascii=False),
is_template=True,
sort_index=0,
is_active=True,
)
db.session.add(mod)
db.session.commit()
print(f'OK - Vorlage "{name}" angelegt (ID {mod.id})')
print(f' Formular-Felder: {len(FORM_JSON)}')
print(f' Regeln: {len(RULES_JSON)}')
if __name__ == '__main__':
run()
+17
View File
@@ -0,0 +1,17 @@
# Startet PostgreSQL-Dienst falls nicht bereits laufend
$svc = Get-Service postgresql-x64-16 -ErrorAction SilentlyContinue
if (-not $svc) {
Write-Host "PostgreSQL Dienst nicht gefunden. Installiert?" -ForegroundColor Red
exit 1
}
if ($svc.Status -ne 'Running') {
Write-Host "Starte PostgreSQL..." -ForegroundColor Yellow
Start-Process -FilePath "powershell" -ArgumentList "-Command Start-Service postgresql-x64-16" -Verb RunAs -Wait
Start-Sleep 2
$svc.Refresh()
}
if ($svc.Status -eq 'Running') {
Write-Host "PostgreSQL laeuft." -ForegroundColor Green
} else {
Write-Host "PostgreSQL Start fehlgeschlagen." -ForegroundColor Red
}
+55
View File
@@ -0,0 +1,55 @@
"""Test the formel update API flow"""
import urllib.request, urllib.parse, json, sys
BASE = 'http://127.0.0.1:5000'
# Login
s = urllib.request.build_opener(urllib.request.HTTPCookieProcessor())
req = urllib.request.Request(BASE + '/auth/login',
data=b'email=fk@kpt-consulting.de&password=kpt2024')
s.open(req)
# Add a test position
data = urllib.parse.urlencode({
'pos_nr': 'FORMEL-TEST', 'kurztext': 'Test Formel',
'einheit': 'M', 'einzelpreis': '50',
'faktor': '1.0', 'laenge': '5.0'
}).encode()
req = urllib.request.Request(BASE + '/projekt/1/1/position/hinzufuegen',
data=data, method='POST')
r = s.open(req)
print('Add position:', r.status)
# Get positions
req = urllib.request.Request(BASE + '/projekt/1/1/positionen')
r = s.open(req)
positions = json.loads(r.read())
pos_id = positions[-1]['id']
print(f'Position ID: {pos_id}')
# Set formel_typ to frei
body = json.dumps({'field': 'formel_typ', 'value': 'frei'}).encode()
req = urllib.request.Request(
BASE + f'/projekt/1/1/position/{pos_id}/update-cell',
data=body, method='POST',
headers={'Content-Type': 'application/json'})
r = s.open(req)
resp = json.loads(r.read())
print(f'After formel_typ=frei: menge={resp.get("menge")} gp={resp.get("gesamtpreis")} hinten={resp.get("menge_hinten")}')
# Set formel value
body = json.dumps({'field': 'formel', 'value': '2*3+1'}).encode()
req = urllib.request.Request(
BASE + f'/projekt/1/1/position/{pos_id}/update-cell',
data=body, method='POST',
headers={'Content-Type': 'application/json'})
r = s.open(req)
resp = json.loads(r.read())
print(f'After formel=2*3+1: menge={resp.get("menge")} gp={resp.get("gesamtpreis")} hinten={resp.get("menge_hinten")}')
# Cleanup: delete test position
req = urllib.request.Request(
BASE + f'/projekt/1/1/position/{pos_id}/loeschen', method='POST')
s.open(req)
print(f'Test position {pos_id} deleted')
print('\nSUCCESS: menge_hinten was correctly returned in response')
+74
View File
@@ -0,0 +1,74 @@
"""Quick integration test for v2 changes"""
import sys, os, json
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from app import create_app
from app.extensions import db
from app.models.position import Position
from app.models.user import User
app = create_app()
with app.test_client() as c:
c.post('/auth/login', data={'email': 'fk@kpt-consulting.de', 'password': 'kpt2024'})
# 1. Add a custom position
r = c.post('/projekt/1/1/position/hinzufuegen', data={
'pos_nr': 'TEST-001', 'kurztext': 'Testposition',
'einheit': 'ST', 'einzelpreis': '100.00'
})
print(f'1. Position hinzufuegen: {r.status_code}')
# 2. List positions
r = c.get('/projekt/1/1/positionen')
data = json.loads(r.data)
print(f'2. Positionen: {len(data)} found')
# 3. Update cell
positions = Position.query.filter_by(aufmass_id=1).all()
if positions:
pos_id = positions[0].id
r = c.post(f'/projekt/1/1/position/{pos_id}/update-cell', json={
'field': 'menge', 'value': 5.0
})
res = json.loads(r.data)
print(f'3. Update-cell menge: {r.status_code} -> hinten={res.get("menge_hinten")} gp={res.get("gesamtpreis")}')
# 4. Farben endpoint
r = c.get('/projekt/1/1/positionen/farben')
print(f'4. Farben: {r.status_code}')
# 5. Firmen page
r = c.get('/admin/firma')
print(f'5. Firma page: {r.status_code}')
# 6. Superadmin test
c.post('/auth/login', data={'email': 'super@admin.de', 'password': 'admin'})
r = c.get('/superadmin/')
html = r.data.decode()
print(f'6. Superadmin dashboard: {r.status_code} (Firmen: {"KPT" in html})')
# 7. Firm detail
r = c.get('/superadmin/firma/1')
print(f'7. Firm detail: {r.status_code}')
# 8. LV page as superadmin
r = c.get('/lv/')
print(f'8. LV page: {r.status_code}')
# 9. Create new aufmass
c.post('/auth/login', data={'email': 'fk@kpt-consulting.de', 'password': 'kpt2024'})
r = c.post('/projekt/1/projekt/neu', data={
'name': 'Teilaufmass 1', 'typ': 'Teilaufmass'
}, follow_redirects=True)
print(f'9. Aufmass created: {r.status_code}')
# 10. List aufmasse
r = c.get('/projekt/1')
print(f'10. Aufmass list: {r.status_code}')
# 11. Check new aufmass in editor
r = c.get('/projekt/1/2')
print(f'11. Editor for new aufmass: {r.status_code}')
print('\n=== ALL TESTS PASSED ===')