346 lines
15 KiB
C#
346 lines
15 KiB
C#
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/purchases")]
|
||
[Authorize]
|
||
public class PurchasesController(
|
||
AppDbContext db,
|
||
DataScopeService scope,
|
||
CurrentUserService currentUser,
|
||
NumberGenerator numberGen) : ControllerBase
|
||
{
|
||
/// <summary>收购单列表(分页 + 多条件 + 数据权限)</summary>
|
||
[HttpGet]
|
||
public async Task<ActionResult<PagedResult<PurchaseDto>>> List(
|
||
string? status, int? farmerId, int? productId, int? orgId, 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).Include(p => p.Operator)
|
||
.Include(p => p.Weighs)
|
||
.AsQueryable();
|
||
|
||
if (visible is not null)
|
||
query = query.Where(p => visible.Contains(p.PurchaserOrgId));
|
||
if (!string.IsNullOrWhiteSpace(status) && Enum.TryParse<PurchaseStatus>(status, out var ps))
|
||
query = query.Where(p => p.Status == ps);
|
||
if (farmerId.HasValue) query = query.Where(p => p.FarmerId == farmerId);
|
||
if (productId.HasValue) query = query.Where(p => p.ProductId == productId);
|
||
if (orgId.HasValue) query = query.Where(p => p.PurchaserOrgId == orgId);
|
||
if (from.HasValue) query = query.Where(p => p.CreatedAt >= from);
|
||
if (to.HasValue) query = query.Where(p => p.CreatedAt < 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.Phone.Contains(keyword)));
|
||
|
||
var total = await query.CountAsync();
|
||
var items = await query.OrderByDescending(p => p.CreatedAt)
|
||
.Skip((page - 1) * pageSize).Take(pageSize)
|
||
.ToListAsync();
|
||
return Ok(new PagedResult<PurchaseDto>(items.ToDtos(), total));
|
||
}
|
||
|
||
/// <summary>进行中的过磅单(触摸屏/大屏实时展示)</summary>
|
||
[HttpGet("weighing")]
|
||
public async Task<ActionResult> Weighing()
|
||
{
|
||
var visible = await scope.GetVisibleOrgIdsAsync(currentUser.Get());
|
||
var query = db.PurchaseOrders
|
||
.Include(p => p.Farmer).Include(p => p.Product)
|
||
.Where(p => p.Status == PurchaseStatus.Weighing)
|
||
.AsQueryable();
|
||
if (visible is not null)
|
||
query = query.Where(p => visible.Contains(p.PurchaserOrgId));
|
||
|
||
var items = await query.OrderByDescending(p => p.WeighInAt).Take(20).ToListAsync();
|
||
return Ok(items.ToDtos());
|
||
}
|
||
|
||
/// <summary>收购单详情</summary>
|
||
[HttpGet("{id:int}")]
|
||
public async Task<ActionResult<PurchaseDto>> Get(int id)
|
||
{
|
||
var order = await db.PurchaseOrders
|
||
.Include(p => p.Farmer).Include(p => p.Product)
|
||
.Include(p => p.PurchaserOrg).Include(p => p.Operator)
|
||
.Include(p => p.Weighs)
|
||
.FirstOrDefaultAsync(p => p.Id == id);
|
||
if (order is null) return NotFound();
|
||
return Ok(order.ToDto());
|
||
}
|
||
|
||
/// <summary>收购单统计(单数、重量合计、均价、金额合计)</summary>
|
||
[HttpGet("summary")]
|
||
public async Task<ActionResult<PurchaseSummaryDto>> Summary(
|
||
int? farmerId, int? productId, int? orgId, DateTime? from, DateTime? to)
|
||
{
|
||
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));
|
||
if (farmerId.HasValue) query = query.Where(p => p.FarmerId == farmerId);
|
||
if (productId.HasValue) query = query.Where(p => p.ProductId == productId);
|
||
if (orgId.HasValue) query = query.Where(p => p.PurchaserOrgId == orgId);
|
||
if (from.HasValue) query = query.Where(p => p.CreatedAt >= from);
|
||
if (to.HasValue) query = query.Where(p => p.CreatedAt < to.Value.Date.AddDays(1));
|
||
|
||
var orderCount = await query.CountAsync();
|
||
var totalNet = await query.SumAsync(p => (decimal?)p.NetWeight) ?? 0;
|
||
var totalAmount = await query.SumAsync(p => (decimal?)p.Amount) ?? 0;
|
||
var farmerCount = await query.Select(p => p.FarmerId).Distinct().CountAsync();
|
||
var todayStart = DateTime.Today;
|
||
var todayOrderCount = await db.PurchaseOrders
|
||
.CountAsync(p => p.Status == PurchaseStatus.Completed && p.CreatedAt >= todayStart);
|
||
var avgPrice = totalNet > 0 ? Math.Round(totalAmount / totalNet, 2) : 0;
|
||
|
||
return Ok(new PurchaseSummaryDto(orderCount, Math.Round(totalNet, 2), avgPrice,
|
||
Math.Round(totalAmount, 2), farmerCount, todayOrderCount));
|
||
}
|
||
|
||
/// <summary>新建收购单(第一次过磅:记录毛重)</summary>
|
||
[HttpPost]
|
||
public async Task<ActionResult<PurchaseDto>> Create(PurchaseCreateRequest 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 = "收购方组织不存在" });
|
||
|
||
var cu = currentUser.Get()!;
|
||
var orderNo = await numberGen.NextAsync("CG");
|
||
var order = new PurchaseOrder
|
||
{
|
||
OrderNo = orderNo,
|
||
FarmerId = req.FarmerId,
|
||
ProductId = req.ProductId,
|
||
PurchaserOrgId = req.PurchaserOrgId,
|
||
Grade = req.Grade,
|
||
Unit = req.Unit,
|
||
UnitPrice = req.UnitPrice,
|
||
GrossWeight = req.GrossWeight,
|
||
WeighCount = 1,
|
||
Status = PurchaseStatus.Weighing,
|
||
OperatorId = cu.Id,
|
||
Notes = req.Notes
|
||
};
|
||
db.PurchaseOrders.Add(order);
|
||
await db.SaveChangesAsync();
|
||
|
||
return Ok((await db.PurchaseOrders
|
||
.Include(p => p.Farmer).Include(p => p.Product)
|
||
.Include(p => p.PurchaserOrg).Include(p => p.Operator)
|
||
.FirstAsync(p => p.Id == order.Id)).ToDto());
|
||
}
|
||
|
||
/// <summary>回皮完成(第二次过磅:记录皮重,计算净重与金额)</summary>
|
||
[HttpPut("{id:int}/tare")]
|
||
public async Task<ActionResult<PurchaseDto>> CompleteTare(int id, PurchaseTareRequest req)
|
||
{
|
||
var order = await db.PurchaseOrders.FindAsync(id);
|
||
if (order is null) return NotFound();
|
||
if (order.Status != PurchaseStatus.Weighing)
|
||
return BadRequest(new { message = "该收购单当前状态不可回皮" });
|
||
if (req.TareWeight >= order.GrossWeight)
|
||
return BadRequest(new { message = "皮重不能大于等于毛重" });
|
||
|
||
order.TareWeight = req.TareWeight;
|
||
order.NetWeight = Math.Round(order.GrossWeight - req.TareWeight, 2);
|
||
order.Amount = Math.Round(order.NetWeight * order.UnitPrice, 2);
|
||
order.WeighCount = 2;
|
||
order.Status = PurchaseStatus.Completed;
|
||
order.WeighOutAt = DateTime.Now;
|
||
order.UpdatedAt = DateTime.Now;
|
||
await db.SaveChangesAsync();
|
||
|
||
return Ok((await db.PurchaseOrders
|
||
.Include(p => p.Farmer).Include(p => p.Product)
|
||
.Include(p => p.PurchaserOrg).Include(p => p.Operator)
|
||
.FirstAsync(p => p.Id == order.Id)).ToDto());
|
||
}
|
||
|
||
/// <summary>修改单价/等级(完成前可调整)</summary>
|
||
[HttpPut("{id:int}/price")]
|
||
public async Task<ActionResult<PurchaseDto>> UpdatePrice(int id, [FromBody] decimal unitPrice, string? grade = null)
|
||
{
|
||
var order = await db.PurchaseOrders.FindAsync(id);
|
||
if (order is null) return NotFound();
|
||
if (order.Status == PurchaseStatus.Cancelled)
|
||
return BadRequest(new { message = "作废单据不可修改" });
|
||
|
||
order.UnitPrice = unitPrice;
|
||
if (!string.IsNullOrWhiteSpace(grade)) order.Grade = grade;
|
||
order.Amount = Math.Round(order.NetWeight * order.UnitPrice, 2);
|
||
order.UpdatedAt = DateTime.Now;
|
||
await db.SaveChangesAsync();
|
||
|
||
return Ok((await db.PurchaseOrders
|
||
.Include(p => p.Farmer).Include(p => p.Product)
|
||
.Include(p => p.PurchaserOrg).Include(p => p.Operator)
|
||
.FirstAsync(p => p.Id == order.Id)).ToDto());
|
||
}
|
||
|
||
/// <summary>多磅收购单:一次性保存(一单多磅、多等级),完成后直接打印</summary>
|
||
[HttpPost("multi")]
|
||
public async Task<ActionResult<PurchaseDto>> CreateMulti(PurchaseMultiCreateRequest req)
|
||
{
|
||
if (req.Weighs is null || req.Weighs.Count == 0)
|
||
return BadRequest(new { message = "请至少录入一磅数据" });
|
||
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 = "收购方组织不存在" });
|
||
|
||
var cu = currentUser.Get()!;
|
||
var orderNo = await numberGen.NextAsync("CG");
|
||
var order = new PurchaseOrder
|
||
{
|
||
OrderNo = orderNo,
|
||
FarmerId = req.FarmerId,
|
||
ProductId = req.ProductId,
|
||
PurchaserOrgId = req.PurchaserOrgId,
|
||
Unit = string.IsNullOrWhiteSpace(req.Unit) ? "公斤" : req.Unit,
|
||
WeighCount = req.Weighs.Count,
|
||
Status = PurchaseStatus.Completed,
|
||
OperatorId = cu.Id,
|
||
Notes = req.Notes ?? "",
|
||
WeighInAt = DateTime.Now,
|
||
WeighOutAt = DateTime.Now,
|
||
CreatedAt = DateTime.Now,
|
||
UpdatedAt = DateTime.Now
|
||
};
|
||
|
||
var sortNo = 1;
|
||
foreach (var w in req.Weighs)
|
||
{
|
||
var net = w.NetWeight ?? Math.Round(w.GrossWeight - w.TareWeight, 2);
|
||
var amount = Math.Round(net * w.UnitPrice, 2);
|
||
order.Weighs.Add(new PurchaseWeigh
|
||
{
|
||
SortNo = sortNo++,
|
||
Grade = w.Grade,
|
||
PriceType = w.PriceType,
|
||
UnitPrice = w.UnitPrice,
|
||
GrossWeight = w.GrossWeight,
|
||
TareWeight = w.TareWeight,
|
||
NetWeight = net,
|
||
BoxCount = w.BoxCount,
|
||
Amount = amount,
|
||
CreatedAt = DateTime.Now
|
||
});
|
||
order.GrossWeight += w.GrossWeight;
|
||
order.TareWeight += w.TareWeight;
|
||
order.NetWeight += net;
|
||
order.Amount += amount;
|
||
order.BoxCount += w.BoxCount;
|
||
}
|
||
order.NetWeight = Math.Round(order.NetWeight, 2);
|
||
order.Amount = Math.Round(order.Amount, 2);
|
||
|
||
// 主字段取第一磅
|
||
var first = order.Weighs.First();
|
||
order.Grade = first.Grade;
|
||
order.UnitPrice = first.UnitPrice;
|
||
|
||
db.PurchaseOrders.Add(order);
|
||
await db.SaveChangesAsync();
|
||
|
||
return Ok((await db.PurchaseOrders
|
||
.Include(p => p.Farmer).Include(p => p.Product)
|
||
.Include(p => p.PurchaserOrg).Include(p => p.Operator)
|
||
.Include(p => p.Weighs)
|
||
.FirstAsync(p => p.Id == order.Id)).ToDto());
|
||
}
|
||
|
||
/// <summary>补打票据(记录补打日志到 Notes)</summary>
|
||
[HttpPost("{id:int}/reprint")]
|
||
public async Task<ActionResult> Reprint(int id)
|
||
{
|
||
var order = await db.PurchaseOrders.FindAsync(id);
|
||
if (order is null) return NotFound();
|
||
var cu = currentUser.Get()!;
|
||
var log = $"补打票据 {DateTime.Now:yyyy-MM-dd HH:mm} by {cu.RealName}";
|
||
order.Notes = string.IsNullOrWhiteSpace(order.Notes) ? log : order.Notes + ";" + log;
|
||
order.UpdatedAt = DateTime.Now;
|
||
await db.SaveChangesAsync();
|
||
return Ok(new { message = "已记录补打" });
|
||
}
|
||
|
||
/// <summary>票据过户(生成过户单号并记录)</summary>
|
||
[HttpPost("{id:int}/transfer")]
|
||
public async Task<ActionResult> Transfer(int id, PurchaseTransferRequest req)
|
||
{
|
||
var order = await db.PurchaseOrders.Include(p => p.Farmer).FirstOrDefaultAsync(p => p.Id == id);
|
||
if (order is null) return NotFound();
|
||
if (string.IsNullOrWhiteSpace(req.ToFarmer))
|
||
return BadRequest(new { message = "请填写过户到农户" });
|
||
if (order.Status != PurchaseStatus.Completed)
|
||
return BadRequest(new { message = "仅已完成单据可过户" });
|
||
|
||
var cu = currentUser.Get()!;
|
||
var transfer = new PurchaseTransfer
|
||
{
|
||
PurchaseOrderId = order.Id,
|
||
TransferNo = await numberGen.NextAsync("GH"),
|
||
FromFarmer = order.Farmer?.Name ?? "",
|
||
ToFarmer = req.ToFarmer,
|
||
ToIdCard = req.ToIdCard ?? "",
|
||
Reason = req.Reason ?? "",
|
||
OperatorId = cu.Id,
|
||
CreatedAt = DateTime.Now
|
||
};
|
||
db.PurchaseTransfers.Add(transfer);
|
||
order.UpdatedAt = DateTime.Now;
|
||
await db.SaveChangesAsync();
|
||
return Ok(new { message = "过户成功", transferNo = transfer.TransferNo });
|
||
}
|
||
|
||
/// <summary>收购单过户记录</summary>
|
||
[HttpGet("{id:int}/transfers")]
|
||
public async Task<ActionResult> Transfers(int id)
|
||
{
|
||
var items = await db.PurchaseTransfers.AsNoTracking()
|
||
.Include(t => t.Operator)
|
||
.Where(t => t.PurchaseOrderId == id)
|
||
.OrderByDescending(t => t.CreatedAt)
|
||
.Select(t => new PurchaseTransferDto(
|
||
t.Id, t.TransferNo, t.FromFarmer, t.ToFarmer, t.ToIdCard, t.Reason,
|
||
t.Operator != null ? t.Operator.RealName : "", t.CreatedAt))
|
||
.ToListAsync();
|
||
return Ok(items);
|
||
}
|
||
|
||
/// <summary>作废收购单</summary>
|
||
[HttpPut("{id:int}/cancel")]
|
||
public async Task<IActionResult> Cancel(int id)
|
||
{
|
||
var order = await db.PurchaseOrders.FindAsync(id);
|
||
if (order is null) return NotFound();
|
||
if (order.Status == PurchaseStatus.Cancelled)
|
||
return BadRequest(new { message = "该单据已作废" });
|
||
if (await db.PaymentRecords.AnyAsync(p => p.PurchaseOrderId == id && p.Status == PayStatus.Success))
|
||
return BadRequest(new { message = "该单据已付款,请先退款后再作废" });
|
||
|
||
order.Status = PurchaseStatus.Cancelled;
|
||
order.UpdatedAt = DateTime.Now;
|
||
await db.SaveChangesAsync();
|
||
return Ok(new { message = "作废成功" });
|
||
}
|
||
}
|