Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 149b87bfeb | |||
| b3b6a25ce8 | |||
| f664961fce | |||
| bebec10eb3 | |||
| 160c687db3 | |||
| d413ad4064 | |||
| 9b8c2493ad | |||
| 99b669395e | |||
| 4cc6a546c7 | |||
| 8835ba9500 | |||
| 251b23f4b5 | |||
| 283a56723f | |||
| 9a38cc1131 | |||
| 1c5b05e315 | |||
| efa4ad3770 | |||
| 6528fc8d9d | |||
| ae6a87022d | |||
| ee9fa40edf | |||
| b1b0fe4bac | |||
| 8555702a76 | |||
| fd95e92eb1 | |||
| 6e008773db | |||
| 179d5b0803 | |||
| 342012a321 | |||
| ee6ac913f1 | |||
| 2661169351 | |||
| 80213cf47d | |||
| 18475fdc9f | |||
| 7219480ca0 |
+1
-4
@@ -36,7 +36,4 @@ Testing/e2e/test-results/
|
||||
Testing/e2e/.auth/
|
||||
Testing/e2e/blob-report/
|
||||
|
||||
# ── Migrations ─────────────────────────────────────────────────────────
|
||||
# Each dev keeps EF Core migrations local; DB schema changes are announced
|
||||
# to the team instead of committed, so migration files aren't shared here.
|
||||
Migrations/
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
[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]
|
||||
[ProducesResponseType(typeof(PagedResponse<BundleSaleSummaryDto>), StatusCodes.Status200OK)]
|
||||
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>
|
||||
[HttpGet("stats")]
|
||||
[ProducesResponseType(typeof(DashboardStatsDto), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<DashboardStatsDto>> GetStats(CancellationToken ct)
|
||||
=> Ok(await _dashboard.GetStatsAsync(ct));
|
||||
public async Task<ActionResult<DashboardStatsDto>> GetStats([FromQuery] int? branchId, CancellationToken ct)
|
||||
=> Ok(await _dashboard.GetStatsAsync(branchId, ct));
|
||||
}
|
||||
|
||||
@@ -12,15 +12,18 @@ public sealed class GrnsController : ApiControllerBase
|
||||
{
|
||||
private readonly IGrnService _grns;
|
||||
|
||||
public GrnsController(IGrnService grns) => _grns = grns;
|
||||
public GrnsController(IGrnService grns)
|
||||
{
|
||||
_grns = grns;
|
||||
}
|
||||
|
||||
/// <summary>List GRNs, newest first.</summary>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(PagedResponse<GrnSummaryDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedResponse<GrnSummaryDto>>> List(
|
||||
[FromQuery] PageQuery query, [FromQuery] GrnStatus? status, [FromQuery] int? poId,
|
||||
[FromQuery] int? vendorId, [FromQuery] int? warehouseId, CancellationToken ct)
|
||||
=> Ok(await _grns.ListAsync(query, status, poId, vendorId, warehouseId, ct));
|
||||
[FromQuery] int? vendorId, [FromQuery] int? warehouseId, [FromQuery] int? branchId, CancellationToken ct)
|
||||
=> Ok(await _grns.ListAsync(query, status, poId, vendorId, warehouseId, branchId, ct));
|
||||
|
||||
[HttpGet("{grnId:int}")]
|
||||
[ProducesResponseType(typeof(GrnDto), StatusCodes.Status200OK)]
|
||||
|
||||
@@ -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)
|
||||
=> 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}")]
|
||||
[ProducesResponseType(typeof(RunGraphDto), StatusCodes.Status200OK)]
|
||||
[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));
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Sales;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>Cashier day-end close-out — locks a cashier's Posted sales slips for a business
|
||||
/// date and posts one consolidated GL journal entry for the day (docs/14 Sales API).</summary>
|
||||
[Route("api/v1/sales-day-end")]
|
||||
public sealed class SalesDayEndController : ApiControllerBase
|
||||
{
|
||||
private readonly ISalesDayEndService _dayEnds;
|
||||
|
||||
public SalesDayEndController(ISalesDayEndService dayEnds) => _dayEnds = dayEnds;
|
||||
|
||||
/// <summary>List day-end closes, newest first.</summary>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(PagedResponse<SalesDayEndSummaryDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedResponse<SalesDayEndSummaryDto>>> List(
|
||||
[FromQuery] PageQuery query, [FromQuery] int? cashierUserId, CancellationToken ct)
|
||||
=> Ok(await _dayEnds.ListAsync(query, cashierUserId, ct));
|
||||
|
||||
/// <summary>What closing this cashier/date right now would include (or the existing close's totals, if already closed).</summary>
|
||||
[HttpGet("preview")]
|
||||
[ProducesResponseType(typeof(SalesDayEndPreviewDto), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<SalesDayEndPreviewDto>> Preview(
|
||||
[FromQuery] int cashierUserId, [FromQuery] DateOnly? businessDate, CancellationToken ct)
|
||||
=> Ok(await _dayEnds.PreviewAsync(cashierUserId, businessDate ?? DateOnly.FromDateTime(DateTime.UtcNow), ct));
|
||||
|
||||
[HttpGet("{salesDayEndId:int}")]
|
||||
[ProducesResponseType(typeof(SalesDayEndDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<SalesDayEndDto>> GetById(int salesDayEndId, CancellationToken ct)
|
||||
{
|
||||
var dto = await _dayEnds.GetAsync(salesDayEndId, ct);
|
||||
return dto is null ? NotFound() : Ok(dto);
|
||||
}
|
||||
|
||||
/// <summary>Close the day: lock every Posted slip for (CashierUserId, BusinessDate) and post the
|
||||
/// consolidated GL journal entry. Idempotent — closing an already-closed date replays it.</summary>
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(SalesDayEndDto), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
public async Task<ActionResult<SalesDayEndDto>> Close([FromBody] CreateSalesDayEndRequest request, CancellationToken ct)
|
||||
{
|
||||
var dto = await _dayEnds.CloseAsync(request, ct);
|
||||
return Created($"/api/v1/sales-day-end/{dto.SalesDayEndId}", dto);
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,13 @@ namespace ERPCore.Controllers;
|
||||
public sealed class SalesInvoicesController : ApiControllerBase
|
||||
{
|
||||
private readonly ISalesInvoiceService _invoices;
|
||||
private readonly ISalesInvoicePaymentService _payments;
|
||||
|
||||
public SalesInvoicesController(ISalesInvoiceService invoices) => _invoices = invoices;
|
||||
public SalesInvoicesController(ISalesInvoiceService invoices, ISalesInvoicePaymentService payments)
|
||||
{
|
||||
_invoices = invoices;
|
||||
_payments = payments;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(PagedResponse<SalesInvoiceSummaryDto>), StatusCodes.Status200OK)]
|
||||
@@ -20,8 +25,9 @@ public sealed class SalesInvoicesController : ApiControllerBase
|
||||
[FromQuery] SalesInvoiceStatus? status,
|
||||
[FromQuery] int? customerId,
|
||||
[FromQuery] int? warehouseId,
|
||||
[FromQuery] int? branchId,
|
||||
CancellationToken ct)
|
||||
=> Ok(await _invoices.ListAsync(query, status, customerId, warehouseId, ct));
|
||||
=> Ok(await _invoices.ListAsync(query, status, customerId, warehouseId, branchId, ct));
|
||||
|
||||
[HttpGet("{salesInvoiceId:int}")]
|
||||
[ProducesResponseType(typeof(SalesInvoiceDto), StatusCodes.Status200OK)]
|
||||
@@ -70,4 +76,23 @@ public sealed class SalesInvoicesController : ApiControllerBase
|
||||
[ProducesResponseType(typeof(SalesInvoiceDto), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<SalesInvoiceDto>> Cancel(int salesInvoiceId, CancellationToken ct)
|
||||
=> Ok(await _invoices.CancelAsync(salesInvoiceId, ct));
|
||||
|
||||
/// <summary>Pay the customer's balance against this invoice, in full or in installments; posts a real GL journal entry.</summary>
|
||||
[HttpPost("{salesInvoiceId:int}/payments")]
|
||||
[ProducesResponseType(typeof(SalesInvoicePaymentDto), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
public async Task<ActionResult<SalesInvoicePaymentDto>> Pay(int salesInvoiceId, [FromBody] CreateSalesInvoicePaymentRequest request, CancellationToken ct)
|
||||
{
|
||||
var dto = await _payments.PayAsync(salesInvoiceId, request, ct);
|
||||
return Created($"/api/v1/sales-invoices/{salesInvoiceId}/payments/{dto.SalesInvoicePaymentId}", dto);
|
||||
}
|
||||
|
||||
/// <summary>Payment history for this invoice, newest first.</summary>
|
||||
[HttpGet("{salesInvoiceId:int}/payments")]
|
||||
[ProducesResponseType(typeof(IReadOnlyList<SalesInvoicePaymentDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<IReadOnlyList<SalesInvoicePaymentDto>>> ListPayments(int salesInvoiceId, CancellationToken ct)
|
||||
=> Ok(await _payments.ListAsync(salesInvoiceId, ct));
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ public sealed class StockAdjustmentsController : ApiControllerBase
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(PagedResponse<AdjustmentSummaryDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedResponse<AdjustmentSummaryDto>>> List(
|
||||
[FromQuery] PageQuery query, [FromQuery] int? warehouseId, [FromQuery] int? reasonCodeId, CancellationToken ct)
|
||||
=> Ok(await _adjustments.ListAsync(query, warehouseId, reasonCodeId, ct));
|
||||
[FromQuery] PageQuery query, [FromQuery] int? warehouseId, [FromQuery] int? branchId, [FromQuery] int? reasonCodeId, CancellationToken ct)
|
||||
=> Ok(await _adjustments.ListAsync(query, warehouseId, branchId, reasonCodeId, ct));
|
||||
|
||||
/// <summary>Get one adjustment with its lines and the ledger entries it posted.</summary>
|
||||
[HttpGet("{adjustmentId:int}")]
|
||||
|
||||
@@ -28,8 +28,8 @@ public sealed class StockController : ApiControllerBase
|
||||
[HttpGet("on-hand/list")]
|
||||
[ProducesResponseType(typeof(PagedResponse<StockOnHandDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedResponse<StockOnHandDto>>> OnHandList(
|
||||
[FromQuery] int? itemId, [FromQuery] int? warehouseId, [FromQuery] PageQuery query, CancellationToken ct)
|
||||
=> Ok(await _stock.GetOnHandListAsync(itemId, warehouseId, query, ct));
|
||||
[FromQuery] int? itemId, [FromQuery] int? warehouseId, [FromQuery] int? branchId, [FromQuery] PageQuery query, CancellationToken ct)
|
||||
=> Ok(await _stock.GetOnHandListAsync(itemId, warehouseId, branchId, query, ct));
|
||||
|
||||
/// <summary>
|
||||
/// Immutable movement history. <c>sourceDocType</c>/<c>sourceDocId</c> answer "what did
|
||||
@@ -39,11 +39,11 @@ public sealed class StockController : ApiControllerBase
|
||||
[HttpGet("ledger")]
|
||||
[ProducesResponseType(typeof(PagedResponse<StockLedgerRowDto>), StatusCodes.Status200OK)]
|
||||
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] string? sourceDocType, [FromQuery] int? sourceDocId,
|
||||
[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")]
|
||||
[ProducesResponseType(typeof(StockValuationDto), StatusCodes.Status200OK)]
|
||||
@@ -54,8 +54,8 @@ public sealed class StockController : ApiControllerBase
|
||||
[HttpGet("reorder-alerts")]
|
||||
[ProducesResponseType(typeof(PagedResponse<ReorderAlertDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedResponse<ReorderAlertDto>>> ReorderAlerts(
|
||||
[FromQuery] int? warehouseId, [FromQuery] PageQuery query, CancellationToken ct)
|
||||
=> Ok(await _reorder.GetAlertsAsync(warehouseId, query, ct));
|
||||
[FromQuery] int? warehouseId, [FromQuery] int? branchId, [FromQuery] PageQuery query, CancellationToken ct)
|
||||
=> Ok(await _reorder.GetAlertsAsync(warehouseId, branchId, query, ct));
|
||||
|
||||
/// <summary>Create a draft requisition for an item's suggested reorder quantity.</summary>
|
||||
[HttpPost("reorder-alerts/{itemId:int}/requisition")]
|
||||
|
||||
@@ -18,8 +18,8 @@ public sealed class StockCountsController : ApiControllerBase
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(PagedResponse<CountSummaryDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedResponse<CountSummaryDto>>> List(
|
||||
[FromQuery] PageQuery query, [FromQuery] CountStatus? status, [FromQuery] int? warehouseId, CancellationToken ct)
|
||||
=> Ok(await _counts.ListAsync(query, status, warehouseId, ct));
|
||||
[FromQuery] PageQuery query, [FromQuery] CountStatus? status, [FromQuery] int? warehouseId, [FromQuery] int? branchId, CancellationToken ct)
|
||||
=> Ok(await _counts.ListAsync(query, status, warehouseId, branchId, ct));
|
||||
|
||||
[HttpGet("{countId:int}")]
|
||||
[ProducesResponseType(typeof(CountDto), StatusCodes.Status200OK)]
|
||||
|
||||
@@ -19,8 +19,8 @@ public sealed class StockTransfersController : ApiControllerBase
|
||||
[ProducesResponseType(typeof(PagedResponse<TransferSummaryDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedResponse<TransferSummaryDto>>> List(
|
||||
[FromQuery] PageQuery query, [FromQuery] TransferStatus? status,
|
||||
[FromQuery] int? srcWarehouseId, [FromQuery] int? destWarehouseId, CancellationToken ct)
|
||||
=> Ok(await _transfers.ListAsync(query, status, srcWarehouseId, destWarehouseId, ct));
|
||||
[FromQuery] int? srcWarehouseId, [FromQuery] int? destWarehouseId, [FromQuery] int? branchId, CancellationToken ct)
|
||||
=> Ok(await _transfers.ListAsync(query, status, srcWarehouseId, destWarehouseId, branchId, ct));
|
||||
|
||||
[HttpGet("{transferId:int}")]
|
||||
[ProducesResponseType(typeof(TransferDto), StatusCodes.Status200OK)]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Uoms;
|
||||
using ERPCore.Services.Interfaces;
|
||||
@@ -15,8 +16,9 @@ public sealed class UomsController : ApiControllerBase
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(PagedResponse<UomDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedResponse<UomDto>>> List([FromQuery] PageQuery query, CancellationToken ct)
|
||||
=> Ok(await _uoms.ListAsync(query, ct));
|
||||
public async Task<ActionResult<PagedResponse<UomDto>>> List(
|
||||
[FromQuery] PageQuery query, [FromQuery] EntityStatus? status, CancellationToken ct)
|
||||
=> Ok(await _uoms.ListAsync(query, status, ct));
|
||||
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(UomDto), StatusCodes.Status201Created)]
|
||||
@@ -26,4 +28,14 @@ public sealed class UomsController : ApiControllerBase
|
||||
var dto = await _uoms.CreateAsync(request, ct);
|
||||
return Created($"/api/v1/uoms/{dto.UomId}", dto);
|
||||
}
|
||||
|
||||
/// <summary>Deactivate/reactivate. Masters are never hard-deleted (FR-MD-08).</summary>
|
||||
[HttpPatch("{uomId:int}/status")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> SetStatus(int uomId, [FromBody] UpdateUomStatusRequest request, CancellationToken ct)
|
||||
{
|
||||
await _uoms.SetStatusAsync(uomId, request.Status, ct);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Hrm;
|
||||
using ERPCore.Dtos.Users;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@@ -15,11 +16,13 @@ public sealed class UsersController : ApiControllerBase
|
||||
{
|
||||
private readonly IUserManagementService _users;
|
||||
private readonly IEmployeeUserLinkService _links;
|
||||
private readonly ICurrentUser _currentUser;
|
||||
|
||||
public UsersController(IUserManagementService users, IEmployeeUserLinkService links)
|
||||
public UsersController(IUserManagementService users, IEmployeeUserLinkService links, ICurrentUser currentUser)
|
||||
{
|
||||
_users = users;
|
||||
_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>
|
||||
@@ -53,7 +56,7 @@ public sealed class UsersController : ApiControllerBase
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ public static class DocumentTypes
|
||||
public const string Rfq = "RFQ";
|
||||
public const string PurchaseOrder = "PO";
|
||||
public const string Grn = "GRN";
|
||||
public const string PurchaseInvoice = "PINV";
|
||||
public const string Transfer = "TRF";
|
||||
public const string Adjustment = "ADJ";
|
||||
public const string Count = "CNT";
|
||||
@@ -20,5 +21,9 @@ public static class DocumentTypes
|
||||
public const string SalesInvoice = "SI";
|
||||
public const string SalesSlip = "SSL";
|
||||
public const string BundleSale = "BND";
|
||||
public const string BundleSaleTemplate = "TPL";
|
||||
public const string SalesReturn = "SRET";
|
||||
|
||||
/// <summary>Cashier day-end close-out (Sales) — <c>DEND-2026-00001</c>.</summary>
|
||||
public const string SalesDayEnd = "DEND";
|
||||
}
|
||||
|
||||
@@ -3,16 +3,19 @@ using ERPCore.Domain.Enums;
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Branch/location master (FR-HR-MD-01) — multi-branch readiness. Referenced
|
||||
/// optionally by <see cref="Employee.BranchId"/> and <see cref="PayrollRun.BranchId"/>.
|
||||
/// Branch/location master (FR-HR-MD-01) — multi-branch readiness. Required by
|
||||
/// <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.
|
||||
/// </summary>
|
||||
public class Branch
|
||||
{
|
||||
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 Name { get; set; } = string.Empty;
|
||||
public string? Address { get; set; }
|
||||
public string? PhoneNumber { get; set; }
|
||||
public EntityStatus Status { get; set; } = EntityStatus.Active;
|
||||
|
||||
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; }
|
||||
}
|
||||
@@ -29,5 +29,11 @@ public class BundleSale
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
public int ConcurrencyStamp { get; set; }
|
||||
|
||||
/// <summary>Set once this bundle's business date is closed by <see cref="SalesDayEnd"/> — a
|
||||
/// closed bundle sale is frozen (no further edits). Same role as <see cref="SalesSlip.DayEndId"/>;
|
||||
/// a bundle sale is a cashier document exactly like a sales slip, just priced as a set.</summary>
|
||||
public int? DayEndId { get; set; }
|
||||
public SalesDayEnd? DayEnd { get; set; }
|
||||
|
||||
public ICollection<BundleSaleLine> Lines { get; set; } = new List<BundleSaleLine>();
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ public class Employee
|
||||
public Designation? Designation { get; set; }
|
||||
public int EmploymentTypeId { get; set; }
|
||||
public EmploymentType? EmploymentType { get; set; }
|
||||
public int? BranchId { get; set; }
|
||||
public int BranchId { get; set; }
|
||||
public Branch? Branch { get; set; }
|
||||
public int WorkShiftId { get; set; }
|
||||
public WorkShift? WorkShift { get; set; }
|
||||
|
||||
@@ -30,6 +30,16 @@ public class Grn
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime? PostedAt { get; set; }
|
||||
|
||||
/// <summary>Journal number of the real GL journal entry posted for this GRN's receipt (set on confirm).</summary>
|
||||
public string? GlJournalNo { get; set; }
|
||||
public DateTime? GlPostedAt { get; set; }
|
||||
|
||||
/// <summary>The Purchase Invoice this GRN has been consolidated into, if any (null until
|
||||
/// invoiced). Vendor settlement is tracked on the invoice, not per-GRN — see
|
||||
/// <see cref="PurchaseInvoice"/>.</summary>
|
||||
public int? PurchaseInvoiceId { get; set; }
|
||||
public PurchaseInvoice? PurchaseInvoice { get; set; }
|
||||
|
||||
/// <summary>PostgreSQL xmin-backed optimistic concurrency token.</summary>
|
||||
public uint RowVersion { get; set; }
|
||||
|
||||
|
||||
@@ -58,4 +58,11 @@ public class GrnLine
|
||||
public decimal LineTotal { get; set; }
|
||||
|
||||
public HoldStatus HoldStatus { get; set; } = HoldStatus.Available;
|
||||
|
||||
/// <summary>
|
||||
/// One row per received unit when <see cref="Item.Warranty"/> is
|
||||
/// <see cref="Enums.Warranty.Warranty"/> — count must equal <see cref="Qty"/>.
|
||||
/// Empty for a non-warranty item.
|
||||
/// </summary>
|
||||
public ICollection<GrnLineWarrantyNumber> WarrantyNumbers { get; set; } = new List<GrnLineWarrantyNumber>();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// One warranty number captured against a single received unit of a warranty-tracked
|
||||
/// item (<see cref="Item.Warranty"/> = <see cref="Enums.Warranty.Warranty"/>). A GRN line
|
||||
/// for such an item must carry exactly <see cref="GrnLine.Qty"/> of these — one per unit —
|
||||
/// mirroring how a Serial-tracked item requires one serial per unit (docs/10 Part C.3).
|
||||
/// </summary>
|
||||
public class GrnLineWarrantyNumber
|
||||
{
|
||||
public int GrnLineWarrantyNumberId { get; set; }
|
||||
|
||||
public int GrnLineId { get; set; }
|
||||
public GrnLine? GrnLine { get; set; }
|
||||
|
||||
public string WarrantyNo { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Warranty coverage length in months, selected at receipt (e.g. 3/6/12/18).</summary>
|
||||
public int WarrantyPeriodMonths { get; set; }
|
||||
}
|
||||
@@ -38,14 +38,18 @@ public class Item
|
||||
|
||||
public StockNature StockNature { get; set; }
|
||||
public TrackingMode TrackingMode { get; set; }
|
||||
public Warranty Warranty { get; set; } = Warranty.NonWarranty;
|
||||
/// <summary>Coverage length in months (see <see cref="Enums.WarrantyPeriods.AllowedMonths"/>) when <see cref="Warranty"/> is Warranty; null otherwise.</summary>
|
||||
public int? WarrantyPeriodMonths { get; set; }
|
||||
public string? TaxClass { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional fixed selling price used by Sales only. <c>null</c> means "use stock value"
|
||||
/// (the item is sold at its FIFO stock cost at sale time); a value is the fixed sale price.
|
||||
/// Never enters costing/GRN/FIFO (docs/10 Part C.1, C.9).
|
||||
/// Optional selling price range and flat price used by Sales only.
|
||||
/// <c>null</c> values mean the field is not set. These values never enter costing/GRN/FIFO.
|
||||
/// </summary>
|
||||
public decimal? SalePrice { get; set; }
|
||||
public decimal? SellingMinPrice { get; set; }
|
||||
public decimal? SellingMaxPrice { get; set; }
|
||||
public decimal? SellingFlatPrice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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;
|
||||
|
||||
/// <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.
|
||||
/// <para>
|
||||
/// <see cref="SubcategoriesEnabled"/> and <see cref="BrandsEnabled"/> are enforced
|
||||
/// 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
|
||||
/// <b>advisory only</b> — items carry no item-type reference (see <see cref="ItemType"/>),
|
||||
/// so there is nothing on a write to reject; the frontend honours it by hiding the
|
||||
/// builder's type section. Reads are never gated, so existing data stays visible after a
|
||||
/// flag is switched off.
|
||||
/// rejected with <c>CONFIG_DISABLED</c>. <see cref="ItemTypesEnabled"/> is advisory only
|
||||
/// - items carry no item-type reference (see <see cref="ItemType"/>), so there is nothing
|
||||
/// on a write to reject; the frontend honours it by hiding the variant builder.
|
||||
/// <see cref="SellingPricesEnabled"/>, <see cref="SerialNumbersEnabled"/>,
|
||||
/// <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>
|
||||
/// Model: docs/10-BACKEND-PHASE1.md Part C.1.
|
||||
/// </summary>
|
||||
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 int ConfigId { get; set; }
|
||||
@@ -24,6 +26,10 @@ public class ProductConfig
|
||||
public bool SubcategoriesEnabled { get; set; } = true;
|
||||
public bool BrandsEnabled { 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; }
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// A vendor payment made against a Posted purchase invoice's balance (installments
|
||||
/// allowed — see PurchaseInvoicePaymentService.PayAsync). Posts its own real GL
|
||||
/// journal entry (Debit Accounts Payable / Credit the selected bank-or-cash account)
|
||||
/// before being recorded here.
|
||||
/// </summary>
|
||||
public class PurchaseInvoicePayment
|
||||
{
|
||||
public int PurchaseInvoicePaymentId { get; set; }
|
||||
|
||||
public int PurchaseInvoiceId { get; set; }
|
||||
public PurchaseInvoice? PurchaseInvoice { get; set; }
|
||||
|
||||
public decimal Amount { get; set; }
|
||||
public DateTime PaymentDate { get; set; }
|
||||
|
||||
/// <summary>GL's numeric id for the bank/cash account the payment was made from.</summary>
|
||||
public long GlBankAccountId { get; set; }
|
||||
/// <summary>Snapshot of the account's display name at payment time (GL account lists have no local FK).</summary>
|
||||
public string BankAccountName { get; set; } = string.Empty;
|
||||
|
||||
public string? Reference { get; set; }
|
||||
|
||||
/// <summary>Journal number of the real GL journal entry this payment posted.</summary>
|
||||
public string? GlJournalNo { get; set; }
|
||||
|
||||
public int CreatedBy { get; set; }
|
||||
public User? Creator { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
@@ -28,5 +28,12 @@ public class PurchaseReturn
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>Journal number of the real GL journal entry posted for this return (set on create — it auto-posts).
|
||||
/// Reverses the Inventory/Clearing lines a Grn posts — <see cref="PurchaseReturnLine"/> carries no unit
|
||||
/// price/VAT of its own, so it can't reverse a VAT-recoverable line the way a real credit note would.</summary>
|
||||
public string? GlJournalNo { get; set; }
|
||||
public DateTime? GlPostedAt { get; set; }
|
||||
|
||||
public ICollection<PurchaseReturnLine> Lines { get; set; } = new List<PurchaseReturnLine>();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// A cashier's end-of-day close-out. Aggregates every <see cref="SalesSlip"/> the
|
||||
/// cashier posted on <see cref="BusinessDate"/>, freezes them against this record
|
||||
/// (<see cref="SalesSlip.DayEndId"/>) so a slip can only ever belong to one day-end,
|
||||
/// and posts one consolidated GL journal entry for the day's revenue/tax/COGS —
|
||||
/// the sales-side equivalent of how <see cref="Grn"/> posts per receipt. Unique per
|
||||
/// (CashierUserId, BusinessDate): closing twice replays the existing record instead
|
||||
/// of creating a second one (same idempotent-replay pattern as Grn.ConfirmAsync).
|
||||
/// </summary>
|
||||
public class SalesDayEnd
|
||||
{
|
||||
public int SalesDayEndId { get; set; }
|
||||
public string DocNo { get; set; } = string.Empty;
|
||||
|
||||
public int CashierUserId { get; set; }
|
||||
public User? CashierUser { get; set; }
|
||||
|
||||
/// <summary>The calendar date being closed (matched against each slip's <see cref="SalesSlip.SlipDate"/>, UTC).</summary>
|
||||
public DateOnly BusinessDate { get; set; }
|
||||
|
||||
public int SlipCount { get; set; }
|
||||
public int BundleCount { get; set; }
|
||||
public decimal Subtotal { get; set; }
|
||||
public decimal DiscountTotal { get; set; }
|
||||
public decimal TaxTotal { get; set; }
|
||||
public decimal GrandTotal { get; set; }
|
||||
|
||||
/// <summary>Sum of the FIFO cost consumed for these slips (from <see cref="StockLedger"/>), for the COGS GL lines.</summary>
|
||||
public decimal CostOfGoodsSold { get; set; }
|
||||
|
||||
/// <summary>Journal number of the real GL journal entry posted for this close (null when there was nothing to post).</summary>
|
||||
public string? GlJournalNo { get; set; }
|
||||
public DateTime? GlPostedAt { get; set; }
|
||||
|
||||
public int ClosedBy { get; set; }
|
||||
public User? ClosedByUser { get; set; }
|
||||
public DateTime ClosedAt { get; set; }
|
||||
|
||||
/// <summary>PostgreSQL xmin-backed optimistic concurrency token.</summary>
|
||||
public uint RowVersion { get; set; }
|
||||
|
||||
public ICollection<SalesSlip> Slips { get; set; } = new List<SalesSlip>();
|
||||
public ICollection<BundleSale> BundleSales { get; set; } = new List<BundleSale>();
|
||||
}
|
||||
@@ -29,6 +29,14 @@ public class SalesInvoice
|
||||
public decimal PaidAmount { get; set; }
|
||||
public decimal BalanceAmount { get; set; }
|
||||
|
||||
/// <summary>Journal number of the revenue-recognition GL journal entry posted when this
|
||||
/// invoice is Posted (Debit Accounts Receivable / Credit Sales Revenue + Tax, plus COGS —
|
||||
/// see SalesPostingService.PostInvoiceAsync). Unlike a SalesSlip, an invoice posts its own
|
||||
/// GL entry immediately rather than waiting for Sales Day End, since invoices aren't a
|
||||
/// cashier document and don't go through that close-out.</summary>
|
||||
public string? GlJournalNo { get; set; }
|
||||
public DateTime? GlPostedAt { get; set; }
|
||||
|
||||
public int CreatedBy { get; set; }
|
||||
public User? Creator { get; set; }
|
||||
|
||||
@@ -38,4 +46,5 @@ public class SalesInvoice
|
||||
public uint RowVersion { get; set; }
|
||||
|
||||
public ICollection<SalesInvoiceLine> Lines { get; set; } = new List<SalesInvoiceLine>();
|
||||
public ICollection<SalesInvoicePayment> Payments { get; set; } = new List<SalesInvoicePayment>();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// A customer payment made against a Posted sales invoice's balance (installments
|
||||
/// allowed — see SalesInvoicePaymentService.PayAsync). Posts its own real GL journal
|
||||
/// entry (Debit the selected bank-or-cash account / Credit Accounts Receivable) before
|
||||
/// being recorded here — the mirror image of <see cref="PurchaseInvoicePayment"/>.
|
||||
/// </summary>
|
||||
public class SalesInvoicePayment
|
||||
{
|
||||
public int SalesInvoicePaymentId { get; set; }
|
||||
|
||||
public int SalesInvoiceId { get; set; }
|
||||
public SalesInvoice? SalesInvoice { get; set; }
|
||||
|
||||
public decimal Amount { get; set; }
|
||||
public DateTime PaymentDate { get; set; }
|
||||
|
||||
/// <summary>GL's numeric id for the bank/cash account the payment was received into.</summary>
|
||||
public long GlBankAccountId { get; set; }
|
||||
/// <summary>Snapshot of the account's display name at payment time (GL account lists have no local FK).</summary>
|
||||
public string BankAccountName { get; set; } = string.Empty;
|
||||
|
||||
public string? Reference { get; set; }
|
||||
|
||||
/// <summary>Journal number of the real GL journal entry this payment posted.</summary>
|
||||
public string? GlJournalNo { get; set; }
|
||||
|
||||
public int CreatedBy { get; set; }
|
||||
public User? Creator { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
@@ -28,5 +28,11 @@ public class SalesReturn
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>Journal number of the real GL journal entry posted for this return (set on create — it auto-posts).
|
||||
/// Reverses Inventory/COGS only — <see cref="SalesReturnLine"/> carries no unit price, so revenue/tax aren't
|
||||
/// reversed here (they'd need a per-line price this document doesn't capture).</summary>
|
||||
public string? GlJournalNo { get; set; }
|
||||
public DateTime? GlPostedAt { get; set; }
|
||||
|
||||
public ICollection<SalesReturnLine> Lines { get; set; } = new List<SalesReturnLine>();
|
||||
}
|
||||
|
||||
@@ -27,6 +27,10 @@ public class SalesSlip
|
||||
public decimal PaidAmount { get; set; }
|
||||
public decimal BalanceAmount { get; set; }
|
||||
|
||||
/// <summary>Set once this slip's business date is closed by <see cref="SalesDayEnd"/> — a closed slip is frozen (no further edits).</summary>
|
||||
public int? DayEndId { get; set; }
|
||||
public SalesDayEnd? DayEnd { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
|
||||
@@ -27,5 +27,10 @@ public class StockAdjustment
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public uint RowVersion { get; set; }
|
||||
|
||||
/// <summary>Journal number of the real GL journal entry posted for this adjustment (set on create — it
|
||||
/// auto-posts). Increases and decreases post separately (never netted) so gains/losses stay visible.</summary>
|
||||
public string? GlJournalNo { get; set; }
|
||||
public DateTime? GlPostedAt { get; set; }
|
||||
|
||||
public ICollection<StockAdjustmentLine> Lines { get; set; } = new List<StockAdjustmentLine>();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
@@ -10,4 +12,8 @@ public class Uom
|
||||
{
|
||||
public int UomId { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public EntityStatus Status { get; set; } = EntityStatus.Active;
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ public class Warehouse
|
||||
public int WarehouseId { get; set; }
|
||||
public string Code { 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>();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace ERPCore.Domain.Enums;
|
||||
|
||||
public enum PurchaseInvoiceStatus
|
||||
{
|
||||
Draft,
|
||||
Posted,
|
||||
Cancelled
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace ERPCore.Domain.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Whether an item is sold under warranty (FR-MD-01). Stored as a string, same
|
||||
/// convention as <see cref="StockNature"/> and <see cref="TrackingMode"/>.
|
||||
/// </summary>
|
||||
public enum Warranty
|
||||
{
|
||||
NonWarranty,
|
||||
Warranty
|
||||
}
|
||||
|
||||
/// <summary>Allowed warranty coverage lengths, in months — set once on the item (FR-MD-01).</summary>
|
||||
public static class WarrantyPeriods
|
||||
{
|
||||
public static readonly int[] AllowedMonths = { 3, 6, 12, 18 };
|
||||
}
|
||||
@@ -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>
|
||||
/// 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>
|
||||
public sealed record ProductConfigDto(
|
||||
bool SubcategoriesEnabled, bool BrandsEnabled, bool ItemTypesEnabled,
|
||||
bool SellingPricesEnabled, bool SerialNumbersEnabled, bool BatchListsEnabled, bool WarrantyPeriodsEnabled,
|
||||
DateTime? UpdatedAt, int? UpdatedBy);
|
||||
|
||||
/// <summary>
|
||||
@@ -15,8 +19,8 @@ public sealed record ProductConfigDto(
|
||||
/// <para>
|
||||
/// 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
|
||||
/// <c>false</c> and silently switch all three features off. Nullable makes the requirement
|
||||
/// actually bind — an omitted flag is a 400, not an accidental disable.
|
||||
/// <c>false</c> and silently switch everything off. Nullable makes the requirement actually
|
||||
/// bind - an omitted flag is a 400, not an accidental disable.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class UpdateProductConfigRequest
|
||||
@@ -24,4 +28,8 @@ public sealed class UpdateProductConfigRequest
|
||||
[Required] public bool? SubcategoriesEnabled { get; set; }
|
||||
[Required] public bool? BrandsEnabled { 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; }
|
||||
}
|
||||
|
||||
@@ -5,32 +5,41 @@ namespace ERPCore.Dtos.Grn;
|
||||
|
||||
// Responses (docs/11 §4) --------------------------------------------------------
|
||||
|
||||
public sealed record GrnLineWarrantyNumberDto(string WarrantyNo, int WarrantyPeriodMonths);
|
||||
|
||||
public sealed record GrnLineDto(
|
||||
int GrnLineId, int? PoLineId, int ItemId, int? BinId,
|
||||
decimal Qty, decimal UnitCost, decimal? PoUnitPrice,
|
||||
decimal DiscountPct, decimal NetUnitCost, decimal VatPct, decimal VatAmount,
|
||||
decimal ReceivedValue, decimal LineTotal, decimal PriceVariance,
|
||||
HoldStatus HoldStatus, int? BatchId);
|
||||
HoldStatus HoldStatus, int? BatchId, IReadOnlyList<GrnLineWarrantyNumberDto> WarrantyNumbers);
|
||||
|
||||
public sealed record GrnDto(
|
||||
int GrnId, string DocNo, int? PoId, int VendorId, int WarehouseId, GrnStatus Status,
|
||||
int CreatedBy, DateTime CreatedAt, DateTime? PostedAt, IReadOnlyList<GrnLineDto> Lines);
|
||||
int CreatedBy, DateTime CreatedAt, DateTime? PostedAt, string? GlJournalNo,
|
||||
int? PurchaseInvoiceId, string? PurchaseInvoiceNo, IReadOnlyList<GrnLineDto> Lines);
|
||||
|
||||
/// <summary>Row shape for <c>GET /grns</c> — line count instead of the lines themselves.</summary>
|
||||
public sealed record GrnSummaryDto(
|
||||
int GrnId, string DocNo, int? PoId, int VendorId, int WarehouseId, GrnStatus Status,
|
||||
int CreatedBy, DateTime CreatedAt, DateTime? PostedAt, int LineCount);
|
||||
int CreatedBy, DateTime CreatedAt, DateTime? PostedAt, int LineCount,
|
||||
int? PurchaseInvoiceId, string? PurchaseInvoiceNo);
|
||||
|
||||
public sealed record CreatedLayerDto(
|
||||
int LayerId, int ItemId, int WarehouseId, int? BatchId,
|
||||
decimal QtyReceived, decimal QtyRemaining, decimal UnitCost, DateTime ReceiptDate);
|
||||
|
||||
public sealed record GrnConfirmResultDto(
|
||||
int GrnId, GrnStatus Status, DateTime PostedAt,
|
||||
int GrnId, GrnStatus Status, DateTime PostedAt, string GlJournalNo,
|
||||
IReadOnlyList<CreatedLayerDto> CreatedLayers, IReadOnlyList<int> LedgerRefs, PurchaseOrderStatus? PoStatus);
|
||||
|
||||
public sealed record ReleaseLineResultDto(int GrnLineId, HoldStatus HoldStatus);
|
||||
|
||||
/// <summary>A Confirmed, not-yet-invoiced GRN eligible to be consolidated onto a Purchase Invoice.</summary>
|
||||
public sealed record GrnEligibleForInvoiceDto(
|
||||
int GrnId, string DocNo, DateTime PostedAt, int WarehouseId,
|
||||
int LineCount, decimal Subtotal, decimal VatTotal, decimal GrandTotal);
|
||||
|
||||
// Requests ----------------------------------------------------------------------
|
||||
|
||||
public sealed class BatchInput
|
||||
@@ -58,6 +67,11 @@ public sealed class CreateGrnLineInput
|
||||
[Range(0, 100)] public decimal VatPct { get; set; }
|
||||
[EnumDataType(typeof(HoldStatus))] public HoldStatus HoldStatus { get; set; } = HoldStatus.Available;
|
||||
public BatchInput? Batch { get; set; }
|
||||
/// <summary>
|
||||
/// Required, one per unit (count must equal <see cref="Qty"/>), when the item is
|
||||
/// warranty-tracked (<c>Item.Warranty == Warranty.Warranty</c>). Ignored otherwise.
|
||||
/// </summary>
|
||||
public List<string>? WarrantyNumbers { get; set; }
|
||||
}
|
||||
|
||||
public sealed class CreateGrnRequest
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace ERPCore.Dtos.Hrm;
|
||||
public sealed record EmployeeListItemDto(
|
||||
int EmployeeId, string EmployeeCode, string FullName, string? Email,
|
||||
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);
|
||||
|
||||
public sealed record EmployeeDetailDto(
|
||||
@@ -16,7 +16,7 @@ public sealed record EmployeeDetailDto(
|
||||
string? City, string? PostalCode, string? Country,
|
||||
string? EmergencyContactName, string? EmergencyContactRelationship, string? EmergencyContactPhone,
|
||||
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? UserId, EmployeeStatus Status, DateTime CreatedAt, DateTime? UpdatedAt);
|
||||
|
||||
@@ -45,7 +45,7 @@ public sealed class CreateEmployeeRequest
|
||||
[Required] public int DepartmentId { get; set; }
|
||||
[Required] public int DesignationId { 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; }
|
||||
public int? ReportingManagerId { get; set; }
|
||||
|
||||
@@ -82,7 +82,7 @@ public sealed class UpdateEmployeeRequest
|
||||
[Required] public int DepartmentId { get; set; }
|
||||
[Required] public int DesignationId { 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; }
|
||||
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).
|
||||
|
||||
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);
|
||||
|
||||
// Code is server-generated (BranchService.GenerateCodeAsync) — never accepted from the client.
|
||||
public sealed class CreateBranchRequest
|
||||
{
|
||||
[Required, StringLength(20)] public string Code { get; set; } = string.Empty;
|
||||
[Required, StringLength(200)] public string Name { get; set; } = string.Empty;
|
||||
[StringLength(500)] public string? Address { get; set; }
|
||||
[StringLength(30)] public string? PhoneNumber { get; set; }
|
||||
}
|
||||
|
||||
public sealed class UpdateBranchRequest
|
||||
{
|
||||
[Required, StringLength(200)] public string Name { get; set; } = string.Empty;
|
||||
[StringLength(500)] public string? Address { get; set; }
|
||||
[StringLength(30)] public string? PhoneNumber { get; set; }
|
||||
}
|
||||
|
||||
public sealed record DepartmentDto(
|
||||
|
||||
@@ -9,7 +9,8 @@ namespace ERPCore.Dtos.Items;
|
||||
public sealed record ItemListItemDto(
|
||||
int ItemId, string Sku, string Name, int CategoryId, int? SubCategoryId, int? BrandId,
|
||||
int BaseUomId, int? DefaultVendorId, StockNature StockNature, TrackingMode TrackingMode,
|
||||
string? TaxClass, decimal? SalePrice,
|
||||
Warranty Warranty, int? WarrantyPeriodMonths,
|
||||
string? TaxClass, decimal? SellingMinPrice, decimal? SellingMaxPrice, decimal? SellingFlatPrice,
|
||||
decimal? ContentQty, MeasureUnit? ContentUnit,
|
||||
decimal? ContentBaseQty, MeasureUnit? ContentBaseUnit,
|
||||
EntityStatus Status);
|
||||
@@ -29,7 +30,8 @@ public sealed record ItemDetailDto(
|
||||
int ItemId, string Sku, string Name, string? Description, int CategoryId,
|
||||
int? SubCategoryId, int? BrandId, int BaseUomId, int? DefaultVendorId,
|
||||
StockNature StockNature, TrackingMode TrackingMode,
|
||||
string? TaxClass, decimal? SalePrice,
|
||||
Warranty Warranty, int? WarrantyPeriodMonths,
|
||||
string? TaxClass, decimal? SellingMinPrice, decimal? SellingMaxPrice, decimal? SellingFlatPrice,
|
||||
decimal? ContentQty, MeasureUnit? ContentUnit,
|
||||
decimal? ContentBaseQty, MeasureUnit? ContentBaseUnit,
|
||||
EntityStatus Status, IReadOnlyList<ItemReorderDto> Reorder,
|
||||
@@ -59,9 +61,14 @@ public sealed class CreateItemRequest
|
||||
public int? DefaultVendorId { get; set; }
|
||||
[Required, EnumDataType(typeof(StockNature))] public StockNature StockNature { get; set; }
|
||||
[EnumDataType(typeof(TrackingMode))] public TrackingMode TrackingMode { get; set; } = TrackingMode.None;
|
||||
[EnumDataType(typeof(Warranty))] public Warranty Warranty { get; set; } = Warranty.NonWarranty;
|
||||
/// <summary>Required (one of <see cref="Enums.WarrantyPeriods.AllowedMonths"/>) when <see cref="Warranty"/> is Warranty; ignored otherwise.</summary>
|
||||
public int? WarrantyPeriodMonths { get; set; }
|
||||
[StringLength(20)] public string? TaxClass { get; set; }
|
||||
/// <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>
|
||||
/// How much one pack holds. Supply with <see cref="ContentUnit"/> or leave both null
|
||||
@@ -86,9 +93,14 @@ public sealed class UpdateItemRequest
|
||||
public int? DefaultVendorId { get; set; }
|
||||
[Required, EnumDataType(typeof(StockNature))] public StockNature StockNature { get; set; }
|
||||
[EnumDataType(typeof(TrackingMode))] public TrackingMode TrackingMode { get; set; } = TrackingMode.None;
|
||||
[EnumDataType(typeof(Warranty))] public Warranty Warranty { get; set; } = Warranty.NonWarranty;
|
||||
/// <summary>Required (one of <see cref="Enums.WarrantyPeriods.AllowedMonths"/>) when <see cref="Warranty"/> is Warranty; ignored otherwise.</summary>
|
||||
public int? WarrantyPeriodMonths { get; set; }
|
||||
[StringLength(20)] public string? TaxClass { get; set; }
|
||||
/// <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>
|
||||
/// 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();
|
||||
}
|
||||
@@ -9,12 +9,13 @@ public sealed record PurchaseReturnLineDto(int ReturnLineId, int? GrnLineId, int
|
||||
|
||||
public sealed record PurchaseReturnDto(
|
||||
int ReturnId, string DocNo, int VendorId, int WarehouseId, int ReasonCodeId, ReturnStatus Status,
|
||||
int CreatedBy, DateTime CreatedAt, IReadOnlyList<PurchaseReturnLineDto> Lines, IReadOnlyList<int> LedgerRefs);
|
||||
int CreatedBy, DateTime CreatedAt, string? GlJournalNo, DateTime? GlPostedAt,
|
||||
IReadOnlyList<PurchaseReturnLineDto> Lines, IReadOnlyList<int> LedgerRefs);
|
||||
|
||||
/// <summary>Row shape for <c>GET /purchase-returns</c> — no lines/ledgerRefs (those need a per-row query).</summary>
|
||||
public sealed record PurchaseReturnSummaryDto(
|
||||
int ReturnId, string DocNo, int VendorId, int WarehouseId, int ReasonCodeId, ReturnStatus Status,
|
||||
int CreatedBy, DateTime CreatedAt, int LineCount);
|
||||
int CreatedBy, DateTime CreatedAt, int LineCount, string? GlJournalNo);
|
||||
|
||||
// Requests ----------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -70,6 +70,35 @@ public sealed record RunEventDto(
|
||||
int EventId, int? RunStageId, RunStageEventType EventType, string? Note,
|
||||
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>
|
||||
public sealed record RunGraphDto(
|
||||
int RunId, string DocNo, int TemplateId, string TemplateName,
|
||||
@@ -99,8 +128,10 @@ public sealed class CreateRunRequest
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Per-run scaling override (FR-MFG-08). Only accepted while the stage has not started
|
||||
/// (<c>409 STAGE_NOT_EDITABLE</c>).
|
||||
/// Per-run scaling override (FR-MFG-08). Accepted before the stage starts, and — for a Stock
|
||||
/// 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>
|
||||
public sealed class UpdateStageQuantitiesRequest
|
||||
{
|
||||
@@ -116,4 +147,11 @@ public sealed class StageQuantityLine
|
||||
|
||||
[Range(0.0001, double.MaxValue)]
|
||||
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
|
||||
/// for the current user's role and permitted sidebar nav codes (replaces trusting
|
||||
/// the stale, client-only `roleId` cached in localStorage).</summary>
|
||||
public sealed record MeResponseDto(string? RoleCode, string? RoleName, IReadOnlyList<string> NavCodes);
|
||||
/// the stale, client-only `roleId` cached in localStorage). <see cref="BranchId"/>/
|
||||
/// <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
|
||||
{
|
||||
[Required] public string TemplateCode { get; set; } = string.Empty;
|
||||
[Required] public string TemplateName { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
[Required, MinLength(1)] public List<CreateBundleSaleTemplateLineRequest> Lines { get; set; } = new();
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace ERPCore.Dtos.Sales;
|
||||
|
||||
// Responses -----------------------------------------------------------------------
|
||||
|
||||
/// <summary>Qty/revenue sold for one item across a closed (or about-to-close) day, for the day-end report.
|
||||
/// 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);
|
||||
|
||||
/// <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(
|
||||
int SalesDayEndId, string DocNo, int CashierUserId, DateOnly BusinessDate,
|
||||
int SlipCount, int BundleCount, decimal Subtotal, decimal DiscountTotal, decimal TaxTotal, decimal GrandTotal,
|
||||
decimal CostOfGoodsSold, string? GlJournalNo, DateTime? GlPostedAt, int ClosedBy, DateTime ClosedAt,
|
||||
IReadOnlyList<string> SlipNumbers, IReadOnlyList<string> BundleNumbers, IReadOnlyList<SalesDayEndItemLineDto> ItemBreakdown,
|
||||
SalesDayEndInvoiceSummaryDto Invoices);
|
||||
|
||||
public sealed record SalesDayEndSummaryDto(
|
||||
int SalesDayEndId, string DocNo, int CashierUserId, DateOnly BusinessDate,
|
||||
int SlipCount, int BundleCount, decimal GrandTotal, string? GlJournalNo, DateTime ClosedAt);
|
||||
|
||||
/// <summary>What closing right now would include — call before <c>POST /sales-day-end</c> so the
|
||||
/// cashier can see the day's totals, and any still-Draft slips/bundles blocking the close.</summary>
|
||||
public sealed record SalesDayEndPreviewDto(
|
||||
int CashierUserId, DateOnly BusinessDate, bool AlreadyClosed,
|
||||
int SlipCount, int BundleCount, decimal Subtotal, decimal DiscountTotal, decimal TaxTotal, decimal GrandTotal,
|
||||
IReadOnlyList<string> SlipNumbers, IReadOnlyList<string> BundleNumbers,
|
||||
IReadOnlyList<DraftSlipBlockingCloseDto> DraftSlipsBlockingClose,
|
||||
IReadOnlyList<DraftBundleBlockingCloseDto> DraftBundleSalesBlockingClose,
|
||||
SalesDayEndInvoiceSummaryDto Invoices);
|
||||
|
||||
public sealed record DraftSlipBlockingCloseDto(int SalesSlipId, string SlipNo, decimal GrandTotal);
|
||||
public sealed record DraftBundleBlockingCloseDto(int BundleSaleId, string BundleNo, decimal GrandTotal);
|
||||
|
||||
// Requests ------------------------------------------------------------------------
|
||||
|
||||
public sealed class CreateSalesDayEndRequest
|
||||
{
|
||||
[Required] public int CashierUserId { get; set; }
|
||||
/// <summary>Defaults to today (UTC) when omitted.</summary>
|
||||
public DateOnly? BusinessDate { get; set; }
|
||||
}
|
||||
@@ -17,12 +17,24 @@ public sealed record SalesInvoiceDto(
|
||||
int SalesInvoiceId, string InvoiceNo, DateTime InvoiceDate, int CustomerId,
|
||||
string CustomerSnapshotName, string? CustomerSnapshotTaxNo, int WarehouseId,
|
||||
SalesInvoiceType InvoiceType, SalesInvoiceStatus Status, int CreatedBy, DateTime CreatedAt,
|
||||
DateTime? UpdatedAt, SalesInvoiceTotalsDto Totals, IReadOnlyList<SalesInvoiceLineDto> Lines);
|
||||
DateTime? UpdatedAt, string? GlJournalNo, DateTime? GlPostedAt,
|
||||
SalesInvoiceTotalsDto Totals, IReadOnlyList<SalesInvoiceLineDto> Lines);
|
||||
|
||||
public sealed record SalesInvoiceSummaryDto(
|
||||
int SalesInvoiceId, string InvoiceNo, DateTime InvoiceDate, int CustomerId,
|
||||
string CustomerSnapshotName, int WarehouseId, SalesInvoiceType InvoiceType,
|
||||
SalesInvoiceStatus Status, SalesInvoiceTotalsDto Totals, DateTime CreatedAt);
|
||||
SalesInvoiceStatus Status, SalesInvoiceTotalsDto Totals, DateTime CreatedAt, string? GlJournalNo);
|
||||
|
||||
public sealed record SalesInvoicePaymentDto(
|
||||
int SalesInvoicePaymentId, int SalesInvoiceId, decimal Amount, DateTime PaymentDate,
|
||||
long GlBankAccountId, string BankAccountName, string? Reference, string? GlJournalNo, DateTime CreatedAt);
|
||||
|
||||
public sealed class CreateSalesInvoicePaymentRequest
|
||||
{
|
||||
[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 record SalesInvoicePostingIssueDto(
|
||||
int SalesInvoiceLineId, int ItemId, string ItemSku, string ItemName, int WarehouseId,
|
||||
|
||||
@@ -4,7 +4,7 @@ public sealed record SalesFreeIssueRewardOptionDto(
|
||||
int ItemId,
|
||||
string Sku,
|
||||
string Name,
|
||||
decimal? SalePrice);
|
||||
decimal? SellingFlatPrice);
|
||||
|
||||
public sealed record SalesFreeIssueSuggestionLineDto(
|
||||
int SalesSlipLineId,
|
||||
@@ -20,4 +20,4 @@ public sealed record SalesFreeIssueSuggestionDto(
|
||||
int SalesSlipId,
|
||||
string SlipNo,
|
||||
DateTime SlipDate,
|
||||
IReadOnlyList<SalesFreeIssueSuggestionLineDto> Lines);
|
||||
IReadOnlyList<SalesFreeIssueSuggestionLineDto> Lines);
|
||||
|
||||
@@ -9,12 +9,13 @@ public sealed record SalesReturnLineDto(int ReturnLineId, int? SalesInvoiceLineI
|
||||
|
||||
public sealed record SalesReturnDto(
|
||||
int ReturnId, string DocNo, int CustomerId, int WarehouseId, int ReasonCodeId, ReturnStatus Status,
|
||||
int CreatedBy, DateTime CreatedAt, IReadOnlyList<SalesReturnLineDto> Lines, IReadOnlyList<int> LedgerRefs);
|
||||
int CreatedBy, DateTime CreatedAt, string? GlJournalNo, DateTime? GlPostedAt,
|
||||
IReadOnlyList<SalesReturnLineDto> Lines, IReadOnlyList<int> LedgerRefs);
|
||||
|
||||
/// <summary>Row shape for <c>GET /sales-returns</c> — no lines/ledgerRefs (those need a per-row query).</summary>
|
||||
public sealed record SalesReturnSummaryDto(
|
||||
int ReturnId, string DocNo, int CustomerId, int WarehouseId, int ReasonCodeId, ReturnStatus Status,
|
||||
int CreatedBy, DateTime CreatedAt, int LineCount, decimal TotalQty);
|
||||
int CreatedBy, DateTime CreatedAt, int LineCount, decimal TotalQty, string? GlJournalNo);
|
||||
|
||||
/// <summary>
|
||||
/// Remaining returnable qty for one sales invoice line — the invoiced qty minus
|
||||
|
||||
@@ -9,12 +9,13 @@ public sealed record AdjustmentLineDto(int AdjLineId, int ItemId, int? BinId, in
|
||||
|
||||
public sealed record AdjustmentDto(
|
||||
int AdjustmentId, string DocNo, int WarehouseId, int ReasonCodeId, AdjustmentStatus Status,
|
||||
int CreatedBy, DateTime CreatedAt, IReadOnlyList<AdjustmentLineDto> Lines, IReadOnlyList<int> LedgerRefs);
|
||||
int CreatedBy, DateTime CreatedAt, string? GlJournalNo, DateTime? GlPostedAt,
|
||||
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>
|
||||
public sealed record AdjustmentSummaryDto(
|
||||
int AdjustmentId, string DocNo, int WarehouseId, int ReasonCodeId, AdjustmentStatus Status,
|
||||
int CreatedBy, DateTime CreatedAt, int LineCount);
|
||||
int CreatedBy, DateTime CreatedAt, int LineCount, string? GlJournalNo);
|
||||
|
||||
// Requests ----------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ public sealed record CountSummaryDto(
|
||||
int CountId, string DocNo, int WarehouseId, CountType CountType, CountStatus Status,
|
||||
int CreatedBy, DateTime CreatedAt, int LineCount);
|
||||
|
||||
public sealed record CountPostResultDto(int CountId, CountStatus Status, int? AdjustmentId, IReadOnlyList<int> LedgerRefs);
|
||||
public sealed record CountPostResultDto(int CountId, CountStatus Status, int? AdjustmentId, string? GlJournalNo, IReadOnlyList<int> LedgerRefs);
|
||||
|
||||
// Requests ----------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Dtos.Uoms;
|
||||
|
||||
/// <summary>UOM resource (docs/11-BACKEND-PHASE1.md §2.2).</summary>
|
||||
public sealed record UomDto(int UomId, string Name);
|
||||
public sealed record UomDto(int UomId, string Name, EntityStatus Status, DateTime CreatedAt, DateTime? UpdatedAt);
|
||||
|
||||
public sealed class CreateUomRequest
|
||||
{
|
||||
[Required, StringLength(50)] public string Name { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class UpdateUomStatusRequest
|
||||
{
|
||||
[Required, EnumDataType(typeof(EntityStatus))] public EntityStatus Status { get; set; }
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
/// </summary>
|
||||
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
|
||||
|
||||
@@ -2,8 +2,10 @@ using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace ERPCore.Dtos.Warehouses;
|
||||
|
||||
/// <summary>Warehouse resource (docs/11-BACKEND-PHASE1.md §2.5).</summary>
|
||||
public sealed record WarehouseDto(int WarehouseId, string Code, string Name);
|
||||
/// <summary>Warehouse resource (docs/11-BACKEND-PHASE1.md §2.5). <see cref="BranchId"/> is
|
||||
/// 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>
|
||||
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(200)] public string Name { get; set; } = string.Empty;
|
||||
[Required] public int BranchId { get; set; }
|
||||
}
|
||||
|
||||
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 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>
|
||||
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.Property(b => b.Name).IsRequired().HasMaxLength(200);
|
||||
builder.Property(b => b.Address).HasMaxLength(500);
|
||||
builder.Property(b => b.PhoneNumber).HasMaxLength(30);
|
||||
|
||||
builder.Property(b => b.Status)
|
||||
.HasConversion<string>().HasMaxLength(20).IsRequired()
|
||||
|
||||
@@ -16,12 +16,14 @@ public sealed class GrnConfiguration : IEntityTypeConfiguration<Grn>
|
||||
|
||||
builder.Property(g => g.Status).HasConversion<string>().HasMaxLength(20).IsRequired();
|
||||
builder.Property(g => g.CreatedAt).IsRequired();
|
||||
builder.Property(g => g.GlJournalNo).HasMaxLength(30);
|
||||
builder.Property(g => g.RowVersion).IsRowVersion();
|
||||
|
||||
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.Warehouse).WithMany().HasForeignKey(g => g.WarehouseId).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.PoId);
|
||||
@@ -53,3 +55,21 @@ public sealed class GrnLineConfiguration : IEntityTypeConfiguration<GrnLine>
|
||||
builder.HasOne(l => l.Batch).WithMany().HasForeignKey(l => l.BatchId).OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class GrnLineWarrantyNumberConfiguration : IEntityTypeConfiguration<GrnLineWarrantyNumber>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<GrnLineWarrantyNumber> builder)
|
||||
{
|
||||
builder.ToTable("grn_line_warranty_numbers");
|
||||
builder.HasKey(w => w.GrnLineWarrantyNumberId);
|
||||
|
||||
builder.Property(w => w.WarrantyNo).IsRequired().HasMaxLength(100);
|
||||
|
||||
builder.HasOne(w => w.GrnLine).WithMany(l => l.WarrantyNumbers)
|
||||
.HasForeignKey(w => w.GrnLineId).OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
// A warranty number entered twice on the same line is almost certainly a typo —
|
||||
// catch it at the DB, not just client-side.
|
||||
builder.HasIndex(w => new { w.GrnLineId, w.WarrantyNo }).IsUnique();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,9 @@ public sealed class ItemConfiguration : IEntityTypeConfiguration<Item>
|
||||
builder.Property(i => i.TaxClass).HasMaxLength(20);
|
||||
|
||||
// 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
|
||||
// together: null ⇒ the item has no measurable content. The base pair is derived
|
||||
@@ -36,6 +38,9 @@ public sealed class ItemConfiguration : IEntityTypeConfiguration<Item>
|
||||
.HasConversion<string>().HasMaxLength(20).IsRequired();
|
||||
builder.Property(i => i.TrackingMode)
|
||||
.HasConversion<string>().HasMaxLength(20).IsRequired();
|
||||
builder.Property(i => i.Warranty)
|
||||
.HasConversion<string>().HasMaxLength(20).IsRequired()
|
||||
.HasDefaultValue(Warranty.NonWarranty);
|
||||
builder.Property(i => i.Status)
|
||||
.HasConversion<string>().HasMaxLength(20).IsRequired()
|
||||
.HasDefaultValue(EntityStatus.Active);
|
||||
|
||||
@@ -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 = 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 = 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 = 32, Code = "NAV:accounts", NavItemId = 12 },
|
||||
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.BrandsEnabled).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).
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ public sealed class PurchaseReturnConfiguration : IEntityTypeConfiguration<Purch
|
||||
|
||||
builder.Property(r => r.Status).HasConversion<string>().HasMaxLength(20).IsRequired();
|
||||
builder.Property(r => r.CreatedAt).IsRequired();
|
||||
builder.Property(r => r.GlJournalNo).HasMaxLength(30);
|
||||
|
||||
builder.HasOne(r => r.Vendor).WithMany().HasForeignKey(r => r.VendorId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne(r => r.Warehouse).WithMany().HasForeignKey(r => r.WarehouseId).OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
using ERPCore.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace ERPCore.Infra.Persistence.Configurations;
|
||||
|
||||
public sealed class SalesDayEndConfiguration : IEntityTypeConfiguration<SalesDayEnd>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<SalesDayEnd> builder)
|
||||
{
|
||||
builder.ToTable("sales_day_ends");
|
||||
builder.HasKey(x => x.SalesDayEndId);
|
||||
|
||||
builder.Property(x => x.DocNo).IsRequired().HasMaxLength(30);
|
||||
builder.HasIndex(x => x.DocNo).IsUnique();
|
||||
|
||||
builder.Property(x => x.BusinessDate).IsRequired();
|
||||
|
||||
foreach (var p in new[] { nameof(SalesDayEnd.Subtotal), nameof(SalesDayEnd.DiscountTotal), nameof(SalesDayEnd.TaxTotal), nameof(SalesDayEnd.GrandTotal), nameof(SalesDayEnd.CostOfGoodsSold) })
|
||||
builder.Property<decimal>(p).HasPrecision(18, 4);
|
||||
builder.Property(x => x.BundleCount).HasDefaultValue(0);
|
||||
|
||||
builder.Property(x => x.GlJournalNo).HasMaxLength(30);
|
||||
builder.Property(x => x.ClosedAt).IsRequired();
|
||||
builder.Property(x => x.RowVersion).IsRowVersion();
|
||||
|
||||
builder.HasOne(x => x.CashierUser).WithMany().HasForeignKey(x => x.CashierUserId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne(x => x.ClosedByUser).WithMany().HasForeignKey(x => x.ClosedBy).OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
// A cashier can only close a given business date once (CloseAsync replays this
|
||||
// row idempotently on a second call instead of erroring — see SalesDayEndService).
|
||||
builder.HasIndex(x => new { x.CashierUserId, x.BusinessDate }).IsUnique();
|
||||
|
||||
builder.HasMany(x => x.Slips)
|
||||
.WithOne(x => x.DayEnd)
|
||||
.HasForeignKey(x => x.DayEndId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasMany(x => x.BundleSales)
|
||||
.WithOne(x => x.DayEnd)
|
||||
.HasForeignKey(x => x.DayEndId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,7 @@ public sealed class SalesInvoiceConfiguration : IEntityTypeConfiguration<SalesIn
|
||||
|
||||
foreach (var p in new[] { nameof(SalesInvoice.Subtotal), nameof(SalesInvoice.DiscountTotal), nameof(SalesInvoice.TaxTotal), nameof(SalesInvoice.GrandTotal), nameof(SalesInvoice.RoundOff), nameof(SalesInvoice.NetPayable), nameof(SalesInvoice.PaidAmount), nameof(SalesInvoice.BalanceAmount) })
|
||||
builder.Property<decimal>(p).HasPrecision(18, 4);
|
||||
builder.Property(x => x.GlJournalNo).HasMaxLength(30);
|
||||
|
||||
builder.Property(x => x.CreatedAt).IsRequired();
|
||||
builder.Property(x => x.RowVersion).IsRowVersion();
|
||||
@@ -54,6 +55,27 @@ public sealed class SalesInvoiceConfiguration : IEntityTypeConfiguration<SalesIn
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class SalesInvoicePaymentConfiguration : IEntityTypeConfiguration<SalesInvoicePayment>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<SalesInvoicePayment> builder)
|
||||
{
|
||||
builder.ToTable("sales_invoice_payments");
|
||||
builder.HasKey(p => p.SalesInvoicePaymentId);
|
||||
|
||||
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.SalesInvoice).WithMany(x => x.Payments).HasForeignKey(p => p.SalesInvoiceId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne(p => p.Creator).WithMany().HasForeignKey(p => p.CreatedBy).OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasIndex(p => p.SalesInvoiceId);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class SalesInvoiceLineConfiguration : IEntityTypeConfiguration<SalesInvoiceLine>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<SalesInvoiceLine> builder)
|
||||
|
||||
@@ -16,6 +16,7 @@ public sealed class SalesReturnConfiguration : IEntityTypeConfiguration<SalesRet
|
||||
|
||||
builder.Property(r => r.Status).HasConversion<string>().HasMaxLength(20).IsRequired();
|
||||
builder.Property(r => r.CreatedAt).IsRequired();
|
||||
builder.Property(r => r.GlJournalNo).HasMaxLength(30);
|
||||
|
||||
builder.HasOne(r => r.Customer).WithMany().HasForeignKey(r => r.CustomerId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne(r => r.Warehouse).WithMany().HasForeignKey(r => r.WarehouseId).OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
@@ -16,6 +16,7 @@ public sealed class StockAdjustmentConfiguration : IEntityTypeConfiguration<Stoc
|
||||
|
||||
builder.Property(a => a.Status).HasConversion<string>().HasMaxLength(20).IsRequired();
|
||||
builder.Property(a => a.CreatedAt).IsRequired();
|
||||
builder.Property(a => a.GlJournalNo).HasMaxLength(30);
|
||||
builder.Property(a => a.RowVersion).IsRowVersion();
|
||||
|
||||
builder.HasOne(a => a.Warehouse).WithMany().HasForeignKey(a => a.WarehouseId).OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
@@ -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 = 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 = 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 = 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 },
|
||||
// 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;
|
||||
@@ -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 = 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 = 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 = 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 },
|
||||
@@ -53,7 +56,15 @@ public sealed class SubNavItemConfiguration : IEntityTypeConfiguration<SubNavIte
|
||||
// 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 = 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using ERPCore.Domain.Entities;
|
||||
using ERPCore.Domain.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
@@ -13,5 +14,13 @@ public sealed class UomConfiguration : IEntityTypeConfiguration<Uom>
|
||||
|
||||
builder.Property(u => u.Name).IsRequired().HasMaxLength(50);
|
||||
builder.HasIndex(u => u.Name).IsUnique();
|
||||
|
||||
builder.Property(u => u.Status)
|
||||
.HasConversion<string>().HasMaxLength(20).IsRequired()
|
||||
.HasDefaultValue(EntityStatus.Active);
|
||||
|
||||
builder.Property(u => u.CreatedAt).IsRequired();
|
||||
|
||||
builder.HasIndex(u => u.Status);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,5 +15,9 @@ public sealed class WarehouseConfiguration : IEntityTypeConfiguration<Warehouse>
|
||||
builder.HasIndex(w => w.Code).IsUnique();
|
||||
|
||||
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);
|
||||
dirty |= await SeedItemTypesAsync(db, ct);
|
||||
// dirty |= await SeedCompanyProfileAsync(db, ct);
|
||||
// dirty |= await SeedCompanyProfileAsync(db, ct);
|
||||
dirty |= await SeedProductConfigAsync(db, ct);
|
||||
dirty |= await SeedBranchConfigAsync(db, ct);
|
||||
dirty |= await SeedNotificationConfigAsync(db, ct);
|
||||
dirty |= await SeedSalesMastersAsync(db, ct);
|
||||
dirty |= await SeedSalesStockAsync(db, ct);
|
||||
dirty |= await SeedSalesAsync(db, ct);
|
||||
@@ -100,7 +102,39 @@ public static class DataSeeder
|
||||
ConfigId = ProductConfig.SingletonId,
|
||||
SubcategoriesEnabled = 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;
|
||||
}
|
||||
@@ -182,7 +216,7 @@ public static class DataSeeder
|
||||
WarehouseId = warehouse.WarehouseId,
|
||||
QtyReceived = 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)
|
||||
});
|
||||
db.StockLayers.Add(new StockLayer
|
||||
@@ -191,7 +225,7 @@ public static class DataSeeder
|
||||
WarehouseId = warehouse.WarehouseId,
|
||||
QtyReceived = 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)
|
||||
});
|
||||
return true;
|
||||
@@ -278,7 +312,7 @@ public static class DataSeeder
|
||||
BaseUomId = uom.UomId,
|
||||
StockNature = StockNature.Stocked,
|
||||
TrackingMode = TrackingMode.None,
|
||||
SalePrice = 100m,
|
||||
SellingFlatPrice = 100m,
|
||||
Status = EntityStatus.Active,
|
||||
CreatedAt = now
|
||||
},
|
||||
@@ -292,7 +326,7 @@ public static class DataSeeder
|
||||
BaseUomId = uom.UomId,
|
||||
StockNature = StockNature.Stocked,
|
||||
TrackingMode = TrackingMode.None,
|
||||
SalePrice = 50m,
|
||||
SellingFlatPrice = 50m,
|
||||
ContentQty = 500m,
|
||||
ContentUnit = MeasureUnit.Ml,
|
||||
ContentBaseQty = 500m,
|
||||
@@ -445,7 +479,7 @@ public static class DataSeeder
|
||||
ItemId = items[0].ItemId,
|
||||
WarehouseId = warehouse.WarehouseId,
|
||||
Qty = 1m,
|
||||
UnitPrice = items[0].SalePrice.GetValueOrDefault(),
|
||||
UnitPrice = items[0].SellingFlatPrice.GetValueOrDefault(),
|
||||
IncludeInBundle = true,
|
||||
SortOrder = 1
|
||||
},
|
||||
@@ -454,7 +488,7 @@ public static class DataSeeder
|
||||
ItemId = items[1].ItemId,
|
||||
WarehouseId = warehouse.WarehouseId,
|
||||
Qty = 1m,
|
||||
UnitPrice = items[1].SalePrice.GetValueOrDefault(),
|
||||
UnitPrice = items[1].SellingFlatPrice.GetValueOrDefault(),
|
||||
IncludeInBundle = true,
|
||||
SortOrder = 2
|
||||
}
|
||||
@@ -478,10 +512,10 @@ public static class DataSeeder
|
||||
BundleName = "Demo Bundle Draft",
|
||||
BundleCode = "BND-DEMO-001",
|
||||
Status = BundleSaleStatus.Draft,
|
||||
ComponentSubtotal = items[0].SalePrice.GetValueOrDefault() + items[1].SalePrice.GetValueOrDefault(),
|
||||
ComponentSubtotal = items[0].SellingFlatPrice.GetValueOrDefault() + items[1].SellingFlatPrice.GetValueOrDefault(),
|
||||
BundlePrice = 0m,
|
||||
MarginAmount = -(items[0].SalePrice.GetValueOrDefault() + items[1].SalePrice.GetValueOrDefault()),
|
||||
DiscountTotal = items[0].SalePrice.GetValueOrDefault() + items[1].SalePrice.GetValueOrDefault(),
|
||||
MarginAmount = -(items[0].SellingFlatPrice.GetValueOrDefault() + items[1].SellingFlatPrice.GetValueOrDefault()),
|
||||
DiscountTotal = items[0].SellingFlatPrice.GetValueOrDefault() + items[1].SellingFlatPrice.GetValueOrDefault(),
|
||||
TaxTotal = 0m,
|
||||
GrandTotal = 0m,
|
||||
CreatedAt = now.AddDays(-2),
|
||||
@@ -493,8 +527,8 @@ public static class DataSeeder
|
||||
Description = items[0].Name,
|
||||
Qty = 1m,
|
||||
WarehouseId = warehouse.WarehouseId,
|
||||
UnitPrice = items[0].SalePrice.GetValueOrDefault(),
|
||||
LineTotal = items[0].SalePrice.GetValueOrDefault(),
|
||||
UnitPrice = items[0].SellingFlatPrice.GetValueOrDefault(),
|
||||
LineTotal = items[0].SellingFlatPrice.GetValueOrDefault(),
|
||||
IncludeInBundle = true,
|
||||
IsComponent = true
|
||||
},
|
||||
@@ -504,8 +538,8 @@ public static class DataSeeder
|
||||
Description = items[1].Name,
|
||||
Qty = 1m,
|
||||
WarehouseId = warehouse.WarehouseId,
|
||||
UnitPrice = items[1].SalePrice.GetValueOrDefault(),
|
||||
LineTotal = items[1].SalePrice.GetValueOrDefault(),
|
||||
UnitPrice = items[1].SellingFlatPrice.GetValueOrDefault(),
|
||||
LineTotal = items[1].SellingFlatPrice.GetValueOrDefault(),
|
||||
IncludeInBundle = true,
|
||||
IsComponent = true
|
||||
}
|
||||
@@ -523,12 +557,12 @@ public static class DataSeeder
|
||||
BundleName = "Demo Bundle Posted",
|
||||
BundleCode = "BND-DEMO-001",
|
||||
Status = BundleSaleStatus.Posted,
|
||||
ComponentSubtotal = items[0].SalePrice.GetValueOrDefault() + items[1].SalePrice.GetValueOrDefault(),
|
||||
BundlePrice = (items[0].SalePrice.GetValueOrDefault() + items[1].SalePrice.GetValueOrDefault()) - 10m,
|
||||
ComponentSubtotal = items[0].SellingFlatPrice.GetValueOrDefault() + items[1].SellingFlatPrice.GetValueOrDefault(),
|
||||
BundlePrice = (items[0].SellingFlatPrice.GetValueOrDefault() + items[1].SellingFlatPrice.GetValueOrDefault()) - 10m,
|
||||
MarginAmount = -10m,
|
||||
DiscountTotal = 10m,
|
||||
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),
|
||||
UpdatedAt = now.AddHours(-2),
|
||||
Lines =
|
||||
@@ -539,8 +573,8 @@ public static class DataSeeder
|
||||
Description = items[0].Name,
|
||||
Qty = 1m,
|
||||
WarehouseId = warehouse.WarehouseId,
|
||||
UnitPrice = items[0].SalePrice.GetValueOrDefault(),
|
||||
LineTotal = items[0].SalePrice.GetValueOrDefault(),
|
||||
UnitPrice = items[0].SellingFlatPrice.GetValueOrDefault(),
|
||||
LineTotal = items[0].SellingFlatPrice.GetValueOrDefault(),
|
||||
IncludeInBundle = true,
|
||||
IsComponent = true
|
||||
},
|
||||
@@ -550,8 +584,8 @@ public static class DataSeeder
|
||||
Description = items[1].Name,
|
||||
Qty = 1m,
|
||||
WarehouseId = warehouse.WarehouseId,
|
||||
UnitPrice = items[1].SalePrice.GetValueOrDefault(),
|
||||
LineTotal = items[1].SalePrice.GetValueOrDefault(),
|
||||
UnitPrice = items[1].SellingFlatPrice.GetValueOrDefault(),
|
||||
LineTotal = items[1].SellingFlatPrice.GetValueOrDefault(),
|
||||
IncludeInBundle = true,
|
||||
IsComponent = true
|
||||
}
|
||||
@@ -569,12 +603,12 @@ public static class DataSeeder
|
||||
BundleName = "Demo Bundle Cancelled",
|
||||
BundleCode = "BND-DEMO-001",
|
||||
Status = BundleSaleStatus.Cancelled,
|
||||
ComponentSubtotal = items[0].SalePrice.GetValueOrDefault(),
|
||||
BundlePrice = items[0].SalePrice.GetValueOrDefault(),
|
||||
ComponentSubtotal = items[0].SellingFlatPrice.GetValueOrDefault(),
|
||||
BundlePrice = items[0].SellingFlatPrice.GetValueOrDefault(),
|
||||
MarginAmount = 0m,
|
||||
DiscountTotal = 0m,
|
||||
TaxTotal = 0m,
|
||||
GrandTotal = items[0].SalePrice.GetValueOrDefault(),
|
||||
GrandTotal = items[0].SellingFlatPrice.GetValueOrDefault(),
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now,
|
||||
Lines =
|
||||
@@ -585,8 +619,8 @@ public static class DataSeeder
|
||||
Description = items[0].Name,
|
||||
Qty = 1m,
|
||||
WarehouseId = secondaryWarehouse.WarehouseId,
|
||||
UnitPrice = items[0].SalePrice.GetValueOrDefault(),
|
||||
LineTotal = items[0].SalePrice.GetValueOrDefault(),
|
||||
UnitPrice = items[0].SellingFlatPrice.GetValueOrDefault(),
|
||||
LineTotal = items[0].SellingFlatPrice.GetValueOrDefault(),
|
||||
IncludeInBundle = true,
|
||||
IsComponent = true
|
||||
}
|
||||
|
||||
@@ -38,6 +38,11 @@ public class ErpDbContext : DbContext
|
||||
|
||||
/// <summary>Singleton row (FR-MD-11).</summary>
|
||||
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) ---
|
||||
public DbSet<User> Users => Set<User>();
|
||||
@@ -64,6 +69,11 @@ public class ErpDbContext : DbContext
|
||||
public DbSet<Grn> Grns => Set<Grn>();
|
||||
public DbSet<GrnLine> GrnLines => Set<GrnLine>();
|
||||
|
||||
// --- 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) ---
|
||||
public DbSet<Batch> Batches => Set<Batch>();
|
||||
public DbSet<Serial> Serials => Set<Serial>();
|
||||
@@ -87,8 +97,10 @@ public class ErpDbContext : DbContext
|
||||
// --- Sales (Phase 1) ---
|
||||
public DbSet<SalesInvoice> SalesInvoices => Set<SalesInvoice>();
|
||||
public DbSet<SalesInvoiceLine> SalesInvoiceLines => Set<SalesInvoiceLine>();
|
||||
public DbSet<SalesInvoicePayment> SalesInvoicePayments => Set<SalesInvoicePayment>();
|
||||
public DbSet<SalesSlip> SalesSlips => Set<SalesSlip>();
|
||||
public DbSet<SalesSlipLine> SalesSlipLines => Set<SalesSlipLine>();
|
||||
public DbSet<SalesDayEnd> SalesDayEnds => Set<SalesDayEnd>();
|
||||
public DbSet<BundleSaleTemplate> BundleSaleTemplates => Set<BundleSaleTemplate>();
|
||||
public DbSet<BundleSaleTemplateLine> BundleSaleTemplateLines => Set<BundleSaleTemplateLine>();
|
||||
public DbSet<BundleSale> BundleSales => Set<BundleSale>();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+6956
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 AddItemTypeIsMeasurable : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsMeasurable",
|
||||
table: "item_types",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsMeasurable",
|
||||
table: "item_types");
|
||||
}
|
||||
}
|
||||
}
|
||||
+7188
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,113 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ERPCore.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddGrnGlPostingAndPayments : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<decimal>(
|
||||
name: "BalanceAmount",
|
||||
table: "grns",
|
||||
type: "numeric(18,4)",
|
||||
precision: 18,
|
||||
scale: 4,
|
||||
nullable: false,
|
||||
defaultValue: 0m);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "GlJournalNo",
|
||||
table: "grns",
|
||||
type: "character varying(30)",
|
||||
maxLength: 30,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "GlPostedAt",
|
||||
table: "grns",
|
||||
type: "timestamp with time zone",
|
||||
nullable: true);
|
||||
|
||||
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),
|
||||
GrnId = 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_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");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "grn_payments");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "BalanceAmount",
|
||||
table: "grns");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "GlJournalNo",
|
||||
table: "grns");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "GlPostedAt",
|
||||
table: "grns");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PaidAmount",
|
||||
table: "grns");
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user