serial numbers
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
Reference in New Issue
Block a user