Merge pull request 'feat(expeditions): prevent reuse of stockitem_id in active expeditions' (#6) from feature/leandro/5-double-trace-lock into master
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 2m18s
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 2m18s
Reviewed-on: http://saludlab.com.ar:3000/leandro/phronCare/pulls/6
This commit is contained in:
commit
d99f1c34d2
@ -1,4 +1,5 @@
|
|||||||
using Core.Interfaces.Stock;
|
using Core.Interfaces.Stock;
|
||||||
|
using Domain.Constants;
|
||||||
using Domain.Dtos.Stock;
|
using Domain.Dtos.Stock;
|
||||||
using Domain.Entities;
|
using Domain.Entities;
|
||||||
using Domain.Generics;
|
using Domain.Generics;
|
||||||
@ -12,32 +13,155 @@ namespace Core.Services.Stock
|
|||||||
{
|
{
|
||||||
#region Declaraciones
|
#region Declaraciones
|
||||||
private readonly IExpeditionRepository _repo;
|
private readonly IExpeditionRepository _repo;
|
||||||
public ExpeditionService(IExpeditionRepository repo) => _repo = repo;
|
private readonly IPhLSMStockItemRepository _stockItemRepository;
|
||||||
|
public ExpeditionService(
|
||||||
|
IExpeditionRepository repo,
|
||||||
|
IPhLSMStockItemRepository stockItemRepository)
|
||||||
|
{
|
||||||
|
_repo = repo;
|
||||||
|
_stockItemRepository = stockItemRepository;
|
||||||
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Guardado completo de expedicion (encabezado + detalles)
|
#region Guardado completo de expedicion (encabezado + detalles)
|
||||||
public async Task<(int Id, string ExpeditionNumber)> CreateAndIssueAsync(
|
public async Task<(int Id, string ExpeditionNumber)> CreateAndIssueAsync(
|
||||||
ELSExpeditionHeader header,
|
ELSExpeditionHeader header,
|
||||||
IEnumerable<ELSExpeditionDetail> details,
|
IEnumerable<ELSExpeditionDetail> details,
|
||||||
int formSeriesId)
|
int formSeriesId)
|
||||||
{
|
{
|
||||||
if (header is null) throw new ArgumentNullException(nameof(header));
|
if (header is null)
|
||||||
|
throw new ArgumentNullException(nameof(header));
|
||||||
|
|
||||||
if (details is null || !details.Any())
|
if (details is null || !details.Any())
|
||||||
throw new InvalidOperationException("Debe incluir al menos un ítem.");
|
throw new InvalidOperationException("Debe incluir al menos un ítem.");
|
||||||
|
|
||||||
if (formSeriesId <= 0)
|
if (formSeriesId <= 0)
|
||||||
throw new ArgumentOutOfRangeException(nameof(formSeriesId), "Serie inválida.");
|
throw new ArgumentOutOfRangeException(nameof(formSeriesId), "Serie inválida.");
|
||||||
|
|
||||||
// Reemplazo directo de la colección (más claro que Clear()+Add)
|
var detailList = details.ToList();
|
||||||
header.PhLsmExpeditionDetails = details.ToList();
|
|
||||||
|
ValidateNoDuplicateStockItems(detailList);
|
||||||
|
await ValidateSerializedConflictsAsync(detailList);
|
||||||
|
await ValidateStockAvailabilityAsync(detailList);
|
||||||
|
|
||||||
|
header.PhLsmExpeditionDetails = detailList;
|
||||||
|
|
||||||
return await _repo.CreateFullExpeditionAsync(header, formSeriesId);
|
return await _repo.CreateFullExpeditionAsync(header, formSeriesId);
|
||||||
}
|
}
|
||||||
|
private static void ValidateNoDuplicateStockItems(List<ELSExpeditionDetail> detailList)
|
||||||
|
{
|
||||||
|
var duplicateIds = detailList
|
||||||
|
.Where(d => d.StockitemId > 0)
|
||||||
|
.GroupBy(d => d.StockitemId)
|
||||||
|
.Where(g => g.Count() > 1)
|
||||||
|
.Select(g => g.Key)
|
||||||
|
.OrderBy(x => x)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (duplicateIds.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var msg = "No se puede emitir la expedición. " +
|
||||||
|
"El mismo StockItem fue seleccionado más de una vez: " +
|
||||||
|
string.Join(", ", duplicateIds);
|
||||||
|
|
||||||
|
throw new InvalidOperationException(msg);
|
||||||
|
}
|
||||||
|
private async Task ValidateSerializedConflictsAsync(List<ELSExpeditionDetail> detailList)
|
||||||
|
{
|
||||||
|
var requestedStockItemIds = detailList
|
||||||
|
.Where(d => d.StockitemId > 0)
|
||||||
|
.Select(d => d.StockitemId)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (requestedStockItemIds.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var conflicts = await _repo.CheckStockItemConflictsAsync(
|
||||||
|
requestedStockItemIds,
|
||||||
|
ignoreExpeditionId: null);
|
||||||
|
|
||||||
|
if (conflicts.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var lines = new List<string>
|
||||||
|
{
|
||||||
|
$"No se puede emitir la expedición: se detectaron {conflicts.Count} stock items serializados ya asignados a expediciones activas."
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var conflict in conflicts
|
||||||
|
.OrderBy(x => x.StockitemId)
|
||||||
|
.ThenBy(x => x.Expeditionnumber))
|
||||||
|
{
|
||||||
|
var statusLabel = ((ExpeditionStatus)conflict.Status).ToLabel();
|
||||||
|
lines.Add($"• StockItem {conflict.StockitemId} → {conflict.Expeditionnumber} ({statusLabel})");
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException(string.Join(Environment.NewLine, lines));
|
||||||
|
}
|
||||||
|
private async Task ValidateStockAvailabilityAsync(List<ELSExpeditionDetail> detailList)
|
||||||
|
{
|
||||||
|
var requestedByStockItem = detailList
|
||||||
|
.Where(d => d.StockitemId > 0 && d.Quantity > 0)
|
||||||
|
.GroupBy(d => d.StockitemId)
|
||||||
|
.Select(g => new
|
||||||
|
{
|
||||||
|
StockitemId = g.Key,
|
||||||
|
RequestedQuantity = g.Sum(x => x.Quantity)
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (requestedByStockItem.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var availability = await _stockItemRepository.GetAvailabilityByStockItemIdsAsync(
|
||||||
|
requestedByStockItem.Select(x => x.StockitemId));
|
||||||
|
|
||||||
|
var availabilityMap = availability.ToDictionary(x => x.StockitemId);
|
||||||
|
|
||||||
|
var errors = new List<string>();
|
||||||
|
|
||||||
|
foreach (var request in requestedByStockItem.OrderBy(x => x.StockitemId))
|
||||||
|
{
|
||||||
|
if (!availabilityMap.TryGetValue(request.StockitemId, out var stock))
|
||||||
|
{
|
||||||
|
errors.Add($"• StockItem {request.StockitemId} → no fue encontrado en stock.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasSerial = !string.IsNullOrWhiteSpace(stock.Serial);
|
||||||
|
|
||||||
|
// Los serializados ya se validan por exclusividad en ValidateSerializedConflictsAsync
|
||||||
|
if (hasSerial)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (request.RequestedQuantity > stock.AvailableQuantity)
|
||||||
|
{
|
||||||
|
errors.Add(
|
||||||
|
$"• StockItem {request.StockitemId} → solicitado: {request.RequestedQuantity}, disponible: {stock.AvailableQuantity}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var lines = new List<string>
|
||||||
|
{
|
||||||
|
"No se puede emitir la expedición: algunos stock items no serializados no tienen cantidad disponible suficiente."
|
||||||
|
};
|
||||||
|
|
||||||
|
lines.AddRange(errors);
|
||||||
|
|
||||||
|
throw new InvalidOperationException(string.Join(Environment.NewLine, lines));
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
// Otros métodos de la clase...
|
||||||
public Task<ExpeditionDto?> GetDtoByExpeditionNumberAsync(string expeditionNumber)
|
public Task<ExpeditionDto?> GetDtoByExpeditionNumberAsync(string expeditionNumber)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
#endregion
|
|
||||||
|
|
||||||
public Task<PagedResult<ExpeditionDto>> SearchAsync(
|
public Task<PagedResult<ExpeditionDto>> SearchAsync(
|
||||||
string? expeditionNumber,
|
string? expeditionNumber,
|
||||||
string? status,
|
string? status,
|
||||||
@ -49,7 +173,6 @@ namespace Core.Services.Stock
|
|||||||
=> _repo.SearchAsync(expeditionNumber, status, issueDateFrom, issueDateTo, locationId, page, pageSize);
|
=> _repo.SearchAsync(expeditionNumber, status, issueDateFrom, issueDateTo, locationId, page, pageSize);
|
||||||
public Task<ExpeditionDto?> GetDtoByIdAsync(int id)
|
public Task<ExpeditionDto?> GetDtoByIdAsync(int id)
|
||||||
=> _repo.GetDtoByIdAsync(id);
|
=> _repo.GetDtoByIdAsync(id);
|
||||||
|
|
||||||
public async Task<byte[]> ExportFilteredToExcelAsync(ExpeditionSearchParams searchParams)
|
public async Task<byte[]> ExportFilteredToExcelAsync(ExpeditionSearchParams searchParams)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
11
Domain/Dtos/Stock/StockItemAvailabilityDto.cs
Normal file
11
Domain/Dtos/Stock/StockItemAvailabilityDto.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
namespace Domain.Dtos.Stock
|
||||||
|
{
|
||||||
|
public sealed class StockItemAvailabilityDto
|
||||||
|
{
|
||||||
|
public int StockitemId { get; set; }
|
||||||
|
public decimal Quantity { get; set; }
|
||||||
|
public decimal ReservedQuantity { get; set; }
|
||||||
|
public decimal AvailableQuantity { get; set; }
|
||||||
|
public string? Serial { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
10
Domain/Dtos/Stock/StockItemExpeditionConflictDto.cs
Normal file
10
Domain/Dtos/Stock/StockItemExpeditionConflictDto.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
namespace Domain.Dtos.Stock
|
||||||
|
{
|
||||||
|
public sealed class StockItemExpeditionConflictDto
|
||||||
|
{
|
||||||
|
public int StockitemId { get; set; }
|
||||||
|
public int ExpeditionId { get; set; }
|
||||||
|
public string Expeditionnumber { get; set; } = string.Empty;
|
||||||
|
public int Status { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,6 +7,16 @@ namespace Models.Interfaces
|
|||||||
// 1.1 Data (Repo)
|
// 1.1 Data (Repo)
|
||||||
public interface IExpeditionRepository
|
public interface IExpeditionRepository
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Verifica si alguno de los stock items indicados ya está asociado
|
||||||
|
/// a otra expedición activa. Utilizado para prevenir doble traza.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="stockItemIds">Lista de stockitem_id a validar.</param>
|
||||||
|
/// <param name="ignoreExpeditionId">
|
||||||
|
/// Expedición a ignorar (usado en edición para no detectar conflicto consigo misma).
|
||||||
|
/// </param>
|
||||||
|
/// <returns>Lista de conflictos encontrados.</returns>
|
||||||
|
Task<List<StockItemExpeditionConflictDto>> CheckStockItemConflictsAsync(IEnumerable<int> stockItemIds, int? ignoreExpeditionId);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Crea la expedición completa (encabezado + detalles) y la deja emitida con numeración de serie.
|
/// Crea la expedición completa (encabezado + detalles) y la deja emitida con numeración de serie.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -24,5 +24,9 @@ namespace Models.Interfaces
|
|||||||
int? locationId,
|
int? locationId,
|
||||||
int page,
|
int page,
|
||||||
int take);
|
int take);
|
||||||
|
|
||||||
|
// Obtener disponibilidad por IDs de StockItem
|
||||||
|
Task<List<StockItemAvailabilityDto>> GetAvailabilityByStockItemIdsAsync(
|
||||||
|
IEnumerable<int> stockItemIds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,10 +2,12 @@
|
|||||||
using Domain.Dtos.Stock;
|
using Domain.Dtos.Stock;
|
||||||
using Domain.Entities;
|
using Domain.Entities;
|
||||||
using Domain.Generics;
|
using Domain.Generics;
|
||||||
|
using Microsoft.Data.SqlClient;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Models.Helpers;
|
using Models.Helpers;
|
||||||
using Models.Interfaces;
|
using Models.Interfaces;
|
||||||
using Models.Models;
|
using Models.Models;
|
||||||
|
using System.Data;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
@ -56,7 +58,6 @@ namespace Models.Repositories.Stock
|
|||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Devuelve el DTO completo de Expedición (cabecera + ítems) listo para UI/impresión.
|
/// Devuelve el DTO completo de Expedición (cabecera + ítems) listo para UI/impresión.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -164,9 +165,7 @@ namespace Models.Repositories.Stock
|
|||||||
|
|
||||||
return dto;
|
return dto;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----- helpers -----
|
// ----- helpers -----
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Mapea el estado entero a etiqueta amigable (enum: Emitida=1, EnTransito=2, EnDestino=3, Retorno=4, Cerrada=5, Anulada=6).
|
/// Mapea el estado entero a etiqueta amigable (enum: Emitida=1, EnTransito=2, EnDestino=3, Retorno=4, Cerrada=5, Anulada=6).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -310,6 +309,73 @@ namespace Models.Repositories.Stock
|
|||||||
PageSize = pageSize
|
PageSize = pageSize
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
public async Task<List<StockItemExpeditionConflictDto>> CheckStockItemConflictsAsync(
|
||||||
|
IEnumerable<int> stockItemIds,
|
||||||
|
int? ignoreExpeditionId)
|
||||||
|
{
|
||||||
|
// Normalización defensiva
|
||||||
|
var ids = (stockItemIds ?? Enumerable.Empty<int>())
|
||||||
|
.Where(x => x > 0)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (ids.Count == 0)
|
||||||
|
return new List<StockItemExpeditionConflictDto>();
|
||||||
|
|
||||||
|
// TVP: dbo.PhLSM_StockItemIdList(stockitem_id int not null)
|
||||||
|
var tvp = new DataTable();
|
||||||
|
tvp.Columns.Add("stockitem_id", typeof(int));
|
||||||
|
|
||||||
|
foreach (var id in ids)
|
||||||
|
tvp.Rows.Add(id);
|
||||||
|
|
||||||
|
var results = new List<StockItemExpeditionConflictDto>();
|
||||||
|
|
||||||
|
// Usamos la conexión del DbContext (no creamos otra)
|
||||||
|
var conn = _context.Database.GetDbConnection();
|
||||||
|
if (conn.State != ConnectionState.Open)
|
||||||
|
await _context.Database.OpenConnectionAsync();
|
||||||
|
|
||||||
|
await using var cmd = conn.CreateCommand();
|
||||||
|
cmd.CommandText = "dbo.PhLSM_Expedition_CheckStockItemConflicts";
|
||||||
|
cmd.CommandType = CommandType.StoredProcedure;
|
||||||
|
|
||||||
|
// Param TVP
|
||||||
|
var pIds = new SqlParameter("@StockItemIds", SqlDbType.Structured)
|
||||||
|
{
|
||||||
|
TypeName = "dbo.PhLSM_StockItemIdList",
|
||||||
|
Value = tvp
|
||||||
|
};
|
||||||
|
cmd.Parameters.Add(pIds);
|
||||||
|
|
||||||
|
// Param opcional para edición
|
||||||
|
var pIgnore = new SqlParameter("@IgnoreExpeditionId", SqlDbType.Int)
|
||||||
|
{
|
||||||
|
Value = ignoreExpeditionId.HasValue ? ignoreExpeditionId.Value : DBNull.Value
|
||||||
|
};
|
||||||
|
cmd.Parameters.Add(pIgnore);
|
||||||
|
|
||||||
|
await using var reader = await cmd.ExecuteReaderAsync();
|
||||||
|
|
||||||
|
// Ordinals por nombre (más robusto ante cambios de orden)
|
||||||
|
var ordStockItemId = reader.GetOrdinal("StockitemId");
|
||||||
|
var ordExpId = reader.GetOrdinal("ExpeditionId");
|
||||||
|
var ordExpNum = reader.GetOrdinal("Expeditionnumber");
|
||||||
|
var ordStatus = reader.GetOrdinal("Status");
|
||||||
|
|
||||||
|
while (await reader.ReadAsync())
|
||||||
|
{
|
||||||
|
results.Add(new StockItemExpeditionConflictDto
|
||||||
|
{
|
||||||
|
StockitemId = reader.GetInt32(ordStockItemId),
|
||||||
|
ExpeditionId = reader.GetInt32(ordExpId),
|
||||||
|
Expeditionnumber = reader.IsDBNull(ordExpNum) ? string.Empty : reader.GetString(ordExpNum),
|
||||||
|
Status = reader.GetInt32(ordStatus)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
private static int? MapStatusLabelToInt(string labelOrNumber)
|
private static int? MapStatusLabelToInt(string labelOrNumber)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(labelOrNumber)) return null;
|
if (string.IsNullOrWhiteSpace(labelOrNumber)) return null;
|
||||||
@ -329,7 +395,6 @@ namespace Models.Repositories.Stock
|
|||||||
_ => (int?)null
|
_ => (int?)null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string NormalizeKey(string s)
|
private static string NormalizeKey(string s)
|
||||||
{
|
{
|
||||||
var norm = s.Trim().ToLowerInvariant().Normalize(NormalizationForm.FormD);
|
var norm = s.Trim().ToLowerInvariant().Normalize(NormalizationForm.FormD);
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
using Domain.Dtos.Stock; // StockItemScanResultDto
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Data.SqlClient;
|
||||||
|
using Domain.Dtos.Stock; // StockItemScanResultDto
|
||||||
using Domain.Generics; // PagedResult<T>
|
using Domain.Generics; // PagedResult<T>
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Models.Helpers; // ToPagedResultAsync
|
using Models.Helpers; // ToPagedResultAsync
|
||||||
using Models.Interfaces; // IPhLSMStockItemRepository
|
using Models.Interfaces; // IPhLSMStockItemRepository
|
||||||
using Models.Models; // PhronCareOperationsHubContext
|
using Models.Models;
|
||||||
|
using System.Data; // PhronCareOperationsHubContext
|
||||||
|
|
||||||
namespace Models.Repositories.Stock
|
namespace Models.Repositories.Stock
|
||||||
{
|
{
|
||||||
@ -324,5 +326,74 @@ namespace Models.Repositories.Stock
|
|||||||
PageSize = paged.PageSize
|
PageSize = paged.PageSize
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<List<StockItemAvailabilityDto>> GetAvailabilityByStockItemIdsAsync(
|
||||||
|
IEnumerable<int> stockItemIds)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var ids = stockItemIds?
|
||||||
|
.Where(x => x > 0)
|
||||||
|
.Distinct()
|
||||||
|
.ToList() ?? new List<int>();
|
||||||
|
|
||||||
|
if (ids.Count == 0)
|
||||||
|
return new List<StockItemAvailabilityDto>();
|
||||||
|
|
||||||
|
// TVP = Table Valued Parameter
|
||||||
|
var tvp = new DataTable();
|
||||||
|
tvp.Columns.Add("stockitem_id", typeof(int));
|
||||||
|
|
||||||
|
foreach (var id in ids)
|
||||||
|
tvp.Rows.Add(id);
|
||||||
|
|
||||||
|
var result = new List<StockItemAvailabilityDto>();
|
||||||
|
var conn = _context.Database.GetDbConnection();
|
||||||
|
var cs = _context.Database.GetConnectionString();
|
||||||
|
if (conn.State != ConnectionState.Open)
|
||||||
|
await _context.Database.OpenConnectionAsync();
|
||||||
|
|
||||||
|
|
||||||
|
await using var cmd = conn.CreateCommand();
|
||||||
|
cmd.CommandText = "dbo.PhLSM_Stock_GetAvailabilityByStockItemIds";
|
||||||
|
cmd.CommandType = CommandType.StoredProcedure;
|
||||||
|
|
||||||
|
var pIds = new SqlParameter("@StockItemIds", SqlDbType.Structured)
|
||||||
|
{
|
||||||
|
TypeName = "dbo.PhLSM_StockItemIdList",
|
||||||
|
Value = tvp
|
||||||
|
};
|
||||||
|
|
||||||
|
cmd.Parameters.Add(pIds);
|
||||||
|
|
||||||
|
await using var reader = await cmd.ExecuteReaderAsync();
|
||||||
|
|
||||||
|
var ordStockItemId = reader.GetOrdinal("StockitemId");
|
||||||
|
var ordQuantity = reader.GetOrdinal("Quantity");
|
||||||
|
var ordReserved = reader.GetOrdinal("ReservedQuantity");
|
||||||
|
var ordAvailable = reader.GetOrdinal("AvailableQuantity");
|
||||||
|
var ordSerial = reader.GetOrdinal("Serial");
|
||||||
|
|
||||||
|
while (await reader.ReadAsync())
|
||||||
|
{
|
||||||
|
result.Add(new StockItemAvailabilityDto
|
||||||
|
{
|
||||||
|
StockitemId = reader.GetInt32(ordStockItemId),
|
||||||
|
Quantity = reader.GetDecimal(ordQuantity),
|
||||||
|
ReservedQuantity = reader.GetDecimal(ordReserved),
|
||||||
|
AvailableQuantity = reader.GetDecimal(ordAvailable),
|
||||||
|
Serial = reader.IsDBNull(ordSerial)
|
||||||
|
? null
|
||||||
|
: reader.GetString(ordSerial)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new Exception(ex.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
44
PhLSM_Expedition_CheckStockItemConflicts.sql
Normal file
44
PhLSM_Expedition_CheckStockItemConflicts.sql
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
USE [phronCare_OperationsHub]
|
||||||
|
GO
|
||||||
|
|
||||||
|
SET ANSI_NULLS ON
|
||||||
|
GO
|
||||||
|
SET QUOTED_IDENTIFIER ON
|
||||||
|
GO
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- Procedure: PhLSM_Expedition_CheckStockItemConflicts
|
||||||
|
-- Module: Logistics / Stock (PhLSM)
|
||||||
|
-- Purpose: Detect serialized stock items already assigned
|
||||||
|
-- to active expeditions.
|
||||||
|
-- Author: Leandro Rojas
|
||||||
|
-- Created: 2026-03-05
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
ALTER PROCEDURE [dbo].[PhLSM_Expedition_CheckStockItemConflicts]
|
||||||
|
(
|
||||||
|
@StockItemIds dbo.PhLSM_StockItemIdList READONLY,
|
||||||
|
@IgnoreExpeditionId INT = NULL
|
||||||
|
)
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
d.stockitem_id AS StockitemId,
|
||||||
|
h.id AS ExpeditionId,
|
||||||
|
h.expeditionnumber AS Expeditionnumber,
|
||||||
|
h.status AS Status
|
||||||
|
FROM dbo.PhLSM_ExpeditionDetails d
|
||||||
|
INNER JOIN @StockItemIds ids
|
||||||
|
ON ids.stockitem_id = d.stockitem_id
|
||||||
|
INNER JOIN dbo.PhLSM_ExpeditionHeaders h
|
||||||
|
ON h.id = d.expedition_id
|
||||||
|
INNER JOIN dbo.PhLSM_StockItem si
|
||||||
|
ON si.id = d.stockitem_id
|
||||||
|
WHERE h.status NOT IN (5,6) -- 5=Cerrada, 6=Anulada
|
||||||
|
AND (@IgnoreExpeditionId IS NULL OR h.id <> @IgnoreExpeditionId)
|
||||||
|
AND si.serial IS NOT NULL
|
||||||
|
AND LTRIM(RTRIM(si.serial)) <> '';
|
||||||
|
END
|
||||||
|
GO
|
||||||
5
PhLSM_StockItemIdList.sql
Normal file
5
PhLSM_StockItemIdList.sql
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
CREATE TYPE dbo.PhLSM_StockItemIdList AS TABLE
|
||||||
|
(
|
||||||
|
stockitem_id INT NOT NULL
|
||||||
|
);
|
||||||
|
GO
|
||||||
36
PhLSM_Stock_GetAvailabilityByStockItemIds.sql
Normal file
36
PhLSM_Stock_GetAvailabilityByStockItemIds.sql
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
USE [phronCare_OperationsHub]
|
||||||
|
GO
|
||||||
|
|
||||||
|
SET ANSI_NULLS ON
|
||||||
|
GO
|
||||||
|
SET QUOTED_IDENTIFIER ON
|
||||||
|
GO
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- Procedure: PhLSM_Stock_GetAvailabilityByStockItemIds
|
||||||
|
-- Module: Logistics / Stock (PhLSM)
|
||||||
|
-- Purpose: Returns quantity and availability data
|
||||||
|
-- for the requested stock items.
|
||||||
|
-- Author: Leandro Rojas
|
||||||
|
-- Created: 2026-03-09
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[PhLSM_Stock_GetAvailabilityByStockItemIds]
|
||||||
|
(
|
||||||
|
@StockItemIds dbo.PhLSM_StockItemIdList READONLY
|
||||||
|
)
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
si.id AS StockitemId,
|
||||||
|
si.quantity AS Quantity,
|
||||||
|
ISNULL(si.reserved_quantity, 0) AS ReservedQuantity,
|
||||||
|
si.quantity - ISNULL(si.reserved_quantity, 0) AS AvailableQuantity,
|
||||||
|
si.serial AS Serial
|
||||||
|
FROM dbo.PhLSM_StockItem si
|
||||||
|
INNER JOIN @StockItemIds ids
|
||||||
|
ON ids.stockitem_id = si.id;
|
||||||
|
END
|
||||||
|
GO
|
||||||
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Version 17
|
# Visual Studio Version 18
|
||||||
VisualStudioVersion = 17.6.33815.320
|
VisualStudioVersion = 18.3.11520.95
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "phronCare.API", "phronCare.API\phronCare.API.csproj", "{13FF5898-C6D4-42AE-AC36-8D42C88FEDAF}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "phronCare.API", "phronCare.API\phronCare.API.csproj", "{13FF5898-C6D4-42AE-AC36-8D42C88FEDAF}"
|
||||||
EndProject
|
EndProject
|
||||||
@ -43,6 +43,21 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "1.5 Documents", "1.5 Docume
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Documents", "Documents\Documents.csproj", "{0EFF27D3-C585-49F3-BBB5-A5E99C52207B}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Documents", "Documents\Documents.csproj", "{0EFF27D3-C585-49F3-BBB5-A5E99C52207B}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "2.Database", "2.Database", "{E93C8350-6A6C-40F1-99C0-14CF7459EA8B}"
|
||||||
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Migrations", "Migrations", "{5826BD19-0018-4F9C-B405-9BAB997CC8C7}"
|
||||||
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Types", "Types", "{3C3276F6-7320-4AB8-9C1E-1893E4646FD4}"
|
||||||
|
ProjectSection(SolutionItems) = preProject
|
||||||
|
PhLSM_StockItemIdList.sql = PhLSM_StockItemIdList.sql
|
||||||
|
EndProjectSection
|
||||||
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Procedures", "Procedures", "{E4F7BA13-B838-42D5-AADD-481DD76DDD5E}"
|
||||||
|
ProjectSection(SolutionItems) = preProject
|
||||||
|
PhLSM_Expedition_CheckStockItemConflicts.sql = PhLSM_Expedition_CheckStockItemConflicts.sql
|
||||||
|
PhLSM_Stock_GetAvailabilityByStockItemIds.sql = PhLSM_Stock_GetAvailabilityByStockItemIds.sql
|
||||||
|
EndProjectSection
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@ -104,6 +119,9 @@ Global
|
|||||||
{34FC5538-4779-41F5-8355-7866B1395A4F} = {13328F60-28A6-446D-9935-23866F3F3D9D}
|
{34FC5538-4779-41F5-8355-7866B1395A4F} = {13328F60-28A6-446D-9935-23866F3F3D9D}
|
||||||
{02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {74280C0B-F2CC-4A3E-86D6-05530F9766D5}
|
{02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {74280C0B-F2CC-4A3E-86D6-05530F9766D5}
|
||||||
{0EFF27D3-C585-49F3-BBB5-A5E99C52207B} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
|
{0EFF27D3-C585-49F3-BBB5-A5E99C52207B} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
|
||||||
|
{5826BD19-0018-4F9C-B405-9BAB997CC8C7} = {E93C8350-6A6C-40F1-99C0-14CF7459EA8B}
|
||||||
|
{3C3276F6-7320-4AB8-9C1E-1893E4646FD4} = {E93C8350-6A6C-40F1-99C0-14CF7459EA8B}
|
||||||
|
{E4F7BA13-B838-42D5-AADD-481DD76DDD5E} = {E93C8350-6A6C-40F1-99C0-14CF7459EA8B}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {92DE3FEB-7D7E-4C78-BE8C-34931CA1DAED}
|
SolutionGuid = {92DE3FEB-7D7E-4C78-BE8C-34931CA1DAED}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user