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