[CLAUDE] Domain+Infra: Migration 17 — manual budget fields cho PE + HĐ
Chunk 1/5 — DB schema + Domain layer cho fallback "user nhập số tiền ngân sách
tay khi chưa link Budget entity" (UAT request 2026-05-07). Áp cho cả PE + HĐ
(mirror logic per user Q3 chốt).
Migration 17 `AddManualBudgetFieldsToPeAndContract` — 4 ALTER:
- PurchaseEvaluations.BudgetManualName nvarchar(200) NULL
- PurchaseEvaluations.BudgetManualAmount decimal(18,2) NULL
- Contracts.BudgetManualName nvarchar(200) NULL
- Contracts.BudgetManualAmount decimal(18,2) NULL
Validation Q2: cả 2 cùng null OK (PE/HĐ chưa có ngân sách gì cả). KHÔNG 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ị/note).
Files:
~ Domain/PurchaseEvaluations/PurchaseEvaluation.cs — 2 property mới
~ Domain/Contracts/Contract.cs — 2 property mới
~ Infrastructure/Persistence/Configurations/PurchaseEvaluationConfiguration.cs
— HasMaxLength(200) + HasPrecision(18,2)
~ Infrastructure/Persistence/Configurations/ContractConfiguration.cs — same
+ Migration 17 .cs + .Designer.cs (3-file rule per ef-core-migration skill)
~ ApplicationDbContextModelSnapshot.cs (auto-overwrite)
Verify:
- dotnet ef migrations add → 3 file gen sạch (4 AddColumn Up + 4 DropColumn Down)
- dotnet ef database update → applied LocalDB OK
- dotnet test SolutionErp.slnx → 83 pass (54 Domain + 29 Infra) — không regression
Next: Chunk 2 App CQRS Create/Update commands + DTO + AutoMapper.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -25,6 +25,11 @@ public class Contract : AuditableEntity
|
|||||||
public bool SlaWarningSent { get; set; } // Flag để không gửi warning 2 lần
|
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 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.
|
||||||
|
public string? BudgetManualName { get; set; } // Tên tham chiếu
|
||||||
|
public decimal? BudgetManualAmount { get; set; } // Tổng số tiền nhập tay (đ)
|
||||||
|
|
||||||
// Smart reject (Phase 9 — Migration 16): Phase nguồn khi reject. Drafter
|
// 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
|
// sửa lại + trình lại → quay về RejectedFromPhase thay vì DangSoanThao
|
||||||
// tuần tự lại từ đầu. Null khi chưa từng reject hoặc đã trình lại xong.
|
// tuần tự lại từ đầu. Null khi chưa từng reject hoặc đã trình lại xong.
|
||||||
|
|||||||
@ -28,6 +28,14 @@ public class PurchaseEvaluation : AuditableEntity
|
|||||||
public Guid? ContractId { get; set; } // FK Contracts — set khi user gen HĐ từ phiếu
|
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 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 (đ)
|
||||||
|
|
||||||
// Smart reject (Phase 9 — Migration 16): Phase nguồn khi reject. Drafter
|
// 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ự.
|
// sửa lại + trình lại → quay về RejectedFromPhase thay vì đi tuần tự.
|
||||||
public PurchaseEvaluationPhase? RejectedFromPhase { get; set; }
|
public PurchaseEvaluationPhase? RejectedFromPhase { get; set; }
|
||||||
|
|||||||
@ -19,6 +19,8 @@ public class ContractConfiguration : IEntityTypeConfiguration<Contract>
|
|||||||
b.Property(x => x.TenHopDong).HasMaxLength(500);
|
b.Property(x => x.TenHopDong).HasMaxLength(500);
|
||||||
b.Property(x => x.NoiDung).HasMaxLength(2000);
|
b.Property(x => x.NoiDung).HasMaxLength(2000);
|
||||||
b.Property(x => x.DraftData).HasColumnType("nvarchar(max)");
|
b.Property(x => x.DraftData).HasColumnType("nvarchar(max)");
|
||||||
|
b.Property(x => x.BudgetManualName).HasMaxLength(200);
|
||||||
|
b.Property(x => x.BudgetManualAmount).HasPrecision(18, 2);
|
||||||
|
|
||||||
b.HasIndex(x => x.MaHopDong).IsUnique().HasFilter("[MaHopDong] IS NOT NULL");
|
b.HasIndex(x => x.MaHopDong).IsUnique().HasFilter("[MaHopDong] IS NOT NULL");
|
||||||
b.HasIndex(x => new { x.Phase, x.IsDeleted });
|
b.HasIndex(x => new { x.Phase, x.IsDeleted });
|
||||||
|
|||||||
@ -19,6 +19,8 @@ public class PurchaseEvaluationConfiguration : IEntityTypeConfiguration<Purchase
|
|||||||
b.Property(x => x.DiaDiem).HasMaxLength(500);
|
b.Property(x => x.DiaDiem).HasMaxLength(500);
|
||||||
b.Property(x => x.MoTa).HasMaxLength(2000);
|
b.Property(x => x.MoTa).HasMaxLength(2000);
|
||||||
b.Property(x => x.PaymentTerms).HasColumnType("nvarchar(max)");
|
b.Property(x => x.PaymentTerms).HasColumnType("nvarchar(max)");
|
||||||
|
b.Property(x => x.BudgetManualName).HasMaxLength(200);
|
||||||
|
b.Property(x => x.BudgetManualAmount).HasPrecision(18, 2);
|
||||||
|
|
||||||
b.HasIndex(x => x.MaPhieu).IsUnique().HasFilter("[MaPhieu] IS NOT NULL");
|
b.HasIndex(x => x.MaPhieu).IsUnique().HasFilter("[MaPhieu] IS NOT NULL");
|
||||||
b.HasIndex(x => new { x.Phase, x.IsDeleted });
|
b.HasIndex(x => new { x.Phase, x.IsDeleted });
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,64 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddManualBudgetFieldsToPeAndContract : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<decimal>(
|
||||||
|
name: "BudgetManualAmount",
|
||||||
|
table: "PurchaseEvaluations",
|
||||||
|
type: "decimal(18,2)",
|
||||||
|
precision: 18,
|
||||||
|
scale: 2,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "BudgetManualName",
|
||||||
|
table: "PurchaseEvaluations",
|
||||||
|
type: "nvarchar(200)",
|
||||||
|
maxLength: 200,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<decimal>(
|
||||||
|
name: "BudgetManualAmount",
|
||||||
|
table: "Contracts",
|
||||||
|
type: "decimal(18,2)",
|
||||||
|
precision: 18,
|
||||||
|
scale: 2,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "BudgetManualName",
|
||||||
|
table: "Contracts",
|
||||||
|
type: "nvarchar(200)",
|
||||||
|
maxLength: 200,
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "BudgetManualAmount",
|
||||||
|
table: "PurchaseEvaluations");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "BudgetManualName",
|
||||||
|
table: "PurchaseEvaluations");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "BudgetManualAmount",
|
||||||
|
table: "Contracts");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "BudgetManualName",
|
||||||
|
table: "Contracts");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -467,6 +467,14 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
|||||||
b.Property<Guid?>("BudgetId")
|
b.Property<Guid?>("BudgetId")
|
||||||
.HasColumnType("uniqueidentifier");
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<decimal?>("BudgetManualAmount")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<string>("BudgetManualName")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("nvarchar(200)");
|
||||||
|
|
||||||
b.Property<bool>("BypassProcurementAndCCM")
|
b.Property<bool>("BypassProcurementAndCCM")
|
||||||
.HasColumnType("bit");
|
.HasColumnType("bit");
|
||||||
|
|
||||||
@ -2328,6 +2336,14 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
|||||||
b.Property<Guid?>("BudgetId")
|
b.Property<Guid?>("BudgetId")
|
||||||
.HasColumnType("uniqueidentifier");
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<decimal?>("BudgetManualAmount")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
|
b.Property<string>("BudgetManualName")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("nvarchar(200)");
|
||||||
|
|
||||||
b.Property<Guid?>("ContractId")
|
b.Property<Guid?>("ContractId")
|
||||||
.HasColumnType("uniqueidentifier");
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user