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
+10
-6
@@ -1026,10 +1026,10 @@ class Database:
|
||||
balance = cursor.fetchone()[0]
|
||||
return initial_overtime + balance
|
||||
|
||||
def get_today_overtime_usage(self) -> int:
|
||||
"""오늘 사용한 추가근무 시간 조회 (분)"""
|
||||
def get_today_overtime_usage(self, date_str: str = None) -> int:
|
||||
"""오늘 사용한 추가근무 시간 조회 (분). date_str 지정 시 그 날짜 기준."""
|
||||
from datetime import date
|
||||
today = date.today().isoformat()
|
||||
today = date_str or date.today().isoformat()
|
||||
with self._conn() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT SUM(used_minutes) FROM overtime_usage WHERE date = ?',
|
||||
@@ -1590,10 +1590,14 @@ class Database:
|
||||
cursor.execute('DELETE FROM break_records WHERE id = ?', (break_id,))
|
||||
conn.commit()
|
||||
|
||||
def get_total_break_minutes_today(self) -> int:
|
||||
"""오늘의 총 외출 시간 (분), 진행 중인 외출 포함"""
|
||||
def get_total_break_minutes_today(self, date_str: str = None) -> int:
|
||||
"""오늘의 총 외출 시간 (분), 진행 중인 외출 포함.
|
||||
|
||||
date_str 지정 시 그 근무일 기준 (야근으로 자정을 넘긴 뒤 퇴근할 때
|
||||
호출자가 출근일을 넘겨줘야 그 날의 외출이 집계된다).
|
||||
"""
|
||||
from datetime import date, datetime
|
||||
today = date.today().isoformat()
|
||||
today = date_str or date.today().isoformat()
|
||||
with self._conn() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
|
||||
+12
-4
@@ -466,6 +466,10 @@ _DICT = {
|
||||
'report.memo': '📝 메모: {memo}',
|
||||
'report.copied.title': '보고서 복사 완료',
|
||||
'report.copied.body': '일일 근무 보고서가 클립보드에 복사되었습니다.\n\n{report}',
|
||||
# 익일(자정 넘김) 퇴근 표기
|
||||
'label.next_day_time': '익일 {time}',
|
||||
'edit.overnight_note': '🌙 익일 퇴근으로 저장됩니다 (출근~퇴근 {hours}시간 {minutes}분)',
|
||||
'cal.copy_report': '보고서 복사',
|
||||
'label.lunch': '🍱 점심시간',
|
||||
'label.lunch_short': '점심',
|
||||
'label.dinner': '🍽️ 저녁시간',
|
||||
@@ -732,7 +736,7 @@ _DICT = {
|
||||
'cal.save_memo': '메모 저장',
|
||||
'cal.save_memo_body': '{date}의 메모가 저장되었습니다.',
|
||||
'cal.save_memo_title': '메모 저장',
|
||||
'cal.time_error_body': '퇴근 시간은 출근 시간보다 늦어야 합니다.',
|
||||
'cal.time_error_body': '퇴근 시간이 출근 시간과 같습니다.\n(퇴근이 출근보다 이르면 익일 퇴근으로 저장됩니다.)',
|
||||
'cal.time_error_title': '시간 오류',
|
||||
'clock_in_dialog.cancelled': '취소됨',
|
||||
'clock_in_dialog.selected': '선택된 시간: {time}',
|
||||
@@ -814,7 +818,7 @@ _DICT = {
|
||||
'past_record.check_lunch': '점심시간 포함',
|
||||
'past_record.dialog_title': '기록 추가 — {date}',
|
||||
'past_record.info': '{date} 근무 기록을 입력하세요.',
|
||||
'past_record.input_error_body': '퇴근 시간이 출근 시간보다 빠르거나 같습니다.',
|
||||
'past_record.input_error_body': '퇴근 시간이 출근 시간과 같습니다.\n(퇴근이 출근보다 이르면 익일 퇴근으로 저장됩니다.)',
|
||||
'past_record.input_error_title': '입력 오류',
|
||||
'past_record.label_clock_in': '출근:',
|
||||
'past_record.label_clock_out': '퇴근:',
|
||||
@@ -1625,6 +1629,10 @@ _DICT = {
|
||||
'report.memo': '📝 Memo: {memo}',
|
||||
'report.copied.title': 'Report Copied',
|
||||
'report.copied.body': 'Daily work report copied to clipboard.\n\n{report}',
|
||||
# Next-day (past-midnight) clock-out
|
||||
'label.next_day_time': 'next day {time}',
|
||||
'edit.overnight_note': '🌙 Saved as a next-day clock-out ({hours}h {minutes}m in to out)',
|
||||
'cal.copy_report': 'Copy Report',
|
||||
'label.lunch': '🍱 Lunch',
|
||||
'label.lunch_short': 'Lunch',
|
||||
'label.dinner': '🍽️ Dinner',
|
||||
@@ -1891,7 +1899,7 @@ _DICT = {
|
||||
'cal.save_memo': 'Save Memo',
|
||||
'cal.save_memo_body': 'Memo saved for {date}.',
|
||||
'cal.save_memo_title': 'Save Memo',
|
||||
'cal.time_error_body': 'Clock-out must be later than clock-in.',
|
||||
'cal.time_error_body': 'Clock-out is the same as clock-in.\n(An earlier clock-out is saved as the next day.)',
|
||||
'cal.time_error_title': 'Time Error',
|
||||
'clock_in_dialog.cancelled': 'Cancelled',
|
||||
'clock_in_dialog.selected': 'Selected time: {time}',
|
||||
@@ -1973,7 +1981,7 @@ _DICT = {
|
||||
'past_record.check_lunch': 'Include lunch',
|
||||
'past_record.dialog_title': 'Add Record — {date}',
|
||||
'past_record.info': 'Enter work record for {date}.',
|
||||
'past_record.input_error_body': 'Clock-out must be later than clock-in.',
|
||||
'past_record.input_error_body': 'Clock-out is the same as clock-in.\n(An earlier clock-out is saved as the next day.)',
|
||||
'past_record.input_error_title': 'Input Error',
|
||||
'past_record.label_clock_in': 'Clock-in:',
|
||||
'past_record.label_clock_out': 'Clock-out:',
|
||||
|
||||
@@ -6,6 +6,32 @@ from datetime import datetime, time, timedelta
|
||||
from typing import Tuple, Optional
|
||||
|
||||
|
||||
def resolve_work_datetimes(date_str: str, clock_in: str, clock_out: str) -> Tuple[datetime, datetime]:
|
||||
"""출근일 기준 (출근, 퇴근) datetime. 퇴근이 출근보다 이르면 익일로 해석.
|
||||
|
||||
work_records는 출근일 날짜 한 행에 clock_in/clock_out TIME만 저장하므로
|
||||
"09:00 출근 → 01:30 퇴근"(야근)은 clock_out < clock_in 으로 나타난다.
|
||||
break_records의 자정 처리(복귀 < 외출 → 익일)와 같은 규칙.
|
||||
|
||||
Args:
|
||||
date_str: 출근일 'YYYY-MM-DD'
|
||||
clock_in / clock_out: 'HH:MM:SS' (또는 'HH:MM')
|
||||
"""
|
||||
day = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||||
fmt = '%H:%M:%S' if clock_in.count(':') == 2 else '%H:%M'
|
||||
ci = datetime.combine(day, datetime.strptime(clock_in, fmt).time())
|
||||
fmt = '%H:%M:%S' if clock_out.count(':') == 2 else '%H:%M'
|
||||
co = datetime.combine(day, datetime.strptime(clock_out, fmt).time())
|
||||
if co < ci:
|
||||
co += timedelta(days=1)
|
||||
return ci, co
|
||||
|
||||
|
||||
def is_overnight(clock_in: str, clock_out: str) -> bool:
|
||||
"""퇴근 시각이 익일인지 (문자열 비교 — 'HH:MM:SS' 는 사전순=시간순)."""
|
||||
return bool(clock_out) and clock_out < clock_in
|
||||
|
||||
|
||||
class TimeCalculator:
|
||||
"""근무시간 계산 클래스"""
|
||||
|
||||
|
||||
+1
-1
@@ -4,4 +4,4 @@
|
||||
릴리스 시 이 값을 올린 후 git tag → push.
|
||||
CHANGELOG.md의 최상단 항목과 일치시킬 것.
|
||||
"""
|
||||
__version__ = '2.12.0'
|
||||
__version__ = '2.13.0'
|
||||
|
||||
Reference in New Issue
Block a user