""" POS 판매 내역 조회 GUI (PyQt5) MSSQL SALE_MAIN 테이블에서 오늘 판매 내역을 조회하여 표시 """ import sys import os from datetime import datetime from PyQt5.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QGroupBox, QTableWidget, QTableWidgetItem, QDialog, QMessageBox, QDateEdit ) from PyQt5.QtCore import QThread, pyqtSignal, Qt, QDate from PyQt5.QtGui import QFont # 데이터베이스 연결 (backend/ 폴더를 Python 경로에 추가) sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) from db.dbsetup import DatabaseManager class SalesQueryThread(QThread): """ 판매 내역 조회 백그라운드 스레드 GUI 블로킹을 방지하기 위해 DB 쿼리를 별도 스레드에서 실행 """ query_complete = pyqtSignal(list) # 조회 완료 시그널 query_error = pyqtSignal(str) # 에러 발생 시그널 def __init__(self, date_str): """ Args: date_str: 조회할 날짜 (YYYYMMDD 형식) """ super().__init__() self.date_str = date_str def run(self): """스레드 실행 (SALE_MAIN 조회)""" conn = None try: db_manager = DatabaseManager() conn = db_manager.get_engine('PM_PRES').raw_connection() cursor = conn.cursor() # 메인 쿼리: SALE_MAIN에서 오늘 판매 내역 조회 query = """ SELECT M.SL_NO_order, M.InsertTime, M.SL_MY_sale, ISNULL(M.SL_NM_custom, '[비고객]') AS customer_name FROM SALE_MAIN M WHERE M.SL_DT_appl = ? ORDER BY M.InsertTime DESC """ cursor.execute(query, self.date_str) rows = cursor.fetchall() sales_list = [] for row in rows: order_no, insert_time, sale_amount, customer = row # 품목 수 조회 (SALE_SUB) cursor.execute(""" SELECT COUNT(*) FROM SALE_SUB WHERE SL_NO_order = ? """, order_no) item_count_row = cursor.fetchone() item_count = item_count_row[0] if item_count_row else 0 sales_list.append({ 'order_no': order_no, 'time': insert_time.strftime('%H:%M') if insert_time else '--:--', 'amount': float(sale_amount) if sale_amount else 0.0, 'customer': customer, 'item_count': item_count }) self.query_complete.emit(sales_list) except Exception as e: self.query_error.emit(str(e)) finally: if conn: conn.close() class SaleDetailDialog(QDialog): """ 판매 상세 조회 팝업 더블클릭한 판매 건의 SALE_SUB 품목 상세 표시 """ def __init__(self, order_no, parent=None): """ Args: order_no: 판매 주문번호 (SL_NO_order) parent: 부모 위젯 """ super().__init__(parent) self.order_no = order_no self.setWindowTitle(f'판매 상세: {order_no}') self.setModal(True) self.resize(700, 400) self.init_ui() self.load_details() def init_ui(self): """UI 초기화""" layout = QVBoxLayout() # 제목 title_label = QLabel(f'주문번호: {self.order_no}') title_label.setStyleSheet('font-size: 14px; font-weight: bold; padding: 10px;') layout.addWidget(title_label) # 상세 테이블 self.detail_table = QTableWidget() self.detail_table.setColumnCount(4) self.detail_table.setHorizontalHeaderLabels([ '약품코드', '약품명', '수량', '금액' ]) self.detail_table.setColumnWidth(0, 100) self.detail_table.setColumnWidth(1, 300) self.detail_table.setColumnWidth(2, 80) self.detail_table.setColumnWidth(3, 100) layout.addWidget(self.detail_table) # 닫기 버튼 close_btn = QPushButton('닫기') close_btn.setStyleSheet('background-color: #2196F3; color: white; padding: 8px; font-weight: bold;') close_btn.clicked.connect(self.close) layout.addWidget(close_btn) self.setLayout(layout) def load_details(self): """SALE_SUB + CD_GOODS 조인 조회""" conn = None try: db_manager = DatabaseManager() conn = db_manager.get_engine('PM_PRES').raw_connection() cursor = conn.cursor() query = """ SELECT S.DrugCode, ISNULL(G.GoodsName, '(약품명 없음)') AS goods_name, S.SL_NM_item AS quantity, S.SL_TOTAL_PRICE FROM SALE_SUB S LEFT JOIN PM_DRUG.dbo.CD_GOODS G ON S.DrugCode = G.DrugCode WHERE S.SL_NO_order = ? ORDER BY S.DrugCode """ cursor.execute(query, self.order_no) rows = cursor.fetchall() # 테이블에 데이터 채우기 self.detail_table.setRowCount(len(rows)) for row_idx, row in enumerate(rows): drug_code, goods_name, quantity, price = row # 약품코드 self.detail_table.setItem(row_idx, 0, QTableWidgetItem(str(drug_code))) # 약품명 self.detail_table.setItem(row_idx, 1, QTableWidgetItem(goods_name)) # 수량 (중앙 정렬) qty_item = QTableWidgetItem(str(quantity)) qty_item.setTextAlignment(Qt.AlignCenter) self.detail_table.setItem(row_idx, 2, qty_item) # 금액 (우측 정렬, 천단위 콤마) price_value = float(price) if price else 0.0 price_item = QTableWidgetItem(f'{price_value:,.0f}원') price_item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter) self.detail_table.setItem(row_idx, 3, price_item) except Exception as e: QMessageBox.critical(self, '오류', f'상세 조회 실패:\n{str(e)}') finally: if conn: conn.close() class POSSalesGUI(QMainWindow): """ POS 판매 내역 조회 메인 GUI """ def __init__(self): super().__init__() self.db_manager = DatabaseManager() self.sales_thread = None self.sales_data = [] self.init_ui() def init_ui(self): """UI 초기화""" self.setWindowTitle('POS 판매 조회') self.setGeometry(100, 100, 900, 600) # 중앙 위젯 central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QVBoxLayout() central_widget.setLayout(main_layout) # === 1. 조회 설정 그룹 === settings_group = QGroupBox('조회 설정') settings_layout = QHBoxLayout() settings_group.setLayout(settings_layout) # 날짜 선택기 settings_layout.addWidget(QLabel('날짜:')) self.date_edit = QDateEdit() self.date_edit.setCalendarPopup(True) self.date_edit.setDate(QDate.currentDate()) self.date_edit.setDisplayFormat('yyyy-MM-dd') self.date_edit.dateChanged.connect(self.on_date_changed) settings_layout.addWidget(self.date_edit) # 새로고침 버튼 self.refresh_btn = QPushButton('새로고침') self.refresh_btn.setStyleSheet('background-color: #4CAF50; color: white; padding: 8px; font-weight: bold;') self.refresh_btn.clicked.connect(self.refresh_sales) settings_layout.addWidget(self.refresh_btn) # QR 생성 버튼 (Phase 2 준비 - 현재 비활성화) self.qr_btn = QPushButton('QR 생성') self.qr_btn.setEnabled(False) self.qr_btn.setStyleSheet('background-color: #9E9E9E; color: white; padding: 8px; font-weight: bold;') self.qr_btn.setToolTip('후향적 적립 QR (추후 개발)') settings_layout.addWidget(self.qr_btn) settings_layout.addStretch() main_layout.addWidget(settings_group) # === 2. 판매 내역 테이블 === sales_group = QGroupBox('판매 내역') sales_layout = QVBoxLayout() sales_group.setLayout(sales_layout) self.sales_table = QTableWidget() self.sales_table.setColumnCount(5) self.sales_table.setHorizontalHeaderLabels([ '주문번호', '시간', '금액', '고객명', '품목수' ]) self.sales_table.setColumnWidth(0, 180) self.sales_table.setColumnWidth(1, 80) self.sales_table.setColumnWidth(2, 120) self.sales_table.setColumnWidth(3, 120) self.sales_table.setColumnWidth(4, 80) self.sales_table.setSelectionBehavior(QTableWidget.SelectRows) self.sales_table.doubleClicked.connect(self.show_sale_detail) sales_layout.addWidget(self.sales_table) main_layout.addWidget(sales_group) # === 3. 상태바 === status_layout = QHBoxLayout() self.status_label = QLabel('대기 중...') self.status_label.setStyleSheet('color: gray; font-size: 12px; padding: 5px;') status_layout.addWidget(self.status_label) status_layout.addStretch() self.total_label = QLabel('총 매출: 0원') self.total_label.setStyleSheet('color: blue; font-size: 12px; font-weight: bold; padding: 5px;') status_layout.addWidget(self.total_label) main_layout.addLayout(status_layout) # 초기 조회 실행 self.refresh_sales() def on_date_changed(self, date): """날짜 변경 시 유효성 검사""" today = QDate.currentDate() if date > today: QMessageBox.warning(self, '경고', '미래 날짜는 선택할 수 없습니다.') self.date_edit.setDate(today) def refresh_sales(self): """판매 내역 조회 시작""" # 중복 방지 if self.sales_thread and self.sales_thread.isRunning(): return date_str = self.date_edit.date().toString('yyyyMMdd') # 스레드 시작 self.sales_thread = SalesQueryThread(date_str) self.sales_thread.query_complete.connect(self.on_sales_loaded) self.sales_thread.query_error.connect(self.on_query_error) self.sales_thread.start() # UI 업데이트 self.status_label.setText('조회 중...') self.status_label.setStyleSheet('color: orange; font-size: 12px; padding: 5px;') self.refresh_btn.setEnabled(False) def on_sales_loaded(self, sales_list): """조회 완료 시 테이블 갱신""" self.sales_data = sales_list self.populate_table(sales_list) # 총 매출 계산 total_amount = sum(sale['amount'] for sale in sales_list) # 상태 업데이트 self.status_label.setText(f'{len(sales_list)}건 조회 완료') self.status_label.setStyleSheet('color: green; font-size: 12px; padding: 5px;') self.total_label.setText(f'총 매출: {total_amount:,.0f}원') self.refresh_btn.setEnabled(True) def populate_table(self, sales_list): """QTableWidget에 데이터 채우기""" self.sales_table.setRowCount(len(sales_list)) for row, sale in enumerate(sales_list): # 주문번호 self.sales_table.setItem(row, 0, QTableWidgetItem(sale['order_no'])) # 시간 self.sales_table.setItem(row, 1, QTableWidgetItem(sale['time'])) # 금액 (우측 정렬, 천단위 콤마) amount_item = QTableWidgetItem(f"{sale['amount']:,.0f}원") amount_item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter) self.sales_table.setItem(row, 2, amount_item) # 고객명 self.sales_table.setItem(row, 3, QTableWidgetItem(sale['customer'])) # 품목수 (중앙 정렬) count_item = QTableWidgetItem(str(sale['item_count'])) count_item.setTextAlignment(Qt.AlignCenter) self.sales_table.setItem(row, 4, count_item) def on_query_error(self, error_msg): """DB 조회 에러 처리""" QMessageBox.critical(self, '오류', f'조회 실패:\n{error_msg}') self.status_label.setText('오류 발생') self.status_label.setStyleSheet('color: red; font-size: 12px; padding: 5px;') self.refresh_btn.setEnabled(True) def show_sale_detail(self): """선택된 판매 건의 상세 조회""" current_row = self.sales_table.currentRow() if current_row < 0: return order_no = self.sales_table.item(current_row, 0).text() detail_dialog = SaleDetailDialog(order_no, self) detail_dialog.exec_() def closeEvent(self, event): """종료 시 정리""" if self.sales_thread and self.sales_thread.isRunning(): self.sales_thread.wait() self.db_manager.close_all() event.accept() if __name__ == '__main__': app = QApplication(sys.argv) # 한글 폰트 설정 font = QFont('맑은 고딕', 10) app.setFont(font) window = POSSalesGUI() window.show() sys.exit(app.exec_())