月次レポート機能の改善として、開始日・終了日によるフィルタリング機能を追加し、店舗区分および経費区分の選択肢を実装。経費区分と店舗マスタの管理画面を新規作成し、関連するマイグレーションを追加。CSV取込画面における経費区分の表示を改善。作業ログをdiary.mdに追記。
This commit is contained in:
@@ -31,3 +31,6 @@
|
|||||||
- 月次レポートに明細行一覧を追加(各集計の下に日付/利用先/金額/備考)
|
- 月次レポートに明細行一覧を追加(各集計の下に日付/利用先/金額/備考)
|
||||||
- 金額のカンマ区切りと右寄せを全体に適用(humanize + amountクラス)
|
- 金額のカンマ区切りと右寄せを全体に適用(humanize + amountクラス)
|
||||||
- favicon/apple-touch-iconの404回避と簡易アイコンを追加
|
- 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
|
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 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
|
unclassified_counts: dict
|
||||||
|
|
||||||
|
|
||||||
def build_monthly_report(start_date: date, end_date: date) -> MonthlyReportResult:
|
def build_monthly_report(
|
||||||
queryset = Expense.objects.filter(
|
start_date: date | None,
|
||||||
is_canceled=False,
|
end_date: date | None,
|
||||||
use_date__gte=start_date,
|
store_id: int | None = None,
|
||||||
use_date__lt=end_date,
|
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 = (
|
detail_queryset = (
|
||||||
queryset.select_related('store', 'expense_category')
|
queryset.select_related('store', 'expense_category')
|
||||||
.order_by('use_date', 'id')
|
.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:
|
for expense in detail_queryset:
|
||||||
detail = {
|
detail = {
|
||||||
'use_date': expense.use_date,
|
'use_date': expense.use_date,
|
||||||
|
'expense_category_name': expense.expense_category.name if expense.expense_category else None,
|
||||||
'description': expense.description,
|
'description': expense.description,
|
||||||
'amount': expense.amount,
|
'amount': expense.amount,
|
||||||
'note': expense.note,
|
'note': expense.note,
|
||||||
@@ -103,7 +122,7 @@ def build_monthly_report(start_date: date, end_date: date) -> MonthlyReportResul
|
|||||||
'total': queryset.count(),
|
'total': queryset.count(),
|
||||||
}
|
}
|
||||||
return MonthlyReportResult(
|
return MonthlyReportResult(
|
||||||
target_month=start_date,
|
target_month=start_date or date.today(),
|
||||||
total_amount=total_amount,
|
total_amount=total_amount,
|
||||||
store_totals=store_totals,
|
store_totals=store_totals,
|
||||||
category_totals=category_totals,
|
category_totals=category_totals,
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ from . import views
|
|||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', views.csv_upload, name='csv_upload'),
|
path('', views.csv_upload, name='csv_upload'),
|
||||||
path('expenses/', views.expense_list, name='expense_list'),
|
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>/update/', views.expense_update, name='expense_update'),
|
||||||
path('expenses/<int:expense_id>/cancel/', views.expense_cancel, name='expense_cancel'),
|
path('expenses/<int:expense_id>/cancel/', views.expense_cancel, name='expense_cancel'),
|
||||||
path('reports/monthly/', views.monthly_report, name='monthly_report'),
|
path('reports/monthly/', views.monthly_report, name='monthly_report'),
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime, timedelta
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
import chardet
|
import chardet
|
||||||
|
from django.db import IntegrityError
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.shortcuts import get_object_or_404, render
|
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
|
@require_POST
|
||||||
def expense_update(request, expense_id: int):
|
def expense_update(request, expense_id: int):
|
||||||
try:
|
try:
|
||||||
@@ -111,51 +184,165 @@ def expense_update(request, expense_id: int):
|
|||||||
|
|
||||||
def monthly_report(request):
|
def monthly_report(request):
|
||||||
error_message = None
|
error_message = None
|
||||||
target_month = request.GET.get('target_month')
|
start_date_param = request.GET.get('start_date', '')
|
||||||
if target_month:
|
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:
|
try:
|
||||||
start_date = datetime.strptime(target_month, '%Y-%m').date().replace(day=1)
|
store_id = int(store_param)
|
||||||
except ValueError:
|
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)
|
start_date = date.today().replace(day=1)
|
||||||
else:
|
if start_date.month == 12:
|
||||||
start_date = date.today().replace(day=1)
|
next_month = start_date.replace(year=start_date.year + 1, month=1)
|
||||||
if start_date.month == 12:
|
else:
|
||||||
end_date = start_date.replace(year=start_date.year + 1, month=1)
|
next_month = start_date.replace(month=start_date.month + 1)
|
||||||
else:
|
end_date = next_month - timedelta(days=1)
|
||||||
end_date = start_date.replace(month=start_date.month + 1)
|
start_date_param = start_date.strftime('%Y-%m-%d')
|
||||||
report = build_monthly_report(start_date, end_date)
|
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(
|
return render(
|
||||||
request,
|
request,
|
||||||
'expenses/monthly_report.html',
|
'expenses/monthly_report.html',
|
||||||
{
|
{
|
||||||
'report': report,
|
'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,
|
'error_message': error_message,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def monthly_report_pdf(request):
|
def monthly_report_pdf(request):
|
||||||
target_month = request.GET.get('target_month')
|
start_date_param = request.GET.get('start_date', '')
|
||||||
if target_month:
|
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:
|
try:
|
||||||
start_date = datetime.strptime(target_month, '%Y-%m').date().replace(day=1)
|
store_id = int(store_param)
|
||||||
except ValueError:
|
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)
|
start_date = date.today().replace(day=1)
|
||||||
else:
|
if start_date.month == 12:
|
||||||
start_date = date.today().replace(day=1)
|
next_month = start_date.replace(year=start_date.year + 1, month=1)
|
||||||
if start_date.month == 12:
|
else:
|
||||||
end_date = start_date.replace(year=start_date.year + 1, month=1)
|
next_month = start_date.replace(month=start_date.month + 1)
|
||||||
else:
|
end_date = next_month - timedelta(days=1)
|
||||||
end_date = start_date.replace(month=start_date.month + 1)
|
start_label = start_date.strftime('%Y-%m-%d') if start_date else '未指定'
|
||||||
report = build_monthly_report(start_date, end_date)
|
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(
|
return render(
|
||||||
request,
|
request,
|
||||||
'expenses/monthly_report_pdf.html',
|
'expenses/monthly_report_pdf.html',
|
||||||
{
|
{
|
||||||
'report': report,
|
'report': report,
|
||||||
'target_month': start_date.strftime('%Y-%m'),
|
'target_period': target_period,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
<nav>
|
<nav>
|
||||||
<a href="{% url 'csv_upload' %}">CSV取込</a>
|
<a href="{% url 'csv_upload' %}">CSV取込</a>
|
||||||
<a href="{% url 'expense_list' %}">明細編集</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>
|
<a href="{% url 'monthly_report' %}">月次レポート</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</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) {
|
function coerceValue(field, value) {
|
||||||
|
if (value === '') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (field === 'store_id' || field === 'expense_category_id') {
|
||||||
|
return Number(value);
|
||||||
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,14 +8,87 @@
|
|||||||
<h2>月次レポート</h2>
|
<h2>月次レポート</h2>
|
||||||
<form method="get">
|
<form method="get">
|
||||||
<label>
|
<label>
|
||||||
年月
|
開始日
|
||||||
<input type="month" name="target_month" value="{{ target_month }}">
|
<input type="date" name="start_date" value="{{ start_date }}">
|
||||||
</label>
|
</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>
|
<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>
|
</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 %}
|
{% if error_message %}
|
||||||
<p style="color: #b00020;">{{ error_message }}</p>
|
<p style="color: #b00020;">{{ error_message }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if not show_report %}
|
||||||
|
<p>条件を指定して表示ボタンを押してください。</p>
|
||||||
|
{% else %}
|
||||||
<div>
|
<div>
|
||||||
<h3>合計</h3>
|
<h3>合計</h3>
|
||||||
<p class="amount">{{ report.total_amount|intcomma }} 円</p>
|
<p class="amount">{{ report.total_amount|intcomma }} 円</p>
|
||||||
@@ -50,6 +123,7 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>日付</th>
|
<th>日付</th>
|
||||||
|
<th>経費区分</th>
|
||||||
<th>利用先</th>
|
<th>利用先</th>
|
||||||
<th class="amount">金額</th>
|
<th class="amount">金額</th>
|
||||||
<th>備考</th>
|
<th>備考</th>
|
||||||
@@ -59,13 +133,14 @@
|
|||||||
{% for detail in row.details %}
|
{% for detail in row.details %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ detail.use_date }}</td>
|
<td>{{ detail.use_date }}</td>
|
||||||
|
<td>{{ detail.expense_category_name|default:"未設定" }}</td>
|
||||||
<td>{{ detail.description }}</td>
|
<td>{{ detail.description }}</td>
|
||||||
<td class="amount">{{ detail.amount|intcomma }}</td>
|
<td class="amount">{{ detail.amount|intcomma }}</td>
|
||||||
<td>{{ detail.note }}</td>
|
<td>{{ detail.note }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="4">対象データがありません。</td>
|
<td colspan="5">対象データがありません。</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -102,6 +177,7 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>日付</th>
|
<th>日付</th>
|
||||||
|
<th>経費区分</th>
|
||||||
<th>利用先</th>
|
<th>利用先</th>
|
||||||
<th class="amount">金額</th>
|
<th class="amount">金額</th>
|
||||||
<th>備考</th>
|
<th>備考</th>
|
||||||
@@ -111,13 +187,14 @@
|
|||||||
{% for detail in row.details %}
|
{% for detail in row.details %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ detail.use_date }}</td>
|
<td>{{ detail.use_date }}</td>
|
||||||
|
<td>{{ detail.expense_category_name|default:"未設定" }}</td>
|
||||||
<td>{{ detail.description }}</td>
|
<td>{{ detail.description }}</td>
|
||||||
<td class="amount">{{ detail.amount|intcomma }}</td>
|
<td class="amount">{{ detail.amount|intcomma }}</td>
|
||||||
<td>{{ detail.note }}</td>
|
<td>{{ detail.note }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="4">対象データがありません。</td>
|
<td colspan="5">対象データがありません。</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -154,6 +231,7 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>日付</th>
|
<th>日付</th>
|
||||||
|
<th>経費区分</th>
|
||||||
<th>利用先</th>
|
<th>利用先</th>
|
||||||
<th class="amount">金額</th>
|
<th class="amount">金額</th>
|
||||||
<th>備考</th>
|
<th>備考</th>
|
||||||
@@ -163,13 +241,14 @@
|
|||||||
{% for detail in row.details %}
|
{% for detail in row.details %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ detail.use_date }}</td>
|
<td>{{ detail.use_date }}</td>
|
||||||
|
<td>{{ detail.expense_category_name|default:"未設定" }}</td>
|
||||||
<td>{{ detail.description }}</td>
|
<td>{{ detail.description }}</td>
|
||||||
<td class="amount">{{ detail.amount|intcomma }}</td>
|
<td class="amount">{{ detail.amount|intcomma }}</td>
|
||||||
<td>{{ detail.note }}</td>
|
<td>{{ detail.note }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="4">対象データがありません。</td>
|
<td colspan="5">対象データがありません。</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -185,5 +264,6 @@
|
|||||||
<li>対象総件数: {{ report.unclassified_counts.total }}</li>
|
<li>対象総件数: {{ report.unclassified_counts.total }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>月次レポート</h1>
|
<h1>月次レポート</h1>
|
||||||
<p>対象年月: {{ target_month }}</p>
|
<p>対象期間: {{ target_period }}</p>
|
||||||
<h2>合計</h2>
|
<h2>合計</h2>
|
||||||
<p class="amount">{{ report.total_amount|intcomma }} 円</p>
|
<p class="amount">{{ report.total_amount|intcomma }} 円</p>
|
||||||
<h2>店舗別合計</h2>
|
<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装飾やフロントフレームワークは不要。
|
過剰な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