from PySide6.QtWidgets import ( QDialog, QVBoxLayout, QTextEdit, QPushButton, QHBoxLayout, QPlainTextEdit ) from PySide6.QtGui import QFont from PySide6.QtCore import Qt import os import builtins import socket def open_printer_from_setup(setup): if setup.printer_type == "tcp": ... #return open_printer_tcp( # setup.printer_host, # setup.printer_port or 9100) def show_receipt_preview(parent, ucet, width: int = 40): dlg = QDialog(parent) dlg.setWindowTitle("Náhled účtenky") dlg.setModal(True) dlg.resize(600, 780) main = QVBoxLayout(dlg) main.setSpacing(10) main.setContentsMargins(10, 10, 10, 10) # ===================================================== # TEXT ÚČTENKY – MONOSPACE (VYNUCENÝ) # ===================================================== receipt = QPlainTextEdit() receipt.setReadOnly(True) receipt.setLineWrapMode(QPlainTextEdit.NoWrap) # 🔒 VYNUCENÝ MONOSPACE – žádné QSS to nepřebije font = QFont() font.setFamily("Consolas") font.setStyleHint(QFont.Monospace) font.setFixedPitch(True) font.setPointSize(11) receipt.setFont(font) # pojistka proti QSS receipt.setStyleSheet(""" QPlainTextEdit { font-family: Consolas, "Courier New", monospace; font-size: 11pt; } """) receipt.setPlainText("\n".join(format_uctu_text(ucet, width))) main.addWidget(receipt, stretch=1) # ===================================================== # SPODNÍ TOUCH PANEL # ===================================================== bottom = QHBoxLayout() bottom.setSpacing(12) BTN_W = 180 BTN_H = 70 btn_print = QPushButton("TISK") btn_print.setEnabled(False) btn_print.setFixedSize(BTN_W, BTN_H) btn_print.setStyleSheet(""" QPushButton { font-size: 18px; font-weight: bold; background: #CCCCCC; color: #666666; } """) btn_close = QPushButton("ZAVŘÍT") btn_close.setFixedSize(BTN_W, BTN_H) btn_close.setStyleSheet(""" QPushButton { font-size: 18px; font-weight: bold; background: #E6B3B3; color: black; } QPushButton:pressed { background: #D99A9A; } """) btn_close.clicked.connect(dlg.accept) bottom.addStretch(1) bottom.addWidget(btn_print) bottom.addWidget(btn_close) main.addLayout(bottom) dlg.exec() def format_uctu_str( ucet, width: int = 40) -> str: return "\n".join(format_uctu_text(ucet, width)) def format_uctu_text(ucet, width: int = 40) -> list[str]: lines: list[str] = [] def line(left: str = "", right: str = ""): #Zarovná left vlevo a right doprava na pevnou šířku if right: space = width - len(left) - len(right) if space < 1: space = 1 lines.append((left + " " * space + right)[:width]) else: lines.append(left[:width]) def sep(ch: str = "-"): lines.append(ch * width) discount = round(getattr(ucet, "discount_abs", 0.0) or 0.0, 2) # ================= HLAVIČKA ================= sep("=") dt = ucet.closed_at or ucet.datetime or "" lines.append(dt.center(width)) cislo = ucet.ucislo or "-" lines.append(f"UCET {cislo}".center(width)) # STORNO OZNAČENÍ if getattr(ucet, "is_storno", None): lines.append(" STORNO ".center(width)) if ucet.autor: lines.append(f"Obsluha: {ucet.autor}".center(width)) sep("=") # ================= POLOŽKY ================= total = 0.0 total_before_discount = 0.0 for p in ucet.poloz: # je to dělená porce? delene = bool(p.delitel and p.delitel != 1) if delene: kusu = p.pocet / p.delitel qty_txt = f"{p.pocet}×1/{p.delitel}" # např. 2×1/2 else: kusu = p.pocet qty_txt = str(p.pocet) cena = round(p.cena * kusu, 2) cena_before = round((p.cena_puv if getattr(p, "cena_puv", None) is not None else p.cena) * kusu, 2) total += cena total_before_discount += cena_before # ====== JEDEN ŘÁDEK – jen NEDĚLENÝ 1 KS ====== if not delene and kusu == 1: left = p.nazev right = f"{cena:.2f}" line(left.ljust(width - len(right)) + right) # ====== DĚLENÉ NEBO VÍCE KS → VŽDY DVA ŘÁDKY ====== else: line(p.nazev[:width]) left = f"{qty_txt} x {p.cena:.2f}" right = f"{cena:.2f}" line(left.ljust(width - len(right)) + right) # ================= SOUČET ================= sep("-") prorated = getattr(ucet, "discounts_prorated", False) payable = total if prorated else total - discount subtotal_for_print = total_before_discount if prorated else total line("MEZISOUČET", f"{subtotal_for_print:.2f} Kč") if discount != 0: if discount > 0: line("SLEVA", f"-{discount:.2f} Kč") else: line("PRIRAZKA", f"{-discount:.2f} Kč") sep("-") line("K ÚHRADĚ", f"{payable:.2f} Kč") else: line("K ÚHRADĚ", f"{payable:.2f} Kč") # ================= DPH ================= sep("-") for d in ucet.dane: # d.rate např. "0.21" _, pct = d.rate.split(".", 1) pct = pct.ljust(2, "0") zaklad = round(d.zaklad, 2) dph_castka = round(d.zaklad * float(d.rate), 2) left = f"DPH {pct} % Zaklad {zaklad:.2f}" right = f"{dph_castka:.2f}" line(left, right) # ================= PLATBY ================= if getattr(ucet, "platby", None): sep("-") for p in ucet.platby: if p.unit != "CZK" and p.rate != 1: left = f"{p.nazev} {p.suma:.2f} {p.unit}" right = f"{p.suma * p.rate:.2f}" else: left = p.nazev right = f"{p.suma:.2f}" line(left, right) sep("=") return lines def normalize_cp852(text: str) -> str: replacements = { "—": "-", "–": "-", "−": "-", "“": '"', "”": '"', "„": '"', "’": "'", "‘": "'", "…": "...", "\u00a0": " ", # NBSP "č": "c", "ě": "e", "š": "s", "ř": "r", "ž": "z", "ý": "y", "á": "a", "í": "i", "é": "e", "ú": "u", "ů": "u", } for k, v in replacements.items(): text = text.replace(k, v) return text def tisk_uctu( ucet, printer, *, width: int = 40, init_cmd: bytes = b"\x1b@", cut_cmd: bytes = b"\x1dV\x00",): # inicializace tiskárny if init_cmd: printer.write(init_cmd) for l in format_uctu_text(ucet, width): safe = normalize_cp852(l) printer.write((safe + "\n").encode("cp852", errors="replace")) # odřez if cut_cmd: printer.write(cut_cmd) class Printer: def __init__(self, stream): self.stream = stream def write(self, data: bytes): self.stream.write(data) def close(self): if hasattr(self.stream, "close"): self.stream.close() class ConsolePrinter(Printer): def write(self, data: bytes): data = data.replace(b"\x1b", b"") print(data.decode("ascii", errors="replace"), end="")