처음에는 QTableWidget을 사용하여 표를 구성하였지만 조금 알아보니 QTableView를 사용하는 것이 성능면에서
더 낫다는 듯 해서 교체작업을 진행했다.
1. 테이블 뷰 생성
from PySide6.QtWidgets import QTabWidget, QWidget, QGridLayout, QTableView
from controller import actions
from models.table_models import DayCalTableModel, DayCalOthersTableModel, DayCalResultTableModel
# 일일 정산서 계산서 위젯
class DayCal(QWidget):
# 생성자
def __init__(self):
super().__init__()
self.input_table = QTableView()
self.other_table = QTableView()
self.result_table = QTableView()
self.init_ui()
''' 생략 '''
화주별 데이터를 입력받고 일부 결과를 표시하기 위한 input_table, 화주와 관계없는 별도의 데이터를 입력하기
위한 other_table, 입력데이터를 기반으로 결과를 나타낼 result_table 로 총 세개의 QTableView를 생성하였다.
2. input_table 테이블 모델 작성
import operator
from PySide6.QtCore import QAbstractTableModel, Qt, SIGNAL, QModelIndex
from PySide6.QtGui import *
class DayCalTableModel(QAbstractTableModel):
def __init__(self, parent, horizontal_header, data, *args):
QAbstractTableModel.__init__(self, parent, *args)
self.setParent(parent)
self.table_data = data
self.horizontal_header = horizontal_header
self.vertical_header = ['강동총금액', '강동운임', '강동하차비', '강동수수료 4%', '공제후 금액', '중매수수료 5%', '화주운임', '화주하차비', '상장수수료 4%', '강동선지급금', '공제합계', '선지급금포함 공제합계']
self.row_count = len(self.vertical_header)
self.column_count = len(self.horizontal_header)
def rowCount(self, parent):
return self.row_count
def columnCount(self, parent):
return self.column_count
def data(self, index, role):
if index.isValid():
if role == Qt.DisplayRole or role == Qt.EditRole:
value = self.table_data[index.column()][index.row()]
return str(value)
def headerData(self, section: int, orientation: Qt.Orientation, role: int = ...):
if role == Qt.DisplayRole:
if orientation == Qt.Horizontal:
return self.horizontal_header[section]
else:
return self.vertical_header[section]
return None
def sort(self, col, order):
self.emit(SIGNAL("layoutAboutToBeChanged()"))
self.table_data = sorted(self.table_data, key=operator.itemgetter(col))
if order == Qt.DescendingOrder:
self.table_data.reverse()
self.emit(SIGNAL("layoutChanged()"))
def flags(self, index):
if index.row() in [3, 4, 10, 11]:
return Qt.ItemIsSelectable | Qt.ItemIsEnabled
return Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable
def setData(self, index, value, role):
if role == Qt.EditRole:
self.table_data[index.column()][index.row()] = value
return True
return False
def owner_added(self, name):
self.beginInsertColumns(QModelIndex(), self.column_count, self.column_count)
self.horizontal_header.append(name)
self.table_data.append([0]*12)
self.column_count += 1
self.endInsertColumns()
def owner_removed(self, name):
idx = self.horizontal_header.index(name)
self.beginRemoveColumns(QModelIndex(), idx, idx)
self.table_data.pop(idx)
self.horizontal_header.pop(idx)
self.column_count -= 1
self.endRemoveColumns()
def owner_modified(self, org, chg):
idx = self.horizontal_header.index(org)
self.horizontal_header[idx] = chg
''' 생략 '''
여기서는 QAbstractTableModel 을 상속한 테이블 모델을 작성하였다. (이 외에도 다른 여러가지 방법들이 있는 듯 했다.)
- __init__(self, parent, horizontal_header, data, *args)
- parent 위젯과 헤더, 그리고 표시할 데이터를 입력받는 초기화 메소드
- 해당 모델의 parent를 입력받은 parent 위젯으로 설정
- 해당 모델의 table_data 를 입력받은 데이터로 설정한다.
- horizontal_header를 입력받은 헤더 데이터로 설정한다. 이는 화주 명단과 같다.
- vertical_header를 설정한다. 이 항목들은 변경되지 않기 때문에 하드코딩으로 설정해주었다.
- row_count, column_count(행과 열의 길이) 값을 각각 vertical, horizontal 헤더의 길이로 설정한다.
- rowCount(self, parent) / columnCount(self, parent)
- QAbstractModel에 정의된 rowCount, columnCount 메소드를 오버라이딩
- 테이블의 열 수와 행 수를 반환한다.
- data(self, index, role)
- QAbstractModel에 정의된 data 메소드를 오버라이딩
- role은 이 메소드를 어떤 상태(역할)에서 호출하는지를 나타내며 index는 참조하려는 데이터의
위치(행과 열)를 나타낸다. role 과 index에 따라 적절히 데이터를 반환하도록 구현한다. - 여기서는 Qt.DisplayRole(표시), Qt.EditRole(수정) 상태에서 호출할 경우 index에 해당하는 데이터를
반환하도록 구현하였다. 만약 role == Qt.EditRole을 조건에서 뺄 경우 수정상태에 들어간 셀에서는
데이터가 표시되지 않는다.
- QAbstractModel에 정의된 data 메소드를 오버라이딩
- headerData(self, section, orientation, role)
- QAbstractModel에 정의된 headerData 메소드를 오버라이딩
- orientation은 방향(행방향 / 열방향)을 나타내며 section은 인덱스를 나타낸다.
- horizontal, vertical 의 헤더를 반환하기 위해 사용한다.
- orientation 이 horizontal인지 vertical인지에 따라 각각 해당하는 헤더값을 반환하도록 구현하였다.
- QAbstractModel에 정의된 headerData 메소드를 오버라이딩
- flags(self, index)
- QAbstractModel에 정의된 flags 메소드를 오버라이딩
- index 에 해당하는 셀에 대해 flag를 설정하기 위해 사용한다
- 플래그를 통해 특정 셀의 편집가능 여부를 수정하는 등의 작업이 가능하다.
- 사용자가 값을 입력하는 셀이 아닌 부분을 편집 불가능하게 설정하였다.
- QAbstractModel에 정의된 flags 메소드를 오버라이딩
- setData(self, index, value, role)
- QAbstractModel에 정의된 setData 메소드를 오버라이딩
- 셀의 값 변경을 반영하기 위해 구현해야한다.
- edit role 일 경우에 수정사항이 반영되도록 구현하였다.
- 반영되었을 경우 True, 아닐 경우 False를 반환한다.
- QAbstractModel에 정의된 setData 메소드를 오버라이딩
- owner_added(self, name) / owner_removed(self, name) / owner_modified(self, org, chg)
- 화주의 추가, 삭제, 이름변경을 반영하기 위해 직접 구현한 메소드이다.
- 화주가 추가되고 그에따라 화주별 데이터도 추가되어 데이터베이스에 반영되지만
데이터베이스에서 데이터를 읽어오는 것은 프로그램 실행시에만 수행하는 작업이며
반영된 데이터를 굳이 데이터베이스에서 다시 읽어오는것도 비효율적이기에
모델의 메소드를 호출하여 즉각 반영하도록 하였다. - added 와 removed 의 경우 테이블 자체가 변경되는 작업이기에 (열의 추가/삭제)
beginInsertColumns, beginRemoveColumns 등 테이블의 변경작업이 시작됨을 알리는
메소드를 호출한 뒤 작업을 수행하고 마지막에 endInsertColumns, endRemoveColumns 등
테이블의 변경작업이 끝났음을 알리는 메소드를 호출해줘야 변경사항이 정상적으로 반영된다. - modified 의 경우 단순히 값을 수정하는 것 뿐이기에 header 리스트를 수정해주는 것만으로
간단하게 반영된다.
- 화주의 추가, 삭제, 이름변경을 반영하기 위해 직접 구현한 메소드이다.
3. other_table 테이블 모델 작성
import operator
from PySide6.QtCore import QAbstractTableModel, Qt, SIGNAL, QModelIndex
from PySide6.QtGui import *
''' 생략 '''
class DayCalOthersTableModel(QAbstractTableModel):
def __init__(self, parent, data, *args):
QAbstractTableModel.__init__(self, parent, *args)
self.setParent(parent)
self.table_data = [data]
self.vertical_header = ['경매 사무실 입금', '가라경매 강동 입금', '직접 지출', '우리 경매', '강동 사입']
self.row_count = len(self.vertical_header)
self.column_count = 1
def rowCount(self, parent):
return self.row_count
def columnCount(self, parent):
return self.column_count
def data(self, index, role):
if index.isValid():
if role == Qt.DisplayRole or role == Qt.EditRole:
value = self.table_data[index.column()][index.row()]
return str(value)
def headerData(self, section: int, orientation: Qt.Orientation, role: int = ...):
if role == Qt.DisplayRole:
if orientation == Qt.Vertical:
return self.vertical_header[section]
return None
def flags(self, index):
return Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable
def setData(self, index, value, role):
if role == Qt.EditRole:
self.table_data[index.column()][index.row()] = value
return True
return False
''' 생략 '''
- other_table 의 테이블 모델도 기본적으로 input_table의 그것과 거의 동일하게 구현
- horizontal header가 필요하지 않기 때문에 vertical header만을 설정
- 모두 사용자가 입력하는 데이터로만 이루어졌기에 특정 셀의 수정 불가등의 설정을 하지 않음
4. result_table 테이블 모델 작성
import operator
from PySide6.QtCore import QAbstractTableModel, Qt, SIGNAL, QModelIndex
from PySide6.QtGui import *
''' 생략 '''
class DayCalResultTableModel(QAbstractTableModel):
def __init__(self, parent, result_data, *args):
QAbstractTableModel.__init__(self, parent, *args)
self.setParent(parent)
self.horizontal_header = ['계']
self.vertical_header = ['강동총금액 합계', '강동운임 합계', '강동하차비 합계', '강동수수료 4% 합계', '공제후금액 합계',
'중매수수료 5% 합계', '화주운임 합계', '화주하차비 합계', '상장수수료 4% 합계', '경매확인',
'경매 차액', '중개수수료 5%', '경매 차익']
self.row_count = len(self.vertical_header)
self.column_count = 1
self.table_data = [result_data]
def rowCount(self, parent):
return self.row_count
def columnCount(self, parent):
return self.column_count
def data(self, index, role):
if index.isValid():
if role == Qt.DisplayRole or role == Qt.EditRole:
value = self.table_data[index.column()][index.row()]
return str(value)
def headerData(self, section: int, orientation: Qt.Orientation, role: int = ...):
if role == Qt.DisplayRole:
if orientation == Qt.Vertical:
return self.vertical_header[section]
else:
return self.horizontal_header[section]
return None
def flags(self, index):
return Qt.ItemIsSelectable | Qt.ItemIsEnabled
- 마찬가지로 input_table과 거의 동일하게 구현
- 해당 테이블은 모든 셀을 수정 불가능하도록 설정
5. 테이블 뷰에 모델 세팅
from PySide6.QtWidgets import QTabWidget, QWidget, QGridLayout, QTableView
from controller import actions
from models.table_models import DayCalTableModel, DayCalOthersTableModel, DayCalResultTableModel
# 일일 정산서 계산서 위젯
class DayCal(QWidget):
# 생성자
def __init__(self):
super().__init__()
self.input_table = QTableView()
self.other_table = QTableView()
self.result_table = QTableView()
self.init_ui()
# ui 초기화
def init_ui(self):
# 화주별 데이터 입력테이블
self.data_model = DayCalTableModel(self, actions.get_daycal_owner_list(), actions.get_daycal_owner_values())
self.input_table.setModel(self.data_model)
# 기타 데이터 입력 테이블
self.other_data_model = DayCalOthersTableModel(self, actions.get_daycal_other_values())
self.other_table.setModel(self.other_data_model)
# 결과 테이블
self.result_data_model = DayCalResultTableModel(self, actions.get_daycal_result())
self.result_table.setModel(self.result_data_model)
# 그리드 레이아웃
grid = QGridLayout()
# 테이블위젯 추가
grid.addWidget(self.input_table, 0, 0)
grid.addWidget(self.other_table, 1, 0)
grid.addWidget(self.result_table, 0, 1, 2, 1)
grid.setRowStretch(0, 5)
grid.setColumnStretch(0, 5)
# 레이아웃 세팅
self.setLayout(grid)
# 화주 추가 반영
def owner_added(self, added_user):
self.data_model.owner_added(added_user)
# 화주 삭제 반영
def owner_removed(self, removed_name):
self.data_model.owner_removed(removed_name)
# 화주 이름 변경 반영
def owner_modified(self, org_name, chg_name):
self.data_model.owner_modified(org_name, chg_name)
''' 생략 '''
- 작성한 모델의 인스턴스를 생성(이 때, DB에서 데이터를 읽어와서 인자로 넘겨준다.)
- 테이블 뷰에 각각 해당하는 모델의 인스턴스를 세팅
- 레이아웃에 추가한 뒤 위젯에 레이아웃을 세팅한다.
- DayCal 위젯은 owner_added, owner_removed, owner_modified 메소드를 각각 구현하여
화주 상태변경을 인식하고 각 테이블의 데이터모델의 메소드를 호출하여 이를 반영한다.
6. DB 작업을 위한 함수 작성
# 화주 명단 가져오기
def get_daycal_owner_list():
return [q.name for q in session.query(DayCalOwner).order_by(DayCalOwner.id)]
# 화주별 데이터 가져오기
def get_daycal_owner_values():
values = []
today = date.today()
for owner_id in session.query(DayCalOwner.id).order_by(DayCalOwner.id):
id = owner_id[0]
value = session.query(DayCalOwnerValues).filter(and_(DayCalOwnerValues.owner_id == id, DayCalOwnerValues.date == today)).first()
if not value:
value = DayCalOwnerValues(today, id)
session.add(value)
session.commit()
values.append(value.to_list())
return values
# 기타 데이터 가져오기
def get_daycal_other_values():
today = date.today()
value = session.query(DayCalOtherValues).filter(DayCalOtherValues.date == today).first()
if not value:
value = DayCalOtherValues(today)
session.add(value)
session.commit()
return value.to_list()
# 결과 데이터 가져오기
def get_daycal_result():
today = date.today()
value = session.query(DayCalResult).filter(DayCalResult.date == today).first()
if not value:
value = DayCalResult(today)
session.add(value)
session.commit()
return value.to_list()
- 데이터 모델의 인스턴스 생성에 필요한 데이터를 DB에서 가져오기 위한 함수 구현
- get_daycal_owner_list
- 화주 이름 명단을 반환
- 화주 id를 기준으로 오름차순 정렬
- 화주 이름 명단을 반환
- get_daycal_owner_values
- 화주별 오늘의 데이터를 리스트 형태로 반환
- 화주 id를 기준으로 오름차순 정렬
- 오늘 날짜의 데이터가 존재하지 않을 경우 디폴트로 모든 값이 0인 기본 데이터를 생성하여
데이터베이스에 추가하고 해당 데이터를 반환한다.
- 화주별 오늘의 데이터를 리스트 형태로 반환
- get_daycal_other_values
- 오늘의 기타 데이터를 리스트 형태로 반환
- 오늘 날짜의 데이터가 존재하지 않을 경우 디폴트로 모든 값이 0인 기본 데이터를 생성하여
데이터베이스에 추가하고 해당 데이터를 반환한다.
- 오늘의 기타 데이터를 리스트 형태로 반환
- get_daycal_result
- 오늘의 결과 데이터를 리스트 형태로 반환
- 오늘 날짜의 데이터가 존재하지 않을 경우 디폴트로 모든 값이 0인 기본 데이터를 생성하여
데이터베이스에 추가하고 해당 데이터를 반환한다.
- 오늘의 결과 데이터를 리스트 형태로 반환
7. to_list 메소드 추가
# 화주별 일일정산 데이터 모델
class DayCalOwnerValues(Base):
__tablename__ = 'daycal_owner_values'
date = Column(Date, primary_key=True)
owner_id = Column(Integer, primary_key=True)
kd_total = Column(Integer, nullable=False)
kd_fare = Column(Integer, nullable=False)
kd_drop = Column(Integer, nullable=False)
kd_fee4 = Column(Integer, nullable=False)
after_deduction = Column(Integer, nullable=False)
match_fee5 = Column(Integer, nullable=False)
owner_fare = Column(Integer, nullable=False)
owner_drop = Column(Integer, nullable=False)
listing_fee4 = Column(Integer, nullable=False)
kd_pre = Column(Integer, nullable=False)
deduction_total = Column(Integer, nullable=False)
total_include_pre = Column(Integer, nullable=False)
def __init__(self, date, owner_id):
self.date = date
self.owner_id = owner_id
self.kd_total = 0
self.kd_fare = 0
self.kd_drop = 0
self.kd_fee4 = 0
self.after_deduction = 0
self.match_fee5 = 0
self.owner_fare = 0
self.owner_drop = 0
self.listing_fee4 = 0
self.kd_pre = 0
self.deduction_total = 0
self.total_include_pre = 0
def to_list(self):
return [self.kd_total, self.kd_fare, self.kd_drop, self.kd_fee4, self.after_deduction,
self.match_fee5, self.owner_fare, self.owner_drop, self.listing_fee4, self.kd_pre,
self.deduction_total, self.total_include_pre]
# 일일정산서에 기타 데이터 모델
class DayCalOtherValues(Base):
__tablename__ = 'daycal_other_values'
date = Column(Date, primary_key=True)
office_deposit = Column(Integer)
kd_deposit = Column(Integer)
direct_exp = Column(Integer)
our_auc = Column(Integer)
kd_buy = Column(Integer)
def __init__(self, date):
self.date = date
self.office_deposit = 0
self.kd_deposit = 0
self.direct_exp = 0
self.our_auc = 0
self.kd_buy = 0
def to_list(self):
return [self.office_deposit, self.kd_deposit, self.direct_exp, self.our_auc, self.kd_buy]
# 일일정산서 결과 모델
class DayCalResult(Base):
__tablename__ = 'daycal_result'
date = Column(Date, primary_key=True)
kd_total = Column(Integer, nullable=False)
kd_fare = Column(Integer, nullable=False)
kd_drop = Column(Integer, nullable=False)
kd_fee4 = Column(Integer, nullable=False)
after_deduction = Column(Integer, nullable=False)
match_fee5 = Column(Integer, nullable=False)
owner_fare = Column(Integer, nullable=False)
owner_drop = Column(Integer, nullable=False)
listing_fee4 = Column(Integer, nullable=False)
auc_check = Column(Integer, nullable=False)
auc_diff = Column(Integer, nullable=False)
match_fee5_final = Column(Integer, nullable=False)
auc_profit = Column(Integer, nullable=False)
def __init__(self, date):
self.date = date
self.kd_total = 0
self.kd_fare = 0
self.kd_drop = 0
self.kd_fee4 = 0
self.after_deduction = 0
self.match_fee5 = 0
self.owner_fare = 0
self.owner_drop = 0
self.listing_fee4 = 0
self.auc_check = 0
self.auc_diff = 0
self.match_fee5_final = 0
self.auc_profit = 0
def to_list(self):
return [self.kd_total, self.kd_fare, self.kd_drop, self.kd_fee4, self.after_deduction,
self.match_fee5, self.owner_fare, self.owner_drop, self.listing_fee4, self.auc_check,
self.auc_diff, self.match_fee5_final, self.auc_profit]
- DB에서 모델단위로 읽어온 데이터를 보다 쉽게 리스트 형태로 전달하기 위해 to_list 메소드를 추가
- 단순히 테이블에 표현하기 위해 필요한 column 값들을 리스트형태로 반환하도록 구현
이제 날짜가 바뀌면 자동으로 그날의 디폴트 데이터를 DB에 삽입하고 테이블에 표시해준다.
또한 QTableWidget을 사용했을 때 보다 좀더 유연하게 테이블을 사용할 수 있게 되었다.
'개인 프로젝트 > Accounting Program' 카테고리의 다른 글
#19 데이터 조회기능 구현 (0) | 2021.12.01 |
---|---|
#18 저장기능 구현 (0) | 2021.11.25 |
#16 화주 삭제, 화주 이름 변경 (0) | 2021.11.02 |
#15 모델/구조 변경, 화주 추가 기능 수정 (0) | 2021.11.02 |
#14 화주 추가 (0) | 2021.11.01 |