ffeat(expeditions): persist stockitem_id in ExpeditionDetails (traceability base)
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 6m37s
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 6m37s
- Added stockitem_id column to PhLSM_ExpeditionDetails - Added FK to PhLSM_StockItem - Added indexes (StockItem and Expedition_StockItem) - Updated scaffold models - Updated UI merge to preserve StockItemId - CreateFullExpeditionAsync now persists stockitem_id - Base step to enable logistic states and double-trace prevention Closes #3
This commit is contained in:
parent
6e22969787
commit
394c864dfa
@ -7,6 +7,7 @@
|
|||||||
public class StockSnapshotItem
|
public class StockSnapshotItem
|
||||||
{
|
{
|
||||||
public int ProductId { get; set; }
|
public int ProductId { get; set; }
|
||||||
|
public int StockitemId { get; set; }
|
||||||
public string? ProductName { get; set; } = string.Empty;
|
public string? ProductName { get; set; } = string.Empty;
|
||||||
public int LocationId { get; set; }
|
public int LocationId { get; set; }
|
||||||
public string Batch { get; set; } = string.Empty;
|
public string Batch { get; set; } = string.Empty;
|
||||||
|
|||||||
@ -17,6 +17,11 @@
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int ProductId { get; set; }
|
public int ProductId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Referencia a StockItem (PhLSM_StockItem)
|
||||||
|
/// </summary>
|
||||||
|
public int StockitemId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cantidad solicitada del producto
|
/// Cantidad solicitada del producto
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -20,6 +20,11 @@ public partial class PhLsmExpeditionDetail
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int ProductId { get; set; }
|
public int ProductId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Referencia a StockItem (PhLSM_StockItem)
|
||||||
|
/// </summary>
|
||||||
|
public int StockitemId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cantidad solicitada del producto
|
/// Cantidad solicitada del producto
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -68,4 +73,6 @@ public partial class PhLsmExpeditionDetail
|
|||||||
public virtual PhLsmExpeditionHeader Expedition { get; set; } = null!;
|
public virtual PhLsmExpeditionHeader Expedition { get; set; } = null!;
|
||||||
|
|
||||||
public virtual PhLsmProduct Product { get; set; } = null!;
|
public virtual PhLsmProduct Product { get; set; } = null!;
|
||||||
|
|
||||||
|
public virtual PhLsmStockItem Stockitem { get; set; } = null!;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -67,5 +67,9 @@ public partial class PhLsmStockItem
|
|||||||
|
|
||||||
public virtual PhLsmStockLocation Location { get; set; } = null!;
|
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!;
|
public virtual PhLsmProduct Product { get; set; } = null!;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
namespace Models.Models;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Models.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <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.
|
/// 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.
|
||||||
|
|||||||
@ -6,10 +6,6 @@ namespace Models.Models;
|
|||||||
|
|
||||||
public partial class PhronCareOperationsHubContext : DbContext
|
public partial class PhronCareOperationsHubContext : DbContext
|
||||||
{
|
{
|
||||||
public PhronCareOperationsHubContext()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public PhronCareOperationsHubContext(DbContextOptions<PhronCareOperationsHubContext> options)
|
public PhronCareOperationsHubContext(DbContextOptions<PhronCareOperationsHubContext> options)
|
||||||
: base(options)
|
: base(options)
|
||||||
{
|
{
|
||||||
@ -35,6 +31,8 @@ public partial class PhronCareOperationsHubContext : DbContext
|
|||||||
|
|
||||||
public virtual DbSet<PhLsmStockOut> PhLsmStockOuts { get; set; }
|
public virtual DbSet<PhLsmStockOut> PhLsmStockOuts { get; set; }
|
||||||
|
|
||||||
|
public virtual DbSet<PhLsmStockReservation> PhLsmStockReservations { get; set; }
|
||||||
|
|
||||||
public virtual DbSet<PhLsmUnitOfMeasure> PhLsmUnitOfMeasures { get; set; }
|
public virtual DbSet<PhLsmUnitOfMeasure> PhLsmUnitOfMeasures { get; set; }
|
||||||
|
|
||||||
public virtual DbSet<PhOhArcadocumentType> PhOhArcadocumentTypes { 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; }
|
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)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
modelBuilder.UseCollation("Modern_Spanish_CI_AS");
|
modelBuilder.UseCollation("Modern_Spanish_CI_AS");
|
||||||
@ -116,6 +103,14 @@ public partial class PhronCareOperationsHubContext : DbContext
|
|||||||
|
|
||||||
entity.ToTable("PhLSM_ExpeditionDetails");
|
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)
|
entity.Property(e => e.Id)
|
||||||
.HasComment("Identificador interno del ítem de expedición")
|
.HasComment("Identificador interno del ítem de expedición")
|
||||||
.HasColumnName("id");
|
.HasColumnName("id");
|
||||||
@ -162,6 +157,9 @@ public partial class PhronCareOperationsHubContext : DbContext
|
|||||||
.HasMaxLength(100)
|
.HasMaxLength(100)
|
||||||
.HasComment("Número de serie de la unidad individual, según etiqueta de trazabilidad del fabricante.")
|
.HasComment("Número de serie de la unidad individual, según etiqueta de trazabilidad del fabricante.")
|
||||||
.HasColumnName("serial");
|
.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)
|
entity.HasOne(d => d.Expedition).WithMany(p => p.PhLsmExpeditionDetails)
|
||||||
.HasForeignKey(d => d.ExpeditionId)
|
.HasForeignKey(d => d.ExpeditionId)
|
||||||
@ -172,6 +170,11 @@ public partial class PhronCareOperationsHubContext : DbContext
|
|||||||
.HasForeignKey(d => d.ProductId)
|
.HasForeignKey(d => d.ProductId)
|
||||||
.OnDelete(DeleteBehavior.ClientSetNull)
|
.OnDelete(DeleteBehavior.ClientSetNull)
|
||||||
.HasConstraintName("FK_PhLSM_ExpeditionDetails_PhLSM_Product");
|
.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 =>
|
modelBuilder.Entity<PhLsmExpeditionHeader>(entity =>
|
||||||
@ -180,6 +183,8 @@ public partial class PhronCareOperationsHubContext : DbContext
|
|||||||
|
|
||||||
entity.ToTable("PhLSM_ExpeditionHeaders");
|
entity.ToTable("PhLSM_ExpeditionHeaders");
|
||||||
|
|
||||||
|
entity.HasIndex(e => e.Expeditionnumber, "UX_PhLSM_ExpeditionHeaders_Number").IsUnique();
|
||||||
|
|
||||||
entity.Property(e => e.Id)
|
entity.Property(e => e.Id)
|
||||||
.HasComment("Identificador interno de la expedición")
|
.HasComment("Identificador interno de la expedición")
|
||||||
.HasColumnName("id");
|
.HasColumnName("id");
|
||||||
@ -641,6 +646,64 @@ public partial class PhronCareOperationsHubContext : DbContext
|
|||||||
.HasConstraintName("FK_PhLSM_StockOut_PhLSM_Product");
|
.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 =>
|
modelBuilder.Entity<PhLsmUnitOfMeasure>(entity =>
|
||||||
{
|
{
|
||||||
entity.HasKey(e => e.Id).HasName("PK__PhLSM_Un__3213E83FD70349B6");
|
entity.HasKey(e => e.Id).HasName("PK__PhLSM_Un__3213E83FD70349B6");
|
||||||
|
|||||||
@ -360,6 +360,12 @@
|
|||||||
existing.Expiration = exp;
|
existing.Expiration = exp;
|
||||||
existing.LocationId = s.LocationId;
|
existing.LocationId = s.LocationId;
|
||||||
existing.TraceabilityType = s.TraceabilityType; // UI only
|
existing.TraceabilityType = s.TraceabilityType; // UI only
|
||||||
|
|
||||||
|
// 🆕 AGREGAR ESTO
|
||||||
|
if (s.StockItemId != 0 && existing.StockitemId != s.StockItemId)
|
||||||
|
{
|
||||||
|
existing.StockitemId = s.StockItemId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -374,7 +380,8 @@
|
|||||||
Expiration = exp,
|
Expiration = exp,
|
||||||
TraceabilityType = s.TraceabilityType, // UI only (no DB)
|
TraceabilityType = s.TraceabilityType, // UI only (no DB)
|
||||||
Serial = s.Serial,
|
Serial = s.Serial,
|
||||||
LocationId = s.LocationId
|
LocationId = s.LocationId,
|
||||||
|
StockitemId = s.StockItemId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// si newQty == 0 y no existía, no hacemos nada
|
// si newQty == 0 y no existía, no hacemos nada
|
||||||
@ -397,6 +404,7 @@
|
|||||||
return new StockSnapshotItem
|
return new StockSnapshotItem
|
||||||
{
|
{
|
||||||
ProductId = d.ProductId,
|
ProductId = d.ProductId,
|
||||||
|
StockitemId = d.StockitemId, // 🆕 incluir StockitemId en el snapshot
|
||||||
ProductName = d.ProductName,
|
ProductName = d.ProductName,
|
||||||
LocationId = d.LocationId,
|
LocationId = d.LocationId,
|
||||||
Batch = d.Batch ?? string.Empty,
|
Batch = d.Batch ?? string.Empty,
|
||||||
|
|||||||
@ -114,6 +114,7 @@
|
|||||||
StockList = Snapshot.Select(s => new StockDisplayRow
|
StockList = Snapshot.Select(s => new StockDisplayRow
|
||||||
{
|
{
|
||||||
ProductId = s.ProductId,
|
ProductId = s.ProductId,
|
||||||
|
StockItemId = s.StockitemId,
|
||||||
ProductName = s.ProductName ?? "<Producto sin descripción>",
|
ProductName = s.ProductName ?? "<Producto sin descripción>",
|
||||||
Batch = s.Batch,
|
Batch = s.Batch,
|
||||||
Serial = s.Serial,
|
Serial = s.Serial,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user