feat: update finance manager and fix some issue

This commit is contained in:
2025-09-19 23:10:54 +08:00
parent b36c8183f2
commit fabc00c4f3
23 changed files with 1982 additions and 22 deletions
View File
+30
View File
@@ -0,0 +1,30 @@
from django.contrib import admin
from .models import Department, Category, FinancialRecord, ProofImage
@admin.register(Department)
class DepartmentAdmin(admin.ModelAdmin):
list_display = ['name']
search_fields = ['name']
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ['name']
search_fields = ['name']
@admin.register(ProofImage)
class ProofImageAdmin(admin.ModelAdmin):
list_display = ['financial_record', 'image', 'description', 'uploaded_at']
list_filter = ['uploaded_at', 'financial_record']
search_fields = ['description', 'financial_record__title']
readonly_fields = ['uploaded_at']
@admin.register(FinancialRecord)
class FinancialRecordAdmin(admin.ModelAdmin):
list_display = ['title', 'amount', 'record_type', 'transaction_date', 'department', 'category']
list_filter = ['record_type', 'department', 'category', 'transaction_date']
search_fields = ['title', 'description']
date_hierarchy = 'transaction_date'
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class FinanceConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "finance"
@@ -0,0 +1 @@
@@ -0,0 +1,55 @@
from django.core.management.base import BaseCommand
from finance.models import Department, Category
# 创建:python manage.py init_finance_data
class Command(BaseCommand):
help = '初始化财务系统的部门和类别数据'
def handle(self, *args, **options):
# 创建部门
departments = [
'爱特工作室本部',
'程序部',
'Web部',
'游戏部',
'IOS部',
'APP部',
'UI部',
'智能应用部',
'OpenHarmony部',
'FOSS部'
]
for dept_name in departments:
dept, created = Department.objects.get_or_create(name=dept_name)
if created:
self.stdout.write(self.style.SUCCESS(f'创建部门: {dept_name}'))
else:
self.stdout.write(f'部门已存在: {dept_name}')
# 创建类别
categories = [
'办公用品',
'设备采购',
'软件授权',
'差旅费',
'会议费',
'宣传费用',
'日用品费用',
'维护费用',
'其他支出',
'项目收入',
'服务收入',
'资金交接',
'其他收入'
]
for cat_name in categories:
cat, created = Category.objects.get_or_create(name=cat_name)
if created:
self.stdout.write(self.style.SUCCESS(f'创建类别: {cat_name}'))
else:
self.stdout.write(f'类别已存在: {cat_name}')
self.stdout.write(self.style.SUCCESS('初始化完成!'))
+80
View File
@@ -0,0 +1,80 @@
from django.db import models
from django.contrib.auth.models import User
def proof_image_upload_path(instance, filename):
"""
自定义文件上传路径
格式: proofs/{记录ID}-{年月日}/{文件名}
"""
record = instance.financial_record
date_str = record.transaction_date.strftime('%Y%m%d')
folder_name = f"{record.id}-{date_str}"
return f'proofs/{folder_name}/{filename}'
class Department(models.Model):
"""部门"""
name = models.CharField(max_length=100, unique=True, verbose_name="部门名称")
def __str__(self):
return self.name
class Meta:
verbose_name = "部门"
verbose_name_plural = verbose_name
class Category(models.Model):
"""财务记录类别"""
name = models.CharField(max_length=100, unique=True, verbose_name="类别名称")
def __str__(self):
return self.name
class Meta:
verbose_name = "财务类别"
verbose_name_plural = verbose_name
class FinancialRecord(models.Model):
"""财务记录"""
RECORD_TYPE_CHOICES = [
('expense', '支出'),
('income', '收入'),
]
title = models.CharField(max_length=200, verbose_name="标题")
description = models.TextField(blank=True, null=True, verbose_name="描述")
amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="金额")
record_type = models.CharField(max_length=10, choices=RECORD_TYPE_CHOICES, verbose_name="记录类型")
transaction_date = models.DateField(verbose_name="交易日期")
department = models.ForeignKey(Department, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="所属部门")
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="类别")
fund_manager = models.CharField(max_length=100, blank=True, null=True, verbose_name="批准人")
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="创建人")
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
def __str__(self):
return f"{self.title} - {self.amount}"
class Meta:
verbose_name = "财务记录"
verbose_name_plural = verbose_name
ordering = ['-transaction_date']
class ProofImage(models.Model):
"""凭证图片"""
financial_record = models.ForeignKey(FinancialRecord, on_delete=models.CASCADE, related_name='proof_images', verbose_name="财务记录")
image = models.ImageField(upload_to=proof_image_upload_path, verbose_name="凭证图片")
description = models.CharField(max_length=200, blank=True, null=True, verbose_name="图片描述")
uploaded_at = models.DateTimeField(auto_now_add=True, verbose_name="上传时间")
def __str__(self):
return f"{self.financial_record.title} - 凭证{self.id}"
class Meta:
verbose_name = "凭证图片"
verbose_name_plural = verbose_name
+48
View File
@@ -0,0 +1,48 @@
from rest_framework import serializers
from .models import FinancialRecord, Department, Category, ProofImage
class DepartmentSerializer(serializers.ModelSerializer):
class Meta:
model = Department
fields = '__all__'
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = '__all__'
class ProofImageSerializer(serializers.ModelSerializer):
"""凭证图片序列化器"""
class Meta:
model = ProofImage
fields = ['id', 'image', 'description', 'uploaded_at']
class FinancialRecordWriteSerializer(serializers.ModelSerializer):
"""用于创建和更新财务记录的序列化器"""
class Meta:
model = FinancialRecord
fields = '__all__'
class FinancialRecordReadSerializer(serializers.ModelSerializer):
"""用于读取财务记录的序列化器"""
department = DepartmentSerializer(read_only=True)
category = CategorySerializer(read_only=True)
proof_images = ProofImageSerializer(many=True, read_only=True)
class Meta:
model = FinancialRecord
fields = '__all__'
# 保持向后兼容的默认序列化器
class FinancialRecordSerializer(serializers.ModelSerializer):
proof_images = ProofImageSerializer(many=True, read_only=True)
class Meta:
model = FinancialRecord
fields = '__all__'
+1
View File
@@ -0,0 +1 @@
+13
View File
@@ -0,0 +1,13 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import FinancialRecordViewSet, DepartmentViewSet, CategoryViewSet, ProofImageViewSet
router = DefaultRouter()
router.register(r'finance', FinancialRecordViewSet)
router.register(r'departments', DepartmentViewSet)
router.register(r'finance_categories', CategoryViewSet)
router.register(r'proof-images', ProofImageViewSet)
urlpatterns = [
path('api/', include(router.urls)),
]
+98
View File
@@ -0,0 +1,98 @@
import os
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from .models import FinancialRecord, Department, Category, ProofImage
from .serializers import (
FinancialRecordWriteSerializer,
FinancialRecordReadSerializer,
DepartmentSerializer,
CategorySerializer,
ProofImageSerializer
)
class FinancialRecordViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows financial records to be viewed or edited.
"""
queryset = FinancialRecord.objects.all()
def get_serializer_class(self):
"""根据操作类型返回不同的序列化器"""
if self.action in ['list', 'retrieve']:
return FinancialRecordReadSerializer
return FinancialRecordWriteSerializer
@action(detail=True, methods=['post'])
def upload_images(self, request, pk=None):
"""为财务记录上传多张凭证图片"""
record = self.get_object()
files = request.FILES.getlist('images')
if not files:
return Response({'error': '没有接收到图片文件'}, status=status.HTTP_400_BAD_REQUEST)
created_images = []
for file in files:
proof_image = ProofImage.objects.create(
financial_record=record,
image=file,
description=request.data.get('description', '')
)
created_images.append(ProofImageSerializer(proof_image).data)
return Response({
'message': f'成功上传 {len(created_images)} 张图片',
'images': created_images
}, status=status.HTTP_201_CREATED)
class ProofImageViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows proof images to be viewed or edited.
"""
queryset = ProofImage.objects.all()
serializer_class = ProofImageSerializer
def destroy(self, request, *args, **kwargs):
"""重写删除方法,确保删除图片记录时同时删除物理文件"""
instance = self.get_object()
# 获取文件路径
file_path = None
if instance.image:
try:
file_path = instance.image.path
except (ValueError, AttributeError):
# 如果文件路径无效或文件不存在,只删除数据库记录
pass
# 删除数据库记录
super().destroy(request, *args, **kwargs)
# 删除物理文件
if file_path and os.path.isfile(file_path):
try:
os.remove(file_path)
print(f"成功删除文件: {file_path}")
except OSError as e:
print(f"删除文件失败: {file_path}, 错误: {e}")
# 即使文件删除失败,也不抛出异常,因为数据库记录已经删除
return Response({'message': '图片删除成功'}, status=status.HTTP_200_OK)
class DepartmentViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows departments to be viewed or edited.
"""
queryset = Department.objects.all()
serializer_class = DepartmentSerializer
class CategoryViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows categories to be viewed or edited.
"""
queryset = Category.objects.all()
serializer_class = CategorySerializer
+4
View File
@@ -40,6 +40,7 @@ INSTALLED_APPS = [
"rest_framework",
"corsheaders",
"items",
"finance",
]
MIDDLEWARE = [
@@ -141,6 +142,9 @@ USE_TZ = True
STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"
# 安全设置(生产环境)
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SECURE_SSL_REDIRECT = False # 如果使用HTTPS,设置为True
+6
View File
@@ -17,9 +17,15 @@ Including another URLconf
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("items.urls")),
path("", include("finance.urls")),
path("api-auth/", include("rest_framework.urls")),
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
+1 -1
View File
@@ -5,7 +5,7 @@ from . import views
router = DefaultRouter()
router.register(r'items', views.ItemViewSet)
router.register(r'usages', views.ItemUsageViewSet)
router.register(r'categories', views.CategoryViewSet)
router.register(r'item_categories', views.CategoryViewSet)
router.register(r'users', views.UserViewSet)
urlpatterns = [