月次レポート機能の改善として、開始日・終了日によるフィルタリング機能を追加し、店舗区分および経費区分の選択肢を実装。経費区分と店舗マスタの管理画面を新規作成し、関連するマイグレーションを追加。CSV取込画面における経費区分の表示を改善。作業ログをdiary.mdに追記。
This commit is contained in:
@@ -31,3 +31,6 @@
|
||||
- 月次レポートに明細行一覧を追加(各集計の下に日付/利用先/金額/備考)
|
||||
- 金額のカンマ区切りと右寄せを全体に適用(humanize + amountクラス)
|
||||
- favicon/apple-touch-iconの404回避と簡易アイコンを追加
|
||||
- 店舗区分の初期値(砂村板金/インフラ)投入用マイグレーションを追加・適用
|
||||
- 月次レポートに抽出条件(期間/店舗区分/経費区分/利用先)を追加し、PDF出力に条件を引き継ぐように対応
|
||||
- サブスク候補を仕様書に整理し、Apple課金やドメイン/レンタルサーバーの注記(価格変動・表記揺れ)を追加
|
||||
|
||||
130
Sample-data/z.csv
Normal file
130
Sample-data/z.csv
Normal file
@@ -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,
|
||||
|
@@ -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
|
||||
|
||||
22
expenses/migrations/0003_seed_stores.py
Normal file
22
expenses/migrations/0003_seed_stores.py
Normal file
@@ -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),
|
||||
]
|
||||
24
expenses/migrations/0004_seed_expense_categories.py
Normal file
24
expenses/migrations/0004_seed_expense_categories.py
Normal file
@@ -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),
|
||||
]
|
||||
@@ -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,
|
||||
|
||||
@@ -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/<int:expense_id>/update/', views.expense_update, name='expense_update'),
|
||||
path('expenses/<int:expense_id>/cancel/', views.expense_cancel, name='expense_cancel'),
|
||||
path('reports/monthly/', views.monthly_report, name='monthly_report'),
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
<nav>
|
||||
<a href="{% url 'csv_upload' %}">CSV取込</a>
|
||||
<a href="{% url 'expense_list' %}">明細編集</a>
|
||||
<a href="{% url 'store_master' %}">店舗マスタ</a>
|
||||
<a href="{% url 'expense_category_master' %}">経費区分マスタ</a>
|
||||
<a href="{% url 'monthly_report' %}">月次レポート</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
74
templates/expenses/expense_category_master.html
Normal file
74
templates/expenses/expense_category_master.html
Normal file
@@ -0,0 +1,74 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}経費区分マスタ管理{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section>
|
||||
<h2>経費区分マスタ管理</h2>
|
||||
{% if error_message %}
|
||||
<p>{{ error_message }}</p>
|
||||
{% endif %}
|
||||
<h3>経費区分を追加</h3>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<label>
|
||||
経費区分名
|
||||
<input type="text" name="name" required>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" name="is_active" value="1" checked>
|
||||
有効
|
||||
</label>
|
||||
<button type="submit">追加</button>
|
||||
</form>
|
||||
<h3>一覧</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>経費区分名</th>
|
||||
<th>有効</th>
|
||||
<th>更新</th>
|
||||
<th>削除</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for category in categories %}
|
||||
<tr>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="category_id" value="{{ category.id }}">
|
||||
<td>
|
||||
<input type="text" name="name" value="{{ category.name }}" required>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="is_active"
|
||||
value="1"
|
||||
{% if category.is_active %}checked{% endif %}
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<button type="submit">保存</button>
|
||||
</td>
|
||||
</form>
|
||||
<td>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="category_id" value="{{ category.id }}">
|
||||
<input type="hidden" name="action" value="deactivate">
|
||||
<button type="submit" {% if not category.is_active %}disabled{% endif %}>
|
||||
無効化
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="4">経費区分がありません。</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,14 +8,87 @@
|
||||
<h2>月次レポート</h2>
|
||||
<form method="get">
|
||||
<label>
|
||||
年月
|
||||
<input type="month" name="target_month" value="{{ target_month }}">
|
||||
開始日
|
||||
<input type="date" name="start_date" value="{{ start_date }}">
|
||||
</label>
|
||||
<label>
|
||||
終了日
|
||||
<input type="date" name="end_date" value="{{ end_date }}">
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" name="all_time" value="1" {% if all_time %}checked{% endif %}>
|
||||
全期間
|
||||
</label>
|
||||
<label>
|
||||
店舗区分
|
||||
<select name="store_id">
|
||||
<option value="" {% if not selected_store %}selected{% endif %}>全て</option>
|
||||
<option value="unassigned" {% if selected_store == "unassigned" %}selected{% endif %}>
|
||||
未設定
|
||||
</option>
|
||||
{% for store in stores %}
|
||||
<option value="{{ store.id }}" {% if selected_store == store.id|stringformat:"s" %}selected{% endif %}>
|
||||
{{ store.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
経費区分
|
||||
<select name="expense_category_id">
|
||||
<option value="" {% if not selected_category %}selected{% endif %}>全て</option>
|
||||
<option value="unassigned" {% if selected_category == "unassigned" %}selected{% endif %}>
|
||||
未設定
|
||||
</option>
|
||||
{% for category in categories %}
|
||||
<option value="{{ category.id }}" {% if selected_category == category.id|stringformat:"s" %}selected{% endif %}>
|
||||
{{ category.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
利用先
|
||||
<input type="search" name="description" value="{{ description_query }}">
|
||||
</label>
|
||||
<input type="hidden" name="show" value="1">
|
||||
<button type="submit">表示</button>
|
||||
{% if show_report %}
|
||||
<a
|
||||
href="{% url 'monthly_report_pdf' %}?start_date={{ start_date }}&end_date={{ end_date }}&store_id={{ selected_store }}&expense_category_id={{ selected_category }}&description={{ description_query|urlencode }}&all_time={% if all_time %}1{% endif %}"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
PDF出力
|
||||
</a>
|
||||
{% endif %}
|
||||
</form>
|
||||
<script>
|
||||
const startInput = document.querySelector('input[name="start_date"]');
|
||||
const endInput = document.querySelector('input[name="end_date"]');
|
||||
const allTimeInput = document.querySelector('input[name="all_time"]');
|
||||
if (startInput && endInput && allTimeInput) {
|
||||
const syncAllTime = () => {
|
||||
if (startInput.value || endInput.value) {
|
||||
allTimeInput.checked = false;
|
||||
}
|
||||
};
|
||||
startInput.addEventListener('change', syncAllTime);
|
||||
endInput.addEventListener('change', syncAllTime);
|
||||
allTimeInput.addEventListener('change', () => {
|
||||
if (allTimeInput.checked) {
|
||||
startInput.value = '';
|
||||
endInput.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% if error_message %}
|
||||
<p style="color: #b00020;">{{ error_message }}</p>
|
||||
{% endif %}
|
||||
{% if not show_report %}
|
||||
<p>条件を指定して表示ボタンを押してください。</p>
|
||||
{% else %}
|
||||
<div>
|
||||
<h3>合計</h3>
|
||||
<p class="amount">{{ report.total_amount|intcomma }} 円</p>
|
||||
@@ -50,6 +123,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>日付</th>
|
||||
<th>経費区分</th>
|
||||
<th>利用先</th>
|
||||
<th class="amount">金額</th>
|
||||
<th>備考</th>
|
||||
@@ -59,13 +133,14 @@
|
||||
{% for detail in row.details %}
|
||||
<tr>
|
||||
<td>{{ detail.use_date }}</td>
|
||||
<td>{{ detail.expense_category_name|default:"未設定" }}</td>
|
||||
<td>{{ detail.description }}</td>
|
||||
<td class="amount">{{ detail.amount|intcomma }}</td>
|
||||
<td>{{ detail.note }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="4">対象データがありません。</td>
|
||||
<td colspan="5">対象データがありません。</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@@ -102,6 +177,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>日付</th>
|
||||
<th>経費区分</th>
|
||||
<th>利用先</th>
|
||||
<th class="amount">金額</th>
|
||||
<th>備考</th>
|
||||
@@ -111,13 +187,14 @@
|
||||
{% for detail in row.details %}
|
||||
<tr>
|
||||
<td>{{ detail.use_date }}</td>
|
||||
<td>{{ detail.expense_category_name|default:"未設定" }}</td>
|
||||
<td>{{ detail.description }}</td>
|
||||
<td class="amount">{{ detail.amount|intcomma }}</td>
|
||||
<td>{{ detail.note }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="4">対象データがありません。</td>
|
||||
<td colspan="5">対象データがありません。</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@@ -154,6 +231,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>日付</th>
|
||||
<th>経費区分</th>
|
||||
<th>利用先</th>
|
||||
<th class="amount">金額</th>
|
||||
<th>備考</th>
|
||||
@@ -163,13 +241,14 @@
|
||||
{% for detail in row.details %}
|
||||
<tr>
|
||||
<td>{{ detail.use_date }}</td>
|
||||
<td>{{ detail.expense_category_name|default:"未設定" }}</td>
|
||||
<td>{{ detail.description }}</td>
|
||||
<td class="amount">{{ detail.amount|intcomma }}</td>
|
||||
<td>{{ detail.note }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="4">対象データがありません。</td>
|
||||
<td colspan="5">対象データがありません。</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@@ -185,5 +264,6 @@
|
||||
<li>対象総件数: {{ report.unclassified_counts.total }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<h1>月次レポート</h1>
|
||||
<p>対象年月: {{ target_month }}</p>
|
||||
<p>対象期間: {{ target_period }}</p>
|
||||
<h2>合計</h2>
|
||||
<p class="amount">{{ report.total_amount|intcomma }} 円</p>
|
||||
<h2>店舗別合計</h2>
|
||||
|
||||
74
templates/expenses/store_master.html
Normal file
74
templates/expenses/store_master.html
Normal file
@@ -0,0 +1,74 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}店舗マスタ管理{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section>
|
||||
<h2>店舗マスタ管理</h2>
|
||||
{% if error_message %}
|
||||
<p>{{ error_message }}</p>
|
||||
{% endif %}
|
||||
<h3>店舗を追加</h3>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<label>
|
||||
店舗名
|
||||
<input type="text" name="name" required>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" name="is_active" value="1" checked>
|
||||
有効
|
||||
</label>
|
||||
<button type="submit">追加</button>
|
||||
</form>
|
||||
<h3>一覧</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>店舗名</th>
|
||||
<th>有効</th>
|
||||
<th>更新</th>
|
||||
<th>削除</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for store in stores %}
|
||||
<tr>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="store_id" value="{{ store.id }}">
|
||||
<td>
|
||||
<input type="text" name="name" value="{{ store.name }}" required>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="is_active"
|
||||
value="1"
|
||||
{% if store.is_active %}checked{% endif %}
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<button type="submit">保存</button>
|
||||
</td>
|
||||
</form>
|
||||
<td>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="store_id" value="{{ store.id }}">
|
||||
<input type="hidden" name="action" value="deactivate">
|
||||
<button type="submit" {% if not store.is_active %}disabled{% endif %}>
|
||||
無効化
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="4">店舗がありません。</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
{% endblock %}
|
||||
18
仕様書.md
18
仕様書.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 -> (要確認)
|
||||
|
||||
Reference in New Issue
Block a user