using AgriculturalPlatform.Api.Data; using AgriculturalPlatform.Api.Dtos; using AgriculturalPlatform.Api.Models; using AgriculturalPlatform.Api.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace AgriculturalPlatform.Api.Controllers; [ApiController] [Route("api/invoices")] [Authorize] public class InvoicesController( AppDbContext db, DataScopeService scope, CurrentUserService currentUser, NumberGenerator numberGen) : ControllerBase { /// 发票列表 [HttpGet] public async Task>> List( string? status, int? farmerId, int? productId, DateTime? from, DateTime? to, [FromQuery] int page = 1, [FromQuery] int pageSize = 20, [FromQuery] string? keyword = null) { var visible = await scope.GetVisibleOrgIdsAsync(currentUser.Get()); var query = db.Invoices .Include(i => i.Farmer).Include(i => i.PurchaserOrg) .Include(i => i.Product).Include(i => i.PurchaseOrder).Include(i => i.Operator) .AsQueryable(); if (visible is not null) query = query.Where(i => visible.Contains(i.PurchaserOrgId)); if (!string.IsNullOrWhiteSpace(status) && Enum.TryParse(status, out var st)) query = query.Where(i => i.Status == st); if (farmerId.HasValue) query = query.Where(i => i.FarmerId == farmerId); if (productId.HasValue) query = query.Where(i => i.ProductId == productId); if (from.HasValue) query = query.Where(i => i.IssueDate >= from); if (to.HasValue) query = query.Where(i => i.IssueDate < to.Value.Date.AddDays(1)); if (!string.IsNullOrWhiteSpace(keyword)) query = query.Where(i => i.InvoiceNo.Contains(keyword) || (i.Farmer != null && i.Farmer.Name.Contains(keyword)) || (i.Farmer != null && i.Farmer.IdCard.Contains(keyword))); var total = await query.CountAsync(); var items = await query.OrderByDescending(i => i.IssueDate) .Skip((page - 1) * pageSize).Take(pageSize) .ToListAsync(); return Ok(new PagedResult(items.ToDtos(), total)); } /// /// 开票状态统计:以「过磅称重有效收购单」为口径。 /// 待开票 + 开票成功 + 开票失败 + 开票异常 = 过磅称重有效收购单总数。 /// [HttpGet("stats")] public async Task Stats() { var visible = await scope.GetVisibleOrgIdsAsync(currentUser.Get()); var query = db.PurchaseOrders .Where(p => p.Status == PurchaseStatus.Completed) .AsQueryable(); if (visible is not null) query = query.Where(p => visible.Contains(p.PurchaserOrgId)); var total = await query.CountAsync(); var issued = await query.CountAsync(p => db.Invoices.Any(i => i.PurchaseOrderId == p.Id && i.Status == InvoiceStatus.Issued)); var failed = await query.CountAsync(p => !db.Invoices.Any(i => i.PurchaseOrderId == p.Id && i.Status == InvoiceStatus.Issued) && db.Invoices.Any(i => i.PurchaseOrderId == p.Id && i.Status == InvoiceStatus.Failed)); var abnormal = await query.CountAsync(p => !db.Invoices.Any(i => i.PurchaseOrderId == p.Id && i.Status == InvoiceStatus.Issued) && !db.Invoices.Any(i => i.PurchaseOrderId == p.Id && i.Status == InvoiceStatus.Failed) && db.Invoices.Any(i => i.PurchaseOrderId == p.Id && i.Status == InvoiceStatus.Abnormal)); var pending = total - issued - failed - abnormal; return Ok(new { total, pending, issued, failed, abnormal }); } /// 开票状态优先级(用于收购单多张发票时取最终状态) private static int StatusRank(InvoiceStatus s) => s switch { InvoiceStatus.Issued => 4, InvoiceStatus.Failed => 3, InvoiceStatus.Abnormal => 2, InvoiceStatus.Pending => 1, _ => 0 }; /// 收购单维度开票清单(反向开票数据源:过磅称重有效收购单) /// None/Pending/Issued/Failed/Abnormal,空为全部 [HttpGet("orders")] public async Task>> OrderList( string? status, DateTime? from, DateTime? to, [FromQuery] int page = 1, [FromQuery] int pageSize = 20, [FromQuery] string? keyword = null) { var visible = await scope.GetVisibleOrgIdsAsync(currentUser.Get()); var query = db.PurchaseOrders .Include(p => p.Farmer).Include(p => p.Product).Include(p => p.PurchaserOrg) .Where(p => p.Status == PurchaseStatus.Completed) .AsQueryable(); if (visible is not null) query = query.Where(p => visible.Contains(p.PurchaserOrgId)); if (from.HasValue) query = query.Where(p => p.WeighOutAt >= from); if (to.HasValue) query = query.Where(p => p.WeighOutAt < to.Value.Date.AddDays(1)); if (!string.IsNullOrWhiteSpace(keyword)) query = query.Where(p => p.OrderNo.Contains(keyword) || (p.Farmer != null && p.Farmer.Name.Contains(keyword)) || (p.Farmer != null && p.Farmer.IdCard.Contains(keyword))); var all = await query.OrderByDescending(p => p.WeighOutAt).ToListAsync(); // 非作废发票按收购单聚合,取优先级最高的那条 var orderIds = all.Select(p => p.Id).ToArray(); var invoices = await db.Invoices .Where(i => i.Status != InvoiceStatus.Reversed && i.PurchaseOrderId != null && orderIds.Contains(i.PurchaseOrderId.Value)) .ToListAsync(); var invMap = invoices .Where(i => i.PurchaseOrderId.HasValue) .GroupBy(i => i.PurchaseOrderId!.Value) .ToDictionary(g => g.Key, g => g.OrderByDescending(x => StatusRank(x.Status)).First()); var mapped = all.Select(p => { invMap.TryGetValue(p.Id, out var inv); var invStatus = inv is null ? "None" : inv.Status.ToString(); return new OrderInvoiceDto( p.Id, p.OrderNo, p.WeighOutAt, p.FarmerId, p.Farmer?.Name ?? "", p.Farmer?.IdCard ?? "", p.ProductId, p.Product?.Name ?? "", p.Unit, p.NetWeight, p.UnitPrice, p.Amount, p.PurchaserOrgId, p.PurchaserOrg?.Name ?? "", p.PurchaserOrg?.TaxNo, inv?.Id, inv?.InvoiceNo, string.IsNullOrEmpty(inv?.BatchNo) ? null : inv.BatchNo, invStatus, inv?.IssueDate); }); // OPEN = 待开票(未开票 None 或 开票中 Pending) if (!string.IsNullOrWhiteSpace(status)) mapped = status == "OPEN" ? mapped.Where(x => x.InvoiceStatus is "None" or "Pending") : mapped.Where(x => x.InvoiceStatus == status); var list = mapped.ToList(); var total = list.Count; var items = list.Skip((page - 1) * pageSize).Take(pageSize).ToList(); return Ok(new PagedResult(items, total)); } /// 可开票的已完成收购单(按农户) [HttpGet("available-orders")] public async Task AvailableOrders(int farmerId) { var orders = await db.PurchaseOrders .Where(p => p.FarmerId == farmerId && p.Status == PurchaseStatus.Completed) .Where(p => !db.Invoices.Any(i => i.PurchaseOrderId == p.Id && i.Status == InvoiceStatus.Issued)) .Include(p => p.Product) .Select(p => new { p.Id, p.OrderNo, ProductName = p.Product!.Name, p.ProductId, p.NetWeight, p.Unit, p.UnitPrice, p.Amount, p.WeighOutAt }) .ToListAsync(); return Ok(orders); } /// 开具收购发票(反向开票:收购方向农户开具) [HttpPost] public async Task> Create(InvoiceCreateRequest req) { if (!await db.Farmers.AnyAsync(f => f.Id == req.FarmerId)) return BadRequest(new { message = "农户不存在" }); if (!await db.Products.AnyAsync(p => p.Id == req.ProductId)) return BadRequest(new { message = "品种不存在" }); if (!await db.Organizations.AnyAsync(o => o.Id == req.PurchaserOrgId)) return BadRequest(new { message = "收购方组织不存在" }); if (req.PurchaseOrderId.HasValue) { var order = await db.PurchaseOrders.FindAsync(req.PurchaseOrderId); if (order is null || order.FarmerId != req.FarmerId) return BadRequest(new { message = "收购单不存在或与农户不匹配" }); if (await db.Invoices.AnyAsync(i => i.PurchaseOrderId == req.PurchaseOrderId && i.Status == InvoiceStatus.Issued)) return BadRequest(new { message = "该收购单已开具发票" }); } var cu = currentUser.Get()!; var invoiceNo = await numberGen.NextAsync("FP"); var taxAmount = Math.Round(req.Amount * req.TaxRate / 100m, 2); var invoice = new Invoice { InvoiceNo = invoiceNo, InvoiceKind = req.InvoiceKind, FarmerId = req.FarmerId, PurchaserOrgId = req.PurchaserOrgId, PurchaseOrderId = req.PurchaseOrderId, ProductId = req.ProductId, Quantity = req.Quantity, Unit = req.Unit, UnitPrice = req.UnitPrice, Amount = req.Amount, TaxRate = req.TaxRate, TaxAmount = taxAmount, IssueDate = DateTime.Now, Status = InvoiceStatus.Pending, OperatorId = cu.Id }; db.Invoices.Add(invoice); await db.SaveChangesAsync(); return Ok((await db.Invoices .Include(i => i.Farmer).Include(i => i.PurchaserOrg) .Include(i => i.Product).Include(i => i.PurchaseOrder).Include(i => i.Operator) .FirstAsync(i => i.Id == invoice.Id)).ToDto()); } /// 开票结果回填(模拟税控平台回调:Issued/Failed/Abnormal) [HttpPut("{id:int}/result")] public async Task> UpdateResult(int id, InvoiceResultRequest req) { var invoice = await db.Invoices.FindAsync(id); if (invoice is null) return NotFound(); if (invoice.Status == InvoiceStatus.Reversed) return BadRequest(new { message = "已作废发票不可更新状态" }); if (!Enum.TryParse(req.Status, out var st) || st == InvoiceStatus.Reversed) return BadRequest(new { message = "开票结果状态不正确" }); invoice.Status = st; invoice.IssueDate = DateTime.Now; if (!string.IsNullOrWhiteSpace(req.Message)) invoice.ReverseReason = req.Message; await db.SaveChangesAsync(); return Ok((await db.Invoices .Include(i => i.Farmer).Include(i => i.PurchaserOrg) .Include(i => i.Product).Include(i => i.PurchaseOrder).Include(i => i.Operator) .FirstAsync(i => i.Id == invoice.Id)).ToDto()); } /// 发票作废(红冲) [HttpPut("{id:int}/reverse")] public async Task Reverse(int id, InvoiceReverseRequest req) { var invoice = await db.Invoices.FindAsync(id); if (invoice is null) return NotFound(); if (invoice.Status != InvoiceStatus.Issued) return BadRequest(new { message = "该发票已作废" }); invoice.Status = InvoiceStatus.Reversed; invoice.ReversedAt = DateTime.Now; invoice.ReverseReason = req.Reason; await db.SaveChangesAsync(); return Ok(new { message = "作废成功" }); } /// 创建开票批次并批量发起开票(基于过磅称重有效收购单,生成待开票 Pending 发票) [HttpPost("batches")] public async Task> CreateBatch(CreateBatchRequest req) { if (req.OrderIds is null || req.OrderIds.Length == 0) return BadRequest(new { message = "请选择要开票的收购单" }); var cu = currentUser.Get()!; var visible = await scope.GetVisibleOrgIdsAsync(currentUser.Get()); var orderIds = req.OrderIds.Distinct().ToArray(); var orders = await db.PurchaseOrders .Where(p => orderIds.Contains(p.Id) && p.Status == PurchaseStatus.Completed) .ToListAsync(); if (visible is not null) orders = orders.Where(p => visible.Contains(p.PurchaserOrgId)).ToList(); var invoicedIds = await db.Invoices .Where(i => i.Status != InvoiceStatus.Reversed && i.PurchaseOrderId != null && orderIds.Contains(i.PurchaseOrderId.Value)) .Select(i => i.PurchaseOrderId!.Value) .ToListAsync(); var validOrders = orders.Where(p => !invoicedIds.Contains(p.Id)).ToList(); if (validOrders.Count == 0) return BadRequest(new { message = "所选收购单均已开票,无可开票单据" }); var batchNo = await numberGen.NextAsync("PK"); foreach (var order in validOrders) { db.Invoices.Add(new Invoice { InvoiceNo = await numberGen.NextAsync("FP"), BatchNo = batchNo, InvoiceKind = "销售发票", FarmerId = order.FarmerId, PurchaserOrgId = order.PurchaserOrgId, PurchaseOrderId = order.Id, ProductId = order.ProductId, Quantity = order.NetWeight, Unit = order.Unit, UnitPrice = order.UnitPrice, Amount = order.Amount, TaxRate = 0, TaxAmount = 0, IssueDate = DateTime.Now, Status = InvoiceStatus.Pending, OperatorId = cu.Id }); } await db.SaveChangesAsync(); return Ok(new BatchProgressDto(batchNo, validOrders.Count, validOrders.Count, 0, 0, 0, false)); } /// 开票批次进度(开票中/成功/失败/异常) [HttpGet("batches/{batchNo}/progress")] public async Task> BatchProgress(string batchNo) { var query = db.Invoices.Where(i => i.BatchNo == batchNo); var total = await query.CountAsync(); if (total == 0) return NotFound(); var pending = await query.CountAsync(i => i.Status == InvoiceStatus.Pending); var issued = await query.CountAsync(i => i.Status == InvoiceStatus.Issued); var failed = await query.CountAsync(i => i.Status == InvoiceStatus.Failed); var abnormal = await query.CountAsync(i => i.Status == InvoiceStatus.Abnormal); return Ok(new BatchProgressDto(batchNo, total, pending, issued, failed, abnormal, pending == 0)); } /// 模拟税控平台处理:每次推进批次中的部分待开票发票为成功/失败/异常 [HttpPost("batches/{batchNo}/step")] public async Task> BatchStep(string batchNo) { var pendingInvoices = await db.Invoices .Where(i => i.BatchNo == batchNo && i.Status == InvoiceStatus.Pending) .OrderBy(i => i.Id) .Take(3) .ToListAsync(); var rnd = Random.Shared; foreach (var inv in pendingInvoices) { var r = rnd.Next(100); inv.Status = r < 70 ? InvoiceStatus.Issued : r < 85 ? InvoiceStatus.Failed : InvoiceStatus.Abnormal; if (inv.Status == InvoiceStatus.Failed) inv.ReverseReason = "开票平台处理失败(模拟):税控设备连接超时"; else if (inv.Status == InvoiceStatus.Abnormal) inv.ReverseReason = "开票平台返回异常(模拟):数据校验未通过"; inv.IssueDate = DateTime.Now; } await db.SaveChangesAsync(); return await BatchProgress(batchNo); } }