[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:
@ -42,13 +42,35 @@ public static class MenuKeys
|
||||
// → mở /system/workflows/{typeCode} (filter theo type thay vì tab).
|
||||
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 =
|
||||
[
|
||||
Dashboard,
|
||||
Master, Suppliers, Projects, Departments,
|
||||
Catalogs, CatalogUnits, CatalogMaterials, CatalogServices, CatalogWorkItems,
|
||||
Contracts, Forms, Reports,
|
||||
System, Users, Roles, Permissions, Workflows,
|
||||
PurchaseEvaluations,
|
||||
System, Users, Roles, Permissions, Workflows, PeWorkflows,
|
||||
];
|
||||
|
||||
public static readonly string[] Actions = ["Read", "Create", "Update", "Delete"];
|
||||
|
||||
@ -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();
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
Reference in New Issue
Block a user