Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 629f3d1f44 | |||
| b3b6a25ce8 | |||
| f664961fce | |||
| bebec10eb3 | |||
| 160c687db3 | |||
| d413ad4064 | |||
| 9b8c2493ad | |||
| 99b669395e | |||
| 4cc6a546c7 | |||
| 8835ba9500 | |||
| 251b23f4b5 | |||
| 283a56723f | |||
| 1c5b05e315 |
@@ -0,0 +1,26 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.9" />
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\ERPCore\ERPCore.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
using ERPCore.Domain.Entities;
|
||||||
|
using ERPCore.Domain.Notifications;
|
||||||
|
using ERPCore.Repositories;
|
||||||
|
using ERPCore.Services;
|
||||||
|
using ERPCore.System.Errors;
|
||||||
|
using ERPCore.Tests.TestSupport;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace ERPCore.Tests;
|
||||||
|
|
||||||
|
public class NotificationConfigServiceTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task GetAsync_MergesSavedValuesWithDiscoveredModules()
|
||||||
|
{
|
||||||
|
var dbName = Guid.NewGuid().ToString("N");
|
||||||
|
await using var db = NotificationConfigTestSupport.CreateContext(dbName);
|
||||||
|
await NotificationConfigTestSupport.SeedAsync(db, new[]
|
||||||
|
{
|
||||||
|
new NotificationConfigModule
|
||||||
|
{
|
||||||
|
ModuleKey = "sale.create",
|
||||||
|
EmailEnabled = true,
|
||||||
|
SmsEnabled = false,
|
||||||
|
InAppEnabled = true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var service = CreateService(db);
|
||||||
|
|
||||||
|
var result = await service.GetAsync();
|
||||||
|
|
||||||
|
Assert.Equal(7u, result.RowVersion);
|
||||||
|
Assert.Contains(result.Value.Modules, m => m.ModuleKey == "sale.create" && m.EmailEnabled && !m.SmsEnabled && m.InAppEnabled);
|
||||||
|
Assert.Contains(result.Value.Modules, m => m.ModuleKey == "grn.confirmed" && !m.EmailEnabled && !m.SmsEnabled && !m.InAppEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateAsync_PersistsEmptyAndMultiChannelSelections()
|
||||||
|
{
|
||||||
|
var dbName = Guid.NewGuid().ToString("N");
|
||||||
|
await using var db = NotificationConfigTestSupport.CreateContext(dbName);
|
||||||
|
await NotificationConfigTestSupport.SeedAsync(db);
|
||||||
|
|
||||||
|
var service = CreateService(db);
|
||||||
|
var catalog = new NotificationModuleCatalog();
|
||||||
|
var request = new ERPCore.Dtos.Notifications.UpdateNotificationConfigRequest
|
||||||
|
{
|
||||||
|
Modules = catalog.GetAll().Select((module, index) => new ERPCore.Dtos.Notifications.UpdateNotificationModuleRequest
|
||||||
|
{
|
||||||
|
ModuleKey = module.Key,
|
||||||
|
EmailEnabled = index == 0,
|
||||||
|
SmsEnabled = index == 1,
|
||||||
|
InAppEnabled = index == 1
|
||||||
|
}).ToList()
|
||||||
|
};
|
||||||
|
|
||||||
|
var updated = await service.UpdateAsync(request, 7);
|
||||||
|
|
||||||
|
Assert.Equal(7u, updated.RowVersion);
|
||||||
|
Assert.Contains(updated.Value.Modules, m => m.ModuleKey == catalog.GetAll()[0].Key && m.EmailEnabled && !m.SmsEnabled && !m.InAppEnabled);
|
||||||
|
Assert.Contains(updated.Value.Modules, m => m.ModuleKey == catalog.GetAll()[1].Key && !m.EmailEnabled && m.SmsEnabled && m.InAppEnabled);
|
||||||
|
|
||||||
|
await using var verifyDb = NotificationConfigTestSupport.CreateContext(dbName);
|
||||||
|
var persisted = await verifyDb.NotificationConfigModules.AsNoTracking().ToListAsync();
|
||||||
|
Assert.Contains(persisted, m => m.ModuleKey == catalog.GetAll()[0].Key && m.EmailEnabled && !m.SmsEnabled && !m.InAppEnabled);
|
||||||
|
Assert.Contains(persisted, m => m.ModuleKey == catalog.GetAll()[1].Key && !m.EmailEnabled && m.SmsEnabled && m.InAppEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ResolveEnabledChannelsAsync_ReturnsEmptyWhenNothingEnabled()
|
||||||
|
{
|
||||||
|
var dbName = Guid.NewGuid().ToString("N");
|
||||||
|
await using var db = NotificationConfigTestSupport.CreateContext(dbName);
|
||||||
|
await NotificationConfigTestSupport.SeedAsync(db, new[]
|
||||||
|
{
|
||||||
|
new NotificationConfigModule
|
||||||
|
{
|
||||||
|
ModuleKey = "sale.create",
|
||||||
|
EmailEnabled = false,
|
||||||
|
SmsEnabled = false,
|
||||||
|
InAppEnabled = false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var service = CreateService(db);
|
||||||
|
|
||||||
|
var channels = await service.ResolveEnabledChannelsAsync("sale.create");
|
||||||
|
|
||||||
|
Assert.Empty(channels);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ResolveEnabledChannelsAsync_ReturnsMultipleChannels()
|
||||||
|
{
|
||||||
|
var dbName = Guid.NewGuid().ToString("N");
|
||||||
|
await using var db = NotificationConfigTestSupport.CreateContext(dbName);
|
||||||
|
await NotificationConfigTestSupport.SeedAsync(db, new[]
|
||||||
|
{
|
||||||
|
new NotificationConfigModule
|
||||||
|
{
|
||||||
|
ModuleKey = "sale.create",
|
||||||
|
EmailEnabled = true,
|
||||||
|
SmsEnabled = true,
|
||||||
|
InAppEnabled = false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var service = CreateService(db);
|
||||||
|
|
||||||
|
var channels = await service.ResolveEnabledChannelsAsync("sale.create");
|
||||||
|
|
||||||
|
Assert.Equal(new[] { NotificationChannel.Email, NotificationChannel.Sms }, channels);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateAsync_MapsConcurrencyFailuresToDomainException()
|
||||||
|
{
|
||||||
|
var dbName = Guid.NewGuid().ToString("N");
|
||||||
|
await using var db = NotificationConfigTestSupport.CreateContext(dbName);
|
||||||
|
await NotificationConfigTestSupport.SeedAsync(db);
|
||||||
|
|
||||||
|
var service = CreateService(db, new ThrowingUnitOfWork());
|
||||||
|
var catalog = new NotificationModuleCatalog();
|
||||||
|
var request = new ERPCore.Dtos.Notifications.UpdateNotificationConfigRequest
|
||||||
|
{
|
||||||
|
Modules = catalog.GetAll().Select(module => new ERPCore.Dtos.Notifications.UpdateNotificationModuleRequest
|
||||||
|
{
|
||||||
|
ModuleKey = module.Key,
|
||||||
|
EmailEnabled = false,
|
||||||
|
SmsEnabled = false,
|
||||||
|
InAppEnabled = false
|
||||||
|
}).ToList()
|
||||||
|
};
|
||||||
|
|
||||||
|
var ex = await Assert.ThrowsAsync<DomainException>(() => service.UpdateAsync(request, 7));
|
||||||
|
Assert.Equal(ErrorCodes.ConcurrencyConflict, ex.Code);
|
||||||
|
Assert.Equal(412, ex.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static NotificationConfigService CreateService(ERPCore.Infra.Persistence.ErpDbContext db, ERPCore.Infra.UoW.IUnitOfWork? uow = null)
|
||||||
|
{
|
||||||
|
var configRepo = new Repository<NotificationConfig>(db);
|
||||||
|
var moduleRepo = new Repository<NotificationConfigModule>(db);
|
||||||
|
return new NotificationConfigService(
|
||||||
|
configRepo,
|
||||||
|
moduleRepo,
|
||||||
|
new NotificationModuleCatalog(),
|
||||||
|
new TestCurrentUser(),
|
||||||
|
uow ?? new ERPCore.Infra.UoW.UnitOfWork(db));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using ERPCore.Services;
|
||||||
|
|
||||||
|
namespace ERPCore.Tests;
|
||||||
|
|
||||||
|
public class NotificationModuleCatalogTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void GetAll_ReturnsStableUniqueModuleKeys()
|
||||||
|
{
|
||||||
|
var catalog = new NotificationModuleCatalog();
|
||||||
|
|
||||||
|
Assert.NotEmpty(catalog.GetAll());
|
||||||
|
Assert.Contains(catalog.GetAll(), m => m.Key == "sale.create");
|
||||||
|
Assert.Contains(catalog.GetAll(), m => m.Key == "grn.confirmed");
|
||||||
|
Assert.Equal(catalog.GetAll().Count, catalog.GetAll().Select(m => m.Key).Distinct(StringComparer.OrdinalIgnoreCase).Count());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
using ERPCore.Domain.Entities;
|
||||||
|
using ERPCore.Infra.Auth;
|
||||||
|
using ERPCore.Infra.Persistence;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage;
|
||||||
|
|
||||||
|
namespace ERPCore.Tests.TestSupport;
|
||||||
|
|
||||||
|
internal sealed class TestCurrentUser : ICurrentUser
|
||||||
|
{
|
||||||
|
public string UserId => "system";
|
||||||
|
public int AuditUserId => User.SystemUserId;
|
||||||
|
public bool IsAuthenticated => false;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class ThrowingUnitOfWork : ERPCore.Infra.UoW.IUnitOfWork
|
||||||
|
{
|
||||||
|
public Task<int> SaveChangesAsync(CancellationToken ct = default)
|
||||||
|
=> throw new DbUpdateConcurrencyException("Test concurrency conflict.");
|
||||||
|
|
||||||
|
public Task ExecuteInTransactionAsync(Func<CancellationToken, Task> action, CancellationToken ct = default)
|
||||||
|
=> throw new NotSupportedException();
|
||||||
|
|
||||||
|
public Task<T> ExecuteInTransactionAsync<T>(Func<CancellationToken, Task<T>> action, CancellationToken ct = default)
|
||||||
|
=> throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class NotificationConfigTestSupport
|
||||||
|
{
|
||||||
|
private static readonly InMemoryDatabaseRoot Root = new();
|
||||||
|
|
||||||
|
public static ErpDbContext CreateContext(string name)
|
||||||
|
{
|
||||||
|
var options = new DbContextOptionsBuilder<ErpDbContext>()
|
||||||
|
.UseInMemoryDatabase(name, Root)
|
||||||
|
.Options;
|
||||||
|
|
||||||
|
return new ErpDbContext(options, new TestCurrentUser());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task SeedAsync(ErpDbContext db, IEnumerable<NotificationConfigModule>? modules = null)
|
||||||
|
{
|
||||||
|
db.Users.Add(new User
|
||||||
|
{
|
||||||
|
UserId = User.SystemUserId,
|
||||||
|
Username = "system",
|
||||||
|
DisplayName = "System"
|
||||||
|
});
|
||||||
|
|
||||||
|
var config = new NotificationConfig
|
||||||
|
{
|
||||||
|
NotificationConfigId = NotificationConfig.SingletonId,
|
||||||
|
UpdatedBy = User.SystemUserId,
|
||||||
|
UpdatedAt = DateTime.UtcNow,
|
||||||
|
RowVersion = 7,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (modules is not null)
|
||||||
|
{
|
||||||
|
foreach (var module in modules)
|
||||||
|
{
|
||||||
|
module.NotificationConfigId = NotificationConfig.SingletonId;
|
||||||
|
config.Modules.Add(module);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
db.NotificationConfig.Add(config);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using ERPCore.Dtos.Config;
|
||||||
|
using ERPCore.Services.Interfaces;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace ERPCore.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Branch configuration endpoints — the singleton toggle for whether this deployment
|
||||||
|
/// requires a branch on Employee/Warehouse records at all.
|
||||||
|
/// <para>
|
||||||
|
/// <b>Authorization:</b> writes are admitted by the inherited ERP door policy only, same
|
||||||
|
/// Phase-1 posture as <see cref="ProductConfigController"/> — any ERP-admitted user can
|
||||||
|
/// flip this flag until per-endpoint RBAC lands.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
[Route("api/v1/branch-config")]
|
||||||
|
public sealed class BranchConfigController : ApiControllerBase
|
||||||
|
{
|
||||||
|
private readonly IBranchConfigService _config;
|
||||||
|
|
||||||
|
public BranchConfigController(IBranchConfigService config) => _config = config;
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[ProducesResponseType(typeof(BranchConfigDto), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ActionResult<BranchConfigDto>> Get(CancellationToken ct)
|
||||||
|
{
|
||||||
|
var result = await _config.GetAsync(ct);
|
||||||
|
SetETag(result.RowVersion);
|
||||||
|
return Ok(result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut]
|
||||||
|
[ProducesResponseType(typeof(BranchConfigDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status412PreconditionFailed)]
|
||||||
|
public async Task<ActionResult<BranchConfigDto>> Update(
|
||||||
|
[FromBody] UpdateBranchConfigRequest request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var expected = RequireIfMatch();
|
||||||
|
var result = await _config.UpdateAsync(request, expected, ct);
|
||||||
|
SetETag(result.RowVersion);
|
||||||
|
return Ok(result.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,14 @@ public sealed class BundleSalesController : ApiControllerBase
|
|||||||
return result is null ? NotFound() : Ok(result);
|
return result is null ? NotFound() : Ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("templates")]
|
||||||
|
[ProducesResponseType(typeof(BundleSaleTemplateDto), StatusCodes.Status201Created)]
|
||||||
|
public async Task<ActionResult<BundleSaleTemplateDto>> CreateTemplate([FromBody] CreateBundleSaleTemplateRequest request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var result = await _bundles.CreateTemplateAsync(request, ct);
|
||||||
|
return Created($"/api/v1/bundle-sales/templates/{result.BundleSaleTemplateId}", result);
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[ProducesResponseType(typeof(PagedResponse<BundleSaleSummaryDto>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(PagedResponse<BundleSaleSummaryDto>), StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult<PagedResponse<BundleSaleSummaryDto>>> List(
|
public async Task<ActionResult<PagedResponse<BundleSaleSummaryDto>>> List(
|
||||||
|
|||||||
@@ -15,6 +15,6 @@ public sealed class DashboardController : ApiControllerBase
|
|||||||
/// <summary>Aggregate counts for stock, GRN, and procurement.</summary>
|
/// <summary>Aggregate counts for stock, GRN, and procurement.</summary>
|
||||||
[HttpGet("stats")]
|
[HttpGet("stats")]
|
||||||
[ProducesResponseType(typeof(DashboardStatsDto), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(DashboardStatsDto), StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult<DashboardStatsDto>> GetStats(CancellationToken ct)
|
public async Task<ActionResult<DashboardStatsDto>> GetStats([FromQuery] int? branchId, CancellationToken ct)
|
||||||
=> Ok(await _dashboard.GetStatsAsync(ct));
|
=> Ok(await _dashboard.GetStatsAsync(branchId, ct));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,12 +11,10 @@ namespace ERPCore.Controllers;
|
|||||||
public sealed class GrnsController : ApiControllerBase
|
public sealed class GrnsController : ApiControllerBase
|
||||||
{
|
{
|
||||||
private readonly IGrnService _grns;
|
private readonly IGrnService _grns;
|
||||||
private readonly IGrnPaymentService _payments;
|
|
||||||
|
|
||||||
public GrnsController(IGrnService grns, IGrnPaymentService payments)
|
public GrnsController(IGrnService grns)
|
||||||
{
|
{
|
||||||
_grns = grns;
|
_grns = grns;
|
||||||
_payments = payments;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>List GRNs, newest first.</summary>
|
/// <summary>List GRNs, newest first.</summary>
|
||||||
@@ -24,8 +22,8 @@ public sealed class GrnsController : ApiControllerBase
|
|||||||
[ProducesResponseType(typeof(PagedResponse<GrnSummaryDto>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(PagedResponse<GrnSummaryDto>), StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult<PagedResponse<GrnSummaryDto>>> List(
|
public async Task<ActionResult<PagedResponse<GrnSummaryDto>>> List(
|
||||||
[FromQuery] PageQuery query, [FromQuery] GrnStatus? status, [FromQuery] int? poId,
|
[FromQuery] PageQuery query, [FromQuery] GrnStatus? status, [FromQuery] int? poId,
|
||||||
[FromQuery] int? vendorId, [FromQuery] int? warehouseId, CancellationToken ct)
|
[FromQuery] int? vendorId, [FromQuery] int? warehouseId, [FromQuery] int? branchId, CancellationToken ct)
|
||||||
=> Ok(await _grns.ListAsync(query, status, poId, vendorId, warehouseId, ct));
|
=> Ok(await _grns.ListAsync(query, status, poId, vendorId, warehouseId, branchId, ct));
|
||||||
|
|
||||||
[HttpGet("{grnId:int}")]
|
[HttpGet("{grnId:int}")]
|
||||||
[ProducesResponseType(typeof(GrnDto), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(GrnDto), StatusCodes.Status200OK)]
|
||||||
@@ -64,23 +62,4 @@ public sealed class GrnsController : ApiControllerBase
|
|||||||
public async Task<ActionResult<ReleaseLineResultDto>> Release(
|
public async Task<ActionResult<ReleaseLineResultDto>> Release(
|
||||||
int grnId, int grnLineId, [FromBody] ReleaseLineRequest request, CancellationToken ct)
|
int grnId, int grnLineId, [FromBody] ReleaseLineRequest request, CancellationToken ct)
|
||||||
=> Ok(await _grns.ReleaseLineAsync(grnId, grnLineId, request.Action, ct));
|
=> Ok(await _grns.ReleaseLineAsync(grnId, grnLineId, request.Action, ct));
|
||||||
|
|
||||||
/// <summary>Pay the vendor against this GRN's balance, in full or in installments; posts a real GL journal entry.</summary>
|
|
||||||
[HttpPost("{grnId:int}/payments")]
|
|
||||||
[ProducesResponseType(typeof(GrnPaymentDto), StatusCodes.Status201Created)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
|
||||||
public async Task<ActionResult<GrnPaymentDto>> Pay(int grnId, [FromBody] CreateGrnPaymentRequest request, CancellationToken ct)
|
|
||||||
{
|
|
||||||
var dto = await _payments.PayAsync(grnId, request, ct);
|
|
||||||
return Created($"/api/v1/grns/{grnId}/payments/{dto.GrnPaymentId}", dto);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Payment history for this GRN, newest first.</summary>
|
|
||||||
[HttpGet("{grnId:int}/payments")]
|
|
||||||
[ProducesResponseType(typeof(IReadOnlyList<GrnPaymentDto>), StatusCodes.Status200OK)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
||||||
public async Task<ActionResult<IReadOnlyList<GrnPaymentDto>>> ListPayments(int grnId, CancellationToken ct)
|
|
||||||
=> Ok(await _payments.ListAsync(grnId, ct));
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using ERPCore.Dtos.Notifications;
|
||||||
|
using ERPCore.Services.Interfaces;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace ERPCore.Controllers;
|
||||||
|
|
||||||
|
[Route("api/v1/notification-config")]
|
||||||
|
public sealed class NotificationConfigController : ApiControllerBase
|
||||||
|
{
|
||||||
|
private readonly INotificationConfigService _config;
|
||||||
|
|
||||||
|
public NotificationConfigController(INotificationConfigService config) => _config = config;
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[ProducesResponseType(typeof(NotificationConfigDto), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ActionResult<NotificationConfigDto>> Get(CancellationToken ct)
|
||||||
|
{
|
||||||
|
var result = await _config.GetAsync(ct);
|
||||||
|
SetETag(result.RowVersion);
|
||||||
|
return Ok(result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut]
|
||||||
|
[ProducesResponseType(typeof(NotificationConfigDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status412PreconditionFailed)]
|
||||||
|
public async Task<ActionResult<NotificationConfigDto>> Update([FromBody] UpdateNotificationConfigRequest request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var expected = RequireIfMatch();
|
||||||
|
var result = await _config.UpdateAsync(request, expected, ct);
|
||||||
|
SetETag(result.RowVersion);
|
||||||
|
return Ok(result.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,19 @@ public sealed class ProductionRunsController : ApiControllerBase
|
|||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
=> Ok(await _runs.ListAsync(query, status, templateId, warehouseId, ct));
|
=> Ok(await _runs.ListAsync(query, status, templateId, warehouseId, ct));
|
||||||
|
|
||||||
|
/// <summary>Produced-items report (FR-MFG-13): one row per run that has produced
|
||||||
|
/// something, plus a summary total over every filtered row.</summary>
|
||||||
|
[HttpGet("produced")]
|
||||||
|
[ProducesResponseType(typeof(ProducedListResponseDto), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ActionResult<ProducedListResponseDto>> ListProduced(
|
||||||
|
[FromQuery] DateOnly? from,
|
||||||
|
[FromQuery] DateOnly? to,
|
||||||
|
[FromQuery] int? templateId,
|
||||||
|
[FromQuery] int? warehouseId,
|
||||||
|
[FromQuery] ProductionRunStatus? status,
|
||||||
|
CancellationToken ct)
|
||||||
|
=> Ok(await _runs.ListProducedAsync(from, to, templateId, warehouseId, status, ct));
|
||||||
|
|
||||||
[HttpGet("{runId:int}")]
|
[HttpGet("{runId:int}")]
|
||||||
[ProducesResponseType(typeof(RunGraphDto), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(RunGraphDto), StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
using ERPCore.Domain.Enums;
|
||||||
|
using ERPCore.Dtos.Common;
|
||||||
|
using ERPCore.Dtos.Grn;
|
||||||
|
using ERPCore.Dtos.Procurement;
|
||||||
|
using ERPCore.Services.Interfaces;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace ERPCore.Controllers;
|
||||||
|
|
||||||
|
/// <summary>Purchase invoice endpoints — vendor invoices consolidating Confirmed GRNs.</summary>
|
||||||
|
[Route("api/v1/purchase-invoices")]
|
||||||
|
public sealed class PurchaseInvoicesController : ApiControllerBase
|
||||||
|
{
|
||||||
|
private readonly IPurchaseInvoiceService _invoices;
|
||||||
|
private readonly IPurchaseInvoicePaymentService _payments;
|
||||||
|
|
||||||
|
public PurchaseInvoicesController(IPurchaseInvoiceService invoices, IPurchaseInvoicePaymentService payments)
|
||||||
|
{
|
||||||
|
_invoices = invoices;
|
||||||
|
_payments = payments;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[ProducesResponseType(typeof(PagedResponse<PurchaseInvoiceSummaryDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ActionResult<PagedResponse<PurchaseInvoiceSummaryDto>>> List(
|
||||||
|
[FromQuery] PageQuery query, [FromQuery] PurchaseInvoiceStatus? status, [FromQuery] int? vendorId, CancellationToken ct)
|
||||||
|
=> Ok(await _invoices.ListAsync(query, status, vendorId, ct));
|
||||||
|
|
||||||
|
/// <summary>Confirmed, not-yet-invoiced GRNs for a vendor — candidates for a new/edited invoice.</summary>
|
||||||
|
[HttpGet("eligible-grns")]
|
||||||
|
[ProducesResponseType(typeof(IReadOnlyList<GrnEligibleForInvoiceDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ActionResult<IReadOnlyList<GrnEligibleForInvoiceDto>>> EligibleGrns([FromQuery] int vendorId, CancellationToken ct)
|
||||||
|
=> Ok(await _invoices.ListEligibleGrnsAsync(vendorId, ct));
|
||||||
|
|
||||||
|
[HttpGet("{purchaseInvoiceId:int}")]
|
||||||
|
[ProducesResponseType(typeof(PurchaseInvoiceDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ActionResult<PurchaseInvoiceDto>> GetById(int purchaseInvoiceId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var result = await _invoices.GetAsync(purchaseInvoiceId, ct);
|
||||||
|
if (result is null) return NotFound();
|
||||||
|
SetETag(result.RowVersion);
|
||||||
|
return Ok(result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[ProducesResponseType(typeof(PurchaseInvoiceDto), StatusCodes.Status201Created)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
|
||||||
|
public async Task<ActionResult<PurchaseInvoiceDto>> Create([FromBody] CreatePurchaseInvoiceRequest request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var result = await _invoices.CreateAsync(request, ct);
|
||||||
|
SetETag(result.RowVersion);
|
||||||
|
return Created($"/api/v1/purchase-invoices/{result.Value.PurchaseInvoiceId}", result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("{purchaseInvoiceId:int}")]
|
||||||
|
[ProducesResponseType(typeof(PurchaseInvoiceDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status412PreconditionFailed)]
|
||||||
|
public async Task<ActionResult<PurchaseInvoiceDto>> Update(int purchaseInvoiceId, [FromBody] UpdatePurchaseInvoiceRequest request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var expected = RequireIfMatch();
|
||||||
|
var result = await _invoices.UpdateAsync(purchaseInvoiceId, request, expected, ct);
|
||||||
|
SetETag(result.RowVersion);
|
||||||
|
return Ok(result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{purchaseInvoiceId:int}/post")]
|
||||||
|
[ProducesResponseType(typeof(PurchaseInvoiceDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||||
|
public async Task<ActionResult<PurchaseInvoiceDto>> Post(int purchaseInvoiceId, CancellationToken ct)
|
||||||
|
=> Ok(await _invoices.PostAsync(purchaseInvoiceId, ct));
|
||||||
|
|
||||||
|
[HttpPost("{purchaseInvoiceId:int}/cancel")]
|
||||||
|
[ProducesResponseType(typeof(PurchaseInvoiceDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||||
|
public async Task<ActionResult<PurchaseInvoiceDto>> Cancel(int purchaseInvoiceId, CancellationToken ct)
|
||||||
|
=> Ok(await _invoices.CancelAsync(purchaseInvoiceId, ct));
|
||||||
|
|
||||||
|
/// <summary>Pay the vendor against this invoice's balance, in full or in installments; posts a real GL journal entry.</summary>
|
||||||
|
[HttpPost("{purchaseInvoiceId:int}/payments")]
|
||||||
|
[ProducesResponseType(typeof(PurchaseInvoicePaymentDto), StatusCodes.Status201Created)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||||
|
public async Task<ActionResult<PurchaseInvoicePaymentDto>> Pay(int purchaseInvoiceId, [FromBody] CreatePurchaseInvoicePaymentRequest request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var dto = await _payments.PayAsync(purchaseInvoiceId, request, ct);
|
||||||
|
return Created($"/api/v1/purchase-invoices/{purchaseInvoiceId}/payments/{dto.PurchaseInvoicePaymentId}", dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Payment history for this invoice, newest first.</summary>
|
||||||
|
[HttpGet("{purchaseInvoiceId:int}/payments")]
|
||||||
|
[ProducesResponseType(typeof(IReadOnlyList<PurchaseInvoicePaymentDto>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ActionResult<IReadOnlyList<PurchaseInvoicePaymentDto>>> ListPayments(int purchaseInvoiceId, CancellationToken ct)
|
||||||
|
=> Ok(await _payments.ListAsync(purchaseInvoiceId, ct));
|
||||||
|
}
|
||||||
@@ -25,8 +25,9 @@ public sealed class SalesInvoicesController : ApiControllerBase
|
|||||||
[FromQuery] SalesInvoiceStatus? status,
|
[FromQuery] SalesInvoiceStatus? status,
|
||||||
[FromQuery] int? customerId,
|
[FromQuery] int? customerId,
|
||||||
[FromQuery] int? warehouseId,
|
[FromQuery] int? warehouseId,
|
||||||
|
[FromQuery] int? branchId,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
=> Ok(await _invoices.ListAsync(query, status, customerId, warehouseId, ct));
|
=> Ok(await _invoices.ListAsync(query, status, customerId, warehouseId, branchId, ct));
|
||||||
|
|
||||||
[HttpGet("{salesInvoiceId:int}")]
|
[HttpGet("{salesInvoiceId:int}")]
|
||||||
[ProducesResponseType(typeof(SalesInvoiceDto), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(SalesInvoiceDto), StatusCodes.Status200OK)]
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ public sealed class StockAdjustmentsController : ApiControllerBase
|
|||||||
[HttpGet]
|
[HttpGet]
|
||||||
[ProducesResponseType(typeof(PagedResponse<AdjustmentSummaryDto>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(PagedResponse<AdjustmentSummaryDto>), StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult<PagedResponse<AdjustmentSummaryDto>>> List(
|
public async Task<ActionResult<PagedResponse<AdjustmentSummaryDto>>> List(
|
||||||
[FromQuery] PageQuery query, [FromQuery] int? warehouseId, [FromQuery] int? reasonCodeId, CancellationToken ct)
|
[FromQuery] PageQuery query, [FromQuery] int? warehouseId, [FromQuery] int? branchId, [FromQuery] int? reasonCodeId, CancellationToken ct)
|
||||||
=> Ok(await _adjustments.ListAsync(query, warehouseId, reasonCodeId, ct));
|
=> Ok(await _adjustments.ListAsync(query, warehouseId, branchId, reasonCodeId, ct));
|
||||||
|
|
||||||
/// <summary>Get one adjustment with its lines and the ledger entries it posted.</summary>
|
/// <summary>Get one adjustment with its lines and the ledger entries it posted.</summary>
|
||||||
[HttpGet("{adjustmentId:int}")]
|
[HttpGet("{adjustmentId:int}")]
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ public sealed class StockController : ApiControllerBase
|
|||||||
[HttpGet("on-hand/list")]
|
[HttpGet("on-hand/list")]
|
||||||
[ProducesResponseType(typeof(PagedResponse<StockOnHandDto>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(PagedResponse<StockOnHandDto>), StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult<PagedResponse<StockOnHandDto>>> OnHandList(
|
public async Task<ActionResult<PagedResponse<StockOnHandDto>>> OnHandList(
|
||||||
[FromQuery] int? itemId, [FromQuery] int? warehouseId, [FromQuery] PageQuery query, CancellationToken ct)
|
[FromQuery] int? itemId, [FromQuery] int? warehouseId, [FromQuery] int? branchId, [FromQuery] PageQuery query, CancellationToken ct)
|
||||||
=> Ok(await _stock.GetOnHandListAsync(itemId, warehouseId, query, ct));
|
=> Ok(await _stock.GetOnHandListAsync(itemId, warehouseId, branchId, query, ct));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Immutable movement history. <c>sourceDocType</c>/<c>sourceDocId</c> answer "what did
|
/// Immutable movement history. <c>sourceDocType</c>/<c>sourceDocId</c> answer "what did
|
||||||
@@ -39,11 +39,11 @@ public sealed class StockController : ApiControllerBase
|
|||||||
[HttpGet("ledger")]
|
[HttpGet("ledger")]
|
||||||
[ProducesResponseType(typeof(PagedResponse<StockLedgerRowDto>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(PagedResponse<StockLedgerRowDto>), StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult<PagedResponse<StockLedgerRowDto>>> Ledger(
|
public async Task<ActionResult<PagedResponse<StockLedgerRowDto>>> Ledger(
|
||||||
[FromQuery] int? itemId, [FromQuery] int? warehouseId,
|
[FromQuery] int? itemId, [FromQuery] int? warehouseId, [FromQuery] int? branchId,
|
||||||
[FromQuery] DateOnly? from, [FromQuery] DateOnly? to,
|
[FromQuery] DateOnly? from, [FromQuery] DateOnly? to,
|
||||||
[FromQuery] string? sourceDocType, [FromQuery] int? sourceDocId,
|
[FromQuery] string? sourceDocType, [FromQuery] int? sourceDocId,
|
||||||
[FromQuery] PageQuery query, CancellationToken ct)
|
[FromQuery] PageQuery query, CancellationToken ct)
|
||||||
=> Ok(await _stock.GetLedgerAsync(itemId, warehouseId, from, to, sourceDocType, sourceDocId, query, ct));
|
=> Ok(await _stock.GetLedgerAsync(itemId, warehouseId, branchId, from, to, sourceDocType, sourceDocId, query, ct));
|
||||||
|
|
||||||
[HttpGet("valuation")]
|
[HttpGet("valuation")]
|
||||||
[ProducesResponseType(typeof(StockValuationDto), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(StockValuationDto), StatusCodes.Status200OK)]
|
||||||
@@ -54,8 +54,8 @@ public sealed class StockController : ApiControllerBase
|
|||||||
[HttpGet("reorder-alerts")]
|
[HttpGet("reorder-alerts")]
|
||||||
[ProducesResponseType(typeof(PagedResponse<ReorderAlertDto>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(PagedResponse<ReorderAlertDto>), StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult<PagedResponse<ReorderAlertDto>>> ReorderAlerts(
|
public async Task<ActionResult<PagedResponse<ReorderAlertDto>>> ReorderAlerts(
|
||||||
[FromQuery] int? warehouseId, [FromQuery] PageQuery query, CancellationToken ct)
|
[FromQuery] int? warehouseId, [FromQuery] int? branchId, [FromQuery] PageQuery query, CancellationToken ct)
|
||||||
=> Ok(await _reorder.GetAlertsAsync(warehouseId, query, ct));
|
=> Ok(await _reorder.GetAlertsAsync(warehouseId, branchId, query, ct));
|
||||||
|
|
||||||
/// <summary>Create a draft requisition for an item's suggested reorder quantity.</summary>
|
/// <summary>Create a draft requisition for an item's suggested reorder quantity.</summary>
|
||||||
[HttpPost("reorder-alerts/{itemId:int}/requisition")]
|
[HttpPost("reorder-alerts/{itemId:int}/requisition")]
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ public sealed class StockCountsController : ApiControllerBase
|
|||||||
[HttpGet]
|
[HttpGet]
|
||||||
[ProducesResponseType(typeof(PagedResponse<CountSummaryDto>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(PagedResponse<CountSummaryDto>), StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult<PagedResponse<CountSummaryDto>>> List(
|
public async Task<ActionResult<PagedResponse<CountSummaryDto>>> List(
|
||||||
[FromQuery] PageQuery query, [FromQuery] CountStatus? status, [FromQuery] int? warehouseId, CancellationToken ct)
|
[FromQuery] PageQuery query, [FromQuery] CountStatus? status, [FromQuery] int? warehouseId, [FromQuery] int? branchId, CancellationToken ct)
|
||||||
=> Ok(await _counts.ListAsync(query, status, warehouseId, ct));
|
=> Ok(await _counts.ListAsync(query, status, warehouseId, branchId, ct));
|
||||||
|
|
||||||
[HttpGet("{countId:int}")]
|
[HttpGet("{countId:int}")]
|
||||||
[ProducesResponseType(typeof(CountDto), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(CountDto), StatusCodes.Status200OK)]
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ public sealed class StockTransfersController : ApiControllerBase
|
|||||||
[ProducesResponseType(typeof(PagedResponse<TransferSummaryDto>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(PagedResponse<TransferSummaryDto>), StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult<PagedResponse<TransferSummaryDto>>> List(
|
public async Task<ActionResult<PagedResponse<TransferSummaryDto>>> List(
|
||||||
[FromQuery] PageQuery query, [FromQuery] TransferStatus? status,
|
[FromQuery] PageQuery query, [FromQuery] TransferStatus? status,
|
||||||
[FromQuery] int? srcWarehouseId, [FromQuery] int? destWarehouseId, CancellationToken ct)
|
[FromQuery] int? srcWarehouseId, [FromQuery] int? destWarehouseId, [FromQuery] int? branchId, CancellationToken ct)
|
||||||
=> Ok(await _transfers.ListAsync(query, status, srcWarehouseId, destWarehouseId, ct));
|
=> Ok(await _transfers.ListAsync(query, status, srcWarehouseId, destWarehouseId, branchId, ct));
|
||||||
|
|
||||||
[HttpGet("{transferId:int}")]
|
[HttpGet("{transferId:int}")]
|
||||||
[ProducesResponseType(typeof(TransferDto), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(TransferDto), StatusCodes.Status200OK)]
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using ERPCore.Dtos.Common;
|
using ERPCore.Dtos.Common;
|
||||||
using ERPCore.Dtos.Hrm;
|
using ERPCore.Dtos.Hrm;
|
||||||
using ERPCore.Dtos.Users;
|
using ERPCore.Dtos.Users;
|
||||||
|
using ERPCore.Infra.Auth;
|
||||||
using ERPCore.Services.Interfaces;
|
using ERPCore.Services.Interfaces;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
@@ -15,11 +16,13 @@ public sealed class UsersController : ApiControllerBase
|
|||||||
{
|
{
|
||||||
private readonly IUserManagementService _users;
|
private readonly IUserManagementService _users;
|
||||||
private readonly IEmployeeUserLinkService _links;
|
private readonly IEmployeeUserLinkService _links;
|
||||||
|
private readonly ICurrentUser _currentUser;
|
||||||
|
|
||||||
public UsersController(IUserManagementService users, IEmployeeUserLinkService links)
|
public UsersController(IUserManagementService users, IEmployeeUserLinkService links, ICurrentUser currentUser)
|
||||||
{
|
{
|
||||||
_users = users;
|
_users = users;
|
||||||
_links = links;
|
_links = links;
|
||||||
|
_currentUser = currentUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Advisory forward-direction lookup: does a Staff record already exist with this email? (docs/12-BACKEND-HRM.md A.5)</summary>
|
/// <summary>Advisory forward-direction lookup: does a Staff record already exist with this email? (docs/12-BACKEND-HRM.md A.5)</summary>
|
||||||
@@ -53,7 +56,7 @@ public sealed class UsersController : ApiControllerBase
|
|||||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||||
public async Task<ActionResult<ManagedUserDto>> Create([FromBody] CreateUserRequest request, CancellationToken ct)
|
public async Task<ActionResult<ManagedUserDto>> Create([FromBody] CreateUserRequest request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var result = await _users.CreateAsync(request, ct);
|
var result = await _users.CreateAsync(request, _currentUser.AuditUserId, ct);
|
||||||
return Created($"/api/v1/users/{result.UserId}", result);
|
return Created($"/api/v1/users/{result.UserId}", result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ public static class DocumentTypes
|
|||||||
public const string Rfq = "RFQ";
|
public const string Rfq = "RFQ";
|
||||||
public const string PurchaseOrder = "PO";
|
public const string PurchaseOrder = "PO";
|
||||||
public const string Grn = "GRN";
|
public const string Grn = "GRN";
|
||||||
|
public const string PurchaseInvoice = "PINV";
|
||||||
public const string Transfer = "TRF";
|
public const string Transfer = "TRF";
|
||||||
public const string Adjustment = "ADJ";
|
public const string Adjustment = "ADJ";
|
||||||
public const string Count = "CNT";
|
public const string Count = "CNT";
|
||||||
@@ -20,6 +21,7 @@ public static class DocumentTypes
|
|||||||
public const string SalesInvoice = "SI";
|
public const string SalesInvoice = "SI";
|
||||||
public const string SalesSlip = "SSL";
|
public const string SalesSlip = "SSL";
|
||||||
public const string BundleSale = "BND";
|
public const string BundleSale = "BND";
|
||||||
|
public const string BundleSaleTemplate = "TPL";
|
||||||
public const string SalesReturn = "SRET";
|
public const string SalesReturn = "SRET";
|
||||||
|
|
||||||
/// <summary>Cashier day-end close-out (Sales) — <c>DEND-2026-00001</c>.</summary>
|
/// <summary>Cashier day-end close-out (Sales) — <c>DEND-2026-00001</c>.</summary>
|
||||||
|
|||||||
@@ -3,16 +3,19 @@ using ERPCore.Domain.Enums;
|
|||||||
namespace ERPCore.Domain.Entities;
|
namespace ERPCore.Domain.Entities;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Branch/location master (FR-HR-MD-01) — multi-branch readiness. Referenced
|
/// Branch/location master (FR-HR-MD-01) — multi-branch readiness. Required by
|
||||||
/// optionally by <see cref="Employee.BranchId"/> and <see cref="PayrollRun.BranchId"/>.
|
/// <see cref="Employee.BranchId"/>, optionally referenced by <see cref="PayrollRun.BranchId"/>.
|
||||||
/// Deactivated, not deleted, when referenced. Model: docs/12-BACKEND-HRM.md Part C.1.
|
/// Deactivated, not deleted, when referenced. Model: docs/12-BACKEND-HRM.md Part C.1.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Branch
|
public class Branch
|
||||||
{
|
{
|
||||||
public int BranchId { get; set; }
|
public int BranchId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Server-generated on create ("BR-001", "BR-002", ...) — never accepted from the client.</summary>
|
||||||
public string Code { get; set; } = string.Empty;
|
public string Code { get; set; } = string.Empty;
|
||||||
public string Name { get; set; } = string.Empty;
|
public string Name { get; set; } = string.Empty;
|
||||||
public string? Address { get; set; }
|
public string? Address { get; set; }
|
||||||
|
public string? PhoneNumber { get; set; }
|
||||||
public EntityStatus Status { get; set; } = EntityStatus.Active;
|
public EntityStatus Status { get; set; } = EntityStatus.Active;
|
||||||
|
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
namespace ERPCore.Domain.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Branch configuration — a singleton row (single-tenant, docs/00-CORE §1) gating whether
|
||||||
|
/// this deployment uses branches at all. When <see cref="BranchingEnabled"/> is off, the
|
||||||
|
/// Employee/Warehouse create forms stop asking for a branch and every new record is
|
||||||
|
/// silently assigned to the deployment's one default branch instead — schema-wise
|
||||||
|
/// <c>Employee.BranchId</c>/<c>Warehouse.BranchId</c> stay <c>NOT NULL</c> either way, only
|
||||||
|
/// the UI/UX requirement toggles. See <see cref="Services.BranchConfigService.ResolveBranchIdAsync"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class BranchConfig
|
||||||
|
{
|
||||||
|
/// <summary>Always 1 - the singleton row's id.</summary>
|
||||||
|
public const int SingletonId = 1;
|
||||||
|
|
||||||
|
public int ConfigId { get; set; }
|
||||||
|
|
||||||
|
public bool BranchingEnabled { get; set; } = true;
|
||||||
|
|
||||||
|
public DateTime? UpdatedAt { get; set; }
|
||||||
|
|
||||||
|
public int? UpdatedBy { get; set; }
|
||||||
|
public User? UpdatedByUser { get; set; }
|
||||||
|
|
||||||
|
/// <summary>PostgreSQL xmin-backed optimistic concurrency token (ETag source).</summary>
|
||||||
|
public uint RowVersion { get; set; }
|
||||||
|
}
|
||||||
@@ -50,7 +50,7 @@ public class Employee
|
|||||||
public Designation? Designation { get; set; }
|
public Designation? Designation { get; set; }
|
||||||
public int EmploymentTypeId { get; set; }
|
public int EmploymentTypeId { get; set; }
|
||||||
public EmploymentType? EmploymentType { get; set; }
|
public EmploymentType? EmploymentType { get; set; }
|
||||||
public int? BranchId { get; set; }
|
public int BranchId { get; set; }
|
||||||
public Branch? Branch { get; set; }
|
public Branch? Branch { get; set; }
|
||||||
public int WorkShiftId { get; set; }
|
public int WorkShiftId { get; set; }
|
||||||
public WorkShift? WorkShift { get; set; }
|
public WorkShift? WorkShift { get; set; }
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ public class Grn
|
|||||||
public int WarehouseId { get; set; }
|
public int WarehouseId { get; set; }
|
||||||
public Warehouse? Warehouse { get; set; }
|
public Warehouse? Warehouse { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Owning branch, set explicitly at creation — warehouses are shared across
|
||||||
|
/// branches, so branch scope can no longer be derived from <see cref="Warehouse"/>.</summary>
|
||||||
|
public int BranchId { get; set; }
|
||||||
|
public Branch? Branch { get; set; }
|
||||||
|
|
||||||
public GrnStatus Status { get; set; } = GrnStatus.Draft;
|
public GrnStatus Status { get; set; } = GrnStatus.Draft;
|
||||||
|
|
||||||
public int CreatedBy { get; set; }
|
public int CreatedBy { get; set; }
|
||||||
@@ -34,14 +39,14 @@ public class Grn
|
|||||||
public string? GlJournalNo { get; set; }
|
public string? GlJournalNo { get; set; }
|
||||||
public DateTime? GlPostedAt { get; set; }
|
public DateTime? GlPostedAt { get; set; }
|
||||||
|
|
||||||
/// <summary>Sum of vendor payments made against this GRN's total payable (<see cref="GrnLine.LineTotal"/>).</summary>
|
/// <summary>The Purchase Invoice this GRN has been consolidated into, if any (null until
|
||||||
public decimal PaidAmount { get; set; }
|
/// invoiced). Vendor settlement is tracked on the invoice, not per-GRN — see
|
||||||
/// <summary>Total payable minus <see cref="PaidAmount"/>; installments accrue against this until it reaches zero.</summary>
|
/// <see cref="PurchaseInvoice"/>.</summary>
|
||||||
public decimal BalanceAmount { get; set; }
|
public int? PurchaseInvoiceId { get; set; }
|
||||||
|
public PurchaseInvoice? PurchaseInvoice { get; set; }
|
||||||
|
|
||||||
/// <summary>PostgreSQL xmin-backed optimistic concurrency token.</summary>
|
/// <summary>PostgreSQL xmin-backed optimistic concurrency token.</summary>
|
||||||
public uint RowVersion { get; set; }
|
public uint RowVersion { get; set; }
|
||||||
|
|
||||||
public ICollection<GrnLine> Lines { get; set; } = new List<GrnLine>();
|
public ICollection<GrnLine> Lines { get; set; } = new List<GrnLine>();
|
||||||
public ICollection<GrnPayment> Payments { get; set; } = new List<GrnPayment>();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,11 +44,12 @@ public class Item
|
|||||||
public string? TaxClass { get; set; }
|
public string? TaxClass { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Optional fixed selling price used by Sales only. <c>null</c> means "use stock value"
|
/// Optional selling price range and flat price used by Sales only.
|
||||||
/// (the item is sold at its FIFO stock cost at sale time); a value is the fixed sale price.
|
/// <c>null</c> values mean the field is not set. These values never enter costing/GRN/FIFO.
|
||||||
/// Never enters costing/GRN/FIFO (docs/10 Part C.1, C.9).
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public decimal? SalePrice { get; set; }
|
public decimal? SellingMinPrice { get; set; }
|
||||||
|
public decimal? SellingMaxPrice { get; set; }
|
||||||
|
public decimal? SellingFlatPrice { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// How much one pack holds, as the user entered it — <c>500</c> with
|
/// How much one pack holds, as the user entered it — <c>500</c> with
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
namespace ERPCore.Domain.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Notification configuration (new settings module) - singleton root row.
|
||||||
|
/// </summary>
|
||||||
|
public class NotificationConfig
|
||||||
|
{
|
||||||
|
public const int SingletonId = 1;
|
||||||
|
|
||||||
|
public int NotificationConfigId { get; set; }
|
||||||
|
|
||||||
|
public DateTime? UpdatedAt { get; set; }
|
||||||
|
public int? UpdatedBy { get; set; }
|
||||||
|
public User? UpdatedByUser { get; set; }
|
||||||
|
|
||||||
|
public uint RowVersion { get; set; }
|
||||||
|
|
||||||
|
public ICollection<NotificationConfigModule> Modules { get; set; } = new List<NotificationConfigModule>();
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace ERPCore.Domain.Entities;
|
||||||
|
|
||||||
|
public class NotificationConfigModule
|
||||||
|
{
|
||||||
|
public int NotificationConfigModuleId { get; set; }
|
||||||
|
|
||||||
|
public int NotificationConfigId { get; set; }
|
||||||
|
public NotificationConfig? NotificationConfig { get; set; }
|
||||||
|
|
||||||
|
public string ModuleKey { get; set; } = string.Empty;
|
||||||
|
public bool EmailEnabled { get; set; }
|
||||||
|
public bool SmsEnabled { get; set; }
|
||||||
|
public bool InAppEnabled { get; set; }
|
||||||
|
}
|
||||||
@@ -1,22 +1,24 @@
|
|||||||
namespace ERPCore.Domain.Entities;
|
namespace ERPCore.Domain.Entities;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Product configuration (FR-MD-11) — a <b>singleton row</b> (single-tenant, docs/00-CORE §1)
|
/// Product configuration (FR-MD-11) - a singleton row (single-tenant, docs/00-CORE §1)
|
||||||
/// gating optional product master-data features.
|
/// gating optional product master-data features.
|
||||||
/// <para>
|
/// <para>
|
||||||
/// <see cref="SubcategoriesEnabled"/> and <see cref="BrandsEnabled"/> are enforced
|
/// <see cref="SubcategoriesEnabled"/> and <see cref="BrandsEnabled"/> are enforced
|
||||||
/// server-side: an Item write carrying a subcategory/brand while the flag is off is
|
/// server-side: an Item write carrying a subcategory/brand while the flag is off is
|
||||||
/// rejected with <c>CONFIG_DISABLED</c>. <see cref="ItemTypesEnabled"/> is
|
/// rejected with <c>CONFIG_DISABLED</c>. <see cref="ItemTypesEnabled"/> is advisory only
|
||||||
/// <b>advisory only</b> — items carry no item-type reference (see <see cref="ItemType"/>),
|
/// - items carry no item-type reference (see <see cref="ItemType"/>), so there is nothing
|
||||||
/// so there is nothing on a write to reject; the frontend honours it by hiding the
|
/// on a write to reject; the frontend honours it by hiding the variant builder.
|
||||||
/// builder's type section. Reads are never gated, so existing data stays visible after a
|
/// <see cref="SellingPricesEnabled"/>, <see cref="SerialNumbersEnabled"/>,
|
||||||
/// flag is switched off.
|
/// <see cref="BatchListsEnabled"/> and <see cref="WarrantyPeriodsEnabled"/> are enforced
|
||||||
|
/// on item writes and drive the create/edit screens. Reads are never gated, so existing
|
||||||
|
/// data stays visible after a flag is switched off.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// Model: docs/10-BACKEND-PHASE1.md Part C.1.
|
/// Model: docs/10-BACKEND-PHASE1.md Part C.1.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ProductConfig
|
public class ProductConfig
|
||||||
{
|
{
|
||||||
/// <summary>Always 1 — the singleton row's id.</summary>
|
/// <summary>Always 1 - the singleton row's id.</summary>
|
||||||
public const int SingletonId = 1;
|
public const int SingletonId = 1;
|
||||||
|
|
||||||
public int ConfigId { get; set; }
|
public int ConfigId { get; set; }
|
||||||
@@ -24,6 +26,10 @@ public class ProductConfig
|
|||||||
public bool SubcategoriesEnabled { get; set; } = true;
|
public bool SubcategoriesEnabled { get; set; } = true;
|
||||||
public bool BrandsEnabled { get; set; } = true;
|
public bool BrandsEnabled { get; set; } = true;
|
||||||
public bool ItemTypesEnabled { get; set; } = true;
|
public bool ItemTypesEnabled { get; set; } = true;
|
||||||
|
public bool SellingPricesEnabled { get; set; } = true;
|
||||||
|
public bool SerialNumbersEnabled { get; set; } = true;
|
||||||
|
public bool BatchListsEnabled { get; set; } = true;
|
||||||
|
public bool WarrantyPeriodsEnabled { get; set; } = true;
|
||||||
|
|
||||||
public DateTime? UpdatedAt { get; set; }
|
public DateTime? UpdatedAt { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using ERPCore.Domain.Enums;
|
||||||
|
|
||||||
|
namespace ERPCore.Domain.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Vendor invoice consolidating one or more Confirmed GRNs for the same vendor
|
||||||
|
/// (each linked GRN becomes one <see cref="PurchaseInvoiceLine"/>, not re-itemized).
|
||||||
|
/// Draft is editable (GRNs can be re-selected); Posted reclassifies the GRN clearing
|
||||||
|
/// balance to Accounts Payable (see PurchaseInvoiceService.PostAsync); vendor payments
|
||||||
|
/// are then recorded against this aggregate rather than against the individual GRNs.
|
||||||
|
/// </summary>
|
||||||
|
public class PurchaseInvoice
|
||||||
|
{
|
||||||
|
public int PurchaseInvoiceId { get; set; }
|
||||||
|
public string DocNo { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string VendorInvoiceNo { get; set; } = string.Empty;
|
||||||
|
public DateTime VendorInvoiceDate { get; set; }
|
||||||
|
|
||||||
|
public int VendorId { get; set; }
|
||||||
|
public Vendor? Vendor { get; set; }
|
||||||
|
|
||||||
|
public string VendorSnapshotName { get; set; } = string.Empty;
|
||||||
|
public string? VendorSnapshotTaxNo { get; set; }
|
||||||
|
|
||||||
|
public PurchaseInvoiceStatus Status { get; set; } = PurchaseInvoiceStatus.Draft;
|
||||||
|
|
||||||
|
public decimal Subtotal { get; set; }
|
||||||
|
public decimal VatTotal { get; set; }
|
||||||
|
public decimal GrandTotal { get; set; }
|
||||||
|
public decimal PaidAmount { get; set; }
|
||||||
|
public decimal BalanceAmount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Journal number of the AP-recognition GL journal entry posted when this
|
||||||
|
/// invoice is Posted (Debit GRN Clearing / Credit Accounts Payable) — see
|
||||||
|
/// PurchaseInvoiceService.PostAsync.</summary>
|
||||||
|
public string? GlJournalNo { get; set; }
|
||||||
|
public DateTime? GlPostedAt { get; set; }
|
||||||
|
|
||||||
|
public int CreatedBy { get; set; }
|
||||||
|
public User? Creator { get; set; }
|
||||||
|
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public DateTime? UpdatedAt { get; set; }
|
||||||
|
|
||||||
|
public uint RowVersion { get; set; }
|
||||||
|
|
||||||
|
public ICollection<PurchaseInvoiceLine> Lines { get; set; } = new List<PurchaseInvoiceLine>();
|
||||||
|
public ICollection<PurchaseInvoicePayment> Payments { get; set; } = new List<PurchaseInvoicePayment>();
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
namespace ERPCore.Domain.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One row per consolidated GRN on a <see cref="PurchaseInvoice"/> — a straight
|
||||||
|
/// consolidation of that GRN's line totals, not a re-itemization (docs: Purchase
|
||||||
|
/// Invoice consolidates GRNs, it does not re-enter item lines).
|
||||||
|
/// </summary>
|
||||||
|
public class PurchaseInvoiceLine
|
||||||
|
{
|
||||||
|
public int PurchaseInvoiceLineId { get; set; }
|
||||||
|
|
||||||
|
public int PurchaseInvoiceId { get; set; }
|
||||||
|
public PurchaseInvoice? PurchaseInvoice { get; set; }
|
||||||
|
|
||||||
|
public int GrnId { get; set; }
|
||||||
|
public Grn? Grn { get; set; }
|
||||||
|
|
||||||
|
public decimal Subtotal { get; set; }
|
||||||
|
public decimal VatTotal { get; set; }
|
||||||
|
public decimal LineTotal { get; set; }
|
||||||
|
}
|
||||||
+8
-7
@@ -1,16 +1,17 @@
|
|||||||
namespace ERPCore.Domain.Entities;
|
namespace ERPCore.Domain.Entities;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A vendor payment made against a confirmed GRN's balance (installments allowed —
|
/// A vendor payment made against a Posted purchase invoice's balance (installments
|
||||||
/// see GrnPaymentService.PayAsync). Posts its own real GL journal entry (Debit GRN
|
/// allowed — see PurchaseInvoicePaymentService.PayAsync). Posts its own real GL
|
||||||
/// Clearing / Credit the selected bank-or-cash account) before being recorded here.
|
/// journal entry (Debit Accounts Payable / Credit the selected bank-or-cash account)
|
||||||
|
/// before being recorded here.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class GrnPayment
|
public class PurchaseInvoicePayment
|
||||||
{
|
{
|
||||||
public int GrnPaymentId { get; set; }
|
public int PurchaseInvoicePaymentId { get; set; }
|
||||||
|
|
||||||
public int GrnId { get; set; }
|
public int PurchaseInvoiceId { get; set; }
|
||||||
public Grn? Grn { get; set; }
|
public PurchaseInvoice? PurchaseInvoice { get; set; }
|
||||||
|
|
||||||
public decimal Amount { get; set; }
|
public decimal Amount { get; set; }
|
||||||
public DateTime PaymentDate { get; set; }
|
public DateTime PaymentDate { get; set; }
|
||||||
@@ -17,6 +17,11 @@ public class SalesInvoice
|
|||||||
public int WarehouseId { get; set; }
|
public int WarehouseId { get; set; }
|
||||||
public Warehouse? Warehouse { get; set; }
|
public Warehouse? Warehouse { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Owning branch, set explicitly at creation — warehouses are shared across
|
||||||
|
/// branches, so branch scope can no longer be derived from <see cref="Warehouse"/>.</summary>
|
||||||
|
public int BranchId { get; set; }
|
||||||
|
public Branch? Branch { get; set; }
|
||||||
|
|
||||||
public SalesInvoiceType InvoiceType { get; set; } = SalesInvoiceType.B2C;
|
public SalesInvoiceType InvoiceType { get; set; } = SalesInvoiceType.B2C;
|
||||||
public SalesInvoiceStatus Status { get; set; } = SalesInvoiceStatus.Draft;
|
public SalesInvoiceStatus Status { get; set; } = SalesInvoiceStatus.Draft;
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ namespace ERPCore.Domain.Entities;
|
|||||||
/// A customer payment made against a Posted sales invoice's balance (installments
|
/// A customer payment made against a Posted sales invoice's balance (installments
|
||||||
/// allowed — see SalesInvoicePaymentService.PayAsync). Posts its own real GL journal
|
/// allowed — see SalesInvoicePaymentService.PayAsync). Posts its own real GL journal
|
||||||
/// entry (Debit the selected bank-or-cash account / Credit Accounts Receivable) before
|
/// entry (Debit the selected bank-or-cash account / Credit Accounts Receivable) before
|
||||||
/// being recorded here — the mirror image of <see cref="GrnPayment"/>.
|
/// being recorded here — the mirror image of <see cref="PurchaseInvoicePayment"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SalesInvoicePayment
|
public class SalesInvoicePayment
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ public class StockAdjustment
|
|||||||
public int ReasonCodeId { get; set; }
|
public int ReasonCodeId { get; set; }
|
||||||
public ReasonCode? ReasonCode { get; set; }
|
public ReasonCode? ReasonCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Owning branch, set explicitly at creation — warehouses are shared across
|
||||||
|
/// branches, so branch scope can no longer be derived from <see cref="Warehouse"/>.</summary>
|
||||||
|
public int BranchId { get; set; }
|
||||||
|
public Branch? Branch { get; set; }
|
||||||
|
|
||||||
public AdjustmentStatus Status { get; set; } = AdjustmentStatus.Posted;
|
public AdjustmentStatus Status { get; set; } = AdjustmentStatus.Posted;
|
||||||
|
|
||||||
public int CreatedBy { get; set; }
|
public int CreatedBy { get; set; }
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ public class StockCount
|
|||||||
public int WarehouseId { get; set; }
|
public int WarehouseId { get; set; }
|
||||||
public Warehouse? Warehouse { get; set; }
|
public Warehouse? Warehouse { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Owning branch, set explicitly at creation — warehouses are shared across
|
||||||
|
/// branches, so branch scope can no longer be derived from <see cref="Warehouse"/>.</summary>
|
||||||
|
public int BranchId { get; set; }
|
||||||
|
public Branch? Branch { get; set; }
|
||||||
|
|
||||||
public CountType CountType { get; set; }
|
public CountType CountType { get; set; }
|
||||||
public CountStatus Status { get; set; } = CountStatus.Draft;
|
public CountStatus Status { get; set; } = CountStatus.Draft;
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ public class StockTransfer
|
|||||||
public int DestWarehouseId { get; set; }
|
public int DestWarehouseId { get; set; }
|
||||||
public Warehouse? DestWarehouse { get; set; }
|
public Warehouse? DestWarehouse { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Owning branch, set explicitly at creation — warehouses are shared across
|
||||||
|
/// branches, so branch scope can no longer be derived from src/dest warehouse.</summary>
|
||||||
|
public int BranchId { get; set; }
|
||||||
|
public Branch? Branch { get; set; }
|
||||||
|
|
||||||
public TransferStatus Status { get; set; } = TransferStatus.Draft;
|
public TransferStatus Status { get; set; } = TransferStatus.Draft;
|
||||||
|
|
||||||
public int CreatedBy { get; set; }
|
public int CreatedBy { get; set; }
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ public class Warehouse
|
|||||||
public int WarehouseId { get; set; }
|
public int WarehouseId { get; set; }
|
||||||
public string Code { get; set; } = string.Empty;
|
public string Code { get; set; } = string.Empty;
|
||||||
public string Name { get; set; } = string.Empty;
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public int BranchId { get; set; }
|
||||||
|
public Branch? Branch { get; set; }
|
||||||
|
|
||||||
public ICollection<Bin> Bins { get; set; } = new List<Bin>();
|
public ICollection<Bin> Bins { get; set; } = new List<Bin>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace ERPCore.Domain.Enums;
|
||||||
|
|
||||||
|
public enum PurchaseInvoiceStatus
|
||||||
|
{
|
||||||
|
Draft,
|
||||||
|
Posted,
|
||||||
|
Cancelled
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
namespace ERPCore.Domain.Notifications.Modules;
|
||||||
|
|
||||||
|
[NotificationModule("sale.create", "Sale Created", "Raised when a sales document is created.", 10)]
|
||||||
|
public sealed class SaleCreateNotificationModule;
|
||||||
|
|
||||||
|
[NotificationModule("sale.posted", "Sale Posted", "Raised when a sales document is posted.", 20)]
|
||||||
|
public sealed class SalePostedNotificationModule;
|
||||||
|
|
||||||
|
[NotificationModule("grn.created", "GRN Created", "Raised when a goods receipt note is created.", 30)]
|
||||||
|
public sealed class GrnCreatedNotificationModule;
|
||||||
|
|
||||||
|
[NotificationModule("grn.confirmed", "GRN Confirmed", "Raised when a goods receipt note is confirmed.", 40)]
|
||||||
|
public sealed class GrnConfirmedNotificationModule;
|
||||||
|
|
||||||
|
[NotificationModule("purchase.order.created", "Purchase Order Created", "Raised when a purchase order is created.", 50)]
|
||||||
|
public sealed class PurchaseOrderCreatedNotificationModule;
|
||||||
|
|
||||||
|
[NotificationModule("purchase.order.approved", "Purchase Order Approved", "Raised when a purchase order is approved.", 60)]
|
||||||
|
public sealed class PurchaseOrderApprovedNotificationModule;
|
||||||
|
|
||||||
|
[NotificationModule("purchase.return.created", "Purchase Return Created", "Raised when a purchase return is created.", 70)]
|
||||||
|
public sealed class PurchaseReturnCreatedNotificationModule;
|
||||||
|
|
||||||
|
[NotificationModule("stock.adjustment.created", "Stock Adjustment Created", "Raised when a stock adjustment is created.", 80)]
|
||||||
|
public sealed class StockAdjustmentCreatedNotificationModule;
|
||||||
|
|
||||||
|
[NotificationModule("stock.transfer.created", "Stock Transfer Created", "Raised when a stock transfer is created.", 90)]
|
||||||
|
public sealed class StockTransferCreatedNotificationModule;
|
||||||
|
|
||||||
|
[NotificationModule("item.created", "Item Created", "Raised when an item master record is created.", 100)]
|
||||||
|
public sealed class ItemCreatedNotificationModule;
|
||||||
|
|
||||||
|
[NotificationModule("item.updated", "Item Updated", "Raised when an item master record is updated.", 110)]
|
||||||
|
public sealed class ItemUpdatedNotificationModule;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace ERPCore.Domain.Notifications;
|
||||||
|
|
||||||
|
public enum NotificationChannel
|
||||||
|
{
|
||||||
|
Email,
|
||||||
|
Sms,
|
||||||
|
InApp
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
namespace ERPCore.Domain.Notifications;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
|
||||||
|
public sealed class NotificationModuleAttribute : Attribute
|
||||||
|
{
|
||||||
|
public string Key { get; }
|
||||||
|
public string Label { get; }
|
||||||
|
public string Description { get; }
|
||||||
|
public int SortOrder { get; }
|
||||||
|
|
||||||
|
public NotificationModuleAttribute(string key, string label, string description, int sortOrder)
|
||||||
|
{
|
||||||
|
Key = key;
|
||||||
|
Label = label;
|
||||||
|
Description = description;
|
||||||
|
SortOrder = sortOrder;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace ERPCore.Domain.Notifications;
|
||||||
|
|
||||||
|
public sealed record NotificationModuleDefinition(
|
||||||
|
string Key,
|
||||||
|
string Label,
|
||||||
|
string Description,
|
||||||
|
int SortOrder);
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace ERPCore.Dtos.Config;
|
||||||
|
|
||||||
|
/// <summary>Branch configuration resource — the singleton toggle for whether this deployment
|
||||||
|
/// requires a branch on Employee/Warehouse records at all.</summary>
|
||||||
|
public sealed record BranchConfigDto(bool BranchingEnabled, DateTime? UpdatedAt, int? UpdatedBy);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Full replacement of the flag. <c>UpdatedBy</c> is derived from the token, never posted.
|
||||||
|
/// <para>
|
||||||
|
/// <see cref="BranchingEnabled"/> is <see cref="bool"/>? deliberately: <c>[Required]</c> on a
|
||||||
|
/// non-nullable bool is a no-op, so an omitted flag would otherwise silently bind to
|
||||||
|
/// <c>false</c>. Nullable makes the requirement actually bind — an omitted flag is a 400.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UpdateBranchConfigRequest
|
||||||
|
{
|
||||||
|
[Required] public bool? BranchingEnabled { get; set; }
|
||||||
|
}
|
||||||
@@ -4,10 +4,14 @@ namespace ERPCore.Dtos.Config;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Product configuration resource (docs/11-BACKEND-PHASE1.md §2.8). Singleton.
|
/// Product configuration resource (docs/11-BACKEND-PHASE1.md §2.8). Singleton.
|
||||||
/// <see cref="ItemTypesEnabled"/> is advisory (frontend-honoured) — see the entity docs.
|
/// <see cref="ItemTypesEnabled"/> is advisory (frontend-honoured) - see the entity docs.
|
||||||
|
/// <see cref="SellingPricesEnabled"/>, <see cref="SerialNumbersEnabled"/>,
|
||||||
|
/// <see cref="BatchListsEnabled"/> and <see cref="WarrantyPeriodsEnabled"/> are enforced
|
||||||
|
/// when items are written.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed record ProductConfigDto(
|
public sealed record ProductConfigDto(
|
||||||
bool SubcategoriesEnabled, bool BrandsEnabled, bool ItemTypesEnabled,
|
bool SubcategoriesEnabled, bool BrandsEnabled, bool ItemTypesEnabled,
|
||||||
|
bool SellingPricesEnabled, bool SerialNumbersEnabled, bool BatchListsEnabled, bool WarrantyPeriodsEnabled,
|
||||||
DateTime? UpdatedAt, int? UpdatedBy);
|
DateTime? UpdatedAt, int? UpdatedBy);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -15,8 +19,8 @@ public sealed record ProductConfigDto(
|
|||||||
/// <para>
|
/// <para>
|
||||||
/// The flags are <see cref="bool"/>? deliberately: <c>[Required]</c> on a non-nullable bool
|
/// The flags are <see cref="bool"/>? deliberately: <c>[Required]</c> on a non-nullable bool
|
||||||
/// is a no-op (it always has a value), so a body of <c>{}</c> would bind every flag to
|
/// is a no-op (it always has a value), so a body of <c>{}</c> would bind every flag to
|
||||||
/// <c>false</c> and silently switch all three features off. Nullable makes the requirement
|
/// <c>false</c> and silently switch everything off. Nullable makes the requirement actually
|
||||||
/// actually bind — an omitted flag is a 400, not an accidental disable.
|
/// bind - an omitted flag is a 400, not an accidental disable.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class UpdateProductConfigRequest
|
public sealed class UpdateProductConfigRequest
|
||||||
@@ -24,4 +28,8 @@ public sealed class UpdateProductConfigRequest
|
|||||||
[Required] public bool? SubcategoriesEnabled { get; set; }
|
[Required] public bool? SubcategoriesEnabled { get; set; }
|
||||||
[Required] public bool? BrandsEnabled { get; set; }
|
[Required] public bool? BrandsEnabled { get; set; }
|
||||||
[Required] public bool? ItemTypesEnabled { get; set; }
|
[Required] public bool? ItemTypesEnabled { get; set; }
|
||||||
|
[Required] public bool? SellingPricesEnabled { get; set; }
|
||||||
|
[Required] public bool? SerialNumbersEnabled { get; set; }
|
||||||
|
[Required] public bool? BatchListsEnabled { get; set; }
|
||||||
|
[Required] public bool? WarrantyPeriodsEnabled { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,36 +15,30 @@ public sealed record GrnLineDto(
|
|||||||
HoldStatus HoldStatus, int? BatchId, IReadOnlyList<GrnLineWarrantyNumberDto> WarrantyNumbers);
|
HoldStatus HoldStatus, int? BatchId, IReadOnlyList<GrnLineWarrantyNumberDto> WarrantyNumbers);
|
||||||
|
|
||||||
public sealed record GrnDto(
|
public sealed record GrnDto(
|
||||||
int GrnId, string DocNo, int? PoId, int VendorId, int WarehouseId, GrnStatus Status,
|
int GrnId, string DocNo, int? PoId, int VendorId, int WarehouseId, int BranchId, GrnStatus Status,
|
||||||
int CreatedBy, DateTime CreatedAt, DateTime? PostedAt, string? GlJournalNo,
|
int CreatedBy, DateTime CreatedAt, DateTime? PostedAt, string? GlJournalNo,
|
||||||
decimal PaidAmount, decimal BalanceAmount, IReadOnlyList<GrnLineDto> Lines);
|
int? PurchaseInvoiceId, string? PurchaseInvoiceNo, IReadOnlyList<GrnLineDto> Lines);
|
||||||
|
|
||||||
/// <summary>Row shape for <c>GET /grns</c> — line count instead of the lines themselves.</summary>
|
/// <summary>Row shape for <c>GET /grns</c> — line count instead of the lines themselves.</summary>
|
||||||
public sealed record GrnSummaryDto(
|
public sealed record GrnSummaryDto(
|
||||||
int GrnId, string DocNo, int? PoId, int VendorId, int WarehouseId, GrnStatus Status,
|
int GrnId, string DocNo, int? PoId, int VendorId, int WarehouseId, int BranchId, GrnStatus Status,
|
||||||
int CreatedBy, DateTime CreatedAt, DateTime? PostedAt, int LineCount,
|
int CreatedBy, DateTime CreatedAt, DateTime? PostedAt, int LineCount,
|
||||||
decimal PaidAmount, decimal BalanceAmount);
|
int? PurchaseInvoiceId, string? PurchaseInvoiceNo);
|
||||||
|
|
||||||
public sealed record CreatedLayerDto(
|
public sealed record CreatedLayerDto(
|
||||||
int LayerId, int ItemId, int WarehouseId, int? BatchId,
|
int LayerId, int ItemId, int WarehouseId, int? BatchId,
|
||||||
decimal QtyReceived, decimal QtyRemaining, decimal UnitCost, DateTime ReceiptDate);
|
decimal QtyReceived, decimal QtyRemaining, decimal UnitCost, DateTime ReceiptDate);
|
||||||
|
|
||||||
public sealed record GrnConfirmResultDto(
|
public sealed record GrnConfirmResultDto(
|
||||||
int GrnId, GrnStatus Status, DateTime PostedAt, string GlJournalNo, decimal BalanceAmount,
|
int GrnId, GrnStatus Status, DateTime PostedAt, string GlJournalNo,
|
||||||
IReadOnlyList<CreatedLayerDto> CreatedLayers, IReadOnlyList<int> LedgerRefs, PurchaseOrderStatus? PoStatus);
|
IReadOnlyList<CreatedLayerDto> CreatedLayers, IReadOnlyList<int> LedgerRefs, PurchaseOrderStatus? PoStatus);
|
||||||
|
|
||||||
public sealed record ReleaseLineResultDto(int GrnLineId, HoldStatus HoldStatus);
|
public sealed record ReleaseLineResultDto(int GrnLineId, HoldStatus HoldStatus);
|
||||||
|
|
||||||
public sealed record GrnPaymentDto(
|
/// <summary>A Confirmed, not-yet-invoiced GRN eligible to be consolidated onto a Purchase Invoice.</summary>
|
||||||
int GrnPaymentId, int GrnId, decimal Amount, DateTime PaymentDate,
|
public sealed record GrnEligibleForInvoiceDto(
|
||||||
long GlBankAccountId, string BankAccountName, string? Reference, string? GlJournalNo, DateTime CreatedAt);
|
int GrnId, string DocNo, DateTime PostedAt, int WarehouseId,
|
||||||
|
int LineCount, decimal Subtotal, decimal VatTotal, decimal GrandTotal);
|
||||||
public sealed class CreateGrnPaymentRequest
|
|
||||||
{
|
|
||||||
[Range(0.01, double.MaxValue)] public decimal Amount { get; set; }
|
|
||||||
[Required] public long GlBankAccountId { get; set; }
|
|
||||||
[StringLength(100)] public string? Reference { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Requests ----------------------------------------------------------------------
|
// Requests ----------------------------------------------------------------------
|
||||||
|
|
||||||
@@ -87,6 +81,9 @@ public sealed class CreateGrnRequest
|
|||||||
/// <summary>Required for a direct receipt (no PO); ignored otherwise (vendor derives from the PO).</summary>
|
/// <summary>Required for a direct receipt (no PO); ignored otherwise (vendor derives from the PO).</summary>
|
||||||
public int? VendorId { get; set; }
|
public int? VendorId { get; set; }
|
||||||
[Required] public int WarehouseId { get; set; }
|
[Required] public int WarehouseId { get; set; }
|
||||||
|
/// <summary>Owning branch for this GRN — warehouses are shared, so this is what actually
|
||||||
|
/// scopes the document (resolved via IBranchConfigService; required when branching is enabled).</summary>
|
||||||
|
[Required] public int BranchId { get; set; }
|
||||||
[Required, MinLength(1)] public List<CreateGrnLineInput> Lines { get; set; } = new();
|
[Required, MinLength(1)] public List<CreateGrnLineInput> Lines { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ namespace ERPCore.Dtos.Hrm;
|
|||||||
public sealed record EmployeeListItemDto(
|
public sealed record EmployeeListItemDto(
|
||||||
int EmployeeId, string EmployeeCode, string FullName, string? Email,
|
int EmployeeId, string EmployeeCode, string FullName, string? Email,
|
||||||
int DepartmentId, string? DepartmentName, int DesignationId, string? DesignationName,
|
int DepartmentId, string? DepartmentName, int DesignationId, string? DesignationName,
|
||||||
int EmploymentTypeId, string? EmploymentTypeName, int? BranchId, string? BranchName,
|
int EmploymentTypeId, string? EmploymentTypeName, int BranchId, string? BranchName,
|
||||||
EmployeeStatus Status, bool HasUserLink, DateTime HireDate);
|
EmployeeStatus Status, bool HasUserLink, DateTime HireDate);
|
||||||
|
|
||||||
public sealed record EmployeeDetailDto(
|
public sealed record EmployeeDetailDto(
|
||||||
@@ -16,7 +16,7 @@ public sealed record EmployeeDetailDto(
|
|||||||
string? City, string? PostalCode, string? Country,
|
string? City, string? PostalCode, string? Country,
|
||||||
string? EmergencyContactName, string? EmergencyContactRelationship, string? EmergencyContactPhone,
|
string? EmergencyContactName, string? EmergencyContactRelationship, string? EmergencyContactPhone,
|
||||||
DateTime HireDate, DateTime? ConfirmationDate, DateTime? LastWorkingDate,
|
DateTime HireDate, DateTime? ConfirmationDate, DateTime? LastWorkingDate,
|
||||||
int DepartmentId, int DesignationId, int EmploymentTypeId, int? BranchId, int WorkShiftId,
|
int DepartmentId, int DesignationId, int EmploymentTypeId, int BranchId, int WorkShiftId,
|
||||||
int? ReportingManagerId, string? EpfNumber, string? EtfNumber, string? TaxIdentificationNumber,
|
int? ReportingManagerId, string? EpfNumber, string? EtfNumber, string? TaxIdentificationNumber,
|
||||||
int? UserId, EmployeeStatus Status, DateTime CreatedAt, DateTime? UpdatedAt);
|
int? UserId, EmployeeStatus Status, DateTime CreatedAt, DateTime? UpdatedAt);
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ public sealed class CreateEmployeeRequest
|
|||||||
[Required] public int DepartmentId { get; set; }
|
[Required] public int DepartmentId { get; set; }
|
||||||
[Required] public int DesignationId { get; set; }
|
[Required] public int DesignationId { get; set; }
|
||||||
[Required] public int EmploymentTypeId { get; set; }
|
[Required] public int EmploymentTypeId { get; set; }
|
||||||
public int? BranchId { get; set; }
|
[Required] public int BranchId { get; set; }
|
||||||
[Required] public int WorkShiftId { get; set; }
|
[Required] public int WorkShiftId { get; set; }
|
||||||
public int? ReportingManagerId { get; set; }
|
public int? ReportingManagerId { get; set; }
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ public sealed class UpdateEmployeeRequest
|
|||||||
[Required] public int DepartmentId { get; set; }
|
[Required] public int DepartmentId { get; set; }
|
||||||
[Required] public int DesignationId { get; set; }
|
[Required] public int DesignationId { get; set; }
|
||||||
[Required] public int EmploymentTypeId { get; set; }
|
[Required] public int EmploymentTypeId { get; set; }
|
||||||
public int? BranchId { get; set; }
|
[Required] public int BranchId { get; set; }
|
||||||
[Required] public int WorkShiftId { get; set; }
|
[Required] public int WorkShiftId { get; set; }
|
||||||
public int? ReportingManagerId { get; set; }
|
public int? ReportingManagerId { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -7,20 +7,22 @@ namespace ERPCore.Dtos.Hrm;
|
|||||||
// ids, timestamps) excluded from create/update requests (02-SECURITY B.6/C.1).
|
// ids, timestamps) excluded from create/update requests (02-SECURITY B.6/C.1).
|
||||||
|
|
||||||
public sealed record BranchDto(
|
public sealed record BranchDto(
|
||||||
int BranchId, string Code, string Name, string? Address, EntityStatus Status,
|
int BranchId, string Code, string Name, string? Address, string? PhoneNumber, EntityStatus Status,
|
||||||
DateTime CreatedAt, DateTime? UpdatedAt);
|
DateTime CreatedAt, DateTime? UpdatedAt);
|
||||||
|
|
||||||
|
// Code is server-generated (BranchService.GenerateCodeAsync) — never accepted from the client.
|
||||||
public sealed class CreateBranchRequest
|
public sealed class CreateBranchRequest
|
||||||
{
|
{
|
||||||
[Required, StringLength(20)] public string Code { get; set; } = string.Empty;
|
|
||||||
[Required, StringLength(200)] public string Name { get; set; } = string.Empty;
|
[Required, StringLength(200)] public string Name { get; set; } = string.Empty;
|
||||||
[StringLength(500)] public string? Address { get; set; }
|
[StringLength(500)] public string? Address { get; set; }
|
||||||
|
[StringLength(30)] public string? PhoneNumber { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class UpdateBranchRequest
|
public sealed class UpdateBranchRequest
|
||||||
{
|
{
|
||||||
[Required, StringLength(200)] public string Name { get; set; } = string.Empty;
|
[Required, StringLength(200)] public string Name { get; set; } = string.Empty;
|
||||||
[StringLength(500)] public string? Address { get; set; }
|
[StringLength(500)] public string? Address { get; set; }
|
||||||
|
[StringLength(30)] public string? PhoneNumber { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record DepartmentDto(
|
public sealed record DepartmentDto(
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ public sealed record ItemListItemDto(
|
|||||||
int ItemId, string Sku, string Name, int CategoryId, int? SubCategoryId, int? BrandId,
|
int ItemId, string Sku, string Name, int CategoryId, int? SubCategoryId, int? BrandId,
|
||||||
int BaseUomId, int? DefaultVendorId, StockNature StockNature, TrackingMode TrackingMode,
|
int BaseUomId, int? DefaultVendorId, StockNature StockNature, TrackingMode TrackingMode,
|
||||||
Warranty Warranty, int? WarrantyPeriodMonths,
|
Warranty Warranty, int? WarrantyPeriodMonths,
|
||||||
string? TaxClass, decimal? SalePrice,
|
string? TaxClass, decimal? SellingMinPrice, decimal? SellingMaxPrice, decimal? SellingFlatPrice,
|
||||||
decimal? ContentQty, MeasureUnit? ContentUnit,
|
decimal? ContentQty, MeasureUnit? ContentUnit,
|
||||||
decimal? ContentBaseQty, MeasureUnit? ContentBaseUnit,
|
decimal? ContentBaseQty, MeasureUnit? ContentBaseUnit,
|
||||||
EntityStatus Status);
|
EntityStatus Status);
|
||||||
@@ -31,7 +31,7 @@ public sealed record ItemDetailDto(
|
|||||||
int? SubCategoryId, int? BrandId, int BaseUomId, int? DefaultVendorId,
|
int? SubCategoryId, int? BrandId, int BaseUomId, int? DefaultVendorId,
|
||||||
StockNature StockNature, TrackingMode TrackingMode,
|
StockNature StockNature, TrackingMode TrackingMode,
|
||||||
Warranty Warranty, int? WarrantyPeriodMonths,
|
Warranty Warranty, int? WarrantyPeriodMonths,
|
||||||
string? TaxClass, decimal? SalePrice,
|
string? TaxClass, decimal? SellingMinPrice, decimal? SellingMaxPrice, decimal? SellingFlatPrice,
|
||||||
decimal? ContentQty, MeasureUnit? ContentUnit,
|
decimal? ContentQty, MeasureUnit? ContentUnit,
|
||||||
decimal? ContentBaseQty, MeasureUnit? ContentBaseUnit,
|
decimal? ContentBaseQty, MeasureUnit? ContentBaseUnit,
|
||||||
EntityStatus Status, IReadOnlyList<ItemReorderDto> Reorder,
|
EntityStatus Status, IReadOnlyList<ItemReorderDto> Reorder,
|
||||||
@@ -66,7 +66,9 @@ public sealed class CreateItemRequest
|
|||||||
public int? WarrantyPeriodMonths { get; set; }
|
public int? WarrantyPeriodMonths { get; set; }
|
||||||
[StringLength(20)] public string? TaxClass { get; set; }
|
[StringLength(20)] public string? TaxClass { get; set; }
|
||||||
/// <summary>Optional fixed sale price (Sales only). Null ⇒ sell at stock/FIFO value.</summary>
|
/// <summary>Optional fixed sale price (Sales only). Null ⇒ sell at stock/FIFO value.</summary>
|
||||||
[Range(0, double.MaxValue)] public decimal? SalePrice { get; set; }
|
[Range(0, double.MaxValue)] public decimal? SellingMinPrice { get; set; }
|
||||||
|
[Range(0, double.MaxValue)] public decimal? SellingMaxPrice { get; set; }
|
||||||
|
[Range(0, double.MaxValue)] public decimal? SellingFlatPrice { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// How much one pack holds. Supply with <see cref="ContentUnit"/> or leave both null
|
/// How much one pack holds. Supply with <see cref="ContentUnit"/> or leave both null
|
||||||
@@ -96,7 +98,9 @@ public sealed class UpdateItemRequest
|
|||||||
public int? WarrantyPeriodMonths { get; set; }
|
public int? WarrantyPeriodMonths { get; set; }
|
||||||
[StringLength(20)] public string? TaxClass { get; set; }
|
[StringLength(20)] public string? TaxClass { get; set; }
|
||||||
/// <summary>Optional fixed sale price (Sales only). Null ⇒ sell at stock/FIFO value.</summary>
|
/// <summary>Optional fixed sale price (Sales only). Null ⇒ sell at stock/FIFO value.</summary>
|
||||||
[Range(0, double.MaxValue)] public decimal? SalePrice { get; set; }
|
[Range(0, double.MaxValue)] public decimal? SellingMinPrice { get; set; }
|
||||||
|
[Range(0, double.MaxValue)] public decimal? SellingMaxPrice { get; set; }
|
||||||
|
[Range(0, double.MaxValue)] public decimal? SellingFlatPrice { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// How much one pack holds. Supply with <see cref="ContentUnit"/> or leave both null
|
/// How much one pack holds. Supply with <see cref="ContentUnit"/> or leave both null
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace ERPCore.Dtos.Notifications;
|
||||||
|
|
||||||
|
public sealed record NotificationModuleConfigDto(
|
||||||
|
string ModuleKey,
|
||||||
|
string Label,
|
||||||
|
string Description,
|
||||||
|
bool EmailEnabled,
|
||||||
|
bool SmsEnabled,
|
||||||
|
bool InAppEnabled);
|
||||||
|
|
||||||
|
public sealed record NotificationConfigDto(
|
||||||
|
IReadOnlyList<NotificationModuleConfigDto> Modules,
|
||||||
|
DateTime? UpdatedAt,
|
||||||
|
int? UpdatedBy);
|
||||||
|
|
||||||
|
public sealed class UpdateNotificationModuleRequest
|
||||||
|
{
|
||||||
|
[Required, StringLength(100)] public string ModuleKey { get; set; } = string.Empty;
|
||||||
|
[Required] public bool? EmailEnabled { get; set; }
|
||||||
|
[Required] public bool? SmsEnabled { get; set; }
|
||||||
|
[Required] public bool? InAppEnabled { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class UpdateNotificationConfigRequest
|
||||||
|
{
|
||||||
|
[Required, MinLength(0)] public List<UpdateNotificationModuleRequest> Modules { get; set; } = new();
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using ERPCore.Domain.Enums;
|
||||||
|
|
||||||
|
namespace ERPCore.Dtos.Procurement;
|
||||||
|
|
||||||
|
public sealed record PurchaseInvoiceLineDto(
|
||||||
|
int PurchaseInvoiceLineId, int GrnId, string GrnDocNo, DateTime GrnPostedAt,
|
||||||
|
decimal Subtotal, decimal VatTotal, decimal LineTotal);
|
||||||
|
|
||||||
|
public sealed record PurchaseInvoiceTotalsDto(
|
||||||
|
decimal Subtotal, decimal VatTotal, decimal GrandTotal, decimal PaidAmount, decimal BalanceAmount);
|
||||||
|
|
||||||
|
public sealed record PurchaseInvoiceDto(
|
||||||
|
int PurchaseInvoiceId, string DocNo, string VendorInvoiceNo, DateTime VendorInvoiceDate,
|
||||||
|
int VendorId, string VendorSnapshotName, string? VendorSnapshotTaxNo,
|
||||||
|
PurchaseInvoiceStatus Status, int GrnCount, int CreatedBy, DateTime CreatedAt, DateTime? UpdatedAt,
|
||||||
|
string? GlJournalNo, DateTime? GlPostedAt,
|
||||||
|
PurchaseInvoiceTotalsDto Totals, IReadOnlyList<PurchaseInvoiceLineDto> Lines);
|
||||||
|
|
||||||
|
public sealed record PurchaseInvoiceSummaryDto(
|
||||||
|
int PurchaseInvoiceId, string DocNo, string VendorInvoiceNo, DateTime VendorInvoiceDate,
|
||||||
|
int VendorId, string VendorSnapshotName, PurchaseInvoiceStatus Status, int GrnCount,
|
||||||
|
PurchaseInvoiceTotalsDto Totals, DateTime CreatedAt, string? GlJournalNo);
|
||||||
|
|
||||||
|
public sealed record PurchaseInvoicePaymentDto(
|
||||||
|
int PurchaseInvoicePaymentId, int PurchaseInvoiceId, decimal Amount, DateTime PaymentDate,
|
||||||
|
long GlBankAccountId, string BankAccountName, string? Reference, string? GlJournalNo, DateTime CreatedAt);
|
||||||
|
|
||||||
|
public sealed class CreatePurchaseInvoicePaymentRequest
|
||||||
|
{
|
||||||
|
[Range(0.01, double.MaxValue)] public decimal Amount { get; set; }
|
||||||
|
[Required] public long GlBankAccountId { get; set; }
|
||||||
|
[StringLength(100)] public string? Reference { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class CreatePurchaseInvoiceRequest
|
||||||
|
{
|
||||||
|
[Required] public int VendorId { get; set; }
|
||||||
|
[Required, StringLength(50)] public string VendorInvoiceNo { get; set; } = string.Empty;
|
||||||
|
[Required] public DateTime VendorInvoiceDate { get; set; }
|
||||||
|
[Required, MinLength(1)] public List<int> GrnIds { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class UpdatePurchaseInvoiceRequest
|
||||||
|
{
|
||||||
|
[Required] public int VendorId { get; set; }
|
||||||
|
[Required, StringLength(50)] public string VendorInvoiceNo { get; set; } = string.Empty;
|
||||||
|
[Required] public DateTime VendorInvoiceDate { get; set; }
|
||||||
|
[Required, MinLength(1)] public List<int> GrnIds { get; set; } = new();
|
||||||
|
}
|
||||||
@@ -70,6 +70,35 @@ public sealed record RunEventDto(
|
|||||||
int EventId, int? RunStageId, RunStageEventType EventType, string? Note,
|
int EventId, int? RunStageId, RunStageEventType EventType, string? Note,
|
||||||
JsonElement? Payload, int UserId, DateTime CreatedAt);
|
JsonElement? Payload, int UserId, DateTime CreatedAt);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One row of the produced-items report (<c>GET /production-runs/produced</c>) — the
|
||||||
|
/// terminal output of a run that has produced something, costed the same way
|
||||||
|
/// <c>PostReceiptAsync</c> costs the finished layer at approve (FR-MFG-13):
|
||||||
|
/// <c>unitCost = (Σ ConsumedValue − Σ ReturnedValue) / (ProducedQty − ScrappedQty)</c>.
|
||||||
|
/// One row per run — server validation (<see cref="ProductionGraphValidator"/> via
|
||||||
|
/// FR-MFG-05) guarantees exactly one item-bearing output per run, always the terminal
|
||||||
|
/// one, so run and produced line coincide 1:1.
|
||||||
|
/// <para><c>UnitCost</c>/<c>TotalCost</c> are final only once <c>Status</c> is
|
||||||
|
/// <c>Completed</c> — a leftover return can still shrink the pool between the terminal
|
||||||
|
/// stage's Done and its Approve, same caveat as <see cref="CostPoolDto"/>.</para>
|
||||||
|
/// </summary>
|
||||||
|
public sealed record ProducedLineDto(
|
||||||
|
int RunId, string DocNo, ProductionRunStatus Status,
|
||||||
|
int TemplateId, string TemplateName,
|
||||||
|
int WarehouseId, string WarehouseName,
|
||||||
|
int ItemId, string ItemName,
|
||||||
|
decimal ProducedQty, decimal ScrappedQty, decimal GoodQty,
|
||||||
|
decimal UnitCost, decimal TotalCost,
|
||||||
|
DateTime CreatedAt, DateTime? CompletedAt);
|
||||||
|
|
||||||
|
/// <summary>Aggregate footer for the produced-items report — totals over every row the filter
|
||||||
|
/// matched, not just the current page (there is no pagination here, matching the Sales report
|
||||||
|
/// convention of flat, filter-bounded rows).</summary>
|
||||||
|
public sealed record ProducedSummaryDto(
|
||||||
|
int RunCount, decimal TotalProducedQty, decimal TotalScrappedQty, decimal TotalGoodQty, decimal TotalCost);
|
||||||
|
|
||||||
|
public sealed record ProducedListResponseDto(IReadOnlyList<ProducedLineDto> Items, ProducedSummaryDto Summary);
|
||||||
|
|
||||||
/// <summary>Full run graph (docs/30 §D.2 <c>GET /production-runs/{id}</c>).</summary>
|
/// <summary>Full run graph (docs/30 §D.2 <c>GET /production-runs/{id}</c>).</summary>
|
||||||
public sealed record RunGraphDto(
|
public sealed record RunGraphDto(
|
||||||
int RunId, string DocNo, int TemplateId, string TemplateName,
|
int RunId, string DocNo, int TemplateId, string TemplateName,
|
||||||
@@ -99,8 +128,10 @@ public sealed class CreateRunRequest
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Per-run scaling override (FR-MFG-08). Only accepted while the stage has not started
|
/// Per-run scaling override (FR-MFG-08). Accepted before the stage starts, and — for a Stock
|
||||||
/// (<c>409 STAGE_NOT_EDITABLE</c>).
|
/// input only — while it is running too: raising <see cref="StageQuantityLine.PlannedQty"/>
|
||||||
|
/// draws the extra amount from FIFO, lowering it returns the excess (<c>409
|
||||||
|
/// STAGE_NOT_EDITABLE</c> for every other status).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class UpdateStageQuantitiesRequest
|
public sealed class UpdateStageQuantitiesRequest
|
||||||
{
|
{
|
||||||
@@ -116,4 +147,11 @@ public sealed class StageQuantityLine
|
|||||||
|
|
||||||
[Range(0.0001, double.MaxValue)]
|
[Range(0.0001, double.MaxValue)]
|
||||||
public decimal PlannedQty { get; set; }
|
public decimal PlannedQty { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Required only when this line lowers a running Stock input below what it has already
|
||||||
|
/// consumed — the excess is returned to stock the same way <c>ReturnLeftoverAsync</c>
|
||||||
|
/// does, and that always needs a reason (FR-MFG-14). Ignored otherwise.
|
||||||
|
/// </summary>
|
||||||
|
public int? ReasonCodeId { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,13 @@ namespace ERPCore.Dtos.Rbac;
|
|||||||
|
|
||||||
/// <summary>Response for `GET /api/v1/auth/me` — the frontend's authoritative source
|
/// <summary>Response for `GET /api/v1/auth/me` — the frontend's authoritative source
|
||||||
/// for the current user's role and permitted sidebar nav codes (replaces trusting
|
/// for the current user's role and permitted sidebar nav codes (replaces trusting
|
||||||
/// the stale, client-only `roleId` cached in localStorage).</summary>
|
/// the stale, client-only `roleId` cached in localStorage). <see cref="BranchId"/>/
|
||||||
public sealed record MeResponseDto(string? RoleCode, string? RoleName, IReadOnlyList<string> NavCodes);
|
/// <see cref="BranchName"/> come from the caller's linked <c>Employee</c> record, when
|
||||||
|
/// one exists — null for a login with no Employee link. <see cref="IsAdmin"/> gates
|
||||||
|
/// branch pickers client-side only (no server-side branch enforcement exists yet,
|
||||||
|
/// see docs/BRANCH_ISOLATION_PLAN.md). <see cref="BranchingEnabled"/> mirrors the
|
||||||
|
/// singleton <c>BranchConfig</c> toggle — when false the frontend hides branch pickers
|
||||||
|
/// entirely rather than merely restricting them by role.</summary>
|
||||||
|
public sealed record MeResponseDto(
|
||||||
|
string? RoleCode, string? RoleName, IReadOnlyList<string> NavCodes,
|
||||||
|
bool IsAdmin, int? BranchId, string? BranchName, bool BranchingEnabled);
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ public sealed class CreateBundleSaleTemplateLineRequest
|
|||||||
|
|
||||||
public sealed class CreateBundleSaleTemplateRequest
|
public sealed class CreateBundleSaleTemplateRequest
|
||||||
{
|
{
|
||||||
[Required] public string TemplateCode { get; set; } = string.Empty;
|
|
||||||
[Required] public string TemplateName { get; set; } = string.Empty;
|
[Required] public string TemplateName { get; set; } = string.Empty;
|
||||||
public string? Description { get; set; }
|
public string? Description { get; set; }
|
||||||
[Required, MinLength(1)] public List<CreateBundleSaleTemplateLineRequest> Lines { get; set; } = new();
|
[Required, MinLength(1)] public List<CreateBundleSaleTemplateLineRequest> Lines { get; set; } = new();
|
||||||
|
|||||||
@@ -8,11 +8,21 @@ namespace ERPCore.Dtos.Sales;
|
|||||||
/// Merges sales-slip and bundle-sale lines for the same item into one row.</summary>
|
/// Merges sales-slip and bundle-sale lines for the same item into one row.</summary>
|
||||||
public sealed record SalesDayEndItemLineDto(int ItemId, string Sku, string Name, decimal Qty, decimal Revenue);
|
public sealed record SalesDayEndItemLineDto(int ItemId, string Sku, string Name, decimal Qty, decimal Revenue);
|
||||||
|
|
||||||
|
/// <summary>Read-only visibility into that cashier's Sales Invoices for the date — informational
|
||||||
|
/// only, appended to <see cref="SalesDayEndDto"/> and <see cref="SalesDayEndPreviewDto"/>.
|
||||||
|
/// Invoices post their own GL entry on Confirm (SalesPostingService.PostInvoiceAsync) and have no
|
||||||
|
/// DayEndId, so they're never locked against a close: these fields are computed live from current
|
||||||
|
/// data on every read, unlike the frozen Slip/Bundle totals alongside them.</summary>
|
||||||
|
public sealed record SalesDayEndInvoiceSummaryDto(
|
||||||
|
int InvoiceCount, decimal InvoiceSubtotal, decimal InvoiceDiscountTotal, decimal InvoiceTaxTotal,
|
||||||
|
decimal InvoiceGrandTotal, IReadOnlyList<string> InvoiceNumbers);
|
||||||
|
|
||||||
public sealed record SalesDayEndDto(
|
public sealed record SalesDayEndDto(
|
||||||
int SalesDayEndId, string DocNo, int CashierUserId, DateOnly BusinessDate,
|
int SalesDayEndId, string DocNo, int CashierUserId, DateOnly BusinessDate,
|
||||||
int SlipCount, int BundleCount, decimal Subtotal, decimal DiscountTotal, decimal TaxTotal, decimal GrandTotal,
|
int SlipCount, int BundleCount, decimal Subtotal, decimal DiscountTotal, decimal TaxTotal, decimal GrandTotal,
|
||||||
decimal CostOfGoodsSold, string? GlJournalNo, DateTime? GlPostedAt, int ClosedBy, DateTime ClosedAt,
|
decimal CostOfGoodsSold, string? GlJournalNo, DateTime? GlPostedAt, int ClosedBy, DateTime ClosedAt,
|
||||||
IReadOnlyList<string> SlipNumbers, IReadOnlyList<string> BundleNumbers, IReadOnlyList<SalesDayEndItemLineDto> ItemBreakdown);
|
IReadOnlyList<string> SlipNumbers, IReadOnlyList<string> BundleNumbers, IReadOnlyList<SalesDayEndItemLineDto> ItemBreakdown,
|
||||||
|
SalesDayEndInvoiceSummaryDto Invoices);
|
||||||
|
|
||||||
public sealed record SalesDayEndSummaryDto(
|
public sealed record SalesDayEndSummaryDto(
|
||||||
int SalesDayEndId, string DocNo, int CashierUserId, DateOnly BusinessDate,
|
int SalesDayEndId, string DocNo, int CashierUserId, DateOnly BusinessDate,
|
||||||
@@ -25,7 +35,8 @@ public sealed record SalesDayEndPreviewDto(
|
|||||||
int SlipCount, int BundleCount, decimal Subtotal, decimal DiscountTotal, decimal TaxTotal, decimal GrandTotal,
|
int SlipCount, int BundleCount, decimal Subtotal, decimal DiscountTotal, decimal TaxTotal, decimal GrandTotal,
|
||||||
IReadOnlyList<string> SlipNumbers, IReadOnlyList<string> BundleNumbers,
|
IReadOnlyList<string> SlipNumbers, IReadOnlyList<string> BundleNumbers,
|
||||||
IReadOnlyList<DraftSlipBlockingCloseDto> DraftSlipsBlockingClose,
|
IReadOnlyList<DraftSlipBlockingCloseDto> DraftSlipsBlockingClose,
|
||||||
IReadOnlyList<DraftBundleBlockingCloseDto> DraftBundleSalesBlockingClose);
|
IReadOnlyList<DraftBundleBlockingCloseDto> DraftBundleSalesBlockingClose,
|
||||||
|
SalesDayEndInvoiceSummaryDto Invoices);
|
||||||
|
|
||||||
public sealed record DraftSlipBlockingCloseDto(int SalesSlipId, string SlipNo, decimal GrandTotal);
|
public sealed record DraftSlipBlockingCloseDto(int SalesSlipId, string SlipNo, decimal GrandTotal);
|
||||||
public sealed record DraftBundleBlockingCloseDto(int BundleSaleId, string BundleNo, decimal GrandTotal);
|
public sealed record DraftBundleBlockingCloseDto(int BundleSaleId, string BundleNo, decimal GrandTotal);
|
||||||
|
|||||||
@@ -15,14 +15,14 @@ public sealed record SalesInvoiceTotalsDto(
|
|||||||
|
|
||||||
public sealed record SalesInvoiceDto(
|
public sealed record SalesInvoiceDto(
|
||||||
int SalesInvoiceId, string InvoiceNo, DateTime InvoiceDate, int CustomerId,
|
int SalesInvoiceId, string InvoiceNo, DateTime InvoiceDate, int CustomerId,
|
||||||
string CustomerSnapshotName, string? CustomerSnapshotTaxNo, int WarehouseId,
|
string CustomerSnapshotName, string? CustomerSnapshotTaxNo, int WarehouseId, int BranchId,
|
||||||
SalesInvoiceType InvoiceType, SalesInvoiceStatus Status, int CreatedBy, DateTime CreatedAt,
|
SalesInvoiceType InvoiceType, SalesInvoiceStatus Status, int CreatedBy, DateTime CreatedAt,
|
||||||
DateTime? UpdatedAt, string? GlJournalNo, DateTime? GlPostedAt,
|
DateTime? UpdatedAt, string? GlJournalNo, DateTime? GlPostedAt,
|
||||||
SalesInvoiceTotalsDto Totals, IReadOnlyList<SalesInvoiceLineDto> Lines);
|
SalesInvoiceTotalsDto Totals, IReadOnlyList<SalesInvoiceLineDto> Lines);
|
||||||
|
|
||||||
public sealed record SalesInvoiceSummaryDto(
|
public sealed record SalesInvoiceSummaryDto(
|
||||||
int SalesInvoiceId, string InvoiceNo, DateTime InvoiceDate, int CustomerId,
|
int SalesInvoiceId, string InvoiceNo, DateTime InvoiceDate, int CustomerId,
|
||||||
string CustomerSnapshotName, int WarehouseId, SalesInvoiceType InvoiceType,
|
string CustomerSnapshotName, int WarehouseId, int BranchId, SalesInvoiceType InvoiceType,
|
||||||
SalesInvoiceStatus Status, SalesInvoiceTotalsDto Totals, DateTime CreatedAt, string? GlJournalNo);
|
SalesInvoiceStatus Status, SalesInvoiceTotalsDto Totals, DateTime CreatedAt, string? GlJournalNo);
|
||||||
|
|
||||||
public sealed record SalesInvoicePaymentDto(
|
public sealed record SalesInvoicePaymentDto(
|
||||||
@@ -65,6 +65,9 @@ public sealed class CreateSalesInvoiceRequest
|
|||||||
{
|
{
|
||||||
[Required] public int CustomerId { get; set; }
|
[Required] public int CustomerId { get; set; }
|
||||||
[Required] public int WarehouseId { get; set; }
|
[Required] public int WarehouseId { get; set; }
|
||||||
|
/// <summary>Owning branch — warehouses are shared, so this is what actually scopes the
|
||||||
|
/// document (resolved via IBranchConfigService).</summary>
|
||||||
|
[Required] public int BranchId { get; set; }
|
||||||
[Required, EnumDataType(typeof(SalesInvoiceType))] public SalesInvoiceType InvoiceType { get; set; } = SalesInvoiceType.B2C;
|
[Required, EnumDataType(typeof(SalesInvoiceType))] public SalesInvoiceType InvoiceType { get; set; } = SalesInvoiceType.B2C;
|
||||||
[Required, MinLength(1)] public List<CreateSalesInvoiceLineRequest> Lines { get; set; } = new();
|
[Required, MinLength(1)] public List<CreateSalesInvoiceLineRequest> Lines { get; set; } = new();
|
||||||
}
|
}
|
||||||
@@ -73,6 +76,9 @@ public sealed class UpdateSalesInvoiceRequest
|
|||||||
{
|
{
|
||||||
[Required] public int CustomerId { get; set; }
|
[Required] public int CustomerId { get; set; }
|
||||||
[Required] public int WarehouseId { get; set; }
|
[Required] public int WarehouseId { get; set; }
|
||||||
|
/// <summary>Owning branch — warehouses are shared, so this is what actually scopes the
|
||||||
|
/// document (resolved via IBranchConfigService).</summary>
|
||||||
|
[Required] public int BranchId { get; set; }
|
||||||
[Required, EnumDataType(typeof(SalesInvoiceType))] public SalesInvoiceType InvoiceType { get; set; } = SalesInvoiceType.B2C;
|
[Required, EnumDataType(typeof(SalesInvoiceType))] public SalesInvoiceType InvoiceType { get; set; } = SalesInvoiceType.B2C;
|
||||||
[Required, MinLength(1)] public List<CreateSalesInvoiceLineRequest> Lines { get; set; } = new();
|
[Required, MinLength(1)] public List<CreateSalesInvoiceLineRequest> Lines { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ public sealed record SalesFreeIssueRewardOptionDto(
|
|||||||
int ItemId,
|
int ItemId,
|
||||||
string Sku,
|
string Sku,
|
||||||
string Name,
|
string Name,
|
||||||
decimal? SalePrice);
|
decimal? SellingFlatPrice);
|
||||||
|
|
||||||
public sealed record SalesFreeIssueSuggestionLineDto(
|
public sealed record SalesFreeIssueSuggestionLineDto(
|
||||||
int SalesSlipLineId,
|
int SalesSlipLineId,
|
||||||
@@ -20,4 +20,4 @@ public sealed record SalesFreeIssueSuggestionDto(
|
|||||||
int SalesSlipId,
|
int SalesSlipId,
|
||||||
string SlipNo,
|
string SlipNo,
|
||||||
DateTime SlipDate,
|
DateTime SlipDate,
|
||||||
IReadOnlyList<SalesFreeIssueSuggestionLineDto> Lines);
|
IReadOnlyList<SalesFreeIssueSuggestionLineDto> Lines);
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ namespace ERPCore.Dtos.Stock;
|
|||||||
public sealed record AdjustmentLineDto(int AdjLineId, int ItemId, int? BinId, int? BatchId, decimal QtyDelta);
|
public sealed record AdjustmentLineDto(int AdjLineId, int ItemId, int? BinId, int? BatchId, decimal QtyDelta);
|
||||||
|
|
||||||
public sealed record AdjustmentDto(
|
public sealed record AdjustmentDto(
|
||||||
int AdjustmentId, string DocNo, int WarehouseId, int ReasonCodeId, AdjustmentStatus Status,
|
int AdjustmentId, string DocNo, int WarehouseId, int BranchId, int ReasonCodeId, AdjustmentStatus Status,
|
||||||
int CreatedBy, DateTime CreatedAt, string? GlJournalNo, DateTime? GlPostedAt,
|
int CreatedBy, DateTime CreatedAt, string? GlJournalNo, DateTime? GlPostedAt,
|
||||||
IReadOnlyList<AdjustmentLineDto> Lines, IReadOnlyList<int> LedgerRefs);
|
IReadOnlyList<AdjustmentLineDto> Lines, IReadOnlyList<int> LedgerRefs);
|
||||||
|
|
||||||
/// <summary>Row shape for <c>GET /stock-adjustments</c> — no lines/ledgerRefs (those need a per-row query).</summary>
|
/// <summary>Row shape for <c>GET /stock-adjustments</c> — no lines/ledgerRefs (those need a per-row query).</summary>
|
||||||
public sealed record AdjustmentSummaryDto(
|
public sealed record AdjustmentSummaryDto(
|
||||||
int AdjustmentId, string DocNo, int WarehouseId, int ReasonCodeId, AdjustmentStatus Status,
|
int AdjustmentId, string DocNo, int WarehouseId, int BranchId, int ReasonCodeId, AdjustmentStatus Status,
|
||||||
int CreatedBy, DateTime CreatedAt, int LineCount, string? GlJournalNo);
|
int CreatedBy, DateTime CreatedAt, int LineCount, string? GlJournalNo);
|
||||||
|
|
||||||
// Requests ----------------------------------------------------------------------
|
// Requests ----------------------------------------------------------------------
|
||||||
@@ -31,6 +31,9 @@ public sealed class CreateAdjustmentLineInput
|
|||||||
public sealed class CreateAdjustmentRequest
|
public sealed class CreateAdjustmentRequest
|
||||||
{
|
{
|
||||||
[Required] public int WarehouseId { get; set; }
|
[Required] public int WarehouseId { get; set; }
|
||||||
|
/// <summary>Owning branch — warehouses are shared, so this is what actually scopes the
|
||||||
|
/// document (resolved via IBranchConfigService).</summary>
|
||||||
|
[Required] public int BranchId { get; set; }
|
||||||
/// <summary>Nullable so an omitted value is a distinct <c>REASON_CODE_REQUIRED</c> error, not 0.</summary>
|
/// <summary>Nullable so an omitted value is a distinct <c>REASON_CODE_REQUIRED</c> error, not 0.</summary>
|
||||||
public int? ReasonCodeId { get; set; }
|
public int? ReasonCodeId { get; set; }
|
||||||
[Required, MinLength(1)] public List<CreateAdjustmentLineInput> Lines { get; set; } = new();
|
[Required, MinLength(1)] public List<CreateAdjustmentLineInput> Lines { get; set; } = new();
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ namespace ERPCore.Dtos.Stock;
|
|||||||
public sealed record CountLineDto(int CountLineId, int ItemId, int? BinId, decimal SystemQty, decimal? CountedQty, decimal? Variance);
|
public sealed record CountLineDto(int CountLineId, int ItemId, int? BinId, decimal SystemQty, decimal? CountedQty, decimal? Variance);
|
||||||
|
|
||||||
public sealed record CountDto(
|
public sealed record CountDto(
|
||||||
int CountId, string DocNo, int WarehouseId, CountType CountType, CountStatus Status,
|
int CountId, string DocNo, int WarehouseId, int BranchId, CountType CountType, CountStatus Status,
|
||||||
int CreatedBy, DateTime CreatedAt, IReadOnlyList<CountLineDto> Lines);
|
int CreatedBy, DateTime CreatedAt, IReadOnlyList<CountLineDto> Lines);
|
||||||
|
|
||||||
/// <summary>Row shape for <c>GET /stock-counts</c> — line count instead of the lines themselves.</summary>
|
/// <summary>Row shape for <c>GET /stock-counts</c> — line count instead of the lines themselves.</summary>
|
||||||
public sealed record CountSummaryDto(
|
public sealed record CountSummaryDto(
|
||||||
int CountId, string DocNo, int WarehouseId, CountType CountType, CountStatus Status,
|
int CountId, string DocNo, int WarehouseId, int BranchId, CountType CountType, CountStatus Status,
|
||||||
int CreatedBy, DateTime CreatedAt, int LineCount);
|
int CreatedBy, DateTime CreatedAt, int LineCount);
|
||||||
|
|
||||||
public sealed record CountPostResultDto(int CountId, CountStatus Status, int? AdjustmentId, string? GlJournalNo, IReadOnlyList<int> LedgerRefs);
|
public sealed record CountPostResultDto(int CountId, CountStatus Status, int? AdjustmentId, string? GlJournalNo, IReadOnlyList<int> LedgerRefs);
|
||||||
@@ -23,6 +23,9 @@ public sealed record CountPostResultDto(int CountId, CountStatus Status, int? Ad
|
|||||||
public sealed class CreateCountRequest
|
public sealed class CreateCountRequest
|
||||||
{
|
{
|
||||||
[Required] public int WarehouseId { get; set; }
|
[Required] public int WarehouseId { get; set; }
|
||||||
|
/// <summary>Owning branch — warehouses are shared, so this is what actually scopes the
|
||||||
|
/// document (resolved via IBranchConfigService).</summary>
|
||||||
|
[Required] public int BranchId { get; set; }
|
||||||
[Required, EnumDataType(typeof(CountType))] public CountType CountType { get; set; }
|
[Required, EnumDataType(typeof(CountType))] public CountType CountType { get; set; }
|
||||||
[Required, MinLength(1)] public List<int> ItemIds { get; set; } = new();
|
[Required, MinLength(1)] public List<int> ItemIds { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ public sealed record TransferLineDto(
|
|||||||
int TransferLineId, int ItemId, int? SrcBinId, int? DestBinId, int? BatchId, decimal Qty, decimal QtyReceived);
|
int TransferLineId, int ItemId, int? SrcBinId, int? DestBinId, int? BatchId, decimal Qty, decimal QtyReceived);
|
||||||
|
|
||||||
public sealed record TransferDto(
|
public sealed record TransferDto(
|
||||||
int TransferId, string DocNo, int SrcWarehouseId, int DestWarehouseId,
|
int TransferId, string DocNo, int SrcWarehouseId, int DestWarehouseId, int BranchId,
|
||||||
TransferStatus Status, int CreatedBy, DateTime CreatedAt, IReadOnlyList<TransferLineDto> Lines);
|
TransferStatus Status, int CreatedBy, DateTime CreatedAt, IReadOnlyList<TransferLineDto> Lines);
|
||||||
|
|
||||||
/// <summary>Row shape for <c>GET /stock-transfers</c> — line count instead of the lines themselves.</summary>
|
/// <summary>Row shape for <c>GET /stock-transfers</c> — line count instead of the lines themselves.</summary>
|
||||||
public sealed record TransferSummaryDto(
|
public sealed record TransferSummaryDto(
|
||||||
int TransferId, string DocNo, int SrcWarehouseId, int DestWarehouseId,
|
int TransferId, string DocNo, int SrcWarehouseId, int DestWarehouseId, int BranchId,
|
||||||
TransferStatus Status, int CreatedBy, DateTime CreatedAt, int LineCount);
|
TransferStatus Status, int CreatedBy, DateTime CreatedAt, int LineCount);
|
||||||
|
|
||||||
public sealed record ConsumedLayerDto(int LayerId, decimal QtyConsumed, decimal UnitCost);
|
public sealed record ConsumedLayerDto(int LayerId, decimal QtyConsumed, decimal UnitCost);
|
||||||
@@ -42,6 +42,9 @@ public sealed class CreateTransferRequest
|
|||||||
{
|
{
|
||||||
[Required] public int SrcWarehouseId { get; set; }
|
[Required] public int SrcWarehouseId { get; set; }
|
||||||
[Required] public int DestWarehouseId { get; set; }
|
[Required] public int DestWarehouseId { get; set; }
|
||||||
|
/// <summary>Owning branch for this transfer — warehouses are shared, so this is what
|
||||||
|
/// actually scopes the document (resolved via IBranchConfigService).</summary>
|
||||||
|
[Required] public int BranchId { get; set; }
|
||||||
[Required, MinLength(1)] public List<CreateTransferLineInput> Lines { get; set; } = new();
|
[Required, MinLength(1)] public List<CreateTransferLineInput> Lines { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,22 @@ public sealed class CreateUserRequest
|
|||||||
/// email-lookup — never automatic, even on an exact email match (docs/12-BACKEND-HRM.md A.5).
|
/// email-lookup — never automatic, even on an exact email match (docs/12-BACKEND-HRM.md A.5).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? LinkEmployeeId { get; set; }
|
public int? LinkEmployeeId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Required, together with <see cref="DepartmentId"/>/<see cref="DesignationId"/>/
|
||||||
|
/// <see cref="EmploymentTypeId"/>/<see cref="WorkShiftId"/>/<see cref="BranchId"/>, whenever
|
||||||
|
/// the assigned Role is not Admin and <see cref="LinkEmployeeId"/> is not supplied: creates
|
||||||
|
/// the linked Employee record whose BranchId is what actually scopes this account to one
|
||||||
|
/// branch (see BranchAccessService.EnsureBranchVisibleAsync).
|
||||||
|
/// </summary>
|
||||||
|
public string? EmployeeCode { get; set; }
|
||||||
|
public int? BranchId { get; set; }
|
||||||
|
public int? DepartmentId { get; set; }
|
||||||
|
public int? DesignationId { get; set; }
|
||||||
|
public int? EmploymentTypeId { get; set; }
|
||||||
|
public int? WorkShiftId { get; set; }
|
||||||
|
/// <summary>Defaults to today when omitted.</summary>
|
||||||
|
public DateTime? HireDate { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class UpdateUserRoleRequest
|
public sealed class UpdateUserRoleRequest
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ using System.ComponentModel.DataAnnotations;
|
|||||||
|
|
||||||
namespace ERPCore.Dtos.Warehouses;
|
namespace ERPCore.Dtos.Warehouses;
|
||||||
|
|
||||||
/// <summary>Warehouse resource (docs/11-BACKEND-PHASE1.md §2.5).</summary>
|
/// <summary>Warehouse resource (docs/11-BACKEND-PHASE1.md §2.5). <see cref="BranchId"/> is
|
||||||
public sealed record WarehouseDto(int WarehouseId, string Code, string Name);
|
/// the anchor `docs/BRANCH_ISOLATION_PLAN.md` uses to branch-scope everything that carries
|
||||||
|
/// a `warehouseId` (Sales, GRN, stock, ...) without a column on every one of those entities.</summary>
|
||||||
|
public sealed record WarehouseDto(int WarehouseId, string Code, string Name, int BranchId, string? BranchName);
|
||||||
|
|
||||||
/// <summary>Bin/location resource (docs/11 §2.5).</summary>
|
/// <summary>Bin/location resource (docs/11 §2.5).</summary>
|
||||||
public sealed record BinDto(int BinId, int WarehouseId, string Code, string? BinType);
|
public sealed record BinDto(int BinId, int WarehouseId, string Code, string? BinType);
|
||||||
@@ -12,6 +14,7 @@ public sealed class CreateWarehouseRequest
|
|||||||
{
|
{
|
||||||
[Required, StringLength(50)] public string Code { get; set; } = string.Empty;
|
[Required, StringLength(50)] public string Code { get; set; } = string.Empty;
|
||||||
[Required, StringLength(200)] public string Name { get; set; } = string.Empty;
|
[Required, StringLength(200)] public string Name { get; set; } = string.Empty;
|
||||||
|
[Required] public int BranchId { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class CreateBinRequest
|
public sealed class CreateBinRequest
|
||||||
|
|||||||
@@ -31,4 +31,8 @@ public sealed class CurrentUser : ICurrentUser
|
|||||||
}
|
}
|
||||||
|
|
||||||
public int AuditUserId => int.TryParse(UserId, out var id) ? id : User.SystemUserId;
|
public int AuditUserId => int.TryParse(UserId, out var id) ? id : User.SystemUserId;
|
||||||
|
|
||||||
|
public string? RoleCode => _accessor.HttpContext?.User?.FindFirstValue(AuthHexClaims.RoleCode);
|
||||||
|
|
||||||
|
public bool IsAdmin => string.Equals(RoleCode?.Trim(), "Admin", StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,4 +19,14 @@ public interface ICurrentUser
|
|||||||
|
|
||||||
/// <summary>True when the request carries an authenticated principal.</summary>
|
/// <summary>True when the request carries an authenticated principal.</summary>
|
||||||
bool IsAuthenticated { get; }
|
bool IsAuthenticated { get; }
|
||||||
|
|
||||||
|
/// <summary>The AuthHex <c>RoleCode</c> claim verbatim, or null if absent/unauthenticated.</summary>
|
||||||
|
string? RoleCode { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True when <see cref="RoleCode"/> is "Admin" (case-insensitive — AuthHex mints the claim
|
||||||
|
/// independently of ERPCore's stored casing). Mirrors <c>RoleService.GetMeAsync</c>'s check;
|
||||||
|
/// the only role that bypasses branch-scoping (see <see cref="Services.Interfaces.IBranchAccessService"/>).
|
||||||
|
/// </summary>
|
||||||
|
bool IsAdmin { get; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using ERPCore.Domain.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace ERPCore.Infra.Persistence.Configurations;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configures the singleton branch-configuration row. The check constraint is what makes
|
||||||
|
/// "singleton" a database guarantee rather than a convention (mirrors ProductConfig).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class BranchConfigConfiguration : IEntityTypeConfiguration<BranchConfig>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<BranchConfig> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("branch_config", t =>
|
||||||
|
t.HasCheckConstraint("ck_branch_config_singleton", $"\"ConfigId\" = {BranchConfig.SingletonId}"));
|
||||||
|
|
||||||
|
builder.HasKey(c => c.ConfigId);
|
||||||
|
|
||||||
|
// The id is fixed, never generated — there is exactly one row, seeded by DataSeeder.
|
||||||
|
builder.Property(c => c.ConfigId).ValueGeneratedNever();
|
||||||
|
|
||||||
|
builder.Property(c => c.BranchingEnabled).IsRequired().HasDefaultValue(true);
|
||||||
|
|
||||||
|
// PostgreSQL xmin system column as the optimistic concurrency token (ETag).
|
||||||
|
builder.Property(c => c.RowVersion).IsRowVersion();
|
||||||
|
|
||||||
|
builder.HasOne(c => c.UpdatedByUser)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(c => c.UpdatedBy)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ public sealed class BranchConfiguration : IEntityTypeConfiguration<Branch>
|
|||||||
builder.HasIndex(b => b.Code).IsUnique();
|
builder.HasIndex(b => b.Code).IsUnique();
|
||||||
builder.Property(b => b.Name).IsRequired().HasMaxLength(200);
|
builder.Property(b => b.Name).IsRequired().HasMaxLength(200);
|
||||||
builder.Property(b => b.Address).HasMaxLength(500);
|
builder.Property(b => b.Address).HasMaxLength(500);
|
||||||
|
builder.Property(b => b.PhoneNumber).HasMaxLength(30);
|
||||||
|
|
||||||
builder.Property(b => b.Status)
|
builder.Property(b => b.Status)
|
||||||
.HasConversion<string>().HasMaxLength(20).IsRequired()
|
.HasConversion<string>().HasMaxLength(20).IsRequired()
|
||||||
|
|||||||
@@ -17,38 +17,18 @@ public sealed class GrnConfiguration : IEntityTypeConfiguration<Grn>
|
|||||||
builder.Property(g => g.Status).HasConversion<string>().HasMaxLength(20).IsRequired();
|
builder.Property(g => g.Status).HasConversion<string>().HasMaxLength(20).IsRequired();
|
||||||
builder.Property(g => g.CreatedAt).IsRequired();
|
builder.Property(g => g.CreatedAt).IsRequired();
|
||||||
builder.Property(g => g.GlJournalNo).HasMaxLength(30);
|
builder.Property(g => g.GlJournalNo).HasMaxLength(30);
|
||||||
builder.Property(g => g.PaidAmount).HasPrecision(18, 4);
|
|
||||||
builder.Property(g => g.BalanceAmount).HasPrecision(18, 4);
|
|
||||||
builder.Property(g => g.RowVersion).IsRowVersion();
|
builder.Property(g => g.RowVersion).IsRowVersion();
|
||||||
|
|
||||||
builder.HasOne(g => g.PurchaseOrder).WithMany().HasForeignKey(g => g.PoId).OnDelete(DeleteBehavior.Restrict);
|
builder.HasOne(g => g.PurchaseOrder).WithMany().HasForeignKey(g => g.PoId).OnDelete(DeleteBehavior.Restrict);
|
||||||
builder.HasOne(g => g.Vendor).WithMany().HasForeignKey(g => g.VendorId).OnDelete(DeleteBehavior.Restrict);
|
builder.HasOne(g => g.Vendor).WithMany().HasForeignKey(g => g.VendorId).OnDelete(DeleteBehavior.Restrict);
|
||||||
builder.HasOne(g => g.Warehouse).WithMany().HasForeignKey(g => g.WarehouseId).OnDelete(DeleteBehavior.Restrict);
|
builder.HasOne(g => g.Warehouse).WithMany().HasForeignKey(g => g.WarehouseId).OnDelete(DeleteBehavior.Restrict);
|
||||||
|
builder.HasOne(g => g.Branch).WithMany().HasForeignKey(g => g.BranchId).OnDelete(DeleteBehavior.Restrict);
|
||||||
builder.HasOne(g => g.Creator).WithMany().HasForeignKey(g => g.CreatedBy).OnDelete(DeleteBehavior.Restrict);
|
builder.HasOne(g => g.Creator).WithMany().HasForeignKey(g => g.CreatedBy).OnDelete(DeleteBehavior.Restrict);
|
||||||
|
builder.HasOne(g => g.PurchaseInvoice).WithMany().HasForeignKey(g => g.PurchaseInvoiceId).OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
builder.HasIndex(g => g.Status);
|
builder.HasIndex(g => g.Status);
|
||||||
builder.HasIndex(g => g.PoId);
|
builder.HasIndex(g => g.PoId);
|
||||||
}
|
builder.HasIndex(g => g.BranchId);
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class GrnPaymentConfiguration : IEntityTypeConfiguration<GrnPayment>
|
|
||||||
{
|
|
||||||
public void Configure(EntityTypeBuilder<GrnPayment> builder)
|
|
||||||
{
|
|
||||||
builder.ToTable("grn_payments");
|
|
||||||
builder.HasKey(p => p.GrnPaymentId);
|
|
||||||
|
|
||||||
builder.Property(p => p.Amount).HasPrecision(18, 4);
|
|
||||||
builder.Property(p => p.PaymentDate).IsRequired();
|
|
||||||
builder.Property(p => p.BankAccountName).IsRequired().HasMaxLength(200);
|
|
||||||
builder.Property(p => p.Reference).HasMaxLength(100);
|
|
||||||
builder.Property(p => p.GlJournalNo).HasMaxLength(30);
|
|
||||||
builder.Property(p => p.CreatedAt).IsRequired();
|
|
||||||
|
|
||||||
builder.HasOne(p => p.Grn).WithMany(g => g.Payments).HasForeignKey(p => p.GrnId).OnDelete(DeleteBehavior.Restrict);
|
|
||||||
builder.HasOne(p => p.Creator).WithMany().HasForeignKey(p => p.CreatedBy).OnDelete(DeleteBehavior.Restrict);
|
|
||||||
|
|
||||||
builder.HasIndex(p => p.GrnId);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ public sealed class ItemConfiguration : IEntityTypeConfiguration<Item>
|
|||||||
builder.Property(i => i.TaxClass).HasMaxLength(20);
|
builder.Property(i => i.TaxClass).HasMaxLength(20);
|
||||||
|
|
||||||
// Sales-only fixed selling price; nullable (null ⇒ sell at stock/FIFO value).
|
// Sales-only fixed selling price; nullable (null ⇒ sell at stock/FIFO value).
|
||||||
builder.Property(i => i.SalePrice).HasPrecision(18, 4);
|
builder.Property(i => i.SellingMinPrice).HasPrecision(18, 4);
|
||||||
|
builder.Property(i => i.SellingMaxPrice).HasPrecision(18, 4);
|
||||||
|
builder.Property(i => i.SellingFlatPrice).HasPrecision(18, 4);
|
||||||
|
|
||||||
// Optional content size (how much one stocked pack holds). All four are nullable
|
// Optional content size (how much one stocked pack holds). All four are nullable
|
||||||
// together: null ⇒ the item has no measurable content. The base pair is derived
|
// together: null ⇒ the item has no measurable content. The base pair is derived
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ public sealed class NavItemConfiguration : IEntityTypeConfiguration<NavItem>
|
|||||||
new NavItem { NavItemId = 10, Code = "help", Label = "Help", Href = "/dashboard/help", SortOrder = 10 },
|
new NavItem { NavItemId = 10, Code = "help", Label = "Help", Href = "/dashboard/help", SortOrder = 10 },
|
||||||
new NavItem { NavItemId = 11, Code = "ledgers", Label = "Ledgers", Href = "/dashboard/ledgers", SortOrder = 11 },
|
new NavItem { NavItemId = 11, Code = "ledgers", Label = "Ledgers", Href = "/dashboard/ledgers", SortOrder = 11 },
|
||||||
new NavItem { NavItemId = 12, Code = "accounts", Label = "Accounts", Href = "/dashboard/accounts", SortOrder = 12 },
|
new NavItem { NavItemId = 12, Code = "accounts", Label = "Accounts", Href = "/dashboard/accounts", SortOrder = 12 },
|
||||||
new NavItem { NavItemId = 13, Code = "sales", Label = "Sales", Href = "/dashboard/sales", SortOrder = 13 }
|
new NavItem { NavItemId = 13, Code = "sales", Label = "Sales", Href = "/dashboard/sales", SortOrder = 13 },
|
||||||
|
new NavItem { NavItemId = 14, Code = "production", Label = "Production", Href = "/dashboard/production", SortOrder = 14 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using ERPCore.Domain.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace ERPCore.Infra.Persistence.Configurations;
|
||||||
|
|
||||||
|
public sealed class NotificationConfigConfiguration : IEntityTypeConfiguration<NotificationConfig>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<NotificationConfig> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("notification_config", t =>
|
||||||
|
t.HasCheckConstraint("ck_notification_config_singleton", $"\"NotificationConfigId\" = {NotificationConfig.SingletonId}"));
|
||||||
|
|
||||||
|
builder.HasKey(c => c.NotificationConfigId);
|
||||||
|
builder.Property(c => c.NotificationConfigId).ValueGeneratedNever();
|
||||||
|
|
||||||
|
builder.Property(c => c.RowVersion).IsRowVersion();
|
||||||
|
|
||||||
|
builder.HasMany(c => c.Modules)
|
||||||
|
.WithOne(m => m.NotificationConfig)
|
||||||
|
.HasForeignKey(m => m.NotificationConfigId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
builder.HasOne(c => c.UpdatedByUser)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(c => c.UpdatedBy)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
using ERPCore.Domain.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace ERPCore.Infra.Persistence.Configurations;
|
||||||
|
|
||||||
|
public sealed class NotificationConfigModuleConfiguration : IEntityTypeConfiguration<NotificationConfigModule>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<NotificationConfigModule> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("notification_config_modules");
|
||||||
|
builder.HasKey(m => m.NotificationConfigModuleId);
|
||||||
|
|
||||||
|
builder.Property(m => m.ModuleKey).IsRequired().HasMaxLength(100);
|
||||||
|
builder.HasIndex(m => m.ModuleKey).IsUnique();
|
||||||
|
|
||||||
|
builder.Property(m => m.EmailEnabled).IsRequired().HasDefaultValue(false);
|
||||||
|
builder.Property(m => m.SmsEnabled).IsRequired().HasDefaultValue(false);
|
||||||
|
builder.Property(m => m.InAppEnabled).IsRequired().HasDefaultValue(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -61,7 +61,13 @@ public sealed class PermissionConfiguration : IEntityTypeConfiguration<Permissio
|
|||||||
new Permission { PermissionId = 26, Code = "NAV:accounts.bank-accounts", SubNavItemId = 15 },
|
new Permission { PermissionId = 26, Code = "NAV:accounts.bank-accounts", SubNavItemId = 15 },
|
||||||
new Permission { PermissionId = 32, Code = "NAV:accounts", NavItemId = 12 },
|
new Permission { PermissionId = 32, Code = "NAV:accounts", NavItemId = 12 },
|
||||||
new Permission { PermissionId = 33, Code = "NAV:accounts.cheque-books", SubNavItemId = 21 },
|
new Permission { PermissionId = 33, Code = "NAV:accounts.cheque-books", SubNavItemId = 21 },
|
||||||
new Permission { PermissionId = 34, Code = "NAV:accounts.received-cheques", SubNavItemId = 22 }
|
new Permission { PermissionId = 34, Code = "NAV:accounts.received-cheques", SubNavItemId = 22 },
|
||||||
|
new Permission { PermissionId = 35, Code = "NAV:production", NavItemId = 14 },
|
||||||
|
new Permission { PermissionId = 36, Code = "NAV:production.templates", SubNavItemId = 27 },
|
||||||
|
new Permission { PermissionId = 37, Code = "NAV:production.runs", SubNavItemId = 25 },
|
||||||
|
new Permission { PermissionId = 38, Code = "NAV:production.produced", SubNavItemId = 26 },
|
||||||
|
new Permission { PermissionId = 39, Code = "NAV:procurement.purchase-invoices", SubNavItemId = 28 },
|
||||||
|
new Permission { PermissionId = 40, Code = "NAV:settings.branches", SubNavItemId = 29 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ public sealed class ProductConfigConfiguration : IEntityTypeConfiguration<Produc
|
|||||||
builder.Property(c => c.SubcategoriesEnabled).IsRequired().HasDefaultValue(true);
|
builder.Property(c => c.SubcategoriesEnabled).IsRequired().HasDefaultValue(true);
|
||||||
builder.Property(c => c.BrandsEnabled).IsRequired().HasDefaultValue(true);
|
builder.Property(c => c.BrandsEnabled).IsRequired().HasDefaultValue(true);
|
||||||
builder.Property(c => c.ItemTypesEnabled).IsRequired().HasDefaultValue(true);
|
builder.Property(c => c.ItemTypesEnabled).IsRequired().HasDefaultValue(true);
|
||||||
|
builder.Property(c => c.SellingPricesEnabled).IsRequired().HasDefaultValue(true);
|
||||||
|
builder.Property(c => c.SerialNumbersEnabled).IsRequired().HasDefaultValue(true);
|
||||||
|
builder.Property(c => c.BatchListsEnabled).IsRequired().HasDefaultValue(true);
|
||||||
|
builder.Property(c => c.WarrantyPeriodsEnabled).IsRequired().HasDefaultValue(true);
|
||||||
|
|
||||||
// PostgreSQL xmin system column as the optimistic concurrency token (ETag).
|
// PostgreSQL xmin system column as the optimistic concurrency token (ETag).
|
||||||
builder.Property(c => c.RowVersion).IsRowVersion();
|
builder.Property(c => c.RowVersion).IsRowVersion();
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
using ERPCore.Domain.Entities;
|
||||||
|
using ERPCore.Domain.Enums;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace ERPCore.Infra.Persistence.Configurations;
|
||||||
|
|
||||||
|
public sealed class PurchaseInvoiceConfiguration : IEntityTypeConfiguration<PurchaseInvoice>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<PurchaseInvoice> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("purchase_invoices");
|
||||||
|
builder.HasKey(x => x.PurchaseInvoiceId);
|
||||||
|
|
||||||
|
builder.Property(x => x.DocNo).IsRequired().HasMaxLength(30);
|
||||||
|
builder.HasIndex(x => x.DocNo).IsUnique();
|
||||||
|
|
||||||
|
builder.Property(x => x.VendorInvoiceNo).IsRequired().HasMaxLength(50);
|
||||||
|
builder.Property(x => x.VendorInvoiceDate).IsRequired();
|
||||||
|
// A vendor invoice number is only meaningful per-vendor; the service also excludes
|
||||||
|
// Cancelled invoices from the duplicate check, so this composite index is a coarse
|
||||||
|
// backstop (it does not distinguish Cancelled rows) rather than the sole enforcement.
|
||||||
|
builder.HasIndex(x => new { x.VendorId, x.VendorInvoiceNo });
|
||||||
|
|
||||||
|
builder.HasOne(x => x.Vendor)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.VendorId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
builder.Property(x => x.VendorSnapshotName).IsRequired().HasMaxLength(200);
|
||||||
|
builder.Property(x => x.VendorSnapshotTaxNo).HasMaxLength(50);
|
||||||
|
|
||||||
|
builder.Property(x => x.Status)
|
||||||
|
.HasConversion<string>().HasMaxLength(20).IsRequired()
|
||||||
|
.HasDefaultValue(PurchaseInvoiceStatus.Draft);
|
||||||
|
|
||||||
|
foreach (var p in new[] { nameof(PurchaseInvoice.Subtotal), nameof(PurchaseInvoice.VatTotal), nameof(PurchaseInvoice.GrandTotal), nameof(PurchaseInvoice.PaidAmount), nameof(PurchaseInvoice.BalanceAmount) })
|
||||||
|
builder.Property<decimal>(p).HasPrecision(18, 4);
|
||||||
|
builder.Property(x => x.GlJournalNo).HasMaxLength(30);
|
||||||
|
|
||||||
|
builder.HasOne(x => x.Creator).WithMany().HasForeignKey(x => x.CreatedBy).OnDelete(DeleteBehavior.Restrict);
|
||||||
|
builder.Property(x => x.CreatedAt).IsRequired();
|
||||||
|
builder.Property(x => x.RowVersion).IsRowVersion();
|
||||||
|
|
||||||
|
builder.HasIndex(x => x.Status);
|
||||||
|
builder.HasIndex(x => x.VendorId);
|
||||||
|
|
||||||
|
builder.HasMany(x => x.Lines)
|
||||||
|
.WithOne(x => x.PurchaseInvoice)
|
||||||
|
.HasForeignKey(x => x.PurchaseInvoiceId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PurchaseInvoiceLineConfiguration : IEntityTypeConfiguration<PurchaseInvoiceLine>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<PurchaseInvoiceLine> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("purchase_invoice_lines");
|
||||||
|
builder.HasKey(x => x.PurchaseInvoiceLineId);
|
||||||
|
|
||||||
|
builder.Property(x => x.Subtotal).HasPrecision(18, 4);
|
||||||
|
builder.Property(x => x.VatTotal).HasPrecision(18, 4);
|
||||||
|
builder.Property(x => x.LineTotal).HasPrecision(18, 4);
|
||||||
|
|
||||||
|
builder.HasOne(x => x.Grn)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.GrnId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
// A GRN can be consolidated onto at most one active line at a time — cancelling the
|
||||||
|
// invoice removes its lines and frees the GRN (PurchaseInvoiceService.CancelAsync), so
|
||||||
|
// this stays enforceable rather than blocking a later re-invoice of the same GRN.
|
||||||
|
builder.HasIndex(x => x.GrnId).IsUnique();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class PurchaseInvoicePaymentConfiguration : IEntityTypeConfiguration<PurchaseInvoicePayment>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<PurchaseInvoicePayment> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("purchase_invoice_payments");
|
||||||
|
builder.HasKey(p => p.PurchaseInvoicePaymentId);
|
||||||
|
|
||||||
|
builder.Property(p => p.Amount).HasPrecision(18, 4);
|
||||||
|
builder.Property(p => p.PaymentDate).IsRequired();
|
||||||
|
builder.Property(p => p.BankAccountName).IsRequired().HasMaxLength(200);
|
||||||
|
builder.Property(p => p.Reference).HasMaxLength(100);
|
||||||
|
builder.Property(p => p.GlJournalNo).HasMaxLength(30);
|
||||||
|
builder.Property(p => p.CreatedAt).IsRequired();
|
||||||
|
|
||||||
|
builder.HasOne(p => p.PurchaseInvoice).WithMany(x => x.Payments).HasForeignKey(p => p.PurchaseInvoiceId).OnDelete(DeleteBehavior.Restrict);
|
||||||
|
builder.HasOne(p => p.Creator).WithMany().HasForeignKey(p => p.CreatedBy).OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
builder.HasIndex(p => p.PurchaseInvoiceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,6 +30,11 @@ public sealed class SalesInvoiceConfiguration : IEntityTypeConfiguration<SalesIn
|
|||||||
.HasForeignKey(x => x.WarehouseId)
|
.HasForeignKey(x => x.WarehouseId)
|
||||||
.OnDelete(DeleteBehavior.Restrict);
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
builder.HasOne(x => x.Branch)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.BranchId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
builder.Property(x => x.InvoiceType)
|
builder.Property(x => x.InvoiceType)
|
||||||
.HasConversion<string>().HasMaxLength(20).IsRequired()
|
.HasConversion<string>().HasMaxLength(20).IsRequired()
|
||||||
.HasDefaultValue(SalesInvoiceType.B2C);
|
.HasDefaultValue(SalesInvoiceType.B2C);
|
||||||
@@ -47,6 +52,7 @@ public sealed class SalesInvoiceConfiguration : IEntityTypeConfiguration<SalesIn
|
|||||||
|
|
||||||
builder.HasIndex(x => x.Status);
|
builder.HasIndex(x => x.Status);
|
||||||
builder.HasIndex(x => x.InvoiceDate);
|
builder.HasIndex(x => x.InvoiceDate);
|
||||||
|
builder.HasIndex(x => x.BranchId);
|
||||||
|
|
||||||
builder.HasMany(x => x.Lines)
|
builder.HasMany(x => x.Lines)
|
||||||
.WithOne(x => x.SalesInvoice)
|
.WithOne(x => x.SalesInvoice)
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ public sealed class StockAdjustmentConfiguration : IEntityTypeConfiguration<Stoc
|
|||||||
|
|
||||||
builder.HasOne(a => a.Warehouse).WithMany().HasForeignKey(a => a.WarehouseId).OnDelete(DeleteBehavior.Restrict);
|
builder.HasOne(a => a.Warehouse).WithMany().HasForeignKey(a => a.WarehouseId).OnDelete(DeleteBehavior.Restrict);
|
||||||
builder.HasOne(a => a.ReasonCode).WithMany().HasForeignKey(a => a.ReasonCodeId).OnDelete(DeleteBehavior.Restrict);
|
builder.HasOne(a => a.ReasonCode).WithMany().HasForeignKey(a => a.ReasonCodeId).OnDelete(DeleteBehavior.Restrict);
|
||||||
|
builder.HasOne(a => a.Branch).WithMany().HasForeignKey(a => a.BranchId).OnDelete(DeleteBehavior.Restrict);
|
||||||
builder.HasOne(a => a.Creator).WithMany().HasForeignKey(a => a.CreatedBy).OnDelete(DeleteBehavior.Restrict);
|
builder.HasOne(a => a.Creator).WithMany().HasForeignKey(a => a.CreatedBy).OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
builder.HasIndex(a => a.BranchId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +20,11 @@ public sealed class StockCountConfiguration : IEntityTypeConfiguration<StockCoun
|
|||||||
builder.Property(c => c.RowVersion).IsRowVersion();
|
builder.Property(c => c.RowVersion).IsRowVersion();
|
||||||
|
|
||||||
builder.HasOne(c => c.Warehouse).WithMany().HasForeignKey(c => c.WarehouseId).OnDelete(DeleteBehavior.Restrict);
|
builder.HasOne(c => c.Warehouse).WithMany().HasForeignKey(c => c.WarehouseId).OnDelete(DeleteBehavior.Restrict);
|
||||||
|
builder.HasOne(c => c.Branch).WithMany().HasForeignKey(c => c.BranchId).OnDelete(DeleteBehavior.Restrict);
|
||||||
builder.HasOne(c => c.Creator).WithMany().HasForeignKey(c => c.CreatedBy).OnDelete(DeleteBehavior.Restrict);
|
builder.HasOne(c => c.Creator).WithMany().HasForeignKey(c => c.CreatedBy).OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
builder.HasIndex(c => c.Status);
|
builder.HasIndex(c => c.Status);
|
||||||
|
builder.HasIndex(c => c.BranchId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +20,11 @@ public sealed class StockTransferConfiguration : IEntityTypeConfiguration<StockT
|
|||||||
|
|
||||||
builder.HasOne(t => t.SrcWarehouse).WithMany().HasForeignKey(t => t.SrcWarehouseId).OnDelete(DeleteBehavior.Restrict);
|
builder.HasOne(t => t.SrcWarehouse).WithMany().HasForeignKey(t => t.SrcWarehouseId).OnDelete(DeleteBehavior.Restrict);
|
||||||
builder.HasOne(t => t.DestWarehouse).WithMany().HasForeignKey(t => t.DestWarehouseId).OnDelete(DeleteBehavior.Restrict);
|
builder.HasOne(t => t.DestWarehouse).WithMany().HasForeignKey(t => t.DestWarehouseId).OnDelete(DeleteBehavior.Restrict);
|
||||||
|
builder.HasOne(t => t.Branch).WithMany().HasForeignKey(t => t.BranchId).OnDelete(DeleteBehavior.Restrict);
|
||||||
builder.HasOne(t => t.Creator).WithMany().HasForeignKey(t => t.CreatedBy).OnDelete(DeleteBehavior.Restrict);
|
builder.HasOne(t => t.Creator).WithMany().HasForeignKey(t => t.CreatedBy).OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
builder.HasIndex(t => t.Status);
|
builder.HasIndex(t => t.Status);
|
||||||
|
builder.HasIndex(t => t.BranchId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,8 +31,10 @@ public sealed class SubNavItemConfiguration : IEntityTypeConfiguration<SubNavIte
|
|||||||
new SubNavItem { SubNavItemId = 4, NavItemId = 2, Code = "products.item-type", Label = "Item Type", Href = "/dashboard/products/item-types", SortOrder = 4 },
|
new SubNavItem { SubNavItemId = 4, NavItemId = 2, Code = "products.item-type", Label = "Item Type", Href = "/dashboard/products/item-types", SortOrder = 4 },
|
||||||
new SubNavItem { SubNavItemId = 5, NavItemId = 2, Code = "products.uom", Label = "UOM", Href = "/dashboard/products/uoms", SortOrder = 5 },
|
new SubNavItem { SubNavItemId = 5, NavItemId = 2, Code = "products.uom", Label = "UOM", Href = "/dashboard/products/uoms", SortOrder = 5 },
|
||||||
new SubNavItem { SubNavItemId = 6, NavItemId = 2, Code = "products.configuration", Label = "Configuration", Href = "/dashboard/products/settings", SortOrder = 6 },
|
new SubNavItem { SubNavItemId = 6, NavItemId = 2, Code = "products.configuration", Label = "Configuration", Href = "/dashboard/products/settings", SortOrder = 6 },
|
||||||
|
new SubNavItem { SubNavItemId = 29, NavItemId = 9, Code = "settings.branches", Label = "Branches", Href = "/dashboard/settings/branches", SortOrder = 0 },
|
||||||
new SubNavItem { SubNavItemId = 7, NavItemId = 9, Code = "settings.roles", Label = "Roles", Href = "/dashboard/settings/roles", SortOrder = 1 },
|
new SubNavItem { SubNavItemId = 7, NavItemId = 9, Code = "settings.roles", Label = "Roles", Href = "/dashboard/settings/roles", SortOrder = 1 },
|
||||||
new SubNavItem { SubNavItemId = 8, NavItemId = 9, Code = "settings.users", Label = "Users", Href = "/dashboard/settings/users", SortOrder = 2 },
|
new SubNavItem { SubNavItemId = 8, NavItemId = 9, Code = "settings.users", Label = "Users", Href = "/dashboard/settings/users", SortOrder = 2 },
|
||||||
|
new SubNavItem { SubNavItemId = 24, NavItemId = 9, Code = "settings.notification-config", Label = "Notification Config", Href = "/dashboard/settings/notification-config", SortOrder = 3 },
|
||||||
new SubNavItem { SubNavItemId = 23, NavItemId = 13, Code = "sales.bundle-sales", Label = "Bundle Sales", Href = "/dashboard/sales/bundles", SortOrder = 1 },
|
new SubNavItem { SubNavItemId = 23, NavItemId = 13, Code = "sales.bundle-sales", Label = "Bundle Sales", Href = "/dashboard/sales/bundles", SortOrder = 1 },
|
||||||
// Procurement (NavItemId 4) children — mirror the hub page order.
|
// Procurement (NavItemId 4) children — mirror the hub page order.
|
||||||
// IDs 17-20 (not 9-12): 9-12 were already claimed by the Ledgers sub-items below;
|
// IDs 17-20 (not 9-12): 9-12 were already claimed by the Ledgers sub-items below;
|
||||||
@@ -41,6 +43,7 @@ public sealed class SubNavItemConfiguration : IEntityTypeConfiguration<SubNavIte
|
|||||||
new SubNavItem { SubNavItemId = 18, NavItemId = 4, Code = "procurement.rfqs", Label = "RFQs", Href = "/dashboard/procurement/rfqs", SortOrder = 2 },
|
new SubNavItem { SubNavItemId = 18, NavItemId = 4, Code = "procurement.rfqs", Label = "RFQs", Href = "/dashboard/procurement/rfqs", SortOrder = 2 },
|
||||||
new SubNavItem { SubNavItemId = 19, NavItemId = 4, Code = "procurement.purchase-orders", Label = "Purchase Orders", Href = "/dashboard/procurement/purchase-orders", SortOrder = 3 },
|
new SubNavItem { SubNavItemId = 19, NavItemId = 4, Code = "procurement.purchase-orders", Label = "Purchase Orders", Href = "/dashboard/procurement/purchase-orders", SortOrder = 3 },
|
||||||
new SubNavItem { SubNavItemId = 20, NavItemId = 4, Code = "procurement.purchase-returns", Label = "Purchase Returns", Href = "/dashboard/procurement/purchase-returns", SortOrder = 4 },
|
new SubNavItem { SubNavItemId = 20, NavItemId = 4, Code = "procurement.purchase-returns", Label = "Purchase Returns", Href = "/dashboard/procurement/purchase-returns", SortOrder = 4 },
|
||||||
|
new SubNavItem { SubNavItemId = 28, NavItemId = 4, Code = "procurement.purchase-invoices", Label = "Purchase Invoices", Href = "/dashboard/procurement/purchase-invoices", SortOrder = 5 },
|
||||||
new SubNavItem { SubNavItemId = 9, NavItemId = 11, Code = "ledgers.trial-balance", Label = "Trial Balance", Href = "/dashboard/ledgers/trial-balance", SortOrder = 1 },
|
new SubNavItem { SubNavItemId = 9, NavItemId = 11, Code = "ledgers.trial-balance", Label = "Trial Balance", Href = "/dashboard/ledgers/trial-balance", SortOrder = 1 },
|
||||||
new SubNavItem { SubNavItemId = 10, NavItemId = 11, Code = "ledgers.balance-sheet", Label = "Balance Sheet", Href = "/dashboard/ledgers/balance-sheet", SortOrder = 2 },
|
new SubNavItem { SubNavItemId = 10, NavItemId = 11, Code = "ledgers.balance-sheet", Label = "Balance Sheet", Href = "/dashboard/ledgers/balance-sheet", SortOrder = 2 },
|
||||||
new SubNavItem { SubNavItemId = 11, NavItemId = 11, Code = "ledgers.general-ledger", Label = "General Ledger", Href = "/dashboard/ledgers/general-ledger", SortOrder = 3 },
|
new SubNavItem { SubNavItemId = 11, NavItemId = 11, Code = "ledgers.general-ledger", Label = "General Ledger", Href = "/dashboard/ledgers/general-ledger", SortOrder = 3 },
|
||||||
@@ -53,7 +56,15 @@ public sealed class SubNavItemConfiguration : IEntityTypeConfiguration<SubNavIte
|
|||||||
// doesn't silently lose it just because the section it lives under changed.
|
// doesn't silently lose it just because the section it lives under changed.
|
||||||
new SubNavItem { SubNavItemId = 15, NavItemId = 12, Code = "accounts.bank-accounts", Label = "Cash / Bank Accounts", Href = "/dashboard/accounts/bank-accounts", SortOrder = 1 },
|
new SubNavItem { SubNavItemId = 15, NavItemId = 12, Code = "accounts.bank-accounts", Label = "Cash / Bank Accounts", Href = "/dashboard/accounts/bank-accounts", SortOrder = 1 },
|
||||||
new SubNavItem { SubNavItemId = 21, NavItemId = 12, Code = "accounts.cheque-books", Label = "Cheque Books", Href = "/dashboard/accounts/cheque-books", SortOrder = 2 },
|
new SubNavItem { SubNavItemId = 21, NavItemId = 12, Code = "accounts.cheque-books", Label = "Cheque Books", Href = "/dashboard/accounts/cheque-books", SortOrder = 2 },
|
||||||
new SubNavItem { SubNavItemId = 22, NavItemId = 12, Code = "accounts.received-cheques", Label = "Received Cheques", Href = "/dashboard/accounts/received-cheques", SortOrder = 3 }
|
new SubNavItem { SubNavItemId = 22, NavItemId = 12, Code = "accounts.received-cheques", Label = "Received Cheques", Href = "/dashboard/accounts/received-cheques", SortOrder = 3 },
|
||||||
|
// Production (NavItemId 14) children — mirror the sidebar order (AppSidebar.tsx).
|
||||||
|
// ID 27 (not 24): 24 collides with settings.notification-config above — that ID was
|
||||||
|
// reused by mistake in migration 20260820100345, which upserts row 24 to become the
|
||||||
|
// notification-config item and silently overwrites this one. See migration
|
||||||
|
// FixProductionTemplatesSubNavItemIdConflict for the data repair.
|
||||||
|
new SubNavItem { SubNavItemId = 27, NavItemId = 14, Code = "production.templates", Label = "Templates", Href = "/dashboard/production/templates", SortOrder = 1 },
|
||||||
|
new SubNavItem { SubNavItemId = 25, NavItemId = 14, Code = "production.runs", Label = "Runs", Href = "/dashboard/production/runs", SortOrder = 2 },
|
||||||
|
new SubNavItem { SubNavItemId = 26, NavItemId = 14, Code = "production.produced", Label = "Produced", Href = "/dashboard/production/produced", SortOrder = 3 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,5 +15,9 @@ public sealed class WarehouseConfiguration : IEntityTypeConfiguration<Warehouse>
|
|||||||
builder.HasIndex(w => w.Code).IsUnique();
|
builder.HasIndex(w => w.Code).IsUnique();
|
||||||
|
|
||||||
builder.Property(w => w.Name).IsRequired().HasMaxLength(200);
|
builder.Property(w => w.Name).IsRequired().HasMaxLength(200);
|
||||||
|
|
||||||
|
builder.HasOne(w => w.Branch).WithMany()
|
||||||
|
.HasForeignKey(w => w.BranchId).OnDelete(DeleteBehavior.Restrict);
|
||||||
|
builder.HasIndex(w => w.BranchId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,8 +43,10 @@ public static class DataSeeder
|
|||||||
{
|
{
|
||||||
var dirty = await SeedReasonCodesAsync(db, ct);
|
var dirty = await SeedReasonCodesAsync(db, ct);
|
||||||
dirty |= await SeedItemTypesAsync(db, ct);
|
dirty |= await SeedItemTypesAsync(db, ct);
|
||||||
// dirty |= await SeedCompanyProfileAsync(db, ct);
|
// dirty |= await SeedCompanyProfileAsync(db, ct);
|
||||||
dirty |= await SeedProductConfigAsync(db, ct);
|
dirty |= await SeedProductConfigAsync(db, ct);
|
||||||
|
dirty |= await SeedBranchConfigAsync(db, ct);
|
||||||
|
dirty |= await SeedNotificationConfigAsync(db, ct);
|
||||||
dirty |= await SeedSalesMastersAsync(db, ct);
|
dirty |= await SeedSalesMastersAsync(db, ct);
|
||||||
dirty |= await SeedSalesStockAsync(db, ct);
|
dirty |= await SeedSalesStockAsync(db, ct);
|
||||||
dirty |= await SeedSalesAsync(db, ct);
|
dirty |= await SeedSalesAsync(db, ct);
|
||||||
@@ -100,7 +102,39 @@ public static class DataSeeder
|
|||||||
ConfigId = ProductConfig.SingletonId,
|
ConfigId = ProductConfig.SingletonId,
|
||||||
SubcategoriesEnabled = true,
|
SubcategoriesEnabled = true,
|
||||||
BrandsEnabled = true,
|
BrandsEnabled = true,
|
||||||
ItemTypesEnabled = true
|
ItemTypesEnabled = true,
|
||||||
|
SellingPricesEnabled = true,
|
||||||
|
SerialNumbersEnabled = true,
|
||||||
|
BatchListsEnabled = true,
|
||||||
|
WarrantyPeriodsEnabled = true
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ensures the singleton branch-config row exists. New deployments start with branching
|
||||||
|
/// on — today's mandatory-branch behaviour — so nothing changes for anyone until an
|
||||||
|
/// admin deliberately switches it off from Settings.
|
||||||
|
/// </summary>
|
||||||
|
private static async Task<bool> SeedBranchConfigAsync(ErpDbContext db, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (await db.BranchConfig.AnyAsync(c => c.ConfigId == BranchConfig.SingletonId, ct)) return false;
|
||||||
|
|
||||||
|
db.BranchConfig.Add(new BranchConfig
|
||||||
|
{
|
||||||
|
ConfigId = BranchConfig.SingletonId,
|
||||||
|
BranchingEnabled = true
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<bool> SeedNotificationConfigAsync(ErpDbContext db, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (await db.NotificationConfig.AnyAsync(c => c.NotificationConfigId == NotificationConfig.SingletonId, ct)) return false;
|
||||||
|
|
||||||
|
db.NotificationConfig.Add(new NotificationConfig
|
||||||
|
{
|
||||||
|
NotificationConfigId = NotificationConfig.SingletonId
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -182,7 +216,7 @@ public static class DataSeeder
|
|||||||
WarehouseId = warehouse.WarehouseId,
|
WarehouseId = warehouse.WarehouseId,
|
||||||
QtyReceived = 100m,
|
QtyReceived = 100m,
|
||||||
QtyRemaining = 100m,
|
QtyRemaining = 100m,
|
||||||
UnitCost = item.SalePrice.GetValueOrDefault() > 0m ? item.SalePrice.GetValueOrDefault() / 2m : 25m,
|
UnitCost = item.SellingFlatPrice.GetValueOrDefault() > 0m ? item.SellingFlatPrice.GetValueOrDefault() / 2m : 25m,
|
||||||
ReceiptDate = DateTime.UtcNow.AddDays(-7)
|
ReceiptDate = DateTime.UtcNow.AddDays(-7)
|
||||||
});
|
});
|
||||||
db.StockLayers.Add(new StockLayer
|
db.StockLayers.Add(new StockLayer
|
||||||
@@ -191,7 +225,7 @@ public static class DataSeeder
|
|||||||
WarehouseId = warehouse.WarehouseId,
|
WarehouseId = warehouse.WarehouseId,
|
||||||
QtyReceived = 5m,
|
QtyReceived = 5m,
|
||||||
QtyRemaining = 5m,
|
QtyRemaining = 5m,
|
||||||
UnitCost = secondItem.SalePrice.GetValueOrDefault() > 0m ? secondItem.SalePrice.GetValueOrDefault() / 2m : 15m,
|
UnitCost = secondItem.SellingFlatPrice.GetValueOrDefault() > 0m ? secondItem.SellingFlatPrice.GetValueOrDefault() / 2m : 15m,
|
||||||
ReceiptDate = DateTime.UtcNow.AddDays(-6)
|
ReceiptDate = DateTime.UtcNow.AddDays(-6)
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
@@ -278,7 +312,7 @@ public static class DataSeeder
|
|||||||
BaseUomId = uom.UomId,
|
BaseUomId = uom.UomId,
|
||||||
StockNature = StockNature.Stocked,
|
StockNature = StockNature.Stocked,
|
||||||
TrackingMode = TrackingMode.None,
|
TrackingMode = TrackingMode.None,
|
||||||
SalePrice = 100m,
|
SellingFlatPrice = 100m,
|
||||||
Status = EntityStatus.Active,
|
Status = EntityStatus.Active,
|
||||||
CreatedAt = now
|
CreatedAt = now
|
||||||
},
|
},
|
||||||
@@ -292,7 +326,7 @@ public static class DataSeeder
|
|||||||
BaseUomId = uom.UomId,
|
BaseUomId = uom.UomId,
|
||||||
StockNature = StockNature.Stocked,
|
StockNature = StockNature.Stocked,
|
||||||
TrackingMode = TrackingMode.None,
|
TrackingMode = TrackingMode.None,
|
||||||
SalePrice = 50m,
|
SellingFlatPrice = 50m,
|
||||||
ContentQty = 500m,
|
ContentQty = 500m,
|
||||||
ContentUnit = MeasureUnit.Ml,
|
ContentUnit = MeasureUnit.Ml,
|
||||||
ContentBaseQty = 500m,
|
ContentBaseQty = 500m,
|
||||||
@@ -445,7 +479,7 @@ public static class DataSeeder
|
|||||||
ItemId = items[0].ItemId,
|
ItemId = items[0].ItemId,
|
||||||
WarehouseId = warehouse.WarehouseId,
|
WarehouseId = warehouse.WarehouseId,
|
||||||
Qty = 1m,
|
Qty = 1m,
|
||||||
UnitPrice = items[0].SalePrice.GetValueOrDefault(),
|
UnitPrice = items[0].SellingFlatPrice.GetValueOrDefault(),
|
||||||
IncludeInBundle = true,
|
IncludeInBundle = true,
|
||||||
SortOrder = 1
|
SortOrder = 1
|
||||||
},
|
},
|
||||||
@@ -454,7 +488,7 @@ public static class DataSeeder
|
|||||||
ItemId = items[1].ItemId,
|
ItemId = items[1].ItemId,
|
||||||
WarehouseId = warehouse.WarehouseId,
|
WarehouseId = warehouse.WarehouseId,
|
||||||
Qty = 1m,
|
Qty = 1m,
|
||||||
UnitPrice = items[1].SalePrice.GetValueOrDefault(),
|
UnitPrice = items[1].SellingFlatPrice.GetValueOrDefault(),
|
||||||
IncludeInBundle = true,
|
IncludeInBundle = true,
|
||||||
SortOrder = 2
|
SortOrder = 2
|
||||||
}
|
}
|
||||||
@@ -478,10 +512,10 @@ public static class DataSeeder
|
|||||||
BundleName = "Demo Bundle Draft",
|
BundleName = "Demo Bundle Draft",
|
||||||
BundleCode = "BND-DEMO-001",
|
BundleCode = "BND-DEMO-001",
|
||||||
Status = BundleSaleStatus.Draft,
|
Status = BundleSaleStatus.Draft,
|
||||||
ComponentSubtotal = items[0].SalePrice.GetValueOrDefault() + items[1].SalePrice.GetValueOrDefault(),
|
ComponentSubtotal = items[0].SellingFlatPrice.GetValueOrDefault() + items[1].SellingFlatPrice.GetValueOrDefault(),
|
||||||
BundlePrice = 0m,
|
BundlePrice = 0m,
|
||||||
MarginAmount = -(items[0].SalePrice.GetValueOrDefault() + items[1].SalePrice.GetValueOrDefault()),
|
MarginAmount = -(items[0].SellingFlatPrice.GetValueOrDefault() + items[1].SellingFlatPrice.GetValueOrDefault()),
|
||||||
DiscountTotal = items[0].SalePrice.GetValueOrDefault() + items[1].SalePrice.GetValueOrDefault(),
|
DiscountTotal = items[0].SellingFlatPrice.GetValueOrDefault() + items[1].SellingFlatPrice.GetValueOrDefault(),
|
||||||
TaxTotal = 0m,
|
TaxTotal = 0m,
|
||||||
GrandTotal = 0m,
|
GrandTotal = 0m,
|
||||||
CreatedAt = now.AddDays(-2),
|
CreatedAt = now.AddDays(-2),
|
||||||
@@ -493,8 +527,8 @@ public static class DataSeeder
|
|||||||
Description = items[0].Name,
|
Description = items[0].Name,
|
||||||
Qty = 1m,
|
Qty = 1m,
|
||||||
WarehouseId = warehouse.WarehouseId,
|
WarehouseId = warehouse.WarehouseId,
|
||||||
UnitPrice = items[0].SalePrice.GetValueOrDefault(),
|
UnitPrice = items[0].SellingFlatPrice.GetValueOrDefault(),
|
||||||
LineTotal = items[0].SalePrice.GetValueOrDefault(),
|
LineTotal = items[0].SellingFlatPrice.GetValueOrDefault(),
|
||||||
IncludeInBundle = true,
|
IncludeInBundle = true,
|
||||||
IsComponent = true
|
IsComponent = true
|
||||||
},
|
},
|
||||||
@@ -504,8 +538,8 @@ public static class DataSeeder
|
|||||||
Description = items[1].Name,
|
Description = items[1].Name,
|
||||||
Qty = 1m,
|
Qty = 1m,
|
||||||
WarehouseId = warehouse.WarehouseId,
|
WarehouseId = warehouse.WarehouseId,
|
||||||
UnitPrice = items[1].SalePrice.GetValueOrDefault(),
|
UnitPrice = items[1].SellingFlatPrice.GetValueOrDefault(),
|
||||||
LineTotal = items[1].SalePrice.GetValueOrDefault(),
|
LineTotal = items[1].SellingFlatPrice.GetValueOrDefault(),
|
||||||
IncludeInBundle = true,
|
IncludeInBundle = true,
|
||||||
IsComponent = true
|
IsComponent = true
|
||||||
}
|
}
|
||||||
@@ -523,12 +557,12 @@ public static class DataSeeder
|
|||||||
BundleName = "Demo Bundle Posted",
|
BundleName = "Demo Bundle Posted",
|
||||||
BundleCode = "BND-DEMO-001",
|
BundleCode = "BND-DEMO-001",
|
||||||
Status = BundleSaleStatus.Posted,
|
Status = BundleSaleStatus.Posted,
|
||||||
ComponentSubtotal = items[0].SalePrice.GetValueOrDefault() + items[1].SalePrice.GetValueOrDefault(),
|
ComponentSubtotal = items[0].SellingFlatPrice.GetValueOrDefault() + items[1].SellingFlatPrice.GetValueOrDefault(),
|
||||||
BundlePrice = (items[0].SalePrice.GetValueOrDefault() + items[1].SalePrice.GetValueOrDefault()) - 10m,
|
BundlePrice = (items[0].SellingFlatPrice.GetValueOrDefault() + items[1].SellingFlatPrice.GetValueOrDefault()) - 10m,
|
||||||
MarginAmount = -10m,
|
MarginAmount = -10m,
|
||||||
DiscountTotal = 10m,
|
DiscountTotal = 10m,
|
||||||
TaxTotal = 0m,
|
TaxTotal = 0m,
|
||||||
GrandTotal = (items[0].SalePrice.GetValueOrDefault() + items[1].SalePrice.GetValueOrDefault()) - 10m,
|
GrandTotal = (items[0].SellingFlatPrice.GetValueOrDefault() + items[1].SellingFlatPrice.GetValueOrDefault()) - 10m,
|
||||||
CreatedAt = now.AddDays(-1),
|
CreatedAt = now.AddDays(-1),
|
||||||
UpdatedAt = now.AddHours(-2),
|
UpdatedAt = now.AddHours(-2),
|
||||||
Lines =
|
Lines =
|
||||||
@@ -539,8 +573,8 @@ public static class DataSeeder
|
|||||||
Description = items[0].Name,
|
Description = items[0].Name,
|
||||||
Qty = 1m,
|
Qty = 1m,
|
||||||
WarehouseId = warehouse.WarehouseId,
|
WarehouseId = warehouse.WarehouseId,
|
||||||
UnitPrice = items[0].SalePrice.GetValueOrDefault(),
|
UnitPrice = items[0].SellingFlatPrice.GetValueOrDefault(),
|
||||||
LineTotal = items[0].SalePrice.GetValueOrDefault(),
|
LineTotal = items[0].SellingFlatPrice.GetValueOrDefault(),
|
||||||
IncludeInBundle = true,
|
IncludeInBundle = true,
|
||||||
IsComponent = true
|
IsComponent = true
|
||||||
},
|
},
|
||||||
@@ -550,8 +584,8 @@ public static class DataSeeder
|
|||||||
Description = items[1].Name,
|
Description = items[1].Name,
|
||||||
Qty = 1m,
|
Qty = 1m,
|
||||||
WarehouseId = warehouse.WarehouseId,
|
WarehouseId = warehouse.WarehouseId,
|
||||||
UnitPrice = items[1].SalePrice.GetValueOrDefault(),
|
UnitPrice = items[1].SellingFlatPrice.GetValueOrDefault(),
|
||||||
LineTotal = items[1].SalePrice.GetValueOrDefault(),
|
LineTotal = items[1].SellingFlatPrice.GetValueOrDefault(),
|
||||||
IncludeInBundle = true,
|
IncludeInBundle = true,
|
||||||
IsComponent = true
|
IsComponent = true
|
||||||
}
|
}
|
||||||
@@ -569,12 +603,12 @@ public static class DataSeeder
|
|||||||
BundleName = "Demo Bundle Cancelled",
|
BundleName = "Demo Bundle Cancelled",
|
||||||
BundleCode = "BND-DEMO-001",
|
BundleCode = "BND-DEMO-001",
|
||||||
Status = BundleSaleStatus.Cancelled,
|
Status = BundleSaleStatus.Cancelled,
|
||||||
ComponentSubtotal = items[0].SalePrice.GetValueOrDefault(),
|
ComponentSubtotal = items[0].SellingFlatPrice.GetValueOrDefault(),
|
||||||
BundlePrice = items[0].SalePrice.GetValueOrDefault(),
|
BundlePrice = items[0].SellingFlatPrice.GetValueOrDefault(),
|
||||||
MarginAmount = 0m,
|
MarginAmount = 0m,
|
||||||
DiscountTotal = 0m,
|
DiscountTotal = 0m,
|
||||||
TaxTotal = 0m,
|
TaxTotal = 0m,
|
||||||
GrandTotal = items[0].SalePrice.GetValueOrDefault(),
|
GrandTotal = items[0].SellingFlatPrice.GetValueOrDefault(),
|
||||||
CreatedAt = now,
|
CreatedAt = now,
|
||||||
UpdatedAt = now,
|
UpdatedAt = now,
|
||||||
Lines =
|
Lines =
|
||||||
@@ -585,8 +619,8 @@ public static class DataSeeder
|
|||||||
Description = items[0].Name,
|
Description = items[0].Name,
|
||||||
Qty = 1m,
|
Qty = 1m,
|
||||||
WarehouseId = secondaryWarehouse.WarehouseId,
|
WarehouseId = secondaryWarehouse.WarehouseId,
|
||||||
UnitPrice = items[0].SalePrice.GetValueOrDefault(),
|
UnitPrice = items[0].SellingFlatPrice.GetValueOrDefault(),
|
||||||
LineTotal = items[0].SalePrice.GetValueOrDefault(),
|
LineTotal = items[0].SellingFlatPrice.GetValueOrDefault(),
|
||||||
IncludeInBundle = true,
|
IncludeInBundle = true,
|
||||||
IsComponent = true
|
IsComponent = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ public class ErpDbContext : DbContext
|
|||||||
|
|
||||||
/// <summary>Singleton row (FR-MD-11).</summary>
|
/// <summary>Singleton row (FR-MD-11).</summary>
|
||||||
public DbSet<ProductConfig> ProductConfig => Set<ProductConfig>();
|
public DbSet<ProductConfig> ProductConfig => Set<ProductConfig>();
|
||||||
|
/// <summary>Singleton row gating whether this deployment uses branches at all.</summary>
|
||||||
|
public DbSet<BranchConfig> BranchConfig => Set<BranchConfig>();
|
||||||
|
/// <summary>Singleton row for notification delivery preferences.</summary>
|
||||||
|
public DbSet<NotificationConfig> NotificationConfig => Set<NotificationConfig>();
|
||||||
|
public DbSet<NotificationConfigModule> NotificationConfigModules => Set<NotificationConfigModule>();
|
||||||
|
|
||||||
// --- Cross-cutting (docs/10 Part C.7) ---
|
// --- Cross-cutting (docs/10 Part C.7) ---
|
||||||
public DbSet<User> Users => Set<User>();
|
public DbSet<User> Users => Set<User>();
|
||||||
@@ -63,7 +68,11 @@ public class ErpDbContext : DbContext
|
|||||||
// --- Goods Receipt (docs/10 Part C.3) ---
|
// --- Goods Receipt (docs/10 Part C.3) ---
|
||||||
public DbSet<Grn> Grns => Set<Grn>();
|
public DbSet<Grn> Grns => Set<Grn>();
|
||||||
public DbSet<GrnLine> GrnLines => Set<GrnLine>();
|
public DbSet<GrnLine> GrnLines => Set<GrnLine>();
|
||||||
public DbSet<GrnPayment> GrnPayments => Set<GrnPayment>();
|
|
||||||
|
// --- Purchase Invoices (vendor invoices consolidating Confirmed GRNs) ---
|
||||||
|
public DbSet<PurchaseInvoice> PurchaseInvoices => Set<PurchaseInvoice>();
|
||||||
|
public DbSet<PurchaseInvoiceLine> PurchaseInvoiceLines => Set<PurchaseInvoiceLine>();
|
||||||
|
public DbSet<PurchaseInvoicePayment> PurchaseInvoicePayments => Set<PurchaseInvoicePayment>();
|
||||||
|
|
||||||
// --- Batch / Serial (docs/10 Part C.4) ---
|
// --- Batch / Serial (docs/10 Part C.4) ---
|
||||||
public DbSet<Batch> Batches => Set<Batch>();
|
public DbSet<Batch> Batches => Set<Batch>();
|
||||||
|
|||||||
+7531
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,90 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
|
||||||
|
|
||||||
|
namespace ERPCore.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddProductionNavPermissions : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
table: "nav_items",
|
||||||
|
columns: new[] { "NavItemId", "Code", "Href", "Icon", "Label", "SortOrder" },
|
||||||
|
values: new object[] { 14, "production", "/dashboard/production", null, "Production", 14 });
|
||||||
|
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
table: "permissions",
|
||||||
|
columns: new[] { "PermissionId", "Code", "NavItemId", "SubNavItemId" },
|
||||||
|
values: new object[] { 35, "NAV:production", 14, null });
|
||||||
|
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
table: "sub_nav_items",
|
||||||
|
columns: new[] { "SubNavItemId", "Code", "Href", "Icon", "Label", "NavItemId", "SortOrder" },
|
||||||
|
values: new object[,]
|
||||||
|
{
|
||||||
|
{ 24, "production.templates", "/dashboard/production/templates", null, "Templates", 14, 1 },
|
||||||
|
{ 25, "production.runs", "/dashboard/production/runs", null, "Runs", 14, 2 },
|
||||||
|
{ 26, "production.produced", "/dashboard/production/produced", null, "Produced", 14, 3 }
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
table: "permissions",
|
||||||
|
columns: new[] { "PermissionId", "Code", "NavItemId", "SubNavItemId" },
|
||||||
|
values: new object[,]
|
||||||
|
{
|
||||||
|
{ 36, "NAV:production.templates", null, 24 },
|
||||||
|
{ 37, "NAV:production.runs", null, 25 },
|
||||||
|
{ 38, "NAV:production.produced", null, 26 }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
table: "permissions",
|
||||||
|
keyColumn: "PermissionId",
|
||||||
|
keyValue: 35);
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
table: "permissions",
|
||||||
|
keyColumn: "PermissionId",
|
||||||
|
keyValue: 36);
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
table: "permissions",
|
||||||
|
keyColumn: "PermissionId",
|
||||||
|
keyValue: 37);
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
table: "permissions",
|
||||||
|
keyColumn: "PermissionId",
|
||||||
|
keyValue: 38);
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
table: "sub_nav_items",
|
||||||
|
keyColumn: "SubNavItemId",
|
||||||
|
keyValue: 24);
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
table: "sub_nav_items",
|
||||||
|
keyColumn: "SubNavItemId",
|
||||||
|
keyValue: 25);
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
table: "sub_nav_items",
|
||||||
|
keyColumn: "SubNavItemId",
|
||||||
|
keyValue: 26);
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
table: "nav_items",
|
||||||
|
keyColumn: "NavItemId",
|
||||||
|
keyValue: 14);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,52 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ERPCore.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class itemModel : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.RenameColumn(
|
||||||
|
name: "SalePrice",
|
||||||
|
table: "items",
|
||||||
|
newName: "SellingMinPrice");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<decimal>(
|
||||||
|
name: "SellingFlatPrice",
|
||||||
|
table: "items",
|
||||||
|
type: "numeric(18,4)",
|
||||||
|
precision: 18,
|
||||||
|
scale: 4,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<decimal>(
|
||||||
|
name: "SellingMaxPrice",
|
||||||
|
table: "items",
|
||||||
|
type: "numeric(18,4)",
|
||||||
|
precision: 18,
|
||||||
|
scale: 4,
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "SellingFlatPrice",
|
||||||
|
table: "items");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "SellingMaxPrice",
|
||||||
|
table: "items");
|
||||||
|
|
||||||
|
migrationBuilder.RenameColumn(
|
||||||
|
name: "SellingMinPrice",
|
||||||
|
table: "items",
|
||||||
|
newName: "SalePrice");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+7580
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,103 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ERPCore.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddNotificationConfigModulePreferences : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "notification_config",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
NotificationConfigId = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||||
|
UpdatedBy = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
xmin = table.Column<uint>(type: "xid", rowVersion: true, nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_notification_config", x => x.NotificationConfigId);
|
||||||
|
table.CheckConstraint("ck_notification_config_singleton", "\"NotificationConfigId\" = 1");
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_notification_config_users_UpdatedBy",
|
||||||
|
column: x => x.UpdatedBy,
|
||||||
|
principalTable: "users",
|
||||||
|
principalColumn: "UserId",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "notification_config_modules",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
NotificationConfigModuleId = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
NotificationConfigId = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
ModuleKey = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||||
|
EmailEnabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false),
|
||||||
|
SmsEnabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false),
|
||||||
|
InAppEnabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_notification_config_modules", x => x.NotificationConfigModuleId);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_notification_config_modules_notification_config_Notificatio~",
|
||||||
|
column: x => x.NotificationConfigId,
|
||||||
|
principalTable: "notification_config",
|
||||||
|
principalColumn: "NotificationConfigId",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.Sql("""
|
||||||
|
INSERT INTO sub_nav_items ("SubNavItemId", "NavItemId", "Code", "Href", "Icon", "Label", "SortOrder")
|
||||||
|
VALUES (24, 9, 'settings.notification-config', '/dashboard/settings/notification-config', NULL, 'Notification Config', 3)
|
||||||
|
ON CONFLICT ("SubNavItemId") DO UPDATE
|
||||||
|
SET "NavItemId" = EXCLUDED."NavItemId",
|
||||||
|
"Code" = EXCLUDED."Code",
|
||||||
|
"Href" = EXCLUDED."Href",
|
||||||
|
"Icon" = EXCLUDED."Icon",
|
||||||
|
"Label" = EXCLUDED."Label",
|
||||||
|
"SortOrder" = EXCLUDED."SortOrder";
|
||||||
|
""");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_notification_config_UpdatedBy",
|
||||||
|
table: "notification_config",
|
||||||
|
column: "UpdatedBy");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_notification_config_modules_ModuleKey",
|
||||||
|
table: "notification_config_modules",
|
||||||
|
column: "ModuleKey",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_notification_config_modules_NotificationConfigId",
|
||||||
|
table: "notification_config_modules",
|
||||||
|
column: "NotificationConfigId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "notification_config_modules");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "notification_config");
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
table: "sub_nav_items",
|
||||||
|
keyColumn: "SubNavItemId",
|
||||||
|
keyValue: 24);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ERPCore.Migrations
|
||||||
|
{
|
||||||
|
public partial class UpdateItemSellingPrices : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.RenameColumn(
|
||||||
|
name: "SalePrice",
|
||||||
|
table: "items",
|
||||||
|
newName: "SellingFlatPrice");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<decimal>(
|
||||||
|
name: "SellingMinPrice",
|
||||||
|
table: "items",
|
||||||
|
type: "numeric(18,4)",
|
||||||
|
precision: 18,
|
||||||
|
scale: 4,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<decimal>(
|
||||||
|
name: "SellingMaxPrice",
|
||||||
|
table: "items",
|
||||||
|
type: "numeric(18,4)",
|
||||||
|
precision: 18,
|
||||||
|
scale: 4,
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "SellingMinPrice",
|
||||||
|
table: "items");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "SellingMaxPrice",
|
||||||
|
table: "items");
|
||||||
|
|
||||||
|
migrationBuilder.RenameColumn(
|
||||||
|
name: "SellingFlatPrice",
|
||||||
|
table: "items",
|
||||||
|
newName: "SalePrice");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+7600
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,62 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ERPCore.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddProductConfigFeatureFlags : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "BatchListsEnabled",
|
||||||
|
table: "product_config",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "SellingPricesEnabled",
|
||||||
|
table: "product_config",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "SerialNumbersEnabled",
|
||||||
|
table: "product_config",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "WarrantyPeriodsEnabled",
|
||||||
|
table: "product_config",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "BatchListsEnabled",
|
||||||
|
table: "product_config");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "SellingPricesEnabled",
|
||||||
|
table: "product_config");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "SerialNumbersEnabled",
|
||||||
|
table: "product_config");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "WarrantyPeriodsEnabled",
|
||||||
|
table: "product_config");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+24
@@ -0,0 +1,24 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using ERPCore.Infra.Persistence;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ERPCore.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ErpDbContext))]
|
||||||
|
[Migration("20260825000000_FixProductionTemplatesSubNavItemIdConflict")]
|
||||||
|
partial class FixProductionTemplatesSubNavItemIdConflict
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
// This migration only repairs seed row data via raw SQL — no entity/column shape
|
||||||
|
// changes — so it does not redeclare the full model here. The single source of
|
||||||
|
// truth for scaffolding future migrations is ErpDbContextModelSnapshot.cs, which
|
||||||
|
// already reflects this fix (production.templates seeded at SubNavItemId 27).
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ERPCore.Migrations
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Repairs a collision baked into migration history: 20260818090139_AddProductionNavPermissions
|
||||||
|
/// inserted "production.templates" as sub_nav_items row 24, then
|
||||||
|
/// 20260820100345_AddNotificationConfigModulePreferences reused the same literal id 24 for
|
||||||
|
/// "settings.notification-config" via a raw-SQL <c>INSERT ... ON CONFLICT ("SubNavItemId") DO
|
||||||
|
/// UPDATE</c>, silently overwriting the production.templates row instead of inserting a new
|
||||||
|
/// one. Row 24 now correctly holds notification-config; this migration re-creates the lost
|
||||||
|
/// production.templates row under a fresh id (27, matching the corrected seed data in
|
||||||
|
/// SubNavItemConfiguration/PermissionConfiguration) and repoints the NAV:production.templates
|
||||||
|
/// permission (id 36) at it.
|
||||||
|
/// </summary>
|
||||||
|
public partial class FixProductionTemplatesSubNavItemIdConflict : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.Sql("""
|
||||||
|
INSERT INTO sub_nav_items ("SubNavItemId", "NavItemId", "Code", "Href", "Icon", "Label", "SortOrder")
|
||||||
|
VALUES (27, 14, 'production.templates', '/dashboard/production/templates', NULL, 'Templates', 1)
|
||||||
|
ON CONFLICT ("SubNavItemId") DO NOTHING;
|
||||||
|
""");
|
||||||
|
|
||||||
|
migrationBuilder.Sql("""
|
||||||
|
UPDATE permissions SET "SubNavItemId" = 27 WHERE "PermissionId" = 36;
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.Sql("""
|
||||||
|
UPDATE permissions SET "SubNavItemId" = 24 WHERE "PermissionId" = 36;
|
||||||
|
""");
|
||||||
|
|
||||||
|
migrationBuilder.Sql("""
|
||||||
|
DELETE FROM sub_nav_items WHERE "SubNavItemId" = 27;
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,303 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ERPCore.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddPurchaseInvoices : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "grn_payments");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "BalanceAmount",
|
||||||
|
table: "grns");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "PaidAmount",
|
||||||
|
table: "grns");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "PurchaseInvoiceId",
|
||||||
|
table: "grns",
|
||||||
|
type: "integer",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "purchase_invoices",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
PurchaseInvoiceId = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
DocNo = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
|
||||||
|
VendorInvoiceNo = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||||
|
VendorInvoiceDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
VendorId = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
VendorSnapshotName = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||||
|
VendorSnapshotTaxNo = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
|
||||||
|
Status = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false, defaultValue: "Draft"),
|
||||||
|
Subtotal = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false),
|
||||||
|
VatTotal = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false),
|
||||||
|
GrandTotal = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false),
|
||||||
|
PaidAmount = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false),
|
||||||
|
BalanceAmount = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false),
|
||||||
|
GlJournalNo = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: true),
|
||||||
|
GlPostedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||||
|
CreatedBy = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||||
|
xmin = table.Column<uint>(type: "xid", rowVersion: true, nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_purchase_invoices", x => x.PurchaseInvoiceId);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_purchase_invoices_users_CreatedBy",
|
||||||
|
column: x => x.CreatedBy,
|
||||||
|
principalTable: "users",
|
||||||
|
principalColumn: "UserId",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_purchase_invoices_vendors_VendorId",
|
||||||
|
column: x => x.VendorId,
|
||||||
|
principalTable: "vendors",
|
||||||
|
principalColumn: "VendorId",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "purchase_invoice_lines",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
PurchaseInvoiceLineId = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
PurchaseInvoiceId = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
GrnId = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
Subtotal = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false),
|
||||||
|
VatTotal = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false),
|
||||||
|
LineTotal = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_purchase_invoice_lines", x => x.PurchaseInvoiceLineId);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_purchase_invoice_lines_grns_GrnId",
|
||||||
|
column: x => x.GrnId,
|
||||||
|
principalTable: "grns",
|
||||||
|
principalColumn: "GrnId",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_purchase_invoice_lines_purchase_invoices_PurchaseInvoiceId",
|
||||||
|
column: x => x.PurchaseInvoiceId,
|
||||||
|
principalTable: "purchase_invoices",
|
||||||
|
principalColumn: "PurchaseInvoiceId",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "purchase_invoice_payments",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
PurchaseInvoicePaymentId = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
PurchaseInvoiceId = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
Amount = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false),
|
||||||
|
PaymentDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
GlBankAccountId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
BankAccountName = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||||
|
Reference = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
|
||||||
|
GlJournalNo = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: true),
|
||||||
|
CreatedBy = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_purchase_invoice_payments", x => x.PurchaseInvoicePaymentId);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_purchase_invoice_payments_purchase_invoices_PurchaseInvoice~",
|
||||||
|
column: x => x.PurchaseInvoiceId,
|
||||||
|
principalTable: "purchase_invoices",
|
||||||
|
principalColumn: "PurchaseInvoiceId",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_purchase_invoice_payments_users_CreatedBy",
|
||||||
|
column: x => x.CreatedBy,
|
||||||
|
principalTable: "users",
|
||||||
|
principalColumn: "UserId",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
table: "sub_nav_items",
|
||||||
|
columns: new[] { "SubNavItemId", "Code", "Href", "Icon", "Label", "NavItemId", "SortOrder" },
|
||||||
|
values: new object[] { 28, "procurement.purchase-invoices", "/dashboard/procurement/purchase-invoices", null, "Purchase Invoices", 4, 5 });
|
||||||
|
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
table: "permissions",
|
||||||
|
columns: new[] { "PermissionId", "Code", "NavItemId", "SubNavItemId" },
|
||||||
|
values: new object[] { 39, "NAV:procurement.purchase-invoices", null, 28 });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_grns_PurchaseInvoiceId",
|
||||||
|
table: "grns",
|
||||||
|
column: "PurchaseInvoiceId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_purchase_invoice_lines_GrnId",
|
||||||
|
table: "purchase_invoice_lines",
|
||||||
|
column: "GrnId",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_purchase_invoice_lines_PurchaseInvoiceId",
|
||||||
|
table: "purchase_invoice_lines",
|
||||||
|
column: "PurchaseInvoiceId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_purchase_invoice_payments_CreatedBy",
|
||||||
|
table: "purchase_invoice_payments",
|
||||||
|
column: "CreatedBy");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_purchase_invoice_payments_PurchaseInvoiceId",
|
||||||
|
table: "purchase_invoice_payments",
|
||||||
|
column: "PurchaseInvoiceId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_purchase_invoices_CreatedBy",
|
||||||
|
table: "purchase_invoices",
|
||||||
|
column: "CreatedBy");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_purchase_invoices_DocNo",
|
||||||
|
table: "purchase_invoices",
|
||||||
|
column: "DocNo",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_purchase_invoices_Status",
|
||||||
|
table: "purchase_invoices",
|
||||||
|
column: "Status");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_purchase_invoices_VendorId",
|
||||||
|
table: "purchase_invoices",
|
||||||
|
column: "VendorId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_purchase_invoices_VendorId_VendorInvoiceNo",
|
||||||
|
table: "purchase_invoices",
|
||||||
|
columns: new[] { "VendorId", "VendorInvoiceNo" });
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_grns_purchase_invoices_PurchaseInvoiceId",
|
||||||
|
table: "grns",
|
||||||
|
column: "PurchaseInvoiceId",
|
||||||
|
principalTable: "purchase_invoices",
|
||||||
|
principalColumn: "PurchaseInvoiceId",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_grns_purchase_invoices_PurchaseInvoiceId",
|
||||||
|
table: "grns");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "purchase_invoice_lines");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "purchase_invoice_payments");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "purchase_invoices");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_grns_PurchaseInvoiceId",
|
||||||
|
table: "grns");
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
table: "permissions",
|
||||||
|
keyColumn: "PermissionId",
|
||||||
|
keyValue: 39);
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
table: "sub_nav_items",
|
||||||
|
keyColumn: "SubNavItemId",
|
||||||
|
keyValue: 28);
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "PurchaseInvoiceId",
|
||||||
|
table: "grns");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<decimal>(
|
||||||
|
name: "BalanceAmount",
|
||||||
|
table: "grns",
|
||||||
|
type: "numeric(18,4)",
|
||||||
|
precision: 18,
|
||||||
|
scale: 4,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0m);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<decimal>(
|
||||||
|
name: "PaidAmount",
|
||||||
|
table: "grns",
|
||||||
|
type: "numeric(18,4)",
|
||||||
|
precision: 18,
|
||||||
|
scale: 4,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0m);
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "grn_payments",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
GrnPaymentId = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
CreatedBy = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
GrnId = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
Amount = table.Column<decimal>(type: "numeric(18,4)", precision: 18, scale: 4, nullable: false),
|
||||||
|
BankAccountName = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
GlBankAccountId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
GlJournalNo = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: true),
|
||||||
|
PaymentDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
Reference = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_grn_payments", x => x.GrnPaymentId);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_grn_payments_grns_GrnId",
|
||||||
|
column: x => x.GrnId,
|
||||||
|
principalTable: "grns",
|
||||||
|
principalColumn: "GrnId",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_grn_payments_users_CreatedBy",
|
||||||
|
column: x => x.CreatedBy,
|
||||||
|
principalTable: "users",
|
||||||
|
principalColumn: "UserId",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_grn_payments_CreatedBy",
|
||||||
|
table: "grn_payments",
|
||||||
|
column: "CreatedBy");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_grn_payments_GrnId",
|
||||||
|
table: "grn_payments",
|
||||||
|
column: "GrnId");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+7880
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ERPCore.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddBranchPhoneNumberAndCodeAutogen : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "PhoneNumber",
|
||||||
|
table: "hr_branches",
|
||||||
|
type: "character varying(30)",
|
||||||
|
maxLength: 30,
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "PhoneNumber",
|
||||||
|
table: "hr_branches");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+7876
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,38 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ERPCore.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddSettingsBranchesNavItem : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
table: "sub_nav_items",
|
||||||
|
columns: new[] { "SubNavItemId", "Code", "Href", "Icon", "Label", "NavItemId", "SortOrder" },
|
||||||
|
values: new object[] { 29, "settings.branches", "/dashboard/settings/branches", null, "Branches", 9, 0 });
|
||||||
|
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
table: "permissions",
|
||||||
|
columns: new[] { "PermissionId", "Code", "NavItemId", "SubNavItemId" },
|
||||||
|
values: new object[] { 40, "NAV:settings.branches", null, 29 });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
table: "permissions",
|
||||||
|
keyColumn: "PermissionId",
|
||||||
|
keyValue: 40);
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
table: "sub_nav_items",
|
||||||
|
keyColumn: "SubNavItemId",
|
||||||
|
keyValue: 29);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+7881
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ERPCore.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class MakeEmployeeBranchRequired : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
// Branch becomes a required field on Employee going forward. Any pre-existing
|
||||||
|
// employee left with no branch is backfilled to a "Main Branch" placeholder
|
||||||
|
// (created only if it doesn't already exist) so the NOT NULL below never fails
|
||||||
|
// on live data.
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
INSERT INTO hr_branches (""Code"", ""Name"", ""Status"", ""CreatedAt"")
|
||||||
|
SELECT 'BR-000', 'Main Branch', 'Active', now()
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM hr_branches WHERE ""Code"" = 'BR-000')
|
||||||
|
AND EXISTS (SELECT 1 FROM hr_employees WHERE ""BranchId"" IS NULL);
|
||||||
|
|
||||||
|
UPDATE hr_employees
|
||||||
|
SET ""BranchId"" = (SELECT ""BranchId"" FROM hr_branches WHERE ""Code"" = 'BR-000')
|
||||||
|
WHERE ""BranchId"" IS NULL;
|
||||||
|
");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<int>(
|
||||||
|
name: "BranchId",
|
||||||
|
table: "hr_employees",
|
||||||
|
type: "integer",
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(int),
|
||||||
|
oldType: "integer",
|
||||||
|
oldNullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterColumn<int>(
|
||||||
|
name: "BranchId",
|
||||||
|
table: "hr_employees",
|
||||||
|
type: "integer",
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(int),
|
||||||
|
oldType: "integer");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,73 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace ERPCore.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddWarehouseBranch : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "BranchId",
|
||||||
|
table: "warehouses",
|
||||||
|
type: "integer",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
// Backfill any pre-existing warehouse onto the same "Main Branch" placeholder
|
||||||
|
// used by the earlier MakeEmployeeBranchRequired migration (created only if it
|
||||||
|
// doesn't already exist) — keeps a single de-facto default rather than minting
|
||||||
|
// a second one — before the column is locked to NOT NULL below.
|
||||||
|
migrationBuilder.Sql(@"
|
||||||
|
INSERT INTO hr_branches (""Code"", ""Name"", ""Status"", ""CreatedAt"")
|
||||||
|
SELECT 'BR-000', 'Main Branch', 'Active', now()
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM hr_branches WHERE ""Code"" = 'BR-000')
|
||||||
|
AND EXISTS (SELECT 1 FROM warehouses WHERE ""BranchId"" IS NULL);
|
||||||
|
|
||||||
|
UPDATE warehouses
|
||||||
|
SET ""BranchId"" = (SELECT ""BranchId"" FROM hr_branches WHERE ""Code"" = 'BR-000')
|
||||||
|
WHERE ""BranchId"" IS NULL;
|
||||||
|
");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<int>(
|
||||||
|
name: "BranchId",
|
||||||
|
table: "warehouses",
|
||||||
|
type: "integer",
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(int),
|
||||||
|
oldType: "integer",
|
||||||
|
oldNullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_warehouses_BranchId",
|
||||||
|
table: "warehouses",
|
||||||
|
column: "BranchId");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_warehouses_hr_branches_BranchId",
|
||||||
|
table: "warehouses",
|
||||||
|
column: "BranchId",
|
||||||
|
principalTable: "hr_branches",
|
||||||
|
principalColumn: "BranchId",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_warehouses_hr_branches_BranchId",
|
||||||
|
table: "warehouses");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_warehouses_BranchId",
|
||||||
|
table: "warehouses");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "BranchId",
|
||||||
|
table: "warehouses");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user