backend/gui에서 backend/db로의 상대 경로 import 수정 sys.path에 backend 폴더 추가하여 db.dbsetup 모듈 접근 가능하도록 수정 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
386 lines
13 KiB
Python
386 lines
13 KiB
Python
"""
|
|
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_())
|