BonPrinter v1.2.0
Thermal Printer tool
Loading...
Searching...
No Matches
printer.py
Go to the documentation of this file.
1"""!
2********************************************************************************
3@file printer.py
4@brief create bon text and send to printer
5********************************************************************************
6"""
7
8import sys
9import os
10import time
11import enum
12import logging
13import queue
14from typing import Any, TYPE_CHECKING
15import re
16import csv
17
18
19import socket
20import serial
21from escpos.printer import Dummy
22
23from Source.version import __title__
24from Source.Util.app_data import EPaper, read_com_port_settings, read_paper_width_settings, \
25 write_com_port_settings, write_paper_width_settings, thread_loop, S_UNIT, get_computer_name, EUser, get_serial_ports, \
26 I_DEFAULT_PAPER_WIDTH
27from Source.Util.gui_toolkit import THREAD, group_available_menu, config_label
28
29from Source.Model.language import L_YES, L_CANCEL
30from Source.Model.report import Item
31
32if TYPE_CHECKING:
33 from Source.Controller.main_window import MainWindow
34
35log = logging.getLogger(__title__)
36
37
38class EPrinter(str, enum.Enum):
39 """!
40 @brief Supported printers
41 """
42 EPSON = "EPSON"
43 PROLIFIC = "Prolific"
44 DEFAULT = "Default"
45
46
47D_PRINTER = {
48 EPrinter.EPSON: {EPaper.WIDTH_58_MM.name: 34,
49 EPaper.WIDTH_80_MM.name: 48},
50 EPrinter.PROLIFIC: {EPaper.WIDTH_58_MM.name: 30,
51 EPaper.WIDTH_80_MM.name: 44},
52 EPrinter.DEFAULT: {EPaper.WIDTH_58_MM.name: 30, # max length of 58 mm paper
53 EPaper.WIDTH_80_MM.name: 44} # auto line break or max length of 80mm paper
54}
55
56F_PRINTER_SLEEP_TIME = 0.2
57F_PRINTER_PRINT_DELAY = 0.75 # delay for every bon to set to prevent buffer overflow of printer
58S_PRINT_ERROR_FILE = "FailedPrints.csv"
59
60
61def is_ip_address(s: str) -> bool:
62 """!
63 @brief Checks if a string is a network address.
64 @param s : string to check
65 @return status if is ip address
66 """
67 return re.fullmatch(r'(\d{1,3}\.){3}\d{1,3}(:\d+)?', s) is not None # IPv4 with optional port
68
69
70class Printer(THREAD):
71 """!
72 @brief Class create bons and printout.
73 @param ui : main window object
74 """
75
76 def __init__(self, ui: "MainWindow") -> None:
77 super().__init__()
78 self.ui = ui
79 self.b_open_drawer = False
80 self.i_line_break = I_DEFAULT_PAPER_WIDTH
81 self.l_available_ports: list[str] = []
82 self.l_available_port_names: list[str] = []
83 self.s_header1 = ""
84 self.s_header2 = ""
85 self.bon_queue: queue.Queue[Item] = queue.Queue()
86 self.report_queue: queue.Queue[str] = queue.Queue()
88 self.e_printer = EPrinter.DEFAULT
89 self.s_select_com_port = read_com_port_settings()
90 self.i_paper_width = read_paper_width_settings()
91 self.init_com_port()
92
93 def set_header(self, s_header1: str = "", s_header2: str = "") -> None:
94 """!
95 @brief Set bon header
96 @param s_header1 : first line in bon header
97 @param s_header2 : second line in bon header
98 """
99 self.s_header1 = s_header1
100 self.s_header2 = s_header2
101
102 def init_com_port(self) -> None:
103 """!
104 @brief Initialize COM Port.
105 """
106 l_present_ports = get_serial_ports()
107
108 self.b_select_com_port_available = False
109 self.l_available_ports = []
110 for port, name, _ in l_present_ports:
111 log.debug("Present Port: %s (%s)", port, name)
112 b_com_port_available = True
113 if port == self.s_select_com_port:
114 if b_com_port_available:
116 for printer, _value in D_PRINTER.items():
117 if name.startswith(printer.name):
118 self.e_printer = printer
119 if b_com_port_available:
120 self.l_available_ports.append(port)
121 self.l_available_port_names.append(name)
122 log.debug("Available Port: %s (%s)", port, name)
123
125
126 if (self.s_select_com_port is not None) and (not self.b_select_com_port_available):
127 self.ui.set_status([f"Select COM Port not available: {self.s_select_com_port}",
128 f"Ausgewählter COM Port nicht verfügbar: {self.s_select_com_port}"], True)
129 else:
130 self.update_paper_width(self.i_paper_width, False) # call after set self.e_printer
131
132 def update_com_port_menu(self) -> None:
133 """!
134 @brief Update COM port menu
135 """
136 self.ui.ag_port, all_actions_add = group_available_menu(self.ui, self.ui.l_action_com_port, self.s_select_com_port, self.l_available_ports, self.ui.action_none)
137 if not all_actions_add:
138 self.ui.set_status(["Too much COM Ports available", "Zu viele COM Ports verfügbar"], True)
139
140 def update_com_port(self, s_com_port: str) -> None:
141 """!
142 @brief Update COM Port of printer
143 @param s_com_port : selected COM port
144 """
145 if not (not self.bon_queue.empty() or not self.report_queue.empty() or self.b_open_drawer):
146 b_update_port = True
147
148 if b_update_port:
149 self.s_select_com_port = s_com_port
150 write_com_port_settings(self.s_select_com_port)
151 if s_com_port is None:
152 self.ui.set_status(["Printer disabled", "Drucker deaktiviert"])
153 else:
154 self.ui.set_status([f"Select Printer: {s_com_port}", f"Ausgewählter Drucker: {s_com_port}"])
155
156 self.init_com_port()
157 else:
158 pass
159
160 else:
161 self.ui.set_status(["Can not change COM port. Print queue is not empty.",
162 "COM Port kann nicht geändert werden. Es befinden sich Ausdrucke in der Warteschlange."], True)
164
165 def update_paper_width(self, i_paper_width: int, b_statusbar_info: bool = True) -> None:
166 """!
167 @brief Update paper width.
168 @param i_paper_width : paper width in "mm"
169 @param b_statusbar_info : [True] show update info on status bar; [False] show not
170 """
171 self.i_paper_width = i_paper_width
172 if i_paper_width == EPaper.WIDTH_80_MM.value:
173 self.i_line_break = D_PRINTER[self.e_printer][EPaper.WIDTH_80_MM.name]
174 else:
175 self.i_line_break = D_PRINTER[self.e_printer][EPaper.WIDTH_58_MM.name]
176 log.debug("Set line Break to %s", self.i_line_break)
177 write_paper_width_settings(self.i_paper_width)
178 if b_statusbar_info:
179 self.ui.set_status([f"Select Paper width: {i_paper_width} mm",
180 f"Gewählte Papierbreite: {i_paper_width} mm"])
181
182 def add_to_queue(self, l_items: list[Item], i_user_pos: int, f_total_price: float) -> None:
183 """!
184 @brief Add item to bon queue.
185 @param l_items : list of items to add
186 @param i_user_pos : user position
187 @param f_total_price : total price
188 """
189 s_user = l_items[0].user
190 b_save_report = True
191 if self.s_select_com_port is not None:
193 b_print_article = self.ui.model.c_config.get_print_article_status()
194 for item in l_items:
195 b_print_actual_article = self.ui.model.c_config.get_item_print_status(item.pos) if b_print_article else False
196 b_print = b_print_actual_article
197 if b_print:
198 self.bon_queue.put(item)
199 self.ui.set_status([f"Print Articles for user {s_user}",
200 f"Artikel werden gedruckt für Benutzer {s_user}"])
201 else:
202 b_save_report = False
203 self.ui.set_status([f"{self.s_select_com_port} will print and is not available",
204 f"{self.s_select_com_port} ist zum Drucken nicht verfügbar"], True)
205 else:
206 self.ui.set_status([f"Articles saved to user {s_user}",
207 f"Artikel gesichert für Benutzer {s_user}"])
208 if b_save_report:
209 # TODO other printer for kitchen printer and for print_article possible
210 self.ui.model.c_report.write_data_to_print_file(l_items, i_user_pos, f_total_price, self.s_select_com_port)
211
212 def add_to_report(self, s_text: str) -> None:
213 """!
214 @brief Add text to report queue.
215 @param s_text : text to print out
216 """
217 if self.s_select_com_port is not None:
218 self.report_queue.put(s_text)
219
220 def open_drawer(self) -> None:
221 """!
222 @brief Open drawer trigger
223 """
224 if self.s_select_com_port is not None:
225 self.b_open_drawer = True
226
227 def network_print(self, network_printer_ip: str, print_content: bytes) -> bool:
228 """!
229 @brief Network print
230 @param network_printer_ip : IP address of printer
231 @param print_content : content to print
232 @return success status
233 """
234 b_success = False
235 try:
236 with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
237 s.settimeout(5)
238 s.connect((network_printer_ip, 9100))
239 s.sendall(print_content)
240 b_success = True
241 except socket.timeout:
242 self.ui.set_status(["Printout to network printer failed", "Ausdruck auf Netzwerkdrucker fehlgeschlagen"], True, b_thread=True)
243 b_success = False
244 except Exception:
245 self.ui.set_status(["Printout to network printer failed - Error unknown", "Ausdruck auf Netzwerkdrucker fehlgeschlagen - Fehler unbekannt"], True, b_thread=True)
246 b_success = False
247 return b_success
248
249 def loop(self) -> None:
250 """!
251 @brief Printer loop to check for items to print out.
252 """
253 s_actual_used_com_port = None
254 c_serial_port = None
255 while True:
256 if not self.bon_queue.empty() or not self.report_queue.empty() or self.b_open_drawer:
257 if self.b_select_com_port_available and (s_actual_used_com_port != self.s_select_com_port):
258 if self.s_select_com_port is not None:
259 if (c_serial_port is not None) and c_serial_port.isOpen():
260 c_serial_port.close()
261 s_actual_used_com_port = self.s_select_com_port
262 c_serial_port = serial.Serial(self.s_select_com_port)
263 if c_serial_port is not None:
264 if not c_serial_port.isOpen():
265 c_serial_port.open()
266 self.check_open_drawer(c_serial_port)
267 if not self.bon_queue.empty():
268 item = self.bon_queue.get()
269 network_printer_ip = None
270 bon = self.create_bon(item)
271 log.debug("Create Bon: %s", item)
272 for _ in range(item.amount):
273 if network_printer_ip:
274 b_success = self.network_print(network_printer_ip, bon)
275 if not b_success:
276 # write failed network prints to separate log file
277 with open(S_PRINT_ERROR_FILE, mode="a+", encoding="utf-8", newline="") as file:
278 writer = csv.writer(file, delimiter=";")
279 price = float(item.price_total) / item.amount
280 s_price = f"{price:.2f}"
281 l_item = [1, item.name, item.pos, item.group, s_price, item.user, item.date, network_printer_ip]
282 writer.writerow(l_item)
283 else:
284 c_serial_port.write(bon)
285 log.debug("Print Bon")
286 time.sleep(F_PRINTER_PRINT_DELAY)
287 b_check_open_drawer = True
288 if b_check_open_drawer:
289 self.check_open_drawer(c_serial_port) # open after print same items is possible
290 elif not self.report_queue.empty():
291 report = self.report_queue.get()
292 c_serial_port.write(self.create_report_bon(report))
293 else:
294 self.ui.set_status("None printer will print. Disable printer and clear queue.", True, b_thread=True) # state not possible
295 self.s_select_com_port = None
296 self.bon_queue = queue.Queue()
297 self.report_queue = queue.Queue()
298 self.b_open_drawer = False
299 else:
300 if (c_serial_port is not None) and c_serial_port.isOpen():
301 c_serial_port.close()
302 time.sleep(F_PRINTER_SLEEP_TIME)
303
304 def run(self) -> None:
305 """!
306 @brief Printer Thread to check for items to print out.
307 """
308 thread_loop(self, "Printer")
309
310 def create_bon(self, item: Item) -> Any:
311 """!
312 @brief Create bon code for printer.
313 @param item : item to create bon
314 @return return bon code for printer
315 """
316 c_dummy = Dummy()
317 s_pay = f"{S_UNIT} {item.price}".rjust(self.i_line_break)
318 s_info = f"{item.date} {item.user}"[:self.i_line_break]
319 s_info = s_info[:self.i_line_break]
320 s_product = f"{item.name}"[:int(self.i_line_break / 2)] # edit in btn_article_preview too if line break changed
321 c_dummy.set_with_default()
322 if self.s_header1:
323 s_header1 = self.s_header1.center(self.i_line_break)[:self.i_line_break]
324 c_dummy.text(f"{s_header1}\n")
325 if self.s_header2:
326 s_header2 = self.s_header2.center(self.i_line_break)[:self.i_line_break]
327 c_dummy.text(f"{s_header2}\n")
328 c_dummy.text(f"{s_info}\n\n")
329 c_dummy.set(double_width=True, double_height=True)
330 c_dummy.text(f"{s_product}")
331 c_dummy.set_with_default()
332 if (item.user != EUser.FREE) or self.ui.model.c_config.get_print_free_price_status():
333 c_dummy.text(f"\n{s_pay}")
334 if self.e_printer != EPrinter.EPSON: # EPSON write empty lines at end as default
335 c_dummy.text("\n")
336 c_dummy.cut(mode="PART")
337 return c_dummy.output
338
339 def create_report_bon(self, s_text: str) -> Any:
340 """!
341 @brief Create report text for printer
342 @param s_text : text
343 @return return report code for printer
344 """
345 c_dummy = Dummy()
346 c_dummy.set_with_default()
347 c_dummy.text(s_text)
348 c_dummy.cut(mode="PART")
349 return c_dummy.output
350
351 def create_open_drawer(self) -> Any:
352 """!
353 @brief Create open drawer code for printer
354 @return return open drawer code for printer
355 """
356 c_dummy = Dummy()
357 c_dummy.cashdraw(pin=2) # first connector = pin2 ; second connector = pin5
358 return c_dummy.output
359
360 def check_open_drawer(self, serial_port: serial.Serial) -> None:
361 """!
362 @brief Open drawer if required
363 @param serial_port : serial port
364 """
365 if self.b_open_drawer:
366 log.debug("Open Cash Drawer")
367 serial_port.write(self.create_open_drawer())
368 self.b_open_drawer = False
Supported printers.
Definition printer.py:38
Class create bons and printout.
Definition printer.py:70
None add_to_report(self, str s_text)
Add text to report queue.
Definition printer.py:212
None check_open_drawer(self, serial.Serial serial_port)
Open drawer if required.
Definition printer.py:360
Any create_bon(self, Item item)
Create bon code for printer.
Definition printer.py:310
None update_paper_width(self, int i_paper_width, bool b_statusbar_info=True)
Update paper width.
Definition printer.py:165
Any create_report_bon(self, str s_text)
Create report text for printer.
Definition printer.py:339
Any create_open_drawer(self)
Create open drawer code for printer.
Definition printer.py:351
None run(self)
Printer Thread to check for items to print out.
Definition printer.py:304
None init_com_port(self)
Initialize COM Port.
Definition printer.py:102
None loop(self)
Printer loop to check for items to print out.
Definition printer.py:249
queue.Queue[Item] bon_queue
Definition printer.py:85
queue.Queue[str] report_queue
Definition printer.py:86
None set_header(self, str s_header1="", str s_header2="")
Set bon header.
Definition printer.py:93
bool network_print(self, str network_printer_ip, bytes print_content)
Network print.
Definition printer.py:227
None __init__(self, "MainWindow" ui)
Definition printer.py:76
None open_drawer(self)
Open drawer trigger.
Definition printer.py:220
None update_com_port(self, str s_com_port)
Update COM Port of printer.
Definition printer.py:140
None update_com_port_menu(self)
Update COM port menu.
Definition printer.py:132
None add_to_queue(self, list[Item] l_items, int i_user_pos, float f_total_price)
Add item to bon queue.
Definition printer.py:182
bool is_ip_address(str s)
Checks if a string is a network address.
Definition printer.py:61