feat: update finance manager and fix some issue
This commit is contained in:
@@ -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'
|
||||
@@ -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('初始化完成!'))
|
||||
@@ -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
|
||||
@@ -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__'
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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)),
|
||||
]
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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 = [
|
||||
|
||||
Reference in New Issue
Block a user