[CLAUDE] PurchaseEvaluation: CCM 3-fix — multi-winner sum + budget=0 hợp lệ + cấp KẾT THÚC quy trình (Mig 58, anh Kiệt FDC)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m10s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m10s
CCM (Lưu Trần / anh Kiệt FDC) — làm 3/4 mục (mục 3 "CCM sửa ngân sách" để riêng): 1. Multi-winner: chọn NHIỀU NTP/NCC/ĐVDV cùng trúng → winnerQuoteTotal = SUM ThanhTien MỌI đơn vị isWinner (vd 3 ông 37.8+9+10 = 56.8tr). Cột mới PurchaseEvaluationSupplier.IsWinner (Mig 58, backfill từ SelectedSupplierId). SelectWinner → multi-toggle SupplierIds[]. FE: bảng so sánh tick nhiều ô + dòng "Tổng chi phí đã chọn"; mọi guard/checklist/computeGiaChaoThau/tạo-HĐ đổi sang isWinner (selectedSupplierId = null khi liên-danh ≥2). 2. Ngân sách = 0 hợp lệ: nới submit-guard + 3 validator (chỉ chặn khi NULL, số 0 cố ý đi qua; fallback ProInitial chỉ khi null). Mirror BE + FE. 3. Cấp KẾT THÚC quy trình: ApprovalWorkflowLevel.AllowApproverFinalize (Mig 58) — admin bật per-cấp trong Workflow Designer; duyệt tới cấp đó TỰ DaDuyet, bỏ qua CEO (AUTO theo cấu hình quy trình, không ô-tích runtime — anh Kiệt chốt). FE: banner cảnh báo + bắt chọn giá chốt. Mig 58 AddPeMultiWinnerAndLevelFinalize (2 AddColumn bool + backfill IsWinner, additive/reversible). Test 377→395 (+PeMultiWinnerTests +PeApproverFinalizeTests +budget=0; auto-model gọn bớt 2 test bypass/opt-out). FE 2 app SHA-mirror. Build sạch BE + 2 FE local. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -167,7 +167,7 @@ public class PurchaseEvaluationsController(IMediator mediator) : ControllerBase
|
||||
[HttpPost("{id:guid}/select-winner")]
|
||||
public async Task<IActionResult> SelectWinner(Guid id, [FromBody] SelectWinnerBody body, CancellationToken ct)
|
||||
{
|
||||
await mediator.Send(new SelectPurchaseEvaluationWinnerCommand(id, body.SupplierId), ct);
|
||||
await mediator.Send(new SelectPurchaseEvaluationWinnerCommand(id, body.SupplierIds ?? Array.Empty<Guid>()), ct);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
@ -345,7 +345,7 @@ public record AddSupplierBody(
|
||||
string? DisplayName, string? ContactName, string? ContactEmail, string? ContactPhone,
|
||||
string? PaymentTermText, string? Note);
|
||||
|
||||
public record SelectWinnerBody(Guid? SupplierId); // null = bỏ chọn NCC (cho phép xóa + điền lại — anh Kiệt FDC C5)
|
||||
public record SelectWinnerBody(IReadOnlyList<Guid>? SupplierIds); // [Mig 58] multi-winner: tập đơn vị NCC/TP thắng; [] hoặc null = bỏ chọn (anh Kiệt FDC C5 + CCM multi)
|
||||
|
||||
public record DetailBody(
|
||||
string GroupCode, string GroupName, string? ItemCode, string NoiDung, string? DonViTinh,
|
||||
|
||||
@ -38,6 +38,7 @@ public record AwLevelDto(
|
||||
bool AllowApproverEditDetails,
|
||||
bool AllowApproverEditBudget,
|
||||
bool AllowApproverSkipToFinal,
|
||||
bool AllowApproverFinalize, // [Mig 58] slot này Approve = KẾT THÚC (không trình CEO)
|
||||
// [S76] Hiển thị-only: approver này được nhập/điều chỉnh ngân sách cột nào (suy
|
||||
// từ ROLE — KHÔNG đổi quyền). CanEditProBudget = Admin|Procurement (cột PRO);
|
||||
// CanEditCcmBudget = Admin|CostControl (cột CCM). Badge "✎ NS PRO/CCM" trong Designer.
|
||||
@ -184,7 +185,7 @@ public class GetAwAdminOverviewQueryHandler(
|
||||
return new AwLevelDto(l.Id, l.Order, l.Name, l.ApproverUserId, info.FullName, info.Email,
|
||||
l.AllowReturnOneLevel, l.AllowReturnOneStep, l.AllowReturnToAssignee,
|
||||
l.AllowReturnToDrafter, l.AllowApproverEditDetails, l.AllowApproverEditBudget,
|
||||
l.AllowApproverSkipToFinal,
|
||||
l.AllowApproverSkipToFinal, l.AllowApproverFinalize,
|
||||
proBudgetEditors.Contains(l.ApproverUserId), ccmBudgetEditors.Contains(l.ApproverUserId));
|
||||
}).ToList()
|
||||
)).ToList());
|
||||
@ -225,7 +226,8 @@ public record CreateAwLevelInput(
|
||||
bool AllowReturnToDrafter = true,
|
||||
bool AllowApproverEditDetails = false,
|
||||
bool AllowApproverEditBudget = false,
|
||||
bool AllowApproverSkipToFinal = false);
|
||||
bool AllowApproverSkipToFinal = false,
|
||||
bool AllowApproverFinalize = false);
|
||||
|
||||
public record CreateAwStepInput(
|
||||
int Order,
|
||||
@ -363,6 +365,7 @@ public class CreateAwDefinitionCommandHandler(IApplicationDbContext db)
|
||||
AllowApproverEditDetails = l.AllowApproverEditDetails,
|
||||
AllowApproverEditBudget = l.AllowApproverEditBudget,
|
||||
AllowApproverSkipToFinal = l.AllowApproverSkipToFinal,
|
||||
AllowApproverFinalize = l.AllowApproverFinalize,
|
||||
}).ToList(),
|
||||
})
|
||||
.ToList(),
|
||||
|
||||
@ -51,7 +51,8 @@ public record PurchaseEvaluationSupplierDto(
|
||||
string? ContactPhone,
|
||||
string? PaymentTermText,
|
||||
string? Note,
|
||||
int Order);
|
||||
int Order,
|
||||
bool IsWinner); // [Mig 58] đơn vị này có thuộc tập winner không (multi)
|
||||
|
||||
public record PurchaseEvaluationQuoteDto(
|
||||
Guid Id,
|
||||
@ -115,7 +116,8 @@ public record ApprovalWorkflowOptionsDto(
|
||||
bool AllowReturnToDrafter,
|
||||
bool AllowApproverEditDetails,
|
||||
bool AllowApproverEditBudget,
|
||||
bool AllowApproverSkipToFinal);
|
||||
bool AllowApproverSkipToFinal,
|
||||
bool AllowApproverFinalize); // [Mig 58] slot này Approve = KẾT THÚC (không trình CEO)
|
||||
|
||||
public record PurchaseEvaluationWorkflowSummaryDto(
|
||||
string PolicyName,
|
||||
|
||||
@ -385,11 +385,17 @@ public class DeletePurchaseEvaluationQuoteCommandHandler(
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Select winner (NCC được chọn tổng thể) ==========
|
||||
// ========== Select winner (NCC/TP được chọn tổng thể — MULTI) ==========
|
||||
|
||||
// SupplierId nullable [anh Kiệt FDC C5]: null = BỎ CHỌN (xóa đơn vị đã chọn, điền
|
||||
// lại) — un-select winner về none; có giá trị = chọn/đổi NCC (logic cũ giữ 100%).
|
||||
public record SelectPurchaseEvaluationWinnerCommand(Guid PurchaseEvaluationId, Guid? SupplierId) : IRequest;
|
||||
// [Mig 58 2026-06-24 — CCM/anh Kiệt FDC] Multi-winner: chọn NHIỀU đơn vị NCC/TP cùng
|
||||
// trúng (whole-supplier multi-select). SupplierIds = tập đơn vị thắng:
|
||||
// - List rỗng [] = BỎ CHỌN tất cả (un-select về none) — thay null cũ (anh Kiệt C5).
|
||||
// - 1 phần tử = 1 winner (SelectedSupplierId sync = đơn vị đó — backward compat).
|
||||
// - ≥2 phần tử = joint winner (SelectedSupplierId = null vì không còn "1 winner").
|
||||
// IsWinner trên supplier-row là nguồn-sự-thật mới; winnerQuoteTotal = SUM ThanhTien
|
||||
// MỌI winner-row. Mỗi id phải nằm trong danh sách NCC tham gia của phiếu.
|
||||
public record SelectPurchaseEvaluationWinnerCommand(
|
||||
Guid PurchaseEvaluationId, IReadOnlyList<Guid> SupplierIds) : IRequest;
|
||||
|
||||
public class SelectPurchaseEvaluationWinnerCommandHandler(
|
||||
IApplicationDbContext db,
|
||||
@ -400,28 +406,30 @@ public class SelectPurchaseEvaluationWinnerCommandHandler(
|
||||
var entity = await db.PurchaseEvaluations.FirstOrDefaultAsync(x => x.Id == request.PurchaseEvaluationId, ct)
|
||||
?? throw new NotFoundException("PurchaseEvaluation", request.PurchaseEvaluationId);
|
||||
|
||||
if (request.SupplierId is Guid supplierId)
|
||||
{
|
||||
// ── Chọn / đổi NCC trúng thầu (logic cũ giữ nguyên 100%) ──
|
||||
_ = await db.Suppliers.FirstOrDefaultAsync(s => s.Id == supplierId, ct)
|
||||
?? throw new NotFoundException("Supplier", supplierId);
|
||||
// Load mọi supplier-row của phiếu (tracking — để set IsWinner).
|
||||
var rows = await db.PurchaseEvaluationSuppliers
|
||||
.Where(s => s.PurchaseEvaluationId == request.PurchaseEvaluationId)
|
||||
.ToListAsync(ct);
|
||||
|
||||
// Verify supplier nằm trong danh sách phiếu
|
||||
var hasSupplier = await db.PurchaseEvaluationSuppliers
|
||||
.AnyAsync(s => s.PurchaseEvaluationId == request.PurchaseEvaluationId && s.SupplierId == supplierId, ct);
|
||||
if (!hasSupplier) throw new ConflictException("NCC chưa được thêm vào phiếu đánh giá.");
|
||||
// Distinct các id được chọn (FE có thể gửi trùng).
|
||||
var selectedIds = request.SupplierIds?.Distinct().ToHashSet() ?? new HashSet<Guid>();
|
||||
|
||||
entity.SelectedSupplierId = supplierId;
|
||||
}
|
||||
else
|
||||
// Mỗi id chọn phải là 1 đơn vị tham gia phiếu (chặn forge/stale).
|
||||
var participatingSupplierIds = rows.Select(r => r.SupplierId).ToHashSet();
|
||||
foreach (var sid in selectedIds)
|
||||
{
|
||||
// ── Bỏ chọn (clear về none) — KHÔNG validate participating list ──
|
||||
// Row 4 "Giá trị kỳ này" + winnerQuoteTotal tính-khi-đọc theo
|
||||
// SelectedSupplierId (PurchaseEvaluationFeatures GetDetail) → tự về 0.
|
||||
// ApprovedPriceAmount/Source là cột chốt giai-đoạn DaDuyet, KHÔNG đụng ở đây.
|
||||
entity.SelectedSupplierId = null;
|
||||
if (!participatingSupplierIds.Contains(sid))
|
||||
throw new ConflictException("NCC chưa được thêm vào phiếu đánh giá.");
|
||||
}
|
||||
|
||||
// Set IsWinner per-row: true nếu nằm trong tập chọn, false phần còn lại.
|
||||
foreach (var r in rows)
|
||||
r.IsWinner = selectedIds.Contains(r.SupplierId);
|
||||
|
||||
// Sync SelectedSupplierId: đúng 1 winner → đơn vị đó (backward compat luồng
|
||||
// tạo HĐ + DTO cũ); ≥2 hoặc 0 → null.
|
||||
entity.SelectedSupplierId = selectedIds.Count == 1 ? selectedIds.First() : null;
|
||||
|
||||
db.PurchaseEvaluationChangelogs.Add(new PurchaseEvaluationChangelog
|
||||
{
|
||||
PurchaseEvaluationId = entity.Id,
|
||||
@ -430,7 +438,9 @@ public class SelectPurchaseEvaluationWinnerCommandHandler(
|
||||
PhaseAtChange = entity.Phase,
|
||||
UserId = currentUser.UserId,
|
||||
UserName = currentUser.FullName ?? currentUser.Email,
|
||||
Summary = request.SupplierId is null ? "Bỏ chọn NCC trúng thầu" : "Chọn NCC trúng thầu",
|
||||
Summary = selectedIds.Count == 0
|
||||
? "Bỏ chọn đơn vị NCC/TP trúng thầu"
|
||||
: $"Chọn {selectedIds.Count} đơn vị NCC/TP trúng thầu",
|
||||
});
|
||||
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
@ -52,7 +52,7 @@ public class CreatePurchaseEvaluationCommandValidator : AbstractValidator<Create
|
||||
// [HoSoLink] MaxLength MATCH EF config HasMaxLength(1000) (S35 lesson).
|
||||
RuleFor(x => x.HoSoLink).MaximumLength(1000);
|
||||
// [S61] >0 khi có nhập — KHÔNG bắt buộc lúc tạo, submit guard mới chặn.
|
||||
RuleFor(x => x.BudgetPeriodAmount).GreaterThan(0)
|
||||
RuleFor(x => x.BudgetPeriodAmount).GreaterThanOrEqualTo(0) // [Mig 58 item 2] 0 hợp lệ
|
||||
.When(x => x.BudgetPeriodAmount.HasValue)
|
||||
.WithMessage("Ngân sách kỳ này phải lớn hơn 0.");
|
||||
}
|
||||
@ -230,7 +230,7 @@ public class UpdatePurchaseEvaluationDraftCommandValidator : AbstractValidator<U
|
||||
RuleFor(x => x.MoTa).MaximumLength(2000);
|
||||
// [HoSoLink] MaxLength MATCH EF config HasMaxLength(1000) (S35 lesson).
|
||||
RuleFor(x => x.HoSoLink).MaximumLength(1000);
|
||||
RuleFor(x => x.BudgetPeriodAmount).GreaterThan(0)
|
||||
RuleFor(x => x.BudgetPeriodAmount).GreaterThanOrEqualTo(0) // [Mig 58 item 2] 0 hợp lệ
|
||||
.When(x => x.BudgetPeriodAmount.HasValue)
|
||||
.WithMessage("Ngân sách kỳ này phải lớn hơn 0.");
|
||||
}
|
||||
@ -336,12 +336,13 @@ public class AdjustPurchaseEvaluationBudgetCommandValidator : AbstractValidator<
|
||||
{
|
||||
public AdjustPurchaseEvaluationBudgetCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.BudgetPeriodAmount).GreaterThan(0).When(x => x.BudgetPeriodAmount.HasValue);
|
||||
RuleFor(x => x.BudgetPeriodAmount).GreaterThanOrEqualTo(0).When(x => x.BudgetPeriodAmount.HasValue); // [Mig 58 item 2] 0 hợp lệ
|
||||
// [S62] "Giá trị thực hiện dự kiến còn lại" (row 8) CHO PHÉP ÂM. Khi giá trị NCC
|
||||
// vượt ngân sách → số dư còn lại tự ra âm; nghiệp vụ (anh Kiệt FDC 2026-06-13)
|
||||
// yêu cầu VẪN lưu được, chỉ cảnh báo (FE banner "Vượt ngân sách"). Gỡ ràng buộc
|
||||
// GreaterThanOrEqualTo(0) cũ — mirror tiền lệ LeaveBalance (cho phép số dư âm).
|
||||
// BudgetPeriodAmount vẫn phải > 0; submit guard "đã nhập NS kỳ này" không đổi.
|
||||
// [Mig 58 item 2 CCM] BudgetPeriodAmount cho phép = 0 (SSG ngân sách 0 hợp lệ); submit
|
||||
// guard chỉ chặn khi NULL (chưa nhập gì), KHÔNG chặn 0 cố ý.
|
||||
}
|
||||
}
|
||||
|
||||
@ -958,7 +959,8 @@ public class GetPurchaseEvaluationQueryHandler(
|
||||
curLevel.AllowReturnToDrafter,
|
||||
curLevel.AllowApproverEditDetails,
|
||||
curLevel.AllowApproverEditBudget,
|
||||
curLevel.AllowApproverSkipToFinal);
|
||||
curLevel.AllowApproverSkipToFinal,
|
||||
curLevel.AllowApproverFinalize);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1083,7 +1085,7 @@ public class GetPurchaseEvaluationQueryHandler(
|
||||
// Tính từ data đã load (Suppliers + Details.Quotes) — KHÔNG query thêm. 0 khi
|
||||
// chưa chọn NCC. FE so với CeoApprovalThreshold → "CCM duyệt-final" / "cần CEO".
|
||||
var winnerSupplierRowIds = e.Suppliers
|
||||
.Where(s => s.SupplierId == e.SelectedSupplierId)
|
||||
.Where(s => s.IsWinner) // [Mig 58] multi-winner: tổng MỌI đơn vị IsWinner
|
||||
.Select(s => s.Id)
|
||||
.ToHashSet();
|
||||
var winnerQuoteTotal = e.Details
|
||||
@ -1119,7 +1121,7 @@ public class GetPurchaseEvaluationQueryHandler(
|
||||
s.Id, s.SupplierId,
|
||||
suppliers.TryGetValue(s.SupplierId, out var sn) ? sn : "",
|
||||
s.DisplayName, s.ContactName, s.ContactEmail, s.ContactPhone,
|
||||
s.PaymentTermText, s.Note, s.Order))
|
||||
s.PaymentTermText, s.Note, s.Order, s.IsWinner))
|
||||
.ToList(),
|
||||
e.Details
|
||||
.OrderBy(d => d.Order)
|
||||
|
||||
@ -130,5 +130,16 @@ public class ApprovalWorkflowLevel : BaseEntity
|
||||
/// reinforced 3× cumulative. NO BACKFILL — 4 prod user lose value cũ per bro Option A.
|
||||
public bool AllowApproverSkipToFinal { get; set; }
|
||||
|
||||
/// F5 (Mig 58 2026-06-24 — CCM/anh Kiệt FDC) — Cho phép NV slot này, khi duyệt
|
||||
/// ở cấp của mình, KẾT THÚC quy trình luôn (Phase=DaDuyet) KHÔNG cần trình CEO/
|
||||
/// các Bước-Cấp sau. Admin opt-in per slot trong Workflow Designer ("Duyệt là
|
||||
/// kết thúc (không trình CEO)"). Default false. KHÁC AllowApproverSkipToFinal
|
||||
/// (skip = advance pointer tới Cấp cuối, CEO vẫn ký thật): F5 = chính NV này
|
||||
/// approve = terminal NGAY. Service ApproveV2Async TỰ finalize khi
|
||||
/// matchingLevel.AllowApproverFinalize=true (AUTO theo cấu hình quy trình, anh Kiệt
|
||||
/// chốt 2026-06-24 — KHÔNG ô-tích runtime; admin per-level designation = cổng duy
|
||||
/// nhất, KHÔNG threshold/role check). Mirror F2/F3/F4 admin opt-in per-slot pattern.
|
||||
public bool AllowApproverFinalize { get; set; }
|
||||
|
||||
public ApprovalWorkflowStep? Step { get; set; }
|
||||
}
|
||||
|
||||
@ -18,5 +18,14 @@ public class PurchaseEvaluationSupplier : BaseEntity
|
||||
public string? Note { get; set; } // Chip trạng thái so sánh
|
||||
public int Order { get; set; } // Thứ tự cột trong bảng so sánh
|
||||
|
||||
// [Mig 58 2026-06-24 — CCM/anh Kiệt FDC] Multi-winner: chọn NHIỀU đơn vị NCC/TP
|
||||
// cùng trúng (whole-supplier multi-select). IsWinner=true → báo giá của đơn vị
|
||||
// này tính vào "Giá trị kỳ này" (winnerQuoteTotal = SUM ThanhTien của MỌI
|
||||
// winner). Thay tiêu chí cũ "SupplierId == SelectedSupplierId" (1 winner) bằng
|
||||
// IsWinner trên supplier-row = nguồn-sự-thật mới. SelectedSupplierId vẫn sync:
|
||||
// = winner duy nhất khi đúng 1, null khi ≥2 (multi). Backfill Mig 58:
|
||||
// IsWinner=1 cho row khớp SelectedSupplierId cũ (single-winner phiếu UAT giữ).
|
||||
public bool IsWinner { get; set; }
|
||||
|
||||
public PurchaseEvaluation? PurchaseEvaluation { get; set; }
|
||||
}
|
||||
|
||||
@ -93,5 +93,10 @@ public class ApprovalWorkflowLevelConfiguration : IEntityTypeConfiguration<Appro
|
||||
// slot này skip thẳng Cấp cuối lúc đang duyệt ChoDuyet. Default false
|
||||
// (admin opt-in). Semantic + storage cũ ở Users table per-Drafter đã drop.
|
||||
e.Property(x => x.AllowApproverSkipToFinal).HasDefaultValue(false);
|
||||
|
||||
// Mig 58 (2026-06-24 — CCM/anh Kiệt FDC) — F5 per-NV: cho phép NV slot này
|
||||
// duyệt = KẾT THÚC quy trình (Phase=DaDuyet) miễn trình CEO. Default false
|
||||
// (admin opt-in). Khác F2 (skip = advance tới Cấp cuối): F5 = terminal ngay.
|
||||
e.Property(x => x.AllowApproverFinalize).HasDefaultValue(false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,6 +82,9 @@ public class PurchaseEvaluationSupplierConfiguration : IEntityTypeConfiguration<
|
||||
b.Property(x => x.PaymentTermText).HasMaxLength(200);
|
||||
b.Property(x => x.Note).HasMaxLength(500);
|
||||
|
||||
// [Mig 58] Multi-winner flag — default false (mirror per-slot bool pattern).
|
||||
b.Property(x => x.IsWinner).HasDefaultValue(false);
|
||||
|
||||
b.HasIndex(x => new { x.PurchaseEvaluationId, x.SupplierId }).IsUnique();
|
||||
b.HasIndex(x => x.SupplierId);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,50 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddPeMultiWinnerAndLevelFinalize : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsWinner",
|
||||
table: "PurchaseEvaluationSuppliers",
|
||||
type: "bit",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "AllowApproverFinalize",
|
||||
table: "ApprovalWorkflowLevels",
|
||||
type: "bit",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
// [Mig 58] Backfill IsWinner từ winner đơn lẻ cũ (SelectedSupplierId) —
|
||||
// phiếu UAT đang chạy giữ winner đã chọn dưới mô hình multi-winner mới.
|
||||
// Chạy SAU AddColumn (cột đã tồn tại). Idempotent: chỉ set 1 (default 0
|
||||
// không đụng) → re-run no-op. KHÔNG un-backfill ở Down (DropColumn xoá hẳn).
|
||||
migrationBuilder.Sql(@"
|
||||
UPDATE pes SET IsWinner = 1
|
||||
FROM PurchaseEvaluationSuppliers pes
|
||||
INNER JOIN PurchaseEvaluations pe ON pe.Id = pes.PurchaseEvaluationId
|
||||
WHERE pe.SelectedSupplierId IS NOT NULL AND pes.SupplierId = pe.SelectedSupplierId;");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsWinner",
|
||||
table: "PurchaseEvaluationSuppliers");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AllowApproverFinalize",
|
||||
table: "ApprovalWorkflowLevels");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -202,6 +202,11 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<bool>("AllowApproverFinalize")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<bool>("AllowApproverSkipToFinal")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
@ -5275,6 +5280,11 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<bool>("IsWinner")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<string>("Note")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
@ -172,28 +172,25 @@ 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>();
|
||||
if (evaluation.SelectedSupplierId is not Guid winnerId)
|
||||
// [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)
|
||||
{
|
||||
missing.Add("chưa chọn Đơn vị NCC/TP");
|
||||
}
|
||||
else
|
||||
{
|
||||
var winnerRowIds = await db.PurchaseEvaluationSuppliers.AsNoTracking()
|
||||
.Where(s => s.PurchaseEvaluationId == evaluation.Id && s.SupplierId == winnerId)
|
||||
.Select(s => s.Id)
|
||||
.ToListAsync(ct);
|
||||
if (winnerRowIds.Count == 0)
|
||||
{
|
||||
missing.Add("Đơn vị được chọn không còn trong danh sách NCC tham gia");
|
||||
}
|
||||
else
|
||||
{
|
||||
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");
|
||||
}
|
||||
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");
|
||||
}
|
||||
// [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
|
||||
@ -202,15 +199,18 @@ public class PurchaseEvaluationWorkflowService(
|
||||
// (PeWorkItemBudget.ProInitialAmount per Dự-án×Hạng-mục). Effective = BudgetPeriodAmount
|
||||
// ?? ProInitialAmount → phiếu chưa nhập tay row3 nhưng đã có ban-hành PRO vẫn gửi-duyệt được
|
||||
// (FE missingForApproval mirror cùng fallback — đổi đồng bộ 2 tầng).
|
||||
// [Mig 58 item 2 CCM] Ngân sách = 0 HỢP LỆ (SSG ngân sách 0 vẫn gửi duyệt được). Chỉ
|
||||
// fallback ProInitial khi drafter CHƯA nhập (null) — KHÔNG override số 0 cố ý thành
|
||||
// ProInitial rồi vô tình null-block. Block cuối chỉ fire khi null; 0 (cố ý) đi qua.
|
||||
decimal? effectiveBudget = evaluation.BudgetPeriodAmount;
|
||||
if ((effectiveBudget is null || effectiveBudget <= 0) && evaluation.WorkItemId is Guid wiId)
|
||||
if (effectiveBudget is null && evaluation.WorkItemId is Guid wiId)
|
||||
{
|
||||
effectiveBudget = await db.PeWorkItemBudgets.AsNoTracking()
|
||||
.Where(b => b.ProjectId == evaluation.ProjectId && b.WorkItemId == wiId)
|
||||
.Select(b => b.ProInitialAmount)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
}
|
||||
if (effectiveBudget is null || effectiveBudget <= 0)
|
||||
if (effectiveBudget is null)
|
||||
{
|
||||
missing.Add("chưa nhập Ngân sách kỳ này");
|
||||
}
|
||||
@ -851,6 +851,31 @@ public class PurchaseEvaluationWorkflowService(
|
||||
// (winnerQuoteTotal = SUM báo giá NCC được chọn) < ngưỡng. Tích mà KHÔNG đủ điều
|
||||
// kiện → Conflict/Forbidden. KHÔNG tích → bỏ block, advance bình thường lên CEO.
|
||||
// Opinion + approval row đã ghi ở trên → finalize giữ đủ vết. ① giá chốt: ApplyApprovedPriceOnFinalize.
|
||||
// [Mig 58 2026-06-24 — CCM/anh Kiệt FDC item 4] Cấp được cấu hình KẾT THÚC trong Quy
|
||||
// trình duyệt (admin bật AllowApproverFinalize cho slot, vd Cấp anh Chương). Duyệt tới
|
||||
// cấp này = TỰ kết thúc (DaDuyet), bỏ qua MỌI Bước/Cấp còn lại (kể cả CEO) — quy-trình-
|
||||
// driven, KHÔNG cần ô-tích runtime (anh Kiệt chốt 2026-06-24 "cho bật trong quy trình
|
||||
// hay hơn"). KHÔNG threshold/role-gate (admin chỉ-định slot = đủ thẩm quyền). ① giá chốt:
|
||||
// người duyệt phải chọn (Conflict nếu thiếu, isSystem miễn). Đặt SAU skipToFinal, TRƯỚC
|
||||
// ccm-delegation + advance.
|
||||
if (matchingLevel.AllowApproverFinalize)
|
||||
{
|
||||
ApplyApprovedPriceOnFinalize(evaluation, isSystem, approvedPriceAmount, approvedPriceSource);
|
||||
evaluation.Phase = PurchaseEvaluationPhase.DaDuyet;
|
||||
evaluation.CurrentWorkflowStepIndex = null;
|
||||
evaluation.CurrentApprovalLevelOrder = null;
|
||||
evaluation.SlaDeadline = null;
|
||||
await LogTransitionAsync(
|
||||
evaluation,
|
||||
PurchaseEvaluationPhase.ChoDuyet,
|
||||
PurchaseEvaluationPhase.DaDuyet,
|
||||
actorUserId,
|
||||
ApprovalDecision.Approve,
|
||||
$"[Duyệt KẾT THÚC tại Bước {currentIdx + 1} Cấp {currentLevelOrder} — cấu hình quy trình, không trình CEO] {comment ?? ""}".Trim(),
|
||||
ct);
|
||||
return;
|
||||
}
|
||||
|
||||
if (finalizeByCcmDelegation)
|
||||
{
|
||||
if (aw.CeoApprovalThreshold is not decimal ceoThreshold)
|
||||
@ -861,8 +886,7 @@ public class PurchaseEvaluationWorkflowService(
|
||||
"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.SupplierId == evaluation.SelectedSupplierId)
|
||||
.Where(s => s.PurchaseEvaluationId == evaluation.Id && s.IsWinner) // [Mig 58] multi-winner
|
||||
.Select(s => s.Id)
|
||||
.ToListAsync(ct);
|
||||
var winnerQuoteTotal = winnerSupplierRowIds.Count == 0 ? 0m
|
||||
|
||||
Reference in New Issue
Block a user