From 814477b1e20706379d996c8e6ac991834c5767d4 Mon Sep 17 00:00:00 2001 From: president Date: Mon, 22 Dec 2025 12:34:53 +0900 Subject: [PATCH] =?UTF-8?q?=E6=9C=88=E6=AC=A1=E3=83=AC=E3=83=9D=E3=83=BC?= =?UTF-8?q?=E3=83=88=E6=A9=9F=E8=83=BD=E3=81=AE=E6=94=B9=E5=96=84=E3=81=A8?= =?UTF-8?q?=E3=81=97=E3=81=A6=E3=80=81=E9=96=8B=E5=A7=8B=E6=97=A5=E3=83=BB?= =?UTF-8?q?=E7=B5=82=E4=BA=86=E6=97=A5=E3=81=AB=E3=82=88=E3=82=8B=E3=83=95?= =?UTF-8?q?=E3=82=A3=E3=83=AB=E3=82=BF=E3=83=AA=E3=83=B3=E3=82=B0=E6=A9=9F?= =?UTF-8?q?=E8=83=BD=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=97=E3=80=81=E5=BA=97?= =?UTF-8?q?=E8=88=97=E5=8C=BA=E5=88=86=E3=81=8A=E3=82=88=E3=81=B3=E7=B5=8C?= =?UTF-8?q?=E8=B2=BB=E5=8C=BA=E5=88=86=E3=81=AE=E9=81=B8=E6=8A=9E=E8=82=A2?= =?UTF-8?q?=E3=82=92=E5=AE=9F=E8=A3=85=E3=80=82=E7=B5=8C=E8=B2=BB=E5=8C=BA?= =?UTF-8?q?=E5=88=86=E3=81=A8=E5=BA=97=E8=88=97=E3=83=9E=E3=82=B9=E3=82=BF?= =?UTF-8?q?=E3=81=AE=E7=AE=A1=E7=90=86=E7=94=BB=E9=9D=A2=E3=82=92=E6=96=B0?= =?UTF-8?q?=E8=A6=8F=E4=BD=9C=E6=88=90=E3=81=97=E3=80=81=E9=96=A2=E9=80=A3?= =?UTF-8?q?=E3=81=99=E3=82=8B=E3=83=9E=E3=82=A4=E3=82=B0=E3=83=AC=E3=83=BC?= =?UTF-8?q?=E3=82=B7=E3=83=A7=E3=83=B3=E3=82=92=E8=BF=BD=E5=8A=A0=E3=80=82?= =?UTF-8?q?CSV=E5=8F=96=E8=BE=BC=E7=94=BB=E9=9D=A2=E3=81=AB=E3=81=8A?= =?UTF-8?q?=E3=81=91=E3=82=8B=E7=B5=8C=E8=B2=BB=E5=8C=BA=E5=88=86=E3=81=AE?= =?UTF-8?q?=E8=A1=A8=E7=A4=BA=E3=82=92=E6=94=B9=E5=96=84=E3=80=82=E4=BD=9C?= =?UTF-8?q?=E6=A5=AD=E3=83=AD=E3=82=B0=E3=82=92diary.md=E3=81=AB=E8=BF=BD?= =?UTF-8?q?=E8=A8=98=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Doc/diary.md | 3 + Sample-data/z.csv | 130 ++++++++++ database.rules | 6 + expenses/migrations/0003_seed_stores.py | 22 ++ .../0004_seed_expense_categories.py | 24 ++ expenses/services.py | 33 ++- expenses/urls.py | 2 + expenses/views.py | 235 ++++++++++++++++-- templates/base.html | 2 + .../expenses/expense_category_master.html | 74 ++++++ templates/expenses/expense_list.html | 6 + templates/expenses/monthly_report.html | 90 ++++++- templates/expenses/monthly_report_pdf.html | 2 +- templates/expenses/store_master.html | 74 ++++++ 仕様書.md | 18 ++ 15 files changed, 684 insertions(+), 37 deletions(-) create mode 100644 Sample-data/z.csv create mode 100644 expenses/migrations/0003_seed_stores.py create mode 100644 expenses/migrations/0004_seed_expense_categories.py create mode 100644 templates/expenses/expense_category_master.html create mode 100644 templates/expenses/store_master.html diff --git a/Doc/diary.md b/Doc/diary.md index 062c709..32e1530 100644 --- a/Doc/diary.md +++ b/Doc/diary.md @@ -31,3 +31,6 @@ - 月次レポートに明細行一覧を追加(各集計の下に日付/利用先/金額/備考) - 金額のカンマ区切りと右寄せを全体に適用(humanize + amountクラス) - favicon/apple-touch-iconの404回避と簡易アイコンを追加 +- 店舗区分の初期値(砂村板金/インフラ)投入用マイグレーションを追加・適用 +- 月次レポートに抽出条件(期間/店舗区分/経費区分/利用先)を追加し、PDF出力に条件を引き継ぐように対応 +- サブスク候補を仕様書に整理し、Apple課金やドメイン/レンタルサーバーの注記(価格変動・表記揺れ)を追加 diff --git a/Sample-data/z.csv b/Sample-data/z.csv new file mode 100644 index 0000000..4c0ce1a --- /dev/null +++ b/Sample-data/z.csv @@ -0,0 +1,130 @@ +カード名称,apollostation card,,,,, +お支払日,2025/11/07,,,,, +今回ご請求額,3370,,,,, +,,,,,, +利用日,ご利用店名及び商品名,本人・家族区分,支払区分名称,締前入金区分,利用金額,備考 +2025/09/14,ETC東日本  (奈井江砂川→三笠),,1回,,970, +2025/09/14,ETC東日本  (三笠→滝川),,1回,,1430, +2025/09/24,ETC東日本  (三笠→奈井江砂川),,1回,,970, +,,,,,, +カード名称,apollostation card,,,,, +お支払日,2025/12/08,,,,, +今回ご請求額,4690,,,,, +,,,,,, +利用日,ご利用店名及び商品名,本人・家族区分,支払区分名称,締前入金区分,利用金額,備考 +2025/09/26,ETC東日本  (三笠→奈井江砂川),,1回,,970, +2025/09/26,ETC東日本  (奈井江砂川→三笠),,1回,,970, +2025/10/08,ETC東日本  (旭川鷹栖→砂川SAス),,1回,,1780, +2025/10/30,ETC東日本  (三笠→奈井江砂川),,1回,,970, + +鬠ソカード名称,出光ゴールドカード,,,,, +お支払日,2025/08/07,,,,, +今回ご請求額,33794,,,,, +,,,,,, +利用日,ご利用店名及び商品名,本人・家族区分,支払区分名称,締前入金区分,利用金額,備考 +2025/06/10,APPLE COM BILL,,1回,,150, +2025/06/12,APPLE COM BILL,,1回,,480, +2025/06/14,コ−ク オン ペイ,,1回,,130, +2025/06/14,コ−ク オン ペイ,,1回,,130, +2025/06/15,コメリパワー砂川店,,1回,,2474, +2025/06/18,ツルハ,,1回,,1968, +2025/06/18,コ−ク オン ペイ,,1回,,100, +2025/06/20,オカモトスナカワ,,1回,,4164, +2025/06/20,コ−ク オン ペイ,,1回,,120, +2025/06/20,コ−ク オン ペイ,,1回,,130, +2025/06/24,ABEMAプレミアム,,1回,,1080, +2025/06/24,コ−ク オン ペイ,,1回,,110, +2025/06/25,コ−ク オン ペイ,,1回,,110, +2025/06/25,コ−ク オン ペイ,,1回,,160, +2025/06/26,OPENAI *CHATGPT SUBSCR,,1回,,3305,現地通貨額:22.00 USD +,(SAN FRANCISCO),,,,,円換算レート:6/27 150.2273 +2025/06/26,コ−ク オン ペイ,,1回,,130, +2025/06/26,コ−ク オン ペイ,,1回,,130, +2025/07/01,エックスサ−バ−/M,,1回,,2200, +2025/07/01,お名前.com ドメインサービス,,1回,,268, +2025/07/01,APPLE COM BILL,,1回,,1200, +2025/07/03,AMAZON.CO.JP,,1回,,2980, +2025/07/05,AMAZON.CO.JP,,1回,,1520, +2025/07/07,ケ−ズデンキ,,1回,,3241, +2025/07/08,お名前.comレンタルサ−バ−,,1回,,2153, +2025/07/10,アット・ニフティ,,1回,,5361, +,,,,,, +,,,,,, +2025/06/10,APPLE COM BILL,,1回,,150, +2025/06/12,APPLE COM BILL,,1回,,480, +2025/06/14,コ−ク オン ペイ,,1回,,130, +2025/06/14,コ−ク オン ペイ,,1回,,130, +2025/06/15,コメリパワー砂川店,,1回,,2474, +2025/06/18,ツルハ,,1回,,1968, +2025/06/18,コ−ク オン ペイ,,1回,,100, +2025/06/20,オカモトスナカワ,,1回,,4164, +2025/06/20,コ−ク オン ペイ,,1回,,120, +2025/06/20,コ−ク オン ペイ,,1回,,130, +2025/06/24,ABEMAプレミアム,,1回,,1080, +2025/06/24,コ−ク オン ペイ,,1回,,110, +2025/06/25,コ−ク オン ペイ,,1回,,110, +2025/06/25,コ−ク オン ペイ,,1回,,160, +2025/06/26,OPENAI *CHATGPT SUBSCR,,1回,,3305,現地通貨額:22.00 USD +2025/06/26,コ−ク オン ペイ,,1回,,130, +2025/06/26,コ−ク オン ペイ,,1回,,130, +2025/07/01,エックスサ−バ−/M,,1回,,2200, +2025/07/01,お名前.com ドメインサービス,,1回,,268, +2025/07/01,APPLE COM BILL,,1回,,1200, +2025/07/03,AMAZON.CO.JP,,1回,,2980, +2025/07/05,AMAZON.CO.JP,,1回,,1520, +2025/07/07,ケ−ズデンキ,,1回,,3241, +2025/07/08,お名前.comレンタルサ−バ−,,1回,,2153, +2025/07/10,アット・ニフティ,,1回,,5361, + +カード名称,出光ゴールドカード,,,,, +お支払日,2025/10/07,,,,, +今回ご請求額,56846,,,,, +,,,,,, +利用日,ご利用店名及び商品名,本人・家族区分,支払区分名称,締前入金区分,利用金額,備考 +2025/08/09,ETC還元超過 (三笠→奈井江砂川),,1回,,890, +2025/08/10,APPLE COM BILL,,1回,,150, +2025/08/12,APPLE COM BILL,,1回,,480, +2025/08/14,ETC東日本  (奈井江砂川→江別東),,1回,,1530, +2025/08/20,プロノ タキカワテン,,1回,,4927, +2025/08/21,DCM,,1回,,2199, +2025/08/22,ETC東日本  (砂川SAス→旭川鷹栖),,1回,,1780, +2025/08/24,ABEMAプレミアム,,1回,,1080, +2025/08/26,OPENAI *CHATGPT SUBSCR,,1回,,3369,現地通貨額:22.00 USD +,(SAN FRANCISCO),,,,,円換算レート:8/27 153.1364 +2025/08/28,ケ-ズデンキ,,1回,,19950, +2025/08/31,コメリパワー砂川店,,1回,,1652, +2025/08/31,コメリパワー砂川店,,1回,,1144, +2025/09/01,エックスサ-バ-/M,,1回,,2200, +2025/09/01,お名前.com ドメインサービス,,1回,,270, +2025/09/01,APPLE COM BILL,,1回,,1200, +2025/09/05,コ-ク オン ペイ,,1回,,100, +2025/09/08,お名前.comレンタルサ-バ-,,1回,,2612, +2025/09/10,アット・ニフティ,,1回,,11313, + +カード名称,出光ゴールドカード,,,,, +お支払日,2025/11/07,,,,, +今回ご請求額,61542,,,,, +,,,,,, +利用日,ご利用店名及び商品名,本人・家族区分,支払区分名称,締前入金区分,利用金額,備考 +2025/09/10,APPLE COM BILL,,1回,,150, +2025/09/10,ツルハ,,1回,,1423, +2025/09/12,APPLE COM BILL,,1回,,480, +2025/09/14,コメリパワー砂川店,,1回,,2490, +2025/09/16,DCM,,1回,,3058, +2025/09/17,AMAZON.CO.JP,,1回,,3992, +2025/09/18,コメリパワー砂川店,,1回,,2476, +2025/09/21,DCM,,1回,,5026, +2025/09/24,ABEMAプレミアム,,1回,,1080, +2025/09/26,OPENAI *CHATGPT SUBSCR,,1回,,3419,現地通貨額:22.00 USD +,(SAN FRANCISCO),,,,,円換算レート:9/27 155.4091 +2025/09/27,プロノ タキカワテン,,1回,,2133, +2025/09/29,コメリパワー砂川店,,1回,,2050, +2025/09/30,コメリパワー砂川店,,1回,,5440, +2025/10/01,エックスサ-バ-/M,,1回,,2200, +2025/10/01,お名前.com ドメインサービス,,1回,,270, +2025/10/01,プロノ タキカワテン,,1回,,2485, +2025/10/01,ゼンリンストア(BtoB),,1回,,660, +2025/10/01,APPLE COM BILL,,1回,,1200, +2025/10/03,コメリパワー砂川店,,1回,,6160, +2025/10/06,DCM,,1回,,12738, +2025/10/08,お名前.comレンタルサ-バ-,,1回,,2612, \ No newline at end of file diff --git a/database.rules b/database.rules index a3b8185..63717f5 100644 --- a/database.rules +++ b/database.rules @@ -35,3 +35,9 @@ source .venv/bin/activate DB_NAME=accounting DB_USER=account_user DB_PASSWORD=account_Hideyukey-1234 DB_HOST=labo.sunamura-llc.com DB_PORT=5432 .venv/bin/python manage.py runserver DJANGO_DEBUG=true DB_NAME=accounting DB_USER=account_user DB_PASSWORD=account_Hideyukey-1234 DB_HOST=labo.sunamura-llc.com DB_PORT=5432 .venv/bin/python manage.py runserver + +マイグレーション +DJANGO_DEBUG=true DB_NAME=accounting DB_USER=account_user DB_PASSWORD=account_Hideyukey-1234 DB_HOST=labo.sunamura-llc.com DB_PORT=5432 .venv/bin/python manage.py migrate +または  +set -a; source .env; set +a +python manage.py migrate diff --git a/expenses/migrations/0003_seed_stores.py b/expenses/migrations/0003_seed_stores.py new file mode 100644 index 0000000..9e6b097 --- /dev/null +++ b/expenses/migrations/0003_seed_stores.py @@ -0,0 +1,22 @@ +from django.db import migrations + + +def seed_stores(apps, schema_editor): + Store = apps.get_model('expenses', 'Store') + for name in ('砂村板金', 'インフラ'): + Store.objects.get_or_create(name=name, defaults={'is_active': True}) + + +def unseed_stores(apps, schema_editor): + Store = apps.get_model('expenses', 'Store') + Store.objects.filter(name__in=('砂村板金', 'インフラ')).delete() + + +class Migration(migrations.Migration): + dependencies = [ + ('expenses', '0002_owner_type_cancel'), + ] + + operations = [ + migrations.RunPython(seed_stores, unseed_stores), + ] diff --git a/expenses/migrations/0004_seed_expense_categories.py b/expenses/migrations/0004_seed_expense_categories.py new file mode 100644 index 0000000..8162640 --- /dev/null +++ b/expenses/migrations/0004_seed_expense_categories.py @@ -0,0 +1,24 @@ +from django.db import migrations + + +def seed_expense_categories(apps, schema_editor): + ExpenseCategory = apps.get_model('expenses', 'ExpenseCategory') + for name in ('雑費', '交通費', '通信費', '福利厚生費'): + ExpenseCategory.objects.get_or_create(name=name, defaults={'is_active': True}) + + +def unseed_expense_categories(apps, schema_editor): + ExpenseCategory = apps.get_model('expenses', 'ExpenseCategory') + ExpenseCategory.objects.filter( + name__in=('雑費', '交通費', '通信費', '福利厚生費') + ).delete() + + +class Migration(migrations.Migration): + dependencies = [ + ('expenses', '0003_seed_stores'), + ] + + operations = [ + migrations.RunPython(seed_expense_categories, unseed_expense_categories), + ] diff --git a/expenses/services.py b/expenses/services.py index 803c313..1390884 100644 --- a/expenses/services.py +++ b/expenses/services.py @@ -51,12 +51,30 @@ class MonthlyReportResult: unclassified_counts: dict -def build_monthly_report(start_date: date, end_date: date) -> MonthlyReportResult: - queryset = Expense.objects.filter( - is_canceled=False, - use_date__gte=start_date, - use_date__lt=end_date, - ) +def build_monthly_report( + start_date: date | None, + end_date: date | None, + store_id: int | None = None, + store_is_null: bool = False, + expense_category_id: int | None = None, + expense_category_is_null: bool = False, + description_query: str | None = None, +) -> MonthlyReportResult: + queryset = Expense.objects.filter(is_canceled=False) + if start_date is not None: + queryset = queryset.filter(use_date__gte=start_date) + if end_date is not None: + queryset = queryset.filter(use_date__lt=end_date) + if store_is_null: + queryset = queryset.filter(store__isnull=True) + elif store_id is not None: + queryset = queryset.filter(store_id=store_id) + if expense_category_is_null: + queryset = queryset.filter(expense_category__isnull=True) + elif expense_category_id is not None: + queryset = queryset.filter(expense_category_id=expense_category_id) + if description_query: + queryset = queryset.filter(description__icontains=description_query) detail_queryset = ( queryset.select_related('store', 'expense_category') .order_by('use_date', 'id') @@ -67,6 +85,7 @@ def build_monthly_report(start_date: date, end_date: date) -> MonthlyReportResul for expense in detail_queryset: detail = { 'use_date': expense.use_date, + 'expense_category_name': expense.expense_category.name if expense.expense_category else None, 'description': expense.description, 'amount': expense.amount, 'note': expense.note, @@ -103,7 +122,7 @@ def build_monthly_report(start_date: date, end_date: date) -> MonthlyReportResul 'total': queryset.count(), } return MonthlyReportResult( - target_month=start_date, + target_month=start_date or date.today(), total_amount=total_amount, store_totals=store_totals, category_totals=category_totals, diff --git a/expenses/urls.py b/expenses/urls.py index cf498b3..3c9d478 100644 --- a/expenses/urls.py +++ b/expenses/urls.py @@ -6,6 +6,8 @@ from . import views urlpatterns = [ path('', views.csv_upload, name='csv_upload'), path('expenses/', views.expense_list, name='expense_list'), + path('stores/', views.store_master, name='store_master'), + path('expense-categories/', views.expense_category_master, name='expense_category_master'), path('expenses//update/', views.expense_update, name='expense_update'), path('expenses//cancel/', views.expense_cancel, name='expense_cancel'), path('reports/monthly/', views.monthly_report, name='monthly_report'), diff --git a/expenses/views.py b/expenses/views.py index 0d4f011..050dc2f 100644 --- a/expenses/views.py +++ b/expenses/views.py @@ -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, }, ) diff --git a/templates/base.html b/templates/base.html index a396286..2aef113 100644 --- a/templates/base.html +++ b/templates/base.html @@ -18,6 +18,8 @@ diff --git a/templates/expenses/expense_category_master.html b/templates/expenses/expense_category_master.html new file mode 100644 index 0000000..f118857 --- /dev/null +++ b/templates/expenses/expense_category_master.html @@ -0,0 +1,74 @@ +{% extends 'base.html' %} + +{% block title %}経費区分マスタ管理{% endblock %} + +{% block content %} +
+

経費区分マスタ管理

+ {% if error_message %} +

{{ error_message }}

+ {% endif %} +

経費区分を追加

+
+ {% csrf_token %} + + + +
+

一覧

+ + + + + + + + + + + {% for category in categories %} + + + {% csrf_token %} + + + + + + + + {% empty %} + + + + {% endfor %} + +
経費区分名有効更新削除
+ + + + + + +
+ {% csrf_token %} + + + +
+
経費区分がありません。
+
+{% endblock %} diff --git a/templates/expenses/expense_list.html b/templates/expenses/expense_list.html index 3b1aefe..2776b16 100644 --- a/templates/expenses/expense_list.html +++ b/templates/expenses/expense_list.html @@ -129,6 +129,12 @@ } function coerceValue(field, value) { + if (value === '') { + return value; + } + if (field === 'store_id' || field === 'expense_category_id') { + return Number(value); + } return value; } diff --git a/templates/expenses/monthly_report.html b/templates/expenses/monthly_report.html index 10532d8..1aa73a2 100644 --- a/templates/expenses/monthly_report.html +++ b/templates/expenses/monthly_report.html @@ -8,14 +8,87 @@

月次レポート

+ + + + + + + {% if show_report %} + + PDF出力 + + {% endif %}
+ {% if error_message %}

{{ error_message }}

{% endif %} + {% if not show_report %} +

条件を指定して表示ボタンを押してください。

+ {% else %}

合計

{{ report.total_amount|intcomma }} 円

@@ -50,6 +123,7 @@ 日付 + 経費区分 利用先 金額 備考 @@ -59,13 +133,14 @@ {% for detail in row.details %} {{ detail.use_date }} + {{ detail.expense_category_name|default:"未設定" }} {{ detail.description }} {{ detail.amount|intcomma }} {{ detail.note }} {% empty %} - 対象データがありません。 + 対象データがありません。 {% endfor %} @@ -102,6 +177,7 @@ 日付 + 経費区分 利用先 金額 備考 @@ -111,13 +187,14 @@ {% for detail in row.details %} {{ detail.use_date }} + {{ detail.expense_category_name|default:"未設定" }} {{ detail.description }} {{ detail.amount|intcomma }} {{ detail.note }} {% empty %} - 対象データがありません。 + 対象データがありません。 {% endfor %} @@ -154,6 +231,7 @@ 日付 + 経費区分 利用先 金額 備考 @@ -163,13 +241,14 @@ {% for detail in row.details %} {{ detail.use_date }} + {{ detail.expense_category_name|default:"未設定" }} {{ detail.description }} {{ detail.amount|intcomma }} {{ detail.note }} {% empty %} - 対象データがありません。 + 対象データがありません。 {% endfor %} @@ -185,5 +264,6 @@
  • 対象総件数: {{ report.unclassified_counts.total }}
  • + {% endif %} {% endblock %} diff --git a/templates/expenses/monthly_report_pdf.html b/templates/expenses/monthly_report_pdf.html index 54f0019..f5b6cfb 100644 --- a/templates/expenses/monthly_report_pdf.html +++ b/templates/expenses/monthly_report_pdf.html @@ -14,7 +14,7 @@

    月次レポート

    -

    対象年月: {{ target_month }}

    +

    対象期間: {{ target_period }}

    合計

    {{ report.total_amount|intcomma }} 円

    店舗別合計

    diff --git a/templates/expenses/store_master.html b/templates/expenses/store_master.html new file mode 100644 index 0000000..9426633 --- /dev/null +++ b/templates/expenses/store_master.html @@ -0,0 +1,74 @@ +{% extends 'base.html' %} + +{% block title %}店舗マスタ管理{% endblock %} + +{% block content %} +
    +

    店舗マスタ管理

    + {% if error_message %} +

    {{ error_message }}

    + {% endif %} +

    店舗を追加

    +
    + {% csrf_token %} + + + +
    +

    一覧

    + + + + + + + + + + + {% for store in stores %} + + + {% csrf_token %} + + + + + + + + {% empty %} + + + + {% endfor %} + +
    店舗名有効更新削除
    + + + + + + +
    + {% csrf_token %} + + + +
    +
    店舗がありません。
    +
    +{% endblock %} diff --git a/仕様書.md b/仕様書.md index bed410d..5aa028d 100644 --- a/仕様書.md +++ b/仕様書.md @@ -129,3 +129,21 @@ UNIQUE(source, source_hash) 過剰なUI装飾やフロントフレームワークは不要。 業務実装として堅牢・拡張可能なコードを優先する。 + +--- + +## サブスク判定ルール案(暫定) + +### 候補リスト(利用先文字列 + 金額) + +- APPLE COM BILL 480 -> 事務用品費、ウェザーニュース +- APPLE COM BILL 150 -> 事務用品費、iCloud 追加Disk +- APPLE COM BILL 1200 -> 未設定、アップルニュージック +- ABEMAプレミアム 1080 -> 未設定、個人の娯楽 +- OPENAI *CHATGPT SUBSCR 3305 -> 事務用品費、GPT +- エックスサ−バ−/M 2200 ->  通信費               //文字の揺らぎ 下と同じ +- エックスサ-バ-/M 2200 -> 通信費、Nextcloud運営VPS +- お名前.com ドメインサービス 268/270 -> 通信費、ドメインサービス +- お名前.comレンタルサ−バ− 2153 -> 通信費、レンタルサーバー(価格変動あり/表記揺れあり) +- お名前.comレンタルサ-バ- 2612 -> 通信費、レンタルサーバー(価格変動あり/表記揺れあり) +- アット・ニフティ 5361 -> (要確認)