forked from erp-dev/erp
363 lines
16 KiB
Python
363 lines
16 KiB
Python
"""
|
||
库存变动记录列表查询视图
|
||
"""
|
||
|
||
from rest_framework import status, views
|
||
from rest_framework.response import Response
|
||
from rest_framework.permissions import IsAuthenticated
|
||
from datetime import datetime, timedelta
|
||
import logging
|
||
|
||
from stock import models as stock_models
|
||
from drf_spectacular.utils import extend_schema, OpenApiParameter
|
||
from drf_spectacular.types import OpenApiTypes
|
||
|
||
from .mixins import StockChangeViewMixin
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class ListStockChangesView(StockChangeViewMixin, views.APIView):
|
||
"""获取库存变动记录列表"""
|
||
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
@extend_schema(
|
||
tags=['读取出入库列表'],
|
||
parameters=[
|
||
OpenApiParameter(name='start', type=OpenApiTypes.DATE, description='开始日期 (YYYY-MM-DD),默认为昨天'),
|
||
OpenApiParameter(name='end', type=OpenApiTypes.DATE, description='结束日期 (YYYY-MM-DD),默认为昨天'),
|
||
OpenApiParameter(name='type', type=OpenApiTypes.INT, description='变动类型 (1=入库, 2=出库)'),
|
||
OpenApiParameter(name='warehouse', type=OpenApiTypes.INT, description='仓库ID'),
|
||
OpenApiParameter(name='product', type=OpenApiTypes.INT, description='产品ID'),
|
||
OpenApiParameter(name='is_finished', type=OpenApiTypes.BOOL, description='是否已完成'),
|
||
OpenApiParameter(name='include_details', type=OpenApiTypes.BOOL, description='是否包含明细,默认true'),
|
||
],
|
||
responses={200: dict},
|
||
summary="获取库存变动记录列表",
|
||
description="按时间范围查询库存变动记录,支持多种过滤条件"
|
||
)
|
||
def get(self, request):
|
||
"""
|
||
获取库存变动记录列表(按时间范围查询)
|
||
|
||
查询参数:
|
||
- start: 开始日期 (YYYY-MM-DD),可选,默认为昨天
|
||
- end: 结束日期 (YYYY-MM-DD),可选,默认为昨天
|
||
- type: 变动类型 (1=入库, 2=出库),可选
|
||
- warehouse: 仓库ID,可选
|
||
- product: 产品ID,可选
|
||
- is_finished: 是否已完成 (true/false),可选
|
||
- include_details: 是否包含明细记录 (true/false),可选,默认为 true
|
||
"""
|
||
|
||
# 检查员工权限
|
||
if not self.check_employee_permission(request):
|
||
return self.permission_error_response('无权限访问')
|
||
|
||
merchant_id = self.get_merchant_id(request)
|
||
|
||
try:
|
||
# 获取查询参数
|
||
start_date_str = request.GET.get('start')
|
||
end_date_str = request.GET.get('end')
|
||
|
||
# 默认查询昨天的数据
|
||
if not start_date_str and not end_date_str:
|
||
yesterday = datetime.now().date() - timedelta(days=1)
|
||
start_date = yesterday
|
||
end_date = yesterday
|
||
else:
|
||
# 解析日期参数
|
||
try:
|
||
if start_date_str:
|
||
start_date = datetime.strptime(start_date_str, '%Y-%m-%d').date()
|
||
else:
|
||
start_date = datetime.now().date() - timedelta(days=1)
|
||
|
||
if end_date_str:
|
||
end_date = datetime.strptime(end_date_str, '%Y-%m-%d').date()
|
||
else:
|
||
end_date = start_date
|
||
except ValueError:
|
||
return Response({
|
||
'error': '日期格式错误,请使用 YYYY-MM-DD 格式'
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 验证日期范围
|
||
if start_date > end_date:
|
||
return Response({
|
||
'error': '开始日期不能大于结束日期'
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 构建查询
|
||
queryset = stock_models.StockChangeRecord.objects.filter(
|
||
merchant_id=merchant_id,
|
||
created_at__date__gte=start_date,
|
||
created_at__date__lte=end_date
|
||
).select_related('warehouse', 'created_by')
|
||
|
||
# 可选过滤条件
|
||
change_type = request.GET.get('type')
|
||
if change_type:
|
||
try:
|
||
queryset = queryset.filter(type=int(change_type))
|
||
except ValueError:
|
||
return Response({
|
||
'error': 'type 参数必须为整数'
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
warehouse_id = request.GET.get('warehouse')
|
||
if warehouse_id:
|
||
try:
|
||
warehouse_id = int(warehouse_id)
|
||
# 验证仓库是否对用户可见
|
||
if not self.validate_warehouse_visibility(warehouse_id, request):
|
||
return Response({
|
||
'error': f'仓库ID {warehouse_id} 对当前用户不可见'
|
||
}, status=status.HTTP_403_FORBIDDEN)
|
||
queryset = queryset.filter(warehouse_id=warehouse_id)
|
||
except ValueError:
|
||
return Response({
|
||
'error': 'warehouse 参数必须为整数'
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 产品过滤参数
|
||
product_id = request.GET.get('product')
|
||
if product_id:
|
||
try:
|
||
product_id = int(product_id)
|
||
# 验证产品是否对用户可见
|
||
if not self.validate_product_visibility(product_id, request):
|
||
return Response({
|
||
'error': f'产品ID {product_id} 对当前用户不可见'
|
||
}, status=status.HTTP_403_FORBIDDEN)
|
||
# 只返回包含该产品的库存变动记录
|
||
queryset = queryset.filter(details__product_id=product_id).distinct()
|
||
except ValueError:
|
||
return Response({
|
||
'error': 'product 参数必须为整数'
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
is_finished = request.GET.get('is_finished')
|
||
if is_finished is not None:
|
||
if is_finished.lower() == 'true':
|
||
queryset = queryset.filter(is_finished=True)
|
||
elif is_finished.lower() == 'false':
|
||
queryset = queryset.filter(is_finished=False)
|
||
|
||
# 是否包含明细记录,默认为 true
|
||
include_details_str = request.GET.get('include_details', 'true')
|
||
include_details = include_details_str.lower() != 'false'
|
||
|
||
# 过滤掉用户不可见的仓库和产品的记录
|
||
visible_records = []
|
||
for record in queryset:
|
||
# 检查仓库可见性
|
||
if not self.validate_warehouse_visibility(record.warehouse_id, request):
|
||
continue
|
||
|
||
# 检查该记录是否包含至少一个用户可见的产品
|
||
has_visible_product = False
|
||
record_details = stock_models.StockChangeDetail.objects.filter(
|
||
stock_change_record=record
|
||
).select_related('product')
|
||
|
||
for detail in record_details:
|
||
if self.validate_product_visibility(detail.product_id, request):
|
||
has_visible_product = True
|
||
break
|
||
|
||
# 只有当记录包含至少一个可见产品时才添加
|
||
if has_visible_product:
|
||
visible_records.append(record)
|
||
|
||
# 构建响应数据
|
||
results = []
|
||
for record in visible_records:
|
||
# 获取该记录的所有明细
|
||
all_details = stock_models.StockChangeDetail.objects.filter(
|
||
stock_change_record=record
|
||
).select_related('product')
|
||
|
||
# 只统计和返回用户可见的产品明细
|
||
visible_details = self.filter_visible_details(all_details, request)
|
||
total_quantity = sum(float(detail.quantity) for detail in visible_details)
|
||
|
||
record_data = self.build_record_data(record)
|
||
record_data['details_count'] = len(visible_details)
|
||
record_data['total_quantity'] = total_quantity
|
||
|
||
# 如果需要包含明细,则添加明细数据
|
||
if include_details:
|
||
record_data['details'] = [self.build_detail_data(detail) for detail in visible_details]
|
||
|
||
results.append(record_data)
|
||
|
||
response_data = {
|
||
'count': len(results),
|
||
'results': results,
|
||
'date_range': {
|
||
'start': start_date.strftime('%Y-%m-%d'),
|
||
'end': end_date.strftime('%Y-%m-%d')
|
||
}
|
||
}
|
||
|
||
logger.info(
|
||
f"用户 {request.user.username} 查询库存变动记录列表,"
|
||
f"时间范围: {start_date} 到 {end_date},共 {len(results)} 条"
|
||
)
|
||
return Response(response_data, status=status.HTTP_200_OK)
|
||
|
||
except AttributeError as e:
|
||
logger.error(f"用户 {request.user.username} 无员工信息: {str(e)}", exc_info=True)
|
||
return self.permission_error_response('用户无员工信息,无权访问')
|
||
|
||
except Exception as e:
|
||
logger.error(f"查询库存变动记录列表失败: {str(e)}", exc_info=True)
|
||
return Response({
|
||
'error': '查询库存变动记录列表失败',
|
||
'message': str(e)
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
|
||
class ListStockChangeDetailsView(StockChangeViewMixin, views.APIView):
|
||
"""获取库存变动明细列表"""
|
||
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
@extend_schema(
|
||
tags=['读取库存明细'],
|
||
parameters=[
|
||
OpenApiParameter(name='warehouse_id', type=OpenApiTypes.INT, required=True, description='仓库ID'),
|
||
OpenApiParameter(name='product_id', type=OpenApiTypes.INT, required=True, description='产品ID'),
|
||
OpenApiParameter(name='direction', type=OpenApiTypes.STR, required=False, description='变动方向 (inbound=入库, outbound=出库),默认为inbound'),
|
||
],
|
||
responses={200: dict},
|
||
summary="获取库存变动明细列表",
|
||
description="根据仓库和产品查询库存变动明细,不查询StockChangeRecord"
|
||
)
|
||
def get(self, request):
|
||
"""
|
||
获取库存变动明细列表(根据仓库和产品查询)
|
||
|
||
查询参数:
|
||
- warehouse_id: 仓库ID,必填
|
||
- product_id: 产品ID,必填
|
||
- direction: 变动方向 (inbound=入库, outbound=出库),可选,默认为inbound
|
||
"""
|
||
|
||
# 检查员工权限
|
||
if not self.check_employee_permission(request):
|
||
return self.permission_error_response('无权限访问')
|
||
|
||
merchant_id = self.get_merchant_id(request)
|
||
|
||
try:
|
||
# 获取查询参数
|
||
warehouse_id_str = request.GET.get('warehouse_id')
|
||
product_id_str = request.GET.get('product_id')
|
||
direction = request.GET.get('direction', 'inbound') # 默认为入库
|
||
|
||
# 验证必填参数
|
||
if not warehouse_id_str:
|
||
return Response({
|
||
'error': '缺少必要参数',
|
||
'message': 'warehouse_id 参数为必填'
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
if not product_id_str:
|
||
return Response({
|
||
'error': '缺少必要参数',
|
||
'message': 'product_id 参数为必填'
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 验证direction参数
|
||
if direction not in ['inbound', 'outbound']:
|
||
return Response({
|
||
'error': '参数格式错误',
|
||
'message': 'direction 参数必须为 inbound 或 outbound'
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 解析参数
|
||
try:
|
||
warehouse_id = int(warehouse_id_str)
|
||
product_id = int(product_id_str)
|
||
except ValueError:
|
||
return Response({
|
||
'error': '参数格式错误',
|
||
'message': 'warehouse_id 和 product_id 必须为整数'
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 验证仓库是否对用户可见
|
||
if not self.validate_warehouse_visibility(warehouse_id, request):
|
||
return Response({
|
||
'error': f'仓库ID {warehouse_id} 对当前用户不可见'
|
||
}, status=status.HTTP_403_FORBIDDEN)
|
||
|
||
# 验证产品是否对用户可见
|
||
if not self.validate_product_visibility(product_id, request):
|
||
return Response({
|
||
'error': f'产品ID {product_id} 对当前用户不可见'
|
||
}, status=status.HTTP_403_FORBIDDEN)
|
||
|
||
# 根据direction参数确定变动类型
|
||
if direction == 'inbound':
|
||
change_type = stock_models.StockChangeTypeEnum.ADD # 入库
|
||
else: # outbound
|
||
change_type = stock_models.StockChangeTypeEnum.REMOVE # 出库
|
||
|
||
# 查询库存变动明细,仅查询未被消耗的明细(consumed_by_detail为空)
|
||
details = stock_models.StockChangeDetail.objects.filter(
|
||
stock_change_record__merchant_id=merchant_id,
|
||
stock_change_record__warehouse_id=warehouse_id,
|
||
stock_change_record__type=change_type,
|
||
product_id=product_id,
|
||
consumed_by_detail__isnull=True # 仅查询未被消耗的明细
|
||
).exclude(
|
||
stock_freezes__status=stock_models.StockFreezeStatusEnum.FROZEN
|
||
).select_related(
|
||
'stock_change_record',
|
||
'product'
|
||
).order_by('-stock_change_record__created_at')
|
||
|
||
# 构建响应数据
|
||
results = []
|
||
for detail in details:
|
||
detail_data = self.build_detail_data(detail)
|
||
# 添加记录的额外信息
|
||
detail_data['record_type'] = detail.stock_change_record.type
|
||
detail_data['record_type_display'] = detail.stock_change_record.get_type_display()
|
||
detail_data['source_type'] = detail.stock_change_record.source_type
|
||
detail_data['source_type_display'] = detail.stock_change_record.get_source_type_display()
|
||
detail_data['record_created_at'] = detail.stock_change_record.created_at
|
||
detail_data['record_created_by'] = detail.stock_change_record.created_by.username
|
||
results.append(detail_data)
|
||
|
||
response_data = {
|
||
'count': len(results),
|
||
'results': results,
|
||
'filters': {
|
||
'warehouse_id': warehouse_id,
|
||
'product_id': product_id,
|
||
'direction': direction
|
||
}
|
||
}
|
||
|
||
logger.info(
|
||
f"用户 {request.user.username} 查询库存变动明细,"
|
||
f"仓库ID: {warehouse_id},产品ID: {product_id},共 {len(results)} 条"
|
||
)
|
||
return Response(response_data, status=status.HTTP_200_OK)
|
||
|
||
except AttributeError as e:
|
||
logger.error(f"用户 {request.user.username} 无员工信息: {str(e)}", exc_info=True)
|
||
return self.permission_error_response('用户无员工信息,无权访问')
|
||
|
||
except Exception as e:
|
||
logger.error(f"查询库存变动明细失败: {str(e)}", exc_info=True)
|
||
return Response({
|
||
'error': '查询库存变动明细失败',
|
||
'message': str(e)
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|