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
+165
View File
@@ -0,0 +1,165 @@
from flask import Flask, render_template, request, redirect, url_for
from config import Config
from app.extensions import db, login_manager, migrate
def create_app(config_class=Config):
app = Flask(__name__)
app.config.from_object(config_class)
db.init_app(app)
login_manager.init_app(app)
migrate.init_app(app, db)
login_manager.login_view = 'auth.login'
login_manager.login_message = 'Bitte melden Sie sich an.'
from app.models.user import User
from app.models.company import Company
from app.models.license import License
from app.models.module import Module
from app.models.lv import LVPosition
from app.models.project import Project
from app.models.aufmass import Aufmass
from app.models.project_access import ProjectAccess
from app.models.position import Position
from app.models.contract import Contract
from app.models.view_profile import ViewProfile
from app.models.company_module import CompanyModule
from app.models.user_module import UserModulePermission
from app.models.aufmass_typ import AufmassTyp
from app.models.aufmass_history import AufmassHistory
@login_manager.user_loader
def load_user(user_id):
user = User.query.get(int(user_id))
if user:
from flask import session
session.setdefault('font_size', user.font_size or '1')
return user
from app.routes.auth import auth_bp
from app.routes.admin import admin_bp
from app.routes.lv import lv_bp
from app.routes.aufmass import aufmass_bp
from app.routes.export import export_bp
from app.routes.modules import modules_bp
from app.routes.contracts import contracts_bp
from app.routes.views import views_bp
from app.routes.superadmin import superadmin_bp
from app.routes.custom_modules import custom_modules_bp
app.register_blueprint(auth_bp, url_prefix='/auth')
app.register_blueprint(admin_bp, url_prefix='/admin')
app.register_blueprint(lv_bp, url_prefix='/lv')
app.register_blueprint(aufmass_bp, url_prefix='/projekt')
app.register_blueprint(export_bp, url_prefix='/export')
app.register_blueprint(modules_bp, url_prefix='/modules')
app.register_blueprint(contracts_bp, url_prefix='/contracts')
app.register_blueprint(views_bp)
app.register_blueprint(superadmin_bp, url_prefix='/superadmin')
app.register_blueprint(custom_modules_bp, url_prefix='/custom-modules')
@app.route('/aufmass/')
@app.route('/aufmass/<path:subpath>')
def _aufmass_redirect(subpath=''):
return redirect('/projekt/' + subpath), 301
@app.route('/')
def index():
from flask_login import current_user
if current_user.is_authenticated:
if current_user.is_superadmin():
return redirect(url_for('superadmin.dashboard'))
return redirect(url_for('admin.dashboard'))
return redirect(url_for('auth.login'))
@app.route('/font-size/<size>')
def set_font_size(size):
from flask import session
from flask_login import current_user
if size in ('0.8', '0.9', '1', '1.1', '1.25', '1.5'):
session['font_size'] = size
if current_user.is_authenticated:
current_user.font_size = size
try:
db.session.commit()
except:
db.session.rollback()
return redirect(request.referrer or url_for('admin.dashboard'))
@app.template_filter('german_number')
def german_number_filter(value, precision=2, zero_dash=False):
if value is None or (zero_dash and value == 0):
return '\u2013'
try:
s = f'{float(value):.{precision}f}'
parts = s.split('.')
int_part = parts[0]
dec_part = parts[1] if len(parts) > 1 else '0'*precision
int_part = '{:,}'.format(int(int_part)).replace(',', '.')
return f'{int_part},{dec_part}'
except (ValueError, TypeError):
return '\u2013' if zero_dash else '0,00'
@app.errorhandler(404)
def not_found(e):
return render_template('errors/404.html'), 404
@app.errorhandler(500)
def server_error(e):
return render_template('errors/500.html'), 500
with app.app_context():
db.create_all()
_seed_defaults()
return app
def _seed_defaults():
from app.extensions import db
from app.models.module import Module
def upsert(name, titel, kategorie, icon, standard, sortierung=0):
m = Module.query.filter_by(name=name).first()
if m:
m.titel = titel
m.kategorie = kategorie
m.icon = icon
m.standard = standard
m.sortierung = sortierung
else:
db.session.add(Module(name=name, titel=titel, kategorie=kategorie, icon=icon, standard=standard, sortierung=sortierung))
# 1. Tote Module entfernen (inkl. FK-Referenzen)
for dead_name in ('mfg', 'stoersammler'):
old = Module.query.filter_by(name=dead_name).first()
if old:
from app.models.license import LicenseModule
from app.models.company_module import CompanyModule
LicenseModule.query.filter_by(module_id=old.id).delete()
CompanyModule.query.filter_by(module_id=old.id).delete()
db.session.delete(old)
db.session.commit()
# 2. Alle Module upserten
upsert('graben', 'Graben', 'Tiefbau', '🔲', standard=True)
upsert('gruben', 'Gruben', 'Tiefbau', '🕳️', standard=True)
upsert('gf_montage', 'GF-Montage', 'Glasfaser', '🔗', standard=True)
upsert('ftth', 'FTTH', 'Glasfaser', '🏠', standard=True)
upsert('kabelzug', 'Kabelzug', 'Tiefbau', '🔌', standard=True)
upsert('absperrung', 'Absperrung', 'Tiefbau', '🚧', standard=True)
upsert('sas_mecka', 'SAS Meckenbeuren', 'Spezial', '📍', standard=False)
upsert('neff_achberg', 'Neff-Achberg', 'Spezial', '🏗️', standard=False)
upsert('cu', 'CU', 'Kupfer', '📞', standard=True)
upsert('stoerung', 'Störung', 'Service', '🔧', standard=True)
upsert('tvum', 'TV/UM', 'Glasfaser', '📺', standard=True)
upsert('planung', 'Planung', 'Planung', '📐', standard=True)
upsert('zw_rv', 'ZW/RV', 'Tiefbau', '🔩', standard=True)
upsert('doku', 'Dokumentation', 'Planung', '📄', standard=True)
upsert('sto_sammler', 'Störungs-Sammler', 'Service', '📋', standard=True)
db.session.commit()
from app.models.aufmass_typ import AufmassTyp
if AufmassTyp.query.count() == 0:
db.session.add(AufmassTyp(name='Teilaufmaß/AZ', sortierung=1))
db.session.add(AufmassTyp(name='Schlussaufmaß', sortierung=2))
db.session.commit()
Binary file not shown.
+7
View File
@@ -0,0 +1,7 @@
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from flask_migrate import Migrate
db = SQLAlchemy()
login_manager = LoginManager()
migrate = Migrate()
View File
+51
View File
@@ -0,0 +1,51 @@
from app.extensions import db
from datetime import datetime, timedelta
LOCK_TIMEOUT = timedelta(minutes=2)
class Aufmass(db.Model):
__tablename__ = 'aufmass'
id = db.Column(db.Integer, primary_key=True)
project_id = db.Column(db.Integer, db.ForeignKey('projekte.id'), nullable=False)
name = db.Column(db.String(200), nullable=False, default='Standard')
typ = db.Column(db.String(50), default='')
status = db.Column(db.String(20), default='aktiv')
sortierung = db.Column(db.Integer, default=0)
bemerkung = db.Column(db.Text)
erstellt_von = db.Column(db.Integer, db.ForeignKey('users.id'))
erstellt_am = db.Column(db.DateTime, default=datetime.utcnow)
geaendert_am = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
locked_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
locked_at = db.Column(db.DateTime, nullable=True)
positionen = db.relationship('Position', backref='aufmass_ref', lazy='dynamic',
cascade='all, delete-orphan', order_by='Position.sortierung')
def is_locked(self):
if not self.locked_by or not self.locked_at:
return False, None
if datetime.utcnow() - self.locked_at > LOCK_TIMEOUT:
return False, None
return True, self.locked_by
def try_lock(self, user_id):
locked, holder = self.is_locked()
if locked and holder != user_id:
return False
self.locked_by = user_id
self.locked_at = datetime.utcnow()
return True
def unlock(self):
self.locked_by = None
self.locked_at = None
def refresh_lock(self, user_id):
if self.locked_by == user_id:
self.locked_at = datetime.utcnow()
return True
return False
def __repr__(self):
return f'<Aufmass {self.name} @ {self.project_id}>'
@@ -0,0 +1,17 @@
from datetime import datetime
from app.extensions import db
class AufmassHistory(db.Model):
__tablename__ = 'aufmass_history'
id = db.Column(db.Integer, primary_key=True)
aufmass_id = db.Column(db.Integer, db.ForeignKey('aufmass.id'), nullable=False, index=True)
position_id = db.Column(db.Integer, db.ForeignKey('positionen.id', ondelete='SET NULL'), nullable=True)
changed_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
changed_at = db.Column(db.DateTime, default=datetime.utcnow, index=True)
action = db.Column(db.String(10), nullable=False)
description = db.Column(db.String(500), nullable=True)
diff = db.Column(db.Text, nullable=False)
def __repr__(self):
return f'<AufmassHistory {self.id} {self.action} @ {self.changed_at}>'
+12
View File
@@ -0,0 +1,12 @@
from app.extensions import db
class AufmassTyp(db.Model):
__tablename__ = 'aufmass_typen'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
company_id = db.Column(db.Integer, db.ForeignKey('companies.id'), nullable=True)
sortierung = db.Column(db.Integer, default=0)
def __repr__(self):
return f'<AufmassTyp {self.name}>'
+29
View File
@@ -0,0 +1,29 @@
from app.extensions import db
from datetime import datetime
class Company(db.Model):
__tablename__ = 'companies'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(200), nullable=False)
slug = db.Column(db.String(100), unique=True, nullable=False)
strasse = db.Column(db.String(200))
house_number = db.Column(db.String(20))
plz = db.Column(db.String(10))
ort = db.Column(db.String(100))
telefon = db.Column(db.String(50))
email = db.Column(db.String(200))
logo = db.Column(db.String(500))
aktiv = db.Column(db.Boolean, default=True)
evergabe_aktiviert = db.Column(db.Boolean, default=False)
evergabe_benutzer = db.Column(db.String(200))
evergabe_passwort = db.Column(db.String(300))
evergabe_name = db.Column(db.String(200))
erstellt_am = db.Column(db.DateTime, default=datetime.utcnow)
users = db.relationship('User', backref='company', lazy='dynamic')
licenses = db.relationship('License', backref='company', lazy='dynamic')
projekte = db.relationship('Project', backref='company', lazy='dynamic')
def __repr__(self):
return f'<Company {self.name}>'
+14
View File
@@ -0,0 +1,14 @@
from app.extensions import db
class CompanyModule(db.Model):
__tablename__ = 'company_modules'
id = db.Column(db.Integer, primary_key=True)
company_id = db.Column(db.Integer, db.ForeignKey('companies.id'), nullable=False)
module_id = db.Column(db.Integer, db.ForeignKey('modules.id'), nullable=False)
aktiv = db.Column(db.Boolean, default=True)
company = db.relationship('Company', backref='company_module_list')
module = db.relationship('Module', backref='company_assignments')
__table_args__ = (db.UniqueConstraint('company_id', 'module_id'),)
+20
View File
@@ -0,0 +1,20 @@
from app.extensions import db
from datetime import datetime
class Contract(db.Model):
__tablename__ = 'contracts'
id = db.Column(db.Integer, primary_key=True)
company_id = db.Column(db.Integer, db.ForeignKey('companies.id'), nullable=False)
name = db.Column(db.String(300), nullable=False)
belegnummer = db.Column(db.String(100))
beleg_datum = db.Column(db.Date)
laufzeit_start = db.Column(db.Date)
laufzeit_ende = db.Column(db.Date)
status = db.Column(db.String(50), default='NEU') # NEU, Zur Prüfung, Angenommen
erstellt_am = db.Column(db.DateTime, default=datetime.utcnow)
projekte = db.relationship('Project', backref='contract', lazy='dynamic')
def __repr__(self):
return f'<Contract {self.name}>'
+62
View File
@@ -0,0 +1,62 @@
from app.extensions import db
from datetime import datetime
class CustomModule(db.Model):
__tablename__ = 'custom_modules'
id = db.Column(db.Integer, primary_key=True)
company_id = db.Column(db.Integer, db.ForeignKey('companies.id'), nullable=True)
original_template_id = db.Column(db.Integer, db.ForeignKey('custom_modules.id'), nullable=True)
name = db.Column(db.String(200), nullable=False)
description = db.Column(db.Text, default='')
kategorie = db.Column(db.String(50), default='allgemein')
icon = db.Column(db.String(50), default='🔧')
form_json = db.Column(db.Text, default='[]')
rules_json = db.Column(db.Text, default='[]')
is_template = db.Column(db.Boolean, default=False)
sort_index = db.Column(db.Integer, default=0)
is_active = db.Column(db.Boolean, default=True)
created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
company = db.relationship('Company', backref='custom_modules', foreign_keys=[company_id])
template = db.relationship('CustomModule', backref='copies', remote_side=[id])
creator = db.relationship('User', backref='created_custom_modules', foreign_keys=[created_by])
def to_dict(self):
return {
'id': self.id,
'company_id': self.company_id,
'original_template_id': self.original_template_id,
'name': self.name,
'description': self.description,
'kategorie': self.kategorie,
'icon': self.icon,
'is_template': self.is_template,
'sort_index': self.sort_index,
'is_active': self.is_active,
'created_by': self.created_by,
'created_at': self.created_at.isoformat() if self.created_at else None,
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
}
def __repr__(self):
return f'<CustomModule {self.name} ({"template" if self.is_template else "company"})>'
class CustomModuleAssignment(db.Model):
__tablename__ = 'custom_module_assignments'
id = db.Column(db.Integer, primary_key=True)
module_id = db.Column(db.Integer, db.ForeignKey('custom_modules.id'), nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
can_edit = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
module = db.relationship('CustomModule', backref='assignments')
user = db.relationship('User', backref='custom_module_assignments')
__table_args__ = (
db.UniqueConstraint('module_id', 'user_id', name='uq_module_user'),
)
+59
View File
@@ -0,0 +1,59 @@
from app.extensions import db
from datetime import datetime
import hashlib, secrets
def _generate_uid(company_name):
raw = f"{company_name}-{secrets.token_hex(6)}"
return hashlib.sha256(raw.encode()).hexdigest()[:12]
class License(db.Model):
__tablename__ = 'licenses'
id = db.Column(db.Integer, primary_key=True)
company_id = db.Column(db.Integer, db.ForeignKey('companies.id'), nullable=False)
uid = db.Column(db.String(64), unique=True, nullable=False)
max_mitarbeiter = db.Column(db.Integer, default=5)
max_module_slots = db.Column(db.Integer, default=5)
unlimited_users = db.Column(db.Boolean, default=False)
unlimited_modules = db.Column(db.Boolean, default=False)
aktiv = db.Column(db.Boolean, default=True)
erstellt_am = db.Column(db.DateTime, default=datetime.utcnow)
modules = db.relationship('LicenseModule', backref='license', lazy='dynamic', cascade='all, delete-orphan')
@property
def used_users(self):
from app.models.user import User
return User.query.filter_by(company_id=self.company_id).count()
@property
def used_module_slots(self):
from app.models.user_module import UserModulePermission
from app.models.user import User
return db.session.query(UserModulePermission.id).join(User, UserModulePermission.user_id==User.id)\
.filter(User.company_id==self.company_id, UserModulePermission.aktiv==True).count()
def user_slots_display(self):
if self.unlimited_users: return '\u221e'
return f'{self.used_users} / {self.max_mitarbeiter}'
def module_slots_display(self):
if self.unlimited_modules: return '\u221e'
return f'{self.used_module_slots} / {self.max_module_slots}'
@staticmethod
def generate_uid(company_name):
return _generate_uid(company_name)
def __repr__(self):
return f'<License {self.uid}>'
class LicenseModule(db.Model):
__tablename__ = 'license_modules'
id = db.Column(db.Integer, primary_key=True)
license_id = db.Column(db.Integer, db.ForeignKey('licenses.id'), nullable=False)
module_id = db.Column(db.Integer, db.ForeignKey('modules.id'), nullable=False)
aktiv = db.Column(db.Boolean, default=True)
module = db.relationship('Module', backref='license_assignments')
+44
View File
@@ -0,0 +1,44 @@
from app.extensions import db
from datetime import datetime
class LVPosition(db.Model):
__tablename__ = 'lv_positionen'
id = db.Column(db.Integer, primary_key=True)
company_id = db.Column(db.Integer, db.ForeignKey('companies.id'), nullable=False)
contract_id = db.Column(db.Integer, db.ForeignKey('contracts.id'), nullable=True)
lv_name = db.Column(db.String(200), nullable=False)
pos_nr = db.Column(db.String(50), nullable=False)
order_index = db.Column(db.Integer, default=0)
kurztext = db.Column(db.String(300))
langtext = db.Column(db.Text)
einheit = db.Column(db.String(10), default='ST')
einzelpreis = db.Column(db.Float, default=0.0)
gruppe = db.Column(db.String(100))
rsa = db.Column(db.String(20))
abschnitt = db.Column(db.String(100))
notiz = db.Column(db.Text)
favorite = db.Column(db.Boolean, default=False)
erstellt_am = db.Column(db.DateTime, default=datetime.utcnow)
__table_args__ = (
db.UniqueConstraint('company_id', 'lv_name', 'pos_nr', name='uq_lv_position'),
)
def to_dict(self):
return {
'id': self.id,
'lv_name': self.lv_name,
'pos_nr': self.pos_nr,
'kurztext': self.kurztext,
'langtext': self.langtext,
'einheit': self.einheit,
'einzelpreis': self.einzelpreis,
'gruppe': self.gruppe,
'rsa': self.rsa,
'abschnitt': self.abschnitt,
'favorite': self.favorite,
}
def __repr__(self):
return f'<LV {self.lv_name} | {self.pos_nr}>'
+16
View File
@@ -0,0 +1,16 @@
from app.extensions import db
class Module(db.Model):
__tablename__ = 'modules'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), unique=True, nullable=False)
titel = db.Column(db.String(200), nullable=False)
kategorie = db.Column(db.String(100))
icon = db.Column(db.String(20), default='📦')
beschreibung = db.Column(db.Text)
standard = db.Column(db.Boolean, default=False) # immer verfügbar
sortierung = db.Column(db.Integer, default=0)
def __repr__(self):
return f'<Module {self.name}>'
+78
View File
@@ -0,0 +1,78 @@
from app.extensions import db
from datetime import datetime
class Position(db.Model):
__tablename__ = 'positionen'
id = db.Column(db.Integer, primary_key=True)
project_id = db.Column(db.Integer, db.ForeignKey('projekte.id'), nullable=False)
aufmass_id = db.Column(db.Integer, db.ForeignKey('aufmass.id'), nullable=True)
lv_position_id = db.Column(db.Integer, db.ForeignKey('lv_positionen.id'), nullable=True)
pos_nr = db.Column(db.String(50), nullable=False)
sortierung = db.Column(db.Integer, default=0)
rsa = db.Column(db.String(20))
abschnitt = db.Column(db.String(100))
kurztext = db.Column(db.String(300))
langtext = db.Column(db.Text)
einheit = db.Column(db.String(10), default='ST')
einzelpreis = db.Column(db.Float, default=0.0)
menge = db.Column(db.Float, default=0.0)
gesamtpreis = db.Column(db.Float, default=0.0)
faktor = db.Column(db.Float, default=1.0)
laenge = db.Column(db.Float, default=0.0)
breite = db.Column(db.Float, default=0.0)
tiefe = db.Column(db.Float, default=0.0)
formel_typ = db.Column(db.String(10), default='standard')
formel = db.Column(db.String(300))
bemerkung = db.Column(db.Text)
menge_hinten = db.Column(db.Float, default=0.0)
erstellt_am = db.Column(db.DateTime, default=datetime.utcnow)
def berechne_menge(self, recalc_hinten=True, skip_menge_recalc=False):
if not skip_menge_recalc:
if self.formel_typ == 'frei':
if self.formel:
from app.services.formel_rechner import berechne_formel
try:
self.menge = berechne_formel(self.formel)
except Exception:
self.menge = 0
else:
self.menge = 0
elif self.einheit == 'ST':
self.menge = self.faktor * 1
elif self.einheit == 'M':
self.menge = self.laenge
elif self.einheit == 'M2':
self.menge = self.laenge * self.breite
elif self.einheit == 'M3':
self.menge = self.laenge * self.breite * self.tiefe
else:
self.menge = self.laenge
if recalc_hinten:
self.menge_hinten = self.faktor * self.menge
self.gesamtpreis = self.menge_hinten * self.einzelpreis
return self.menge
def to_dict(self):
return {
'id': self.id,
'pos_nr': self.pos_nr,
'sortierung': self.sortierung,
'rsa': self.rsa,
'kurztext': self.kurztext,
'langtext': self.langtext,
'einheit': self.einheit,
'einzelpreis': self.einzelpreis,
'menge': self.menge,
'gesamtpreis': self.gesamtpreis,
'faktor': self.faktor,
'laenge': self.laenge,
'breite': self.breite,
'tiefe': self.tiefe,
'bemerkung': self.bemerkung,
'menge_hinten': self.menge_hinten,
}
def __repr__(self):
return f'<Position {self.pos_nr} @ {self.project_id}>'
+37
View File
@@ -0,0 +1,37 @@
from app.extensions import db
from datetime import datetime
class Project(db.Model):
__tablename__ = 'projekte'
id = db.Column(db.Integer, primary_key=True)
company_id = db.Column(db.Integer, db.ForeignKey('companies.id'), nullable=False)
contract_id = db.Column(db.Integer, db.ForeignKey('contracts.id'), nullable=True)
sm_nr = db.Column(db.String(100), nullable=False, default='')
bezeichnung = db.Column(db.String(300))
baustelle = db.Column(db.String(300))
vertrag = db.Column(db.String(200))
abruf_nr = db.Column(db.String(100))
lv_name = db.Column(db.String(200))
datum_start = db.Column(db.Date)
datum_ende = db.Column(db.Date)
ansprechpartner_vorname = db.Column(db.String(100))
ansprechpartner_nachname = db.Column(db.String(100))
ansprechpartner_tel = db.Column(db.String(50))
ansprechpartner_email = db.Column(db.String(200))
bauabschnitt = db.Column(db.String(200))
datum = db.Column(db.Date)
ev_details_id = db.Column(db.String(50))
status = db.Column(db.String(20), default='aktiv')
erstellt_von = db.Column(db.Integer, db.ForeignKey('users.id'))
erstellt_am = db.Column(db.DateTime, default=datetime.utcnow)
geaendert_am = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
positionen = db.relationship('Position', backref='project', lazy='dynamic',
cascade='all, delete-orphan', order_by='Position.sortierung')
aufmass_liste = db.relationship('Aufmass', backref='aufmass_project',
cascade='all, delete-orphan',
order_by='Aufmass.sortierung')
def __repr__(self):
return f'<Project {self.sm_nr}>'
+17
View File
@@ -0,0 +1,17 @@
from app.extensions import db
class ProjectAccess(db.Model):
__tablename__ = 'project_access'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
project_id = db.Column(db.Integer, db.ForeignKey('projekte.id'), nullable=False)
zugriff = db.Column(db.String(20), default='lesen')
user = db.relationship('User', backref='project_access_list')
project = db.relationship('Project', backref='user_access_list')
__table_args__ = (db.UniqueConstraint('user_id', 'project_id'),)
def __repr__(self):
return f'<ProjectAccess u={self.user_id} p={self.project_id} {self.zugriff}>'
+24
View File
@@ -0,0 +1,24 @@
from app.extensions import db
from datetime import datetime
class Settings(db.Model):
__tablename__ = 'settings'
id = db.Column(db.Integer, primary_key=True)
key = db.Column(db.String(100), unique=True, nullable=False)
value = db.Column(db.Text)
@classmethod
def get(cls, key, default=None):
s = cls.query.filter_by(key=key).first()
return s.value if s else default
@classmethod
def set(cls, key, value):
s = cls.query.filter_by(key=key).first()
if s:
s.value = value
else:
s = cls(key=key, value=value)
db.session.add(s)
db.session.commit()
+80
View File
@@ -0,0 +1,80 @@
from app.extensions import db, login_manager
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from datetime import datetime
class User(UserMixin, db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
company_id = db.Column(db.Integer, db.ForeignKey('companies.id'), nullable=True)
email = db.Column(db.String(200), unique=True, nullable=False)
password_hash = db.Column(db.String(300), nullable=False)
vorname = db.Column(db.String(100))
nachname = db.Column(db.String(100))
rolle = db.Column(db.String(20), default='mitarbeiter')
aktiv = db.Column(db.Boolean, default=True)
font_size = db.Column(db.String(10), default='1')
profile_image = db.Column(db.String(255), nullable=True)
letzter_login = db.Column(db.DateTime)
erstellt_am = db.Column(db.DateTime, default=datetime.utcnow)
darf_projekte_anlegen = db.Column(db.Boolean, default=False)
darf_lv_verwalten = db.Column(db.Boolean, default=False)
darf_preise_sehen = db.Column(db.Boolean, default=False)
darf_aufmass_verwalten = db.Column(db.Boolean, default=False)
darf_evergabe_nutzen = db.Column(db.Boolean, default=False)
darf_kopfdaten_holen = db.Column(db.Boolean, default=False)
darf_aufmass_uebertragen = db.Column(db.Boolean, default=False)
hidden_modules = db.Column(db.Text, default='[]')
def get_hidden_modules(self):
import json
try:
return json.loads(self.hidden_modules or '[]')
except (json.JSONDecodeError, TypeError):
return []
def set_hidden_modules(self, val):
import json
self.hidden_modules = json.dumps(val, ensure_ascii=False)
@property
def full_name(self):
return f"{self.vorname or ''} {self.nachname or ''}".strip() or self.email
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
def is_superadmin(self):
return self.rolle == 'superadmin'
def is_firmadmin(self):
return self.rolle == 'firmadmin'
def is_admin(self):
return self.rolle in ('firmadmin', 'superadmin')
def hat_zugriff(self, project, required='lesen'):
if self.is_superadmin():
return True
if self.is_firmadmin():
from app.models.project import Project
return Project.query.get(project.id).company_id == self.company_id
from app.models.project_access import ProjectAccess
access = ProjectAccess.query.filter_by(
user_id=self.id, project_id=project.id
).first()
if not access:
return False
if required == 'lesen':
return True
if required == 'schreiben':
return access.zugriff in ('lesen', 'schreiben')
return False
def __repr__(self):
return f'<User {self.email} ({self.rolle})>'
+14
View File
@@ -0,0 +1,14 @@
from app.extensions import db
class UserModulePermission(db.Model):
__tablename__ = 'user_module_permissions'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
module_id = db.Column(db.Integer, db.ForeignKey('modules.id'), nullable=False)
aktiv = db.Column(db.Boolean, default=True)
user = db.relationship('User', backref='user_module_list')
module = db.relationship('Module', backref='user_assignments')
__table_args__ = (db.UniqueConstraint('user_id', 'module_id'),)
+34
View File
@@ -0,0 +1,34 @@
from app.extensions import db
from datetime import datetime
import json
class ViewProfile(db.Model):
__tablename__ = 'view_profiles'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
name = db.Column(db.String(100), nullable=False)
view_type = db.Column(db.String(50), default='lv') # lv, aufmass, ...
config_json = db.Column(db.Text, default='{}')
is_default = db.Column(db.Boolean, default=False)
erstellt_am = db.Column(db.DateTime, default=datetime.utcnow)
def get_config(self):
try:
return json.loads(self.config_json or '{}')
except (json.JSONDecodeError, TypeError):
return {}
def set_config(self, config):
self.config_json = json.dumps(config)
@staticmethod
def get_default_config():
return {
'column_order': ['fav', 'drag', 'pos_nr', 'text', 'einheit', 'ep', 'aktion'],
'column_widths': {'fav': 32, 'drag': 28, 'pos_nr': 90, 'text': 400, 'einheit': 60, 'ep': 80, 'aktion': 70},
'column_visible': {'fav': True, 'drag': True, 'pos_nr': True, 'text': True, 'einheit': True, 'ep': True, 'aktion': True},
}
def __repr__(self):
return f'<ViewProfile {self.name} ({self.view_type})>'
+42
View File
@@ -0,0 +1,42 @@
from flask import render_template
TEMPLATE = 'components/modul_absperrung.html'
def get_formular_html():
return render_template(TEMPLATE)
def berechne(form_data):
positionen = []
tage = _int(form_data.get('tage', 1))
typ = form_data.get('absperr_typ', 'voll')
if typ == 'voll':
positionen.append({
'pos_nr': '10041001',
'kurztext': 'Vollsperrung einrichten',
'menge': tage,
'einheit': 'ST',
})
elif typ == 'teil':
positionen.append({
'pos_nr': '10041002',
'kurztext': 'Teilsperrung einrichten',
'menge': tage,
'einheit': 'ST',
})
if form_data.get('ampel') == 'an':
positionen.append({
'pos_nr': '10041003',
'kurztext': 'Baustellenampel',
'menge': tage,
'einheit': 'ST',
})
return positionen
def _int(val, default=0):
try:
return int(float(str(val).replace(',', '.')))
except (ValueError, TypeError):
return default
+14
View File
@@ -0,0 +1,14 @@
from flask import render_template
class AufmassModul:
name = ''
titel = ''
template = ''
@classmethod
def get_formular_html(cls):
return render_template(cls.template)
@classmethod
def berechne(cls, form_data):
raise NotImplementedError
+50
View File
@@ -0,0 +1,50 @@
from flask import render_template
TEMPLATE = 'components/modul_cu.html'
def get_formular_html():
return render_template(TEMPLATE)
def berechne(form_data):
pos = []
abschnitt = form_data.get('abschnitt', '')
anz_verb = _int(form_data.get('anz_cu_verb', 0))
anz_stk = _int(form_data.get('anz_stk', 0))
if form_data.get('muffe_bis10') == 'an':
pos.append(dict(pos_nr='10037500', kurztext='Muffe bis 10 DA montieren', menge=1, einheit='ST', abschnitt=abschnitt))
if form_data.get('apl_bis10') == 'an':
pos.append(dict(pos_nr='10037501', kurztext='APL bis 10 DA montieren', menge=1, einheit='ST', abschnitt=abschnitt))
if form_data.get('anschl_trenn') == 'an':
pos.append(dict(pos_nr='10037502', kurztext='Anschluss-/Trennleisten einbauen', menge=1, einheit='ST', abschnitt=abschnitt))
if form_data.get('muffe_schrumpf') == 'an':
pos.append(dict(pos_nr='10037503', kurztext='Schrumpfmuffe herstellen', menge=1, einheit='ST', abschnitt=abschnitt))
if form_data.get('muffe_klemm') == 'an':
pos.append(dict(pos_nr='10037504', kurztext='Klemmmuffe herstellen', menge=1, einheit='ST', abschnitt=abschnitt))
if form_data.get('muffe_gel') == 'an':
pos.append(dict(pos_nr='10037505', kurztext='Gel-Muffen herstellen', menge=1, einheit='ST', abschnitt=abschnitt))
if form_data.get('muffe_klemm_dlr') == 'an':
pos.append(dict(pos_nr='10037506', kurztext='Klemmmuffen für DLR', menge=1, einheit='ST', abschnitt=abschnitt))
if anz_verb > 0:
pos.append(dict(pos_nr='10037507', kurztext=f'CU verbinden ({anz_verb} Stk)', menge=anz_verb, einheit='ST', abschnitt=abschnitt))
if form_data.get('cu_da_gr') == 'an':
pos.append(dict(pos_nr='10037508', kurztext='CU-DA > 0,8 mm verbinden', menge=1, einheit='ST', abschnitt=abschnitt))
if form_data.get('cu_da_kl') == 'an':
pos.append(dict(pos_nr='10037509', kurztext='CU-DA ≤ 0,8 mm verbinden', menge=1, einheit='ST', abschnitt=abschnitt))
if form_data.get('stopfstelle') == 'an':
pos.append(dict(pos_nr='10037510', kurztext='Druckluftstutzen Stopfstelle einbauen', menge=1, einheit='ST', abschnitt=abschnitt))
if form_data.get('zulage_stopf') == 'an':
pos.append(dict(pos_nr='10037511', kurztext='Zulage Stopfstelle DLR', menge=1, einheit='ST', abschnitt=abschnitt))
if form_data.get('evs_einbauen') == 'an' and anz_stk > 0:
pos.append(dict(pos_nr='10037512', kurztext=f'EVs einbauen ({anz_stk} Stk)', menge=anz_stk, einheit='ST', abschnitt=abschnitt))
if form_data.get('kabel_anlegen_ev') == 'an':
pos.append(dict(pos_nr='10037513', kurztext='Kabel anlegen EVs/TrLe', menge=1, einheit='ST', abschnitt=abschnitt))
return pos
def _float(val, default=0):
try: return float(str(val).replace(',', '.'))
except: return default
def _int(val, default=0):
try: return int(float(str(val).replace(',', '.')))
except: return default
+36
View File
@@ -0,0 +1,36 @@
from flask import render_template
TEMPLATE = 'components/modul_doku.html'
def get_formular_html():
return render_template(TEMPLATE)
def berechne(form_data):
pos = []
laenge = _float(form_data.get('doku_laenge', 0))
breite = _float(form_data.get('doku_breite', 0))
hktr = _float(form_data.get('doku_hktr_m', 0))
vzktr = _float(form_data.get('doku_vzktr_m', 0))
gf_haus = _int(form_data.get('doku_gf_haus_anz', 0))
if laenge > 0 and breite > 0:
pos.append(dict(pos_nr='10038000', kurztext=f'MP-Einarbeitung Gelände/Gebäude ({laenge}×{breite}m)', menge=1, einheit='ST'))
elif laenge > 0:
pos.append(dict(pos_nr='10038000', kurztext='MP-Einarbeitung Gelände/Gebäude', menge=laenge, einheit='M'))
if hktr > 0:
pos.append(dict(pos_nr='10038001', kurztext=f'Dokumentation HK-Trasse ({hktr}m)', menge=hktr, einheit='M'))
if vzktr > 0:
pos.append(dict(pos_nr='10038002', kurztext=f'Dokumentation VzK-Trasse ({vzktr}m)', menge=vzktr, einheit='M'))
if gf_haus > 0:
pos.append(dict(pos_nr='10038003', kurztext=f'Dokumentation GF-Hausanschluss ({gf_haus} Stk)', menge=gf_haus, einheit='ST'))
if form_data.get('doku_geh') == 'an':
pos.append(dict(pos_nr='10038004', kurztext='Dokumentation von Gehäusen', menge=1, einheit='ST'))
return pos
def _float(val, default=0):
try: return float(str(val).replace(',', '.'))
except: return default
def _int(val, default=0):
try: return int(float(str(val).replace(',', '.')))
except: return default
+53
View File
@@ -0,0 +1,53 @@
from flask import render_template
TEMPLATE = 'components/modul_ftth.html'
def get_formular_html():
return render_template(TEMPLATE)
def berechne(form_data):
positionen = []
anzahl = _int(form_data.get('anzahl_ha', 0))
if anzahl <= 0:
return positionen
# Hausanschluss GF-Montage
positionen.append({
'pos_nr': '10039001',
'kurztext': 'GF-Hausanschluss herstellen',
'menge': anzahl,
'einheit': 'ST',
'bemerkung': form_data.get('bemerkung', ''),
})
if form_data.get('tiefbau') == 'an':
positionen.append({
'pos_nr': '10039002',
'kurztext': 'Tiefbau für Hausanschluss',
'menge': anzahl * _float(form_data.get('trassenlaenge', 5)),
'einheit': 'M',
'bemerkung': 'Trassenlänge pro HA',
})
if form_data.get('muffe') == 'an':
positionen.append({
'pos_nr': '10038002',
'kurztext': 'Muffe bauen (FTTH)',
'menge': anzahl,
'einheit': 'ST',
})
return positionen
def _int(val, default=0):
try:
return int(float(str(val).replace(',', '.')))
except (ValueError, TypeError):
return default
def _float(val, default=0):
try:
return float(str(val).replace(',', '.'))
except (ValueError, TypeError):
return default
+50
View File
@@ -0,0 +1,50 @@
from flask import render_template
TEMPLATE = 'components/modul_gf.html'
def get_formular_html():
return render_template(TEMPLATE)
def berechne(form_data):
positionen = []
typ = form_data.get('typ', 'nvt')
anzahl = _int(form_data.get('anzahl', 1))
if typ == 'nvt':
positionen.append({
'pos_nr': '10038001',
'kurztext': 'NVT Verbinden',
'menge': anzahl,
'einheit': 'ST',
'bemerkung': form_data.get('bemerkung', ''),
})
elif typ == 'muffe':
positionen.append({
'pos_nr': '10038002',
'kurztext': 'Muffe bauen',
'menge': anzahl,
'einheit': 'ST',
})
elif typ == 'pegel':
positionen.append({
'pos_nr': '10038003',
'kurztext': 'Pegelmessung durchführen',
'menge': anzahl,
'einheit': 'ST',
})
if form_data.get('fasern_verbinden') == 'an':
positionen.append({
'pos_nr': '10038004',
'kurztext': 'Fasern verbinden',
'menge': anzahl,
'einheit': 'ST',
})
return positionen
def _int(val, default=0):
try:
return int(float(str(val).replace(',', '.')))
except (ValueError, TypeError):
return default
+113
View File
@@ -0,0 +1,113 @@
from flask import render_template
TEMPLATE = 'components/modul_graben.html'
def get_formular_html():
return render_template(TEMPLATE)
def berechne(form_data):
positionen = []
l = _float(form_data.get('laenge', 0))
b = _float(form_data.get('breite', 0))
t = _float(form_data.get('tiefe', 0))
asphalt = _float(form_data.get('asphaltstaerke', 0))
rest_l = _float(form_data.get('rest_laenge', 0))
rest_b = _float(form_data.get('rest_breite', 0))
anz_einz = _int(form_data.get('anz_einzeiler', 0))
lm_bre = _float(form_data.get('lm_bre', 0))
abschnitt = form_data.get('abschnitt', '')
bemerkung = form_data.get('bemerkung', '')
if l <= 0 or b <= 0 or t <= 0:
return positionen
vol = l * b * t / 100
# Mindertiefe
mind_lt = form_data.get('mind_langstrasse') == 'an'
mind_ftth = form_data.get('mind_ftth_ha') == 'an'
if mind_lt:
positionen.append({'pos_nr': '10037491', 'kurztext': 'Mindertiefe Längstrasse', 'menge': vol, 'einheit': 'M3', 'laenge': l, 'breite': b, 'tiefe': t, 'bemerkung': bemerkung, 'abschnitt': abschnitt})
if mind_ftth:
positionen.append({'pos_nr': '10037492', 'kurztext': 'Mindertiefe FTTH Hausanschluss', 'menge': vol, 'einheit': 'M3', 'laenge': l, 'breite': b, 'tiefe': t, 'bemerkung': bemerkung, 'abschnitt': abschnitt})
# Oberfläche - mehrere Checkboxen möglich
oberfl_checks = [
('ob_wiese', '10037463', 'Graben in Wiese herstellen'),
('ob_kies', '10037464', 'Graben in Kies herstellen'),
('ob_asphalt', '10037465', 'Graben in Asphalt herstellen'),
('ob_pflaster', '10037466', 'Graben in Pflaster herstellen'),
('ob_mosaik', '10037467', 'Graben in Mosaik herstellen'),
('ob_bodentausch', '10037468', 'Graben mit Bodentausch herstellen'),
('ob_fels', '10037469', 'Graben in Fels herstellen'),
('ob_winterbau', '10037470', 'Graben im Winterbau herstellen'),
('ob_gr_natur_pfl', '10037471', 'Graben in Groß/Natursteinpflaster herstellen'),
('ob_in_beton', '10037472', 'Graben in Beton Pflaster/Mosaik herstellen'),
]
for key, pnr, txt in oberfl_checks:
if form_data.get(key) == 'an':
positionen.append({'pos_nr': pnr, 'kurztext': txt, 'menge': vol, 'einheit': 'M3', 'laenge': l, 'breite': b, 'tiefe': t, 'bemerkung': bemerkung, 'abschnitt': abschnitt})
# Asphalt durchtrennen
if asphalt > 0:
positionen.append({'pos_nr': '10037473', 'kurztext': 'Asphaltdecke durchtrennen', 'menge': l, 'einheit': 'M', 'laenge': l, 'bemerkung': bemerkung, 'abschnitt': abschnitt})
# Reststreifen
if rest_l > 0 and rest_b > 0:
positionen.append({'pos_nr': '10037480', 'kurztext': 'Reststreifen herstellen', 'menge': rest_l * rest_b / 100, 'einheit': 'M3', 'laenge': rest_l, 'breite': rest_b, 'bemerkung': bemerkung, 'abschnitt': abschnitt})
# Bord/Rinne/Einzeiler
bord = form_data.get('bord') == 'an'
kante = form_data.get('kante') == 'an'
rinne = form_data.get('rinne') == 'an'
liefern = form_data.get('liefern') == 'an'
if bord:
positionen.append({'pos_nr': '10037481', 'kurztext': 'Bordstein setzen', 'menge': l, 'einheit': 'M', 'laenge': l, 'bemerkung': bemerkung, 'abschnitt': abschnitt})
if kante:
positionen.append({'pos_nr': '10037482', 'kurztext': 'Kantenstein setzen', 'menge': l, 'einheit': 'M', 'laenge': l, 'bemerkung': bemerkung, 'abschnitt': abschnitt})
if rinne:
positionen.append({'pos_nr': '10037483', 'kurztext': 'Einzeiler/Rinne setzen', 'menge': l, 'einheit': 'M', 'laenge': l, 'bemerkung': bemerkung, 'abschnitt': abschnitt})
if liefern and (bord or kante or rinne):
positionen.append({'pos_nr': '10037484', 'kurztext': 'Bord/Rinne/Einzeiler liefern', 'menge': anz_einz if anz_einz > 0 else 1, 'einheit': 'ST', 'bemerkung': bemerkung, 'abschnitt': abschnitt})
if anz_einz > 0:
positionen.append({'pos_nr': '10037485', 'kurztext': f'Einzeiler ({anz_einz} Stk)', 'menge': anz_einz, 'einheit': 'ST', 'bemerkung': bemerkung, 'abschnitt': abschnitt})
# Medien
medien_map = [
('cu_kabel', '10037450', f'6-100 DA CU-Kabel ({_int(form_data.get("anz_cu",0))} DA)', _int(form_data.get('anz_cu',0)), 'DA'),
('cu_kabel_gr', '10037451', f'> 100 DA CU-Kabel ({_int(form_data.get("anz_cu",0))} DA)', _int(form_data.get('anz_cu',0)), 'DA'),
('dn110', '10037452', f'DN110 ({_int(form_data.get("anz_dn110",0))} Stk)', _int(form_data.get('anz_dn110',0)), 'ST'),
('dn50_1', '10037453', f'1xDN50 ({_int(form_data.get("anz_dn50_1",0))} Stk)', _int(form_data.get('anz_dn50_1',0)), 'ST'),
('dn50_2', '10037454', f'2xDN50 ({_int(form_data.get("anz_dn50_2",0))} Stk)', _int(form_data.get('anz_dn50_2',0)), 'ST'),
('dn50_3', '10037455', f'3xDN50 ({_int(form_data.get("anz_dn50_3",0))} Stk)', _int(form_data.get('anz_dn50_3',0)), 'ST'),
('snrve_7x12', '10037456', f'SNRVe 7x12 ({_int(form_data.get("anz_snrve_7x12",0))} Stk)', _int(form_data.get('anz_snrve_7x12',0)), 'ST'),
('snrve_22x7', '10037457', f'SNRVe 22x7 ({_int(form_data.get("anz_snrve_22x7",0))} Stk)', _int(form_data.get('anz_snrve_22x7',0)), 'ST'),
('snrve_8x7', '10037458', f'SNRVe 8x7 ({_int(form_data.get("anz_snrve_8x7",0))} Stk)', _int(form_data.get('anz_snrve_8x7',0)), 'ST'),
('snrve_1x7', '10037459', f'SNRVe 1x7 ({_int(form_data.get("anz_snrve_1x7",0))} Stk)', _int(form_data.get('anz_snrve_1x7',0)), 'ST'),
]
for key, pnr, txt, menge, eh in medien_map:
if form_data.get(key) == 'an' and menge > 0:
positionen.append({'pos_nr': pnr, 'kurztext': txt, 'menge': menge, 'einheit': eh, 'bemerkung': bemerkung, 'abschnitt': abschnitt})
# Trasse einmessen
if form_data.get('trasseeinmessen') == 'an':
positionen.append({'pos_nr': '10037486', 'kurztext': 'Trasse einmessen', 'menge': l, 'einheit': 'M', 'laenge': l, 'bemerkung': bemerkung, 'abschnitt': abschnitt})
# Stahlplatte
if form_data.get('stahlplatte') == 'an':
stahl_bem = form_data.get('stahlplatte_bemerk', '')
positionen.append({'pos_nr': '10037487', 'kurztext': 'Stahlplatte', 'menge': 1, 'einheit': 'ST', 'bemerkung': stahl_bem, 'abschnitt': abschnitt})
return positionen
def _float(val, default=0):
try:
return float(str(val).replace(',', '.'))
except (ValueError, TypeError):
return default
def _int(val, default=0):
try:
return int(float(str(val).replace(',', '.')))
except (ValueError, TypeError):
return default
+39
View File
@@ -0,0 +1,39 @@
from flask import render_template
TEMPLATE = 'components/modul_gruben.html'
def get_formular_html():
return render_template(TEMPLATE)
def berechne(form_data):
positionen = []
l = _float(form_data.get('laenge', 0))
b = _float(form_data.get('breite', 0))
t = _float(form_data.get('tiefe', 0))
if l <= 0 or b <= 0 or t <= 0:
return positionen
oberflaeche = form_data.get('oberflaeche', 'wiese')
volumen = l * b * t / 100
oberfl_map = {
'wiese': ('10037463', 'Grube in Wiese herstellen'),
'kies': ('10037464', 'Grube in Kies herstellen'),
'asphalt': ('10037465', 'Grube in Asphalt herstellen'),
'pflaster': ('10037466', 'Grube in Pflaster herstellen'),
}
if oberflaeche in oberfl_map:
pnr, txt = oberfl_map[oberflaeche]
positionen.append({
'pos_nr': pnr, 'kurztext': txt, 'menge': volumen,
'einheit': 'M3', 'laenge': l, 'breite': b, 'tiefe': t,
})
return positionen
def _float(val, default=0):
try:
return float(str(val).replace(',', '.'))
except (ValueError, TypeError):
return default
+43
View File
@@ -0,0 +1,43 @@
from flask import render_template
TEMPLATE = 'components/modul_kabelzug.html'
def get_formular_html():
return render_template(TEMPLATE)
def berechne(form_data):
positionen = []
laenge = _float(form_data.get('laenge', 0))
typ = form_data.get('kabel_typ', 'dn50')
if laenge <= 0:
return positionen
kabel_map = {
'dn50': ('10040001', 'Kabel DN50 einziehen', laenge),
'dn110': ('10040002', 'Kabel DN110 einziehen', laenge),
'cu': ('10040003', 'Cu-Kabel einziehen', laenge),
}
if typ in kabel_map:
pnr, txt, menge = kabel_map[typ]
positionen.append({
'pos_nr': pnr, 'kurztext': txt, 'menge': menge, 'einheit': 'M',
'laenge': laenge,
})
if form_data.get('einblasen') == 'an':
positionen.append({
'pos_nr': '10040004',
'kurztext': 'Einblasen von Kabel',
'menge': laenge,
'einheit': 'M',
'laenge': laenge,
})
return positionen
def _float(val, default=0):
try:
return float(str(val).replace(',', '.'))
except (ValueError, TypeError):
return default
+73
View File
@@ -0,0 +1,73 @@
from flask import render_template
TEMPLATE = 'components/modul_neff_achberg.html'
def get_formular_html():
return render_template(TEMPLATE)
def berechne(form_data):
positionen = []
l = _float(form_data.get('laenge', 0))
typ = form_data.get('typ', 'haus')
if l <= 0:
return positionen
if typ == 'haus':
positionen.append({
'pos_nr': '10043001',
'kurztext': 'Leitungsgraben HA Neff-Achberg',
'menge': l,
'einheit': 'M',
'laenge': l,
'bemerkung': form_data.get('bemerkung', ''),
})
elif typ == 'tb':
positionen.append({
'pos_nr': '10043002',
'kurztext': 'Leitungsgraben TB Neff-Achberg',
'menge': l,
'einheit': 'M',
'laenge': l,
'bemerkung': form_data.get('bemerkung', ''),
})
if form_data.get('rohr_4x20') == 'an':
positionen.append({
'pos_nr': '10043003',
'kurztext': 'Rohr 4x20mm (Neff-Achberg)',
'menge': l,
'einheit': 'M',
'laenge': l,
})
if form_data.get('trassenband') == 'an':
positionen.append({
'pos_nr': '10043004',
'kurztext': 'Trassenband einlegen',
'menge': l,
'einheit': 'M',
'laenge': l,
})
if form_data.get('kopfloch') == 'an':
positionen.append({
'pos_nr': '10043005',
'kurztext': 'Kopfloch herstellen',
'menge': _int(form_data.get('kopfloch_anzahl', 1)),
'einheit': 'ST',
})
return positionen
def _float(val, default=0):
try:
return float(str(val).replace(',', '.'))
except (ValueError, TypeError):
return default
def _int(val, default=0):
try:
return int(float(str(val).replace(',', '.')))
except (ValueError, TypeError):
return default
+66
View File
@@ -0,0 +1,66 @@
from flask import render_template
TEMPLATE = 'components/modul_planung.html'
def get_formular_html():
return render_template(TEMPLATE)
def berechne(form_data):
pos = []
zul_mtb = _float(form_data.get('zulage_struk_mtb', 0))
zul_otb = _float(form_data.get('zulage_struk_otb', 0))
mpp_zn = _float(form_data.get('mpp_gfk_zn', 0))
mpp_kr = _float(form_data.get('mpp_kr', 0))
mpp_snrv = _float(form_data.get('mpp_snrv', 0))
gf_minik = _float(form_data.get('gf_minik', 0))
ivk = _float(form_data.get('ivk_m', 0))
zul_kl = _float(form_data.get('zulage_kl_baum', 0))
s_liste = form_data.get('s_liste', '').strip()
if form_data.get('proj_struk_mtb') == 'an':
txt = 'Projektierung n. Strukturplanung mit TB bis 100m'
if zul_mtb > 0:
txt += f', Zulage {zul_mtb}m ab 101m'
pos.append(dict(pos_nr='10037800', kurztext=txt, menge=1, einheit='ST'))
if form_data.get('proj_struk_otb') == 'an':
txt = 'Projektierung n. Strukturplanung ohne TB bis 100m'
if zul_otb > 0:
txt += f', Zulage {zul_otb}m ab 101m'
pos.append(dict(pos_nr='10037801', kurztext=txt, menge=1, einheit='ST'))
if form_data.get('mpp_gfk_nvt_ap') == 'an':
txt = 'MP-Proj. FTTH-Gf-Kabel v. NVT → GF-AP'
if mpp_zn > 0:
txt += f' ({mpp_zn}m im ZN-Netz)'
pos.append(dict(pos_nr='10037802', kurztext=txt, menge=1, einheit='ST'))
if form_data.get('mpp_gfap') == 'an':
pos.append(dict(pos_nr='10037803', kurztext='MP-Proj. GF-AP', menge=1, einheit='ST'))
if mpp_kr > 0:
pos.append(dict(pos_nr='10037804', kurztext=f'MP-Proj. KR-Anlagen ({mpp_kr}m)', menge=mpp_kr, einheit='M'))
if mpp_snrv > 0:
pos.append(dict(pos_nr='10037805', kurztext=f'MP-Proj. ({mpp_snrv}m)', menge=mpp_snrv, einheit='M'))
if form_data.get('ap_sgs') == 'an':
pos.append(dict(pos_nr='10037806', kurztext='GF-AP + Patchfeld', menge=1, einheit='ST'))
if gf_minik > 0:
pos.append(dict(pos_nr='10037807', kurztext=f'GF-Kabel einbl./verlegen ({gf_minik}m)', menge=gf_minik, einheit='M'))
if ivk > 0:
pos.append(dict(pos_nr='10037808', kurztext=f'IVK ({ivk}m)', menge=ivk, einheit='M'))
if form_data.get('proj_kl_baum') == 'an':
txt = 'Proj. kl. Baumaßnahme'
if zul_kl > 0:
txt += f' ({zul_kl}m)'
pos.append(dict(pos_nr='10037809', kurztext=txt, menge=1, einheit='ST'))
if form_data.get('s_planung_05') == 'an':
pos.append(dict(pos_nr='10037810', kurztext='S-Planung 0,5', menge=1, einheit='ST'))
if s_liste:
lines = [l.strip() for l in s_liste.split('\n') if l.strip()]
for i, line in enumerate(lines):
pos.append(dict(pos_nr='10037811', kurztext=f'S-Liste: {line}', menge=1, einheit='ST'))
return pos
def _float(val, default=0):
try: return float(str(val).replace(',', '.'))
except: return default
def _int(val, default=0):
try: return int(float(str(val).replace(',', '.')))
except: return default
+185
View File
@@ -0,0 +1,185 @@
from flask import render_template
from flask_login import current_user
from app.extensions import db
from app.models.lv import LVPosition
TEMPLATE = 'components/modul_sas_mecka.html'
def get_formular_html():
return render_template(TEMPLATE)
def _lookup_pos(pos_nr):
lv_pos = LVPosition.query.filter_by(
company_id=current_user.company_id,
pos_nr=pos_nr
).first()
if lv_pos:
return {'pos_nr': lv_pos.pos_nr, 'kurztext': lv_pos.kurztext,
'einheit': lv_pos.einheit, 'einzelpreis': lv_pos.einzelpreis}
return {'pos_nr': pos_nr, 'kurztext': f'Position {pos_nr}',
'einheit': 'ST', 'einzelpreis': 0}
def _make_pos(pos_nr, faktor=1.0, laenge=0, breite=0, tiefe=0,
menge=None, bemerkung='', abschnitt=''):
lv = _lookup_pos(pos_nr)
pos = {'pos_nr': pos_nr, 'kurztext': lv['kurztext'],
'einheit': lv['einheit'], 'einzelpreis': lv['einzelpreis'],
'faktor': faktor, 'laenge': laenge, 'breite': breite, 'tiefe': tiefe,
'bemerkung': bemerkung, 'abschnitt': abschnitt}
if menge is not None:
pos['menge'] = menge
return pos
def berechne(form_data):
positionen = []
abschnitt_ha = form_data.get('scan_name_ha', '')
# ── HA (Hausanschluss) ──
if form_data.get('ha_herstellen') == 'an':
for pnr in ['01.06.0001', '01.06.0003', '01.06.0007']:
positionen.append(_make_pos(pnr, faktor=1.0, menge=1,
abschnitt=abschnitt_ha))
anz_qkr = _float(form_data.get('anz_qkr', 0))
anz_qst = _float(form_data.get('anz_qst', 0))
if anz_qkr > 0:
bem = "Siehe Bild: "
positionen.append(_make_pos('01.03.0019',
faktor=anz_qst if anz_qst > 0 else 1,
laenge=1.0, menge=1.0, bemerkung=bem,
abschnitt=abschnitt_ha))
positionen.append(_make_pos('01.03.0020',
faktor=anz_qkr, laenge=0.5, menge=0.5,
bemerkung=bem, abschnitt=abschnitt_ha))
trmeter = _float(form_data.get('trassenmeter', 0))
strqm_ha = _float(form_data.get('strqm', 0))
if trmeter > 0:
positionen.append(_make_pos('01.06.0004',
faktor=1.0, laenge=trmeter,
abschnitt=abschnitt_ha))
einzug10 = form_data.get('einzug_10er') == 'an'
if einzug10:
positionen.append(_make_pos('01.04.0003',
faktor=1.0, laenge=trmeter, menge=trmeter,
abschnitt=abschnitt_ha))
positionen.append(_make_pos('01.06.0006',
faktor=1.0, laenge=trmeter, menge=trmeter,
abschnitt=abschnitt_ha))
else:
leange = trmeter + 1 + strqm_ha
positionen.append(_make_pos('01.06.0006',
faktor=1.0, laenge=leange,
abschnitt=abschnitt_ha))
kabelm_ha = _float(form_data.get('kabelmeter_ha', 0))
if kabelm_ha > 0:
bem = "Siehe Bild: "
positionen.append(_make_pos('01.03.0019',
faktor=1.0, laenge=kabelm_ha, menge=kabelm_ha,
bemerkung=bem, abschnitt=abschnitt_ha))
positionen.append(_make_pos('01.03.0020',
faktor=1.0, laenge=kabelm_ha, menge=kabelm_ha,
bemerkung=bem, abschnitt=abschnitt_ha))
if strqm_ha > 0:
positionen.append(_make_pos('01.03.0008',
faktor=1.0, laenge=strqm_ha, menge=strqm_ha,
bemerkung="Öffentlicherbereich ", abschnitt=abschnitt_ha))
anz_sh = _float(form_data.get('anz_suchgrube_ha', 0))
if anz_sh > 0:
positionen.append(_make_pos('01.03.0018',
faktor=1.0, menge=anz_sh,
bemerkung="Öffentlicherbereich ", abschnitt=abschnitt_ha))
# ── TB (Tiefbau) ──
tb_laenge = _float(form_data.get('tb_laenge', 0))
tb_tiefe = _float(form_data.get('tb_tiefe', 0))
abschnitt_tb = form_data.get('scan_name_tb', '')
if tb_laenge > 0 and tb_tiefe > 0:
if tb_tiefe <= 0.65:
tief_key = '0.6'
elif tb_tiefe <= 0.9:
tief_key = '0.8'
else:
tief_key = '1.2'
tiefe_map = {
'0.6': {'unbe': '01.03.0001', 'be_kg2': '01.03.0004',
'be_kg4': '01.03.0005', 'be_kg6': '01.03.0006'},
'0.8': {'unbe': '01.03.0002', 'be_kg2': '01.03.0008',
'be_kg4': '01.03.0009', 'be_kg6': '01.03.0010'},
'1.2': {'unbe': '01.03.0003', 'be_kg2': '01.03.0012',
'be_kg4': '01.03.0013', 'be_kg6': '01.03.0014'},
}
tm = tiefe_map[tief_key]
if form_data.get('tb_unbefestigt') == 'an':
positionen.append(_make_pos(tm['unbe'],
faktor=1.0, laenge=tb_laenge,
abschnitt=abschnitt_tb))
if form_data.get('tb_befestigt') == 'an':
for kg_key, map_key in [('tb_kg2', 'be_kg2'), ('tb_kg4', 'be_kg4'),
('tb_kg6', 'be_kg6')]:
if form_data.get(kg_key) == 'an':
positionen.append(_make_pos(tm[map_key],
faktor=1.0, laenge=tb_laenge,
abschnitt=abschnitt_tb))
anz_4x20 = _float(form_data.get('tb_anz_4x12', 0))
if anz_4x20 > 0:
positionen.append(_make_pos('01.04.0001',
faktor=anz_4x20, laenge=tb_laenge, menge=tb_laenge,
bemerkung=f"{int(anz_4x20)}x 4x20 Rohre",
abschnitt=abschnitt_tb))
anz_12x10 = _float(form_data.get('tb_anz_12x10', 0))
if anz_12x10 > 0:
positionen.append(_make_pos('01.04.0002',
faktor=anz_12x10, laenge=tb_laenge, menge=tb_laenge,
bemerkung=f"{int(anz_12x10)}x 12x10 Rohre",
abschnitt=abschnitt_tb))
tb_anz_qs = _float(form_data.get('tb_anz_qs', 0))
if tb_anz_qs > 0:
bem = "Siehe Bild: "
tb_anz_qk = _float(form_data.get('tb_anz_qk', 0))
positionen.append(_make_pos('01.03.0019',
faktor=tb_anz_qs, laenge=1.0, menge=1.0,
bemerkung=bem, abschnitt=abschnitt_tb))
positionen.append(_make_pos('01.03.0020',
faktor=tb_anz_qk if tb_anz_qk > 0 else 1,
laenge=0.5, menge=0.5, bemerkung=bem,
abschnitt=abschnitt_tb))
tb_kabelm = _float(form_data.get('tb_kabelmeter', 0))
if tb_kabelm > 0:
bem = "Siehe Bild: "
positionen.append(_make_pos('01.03.0019',
faktor=1.0, laenge=tb_kabelm, menge=tb_kabelm,
bemerkung=bem, abschnitt=abschnitt_tb))
positionen.append(_make_pos('01.03.0020',
faktor=1.0, laenge=tb_kabelm, menge=tb_kabelm,
bemerkung=bem, abschnitt=abschnitt_tb))
anz_tb_sg = _float(form_data.get('tb_anz_suchgrube', 0))
if anz_tb_sg > 0:
positionen.append(_make_pos('01.03.0018',
faktor=1.0, menge=anz_tb_sg,
bemerkung="Öffentlicherbereich ", abschnitt=abschnitt_tb))
return positionen
def _float(val, default=0):
try:
return float(str(val).replace(',', '.'))
except (ValueError, TypeError):
return default
+23
View File
@@ -0,0 +1,23 @@
from flask import render_template
TEMPLATE = 'components/modul_sto_sammler.html'
def get_formular_html():
return render_template(TEMPLATE)
def berechne(form_data):
pos = []
liste = form_data.get('sto_liste', '').strip()
if liste:
lines = [l.strip() for l in liste.split('\n') if l.strip()]
for i, line in enumerate(lines):
pos.append(dict(pos_nr='10038100', kurztext=f'Störung: {line}', menge=1, einheit='ST'))
return pos
def _float(val, default=0):
try: return float(str(val).replace(',', '.'))
except: return default
def _int(val, default=0):
try: return int(float(str(val).replace(',', '.')))
except: return default
+71
View File
@@ -0,0 +1,71 @@
from flask import render_template
TEMPLATE = 'components/modul_stoerung.html'
def get_formular_html():
return render_template(TEMPLATE)
def berechne(form_data):
pos = []
cu_gr = _int(form_data.get('cu_da_gr_anz', 0))
cu_kl = _int(form_data.get('cu_da_kl_anz', 0))
kabel_kl30 = _float(form_data.get('kabel_kl30', 0))
kabel_gr30 = _float(form_data.get('kabel_gr30', 0))
gf_ausbl = _float(form_data.get('gf_ausbl_m', 0))
gf_einbl = _float(form_data.get('gf_einbl_m', 0))
gf_unge = _float(form_data.get('gf_unge_kas', 0))
gf_indoor = _int(form_data.get('gf_verb_indoor', 0))
gf_outdoor = _int(form_data.get('gf_verb_outdoor', 0))
vao = _float(form_data.get('vao_preis', 0))
if form_data.get('cu_fehlerortung') == 'an':
pos.append(dict(pos_nr='10037600', kurztext='Fehlerortung von Kabelfehlern an Cu-Kabel', menge=1, einheit='ST'))
if form_data.get('cu_zul_instan') == 'an':
pos.append(dict(pos_nr='10037601', kurztext='Zulage Instandsetzung v. Kabelfehlern', menge=1, einheit='ST'))
if form_data.get('cu_schaden_beweiss') == 'an':
pos.append(dict(pos_nr='10037602', kurztext='Schadens-/Beweissicherung an TK-Anlagen', menge=1, einheit='ST'))
if form_data.get('cu_instan_beweis') == 'an':
pos.append(dict(pos_nr='10037603', kurztext='Instands. nach Schadensbeweis', menge=1, einheit='ST'))
if cu_gr > 0:
pos.append(dict(pos_nr='10037604', kurztext=f'Cu-DA > 0,8 mm verbinden ({cu_gr} Stk)', menge=cu_gr, einheit='ST'))
if cu_kl > 0:
pos.append(dict(pos_nr='10037605', kurztext=f'Cu-DA ≤ 0,8 mm verbinden ({cu_kl} Stk)', menge=cu_kl, einheit='ST'))
if kabel_kl30 > 0:
pos.append(dict(pos_nr='10037606', kurztext='Kabel bis 30mm auslegen', menge=kabel_kl30, einheit='M'))
if kabel_gr30 > 0:
pos.append(dict(pos_nr='10037607', kurztext='Kabel größer 30mm auslegen', menge=kabel_gr30, einheit='M'))
if form_data.get('gf_fehlerortung') == 'an':
pos.append(dict(pos_nr='10037608', kurztext='Fehlerortung v. Kabelfehlern an GF-Kabel', menge=1, einheit='ST'))
if form_data.get('gf_inst_n_fehl') == 'an':
pos.append(dict(pos_nr='10037609', kurztext='Instands. v. GF-Kabel nach Fehlerortung', menge=1, einheit='ST'))
if form_data.get('gf_beweis') == 'an':
pos.append(dict(pos_nr='10037610', kurztext='Schadens-/Beweissicherung an TK-Anlagen (GF)', menge=1, einheit='ST'))
if form_data.get('gf_instand_ohne') == 'an':
pos.append(dict(pos_nr='10037611', kurztext='Instands. v. GF-Kabel ohne Fehlerortung', menge=1, einheit='ST'))
if gf_ausbl > 0:
pos.append(dict(pos_nr='10037612', kurztext='GF-Kabel ausblasen', menge=gf_ausbl, einheit='M'))
if gf_einbl > 0:
pos.append(dict(pos_nr='10037613', kurztext='GF-Kabel einblasen', menge=gf_einbl, einheit='M'))
if gf_unge > 0:
pos.append(dict(pos_nr='10037614', kurztext='GF ungeschweißt in Kassetten ablegen', menge=gf_unge, einheit='M'))
if gf_indoor > 0:
pos.append(dict(pos_nr='10037615', kurztext=f'Glasfasern verbinden Indoor ({gf_indoor} Stk)', menge=gf_indoor, einheit='ST'))
if gf_outdoor > 0:
pos.append(dict(pos_nr='10037616', kurztext=f'Glasfasern verbinden Outdoor ({gf_outdoor} Stk)', menge=gf_outdoor, einheit='ST'))
if form_data.get('gf_muffe_neu') == 'an':
pos.append(dict(pos_nr='10037617', kurztext='Neue GF-Muffe öffnen', menge=1, einheit='ST'))
if form_data.get('gf_muffe_bestand') == 'an':
pos.append(dict(pos_nr='10037618', kurztext='Bestandsmuffe öffnen', menge=1, einheit='ST'))
if form_data.get('anfahrt_montage') == 'an':
pos.append(dict(pos_nr='10037619', kurztext='Anfahrt Montagestelle', menge=1, einheit='ST'))
if vao > 0:
pos.append(dict(pos_nr='10037620', kurztext='VAO', menge=1, einheit='ST', einzelpreis=vao))
return pos
def _float(val, default=0):
try: return float(str(val).replace(',', '.'))
except: return default
def _int(val, default=0):
try: return int(float(str(val).replace(',', '.')))
except: return default
+45
View File
@@ -0,0 +1,45 @@
from flask import render_template
TEMPLATE = 'components/modul_tvum.html'
def get_formular_html():
return render_template(TEMPLATE)
def berechne(form_data):
pos = []
inst_snr = _int(form_data.get('inst_snr', 0))
gf_innen = _int(form_data.get('gf_innen_bef', 0))
tvum_spl = _int(form_data.get('tvum_anz_spl', 0))
tvum_mess = _int(form_data.get('tvum_anz_mess', 0))
ftth_spl = _int(form_data.get('ftth_anz_spl', 0))
nvt_spl = _int(form_data.get('nvt_anz_spl', 0))
ausbl = _float(form_data.get('ausbl_m', 0))
einbl = _float(form_data.get('einbl_m', 0))
if form_data.get('tvum_ap_mont') == 'an':
pos.append(dict(pos_nr='10037700', kurztext='TVUM-AP montieren Wand', menge=1, einheit='ST'))
if inst_snr > 0:
pos.append(dict(pos_nr='10037701', kurztext='Kanäle/SNR befestigen', menge=inst_snr, einheit='ST'))
if gf_innen > 0:
pos.append(dict(pos_nr='10037702', kurztext='GF-Innenkabel befestigen/einziehen', menge=gf_innen, einheit='M'))
if tvum_spl > 0:
pos.append(dict(pos_nr='10037703', kurztext=f'Spleiße TVUM-AP ({tvum_spl} Stk)', menge=tvum_spl, einheit='ST'))
if tvum_mess > 0:
pos.append(dict(pos_nr='10037704', kurztext=f'Messungen ({tvum_mess} Stk)', menge=tvum_mess, einheit='ST'))
if ftth_spl > 0:
pos.append(dict(pos_nr='10037705', kurztext=f'FTTH Spleiße ({ftth_spl} Stk)', menge=ftth_spl, einheit='ST'))
if nvt_spl > 0:
pos.append(dict(pos_nr='10037706', kurztext=f'NVT Spleiße ({nvt_spl} Stk)', menge=nvt_spl, einheit='ST'))
if ausbl > 0:
pos.append(dict(pos_nr='10037707', kurztext='Kabel ausblasen', menge=ausbl, einheit='M'))
if einbl > 0:
pos.append(dict(pos_nr='10037708', kurztext='Kabel einblasen', menge=einbl, einheit='M'))
return pos
def _float(val, default=0):
try: return float(str(val).replace(',', '.'))
except: return default
def _int(val, default=0):
try: return int(float(str(val).replace(',', '.')))
except: return default
+39
View File
@@ -0,0 +1,39 @@
from flask import render_template
TEMPLATE = 'components/modul_zw_rv.html'
def get_formular_html():
return render_template(TEMPLATE)
def berechne(form_data):
pos = []
abschnitt = form_data.get('abschnitt', '')
veg_m = _float(form_data.get('vegetation_m', 0))
pfl_m = _float(form_data.get('pflaster_m', 0))
asp_m = _float(form_data.get('asphalt_m', 0))
if veg_m > 0:
pos.append(dict(pos_nr='10037900', kurztext='Vegetation (Hausanschluss)', menge=veg_m, einheit='M', abschnitt=abschnitt))
if pfl_m > 0:
pos.append(dict(pos_nr='10037901', kurztext='Pflaster (Hausanschluss)', menge=pfl_m, einheit='M', abschnitt=abschnitt))
if asp_m > 0:
pos.append(dict(pos_nr='10037902', kurztext='Asphalt (Hausanschluss)', menge=asp_m, einheit='M', abschnitt=abschnitt))
if form_data.get('kopfloch_geb') == 'an':
pos.append(dict(pos_nr='10037903', kurztext='Kopfloch Gebäude', menge=1, einheit='ST', abschnitt=abschnitt))
if form_data.get('kopfloch_tr') == 'an':
pos.append(dict(pos_nr='10037904', kurztext='Kopfloch Haupttrasse', menge=1, einheit='ST', abschnitt=abschnitt))
if form_data.get('koordinieren_ha') == 'an':
pos.append(dict(pos_nr='10037905', kurztext='Koordinieren Hausanschluss', menge=1, einheit='ST', abschnitt=abschnitt))
if form_data.get('md1_ftth') == 'an':
pos.append(dict(pos_nr='10037906', kurztext='MD1-FttH', menge=1, einheit='ST', abschnitt=abschnitt))
if form_data.get('kernbohrung') == 'an':
pos.append(dict(pos_nr='10037907', kurztext='Kernbohrung', menge=1, einheit='ST', abschnitt=abschnitt))
return pos
def _float(val, default=0):
try: return float(str(val).replace(',', '.'))
except: return default
def _int(val, default=0):
try: return int(float(str(val).replace(',', '.')))
except: return default
View File

Some files were not shown because too many files have changed in this diff Show More