[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 Microsoft.EntityFrameworkCore;
|
||||||
|
using SolutionErp.Domain.Budgets;
|
||||||
using SolutionErp.Domain.Contracts;
|
using SolutionErp.Domain.Contracts;
|
||||||
using SolutionErp.Domain.Contracts.Details;
|
using SolutionErp.Domain.Contracts.Details;
|
||||||
using SolutionErp.Domain.Forms;
|
using SolutionErp.Domain.Forms;
|
||||||
@ -58,5 +59,11 @@ public interface IApplicationDbContext
|
|||||||
DbSet<PurchaseEvaluationWorkflowStepApprover> PurchaseEvaluationWorkflowStepApprovers { get; }
|
DbSet<PurchaseEvaluationWorkflowStepApprover> PurchaseEvaluationWorkflowStepApprovers { get; }
|
||||||
DbSet<PurchaseEvaluationCodeSequence> PurchaseEvaluationCodeSequences { 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);
|
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 DateTime? SlaDeadline { get; set; } // Hết hạn phase hiện tại
|
||||||
public string? DraftData { get; set; } // JSON field values (render template)
|
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 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<ContractApproval> Approvals { get; set; } = new();
|
||||||
public List<ContractComment> Comments { 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 PurchaseEvaluations = "PurchaseEvaluations"; // root group
|
||||||
public const string PeWorkflows = "PeWorkflows"; // workflow admin root
|
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 =
|
public static readonly string[] PurchaseEvaluationTypeCodes =
|
||||||
["DuyetNcc", "DuyetNccPhuongAn"];
|
["DuyetNcc", "DuyetNccPhuongAn"];
|
||||||
|
|
||||||
@ -70,6 +79,7 @@ public static class MenuKeys
|
|||||||
Catalogs, CatalogUnits, CatalogMaterials, CatalogServices, CatalogWorkItems,
|
Catalogs, CatalogUnits, CatalogMaterials, CatalogServices, CatalogWorkItems,
|
||||||
Contracts, Forms, Reports,
|
Contracts, Forms, Reports,
|
||||||
PurchaseEvaluations,
|
PurchaseEvaluations,
|
||||||
|
Budgets, BudgetList, BudgetCreate, BudgetPending,
|
||||||
System, Users, Roles, Permissions, Workflows, PeWorkflows,
|
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 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? 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<PurchaseEvaluationSupplier> Suppliers { get; set; } = new();
|
||||||
public List<PurchaseEvaluationDetail> Details { get; set; } = new();
|
public List<PurchaseEvaluationDetail> Details { get; set; } = new();
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using SolutionErp.Application.Common.Interfaces;
|
using SolutionErp.Application.Common.Interfaces;
|
||||||
|
using SolutionErp.Domain.Budgets;
|
||||||
using SolutionErp.Domain.Contracts;
|
using SolutionErp.Domain.Contracts;
|
||||||
using SolutionErp.Domain.Contracts.Details;
|
using SolutionErp.Domain.Contracts.Details;
|
||||||
using SolutionErp.Domain.Forms;
|
using SolutionErp.Domain.Forms;
|
||||||
@ -59,6 +60,12 @@ public class ApplicationDbContext
|
|||||||
public DbSet<PurchaseEvaluationWorkflowStepApprover> PurchaseEvaluationWorkflowStepApprovers => Set<PurchaseEvaluationWorkflowStepApprover>();
|
public DbSet<PurchaseEvaluationWorkflowStepApprover> PurchaseEvaluationWorkflowStepApprovers => Set<PurchaseEvaluationWorkflowStepApprover>();
|
||||||
public DbSet<PurchaseEvaluationCodeSequence> PurchaseEvaluationCodeSequences => Set<PurchaseEvaluationCodeSequence>();
|
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)
|
protected override void OnModelCreating(ModelBuilder builder)
|
||||||
{
|
{
|
||||||
base.OnModelCreating(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.SupplierId);
|
||||||
b.HasIndex(x => x.ProjectId);
|
b.HasIndex(x => x.ProjectId);
|
||||||
b.HasIndex(x => x.SlaDeadline);
|
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.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);
|
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.SlaDeadline);
|
||||||
b.HasIndex(x => x.WorkflowDefinitionId);
|
b.HasIndex(x => x.WorkflowDefinitionId);
|
||||||
b.HasIndex(x => x.ContractId);
|
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.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);
|
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Đ)
|
// Module Duyệt NCC (tiền-HĐ)
|
||||||
(MenuKeys.PurchaseEvaluations, "Quy trình chọn Thầu phụ - NCC", null, 25, "ClipboardCheck"),
|
(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"),
|
(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
|
// 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);
|
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 =>
|
modelBuilder.Entity("SolutionErp.Domain.Contracts.Contract", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("uniqueidentifier");
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<Guid?>("BudgetId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
b.Property<bool>("BypassProcurementAndCCM")
|
b.Property<bool>("BypassProcurementAndCCM")
|
||||||
.HasColumnType("bit");
|
.HasColumnType("bit");
|
||||||
|
|
||||||
@ -206,6 +468,8 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("BudgetId");
|
||||||
|
|
||||||
b.HasIndex("MaHopDong")
|
b.HasIndex("MaHopDong")
|
||||||
.IsUnique()
|
.IsUnique()
|
||||||
.HasFilter("[MaHopDong] IS NOT NULL");
|
.HasFilter("[MaHopDong] IS NOT NULL");
|
||||||
@ -1910,6 +2174,9 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
|||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("uniqueidentifier");
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<Guid?>("BudgetId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
b.Property<Guid?>("ContractId")
|
b.Property<Guid?>("ContractId")
|
||||||
.HasColumnType("uniqueidentifier");
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
@ -1983,6 +2250,8 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("BudgetId");
|
||||||
|
|
||||||
b.HasIndex("ContractId");
|
b.HasIndex("ContractId");
|
||||||
|
|
||||||
b.HasIndex("MaPhieu")
|
b.HasIndex("MaPhieu")
|
||||||
@ -2558,6 +2827,39 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
|||||||
.IsRequired();
|
.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 =>
|
modelBuilder.Entity("SolutionErp.Domain.Contracts.ContractApproval", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract")
|
b.HasOne("SolutionErp.Domain.Contracts.Contract", "Contract")
|
||||||
@ -2838,6 +3140,15 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
|||||||
b.Navigation("Step");
|
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 =>
|
modelBuilder.Entity("SolutionErp.Domain.Contracts.Contract", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("Approvals");
|
b.Navigation("Approvals");
|
||||||
|
|||||||
Reference in New Issue
Block a user