[CLAUDE] Domain+App+Infra+Admin: W1 KHKK — Mig 69 ContractSigningPlans 7 bang + policy KeHoachKyKet + seeder UPGRADE-if-exists + CodeGen KHKK + Designer type-10
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m40s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m40s
- Domain/ContractSigningPlans: 7 entity + 3 enum (Phase 1/2/3/98/99; DossierItem Kind/Status start=1); ApplicableType += ContractSigningPlan=10 (slot 10, append-only) - Mig 69 AddContractSigningPlans: 7 CreateTable / 20 CreateIndex / 0 ALTER; UNIQUE filtered (PlanId,SupplierId) WHERE IsDeleted=0 (#57); FK 6 con Cascade 1-hop + 2 Restrict, 0 multiple-cascade-path; database-agent review PASS 8/8 + DB11 - MenuKeys.All += KeHoachKyKet (policy KeHoachKyKet.* x4 sinh runtime; menu-row prod KHONG doi — 142 giu); STATUS canonical 54->55 / 216->220 - DbInitializer.SeedKeHoachKyKetAccessAsync: UPGRADE-if-exists (F-S1 — khong insert-NO-OP) + vế CanDelete (Drafter/Admin), 13/13 role CanCreate+CanUpdate, chay SAU revoke; revoker khong phu Khkk_* (reviewer Truc-1 PASS) - ContractSigningPlanCodeGenerator: KHKK/{YYYY}/{Seq:D3} (owner OK @S161) qua WorkflowAppCodeSequences dung chung, SERIALIZABLE - AWV2AdminFeatures: 3 site count/retained/orphan + nhanh ContractSigningPlanLevelOpinions (chong no FK 500 khi xoa Level); +label VN type-10 (F-7); +ep false 2 co early-finalize server-side cho type-10 (F-8 — containment tang BE, FE da ep o levelFlagsPayload) - fe-admin Designer: deep-link type-10 + an 2 checkbox early-finalize (anh phan "PHA VO" S155) — containment 2 lop render+payload - tests: +4 (566 total, 0 fail) — schema-invariant 7 bang + codegen format (fault-inject 2 chieu chung co rang) Reviewer: PASS_WITH_FLAGS 10 issue -> 8 fixed cung commit, 2 giu trang thai (F-1 acceptance đo-CO cho prod; F-4 ke thua y PE prod) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@ -92,6 +92,9 @@ internal static class AwLabels
|
||||
[ApprovalWorkflowApplicableType.DuyetNcc] = "Duyệt NCC",
|
||||
[ApprovalWorkflowApplicableType.DuyetNccPhuongAn] = "Duyệt NCC + Giải pháp",
|
||||
[ApprovalWorkflowApplicableType.Contract] = "Hợp đồng",
|
||||
// [W1 S161 — reviewer F-7] Thiếu entry = fallback ToString() tiếng Anh lên tiêu đề
|
||||
// Designer VÀ ghi vào cột Name DB (useState `Quy trình ${label}` phía FE).
|
||||
[ApprovalWorkflowApplicableType.ContractSigningPlan] = "Kế hoạch ký kết HĐ",
|
||||
};
|
||||
}
|
||||
|
||||
@ -370,8 +373,12 @@ public class CreateAwDefinitionCommandHandler(IApplicationDbContext db)
|
||||
AllowReturnToDrafter = l.AllowReturnToDrafter,
|
||||
AllowApproverEditDetails = l.AllowApproverEditDetails,
|
||||
AllowApproverEditBudget = l.AllowApproverEditBudget,
|
||||
AllowApproverSkipToFinal = l.AllowApproverSkipToFinal,
|
||||
AllowApproverFinalize = l.AllowApproverFinalize,
|
||||
// [W1 S161 — reviewer F-8, F-B3 containment TẦNG BE] type=10 KHKK: 2 đường
|
||||
// early-finalize anh phán "PHÁ VỠ" (S155), W3 không port code path ⇒ ép false
|
||||
// server-side (FE đã ép ở levelFlagsPayload — đây chặn curl bypass). PUT
|
||||
// sửa-tại-chỗ đã có echo-so-BẰNG chặn đổi cờ ⇒ chỉ cần guard ở CREATE.
|
||||
AllowApproverSkipToFinal = typeEnum != ApprovalWorkflowApplicableType.ContractSigningPlan && l.AllowApproverSkipToFinal,
|
||||
AllowApproverFinalize = typeEnum != ApprovalWorkflowApplicableType.ContractSigningPlan && l.AllowApproverFinalize,
|
||||
AllowApproverDelete = l.AllowApproverDelete, // [Mig 68 S155] F6
|
||||
}).ToList(),
|
||||
})
|
||||
@ -842,7 +849,9 @@ public class UpdateAwDefinitionCommandHandler(
|
||||
// được "quy trình vừa bị sửa lúc nào, ai sửa, vì sao".
|
||||
// ⚠️ KHAI GIỚI HẠN: repo CHƯA có bảng changelog cho quy trình duyệt, mà
|
||||
// đợt 1 bị chốt "no new table" ⇒ vết cho workflow thuộc 5 module Văn
|
||||
// phòng số (Proposal/Leave/Ot/Travel/Vehicle) hiện CHỈ có UpdatedAt/
|
||||
// phòng số (Proposal/Leave/Ot/Travel/Vehicle) + KHKK (W1 S161 — reviewer F-6b:
|
||||
// `ContractSigningPlanChangelogs` là changelog PHIẾU, không phải changelog QUY-TRÌNH;
|
||||
// wire nếu cần ở W2/W3) hiện CHỈ có UpdatedAt/
|
||||
// UpdatedBy ở (a). Bảng `ApprovalWorkflowChangelogs` = việc tách riêng.
|
||||
if (changes.Count > 0 &&
|
||||
def.ApplicableType is ApprovalWorkflowApplicableType.DuyetNcc
|
||||
@ -883,8 +892,8 @@ public class UpdateAwDefinitionCommandHandler(
|
||||
await tx.CommitAsync(ct);
|
||||
}
|
||||
|
||||
// Đếm chữ ký CÒN HIỆU LỰC trên các dòng Cấp — quét CẢ 7 bảng LevelOpinion đang
|
||||
// FK vào ApprovalWorkflowLevels (PE + HĐ + 5 module Văn phòng số). Chỉ soi PE là
|
||||
// Đếm chữ ký CÒN HIỆU LỰC trên các dòng Cấp — quét CẢ 8 bảng LevelOpinion đang
|
||||
// FK vào ApprovalWorkflowLevels (PE + HĐ + 5 module Văn phòng số + KHKK W1). Chỉ soi PE là
|
||||
// vẫn nổ FK 547 khi quy trình thuộc module khác (§F.3 blast radius ≥6 module).
|
||||
// 🔴 `!p.IsDeleted` viết TƯỜNG MINH: chỉ PE + Contract có global query filter,
|
||||
// 5 bảng Office KHÔNG có ⇒ dựa filter là đúng PE mà sai 5 module kia.
|
||||
@ -938,6 +947,16 @@ public class UpdateAwDefinitionCommandHandler(
|
||||
.GroupBy(o => o.ApprovalWorkflowLevelId)
|
||||
.Select(g => new LevelOpinionCount(g.Key, g.Count())).ToListAsync(ct));
|
||||
|
||||
// [W1 Mig 69 — S161] GĐ2 Kế hoạch ký kết HĐ (type=10). Site liệt-kê-thủ-công:
|
||||
// module mới có LevelOpinions PHẢI vào CẢ 3 danh sách trong file này
|
||||
// (count-sống · retained-ref · orphan-delete) — thiếu 1 = admin xoá Level nổ FK 500.
|
||||
// Tự kiểm: grep -n "LevelOpinions.AsNoTracking\|LevelOpinions$" file này.
|
||||
Merge(await db.ContractSigningPlanLevelOpinions.AsNoTracking()
|
||||
.Where(o => levelIds.Contains(o.ApprovalWorkflowLevelId) && !o.IsDeleted
|
||||
&& db.ContractSigningPlans.Any(p => p.Id == o.ContractSigningPlanId && !p.IsDeleted))
|
||||
.GroupBy(o => o.ApprovalWorkflowLevelId)
|
||||
.Select(g => new LevelOpinionCount(g.Key, g.Count())).ToListAsync(ct));
|
||||
|
||||
return acc;
|
||||
}
|
||||
|
||||
@ -948,7 +967,7 @@ public class UpdateAwDefinitionCommandHandler(
|
||||
// 🔴 `IgnoreQueryFilters()` LÀ BẮT BUỘC, không phải trang trí: PE và Contract có global
|
||||
// filter `!IsDeleted` (`PurchaseEvaluationConfiguration.cs:84`). Không tắt filter thì hồ
|
||||
// sơ xoá mềm biến mất khỏi phép JOIN ⇒ ta lại kết luận "mồ côi" ⇒ đúng cái sai đang chữa.
|
||||
// (5 bảng Văn phòng số KHÔNG có filter — gọi vẫn vô hại, giữ 7 khối cùng một hình dạng.)
|
||||
// (5 bảng Văn phòng số + KHKK-LevelOpinions KHÔNG có filter — gọi vẫn vô hại, giữ 8 khối cùng một hình dạng.)
|
||||
private async Task<Dictionary<Guid, List<RetainedOpinionRef>>> ListRetainedOpinionsAsync(
|
||||
List<Guid> levelIds, CancellationToken ct)
|
||||
{
|
||||
@ -1003,6 +1022,14 @@ public class UpdateAwDefinitionCommandHandler(
|
||||
.Select(x => new RetainedOpinionRef(x.o.ApprovalWorkflowLevelId, "Đơn xe", x.p.MaDonTu, x.p.IsDeleted))
|
||||
.ToListAsync(ct));
|
||||
|
||||
// [W1 Mig 69 — S161] GĐ2 Kế hoạch ký kết HĐ — mirror nhánh trên (site 2/3).
|
||||
rows.AddRange(await db.ContractSigningPlanLevelOpinions.AsNoTracking().IgnoreQueryFilters()
|
||||
.Where(o => levelIds.Contains(o.ApprovalWorkflowLevelId))
|
||||
.Join(db.ContractSigningPlans, o => o.ContractSigningPlanId, p => p.Id, (o, p) => new { o, p })
|
||||
.Where(x => x.o.IsDeleted || x.p.IsDeleted)
|
||||
.Select(x => new RetainedOpinionRef(x.o.ApprovalWorkflowLevelId, "Kế hoạch ký kết HĐ", x.p.MaKeHoach, x.p.IsDeleted))
|
||||
.ToListAsync(ct));
|
||||
|
||||
return rows.GroupBy(r => r.LevelId).ToDictionary(g => g.Key, g => g.ToList());
|
||||
}
|
||||
|
||||
@ -1061,6 +1088,11 @@ public class UpdateAwDefinitionCommandHandler(
|
||||
.Where(o => levelIds.Contains(o.ApprovalWorkflowLevelId)
|
||||
&& !db.VehicleBookings.IgnoreQueryFilters().Any(p => p.Id == o.VehicleBookingId))
|
||||
.ExecuteDeleteAsync(ct);
|
||||
// [W1 Mig 69 — S161] GĐ2 Kế hoạch ký kết HĐ — mirror nhánh trên (site 3/3).
|
||||
n += await db.ContractSigningPlanLevelOpinions
|
||||
.Where(o => levelIds.Contains(o.ApprovalWorkflowLevelId)
|
||||
&& !db.ContractSigningPlans.IgnoreQueryFilters().Any(p => p.Id == o.ContractSigningPlanId))
|
||||
.ExecuteDeleteAsync(ct);
|
||||
return n;
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SolutionErp.Domain.ApprovalWorkflowsV2;
|
||||
using SolutionErp.Domain.ContractSigningPlans;
|
||||
using SolutionErp.Domain.Contracts;
|
||||
using SolutionErp.Domain.Contracts.Details;
|
||||
using SolutionErp.Domain.Forms;
|
||||
@ -141,5 +142,17 @@ public interface IApplicationDbContext
|
||||
// Phase 11 P11-B Wave 1 (Mig 42) — Số dư phép theo năm (NV × LoạiPhép × Năm).
|
||||
DbSet<LeaveBalance> LeaveBalances { get; }
|
||||
|
||||
// [W1 Mig 69 — S161] GĐ2 "Kế hoạch ký kết HĐ" (ContractSigningPlan) — 7 bảng.
|
||||
// Workflow V2 ApplicableType = ContractSigningPlan = 10 (`ApprovalWorkflow.cs:66`).
|
||||
// Mã phiếu "KHKK/{YYYY}/{Seq:D3}" atomic qua `WorkflowAppCodeSequences` DÙNG CHUNG (:136),
|
||||
// KHÔNG bảng sequence riêng.
|
||||
DbSet<ContractSigningPlan> ContractSigningPlans { get; }
|
||||
DbSet<ContractSigningPlanLine> ContractSigningPlanLines { get; }
|
||||
DbSet<ContractSigningPlanDossierItem> ContractSigningPlanDossierItems { get; }
|
||||
DbSet<ContractSigningPlanLevelOpinion> ContractSigningPlanLevelOpinions { get; }
|
||||
DbSet<ContractSigningPlanAttachment> ContractSigningPlanAttachments { get; }
|
||||
DbSet<ContractSigningPlanChangelog> ContractSigningPlanChangelogs { get; }
|
||||
DbSet<ContractSigningPlanApproval> ContractSigningPlanApprovals { get; }
|
||||
|
||||
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
namespace SolutionErp.Application.ContractSigningPlans.Services;
|
||||
|
||||
// [W1 Mig 69 — S161 2026-07-29] Atomic codegen mã phiếu "Kế hoạch ký kết HĐ" (GĐ2)
|
||||
// — mirror IPurchaseEvaluationCodeGenerator.
|
||||
// Format anh chốt @S161: KHKK/{YYYY}/{Seq:D3} (VD: KHKK/2026/001, KHKK/2026/002).
|
||||
// - Sequence per-năm qua bảng WorkflowAppCodeSequences DÙNG CHUNG (per-prefix row,
|
||||
// IApplicationDbContext) — KHÔNG bảng sequence riêng (spec W1 §②-9, #8-CẮT spec cũ).
|
||||
// - Seq reset đầu năm tự nhiên (key Prefix "KHKK/{YYYY}" mới).
|
||||
// Caller: W2 Submit (gen lúc trình duyệt, KHÔNG lúc tạo nháp — mirror quyết spec cũ).
|
||||
public interface IContractSigningPlanCodeGenerator
|
||||
{
|
||||
// Gen mã atomic — transaction SERIALIZABLE chống race (mirror PE/Contract codegen).
|
||||
Task<string> GenerateAsync(CancellationToken ct = default);
|
||||
}
|
||||
@ -64,6 +64,10 @@ public enum ApprovalWorkflowApplicableType
|
||||
VehicleBooking = 7, // G-O5 — Đặt xe công
|
||||
ItTicket = 8, // G-O6 — Ticket CNTT
|
||||
TravelRequest = 9, // G-O4 — Đơn công tác (Travel) — Phase 11 P11-A
|
||||
|
||||
// [W1 Mig 69 — S161 2026-07-29] GĐ2 "Kế hoạch ký kết HĐ" (ContractSigningPlan).
|
||||
// Append-only: slot 10 re-verify TRỐNG ngay trước khi viết (enum trước dừng ở 9).
|
||||
ContractSigningPlan = 10, // GĐ2 — Kế hoạch ký kết HĐ (NCC/Thầu phụ)
|
||||
}
|
||||
|
||||
// Bước = Phòng. 1 quy trình có nhiều bước theo Order.
|
||||
|
||||
@ -0,0 +1,48 @@
|
||||
using SolutionErp.Domain.Common;
|
||||
|
||||
namespace SolutionErp.Domain.ContractSigningPlans;
|
||||
|
||||
// [W1 Mig 69 — S161 2026-07-29] Aggregate root "Kế hoạch ký kết HĐ" (GĐ2, tờ FO-002.01).
|
||||
// Nối tiếp phiếu Duyệt NCC (PE) đã DaDuyet → chốt giá per-NCC trước khi sinh HĐ (GĐ3).
|
||||
//
|
||||
// Cookie-cutter mirror PurchaseEvaluation (con-trỏ ĐÔI StepIndex + LevelOrder cho
|
||||
// workflow V2 OR-of-N) — KHÔNG mirror Proposal (Proposal chỉ có LevelOrder, flatten
|
||||
// SelectMany làm OR-of-N thành AND, xem spec §2.1).
|
||||
//
|
||||
// Convention loose-Guid: PurchaseEvaluationId / ProjectId / DepartmentId / DrafterUserId
|
||||
// KHÔNG FK vật lý (mirror PE `PurchaseEvaluation.cs:15-18` + Mig 49). FK vật lý CHỈ:
|
||||
// nội bộ module (Lines/DossierItems/... Cascade) + ApprovalWorkflowId → bảng V2 dùng chung.
|
||||
public class ContractSigningPlan : AuditableEntity
|
||||
{
|
||||
public string? MaKeHoach { get; set; } // "KHKK/2026/001" — gen atomic lúc Submit (W2)
|
||||
|
||||
public Guid PurchaseEvaluationId { get; set; } // loose-Guid + IX — phiếu PE nguồn (DaDuyet)
|
||||
public Guid ProjectId { get; set; } // denorm để lọc list (loose-Guid, mirror PE)
|
||||
public Guid? DepartmentId { get; set; } // [C5 review-schema] phòng của phiếu — vế `DeptManager`
|
||||
// của guard trình so với cột NÀY (Step.DepartmentId chỉ là
|
||||
// hint của quy trình, KHÔNG thay được phòng của phiếu).
|
||||
public Guid DrafterUserId { get; set; } // BCH soạn kế hoạch
|
||||
|
||||
public ContractSigningPlanPhase Phase { get; set; } = ContractSigningPlanPhase.DangSoanThao;
|
||||
|
||||
// Workflow V2 pin (ApplicableType = ContractSigningPlan = 10). Con-trỏ ĐÔI mirror PE:
|
||||
// StepIndex 0-based (Bước) + LevelOrder 1-based (Cấp trong Bước). Null khi terminal.
|
||||
public Guid? ApprovalWorkflowId { get; set; }
|
||||
public int? CurrentWorkflowStepIndex { get; set; }
|
||||
public int? CurrentApprovalLevelOrder { get; set; }
|
||||
|
||||
// 🔸 SLA = FE hiển thị THAM KHẢO (O-2 owner chốt). KHÔNG đăng ký SlaExpiryJob,
|
||||
// KHÔNG có cờ SlaWarningSent — cố ý khai rõ để cột này không rơi vào lớp
|
||||
// "cột có mà 0 ai đọc"; người đọc nó là FE, không phải job.
|
||||
public DateTime? SlaDeadline { get; set; }
|
||||
|
||||
public string? HoSoLink { get; set; } // hyperlink thư mục hồ sơ NAS (mirror PE HoSoLink)
|
||||
public string? GhiChu { get; set; }
|
||||
|
||||
public List<ContractSigningPlanLine> Lines { get; set; } = new();
|
||||
public List<ContractSigningPlanDossierItem> DossierItems { get; set; } = new();
|
||||
public List<ContractSigningPlanAttachment> Attachments { get; set; } = new();
|
||||
public List<ContractSigningPlanChangelog> Changelogs { get; set; } = new();
|
||||
public List<ContractSigningPlanApproval> Approvals { get; set; } = new();
|
||||
public List<ContractSigningPlanLevelOpinion> LevelOpinions { get; set; } = new();
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
using SolutionErp.Domain.Common;
|
||||
using SolutionErp.Domain.Contracts; // reuse ApprovalDecision enum
|
||||
|
||||
namespace SolutionErp.Domain.ContractSigningPlans;
|
||||
|
||||
// [W1 Mig 69 — S161] Lịch sử transition (1 row / lần chuyển phase).
|
||||
// 🔴 THÊM sau review (lens-schema H1): service được lệnh copy CÓ ghi vào bảng này
|
||||
// (`ContractWorkflowService.cs:151` `db.ContractApprovals.Add(...)`), twin PE
|
||||
// `PurchaseEvaluation.cs:137` + index (PeId, ApprovedAt). Thiếu ⇒ đoạn copy trỏ
|
||||
// bảng KHÔNG TỒN TẠI.
|
||||
//
|
||||
// BaseEntity (append-only, KHÔNG soft-delete) — mirror PurchaseEvaluationApproval.
|
||||
//
|
||||
// `ApprovalWorkflowLevelId` = loose-Guid CỐ Ý KHÔNG FK vật lý: nếu để FK Restrict thì
|
||||
// admin xoá Level sẽ nổ 500 vì lịch sử duyệt, mà 3 site dọn dẹp trong
|
||||
// ApprovalWorkflowV2AdminFeatures chỉ biết bảng *LevelOpinions (không biết *Approvals).
|
||||
public class ContractSigningPlanApproval : BaseEntity
|
||||
{
|
||||
public Guid ContractSigningPlanId { get; set; }
|
||||
|
||||
public ContractSigningPlanPhase FromPhase { get; set; }
|
||||
public ContractSigningPlanPhase ToPhase { get; set; }
|
||||
public ApprovalDecision Decision { get; set; }
|
||||
public string? Comment { get; set; }
|
||||
|
||||
public Guid? ApprovalWorkflowLevelId { get; set; } // Cấp đã ký (loose-Guid, no FK)
|
||||
public Guid? ApprovedByUserId { get; set; } // null = system/SLA auto
|
||||
public DateTime ApprovedAt { get; set; }
|
||||
|
||||
public ContractSigningPlan? ContractSigningPlan { get; set; }
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
using SolutionErp.Domain.Common;
|
||||
|
||||
namespace SolutionErp.Domain.ContractSigningPlans;
|
||||
|
||||
// 🔴 Bắt đầu từ 1 (0 falsy JS); `Other = 99` khuôn PurchaseEvaluationAttachmentPurpose.
|
||||
// Append-only khi cần thêm mục đích mới.
|
||||
public enum ContractSigningPlanAttachmentPurpose
|
||||
{
|
||||
DossierScan = 1, // Bản scan căn cứ (mẫu vật liệu / shopdrawing / kết quả TVGS)
|
||||
PlanDocument = 2, // Bản kế hoạch ký kết đã ký/xuất
|
||||
Other = 99,
|
||||
}
|
||||
|
||||
// [W1 Mig 69 — S161] File đính kèm phiếu Kế hoạch ký kết.
|
||||
// FK Cascade Plan (1 đường cascade DUY NHẤT). `ContractSigningPlanDossierItemId`
|
||||
// optional = loose-Guid + index, CỐ Ý KHÔNG FK vật lý — khuôn
|
||||
// `PurchaseEvaluationAttachment.PurchaseEvaluationSupplierId` (config chỉ HasIndex).
|
||||
// Lý do kỹ thuật: Plan→DossierItem→Attachment + Plan→Attachment cùng Cascade sẽ là
|
||||
// MULTIPLE CASCADE PATHS ⇒ SQL Server từ chối tạo bảng.
|
||||
// Khuôn ProposalAttachment.cs + PurchaseEvaluationAttachment.cs.
|
||||
public class ContractSigningPlanAttachment : AuditableEntity
|
||||
{
|
||||
public Guid ContractSigningPlanId { get; set; }
|
||||
public Guid? ContractSigningPlanDossierItemId { get; set; }
|
||||
|
||||
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 ContractSigningPlanAttachmentPurpose Purpose { get; set; } = ContractSigningPlanAttachmentPurpose.Other;
|
||||
public string? Note { get; set; }
|
||||
|
||||
public ContractSigningPlan? ContractSigningPlan { get; set; }
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
using SolutionErp.Domain.Common;
|
||||
using SolutionErp.Domain.Contracts; // reuse ChangelogAction enum
|
||||
|
||||
namespace SolutionErp.Domain.ContractSigningPlans;
|
||||
|
||||
// [W1 Mig 69 — S161] Phạm vi thay đổi được ghi nhật ký. Bắt đầu từ 1 (0 falsy JS).
|
||||
public enum ContractSigningPlanEntityType
|
||||
{
|
||||
Header = 1,
|
||||
Line = 2,
|
||||
DossierItem = 3,
|
||||
Workflow = 4,
|
||||
Attachment = 5,
|
||||
}
|
||||
|
||||
// [W1 Mig 69 — S161] Nhật ký thay đổi phiếu Kế hoạch ký kết — CÓ TỪ NGÀY 1
|
||||
// (tiền lệ PE Mig 60/61: thiếu changelog thì không backfill lại được lịch sử).
|
||||
//
|
||||
// 🔴 4 cột EntityType / EntityId / UserName / FieldChangesJson là BẮT BUỘC (review
|
||||
// lens-schema C1): `LogTransitionAsync` khuôn `ContractWorkflowService.cs:405` gọi
|
||||
// `changelog.LogWorkflowTransitionAsync` cần đủ chữ ký này, thiếu ⇒ đoạn copy KHÔNG
|
||||
// BIÊN DỊCH + mất index twin (PlanId, EntityType).
|
||||
//
|
||||
// BaseEntity (KHÔNG soft-delete) — append-only audit, mirror PurchaseEvaluationChangelog.
|
||||
public class ContractSigningPlanChangelog : BaseEntity
|
||||
{
|
||||
public Guid ContractSigningPlanId { get; set; }
|
||||
|
||||
public ContractSigningPlanEntityType EntityType { get; set; }
|
||||
public Guid? EntityId { get; set; }
|
||||
public ChangelogAction Action { get; set; }
|
||||
public ContractSigningPlanPhase? 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 ContractSigningPlan? ContractSigningPlan { get; set; }
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
using SolutionErp.Domain.Common;
|
||||
|
||||
namespace SolutionErp.Domain.ContractSigningPlans;
|
||||
|
||||
// [W1 Mig 69 — S161] Loại căn cứ (mục b.8-9 tờ FO-002.01).
|
||||
// 🔴 Bắt đầu từ 1 — 0 falsy trong JS. `Khac = 99` theo khuôn `ItTicketCategory.Other = 99`.
|
||||
public enum DossierItemKind
|
||||
{
|
||||
MauVatLieu = 1, // Mẫu vật liệu trình TVGS
|
||||
Shopdrawing = 2, // Bản vẽ shopdrawing
|
||||
Khac = 99,
|
||||
}
|
||||
|
||||
// [W1 Mig 69 — S161] Trạng thái 1 căn cứ.
|
||||
// 🔴 ChuaNop = 1 (KHÔNG 0): mọi enum repo bắt đầu ở 1 (`Office/Enums.cs:9,19,31,41,50,58`;
|
||||
// `ApprovalWorkflow.cs:55`) và 0 là falsy JS ⇒ `status ? A : B` / `?status=0` rơi im lặng ở FE.
|
||||
public enum DossierItemStatus
|
||||
{
|
||||
ChuaNop = 1,
|
||||
DaNop = 2,
|
||||
TvgsDuyet = 3,
|
||||
TvgsBac = 4,
|
||||
}
|
||||
|
||||
// [W1 Mig 69 — S161] Danh mục căn cứ kèm kế hoạch ký kết — INPUT của con số đang duyệt.
|
||||
// CRUD chỉ mở ở phase {DangSoanThao, TraLai} (W2).
|
||||
public class ContractSigningPlanDossierItem : AuditableEntity
|
||||
{
|
||||
public Guid ContractSigningPlanId { get; set; }
|
||||
|
||||
public DossierItemKind Kind { get; set; } = DossierItemKind.Khac;
|
||||
public Guid? SupplierId { get; set; } // loose-Guid — căn cứ gắn NCC nào (null = chung)
|
||||
public string Name { get; set; } = string.Empty; // "Mẫu thép D10 Hoà Phát"
|
||||
public DossierItemStatus Status { get; set; } = DossierItemStatus.ChuaNop;
|
||||
|
||||
public string? TvgsName { get; set; } // tên TVGS xử lý
|
||||
public DateTime? TvgsResultAt { get; set; } // ngày TVGS duyệt/bác
|
||||
public string? Note { get; set; }
|
||||
|
||||
public ContractSigningPlan? ContractSigningPlan { get; set; }
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
using SolutionErp.Domain.ApprovalWorkflowsV2;
|
||||
using SolutionErp.Domain.Common;
|
||||
|
||||
namespace SolutionErp.Domain.ContractSigningPlans;
|
||||
|
||||
// [W1 Mig 69 — S161] Ý kiến cấp duyệt V2 cho Kế hoạch ký kết.
|
||||
// Cookie-cutter mirror ContractLevelOpinion (Mig 33) / PurchaseEvaluationLevelOpinion (Mig 26)
|
||||
// — twin thứ 8 của họ `*LevelOpinion`.
|
||||
//
|
||||
// Mỗi row = 1 (Plan × ApprovalWorkflowLevel). Service ApproveV2Async UPSERT sau khi
|
||||
// duyệt xong Cấp hiện tại (latest-write-wins). Reject (TraLai/TuChoi) KHÔNG sync.
|
||||
//
|
||||
// UNIQUE composite (PlanId, LevelId) — 1 row/level/phiếu.
|
||||
// FK Cascade Plan (xoá phiếu → wipe) + Restrict Level (admin xoá Level bị chặn khi
|
||||
// còn chữ ký — bảo vệ data; 3 site liệt-kê-thủ-công trong ApprovalWorkflowV2AdminFeatures
|
||||
// PHẢI biết bảng này, thiếu ⇒ admin xoá Level có chữ ký nổ FK 500).
|
||||
//
|
||||
// 🔴 CỐ Ý KHÔNG có HasQueryFilter (parity 7 twin còn lại): site "dọn chữ ký MỒ CÔI"
|
||||
// (`ApprovalWorkflowV2AdminFeatures.cs` ExecuteDeleteAsync) phải THẤY được row đã
|
||||
// soft-delete, nếu bị filter che thì row mồ côi sống sót và FK Restrict lại nổ 500.
|
||||
public class ContractSigningPlanLevelOpinion : AuditableEntity
|
||||
{
|
||||
public Guid ContractSigningPlanId { get; set; }
|
||||
public Guid ApprovalWorkflowLevelId { get; set; }
|
||||
|
||||
public string? Comment { get; set; } // max 2000 hoặc placeholder
|
||||
public DateTime SignedAt { get; set; }
|
||||
public Guid SignedByUserId { get; set; } // người ký THẬT (có thể Admin duyệt thay)
|
||||
public string SignedByFullName { get; set; } = string.Empty; // snapshot denorm (KHÔNG nav sang User)
|
||||
|
||||
public ContractSigningPlan? ContractSigningPlan { get; set; }
|
||||
public ApprovalWorkflowLevel? Level { get; set; }
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
using SolutionErp.Domain.Common;
|
||||
|
||||
namespace SolutionErp.Domain.ContractSigningPlans;
|
||||
|
||||
// [W1 Mig 69 — S161] Dòng giá per-NCC-trúng-thầu của Kế hoạch ký kết.
|
||||
// 1 row = 1 NCC (winner của PE nguồn). HĐ (GĐ3) sinh PER-NCC ⇒ dòng này là chỗ tự
|
||||
// nhiên để giữ đường quay ngược Kế-hoạch → HĐ (`ContractId`, twin `PurchaseEvaluation.ContractId`).
|
||||
//
|
||||
// AuditableEntity (soft delete) — BẮT BUỘC vì UNIQUE (PlanId, SupplierId) phải là
|
||||
// filtered `WHERE IsDeleted = 0` (gotcha #57: xoá mềm 1 NCC rồi thêm lại NCC đó
|
||||
// sẽ đụng unique nếu index không filter).
|
||||
public class ContractSigningPlanLine : AuditableEntity
|
||||
{
|
||||
public Guid ContractSigningPlanId { get; set; }
|
||||
public Guid SupplierId { get; set; } // loose-Guid (KHÔNG FK vật lý sang Suppliers)
|
||||
|
||||
// SNAPSHOT tổng báo giá của NCC này trên phiếu PE (SUM Quote.IsSelected) tại thời
|
||||
// điểm TẠO kế hoạch — chụp 1 lần, KHÔNG đọc live (PE sửa sau không được làm trôi
|
||||
// số của kế hoạch đã lập; cùng triết lý freeze ngân sách Mig 67).
|
||||
public decimal PeReferenceAmount { get; set; }
|
||||
|
||||
public decimal ProposedAmount { get; set; } // BCH đề xuất
|
||||
public decimal? ApprovedAmount { get; set; } // CHỈ ghi tại choke-point finalize (W3)
|
||||
|
||||
public Guid? ContractId { get; set; } // [C6 review-schema] HĐ sinh ra từ dòng này (W5)
|
||||
public string? Note { get; set; }
|
||||
|
||||
public ContractSigningPlan? ContractSigningPlan { get; set; }
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
namespace SolutionErp.Domain.ContractSigningPlans;
|
||||
|
||||
// [W1 Mig 69 — S161 2026-07-29] Trạng thái phiếu "Kế hoạch ký kết HĐ" (GĐ2).
|
||||
//
|
||||
// Giá trị SẠCH, KHÔNG copy 7/98 của PurchaseEvaluationPhase (sẹo lịch sử enum PE —
|
||||
// phiếu mới không nợ data cũ). Giữ 98/99 cho TraLai/TuChoi để quen mắt FE badge.
|
||||
// 🔴 Bắt đầu từ 1 — 0 là falsy trong JS (`status ? A : B`, `?phase=0` rơi im lặng ở FE).
|
||||
public enum ContractSigningPlanPhase
|
||||
{
|
||||
DangSoanThao = 1, // Nháp — Drafter (BCH) soạn
|
||||
ChoDuyet = 2, // Đã gửi duyệt — chạy workflow V2 (ApplicableType=10)
|
||||
DaDuyet = 3, // Terminal — chốt giá (ApprovedAmount)
|
||||
TraLai = 98, // Approver trả lại cho người soạn (phase RIÊNG, mirror PE)
|
||||
TuChoi = 99, // Terminal — từ chối
|
||||
}
|
||||
@ -35,9 +35,10 @@ public static class MenuKeys
|
||||
["ThauPhu", "GiaoKhoan", "NhaCungCap", "DichVu", "MuaBan", "NguyenTacNcc", "NguyenTacDv"];
|
||||
|
||||
// [S159 2026-07-29] 2 root PLACEHOLDER giai đoạn 2/4 (owner: menu hiển thị đúng
|
||||
// 4 giai đoạn như tab strip). CỐ Ý NGOÀI `All` — chưa có module ⇒ không sinh
|
||||
// policy per-action (giữ canonical Menu keys/Policies không drift); FE trỏ về
|
||||
// /dashboard. Wave-1 GĐ2 (spec KHKK) phải TÁI DÙNG/thay root `KeHoachKyKet` này.
|
||||
// 4 giai đoạn như tab strip). [S161 W1 — reviewer F-3] TÁCH TRẠNG THÁI 2 key:
|
||||
// - `KeHoachKyKet` ĐÃ VÀO `All` @S161 (W1 KHKK) ⇒ 4 policy KeHoachKyKet.* sinh runtime.
|
||||
// - `HopDongCung` VẪN NGOÀI `All` — chưa có module ⇒ không sinh policy (GĐ4 = 1-mốc
|
||||
// upload trên Contract, có thể không bao giờ cần policy riêng).
|
||||
public const string KeHoachKyKet = "KeHoachKyKet";
|
||||
public const string HopDongCung = "HopDongCung";
|
||||
|
||||
@ -176,6 +177,7 @@ public static class MenuKeys
|
||||
OffDonTu, OffDonTuLeave, OffDonTuOt, OffDonTuTravel, // Phase 10.3 G-O4 — Đơn từ
|
||||
OffDatXe, OffItTicket, OffChamCong, OffAttendanceReport, HrmDashboard, // Phase 10.3-10.4 — G-O5/G-O6/G-P1/G-H3 + P11-E report
|
||||
Personal, // [S57] Cá nhân (Puro grouping — Chấm công re-parent)
|
||||
KeHoachKyKet, // [W1 Mig 69 — S161] GĐ2 KHKK: const :41 vào All ⇒ sinh policy KeHoachKyKet.{Read,Create,Update,Delete}
|
||||
System, Users, Roles, Permissions, MenuVisibility, Workflows, PeWorkflows,
|
||||
ApprovalWorkflowsV2, ApprovalWorkflowDuyetNccV2, ApprovalWorkflowDuyetNccPhuongAnV2, // Mig 22
|
||||
];
|
||||
|
||||
@ -4,6 +4,7 @@ using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SolutionErp.Application.Common.Interfaces;
|
||||
using SolutionErp.Application.Contracts.Services;
|
||||
using SolutionErp.Application.ContractSigningPlans.Services;
|
||||
using SolutionErp.Application.Forms.Services;
|
||||
using SolutionErp.Application.Hrm.Services;
|
||||
using SolutionErp.Application.Master.Suppliers.Import;
|
||||
@ -37,6 +38,8 @@ public static class DependencyInjection
|
||||
services.AddScoped<IContractWorkflowService, ContractWorkflowService>();
|
||||
services.AddScoped<IPurchaseEvaluationWorkflowService, PurchaseEvaluationWorkflowService>();
|
||||
services.AddScoped<IPurchaseEvaluationCodeGenerator, PurchaseEvaluationCodeGenerator>();
|
||||
// [W1 Mig 69 — S161] mã phiếu KHKK/{YYYY}/{Seq:D3} qua WorkflowAppCodeSequences dùng chung.
|
||||
services.AddScoped<IContractSigningPlanCodeGenerator, ContractSigningPlanCodeGenerator>();
|
||||
services.AddScoped<IEmployeeCodeGenerator, EmployeeCodeGenerator>();
|
||||
services.AddScoped<IContractExcelExporter, ContractExcelExporter>();
|
||||
services.AddScoped<IAttendanceReportExcelExporter, AttendanceReportExcelExporter>();
|
||||
|
||||
@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SolutionErp.Application.Common.Interfaces;
|
||||
using SolutionErp.Domain.ApprovalWorkflowsV2;
|
||||
using SolutionErp.Domain.ContractSigningPlans;
|
||||
using SolutionErp.Domain.Contracts;
|
||||
using SolutionErp.Domain.Contracts.Details;
|
||||
using SolutionErp.Domain.Forms;
|
||||
@ -126,6 +127,15 @@ public class ApplicationDbContext
|
||||
// Phase 11 P11-B Wave 1 (Mig 42) — Số dư phép theo năm.
|
||||
public DbSet<LeaveBalance> LeaveBalances => Set<LeaveBalance>();
|
||||
|
||||
// [W1 Mig 69 — S161] GĐ2 "Kế hoạch ký kết HĐ" — 7 bảng (mirror IApplicationDbContext).
|
||||
public DbSet<ContractSigningPlan> ContractSigningPlans => Set<ContractSigningPlan>();
|
||||
public DbSet<ContractSigningPlanLine> ContractSigningPlanLines => Set<ContractSigningPlanLine>();
|
||||
public DbSet<ContractSigningPlanDossierItem> ContractSigningPlanDossierItems => Set<ContractSigningPlanDossierItem>();
|
||||
public DbSet<ContractSigningPlanLevelOpinion> ContractSigningPlanLevelOpinions => Set<ContractSigningPlanLevelOpinion>();
|
||||
public DbSet<ContractSigningPlanAttachment> ContractSigningPlanAttachments => Set<ContractSigningPlanAttachment>();
|
||||
public DbSet<ContractSigningPlanChangelog> ContractSigningPlanChangelogs => Set<ContractSigningPlanChangelog>();
|
||||
public DbSet<ContractSigningPlanApproval> ContractSigningPlanApprovals => Set<ContractSigningPlanApproval>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
base.OnModelCreating(builder);
|
||||
|
||||
@ -0,0 +1,36 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using SolutionErp.Domain.ContractSigningPlans;
|
||||
|
||||
namespace SolutionErp.Infrastructure.Persistence.Configurations;
|
||||
|
||||
// [W1 Mig 69 — S161] Lịch sử transition (1 row / lần chuyển phase).
|
||||
// Khuôn PurchaseEvaluationApprovalConfiguration (`PurchaseEvaluationConfiguration.cs:156-170`)
|
||||
// + index twin (PlanId, ApprovedAt).
|
||||
//
|
||||
// BaseEntity — append-only, KHÔNG soft-delete ⇒ KHÔNG HasQueryFilter (parity PE/Contract).
|
||||
// `ApprovalWorkflowLevelId` = loose-Guid KHÔNG FK vật lý: FK Restrict ở đây sẽ làm admin xoá
|
||||
// Level nổ 500 vì lịch sử duyệt, mà 3 site dọn dẹp trong ApprovalWorkflowV2AdminFeatures chỉ
|
||||
// biết họ bảng *LevelOpinions (không biết *Approvals) — xem chú thích trong entity.
|
||||
public class ContractSigningPlanApprovalConfiguration
|
||||
: IEntityTypeConfiguration<ContractSigningPlanApproval>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ContractSigningPlanApproval> b)
|
||||
{
|
||||
b.ToTable("ContractSigningPlanApprovals");
|
||||
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.ContractSigningPlanId, x.ApprovedAt });
|
||||
b.HasIndex(x => x.ApprovalWorkflowLevelId);
|
||||
|
||||
b.HasOne(x => x.ContractSigningPlan)
|
||||
.WithMany(p => p.Approvals)
|
||||
.HasForeignKey(x => x.ContractSigningPlanId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using SolutionErp.Domain.ContractSigningPlans;
|
||||
|
||||
namespace SolutionErp.Infrastructure.Persistence.Configurations;
|
||||
|
||||
// [W1 Mig 69 — S161] File đính kèm phiếu Kế hoạch ký kết.
|
||||
// Độ dài chuỗi theo spec §②-4 (`:134-137`): FileName 500 / StoragePath 1000 / ContentType 200
|
||||
// — khuôn ProposalAttachmentConfiguration.cs:15-17 (RỘNG hơn PE 255/500/100 vì PE là bản cũ).
|
||||
//
|
||||
// FK Cascade Plan = đường cascade DUY NHẤT. `ContractSigningPlanDossierItemId` CỐ Ý chỉ
|
||||
// HasIndex, KHÔNG FK vật lý — khuôn PurchaseEvaluationAttachmentConfiguration
|
||||
// (`PurchaseEvaluationConfiguration.cs:199-200` chỉ HasIndex cho SupplierId):
|
||||
// Plan→DossierItem→Attachment + Plan→Attachment cùng Cascade = MULTIPLE CASCADE PATHS,
|
||||
// SQL Server từ chối tạo bảng.
|
||||
public class ContractSigningPlanAttachmentConfiguration
|
||||
: IEntityTypeConfiguration<ContractSigningPlanAttachment>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ContractSigningPlanAttachment> b)
|
||||
{
|
||||
b.ToTable("ContractSigningPlanAttachments");
|
||||
b.HasKey(x => x.Id);
|
||||
|
||||
b.Property(x => x.FileName).HasMaxLength(500).IsRequired();
|
||||
b.Property(x => x.StoragePath).HasMaxLength(1000).IsRequired();
|
||||
b.Property(x => x.ContentType).HasMaxLength(200).IsRequired();
|
||||
b.Property(x => x.Purpose).HasConversion<int>();
|
||||
b.Property(x => x.Note).HasMaxLength(1000);
|
||||
|
||||
b.HasIndex(x => x.ContractSigningPlanId);
|
||||
b.HasIndex(x => x.ContractSigningPlanDossierItemId);
|
||||
|
||||
b.HasOne(x => x.ContractSigningPlan)
|
||||
.WithMany(p => p.Attachments)
|
||||
.HasForeignKey(x => x.ContractSigningPlanId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasQueryFilter(x => !x.IsDeleted);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using SolutionErp.Domain.ContractSigningPlans;
|
||||
|
||||
namespace SolutionErp.Infrastructure.Persistence.Configurations;
|
||||
|
||||
// [W1 Mig 69 — S161] Nhật ký thay đổi phiếu Kế hoạch ký kết — CÓ TỪ NGÀY 1
|
||||
// (tiền lệ PE Mig 60/61: thiếu changelog thì không backfill lại được lịch sử).
|
||||
//
|
||||
// Khuôn PurchaseEvaluationChangelogConfiguration (`PurchaseEvaluationConfiguration.cs:172-190`),
|
||||
// độ dài chuỗi theo spec §②-4: Summary 1000 (PE để 500) · ContextNote 2000 · UserName 200 ·
|
||||
// FieldChangesJson nvarchar(max) (JSON tự do, KHÔNG index).
|
||||
//
|
||||
// BaseEntity — KHÔNG soft-delete ⇒ KHÔNG HasQueryFilter (append-only audit, parity PE/Contract).
|
||||
// Index twin (PlanId, EntityType) = vế mà `LogTransitionAsync` copy sang cần để lọc theo phạm vi.
|
||||
public class ContractSigningPlanChangelogConfiguration
|
||||
: IEntityTypeConfiguration<ContractSigningPlanChangelog>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ContractSigningPlanChangelog> b)
|
||||
{
|
||||
b.ToTable("ContractSigningPlanChangelogs");
|
||||
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(1000);
|
||||
b.Property(x => x.ContextNote).HasMaxLength(2000);
|
||||
b.Property(x => x.FieldChangesJson).HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasIndex(x => new { x.ContractSigningPlanId, x.CreatedAt });
|
||||
b.HasIndex(x => new { x.ContractSigningPlanId, x.EntityType });
|
||||
|
||||
b.HasOne(x => x.ContractSigningPlan)
|
||||
.WithMany(p => p.Changelogs)
|
||||
.HasForeignKey(x => x.ContractSigningPlanId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using SolutionErp.Domain.ContractSigningPlans;
|
||||
|
||||
namespace SolutionErp.Infrastructure.Persistence.Configurations;
|
||||
|
||||
// [W1 Mig 69 — S161 2026-07-29] Header "Kế hoạch ký kết HĐ" (GĐ2).
|
||||
// Khuôn PurchaseEvaluationConfiguration.cs (HasQueryFilter :84 + FK Restrict workflow :70-73).
|
||||
//
|
||||
// loose-Guid KHÔNG FK vật lý: PurchaseEvaluationId / ProjectId / DepartmentId /
|
||||
// DrafterUserId — chỉ HasIndex (convention PE + Mig 49).
|
||||
public class ContractSigningPlanConfiguration : IEntityTypeConfiguration<ContractSigningPlan>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ContractSigningPlan> b)
|
||||
{
|
||||
b.ToTable("ContractSigningPlans");
|
||||
b.HasKey(x => x.Id);
|
||||
|
||||
b.Property(x => x.MaKeHoach).HasMaxLength(50); // "KHKK/2026/001"
|
||||
b.Property(x => x.Phase).HasConversion<int>();
|
||||
b.Property(x => x.HoSoLink).HasMaxLength(1000);
|
||||
b.Property(x => x.GhiChu).HasMaxLength(2000);
|
||||
|
||||
// Mã phiếu UNIQUE khi đã gen (gen lúc Submit ⇒ nháp còn null) — mirror
|
||||
// PurchaseEvaluationConfiguration.cs:56 / ProposalConfiguration.cs:23.
|
||||
b.HasIndex(x => x.MaKeHoach).IsUnique().HasFilter("[MaKeHoach] IS NOT NULL");
|
||||
|
||||
b.HasIndex(x => x.PurchaseEvaluationId);
|
||||
b.HasIndex(x => x.ProjectId);
|
||||
b.HasIndex(x => x.DepartmentId);
|
||||
b.HasIndex(x => x.DrafterUserId);
|
||||
b.HasIndex(x => new { x.Phase, x.IsDeleted });
|
||||
b.HasIndex(x => x.ApprovalWorkflowId);
|
||||
|
||||
// FK Restrict sang bảng V2 dùng chung: KHÔNG cho xoá quy trình khi còn phiếu pin
|
||||
// (mirror PE `PurchaseEvaluationConfiguration.cs:70-73`).
|
||||
b.HasOne<SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflow>()
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.ApprovalWorkflowId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.HasQueryFilter(x => !x.IsDeleted);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using SolutionErp.Domain.ContractSigningPlans;
|
||||
|
||||
namespace SolutionErp.Infrastructure.Persistence.Configurations;
|
||||
|
||||
// [W1 Mig 69 — S161] Danh mục căn cứ (b.8-9): mẫu vật liệu / shopdrawing + kết quả TVGS.
|
||||
public class ContractSigningPlanDossierItemConfiguration
|
||||
: IEntityTypeConfiguration<ContractSigningPlanDossierItem>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ContractSigningPlanDossierItem> b)
|
||||
{
|
||||
b.ToTable("ContractSigningPlanDossierItems");
|
||||
b.HasKey(x => x.Id);
|
||||
|
||||
b.Property(x => x.Kind).HasConversion<int>();
|
||||
b.Property(x => x.Status).HasConversion<int>();
|
||||
b.Property(x => x.Name).HasMaxLength(500).IsRequired();
|
||||
b.Property(x => x.TvgsName).HasMaxLength(200);
|
||||
b.Property(x => x.Note).HasMaxLength(1000);
|
||||
|
||||
b.HasIndex(x => x.ContractSigningPlanId);
|
||||
b.HasIndex(x => x.SupplierId);
|
||||
|
||||
b.HasOne(x => x.ContractSigningPlan)
|
||||
.WithMany(p => p.DossierItems)
|
||||
.HasForeignKey(x => x.ContractSigningPlanId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasQueryFilter(x => !x.IsDeleted);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using SolutionErp.Domain.ContractSigningPlans;
|
||||
|
||||
namespace SolutionErp.Infrastructure.Persistence.Configurations;
|
||||
|
||||
// [W1 Mig 69 — S161] Cookie-cutter mirror ContractLevelOpinionConfiguration.cs (Mig 33)
|
||||
// — twin thứ 8 của họ `*LevelOpinion`.
|
||||
//
|
||||
// UNIQUE (PlanId, LevelId) = 1 row/level/phiếu; UPSERT (không soft-delete) ⇒ index
|
||||
// KHÔNG filter, ĐÚNG như 7 twin còn lại (khác Lines: ở đó có xoá-mềm-rồi-thêm-lại thật).
|
||||
// FK Cascade Plan + Restrict Level (bảo vệ chữ ký khi admin sửa quy trình).
|
||||
// CỐ Ý KHÔNG HasQueryFilter — xem chú thích trong entity (site dọn chữ ký MỒ CÔI phải
|
||||
// thấy được row soft-deleted, nếu không FK Restrict sẽ nổ 500 lúc admin xoá Cấp).
|
||||
public class ContractSigningPlanLevelOpinionConfiguration
|
||||
: IEntityTypeConfiguration<ContractSigningPlanLevelOpinion>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ContractSigningPlanLevelOpinion> e)
|
||||
{
|
||||
e.ToTable("ContractSigningPlanLevelOpinions");
|
||||
e.HasKey(x => x.Id);
|
||||
|
||||
e.Property(x => x.Comment).HasMaxLength(2000);
|
||||
e.Property(x => x.SignedByFullName).HasMaxLength(200).IsRequired();
|
||||
|
||||
e.HasOne(x => x.ContractSigningPlan)
|
||||
.WithMany(p => p.LevelOpinions)
|
||||
.HasForeignKey(x => x.ContractSigningPlanId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
e.HasOne(x => x.Level)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.ApprovalWorkflowLevelId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
e.HasIndex(x => new { x.ContractSigningPlanId, x.ApprovalWorkflowLevelId }).IsUnique();
|
||||
e.HasIndex(x => x.ApprovalWorkflowLevelId);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using SolutionErp.Domain.ContractSigningPlans;
|
||||
|
||||
namespace SolutionErp.Infrastructure.Persistence.Configurations;
|
||||
|
||||
// [W1 Mig 69 — S161] Dòng giá per-NCC.
|
||||
// 🔴 UNIQUE (PlanId, SupplierId) PHẢI filtered `WHERE [IsDeleted] = 0` (gotcha #57 —
|
||||
// 7× cumulative: Mig 43/45/47/64). Xoá mềm 1 NCC rồi thêm lại NCC đó = phải được.
|
||||
public class ContractSigningPlanLineConfiguration : IEntityTypeConfiguration<ContractSigningPlanLine>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ContractSigningPlanLine> b)
|
||||
{
|
||||
b.ToTable("ContractSigningPlanLines");
|
||||
b.HasKey(x => x.Id);
|
||||
|
||||
b.Property(x => x.PeReferenceAmount).HasPrecision(18, 2);
|
||||
b.Property(x => x.ProposedAmount).HasPrecision(18, 2);
|
||||
b.Property(x => x.ApprovedAmount).HasPrecision(18, 2);
|
||||
b.Property(x => x.Note).HasMaxLength(1000);
|
||||
|
||||
b.HasIndex(x => new { x.ContractSigningPlanId, x.SupplierId })
|
||||
.IsUnique()
|
||||
.HasFilter("[IsDeleted] = 0");
|
||||
b.HasIndex(x => x.SupplierId);
|
||||
b.HasIndex(x => x.ContractId);
|
||||
|
||||
b.HasOne(x => x.ContractSigningPlan)
|
||||
.WithMany(p => p.Lines)
|
||||
.HasForeignKey(x => x.ContractSigningPlanId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasQueryFilter(x => !x.IsDeleted);
|
||||
}
|
||||
}
|
||||
@ -1769,8 +1769,9 @@ public static class DbInitializer
|
||||
(MenuKeys.CatalogWorkItems,"Hạng mục công việc", MenuKeys.Catalogs, 4, "ListChecks"),
|
||||
// [S57] Order 30→31: nhường slot 30 cho nhóm "Cá nhân" (đứng ngay sau Văn phòng số = 29, mirror Puro).
|
||||
(MenuKeys.Contracts, "Duyệt hợp đồng", null, 31, "FileText"), // [S159] rename theo 4-GĐ (owner tab strip)
|
||||
// [S159] 2 root PLACEHOLDER GĐ2/GĐ4 — menu = 4 giai đoạn (owner). Ngoài MenuKeys.All,
|
||||
// FE staticMap trỏ /dashboard. Prod đã INSERT trực tiếp cùng ngày (idempotent ở đây).
|
||||
// [S159] 2 root PLACEHOLDER GĐ2/GĐ4 — menu = 4 giai đoạn (owner). FE staticMap trỏ
|
||||
// /dashboard. Prod đã INSERT trực tiếp cùng ngày (idempotent ở đây).
|
||||
// [S161 W1 — F-3] `KeHoachKyKet` nay ĐÃ VÀO MenuKeys.All (policy sống); `HopDongCung` vẫn ngoài.
|
||||
(MenuKeys.KeHoachKyKet, "Kế hoạch ký kết HĐ", null, 26, "FileSignature"),
|
||||
(MenuKeys.HopDongCung, "Hợp đồng cứng", null, 32, "Stamp"),
|
||||
// [S159-đợt3] skeleton menu GĐ2/GĐ4 (owner vẽ: "viết hoa + phân list như
|
||||
@ -2101,6 +2102,13 @@ public static class DbInitializer
|
||||
// MỌI role khác (seeder chỉ đụng role Procurement). Dashboard/Reports KHÔNG cấp (anh chốt bỏ).
|
||||
await SeedProcurementMasterAccessAsync(db, roleManager, logger);
|
||||
|
||||
// [W1 Mig 69 — S161 2026-07-29] GĐ2 "Kế hoạch ký kết HĐ" golive: nâng quyền GHI
|
||||
// (CanCreate+CanUpdate 13/13 role — O-A "hiển thị hết để góp ý"; CanDelete chỉ
|
||||
// Drafter+Admin) trên root KeHoachKyKet + 6 leaf Khkk_*. CHẠY SAU revoke để THẮNG.
|
||||
// 🔴 UPGRADE-IF-EXISTS bắt buộc (review F-S1 4-lane hội tụ): row đã seed CanRead-only
|
||||
// @S159 ⇒ nhánh insert skip-existing = NO-OP im lặng ⇒ 403 giết dry-run (gotcha #44).
|
||||
await SeedKeHoachKyKetAccessAsync(db, roleManager, logger);
|
||||
|
||||
// [S65 2026-06-16] CHẠY SAU revoke để THẮNG: mở lại quyền XEM "Hồ sơ Nhân sự"
|
||||
// (+ root nhóm "Nhân sự") cho MỌI role — anh chốt public module Nhân sự cho
|
||||
// user thường tra cứu hồ sơ. Dashboard NS (Hrm_Dashboard) + 6 catalog
|
||||
@ -2397,6 +2405,72 @@ public static class DbInitializer
|
||||
}
|
||||
}
|
||||
|
||||
// [W1 Mig 69 — S161 2026-07-29] GĐ2 "Kế hoạch ký kết HĐ" — nâng quyền GHI cho MỌI role
|
||||
// trên root KeHoachKyKet + 6 leaf Khkk_* (O-A anh chốt "hiển thị hết để góp ý" ⇒ 13/13
|
||||
// role CanCreate+CanUpdate; CanDelete CHỈ Drafter+Admin — spec W1 §②-8).
|
||||
// Khuôn = SeedProcurementMasterAccessAsync (S118) MỞ RỘNG thêm vế CanDelete (khuôn gốc
|
||||
// không có — chốt-cuối S160 dặn). UPGRADE-ONLY: row tồn tại → NÂNG cờ desired, KHÔNG hạ
|
||||
// cờ nào; row chưa có (fresh DB) → tạo đủ cờ. CẤM SQL tay (#84 — grant tay bị revoker lật).
|
||||
// Thu hồi khi cần: xóa call ở SeedAsync — revoke sẽ tự che lại lần seed kế.
|
||||
private static async Task SeedKeHoachKyKetAccessAsync(
|
||||
ApplicationDbContext db, RoleManager<Role> roleManager, ILogger logger)
|
||||
{
|
||||
// Root policy-key + 6 leaf menu-key (khớp bộ seed menu-row S159; Khkk_G1 = group
|
||||
// điều hướng, CanRead-all đã có từ nhánh read-only — KHÔNG cần cờ ghi).
|
||||
var keys = new[]
|
||||
{
|
||||
MenuKeys.KeHoachKyKet,
|
||||
"Khkk_WfView", "Khkk_List", "Khkk_Create", "Khkk_Pending",
|
||||
"Khkk_Approved", "Khkk_Deleted",
|
||||
};
|
||||
|
||||
var roles = await roleManager.Roles.ToListAsync();
|
||||
var existing = (await db.Permissions
|
||||
.Where(p => keys.Contains(p.MenuKey))
|
||||
.ToListAsync())
|
||||
.ToDictionary(p => (p.RoleId, p.MenuKey));
|
||||
|
||||
var added = 0;
|
||||
var upgraded = 0;
|
||||
foreach (var role in roles)
|
||||
{
|
||||
var allowDelete = role.Name == AppRoles.Admin || role.Name == AppRoles.Drafter;
|
||||
foreach (var key in keys)
|
||||
{
|
||||
if (existing.TryGetValue((role.Id, key), out var row))
|
||||
{
|
||||
var changed = false;
|
||||
if (!row.CanRead) { row.CanRead = true; changed = true; }
|
||||
if (!row.CanCreate) { row.CanCreate = true; changed = true; }
|
||||
if (!row.CanUpdate) { row.CanUpdate = true; changed = true; }
|
||||
if (allowDelete && !row.CanDelete) { row.CanDelete = true; changed = true; }
|
||||
if (changed) upgraded++;
|
||||
continue;
|
||||
}
|
||||
|
||||
db.Permissions.Add(new Permission
|
||||
{
|
||||
RoleId = role.Id,
|
||||
MenuKey = key,
|
||||
CanRead = true,
|
||||
CanCreate = true,
|
||||
CanUpdate = true,
|
||||
CanDelete = allowDelete,
|
||||
});
|
||||
added++;
|
||||
}
|
||||
}
|
||||
|
||||
if (added > 0 || upgraded > 0)
|
||||
{
|
||||
await db.SaveChangesAsync();
|
||||
logger.LogInformation(
|
||||
"Seeded KeHoachKyKet access: {Added} added + {Upgraded} upgraded " +
|
||||
"(W1 GĐ2 golive — 13/13 role C+U, Delete=Drafter/Admin; UPGRADE-if-exists F-S1)",
|
||||
added, upgraded);
|
||||
}
|
||||
}
|
||||
|
||||
// [S65 2026-06-16] Mở quyền XEM (Read-only) "Hồ sơ Nhân sự" cho MỌI role: anh chốt
|
||||
// public module Nhân sự (trọng tâm Hồ sơ NS) để user thường tra cứu hồ sơ nhân sự.
|
||||
// CHẠY SAU RevokeTemporarilyHiddenModulesAsync (SeedAsync) để THẮNG revoke: revoke
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,372 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddContractSigningPlans : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ContractSigningPlans",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
MaKeHoach = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
|
||||
PurchaseEvaluationId = table.Column<Guid>(type: "uniqueidentifier", 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: false),
|
||||
Phase = table.Column<int>(type: "int", nullable: false),
|
||||
ApprovalWorkflowId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
CurrentWorkflowStepIndex = table.Column<int>(type: "int", nullable: true),
|
||||
CurrentApprovalLevelOrder = table.Column<int>(type: "int", nullable: true),
|
||||
SlaDeadline = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
HoSoLink = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true),
|
||||
GhiChu = 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),
|
||||
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_ContractSigningPlans", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ContractSigningPlans_ApprovalWorkflows_ApprovalWorkflowId",
|
||||
column: x => x.ApprovalWorkflowId,
|
||||
principalTable: "ApprovalWorkflows",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ContractSigningPlanApprovals",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
ContractSigningPlanId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
FromPhase = table.Column<int>(type: "int", nullable: false),
|
||||
ToPhase = table.Column<int>(type: "int", nullable: false),
|
||||
Decision = table.Column<int>(type: "int", nullable: false),
|
||||
Comment = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true),
|
||||
ApprovalWorkflowLevelId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
ApprovedByUserId = table.Column<Guid>(type: "uniqueidentifier", 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_ContractSigningPlanApprovals", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ContractSigningPlanApprovals_ContractSigningPlans_ContractSigningPlanId",
|
||||
column: x => x.ContractSigningPlanId,
|
||||
principalTable: "ContractSigningPlans",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ContractSigningPlanAttachments",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
ContractSigningPlanId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
ContractSigningPlanDossierItemId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
FileName = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false),
|
||||
StoragePath = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: false),
|
||||
FileSize = table.Column<long>(type: "bigint", nullable: false),
|
||||
ContentType = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
|
||||
Purpose = table.Column<int>(type: "int", nullable: false),
|
||||
Note = 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),
|
||||
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_ContractSigningPlanAttachments", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ContractSigningPlanAttachments_ContractSigningPlans_ContractSigningPlanId",
|
||||
column: x => x.ContractSigningPlanId,
|
||||
principalTable: "ContractSigningPlans",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ContractSigningPlanChangelogs",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
ContractSigningPlanId = 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(1000)", maxLength: 1000, 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_ContractSigningPlanChangelogs", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ContractSigningPlanChangelogs_ContractSigningPlans_ContractSigningPlanId",
|
||||
column: x => x.ContractSigningPlanId,
|
||||
principalTable: "ContractSigningPlans",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ContractSigningPlanDossierItems",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
ContractSigningPlanId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
Kind = table.Column<int>(type: "int", nullable: false),
|
||||
SupplierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
Name = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false),
|
||||
Status = table.Column<int>(type: "int", nullable: false),
|
||||
TvgsName = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
|
||||
TvgsResultAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
Note = 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),
|
||||
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_ContractSigningPlanDossierItems", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ContractSigningPlanDossierItems_ContractSigningPlans_ContractSigningPlanId",
|
||||
column: x => x.ContractSigningPlanId,
|
||||
principalTable: "ContractSigningPlans",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ContractSigningPlanLevelOpinions",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
ContractSigningPlanId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
ApprovalWorkflowLevelId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
Comment = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: true),
|
||||
SignedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
SignedByUserId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
SignedByFullName = table.Column<string>(type: "nvarchar(200)", maxLength: 200, 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),
|
||||
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_ContractSigningPlanLevelOpinions", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ContractSigningPlanLevelOpinions_ApprovalWorkflowLevels_ApprovalWorkflowLevelId",
|
||||
column: x => x.ApprovalWorkflowLevelId,
|
||||
principalTable: "ApprovalWorkflowLevels",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_ContractSigningPlanLevelOpinions_ContractSigningPlans_ContractSigningPlanId",
|
||||
column: x => x.ContractSigningPlanId,
|
||||
principalTable: "ContractSigningPlans",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ContractSigningPlanLines",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
ContractSigningPlanId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
SupplierId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
PeReferenceAmount = table.Column<decimal>(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false),
|
||||
ProposedAmount = table.Column<decimal>(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false),
|
||||
ApprovedAmount = table.Column<decimal>(type: "decimal(18,2)", precision: 18, scale: 2, nullable: true),
|
||||
ContractId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
Note = 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),
|
||||
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_ContractSigningPlanLines", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ContractSigningPlanLines_ContractSigningPlans_ContractSigningPlanId",
|
||||
column: x => x.ContractSigningPlanId,
|
||||
principalTable: "ContractSigningPlans",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ContractSigningPlanApprovals_ApprovalWorkflowLevelId",
|
||||
table: "ContractSigningPlanApprovals",
|
||||
column: "ApprovalWorkflowLevelId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ContractSigningPlanApprovals_ContractSigningPlanId_ApprovedAt",
|
||||
table: "ContractSigningPlanApprovals",
|
||||
columns: new[] { "ContractSigningPlanId", "ApprovedAt" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ContractSigningPlanAttachments_ContractSigningPlanDossierItemId",
|
||||
table: "ContractSigningPlanAttachments",
|
||||
column: "ContractSigningPlanDossierItemId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ContractSigningPlanAttachments_ContractSigningPlanId",
|
||||
table: "ContractSigningPlanAttachments",
|
||||
column: "ContractSigningPlanId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ContractSigningPlanChangelogs_ContractSigningPlanId_CreatedAt",
|
||||
table: "ContractSigningPlanChangelogs",
|
||||
columns: new[] { "ContractSigningPlanId", "CreatedAt" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ContractSigningPlanChangelogs_ContractSigningPlanId_EntityType",
|
||||
table: "ContractSigningPlanChangelogs",
|
||||
columns: new[] { "ContractSigningPlanId", "EntityType" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ContractSigningPlanDossierItems_ContractSigningPlanId",
|
||||
table: "ContractSigningPlanDossierItems",
|
||||
column: "ContractSigningPlanId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ContractSigningPlanDossierItems_SupplierId",
|
||||
table: "ContractSigningPlanDossierItems",
|
||||
column: "SupplierId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ContractSigningPlanLevelOpinions_ApprovalWorkflowLevelId",
|
||||
table: "ContractSigningPlanLevelOpinions",
|
||||
column: "ApprovalWorkflowLevelId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ContractSigningPlanLevelOpinions_ContractSigningPlanId_ApprovalWorkflowLevelId",
|
||||
table: "ContractSigningPlanLevelOpinions",
|
||||
columns: new[] { "ContractSigningPlanId", "ApprovalWorkflowLevelId" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ContractSigningPlanLines_ContractId",
|
||||
table: "ContractSigningPlanLines",
|
||||
column: "ContractId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ContractSigningPlanLines_ContractSigningPlanId_SupplierId",
|
||||
table: "ContractSigningPlanLines",
|
||||
columns: new[] { "ContractSigningPlanId", "SupplierId" },
|
||||
unique: true,
|
||||
filter: "[IsDeleted] = 0");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ContractSigningPlanLines_SupplierId",
|
||||
table: "ContractSigningPlanLines",
|
||||
column: "SupplierId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ContractSigningPlans_ApprovalWorkflowId",
|
||||
table: "ContractSigningPlans",
|
||||
column: "ApprovalWorkflowId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ContractSigningPlans_DepartmentId",
|
||||
table: "ContractSigningPlans",
|
||||
column: "DepartmentId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ContractSigningPlans_DrafterUserId",
|
||||
table: "ContractSigningPlans",
|
||||
column: "DrafterUserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ContractSigningPlans_MaKeHoach",
|
||||
table: "ContractSigningPlans",
|
||||
column: "MaKeHoach",
|
||||
unique: true,
|
||||
filter: "[MaKeHoach] IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ContractSigningPlans_Phase_IsDeleted",
|
||||
table: "ContractSigningPlans",
|
||||
columns: new[] { "Phase", "IsDeleted" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ContractSigningPlans_ProjectId",
|
||||
table: "ContractSigningPlans",
|
||||
column: "ProjectId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ContractSigningPlans_PurchaseEvaluationId",
|
||||
table: "ContractSigningPlans",
|
||||
column: "PurchaseEvaluationId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "ContractSigningPlanApprovals");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ContractSigningPlanAttachments");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ContractSigningPlanChangelogs");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ContractSigningPlanDossierItems");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ContractSigningPlanLevelOpinions");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ContractSigningPlanLines");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ContractSigningPlans");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -312,6 +312,459 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
b.ToTable("ApprovalWorkflowSteps", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.ContractSigningPlans.ContractSigningPlan", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("ApprovalWorkflowId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid?>("CreatedBy")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<int?>("CurrentApprovalLevelOrder")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int?>("CurrentWorkflowStepIndex")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid?>("DeletedBy")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("DepartmentId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("DrafterUserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("GhiChu")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("nvarchar(2000)");
|
||||
|
||||
b.Property<string>("HoSoLink")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("nvarchar(1000)");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("MaKeHoach")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<int>("Phase")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("PurchaseEvaluationId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime?>("SlaDeadline")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid?>("UpdatedBy")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ApprovalWorkflowId");
|
||||
|
||||
b.HasIndex("DepartmentId");
|
||||
|
||||
b.HasIndex("DrafterUserId");
|
||||
|
||||
b.HasIndex("MaKeHoach")
|
||||
.IsUnique()
|
||||
.HasFilter("[MaKeHoach] IS NOT NULL");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.HasIndex("PurchaseEvaluationId");
|
||||
|
||||
b.HasIndex("Phase", "IsDeleted");
|
||||
|
||||
b.ToTable("ContractSigningPlans", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.ContractSigningPlans.ContractSigningPlanApproval", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("ApprovalWorkflowLevelId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("ApprovedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid?>("ApprovedByUserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Comment")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("nvarchar(1000)");
|
||||
|
||||
b.Property<Guid>("ContractSigningPlanId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
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<int>("ToPhase")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid?>("UpdatedBy")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ApprovalWorkflowLevelId");
|
||||
|
||||
b.HasIndex("ContractSigningPlanId", "ApprovedAt");
|
||||
|
||||
b.ToTable("ContractSigningPlanApprovals", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.ContractSigningPlans.ContractSigningPlanAttachment", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("ContentType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<Guid?>("ContractSigningPlanDossierItemId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("ContractSigningPlanId")
|
||||
.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<string>("FileName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<long>("FileSize")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Note")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("nvarchar(1000)");
|
||||
|
||||
b.Property<int>("Purpose")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("StoragePath")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("nvarchar(1000)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid?>("UpdatedBy")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ContractSigningPlanDossierItemId");
|
||||
|
||||
b.HasIndex("ContractSigningPlanId");
|
||||
|
||||
b.ToTable("ContractSigningPlanAttachments", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.ContractSigningPlans.ContractSigningPlanChangelog", 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<Guid>("ContractSigningPlanId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
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<string>("Summary")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("nvarchar(1000)");
|
||||
|
||||
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("ContractSigningPlanId", "CreatedAt");
|
||||
|
||||
b.HasIndex("ContractSigningPlanId", "EntityType");
|
||||
|
||||
b.ToTable("ContractSigningPlanChangelogs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.ContractSigningPlans.ContractSigningPlanDossierItem", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("ContractSigningPlanId")
|
||||
.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<bool>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<int>("Kind")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("Note")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("nvarchar(1000)");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<Guid?>("SupplierId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("TvgsName")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<DateTime?>("TvgsResultAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid?>("UpdatedBy")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ContractSigningPlanId");
|
||||
|
||||
b.HasIndex("SupplierId");
|
||||
|
||||
b.ToTable("ContractSigningPlanDossierItems", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.ContractSigningPlans.ContractSigningPlanLevelOpinion", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("ApprovalWorkflowLevelId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Comment")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("nvarchar(2000)");
|
||||
|
||||
b.Property<Guid>("ContractSigningPlanId")
|
||||
.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<bool>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime>("SignedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("SignedByFullName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<Guid>("SignedByUserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid?>("UpdatedBy")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ApprovalWorkflowLevelId");
|
||||
|
||||
b.HasIndex("ContractSigningPlanId", "ApprovalWorkflowLevelId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ContractSigningPlanLevelOpinions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.ContractSigningPlans.ContractSigningPlanLine", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<decimal?>("ApprovedAmount")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<Guid?>("ContractId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("ContractSigningPlanId")
|
||||
.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<bool>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Note")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("nvarchar(1000)");
|
||||
|
||||
b.Property<decimal>("PeReferenceAmount")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<decimal>("ProposedAmount")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<Guid>("SupplierId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid?>("UpdatedBy")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ContractId");
|
||||
|
||||
b.HasIndex("SupplierId");
|
||||
|
||||
b.HasIndex("ContractSigningPlanId", "SupplierId")
|
||||
.IsUnique()
|
||||
.HasFilter("[IsDeleted] = 0");
|
||||
|
||||
b.ToTable("ContractSigningPlanLines", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.Contracts.Contract", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@ -5683,6 +6136,88 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
b.Navigation("ApprovalWorkflow");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.ContractSigningPlans.ContractSigningPlan", b =>
|
||||
{
|
||||
b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflow", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("ApprovalWorkflowId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.ContractSigningPlans.ContractSigningPlanApproval", b =>
|
||||
{
|
||||
b.HasOne("SolutionErp.Domain.ContractSigningPlans.ContractSigningPlan", "ContractSigningPlan")
|
||||
.WithMany("Approvals")
|
||||
.HasForeignKey("ContractSigningPlanId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ContractSigningPlan");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.ContractSigningPlans.ContractSigningPlanAttachment", b =>
|
||||
{
|
||||
b.HasOne("SolutionErp.Domain.ContractSigningPlans.ContractSigningPlan", "ContractSigningPlan")
|
||||
.WithMany("Attachments")
|
||||
.HasForeignKey("ContractSigningPlanId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ContractSigningPlan");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.ContractSigningPlans.ContractSigningPlanChangelog", b =>
|
||||
{
|
||||
b.HasOne("SolutionErp.Domain.ContractSigningPlans.ContractSigningPlan", "ContractSigningPlan")
|
||||
.WithMany("Changelogs")
|
||||
.HasForeignKey("ContractSigningPlanId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ContractSigningPlan");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.ContractSigningPlans.ContractSigningPlanDossierItem", b =>
|
||||
{
|
||||
b.HasOne("SolutionErp.Domain.ContractSigningPlans.ContractSigningPlan", "ContractSigningPlan")
|
||||
.WithMany("DossierItems")
|
||||
.HasForeignKey("ContractSigningPlanId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ContractSigningPlan");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.ContractSigningPlans.ContractSigningPlanLevelOpinion", b =>
|
||||
{
|
||||
b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowLevel", "Level")
|
||||
.WithMany()
|
||||
.HasForeignKey("ApprovalWorkflowLevelId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("SolutionErp.Domain.ContractSigningPlans.ContractSigningPlan", "ContractSigningPlan")
|
||||
.WithMany("LevelOpinions")
|
||||
.HasForeignKey("ContractSigningPlanId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ContractSigningPlan");
|
||||
|
||||
b.Navigation("Level");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.ContractSigningPlans.ContractSigningPlanLine", b =>
|
||||
{
|
||||
b.HasOne("SolutionErp.Domain.ContractSigningPlans.ContractSigningPlan", "ContractSigningPlan")
|
||||
.WithMany("Lines")
|
||||
.HasForeignKey("ContractSigningPlanId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ContractSigningPlan");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.Contracts.Contract", b =>
|
||||
{
|
||||
b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflow", null)
|
||||
@ -6275,6 +6810,21 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
b.Navigation("Levels");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.ContractSigningPlans.ContractSigningPlan", b =>
|
||||
{
|
||||
b.Navigation("Approvals");
|
||||
|
||||
b.Navigation("Attachments");
|
||||
|
||||
b.Navigation("Changelogs");
|
||||
|
||||
b.Navigation("DossierItems");
|
||||
|
||||
b.Navigation("LevelOpinions");
|
||||
|
||||
b.Navigation("Lines");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.Contracts.Contract", b =>
|
||||
{
|
||||
b.Navigation("Approvals");
|
||||
|
||||
@ -0,0 +1,52 @@
|
||||
using System.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SolutionErp.Application.Common.Interfaces;
|
||||
using SolutionErp.Application.ContractSigningPlans.Services;
|
||||
using SolutionErp.Domain.Office;
|
||||
|
||||
namespace SolutionErp.Infrastructure.Services;
|
||||
|
||||
// [W1 Mig 69 — S161 2026-07-29] Mã phiếu KHKK — format KHKK/{YYYY}/{Seq:D3} (anh OK @S161).
|
||||
// Mirror PurchaseEvaluationCodeGenerator (SERIALIZABLE + LastSeq += 1), khác 1 điểm:
|
||||
// sequence sống trong bảng WorkflowAppCodeSequences DÙNG CHUNG 4 module Office
|
||||
// (per-prefix row "KHKK/{YYYY}" — KHÔNG bảng sequence riêng, spec W1 §②-9).
|
||||
public class ContractSigningPlanCodeGenerator(IApplicationDbContext db, IDateTime dateTime)
|
||||
: IContractSigningPlanCodeGenerator
|
||||
{
|
||||
public async Task<string> GenerateAsync(CancellationToken ct = default)
|
||||
{
|
||||
var year = dateTime.UtcNow.Year;
|
||||
var prefix = $"KHKK/{year}";
|
||||
|
||||
// Transaction SERIALIZABLE chống race 2 người Submit cùng lúc (mirror PE codegen).
|
||||
var context = (DbContext)db;
|
||||
await using var tx = await context.Database
|
||||
.BeginTransactionAsync(IsolationLevel.Serializable, ct);
|
||||
try
|
||||
{
|
||||
var seq = await db.WorkflowAppCodeSequences
|
||||
.FirstOrDefaultAsync(s => s.Prefix == prefix, ct);
|
||||
if (seq is null)
|
||||
{
|
||||
seq = new WorkflowAppCodeSequence
|
||||
{
|
||||
Prefix = prefix, LastSeq = 1, UpdatedAt = dateTime.UtcNow,
|
||||
};
|
||||
db.WorkflowAppCodeSequences.Add(seq);
|
||||
}
|
||||
else
|
||||
{
|
||||
seq.LastSeq += 1;
|
||||
seq.UpdatedAt = dateTime.UtcNow;
|
||||
}
|
||||
await db.SaveChangesAsync(ct);
|
||||
await tx.CommitAsync(ct);
|
||||
return $"{prefix}/{seq.LastSeq:D3}";
|
||||
}
|
||||
catch
|
||||
{
|
||||
await tx.RollbackAsync(ct);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user