pharmacy-pos-qr-system/backend/samples/pos_dummy_gui.py
시골약사 e499e19342 feat: 더미 POS GUI 및 영수증 프린터 설정 추가
- 바코드 스캔 → 제품 조회 → 장바구니 → 결제 흐름의 더미 POS GUI 추가
- ESC/POS 영수증 프린터 설정 다이얼로그 추가
- barcode_reader_gui.py dbsetup import 경로 수정
- POS 프린터 config.json 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 15:02:48 +09:00

714 lines
28 KiB
Python

"""
더미 POS 시스템 GUI (PyQt5)
바코드 스캐너로 제품을 추가하고 수량 조절, 할인 적용, 결제까지 지원
"""
import sys
import os
import serial
import serial.tools.list_ports
from datetime import datetime
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QPushButton, QLabel, QGroupBox, QComboBox, QSpinBox,
QTableWidget, QTableWidgetItem, QHeaderView, QFrame,
QLineEdit, QDialog, QFormLayout, QDoubleSpinBox, QMessageBox,
QAbstractItemView, QCheckBox, QSplitter
)
from PyQt5.QtCore import QThread, pyqtSignal, Qt, QTimer
from PyQt5.QtGui import QFont, QColor, QBrush, QIcon
from sqlalchemy import text
# DB 연결
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'db'))
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from dbsetup import DatabaseManager
# ─── GS1 바코드 파싱 ───────────────────────────────────────────
def parse_gs1_barcode(barcode):
candidates = [barcode]
if barcode.startswith('01') and len(barcode) >= 16:
gtin14 = barcode[2:16]
candidates.append(gtin14)
if gtin14.startswith('0'):
candidates.append(gtin14[1:])
elif barcode.startswith('01') and len(barcode) == 15:
candidates.append(barcode[2:15])
return candidates
def search_drug_by_barcode(barcode):
try:
db_manager = DatabaseManager()
engine = db_manager.get_engine('PM_DRUG')
query = text('''
SELECT TOP 1
BARCODE, GoodsName, DrugCode, SplName,
Price, Saleprice, SUNG_CODE, IsUSE
FROM CD_GOODS
WHERE BARCODE = :barcode
AND (GoodsName NOT LIKE N'%(판매중지)%'
AND GoodsName NOT LIKE N'%(판매중단)%')
ORDER BY
CASE WHEN IsUSE = '1' THEN 0 ELSE 1 END,
CASE WHEN Price > 0 THEN 0 ELSE 1 END,
CASE WHEN SplName IS NOT NULL AND SplName != '' THEN 0 ELSE 1 END,
DrugCode DESC
''')
candidates = parse_gs1_barcode(barcode)
with engine.connect() as conn:
for candidate in candidates:
result = conn.execute(query, {"barcode": candidate})
row = result.fetchone()
if row:
return {
'barcode': row.BARCODE,
'goods_name': row.GoodsName,
'drug_code': row.DrugCode,
'manufacturer': row.SplName or '',
'price': float(row.Price) if row.Price else 0,
'sale_price': float(row.Saleprice) if row.Saleprice else 0,
'sung_code': row.SUNG_CODE or ''
}
return None
except Exception as e:
print(f'[오류] 약품 조회 실패: {e}')
return None
# ─── 바코드 리더 스레드 ────────────────────────────────────────
class BarcodeReaderThread(QThread):
barcode_received = pyqtSignal(str)
connection_status = pyqtSignal(bool, str)
def __init__(self, port='COM3', baudrate=115200):
super().__init__()
self.port = port
self.baudrate = baudrate
self.running = False
self.serial_connection = None
def run(self):
self.running = True
try:
self.serial_connection = serial.Serial(
port=self.port, baudrate=self.baudrate,
bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE, timeout=1
)
self.connection_status.emit(True, f'{self.port} 연결됨 ({self.baudrate} bps)')
while self.running:
if self.serial_connection.in_waiting > 0:
data = self.serial_connection.read(self.serial_connection.in_waiting)
try:
text_data = data.decode('utf-8')
except UnicodeDecodeError:
text_data = data.decode('ascii', errors='ignore')
for line in text_data.strip().split('\n'):
barcode = line.strip()
if barcode and len(barcode) in [13, 15, 16]:
self.barcode_received.emit(barcode)
except serial.SerialException as e:
self.connection_status.emit(False, f'연결 실패: {e}')
except Exception as e:
self.connection_status.emit(False, f'오류: {e}')
finally:
if self.serial_connection and self.serial_connection.is_open:
self.serial_connection.close()
def stop(self):
self.running = False
if self.serial_connection and self.serial_connection.is_open:
self.serial_connection.close()
class DrugSearchThread(QThread):
search_complete = pyqtSignal(str, object)
def __init__(self, barcode):
super().__init__()
self.barcode = barcode
def run(self):
info = search_drug_by_barcode(self.barcode)
self.search_complete.emit(self.barcode, info)
# ─── 할인 다이얼로그 ──────────────────────────────────────────
class DiscountDialog(QDialog):
def __init__(self, item_name, current_price, parent=None):
super().__init__(parent)
self.setWindowTitle(f'할인 적용 - {item_name}')
self.setMinimumWidth(350)
self.result_discount = 0
layout = QVBoxLayout()
info = QLabel(f'제품: {item_name}\n판매가: {current_price:,.0f}')
info.setStyleSheet('font-size: 14px; padding: 10px;')
layout.addWidget(info)
form = QFormLayout()
self.discount_type = QComboBox()
self.discount_type.addItems(['금액 할인 (원)', '비율 할인 (%)'])
form.addRow('할인 방식:', self.discount_type)
self.discount_value = QDoubleSpinBox()
self.discount_value.setMaximum(999999)
self.discount_value.setDecimals(0)
form.addRow('할인값:', self.discount_value)
layout.addLayout(form)
self.preview_label = QLabel('')
self.preview_label.setStyleSheet('font-size: 13px; color: #E53935; padding: 10px; font-weight: bold;')
layout.addWidget(self.preview_label)
self.discount_value.valueChanged.connect(
lambda: self._update_preview(current_price))
self.discount_type.currentIndexChanged.connect(
lambda: self._update_preview(current_price))
btn_layout = QHBoxLayout()
ok_btn = QPushButton('적용')
ok_btn.setStyleSheet('background: #4CAF50; color: white; font-weight: bold; padding: 8px 24px;')
ok_btn.clicked.connect(lambda: self._apply(current_price))
cancel_btn = QPushButton('취소')
cancel_btn.setStyleSheet('padding: 8px 24px;')
cancel_btn.clicked.connect(self.reject)
btn_layout.addWidget(cancel_btn)
btn_layout.addWidget(ok_btn)
layout.addLayout(btn_layout)
self.setLayout(layout)
def _update_preview(self, price):
val = self.discount_value.value()
if self.discount_type.currentIndex() == 0:
disc = val
else:
disc = price * val / 100
final = max(0, price - disc)
self.preview_label.setText(f'할인: -{disc:,.0f}원 → 최종가: {final:,.0f}')
def _apply(self, price):
val = self.discount_value.value()
if self.discount_type.currentIndex() == 0:
self.result_discount = val
else:
self.result_discount = price * val / 100
self.accept()
# ─── 메인 POS GUI ─────────────────────────────────────────────
class POSDummyGUI(QMainWindow):
def __init__(self):
super().__init__()
self.reader_thread = None
self.search_threads = []
self.cart_items = [] # [{barcode, goods_name, manufacturer, price, sale_price, qty, discount}]
self.init_ui()
def init_ui(self):
self.setWindowTitle('청춘약국 POS')
self.setGeometry(50, 50, 1200, 800)
self.setStyleSheet('''
QMainWindow { background: #F5F5F5; }
QGroupBox {
font-weight: bold; font-size: 13px;
border: 1px solid #E0E0E0; border-radius: 6px;
margin-top: 12px; padding-top: 18px;
background: white;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 12px; padding: 0 6px;
}
''')
central = QWidget()
self.setCentralWidget(central)
root_layout = QVBoxLayout()
root_layout.setContentsMargins(12, 8, 12, 8)
central.setLayout(root_layout)
# ── 상단: 연결 설정 ──
conn_group = QGroupBox('스캐너 연결')
conn_layout = QHBoxLayout()
conn_group.setLayout(conn_layout)
conn_layout.addWidget(QLabel('포트:'))
self.port_combo = QComboBox()
self.port_combo.setMinimumWidth(200)
self._refresh_ports()
conn_layout.addWidget(self.port_combo)
refresh_btn = QPushButton('')
refresh_btn.setFixedWidth(36)
refresh_btn.clicked.connect(self._refresh_ports)
conn_layout.addWidget(refresh_btn)
conn_layout.addWidget(QLabel('속도:'))
self.baudrate_spin = QSpinBox()
self.baudrate_spin.setRange(9600, 921600)
self.baudrate_spin.setValue(115200)
self.baudrate_spin.setSingleStep(9600)
conn_layout.addWidget(self.baudrate_spin)
self.connect_btn = QPushButton('연결')
self.connect_btn.setStyleSheet(
'background: #4CAF50; color: white; font-weight: bold; padding: 6px 20px; border-radius: 4px;')
self.connect_btn.clicked.connect(self._toggle_connection)
conn_layout.addWidget(self.connect_btn)
self.status_label = QLabel('대기 중')
self.status_label.setStyleSheet('color: #9E9E9E; font-size: 13px; margin-left: 12px;')
conn_layout.addWidget(self.status_label)
conn_layout.addStretch()
# 수동 바코드 입력
conn_layout.addWidget(QLabel('수동입력:'))
self.manual_input = QLineEdit()
self.manual_input.setPlaceholderText('바코드 번호 입력 후 Enter')
self.manual_input.setMinimumWidth(180)
self.manual_input.returnPressed.connect(self._manual_barcode)
conn_layout.addWidget(self.manual_input)
root_layout.addWidget(conn_group)
# ── 중앙: 장바구니 테이블 + 우측 요약 ──
splitter = QSplitter(Qt.Horizontal)
# 장바구니 테이블
cart_group = QGroupBox('장바구니')
cart_layout = QVBoxLayout()
cart_group.setLayout(cart_layout)
self.cart_table = QTableWidget()
self.cart_table.setColumnCount(8)
self.cart_table.setHorizontalHeaderLabels([
'제품명', '제조사', '바코드', '입고가', '판매가', '수량', '할인', '소계'
])
header = self.cart_table.horizontalHeader()
header.setSectionResizeMode(0, QHeaderView.Stretch)
for i in [1]:
header.setSectionResizeMode(i, QHeaderView.ResizeToContents)
for i in [2, 3, 4, 5, 6, 7]:
header.setSectionResizeMode(i, QHeaderView.ResizeToContents)
self.cart_table.setSelectionBehavior(QAbstractItemView.SelectRows)
self.cart_table.setAlternatingRowColors(True)
self.cart_table.setStyleSheet('''
QTableWidget {
font-size: 13px; gridline-color: #E0E0E0;
alternate-background-color: #FAFAFA;
}
QHeaderView::section {
background: #37474F; color: white;
font-weight: bold; font-size: 12px;
padding: 6px; border: none;
}
''')
self.cart_table.verticalHeader().setVisible(False)
cart_layout.addWidget(self.cart_table)
# 장바구니 아래 버튼들
cart_btn_layout = QHBoxLayout()
qty_up_btn = QPushButton('+1')
qty_up_btn.setStyleSheet(
'background: #2196F3; color: white; font-weight: bold; font-size: 16px; padding: 8px 16px; border-radius: 4px;')
qty_up_btn.clicked.connect(lambda: self._change_qty(1))
cart_btn_layout.addWidget(qty_up_btn)
qty_down_btn = QPushButton('-1')
qty_down_btn.setStyleSheet(
'background: #FF9800; color: white; font-weight: bold; font-size: 16px; padding: 8px 16px; border-radius: 4px;')
qty_down_btn.clicked.connect(lambda: self._change_qty(-1))
cart_btn_layout.addWidget(qty_down_btn)
discount_btn = QPushButton('할인')
discount_btn.setStyleSheet(
'background: #9C27B0; color: white; font-weight: bold; font-size: 14px; padding: 8px 16px; border-radius: 4px;')
discount_btn.clicked.connect(self._apply_discount)
cart_btn_layout.addWidget(discount_btn)
remove_btn = QPushButton('삭제')
remove_btn.setStyleSheet(
'background: #F44336; color: white; font-weight: bold; font-size: 14px; padding: 8px 16px; border-radius: 4px;')
remove_btn.clicked.connect(self._remove_selected)
cart_btn_layout.addWidget(remove_btn)
cart_btn_layout.addStretch()
clear_btn = QPushButton('전체 삭제')
clear_btn.setStyleSheet(
'background: #757575; color: white; font-size: 13px; padding: 8px 16px; border-radius: 4px;')
clear_btn.clicked.connect(self._clear_cart)
cart_btn_layout.addWidget(clear_btn)
cart_layout.addLayout(cart_btn_layout)
splitter.addWidget(cart_group)
# ── 우측 패널: 요약 + 결제 ──
right_panel = QWidget()
right_layout = QVBoxLayout()
right_layout.setContentsMargins(0, 0, 0, 0)
right_panel.setLayout(right_layout)
# 최근 스캔
scan_group = QGroupBox('최근 스캔')
scan_layout = QVBoxLayout()
scan_group.setLayout(scan_layout)
self.last_scan_label = QLabel('바코드를 스캔하세요')
self.last_scan_label.setStyleSheet('font-size: 14px; color: #757575; padding: 12px;')
self.last_scan_label.setWordWrap(True)
self.last_scan_label.setMinimumHeight(80)
scan_layout.addWidget(self.last_scan_label)
right_layout.addWidget(scan_group)
# 합계 요약
summary_group = QGroupBox('합계')
summary_layout = QVBoxLayout()
summary_group.setLayout(summary_layout)
self.item_count_label = QLabel('품목: 0개 / 수량: 0개')
self.item_count_label.setStyleSheet('font-size: 14px; color: #616161; padding: 4px 8px;')
summary_layout.addWidget(self.item_count_label)
sep1 = QFrame()
sep1.setFrameShape(QFrame.HLine)
sep1.setStyleSheet('color: #E0E0E0;')
summary_layout.addWidget(sep1)
self.cost_label = QLabel('입고 합계: 0원')
self.cost_label.setStyleSheet('font-size: 13px; color: #9E9E9E; padding: 4px 8px;')
summary_layout.addWidget(self.cost_label)
self.subtotal_label = QLabel('판매 합계: 0원')
self.subtotal_label.setStyleSheet('font-size: 14px; color: #424242; padding: 4px 8px;')
summary_layout.addWidget(self.subtotal_label)
self.discount_total_label = QLabel('할인 합계: -0원')
self.discount_total_label.setStyleSheet('font-size: 14px; color: #E53935; padding: 4px 8px;')
summary_layout.addWidget(self.discount_total_label)
sep2 = QFrame()
sep2.setFrameShape(QFrame.HLine)
sep2.setStyleSheet('color: #37474F; border: 1px solid #37474F;')
summary_layout.addWidget(sep2)
self.total_label = QLabel('총 결제금액: 0원')
self.total_label.setStyleSheet(
'font-size: 22px; font-weight: bold; color: #1B5E20; padding: 8px;')
summary_layout.addWidget(self.total_label)
self.margin_label = QLabel('마진: 0원 (0%)')
self.margin_label.setStyleSheet('font-size: 13px; color: #1565C0; padding: 4px 8px;')
summary_layout.addWidget(self.margin_label)
right_layout.addWidget(summary_group)
right_layout.addStretch()
# 결제 버튼
pay_btn = QPushButton('결 제')
pay_btn.setMinimumHeight(70)
pay_btn.setStyleSheet('''
QPushButton {
background: #1B5E20; color: white;
font-size: 26px; font-weight: bold;
border-radius: 8px;
}
QPushButton:hover { background: #2E7D32; }
QPushButton:pressed { background: #1B5E20; }
''')
pay_btn.clicked.connect(self._pay)
right_layout.addWidget(pay_btn)
splitter.addWidget(right_panel)
splitter.setSizes([800, 350])
root_layout.addWidget(splitter, 1)
# ── 하단 상태바 ──
self.statusBar().setStyleSheet('font-size: 12px; color: #757575;')
self.statusBar().showMessage('청춘약국 POS | 바코드 스캐너를 연결하고 "연결" 버튼을 누르세요')
# ── 포트 관리 ──
def _refresh_ports(self):
self.port_combo.clear()
for port in serial.tools.list_ports.comports():
self.port_combo.addItem(f'{port.device} - {port.description}', port.device)
for i in range(self.port_combo.count()):
if 'COM3' in (self.port_combo.itemData(i) or ''):
self.port_combo.setCurrentIndex(i)
break
def _toggle_connection(self):
if self.reader_thread and self.reader_thread.isRunning():
self.reader_thread.stop()
self.reader_thread.wait()
self.reader_thread = None
self.connect_btn.setText('연결')
self.connect_btn.setStyleSheet(
'background: #4CAF50; color: white; font-weight: bold; padding: 6px 20px; border-radius: 4px;')
self.status_label.setText('연결 해제됨')
self.status_label.setStyleSheet('color: #9E9E9E; font-size: 13px; margin-left: 12px;')
self.statusBar().showMessage('스캐너 연결 해제')
else:
port = self.port_combo.currentData()
if not port:
self.status_label.setText('포트를 선택하세요')
return
self.reader_thread = BarcodeReaderThread(port, self.baudrate_spin.value())
self.reader_thread.barcode_received.connect(self._on_barcode)
self.reader_thread.connection_status.connect(self._on_connection)
self.reader_thread.start()
self.connect_btn.setText('연결 해제')
self.connect_btn.setStyleSheet(
'background: #F44336; color: white; font-weight: bold; padding: 6px 20px; border-radius: 4px;')
def _on_connection(self, ok, msg):
if ok:
self.status_label.setText(msg)
self.status_label.setStyleSheet(
'color: #2E7D32; font-size: 13px; font-weight: bold; margin-left: 12px;')
self.statusBar().showMessage(f'스캐너 {msg} | 바코드를 스캔하세요')
else:
self.status_label.setText(msg)
self.status_label.setStyleSheet(
'color: #D32F2F; font-size: 13px; font-weight: bold; margin-left: 12px;')
self.connect_btn.setText('연결')
self.connect_btn.setStyleSheet(
'background: #4CAF50; color: white; font-weight: bold; padding: 6px 20px; border-radius: 4px;')
# ── 바코드 수신 ──
def _manual_barcode(self):
barcode = self.manual_input.text().strip()
if barcode:
self.manual_input.clear()
self._on_barcode(barcode)
def _on_barcode(self, barcode):
self.last_scan_label.setText(f'스캔: {barcode}\n조회 중...')
self.last_scan_label.setStyleSheet('font-size: 14px; color: #FF6F00; padding: 12px;')
self.statusBar().showMessage(f'바코드 {barcode} 조회 중...')
thread = DrugSearchThread(barcode)
thread.search_complete.connect(self._on_search_done)
thread.start()
self.search_threads.append(thread)
def _on_search_done(self, barcode, info):
sender = self.sender()
if sender in self.search_threads:
self.search_threads.remove(sender)
if not info:
self.last_scan_label.setText(f'스캔: {barcode}\n제품을 찾을 수 없습니다')
self.last_scan_label.setStyleSheet('font-size: 14px; color: #D32F2F; padding: 12px;')
self.statusBar().showMessage(f'바코드 {barcode}: 데이터베이스에서 찾을 수 없음')
return
# 이미 장바구니에 있으면 수량 +1
for item in self.cart_items:
if item['barcode'] == info['barcode']:
item['qty'] += 1
self._refresh_table()
self.last_scan_label.setText(
f'{info["goods_name"]}\n수량 → {item["qty"]}')
self.last_scan_label.setStyleSheet('font-size: 14px; color: #1565C0; padding: 12px;')
self.statusBar().showMessage(f'{info["goods_name"]} 수량 +1 ({item["qty"]}개)')
return
# 새 항목 추가
self.cart_items.append({
'barcode': info['barcode'],
'goods_name': info['goods_name'],
'manufacturer': info['manufacturer'],
'price': info['price'],
'sale_price': info['sale_price'],
'qty': 1,
'discount': 0,
})
self._refresh_table()
self.last_scan_label.setText(
f'{info["goods_name"]}\n{info["manufacturer"]} | {info["sale_price"]:,.0f}')
self.last_scan_label.setStyleSheet('font-size: 14px; color: #2E7D32; padding: 12px; font-weight: bold;')
self.statusBar().showMessage(f'{info["goods_name"]} 추가됨 ({info["sale_price"]:,.0f}원)')
# ── 장바구니 조작 ──
def _selected_row(self):
rows = self.cart_table.selectionModel().selectedRows()
return rows[0].row() if rows else -1
def _change_qty(self, delta):
row = self._selected_row()
if row < 0:
self.statusBar().showMessage('제품을 선택하세요')
return
item = self.cart_items[row]
item['qty'] = max(1, item['qty'] + delta)
self._refresh_table()
self.cart_table.selectRow(row)
def _apply_discount(self):
row = self._selected_row()
if row < 0:
self.statusBar().showMessage('할인할 제품을 선택하세요')
return
item = self.cart_items[row]
dlg = DiscountDialog(item['goods_name'], item['sale_price'], self)
if dlg.exec_() == QDialog.Accepted:
item['discount'] = dlg.result_discount
self._refresh_table()
self.cart_table.selectRow(row)
def _remove_selected(self):
row = self._selected_row()
if row < 0:
self.statusBar().showMessage('삭제할 제품을 선택하세요')
return
name = self.cart_items[row]['goods_name']
del self.cart_items[row]
self._refresh_table()
self.statusBar().showMessage(f'{name} 삭제됨')
def _clear_cart(self):
if not self.cart_items:
return
reply = QMessageBox.question(
self, '전체 삭제', '장바구니를 비우시겠습니까?',
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if reply == QMessageBox.Yes:
self.cart_items.clear()
self._refresh_table()
self.statusBar().showMessage('장바구니 초기화')
# ── 테이블 갱신 ──
def _refresh_table(self):
self.cart_table.setRowCount(len(self.cart_items))
total_cost = 0
total_sale = 0
total_discount = 0
total_qty = 0
for i, item in enumerate(self.cart_items):
subtotal = (item['sale_price'] - item['discount']) * item['qty']
cost_total = item['price'] * item['qty']
cols = [
item['goods_name'],
item['manufacturer'],
item['barcode'],
f'{item["price"]:,.0f}',
f'{item["sale_price"]:,.0f}',
str(item['qty']),
f'-{item["discount"]:,.0f}' if item['discount'] > 0 else '',
f'{subtotal:,.0f}',
]
for j, val in enumerate(cols):
cell = QTableWidgetItem(val)
cell.setFlags(cell.flags() & ~Qt.ItemIsEditable)
# 숫자 컬럼 오른쪽 정렬
if j >= 3:
cell.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
# 할인 빨간색
if j == 6 and item['discount'] > 0:
cell.setForeground(QBrush(QColor('#E53935')))
# 소계 볼드
if j == 7:
font = cell.font()
font.setBold(True)
cell.setFont(font)
self.cart_table.setItem(i, j, cell)
total_cost += cost_total
total_sale += item['sale_price'] * item['qty']
total_discount += item['discount'] * item['qty']
total_qty += item['qty']
final_total = total_sale - total_discount
margin = final_total - total_cost
margin_pct = (margin / final_total * 100) if final_total > 0 else 0
self.item_count_label.setText(f'품목: {len(self.cart_items)}개 / 수량: {total_qty}')
self.cost_label.setText(f'입고 합계: {total_cost:,.0f}')
self.subtotal_label.setText(f'판매 합계: {total_sale:,.0f}')
self.discount_total_label.setText(f'할인 합계: -{total_discount:,.0f}')
self.total_label.setText(f'총 결제금액: {final_total:,.0f}')
self.margin_label.setText(f'마진: {margin:,.0f}원 ({margin_pct:.1f}%)')
# ── 결제 ──
def _pay(self):
if not self.cart_items:
self.statusBar().showMessage('장바구니가 비어있습니다')
return
total_sale = sum(it['sale_price'] * it['qty'] for it in self.cart_items)
total_discount = sum(it['discount'] * it['qty'] for it in self.cart_items)
final = total_sale - total_discount
items_text = '\n'.join(
f' {it["goods_name"]} x{it["qty"]} {(it["sale_price"] - it["discount"]) * it["qty"]:,.0f}'
for it in self.cart_items
)
reply = QMessageBox.question(
self, '결제 확인',
f'총 결제금액: {final:,.0f}\n\n{items_text}\n\n결제하시겠습니까?',
QMessageBox.Yes | QMessageBox.No, QMessageBox.No
)
if reply == QMessageBox.Yes:
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
QMessageBox.information(
self, '결제 완료',
f'결제가 완료되었습니다.\n\n'
f'시각: {now}\n'
f'금액: {final:,.0f}\n'
f'품목: {len(self.cart_items)}'
)
self.cart_items.clear()
self._refresh_table()
self.last_scan_label.setText('바코드를 스캔하세요')
self.last_scan_label.setStyleSheet('font-size: 14px; color: #757575; padding: 12px;')
self.statusBar().showMessage(f'결제 완료 ({final:,.0f}원) | {now}')
# ── 종료 ──
def closeEvent(self, event):
if self.reader_thread:
self.reader_thread.stop()
self.reader_thread.wait()
for t in self.search_threads:
if t.isRunning():
t.wait()
event.accept()
def main():
app = QApplication(sys.argv)
app.setStyle('Fusion')
window = POSDummyGUI()
window.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()