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 += '' out += f'' out += f'{_xe(ort.strip())}' out += '' out += '' sqty = _fmt_qty(qty) out += f'' out += '' 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 = '\n' x += '\n' x += '' x += '' x += '3.3' x += '2023-01' x += f'{s_iso_datum}' x += f'' x += 'AutoIt REB Engine V1.2' x += 'AutoIt REB X31 Export' x += '' x += '' x += '' x += f'{_xe(s_baust50)}' x += f'{_xe(s_lv_name_20)}' x += '' x += f'' x += 'REB23003-2009' x += '' x += '31' x += '
' x += f'{_xe(s_askan)}' x += '' x += '' x += f'{_xe(s_askatel)}' x += '
' x += '
' x += '' x += '
' x += f'' x += f'{_xe(s_lv_name_20)}' x += f'{s_boq_uid}' if oz_typ == 'A': x += 'BoQLevelTitel2No' x += 'BoQLevelBauteil2No' x += f'ItemPosition{max_pos_len}No' x += 'IndexIndex1No' elif oz_typ == 'B': x += 'BoQLevelTitel1No' x += 'BoQLevelBauteil1No' x += 'BoQLevelAbschnitt2No' x += f'ItemPosition{max_pos_len}No' x += 'IndexIndex1No' else: x += 'BoQLevelTitel2No' x += f'ItemPosition{max_pos_len}No' x += 'IndexIndex1No' x += 'idDIN276_1993' x += 'cost group DIN 276-93' x += 'DIN 276-93' x += '' 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 += '' x += '' b_il = False cur_e3 = '' if not same_e2 and b_il: x += '' x += '' b_il = False cur_e3 = '' if oz_typ == 'B' and not same_e2 and cur_e2 != '': x += '' x += '' cur_e2 = '' cur_e3 = '' if not same_e1 and cur_e1 != '': x += '' x += '' cur_e1 = '' if s_e1 != cur_e1: x += f'' x += '' cur_e1 = s_e1 if oz_typ == 'B' and s_e2 != cur_e2: x += f'' x += '' 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'' else: x += f'' x += '' 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'' x += '' x += f'{_fmt_qty(f_qty_sum).replace(",", ".")}' 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 += '' x += '' if b_il: x += '' x += '' if oz_typ == 'B' and cur_e2 != '': x += '' x += '' if cur_e1 != '': x += '' x += '' x += '' x += '' x += '
' x += '
' 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'()([\s\S]*?)()', 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'', '', content) heute = datetime.now().strftime('%Y-%m-%d') zeit = datetime.now().strftime('%H:%M:%S') content = re.sub(r'()[^<]*()', rf'\g<1>{heute}\g<2>', content) content = re.sub(r'()', rf'\g<1>{zeit}\g<2>', content) content = re.sub(r'()[^<]*()', r'\g<1>Python REB Engine V1.2\g<2>', content) content = re.sub(r'()[^<]*()', r'\g<1>Python REB X31 Export\g<2>', content) if ref_prj_name: content = re.sub(r'()[^<]*()', rf'\g<1>{_xe(ref_prj_name)}\g<2>', content) if ref_prj_id: content = re.sub(r'()[^<]*()', 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'([\s\S]*?)', content) if m_own: own_alt = m_own.group(1) own_neu = re.sub(r'()[^<]*()', rf'\g<1>{_xe(owner_name)}\g<2>', own_alt) content = content.replace(own_alt, own_neu) content = re.sub(r'\s*', '', content) content = re.sub(r'\s*[^<]*', '', content) content = re.sub(r'[^<]*', '', content) imax = 30 while imax > 0: vorher = content content = re.sub( r']+>\s*\s*0[,.]000\s*\s*', '', content ) if content == vorher: break imax -= 1 imax = 10 while imax > 0: vorher = content content = re.sub(r'\s*', '', content) content = re.sub(r'\s*', '', content) content = re.sub(r']*>\s*', '', 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('', '\r\n') xml_str = xml_str.replace('', '\r\n') xml_str = re.sub(r'(]*>)', r'\g<1>\r\n', xml_str) xml_str = re.sub(r'(]*>)', r'\g<1>\r\n', xml_str) close_tags = ['', '', '', '', ''] for tag in close_tags: xml_str = xml_str.replace(tag, '\r\n' + tag + '\r\n') xml_str = xml_str.replace('', '\r\n\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