|
|
@@ -41,7 +41,12 @@ from django.db.models import OuterRef, Subquery
|
|
|
|
|
|
|
|
|
|
|
|
-
|
|
|
+def format_decimal(value):
|
|
|
+ """格式化Decimal值为字符串,避免科学计数法"""
|
|
|
+ if isinstance(value, Decimal):
|
|
|
+ # 格式化为字符串,保留小数点后6位,但去除尾部多余的0
|
|
|
+ return format(value.normalize(), 'f')
|
|
|
+ return str(value)
|
|
|
|
|
|
class MaterialChangeHistoryViewSet(viewsets.ModelViewSet):
|
|
|
filter_backends = [DjangoFilterBackend, OrderingFilter, ]
|
|
|
@@ -242,7 +247,178 @@ class MaterialChangeHistoryViewSet(viewsets.ModelViewSet):
|
|
|
}
|
|
|
|
|
|
return Response(response_data)
|
|
|
+
|
|
|
+ def export_summary(self, request):
|
|
|
+ """
|
|
|
+ 导出物料库存变动汇总数据为CSV
|
|
|
+ """
|
|
|
+ # 重用summary方法获取数据
|
|
|
+ start_time = request.query_params.get('start_time') or None
|
|
|
+ end_time = request.query_params.get('end_time') or None
|
|
|
+ material_ids_code = request.query_params.get('goods_code__icontains') or None
|
|
|
+
|
|
|
+ response = self.summary_all( start_time=start_time, end_time=end_time, material_ids_code=material_ids_code)
|
|
|
+ data = response.data
|
|
|
+ query_time_range = data['query_time_range']
|
|
|
+ # 创建CSV响应
|
|
|
+ from django.http import HttpResponse
|
|
|
+ import csv
|
|
|
+ response = HttpResponse(content_type='text/csv')
|
|
|
+ response['Content-Disposition'] = 'attachment; filename="material_change_summary.csv"'
|
|
|
+
|
|
|
+ # 创建CSV写入器
|
|
|
+ csv_writer = csv.writer(response)
|
|
|
+
|
|
|
+ # 写入表头
|
|
|
+ headers = [
|
|
|
+ '开始时间','结束时间',
|
|
|
+ '存货编码', '存货名称', '计量单位',
|
|
|
+ '期初数量', '期末数量', '期间变化',
|
|
|
+ '入库数量', '出库数量', '出入库差异',
|
|
|
+ '对比结果'
|
|
|
+ ]
|
|
|
+ csv_writer.writerow(headers)
|
|
|
|
|
|
+ # 写入数据行
|
|
|
+ for item in data['results']:
|
|
|
+ # 处理对比结果的显示
|
|
|
+ is_consistent = "一致" if item['is_consistent'] else "不一致"
|
|
|
+
|
|
|
+ row = [
|
|
|
+ query_time_range.get('start_time', ''),
|
|
|
+ query_time_range.get('end_time', ''),
|
|
|
+ item['goods_code'],
|
|
|
+ item['goods_desc'],
|
|
|
+ item['goods_unit'],
|
|
|
+ # 数量字段格式化为字符串,避免科学计数法
|
|
|
+ format_decimal(item['opening_quantity']),
|
|
|
+ format_decimal(item['closing_quantity']),
|
|
|
+ format_decimal(item['net_change']),
|
|
|
+ format_decimal(item['total_in']),
|
|
|
+ format_decimal(item['total_out']),
|
|
|
+ format_decimal(item['theoretical_change']),
|
|
|
+ is_consistent
|
|
|
+ ]
|
|
|
+ csv_writer.writerow(row)
|
|
|
+
|
|
|
+ return response
|
|
|
+
|
|
|
+ def summary_all(self, start_time=None, end_time=None, material_ids_code=None):
|
|
|
+ """
|
|
|
+ 获取物料库存变动汇总数据(支持分页)
|
|
|
+ """
|
|
|
+ # 获取请求参数
|
|
|
+
|
|
|
+
|
|
|
+ # 记录查询时间范围
|
|
|
+ query_time_range = {}
|
|
|
+
|
|
|
+ # 如果没有提供时间段,使用当前月份
|
|
|
+ if not start_time or not end_time:
|
|
|
+ today = timezone.now().date()
|
|
|
+ start_time = datetime(today.year, today.month, 1)
|
|
|
+ end_time = start_time + timedelta(days=32)
|
|
|
+ end_time = datetime(end_time.year, end_time.month, 1) - timedelta(days=1)
|
|
|
+ end_time = datetime.combine(end_time, datetime.max.time())
|
|
|
+ query_time_range['default_time_range'] = True
|
|
|
+ else:
|
|
|
+ query_time_range['default_time_range'] = False
|
|
|
+
|
|
|
+ # 转换为datetime对象
|
|
|
+ if isinstance(start_time, str):
|
|
|
+ start_time = datetime.fromisoformat(start_time)
|
|
|
+ if isinstance(end_time, str):
|
|
|
+ end_time = datetime.fromisoformat(end_time)
|
|
|
+
|
|
|
+ # 存储查询时间范围用于返回
|
|
|
+ query_time_range['start_time'] = start_time.isoformat()
|
|
|
+ query_time_range['end_time'] = end_time.isoformat()
|
|
|
+
|
|
|
+ # 创建基础查询集
|
|
|
+ queryset = MaterialChangeHistory.objects.filter(
|
|
|
+ change_time__gte=start_time,
|
|
|
+ change_time__lte=end_time
|
|
|
+ )
|
|
|
+
|
|
|
+ # 如果有物料ID过滤
|
|
|
+ if material_ids_code:
|
|
|
+ queryset = queryset.filter(goods_code__icontains=material_ids_code)
|
|
|
+
|
|
|
+ # 获取每个物料的期初和期末数量(直接从过滤集中获取)
|
|
|
+ material_codes = queryset.values_list('goods_code', flat=True).distinct()
|
|
|
+ opening_closing_data = {}
|
|
|
+
|
|
|
+ for code in material_codes:
|
|
|
+ # 获取该物料在时间段内的第一条记录(最早记录)
|
|
|
+ first_record = queryset.filter(goods_code=code).order_by('change_time').first()
|
|
|
+ # 获取该物料在时间段内的最后一条记录(最晚记录)
|
|
|
+ last_record = queryset.filter(goods_code=code).order_by('-change_time').first()
|
|
|
+
|
|
|
+ # 期初数量 = 第一条记录的期初数量
|
|
|
+ # 期末数量 = 最后一条记录的期末数量
|
|
|
+ opening_closing_data[code] = {
|
|
|
+ 'opening_quantity': first_record.opening_quantity,
|
|
|
+ 'closing_quantity': last_record.closing_quantity
|
|
|
+ }
|
|
|
+
|
|
|
+ # 计算期间出入库总量
|
|
|
+ period_data = queryset.values('goods_code').annotate(
|
|
|
+ total_in=Sum('in_quantity'),
|
|
|
+ total_out=Sum('out_quantity')
|
|
|
+ )
|
|
|
+
|
|
|
+ # 构建结果字典
|
|
|
+ result = {}
|
|
|
+ for item in period_data:
|
|
|
+ material_code = item['goods_code']
|
|
|
+ goods = MaterialChangeHistory.objects.filter(goods_code=material_code).first()
|
|
|
+
|
|
|
+ # 获取该物料的期初和期末数量
|
|
|
+ oc_data = opening_closing_data.get(material_code, {
|
|
|
+ 'opening_quantity': Decimal('0'),
|
|
|
+ 'closing_quantity': Decimal('0')
|
|
|
+ })
|
|
|
+
|
|
|
+ result[material_code] = {
|
|
|
+ 'material_code': material_code,
|
|
|
+ 'goods_code': goods.goods_code if goods else 'N/A',
|
|
|
+ 'goods_desc': goods.goods_desc if goods else 'N/A',
|
|
|
+ 'goods_unit': goods.goods_unit if goods else 'N/A',
|
|
|
+ 'opening_quantity': oc_data['opening_quantity'],
|
|
|
+ 'closing_quantity': oc_data['closing_quantity'],
|
|
|
+ 'total_in': item['total_in'] or Decimal('0'),
|
|
|
+ 'total_out': item['total_out'] or Decimal('0'),
|
|
|
+ 'net_change': Decimal('0'), # 后面计算
|
|
|
+ 'theoretical_change': Decimal('0'), # 后面计算
|
|
|
+ 'is_consistent': True
|
|
|
+ }
|
|
|
+
|
|
|
+ # 计算净含量变化和理论变化量,并进行一致性校验
|
|
|
+ for material_code, data in result.items():
|
|
|
+ # 净含量变化 = 期末 - 期初
|
|
|
+ net_change = data['closing_quantity'] - data['opening_quantity']
|
|
|
+ data['net_change'] = net_change
|
|
|
+
|
|
|
+ # 理论变化量 = 入库 - 出库
|
|
|
+ theoretical_change = data['total_in'] - data['total_out']
|
|
|
+ data['theoretical_change'] = theoretical_change
|
|
|
+
|
|
|
+ # 检查是否一致(允许小数点后3位的差异)
|
|
|
+ tolerance = Decimal('0.001')
|
|
|
+ data['is_consistent'] = abs(net_change - theoretical_change) <= tolerance
|
|
|
+
|
|
|
+ # 转换为列表格式
|
|
|
+ result_list = list(result.values())
|
|
|
+
|
|
|
+ # 应用分页
|
|
|
+
|
|
|
+ response_data = {
|
|
|
+ 'query_time_range': query_time_range,
|
|
|
+ 'count': len(result_list),
|
|
|
+ 'results': result_list # 返回所有结果,不分页
|
|
|
+ }
|
|
|
+ return Response(response_data)
|
|
|
+
|
|
|
def summary(self, request):
|
|
|
"""
|
|
|
获取物料库存变动汇总数据(支持分页)
|