[CLAUDE] PurchaseEvaluation: multi-NCC per hạng mục — chọn ≥2 NCC-TP trúng thầu/1 hạng mục (mỗi nhà 1 giá riêng)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m17s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m17s
Đổi winner-truth từ whole-supplier Supplier.IsWinner (Mig 58 liên-danh) sang per-hạng-mục
Quote.IsSelected (Mig 12 dormant hook → nay nguồn-sự-thật); Supplier.IsWinner = DERIVED (== Any IsSelected).
Anh Kiệt LOCKED D1=SUM(IsSelected) · D2=THAY-THẾ · D3=giá-chào-gốc · D4=1HĐ/NCC. 0 bảng/cột mới.
BE:
- SelectWinnerCommand + POST /select-winner: body {supplierIds} → {detailId, supplierIds} (per hạng mục).
- Re-key 5 financial-SUM → WHERE IsSelected: winnerQuoteTotal, submit-guard, CCM-threshold, giaTri, budget-B.
- giaTri bỏ shortcut isSingle→detailsSum (single đổi budget→bid theo D4).
- Derive IsWinner invariant ở MỌI writer: SelectWinner + UpsertQuote + seed + DeleteQuote/DeleteDetail (helper PeWinnerInvariant, fix Bước 4).
- GIỮ IsWinner-derived cho winner-names/existence + CreateContract winners-list.
- Migration BackfillPeQuoteIsSelectedAndWinnerInvariant (data-only 2-chiều IsWinner↔IsSelected, Down no-op).
FE 2-app (PeDetailTabs SHA-identical): HangMucCard per-cell winner grid Detail×NCC + toggle per-detail; NccSelectorRow hạ-cấp read-only summary; giữ derived display.
Test: 7 file update + 8 test mới (per-hạng-mục total, giaTri single-partial, threshold-crossing, derive-invariant, delete-path). 486 PASS.
Flow: fable-clone invest ensemble → anh Kiệt 4-decision → fable-clone review ensemble (6 adjustment) → hmw implement (disk-recover #53) → fable-clone verify (bắt MUST bug delete-path → fix test-before).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -189,25 +189,23 @@ public class PurchaseEvaluationWorkflowService(
|
||||
// missingForApproval (FE KHÔNG check Purpose enum → BE cũng không,
|
||||
// tránh mismatch 2 tầng).
|
||||
var missing = new List<string>();
|
||||
// [Mig 58 CCM item 1] Multi-winner: winner = các supplier-row IsWinner (≥1). Tổng giá
|
||||
// gói = SUM ThanhTien MỌI winner-row (vd 3 đơn vị 37.8+9+10=56.8tr). 0 winner →
|
||||
// "chưa chọn"; có winner nhưng tổng ≤ 0 → "chưa có giá chào thầu" (item 2 chỉ nới
|
||||
// NGÂN SÁCH, KHÔNG nới giá NCC — winner phải có giá).
|
||||
var winnerRowIds = await db.PurchaseEvaluationSuppliers.AsNoTracking()
|
||||
.Where(s => s.PurchaseEvaluationId == evaluation.Id && s.IsWinner)
|
||||
.Select(s => s.Id)
|
||||
.ToListAsync(ct);
|
||||
if (winnerRowIds.Count == 0)
|
||||
// [multi-NCC A3(i)[2] + D5] Winner = có ÍT NHẤT 1 báo giá ĐƯỢC CHỌN (Quote.IsSelected
|
||||
// = nguồn-sự-thật per Detail×Supplier). Tổng giá gói = SUM ThanhTien mọi quote IsSelected
|
||||
// toàn phiếu (D5 giữ >0). 0 selected → "chưa chọn"; có selected nhưng tổng ≤0 → "chưa có giá"
|
||||
// (item 2 chỉ nới NGÂN SÁCH, KHÔNG nới giá NCC — winner phải có giá).
|
||||
var selectedThanhTien = await (
|
||||
from q in db.PurchaseEvaluationQuotes.AsNoTracking()
|
||||
join d in db.PurchaseEvaluationDetails.AsNoTracking()
|
||||
on q.PurchaseEvaluationDetailId equals d.Id
|
||||
where d.PurchaseEvaluationId == evaluation.Id && q.IsSelected
|
||||
select q.ThanhTien).ToListAsync(ct);
|
||||
if (selectedThanhTien.Count == 0)
|
||||
{
|
||||
missing.Add("chưa chọn Đơn vị NCC/TP");
|
||||
}
|
||||
else
|
||||
else if (selectedThanhTien.Sum() <= 0)
|
||||
{
|
||||
var winnerQuoteTotal = await db.PurchaseEvaluationQuotes.AsNoTracking()
|
||||
.Where(q => winnerRowIds.Contains(q.PurchaseEvaluationSupplierId))
|
||||
.SumAsync(q => (decimal?)q.ThanhTien, ct) ?? 0m;
|
||||
if (winnerQuoteTotal <= 0)
|
||||
missing.Add("Đơn vị được chọn chưa có giá chào thầu");
|
||||
missing.Add("Đơn vị được chọn chưa có giá chào thầu");
|
||||
}
|
||||
// [S61 Mig 50] Schema ngân sách mới — điều kiện (3) = "Ngân sách - kỳ này"
|
||||
// (BudgetPeriodAmount, drafter nhập). FE PeDetailTabs missingForApproval
|
||||
@ -881,14 +879,14 @@ public class PurchaseEvaluationWorkflowService(
|
||||
throw new ForbiddenException(
|
||||
"Chỉ CCM (Kiểm soát Chi phí) được duyệt done miễn CEO.");
|
||||
|
||||
var winnerSupplierRowIds = await db.PurchaseEvaluationSuppliers.AsNoTracking()
|
||||
.Where(s => s.PurchaseEvaluationId == evaluation.Id && s.IsWinner) // [Mig 58] multi-winner
|
||||
.Select(s => s.Id)
|
||||
.ToListAsync(ct);
|
||||
var winnerQuoteTotal = winnerSupplierRowIds.Count == 0 ? 0m
|
||||
: await db.PurchaseEvaluationQuotes.AsNoTracking()
|
||||
.Where(q => winnerSupplierRowIds.Contains(q.PurchaseEvaluationSupplierId))
|
||||
.SumAsync(q => (decimal?)q.ThanhTien, ct) ?? 0m;
|
||||
// [multi-NCC A3(i)[3]] Giá gói = SUM ThanhTien báo giá ĐƯỢC CHỌN (Quote.IsSelected =
|
||||
// nguồn-sự-thật) toàn phiếu. CCM duyệt done miễn CEO chỉ khi giá gói < ngưỡng (strict).
|
||||
var winnerQuoteTotal = await (
|
||||
from q in db.PurchaseEvaluationQuotes.AsNoTracking()
|
||||
join d in db.PurchaseEvaluationDetails.AsNoTracking()
|
||||
on q.PurchaseEvaluationDetailId equals d.Id
|
||||
where d.PurchaseEvaluationId == evaluation.Id && q.IsSelected
|
||||
select (decimal?)q.ThanhTien).SumAsync(ct) ?? 0m;
|
||||
|
||||
if (winnerQuoteTotal >= ceoThreshold)
|
||||
throw new ConflictException(
|
||||
|
||||
Reference in New Issue
Block a user