Initial commit – AufmaßCreater v2.35
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,150 @@
|
||||
import json
|
||||
import re
|
||||
from app.extensions import db
|
||||
from app.models.lv import LVPosition
|
||||
|
||||
|
||||
def execute_rules(form_data, rules_json, company_id):
|
||||
rules = json.loads(rules_json) if isinstance(rules_json, str) else rules_json
|
||||
positions = []
|
||||
|
||||
for rule in rules:
|
||||
if not _evaluate_conditions(rule.get('conditions', {}), form_data):
|
||||
continue
|
||||
for action in rule.get('actions', []):
|
||||
pos = _create_position(action, form_data, company_id)
|
||||
if pos:
|
||||
positions.append(pos)
|
||||
|
||||
return positions
|
||||
|
||||
|
||||
def _evaluate_conditions(conditions, form_data):
|
||||
if not conditions or not conditions.get('items'):
|
||||
return True
|
||||
|
||||
operator = conditions.get('operator', 'and')
|
||||
items = conditions.get('items', [])
|
||||
|
||||
results = []
|
||||
for cond in items:
|
||||
field = cond.get('field', '')
|
||||
op = cond.get('operator', 'eq')
|
||||
value = cond.get('value', '')
|
||||
value2 = cond.get('value2', '')
|
||||
raw = form_data.get(field, '')
|
||||
|
||||
if op == 'is_checked':
|
||||
results.append(raw == 'an')
|
||||
elif op == 'is_empty':
|
||||
results.append(raw == '' or raw is None)
|
||||
elif op == 'not_empty':
|
||||
results.append(raw != '' and raw is not None)
|
||||
else:
|
||||
try:
|
||||
f_val = _to_float(raw)
|
||||
f_cond = _to_float(value)
|
||||
if op == 'eq':
|
||||
results.append(f_val == f_cond)
|
||||
elif op == 'neq':
|
||||
results.append(f_val != f_cond)
|
||||
elif op == 'gt':
|
||||
results.append(f_val > f_cond)
|
||||
elif op == 'gte':
|
||||
results.append(f_val >= f_cond)
|
||||
elif op == 'lt':
|
||||
results.append(f_val < f_cond)
|
||||
elif op == 'lte':
|
||||
results.append(f_val <= f_cond)
|
||||
elif op == 'between':
|
||||
f_val2 = _to_float(value2)
|
||||
results.append(f_cond <= f_val <= f_val2)
|
||||
else:
|
||||
results.append(str(raw) == str(value))
|
||||
except (ValueError, TypeError):
|
||||
results.append(str(raw) == str(value))
|
||||
|
||||
if operator == 'or':
|
||||
return any(results)
|
||||
return all(results)
|
||||
|
||||
|
||||
def _create_position(action, form_data, company_id):
|
||||
pos_nr = action.get('pos_nr', '')
|
||||
if not pos_nr:
|
||||
return None
|
||||
|
||||
lv_lookup = action.get('lv_lookup', True)
|
||||
columns = action.get('columns', {})
|
||||
|
||||
# Default values
|
||||
pos = {
|
||||
'pos_nr': pos_nr,
|
||||
'kurztext': '',
|
||||
'einheit': 'ST',
|
||||
'menge': 1,
|
||||
'faktor': 1,
|
||||
'laenge': 0,
|
||||
'breite': 0,
|
||||
'tiefe': 0,
|
||||
'bemerkung': '',
|
||||
'einzelpreis': 0,
|
||||
}
|
||||
|
||||
# LV lookup
|
||||
if lv_lookup:
|
||||
lv_pos = LVPosition.query.filter_by(company_id=company_id, pos_nr=pos_nr).first()
|
||||
if lv_pos:
|
||||
pos['kurztext'] = lv_pos.kurztext or ''
|
||||
pos['einheit'] = lv_pos.einheit or 'ST'
|
||||
pos['einzelpreis'] = lv_pos.einzelpreis or 0
|
||||
|
||||
# Apply overrides
|
||||
for col_key, col_def in columns.items():
|
||||
if col_key not in pos and col_key != 'pos_nr':
|
||||
continue
|
||||
val = _resolve_value(col_def, form_data)
|
||||
if val is not None:
|
||||
if col_key in ('menge', 'faktor', 'laenge', 'breite', 'tiefe', 'einzelpreis'):
|
||||
pos[col_key] = _to_float(val)
|
||||
else:
|
||||
pos[col_key] = str(val)
|
||||
|
||||
return pos
|
||||
|
||||
|
||||
def _resolve_value(col_def, form_data):
|
||||
if not col_def:
|
||||
return None
|
||||
ctype = col_def.get('type', 'fixed')
|
||||
value = col_def.get('value', '')
|
||||
|
||||
if ctype == 'fixed':
|
||||
return value
|
||||
elif ctype == 'field':
|
||||
return form_data.get(value, '')
|
||||
elif ctype == 'formula':
|
||||
return _eval_formula(value, form_data)
|
||||
return value
|
||||
|
||||
|
||||
def _eval_formula(expr, form_data):
|
||||
expr = str(expr)
|
||||
def _replace_field(m):
|
||||
fname = m.group(1)
|
||||
raw = form_data.get(fname, '0')
|
||||
return str(raw).replace(',', '.')
|
||||
expr = re.sub(r'\[([^\]]+)\]', _replace_field, expr)
|
||||
safe = re.sub(r'[^\d\+\-\*\/\(\)\. ]', '', expr)
|
||||
if not safe.strip():
|
||||
return 0
|
||||
try:
|
||||
return eval(safe, {'__builtins__': {}}, {})
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
|
||||
def _to_float(val):
|
||||
if val is None or val == '':
|
||||
return 0.0
|
||||
return float(str(val).replace(',', '.').replace(' ', ''))
|
||||
@@ -0,0 +1,134 @@
|
||||
import json
|
||||
from flask import url_for
|
||||
|
||||
def render_form(form_json, module_id, aufmass_id=None):
|
||||
fields = json.loads(form_json) if isinstance(form_json, str) else form_json
|
||||
parts = []
|
||||
berechnen_url = url_for("custom_modules.berechnen", module_id=module_id)
|
||||
if aufmass_id:
|
||||
berechnen_url += f'?aufmass_id={aufmass_id}'
|
||||
parts.append(f'<form hx-post="{berechnen_url}" hx-target="#modul-modal-body" hx-swap="innerHTML" class="box custom-module-form">')
|
||||
|
||||
in_group = False
|
||||
for f in fields:
|
||||
ftype = f.get('type', 'text')
|
||||
name = f.get('name', f.get('id', ''))
|
||||
label = f.get('label', '')
|
||||
placeholder = f.get('placeholder', '')
|
||||
default = f.get('default', '')
|
||||
required = f.get('required', False)
|
||||
cond = f.get('conditional_show')
|
||||
col_size = f.get('columns', '12')
|
||||
cond_attrs = ''
|
||||
if cond and cond.get('field') and cond.get('value'):
|
||||
cond_attrs = f' data-cond-field="{cond["field"]}" data-cond-value="{cond["value"]}" style="display:none"'
|
||||
|
||||
if ftype == 'group_start':
|
||||
if in_group:
|
||||
parts.append('</div>')
|
||||
collapsible = f.get('collapsible', False)
|
||||
title = f.get('title', 'Gruppe')
|
||||
parts.append(f'<div class="box" style="padding:10px;background:#f8faff;margin-bottom:10px"{cond_attrs}>')
|
||||
parts.append(f'<h5 class="title is-6 mb-2">{title}</h5>')
|
||||
parts.append('<div class="columns is-multiline mb-0">')
|
||||
in_group = True
|
||||
continue
|
||||
|
||||
if ftype == 'group_end':
|
||||
if in_group:
|
||||
parts.append('</div>')
|
||||
parts.append('</div>')
|
||||
in_group = False
|
||||
continue
|
||||
|
||||
if ftype == 'separator':
|
||||
parts.append(f'<hr{cond_attrs}>')
|
||||
continue
|
||||
|
||||
if ftype == 'label':
|
||||
parts.append(f'<p{cond_attrs} style="margin-bottom:6px">{_esc(f.get("text", ""))}</p>')
|
||||
continue
|
||||
|
||||
req_mark = ' <span class="has-text-danger">*</span>' if required else ''
|
||||
field_html = ''
|
||||
|
||||
if ftype == 'text':
|
||||
field_html = f'<input class="input" type="text" name="{_esc(name)}" placeholder="{_esc(placeholder)}" value="{_esc(default)}" style="font-size:0.9rem">'
|
||||
|
||||
elif ftype == 'number':
|
||||
inputmode = f.get('inputmode', 'decimal')
|
||||
min_attr = f' min="{f.get("min")}"' if f.get('min') != '' else ''
|
||||
max_attr = f' max="{f.get("max")}"' if f.get('max') != '' else ''
|
||||
step_attr = f' step="{f.get("step")}"' if f.get('step') and f.get('step') != 'any' else ''
|
||||
field_html = f'<input class="input" type="text" inputmode="{inputmode}" name="{_esc(name)}" placeholder="{_esc(placeholder)}" value="{_esc(default)}"{min_attr}{max_attr}{step_attr} style="font-size:0.9rem">'
|
||||
|
||||
elif ftype == 'checkbox':
|
||||
checked = 'checked' if default else ''
|
||||
field_html = f'<label class="checkbox"><input type="checkbox" name="{_esc(name)}" value="an" {checked}> {_esc(label)}</label>'
|
||||
|
||||
elif ftype == 'dropdown':
|
||||
opts = f.get('options', [])
|
||||
options_html = ''
|
||||
for o in opts:
|
||||
val = o.get('value', '')
|
||||
lbl = o.get('label', val)
|
||||
sel = 'selected' if default == val else ''
|
||||
options_html += f'<option value="{_esc(val)}" {sel}>{_esc(lbl)}</option>'
|
||||
field_html = f'<div class="select is-fullwidth"><select name="{_esc(name)}">{options_html}</select></div>'
|
||||
|
||||
elif ftype == 'radio':
|
||||
opts = f.get('options', [])
|
||||
radio_html = ''
|
||||
for o in opts:
|
||||
val = o.get('value', '')
|
||||
lbl = o.get('label', val)
|
||||
checked = 'checked' if default == val else ''
|
||||
radio_html += f'<label class="radio"><input type="radio" name="{_esc(name)}" value="{_esc(val)}" {checked}> {_esc(lbl)}</label>'
|
||||
field_html = radio_html
|
||||
|
||||
if field_html:
|
||||
if col_size != '12' and in_group:
|
||||
parts.append(f'<div class="column is-{col_size}"{cond_attrs}><div class="field"><label class="label" style="font-size:0.85rem">{_esc(label)}{req_mark}</label><div class="control">{field_html}</div></div></div>')
|
||||
else:
|
||||
parts.append(f'<div class="field"{cond_attrs}><label class="label" style="font-size:0.85rem">{_esc(label)}{req_mark}</label><div class="control">{field_html}</div></div>')
|
||||
|
||||
if in_group:
|
||||
parts.append('</div>')
|
||||
|
||||
parts.append(f'<button class="button is-primary mt-3" type="submit">Berechnen & ins Aufmaß übernehmen</button>')
|
||||
parts.append('</form>')
|
||||
|
||||
cond_js = '''
|
||||
<script>
|
||||
document.querySelectorAll('.custom-module-form [data-cond-field]').forEach(function(el) {
|
||||
var fieldName = el.dataset.condField;
|
||||
var condValue = el.dataset.condValue;
|
||||
var input = document.querySelector('[name="'+fieldName+'"]');
|
||||
if (input) {
|
||||
function toggle() {
|
||||
var show = false;
|
||||
if (input.type === 'checkbox') {
|
||||
show = input.checked;
|
||||
} else {
|
||||
show = (input.value === condValue);
|
||||
}
|
||||
el.style.display = show ? '' : 'none';
|
||||
}
|
||||
toggle();
|
||||
input.addEventListener('change', toggle);
|
||||
if (input.type !== 'checkbox') input.addEventListener('input', toggle);
|
||||
}
|
||||
});
|
||||
document.querySelectorAll('.custom-module-form input[inputmode="decimal"]').forEach(function(el) {
|
||||
if (typeof sanitizeNum === 'function') {
|
||||
el.addEventListener('input', function() { sanitizeNum(el); });
|
||||
}
|
||||
});
|
||||
</script>'''
|
||||
parts.append(cond_js)
|
||||
|
||||
return '\n'.join(parts)
|
||||
|
||||
|
||||
def _esc(s):
|
||||
return str(s).replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"')
|
||||
@@ -0,0 +1,332 @@
|
||||
import re
|
||||
import time
|
||||
import logging
|
||||
from urllib.parse import quote, urlencode
|
||||
import requests
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
EV_HOST = "https://evergabe.telekom.de"
|
||||
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:146.0) Gecko/20100101 Firefox/146.0"
|
||||
|
||||
|
||||
class EVergabeError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class EVergabeClient:
|
||||
"""Python-Port der AutoIt EVvergabeWebobj.au3 Funktionen."""
|
||||
|
||||
def __init__(self, username, password, name=""):
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.name = name
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update({
|
||||
"User-Agent": USER_AGENT,
|
||||
})
|
||||
self.session.verify = True
|
||||
|
||||
def _url_encode(self, s):
|
||||
return quote(str(s), safe="")
|
||||
|
||||
def _extract_csrf(self, html):
|
||||
meta = re.search(
|
||||
r'(?i)<meta[^>]+name="csrf-token"[^>]+content="([^"]+)"', html
|
||||
)
|
||||
if meta:
|
||||
return "_csrf", meta.group(1)
|
||||
inp = re.search(
|
||||
r'(?i)name="([^"]*csrf[^"]*)"[^>]*value="([^"]+)"', html
|
||||
)
|
||||
if inp:
|
||||
return inp.group(1), inp.group(2)
|
||||
raise EVergabeError("CSRF-Token nicht gefunden")
|
||||
|
||||
def login(self):
|
||||
html = self._get(f"{EV_HOST}/public/login")
|
||||
csrf_field, csrf_value = self._extract_csrf(html)
|
||||
body = (
|
||||
f"LoginForm[username]={self._url_encode(self.username)}"
|
||||
f"&LoginForm[password]={self._url_encode(self.password)}"
|
||||
f"&{csrf_field}={self._url_encode(csrf_value)}"
|
||||
)
|
||||
resp = self.session.post(
|
||||
f"{EV_HOST}/public/login",
|
||||
data=body,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
allow_redirects=False,
|
||||
)
|
||||
if resp.status_code not in (301, 302, 303) and "Logout" not in resp.text:
|
||||
raise EVergabeError(f"Login fehlgeschlagen, HTTP {resp.status_code}")
|
||||
if resp.status_code in (301, 302, 303):
|
||||
loc = resp.headers.get('Location', '/')
|
||||
if loc.startswith('http'):
|
||||
follow = self.session.get(loc, allow_redirects=True)
|
||||
else:
|
||||
follow = self.session.get(f"{EV_HOST}{loc}", allow_redirects=True)
|
||||
logger.info("EVergabe Login erfolgreich")
|
||||
return True
|
||||
|
||||
def _get(self, url, referer=""):
|
||||
headers = {}
|
||||
if referer:
|
||||
headers["Referer"] = referer
|
||||
resp = self.session.get(url, headers=headers, timeout=30)
|
||||
text = resp.text.replace("amp;", "")
|
||||
with open(r'E:\_Coding\__Deepseek_coding\AufmassCreater v2.35 (20260309)\_aufmass_web\ev_debug.log', 'a', encoding='utf-8') as f:
|
||||
f.write(f"\n{'='*80}\nGET {url}\nReferer: {referer}\nStatus: {resp.status_code}\n{'='*80}\n{text}\n\n")
|
||||
return text
|
||||
|
||||
def _post(self, url, body, referer=""):
|
||||
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
||||
if referer:
|
||||
headers["Referer"] = referer
|
||||
resp = self.session.post(url, data=body, headers=headers, timeout=30)
|
||||
return resp.text.replace("amp;", "")
|
||||
|
||||
# === _EV_Search_SM_obj ===
|
||||
def search_sm(self, sm_nr):
|
||||
url = (
|
||||
f"{EV_HOST}/framework-agreement-call"
|
||||
f"?OrderRecallSearch%5Bhead_line%5D={self._url_encode(sm_nr)}"
|
||||
f"&OrderRecallSearch%5Bdocument_no%5D="
|
||||
f"&OrderRecallSearch%5Bincoming_date%5D="
|
||||
f"&OrderRecallSearch%5Border_date%5D="
|
||||
f"&OrderRecallSearch%5Bgeneral_agreement_id%5D="
|
||||
f"&OrderRecallSearch%5Bdocument_state%5D=-1"
|
||||
)
|
||||
referer = f"{EV_HOST}/framework-agreement-call"
|
||||
html = self._get(url, referer)
|
||||
if sm_nr not in html:
|
||||
return []
|
||||
results = []
|
||||
titles = re.findall(r'(?m)no-wrap" title="([^"]+)', html)
|
||||
bedarfnr = re.findall(r'Bedarfsnr.: 0([^<]+)<', html)
|
||||
belegnr = re.findall(r'(42[^\/]+)\/0001', html)
|
||||
detail_ids = re.findall(r'call/details\?id=([^&]+)&c=1', html)
|
||||
rv = re.findall(r'/framework-agreement/details\?id=[^"]+">([^<]+)', html)
|
||||
dates_all = re.findall(r'(\d{2}\.\d{2}\.\d{4})', html)
|
||||
ausfuehrungsfrist = re.findall(r'(\d{2}\.\d{2}\.\d{4})\s*</td>\s*<td>\s*<a href', html)
|
||||
status = re.findall(r'/framework-agreement/details\?id=[^"]+">[^<]+', html)
|
||||
beleg_eingang = dates_all if dates_all else []
|
||||
max_len = max(len(titles), len(bedarfnr), len(belegnr), len(detail_ids))
|
||||
logger.info(f"search_sm: titles={len(titles)}, bedarfnr={len(bedarfnr)}, belegnr={len(belegnr)}, detail_ids={len(detail_ids)}")
|
||||
logger.info(f"search_sm: detail_ids={detail_ids}")
|
||||
logger.info(f"search_sm: titles_sample={titles[0][:100] if titles else 'none'}")
|
||||
for i in range(max_len):
|
||||
results.append({
|
||||
"title": (titles[i].replace("SM Auftragsnummer: 000", " SM ").replace("\r", " ").replace("\n", " ") if i < len(titles) else ""),
|
||||
"bedarf_nr": bedarfnr[i] if i < len(bedarfnr) else "",
|
||||
"beleg_nr": belegnr[i] if i < len(belegnr) else "",
|
||||
"detail_id": detail_ids[i] if i < len(detail_ids) else "",
|
||||
"rv": rv[i] if i < len(rv) else "",
|
||||
"beleg_eingang": beleg_eingang[i] if i < len(beleg_eingang) else "",
|
||||
"ausfuehrungsfrist": ausfuehrungsfrist[i] if i < len(ausfuehrungsfrist) else "",
|
||||
"status": status[i] if i < len(status) else "",
|
||||
})
|
||||
return results
|
||||
|
||||
# === _EV_Aspa_obj ===
|
||||
def get_aspa(self, details_id):
|
||||
url = f"{EV_HOST}/framework-agreement-call/details?id={details_id}&c=1"
|
||||
referer = f"{EV_HOST}/framework-agreement-call/"
|
||||
html = self._get(url, referer)
|
||||
name = re.search(r'Name\s*</th>\s*<td>\s*([^<]+?)\s*</td>', html)
|
||||
emails = re.findall(r'mailto:([^"?\s&]+)', html)
|
||||
tel = re.search(r'th>Telefon</th><td>([^<]+)', html)
|
||||
name_val = name.group(1).strip() if name else ""
|
||||
email_val = emails[1].strip() if len(emails) > 1 else (emails[0].strip() if emails else "")
|
||||
tel_val = tel.group(1).strip() if tel else ""
|
||||
return {
|
||||
"name": name_val,
|
||||
"email": email_val,
|
||||
"tel": tel_val,
|
||||
}
|
||||
|
||||
# === _EV_Hole_Kopfdaten_obj (kombiniert) ===
|
||||
def hole_kopfdaten(self, sm_nr):
|
||||
self.login()
|
||||
results = self.search_sm(sm_nr)
|
||||
if not results:
|
||||
raise EVergabeError(f"Kein Treffer für SM-Nr: {sm_nr}")
|
||||
first = results[0]
|
||||
logger.info(f"SM-Suche: title={first['title'][:50]}, detail_id={first['detail_id']}, beleg_eingang={first['beleg_eingang']}, ausfuehrungsfrist={first['ausfuehrungsfrist']}")
|
||||
aspa = self.get_aspa(first["detail_id"])
|
||||
return {
|
||||
"bezeichnung": first["title"].strip(),
|
||||
"sm_nr": first["bedarf_nr"],
|
||||
"abruf_nr": first["beleg_nr"],
|
||||
"detail_id": first["detail_id"],
|
||||
"rv": first["rv"],
|
||||
"beleg_eingang": first["beleg_eingang"],
|
||||
"ausfuehrungsfrist": first["ausfuehrungsfrist"],
|
||||
"status": first["status"],
|
||||
"ansprechpartner_name": aspa["name"],
|
||||
"ansprechpartner_email": aspa["email"],
|
||||
"ansprechpartner_tel": aspa["tel"],
|
||||
}
|
||||
|
||||
# === _EV_Pos_eintragen_obj (Aufmaß übertragen) ===
|
||||
def aufmass_uebertragen(self, sm_nr, positionen, bauabschnitt="", leist_zeitv="", leist_zeitb="", leistungsort="", teilaufmass=False, schlussaufmass=False):
|
||||
self.login()
|
||||
results = self.search_sm(sm_nr)
|
||||
if not results:
|
||||
raise EVergabeError(f"Kein Treffer für SM-Nr: {sm_nr}")
|
||||
if len(results) > 1:
|
||||
details_id = results[0]["detail_id"]
|
||||
else:
|
||||
details_id = results[0]["detail_id"]
|
||||
logger.info(f"DetailsID: {details_id}")
|
||||
html = self._get(
|
||||
f"{EV_HOST}/framework-agreement-call/details?id={details_id}&c=1",
|
||||
f"{EV_HOST}/framework-agreement-call"
|
||||
)
|
||||
if "LERF nicht möglich" in html:
|
||||
raise EVergabeError("LERF nicht möglich")
|
||||
html = self._get(
|
||||
f"{EV_HOST}/sheet/index?c=1&importId={details_id}",
|
||||
f"{EV_HOST}/framework-agreement-call/details?id={details_id}&c=1"
|
||||
)
|
||||
html = self._get(
|
||||
f"{EV_HOST}/sheet/create-sheet?c=1&id={details_id}",
|
||||
f"{EV_HOST}/sheet/index?c=1&importId={details_id}"
|
||||
)
|
||||
csrf_field, csrf_value = self._extract_csrf(html)
|
||||
leistungsort1 = leistungsort[:25] if len(leistungsort) >= 25 else leistungsort
|
||||
leistungsort_so = leistungsort
|
||||
sachbearbeiter = self.name[:12] if len(self.name) >= 12 else self.name
|
||||
bauabschnitt_clean = bauabschnitt[:39] if len(bauabschnitt) >= 40 else bauabschnitt
|
||||
kurztext = f"SM {sm_nr}"[:39]
|
||||
langtext = f"{leistungsort} {bauabschnitt} SM {sm_nr}"
|
||||
final = "X" if schlussaufmass else ""
|
||||
body = (
|
||||
f"_csrf={self._url_encode(csrf_value)}%3D%3D"
|
||||
f"&BapiEssr%5Bfinal%5D={final}"
|
||||
f"&BapiEssr%5Bfinal%5D={final}"
|
||||
f"&BapiEssr%5Blzvon%5D={self._url_encode(leist_zeitv)}"
|
||||
f"&BapiEssr%5Blzbis%5D={self._url_encode(leist_zeitb)}"
|
||||
f"&BapiEssr%5Bdlort%5D={self._url_encode(leistungsort1)}"
|
||||
f"&BapiEssr%5Bsbnaman%5D={self._url_encode(sachbearbeiter)}"
|
||||
f"&BapiEssr%5Btxz01%5D={self._url_encode(bauabschnitt_clean)}"
|
||||
f"&BapiEssr%5Bdescription%5D={self._url_encode(langtext)}"
|
||||
f"&save+sheets="
|
||||
)
|
||||
html = self._post(
|
||||
f"{EV_HOST}/sheet/create-sheet?c=1&iid={details_id}",
|
||||
body,
|
||||
f"{EV_HOST}/sheet/create-sheet?c=1&iid={details_id}"
|
||||
)
|
||||
sheet_id_match = re.search(r'sheetId=(.*)">Kopfdaten', html)
|
||||
if not sheet_id_match:
|
||||
raise EVergabeError(f"sheetID nicht gefunden. Response: {html[:200]}")
|
||||
sheet_id = sheet_id_match.group(1)
|
||||
logger.info(f"SheetID: {sheet_id}")
|
||||
ergebnisse = []
|
||||
for idx, pos in enumerate(positionen):
|
||||
pos_html = self._get(
|
||||
f"{EV_HOST}/sheet-position/index?c=1&sheetId={sheet_id}",
|
||||
f"{EV_HOST}/sheet/index?c=1&importId={details_id}"
|
||||
)
|
||||
csrf_field2, csrf_value2 = self._extract_csrf(pos_html)
|
||||
pos_nr = pos.get("pos_nr", "")
|
||||
pos_body = f"_csrf={self._url_encode(csrf_value2)}%3D%3D&ServicePosition%5Bnumber%5D={self._url_encode(pos_nr)}&insertPosition=0"
|
||||
pos_html2 = self._post(
|
||||
f"{EV_HOST}/sheet-position/index?c=1&sheetId={sheet_id}",
|
||||
pos_body,
|
||||
f"{EV_HOST}/sheet-position/index?c=1&sheetId={sheet_id}"
|
||||
)
|
||||
pos_id_match = re.search(r'positionId=([^&]+)', pos_html2)
|
||||
if not pos_id_match:
|
||||
ergebnisse.append({"pos_nr": pos_nr, "status": "error", "msg": "positionId nicht gefunden"})
|
||||
continue
|
||||
pos_id = pos_id_match.group(1)
|
||||
create_url = f"{EV_HOST}/sheet-position/create?insertPosition=0&positionId={pos_id}&c=1&sheetId={sheet_id}"
|
||||
create_html = self._get(
|
||||
f"{EV_HOST}/sheet-position/create?insertPosition=0&positionId={pos_id}&c=1&sheetId={sheet_id}",
|
||||
f"{EV_HOST}/sheet/index?c=1&importId={details_id}"
|
||||
)
|
||||
csrf_field3, csrf_value3 = self._extract_csrf(create_html)
|
||||
einheit = pos.get("einheit", "ST")
|
||||
abschnitt = pos.get("bauabschnitt", bauabschnitt_clean)[:25]
|
||||
langtext_pos = pos.get("langtext", "")
|
||||
pos_kurztext = f"{pos_nr}|{pos.get('bezeichnung', '')}"
|
||||
if einheit == "ST":
|
||||
menge = str(pos.get("menge", "")).replace(",", "%2C")
|
||||
post_body = (
|
||||
f"_csrf={self._url_encode(csrf_value3)}%3D%3D"
|
||||
f"&ServicePosition%5B0%5D%5BsectionText%5D={self._url_encode(abschnitt)}"
|
||||
f"&ServicePosition%5B0%5D%5Bquantity%5D={menge}"
|
||||
f"&ServicePosition%5B0%5D%5BlongText%5D={self._url_encode(langtext_pos)}"
|
||||
f"&ServicePosition%5B0%5D%5Bid%5D={pos_id}"
|
||||
f"&clientId=1&sheetId={sheet_id}&insertPosition={idx}&save="
|
||||
)
|
||||
elif einheit == "M":
|
||||
faktor = str(pos.get("faktor", "1")).replace(",", "%2C")
|
||||
meter = str(pos.get("menge", "")).replace(",", "%2C")
|
||||
post_body = (
|
||||
f"_csrf={self._url_encode(csrf_value3)}%3D%3D"
|
||||
f"&ServicePosition%5B0%5D%5BsectionText%5D={self._url_encode(abschnitt)}"
|
||||
f"&ServicePosition%5B0%5D%5BformulaSymbol%5D=ME"
|
||||
f"&ServicePosition%5B0%5D%5BformulaValueMultiplier%5D={faktor}"
|
||||
f"&ServicePosition%5B0%5D%5BformulaValueLength%5D={meter}"
|
||||
f"&ServicePosition%5B0%5D%5BlongText%5D={self._url_encode(langtext_pos)}"
|
||||
f"&ServicePosition%5B0%5D%5Bid%5D={pos_id}"
|
||||
f"&clientId=1&sheetId={sheet_id}&insertPosition={idx}&save="
|
||||
)
|
||||
elif einheit == "M2":
|
||||
faktor = str(pos.get("faktor", "1")).replace(",", "%2C")
|
||||
laenge = str(pos.get("laenge", "")).replace(",", "%2C")
|
||||
breite = str(pos.get("breite", "")).replace(",", "%2C")
|
||||
post_body = (
|
||||
f"_csrf={self._url_encode(csrf_value3)}%3D%3D"
|
||||
f"&ServicePosition%5B0%5D%5BsectionText%5D={self._url_encode(abschnitt)}"
|
||||
f"&ServicePosition%5B0%5D%5BformulaSymbol%5D=MF"
|
||||
f"&ServicePosition%5B0%5D%5BformulaValueMultiplier%5D={faktor}"
|
||||
f"&ServicePosition%5B0%5D%5BformulaValueLength%5D={laenge}"
|
||||
f"&ServicePosition%5B0%5D%5BformulaValueWidth%5D={breite}"
|
||||
f"&ServicePosition%5B0%5D%5BlongText%5D={self._url_encode(langtext_pos)}"
|
||||
f"&ServicePosition%5B0%5D%5Bid%5D={pos_id}"
|
||||
f"&clientId=1&sheetId={sheet_id}&insertPosition={idx}&save="
|
||||
)
|
||||
elif einheit == "M3":
|
||||
faktor = str(pos.get("faktor", "1")).replace(",", "%2C")
|
||||
laenge = str(pos.get("laenge", "")).replace(",", "%2C")
|
||||
breite = str(pos.get("breite", "")).replace(",", "%2C")
|
||||
tiefe = str(pos.get("tiefe", "")).replace(",", "%2C")
|
||||
post_body = (
|
||||
f"_csrf={self._url_encode(csrf_value3)}%3D%3D"
|
||||
f"&ServicePosition%5B0%5D%5BsectionText%5D={self._url_encode(abschnitt)}"
|
||||
f"&ServicePosition%5B0%5D%5BformulaSymbol%5D=MV"
|
||||
f"&ServicePosition%5B0%5D%5BformulaValueMultiplier%5D={faktor}"
|
||||
f"&ServicePosition%5B0%5D%5BformulaValueLength%5D={laenge}"
|
||||
f"&ServicePosition%5B0%5D%5BformulaValueWidth%5D={breite}"
|
||||
f"&ServicePosition%5B0%5D%5BformulaValueDepth%5D={tiefe}"
|
||||
f"&ServicePosition%5B0%5D%5BlongText%5D={self._url_encode(langtext_pos)}"
|
||||
f"&ServicePosition%5B0%5D%5Bid%5D={pos_id}"
|
||||
f"&clientId=1&sheetId={sheet_id}&insertPosition={idx}&save="
|
||||
)
|
||||
elif einheit in ("STD", "LE"):
|
||||
stueck = str(pos.get("menge", "")).replace(",", "%2C")
|
||||
post_body = (
|
||||
f"_csrf={self._url_encode(csrf_value3)}%3D%3D"
|
||||
f"&ServicePosition%5B0%5D%5BsectionText%5D={self._url_encode(abschnitt)}"
|
||||
f"&ServicePosition%5B0%5D%5Bquantity%5D={stueck}"
|
||||
f"&ServicePosition%5B0%5D%5BlongText%5D={self._url_encode(langtext_pos)}"
|
||||
f"&ServicePosition%5B0%5D%5Bid%5D={pos_id}"
|
||||
f"&clientId=1&sheetId={sheet_id}&insertPosition={idx}&save="
|
||||
)
|
||||
else:
|
||||
ergebnisse.append({"pos_nr": pos_nr, "status": "error", "msg": f"Unbekannte Einheit: {einheit}"})
|
||||
continue
|
||||
result_html = self._post(
|
||||
f"{EV_HOST}/sheet-position/create?insertPosition={idx}&positionId={pos_id}&c=1&sheetId={sheet_id}",
|
||||
post_body,
|
||||
f"{EV_HOST}/sheet-position/create?insertPosition=0&positionId={pos_id}&c=1&sheetId={sheet_id}"
|
||||
)
|
||||
if "erfolgreich gespeichert" in result_html:
|
||||
ergebnisse.append({"pos_nr": pos_nr, "status": "ok", "msg": "eingetragen"})
|
||||
else:
|
||||
ergebnisse.append({"pos_nr": pos_nr, "status": "error", "msg": "Fehlgeschlagen"})
|
||||
time.sleep(0.5)
|
||||
return ergebnisse
|
||||
@@ -0,0 +1,300 @@
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from flask import current_app, render_template
|
||||
from fpdf import FPDF
|
||||
|
||||
# ── helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
def _val(v):
|
||||
return None if v is None or v == 0 or v == '' else str(v)
|
||||
|
||||
def _fmt_date(d):
|
||||
if d is None:
|
||||
return None
|
||||
if isinstance(d, str):
|
||||
d = d[:10]
|
||||
for fmt in ('%Y-%m-%d', '%d.%m.%Y'):
|
||||
try:
|
||||
return datetime.strptime(d, fmt).strftime('%d.%m.%Y')
|
||||
except ValueError:
|
||||
continue
|
||||
return d
|
||||
return d.strftime('%d.%m.%Y')
|
||||
|
||||
def _ist_trenner(pos):
|
||||
return (not pos.pos_nr or pos.pos_nr == '') and (pos.faktor == 0 or pos.faktor is None) and (pos.laenge == 0 or pos.laenge is None) and (pos.breite == 0 or pos.breite is None) and (pos.tiefe == 0 or pos.tiefe is None) and (pos.menge == 0 or pos.menge is None) and (pos.einzelpreis == 0 or pos.einzelpreis is None) and (pos.gesamtpreis == 0 or pos.gesamtpreis is None)
|
||||
|
||||
# ── Farben ──────────────────────────────────────────────────────────────────
|
||||
|
||||
BLUE = (47, 84, 150)
|
||||
LIGHT_GRAY = (0.93, 0.93, 0.93)
|
||||
WHITE = (255, 255, 255)
|
||||
BLACK = (0, 0, 0)
|
||||
|
||||
# ── PDF-Klasse ──────────────────────────────────────────────────────────────
|
||||
|
||||
class AufmassPDF(FPDF):
|
||||
def footer(self):
|
||||
self.set_y(-8)
|
||||
self.set_font('DJ', 'I', 7)
|
||||
self.set_text_color(128, 128, 128)
|
||||
self.cell(0, 6, f'Seite {self.page_no()}/{{nb}}', align='C', new_x='LMARGIN', new_y='NEXT')
|
||||
|
||||
# ── Hauptfunktion ───────────────────────────────────────────────────────────
|
||||
|
||||
def _render_pdf_html(project, aufmass, positionen, company=None):
|
||||
ap_name = _name(project)
|
||||
groups, seen_pos = _build_summary(positionen)
|
||||
return render_template(
|
||||
'aufmass/export_pdf.html',
|
||||
project=project, aufmass=aufmass, positionen=positionen,
|
||||
company=company, ap_name=ap_name, groups=dict(groups),
|
||||
seen_pos=seen_pos, _val=_val, _fmt_date=_fmt_date,
|
||||
_ist_trenner=_ist_trenner,
|
||||
)
|
||||
|
||||
def _name(project):
|
||||
return f'{_val(project.ansprechpartner_vorname) or ""} {_val(project.ansprechpartner_nachname) or ""}'.strip()
|
||||
|
||||
def _build_summary(positionen):
|
||||
groups = defaultdict(lambda: {'kurztext': '', 'menge': 0.0, 'ep': 0.0, 'gp': 0.0})
|
||||
seen = []
|
||||
for p in positionen:
|
||||
if _ist_trenner(p) or not p.pos_nr:
|
||||
continue
|
||||
key = p.pos_nr
|
||||
if key not in groups:
|
||||
seen.append(key)
|
||||
groups[key]['kurztext'] = p.kurztext or ''
|
||||
groups[key]['menge'] += p.menge_hinten if p.menge_hinten else (p.menge or 0)
|
||||
groups[key]['ep'] = p.einzelpreis or 0
|
||||
groups[key]['gp'] += p.gesamtpreis or 0
|
||||
return groups, seen
|
||||
|
||||
def _val_or(v, default=''):
|
||||
return str(v) if v is not None and v != 0 and v != '' else default
|
||||
|
||||
def _fmt(v):
|
||||
return f'{v:.2f}'.replace('.',',') if v is not None else ''
|
||||
|
||||
# ── Spaltenbreiten (mm) – Verhältnis wie Excel max_widths ───────────────────
|
||||
# A4 quer = 297mm, margin 5mm → 287mm verfügbar, aber wir nutzen 282mm für die Tabelle
|
||||
W_POS = [20, 17, 12, 14, 14, 14, 16, 9, 63, 52, 16, 16, 19] # sum = 282mm
|
||||
# 20+17+12+14+14+14+16+9+63+52+16+16+19 = 282 ✓
|
||||
W_SUM = [20, 172, 28, 28, 34] # Zusammenfassung: PNr, Kurztext, Menge, EP, GP
|
||||
# 20+172+28+28+34 = 282mm (gleiche Breite wie Positionstabelle)
|
||||
|
||||
def _font_path(name, fallback=None):
|
||||
win = f'C:/Windows/Fonts/{name}'
|
||||
lin = f'/usr/share/fonts/truetype/dejavu/{name}'
|
||||
if os.path.exists(win):
|
||||
return win
|
||||
if os.path.exists(lin):
|
||||
return lin
|
||||
return _font_path(fallback) if fallback else name
|
||||
|
||||
DJV = _font_path('DejaVuSans.ttf')
|
||||
DJV_B = _font_path('DejaVuSans-Bold.ttf')
|
||||
DJV_O = _font_path('DejaVuSans-Oblique.ttf', fallback='DejaVuSans.ttf')
|
||||
DJV_BI = _font_path('DejaVuSans-BoldOblique.ttf', fallback='DejaVuSans-Bold.ttf')
|
||||
|
||||
def export_project_to_pdf(project, aufmass, positionen, output_path, company=None):
|
||||
pdf = AufmassPDF(orientation='L', format='A4')
|
||||
pdf.alias_nb_pages()
|
||||
pdf.set_auto_page_break(auto=True, margin=10)
|
||||
pdf.add_page()
|
||||
pdf.set_margins(7, 5, 7) # 7mm links/rechts → 283mm verfügbar
|
||||
# Aber wir nutzen 282mm passend zu den Tabellen
|
||||
pdf.set_margins(7.5, 5, 7.5) # 7.5mm links/rechts → 282mm verfügbar
|
||||
pdf.add_font('DJ', '', DJV, uni=True)
|
||||
pdf.add_font('DJ', 'B', DJV_B, uni=True)
|
||||
pdf.add_font('DJ', 'I', DJV_O, uni=True)
|
||||
pdf.add_font('DJ', 'BI', DJV_BI, uni=True)
|
||||
|
||||
ap_name = _name(project)
|
||||
groups, seen_pos = _build_summary(positionen)
|
||||
total_gp = sum(g['gp'] for g in groups.values())
|
||||
|
||||
# ── Logo / Titel ─────────────────────────────────────────────────────────
|
||||
logo_shown = False
|
||||
if company and company.logo:
|
||||
lp = company.logo
|
||||
if not os.path.isabs(lp):
|
||||
lp = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), lp)
|
||||
if os.path.exists(lp):
|
||||
try:
|
||||
pdf.image(lp, x=5, y=5, w=30)
|
||||
logo_shown = True
|
||||
except Exception:
|
||||
pass
|
||||
if not logo_shown:
|
||||
if company and company.name:
|
||||
pdf.set_font('DJ', 'B', 12)
|
||||
pdf.set_text_color(*BLUE)
|
||||
pdf.cell(0, 8, company.name, align='L', new_x='LMARGIN', new_y='NEXT')
|
||||
pdf.set_font('DJ', 'B', 16)
|
||||
pdf.set_text_color(*BLUE)
|
||||
pdf.cell(0, 8, 'Aufmaß', align='C', new_x='LMARGIN', new_y='NEXT')
|
||||
else:
|
||||
pdf.set_font('DJ', 'B', 16)
|
||||
pdf.set_text_color(*BLUE)
|
||||
pdf.cell(0, 8, 'Aufmaß', align='C', new_x='LMARGIN', new_y='NEXT')
|
||||
pdf.set_text_color(*BLACK)
|
||||
pdf.ln(3)
|
||||
|
||||
# ── Kopfzeilen (8 Spalten zu je 35.25mm = 282mm) ───────────────────────────
|
||||
CW = 35.25 # jede Spalte gleich breit
|
||||
LH = 5.5
|
||||
pdf.set_font('DJ', '', 7.5)
|
||||
|
||||
def cell_lbl(txt, w=CW):
|
||||
pdf.set_fill_color(242, 242, 242)
|
||||
pdf.set_font('DJ', 'B', 7.5)
|
||||
pdf.cell(w, LH, txt, border=1, fill=True, align='L')
|
||||
|
||||
def cell_val(txt, w=CW):
|
||||
pdf.set_font('DJ', '', 7.5)
|
||||
pdf.cell(w, LH, txt, border=1, align='L')
|
||||
|
||||
def nl():
|
||||
pdf.set_x(7.5)
|
||||
|
||||
# Row 1: Vertrag(2) / LV-Name(4) / Aufmaß-Datum(2)
|
||||
cell_lbl('Vertrag:'); cell_val(_val_or(project.vertrag), CW)
|
||||
cell_lbl('LV-Name:'); cell_val(_val_or(project.lv_name), CW*3)
|
||||
cell_lbl('Aufmaß-Datum:'); cell_val(_fmt_date(project.datum) or '', CW*2)
|
||||
nl(); pdf.ln(LH)
|
||||
|
||||
# Row 2: Projekt(2) / Baustelle(6)
|
||||
cell_lbl('Projekt:'); cell_val(_val_or(project.bezeichnung), CW)
|
||||
cell_lbl('Baustelle:'); cell_val(_val_or(project.baustelle), CW*5)
|
||||
nl(); pdf.ln(LH)
|
||||
|
||||
# Row 3: Typ(2) / Bauabschnitt(6)
|
||||
cell_lbl('Typ:'); cell_val(_val_or(aufmass.typ if aufmass else None), CW)
|
||||
cell_lbl('Bauabschnitt:'); cell_val(_val_or(project.bauabschnitt), CW*5)
|
||||
nl(); pdf.ln(LH)
|
||||
|
||||
# Row 4: SM-Nr(2) / Startdatum(2) / Name(1) / Wert(1) / Tel(2)
|
||||
cell_lbl('SM-Nr.:'); cell_val(_val_or(project.sm_nr), CW)
|
||||
cell_lbl('Startdatum:'); cell_val(_fmt_date(project.datum_start) or '', CW)
|
||||
cell_lbl('Name:'); cell_val(_val_or(ap_name))
|
||||
cell_lbl('Tel:'); cell_val(_val_or(project.ansprechpartner_tel), CW)
|
||||
nl(); pdf.ln(LH)
|
||||
|
||||
# Row 5: Abruf-Nr(2) / Enddatum(2) / Email(4)
|
||||
cell_lbl('Abruf-Nr.:'); cell_val(_val_or(project.abruf_nr), CW)
|
||||
cell_lbl('Enddatum:'); cell_val(_fmt_date(project.datum_ende) or '', CW)
|
||||
cell_lbl('Email:'); cell_val(_val_or(project.ansprechpartner_email), CW*4)
|
||||
nl(); pdf.ln(LH + 2)
|
||||
|
||||
# ── Positionstabelle ─────────────────────────────────────────────────────
|
||||
headers = ['Abschn.', 'Pos-Nr', 'Fakt.', 'Länge', 'Breite', 'Tiefe',
|
||||
'Menge', 'EH', 'Kurztext', 'Bemerkung', 'Menge', 'EP (€)', 'GP (€)']
|
||||
|
||||
# Tabellenkopf
|
||||
pdf.set_fill_color(*BLUE)
|
||||
pdf.set_text_color(*WHITE)
|
||||
pdf.set_font('DJ', 'B', 6)
|
||||
x0 = pdf.get_x()
|
||||
y0 = pdf.get_y()
|
||||
for i, h in enumerate(headers):
|
||||
pdf.cell(W_POS[i], 5, h, border=1, fill=True, align='C')
|
||||
pdf.ln()
|
||||
pdf.set_text_color(*BLACK)
|
||||
|
||||
# Datenzeilen
|
||||
pdf.set_font('DJ', '', 6)
|
||||
row_h = 4.5
|
||||
gesamt = 0.0
|
||||
pos_counter = 0
|
||||
for pos in positionen:
|
||||
if _ist_trenner(pos):
|
||||
pdf.set_fill_color(245, 245, 245)
|
||||
pdf.cell(sum(W_POS), 3, '', border=1, fill=True)
|
||||
pdf.ln()
|
||||
continue
|
||||
pos_counter += 1
|
||||
menge = pos.menge if pos.menge else None
|
||||
menge_h = pos.menge_hinten if pos.menge_hinten else None
|
||||
if pos.einheit in ('ST', 'LE', 'STD', 'h', 'Psch'):
|
||||
menge = pos.faktor * 1 if pos.faktor else None
|
||||
gp = pos.gesamtpreis or 0
|
||||
gesamt += gp
|
||||
|
||||
y0 = pdf.get_y()
|
||||
x0 = pdf.get_x()
|
||||
|
||||
# Kurztext & Bemerkung: MultiCell für Wrap, dann Höhe bestimmen
|
||||
kurz = _val_or(pos.kurztext)
|
||||
bemerk = _val_or(pos.bemerkung)
|
||||
# Einfache Zeilenhöhe (kein MultiCell – zu komplex)
|
||||
# Stattdessen: Text auf eine Zeile beschränken (abschneiden bei Überlauf)
|
||||
MAX_KURZTEXT = 95
|
||||
MAX_BEMERK = 28
|
||||
if len(kurz) > MAX_KURZTEXT:
|
||||
kurz = kurz[:MAX_KURZTEXT-3] + '...'
|
||||
if len(bemerk) > MAX_BEMERK:
|
||||
bemerk = bemerk[:MAX_BEMERK-3] + '...'
|
||||
|
||||
vals = [
|
||||
pos.abschnitt or '', pos.pos_nr or '',
|
||||
_fmt(pos.faktor), _fmt(pos.laenge), _fmt(pos.breite), _fmt(pos.tiefe),
|
||||
_fmt(menge), pos.einheit or '', kurz, bemerk,
|
||||
_fmt(menge_h), _fmt(pos.einzelpreis), _fmt(gp)
|
||||
]
|
||||
|
||||
for i, v in enumerate(vals):
|
||||
a = 'L' if i in (8, 9) else ('C' if i in (0, 7) else 'R')
|
||||
pdf.cell(W_POS[i], row_h, v, border=1, align=a)
|
||||
pdf.ln()
|
||||
|
||||
# Summenzeile
|
||||
pdf.set_font('DJ', 'B', 6)
|
||||
pdf.cell(sum(W_POS[:11]), row_h, '', border=1, align='R')
|
||||
pdf.cell(W_POS[11], row_h, 'Summe:', border=1, align='R')
|
||||
pdf.cell(W_POS[12], row_h, f'{gesamt:.2f}', border=1, align='R')
|
||||
pdf.ln()
|
||||
|
||||
# ── Mengen-Zusammenfassung ─────────────────────────────────────────────
|
||||
if seen_pos:
|
||||
pdf.ln(4)
|
||||
pdf.set_fill_color(214, 228, 240)
|
||||
pdf.set_text_color(*BLUE)
|
||||
pdf.set_font('DJ', 'B', 9)
|
||||
pdf.cell(sum(W_SUM), 6, 'Mengen- und Positions-Zusammenfassung', border=1, fill=True, align='C')
|
||||
pdf.ln()
|
||||
|
||||
pdf.set_text_color(*WHITE)
|
||||
pdf.set_fill_color(*BLUE)
|
||||
pdf.set_font('DJ', 'B', 6)
|
||||
for i, h in enumerate(['Pos-Nr', 'Kurztext', 'Menge', 'EP (€)', 'GP (€)']):
|
||||
pdf.cell(W_SUM[i], 5, h, border=1, fill=True, align='C')
|
||||
pdf.ln()
|
||||
pdf.set_text_color(*BLACK)
|
||||
|
||||
total = 0.0
|
||||
pdf.set_font('DJ', '', 7)
|
||||
MAX_KURZTEXT_SUM = 120
|
||||
for key in seen_pos:
|
||||
g = groups[key]
|
||||
total += g['gp']
|
||||
kurz_sum = g['kurztext']
|
||||
if len(kurz_sum) > MAX_KURZTEXT_SUM:
|
||||
kurz_sum = kurz_sum[:MAX_KURZTEXT_SUM-3] + '...'
|
||||
vals = [key, kurz_sum, _fmt(g['menge']), _fmt(g['ep']), _fmt(g['gp'])]
|
||||
for i, v in enumerate(vals):
|
||||
a = 'L' if i == 1 else 'C' if i == 0 else 'R'
|
||||
pdf.cell(W_SUM[i], 5, v, border=1, align=a)
|
||||
pdf.ln()
|
||||
|
||||
pdf.set_font('DJ', 'B', 7)
|
||||
pdf.cell(W_SUM[0] + W_SUM[1], 5, '', border=1)
|
||||
pdf.cell(W_SUM[2], 5, 'Summe:', border=1, align='R')
|
||||
pdf.cell(W_SUM[3], 5, '', border=1)
|
||||
pdf.cell(W_SUM[4], 5, f'{total:.2f}', border=1, align='R')
|
||||
pdf.ln()
|
||||
|
||||
pdf.output(output_path)
|
||||
return output_path
|
||||
@@ -0,0 +1,359 @@
|
||||
import os
|
||||
from datetime import datetime
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import Font, Alignment, Border, Side, PatternFill
|
||||
from openpyxl.utils import get_column_letter
|
||||
from openpyxl.drawing.image import Image as XlImage
|
||||
from collections import defaultdict
|
||||
|
||||
def _val(v):
|
||||
if v is None or v == 0 or v == '':
|
||||
return None
|
||||
return v
|
||||
|
||||
def _ist_trenner(pos):
|
||||
return (not pos.pos_nr or pos.pos_nr == '') and (pos.faktor == 0 or pos.faktor is None) and (pos.laenge == 0 or pos.laenge is None) and (pos.breite == 0 or pos.breite is None) and (pos.tiefe == 0 or pos.tiefe is None) and (pos.menge == 0 or pos.menge is None) and (pos.einzelpreis == 0 or pos.einzelpreis is None) and (pos.gesamtpreis == 0 or pos.gesamtpreis is None)
|
||||
|
||||
def _tb():
|
||||
return Border(left=Side(style='thin'), right=Side(style='thin'), top=Side(style='thin'), bottom=Side(style='thin'))
|
||||
|
||||
def _fmt_date(d):
|
||||
if d is None:
|
||||
return None
|
||||
if isinstance(d, str):
|
||||
d = d[:10]
|
||||
for fmt in ('%Y-%m-%d', '%d.%m.%Y'):
|
||||
try:
|
||||
return datetime.strptime(d, fmt).strftime('%d.%m.%Y')
|
||||
except ValueError:
|
||||
continue
|
||||
return d
|
||||
return d.strftime('%d.%m.%Y')
|
||||
|
||||
def export_project_to_excel(project, aufmass, positionen, output_path, company=None):
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.title = "Aufmaß"
|
||||
|
||||
ws.page_setup.paperSize = ws.PAPERSIZE_A4
|
||||
ws.page_setup.orientation = 'landscape'
|
||||
ws.page_setup.fitToWidth = 1
|
||||
ws.page_setup.fitToHeight = 0
|
||||
ws.sheet_properties.pageSetUpPr.fitToPage = True
|
||||
ws.page_margins.left = 0.3
|
||||
ws.page_margins.right = 0.3
|
||||
ws.page_margins.top = 0.4
|
||||
ws.page_margins.bottom = 0.7
|
||||
|
||||
ws.oddFooter.center.text = ""
|
||||
ws.oddFooter.right.text = "&P/&N"
|
||||
ws.oddFooter.right.font = "Calibri,11"
|
||||
|
||||
header_fill = PatternFill(start_color='2F5496', end_color='2F5496', fill_type='solid')
|
||||
header_font = Font(name='Calibri', size=11, bold=True, color='FFFFFF')
|
||||
label_font = Font(name='Calibri', size=10, bold=True)
|
||||
value_font = Font(name='Calibri', size=10)
|
||||
value_font_bold = Font(name='Calibri', size=10, bold=True)
|
||||
title_font = Font(name='Calibri', size=16, bold=True)
|
||||
header_box_fill = PatternFill(start_color='F2F2F2', end_color='F2F2F2', fill_type='solid')
|
||||
trenner_fill = PatternFill(start_color='F5F5F5', end_color='F5F5F5', fill_type='solid')
|
||||
|
||||
row = 1
|
||||
|
||||
firmen_name = company.name if company else 'Aufmaß'
|
||||
logo_placed = False
|
||||
if company and company.logo:
|
||||
logo_path = company.logo
|
||||
if not os.path.isabs(logo_path):
|
||||
logo_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), logo_path)
|
||||
if os.path.exists(logo_path):
|
||||
try:
|
||||
img = XlImage(logo_path)
|
||||
img.width = 120
|
||||
img.height = 60
|
||||
ws.add_image(img, 'A1')
|
||||
ws.row_dimensions[1].height = 60
|
||||
ws.merge_cells(start_row=1, start_column=4, end_row=1, end_column=13)
|
||||
ws.cell(row=1, column=4, value='Aufmaß').font = title_font
|
||||
ws.cell(row=1, column=4).alignment = Alignment(horizontal='center', vertical='center')
|
||||
logo_placed = True
|
||||
except Exception:
|
||||
pass
|
||||
if not logo_placed:
|
||||
ws.cell(row=1, column=1, value=firmen_name).font = title_font
|
||||
row = 2
|
||||
|
||||
row += 1
|
||||
|
||||
label_align = Alignment(horizontal='left', vertical='center')
|
||||
shrink_val = Alignment(horizontal='left', vertical='center', shrink_to_fit=True)
|
||||
|
||||
line = row
|
||||
for ci in range(1, 14):
|
||||
ws.cell(row=line, column=ci).border = _tb()
|
||||
pairs = [
|
||||
('Vertrag:', _val(project.vertrag), (1, 2, 4)),
|
||||
('LV-Name:', _val(project.lv_name), (5, 6, 9)),
|
||||
('Aufmaß-Datum:', _fmt_date(project.datum), (10, 11, 13)),
|
||||
]
|
||||
for label, val, (ls, vs, ve) in pairs:
|
||||
c = ws.cell(row=line, column=ls, value=label)
|
||||
c.font = label_font; c.fill = header_box_fill; c.alignment = label_align
|
||||
c = ws.cell(row=line, column=vs, value=val)
|
||||
c.font = value_font; c.alignment = shrink_val
|
||||
if ve > vs:
|
||||
ws.merge_cells(start_row=line, start_column=vs, end_row=line, end_column=ve)
|
||||
row += 1
|
||||
|
||||
line = row
|
||||
for ci in range(1, 14):
|
||||
ws.cell(row=line, column=ci).border = _tb()
|
||||
c = ws.cell(row=line, column=1, value='Projekt:')
|
||||
c.font = label_font; c.fill = header_box_fill; c.alignment = label_align
|
||||
c = ws.cell(row=line, column=2, value=_val(project.bezeichnung))
|
||||
c.font = value_font_bold; c.alignment = shrink_val
|
||||
ws.merge_cells(start_row=line, start_column=2, end_row=line, end_column=4)
|
||||
c = ws.cell(row=line, column=5, value='Baustelle:')
|
||||
c.font = label_font; c.fill = header_box_fill; c.alignment = label_align
|
||||
c = ws.cell(row=line, column=6, value=_val(project.baustelle))
|
||||
c.font = value_font; c.alignment = shrink_val
|
||||
ws.merge_cells(start_row=line, start_column=6, end_row=line, end_column=13)
|
||||
row += 1
|
||||
|
||||
line = row
|
||||
for ci in range(1, 14):
|
||||
ws.cell(row=line, column=ci).border = _tb()
|
||||
c = ws.cell(row=line, column=1, value='Typ:')
|
||||
c.font = label_font; c.fill = header_box_fill; c.alignment = label_align
|
||||
c = ws.cell(row=line, column=2, value=_val(aufmass.typ if aufmass else None))
|
||||
c.font = value_font; c.alignment = shrink_val
|
||||
ws.merge_cells(start_row=line, start_column=2, end_row=line, end_column=4)
|
||||
c = ws.cell(row=line, column=5, value='Bauabschnitt:')
|
||||
c.font = label_font; c.fill = header_box_fill; c.alignment = label_align
|
||||
c = ws.cell(row=line, column=6, value=_val(project.bauabschnitt))
|
||||
c.font = value_font; c.alignment = shrink_val
|
||||
ws.merge_cells(start_row=line, start_column=6, end_row=line, end_column=13)
|
||||
row += 1
|
||||
|
||||
ap_name = f'{_val(project.ansprechpartner_vorname)} {_val(project.ansprechpartner_nachname)}'.strip()
|
||||
|
||||
# Row A: SM-Nr + Startdatum + Ansprechpartner Name + Tel
|
||||
line = row
|
||||
for ci in range(1, 14):
|
||||
ws.cell(row=line, column=ci).border = _tb()
|
||||
c = ws.cell(row=line, column=1, value='SM-Nr.:')
|
||||
c.font = label_font; c.fill = header_box_fill; c.alignment = label_align
|
||||
c = ws.cell(row=line, column=2, value=_val(project.sm_nr))
|
||||
c.font = value_font; c.alignment = shrink_val
|
||||
ws.merge_cells(start_row=line, start_column=2, end_row=line, end_column=3)
|
||||
c = ws.cell(row=line, column=4, value='Startdatum:')
|
||||
c.font = label_font; c.fill = header_box_fill; c.alignment = label_align
|
||||
c = ws.cell(row=line, column=5, value=_fmt_date(project.datum_start))
|
||||
c.font = value_font; c.alignment = shrink_val
|
||||
ws.merge_cells(start_row=line, start_column=5, end_row=line, end_column=6)
|
||||
c = ws.cell(row=line, column=7, value='Name:')
|
||||
c.font = label_font; c.fill = header_box_fill; c.alignment = label_align
|
||||
c = ws.cell(row=line, column=8, value=_val(ap_name))
|
||||
c.font = value_font; c.alignment = shrink_val
|
||||
ws.merge_cells(start_row=line, start_column=8, end_row=line, end_column=10)
|
||||
c = ws.cell(row=line, column=11, value='Tel:')
|
||||
c.font = label_font; c.fill = header_box_fill; c.alignment = label_align
|
||||
c = ws.cell(row=line, column=12, value=_val(project.ansprechpartner_tel))
|
||||
c.font = value_font; c.alignment = shrink_val
|
||||
ws.merge_cells(start_row=line, start_column=12, end_row=line, end_column=13)
|
||||
row += 1
|
||||
|
||||
# Row B: Abruf-Nr + Enddatum + Email
|
||||
line = row
|
||||
for ci in range(1, 14):
|
||||
ws.cell(row=line, column=ci).border = _tb()
|
||||
c = ws.cell(row=line, column=1, value='Abruf-Nr.:')
|
||||
c.font = label_font; c.fill = header_box_fill; c.alignment = label_align
|
||||
c = ws.cell(row=line, column=2, value=_val(project.abruf_nr))
|
||||
c.font = value_font; c.alignment = shrink_val
|
||||
ws.merge_cells(start_row=line, start_column=2, end_row=line, end_column=3)
|
||||
c = ws.cell(row=line, column=4, value='Enddatum:')
|
||||
c.font = label_font; c.fill = header_box_fill; c.alignment = label_align
|
||||
c = ws.cell(row=line, column=5, value=_fmt_date(project.datum_ende))
|
||||
c.font = value_font; c.alignment = shrink_val
|
||||
ws.merge_cells(start_row=line, start_column=5, end_row=line, end_column=6)
|
||||
c = ws.cell(row=line, column=7, value='Email:')
|
||||
c.font = label_font; c.fill = header_box_fill; c.alignment = label_align
|
||||
c = ws.cell(row=line, column=8, value=_val(project.ansprechpartner_email))
|
||||
c.font = value_font; c.alignment = shrink_val
|
||||
ws.merge_cells(start_row=line, start_column=8, end_row=line, end_column=13)
|
||||
row += 1
|
||||
|
||||
row += 1
|
||||
|
||||
headers = [
|
||||
'Abschnitt', 'Pos-Nr', 'Faktor', 'Länge', 'Breite', 'Tiefe',
|
||||
'Menge', 'EH', 'Kurztext', 'Bemerkung', 'Menge', 'EP (€)', 'GP (€)'
|
||||
]
|
||||
header_row = row
|
||||
for col, h in enumerate(headers, 1):
|
||||
cell = ws.cell(row=row, column=col, value=h)
|
||||
cell.font = header_font
|
||||
cell.fill = header_fill
|
||||
cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
|
||||
cell.border = _tb()
|
||||
|
||||
data_last_row = header_row + len(positionen)
|
||||
if positionen:
|
||||
ws.auto_filter.ref = f'A{header_row}:M{data_last_row}'
|
||||
|
||||
start_row = row
|
||||
|
||||
pos_counter = 0
|
||||
col_widths = [0.0] * 14
|
||||
for i, pos in enumerate(positionen):
|
||||
row = start_row + 1 + i
|
||||
menge = pos.menge if pos.menge else None
|
||||
menge_hinten = pos.menge_hinten if pos.menge_hinten else None
|
||||
if pos.einheit in ('ST', 'LE', 'STD', 'h', 'Psch'):
|
||||
menge = pos.faktor * 1 if pos.faktor else None
|
||||
l = pos.laenge if pos.laenge else None
|
||||
b = pos.breite if pos.breite else None
|
||||
t = pos.tiefe if pos.tiefe else None
|
||||
ep = pos.einzelpreis if pos.einzelpreis else None
|
||||
gp = pos.gesamtpreis if pos.gesamtpreis else None
|
||||
faktor = pos.faktor if pos.faktor else None
|
||||
|
||||
ist_trenner = _ist_trenner(pos)
|
||||
|
||||
if ist_trenner:
|
||||
ws.merge_cells(start_row=row, start_column=1, end_row=row, end_column=13)
|
||||
for c in range(1, 14):
|
||||
cell = ws.cell(row=row, column=c)
|
||||
cell.fill = trenner_fill
|
||||
cell.border = _tb()
|
||||
continue
|
||||
|
||||
pos_counter += 1
|
||||
values = [
|
||||
_val(pos.abschnitt),
|
||||
pos.pos_nr or None,
|
||||
faktor,
|
||||
l, b, t,
|
||||
menge,
|
||||
pos.einheit or None,
|
||||
_val(pos.kurztext),
|
||||
_val(pos.bemerkung),
|
||||
menge_hinten,
|
||||
ep, gp,
|
||||
]
|
||||
for col, val in enumerate(values, 1):
|
||||
cell = ws.cell(row=row, column=col, value=val) if val is not None else ws.cell(row=row, column=col, value='')
|
||||
cell.font = value_font
|
||||
cell.border = _tb()
|
||||
if val is not None:
|
||||
if col in (7, 11):
|
||||
cell.number_format = '#,##0.00'
|
||||
display = '{:,.2f}'.format(val)
|
||||
elif col in (12, 13):
|
||||
cell.number_format = '#,##0.00'
|
||||
display = '{:,.2f}'.format(val)
|
||||
elif col == 3:
|
||||
display = '{:.2f}'.format(val) if isinstance(val, float) else str(val)
|
||||
elif col in (4, 5, 6):
|
||||
display = '{:.2f}'.format(val) if isinstance(val, float) else str(val)
|
||||
else:
|
||||
display = str(val)
|
||||
else:
|
||||
display = ''
|
||||
col_widths[col] = max(col_widths[col], len(display))
|
||||
|
||||
sum_row = None
|
||||
if positionen:
|
||||
sum_row = start_row + 1 + len(positionen) + 1
|
||||
ws.merge_cells(start_row=sum_row, start_column=1, end_row=sum_row, end_column=11)
|
||||
ws.cell(row=sum_row, column=12, value='Summe:').font = Font(name='Calibri', size=11, bold=True)
|
||||
ws.cell(row=sum_row, column=12).alignment = Alignment(horizontal='right')
|
||||
gesamt = sum(p.gesamtpreis or 0 for p in positionen if not _ist_trenner(p))
|
||||
cell = ws.cell(row=sum_row, column=13, value=gesamt)
|
||||
cell.font = Font(name='Calibri', size=11, bold=True)
|
||||
cell.number_format = '#,##0.00'
|
||||
for c in range(1, 14):
|
||||
ws.cell(row=sum_row, column=c).border = _tb()
|
||||
|
||||
if pos_counter > 0:
|
||||
sum_end = sum_row if sum_row else (start_row + 1 + len(positionen))
|
||||
summary_start = sum_end + 2
|
||||
|
||||
# Title row – merged across all 13 cols
|
||||
for ci in range(1, 14):
|
||||
ws.cell(row=summary_start, column=ci).border = _tb()
|
||||
ws.cell(row=summary_start, column=ci).fill = PatternFill(start_color='D6E4F0', end_color='D6E4F0', fill_type='solid')
|
||||
sc = ws.cell(row=summary_start, column=1, value='Mengen- und Positions-Zusammenfassung')
|
||||
sc.font = Font(name='Calibri', size=12, bold=True, color='2F5496')
|
||||
sc.alignment = Alignment(horizontal='center', vertical='center')
|
||||
ws.merge_cells(start_row=summary_start, start_column=1, end_row=summary_start, end_column=13)
|
||||
|
||||
# Summary header – borders on 13 cols, gap cols 6-10 merged
|
||||
shr = summary_start + 1
|
||||
for ci in range(1, 14):
|
||||
ws.cell(row=shr, column=ci).border = _tb()
|
||||
ws.cell(row=shr, column=ci).fill = header_fill
|
||||
sum_headers = {'Pos-Nr': (1, 1), 'Kurztext': (2, 10), 'Menge': (11, 11), 'EP (€)': (12, 12), 'GP (€)': (13, 13)}
|
||||
for h, (cs, ce) in sum_headers.items():
|
||||
cell = ws.cell(row=shr, column=cs, value=h)
|
||||
cell.font = header_font
|
||||
cell.alignment = Alignment(horizontal='center', vertical='center')
|
||||
if ce > cs:
|
||||
ws.merge_cells(start_row=shr, start_column=cs, end_row=shr, end_column=ce)
|
||||
ws.merge_cells(start_row=shr, start_column=2, end_row=shr, end_column=10)
|
||||
|
||||
groups = defaultdict(lambda: {'kurztext': '', 'menge': 0.0, 'ep': 0.0, 'gp': 0.0})
|
||||
seen_pos = []
|
||||
for pos in positionen:
|
||||
if _ist_trenner(pos) or not pos.pos_nr:
|
||||
continue
|
||||
key = pos.pos_nr
|
||||
if key not in groups:
|
||||
seen_pos.append(key)
|
||||
groups[key]['kurztext'] = pos.kurztext or ''
|
||||
groups[key]['menge'] += pos.menge_hinten if pos.menge_hinten else (pos.menge or 0)
|
||||
groups[key]['ep'] = pos.einzelpreis or 0
|
||||
groups[key]['gp'] += pos.gesamtpreis or 0
|
||||
|
||||
# Summary data rows – borders on 13 cols, gap cols 6-10 merged
|
||||
r = shr + 1
|
||||
for key in seen_pos:
|
||||
g = groups[key]
|
||||
for ci in range(1, 14):
|
||||
ws.cell(row=r, column=ci).border = _tb()
|
||||
vals = [(1, 1, key), (2, 10, g['kurztext']), (11, 11, g['menge']), (12, 12, g['ep']), (13, 13, g['gp'])]
|
||||
for cs, ce, v in vals:
|
||||
cell = ws.cell(row=r, column=cs, value=v)
|
||||
cell.font = value_font
|
||||
if cs >= 11:
|
||||
cell.number_format = '#,##0.00'
|
||||
if ce > cs:
|
||||
ws.merge_cells(start_row=r, start_column=cs, end_row=r, end_column=ce)
|
||||
# No gap merge needed since Kurztext goes to column 10
|
||||
r += 1
|
||||
|
||||
# Summary sum row – borders on 13 cols, gap cols 1-10 merged
|
||||
for ci in range(1, 14):
|
||||
ws.cell(row=r, column=ci).border = _tb()
|
||||
ws.merge_cells(start_row=r, start_column=1, end_row=r, end_column=10)
|
||||
ws.cell(row=r, column=12, value='Summe:').font = Font(name='Calibri', size=10, bold=True)
|
||||
ws.cell(row=r, column=12).alignment = Alignment(horizontal='right')
|
||||
total_gp = sum(g['gp'] for g in groups.values())
|
||||
cell = ws.cell(row=r, column=13, value=total_gp)
|
||||
cell.font = Font(name='Calibri', size=10, bold=True)
|
||||
cell.number_format = '#,##0.00'
|
||||
|
||||
min_widths = [0, 17, 9, 10, 10, 9, 11, 10, 6, 9, 14, 10, 8, 10]
|
||||
max_widths = [0, 17, 15, 10, 12, 12, 12, 14, 8, 55, 45, 14, 14, 16]
|
||||
for i in range(1, 14):
|
||||
raw = col_widths[i]
|
||||
w = max(raw * 1.2 + 1, min_widths[i])
|
||||
w = min(w, max_widths[i])
|
||||
ws.column_dimensions[get_column_letter(i)].width = w
|
||||
|
||||
ws.print_area = f'A1:M{r}'
|
||||
|
||||
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||||
wb.save(output_path)
|
||||
return output_path
|
||||
@@ -0,0 +1,485 @@
|
||||
import re, uuid, io
|
||||
from datetime import datetime
|
||||
|
||||
_g_id_counter = 1000001
|
||||
|
||||
def _next_id():
|
||||
global _g_id_counter
|
||||
rv = _g_id_counter
|
||||
_g_id_counter += 1
|
||||
return rv
|
||||
|
||||
def _xe(s):
|
||||
s = s.replace('&', '&')
|
||||
s = s.replace('<', '<')
|
||||
s = s.replace('>', '>')
|
||||
s = s.replace('"', '"')
|
||||
s = s.replace("'", ''')
|
||||
return s
|
||||
|
||||
def _to_float(s):
|
||||
if not s:
|
||||
return 0.0
|
||||
s = s.strip()
|
||||
while s and s[-1] in (',', '.'):
|
||||
s = s[:-1]
|
||||
s = s.replace(',', '.')
|
||||
try:
|
||||
return float(s)
|
||||
except ValueError:
|
||||
return 0.0
|
||||
|
||||
def _fmt_qty(fval):
|
||||
fabs = abs(fval)
|
||||
ganz = int(fabs)
|
||||
dez = fabs - ganz
|
||||
d3 = int(dez * 1000 + 0.5)
|
||||
if d3 >= 1000:
|
||||
ganz += 1
|
||||
d3 = 0
|
||||
vorz = '-' if fval < 0 else ''
|
||||
if d3 == 0:
|
||||
return f'{vorz}{ganz}'
|
||||
sdez = str(d3).zfill(3).rstrip('0')
|
||||
return f'{vorz}{ganz},{sdez}'
|
||||
|
||||
def _oz_info(oz):
|
||||
result = {'typ': '?', 'e1': '', 'e2': '', 'e3': '', 'pos': oz,
|
||||
'original': oz, 'len_e1': 0, 'len_e2': 0, 'len_e3': 0, 'len_pos': 0}
|
||||
if not oz:
|
||||
return result
|
||||
if re.match(r'^\d{6,10}$', oz):
|
||||
result['typ'] = 'C'
|
||||
result['pos'] = oz
|
||||
result['len_pos'] = len(oz)
|
||||
return result
|
||||
parts = oz.split('.')
|
||||
n = len(parts)
|
||||
if n == 3:
|
||||
result['typ'] = 'A'
|
||||
result['e1'] = parts[0]
|
||||
result['e2'] = parts[1]
|
||||
result['pos'] = parts[2]
|
||||
result['len_e1'] = len(parts[0])
|
||||
result['len_e2'] = len(parts[1])
|
||||
result['len_pos'] = len(parts[2])
|
||||
elif n == 4:
|
||||
result['typ'] = 'B'
|
||||
result['e1'] = parts[0]
|
||||
result['e2'] = parts[1]
|
||||
result['e3'] = parts[2]
|
||||
result['pos'] = parts[3]
|
||||
result['len_e1'] = len(parts[0])
|
||||
result['len_e2'] = len(parts[1])
|
||||
result['len_e3'] = len(parts[2])
|
||||
result['len_pos'] = len(parts[3])
|
||||
return result
|
||||
|
||||
def _is_valid_oz(oz):
|
||||
if not oz:
|
||||
return False
|
||||
if re.match(r'^\d{1,4}\.\d{1,4}\.\d{1,6}$', oz):
|
||||
return True
|
||||
if re.match(r'^\d{1,4}\.\d{1,4}\.\d{1,4}\.\d{1,6}$', oz):
|
||||
return True
|
||||
if re.match(r'^\d{6,10}$', oz):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _reboz_code(idx):
|
||||
blatt = 1000 + idx // 26
|
||||
zeile = chr(65 + idx % 26)
|
||||
return f'{blatt}{zeile}0'
|
||||
|
||||
def _k_zeile(ort, oz_code):
|
||||
ort_pad = (ort + ' ' * 56)[:56]
|
||||
return f' *{ort_pad}{oz_code} '
|
||||
|
||||
def _l_zeile(menge, oz_code):
|
||||
menge_k = menge.strip().replace('.', ',')
|
||||
formel = f'100091{menge_k}='
|
||||
formel = formel[:44].ljust(44)
|
||||
return f' {formel}{oz_code} '
|
||||
|
||||
def _make_qdeterm_pair(ort, qty, oz_cnt):
|
||||
oz1 = _reboz_code(oz_cnt)
|
||||
oz_cnt += 1
|
||||
oz2 = _reboz_code(oz_cnt)
|
||||
oz_cnt += 1
|
||||
out = ''
|
||||
out += '<QDetermItem>'
|
||||
out += f'<QTakeoff Row="{_xe(_k_zeile(ort, oz1))}"/>'
|
||||
out += f'<BVBS:Explanation>{_xe(ort.strip())}</BVBS:Explanation>'
|
||||
out += '</QDetermItem>'
|
||||
out += '<QDetermItem>'
|
||||
sqty = _fmt_qty(qty)
|
||||
out += f'<QTakeoff Row="{_xe(_l_zeile(sqty, oz2))}"/>'
|
||||
out += '</QDetermItem>'
|
||||
return out, oz_cnt
|
||||
|
||||
def _gen_uuid():
|
||||
return str(uuid.uuid4()).lower()
|
||||
|
||||
def _datum_iso(d):
|
||||
if d is None:
|
||||
return ''
|
||||
if isinstance(d, str):
|
||||
d = d[:10]
|
||||
for fmt in ('%Y-%m-%d', '%d.%m.%Y'):
|
||||
try:
|
||||
return datetime.strptime(d, fmt).strftime('%Y-%m-%d')
|
||||
except ValueError:
|
||||
continue
|
||||
return d
|
||||
return d.strftime('%Y-%m-%d')
|
||||
|
||||
def export_to_x31(project, aufmass, positionen):
|
||||
global _g_id_counter
|
||||
_g_id_counter = 1000001
|
||||
|
||||
s_datum = _datum_iso(project.datum) if project.datum else datetime.now().strftime('%Y-%m-%d')
|
||||
s_baustelle = project.baustelle or ''
|
||||
s_bauabs = project.bauabschnitt or ''
|
||||
s_vertrag = project.lv_name or ''
|
||||
ap_vorname = project.ansprechpartner_vorname or ''
|
||||
ap_nachname = project.ansprechpartner_nachname or ''
|
||||
s_askan = f'{ap_vorname} {ap_nachname}'.strip()
|
||||
s_askatel = project.ansprechpartner_tel or ''
|
||||
s_iso_datum = _datum_iso(project.datum) if project.datum else datetime.now().strftime('%Y-%m-%d')
|
||||
s_uid = _gen_uuid()
|
||||
s_boq_uid = _gen_uuid()
|
||||
s_lv_name = s_vertrag or f'{s_baustelle} {s_bauabs}'.strip()
|
||||
s_lv_name_20 = s_lv_name[:20]
|
||||
s_baust50 = s_baustelle[:50]
|
||||
|
||||
pos_data = []
|
||||
for p in positionen:
|
||||
if not p.pos_nr or not _is_valid_oz(p.pos_nr.strip()):
|
||||
continue
|
||||
qty = p.menge_hinten or 0.0
|
||||
pos_data.append({
|
||||
'oz': p.pos_nr.strip(),
|
||||
'qty': qty,
|
||||
'ort': p.abschnitt or '',
|
||||
'beschr': p.kurztext or '',
|
||||
'bemerk': p.bemerkung or '',
|
||||
'einh': p.einheit or '',
|
||||
'ep': p.einzelpreis or 0.0,
|
||||
})
|
||||
|
||||
if not pos_data:
|
||||
return None
|
||||
|
||||
oz_typ = 'A'
|
||||
max_pos_len = 4
|
||||
for pd in pos_data:
|
||||
info = _oz_info(pd['oz'])
|
||||
if info['typ'] == 'B':
|
||||
oz_typ = 'B'
|
||||
if info['typ'] == 'C' and oz_typ == 'A':
|
||||
oz_typ = 'C'
|
||||
if info['len_pos'] > max_pos_len:
|
||||
max_pos_len = info['len_pos']
|
||||
|
||||
now = datetime.now()
|
||||
s_time = now.strftime('%H:%M:%S')
|
||||
|
||||
x = '<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
x += '<!-- REB 23.003 (2009) - X31 Export AutoIt v8 (Dataflor-kompatibel) -->\n'
|
||||
x += '<GAEB xmlns="http://www.gaeb.de/GAEB_DA_XML/DA31/3.3" xmlns:BVBS="BVBS">'
|
||||
x += '<GAEBInfo>'
|
||||
x += '<Version>3.3</Version>'
|
||||
x += '<VersDate>2023-01</VersDate>'
|
||||
x += f'<Date>{s_iso_datum}</Date>'
|
||||
x += f'<Time>{s_time}</Time>'
|
||||
x += '<ProgSystem>AutoIt REB Engine V1.2</ProgSystem>'
|
||||
x += '<ProgName>AutoIt REB X31 Export</ProgName>'
|
||||
x += '</GAEBInfo>'
|
||||
|
||||
x += '<QtyDeterm>'
|
||||
x += '<PrjInfo>'
|
||||
x += f'<RefPrjName>{_xe(s_baust50)}</RefPrjName>'
|
||||
x += f'<RefPrjID>{_xe(s_lv_name_20)}</RefPrjID>'
|
||||
x += '</PrjInfo>'
|
||||
x += f'<QtyDetermInfo ID="{s_uid}">'
|
||||
x += '<MethodDescription>REB23003-2009</MethodDescription>'
|
||||
x += '</QtyDetermInfo>'
|
||||
x += '<DP>31</DP>'
|
||||
x += '<OWN><Address>'
|
||||
x += f'<Name1>{_xe(s_askan)}</Name1>'
|
||||
x += '<Name2></Name2><Name3/><Name4/>'
|
||||
x += '<Street></Street><PCode></PCode><City></City>'
|
||||
x += f'<Contact/><Phone>{_xe(s_askatel)}</Phone><Fax/><Email/>'
|
||||
x += '</Address></OWN>'
|
||||
x += '<CTR><Address><Name1/><Name2></Name2><Name3/><Name4/>'
|
||||
x += '<Street></Street><PCode></PCode><City></City>'
|
||||
x += '<Contact/><Phone/><Fax/><Email/></Address></CTR>'
|
||||
x += f'<BoQ ID="DF_{_next_id()}">'
|
||||
x += f'<RefBoQName>{_xe(s_lv_name_20)}</RefBoQName>'
|
||||
x += f'<RefBoQID>{s_boq_uid}</RefBoQID>'
|
||||
|
||||
if oz_typ == 'A':
|
||||
x += '<BoQBkdn><Type>BoQLevel</Type><LblBoQBkdn>Titel</LblBoQBkdn><Length>2</Length><Num>No</Num></BoQBkdn>'
|
||||
x += '<BoQBkdn><Type>BoQLevel</Type><LblBoQBkdn>Bauteil</LblBoQBkdn><Length>2</Length><Num>No</Num></BoQBkdn>'
|
||||
x += f'<BoQBkdn><Type>Item</Type><LblBoQBkdn>Position</LblBoQBkdn><Length>{max_pos_len}</Length><Num>No</Num></BoQBkdn>'
|
||||
x += '<BoQBkdn><Type>Index</Type><LblBoQBkdn>Index</LblBoQBkdn><Length>1</Length><Num>No</Num></BoQBkdn>'
|
||||
elif oz_typ == 'B':
|
||||
x += '<BoQBkdn><Type>BoQLevel</Type><LblBoQBkdn>Titel</LblBoQBkdn><Length>1</Length><Num>No</Num></BoQBkdn>'
|
||||
x += '<BoQBkdn><Type>BoQLevel</Type><LblBoQBkdn>Bauteil</LblBoQBkdn><Length>1</Length><Num>No</Num></BoQBkdn>'
|
||||
x += '<BoQBkdn><Type>BoQLevel</Type><LblBoQBkdn>Abschnitt</LblBoQBkdn><Length>2</Length><Num>No</Num></BoQBkdn>'
|
||||
x += f'<BoQBkdn><Type>Item</Type><LblBoQBkdn>Position</LblBoQBkdn><Length>{max_pos_len}</Length><Num>No</Num></BoQBkdn>'
|
||||
x += '<BoQBkdn><Type>Index</Type><LblBoQBkdn>Index</LblBoQBkdn><Length>1</Length><Num>No</Num></BoQBkdn>'
|
||||
else:
|
||||
x += '<BoQBkdn><Type>BoQLevel</Type><LblBoQBkdn>Titel</LblBoQBkdn><Length>2</Length><Num>No</Num></BoQBkdn>'
|
||||
x += f'<BoQBkdn><Type>Item</Type><LblBoQBkdn>Position</LblBoQBkdn><Length>{max_pos_len}</Length><Num>No</Num></BoQBkdn>'
|
||||
x += '<BoQBkdn><Type>Index</Type><LblBoQBkdn>Index</LblBoQBkdn><Length>1</Length><Num>No</Num></BoQBkdn>'
|
||||
|
||||
x += '<Ctlg><CtlgID>idDIN276_1993</CtlgID>'
|
||||
x += '<CtlgType>cost group DIN 276-93</CtlgType>'
|
||||
x += '<CtlgName>DIN 276-93</CtlgName></Ctlg>'
|
||||
x += '<BoQBody>'
|
||||
|
||||
oz_cnt = 0
|
||||
oz_order = []
|
||||
seen_keys = set()
|
||||
for pd in pos_data:
|
||||
info = _oz_info(pd['oz'])
|
||||
key = (info['e1'], info['e2'], info['e3'], info['pos'])
|
||||
if key not in seen_keys:
|
||||
seen_keys.add(key)
|
||||
oz_order.append(key)
|
||||
|
||||
cur_e1 = ''
|
||||
cur_e2 = ''
|
||||
cur_e3 = ''
|
||||
b_il = False
|
||||
|
||||
for oi, (s_e1, s_e2, s_e3, s_pos) in enumerate(oz_order):
|
||||
same_e1 = (s_e1 == cur_e1)
|
||||
same_e2 = (s_e2 == cur_e2) and same_e1
|
||||
same_e3 = (s_e3 == cur_e3) and same_e2
|
||||
|
||||
if oz_typ == 'B' and not same_e3 and b_il:
|
||||
x += '</Itemlist></BoQBody>'
|
||||
x += '</BoQCtgy>'
|
||||
b_il = False
|
||||
cur_e3 = ''
|
||||
if not same_e2 and b_il:
|
||||
x += '</Itemlist></BoQBody>'
|
||||
x += '</BoQCtgy>'
|
||||
b_il = False
|
||||
cur_e3 = ''
|
||||
if oz_typ == 'B' and not same_e2 and cur_e2 != '':
|
||||
x += '</BoQBody>'
|
||||
x += '</BoQCtgy>'
|
||||
cur_e2 = ''
|
||||
cur_e3 = ''
|
||||
if not same_e1 and cur_e1 != '':
|
||||
x += '</BoQBody>'
|
||||
x += '</BoQCtgy>'
|
||||
cur_e1 = ''
|
||||
|
||||
if s_e1 != cur_e1:
|
||||
x += f'<BoQCtgy RNoPart="{_xe(s_e1)}" ID="DF_{_next_id()}">'
|
||||
x += '<BoQBody>'
|
||||
cur_e1 = s_e1
|
||||
|
||||
if oz_typ == 'B' and s_e2 != cur_e2:
|
||||
x += f'<BoQCtgy RNoPart="{_xe(s_e2)}" ID="DF_{_next_id()}">'
|
||||
x += '<BoQBody>'
|
||||
cur_e2 = s_e2
|
||||
cur_e3 = ''
|
||||
|
||||
if oz_typ != 'B':
|
||||
cur_e2 = s_e2
|
||||
if oz_typ == 'B':
|
||||
cur_e3 = s_e3
|
||||
|
||||
if not b_il:
|
||||
if oz_typ == 'B':
|
||||
x += f'<BoQCtgy RNoPart="{_xe(cur_e3)}" ID="DF_{_next_id()}">'
|
||||
else:
|
||||
x += f'<BoQCtgy RNoPart="{_xe(cur_e2)}" ID="DF_{_next_id()}">'
|
||||
x += '<BoQBody><Itemlist>'
|
||||
b_il = True
|
||||
|
||||
f_qty_sum = 0.0
|
||||
for pd in pos_data:
|
||||
ick = _oz_info(pd['oz'])
|
||||
if ick['e1'] != s_e1 or ick['e2'] != s_e2:
|
||||
continue
|
||||
if oz_typ == 'B' and ick['e3'] != s_e3:
|
||||
continue
|
||||
if ick['pos'] != s_pos:
|
||||
continue
|
||||
f_qty_sum += pd['qty']
|
||||
|
||||
x += f'<Item ID="DF_{_next_id()}" RNoPart="{_xe(s_pos)}">'
|
||||
x += '<QtyDeterm>'
|
||||
x += f'<Qty>{_fmt_qty(f_qty_sum).replace(",", ".")}</Qty>'
|
||||
|
||||
for pd in pos_data:
|
||||
ian = _oz_info(pd['oz'])
|
||||
if ian['e1'] != s_e1 or ian['e2'] != s_e2:
|
||||
continue
|
||||
if oz_typ == 'B' and ian['e3'] != s_e3:
|
||||
continue
|
||||
if ian['pos'] != s_pos:
|
||||
continue
|
||||
pair, oz_cnt = _make_qdeterm_pair(pd['ort'], pd['qty'], oz_cnt)
|
||||
x += pair
|
||||
|
||||
x += '</QtyDeterm>'
|
||||
x += '</Item>'
|
||||
|
||||
if b_il:
|
||||
x += '</Itemlist></BoQBody>'
|
||||
x += '</BoQCtgy>'
|
||||
if oz_typ == 'B' and cur_e2 != '':
|
||||
x += '</BoQBody>'
|
||||
x += '</BoQCtgy>'
|
||||
if cur_e1 != '':
|
||||
x += '</BoQBody>'
|
||||
x += '</BoQCtgy>'
|
||||
x += '</BoQBody>'
|
||||
x += '</BoQ>'
|
||||
x += '</QtyDeterm>'
|
||||
x += '</GAEB>'
|
||||
|
||||
bom = b'\xef\xbb\xbf'
|
||||
return bom + x.encode('utf-8')
|
||||
|
||||
|
||||
def convert_to_california(xml_bytes, ref_prj_name='', ref_prj_id='', owner_name=''):
|
||||
content = xml_bytes.decode('utf-8')
|
||||
|
||||
if content.startswith('\ufeff'):
|
||||
content = content[1:]
|
||||
|
||||
content = content.replace('\r\n', '\n')
|
||||
content = content.replace('\r', '\n')
|
||||
content = content.replace('\n', '\r\n')
|
||||
|
||||
m = re.search(r'(<Street>)([\s\S]*?)(</Street>)', content)
|
||||
if m:
|
||||
street_alt = m.group(0)
|
||||
street_neu = m.group(1) + m.group(2).replace('\r\n', ' ') + m.group(3)
|
||||
content = content.replace(street_alt, street_neu)
|
||||
|
||||
content = re.sub(r'<!--[\s\S]*?-->', '<!-- REB 23.003 (2009) - X31 Export AutoIt v5 -->', content)
|
||||
|
||||
heute = datetime.now().strftime('%Y-%m-%d')
|
||||
zeit = datetime.now().strftime('%H:%M:%S')
|
||||
|
||||
content = re.sub(r'(<Date>)[^<]*(</Date>)', rf'\g<1>{heute}\g<2>', content)
|
||||
content = re.sub(r'(<Time>)[^<]*(</Time>)', rf'\g<1>{zeit}\g<2>', content)
|
||||
content = re.sub(r'(<ProgSystem>)[^<]*(</ProgSystem>)', r'\g<1>Python REB Engine V1.2\g<2>', content)
|
||||
content = re.sub(r'(<ProgName>)[^<]*(</ProgName>)', r'\g<1>Python REB X31 Export\g<2>', content)
|
||||
|
||||
if ref_prj_name:
|
||||
content = re.sub(r'(<RefPrjName>)[^<]*(</RefPrjName>)', rf'\g<1>{_xe(ref_prj_name)}\g<2>', content)
|
||||
if ref_prj_id:
|
||||
content = re.sub(r'(<RefPrjID>)[^<]*(</RefPrjID>)', rf'\g<1>{_xe(ref_prj_id)}\g<2>', content)
|
||||
|
||||
neue_guid = str(uuid.uuid4()).lower()
|
||||
content = re.sub(
|
||||
r'(QtyDetermInfo\s+ID=")[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}(")',
|
||||
rf'\g<1>{neue_guid}\g<2>',
|
||||
content
|
||||
)
|
||||
|
||||
if owner_name:
|
||||
m_own = re.search(r'(<OWN>[\s\S]*?</OWN>)', content)
|
||||
if m_own:
|
||||
own_alt = m_own.group(1)
|
||||
own_neu = re.sub(r'(<Name1>)[^<]*(</Name1>)', rf'\g<1>{_xe(owner_name)}\g<2>', own_alt)
|
||||
content = content.replace(own_alt, own_neu)
|
||||
|
||||
content = re.sub(r'\s*<CtlgAssignType\s*/>', '', content)
|
||||
content = re.sub(r'\s*<CtlgAssignType>[^<]*</CtlgAssignType>', '', content)
|
||||
content = re.sub(r'<BVBS:Explanation>[^<]*</BVBS:Explanation>', '', content)
|
||||
|
||||
imax = 30
|
||||
while imax > 0:
|
||||
vorher = content
|
||||
content = re.sub(
|
||||
r'<Item\s[^>]+>\s*<QtyDeterm>\s*<Qty>0[,.]000</Qty>\s*</QtyDeterm>\s*</Item>',
|
||||
'',
|
||||
content
|
||||
)
|
||||
if content == vorher:
|
||||
break
|
||||
imax -= 1
|
||||
|
||||
imax = 10
|
||||
while imax > 0:
|
||||
vorher = content
|
||||
content = re.sub(r'<Itemlist>\s*</Itemlist>', '', content)
|
||||
content = re.sub(r'<BoQBody>\s*</BoQBody>', '', content)
|
||||
content = re.sub(r'<BoQCtgy[^>]*>\s*</BoQCtgy>', '', content)
|
||||
if content == vorher:
|
||||
break
|
||||
imax -= 1
|
||||
|
||||
content = _renumber_ids(content)
|
||||
content = _renumber_zeilen_ids(content)
|
||||
content = _format_xml(content)
|
||||
|
||||
bom = b'\xef\xbb\xbf'
|
||||
return bom + content.encode('utf-8')
|
||||
|
||||
|
||||
def _renumber_ids(xml_str):
|
||||
all_ids = re.findall(r'ID="(DF_\d+)"', xml_str)
|
||||
if not all_ids:
|
||||
return xml_str
|
||||
unique = sorted(set(all_ids))
|
||||
next_id = 1000001
|
||||
for old_id in unique:
|
||||
new_id = f'DF_{next_id}'
|
||||
next_id += 1
|
||||
xml_str = xml_str.replace(f'ID="{old_id}"', f'ID="{new_id}"')
|
||||
return xml_str
|
||||
|
||||
|
||||
def _renumber_zeilen_ids(xml_str):
|
||||
blatt = 1
|
||||
pos = 0
|
||||
while True:
|
||||
m = re.search(r'QTakeoff Row="', xml_str[pos:])
|
||||
if not m:
|
||||
break
|
||||
i_start = pos + m.start()
|
||||
i_row_start = i_start + 14
|
||||
i_row_end = xml_str.index('"', i_row_start)
|
||||
row_len = i_row_end - i_row_start
|
||||
if row_len == 80:
|
||||
neue_id = f'{blatt:04d}A0'
|
||||
row_old = xml_str[i_row_start:i_row_end]
|
||||
row_new = row_old[:69] + neue_id + row_old[75:]
|
||||
xml_str = xml_str[:i_row_start] + row_new + xml_str[i_row_end:]
|
||||
blatt += 1
|
||||
pos = i_row_start + 1
|
||||
return xml_str
|
||||
|
||||
|
||||
def _format_xml(xml_str):
|
||||
xml_str = re.sub(r'>\s+<', '>\r\n<', xml_str)
|
||||
xml_str = xml_str.replace('<BoQBody>', '<BoQBody>\r\n')
|
||||
xml_str = xml_str.replace('<Itemlist>', '<Itemlist>\r\n')
|
||||
xml_str = re.sub(r'(<BoQCtgy[^>]*>)', r'\g<1>\r\n', xml_str)
|
||||
xml_str = re.sub(r'(<Item [^>]*>)', r'\g<1>\r\n', xml_str)
|
||||
|
||||
close_tags = ['</BoQBody>', '</Itemlist>', '</BoQCtgy>', '</Item>', '</QtyDeterm>']
|
||||
for tag in close_tags:
|
||||
xml_str = xml_str.replace(tag, '\r\n' + tag + '\r\n')
|
||||
|
||||
xml_str = xml_str.replace('<QtyDeterm>', '\r\n<QtyDeterm>\r\n')
|
||||
|
||||
while '\r\n\r\n\r\n' in xml_str:
|
||||
xml_str = xml_str.replace('\r\n\r\n\r\n', '\r\n\r\n')
|
||||
|
||||
while xml_str.startswith('\r\n'):
|
||||
xml_str = xml_str[2:]
|
||||
|
||||
return xml_str
|
||||
@@ -0,0 +1,92 @@
|
||||
"""Mathematical expression evaluator with precedence (Punkt vor Strich).
|
||||
Supports +, -, *, /, (, ), and decimal numbers. Safe – no eval()."""
|
||||
|
||||
import re
|
||||
from decimal import Decimal, InvalidOperation
|
||||
|
||||
TOKEN_NUM = 'NUM'
|
||||
TOKEN_PLUS = '+'
|
||||
TOKEN_MINUS = '-'
|
||||
TOKEN_MUL = '*'
|
||||
TOKEN_DIV = '/'
|
||||
TOKEN_LPAR = '('
|
||||
TOKEN_RPAR = ')'
|
||||
|
||||
def tokenize(expr):
|
||||
tokens = []
|
||||
i = 0
|
||||
while i < len(expr):
|
||||
c = expr[i]
|
||||
if c in ' \t':
|
||||
i += 1
|
||||
continue
|
||||
if c in '+-*/()':
|
||||
tokens.append(c)
|
||||
i += 1
|
||||
elif c.isdigit() or c == '.':
|
||||
start = i
|
||||
had_dot = False
|
||||
while i < len(expr) and (expr[i].isdigit() or (expr[i] == '.' and not had_dot)):
|
||||
if expr[i] == '.':
|
||||
had_dot = True
|
||||
i += 1
|
||||
tokens.append(('NUM', expr[start:i]))
|
||||
else:
|
||||
raise ValueError(f"Ungültiges Zeichen: '{c}'")
|
||||
return tokens
|
||||
|
||||
def parse_primary(tokens, pos):
|
||||
if pos >= len(tokens):
|
||||
raise ValueError("Erwarte Zahl oder (")
|
||||
tok = tokens[pos]
|
||||
if isinstance(tok, tuple) and tok[0] == 'NUM':
|
||||
return Decimal(tok[1]), pos + 1
|
||||
if tok == '(':
|
||||
val, pos = parse_expr(tokens, pos + 1)
|
||||
if pos >= len(tokens) or tokens[pos] != ')':
|
||||
raise ValueError("Fehlende schließende Klammer")
|
||||
return val, pos + 1
|
||||
raise ValueError(f"Unerwartetes Token: {tok}")
|
||||
|
||||
def parse_mul(tokens, pos):
|
||||
val, pos = parse_primary(tokens, pos)
|
||||
while pos < len(tokens):
|
||||
tok = tokens[pos]
|
||||
if tok in ('*', '/'):
|
||||
right, pos = parse_primary(tokens, pos + 1)
|
||||
if tok == '*':
|
||||
val *= right
|
||||
else:
|
||||
if right == 0:
|
||||
raise ValueError("Division durch Null")
|
||||
val /= right
|
||||
else:
|
||||
break
|
||||
return val, pos
|
||||
|
||||
def parse_expr(tokens, pos):
|
||||
val, pos = parse_mul(tokens, pos)
|
||||
while pos < len(tokens):
|
||||
tok = tokens[pos]
|
||||
if tok in ('+', '-'):
|
||||
right, pos = parse_mul(tokens, pos + 1)
|
||||
if tok == '+':
|
||||
val += right
|
||||
else:
|
||||
val -= right
|
||||
else:
|
||||
break
|
||||
return val, pos
|
||||
|
||||
def berechne_formel(expr_str):
|
||||
if not expr_str or not expr_str.strip():
|
||||
return 0
|
||||
# Deutsche Dezimaltrenner ersetzen
|
||||
expr_str = expr_str.replace(',', '.')
|
||||
tokens = tokenize(expr_str.strip())
|
||||
if not tokens:
|
||||
return 0
|
||||
val, pos = parse_expr(tokens, 0)
|
||||
if pos != len(tokens):
|
||||
raise ValueError("Überflüssige Zeichen am Ende")
|
||||
return float(val)
|
||||
@@ -0,0 +1,90 @@
|
||||
from app.extensions import db
|
||||
from app.models.license import License, LicenseModule
|
||||
from app.models.module import Module
|
||||
|
||||
|
||||
def check_company_module_access(company_id, module_name):
|
||||
"""Prüft, ob eine Firma ein Modul nutzen darf (License + CompanyModule)."""
|
||||
module = Module.query.filter_by(name=module_name).first()
|
||||
if not module:
|
||||
return False
|
||||
if module.standard:
|
||||
return True
|
||||
|
||||
license = License.query.filter_by(company_id=company_id, aktiv=True).first()
|
||||
if license:
|
||||
if LicenseModule.query.filter_by(
|
||||
license_id=license.id, module_id=module.id, aktiv=True
|
||||
).first():
|
||||
return True
|
||||
|
||||
from app.models.company_module import CompanyModule
|
||||
if CompanyModule.query.filter_by(company_id=company_id, module_id=module.id, aktiv=True).first():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def check_module_access(company_id, module_name):
|
||||
"""Prüft, ob eine Firma ein Modul nutzen darf (ohne User-Check)."""
|
||||
return check_company_module_access(company_id, module_name)
|
||||
|
||||
|
||||
def check_user_module_access(user, module_name):
|
||||
"""Prüft, ob ein bestimmter Benutzer ein Modul nutzen darf."""
|
||||
if user.is_superadmin():
|
||||
return True
|
||||
|
||||
module = Module.query.filter_by(name=module_name).first()
|
||||
if not module:
|
||||
return False
|
||||
if module.standard:
|
||||
return True
|
||||
|
||||
if not check_company_module_access(user.company_id, module_name):
|
||||
return False
|
||||
|
||||
if user.is_firmadmin():
|
||||
return True
|
||||
|
||||
from app.models.user_module import UserModulePermission
|
||||
if UserModulePermission.query.filter_by(user_id=user.id, module_id=module.id, aktiv=True).first():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_aktive_module(company_id, user=None):
|
||||
"""Liefert alle Module, die die Firma (bzw. der Benutzer) nutzen darf."""
|
||||
all_modules = Module.query.order_by(Module.sortierung).all()
|
||||
|
||||
license = License.query.filter_by(company_id=company_id, aktiv=True).first()
|
||||
aktiv_module_ids = set()
|
||||
if license:
|
||||
for lm in LicenseModule.query.filter_by(license_id=license.id, aktiv=True).all():
|
||||
aktiv_module_ids.add(lm.module_id)
|
||||
|
||||
from app.models.company_module import CompanyModule
|
||||
company_module_ids = {
|
||||
cm.module_id for cm in CompanyModule.query.filter_by(company_id=company_id, aktiv=True).all()
|
||||
}
|
||||
|
||||
from app.models.user_module import UserModulePermission
|
||||
user_module_ids = set()
|
||||
if user and not user.is_firmadmin():
|
||||
user_module_ids = {
|
||||
um.module_id for um in UserModulePermission.query.filter_by(user_id=user.id, aktiv=True).all()
|
||||
}
|
||||
|
||||
result = []
|
||||
for m in all_modules:
|
||||
if m.standard:
|
||||
result.append(m)
|
||||
elif m.id in aktiv_module_ids:
|
||||
result.append(m)
|
||||
elif m.id in company_module_ids:
|
||||
if user is None or user.is_firmadmin():
|
||||
result.append(m)
|
||||
elif m.id in user_module_ids:
|
||||
result.append(m)
|
||||
return result
|
||||
Reference in New Issue
Block a user