1
0
forked from erp-dev/erp
Files
erpnew/api_v1/views/stock_change_views/list.py

361 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
库存变动记录列表查询视图
"""
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 # 仅查询未被消耗的明细
).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)