serial numbers

This commit is contained in:
2026-10-07 14:00:32 +05:30
committed by ImanThiyanga
parent 3abcfa6e6f
commit a788349138
89 changed files with 15320 additions and 216 deletions
@@ -0,0 +1,41 @@
using ERPCore.Domain.Enums;
using ERPCore.Dtos.Common;
using ERPCore.Dtos.Stock;
using ERPCore.Infra.Auth;
using ERPCore.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers;
/// <summary>Serial register and per-serial history (docs/ITEM_TRACKING_PLAN.md Phase 9).</summary>
[Route("api/v1/serials")]
[Authorize(Policy = NavPolicies.Prefix + "stock")]
public sealed class SerialsController : ApiControllerBase
{
private readonly IItemTrackingService _tracking;
public SerialsController(IItemTrackingService tracking) => _tracking = tracking;
[HttpGet]
[ProducesResponseType(typeof(PagedResponse<SerialSummaryDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedResponse<SerialSummaryDto>>> List(
[FromQuery] int? itemId, [FromQuery] int? warehouseId, [FromQuery] SerialStatus? status,
[FromQuery] PageQuery query, CancellationToken ct)
=> Ok(await _tracking.ListSerialsAsync(query, itemId, warehouseId, status, ct));
/// <summary>Exact lookup by serial number or barcode, across items.</summary>
[HttpGet("lookup")]
[ProducesResponseType(typeof(IReadOnlyList<SerialDetailDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<IReadOnlyList<SerialDetailDto>>> Lookup([FromQuery] string serialNo, CancellationToken ct)
=> Ok(await _tracking.FindSerialAsync(serialNo, ct));
[HttpGet("{serialId:int}")]
[ProducesResponseType(typeof(SerialDetailDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<SerialDetailDto>> Get(int serialId, CancellationToken ct)
{
var dto = await _tracking.GetSerialAsync(serialId, ct);
return dto is null ? NotFound() : Ok(dto);
}
}
+14 -1
View File
@@ -15,13 +15,26 @@ public sealed class StockController : ApiControllerBase
{
private readonly IStockService _stock;
private readonly IReorderService _reorder;
private readonly IItemTrackingService _tracking;
public StockController(IStockService stock, IReorderService reorder)
public StockController(IStockService stock, IReorderService reorder, IItemTrackingService tracking)
{
_stock = stock;
_reorder = reorder;
_tracking = tracking;
}
/// <summary>
/// Stock on hand per batch with expiry (docs/ITEM_TRACKING_PLAN.md Phase 9), soonest expiry
/// first. <c>expiringWithinDays</c> keeps batches expiring within that window, expired included.
/// </summary>
[HttpGet("batches")]
[ProducesResponseType(typeof(PagedResponse<BatchStockDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedResponse<BatchStockDto>>> Batches(
[FromQuery] int? itemId, [FromQuery] int? warehouseId, [FromQuery] int? expiringWithinDays,
[FromQuery] PageQuery query, CancellationToken ct)
=> Ok(await _tracking.ListBatchStockAsync(query, itemId, warehouseId, expiringWithinDays, ct));
[HttpGet("on-hand")]
[ProducesResponseType(typeof(StockOnHandDto), StatusCodes.Status200OK)]
public async Task<ActionResult<StockOnHandDto>> OnHand([FromQuery] int itemId, [FromQuery] int warehouseId, CancellationToken ct)
@@ -0,0 +1,64 @@
using ERPCore.Dtos.Stock;
using ERPCore.Services.Interfaces;
using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers;
/// <summary>
/// Serial/batch pickers used across GRN, sales, returns and stock screens
/// (docs/ITEM_TRACKING_PLAN.md Phase 8). Read-only lookups like the item scan, so they need
/// only ERP access — not one module's nav permission — as every one of those screens calls them.
/// </summary>
[Route("api/v1/tracking")]
public sealed class TrackingController : ApiControllerBase
{
private readonly IItemTrackingService _tracking;
private readonly ISerialTrackingService _serialTracking;
public TrackingController(IItemTrackingService tracking, ISerialTrackingService serialTracking)
{
_tracking = tracking;
_serialTracking = serialTracking;
}
/// <summary>
/// Which tracking features are switched on (Product Configuration) — readable by every
/// screen, unlike <c>/product-config</c> which needs the Products permission.
/// </summary>
[HttpGet("settings")]
[ProducesResponseType(typeof(TrackingSettingsDto), StatusCodes.Status200OK)]
public async Task<ActionResult<TrackingSettingsDto>> Settings(CancellationToken ct)
=> Ok(new TrackingSettingsDto(await _serialTracking.IsEnabledAsync(ct)));
/// <summary>
/// Auto-generate: the next <c>count</c> free serial numbers for an item under <c>prefix</c>
/// (continues the highest number already used). Nothing is reserved — the GRN still checks
/// uniqueness when it is saved and confirmed.
/// </summary>
[HttpGet("serials/next-numbers")]
[ProducesResponseType(typeof(IReadOnlyList<string>), StatusCodes.Status200OK)]
public async Task<ActionResult<IReadOnlyList<string>>> NextSerialNumbers(
[FromQuery] int itemId, [FromQuery] string? prefix, [FromQuery] int count = 1, CancellationToken ct = default)
=> Ok(await _tracking.NextSerialNumbersAsync(itemId, prefix ?? string.Empty, count, ct));
/// <summary>Issuable serials of an item at a warehouse (search by number/barcode with <c>q</c>).</summary>
[HttpGet("serials/available")]
[ProducesResponseType(typeof(IReadOnlyList<AvailableSerialDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<IReadOnlyList<AvailableSerialDto>>> AvailableSerials(
[FromQuery] int itemId, [FromQuery] int warehouseId, [FromQuery] string? q, [FromQuery] int take = 200, CancellationToken ct = default)
=> Ok(await _tracking.GetAvailableSerialsAsync(itemId, warehouseId, q, take, ct));
/// <summary>Issuable stock per batch with expiry, soonest expiry first.</summary>
[HttpGet("batches/available")]
[ProducesResponseType(typeof(IReadOnlyList<AvailableBatchDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<IReadOnlyList<AvailableBatchDto>>> AvailableBatches(
[FromQuery] int itemId, [FromQuery] int warehouseId, CancellationToken ct)
=> Ok(await _tracking.GetAvailableBatchesAsync(itemId, warehouseId, ct));
/// <summary>The FEFO pick posting would make for <c>qty</c> when no batch is chosen.</summary>
[HttpGet("batches/suggest")]
[ProducesResponseType(typeof(IReadOnlyList<BatchSuggestionDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<IReadOnlyList<BatchSuggestionDto>>> SuggestBatches(
[FromQuery] int itemId, [FromQuery] int warehouseId, [FromQuery] decimal qty, CancellationToken ct)
=> Ok(await _tracking.SuggestBatchesAsync(itemId, warehouseId, qty, ct));
}
@@ -0,0 +1,30 @@
using ERPCore.Domain.Enums;
using ERPCore.Dtos.Common;
using ERPCore.Dtos.Stock;
using ERPCore.Infra.Auth;
using ERPCore.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace ERPCore.Controllers;
/// <summary>
/// Customer warranty register (docs/ITEM_TRACKING_PLAN.md Phase 5). Warranties are created by
/// posting a sale, never directly — this controller only reads.
/// </summary>
[Route("api/v1/warranties")]
[Authorize(Policy = NavPolicies.Prefix + "sales")]
public sealed class WarrantiesController : ApiControllerBase
{
private readonly IItemTrackingService _tracking;
public WarrantiesController(IItemTrackingService tracking) => _tracking = tracking;
[HttpGet]
[ProducesResponseType(typeof(PagedResponse<WarrantyRegistrationDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedResponse<WarrantyRegistrationDto>>> List(
[FromQuery] int? customerId, [FromQuery] int? itemId, [FromQuery] string? serialNo,
[FromQuery] WarrantyStatus? status, [FromQuery] int? expiringWithinDays, [FromQuery] bool? expired,
[FromQuery] PageQuery query, CancellationToken ct)
=> Ok(await _tracking.ListWarrantiesAsync(query, customerId, itemId, serialNo, status, expiringWithinDays, expired, ct));
}