Initial commit – AufmaßCreater v2.35

This commit is contained in:
2026-06-10 11:03:43 +02:00
commit 84c933ea9c
2823 changed files with 490495 additions and 0 deletions
View File
+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})>'