v2.13.0: 익일 퇴근 처리 수정 + 날짜 지정 일일보고 + 연차 직접입력
Fixed: - clock_out()이 datetime.now().date() 행에 퇴근을 써서 자정 넘긴 퇴근이 UPDATE 0건으로 유실되던 버그. 출근일(근무일) 기준으로 수정. cancel_clock_out() 및 주말/공휴일 판정·외출 집계도 동일 기준으로 통일. - 캘린더 시간 수정 / 과거기록 추가가 퇴근<=출근을 반려해 익일 퇴근을 저장할 수 없던 문제. 익일로 해석하고 동시각만 반려. 총 근무시간·연장근무 음수 계산도 수정. Added: - generate_daily_report(target_date) 날짜 파라미터화 + 캘린더 '보고서 복사' 버튼. 기본값은 진행 중인 근무일(경계시간 전이면 출근일). - 연차 직접입력 버튼(메인 창) — 미연결 상태였던 use_custom_leave()를 기존 AddLeaveDialog 재사용으로 연결. - resolve_work_datetimes() / is_overnight() 공용 헬퍼 + 유닛 테스트 6개. Changed: - 연차 빠른 사용에서 날짜·메모 팝업 제거 (연장근무와 동일하게 확인 1회). - get_today_overtime_usage() / get_total_break_minutes_today()에 선택적 date_str 파라미터 추가. 테스트: 유닛 200 / 통합 53 / GUI 8 / i18n 5 통과. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
d8c6a9d784
commit
f28f0619a4
+57
-12
@@ -85,6 +85,11 @@ class CalendarView(QDialog):
|
||||
self.edit_time_button.clicked.connect(self.edit_work_time)
|
||||
button_layout.addWidget(self.edit_time_button)
|
||||
|
||||
self.copy_report_button = QPushButton(tr('cal.copy_report'))
|
||||
self.copy_report_button.setEnabled(False)
|
||||
self.copy_report_button.clicked.connect(self.copy_daily_report)
|
||||
button_layout.addWidget(self.copy_report_button)
|
||||
|
||||
self.delete_record_button = QPushButton(tr('cal.delete_record'))
|
||||
self.delete_record_button.setObjectName("btn_danger")
|
||||
self.delete_record_button.setEnabled(False)
|
||||
@@ -196,10 +201,9 @@ class CalendarView(QDialog):
|
||||
try:
|
||||
wid = self.db.add_work_record(date_str, data['clock_in'], is_manual=True)
|
||||
if data.get('clock_out'):
|
||||
# 총 시간/연장근무 계산
|
||||
from datetime import datetime as _dt
|
||||
ci = _dt.strptime(f"{date_str} {data['clock_in']}", '%Y-%m-%d %H:%M:%S')
|
||||
co = _dt.strptime(f"{date_str} {data['clock_out']}", '%Y-%m-%d %H:%M:%S')
|
||||
# 총 시간/연장근무 계산 (익일 퇴근이면 퇴근에 +1일)
|
||||
from core.time_calculator import resolve_work_datetimes
|
||||
ci, co = resolve_work_datetimes(date_str, data['clock_in'], data['clock_out'])
|
||||
from core.time_calculator import TimeCalculator
|
||||
wm = self.db.get_work_minutes()
|
||||
lunch = self.db.get_setting_int('lunch_duration_minutes', 60)
|
||||
@@ -275,7 +279,11 @@ class CalendarView(QDialog):
|
||||
detail += tr('cal.detail_clock_in', time=record['clock_in']) + '\n'
|
||||
|
||||
if record.get('clock_out'):
|
||||
detail += tr('cal.detail_clock_out', time=record['clock_out']) + '\n'
|
||||
from core.time_calculator import is_overnight
|
||||
out_str = record['clock_out']
|
||||
if is_overnight(record['clock_in'], out_str):
|
||||
out_str = tr('label.next_day_time', time=out_str)
|
||||
detail += tr('cal.detail_clock_out', time=out_str) + '\n'
|
||||
detail += tr('cal.detail_total_hours', hours=record.get('total_hours', 0)) + '\n'
|
||||
|
||||
if record.get('lunch_break'):
|
||||
@@ -302,6 +310,7 @@ class CalendarView(QDialog):
|
||||
self.detail_text.setText(detail)
|
||||
self.edit_time_button.setEnabled(True)
|
||||
self.delete_record_button.setEnabled(True)
|
||||
self.copy_report_button.setEnabled(True)
|
||||
|
||||
# 메모 필드 업데이트
|
||||
self.memo_edit.setPlainText(record.get('memo', ''))
|
||||
@@ -310,9 +319,22 @@ class CalendarView(QDialog):
|
||||
self.detail_text.setText(tr('cal.detail_date_fmt', year=selected_date.year, month=selected_date.month, day=selected_date.day) + '\n\n' + tr('cal.no_record'))
|
||||
self.edit_time_button.setEnabled(False)
|
||||
self.delete_record_button.setEnabled(False)
|
||||
self.copy_report_button.setEnabled(False)
|
||||
self.memo_edit.setPlainText('')
|
||||
self.save_memo_button.setEnabled(False)
|
||||
|
||||
def copy_daily_report(self):
|
||||
"""선택한 날짜의 일일보고서를 클립보드로 복사.
|
||||
|
||||
메인 창의 생성기를 그대로 재사용 — 당일 보고를 놓쳐도 나중에 다시 뽑을 수 있다.
|
||||
"""
|
||||
if not self.selected_date_str:
|
||||
return
|
||||
main_window = self.parent()
|
||||
if main_window is None or not hasattr(main_window, 'generate_daily_report'):
|
||||
return # 단독 실행(__main__) 등 부모가 메인 창이 아닌 경우
|
||||
main_window.generate_daily_report(self.selected_date_str)
|
||||
|
||||
def delete_selected_record(self):
|
||||
"""선택된 날짜의 출근 기록 삭제"""
|
||||
if not self.selected_date_str:
|
||||
@@ -455,6 +477,10 @@ class EditWorkTimeDialog(QDialog):
|
||||
if self.record.get('clock_out'):
|
||||
clock_out_time = QTime.fromString(self.record['clock_out'], "HH:mm:ss")
|
||||
self.clock_out_edit.setTime(clock_out_time)
|
||||
else:
|
||||
# 미퇴근 기록: 기본값 00:00은 "익일 자정 퇴근"으로 오인될 수 있으므로
|
||||
# 출근 시각과 동일하게 두어 save_changes()의 동시각 반려에 걸리게 한다.
|
||||
self.clock_out_edit.setTime(clock_in_time)
|
||||
clock_out_layout.addWidget(self.clock_out_edit)
|
||||
|
||||
clock_out_plus_btn = QPushButton(tr('cal.btn_plus_30'))
|
||||
@@ -463,6 +489,15 @@ class EditWorkTimeDialog(QDialog):
|
||||
clock_out_layout.addWidget(clock_out_plus_btn)
|
||||
layout.addLayout(clock_out_layout)
|
||||
|
||||
# 익일 퇴근 안내 (퇴근 < 출근이면 표시)
|
||||
self.overnight_note = QLabel('')
|
||||
self.overnight_note.setObjectName("note_text")
|
||||
self.overnight_note.setVisible(False)
|
||||
layout.addWidget(self.overnight_note)
|
||||
self.clock_in_edit.timeChanged.connect(self._refresh_overnight_note)
|
||||
self.clock_out_edit.timeChanged.connect(self._refresh_overnight_note)
|
||||
self._refresh_overnight_note()
|
||||
|
||||
# 점심/저녁 체크박스 - 한 줄에
|
||||
from PyQt5.QtWidgets import QCheckBox
|
||||
check_layout = QHBoxLayout()
|
||||
@@ -501,6 +536,18 @@ class EditWorkTimeDialog(QDialog):
|
||||
new_time = current_time.addSecs(minutes * 60)
|
||||
time_edit.setTime(new_time)
|
||||
|
||||
def _refresh_overnight_note(self):
|
||||
"""퇴근이 출근보다 이르면 익일 퇴근으로 해석됨을 표시."""
|
||||
ci = self.clock_in_edit.time()
|
||||
co = self.clock_out_edit.time()
|
||||
if co >= ci:
|
||||
self.overnight_note.setVisible(False)
|
||||
return
|
||||
total_min = ci.secsTo(co) // 60 + 24 * 60 # 익일까지
|
||||
self.overnight_note.setText(tr('edit.overnight_note',
|
||||
hours=total_min // 60, minutes=total_min % 60))
|
||||
self.overnight_note.setVisible(True)
|
||||
|
||||
def save_changes(self):
|
||||
"""변경사항 저장"""
|
||||
clock_in = self.clock_in_edit.time().toString("HH:mm:ss")
|
||||
@@ -508,8 +555,8 @@ class EditWorkTimeDialog(QDialog):
|
||||
lunch_break = self.lunch_check.isChecked()
|
||||
dinner_break = self.dinner_check.isChecked()
|
||||
|
||||
# 퇴근 시간이 출근 시간보다 빠른지 확인
|
||||
if clock_out <= clock_in:
|
||||
# 퇴근 < 출근은 익일 퇴근(야근)으로 허용. 같은 시각만 반려.
|
||||
if clock_out == clock_in:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
tr('cal.time_error_title'),
|
||||
@@ -519,12 +566,10 @@ class EditWorkTimeDialog(QDialog):
|
||||
|
||||
# 근무 시간 계산
|
||||
from datetime import datetime, timedelta
|
||||
from core.time_calculator import TimeCalculator
|
||||
from core.time_calculator import TimeCalculator, resolve_work_datetimes
|
||||
|
||||
# 해당 날짜의 datetime 객체 생성
|
||||
date_obj = datetime.strptime(self.date_str, "%Y-%m-%d").date()
|
||||
clock_in_dt = datetime.combine(date_obj, datetime.strptime(clock_in, "%H:%M:%S").time())
|
||||
clock_out_dt = datetime.combine(date_obj, datetime.strptime(clock_out, "%H:%M:%S").time())
|
||||
# 해당 날짜의 datetime 객체 생성 (익일 퇴근이면 퇴근에 +1일)
|
||||
clock_in_dt, clock_out_dt = resolve_work_datetimes(self.date_str, clock_in, clock_out)
|
||||
|
||||
# 총 근무시간 계산
|
||||
total_hours = (clock_out_dt - clock_in_dt).total_seconds() / 3600
|
||||
|
||||
+93
-101
@@ -25,7 +25,7 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from core.database import Database
|
||||
from core.event_monitor import EventMonitor
|
||||
from core.time_calculator import TimeCalculator
|
||||
from core.time_calculator import TimeCalculator, resolve_work_datetimes
|
||||
from ui.clock_in_dialog import ClockInDialog
|
||||
from ui.calendar_view import CalendarView
|
||||
from ui.stats_view import StatsView
|
||||
@@ -608,9 +608,11 @@ class MainWindow(QMainWindow):
|
||||
use_1hour_leave_button = QPushButton(tr('btn.use_1hour'))
|
||||
use_half_leave_button = QPushButton(tr('btn.half_leave'))
|
||||
use_full_leave_button = QPushButton(tr('btn.full_leave'))
|
||||
use_custom_leave_button = QPushButton(tr('btn.custom_input'))
|
||||
leave_detail_button = QPushButton(tr('btn.detail'))
|
||||
|
||||
for btn in [use_30min_leave_button, use_1hour_leave_button, use_half_leave_button, use_full_leave_button, leave_detail_button]:
|
||||
for btn in [use_30min_leave_button, use_1hour_leave_button, use_half_leave_button,
|
||||
use_full_leave_button, use_custom_leave_button, leave_detail_button]:
|
||||
btn.setObjectName("btn_small")
|
||||
leave_button_layout.addWidget(btn)
|
||||
|
||||
@@ -618,6 +620,7 @@ class MainWindow(QMainWindow):
|
||||
use_1hour_leave_button.clicked.connect(lambda: self.use_leave(1.0/8)) # 0.125일
|
||||
use_half_leave_button.clicked.connect(lambda: self.use_leave(0.5))
|
||||
use_full_leave_button.clicked.connect(lambda: self.use_leave(1.0))
|
||||
use_custom_leave_button.clicked.connect(self.use_custom_leave) # 연장근무와 동일한 상세입력
|
||||
leave_detail_button.clicked.connect(self.show_leave_detail)
|
||||
|
||||
layout.addLayout(leave_button_layout)
|
||||
@@ -1060,8 +1063,13 @@ class MainWindow(QMainWindow):
|
||||
# 다이얼로그 종료 후 잔액 업데이트
|
||||
self.update_overtime_balance()
|
||||
|
||||
def use_leave(self, days: float):
|
||||
"""연차 사용"""
|
||||
def use_leave(self, days: float, date_str: str = None, memo: str = None):
|
||||
"""연차 사용.
|
||||
|
||||
date_str 생략 시 오늘 날짜로 바로 확인만 받고 처리 — 연장근무 사용
|
||||
(use_overtime)과 같은 흐름. 날짜·사유를 지정하려면 상세입력
|
||||
(use_custom_leave → AddLeaveDialog)을 쓴다.
|
||||
"""
|
||||
balance = self.db.get_leave_balance()
|
||||
|
||||
if balance < days:
|
||||
@@ -1072,44 +1080,21 @@ class MainWindow(QMainWindow):
|
||||
)
|
||||
return
|
||||
|
||||
# 사용 날짜 입력
|
||||
from PyQt5.QtWidgets import QInputDialog, QLineEdit
|
||||
from datetime import date
|
||||
|
||||
today = date.today().isoformat()
|
||||
date_str, ok = QInputDialog.getText(
|
||||
self,
|
||||
tr('msg.leave_use_date.title'),
|
||||
tr('msg.leave_use_date.body'),
|
||||
QLineEdit.Normal,
|
||||
today
|
||||
)
|
||||
|
||||
if not ok or not date_str:
|
||||
return
|
||||
|
||||
# 날짜 형식 검증
|
||||
try:
|
||||
datetime.strptime(date_str, "%Y-%m-%d")
|
||||
except ValueError:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
tr('msg.input_error.title'),
|
||||
tr('msg.input_error.date_format')
|
||||
)
|
||||
return
|
||||
|
||||
# 메모 입력
|
||||
memo, ok = QInputDialog.getText(
|
||||
self,
|
||||
tr('msg.leave_use_reason.title'),
|
||||
tr('msg.leave_use_reason.body'),
|
||||
QLineEdit.Normal,
|
||||
""
|
||||
)
|
||||
|
||||
if not ok:
|
||||
return
|
||||
if date_str is None:
|
||||
date_str = date.today().isoformat()
|
||||
else:
|
||||
# 날짜 형식 검증
|
||||
try:
|
||||
datetime.strptime(date_str, "%Y-%m-%d")
|
||||
except ValueError:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
tr('msg.input_error.title'),
|
||||
tr('msg.input_error.date_format')
|
||||
)
|
||||
return
|
||||
|
||||
# 사용 확인
|
||||
if days == 1.0:
|
||||
@@ -1197,38 +1182,18 @@ class MainWindow(QMainWindow):
|
||||
self.use_overtime(minutes)
|
||||
|
||||
def use_custom_leave(self):
|
||||
"""사용자 정의 연차 사용"""
|
||||
from PyQt5.QtWidgets import QInputDialog
|
||||
"""연차 상세 입력 — 날짜(캘린더)/유형/시간/사유를 한 화면에서.
|
||||
|
||||
balance = self.db.get_leave_balance()
|
||||
연차 내역 화면의 AddLeaveDialog를 그대로 재사용한다. 그쪽은 잔액 외에
|
||||
주말·공휴일 차단과 같은 날 1일 초과 중복 검증까지 갖추고 있어,
|
||||
메인 창에서 바로 띄우는 편이 QInputDialog 연쇄보다 안전하고 간편하다.
|
||||
"""
|
||||
from PyQt5.QtWidgets import QDialog
|
||||
from ui.leave_view import AddLeaveDialog
|
||||
|
||||
# 사용할 시간 입력 (시간 단위)
|
||||
hours, ok = QInputDialog.getDouble(
|
||||
self,
|
||||
tr('msg.input.title'),
|
||||
tr('msg.leave_input.body'),
|
||||
0.5,
|
||||
0.5,
|
||||
80.0,
|
||||
1
|
||||
)
|
||||
|
||||
if not ok:
|
||||
return
|
||||
|
||||
# 시간을 일수로 변환 (8시간 = 1일)
|
||||
days = hours / 8.0
|
||||
|
||||
if days > balance:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
tr('msg.leave_short.title'),
|
||||
tr('msg.leave_short_hours.body', balance=balance, balance_hours=int(balance * 8), days=days, hours=hours)
|
||||
)
|
||||
return
|
||||
|
||||
# use_leave 메서드 호출
|
||||
self.use_leave(days)
|
||||
dialog = AddLeaveDialog(self, self.db)
|
||||
if dialog.exec_() == QDialog.Accepted:
|
||||
self.update_leave_balance()
|
||||
|
||||
def show_leave_detail(self):
|
||||
"""연차 상세 내역 보기"""
|
||||
@@ -1247,12 +1212,32 @@ class MainWindow(QMainWindow):
|
||||
# 퇴근 완료 -> 퇴근 취소
|
||||
self.cancel_clock_out()
|
||||
|
||||
def current_workday_date(self) -> str:
|
||||
"""진행 중인(또는 방금 끝난) 근무일 날짜 'YYYY-MM-DD'.
|
||||
|
||||
자정~근무일 경계시간(기본 6시) 사이에는 달력상 날짜가 이미 넘어갔어도
|
||||
출근일이 근무일이다. handle_workday_rollover()가 경계시간 전까지
|
||||
전날 근무를 유지하는 것과 같은 기준.
|
||||
"""
|
||||
if self.is_clocked_in and self.clock_in_time:
|
||||
return self.clock_in_time.date().isoformat()
|
||||
now = datetime.now()
|
||||
boundary = int(self.db.get_setting(WORKDAY_BOUNDARY_HOUR, '6'))
|
||||
today_str = now.date().isoformat()
|
||||
# 새벽에 퇴근 직후: 오늘 기록이 아직 없으면 전날이 근무일
|
||||
if now.hour < boundary and not self.db.get_work_record(today_str):
|
||||
return (now.date() - timedelta(days=1)).isoformat()
|
||||
return today_str
|
||||
|
||||
def clock_out(self):
|
||||
"""퇴근 처리"""
|
||||
if not self.is_clocked_in:
|
||||
return
|
||||
|
||||
now = datetime.now()
|
||||
# 근무일 = 출근일. 자정 넘겨 퇴근하면 now.date()는 이미 다음 날이므로
|
||||
# 이 값을 써야 출근일 행에 퇴근이 기록된다 (UPDATE 0건 방지).
|
||||
workday = self.clock_in_time.date().isoformat()
|
||||
|
||||
# 확인 메시지
|
||||
reply = QMessageBox.question(
|
||||
@@ -1272,12 +1257,13 @@ class MainWindow(QMainWindow):
|
||||
self.clock_in_time, now
|
||||
)
|
||||
|
||||
# 주말/공휴일 체크
|
||||
is_non_working_day = self.time_calc.is_non_working_day(now, self.db)
|
||||
day_type = self.time_calc.get_day_type(now, self.db)
|
||||
# 주말/공휴일 체크 — 근무 성격은 출근일로 판정 (금요일 야근이 토요일
|
||||
# 근무로 뒤바뀌지 않게). 롤오버 경로와 동일 기준.
|
||||
is_non_working_day = self.time_calc.is_non_working_day(self.clock_in_time, self.db)
|
||||
day_type = self.time_calc.get_day_type(self.clock_in_time, self.db)
|
||||
|
||||
# 오늘의 외출 시간 가져오기
|
||||
break_minutes = self.db.get_total_break_minutes_today()
|
||||
# 근무일의 외출 시간 가져오기
|
||||
break_minutes = self.db.get_total_break_minutes_today(workday)
|
||||
|
||||
# 적립 단위(분) — 사용자 설정. 기본 30, 옵션 15/60.
|
||||
unit_minutes = self.db.get_setting_int('overtime_unit', 30)
|
||||
@@ -1318,7 +1304,7 @@ class MainWindow(QMainWindow):
|
||||
overtime_earned = 0 # 적립 스킵 (overtime_actual은 기록용으로 유지)
|
||||
|
||||
# DB 업데이트
|
||||
today = datetime.now().date().isoformat()
|
||||
today = workday
|
||||
clock_out_str = now.strftime("%H:%M:%S")
|
||||
|
||||
self.db.update_clock_out(
|
||||
@@ -1328,7 +1314,7 @@ class MainWindow(QMainWindow):
|
||||
|
||||
# 연장근무 적립 기록
|
||||
if overtime_earned > 0:
|
||||
today_record = self.db.get_today_record()
|
||||
today_record = self.db.get_work_record(today)
|
||||
if today_record:
|
||||
self.db.add_overtime_earned(
|
||||
today_record['id'], overtime_earned, today
|
||||
@@ -1387,9 +1373,8 @@ class MainWindow(QMainWindow):
|
||||
return
|
||||
|
||||
try:
|
||||
# DB에서 퇴근 취소
|
||||
today = datetime.now().date().isoformat()
|
||||
success = self.db.cancel_clock_out(today)
|
||||
# DB에서 퇴근 취소 — 새벽 퇴근이면 달력 날짜가 아니라 출근일 행
|
||||
success = self.db.cancel_clock_out(self.current_workday_date())
|
||||
|
||||
if success:
|
||||
# 상태 복원
|
||||
@@ -2315,10 +2300,13 @@ class MainWindow(QMainWindow):
|
||||
return
|
||||
report_lines.append("")
|
||||
|
||||
def generate_daily_report(self):
|
||||
"""오늘 하루 근무 내역 보고서 생성 및 클립보드 복사"""
|
||||
from datetime import date
|
||||
def generate_daily_report(self, target_date: str = None):
|
||||
"""근무 내역 보고서 생성 및 클립보드 복사.
|
||||
|
||||
Args:
|
||||
target_date: 'YYYY-MM-DD'. 생략 시 진행 중인 근무일
|
||||
(자정~경계시간 사이면 출근일). 캘린더에서 과거 날짜로도 호출.
|
||||
"""
|
||||
# 도전과제 카운터 (보고서 생성 횟수)
|
||||
try:
|
||||
cur = self.db.get_setting_int('daily_report_count', 0)
|
||||
@@ -2326,10 +2314,10 @@ class MainWindow(QMainWindow):
|
||||
except Exception as e:
|
||||
dlog(f"daily report counter failed: {e}")
|
||||
|
||||
today = date.today().isoformat()
|
||||
date_str = target_date or self.current_workday_date()
|
||||
|
||||
# 오늘의 근무 기록 조회
|
||||
work_record = self.db.get_today_record()
|
||||
# 해당 근무일의 기록 조회
|
||||
work_record = self.db.get_work_record(date_str)
|
||||
|
||||
if not work_record:
|
||||
QMessageBox.warning(
|
||||
@@ -2342,17 +2330,21 @@ class MainWindow(QMainWindow):
|
||||
# 보고서 작성
|
||||
report_lines = []
|
||||
report_lines.append("=" * 40)
|
||||
report_lines.append(tr('report.title', date=today))
|
||||
report_lines.append(tr('report.title', date=date_str))
|
||||
report_lines.append("=" * 40)
|
||||
report_lines.append("")
|
||||
|
||||
# 출근/퇴근 시간
|
||||
clock_in_dt = datetime.fromisoformat(f"{today} {work_record['clock_in']}")
|
||||
# 출근/퇴근 시간 (퇴근이 출근보다 이르면 익일 퇴근)
|
||||
clock_in_dt = datetime.fromisoformat(f"{date_str} {work_record['clock_in']}")
|
||||
report_lines.append(tr('report.clock_in', time=self.format_time(clock_in_dt, include_seconds=True)))
|
||||
|
||||
if work_record['clock_out']:
|
||||
clock_out_dt = datetime.fromisoformat(f"{today} {work_record['clock_out']}")
|
||||
report_lines.append(tr('report.clock_out', time=self.format_time(clock_out_dt, include_seconds=True)))
|
||||
_, clock_out_dt = resolve_work_datetimes(
|
||||
date_str, work_record['clock_in'], work_record['clock_out'])
|
||||
out_str = self.format_time(clock_out_dt, include_seconds=True)
|
||||
if clock_out_dt.date() != clock_in_dt.date():
|
||||
out_str = tr('label.next_day_time', time=out_str)
|
||||
report_lines.append(tr('report.clock_out', time=out_str))
|
||||
|
||||
# 총 근무 시간
|
||||
total_work_hours = work_record.get('total_hours') or work_record.get('work_hours', 0)
|
||||
@@ -2367,7 +2359,7 @@ class MainWindow(QMainWindow):
|
||||
|
||||
# 외출 / 점심 / 저녁 분리 — break_type 으로 구분 (v2.7.0+)
|
||||
# 'break'(또는 NULL) = 일반 외출, 'lunch'/'dinner' = 실측 식사 기록
|
||||
all_break_records = self.db.get_today_break_records()
|
||||
all_break_records = self.db.get_break_records_by_date(date_str)
|
||||
real_break_records = [b for b in all_break_records
|
||||
if (b.get('break_type') or 'break') == 'break']
|
||||
lunch_records = [b for b in all_break_records if b.get('break_type') == 'lunch']
|
||||
@@ -2379,9 +2371,9 @@ class MainWindow(QMainWindow):
|
||||
if real_break_minutes > 0 or has_active_break:
|
||||
report_lines.append(tr('report.break_time', time=format_hours_minutes(real_break_minutes)))
|
||||
for br in real_break_records:
|
||||
break_out_time = datetime.fromisoformat(f"{today} {br['break_out']}")
|
||||
break_out_time = datetime.fromisoformat(f"{date_str} {br['break_out']}")
|
||||
if br['break_in']:
|
||||
break_in_time = datetime.fromisoformat(f"{today} {br['break_in']}")
|
||||
break_in_time = datetime.fromisoformat(f"{date_str} {br['break_in']}")
|
||||
# 자정 경계 처리: 복귀 시간이 외출 시간보다 이전이면 다음날로 간주
|
||||
if break_in_time < break_out_time:
|
||||
break_in_time += timedelta(days=1)
|
||||
@@ -2397,7 +2389,7 @@ class MainWindow(QMainWindow):
|
||||
lunch_flag = bool(work_record.get('lunch_break', False))
|
||||
if lunch_flag or lunch_records:
|
||||
self._append_meal_section(
|
||||
report_lines, today, tr('label.lunch'),
|
||||
report_lines, date_str, tr('label.lunch'),
|
||||
lunch_flag, lunch_records,
|
||||
self.time_calc.lunch_duration_minutes,
|
||||
)
|
||||
@@ -2406,7 +2398,7 @@ class MainWindow(QMainWindow):
|
||||
dinner_flag = bool(work_record.get('dinner_break', False))
|
||||
if dinner_flag or dinner_records:
|
||||
self._append_meal_section(
|
||||
report_lines, today, tr('label.dinner'),
|
||||
report_lines, date_str, tr('label.dinner'),
|
||||
dinner_flag, dinner_records,
|
||||
self.time_calc.dinner_duration_minutes,
|
||||
)
|
||||
@@ -2425,8 +2417,8 @@ class MainWindow(QMainWindow):
|
||||
report_lines.append(tr('report.overtime_banked', time=tr('label.time_hours_minutes', hours=earned_hours, minutes=earned_mins)))
|
||||
report_lines.append("")
|
||||
|
||||
# 오늘 사용한 추가근무
|
||||
overtime_used_today = self.db.get_today_overtime_usage()
|
||||
# 해당 날짜에 사용한 추가근무
|
||||
overtime_used_today = self.db.get_today_overtime_usage(date_str)
|
||||
if overtime_used_today > 0:
|
||||
used_hours = overtime_used_today // 60
|
||||
used_mins = overtime_used_today % 60
|
||||
@@ -2440,7 +2432,7 @@ class MainWindow(QMainWindow):
|
||||
FROM overtime_usage
|
||||
WHERE date = ?
|
||||
ORDER BY created_at ASC
|
||||
''', (today,))
|
||||
''', (date_str,))
|
||||
usage_records = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
@@ -2453,8 +2445,8 @@ class MainWindow(QMainWindow):
|
||||
report_lines.append(tr('report.overtime_used_detail', time=tr('label.time_hours_minutes', hours=used_h, minutes=used_m), reason=reason_text))
|
||||
report_lines.append("")
|
||||
|
||||
# 오늘 사용한 연차 (일괄 추가 및 수동 조정 제외)
|
||||
leave_records = self.db.get_leave_records(start_date=today, end_date=today, exclude_bulk=False)
|
||||
# 해당 날짜에 사용한 연차 (일괄 추가 및 수동 조정 제외)
|
||||
leave_records = self.db.get_leave_records(start_date=date_str, end_date=date_str, exclude_bulk=False)
|
||||
|
||||
# manual 타입이거나 메모에 "일괄 추가"가 포함된 것은 제외
|
||||
filtered_leave_records = [
|
||||
|
||||
@@ -21,7 +21,7 @@ class PastRecordDialog(QDialog):
|
||||
self.date_str = date_str
|
||||
self.setWindowTitle(tr('past_record.dialog_title', date=date_str))
|
||||
self.setModal(True)
|
||||
self.setFixedSize(380, 320)
|
||||
self.setFixedSize(380, 348)
|
||||
|
||||
layout = QVBoxLayout()
|
||||
layout.setSpacing(8)
|
||||
@@ -55,6 +55,15 @@ class PastRecordDialog(QDialog):
|
||||
co_row.addStretch()
|
||||
layout.addLayout(co_row)
|
||||
|
||||
# 익일 퇴근 안내 (퇴근 < 출근이면 표시)
|
||||
self.overnight_note = QLabel('')
|
||||
self.overnight_note.setObjectName("note_text")
|
||||
self.overnight_note.setVisible(False)
|
||||
layout.addWidget(self.overnight_note)
|
||||
self.clock_in_edit.timeChanged.connect(self._refresh_overnight_note)
|
||||
self.clock_out_edit.timeChanged.connect(self._refresh_overnight_note)
|
||||
self.clock_out_check.toggled.connect(self._refresh_overnight_note)
|
||||
|
||||
# 점심/저녁
|
||||
meal_row = QHBoxLayout()
|
||||
self.lunch_check = QCheckBox(tr('past_record.check_lunch'))
|
||||
@@ -88,11 +97,25 @@ class PastRecordDialog(QDialog):
|
||||
self.setLayout(layout)
|
||||
apply_dark_titlebar(self)
|
||||
|
||||
def _refresh_overnight_note(self):
|
||||
"""퇴근이 출근보다 이르면 익일 퇴근으로 해석됨을 표시."""
|
||||
if not self.clock_out_check.isChecked():
|
||||
self.overnight_note.setVisible(False)
|
||||
return
|
||||
ci = self.clock_in_edit.time()
|
||||
co = self.clock_out_edit.time()
|
||||
if co >= ci:
|
||||
self.overnight_note.setVisible(False)
|
||||
return
|
||||
total_min = ci.secsTo(co) // 60 + 24 * 60 # 익일까지
|
||||
self.overnight_note.setText(tr('edit.overnight_note',
|
||||
hours=total_min // 60, minutes=total_min % 60))
|
||||
self.overnight_note.setVisible(True)
|
||||
|
||||
def _validate_and_accept(self):
|
||||
# 퇴근 < 출근은 익일 퇴근(야근)으로 허용. 같은 시각만 반려.
|
||||
if self.clock_out_check.isChecked():
|
||||
ci = self.clock_in_edit.time()
|
||||
co = self.clock_out_edit.time()
|
||||
if co <= ci:
|
||||
if self.clock_out_edit.time() == self.clock_in_edit.time():
|
||||
QMessageBox.warning(self, tr('past_record.input_error_title'),
|
||||
tr('past_record.input_error_body'))
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user