feat(expeditions): persist stockitem_id in ExpeditionDetails (traceability base) #4

Merged
leandro merged 1 commits from feature/leandro/3-persist-stockitemid-expeditiondetail into master 2026-03-03 02:56:16 +00:00
8 changed files with 109 additions and 17 deletions

View File

@ -7,6 +7,7 @@
public class StockSnapshotItem
{
public int ProductId { get; set; }
public int StockitemId { get; set; }
public string? ProductName { get; set; } = string.Empty;
public int LocationId { get; set; }
public string Batch { get; set; } = string.Empty;

View File

@ -17,6 +17,11 @@
/// </summary>
public int ProductId { get; set; }
/// <summary>
/// Referencia a StockItem (PhLSM_StockItem)
/// </summary>
public int StockitemId { get; set; }
/// <summary>
/// Cantidad solicitada del producto
/// </summary>

View File

@ -20,6 +20,11 @@ public partial class PhLsmExpeditionDetail
/// </summary>
public int ProductId { get; set; }
/// <summary>
/// Referencia a StockItem (PhLSM_StockItem)
/// </summary>
public int StockitemId { get; set; }
/// <summary>
/// Cantidad solicitada del producto
/// </summary>
@ -68,4 +73,6 @@ public partial class PhLsmExpeditionDetail
public virtual PhLsmExpeditionHeader Expedition { get; set; } = null!;
public virtual PhLsmProduct Product { get; set; } = null!;
public virtual PhLsmStockItem Stockitem { get; set; } = null!;
}

View File

@ -67,5 +67,9 @@ public partial class PhLsmStockItem
public virtual PhLsmStockLocation Location { get; set; } = null!;
public virtual ICollection<PhLsmExpeditionDetail> PhLsmExpeditionDetails { get; set; } = new List<PhLsmExpeditionDetail>();
public virtual ICollection<PhLsmStockReservation> PhLsmStockReservations { get; set; } = new List<PhLsmStockReservation>();
public virtual PhLsmProduct Product { get; set; } = null!;
}

View File

@ -1,4 +1,7 @@
namespace Models.Models;
using System;
using System.Collections.Generic;
namespace Models.Models;
/// <summary>
/// Reservas de stock por origen genérico (source_type/source_id). Cada fila bloquea cantidad sobre un StockItem. No duplica lote/serie/vencimiento; se resuelve por JOIN a PhLSM_StockItem.

View File

@ -6,10 +6,6 @@ namespace Models.Models;
public partial class PhronCareOperationsHubContext : DbContext
{
public PhronCareOperationsHubContext()
{
}
public PhronCareOperationsHubContext(DbContextOptions<PhronCareOperationsHubContext> options)
: base(options)
{
@ -35,6 +31,8 @@ public partial class PhronCareOperationsHubContext : DbContext
public virtual DbSet<PhLsmStockOut> PhLsmStockOuts { get; set; }
public virtual DbSet<PhLsmStockReservation> PhLsmStockReservations { get; set; }
public virtual DbSet<PhLsmUnitOfMeasure> PhLsmUnitOfMeasures { get; set; }
public virtual DbSet<PhOhArcadocumentType> PhOhArcadocumentTypes { get; set; }
@ -95,17 +93,6 @@ public partial class PhronCareOperationsHubContext : DbContext
public virtual DbSet<PhSQuoteTaxis> PhSQuoteTaxes { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
#region VERSION DOCKER
{
if (!optionsBuilder.IsConfigured)
{
// Dejarlo vacío para usar la configuración externa desde Program.cs o Startup.cs
}
}
#endregion
//=> optionsBuilder.UseSqlServer("data source=srv01.saludlab.com.ar,39458;initial catalog=phronCare_OperationsHub;User ID=sa;Password=HS|s[~xxQzTo/n>9jO;encrypt=False;trustServerCertificate=True;MultipleActiveResultSets=True");
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.UseCollation("Modern_Spanish_CI_AS");
@ -116,6 +103,14 @@ public partial class PhronCareOperationsHubContext : DbContext
entity.ToTable("PhLSM_ExpeditionDetails");
entity.HasIndex(e => e.ExpeditionId, "IX_PhLSM_ExpeditionDetails_Expedition");
entity.HasIndex(e => new { e.ExpeditionId, e.StockitemId }, "IX_PhLSM_ExpeditionDetails_Expedition_StockItem");
entity.HasIndex(e => e.ProductId, "IX_PhLSM_ExpeditionDetails_Product");
entity.HasIndex(e => e.StockitemId, "IX_PhLSM_ExpeditionDetails_StockItem");
entity.Property(e => e.Id)
.HasComment("Identificador interno del ítem de expedición")
.HasColumnName("id");
@ -162,6 +157,9 @@ public partial class PhronCareOperationsHubContext : DbContext
.HasMaxLength(100)
.HasComment("Número de serie de la unidad individual, según etiqueta de trazabilidad del fabricante.")
.HasColumnName("serial");
entity.Property(e => e.StockitemId)
.HasComment("Referencia a StockItem (PhLSM_StockItem)")
.HasColumnName("stockitem_id");
entity.HasOne(d => d.Expedition).WithMany(p => p.PhLsmExpeditionDetails)
.HasForeignKey(d => d.ExpeditionId)
@ -172,6 +170,11 @@ public partial class PhronCareOperationsHubContext : DbContext
.HasForeignKey(d => d.ProductId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_PhLSM_ExpeditionDetails_PhLSM_Product");
entity.HasOne(d => d.Stockitem).WithMany(p => p.PhLsmExpeditionDetails)
.HasForeignKey(d => d.StockitemId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_PhLSM_ExpeditionDetails_PhLSM_StockItem");
});
modelBuilder.Entity<PhLsmExpeditionHeader>(entity =>
@ -180,6 +183,8 @@ public partial class PhronCareOperationsHubContext : DbContext
entity.ToTable("PhLSM_ExpeditionHeaders");
entity.HasIndex(e => e.Expeditionnumber, "UX_PhLSM_ExpeditionHeaders_Number").IsUnique();
entity.Property(e => e.Id)
.HasComment("Identificador interno de la expedición")
.HasColumnName("id");
@ -641,6 +646,64 @@ public partial class PhronCareOperationsHubContext : DbContext
.HasConstraintName("FK_PhLSM_StockOut_PhLSM_Product");
});
modelBuilder.Entity<PhLsmStockReservation>(entity =>
{
entity.ToTable("PhLSM_StockReservation", tb => tb.HasComment("Reservas de stock por origen genérico (source_type/source_id). Cada fila bloquea cantidad sobre un StockItem. No duplica lote/serie/vencimiento; se resuelve por JOIN a PhLSM_StockItem."));
entity.HasIndex(e => new { e.SourceType, e.SourceId, e.Status }, "IX_PhLSM_StockReservation_Source_Status");
entity.HasIndex(e => e.StockitemId, "IX_PhLSM_StockReservation_StockItem_Reserved").HasFilter("([status]=(1))");
entity.HasIndex(e => new { e.SourceType, e.SourceId, e.StockitemId }, "UX_PhLSM_StockReservation_Source_StockItem_Consumed")
.IsUnique()
.HasFilter("([status]=(3))");
entity.HasIndex(e => new { e.SourceType, e.SourceId, e.StockitemId }, "UX_PhLSM_StockReservation_Source_StockItem_Reserved")
.IsUnique()
.HasFilter("([status]=(1))");
entity.Property(e => e.Id)
.HasComment("Identificador autoincremental de la reserva.")
.HasColumnName("id");
entity.Property(e => e.Createdat)
.HasPrecision(0)
.HasDefaultValueSql("(sysutcdatetime())")
.HasComment("Fecha/hora de creación (UTC).")
.HasColumnName("createdat");
entity.Property(e => e.Modifiedat)
.HasPrecision(0)
.HasComment("Última modificación (UTC). Puede ser NULL si nunca se actualizó.")
.HasColumnName("modifiedat");
entity.Property(e => e.ReservedQuantity)
.HasComment("Cantidad reservada (bloqueada). No disponible mientras status=1 (Reserved).")
.HasColumnType("decimal(18, 2)")
.HasColumnName("reserved_quantity");
entity.Property(e => e.Rowversion)
.IsRowVersion()
.IsConcurrencyToken()
.HasComment("Token de concurrencia optimista (ROWVERSION) para actualizaciones seguras.")
.HasColumnName("rowversion");
entity.Property(e => e.SourceId)
.HasComment("Identificador del origen. Ej.: expedition_id cuando source_type=1.")
.HasColumnName("source_id");
entity.Property(e => e.SourceType)
.HasDefaultValue((byte)1)
.HasComment("Tipo de origen de la reserva. 1=Expedition (extensible a futuros orígenes).")
.HasColumnName("source_type");
entity.Property(e => e.Status)
.HasDefaultValue(1)
.HasComment("Estado de la reserva: 1=Reserved, 2=Released, 3=Consumed.")
.HasColumnName("status");
entity.Property(e => e.StockitemId)
.HasComment("Referencia al StockItem exacto bloqueado (FK a PhLSM_StockItem). Define producto/ubicación/trazabilidad por JOIN.")
.HasColumnName("stockitem_id");
entity.HasOne(d => d.Stockitem).WithMany(p => p.PhLsmStockReservations)
.HasForeignKey(d => d.StockitemId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_PhLSM_StockReservation_StockItem");
});
modelBuilder.Entity<PhLsmUnitOfMeasure>(entity =>
{
entity.HasKey(e => e.Id).HasName("PK__PhLSM_Un__3213E83FD70349B6");

View File

@ -360,6 +360,12 @@
existing.Expiration = exp;
existing.LocationId = s.LocationId;
existing.TraceabilityType = s.TraceabilityType; // UI only
// 🆕 AGREGAR ESTO
if (s.StockItemId != 0 && existing.StockitemId != s.StockItemId)
{
existing.StockitemId = s.StockItemId;
}
}
else
{
@ -374,7 +380,8 @@
Expiration = exp,
TraceabilityType = s.TraceabilityType, // UI only (no DB)
Serial = s.Serial,
LocationId = s.LocationId
LocationId = s.LocationId,
StockitemId = s.StockItemId
});
}
// si newQty == 0 y no existía, no hacemos nada
@ -397,6 +404,7 @@
return new StockSnapshotItem
{
ProductId = d.ProductId,
StockitemId = d.StockitemId, // 🆕 incluir StockitemId en el snapshot
ProductName = d.ProductName,
LocationId = d.LocationId,
Batch = d.Batch ?? string.Empty,

View File

@ -114,6 +114,7 @@
StockList = Snapshot.Select(s => new StockDisplayRow
{
ProductId = s.ProductId,
StockItemId = s.StockitemId,
ProductName = s.ProductName ?? "<Producto sin descripción>",
Batch = s.Batch,
Serial = s.Serial,