[CLAUDE] Domain+Infra: PurchaseEvaluation module — 10 bảng + 2 workflow seed (migration 12)

Module Duyệt NCC (tiền-HĐ): phiếu trình duyệt so sánh giá N NCC × M hạng
mục trước khi ký HĐ. 2 quy trình: A DuyetNcc (3-step: Purchasing→CCM→CEO),
B DuyetNccPhuongAn (5-step: Purchasing→DựÁn→CCM→CEO PA→CEO NCC).

Domain (7 core + 3 workflow admin):
 - PurchaseEvaluation (header, AuditableEntity, pin WorkflowDefinitionId,
   SelectedSupplierId, PaymentTerms JSON, ContractId? FK kế thừa)
 - PurchaseEvaluationSupplier (N:M Phiếu × Supplier + contact + payment term)
 - PurchaseEvaluationDetail (hạng mục + ngân sách, group A.I/A.II/...)
 - PurchaseEvaluationQuote (báo giá per NCC per hạng mục + IsSelected)
 - PurchaseEvaluationApproval (workflow history, reuse ApprovalDecision)
 - PurchaseEvaluationChangelog (audit log, reuse ChangelogAction)
 - PurchaseEvaluationAttachment (file upload — báo giá NCC + spec...)
 - PurchaseEvaluationWorkflowDefinition/Step/StepApprover (config y như HĐ,
   tách table riêng vì Phase là PurchaseEvaluationPhase enum riêng)

Policy:
 - PurchaseEvaluationPolicy record + PurchaseEvaluationPolicies.NccOnly/
   NccWithPlan (default hardcoded) + FromDefinition(def) build runtime policy
   từ DB admin-authored. Default SLA: soạn 3d, step 1-2d, CEO 1d.

EF: 10 configurations với index phase+isDeleted, SupplierId, ProjectId,
SlaDeadline, WorkflowDefinitionId, ContractId. UX index (PeId, SupplierId)
+ (DetailId, SupplierId). HasQueryFilter soft delete cho header.

Migration 12 AddPurchaseEvaluations tạo 10 bảng. Idempotent seed:
 - SeedMenuTreeAsync +13 menu item (Pe_* root + 2 group + 6 action leaf
   + PeWorkflows root + 2 admin leaf)
 - SeedPurchaseEvaluationWorkflowsAsync seed QT-DN-A-v01 + QT-DN-B-v01
This commit is contained in:
pqhuy1987
2026-04-23 16:37:55 +07:00
parent a7ea6ad3d6
commit 2c6f0cabfb
19 changed files with 4884 additions and 1 deletions

View File

@ -6,6 +6,7 @@ using SolutionErp.Domain.Identity;
using SolutionErp.Domain.Master; using SolutionErp.Domain.Master;
using SolutionErp.Domain.Master.Catalogs; using SolutionErp.Domain.Master.Catalogs;
using SolutionErp.Domain.Notifications; using SolutionErp.Domain.Notifications;
using SolutionErp.Domain.PurchaseEvaluations;
namespace SolutionErp.Application.Common.Interfaces; namespace SolutionErp.Application.Common.Interfaces;
@ -44,5 +45,17 @@ public interface IApplicationDbContext
DbSet<NguyenTacNccDetail> NguyenTacNccDetails { get; } DbSet<NguyenTacNccDetail> NguyenTacNccDetails { get; }
DbSet<NguyenTacDvDetail> NguyenTacDvDetails { get; } DbSet<NguyenTacDvDetail> NguyenTacDvDetails { get; }
// Module Duyệt NCC (tiền-HĐ) — 7 core + 3 workflow config
DbSet<PurchaseEvaluation> PurchaseEvaluations { get; }
DbSet<PurchaseEvaluationSupplier> PurchaseEvaluationSuppliers { get; }
DbSet<PurchaseEvaluationDetail> PurchaseEvaluationDetails { get; }
DbSet<PurchaseEvaluationQuote> PurchaseEvaluationQuotes { get; }
DbSet<PurchaseEvaluationApproval> PurchaseEvaluationApprovals { get; }
DbSet<PurchaseEvaluationChangelog> PurchaseEvaluationChangelogs { get; }
DbSet<PurchaseEvaluationAttachment> PurchaseEvaluationAttachments { get; }
DbSet<PurchaseEvaluationWorkflowDefinition> PurchaseEvaluationWorkflowDefinitions { get; }
DbSet<PurchaseEvaluationWorkflowStep> PurchaseEvaluationWorkflowSteps { get; }
DbSet<PurchaseEvaluationWorkflowStepApprover> PurchaseEvaluationWorkflowStepApprovers { get; }
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default); Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
} }

View File

@ -42,13 +42,35 @@ public static class MenuKeys
// → mở /system/workflows/{typeCode} (filter theo type thay vì tab). // → mở /system/workflows/{typeCode} (filter theo type thay vì tab).
public static string WorkflowTypeLeaf(string typeCode) => $"Wf_{typeCode}"; public static string WorkflowTypeLeaf(string typeCode) => $"Wf_{typeCode}";
// ============================================================
// Module Duyệt NCC (tiền-HĐ) — Pe_* prefix. 2 EvaluationType:
// DuyetNcc (A, NccOnly 3-step) + DuyetNccPhuongAn (B, NccWithPlan 5-step).
// Mỗi type có 3 action leaf (Danh sách / Thao tác / Duyệt) + 1 group.
// Workflow admin cho PE ở /system/pe-workflows/:typeCode.
// ============================================================
public const string PurchaseEvaluations = "PurchaseEvaluations"; // root group
public const string PeWorkflows = "PeWorkflows"; // workflow admin root
public static readonly string[] PurchaseEvaluationTypeCodes =
["DuyetNcc", "DuyetNccPhuongAn"];
public static string PurchaseEvaluationGroup(string typeCode) => $"Pe_{typeCode}";
public static string PurchaseEvaluationList(string typeCode) => $"Pe_{typeCode}_List";
public static string PurchaseEvaluationCreate(string typeCode) => $"Pe_{typeCode}_Create";
public static string PurchaseEvaluationPending(string typeCode) => $"Pe_{typeCode}_Pending";
// Workflow admin leaf per PE type — dưới PeWorkflows, click leaf mở
// /system/pe-workflows/{typeCode}
public static string PeWorkflowTypeLeaf(string typeCode) => $"PeWf_{typeCode}";
public static readonly string[] All = public static readonly string[] All =
[ [
Dashboard, Dashboard,
Master, Suppliers, Projects, Departments, Master, Suppliers, Projects, Departments,
Catalogs, CatalogUnits, CatalogMaterials, CatalogServices, CatalogWorkItems, Catalogs, CatalogUnits, CatalogMaterials, CatalogServices, CatalogWorkItems,
Contracts, Forms, Reports, Contracts, Forms, Reports,
System, Users, Roles, Permissions, Workflows, PurchaseEvaluations,
System, Users, Roles, Permissions, Workflows, PeWorkflows,
]; ];
public static readonly string[] Actions = ["Read", "Create", "Update", "Delete"]; public static readonly string[] Actions = ["Read", "Create", "Update", "Delete"];

View File

@ -0,0 +1,36 @@
using SolutionErp.Domain.Common;
namespace SolutionErp.Domain.PurchaseEvaluations;
// Aggregate root cho phiếu Duyệt NCC — module tiền-HĐ.
// Sau khi phê duyệt xong (Phase=DaDuyet) user click "Tạo HĐ từ phiếu" →
// kế thừa Details/Quotes sang Contract + ContractDetails.
public class PurchaseEvaluation : AuditableEntity
{
public string? MaPhieu { get; set; } // Auto-gen khi create (format tính sau)
public PurchaseEvaluationType Type { get; set; }
public PurchaseEvaluationPhase Phase { get; set; } = PurchaseEvaluationPhase.DangSoanThao;
public string TenGoiThau { get; set; } = string.Empty; // "Cung cấp bê tông"
public Guid ProjectId { get; set; } // Dự án (FK Projects)
public Guid? DepartmentId { get; set; }
public Guid? DrafterUserId { get; set; } // QS/NV.PB soạn
public string? DiaDiem { get; set; } // Lô K, KCN Lộc An...
public string? MoTa { get; set; }
public Guid? WorkflowDefinitionId { get; set; } // Pinned at create — config y như HĐ
public DateTime? SlaDeadline { get; set; }
public bool SlaWarningSent { get; set; }
public Guid? SelectedSupplierId { get; set; } // NCC thắng — null tới khi DaDuyet
public string? PaymentTerms { get; set; } // JSON {tamUng, thanhToanTam, quyetToan, baoHanh, hanMucCongNo, danhGia}
public Guid? ContractId { get; set; } // FK Contracts — set khi user gen HĐ từ phiếu
public List<PurchaseEvaluationSupplier> Suppliers { get; set; } = new();
public List<PurchaseEvaluationDetail> Details { get; set; } = new();
public List<PurchaseEvaluationQuote> Quotes { get; set; } = new();
public List<PurchaseEvaluationApproval> Approvals { get; set; } = new();
public List<PurchaseEvaluationChangelog> Changelogs { get; set; } = new();
public List<PurchaseEvaluationAttachment> Attachments { get; set; } = new();
}

View File

@ -0,0 +1,18 @@
using SolutionErp.Domain.Common;
using SolutionErp.Domain.Contracts; // reuse ApprovalDecision enum
namespace SolutionErp.Domain.PurchaseEvaluations;
// Lịch sử phê duyệt — giống ContractApproval pattern.
public class PurchaseEvaluationApproval : BaseEntity
{
public Guid PurchaseEvaluationId { get; set; }
public PurchaseEvaluationPhase FromPhase { get; set; }
public PurchaseEvaluationPhase ToPhase { get; set; }
public Guid? ApproverUserId { get; set; } // null = system SLA auto
public ApprovalDecision Decision { get; set; }
public string? Comment { get; set; }
public DateTime ApprovedAt { get; set; }
public PurchaseEvaluation? PurchaseEvaluation { get; set; }
}

View File

@ -0,0 +1,25 @@
using SolutionErp.Domain.Common;
namespace SolutionErp.Domain.PurchaseEvaluations;
public enum PurchaseEvaluationAttachmentPurpose
{
QuoteDocument = 1, // File báo giá NCC gửi (PDF/xlsx)
RequirementSpec = 2, // Bản vẽ/yêu cầu kỹ thuật kèm theo
DecisionExport = 3, // Bản phiếu duyệt đã export
Other = 99,
}
public class PurchaseEvaluationAttachment : BaseEntity
{
public Guid PurchaseEvaluationId { get; set; }
public Guid? PurchaseEvaluationSupplierId { get; set; } // Null nếu không gắn với NCC cụ thể
public string FileName { get; set; } = string.Empty;
public string StoragePath { get; set; } = string.Empty;
public long FileSize { get; set; }
public string ContentType { get; set; } = string.Empty;
public PurchaseEvaluationAttachmentPurpose Purpose { get; set; }
public string? Note { get; set; }
public PurchaseEvaluation? PurchaseEvaluation { get; set; }
}

View File

@ -0,0 +1,33 @@
using SolutionErp.Domain.Common;
using SolutionErp.Domain.Contracts; // reuse ChangelogAction enum
namespace SolutionErp.Domain.PurchaseEvaluations;
// Audit log unified cho mọi thay đổi trên phiếu — Header / Supplier / Detail
// / Quote / Workflow / Attachment. Populate tương tự ContractChangelog qua
// IPurchaseEvaluationChangelogService.
public class PurchaseEvaluationChangelog : BaseEntity
{
public Guid PurchaseEvaluationId { get; set; }
public PurchaseEvaluation? PurchaseEvaluation { get; set; }
public PurchaseEvaluationEntityType EntityType { get; set; }
public Guid? EntityId { get; set; }
public ChangelogAction Action { get; set; }
public PurchaseEvaluationPhase? PhaseAtChange { get; set; }
public Guid? UserId { get; set; }
public string? UserName { get; set; }
public string? Summary { get; set; }
public string? FieldChangesJson { get; set; }
public string? ContextNote { get; set; }
}
public enum PurchaseEvaluationEntityType
{
Header = 1,
Supplier = 2,
Detail = 3,
Quote = 4,
Workflow = 5,
Attachment = 6,
}

View File

@ -0,0 +1,24 @@
using SolutionErp.Domain.Common;
namespace SolutionErp.Domain.PurchaseEvaluations;
// Hạng mục so sánh giá + ngân sách (Excel III + "CHI TIẾT SO SÁNH").
// Tách Quotes ra PurchaseEvaluationQuote để normalize N NCC × M hạng mục.
public class PurchaseEvaluationDetail : BaseEntity
{
public Guid PurchaseEvaluationId { get; set; }
public string GroupCode { get; set; } = string.Empty; // "A.I", "A.II", "A.III", "A.IV"
public string GroupName { get; set; } = string.Empty; // "Bê tông", "Phụ gia", "Bơm bê tông", "Vận chuyển"
public string? ItemCode { get; set; } // "DMCCC0001"
public string NoiDung { get; set; } = string.Empty; // "Concrete M100"
public string? DonViTinh { get; set; } // "m3"
public decimal KhoiLuongNganSach { get; set; }
public decimal KhoiLuongThiCong { get; set; }
public decimal DonGiaNganSach { get; set; } // Chưa VAT
public decimal ThanhTienNganSach { get; set; } // = KL × Đơn giá (hoặc nhập tay)
public int Order { get; set; }
public string? GhiChu { get; set; }
public PurchaseEvaluation? PurchaseEvaluation { get; set; }
public List<PurchaseEvaluationQuote> Quotes { get; set; } = new();
}

View File

@ -0,0 +1,20 @@
namespace SolutionErp.Domain.PurchaseEvaluations;
// State machine cho phiếu Duyệt NCC (tiền-HĐ). 2 workflow khác nhau cùng
// share state space này — A (DuyetNcc) dùng subset, B (DuyetNccPhuongAn)
// dùng full.
//
// A: DangSoanThao → ChoPurchasing → ChoCCM → ChoCEODuyetNCC → DaDuyet
// B: DangSoanThao → ChoPurchasing → ChoDuAn → ChoCCM → ChoCEODuyetPA → ChoCEODuyetNCC → DaDuyet
// Cả 2: từ DangSoanThao có thể → TuChoi; từ mọi phase duyệt reject → DangSoanThao.
public enum PurchaseEvaluationPhase
{
DangSoanThao = 1,
ChoPurchasing = 2,
ChoDuAn = 3, // chỉ B
ChoCCM = 4,
ChoCEODuyetPA = 5, // chỉ B (duyệt phương án trước)
ChoCEODuyetNCC = 6, // chung cả A & B — duyệt chọn đơn vị
DaDuyet = 7, // terminal thành công
TuChoi = 99, // terminal từ chối
}

View File

@ -0,0 +1,203 @@
using SolutionErp.Domain.Contracts; // WorkflowApproverKind
using SolutionErp.Domain.Identity;
namespace SolutionErp.Domain.PurchaseEvaluations;
// Policy record cho phiếu Duyệt NCC — mirror WorkflowPolicy của HĐ nhưng
// dùng PurchaseEvaluationPhase enum. 2 default policy hardcoded (A/B)
// phục vụ seed + fallback khi admin chưa author định nghĩa DB.
public sealed record PurchaseEvaluationPolicy(
string Name,
string Description,
IReadOnlyDictionary<(PurchaseEvaluationPhase From, PurchaseEvaluationPhase To), string[]> Transitions,
IReadOnlyDictionary<PurchaseEvaluationPhase, TimeSpan?> PhaseSla,
IReadOnlyList<PurchaseEvaluationPhase> ActivePhases,
IReadOnlyDictionary<(PurchaseEvaluationPhase From, PurchaseEvaluationPhase To), string[]>? UserTransitions = null)
{
public bool HasPhase(PurchaseEvaluationPhase phase) => ActivePhases.Contains(phase);
public bool IsTransitionAllowed(
PurchaseEvaluationPhase from, PurchaseEvaluationPhase to,
IReadOnlyList<string> actorRoles, Guid? actorUserId = null)
{
if (!Transitions.TryGetValue((from, to), out var roles)) return false;
if (actorRoles.Any(r => roles.Contains(r))) return true;
if (actorUserId is null) return false;
if (UserTransitions is null) return false;
if (!UserTransitions.TryGetValue((from, to), out var userIds)) return false;
return userIds.Contains(actorUserId.Value.ToString());
}
public IReadOnlyList<PurchaseEvaluationPhase> NextPhasesFrom(PurchaseEvaluationPhase from) =>
Transitions.Keys.Where(k => k.From == from).Select(k => k.To).Distinct().ToList();
}
public static class PurchaseEvaluationPolicies
{
private static readonly Dictionary<PurchaseEvaluationPhase, TimeSpan?> DefaultSla = new()
{
[PurchaseEvaluationPhase.DangSoanThao] = TimeSpan.FromDays(3),
[PurchaseEvaluationPhase.ChoPurchasing] = TimeSpan.FromDays(2),
[PurchaseEvaluationPhase.ChoDuAn] = TimeSpan.FromDays(2),
[PurchaseEvaluationPhase.ChoCCM] = TimeSpan.FromDays(2),
[PurchaseEvaluationPhase.ChoCEODuyetPA] = TimeSpan.FromDays(1),
[PurchaseEvaluationPhase.ChoCEODuyetNCC] = TimeSpan.FromDays(1),
[PurchaseEvaluationPhase.DaDuyet] = null,
[PurchaseEvaluationPhase.TuChoi] = null,
};
// A — DuyetNcc (3 step thực + Drafter soạn): Drafter → Purchasing → CCM → CEO
public static readonly PurchaseEvaluationPolicy NccOnly = new(
Name: "NccOnly",
Description: "Duyệt NCC — 3 step (Purchasing → CCM → CEO). Không cần duyệt phương án.",
Transitions: new Dictionary<(PurchaseEvaluationPhase, PurchaseEvaluationPhase), string[]>
{
[(PurchaseEvaluationPhase.DangSoanThao, PurchaseEvaluationPhase.ChoPurchasing)] = [AppRoles.Drafter, AppRoles.DeptManager],
[(PurchaseEvaluationPhase.DangSoanThao, PurchaseEvaluationPhase.TuChoi)] = [AppRoles.Drafter, AppRoles.DeptManager],
[(PurchaseEvaluationPhase.ChoPurchasing, PurchaseEvaluationPhase.ChoCCM)] = [AppRoles.Procurement],
[(PurchaseEvaluationPhase.ChoPurchasing, PurchaseEvaluationPhase.DangSoanThao)] = [AppRoles.Procurement],
[(PurchaseEvaluationPhase.ChoCCM, PurchaseEvaluationPhase.ChoCEODuyetNCC)] = [AppRoles.CostControl],
[(PurchaseEvaluationPhase.ChoCCM, PurchaseEvaluationPhase.DangSoanThao)] = [AppRoles.CostControl],
[(PurchaseEvaluationPhase.ChoCEODuyetNCC, PurchaseEvaluationPhase.DaDuyet)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
[(PurchaseEvaluationPhase.ChoCEODuyetNCC, PurchaseEvaluationPhase.DangSoanThao)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
},
PhaseSla: DefaultSla,
ActivePhases:
[
PurchaseEvaluationPhase.DangSoanThao,
PurchaseEvaluationPhase.ChoPurchasing,
PurchaseEvaluationPhase.ChoCCM,
PurchaseEvaluationPhase.ChoCEODuyetNCC,
PurchaseEvaluationPhase.DaDuyet,
PurchaseEvaluationPhase.TuChoi,
]);
// B — DuyetNccPhuongAn (5 step thực + Drafter): Drafter → Purchasing → Dự án → CCM → CEO(PA) → CEO(NCC)
public static readonly PurchaseEvaluationPolicy NccWithPlan = new(
Name: "NccWithPlan",
Description: "Duyệt NCC + Phương án — 5 step (Purchasing → Dự án → CCM → CEO duyệt PA → CEO duyệt NCC).",
Transitions: new Dictionary<(PurchaseEvaluationPhase, PurchaseEvaluationPhase), string[]>
{
[(PurchaseEvaluationPhase.DangSoanThao, PurchaseEvaluationPhase.ChoPurchasing)] = [AppRoles.Drafter, AppRoles.DeptManager],
[(PurchaseEvaluationPhase.DangSoanThao, PurchaseEvaluationPhase.TuChoi)] = [AppRoles.Drafter, AppRoles.DeptManager],
[(PurchaseEvaluationPhase.ChoPurchasing, PurchaseEvaluationPhase.ChoDuAn)] = [AppRoles.Procurement],
[(PurchaseEvaluationPhase.ChoPurchasing, PurchaseEvaluationPhase.DangSoanThao)] = [AppRoles.Procurement],
[(PurchaseEvaluationPhase.ChoDuAn, PurchaseEvaluationPhase.ChoCCM)] = [AppRoles.ProjectManager],
[(PurchaseEvaluationPhase.ChoDuAn, PurchaseEvaluationPhase.DangSoanThao)] = [AppRoles.ProjectManager],
[(PurchaseEvaluationPhase.ChoCCM, PurchaseEvaluationPhase.ChoCEODuyetPA)] = [AppRoles.CostControl],
[(PurchaseEvaluationPhase.ChoCCM, PurchaseEvaluationPhase.DangSoanThao)] = [AppRoles.CostControl],
[(PurchaseEvaluationPhase.ChoCEODuyetPA, PurchaseEvaluationPhase.ChoCEODuyetNCC)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
[(PurchaseEvaluationPhase.ChoCEODuyetPA, PurchaseEvaluationPhase.DangSoanThao)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
[(PurchaseEvaluationPhase.ChoCEODuyetNCC, PurchaseEvaluationPhase.DaDuyet)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
[(PurchaseEvaluationPhase.ChoCEODuyetNCC, PurchaseEvaluationPhase.DangSoanThao)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
},
PhaseSla: DefaultSla,
ActivePhases:
[
PurchaseEvaluationPhase.DangSoanThao,
PurchaseEvaluationPhase.ChoPurchasing,
PurchaseEvaluationPhase.ChoDuAn,
PurchaseEvaluationPhase.ChoCCM,
PurchaseEvaluationPhase.ChoCEODuyetPA,
PurchaseEvaluationPhase.ChoCEODuyetNCC,
PurchaseEvaluationPhase.DaDuyet,
PurchaseEvaluationPhase.TuChoi,
]);
}
public static class PurchaseEvaluationPolicyRegistry
{
public static readonly string[] AvailablePolicyNames = ["NccOnly", "NccWithPlan"];
public static PurchaseEvaluationPolicy ByName(string name) => name switch
{
"NccWithPlan" => PurchaseEvaluationPolicies.NccWithPlan,
_ => PurchaseEvaluationPolicies.NccOnly,
};
public static string DefaultPolicyNameFor(PurchaseEvaluationType type) => type switch
{
PurchaseEvaluationType.DuyetNccPhuongAn => "NccWithPlan",
_ => "NccOnly",
};
public static PurchaseEvaluationPolicy For(PurchaseEvaluationType type) =>
ByName(DefaultPolicyNameFor(type));
public static PurchaseEvaluationPolicy ForEvaluation(PurchaseEvaluation ev) =>
For(ev.Type);
// Build policy from persisted admin-authored definition (mirror
// WorkflowPolicyRegistry.FromDefinition for HĐ).
public static PurchaseEvaluationPolicy FromDefinition(PurchaseEvaluationWorkflowDefinition def)
{
var steps = def.Steps.OrderBy(s => s.Order).ToList();
var transitions = new Dictionary<(PurchaseEvaluationPhase From, PurchaseEvaluationPhase To), string[]>();
var userTransitions = new Dictionary<(PurchaseEvaluationPhase From, PurchaseEvaluationPhase To), string[]>();
var sla = new Dictionary<PurchaseEvaluationPhase, TimeSpan?>();
var activePhases = new List<PurchaseEvaluationPhase>();
PurchaseEvaluationPhase? prev = null;
foreach (var s in steps)
{
activePhases.Add(s.Phase);
sla[s.Phase] = s.SlaDays is int d ? TimeSpan.FromDays(d) : null;
var roles = s.Approvers
.Where(a => a.Kind == WorkflowApproverKind.Role)
.Select(a => a.AssignmentValue)
.Distinct()
.ToArray();
var hasUserKind = s.Approvers.Any(a => a.Kind == WorkflowApproverKind.User);
if (roles.Length == 0 && !hasUserKind) roles = [AppRoles.DeptManager];
var userIds = s.Approvers
.Where(a => a.Kind == WorkflowApproverKind.User)
.Select(a => a.AssignmentValue)
.Distinct()
.ToArray();
if (prev is not null)
{
transitions[(prev.Value, s.Phase)] = roles;
if (userIds.Length > 0) userTransitions[(prev.Value, s.Phase)] = userIds;
// Reject path back to Drafter (common pattern)
if (prev.Value != PurchaseEvaluationPhase.DangSoanThao && s.Phase != PurchaseEvaluationPhase.DangSoanThao)
{
transitions.TryAdd((s.Phase, PurchaseEvaluationPhase.DangSoanThao), roles);
if (userIds.Length > 0)
userTransitions.TryAdd((s.Phase, PurchaseEvaluationPhase.DangSoanThao), userIds);
}
}
prev = s.Phase;
}
// First step có thể reject to TuChoi
if (steps.Count > 0)
transitions.TryAdd((steps[0].Phase, PurchaseEvaluationPhase.TuChoi),
[AppRoles.Drafter, AppRoles.DeptManager]);
// Terminal states always available
if (!activePhases.Contains(PurchaseEvaluationPhase.TuChoi))
activePhases.Add(PurchaseEvaluationPhase.TuChoi);
if (!activePhases.Contains(PurchaseEvaluationPhase.DaDuyet))
activePhases.Add(PurchaseEvaluationPhase.DaDuyet);
return new PurchaseEvaluationPolicy(
Name: $"{def.Code}-v{def.Version:D2}",
Description: def.Description ?? def.Name,
Transitions: transitions,
PhaseSla: sla,
ActivePhases: activePhases,
UserTransitions: userTransitions.Count > 0 ? userTransitions : null);
}
}

View File

@ -0,0 +1,20 @@
using SolutionErp.Domain.Common;
namespace SolutionErp.Domain.PurchaseEvaluations;
// Báo giá N NCC × M hạng mục. 1 row = 1 NCC chào 1 hạng mục.
// IsSelected cho per-item winner (nếu user chọn mỗi hạng mục 1 NCC khác nhau);
// PurchaseEvaluation.SelectedSupplierId là winner tổng thể (trường hợp 1 NCC thắng toàn bộ).
public class PurchaseEvaluationQuote : BaseEntity
{
public Guid PurchaseEvaluationDetailId { get; set; }
public Guid PurchaseEvaluationSupplierId { get; set; } // FK PurchaseEvaluationSuppliers
public decimal BgVat { get; set; } // Báo giá NCC gửi (đã VAT)
public decimal ChuaVat { get; set; } // Chưa VAT
public decimal ThanhTien { get; set; } // = KL × ChuaVat (tính sẵn)
public bool IsSelected { get; set; } // NCC được chọn cho hạng mục này
public string? Note { get; set; }
public PurchaseEvaluationDetail? Detail { get; set; }
public PurchaseEvaluationSupplier? Supplier { get; set; }
}

View File

@ -0,0 +1,22 @@
using SolutionErp.Domain.Common;
namespace SolutionErp.Domain.PurchaseEvaluations;
// N:M giữa PurchaseEvaluation × Supplier (master). Lưu contact + payment
// term per NCC (file Excel section E + cột header "TGN-30 ngày" /
// "Tiến Phát" / ... ngay dưới bảng II). Note chứa yellow chip:
// "ĐÃ CHỐT SO SÁNH LẦN 1/2", "ĐÀM PHÁN THÊM".
public class PurchaseEvaluationSupplier : BaseEntity
{
public Guid PurchaseEvaluationId { get; set; }
public Guid SupplierId { get; set; } // FK Suppliers master
public string? DisplayName { get; set; } // Override nếu khác Supplier.Name (vd kèm term: "TGN-30 ngày")
public string? ContactName { get; set; }
public string? ContactEmail { get; set; }
public string? ContactPhone { get; set; }
public string? PaymentTermText { get; set; } // Free text: "30 ngày", "45 ngày", "300tr"
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
public PurchaseEvaluation? PurchaseEvaluation { get; set; }
}

View File

@ -0,0 +1,10 @@
namespace SolutionErp.Domain.PurchaseEvaluations;
// 2 quy trình theo flowchart QT chọn NTP/NCC:
// A — NccOnly (3 step): Purchasing → CCM → CEO
// B — NccWithPlan (5 step): Purchasing → Dự án → CCM → CEO (PA) → CEO (NCC)
public enum PurchaseEvaluationType
{
DuyetNcc = 1, // A
DuyetNccPhuongAn = 2, // B
}

View File

@ -0,0 +1,46 @@
using SolutionErp.Domain.Common;
using SolutionErp.Domain.Contracts; // reuse WorkflowApproverKind
namespace SolutionErp.Domain.PurchaseEvaluations;
// Versioned workflow definition cho module Duyệt NCC — pattern giống HĐ
// nhưng tách table riêng vì Phase là PurchaseEvaluationPhase enum (không
// phải ContractPhase). Admin có UI /system/pe-workflows/:typeCode tương
// tự /system/workflows/:typeCode.
//
// Invariant: AT MOST ONE IsActive=true per PurchaseEvaluationType tại 1
// thời điểm. PurchaseEvaluation.WorkflowDefinitionId pin tại create →
// phiếu cũ không bị ảnh hưởng khi admin active version mới.
public class PurchaseEvaluationWorkflowDefinition : BaseEntity
{
public string Code { get; set; } = string.Empty; // "QT-DN-A" / "QT-DN-B" default
public int Version { get; set; }
public PurchaseEvaluationType EvaluationType { get; set; }
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public bool IsActive { get; set; }
public DateTime? ActivatedAt { get; set; }
public List<PurchaseEvaluationWorkflowStep> Steps { get; set; } = new();
}
public class PurchaseEvaluationWorkflowStep : BaseEntity
{
public Guid PurchaseEvaluationWorkflowDefinitionId { get; set; }
public int Order { get; set; }
public PurchaseEvaluationPhase Phase { get; set; }
public string Name { get; set; } = string.Empty;
public int? SlaDays { get; set; }
public PurchaseEvaluationWorkflowDefinition? Definition { get; set; }
public List<PurchaseEvaluationWorkflowStepApprover> Approvers { get; set; } = new();
}
public class PurchaseEvaluationWorkflowStepApprover : BaseEntity
{
public Guid PurchaseEvaluationWorkflowStepId { get; set; }
public WorkflowApproverKind Kind { get; set; } // reuse Role/User enum từ Contract
public string AssignmentValue { get; set; } = string.Empty;
public PurchaseEvaluationWorkflowStep? Step { get; set; }
}

View File

@ -8,6 +8,7 @@ using SolutionErp.Domain.Identity;
using SolutionErp.Domain.Master; using SolutionErp.Domain.Master;
using SolutionErp.Domain.Master.Catalogs; using SolutionErp.Domain.Master.Catalogs;
using SolutionErp.Domain.Notifications; using SolutionErp.Domain.Notifications;
using SolutionErp.Domain.PurchaseEvaluations;
namespace SolutionErp.Infrastructure.Persistence; namespace SolutionErp.Infrastructure.Persistence;
@ -46,6 +47,17 @@ public class ApplicationDbContext
public DbSet<NguyenTacNccDetail> NguyenTacNccDetails => Set<NguyenTacNccDetail>(); public DbSet<NguyenTacNccDetail> NguyenTacNccDetails => Set<NguyenTacNccDetail>();
public DbSet<NguyenTacDvDetail> NguyenTacDvDetails => Set<NguyenTacDvDetail>(); public DbSet<NguyenTacDvDetail> NguyenTacDvDetails => Set<NguyenTacDvDetail>();
public DbSet<PurchaseEvaluation> PurchaseEvaluations => Set<PurchaseEvaluation>();
public DbSet<PurchaseEvaluationSupplier> PurchaseEvaluationSuppliers => Set<PurchaseEvaluationSupplier>();
public DbSet<PurchaseEvaluationDetail> PurchaseEvaluationDetails => Set<PurchaseEvaluationDetail>();
public DbSet<PurchaseEvaluationQuote> PurchaseEvaluationQuotes => Set<PurchaseEvaluationQuote>();
public DbSet<PurchaseEvaluationApproval> PurchaseEvaluationApprovals => Set<PurchaseEvaluationApproval>();
public DbSet<PurchaseEvaluationChangelog> PurchaseEvaluationChangelogs => Set<PurchaseEvaluationChangelog>();
public DbSet<PurchaseEvaluationAttachment> PurchaseEvaluationAttachments => Set<PurchaseEvaluationAttachment>();
public DbSet<PurchaseEvaluationWorkflowDefinition> PurchaseEvaluationWorkflowDefinitions => Set<PurchaseEvaluationWorkflowDefinition>();
public DbSet<PurchaseEvaluationWorkflowStep> PurchaseEvaluationWorkflowSteps => Set<PurchaseEvaluationWorkflowStep>();
public DbSet<PurchaseEvaluationWorkflowStepApprover> PurchaseEvaluationWorkflowStepApprovers => Set<PurchaseEvaluationWorkflowStepApprover>();
protected override void OnModelCreating(ModelBuilder builder) protected override void OnModelCreating(ModelBuilder builder)
{ {
base.OnModelCreating(builder); base.OnModelCreating(builder);

View File

@ -0,0 +1,205 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using SolutionErp.Domain.PurchaseEvaluations;
namespace SolutionErp.Infrastructure.Persistence.Configurations;
public class PurchaseEvaluationConfiguration : IEntityTypeConfiguration<PurchaseEvaluation>
{
public void Configure(EntityTypeBuilder<PurchaseEvaluation> b)
{
b.ToTable("PurchaseEvaluations");
b.HasKey(x => x.Id);
b.Property(x => x.MaPhieu).HasMaxLength(100);
b.Property(x => x.Type).HasConversion<int>();
b.Property(x => x.Phase).HasConversion<int>();
b.Property(x => x.TenGoiThau).HasMaxLength(500).IsRequired();
b.Property(x => x.DiaDiem).HasMaxLength(500);
b.Property(x => x.MoTa).HasMaxLength(2000);
b.Property(x => x.PaymentTerms).HasColumnType("nvarchar(max)");
b.HasIndex(x => x.MaPhieu).IsUnique().HasFilter("[MaPhieu] IS NOT NULL");
b.HasIndex(x => new { x.Phase, x.IsDeleted });
b.HasIndex(x => x.ProjectId);
b.HasIndex(x => x.SlaDeadline);
b.HasIndex(x => x.WorkflowDefinitionId);
b.HasIndex(x => x.ContractId);
b.HasMany(x => x.Suppliers).WithOne(s => s.PurchaseEvaluation).HasForeignKey(s => s.PurchaseEvaluationId).OnDelete(DeleteBehavior.Cascade);
b.HasMany(x => x.Details).WithOne(d => d.PurchaseEvaluation).HasForeignKey(d => d.PurchaseEvaluationId).OnDelete(DeleteBehavior.Cascade);
b.HasMany(x => x.Approvals).WithOne(a => a.PurchaseEvaluation).HasForeignKey(a => a.PurchaseEvaluationId).OnDelete(DeleteBehavior.Cascade);
b.HasMany(x => x.Changelogs).WithOne(c => c.PurchaseEvaluation).HasForeignKey(c => c.PurchaseEvaluationId).OnDelete(DeleteBehavior.Cascade);
b.HasMany(x => x.Attachments).WithOne(a => a.PurchaseEvaluation).HasForeignKey(a => a.PurchaseEvaluationId).OnDelete(DeleteBehavior.Cascade);
// Quotes không FK trực tiếp tới PurchaseEvaluation (đi qua Detail) —
// nhưng collection navigation có nên cần config riêng bên dưới.
b.HasQueryFilter(x => !x.IsDeleted);
}
}
public class PurchaseEvaluationSupplierConfiguration : IEntityTypeConfiguration<PurchaseEvaluationSupplier>
{
public void Configure(EntityTypeBuilder<PurchaseEvaluationSupplier> b)
{
b.ToTable("PurchaseEvaluationSuppliers");
b.HasKey(x => x.Id);
b.Property(x => x.DisplayName).HasMaxLength(200);
b.Property(x => x.ContactName).HasMaxLength(200);
b.Property(x => x.ContactEmail).HasMaxLength(200);
b.Property(x => x.ContactPhone).HasMaxLength(50);
b.Property(x => x.PaymentTermText).HasMaxLength(200);
b.Property(x => x.Note).HasMaxLength(500);
b.HasIndex(x => new { x.PurchaseEvaluationId, x.SupplierId }).IsUnique();
b.HasIndex(x => x.SupplierId);
}
}
public class PurchaseEvaluationDetailConfiguration : IEntityTypeConfiguration<PurchaseEvaluationDetail>
{
public void Configure(EntityTypeBuilder<PurchaseEvaluationDetail> b)
{
b.ToTable("PurchaseEvaluationDetails");
b.HasKey(x => x.Id);
b.Property(x => x.GroupCode).HasMaxLength(50).IsRequired();
b.Property(x => x.GroupName).HasMaxLength(200).IsRequired();
b.Property(x => x.ItemCode).HasMaxLength(100);
b.Property(x => x.NoiDung).HasMaxLength(500).IsRequired();
b.Property(x => x.DonViTinh).HasMaxLength(50);
b.Property(x => x.GhiChu).HasMaxLength(1000);
b.Property(x => x.KhoiLuongNganSach).HasPrecision(18, 4);
b.Property(x => x.KhoiLuongThiCong).HasPrecision(18, 4);
b.Property(x => x.DonGiaNganSach).HasPrecision(18, 2);
b.Property(x => x.ThanhTienNganSach).HasPrecision(18, 2);
b.HasIndex(x => new { x.PurchaseEvaluationId, x.Order });
b.HasMany(x => x.Quotes).WithOne(q => q.Detail).HasForeignKey(q => q.PurchaseEvaluationDetailId).OnDelete(DeleteBehavior.Cascade);
}
}
public class PurchaseEvaluationQuoteConfiguration : IEntityTypeConfiguration<PurchaseEvaluationQuote>
{
public void Configure(EntityTypeBuilder<PurchaseEvaluationQuote> b)
{
b.ToTable("PurchaseEvaluationQuotes");
b.HasKey(x => x.Id);
b.Property(x => x.BgVat).HasPrecision(18, 2);
b.Property(x => x.ChuaVat).HasPrecision(18, 2);
b.Property(x => x.ThanhTien).HasPrecision(18, 2);
b.Property(x => x.Note).HasMaxLength(500);
b.HasIndex(x => new { x.PurchaseEvaluationDetailId, x.PurchaseEvaluationSupplierId }).IsUnique();
// Quote → Supplier (restrict — không xóa Supplier-row khỏi phiếu nếu còn quote)
b.HasOne(x => x.Supplier)
.WithMany()
.HasForeignKey(x => x.PurchaseEvaluationSupplierId)
.OnDelete(DeleteBehavior.Restrict);
}
}
public class PurchaseEvaluationApprovalConfiguration : IEntityTypeConfiguration<PurchaseEvaluationApproval>
{
public void Configure(EntityTypeBuilder<PurchaseEvaluationApproval> b)
{
b.ToTable("PurchaseEvaluationApprovals");
b.HasKey(x => x.Id);
b.Property(x => x.FromPhase).HasConversion<int>();
b.Property(x => x.ToPhase).HasConversion<int>();
b.Property(x => x.Decision).HasConversion<int>();
b.Property(x => x.Comment).HasMaxLength(1000);
b.HasIndex(x => new { x.PurchaseEvaluationId, x.ApprovedAt });
}
}
public class PurchaseEvaluationChangelogConfiguration : IEntityTypeConfiguration<PurchaseEvaluationChangelog>
{
public void Configure(EntityTypeBuilder<PurchaseEvaluationChangelog> b)
{
b.ToTable("PurchaseEvaluationChangelogs");
b.HasKey(x => x.Id);
b.Property(x => x.EntityType).HasConversion<int>();
b.Property(x => x.Action).HasConversion<int>();
b.Property(x => x.PhaseAtChange).HasConversion<int>();
b.Property(x => x.UserName).HasMaxLength(200);
b.Property(x => x.Summary).HasMaxLength(500);
b.Property(x => x.ContextNote).HasMaxLength(2000);
b.Property(x => x.FieldChangesJson).HasColumnType("nvarchar(max)");
b.HasIndex(x => new { x.PurchaseEvaluationId, x.CreatedAt });
b.HasIndex(x => new { x.PurchaseEvaluationId, x.EntityType });
}
}
public class PurchaseEvaluationAttachmentConfiguration : IEntityTypeConfiguration<PurchaseEvaluationAttachment>
{
public void Configure(EntityTypeBuilder<PurchaseEvaluationAttachment> b)
{
b.ToTable("PurchaseEvaluationAttachments");
b.HasKey(x => x.Id);
b.Property(x => x.FileName).HasMaxLength(255).IsRequired();
b.Property(x => x.StoragePath).HasMaxLength(500).IsRequired();
b.Property(x => x.ContentType).HasMaxLength(100).IsRequired();
b.Property(x => x.Purpose).HasConversion<int>();
b.Property(x => x.Note).HasMaxLength(500);
b.HasIndex(x => x.PurchaseEvaluationId);
b.HasIndex(x => x.PurchaseEvaluationSupplierId);
}
}
public class PurchaseEvaluationWorkflowDefinitionConfiguration : IEntityTypeConfiguration<PurchaseEvaluationWorkflowDefinition>
{
public void Configure(EntityTypeBuilder<PurchaseEvaluationWorkflowDefinition> e)
{
e.ToTable("PurchaseEvaluationWorkflowDefinitions");
e.Property(x => x.Code).HasMaxLength(100).IsRequired();
e.Property(x => x.Name).HasMaxLength(200).IsRequired();
e.Property(x => x.Description).HasMaxLength(1000);
e.Property(x => x.EvaluationType).HasConversion<int>();
e.HasIndex(x => new { x.Code, x.Version }).IsUnique();
e.HasIndex(x => new { x.EvaluationType, x.IsActive });
}
}
public class PurchaseEvaluationWorkflowStepConfiguration : IEntityTypeConfiguration<PurchaseEvaluationWorkflowStep>
{
public void Configure(EntityTypeBuilder<PurchaseEvaluationWorkflowStep> e)
{
e.ToTable("PurchaseEvaluationWorkflowSteps");
e.Property(x => x.Name).HasMaxLength(200).IsRequired();
e.Property(x => x.Phase).HasConversion<int>();
e.HasOne(x => x.Definition)
.WithMany(d => d.Steps)
.HasForeignKey(x => x.PurchaseEvaluationWorkflowDefinitionId)
.OnDelete(DeleteBehavior.Cascade);
e.HasIndex(x => new { x.PurchaseEvaluationWorkflowDefinitionId, x.Order });
}
}
public class PurchaseEvaluationWorkflowStepApproverConfiguration : IEntityTypeConfiguration<PurchaseEvaluationWorkflowStepApprover>
{
public void Configure(EntityTypeBuilder<PurchaseEvaluationWorkflowStepApprover> e)
{
e.ToTable("PurchaseEvaluationWorkflowStepApprovers");
e.Property(x => x.Kind).HasConversion<int>();
e.Property(x => x.AssignmentValue).HasMaxLength(100).IsRequired();
e.HasOne(x => x.Step)
.WithMany(s => s.Approvers)
.HasForeignKey(x => x.PurchaseEvaluationWorkflowStepId)
.OnDelete(DeleteBehavior.Cascade);
}
}

View File

@ -8,6 +8,7 @@ using SolutionErp.Domain.Forms;
using SolutionErp.Domain.Identity; using SolutionErp.Domain.Identity;
using SolutionErp.Domain.Master; using SolutionErp.Domain.Master;
using SolutionErp.Domain.Master.Catalogs; using SolutionErp.Domain.Master.Catalogs;
using SolutionErp.Domain.PurchaseEvaluations;
namespace SolutionErp.Infrastructure.Persistence; namespace SolutionErp.Infrastructure.Persistence;
@ -37,6 +38,7 @@ public static class DbInitializer
await SeedDemoMasterDataAsync(db, logger); await SeedDemoMasterDataAsync(db, logger);
await SeedContractTemplatesAsync(db, logger); await SeedContractTemplatesAsync(db, logger);
await SeedWorkflowDefinitionsAsync(db, logger); await SeedWorkflowDefinitionsAsync(db, logger);
await SeedPurchaseEvaluationWorkflowsAsync(db, logger);
await SeedCatalogsAsync(db, logger); await SeedCatalogsAsync(db, logger);
// Backfill mã HĐ cho HĐ legacy chưa có (sau khi đổi policy gen-tại-create). // Backfill mã HĐ cho HĐ legacy chưa có (sau khi đổi policy gen-tại-create).
@ -317,6 +319,79 @@ public static class DbInitializer
} }
} }
// Seed default workflow v01 cho 2 PurchaseEvaluationType (A NccOnly / B
// NccWithPlan) — mirror SeedWorkflowDefinitionsAsync của HĐ. Admin có
// thể tạo version mới qua /system/pe-workflows/:typeCode designer.
private static async Task SeedPurchaseEvaluationWorkflowsAsync(ApplicationDbContext db, ILogger logger)
{
var typeLabels = new Dictionary<PurchaseEvaluationType, (string Code, string Name)>
{
[PurchaseEvaluationType.DuyetNcc] = ("QT-DN-A", "Quy trình Duyệt NCC"),
[PurchaseEvaluationType.DuyetNccPhuongAn] = ("QT-DN-B", "Quy trình Duyệt NCC - Phương Án"),
};
var phaseNames = new Dictionary<PurchaseEvaluationPhase, string>
{
[PurchaseEvaluationPhase.DangSoanThao] = "Soạn thảo",
[PurchaseEvaluationPhase.ChoPurchasing] = "Purchasing tổng hợp",
[PurchaseEvaluationPhase.ChoDuAn] = "Dự án kiểm tra",
[PurchaseEvaluationPhase.ChoCCM] = "CCM kiểm tra ngân sách",
[PurchaseEvaluationPhase.ChoCEODuyetPA] = "CEO duyệt phương án",
[PurchaseEvaluationPhase.ChoCEODuyetNCC] = "CEO duyệt chọn đơn vị",
[PurchaseEvaluationPhase.DaDuyet] = "Đã duyệt",
};
var added = 0;
foreach (var (type, info) in typeLabels)
{
var alreadyExists = await db.PurchaseEvaluationWorkflowDefinitions
.AnyAsync(w => w.EvaluationType == type);
if (alreadyExists) continue;
var policy = PurchaseEvaluationPolicyRegistry.For(type);
var def = new PurchaseEvaluationWorkflowDefinition
{
Code = info.Code,
Version = 1,
EvaluationType = type,
Name = $"{info.Name} (v01)",
Description = policy.Description,
IsActive = true,
ActivatedAt = DateTime.UtcNow,
Steps = policy.ActivePhases
.Where(p => p != PurchaseEvaluationPhase.TuChoi && p != PurchaseEvaluationPhase.DaDuyet)
.Select((p, idx) =>
{
var roles = policy.Transitions
.Where(t => t.Key.To == p)
.SelectMany(t => t.Value)
.Distinct()
.ToList();
return new PurchaseEvaluationWorkflowStep
{
Order = idx + 1,
Phase = p,
Name = phaseNames.GetValueOrDefault(p, p.ToString()),
SlaDays = policy.PhaseSla.GetValueOrDefault(p) is TimeSpan s ? (int?)s.Days : null,
Approvers = roles.Select(r => new PurchaseEvaluationWorkflowStepApprover
{
Kind = WorkflowApproverKind.Role,
AssignmentValue = r,
}).ToList(),
};
})
.ToList(),
};
db.PurchaseEvaluationWorkflowDefinitions.Add(def);
added++;
}
if (added > 0)
{
await db.SaveChangesAsync();
logger.LogInformation("Seeded {Count} PE workflow definitions (v01)", added);
}
}
// Map ContractType → preferred supplier code (đa dạng dữ liệu demo theo // Map ContractType → preferred supplier code (đa dạng dữ liệu demo theo
// đúng business: ThauPhu/GiaoKhoan đi với NTP/TĐ, NCC/MuaBan/NTNcc đi // đúng business: ThauPhu/GiaoKhoan đi với NTP/TĐ, NCC/MuaBan/NTNcc đi
// với NCC, DichVu/NTDv đi với DV). // với NCC, DichVu/NTDv đi với DV).
@ -786,6 +861,9 @@ public static class DbInitializer
(MenuKeys.Roles, "Vai trò", MenuKeys.System, 92, "Shield"), (MenuKeys.Roles, "Vai trò", MenuKeys.System, 92, "Shield"),
(MenuKeys.Permissions, "Phân quyền", MenuKeys.System, 93, "KeyRound"), (MenuKeys.Permissions, "Phân quyền", MenuKeys.System, 93, "KeyRound"),
(MenuKeys.Workflows, "Quy trình HĐ", MenuKeys.System, 94, "GitBranch"), (MenuKeys.Workflows, "Quy trình HĐ", MenuKeys.System, 94, "GitBranch"),
// Module Duyệt NCC (tiền-HĐ)
(MenuKeys.PurchaseEvaluations, "Quy trình chọn Thầu phụ - NCC", null, 25, "ClipboardCheck"),
(MenuKeys.PeWorkflows, "Quy trình Duyệt NCC", MenuKeys.System, 95, "GitCompareArrows"),
}; };
// Per-type sub-menu under Contracts: 1 group + 3 leaves each // Per-type sub-menu under Contracts: 1 group + 3 leaves each
@ -809,6 +887,30 @@ public static class DbInitializer
tree.Add((MenuKeys.WorkflowTypeLeaf(code), label, MenuKeys.Workflows, wfOrder++, "FileText")); tree.Add((MenuKeys.WorkflowTypeLeaf(code), label, MenuKeys.Workflows, wfOrder++, "FileText"));
} }
// Pe_* group per PurchaseEvaluationType + 3 action leaves each
var peTypeLabels = new Dictionary<string, string>
{
["DuyetNcc"] = "Quy trình Duyệt NCC",
["DuyetNccPhuongAn"] = "Quy trình Duyệt NCC - Phương Án",
};
var peOrder = 1;
foreach (var code in MenuKeys.PurchaseEvaluationTypeCodes)
{
var label = peTypeLabels.GetValueOrDefault(code, code);
tree.Add((MenuKeys.PurchaseEvaluationGroup(code), label, MenuKeys.PurchaseEvaluations, peOrder++, "FileCheck"));
tree.Add((MenuKeys.PurchaseEvaluationList(code), "Danh sách", MenuKeys.PurchaseEvaluationGroup(code), peOrder++, "List"));
tree.Add((MenuKeys.PurchaseEvaluationCreate(code), "Thao tác", MenuKeys.PurchaseEvaluationGroup(code), peOrder++, "Plus"));
tree.Add((MenuKeys.PurchaseEvaluationPending(code),"Duyệt", MenuKeys.PurchaseEvaluationGroup(code), peOrder++, "CheckCircle2"));
}
// PE workflow admin leaves dưới `PeWorkflows`
var peWfOrder = 96;
foreach (var code in MenuKeys.PurchaseEvaluationTypeCodes)
{
var label = peTypeLabels.GetValueOrDefault(code, code);
tree.Add((MenuKeys.PeWorkflowTypeLeaf(code), label, MenuKeys.PeWorkflows, peWfOrder++, "FileCheck"));
}
var existingKeys = await db.MenuItems.Select(m => m.Key).ToListAsync(); var existingKeys = await db.MenuItems.Select(m => m.Key).ToListAsync();
var added = 0; var added = 0;
foreach (var (key, label, parent, o, icon) in tree) foreach (var (key, label, parent, o, icon) in tree)

View File

@ -0,0 +1,455 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SolutionErp.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddPurchaseEvaluations : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "PurchaseEvaluations",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
MaPhieu = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
Type = table.Column<int>(type: "int", nullable: false),
Phase = table.Column<int>(type: "int", nullable: false),
TenGoiThau = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false),
ProjectId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
DepartmentId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
DrafterUserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
DiaDiem = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
MoTa = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: true),
WorkflowDefinitionId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
SlaDeadline = table.Column<DateTime>(type: "datetime2", nullable: true),
SlaWarningSent = table.Column<bool>(type: "bit", nullable: false),
SelectedSupplierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
PaymentTerms = table.Column<string>(type: "nvarchar(max)", nullable: true),
ContractId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
CreatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
UpdatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false),
DeletedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
DeletedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_PurchaseEvaluations", x => x.Id);
});
migrationBuilder.CreateTable(
name: "PurchaseEvaluationWorkflowDefinitions",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Code = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
Version = table.Column<int>(type: "int", nullable: false),
EvaluationType = table.Column<int>(type: "int", nullable: false),
Name = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
Description = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true),
IsActive = table.Column<bool>(type: "bit", nullable: false),
ActivatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
CreatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
UpdatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_PurchaseEvaluationWorkflowDefinitions", x => x.Id);
});
migrationBuilder.CreateTable(
name: "PurchaseEvaluationApprovals",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
PurchaseEvaluationId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
FromPhase = table.Column<int>(type: "int", nullable: false),
ToPhase = table.Column<int>(type: "int", nullable: false),
ApproverUserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
Decision = table.Column<int>(type: "int", nullable: false),
Comment = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true),
ApprovedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
CreatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
UpdatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_PurchaseEvaluationApprovals", x => x.Id);
table.ForeignKey(
name: "FK_PurchaseEvaluationApprovals_PurchaseEvaluations_PurchaseEvaluationId",
column: x => x.PurchaseEvaluationId,
principalTable: "PurchaseEvaluations",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "PurchaseEvaluationAttachments",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
PurchaseEvaluationId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
PurchaseEvaluationSupplierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
FileName = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false),
StoragePath = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false),
FileSize = table.Column<long>(type: "bigint", nullable: false),
ContentType = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
Purpose = table.Column<int>(type: "int", nullable: false),
Note = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
CreatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
UpdatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_PurchaseEvaluationAttachments", x => x.Id);
table.ForeignKey(
name: "FK_PurchaseEvaluationAttachments_PurchaseEvaluations_PurchaseEvaluationId",
column: x => x.PurchaseEvaluationId,
principalTable: "PurchaseEvaluations",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "PurchaseEvaluationChangelogs",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
PurchaseEvaluationId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
EntityType = table.Column<int>(type: "int", nullable: false),
EntityId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
Action = table.Column<int>(type: "int", nullable: false),
PhaseAtChange = table.Column<int>(type: "int", nullable: true),
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
UserName = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
Summary = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
FieldChangesJson = table.Column<string>(type: "nvarchar(max)", nullable: true),
ContextNote = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
CreatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
UpdatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_PurchaseEvaluationChangelogs", x => x.Id);
table.ForeignKey(
name: "FK_PurchaseEvaluationChangelogs_PurchaseEvaluations_PurchaseEvaluationId",
column: x => x.PurchaseEvaluationId,
principalTable: "PurchaseEvaluations",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "PurchaseEvaluationDetails",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
PurchaseEvaluationId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
GroupCode = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
GroupName = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
ItemCode = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
NoiDung = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false),
DonViTinh = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
KhoiLuongNganSach = table.Column<decimal>(type: "decimal(18,4)", precision: 18, scale: 4, nullable: false),
KhoiLuongThiCong = table.Column<decimal>(type: "decimal(18,4)", precision: 18, scale: 4, nullable: false),
DonGiaNganSach = table.Column<decimal>(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false),
ThanhTienNganSach = table.Column<decimal>(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false),
Order = table.Column<int>(type: "int", nullable: false),
GhiChu = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
CreatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
UpdatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_PurchaseEvaluationDetails", x => x.Id);
table.ForeignKey(
name: "FK_PurchaseEvaluationDetails_PurchaseEvaluations_PurchaseEvaluationId",
column: x => x.PurchaseEvaluationId,
principalTable: "PurchaseEvaluations",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "PurchaseEvaluationSuppliers",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
PurchaseEvaluationId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
SupplierId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
DisplayName = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
ContactName = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
ContactEmail = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
ContactPhone = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
PaymentTermText = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
Note = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
Order = table.Column<int>(type: "int", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
CreatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
UpdatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_PurchaseEvaluationSuppliers", x => x.Id);
table.ForeignKey(
name: "FK_PurchaseEvaluationSuppliers_PurchaseEvaluations_PurchaseEvaluationId",
column: x => x.PurchaseEvaluationId,
principalTable: "PurchaseEvaluations",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "PurchaseEvaluationWorkflowSteps",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
PurchaseEvaluationWorkflowDefinitionId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Order = table.Column<int>(type: "int", nullable: false),
Phase = table.Column<int>(type: "int", nullable: false),
Name = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
SlaDays = table.Column<int>(type: "int", nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
CreatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
UpdatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_PurchaseEvaluationWorkflowSteps", x => x.Id);
table.ForeignKey(
name: "FK_PurchaseEvaluationWorkflowSteps_PurchaseEvaluationWorkflowDefinitions_PurchaseEvaluationWorkflowDefinitionId",
column: x => x.PurchaseEvaluationWorkflowDefinitionId,
principalTable: "PurchaseEvaluationWorkflowDefinitions",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "PurchaseEvaluationQuotes",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
PurchaseEvaluationDetailId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
PurchaseEvaluationSupplierId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
BgVat = table.Column<decimal>(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false),
ChuaVat = table.Column<decimal>(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false),
ThanhTien = table.Column<decimal>(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false),
IsSelected = table.Column<bool>(type: "bit", nullable: false),
Note = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
PurchaseEvaluationId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
CreatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
UpdatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_PurchaseEvaluationQuotes", x => x.Id);
table.ForeignKey(
name: "FK_PurchaseEvaluationQuotes_PurchaseEvaluationDetails_PurchaseEvaluationDetailId",
column: x => x.PurchaseEvaluationDetailId,
principalTable: "PurchaseEvaluationDetails",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_PurchaseEvaluationQuotes_PurchaseEvaluationSuppliers_PurchaseEvaluationSupplierId",
column: x => x.PurchaseEvaluationSupplierId,
principalTable: "PurchaseEvaluationSuppliers",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_PurchaseEvaluationQuotes_PurchaseEvaluations_PurchaseEvaluationId",
column: x => x.PurchaseEvaluationId,
principalTable: "PurchaseEvaluations",
principalColumn: "Id");
});
migrationBuilder.CreateTable(
name: "PurchaseEvaluationWorkflowStepApprovers",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
PurchaseEvaluationWorkflowStepId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Kind = table.Column<int>(type: "int", nullable: false),
AssignmentValue = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
CreatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
UpdatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_PurchaseEvaluationWorkflowStepApprovers", x => x.Id);
table.ForeignKey(
name: "FK_PurchaseEvaluationWorkflowStepApprovers_PurchaseEvaluationWorkflowSteps_PurchaseEvaluationWorkflowStepId",
column: x => x.PurchaseEvaluationWorkflowStepId,
principalTable: "PurchaseEvaluationWorkflowSteps",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_PurchaseEvaluationApprovals_PurchaseEvaluationId_ApprovedAt",
table: "PurchaseEvaluationApprovals",
columns: new[] { "PurchaseEvaluationId", "ApprovedAt" });
migrationBuilder.CreateIndex(
name: "IX_PurchaseEvaluationAttachments_PurchaseEvaluationId",
table: "PurchaseEvaluationAttachments",
column: "PurchaseEvaluationId");
migrationBuilder.CreateIndex(
name: "IX_PurchaseEvaluationAttachments_PurchaseEvaluationSupplierId",
table: "PurchaseEvaluationAttachments",
column: "PurchaseEvaluationSupplierId");
migrationBuilder.CreateIndex(
name: "IX_PurchaseEvaluationChangelogs_PurchaseEvaluationId_CreatedAt",
table: "PurchaseEvaluationChangelogs",
columns: new[] { "PurchaseEvaluationId", "CreatedAt" });
migrationBuilder.CreateIndex(
name: "IX_PurchaseEvaluationChangelogs_PurchaseEvaluationId_EntityType",
table: "PurchaseEvaluationChangelogs",
columns: new[] { "PurchaseEvaluationId", "EntityType" });
migrationBuilder.CreateIndex(
name: "IX_PurchaseEvaluationDetails_PurchaseEvaluationId_Order",
table: "PurchaseEvaluationDetails",
columns: new[] { "PurchaseEvaluationId", "Order" });
migrationBuilder.CreateIndex(
name: "IX_PurchaseEvaluationQuotes_PurchaseEvaluationDetailId_PurchaseEvaluationSupplierId",
table: "PurchaseEvaluationQuotes",
columns: new[] { "PurchaseEvaluationDetailId", "PurchaseEvaluationSupplierId" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_PurchaseEvaluationQuotes_PurchaseEvaluationId",
table: "PurchaseEvaluationQuotes",
column: "PurchaseEvaluationId");
migrationBuilder.CreateIndex(
name: "IX_PurchaseEvaluationQuotes_PurchaseEvaluationSupplierId",
table: "PurchaseEvaluationQuotes",
column: "PurchaseEvaluationSupplierId");
migrationBuilder.CreateIndex(
name: "IX_PurchaseEvaluations_ContractId",
table: "PurchaseEvaluations",
column: "ContractId");
migrationBuilder.CreateIndex(
name: "IX_PurchaseEvaluations_MaPhieu",
table: "PurchaseEvaluations",
column: "MaPhieu",
unique: true,
filter: "[MaPhieu] IS NOT NULL");
migrationBuilder.CreateIndex(
name: "IX_PurchaseEvaluations_Phase_IsDeleted",
table: "PurchaseEvaluations",
columns: new[] { "Phase", "IsDeleted" });
migrationBuilder.CreateIndex(
name: "IX_PurchaseEvaluations_ProjectId",
table: "PurchaseEvaluations",
column: "ProjectId");
migrationBuilder.CreateIndex(
name: "IX_PurchaseEvaluations_SlaDeadline",
table: "PurchaseEvaluations",
column: "SlaDeadline");
migrationBuilder.CreateIndex(
name: "IX_PurchaseEvaluations_WorkflowDefinitionId",
table: "PurchaseEvaluations",
column: "WorkflowDefinitionId");
migrationBuilder.CreateIndex(
name: "IX_PurchaseEvaluationSuppliers_PurchaseEvaluationId_SupplierId",
table: "PurchaseEvaluationSuppliers",
columns: new[] { "PurchaseEvaluationId", "SupplierId" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_PurchaseEvaluationSuppliers_SupplierId",
table: "PurchaseEvaluationSuppliers",
column: "SupplierId");
migrationBuilder.CreateIndex(
name: "IX_PurchaseEvaluationWorkflowDefinitions_Code_Version",
table: "PurchaseEvaluationWorkflowDefinitions",
columns: new[] { "Code", "Version" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_PurchaseEvaluationWorkflowDefinitions_EvaluationType_IsActive",
table: "PurchaseEvaluationWorkflowDefinitions",
columns: new[] { "EvaluationType", "IsActive" });
migrationBuilder.CreateIndex(
name: "IX_PurchaseEvaluationWorkflowStepApprovers_PurchaseEvaluationWorkflowStepId",
table: "PurchaseEvaluationWorkflowStepApprovers",
column: "PurchaseEvaluationWorkflowStepId");
migrationBuilder.CreateIndex(
name: "IX_PurchaseEvaluationWorkflowSteps_PurchaseEvaluationWorkflowDefinitionId_Order",
table: "PurchaseEvaluationWorkflowSteps",
columns: new[] { "PurchaseEvaluationWorkflowDefinitionId", "Order" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "PurchaseEvaluationApprovals");
migrationBuilder.DropTable(
name: "PurchaseEvaluationAttachments");
migrationBuilder.DropTable(
name: "PurchaseEvaluationChangelogs");
migrationBuilder.DropTable(
name: "PurchaseEvaluationQuotes");
migrationBuilder.DropTable(
name: "PurchaseEvaluationWorkflowStepApprovers");
migrationBuilder.DropTable(
name: "PurchaseEvaluationDetails");
migrationBuilder.DropTable(
name: "PurchaseEvaluationSuppliers");
migrationBuilder.DropTable(
name: "PurchaseEvaluationWorkflowSteps");
migrationBuilder.DropTable(
name: "PurchaseEvaluations");
migrationBuilder.DropTable(
name: "PurchaseEvaluationWorkflowDefinitions");
}
}
}

View File

@ -1904,6 +1904,592 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
b.ToTable("Notifications", (string)null); b.ToTable("Notifications", (string)null);
}); });
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<Guid?>("ContractId")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("CreatedBy")
.HasColumnType("uniqueidentifier");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("DeletedBy")
.HasColumnType("uniqueidentifier");
b.Property<Guid?>("DepartmentId")
.HasColumnType("uniqueidentifier");
b.Property<string>("DiaDiem")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<Guid?>("DrafterUserId")
.HasColumnType("uniqueidentifier");
b.Property<bool>("IsDeleted")
.HasColumnType("bit");
b.Property<string>("MaPhieu")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("MoTa")
.HasMaxLength(2000)
.HasColumnType("nvarchar(2000)");
b.Property<string>("PaymentTerms")
.HasColumnType("nvarchar(max)");
b.Property<int>("Phase")
.HasColumnType("int");
b.Property<Guid>("ProjectId")
.HasColumnType("uniqueidentifier");
b.Property<Guid?>("SelectedSupplierId")
.HasColumnType("uniqueidentifier");
b.Property<DateTime?>("SlaDeadline")
.HasColumnType("datetime2");
b.Property<bool>("SlaWarningSent")
.HasColumnType("bit");
b.Property<string>("TenGoiThau")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<int>("Type")
.HasColumnType("int");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uniqueidentifier");
b.Property<Guid?>("WorkflowDefinitionId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("ContractId");
b.HasIndex("MaPhieu")
.IsUnique()
.HasFilter("[MaPhieu] IS NOT NULL");
b.HasIndex("ProjectId");
b.HasIndex("SlaDeadline");
b.HasIndex("WorkflowDefinitionId");
b.HasIndex("Phase", "IsDeleted");
b.ToTable("PurchaseEvaluations", (string)null);
});
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationApproval", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("ApprovedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("ApproverUserId")
.HasColumnType("uniqueidentifier");
b.Property<string>("Comment")
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("CreatedBy")
.HasColumnType("uniqueidentifier");
b.Property<int>("Decision")
.HasColumnType("int");
b.Property<int>("FromPhase")
.HasColumnType("int");
b.Property<Guid>("PurchaseEvaluationId")
.HasColumnType("uniqueidentifier");
b.Property<int>("ToPhase")
.HasColumnType("int");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("PurchaseEvaluationId", "ApprovedAt");
b.ToTable("PurchaseEvaluationApprovals", (string)null);
});
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationAttachment", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("ContentType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("CreatedBy")
.HasColumnType("uniqueidentifier");
b.Property<string>("FileName")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<long>("FileSize")
.HasColumnType("bigint");
b.Property<string>("Note")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<Guid>("PurchaseEvaluationId")
.HasColumnType("uniqueidentifier");
b.Property<Guid?>("PurchaseEvaluationSupplierId")
.HasColumnType("uniqueidentifier");
b.Property<int>("Purpose")
.HasColumnType("int");
b.Property<string>("StoragePath")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("PurchaseEvaluationId");
b.HasIndex("PurchaseEvaluationSupplierId");
b.ToTable("PurchaseEvaluationAttachments", (string)null);
});
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationChangelog", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<int>("Action")
.HasColumnType("int");
b.Property<string>("ContextNote")
.HasMaxLength(2000)
.HasColumnType("nvarchar(2000)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("CreatedBy")
.HasColumnType("uniqueidentifier");
b.Property<Guid?>("EntityId")
.HasColumnType("uniqueidentifier");
b.Property<int>("EntityType")
.HasColumnType("int");
b.Property<string>("FieldChangesJson")
.HasColumnType("nvarchar(max)");
b.Property<int?>("PhaseAtChange")
.HasColumnType("int");
b.Property<Guid>("PurchaseEvaluationId")
.HasColumnType("uniqueidentifier");
b.Property<string>("Summary")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uniqueidentifier");
b.Property<Guid?>("UserId")
.HasColumnType("uniqueidentifier");
b.Property<string>("UserName")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.HasKey("Id");
b.HasIndex("PurchaseEvaluationId", "CreatedAt");
b.HasIndex("PurchaseEvaluationId", "EntityType");
b.ToTable("PurchaseEvaluationChangelogs", (string)null);
});
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationDetail", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("CreatedBy")
.HasColumnType("uniqueidentifier");
b.Property<decimal>("DonGiaNganSach")
.HasPrecision(18, 2)
.HasColumnType("decimal(18,2)");
b.Property<string>("DonViTinh")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("GhiChu")
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<string>("GroupCode")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("GroupName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("ItemCode")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<decimal>("KhoiLuongNganSach")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.Property<decimal>("KhoiLuongThiCong")
.HasPrecision(18, 4)
.HasColumnType("decimal(18,4)");
b.Property<string>("NoiDung")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<int>("Order")
.HasColumnType("int");
b.Property<Guid>("PurchaseEvaluationId")
.HasColumnType("uniqueidentifier");
b.Property<decimal>("ThanhTienNganSach")
.HasPrecision(18, 2)
.HasColumnType("decimal(18,2)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("PurchaseEvaluationId", "Order");
b.ToTable("PurchaseEvaluationDetails", (string)null);
});
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationQuote", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<decimal>("BgVat")
.HasPrecision(18, 2)
.HasColumnType("decimal(18,2)");
b.Property<decimal>("ChuaVat")
.HasPrecision(18, 2)
.HasColumnType("decimal(18,2)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("CreatedBy")
.HasColumnType("uniqueidentifier");
b.Property<bool>("IsSelected")
.HasColumnType("bit");
b.Property<string>("Note")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<Guid>("PurchaseEvaluationDetailId")
.HasColumnType("uniqueidentifier");
b.Property<Guid?>("PurchaseEvaluationId")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("PurchaseEvaluationSupplierId")
.HasColumnType("uniqueidentifier");
b.Property<decimal>("ThanhTien")
.HasPrecision(18, 2)
.HasColumnType("decimal(18,2)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("PurchaseEvaluationId");
b.HasIndex("PurchaseEvaluationSupplierId");
b.HasIndex("PurchaseEvaluationDetailId", "PurchaseEvaluationSupplierId")
.IsUnique();
b.ToTable("PurchaseEvaluationQuotes", (string)null);
});
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationSupplier", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("ContactEmail")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("ContactName")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("ContactPhone")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("CreatedBy")
.HasColumnType("uniqueidentifier");
b.Property<string>("DisplayName")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("Note")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<int>("Order")
.HasColumnType("int");
b.Property<string>("PaymentTermText")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<Guid>("PurchaseEvaluationId")
.HasColumnType("uniqueidentifier");
b.Property<Guid>("SupplierId")
.HasColumnType("uniqueidentifier");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("SupplierId");
b.HasIndex("PurchaseEvaluationId", "SupplierId")
.IsUnique();
b.ToTable("PurchaseEvaluationSuppliers", (string)null);
});
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowDefinition", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<DateTime?>("ActivatedAt")
.HasColumnType("datetime2");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("CreatedBy")
.HasColumnType("uniqueidentifier");
b.Property<string>("Description")
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<int>("EvaluationType")
.HasColumnType("int");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uniqueidentifier");
b.Property<int>("Version")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("Code", "Version")
.IsUnique();
b.HasIndex("EvaluationType", "IsActive");
b.ToTable("PurchaseEvaluationWorkflowDefinitions", (string)null);
});
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowStep", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("CreatedBy")
.HasColumnType("uniqueidentifier");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<int>("Order")
.HasColumnType("int");
b.Property<int>("Phase")
.HasColumnType("int");
b.Property<Guid>("PurchaseEvaluationWorkflowDefinitionId")
.HasColumnType("uniqueidentifier");
b.Property<int?>("SlaDays")
.HasColumnType("int");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("PurchaseEvaluationWorkflowDefinitionId", "Order");
b.ToTable("PurchaseEvaluationWorkflowSteps", (string)null);
});
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowStepApprover", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<string>("AssignmentValue")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("CreatedBy")
.HasColumnType("uniqueidentifier");
b.Property<int>("Kind")
.HasColumnType("int");
b.Property<Guid>("PurchaseEvaluationWorkflowStepId")
.HasColumnType("uniqueidentifier");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.Property<Guid?>("UpdatedBy")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("PurchaseEvaluationWorkflowStepId");
b.ToTable("PurchaseEvaluationWorkflowStepApprovers", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b => modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
{ {
b.HasOne("SolutionErp.Domain.Identity.Role", null) b.HasOne("SolutionErp.Domain.Identity.Role", null)
@ -2135,6 +2721,106 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
.OnDelete(DeleteBehavior.Restrict); .OnDelete(DeleteBehavior.Restrict);
}); });
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationApproval", b =>
{
b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", "PurchaseEvaluation")
.WithMany("Approvals")
.HasForeignKey("PurchaseEvaluationId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("PurchaseEvaluation");
});
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationAttachment", b =>
{
b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", "PurchaseEvaluation")
.WithMany("Attachments")
.HasForeignKey("PurchaseEvaluationId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("PurchaseEvaluation");
});
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationChangelog", b =>
{
b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", "PurchaseEvaluation")
.WithMany("Changelogs")
.HasForeignKey("PurchaseEvaluationId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("PurchaseEvaluation");
});
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationDetail", b =>
{
b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", "PurchaseEvaluation")
.WithMany("Details")
.HasForeignKey("PurchaseEvaluationId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("PurchaseEvaluation");
});
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationQuote", b =>
{
b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationDetail", "Detail")
.WithMany("Quotes")
.HasForeignKey("PurchaseEvaluationDetailId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", null)
.WithMany("Quotes")
.HasForeignKey("PurchaseEvaluationId");
b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationSupplier", "Supplier")
.WithMany()
.HasForeignKey("PurchaseEvaluationSupplierId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Detail");
b.Navigation("Supplier");
});
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationSupplier", b =>
{
b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", "PurchaseEvaluation")
.WithMany("Suppliers")
.HasForeignKey("PurchaseEvaluationId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("PurchaseEvaluation");
});
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowStep", b =>
{
b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowDefinition", "Definition")
.WithMany("Steps")
.HasForeignKey("PurchaseEvaluationWorkflowDefinitionId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Definition");
});
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowStepApprover", b =>
{
b.HasOne("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowStep", "Step")
.WithMany("Approvers")
.HasForeignKey("PurchaseEvaluationWorkflowStepId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Step");
});
modelBuilder.Entity("SolutionErp.Domain.Contracts.Contract", b => modelBuilder.Entity("SolutionErp.Domain.Contracts.Contract", b =>
{ {
b.Navigation("Approvals"); b.Navigation("Approvals");
@ -2176,6 +2862,36 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
b.Navigation("Permissions"); b.Navigation("Permissions");
}); });
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", b =>
{
b.Navigation("Approvals");
b.Navigation("Attachments");
b.Navigation("Changelogs");
b.Navigation("Details");
b.Navigation("Quotes");
b.Navigation("Suppliers");
});
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationDetail", b =>
{
b.Navigation("Quotes");
});
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowDefinition", b =>
{
b.Navigation("Steps");
});
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluationWorkflowStep", b =>
{
b.Navigation("Approvers");
});
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }
} }