月次レポート機能の改善として、開始日・終了日によるフィルタリング機能を追加し、店舗区分および経費区分の選択肢を実装。経費区分と店舗マスタの管理画面を新規作成し、関連するマイグレーションを追加。CSV取込画面における経費区分の表示を改善。作業ログをdiary.mdに追記。

This commit is contained in:
president
2025-12-22 12:34:53 +09:00
parent 7ae367cd66
commit 814477b1e2
15 changed files with 684 additions and 37 deletions

View File

@@ -1,9 +1,10 @@
import io
import json
from datetime import date, datetime
from datetime import date, datetime, timedelta
from urllib.parse import urlencode
import chardet
from django.db import IntegrityError
from django.http import JsonResponse
from django.shortcuts import redirect
from django.shortcuts import get_object_or_404, render
@@ -57,6 +58,78 @@ def expense_list(request):
)
def store_master(request):
error_message = None
if request.method == 'POST':
name = request.POST.get('name', '').strip()
store_id = request.POST.get('store_id')
action = request.POST.get('action')
if action == 'deactivate' and store_id:
store = get_object_or_404(Store, pk=store_id)
store.is_active = False
store.save(update_fields=['is_active'])
else:
is_active = request.POST.get('is_active') == '1'
if not name:
error_message = '店舗名を入力してください。'
else:
try:
if store_id:
store = get_object_or_404(Store, pk=store_id)
store.name = name
store.is_active = is_active
store.save(update_fields=['name', 'is_active'])
else:
Store.objects.create(name=name, is_active=is_active)
except IntegrityError:
error_message = '同名の店舗が既に存在します。'
stores = Store.objects.order_by('name', 'id')
return render(
request,
'expenses/store_master.html',
{
'stores': stores,
'error_message': error_message,
},
)
def expense_category_master(request):
error_message = None
if request.method == 'POST':
name = request.POST.get('name', '').strip()
category_id = request.POST.get('category_id')
action = request.POST.get('action')
if action == 'deactivate' and category_id:
category = get_object_or_404(ExpenseCategory, pk=category_id)
category.is_active = False
category.save(update_fields=['is_active'])
else:
is_active = request.POST.get('is_active') == '1'
if not name:
error_message = '経費区分名を入力してください。'
else:
try:
if category_id:
category = get_object_or_404(ExpenseCategory, pk=category_id)
category.name = name
category.is_active = is_active
category.save(update_fields=['name', 'is_active'])
else:
ExpenseCategory.objects.create(name=name, is_active=is_active)
except IntegrityError:
error_message = '同名の経費区分が既に存在します。'
categories = ExpenseCategory.objects.order_by('name', 'id')
return render(
request,
'expenses/expense_category_master.html',
{
'categories': categories,
'error_message': error_message,
},
)
@require_POST
def expense_update(request, expense_id: int):
try:
@@ -111,51 +184,165 @@ def expense_update(request, expense_id: int):
def monthly_report(request):
error_message = None
target_month = request.GET.get('target_month')
if target_month:
start_date_param = request.GET.get('start_date', '')
end_date_param = request.GET.get('end_date', '')
all_time = request.GET.get('all_time') == '1'
store_param = request.GET.get('store_id', '')
category_param = request.GET.get('expense_category_id', '')
description_query = request.GET.get('description', '').strip()
show_report = request.GET.get('show') == '1'
store_id = None
store_is_null = False
expense_category_id = None
expense_category_is_null = False
if store_param == 'unassigned':
store_is_null = True
elif store_param:
try:
start_date = datetime.strptime(target_month, '%Y-%m').date().replace(day=1)
store_id = int(store_param)
except ValueError:
error_message = '年月の形式が不正です。'
error_message = '店舗区分の指定が不正です。'
if category_param == 'unassigned':
expense_category_is_null = True
elif category_param:
try:
expense_category_id = int(category_param)
except ValueError:
error_message = '経費区分の指定が不正です。'
if start_date_param or end_date_param:
all_time = False
if all_time:
start_date = None
end_date = None
end_date_display = ''
else:
start_date = None
end_date = None
end_date_display = end_date_param
if start_date_param:
try:
start_date = datetime.strptime(start_date_param, '%Y-%m-%d').date()
except ValueError:
error_message = '開始日の形式が不正です。'
if end_date_param:
try:
end_date = datetime.strptime(end_date_param, '%Y-%m-%d').date()
except ValueError:
error_message = '終了日の形式が不正です。'
if start_date is None and end_date is None and error_message is None:
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)
if start_date.month == 12:
next_month = start_date.replace(year=start_date.year + 1, month=1)
else:
next_month = start_date.replace(month=start_date.month + 1)
end_date = next_month - timedelta(days=1)
start_date_param = start_date.strftime('%Y-%m-%d')
end_date_display = end_date.strftime('%Y-%m-%d')
if end_date is not None:
end_date = end_date + timedelta(days=1)
stores = Store.objects.filter(is_active=True).order_by('name')
categories = ExpenseCategory.objects.filter(is_active=True).order_by('name')
report = None
if show_report and error_message is None:
report = build_monthly_report(
start_date,
end_date,
store_id=store_id,
store_is_null=store_is_null,
expense_category_id=expense_category_id,
expense_category_is_null=expense_category_is_null,
description_query=description_query or None,
)
return render(
request,
'expenses/monthly_report.html',
{
'report': report,
'target_month': start_date.strftime('%Y-%m'),
'start_date': start_date_param or (start_date.strftime('%Y-%m-%d') if start_date else ''),
'end_date': end_date_display,
'all_time': all_time,
'stores': stores,
'selected_store': store_param,
'categories': categories,
'selected_category': category_param,
'description_query': description_query,
'show_report': show_report,
'error_message': error_message,
},
)
def monthly_report_pdf(request):
target_month = request.GET.get('target_month')
if target_month:
start_date_param = request.GET.get('start_date', '')
end_date_param = request.GET.get('end_date', '')
all_time = request.GET.get('all_time') == '1'
store_param = request.GET.get('store_id', '')
category_param = request.GET.get('expense_category_id', '')
description_query = request.GET.get('description', '').strip()
store_id = None
store_is_null = False
expense_category_id = None
expense_category_is_null = False
if store_param == 'unassigned':
store_is_null = True
elif store_param:
try:
start_date = datetime.strptime(target_month, '%Y-%m').date().replace(day=1)
store_id = int(store_param)
except ValueError:
store_id = None
if category_param == 'unassigned':
expense_category_is_null = True
elif category_param:
try:
expense_category_id = int(category_param)
except ValueError:
expense_category_id = None
if start_date_param or end_date_param:
all_time = False
if all_time:
start_date = None
end_date = None
target_period = '全期間'
else:
start_date = None
end_date = None
if start_date_param:
try:
start_date = datetime.strptime(start_date_param, '%Y-%m-%d').date()
except ValueError:
start_date = None
if end_date_param:
try:
end_date = datetime.strptime(end_date_param, '%Y-%m-%d').date()
except ValueError:
end_date = None
if start_date is None and end_date is None:
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)
if start_date.month == 12:
next_month = start_date.replace(year=start_date.year + 1, month=1)
else:
next_month = start_date.replace(month=start_date.month + 1)
end_date = next_month - timedelta(days=1)
start_label = start_date.strftime('%Y-%m-%d') if start_date else '未指定'
end_label = end_date.strftime('%Y-%m-%d') if end_date else '未指定'
target_period = f'{start_label}{end_label}'
if end_date is not None:
end_date = end_date + timedelta(days=1)
report = build_monthly_report(
start_date,
end_date,
store_id=store_id,
store_is_null=store_is_null,
expense_category_id=expense_category_id,
expense_category_is_null=expense_category_is_null,
description_query=description_query or None,
)
return render(
request,
'expenses/monthly_report_pdf.html',
{
'report': report,
'target_month': start_date.strftime('%Y-%m'),
'target_period': target_period,
},
)