[CLAUDE] Domain+App+Api: Module Ngan sach (Budget) - 4 bang + workflow simple
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m11s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m11s
User request: 'Them cho tao 4 bang luu ve ngan sach: Header / Chi tiet
/ Quy trinh duyet / Lich su thay doi'.
Domain (5 file + 1 enum):
- Budget (header) — Aggregate root, AuditableEntity. Field: MaNganSach,
TenNganSach, Description, NamNganSach, ProjectId FK, DepartmentId?,
DrafterUserId, Phase (BudgetPhase 5-state), TongNganSach (sum auto
tu Details), SlaDeadline, SlaWarningSent.
- BudgetDetail — flat row pattern (GroupCode/GroupName + Item +
KhoiLuong/DonGia/ThanhTien). 18,4 precision KhoiLuong, 18,2 money.
- BudgetApproval — workflow history (FromPhase/ToPhase/Decision/Comment)
- BudgetChangelog — audit log unified (EntityType: Header/Detail/Workflow)
- BudgetPhase enum 5 state: DangSoanThao(1) → ChoCCM(2) → ChoCEO(3) →
DaDuyet(4) | TuChoi(99)
- BudgetPolicy hardcoded (no versioned WF, simple default per user
confirm 'tam thoi don gian'): Drafter/DeptManager → CCM → CEO/
AuthorizedSigner. Reject path back to DangSoanThao.
Migration 14 AddBudgets:
- 4 bang moi: Budgets + BudgetDetails + BudgetApprovals + BudgetChangelogs
- Index: Phase+IsDeleted, ProjectId, NamNganSach, SlaDeadline,
MaNganSach unique filtered. Cascade delete child.
- +2 cot FK ngoai bang (per user 'lien ket ca 3'):
* Contracts.BudgetId Guid? + index
* PurchaseEvaluations.BudgetId Guid? + index
Cho phep doi chieu chi phi HD/PE vs ngan sach goi thau.
Application CQRS (BudgetFeatures.cs ~340 line):
- CreateBudget + UpdateBudgetDraft + TransitionBudget + ListBudgets
(filter Phase/Project/Year + search + paging) + GetBudget bundle
(Header + Details + Approvals + Workflow summary)
- DeleteBudget (only DangSoanThao/TuChoi)
- AddBudgetDetail + UpdateBudgetDetail + DeleteBudgetDetail (auto
recompute TongNganSach = sum Details.ThanhTien)
- ListBudgetChangelogs
Api: BudgetsController 11 endpoint REST /api/budgets:
- GET / /{id} /{id}/changelogs
- POST / /{id}/transitions /{id}/details
- PUT /{id} /{id}/details/{detailId}
- DELETE /{id} /{id}/details/{detailId}
DbContext + IApplicationDbContext: 4 DbSet new (Budgets/Details/
Approvals/Changelogs).
MenuKeys + DbInitializer: 4 menu key (Budgets root + Bg_List/Create/
Pending leaves) seed dau order=27 'Ngan sach' icon Wallet. Auto-grant
admin permission via SeedAdminPermissionsAsync (MenuKeys.All).
MaNganSach format don gian 'NS-YYYYMM-NNNN' Random.Shared (chua atomic
sequence - user said 'tam thoi chua co').
Workflow chua versioned, hardcode BudgetPolicy.Default. Tuong lai admin
config qua UI: them BudgetWorkflowDefinition tables tuong tu PE.
This commit is contained in:
94
src/Backend/SolutionErp.Api/Controllers/BudgetsController.cs
Normal file
94
src/Backend/SolutionErp.Api/Controllers/BudgetsController.cs
Normal file
@ -0,0 +1,94 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SolutionErp.Application.Budgets;
|
||||
using SolutionErp.Application.Budgets.Dtos;
|
||||
using SolutionErp.Application.Common.Models;
|
||||
using SolutionErp.Domain.Budgets;
|
||||
using SolutionErp.Domain.Contracts;
|
||||
|
||||
namespace SolutionErp.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/budgets")]
|
||||
[Authorize]
|
||||
public class BudgetsController(IMediator mediator) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<PagedResult<BudgetListItemDto>>> List(
|
||||
[FromQuery] int page = 1, [FromQuery] int pageSize = 20,
|
||||
[FromQuery] string? search = null, [FromQuery] bool sortDesc = true,
|
||||
[FromQuery] BudgetPhase? phase = null,
|
||||
[FromQuery] Guid? projectId = null,
|
||||
[FromQuery] int? namNganSach = null,
|
||||
CancellationToken ct = default)
|
||||
=> Ok(await mediator.Send(new ListBudgetsQuery(phase, projectId, namNganSach)
|
||||
{ Page = page, PageSize = pageSize, Search = search, SortDesc = sortDesc }, ct));
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
public async Task<ActionResult<BudgetDetailBundleDto>> Get(Guid id, CancellationToken ct)
|
||||
=> Ok(await mediator.Send(new GetBudgetQuery(id), ct));
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<object>> Create([FromBody] CreateBudgetCommand cmd, CancellationToken ct)
|
||||
{
|
||||
var id = await mediator.Send(cmd, ct);
|
||||
return CreatedAtAction(nameof(Get), new { id }, new { id });
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateBudgetDraftCommand cmd, CancellationToken ct)
|
||||
{
|
||||
if (id != cmd.Id) return BadRequest(new { detail = "ID không khớp" });
|
||||
await mediator.Send(cmd, ct);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/transitions")]
|
||||
public async Task<IActionResult> Transition(Guid id, [FromBody] TransitionBudgetBody body, CancellationToken ct)
|
||||
{
|
||||
await mediator.Send(new TransitionBudgetCommand(id, body.TargetPhase, body.Decision, body.Comment), ct);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
|
||||
{
|
||||
await mediator.Send(new DeleteBudgetCommand(id), ct);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/details")]
|
||||
public async Task<ActionResult<object>> AddDetail(Guid id, [FromBody] BudgetDetailBody body, CancellationToken ct)
|
||||
{
|
||||
var newId = await mediator.Send(new AddBudgetDetailCommand(
|
||||
id, body.GroupCode, body.GroupName, body.ItemCode, body.NoiDung, body.DonViTinh,
|
||||
body.KhoiLuong, body.DonGia, body.ThanhTien, body.GhiChu), ct);
|
||||
return Ok(new { id = newId });
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}/details/{detailId:guid}")]
|
||||
public async Task<IActionResult> UpdateDetail(Guid id, Guid detailId, [FromBody] BudgetDetailBody body, CancellationToken ct)
|
||||
{
|
||||
await mediator.Send(new UpdateBudgetDetailCommand(
|
||||
id, detailId, body.GroupCode, body.GroupName, body.ItemCode, body.NoiDung, body.DonViTinh,
|
||||
body.KhoiLuong, body.DonGia, body.ThanhTien, body.GhiChu), ct);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}/details/{detailId:guid}")]
|
||||
public async Task<IActionResult> DeleteDetail(Guid id, Guid detailId, CancellationToken ct)
|
||||
{
|
||||
await mediator.Send(new DeleteBudgetDetailCommand(id, detailId), ct);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}/changelogs")]
|
||||
public async Task<List<BudgetChangelogDto>> GetChangelogs(Guid id, CancellationToken ct)
|
||||
=> await mediator.Send(new ListBudgetChangelogsQuery(id), ct);
|
||||
}
|
||||
|
||||
public record TransitionBudgetBody(BudgetPhase TargetPhase, ApprovalDecision Decision, string? Comment);
|
||||
public record BudgetDetailBody(
|
||||
string GroupCode, string GroupName, string? ItemCode, string NoiDung,
|
||||
string? DonViTinh, decimal KhoiLuong, decimal DonGia, decimal ThanhTien, string? GhiChu);
|
||||
388
src/Backend/SolutionErp.Application/Budgets/BudgetFeatures.cs
Normal file
388
src/Backend/SolutionErp.Application/Budgets/BudgetFeatures.cs
Normal file
@ -0,0 +1,388 @@
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SolutionErp.Application.Budgets.Dtos;
|
||||
using SolutionErp.Application.Common.Exceptions;
|
||||
using SolutionErp.Application.Common.Interfaces;
|
||||
using SolutionErp.Application.Common.Models;
|
||||
using SolutionErp.Domain.Budgets;
|
||||
using SolutionErp.Domain.Contracts; // ApprovalDecision + ChangelogAction
|
||||
using SolutionErp.Domain.Identity;
|
||||
|
||||
namespace SolutionErp.Application.Budgets;
|
||||
|
||||
// Compact CQRS feature file cho module Ngân sách. Pattern simplified vs PE
|
||||
// (no versioned WF, hardcoded BudgetPolicy.Default). Workflow 5 phase:
|
||||
// DangSoanThao → ChoCCM → ChoCEO → DaDuyet/TuChoi.
|
||||
|
||||
// ========== CREATE ==========
|
||||
|
||||
public record CreateBudgetCommand(
|
||||
string TenNganSach,
|
||||
string? Description,
|
||||
int NamNganSach,
|
||||
Guid ProjectId,
|
||||
Guid? DepartmentId) : IRequest<Guid>;
|
||||
|
||||
public class CreateBudgetCommandValidator : AbstractValidator<CreateBudgetCommand>
|
||||
{
|
||||
public CreateBudgetCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.TenNganSach).NotEmpty().MaximumLength(500);
|
||||
RuleFor(x => x.Description).MaximumLength(2000);
|
||||
RuleFor(x => x.ProjectId).NotEmpty();
|
||||
RuleFor(x => x.NamNganSach).InclusiveBetween(2020, 2100);
|
||||
}
|
||||
}
|
||||
|
||||
public class CreateBudgetCommandHandler(
|
||||
IApplicationDbContext db,
|
||||
ICurrentUser currentUser) : IRequestHandler<CreateBudgetCommand, Guid>
|
||||
{
|
||||
public async Task<Guid> Handle(CreateBudgetCommand request, CancellationToken ct)
|
||||
{
|
||||
_ = await db.Projects.FirstOrDefaultAsync(p => p.Id == request.ProjectId, ct)
|
||||
?? throw new NotFoundException("Project", request.ProjectId);
|
||||
|
||||
var sla = BudgetPolicies.Default.PhaseSla.GetValueOrDefault(BudgetPhase.DangSoanThao);
|
||||
var entity = new Budget
|
||||
{
|
||||
TenNganSach = request.TenNganSach,
|
||||
Description = request.Description,
|
||||
NamNganSach = request.NamNganSach,
|
||||
ProjectId = request.ProjectId,
|
||||
DepartmentId = request.DepartmentId,
|
||||
DrafterUserId = currentUser.UserId,
|
||||
Phase = BudgetPhase.DangSoanThao,
|
||||
TongNganSach = 0,
|
||||
SlaDeadline = sla is null ? null : DateTime.UtcNow.Add(sla.Value),
|
||||
// Auto-gen MaNganSach đơn giản — atomic sequence sau (Phase 8)
|
||||
MaNganSach = $"NS-{DateTime.UtcNow:yyyyMM}-{Random.Shared.Next(1000, 9999)}",
|
||||
};
|
||||
db.Budgets.Add(entity);
|
||||
|
||||
db.BudgetChangelogs.Add(new BudgetChangelog
|
||||
{
|
||||
BudgetId = entity.Id,
|
||||
EntityType = BudgetEntityType.Header,
|
||||
Action = ChangelogAction.Insert,
|
||||
PhaseAtChange = entity.Phase,
|
||||
UserId = currentUser.UserId,
|
||||
Summary = $"Tạo ngân sách {entity.MaNganSach} — {entity.TenNganSach}",
|
||||
});
|
||||
|
||||
await db.SaveChangesAsync(ct);
|
||||
return entity.Id;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== UPDATE draft ==========
|
||||
|
||||
public record UpdateBudgetDraftCommand(
|
||||
Guid Id,
|
||||
string TenNganSach,
|
||||
string? Description,
|
||||
int NamNganSach) : IRequest;
|
||||
|
||||
public class UpdateBudgetDraftCommandHandler(
|
||||
IApplicationDbContext db,
|
||||
ICurrentUser currentUser) : IRequestHandler<UpdateBudgetDraftCommand>
|
||||
{
|
||||
public async Task Handle(UpdateBudgetDraftCommand request, CancellationToken ct)
|
||||
{
|
||||
var entity = await db.Budgets.FirstOrDefaultAsync(x => x.Id == request.Id, ct)
|
||||
?? throw new NotFoundException("Budget", request.Id);
|
||||
if (entity.Phase != BudgetPhase.DangSoanThao)
|
||||
throw new ConflictException("Chỉ sửa được ngân sách khi Đang soạn thảo.");
|
||||
|
||||
entity.TenNganSach = request.TenNganSach;
|
||||
entity.Description = request.Description;
|
||||
entity.NamNganSach = request.NamNganSach;
|
||||
|
||||
db.BudgetChangelogs.Add(new BudgetChangelog
|
||||
{
|
||||
BudgetId = entity.Id,
|
||||
EntityType = BudgetEntityType.Header,
|
||||
Action = ChangelogAction.Update,
|
||||
PhaseAtChange = entity.Phase,
|
||||
UserId = currentUser.UserId,
|
||||
Summary = "Cập nhật ngân sách",
|
||||
});
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== TRANSITION ==========
|
||||
|
||||
public record TransitionBudgetCommand(
|
||||
Guid Id, BudgetPhase TargetPhase, ApprovalDecision Decision, string? Comment) : IRequest;
|
||||
|
||||
public class TransitionBudgetCommandHandler(
|
||||
IApplicationDbContext db,
|
||||
ICurrentUser currentUser,
|
||||
UserManager<User> userManager) : IRequestHandler<TransitionBudgetCommand>
|
||||
{
|
||||
public async Task Handle(TransitionBudgetCommand request, CancellationToken ct)
|
||||
{
|
||||
if (!currentUser.IsAuthenticated || currentUser.UserId is null)
|
||||
throw new UnauthorizedException();
|
||||
|
||||
var entity = await db.Budgets.FirstOrDefaultAsync(x => x.Id == request.Id, ct)
|
||||
?? throw new NotFoundException("Budget", request.Id);
|
||||
|
||||
var policy = BudgetPolicies.Default;
|
||||
var isAdmin = currentUser.Roles.Contains(AppRoles.Admin);
|
||||
|
||||
if (!isAdmin && !policy.IsTransitionAllowed(entity.Phase, request.TargetPhase, currentUser.Roles))
|
||||
throw new ForbiddenException(
|
||||
$"Role không đủ quyền chuyển {entity.Phase} → {request.TargetPhase}.");
|
||||
|
||||
var fromPhase = entity.Phase;
|
||||
entity.SlaWarningSent = false;
|
||||
entity.Phase = request.TargetPhase;
|
||||
var sla = policy.PhaseSla.GetValueOrDefault(request.TargetPhase);
|
||||
entity.SlaDeadline = sla is null ? null : DateTime.UtcNow.Add(sla.Value);
|
||||
|
||||
db.BudgetApprovals.Add(new BudgetApproval
|
||||
{
|
||||
BudgetId = entity.Id,
|
||||
FromPhase = fromPhase,
|
||||
ToPhase = request.TargetPhase,
|
||||
ApproverUserId = currentUser.UserId,
|
||||
Decision = request.Decision,
|
||||
Comment = request.Comment,
|
||||
ApprovedAt = DateTime.UtcNow,
|
||||
});
|
||||
|
||||
string? actorName = null;
|
||||
if (currentUser.UserId is Guid uid)
|
||||
{
|
||||
var u = await userManager.FindByIdAsync(uid.ToString());
|
||||
actorName = u?.FullName ?? u?.Email;
|
||||
}
|
||||
db.BudgetChangelogs.Add(new BudgetChangelog
|
||||
{
|
||||
BudgetId = entity.Id,
|
||||
EntityType = BudgetEntityType.Workflow,
|
||||
Action = ChangelogAction.Transition,
|
||||
PhaseAtChange = request.TargetPhase,
|
||||
UserId = currentUser.UserId,
|
||||
UserName = actorName ?? "Hệ thống",
|
||||
Summary = $"Chuyển phase {fromPhase} → {request.TargetPhase}",
|
||||
ContextNote = request.Comment,
|
||||
});
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== LIST ==========
|
||||
|
||||
public record ListBudgetsQuery(
|
||||
BudgetPhase? Phase = null,
|
||||
Guid? ProjectId = null,
|
||||
int? NamNganSach = null) : PagedRequest, IRequest<PagedResult<BudgetListItemDto>>;
|
||||
|
||||
public class ListBudgetsQueryHandler(
|
||||
IApplicationDbContext db) : IRequestHandler<ListBudgetsQuery, PagedResult<BudgetListItemDto>>
|
||||
{
|
||||
public async Task<PagedResult<BudgetListItemDto>> Handle(ListBudgetsQuery request, CancellationToken ct)
|
||||
{
|
||||
var q = from e in db.Budgets.AsNoTracking()
|
||||
join p in db.Projects.AsNoTracking() on e.ProjectId equals p.Id
|
||||
select new { e, p };
|
||||
|
||||
if (request.Phase is not null) q = q.Where(x => x.e.Phase == request.Phase);
|
||||
if (request.ProjectId is not null) q = q.Where(x => x.e.ProjectId == request.ProjectId);
|
||||
if (request.NamNganSach is not null) q = q.Where(x => x.e.NamNganSach == request.NamNganSach);
|
||||
if (!string.IsNullOrWhiteSpace(request.Search))
|
||||
{
|
||||
var s = request.Search.Trim();
|
||||
q = q.Where(x => (x.e.MaNganSach != null && x.e.MaNganSach.Contains(s))
|
||||
|| x.e.TenNganSach.Contains(s) || x.p.Name.Contains(s));
|
||||
}
|
||||
q = request.SortDesc ? q.OrderByDescending(x => x.e.CreatedAt) : q.OrderBy(x => x.e.CreatedAt);
|
||||
|
||||
var total = await q.CountAsync(ct);
|
||||
var items = await q.Skip((request.Page - 1) * request.PageSize).Take(request.PageSize)
|
||||
.Select(x => new BudgetListItemDto(
|
||||
x.e.Id, x.e.MaNganSach, x.e.TenNganSach, x.e.NamNganSach, x.e.Phase,
|
||||
x.e.ProjectId, x.p.Name, x.e.TongNganSach, x.e.SlaDeadline, x.e.CreatedAt))
|
||||
.ToListAsync(ct);
|
||||
return new PagedResult<BudgetListItemDto>(items, total, request.Page, request.PageSize);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== GET detail bundle ==========
|
||||
|
||||
public record GetBudgetQuery(Guid Id) : IRequest<BudgetDetailBundleDto>;
|
||||
|
||||
public class GetBudgetQueryHandler(
|
||||
IApplicationDbContext db,
|
||||
UserManager<User> userManager) : IRequestHandler<GetBudgetQuery, BudgetDetailBundleDto>
|
||||
{
|
||||
public async Task<BudgetDetailBundleDto> Handle(GetBudgetQuery request, CancellationToken ct)
|
||||
{
|
||||
var e = await db.Budgets.AsNoTracking()
|
||||
.Include(x => x.Details)
|
||||
.Include(x => x.Approvals)
|
||||
.FirstOrDefaultAsync(x => x.Id == request.Id, ct)
|
||||
?? throw new NotFoundException("Budget", request.Id);
|
||||
|
||||
var project = await db.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == e.ProjectId, ct);
|
||||
var department = e.DepartmentId is null ? null : await db.Departments.AsNoTracking()
|
||||
.FirstOrDefaultAsync(d => d.Id == e.DepartmentId, ct);
|
||||
|
||||
var userIds = new HashSet<Guid>();
|
||||
if (e.DrafterUserId is Guid did) userIds.Add(did);
|
||||
foreach (var a in e.Approvals) if (a.ApproverUserId is Guid aid) userIds.Add(aid);
|
||||
var users = await userManager.Users.AsNoTracking()
|
||||
.Where(u => userIds.Contains(u.Id))
|
||||
.ToDictionaryAsync(u => u.Id, u => u.FullName, ct);
|
||||
|
||||
var policy = BudgetPolicies.Default;
|
||||
|
||||
return new BudgetDetailBundleDto(
|
||||
e.Id, e.MaNganSach, e.TenNganSach, e.Description, e.NamNganSach, e.Phase,
|
||||
e.ProjectId, project?.Name ?? "",
|
||||
e.DepartmentId, department?.Name,
|
||||
e.DrafterUserId, e.DrafterUserId is Guid d && users.TryGetValue(d, out var dn) ? dn : null,
|
||||
e.TongNganSach, e.SlaDeadline, e.CreatedAt, e.UpdatedAt,
|
||||
e.Details.OrderBy(d => d.Order).Select(d => new BudgetDetailDto(
|
||||
d.Id, d.GroupCode, d.GroupName, d.ItemCode, d.NoiDung, d.DonViTinh,
|
||||
d.KhoiLuong, d.DonGia, d.ThanhTien, d.Order, d.GhiChu)).ToList(),
|
||||
e.Approvals.OrderBy(a => a.ApprovedAt).Select(a => new BudgetApprovalDto(
|
||||
a.Id, a.FromPhase, a.ToPhase, a.ApproverUserId,
|
||||
a.ApproverUserId is Guid uid && users.TryGetValue(uid, out var an) ? an : null,
|
||||
a.Decision, a.Comment, a.ApprovedAt)).ToList(),
|
||||
new BudgetWorkflowSummaryDto(
|
||||
policy.Name, policy.Description,
|
||||
policy.ActivePhases.ToList(),
|
||||
policy.NextPhasesFrom(e.Phase).ToList()));
|
||||
}
|
||||
}
|
||||
|
||||
// ========== DELETE ==========
|
||||
|
||||
public record DeleteBudgetCommand(Guid Id) : IRequest;
|
||||
|
||||
public class DeleteBudgetCommandHandler(
|
||||
IApplicationDbContext db) : IRequestHandler<DeleteBudgetCommand>
|
||||
{
|
||||
public async Task Handle(DeleteBudgetCommand request, CancellationToken ct)
|
||||
{
|
||||
var entity = await db.Budgets.FirstOrDefaultAsync(x => x.Id == request.Id, ct)
|
||||
?? throw new NotFoundException("Budget", request.Id);
|
||||
if (entity.Phase != BudgetPhase.DangSoanThao && entity.Phase != BudgetPhase.TuChoi)
|
||||
throw new ConflictException("Chỉ xóa được ngân sách phase Soạn thảo / Từ chối.");
|
||||
db.Budgets.Remove(entity);
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Detail CRUD ==========
|
||||
|
||||
public record AddBudgetDetailCommand(
|
||||
Guid BudgetId, string GroupCode, string GroupName, string? ItemCode, string NoiDung,
|
||||
string? DonViTinh, decimal KhoiLuong, decimal DonGia, decimal ThanhTien, string? GhiChu) : IRequest<Guid>;
|
||||
|
||||
public class AddBudgetDetailCommandHandler(
|
||||
IApplicationDbContext db,
|
||||
ICurrentUser currentUser) : IRequestHandler<AddBudgetDetailCommand, Guid>
|
||||
{
|
||||
public async Task<Guid> Handle(AddBudgetDetailCommand request, CancellationToken ct)
|
||||
{
|
||||
var bg = await db.Budgets.FirstOrDefaultAsync(x => x.Id == request.BudgetId, ct)
|
||||
?? throw new NotFoundException("Budget", request.BudgetId);
|
||||
var maxOrder = await db.BudgetDetails.Where(d => d.BudgetId == bg.Id)
|
||||
.Select(d => (int?)d.Order).MaxAsync(ct);
|
||||
var entity = new BudgetDetail
|
||||
{
|
||||
BudgetId = bg.Id,
|
||||
GroupCode = request.GroupCode, GroupName = request.GroupName,
|
||||
ItemCode = request.ItemCode, NoiDung = request.NoiDung, DonViTinh = request.DonViTinh,
|
||||
KhoiLuong = request.KhoiLuong, DonGia = request.DonGia, ThanhTien = request.ThanhTien,
|
||||
GhiChu = request.GhiChu, Order = (maxOrder ?? 0) + 1,
|
||||
};
|
||||
db.BudgetDetails.Add(entity);
|
||||
|
||||
// Recompute TongNganSach
|
||||
bg.TongNganSach = await db.BudgetDetails.Where(d => d.BudgetId == bg.Id)
|
||||
.SumAsync(d => d.ThanhTien, ct) + entity.ThanhTien;
|
||||
|
||||
db.BudgetChangelogs.Add(new BudgetChangelog
|
||||
{
|
||||
BudgetId = bg.Id, EntityType = BudgetEntityType.Detail, EntityId = entity.Id,
|
||||
Action = ChangelogAction.Insert, PhaseAtChange = bg.Phase,
|
||||
UserId = currentUser.UserId,
|
||||
Summary = $"Thêm hạng mục {request.GroupCode} — {request.NoiDung}",
|
||||
});
|
||||
await db.SaveChangesAsync(ct);
|
||||
return entity.Id;
|
||||
}
|
||||
}
|
||||
|
||||
public record UpdateBudgetDetailCommand(
|
||||
Guid BudgetId, Guid DetailId, string GroupCode, string GroupName, string? ItemCode, string NoiDung,
|
||||
string? DonViTinh, decimal KhoiLuong, decimal DonGia, decimal ThanhTien, string? GhiChu) : IRequest;
|
||||
|
||||
public class UpdateBudgetDetailCommandHandler(
|
||||
IApplicationDbContext db) : IRequestHandler<UpdateBudgetDetailCommand>
|
||||
{
|
||||
public async Task Handle(UpdateBudgetDetailCommand request, CancellationToken ct)
|
||||
{
|
||||
var entity = await db.BudgetDetails
|
||||
.FirstOrDefaultAsync(d => d.Id == request.DetailId && d.BudgetId == request.BudgetId, ct)
|
||||
?? throw new NotFoundException("BudgetDetail", request.DetailId);
|
||||
entity.GroupCode = request.GroupCode; entity.GroupName = request.GroupName;
|
||||
entity.ItemCode = request.ItemCode; entity.NoiDung = request.NoiDung; entity.DonViTinh = request.DonViTinh;
|
||||
entity.KhoiLuong = request.KhoiLuong; entity.DonGia = request.DonGia; entity.ThanhTien = request.ThanhTien;
|
||||
entity.GhiChu = request.GhiChu;
|
||||
|
||||
var bg = await db.Budgets.FirstOrDefaultAsync(b => b.Id == request.BudgetId, ct);
|
||||
if (bg != null)
|
||||
bg.TongNganSach = await db.BudgetDetails.Where(d => d.BudgetId == bg.Id)
|
||||
.SumAsync(d => d.ThanhTien, ct);
|
||||
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
public record DeleteBudgetDetailCommand(Guid BudgetId, Guid DetailId) : IRequest;
|
||||
|
||||
public class DeleteBudgetDetailCommandHandler(
|
||||
IApplicationDbContext db) : IRequestHandler<DeleteBudgetDetailCommand>
|
||||
{
|
||||
public async Task Handle(DeleteBudgetDetailCommand request, CancellationToken ct)
|
||||
{
|
||||
var entity = await db.BudgetDetails
|
||||
.FirstOrDefaultAsync(d => d.Id == request.DetailId && d.BudgetId == request.BudgetId, ct)
|
||||
?? throw new NotFoundException("BudgetDetail", request.DetailId);
|
||||
db.BudgetDetails.Remove(entity);
|
||||
|
||||
var bg = await db.Budgets.FirstOrDefaultAsync(b => b.Id == request.BudgetId, ct);
|
||||
if (bg != null)
|
||||
bg.TongNganSach = await db.BudgetDetails.Where(d => d.BudgetId == bg.Id && d.Id != entity.Id)
|
||||
.SumAsync(d => d.ThanhTien, ct);
|
||||
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== CHANGELOG list ==========
|
||||
|
||||
public record ListBudgetChangelogsQuery(Guid BudgetId, int Take = 200) : IRequest<List<BudgetChangelogDto>>;
|
||||
|
||||
public class ListBudgetChangelogsQueryHandler(IApplicationDbContext db)
|
||||
: IRequestHandler<ListBudgetChangelogsQuery, List<BudgetChangelogDto>>
|
||||
{
|
||||
public async Task<List<BudgetChangelogDto>> Handle(ListBudgetChangelogsQuery request, CancellationToken ct) =>
|
||||
await db.BudgetChangelogs.AsNoTracking()
|
||||
.Where(c => c.BudgetId == request.BudgetId)
|
||||
.OrderByDescending(c => c.CreatedAt)
|
||||
.Take(request.Take)
|
||||
.Select(c => new BudgetChangelogDto(
|
||||
c.Id, c.EntityType, c.EntityId, c.Action, c.PhaseAtChange,
|
||||
c.UserId, c.UserName, c.Summary, c.FieldChangesJson, c.ContextNote, c.CreatedAt))
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
@ -0,0 +1,79 @@
|
||||
using SolutionErp.Domain.Budgets;
|
||||
using SolutionErp.Domain.Contracts;
|
||||
|
||||
namespace SolutionErp.Application.Budgets.Dtos;
|
||||
|
||||
public record BudgetListItemDto(
|
||||
Guid Id,
|
||||
string? MaNganSach,
|
||||
string TenNganSach,
|
||||
int NamNganSach,
|
||||
BudgetPhase Phase,
|
||||
Guid ProjectId,
|
||||
string ProjectName,
|
||||
decimal TongNganSach,
|
||||
DateTime? SlaDeadline,
|
||||
DateTime CreatedAt);
|
||||
|
||||
public record BudgetDetailDto(
|
||||
Guid Id,
|
||||
string GroupCode,
|
||||
string GroupName,
|
||||
string? ItemCode,
|
||||
string NoiDung,
|
||||
string? DonViTinh,
|
||||
decimal KhoiLuong,
|
||||
decimal DonGia,
|
||||
decimal ThanhTien,
|
||||
int Order,
|
||||
string? GhiChu);
|
||||
|
||||
public record BudgetApprovalDto(
|
||||
Guid Id,
|
||||
BudgetPhase FromPhase,
|
||||
BudgetPhase ToPhase,
|
||||
Guid? ApproverUserId,
|
||||
string? ApproverName,
|
||||
ApprovalDecision Decision,
|
||||
string? Comment,
|
||||
DateTime ApprovedAt);
|
||||
|
||||
public record BudgetChangelogDto(
|
||||
Guid Id,
|
||||
BudgetEntityType EntityType,
|
||||
Guid? EntityId,
|
||||
ChangelogAction Action,
|
||||
BudgetPhase? PhaseAtChange,
|
||||
Guid? UserId,
|
||||
string? UserName,
|
||||
string? Summary,
|
||||
string? FieldChangesJson,
|
||||
string? ContextNote,
|
||||
DateTime CreatedAt);
|
||||
|
||||
public record BudgetWorkflowSummaryDto(
|
||||
string PolicyName,
|
||||
string PolicyDescription,
|
||||
List<BudgetPhase> ActivePhases,
|
||||
List<BudgetPhase> NextPhases);
|
||||
|
||||
public record BudgetDetailBundleDto(
|
||||
Guid Id,
|
||||
string? MaNganSach,
|
||||
string TenNganSach,
|
||||
string? Description,
|
||||
int NamNganSach,
|
||||
BudgetPhase Phase,
|
||||
Guid ProjectId,
|
||||
string ProjectName,
|
||||
Guid? DepartmentId,
|
||||
string? DepartmentName,
|
||||
Guid? DrafterUserId,
|
||||
string? DrafterName,
|
||||
decimal TongNganSach,
|
||||
DateTime? SlaDeadline,
|
||||
DateTime CreatedAt,
|
||||
DateTime? UpdatedAt,
|
||||
List<BudgetDetailDto> Details,
|
||||
List<BudgetApprovalDto> Approvals,
|
||||
BudgetWorkflowSummaryDto Workflow);
|
||||
@ -1,4 +1,5 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SolutionErp.Domain.Budgets;
|
||||
using SolutionErp.Domain.Contracts;
|
||||
using SolutionErp.Domain.Contracts.Details;
|
||||
using SolutionErp.Domain.Forms;
|
||||
@ -58,5 +59,11 @@ public interface IApplicationDbContext
|
||||
DbSet<PurchaseEvaluationWorkflowStepApprover> PurchaseEvaluationWorkflowStepApprovers { get; }
|
||||
DbSet<PurchaseEvaluationCodeSequence> PurchaseEvaluationCodeSequences { get; }
|
||||
|
||||
// Module Ngân sách (Phase 7)
|
||||
DbSet<Budget> Budgets { get; }
|
||||
DbSet<BudgetDetail> BudgetDetails { get; }
|
||||
DbSet<BudgetApproval> BudgetApprovals { get; }
|
||||
DbSet<BudgetChangelog> BudgetChangelogs { get; }
|
||||
|
||||
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
29
src/Backend/SolutionErp.Domain/Budgets/Budget.cs
Normal file
29
src/Backend/SolutionErp.Domain/Budgets/Budget.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using SolutionErp.Domain.Common;
|
||||
|
||||
namespace SolutionErp.Domain.Budgets;
|
||||
|
||||
// Aggregate root quản lý ngân sách. Gắn với Project (required), reference
|
||||
// từ PurchaseEvaluation + Contract (cả 2 nullable FK Budget.Id).
|
||||
//
|
||||
// Workflow đơn giản 3-step: Drafter → CCM → CEO. Pattern hardcoded trong
|
||||
// BudgetPolicy (chưa versioned, tương lai có thể thêm BudgetWorkflowDefinition
|
||||
// nếu cần admin config).
|
||||
public class Budget : AuditableEntity
|
||||
{
|
||||
public string? MaNganSach { get; set; } // Auto-gen NS-YYYYMM-XXXX
|
||||
public string TenNganSach { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
public int NamNganSach { get; set; } // Năm áp dụng (vd 2026)
|
||||
public Guid ProjectId { get; set; } // FK Projects (required)
|
||||
public Guid? DepartmentId { get; set; }
|
||||
public Guid? DrafterUserId { get; set; }
|
||||
|
||||
public BudgetPhase Phase { get; set; } = BudgetPhase.DangSoanThao;
|
||||
public decimal TongNganSach { get; set; } // Tổng = sum BudgetDetails.ThanhTien (computed)
|
||||
public DateTime? SlaDeadline { get; set; }
|
||||
public bool SlaWarningSent { get; set; }
|
||||
|
||||
public List<BudgetDetail> Details { get; set; } = new();
|
||||
public List<BudgetApproval> Approvals { get; set; } = new();
|
||||
public List<BudgetChangelog> Changelogs { get; set; } = new();
|
||||
}
|
||||
17
src/Backend/SolutionErp.Domain/Budgets/BudgetApproval.cs
Normal file
17
src/Backend/SolutionErp.Domain/Budgets/BudgetApproval.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using SolutionErp.Domain.Common;
|
||||
using SolutionErp.Domain.Contracts; // reuse ApprovalDecision
|
||||
|
||||
namespace SolutionErp.Domain.Budgets;
|
||||
|
||||
public class BudgetApproval : BaseEntity
|
||||
{
|
||||
public Guid BudgetId { get; set; }
|
||||
public BudgetPhase FromPhase { get; set; }
|
||||
public BudgetPhase ToPhase { get; set; }
|
||||
public Guid? ApproverUserId { get; set; } // null = system SLA auto
|
||||
public ApprovalDecision Decision { get; set; }
|
||||
public string? Comment { get; set; }
|
||||
public DateTime ApprovedAt { get; set; }
|
||||
|
||||
public Budget? Budget { get; set; }
|
||||
}
|
||||
28
src/Backend/SolutionErp.Domain/Budgets/BudgetChangelog.cs
Normal file
28
src/Backend/SolutionErp.Domain/Budgets/BudgetChangelog.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using SolutionErp.Domain.Common;
|
||||
using SolutionErp.Domain.Contracts; // reuse ChangelogAction
|
||||
|
||||
namespace SolutionErp.Domain.Budgets;
|
||||
|
||||
// Audit log unified cho mọi thay đổi trên ngân sách.
|
||||
public class BudgetChangelog : BaseEntity
|
||||
{
|
||||
public Guid BudgetId { get; set; }
|
||||
public Budget? Budget { get; set; }
|
||||
|
||||
public BudgetEntityType EntityType { get; set; }
|
||||
public Guid? EntityId { get; set; }
|
||||
public ChangelogAction Action { get; set; }
|
||||
public BudgetPhase? PhaseAtChange { get; set; }
|
||||
public Guid? UserId { get; set; }
|
||||
public string? UserName { get; set; }
|
||||
public string? Summary { get; set; }
|
||||
public string? FieldChangesJson { get; set; }
|
||||
public string? ContextNote { get; set; }
|
||||
}
|
||||
|
||||
public enum BudgetEntityType
|
||||
{
|
||||
Header = 1,
|
||||
Detail = 2,
|
||||
Workflow = 3,
|
||||
}
|
||||
22
src/Backend/SolutionErp.Domain/Budgets/BudgetDetail.cs
Normal file
22
src/Backend/SolutionErp.Domain/Budgets/BudgetDetail.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using SolutionErp.Domain.Common;
|
||||
|
||||
namespace SolutionErp.Domain.Budgets;
|
||||
|
||||
// Chi tiết ngân sách — pattern flat row giống PurchaseEvaluationDetail.
|
||||
// Group A.I/A.II/... cho hạng mục cha, NoiDung cho item con.
|
||||
public class BudgetDetail : BaseEntity
|
||||
{
|
||||
public Guid BudgetId { get; set; }
|
||||
public string GroupCode { get; set; } = string.Empty; // "A.I", "A.II", ...
|
||||
public string GroupName { get; set; } = string.Empty; // "Bê tông", "Phụ gia", ...
|
||||
public string? ItemCode { get; set; }
|
||||
public string NoiDung { get; set; } = string.Empty;
|
||||
public string? DonViTinh { get; set; }
|
||||
public decimal KhoiLuong { get; set; }
|
||||
public decimal DonGia { get; set; }
|
||||
public decimal ThanhTien { get; set; } // = KhoiLuong × DonGia (hoặc nhập tay)
|
||||
public int Order { get; set; }
|
||||
public string? GhiChu { get; set; }
|
||||
|
||||
public Budget? Budget { get; set; }
|
||||
}
|
||||
14
src/Backend/SolutionErp.Domain/Budgets/BudgetPhase.cs
Normal file
14
src/Backend/SolutionErp.Domain/Budgets/BudgetPhase.cs
Normal file
@ -0,0 +1,14 @@
|
||||
namespace SolutionErp.Domain.Budgets;
|
||||
|
||||
// State machine ngân sách — đơn giản 3 bước duyệt + 2 terminal.
|
||||
// DangSoanThao → ChoCCM → ChoCEO → DaDuyet
|
||||
// Bất kỳ phase duyệt → DangSoanThao (reject)
|
||||
// DangSoanThao → TuChoi
|
||||
public enum BudgetPhase
|
||||
{
|
||||
DangSoanThao = 1,
|
||||
ChoCCM = 2,
|
||||
ChoCEO = 3,
|
||||
DaDuyet = 4,
|
||||
TuChoi = 99,
|
||||
}
|
||||
63
src/Backend/SolutionErp.Domain/Budgets/BudgetPolicy.cs
Normal file
63
src/Backend/SolutionErp.Domain/Budgets/BudgetPolicy.cs
Normal file
@ -0,0 +1,63 @@
|
||||
using SolutionErp.Domain.Identity;
|
||||
|
||||
namespace SolutionErp.Domain.Budgets;
|
||||
|
||||
// Policy hardcoded đơn giản — chưa versioned (theo user "tạm thời simple
|
||||
// default"). Tương lai nếu admin cần config qua UI: thêm BudgetWorkflow
|
||||
// Definition tables tương tự PE workflow.
|
||||
public sealed record BudgetPolicy(
|
||||
string Name,
|
||||
string Description,
|
||||
IReadOnlyDictionary<(BudgetPhase From, BudgetPhase To), string[]> Transitions,
|
||||
IReadOnlyDictionary<BudgetPhase, TimeSpan?> PhaseSla,
|
||||
IReadOnlyList<BudgetPhase> ActivePhases)
|
||||
{
|
||||
public bool HasPhase(BudgetPhase phase) => ActivePhases.Contains(phase);
|
||||
|
||||
public bool IsTransitionAllowed(
|
||||
BudgetPhase from, BudgetPhase to,
|
||||
IReadOnlyList<string> actorRoles)
|
||||
{
|
||||
if (!Transitions.TryGetValue((from, to), out var roles)) return false;
|
||||
return actorRoles.Any(r => roles.Contains(r));
|
||||
}
|
||||
|
||||
public IReadOnlyList<BudgetPhase> NextPhasesFrom(BudgetPhase from) =>
|
||||
Transitions.Keys.Where(k => k.From == from).Select(k => k.To).Distinct().ToList();
|
||||
}
|
||||
|
||||
public static class BudgetPolicies
|
||||
{
|
||||
private static readonly Dictionary<BudgetPhase, TimeSpan?> DefaultSla = new()
|
||||
{
|
||||
[BudgetPhase.DangSoanThao] = TimeSpan.FromDays(5),
|
||||
[BudgetPhase.ChoCCM] = TimeSpan.FromDays(3),
|
||||
[BudgetPhase.ChoCEO] = TimeSpan.FromDays(2),
|
||||
[BudgetPhase.DaDuyet] = null,
|
||||
[BudgetPhase.TuChoi] = null,
|
||||
};
|
||||
|
||||
public static readonly BudgetPolicy Default = new(
|
||||
Name: "Default",
|
||||
Description: "Quy trình ngân sách 3-step (Drafter → CCM → CEO).",
|
||||
Transitions: new Dictionary<(BudgetPhase, BudgetPhase), string[]>
|
||||
{
|
||||
[(BudgetPhase.DangSoanThao, BudgetPhase.ChoCCM)] = [AppRoles.Drafter, AppRoles.DeptManager],
|
||||
[(BudgetPhase.DangSoanThao, BudgetPhase.TuChoi)] = [AppRoles.Drafter, AppRoles.DeptManager],
|
||||
|
||||
[(BudgetPhase.ChoCCM, BudgetPhase.ChoCEO)] = [AppRoles.CostControl],
|
||||
[(BudgetPhase.ChoCCM, BudgetPhase.DangSoanThao)] = [AppRoles.CostControl],
|
||||
|
||||
[(BudgetPhase.ChoCEO, BudgetPhase.DaDuyet)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
|
||||
[(BudgetPhase.ChoCEO, BudgetPhase.DangSoanThao)] = [AppRoles.Director, AppRoles.AuthorizedSigner],
|
||||
},
|
||||
PhaseSla: DefaultSla,
|
||||
ActivePhases:
|
||||
[
|
||||
BudgetPhase.DangSoanThao,
|
||||
BudgetPhase.ChoCCM,
|
||||
BudgetPhase.ChoCEO,
|
||||
BudgetPhase.DaDuyet,
|
||||
BudgetPhase.TuChoi,
|
||||
]);
|
||||
}
|
||||
@ -23,6 +23,7 @@ public class Contract : AuditableEntity
|
||||
public DateTime? SlaDeadline { get; set; } // Hết hạn phase hiện tại
|
||||
public string? DraftData { get; set; } // JSON field values (render template)
|
||||
public bool SlaWarningSent { get; set; } // Flag để không gửi warning 2 lần
|
||||
public Guid? BudgetId { get; set; } // Reference Budget (Phase 7) — đối chiếu chi phí HĐ vs ngân sách
|
||||
|
||||
public List<ContractApproval> Approvals { get; set; } = new();
|
||||
public List<ContractComment> Comments { get; set; } = new();
|
||||
|
||||
@ -51,6 +51,15 @@ public static class MenuKeys
|
||||
public const string PurchaseEvaluations = "PurchaseEvaluations"; // root group
|
||||
public const string PeWorkflows = "PeWorkflows"; // workflow admin root
|
||||
|
||||
// ============================================================
|
||||
// Module Ngân sách (Phase 7) — 4 bảng quản lý ngân sách dự án/gói thầu.
|
||||
// 1 root + 3 leaf action (Danh sách / Thao tác / Duyệt).
|
||||
// ============================================================
|
||||
public const string Budgets = "Budgets";
|
||||
public const string BudgetList = "Bg_List";
|
||||
public const string BudgetCreate = "Bg_Create";
|
||||
public const string BudgetPending = "Bg_Pending";
|
||||
|
||||
public static readonly string[] PurchaseEvaluationTypeCodes =
|
||||
["DuyetNcc", "DuyetNccPhuongAn"];
|
||||
|
||||
@ -70,6 +79,7 @@ public static class MenuKeys
|
||||
Catalogs, CatalogUnits, CatalogMaterials, CatalogServices, CatalogWorkItems,
|
||||
Contracts, Forms, Reports,
|
||||
PurchaseEvaluations,
|
||||
Budgets, BudgetList, BudgetCreate, BudgetPending,
|
||||
System, Users, Roles, Permissions, Workflows, PeWorkflows,
|
||||
];
|
||||
|
||||
|
||||
@ -26,6 +26,7 @@ public class PurchaseEvaluation : AuditableEntity
|
||||
public string? PaymentTerms { get; set; } // JSON {tamUng, thanhToanTam, quyetToan, baoHanh, hanMucCongNo, danhGia}
|
||||
|
||||
public Guid? ContractId { get; set; } // FK Contracts — set khi user gen HĐ từ phiếu
|
||||
public Guid? BudgetId { get; set; } // FK Budget (Phase 7) — đối chiếu báo giá vs ngân sách gói thầu
|
||||
|
||||
public List<PurchaseEvaluationSupplier> Suppliers { get; set; } = new();
|
||||
public List<PurchaseEvaluationDetail> Details { get; set; } = new();
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SolutionErp.Application.Common.Interfaces;
|
||||
using SolutionErp.Domain.Budgets;
|
||||
using SolutionErp.Domain.Contracts;
|
||||
using SolutionErp.Domain.Contracts.Details;
|
||||
using SolutionErp.Domain.Forms;
|
||||
@ -59,6 +60,12 @@ public class ApplicationDbContext
|
||||
public DbSet<PurchaseEvaluationWorkflowStepApprover> PurchaseEvaluationWorkflowStepApprovers => Set<PurchaseEvaluationWorkflowStepApprover>();
|
||||
public DbSet<PurchaseEvaluationCodeSequence> PurchaseEvaluationCodeSequences => Set<PurchaseEvaluationCodeSequence>();
|
||||
|
||||
// Module Ngân sách (Phase 7) — 4 bảng: Budget header + Details + Approvals + Changelogs.
|
||||
public DbSet<Budget> Budgets => Set<Budget>();
|
||||
public DbSet<BudgetDetail> BudgetDetails => Set<BudgetDetail>();
|
||||
public DbSet<BudgetApproval> BudgetApprovals => Set<BudgetApproval>();
|
||||
public DbSet<BudgetChangelog> BudgetChangelogs => Set<BudgetChangelog>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
base.OnModelCreating(builder);
|
||||
|
||||
@ -0,0 +1,89 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using SolutionErp.Domain.Budgets;
|
||||
|
||||
namespace SolutionErp.Infrastructure.Persistence.Configurations;
|
||||
|
||||
public class BudgetConfiguration : IEntityTypeConfiguration<Budget>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Budget> b)
|
||||
{
|
||||
b.ToTable("Budgets");
|
||||
b.HasKey(x => x.Id);
|
||||
|
||||
b.Property(x => x.MaNganSach).HasMaxLength(100);
|
||||
b.Property(x => x.TenNganSach).HasMaxLength(500).IsRequired();
|
||||
b.Property(x => x.Description).HasMaxLength(2000);
|
||||
b.Property(x => x.Phase).HasConversion<int>();
|
||||
b.Property(x => x.TongNganSach).HasPrecision(18, 2);
|
||||
|
||||
b.HasIndex(x => x.MaNganSach).IsUnique().HasFilter("[MaNganSach] IS NOT NULL");
|
||||
b.HasIndex(x => new { x.Phase, x.IsDeleted });
|
||||
b.HasIndex(x => x.ProjectId);
|
||||
b.HasIndex(x => x.NamNganSach);
|
||||
b.HasIndex(x => x.SlaDeadline);
|
||||
|
||||
b.HasMany(x => x.Details).WithOne(d => d.Budget).HasForeignKey(d => d.BudgetId).OnDelete(DeleteBehavior.Cascade);
|
||||
b.HasMany(x => x.Approvals).WithOne(a => a.Budget).HasForeignKey(a => a.BudgetId).OnDelete(DeleteBehavior.Cascade);
|
||||
b.HasMany(x => x.Changelogs).WithOne(c => c.Budget).HasForeignKey(c => c.BudgetId).OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasQueryFilter(x => !x.IsDeleted);
|
||||
}
|
||||
}
|
||||
|
||||
public class BudgetDetailConfiguration : IEntityTypeConfiguration<BudgetDetail>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<BudgetDetail> b)
|
||||
{
|
||||
b.ToTable("BudgetDetails");
|
||||
b.HasKey(x => x.Id);
|
||||
|
||||
b.Property(x => x.GroupCode).HasMaxLength(50).IsRequired();
|
||||
b.Property(x => x.GroupName).HasMaxLength(200).IsRequired();
|
||||
b.Property(x => x.ItemCode).HasMaxLength(100);
|
||||
b.Property(x => x.NoiDung).HasMaxLength(500).IsRequired();
|
||||
b.Property(x => x.DonViTinh).HasMaxLength(50);
|
||||
b.Property(x => x.GhiChu).HasMaxLength(1000);
|
||||
b.Property(x => x.KhoiLuong).HasPrecision(18, 4);
|
||||
b.Property(x => x.DonGia).HasPrecision(18, 2);
|
||||
b.Property(x => x.ThanhTien).HasPrecision(18, 2);
|
||||
|
||||
b.HasIndex(x => new { x.BudgetId, x.Order });
|
||||
}
|
||||
}
|
||||
|
||||
public class BudgetApprovalConfiguration : IEntityTypeConfiguration<BudgetApproval>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<BudgetApproval> b)
|
||||
{
|
||||
b.ToTable("BudgetApprovals");
|
||||
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.BudgetId, x.ApprovedAt });
|
||||
}
|
||||
}
|
||||
|
||||
public class BudgetChangelogConfiguration : IEntityTypeConfiguration<BudgetChangelog>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<BudgetChangelog> b)
|
||||
{
|
||||
b.ToTable("BudgetChangelogs");
|
||||
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(500);
|
||||
b.Property(x => x.ContextNote).HasMaxLength(2000);
|
||||
b.Property(x => x.FieldChangesJson).HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasIndex(x => new { x.BudgetId, x.CreatedAt });
|
||||
b.HasIndex(x => new { x.BudgetId, x.EntityType });
|
||||
}
|
||||
}
|
||||
@ -24,6 +24,7 @@ public class ContractConfiguration : IEntityTypeConfiguration<Contract>
|
||||
b.HasIndex(x => x.SupplierId);
|
||||
b.HasIndex(x => x.ProjectId);
|
||||
b.HasIndex(x => x.SlaDeadline);
|
||||
b.HasIndex(x => x.BudgetId);
|
||||
|
||||
b.HasMany(x => x.Approvals).WithOne(a => a.Contract).HasForeignKey(a => a.ContractId).OnDelete(DeleteBehavior.Cascade);
|
||||
b.HasMany(x => x.Comments).WithOne(c => c.Contract).HasForeignKey(c => c.ContractId).OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
@ -25,6 +25,7 @@ public class PurchaseEvaluationConfiguration : IEntityTypeConfiguration<Purchase
|
||||
b.HasIndex(x => x.SlaDeadline);
|
||||
b.HasIndex(x => x.WorkflowDefinitionId);
|
||||
b.HasIndex(x => x.ContractId);
|
||||
b.HasIndex(x => x.BudgetId);
|
||||
|
||||
b.HasMany(x => x.Suppliers).WithOne(s => s.PurchaseEvaluation).HasForeignKey(s => s.PurchaseEvaluationId).OnDelete(DeleteBehavior.Cascade);
|
||||
b.HasMany(x => x.Details).WithOne(d => d.PurchaseEvaluation).HasForeignKey(d => d.PurchaseEvaluationId).OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
@ -1308,6 +1308,11 @@ public static class DbInitializer
|
||||
// Module Duyệt NCC (tiền-HĐ)
|
||||
(MenuKeys.PurchaseEvaluations, "Quy trình chọn Thầu phụ - NCC", null, 25, "ClipboardCheck"),
|
||||
(MenuKeys.PeWorkflows, "Quy trình Duyệt NCC", MenuKeys.System, 95, "GitCompareArrows"),
|
||||
// Module Ngân sách (Phase 7)
|
||||
(MenuKeys.Budgets, "Ngân sách", null, 27, "Wallet"),
|
||||
(MenuKeys.BudgetList, "Danh sách", MenuKeys.Budgets, 1, "List"),
|
||||
(MenuKeys.BudgetCreate, "Thao tác", MenuKeys.Budgets, 2, "Plus"),
|
||||
(MenuKeys.BudgetPending, "Duyệt", MenuKeys.Budgets, 3, "CheckCircle2"),
|
||||
};
|
||||
|
||||
// Per-type sub-menu under Contracts: 1 group + 3 leaves each
|
||||
|
||||
3229
src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260428043508_AddBudgets.Designer.cs
generated
Normal file
3229
src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260428043508_AddBudgets.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,236 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddBudgets : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "BudgetId",
|
||||
table: "PurchaseEvaluations",
|
||||
type: "uniqueidentifier",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "BudgetId",
|
||||
table: "Contracts",
|
||||
type: "uniqueidentifier",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Budgets",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
MaNganSach = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||
TenNganSach = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false),
|
||||
Description = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: true),
|
||||
NamNganSach = table.Column<int>(type: "int", 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: true),
|
||||
Phase = table.Column<int>(type: "int", nullable: false),
|
||||
TongNganSach = table.Column<decimal>(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false),
|
||||
SlaDeadline = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
SlaWarningSent = table.Column<bool>(type: "bit", 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_Budgets", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "BudgetApprovals",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
BudgetId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
FromPhase = table.Column<int>(type: "int", nullable: false),
|
||||
ToPhase = table.Column<int>(type: "int", nullable: false),
|
||||
ApproverUserId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
Decision = table.Column<int>(type: "int", nullable: false),
|
||||
Comment = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, 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_BudgetApprovals", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_BudgetApprovals_Budgets_BudgetId",
|
||||
column: x => x.BudgetId,
|
||||
principalTable: "Budgets",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "BudgetChangelogs",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
BudgetId = 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(500)", maxLength: 500, 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_BudgetChangelogs", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_BudgetChangelogs_Budgets_BudgetId",
|
||||
column: x => x.BudgetId,
|
||||
principalTable: "Budgets",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "BudgetDetails",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
BudgetId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
GroupCode = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
|
||||
GroupName = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
|
||||
ItemCode = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||
NoiDung = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false),
|
||||
DonViTinh = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
|
||||
KhoiLuong = table.Column<decimal>(type: "decimal(18,4)", precision: 18, scale: 4, nullable: false),
|
||||
DonGia = table.Column<decimal>(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false),
|
||||
ThanhTien = table.Column<decimal>(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false),
|
||||
Order = table.Column<int>(type: "int", nullable: false),
|
||||
GhiChu = 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)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_BudgetDetails", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_BudgetDetails_Budgets_BudgetId",
|
||||
column: x => x.BudgetId,
|
||||
principalTable: "Budgets",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PurchaseEvaluations_BudgetId",
|
||||
table: "PurchaseEvaluations",
|
||||
column: "BudgetId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Contracts_BudgetId",
|
||||
table: "Contracts",
|
||||
column: "BudgetId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BudgetApprovals_BudgetId_ApprovedAt",
|
||||
table: "BudgetApprovals",
|
||||
columns: new[] { "BudgetId", "ApprovedAt" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BudgetChangelogs_BudgetId_CreatedAt",
|
||||
table: "BudgetChangelogs",
|
||||
columns: new[] { "BudgetId", "CreatedAt" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BudgetChangelogs_BudgetId_EntityType",
|
||||
table: "BudgetChangelogs",
|
||||
columns: new[] { "BudgetId", "EntityType" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BudgetDetails_BudgetId_Order",
|
||||
table: "BudgetDetails",
|
||||
columns: new[] { "BudgetId", "Order" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Budgets_MaNganSach",
|
||||
table: "Budgets",
|
||||
column: "MaNganSach",
|
||||
unique: true,
|
||||
filter: "[MaNganSach] IS NOT NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Budgets_NamNganSach",
|
||||
table: "Budgets",
|
||||
column: "NamNganSach");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Budgets_Phase_IsDeleted",
|
||||
table: "Budgets",
|
||||
columns: new[] { "Phase", "IsDeleted" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Budgets_ProjectId",
|
||||
table: "Budgets",
|
||||
column: "ProjectId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Budgets_SlaDeadline",
|
||||
table: "Budgets",
|
||||
column: "SlaDeadline");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "BudgetApprovals");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "BudgetChangelogs");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "BudgetDetails");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Budgets");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_PurchaseEvaluations_BudgetId",
|
||||
table: "PurchaseEvaluations");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Contracts_BudgetId",
|
||||
table: "Contracts");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "BudgetId",
|
||||
table: "PurchaseEvaluations");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "BudgetId",
|
||||
table: "Contracts");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -125,12 +125,274 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
b.ToTable("UserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.Budgets.Budget", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.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<Guid?>("DepartmentId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("nvarchar(2000)");
|
||||
|
||||
b.Property<Guid?>("DrafterUserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("MaNganSach")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<int>("NamNganSach")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Phase")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime?>("SlaDeadline")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<bool>("SlaWarningSent")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("TenNganSach")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<decimal>("TongNganSach")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid?>("UpdatedBy")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("MaNganSach")
|
||||
.IsUnique()
|
||||
.HasFilter("[MaNganSach] IS NOT NULL");
|
||||
|
||||
b.HasIndex("NamNganSach");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.HasIndex("SlaDeadline");
|
||||
|
||||
b.HasIndex("Phase", "IsDeleted");
|
||||
|
||||
b.ToTable("Budgets", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.Budgets.BudgetApproval", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("ApprovedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid?>("ApproverUserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("BudgetId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Comment")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("nvarchar(1000)");
|
||||
|
||||
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("BudgetId", "ApprovedAt");
|
||||
|
||||
b.ToTable("BudgetApprovals", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.Budgets.BudgetChangelog", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<int>("Action")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<Guid>("BudgetId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("ContextNote")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("nvarchar(2000)");
|
||||
|
||||
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(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
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("BudgetId", "CreatedAt");
|
||||
|
||||
b.HasIndex("BudgetId", "EntityType");
|
||||
|
||||
b.ToTable("BudgetChangelogs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.Budgets.BudgetDetail", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("BudgetId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid?>("CreatedBy")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<decimal>("DonGia")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<string>("DonViTinh")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("GhiChu")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("nvarchar(1000)");
|
||||
|
||||
b.Property<string>("GroupCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("GroupName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("ItemCode")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<decimal>("KhoiLuong")
|
||||
.HasPrecision(18, 4)
|
||||
.HasColumnType("decimal(18,4)");
|
||||
|
||||
b.Property<string>("NoiDung")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<int>("Order")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("ThanhTien")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("decimal(18,2)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid?>("UpdatedBy")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BudgetId", "Order");
|
||||
|
||||
b.ToTable("BudgetDetails", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.Contracts.Contract", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("BudgetId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<bool>("BypassProcurementAndCCM")
|
||||
.HasColumnType("bit");
|
||||
|
||||
@ -206,6 +468,8 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BudgetId");
|
||||
|
||||
b.HasIndex("MaHopDong")
|
||||
.IsUnique()
|
||||
.HasFilter("[MaHopDong] IS NOT NULL");
|
||||
@ -1910,6 +2174,9 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("BudgetId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("ContractId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
@ -1983,6 +2250,8 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BudgetId");
|
||||
|
||||
b.HasIndex("ContractId");
|
||||
|
||||
b.HasIndex("MaPhieu")
|
||||
@ -2558,6 +2827,39 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.Budgets.BudgetApproval", b =>
|
||||
{
|
||||
b.HasOne("SolutionErp.Domain.Budgets.Budget", "Budget")
|
||||
.WithMany("Approvals")
|
||||
.HasForeignKey("BudgetId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Budget");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.Budgets.BudgetChangelog", b =>
|
||||
{
|
||||
b.HasOne("SolutionErp.Domain.Budgets.Budget", "Budget")
|
||||
.WithMany("Changelogs")
|
||||
.HasForeignKey("BudgetId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Budget");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.Budgets.BudgetDetail", b =>
|
||||
{
|
||||
b.HasOne("SolutionErp.Domain.Budgets.Budget", "Budget")
|
||||
.WithMany("Details")
|
||||
.HasForeignKey("BudgetId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Budget");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractApproval", b =>
|
||||
{
|
||||
b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract")
|
||||
@ -2838,6 +3140,15 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
b.Navigation("Step");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.Budgets.Budget", b =>
|
||||
{
|
||||
b.Navigation("Approvals");
|
||||
|
||||
b.Navigation("Changelogs");
|
||||
|
||||
b.Navigation("Details");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.Contracts.Contract", b =>
|
||||
{
|
||||
b.Navigation("Approvals");
|
||||
|
||||
Reference in New Issue
Block a user