Merge pull request 'feat(stock): reserve stock when expedition moves to EnTransito' (#10) from feature/leandro/9-stock-reservation-on-transit into master
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 1m57s
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 1m57s
Reviewed-on: http://saludlab.com.ar:3000/leandro/phronCare/pulls/10
This commit is contained in:
commit
0c035f50d6
4
.gitignore
vendored
4
.gitignore
vendored
@ -397,6 +397,10 @@ FodyWeavers.xsd
|
|||||||
*.msm
|
*.msm
|
||||||
*.msp
|
*.msp
|
||||||
|
|
||||||
|
# Patch files (temporary)
|
||||||
|
*.patch
|
||||||
|
*.diff
|
||||||
|
|
||||||
# JetBrains Rider
|
# JetBrains Rider
|
||||||
*.sln.iml
|
*.sln.iml
|
||||||
/Core/obj/Debug/net8.0/Core.csproj.FileListAbsolute.txt
|
/Core/obj/Debug/net8.0/Core.csproj.FileListAbsolute.txt
|
||||||
|
|||||||
@ -23,6 +23,10 @@ namespace Models.Interfaces
|
|||||||
Task<(int Id, string Expeditionnumber)> CreateFullExpeditionAsync(ELSExpeditionHeader expedition, int formSeriesId);
|
Task<(int Id, string Expeditionnumber)> CreateFullExpeditionAsync(ELSExpeditionHeader expedition, int formSeriesId);
|
||||||
Task<ExpeditionDto?> GetDtoByIdAsync(int id);
|
Task<ExpeditionDto?> GetDtoByIdAsync(int id);
|
||||||
Task<PagedResult<ExpeditionDto>> SearchAsync(string? expeditionNumber, string? status, DateTime? issueDateFrom, DateTime? issueDateTo, int? locationId, int page, int pageSize);
|
Task<PagedResult<ExpeditionDto>> SearchAsync(string? expeditionNumber, string? status, DateTime? issueDateFrom, DateTime? issueDateTo, int? locationId, int page, int pageSize);
|
||||||
|
/// <summary>
|
||||||
|
/// Pasa la expedición a En tránsito y crea las reservas de stock asociadas.
|
||||||
|
/// La operación es transaccional y falla completa si detecta inconsistencias.
|
||||||
|
/// </summary>
|
||||||
Task MarkInTransitAsync(int expeditionId);
|
Task MarkInTransitAsync(int expeditionId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -406,7 +406,11 @@ namespace Models.Repositories.Stock
|
|||||||
}
|
}
|
||||||
public async Task MarkInTransitAsync(int expeditionId)
|
public async Task MarkInTransitAsync(int expeditionId)
|
||||||
{
|
{
|
||||||
|
const byte expeditionReservationSourceType = 1;
|
||||||
|
const int reservedStatus = 1;
|
||||||
|
|
||||||
var header = await _context.PhLsmExpeditionHeaders
|
var header = await _context.PhLsmExpeditionHeaders
|
||||||
|
.Include(x => x.PhLsmExpeditionDetails)
|
||||||
.FirstOrDefaultAsync(x => x.Id == expeditionId);
|
.FirstOrDefaultAsync(x => x.Id == expeditionId);
|
||||||
|
|
||||||
if (header == null)
|
if (header == null)
|
||||||
@ -415,11 +419,150 @@ namespace Models.Repositories.Stock
|
|||||||
if (header.Status != (int)ExpeditionStatus.Emitida)
|
if (header.Status != (int)ExpeditionStatus.Emitida)
|
||||||
throw new InvalidOperationException("Solo las expediciones en estado 'Emitida' pueden pasar a 'En tránsito'.");
|
throw new InvalidOperationException("Solo las expediciones en estado 'Emitida' pueden pasar a 'En tránsito'.");
|
||||||
|
|
||||||
header.Status = (int)ExpeditionStatus.EnTransito;
|
var details = header.PhLsmExpeditionDetails?.ToList() ?? new List<PhLsmExpeditionDetail>();
|
||||||
header.Modifiedat = DateTime.Now;
|
|
||||||
|
|
||||||
await _context.SaveChangesAsync();
|
if (details.Count == 0)
|
||||||
|
throw new InvalidOperationException("No se puede pasar la expedición a 'En tránsito' porque no tiene ítems para reservar.");
|
||||||
|
|
||||||
|
var invalidStockItems = details
|
||||||
|
.Where(d => d.StockitemId <= 0)
|
||||||
|
.Select(d => d.Id)
|
||||||
|
.OrderBy(x => x)
|
||||||
|
.ToList();
|
||||||
|
if (invalidStockItems.Count > 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"No se puede pasar la expedición a 'En tránsito' porque existen detalles sin stockitem_id válido. " +
|
||||||
|
$"Detalle(s): {string.Join(", ", invalidStockItems)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var duplicateStockItems = details
|
||||||
|
.GroupBy(d => d.StockitemId)
|
||||||
|
.Where(g => g.Count() > 1)
|
||||||
|
.Select(g => g.Key)
|
||||||
|
.OrderBy(x => x)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (duplicateStockItems.Count > 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"No se puede pasar la expedición a 'En tránsito' porque el mismo StockItem aparece más de una vez en la expedición: " +
|
||||||
|
string.Join(", ", duplicateStockItems));
|
||||||
|
}
|
||||||
|
|
||||||
|
var detailByStockItem = details
|
||||||
|
.Select(d => new
|
||||||
|
{
|
||||||
|
DetailId = d.Id,
|
||||||
|
StockitemId = d.StockitemId,
|
||||||
|
Quantity = d.Quantity
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var stockItemIds = detailByStockItem
|
||||||
|
.Select(x => x.StockitemId)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var duplicatedReservations = await _context.PhLsmStockReservations
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(r =>
|
||||||
|
r.SourceType == expeditionReservationSourceType &&
|
||||||
|
r.SourceId == expeditionId &&
|
||||||
|
r.Status == reservedStatus &&
|
||||||
|
stockItemIds.Contains(r.StockitemId))
|
||||||
|
.Select(r => r.StockitemId)
|
||||||
|
.Distinct()
|
||||||
|
.OrderBy(x => x)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
if (duplicatedReservations.Count > 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"La expedición ya posee reservas activas para los siguientes StockItem: " +
|
||||||
|
string.Join(", ", duplicatedReservations));
|
||||||
|
}
|
||||||
|
|
||||||
|
var stockItems = await _context.PhLsmStockItems
|
||||||
|
.Where(x => stockItemIds.Contains(x.Id))
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var stockItemsById = stockItems.ToDictionary(x => x.Id);
|
||||||
|
|
||||||
|
var missingStockItems = stockItemIds
|
||||||
|
.Where(id => !stockItemsById.ContainsKey(id))
|
||||||
|
.OrderBy(x => x)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (missingStockItems.Count > 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"No se puede pasar la expedición a 'En tránsito' porque algunos StockItem no existen: " +
|
||||||
|
string.Join(", ", missingStockItems));
|
||||||
|
}
|
||||||
|
|
||||||
|
var insufficientAvailability = new List<string>();
|
||||||
|
|
||||||
|
foreach (var item in detailByStockItem.OrderBy(x => x.StockitemId))
|
||||||
|
{
|
||||||
|
var stockItem = stockItemsById[item.StockitemId];
|
||||||
|
var availableQuantity = stockItem.Quantity - stockItem.ReservedQuantity;
|
||||||
|
|
||||||
|
if (item.Quantity > availableQuantity)
|
||||||
|
{
|
||||||
|
insufficientAvailability.Add(
|
||||||
|
$"• StockItem {item.StockitemId} → solicitado: {item.Quantity}, disponible: {availableQuantity}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (insufficientAvailability.Count > 0)
|
||||||
|
{
|
||||||
|
var lines = new List<string>
|
||||||
|
{
|
||||||
|
"No se puede pasar la expedición a 'En tránsito' porque algunos StockItem no tienen cantidad disponible suficiente para reservar."
|
||||||
|
};
|
||||||
|
|
||||||
|
lines.AddRange(insufficientAvailability);
|
||||||
|
|
||||||
|
throw new InvalidOperationException(string.Join(Environment.NewLine, lines));
|
||||||
|
}
|
||||||
|
|
||||||
|
using var tx = await _context.Database.BeginTransactionAsync();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var now = DateTime.Now;
|
||||||
|
|
||||||
|
var reservations = detailByStockItem.Select(item => new PhLsmStockReservation
|
||||||
|
{
|
||||||
|
SourceType = expeditionReservationSourceType,
|
||||||
|
SourceId = expeditionId,
|
||||||
|
StockitemId = item.StockitemId,
|
||||||
|
ReservedQuantity = item.Quantity,
|
||||||
|
Status = reservedStatus,
|
||||||
|
Createdat = now
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
_context.PhLsmStockReservations.AddRange(reservations);
|
||||||
|
|
||||||
|
foreach (var item in detailByStockItem)
|
||||||
|
{
|
||||||
|
var stockItem = stockItemsById[item.StockitemId];
|
||||||
|
stockItem.ReservedQuantity += item.Quantity;
|
||||||
|
stockItem.Modifiedat = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
header.Status = (int)ExpeditionStatus.EnTransito;
|
||||||
|
header.Modifiedat = now;
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
await tx.CommitAsync();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await tx.RollbackAsync();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
|
|
||||||
namespace Models.Repositories.Stock
|
|
||||||
{
|
|
||||||
internal class PhLSMStockReservationRepository
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user