[CLAUDE] PurchaseEvaluation: ngan sach goi thau theo Excel anh Kiet - bang tong hop 2 block + nhap theo role PRO/CCM + xoa module Budget cu (Mig 50)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m31s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m31s
- Mig 50 ReplaceBudgetModuleWithPeWorkItemBudgets: bang moi PeWorkItemBudgets (1 record/cap Du an x Hang muc, UNIQUE filtered [IsDeleted]=0) + drop 5 bang Budget cu + PE/Contracts drop BudgetId + backfill BudgetManualAmount->BudgetPeriodAmount TRUOC DropColumn (phieu UAT giu so) + DELETE menu/permission Bg_* IN-list children-first
- BE: PUT {id}/budget/pro (role Procurement) + {id}/budget/ccm (role CostControl, Adjustment cho phep AM) fail-closed Forbidden-truoc-side-effect + EnsureTrackedAsync race-safe (catch unique -> re-fetch winner, loi khac rethrow) + auto-create record khi tao phieu + budgetSummary DTO (luy ke trinh-truoc/chon-thau-truoc/de-xuat-ky-nay + full fallback du-tru-PRO + canEdit flags) + submit-guard (3) doi predicate BudgetPeriodAmount -> "chua nhap Ngan sach ky nay" + PATCH budget-adjust absolute-set 2 field moi + Contract GIU BudgetManual* (HD nhap tay khong doi) + ke thua HD map BudgetPeriodAmount
- FE x2 app SHA256 identical: bang "TONG HOP NGAN SACH TRINH KY" block A (full dam + ban hanh + V0 hieu chinh + du tru PRO + ghi chu, editable theo canEditPro/canEditCcm) + block B 9 dong cong thuc Excel (5=1+3, 6=2+4, 7=full-5, 8 tu nhap default 7, 9=4+8) + to mau vuot ngan sach #C00000 / am do / red-soft row8>row7 + "Chua chon" khi count=0 + banner phieu chua gan Hang muc + o "Ngan sach ky nay" o create/header + XOA pages/components/types budgets + routes + menuKeys + Layout staticMap 4-place
- Tests: +22 PeWorkItemBudgetTests (auto-create x3, ensure/race x2, authz matrix PRO x5 + CCM x3, budgetSummary aggregates x5, adjust x4) - 14 BudgetPolicyTests xoa theo module - 1 test via-BudgetId -> 263 PASS (45 Domain + 218 Infra, 0 fail)
- database-agent advise adopted: khong FK vat ly PE/Contracts->Budgets (DropColumn khong can DropForeignKey) + DropIndex truoc DropColumn (SQL 5074) + IN-list thay LIKE Bg_% (underscore wildcard + miss root) + khong Serializable wrap (nested-tx conflict codegen)
- Reviewer PASS-with-minor 0 blocker (verdict-first survived); 2 minor da sua truoc commit (comment adjustMut absolute-set + dead key budgetId); note: F4 approver-edit-budget UI entry tam drafter-only, BE van cho approver scope - cho UAT anh Kiet
- Scaffold-bug caught: EF tu sinh RenameColumn BudgetManualAmount->ExpectedRemainingAmount (SAI semantics) -> thay bang Add+UPDATE+Drop
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@ -1,34 +0,0 @@
|
||||
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; }
|
||||
|
||||
// Smart reject (Phase 9 — Migration 16): Phase nguồn khi reject. Drafter
|
||||
// sửa lại + trình lại → quay về RejectedFromPhase thay vì DangSoanThao.
|
||||
public BudgetPhase? RejectedFromPhase { get; set; }
|
||||
|
||||
public List<BudgetDetail> Details { get; set; } = new();
|
||||
public List<BudgetApproval> Approvals { get; set; } = new();
|
||||
public List<BudgetChangelog> Changelogs { get; set; } = new();
|
||||
public List<BudgetDepartmentApproval> DepartmentApprovals { get; set; } = new();
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
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; }
|
||||
}
|
||||
@ -1,28 +0,0 @@
|
||||
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,
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
using SolutionErp.Domain.Common;
|
||||
|
||||
namespace SolutionErp.Domain.Budgets;
|
||||
|
||||
// 2-stage department approval cho Budget workflow (Phase 9 — Migration 16).
|
||||
// Mirror schema ContractDepartmentApproval / PurchaseEvaluationDepartmentApproval.
|
||||
public class BudgetDepartmentApproval : AuditableEntity
|
||||
{
|
||||
public Guid BudgetId { get; set; }
|
||||
public int PhaseAtApproval { get; set; } // snapshot BudgetPhase int
|
||||
public Guid DepartmentId { get; set; }
|
||||
public ApprovalStage Stage { get; set; } // 1=Review (NV), 2=Confirm (TPB)
|
||||
public Guid ApproverUserId { get; set; }
|
||||
public string? ApproverRoleSnapshot { get; set; }
|
||||
public string? Comment { get; set; }
|
||||
public DateTime ApprovedAt { get; set; }
|
||||
public bool IsBypassed { get; set; }
|
||||
|
||||
public Budget? Budget { get; set; }
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
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; }
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
namespace SolutionErp.Domain.Budgets;
|
||||
|
||||
// State machine ngân sách — Session 17 spec mới (5 trạng thái mirror PE/HĐ):
|
||||
// DangSoanThao (Nháp) → ChoCCM (Drafter trình)
|
||||
// TraLai (Trả lại) → ChoCCM (Drafter sửa+gửi lại, chạy từ đầu)
|
||||
// ChoCCM/ChoCEO → next phase OR TraLai OR TuChoi
|
||||
// ChoCEO → DaDuyet (terminal)
|
||||
// DangSoanThao/TraLai → TuChoi (Drafter huỷ)
|
||||
public enum BudgetPhase
|
||||
{
|
||||
DangSoanThao = 1,
|
||||
ChoCCM = 2,
|
||||
ChoCEO = 3,
|
||||
DaDuyet = 4,
|
||||
TraLai = 98, // Phase riêng (không revert DangSoanThao)
|
||||
TuChoi = 99,
|
||||
}
|
||||
@ -1,71 +0,0 @@
|
||||
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.TraLai] = TimeSpan.FromDays(5),
|
||||
[BudgetPhase.ChoCCM] = TimeSpan.FromDays(3),
|
||||
[BudgetPhase.ChoCEO] = TimeSpan.FromDays(2),
|
||||
[BudgetPhase.DaDuyet] = null,
|
||||
[BudgetPhase.TuChoi] = null,
|
||||
};
|
||||
|
||||
// Session 17 spec: Reject = về TraLai (Phase riêng). Drafter từ TraLai
|
||||
// gửi lại = entry point thứ 2 (mirror DangSoanThao → ChoCCM).
|
||||
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.TraLai, BudgetPhase.ChoCCM)] = [AppRoles.Drafter, AppRoles.DeptManager],
|
||||
[(BudgetPhase.TraLai, BudgetPhase.TuChoi)] = [AppRoles.Drafter, AppRoles.DeptManager],
|
||||
|
||||
[(BudgetPhase.ChoCCM, BudgetPhase.ChoCEO)] = [AppRoles.CostControl],
|
||||
[(BudgetPhase.ChoCCM, BudgetPhase.TraLai)] = [AppRoles.CostControl],
|
||||
[(BudgetPhase.ChoCCM, BudgetPhase.TuChoi)] = [AppRoles.CostControl],
|
||||
|
||||
[(BudgetPhase.ChoCEO, BudgetPhase.DaDuyet)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
|
||||
[(BudgetPhase.ChoCEO, BudgetPhase.TraLai)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
|
||||
[(BudgetPhase.ChoCEO, BudgetPhase.TuChoi)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
|
||||
},
|
||||
PhaseSla: DefaultSla,
|
||||
ActivePhases:
|
||||
[
|
||||
BudgetPhase.DangSoanThao,
|
||||
BudgetPhase.TraLai,
|
||||
BudgetPhase.ChoCCM,
|
||||
BudgetPhase.ChoCEO,
|
||||
BudgetPhase.DaDuyet,
|
||||
BudgetPhase.TuChoi,
|
||||
]);
|
||||
}
|
||||
@ -24,10 +24,10 @@ 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
|
||||
|
||||
// Manual budget fields (Phase 9 — Migration 17 mirror PE): user nhập tay khi
|
||||
// KHÔNG link Budget entity. Cả 2 cùng null OK. KHÔNG XOR validate với BudgetId.
|
||||
// Manual budget fields (Phase 9 — Migration 17): user nhập tay tham chiếu
|
||||
// ngân sách. [S61 Mig 50] BudgetId (FK module Budget cũ) đã DROP cùng module —
|
||||
// 2 field manual GIỮ NGUYÊN (HĐ không đổi theo spec anh Kiệt S61).
|
||||
public string? BudgetManualName { get; set; } // Tên tham chiếu
|
||||
public decimal? BudgetManualAmount { get; set; } // Tổng số tiền nhập tay (đ)
|
||||
|
||||
|
||||
@ -67,14 +67,10 @@ public static class MenuKeys
|
||||
public const string ApprovalWorkflowDuyetNccV2 = "AwV2_DuyetNcc"; // leaf cho Duyệt NCC mới
|
||||
public const string ApprovalWorkflowDuyetNccPhuongAnV2 = "AwV2_DuyetNccPhuongAn"; // leaf cho Duyệt NCC và Giải pháp mới
|
||||
|
||||
// ============================================================
|
||||
// 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";
|
||||
// [S61 Mig 50] Module Ngân sách cũ (Budgets + Bg_List/Bg_Create/Bg_Pending)
|
||||
// đã XÓA — thay bằng "Ngân sách gói thầu" nhúng trong phiếu PE
|
||||
// (PeWorkItemBudgets per cặp Dự án × Hạng mục). Menu/Permission rows cũ
|
||||
// cleanup qua SQL trong migration ReplaceBudgetModuleWithPeWorkItemBudgets.
|
||||
|
||||
// ============================================================
|
||||
// Module Nhân sự (Phase 10.1 G-H1 — Mig 34 S33 2026-05-26).
|
||||
@ -154,7 +150,6 @@ public static class MenuKeys
|
||||
Catalogs, CatalogUnits, CatalogMaterials, CatalogServices, CatalogWorkItems,
|
||||
Contracts, Forms, Reports,
|
||||
PurchaseEvaluations,
|
||||
Budgets, BudgetList, BudgetCreate, BudgetPending,
|
||||
Hrm, HrmHoSo, // Mig 34 — Phase 10.1
|
||||
HrmConfig, HrmConfigLeaveTypes, HrmConfigHolidays, HrmConfigShifts, HrmConfigOtPolicies, // Mig 35 — Phase 10.2 G-H2
|
||||
HrmConfigVehicles, HrmConfigDrivers, // Mig 44 — Phase 11 P11-C
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
using SolutionErp.Domain.Common;
|
||||
|
||||
namespace SolutionErp.Domain.PurchaseEvaluations;
|
||||
|
||||
// [S61 2026-06-12] Ngân sách gói thầu theo cặp (Dự án, Hạng mục công việc) —
|
||||
// thay module Budget cũ (5 bảng, xóa cùng Mig 50). 1 record DUY NHẤT per cặp
|
||||
// (UNIQUE composite filtered [IsDeleted] = 0 — gotcha #57), mọi phiếu PE cùng
|
||||
// cặp dùng chung. Auto-create khi tạo phiếu PE đầu tiên của cặp.
|
||||
//
|
||||
// Loose-Guid convention PE (giống PE.ProjectId/WorkItemId/SelectedSupplierId):
|
||||
// KHÔNG FK vật lý, KHÔNG navigation property.
|
||||
//
|
||||
// Quyền nhập theo ROLE (anh Kiệt chốt S61):
|
||||
// - PRO (Procurement): ProEstimateAmount (dự trù lần đầu) + ProNote.
|
||||
// - CCM (CostControl): InitialAmount (Ban hành lần đầu) + AdjustmentAmount
|
||||
// (NS V0 hiệu chỉnh tăng/giảm — cho phép ÂM).
|
||||
//
|
||||
// "Ngân sách full gói thầu" KHÔNG lưu cột — BE compute:
|
||||
// full = (InitialAmount ?? 0) + (AdjustmentAmount ?? 0);
|
||||
// cả Initial + Adjustment đều null → fallback ProEstimateAmount ?? 0
|
||||
// với cờ fullIsEstimate=true (FE badge "dự trù PRO").
|
||||
public class PeWorkItemBudget : AuditableEntity
|
||||
{
|
||||
public Guid ProjectId { get; set; } // loose-Guid Projects.Id
|
||||
public Guid WorkItemId { get; set; } // loose-Guid WorkItems.Id
|
||||
|
||||
public decimal? ProEstimateAmount { get; set; } // PRO dự trù lần đầu (đ)
|
||||
public string? ProNote { get; set; } // "Ghi chú từ PRO"
|
||||
public decimal? InitialAmount { get; set; } // CCM "Ngân sách Ban hành lần đầu" (đ)
|
||||
public decimal? AdjustmentAmount { get; set; } // CCM "NS V0/hiệu chỉnh tăng giảm" (đ, cho phép ÂM)
|
||||
}
|
||||
@ -28,15 +28,16 @@ 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
|
||||
|
||||
// Manual budget fields (Phase 9 — Migration 17): user nhập tay khi KHÔNG link
|
||||
// Budget entity (vd ngân sách chưa approve hoặc gói nhỏ ko cần workflow ngân
|
||||
// sách riêng). Cả 2 cùng null OK (PE chưa có ngân sách gì cả). KHÔNG validate
|
||||
// XOR với BudgetId — tạm thời cho phép cả 2 cùng có (BE prefer BudgetId nếu
|
||||
// set vì có Phase=DaDuyet guarantee, manual chỉ là fallback hiển thị).
|
||||
public string? BudgetManualName { get; set; } // Tên tham chiếu vd "Tạm tính dự toán T11/2025"
|
||||
public decimal? BudgetManualAmount { get; set; } // Tổng số tiền nhập tay (đ)
|
||||
// [S61 Mig 50] Ngân sách PE theo Excel anh Kiệt — thay BudgetId/BudgetManual*
|
||||
// cũ (drop cùng Mig 50, data BudgetManualAmount migrate → BudgetPeriodAmount).
|
||||
// - BudgetPeriodAmount: "Ngân sách - kỳ này" (row 3) — NGƯỜI SOẠN nhập khi
|
||||
// soạn phiếu. Submit guard yêu cầu > 0.
|
||||
// - ExpectedRemainingAmount: "Giá trị thực hiện dự kiến còn lại" (row 8) —
|
||||
// tự nhập; null = FE default theo NS còn lại (row 7).
|
||||
// Ngân sách FULL gói thầu nằm ở PeWorkItemBudget (per cặp Project × WorkItem).
|
||||
public decimal? BudgetPeriodAmount { get; set; }
|
||||
public decimal? ExpectedRemainingAmount { get; set; }
|
||||
|
||||
// Smart reject (Phase 9 — Migration 16): Phase nguồn khi reject. Drafter
|
||||
// sửa lại + trình lại → quay về RejectedFromPhase thay vì đi tuần tự.
|
||||
|
||||
Reference in New Issue
Block a user