[CLAUDE] PurchaseEvaluation: luy ke TAM TINH vs CHINH XAC — ComputePendingAsync + 5-field DTO + FE 2-app sub-dong amber/banner man duyet + D4 audit-note (UAT anh Kiet 15-07, 0 mig, test 520→528)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m27s

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-07-17 14:27:19 +07:00
parent e53a69307f
commit 6664a6136a
22 changed files with 1324 additions and 56 deletions

View File

@ -318,6 +318,11 @@ public record PurchaseEvaluationDetailBundleDto(
string? FinalizeLevelName,
PurchaseEvaluationWorkflowSummaryDto Workflow);
// [S134 pe-luyke-tam-tinh] Phiếu TRƯỚC cùng cặp (ProjectId, WorkItemId) CHƯA terminal —
// render list "cần lưu ý" ở block ngân sách. MaPhieu nullable (entity string?, data cổ có null).
// Phase = int (PurchaseEvaluationPhase underlying) để FE map label VN qua PurchaseEvaluationPhaseLabel.
public record PePendingPriorPeDto(Guid Id, string? MaPhieu, int Phase);
// [S61 Mig 50] Bảng "TỔNG HỢP NGÂN SÁCH TRÌNH KÝ" theo Excel anh Kiệt — BE compute
// kèm PE detail GET. Record PeWorkItemBudgets dùng chung mọi phiếu cùng cặp
// (ProjectId, WorkItemId). BudgetId null = phiếu cũ chưa gắn Hạng mục.
@ -350,4 +355,12 @@ public record PeBudgetSummaryDto(
// [S133] true = phiếu DaDuyet serve từ snapshot ngân sách (bất biến, đọc cột ApprovedBudget*
// thay record PeWorkItemBudgets live); false = phiếu chưa duyệt đọc LIVE. FE badge "đã chốt" +
// ẩn nút sửa. Append CUỐI record (default false) → construction site cũ không gãy.
bool BudgetFrozen = false);
bool BudgetFrozen = false,
// [S134 pe-luyke-tam-tinh] Lũy kế TẠM TÍNH — phiếu TRƯỚC cùng cặp đã trình/chọn NHƯNG CHƯA duyệt
// xong (BỔ SUNG số CHÍNH XÁC PreviousSubmitted*/PreviousSelected*, KHÔNG thay). Append CUỐI record
// (default 0/null) → construction site cũ + nhánh frozen (DaDuyet bất biến) không gãy.
decimal PendingSubmittedTotal = 0,
int PendingSubmittedCount = 0,
decimal PendingSelectedTotal = 0,
int PendingSelectedCount = 0,
IReadOnlyList<PePendingPriorPeDto>? PendingPriorPes = null);

View File

@ -1,5 +1,6 @@
using Microsoft.EntityFrameworkCore;
using SolutionErp.Application.Common.Interfaces;
using SolutionErp.Application.PurchaseEvaluations.Dtos;
using SolutionErp.Domain.PurchaseEvaluations;
namespace SolutionErp.Application.PurchaseEvaluations;
@ -24,6 +25,16 @@ public readonly record struct PeBudgetAccumulation(
int PrevSelectedCount,
decimal CurrentProposalTotal);
// [S134 pe-luyke-tam-tinh] Lũy kế TẠM TÍNH — phiếu TRƯỚC cùng cặp (ProjectId, WorkItemId)
// đã trình/đã chọn NHƯNG CHƯA duyệt xong. Bổ sung display "tạm tính" — KHÔNG đổi số CHÍNH XÁC
// (PeBudgetAccumulation qua ComputeAsync bất động).
public readonly record struct PePendingAccumulation(
decimal PendingSubmittedTotal,
int PendingSubmittedCount,
decimal PendingSelectedTotal,
int PendingSelectedCount,
IReadOnlyList<PePendingPriorPeDto> PriorPes);
public static class PeBudgetAccumulator
{
public static async Task<PeBudgetAccumulation> ComputeAsync(
@ -69,4 +80,61 @@ public static class PeBudgetAccumulator
prevSelectedTotal, prevSelectedCount,
currentProposalTotal);
}
// [S134] Lũy kế TẠM TÍNH — CHỈ ĐỌC, KHÔNG mutate. peers = window Y HỆT ComputeAsync (:38-40).
// PendingSubmitted* = peers Phase ∉ {DangSoanThao, DaDuyet, ChoDuyet, TuChoi} = {TraLai + legacy 2-6}
// → Count + SUM(BudgetPeriodAmount ?? 0). ChoDuyet/DaDuyet ĐÃ nằm trong
// ComputeAsync PrevSubmitted (chính xác) → loại-trừ tránh double-count.
// PendingSelected* = peers Phase ∉ {DangSoanThao, DaDuyet, TuChoi} AND ≥1 Supplier IsWinner
// (GỒM ChoDuyet-có-winner = gap chính đề bài) → Count + SUM(ThanhTien IsSelected).
// Mirror shape ComputeAsync :50-60 (join Suppliers→Quotes).
// PriorPes = peers CHƯA terminal (Phase ∉ {DaDuyet, TuChoi}) → list render, order CreatedAt.
public static async Task<PePendingAccumulation> ComputePendingAsync(
IApplicationDbContext db,
Guid projectId,
Guid workItemId,
Guid peId,
DateTime peCreatedAt,
CancellationToken ct)
{
var peers = db.PurchaseEvaluations.AsNoTracking()
.Where(p => p.ProjectId == projectId && p.WorkItemId == workItemId
&& p.Id != peId && p.CreatedAt < peCreatedAt);
var pendingSubmitted = await peers
.Where(p => p.Phase != PurchaseEvaluationPhase.DangSoanThao
&& p.Phase != PurchaseEvaluationPhase.DaDuyet
&& p.Phase != PurchaseEvaluationPhase.ChoDuyet
&& p.Phase != PurchaseEvaluationPhase.TuChoi)
.Select(p => p.BudgetPeriodAmount)
.ToListAsync(ct);
var pendingSubmittedCount = pendingSubmitted.Count;
var pendingSubmittedTotal = pendingSubmitted.Sum(v => v ?? 0m);
var pendingSelectedPeers = peers.Where(p => p.Phase != PurchaseEvaluationPhase.DangSoanThao
&& p.Phase != PurchaseEvaluationPhase.DaDuyet
&& p.Phase != PurchaseEvaluationPhase.TuChoi
&& p.Suppliers.Any(s => s.IsWinner));
var pendingSelectedCount = await pendingSelectedPeers.CountAsync(ct);
var pendingSelectedTotal = await (
from p in pendingSelectedPeers
join s in db.PurchaseEvaluationSuppliers.AsNoTracking()
on p.Id equals s.PurchaseEvaluationId
join q in db.PurchaseEvaluationQuotes.AsNoTracking()
on s.Id equals q.PurchaseEvaluationSupplierId
where q.IsSelected
select (decimal?)q.ThanhTien).SumAsync(ct) ?? 0m;
var priorPes = await peers
.Where(p => p.Phase != PurchaseEvaluationPhase.DaDuyet
&& p.Phase != PurchaseEvaluationPhase.TuChoi)
.OrderBy(p => p.CreatedAt)
.Select(p => new PePendingPriorPeDto(p.Id, p.MaPhieu, (int)p.Phase))
.ToListAsync(ct);
return new PePendingAccumulation(
pendingSubmittedTotal, pendingSubmittedCount,
pendingSelectedTotal, pendingSelectedCount,
priorPes);
}
}

View File

@ -952,7 +952,11 @@ public class GetPurchaseEvaluationQueryHandler(
e.ApprovedBudgetPrevSelectedTotal ?? 0m, e.ApprovedBudgetPrevSelectedCount ?? 0,
currentProposalTotal,
e.ApprovedBudgetProInitialAmount, e.ApprovedBudgetProAdjustmentAmount,
BudgetFrozen: true);
BudgetFrozen: true,
// [S134] phiếu DaDuyet bất biến — KHÔNG lũy kế tạm-tính (giữ defaults 0/null).
PendingSubmittedTotal: 0m, PendingSubmittedCount: 0,
PendingSelectedTotal: 0m, PendingSelectedCount: 0,
PendingPriorPes: null);
}
else
{
@ -962,6 +966,12 @@ public class GetPurchaseEvaluationQueryHandler(
var acc = await PeBudgetAccumulator.ComputeAsync(
db, e.ProjectId, wiKey, e.Id, e.CreatedAt, curSupplierRowIds, ct);
// [S134] Lũy kế TẠM TÍNH — phiếu TRƯỚC cùng cặp đã trình/chọn NHƯNG CHƯA duyệt xong
// (row 5/6 "tạm tính" + list phiếu cần lưu ý). CHỈ nhánh live (phiếu chưa chốt); nhánh
// frozen giữ defaults 0/null. KHÔNG đổi số CHÍNH XÁC của acc (ComputeAsync bất động).
var pending = await PeBudgetAccumulator.ComputePendingAsync(
db, e.ProjectId, wiKey, e.Id, e.CreatedAt, ct);
// [S76] Full mỗi cột = Initial + Adjustment (cột đó). Authoritative full cho Block B =
// CCM nếu đã nhập, else PRO (FullIsEstimate=true → FE badge "ngân sách PRO"). PRO full =
// ProInitial + ProAdjust (migrate từ ProEstimate cũ qua Mig 56). Cả 2 trống → full 0.
@ -981,7 +991,13 @@ public class GetPurchaseEvaluationQueryHandler(
acc.PrevSelectedTotal, acc.PrevSelectedCount,
acc.CurrentProposalTotal,
pairRec?.ProInitialAmount, pairRec?.ProAdjustmentAmount,
BudgetFrozen: false);
BudgetFrozen: false,
// [S134] lũy kế tạm-tính chỉ nhánh live.
PendingSubmittedTotal: pending.PendingSubmittedTotal,
PendingSubmittedCount: pending.PendingSubmittedCount,
PendingSelectedTotal: pending.PendingSelectedTotal,
PendingSelectedCount: pending.PendingSelectedCount,
PendingPriorPes: pending.PriorPes);
}
}