[CLAUDE] Domain+App+Api: Module Ngan sach (Budget) - 4 bang + workflow simple
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m11s

User request: 'Them cho tao 4 bang luu ve ngan sach: Header / Chi tiet
/ Quy trinh duyet / Lich su thay doi'.

Domain (5 file + 1 enum):
 - Budget (header) — Aggregate root, AuditableEntity. Field: MaNganSach,
   TenNganSach, Description, NamNganSach, ProjectId FK, DepartmentId?,
   DrafterUserId, Phase (BudgetPhase 5-state), TongNganSach (sum auto
   tu Details), SlaDeadline, SlaWarningSent.
 - BudgetDetail — flat row pattern (GroupCode/GroupName + Item +
   KhoiLuong/DonGia/ThanhTien). 18,4 precision KhoiLuong, 18,2 money.
 - BudgetApproval — workflow history (FromPhase/ToPhase/Decision/Comment)
 - BudgetChangelog — audit log unified (EntityType: Header/Detail/Workflow)
 - BudgetPhase enum 5 state: DangSoanThao(1) → ChoCCM(2) → ChoCEO(3) →
   DaDuyet(4) | TuChoi(99)
 - BudgetPolicy hardcoded (no versioned WF, simple default per user
   confirm 'tam thoi don gian'): Drafter/DeptManager → CCM → CEO/
   AuthorizedSigner. Reject path back to DangSoanThao.

Migration 14 AddBudgets:
 - 4 bang moi: Budgets + BudgetDetails + BudgetApprovals + BudgetChangelogs
 - Index: Phase+IsDeleted, ProjectId, NamNganSach, SlaDeadline,
   MaNganSach unique filtered. Cascade delete child.
 - +2 cot FK ngoai bang (per user 'lien ket ca 3'):
   * Contracts.BudgetId Guid? + index
   * PurchaseEvaluations.BudgetId Guid? + index
   Cho phep doi chieu chi phi HD/PE vs ngan sach goi thau.

Application CQRS (BudgetFeatures.cs ~340 line):
 - CreateBudget + UpdateBudgetDraft + TransitionBudget + ListBudgets
   (filter Phase/Project/Year + search + paging) + GetBudget bundle
   (Header + Details + Approvals + Workflow summary)
 - DeleteBudget (only DangSoanThao/TuChoi)
 - AddBudgetDetail + UpdateBudgetDetail + DeleteBudgetDetail (auto
   recompute TongNganSach = sum Details.ThanhTien)
 - ListBudgetChangelogs

Api: BudgetsController 11 endpoint REST /api/budgets:
 - GET /  /{id}  /{id}/changelogs
 - POST /  /{id}/transitions  /{id}/details
 - PUT /{id}  /{id}/details/{detailId}
 - DELETE /{id}  /{id}/details/{detailId}

DbContext + IApplicationDbContext: 4 DbSet new (Budgets/Details/
Approvals/Changelogs).

MenuKeys + DbInitializer: 4 menu key (Budgets root + Bg_List/Create/
Pending leaves) seed dau order=27 'Ngan sach' icon Wallet. Auto-grant
admin permission via SeedAdminPermissionsAsync (MenuKeys.All).

MaNganSach format don gian 'NS-YYYYMM-NNNN' Random.Shared (chua atomic
sequence - user said 'tam thoi chua co').

Workflow chua versioned, hardcode BudgetPolicy.Default. Tuong lai admin
config qua UI: them BudgetWorkflowDefinition tables tuong tu PE.
This commit is contained in:
pqhuy1987
2026-04-28 11:37:45 +07:00
parent 8097892d20
commit a05c57b081
21 changed files with 4632 additions and 0 deletions

View File

@ -0,0 +1,29 @@
using SolutionErp.Domain.Common;
namespace SolutionErp.Domain.Budgets;
// Aggregate root quản lý ngân sách. Gắn với Project (required), reference
// từ PurchaseEvaluation + Contract (cả 2 nullable FK Budget.Id).
//
// Workflow đơn giản 3-step: Drafter → CCM → CEO. Pattern hardcoded trong
// BudgetPolicy (chưa versioned, tương lai có thể thêm BudgetWorkflowDefinition
// nếu cần admin config).
public class Budget : AuditableEntity
{
public string? MaNganSach { get; set; } // Auto-gen NS-YYYYMM-XXXX
public string TenNganSach { get; set; } = string.Empty;
public string? Description { get; set; }
public int NamNganSach { get; set; } // Năm áp dụng (vd 2026)
public Guid ProjectId { get; set; } // FK Projects (required)
public Guid? DepartmentId { get; set; }
public Guid? DrafterUserId { get; set; }
public BudgetPhase Phase { get; set; } = BudgetPhase.DangSoanThao;
public decimal TongNganSach { get; set; } // Tổng = sum BudgetDetails.ThanhTien (computed)
public DateTime? SlaDeadline { get; set; }
public bool SlaWarningSent { get; set; }
public List<BudgetDetail> Details { get; set; } = new();
public List<BudgetApproval> Approvals { get; set; } = new();
public List<BudgetChangelog> Changelogs { get; set; } = new();
}

View File

@ -0,0 +1,17 @@
using SolutionErp.Domain.Common;
using SolutionErp.Domain.Contracts; // reuse ApprovalDecision
namespace SolutionErp.Domain.Budgets;
public class BudgetApproval : BaseEntity
{
public Guid BudgetId { get; set; }
public BudgetPhase FromPhase { get; set; }
public BudgetPhase 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 Budget? Budget { get; set; }
}

View File

@ -0,0 +1,28 @@
using SolutionErp.Domain.Common;
using SolutionErp.Domain.Contracts; // reuse ChangelogAction
namespace SolutionErp.Domain.Budgets;
// Audit log unified cho mọi thay đổi trên ngân sách.
public class BudgetChangelog : BaseEntity
{
public Guid BudgetId { get; set; }
public Budget? Budget { get; set; }
public BudgetEntityType EntityType { get; set; }
public Guid? EntityId { get; set; }
public ChangelogAction Action { get; set; }
public BudgetPhase? 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 BudgetEntityType
{
Header = 1,
Detail = 2,
Workflow = 3,
}

View File

@ -0,0 +1,22 @@
using SolutionErp.Domain.Common;
namespace SolutionErp.Domain.Budgets;
// Chi tiết ngân sách — pattern flat row giống PurchaseEvaluationDetail.
// Group A.I/A.II/... cho hạng mục cha, NoiDung cho item con.
public class BudgetDetail : BaseEntity
{
public Guid BudgetId { get; set; }
public string GroupCode { get; set; } = string.Empty; // "A.I", "A.II", ...
public string GroupName { get; set; } = string.Empty; // "Bê tông", "Phụ gia", ...
public string? ItemCode { get; set; }
public string NoiDung { get; set; } = string.Empty;
public string? DonViTinh { get; set; }
public decimal KhoiLuong { get; set; }
public decimal DonGia { get; set; }
public decimal ThanhTien { get; set; } // = KhoiLuong × DonGia (hoặc nhập tay)
public int Order { get; set; }
public string? GhiChu { get; set; }
public Budget? Budget { get; set; }
}

View File

@ -0,0 +1,14 @@
namespace SolutionErp.Domain.Budgets;
// State machine ngân sách — đơn giản 3 bước duyệt + 2 terminal.
// DangSoanThao → ChoCCM → ChoCEO → DaDuyet
// Bất kỳ phase duyệt → DangSoanThao (reject)
// DangSoanThao → TuChoi
public enum BudgetPhase
{
DangSoanThao = 1,
ChoCCM = 2,
ChoCEO = 3,
DaDuyet = 4,
TuChoi = 99,
}

View File

@ -0,0 +1,63 @@
using SolutionErp.Domain.Identity;
namespace SolutionErp.Domain.Budgets;
// Policy hardcoded đơn giản — chưa versioned (theo user "tạm thời simple
// default"). Tương lai nếu admin cần config qua UI: thêm BudgetWorkflow
// Definition tables tương tự PE workflow.
public sealed record BudgetPolicy(
string Name,
string Description,
IReadOnlyDictionary<(BudgetPhase From, BudgetPhase To), string[]> Transitions,
IReadOnlyDictionary<BudgetPhase, TimeSpan?> PhaseSla,
IReadOnlyList<BudgetPhase> ActivePhases)
{
public bool HasPhase(BudgetPhase phase) => ActivePhases.Contains(phase);
public bool IsTransitionAllowed(
BudgetPhase from, BudgetPhase to,
IReadOnlyList<string> actorRoles)
{
if (!Transitions.TryGetValue((from, to), out var roles)) return false;
return actorRoles.Any(r => roles.Contains(r));
}
public IReadOnlyList<BudgetPhase> NextPhasesFrom(BudgetPhase from) =>
Transitions.Keys.Where(k => k.From == from).Select(k => k.To).Distinct().ToList();
}
public static class BudgetPolicies
{
private static readonly Dictionary<BudgetPhase, TimeSpan?> DefaultSla = new()
{
[BudgetPhase.DangSoanThao] = TimeSpan.FromDays(5),
[BudgetPhase.ChoCCM] = TimeSpan.FromDays(3),
[BudgetPhase.ChoCEO] = TimeSpan.FromDays(2),
[BudgetPhase.DaDuyet] = null,
[BudgetPhase.TuChoi] = null,
};
public static readonly BudgetPolicy Default = new(
Name: "Default",
Description: "Quy trình ngân sách 3-step (Drafter → CCM → CEO).",
Transitions: new Dictionary<(BudgetPhase, BudgetPhase), string[]>
{
[(BudgetPhase.DangSoanThao, BudgetPhase.ChoCCM)] = [AppRoles.Drafter, AppRoles.DeptManager],
[(BudgetPhase.DangSoanThao, BudgetPhase.TuChoi)] = [AppRoles.Drafter, AppRoles.DeptManager],
[(BudgetPhase.ChoCCM, BudgetPhase.ChoCEO)] = [AppRoles.CostControl],
[(BudgetPhase.ChoCCM, BudgetPhase.DangSoanThao)] = [AppRoles.CostControl],
[(BudgetPhase.ChoCEO, BudgetPhase.DaDuyet)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
[(BudgetPhase.ChoCEO, BudgetPhase.DangSoanThao)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
},
PhaseSla: DefaultSla,
ActivePhases:
[
BudgetPhase.DangSoanThao,
BudgetPhase.ChoCCM,
BudgetPhase.ChoCEO,
BudgetPhase.DaDuyet,
BudgetPhase.TuChoi,
]);
}

View File

@ -23,6 +23,7 @@ public class Contract : AuditableEntity
public DateTime? SlaDeadline { get; set; } // Hết hạn phase hiện tại
public string? DraftData { get; set; } // JSON field values (render template)
public bool SlaWarningSent { get; set; } // Flag để không gửi warning 2 lần
public Guid? BudgetId { get; set; } // Reference Budget (Phase 7) — đối chiếu chi phí HĐ vs ngân sách
public List<ContractApproval> Approvals { get; set; } = new();
public List<ContractComment> Comments { get; set; } = new();

View File

@ -51,6 +51,15 @@ public static class MenuKeys
public const string PurchaseEvaluations = "PurchaseEvaluations"; // root group
public const string PeWorkflows = "PeWorkflows"; // workflow admin root
// ============================================================
// Module Ngân sách (Phase 7) — 4 bảng quản lý ngân sách dự án/gói thầu.
// 1 root + 3 leaf action (Danh sách / Thao tác / Duyệt).
// ============================================================
public const string Budgets = "Budgets";
public const string BudgetList = "Bg_List";
public const string BudgetCreate = "Bg_Create";
public const string BudgetPending = "Bg_Pending";
public static readonly string[] PurchaseEvaluationTypeCodes =
["DuyetNcc", "DuyetNccPhuongAn"];
@ -70,6 +79,7 @@ public static class MenuKeys
Catalogs, CatalogUnits, CatalogMaterials, CatalogServices, CatalogWorkItems,
Contracts, Forms, Reports,
PurchaseEvaluations,
Budgets, BudgetList, BudgetCreate, BudgetPending,
System, Users, Roles, Permissions, Workflows, PeWorkflows,
];

View File

@ -26,6 +26,7 @@ public class PurchaseEvaluation : AuditableEntity
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 Guid? BudgetId { get; set; } // FK Budget (Phase 7) — đối chiếu báo giá vs ngân sách gói thầu
public List<PurchaseEvaluationSupplier> Suppliers { get; set; } = new();
public List<PurchaseEvaluationDetail> Details { get; set; } = new();