月次レポート機能を実装し、経費の取り消し・復帰機能を追加。CSV取込画面にドラッグ&ドロップエリアを実装し、エラーメッセージ表示を追加。金額のカンマ区切り表示を全体に適用。faviconとapple-touch-iconを追加し、404エラーを回避。作業ログをdiary.mdに追記。

This commit is contained in:
president
2025-12-21 16:36:39 +09:00
parent d301ddcbfb
commit 7ae367cd66
17 changed files with 682 additions and 21 deletions

View File

@@ -1,5 +1,6 @@
import io
import json
from datetime import date, datetime
from urllib.parse import urlencode
import chardet
@@ -8,7 +9,7 @@ from django.shortcuts import redirect
from django.shortcuts import get_object_or_404, render
from django.views.decorators.http import require_POST
from .services import import_csv_lines
from .services import build_monthly_report, import_csv_lines
from .models import Expense, ExpenseCategory, Store
@@ -21,7 +22,11 @@ def csv_upload(request):
encoding = detected.get('encoding') or 'utf-8'
text = raw.decode(encoding, errors='ignore')
lines = io.StringIO(text)
result = import_csv_lines(lines)
try:
result = import_csv_lines(lines)
except ValueError as exc:
context['error_message'] = str(exc)
return render(request, 'expenses/csv_upload.html', context)
query = urlencode(
{'imported': result.imported, 'duplicated': result.duplicated, 'encoding': encoding}
)
@@ -30,11 +35,11 @@ def csv_upload(request):
def expense_list(request):
expenses = (
Expense.objects.filter(is_canceled=False)
.select_related('store', 'expense_category')
.order_by('-use_date', '-id')[:200]
)
include_canceled = request.GET.get('include_canceled') == '1'
queryset = Expense.objects.select_related('store', 'expense_category')
if not include_canceled:
queryset = queryset.filter(is_canceled=False)
expenses = queryset.order_by('-use_date', '-id')[:200]
stores = Store.objects.filter(is_active=True).order_by('name')
categories = ExpenseCategory.objects.filter(is_active=True).order_by('name')
return render(
@@ -47,6 +52,7 @@ def expense_list(request):
'imported': request.GET.get('imported'),
'duplicated': request.GET.get('duplicated'),
'encoding': request.GET.get('encoding'),
'include_canceled': include_canceled,
},
)
@@ -104,8 +110,70 @@ def expense_update(request, expense_id: int):
def monthly_report(request):
return render(request, 'expenses/monthly_report.html')
error_message = None
target_month = request.GET.get('target_month')
if target_month:
try:
start_date = datetime.strptime(target_month, '%Y-%m').date().replace(day=1)
except ValueError:
error_message = '年月の形式が不正です。'
start_date = date.today().replace(day=1)
else:
start_date = date.today().replace(day=1)
if start_date.month == 12:
end_date = start_date.replace(year=start_date.year + 1, month=1)
else:
end_date = start_date.replace(month=start_date.month + 1)
report = build_monthly_report(start_date, end_date)
return render(
request,
'expenses/monthly_report.html',
{
'report': report,
'target_month': start_date.strftime('%Y-%m'),
'error_message': error_message,
},
)
def monthly_report_pdf(request):
return render(request, 'expenses/monthly_report_pdf.html')
target_month = request.GET.get('target_month')
if target_month:
try:
start_date = datetime.strptime(target_month, '%Y-%m').date().replace(day=1)
except ValueError:
start_date = date.today().replace(day=1)
else:
start_date = date.today().replace(day=1)
if start_date.month == 12:
end_date = start_date.replace(year=start_date.year + 1, month=1)
else:
end_date = start_date.replace(month=start_date.month + 1)
report = build_monthly_report(start_date, end_date)
return render(
request,
'expenses/monthly_report_pdf.html',
{
'report': report,
'target_month': start_date.strftime('%Y-%m'),
},
)
@require_POST
def expense_cancel(request, expense_id: int):
try:
payload = json.loads(request.body.decode('utf-8'))
except json.JSONDecodeError:
return JsonResponse({'status': 'error', 'message': 'JSON形式が不正です。'}, status=400)
if not isinstance(payload, dict):
return JsonResponse({'status': 'error', 'message': 'JSONはオブジェクトで送信してください。'}, status=400)
if 'is_canceled' not in payload:
return JsonResponse({'status': 'error', 'message': 'is_canceledが必要です。'}, status=400)
if not isinstance(payload['is_canceled'], bool):
return JsonResponse({'status': 'error', 'message': 'is_canceledの型が不正です。'}, status=400)
expense = get_object_or_404(Expense, pk=expense_id)
expense.is_canceled = payload['is_canceled']
expense.human_confirmed = True
expense.save(update_fields=['is_canceled', 'human_confirmed', 'updated_at'])
return JsonResponse({'status': 'ok'})