feat: Migrate MaterialDimensions from TPH to TPC and add Alro catalog seeding

Switch MaterialDimensions inheritance from TPH (single table with discriminator)
to TPC (table per concrete type) with individual tables per shape. Add Swagger
for dev API exploration, expand SeedController with export/import endpoints and
Alro catalog JSON dataset, and include Python scraper for Alro catalog PDFs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-16 14:22:01 -05:00
parent dac2833dd1
commit f04bf02c42
17 changed files with 6231 additions and 90 deletions

View File

@@ -88,8 +88,8 @@ dotnet clean CutList.sln
- **DisplayName**: "{Shape} - {Size}"
- **Relationships**: `Dimensions` (1:1 MaterialDimensions), `StockItems` (1:many), `JobParts` (1:many)
### MaterialDimensions (TPH Inheritance)
Abstract base; derived types per shape: `RoundBarDimensions`, `RoundTubeDimensions`, `FlatBarDimensions`, `SquareBarDimensions`, `SquareTubeDimensions`, `RectangularTubeDimensions`, `AngleDimensions`, `ChannelDimensions`, `IBeamDimensions`, `PipeDimensions`. Each generates its own `SizeString` and `SortOrder`.
### MaterialDimensions (TPC Inheritance)
Abstract base with TPC (Table Per Concrete type) mapping — each shape gets its own standalone table (`DimAngle`, `DimChannel`, `DimFlatBar`, `DimIBeam`, `DimPipe`, `DimRectangularTube`, `DimRoundBar`, `DimRoundTube`, `DimSquareBar`, `DimSquareTube`) with no base table. Each table has its own `Id` (shared sequence) and `MaterialId` FK. Each generates its own `SizeString` and `SortOrder`.
### StockItem
- `MaterialId`, `LengthInches` (decimal), `QuantityOnHand` (int), `IsActive`

View File

@@ -0,0 +1,72 @@
using System.Text.Json.Serialization;
namespace CutList.Web.Controllers.Dtos;
public class SeedExportData
{
public DateTime ExportedAt { get; set; }
public List<SeedSupplierDto> Suppliers { get; set; } = [];
public List<SeedCuttingToolDto> CuttingTools { get; set; } = [];
public List<SeedMaterialDto> Materials { get; set; } = [];
}
public class SeedSupplierDto
{
public string Name { get; set; } = "";
public string? ContactInfo { get; set; }
public string? Notes { get; set; }
}
public class SeedCuttingToolDto
{
public string Name { get; set; } = "";
public decimal KerfInches { get; set; }
public bool IsDefault { get; set; }
}
public class SeedMaterialDto
{
public string Shape { get; set; } = "";
public string Type { get; set; } = "";
public string? Grade { get; set; }
public string Size { get; set; } = "";
public string? Description { get; set; }
public SeedDimensionsDto? Dimensions { get; set; }
public List<SeedStockItemDto> StockItems { get; set; } = [];
}
public class SeedDimensionsDto
{
public decimal? Diameter { get; set; }
public decimal? OuterDiameter { get; set; }
public decimal? Width { get; set; }
public decimal? Height { get; set; }
public decimal? Thickness { get; set; }
public decimal? Wall { get; set; }
public decimal? Size { get; set; }
public decimal? Leg1 { get; set; }
public decimal? Leg2 { get; set; }
public decimal? Flange { get; set; }
public decimal? Web { get; set; }
public decimal? WeightPerFoot { get; set; }
public decimal? NominalSize { get; set; }
public string? Schedule { get; set; }
}
public class SeedStockItemDto
{
public decimal LengthInches { get; set; }
public string? Name { get; set; }
public int QuantityOnHand { get; set; }
public string? Notes { get; set; }
public List<SeedSupplierOfferingDto> SupplierOfferings { get; set; } = [];
}
public class SeedSupplierOfferingDto
{
public string SupplierName { get; set; } = "";
public string? PartNumber { get; set; }
public string? SupplierDescription { get; set; }
public decimal? Price { get; set; }
public string? Notes { get; set; }
}

View File

@@ -0,0 +1,277 @@
using CutList.Web.Controllers.Dtos;
using CutList.Web.Data;
using CutList.Web.Data.Entities;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace CutList.Web.Controllers;
[ApiController]
[Route("api/[controller]")]
public class SeedController : ControllerBase
{
private readonly ApplicationDbContext _context;
public SeedController(ApplicationDbContext context)
{
_context = context;
}
[HttpGet("export")]
public async Task<ActionResult<SeedExportData>> Export()
{
var materials = await _context.Materials
.Include(m => m.Dimensions)
.Include(m => m.StockItems.Where(s => s.IsActive))
.ThenInclude(s => s.SupplierOfferings.Where(o => o.IsActive))
.Where(m => m.IsActive)
.OrderBy(m => m.Shape).ThenBy(m => m.SortOrder)
.AsNoTracking()
.ToListAsync();
var suppliers = await _context.Suppliers
.Where(s => s.IsActive)
.OrderBy(s => s.Name)
.AsNoTracking()
.ToListAsync();
var cuttingTools = await _context.CuttingTools
.Where(t => t.IsActive)
.OrderBy(t => t.Name)
.AsNoTracking()
.ToListAsync();
var export = new SeedExportData
{
ExportedAt = DateTime.UtcNow,
Suppliers = suppliers.Select(s => new SeedSupplierDto
{
Name = s.Name,
ContactInfo = s.ContactInfo,
Notes = s.Notes
}).ToList(),
CuttingTools = cuttingTools.Select(t => new SeedCuttingToolDto
{
Name = t.Name,
KerfInches = t.KerfInches,
IsDefault = t.IsDefault
}).ToList(),
Materials = materials.Select(m => new SeedMaterialDto
{
Shape = m.Shape.ToString(),
Type = m.Type.ToString(),
Grade = m.Grade,
Size = m.Size,
Description = m.Description,
Dimensions = MapDimensionsToDto(m.Dimensions),
StockItems = m.StockItems.OrderBy(s => s.LengthInches).Select(s => new SeedStockItemDto
{
LengthInches = s.LengthInches,
Name = s.Name,
QuantityOnHand = s.QuantityOnHand,
Notes = s.Notes,
SupplierOfferings = s.SupplierOfferings.Select(o => new SeedSupplierOfferingDto
{
SupplierName = suppliers.FirstOrDefault(sup => sup.Id == o.SupplierId)?.Name ?? "Unknown",
PartNumber = o.PartNumber,
SupplierDescription = o.SupplierDescription,
Price = o.Price,
Notes = o.Notes
}).ToList()
}).ToList()
}).ToList()
};
return Ok(export);
}
[HttpPost("import")]
public async Task<ActionResult> Import([FromBody] SeedExportData data)
{
var suppliersCreated = 0;
var toolsCreated = 0;
var materialsCreated = 0;
var materialsSkipped = 0;
var stockCreated = 0;
var offeringsCreated = 0;
// 1. Suppliers - match by name
var supplierMap = new Dictionary<string, Supplier>();
foreach (var dto in data.Suppliers)
{
var existing = await _context.Suppliers.FirstOrDefaultAsync(s => s.Name == dto.Name);
if (existing != null)
{
supplierMap[dto.Name] = existing;
}
else
{
var supplier = new Supplier
{
Name = dto.Name,
ContactInfo = dto.ContactInfo,
Notes = dto.Notes,
CreatedAt = DateTime.UtcNow
};
_context.Suppliers.Add(supplier);
supplierMap[dto.Name] = supplier;
suppliersCreated++;
}
}
await _context.SaveChangesAsync();
// 2. Cutting tools - match by name
foreach (var dto in data.CuttingTools)
{
var exists = await _context.CuttingTools.AnyAsync(t => t.Name == dto.Name);
if (!exists)
{
_context.CuttingTools.Add(new CuttingTool
{
Name = dto.Name,
KerfInches = dto.KerfInches,
IsDefault = dto.IsDefault
});
toolsCreated++;
}
}
await _context.SaveChangesAsync();
// 3. Materials - match by shape + size + grade
foreach (var dto in data.Materials)
{
if (!Enum.TryParse<MaterialShape>(dto.Shape, out var shape))
{
materialsSkipped++;
continue;
}
Enum.TryParse<MaterialType>(dto.Type, out var type);
var existing = await _context.Materials
.Include(m => m.StockItems)
.FirstOrDefaultAsync(m => m.Shape == shape && m.Size == dto.Size && m.Grade == dto.Grade && m.IsActive);
Material material;
if (existing != null)
{
material = existing;
materialsSkipped++;
}
else
{
material = new Material
{
Shape = shape,
Type = type,
Grade = dto.Grade,
Size = dto.Size,
Description = dto.Description,
CreatedAt = DateTime.UtcNow
};
if (dto.Dimensions != null)
material.Dimensions = MapDtoToDimensions(shape, dto.Dimensions);
_context.Materials.Add(material);
await _context.SaveChangesAsync();
materialsCreated++;
}
// 4. Stock items - match by material + length
foreach (var stockDto in dto.StockItems)
{
var existingStock = material.StockItems
.FirstOrDefault(s => s.LengthInches == stockDto.LengthInches && s.IsActive);
StockItem stockItem;
if (existingStock != null)
{
stockItem = existingStock;
}
else
{
stockItem = new StockItem
{
MaterialId = material.Id,
LengthInches = stockDto.LengthInches,
Name = stockDto.Name,
QuantityOnHand = stockDto.QuantityOnHand,
Notes = stockDto.Notes,
CreatedAt = DateTime.UtcNow
};
_context.StockItems.Add(stockItem);
await _context.SaveChangesAsync();
stockCreated++;
}
// 5. Supplier offerings
foreach (var offeringDto in stockDto.SupplierOfferings)
{
if (!supplierMap.TryGetValue(offeringDto.SupplierName, out var supplier))
continue;
var existingOffering = await _context.SupplierOfferings
.AnyAsync(o => o.SupplierId == supplier.Id && o.StockItemId == stockItem.Id);
if (!existingOffering)
{
_context.SupplierOfferings.Add(new SupplierOffering
{
SupplierId = supplier.Id,
StockItemId = stockItem.Id,
PartNumber = offeringDto.PartNumber,
SupplierDescription = offeringDto.SupplierDescription,
Price = offeringDto.Price,
Notes = offeringDto.Notes
});
offeringsCreated++;
}
}
}
}
await _context.SaveChangesAsync();
return Ok(new
{
Message = "Import completed",
SuppliersCreated = suppliersCreated,
CuttingToolsCreated = toolsCreated,
MaterialsCreated = materialsCreated,
MaterialsSkipped = materialsSkipped,
StockItemsCreated = stockCreated,
SupplierOfferingsCreated = offeringsCreated
});
}
private static SeedDimensionsDto? MapDimensionsToDto(MaterialDimensions? dim) => dim switch
{
RoundBarDimensions d => new SeedDimensionsDto { Diameter = d.Diameter },
RoundTubeDimensions d => new SeedDimensionsDto { OuterDiameter = d.OuterDiameter, Wall = d.Wall },
FlatBarDimensions d => new SeedDimensionsDto { Width = d.Width, Thickness = d.Thickness },
SquareBarDimensions d => new SeedDimensionsDto { Size = d.Size },
SquareTubeDimensions d => new SeedDimensionsDto { Size = d.Size, Wall = d.Wall },
RectangularTubeDimensions d => new SeedDimensionsDto { Width = d.Width, Height = d.Height, Wall = d.Wall },
AngleDimensions d => new SeedDimensionsDto { Leg1 = d.Leg1, Leg2 = d.Leg2, Thickness = d.Thickness },
ChannelDimensions d => new SeedDimensionsDto { Height = d.Height, Flange = d.Flange, Web = d.Web },
IBeamDimensions d => new SeedDimensionsDto { Height = d.Height, WeightPerFoot = d.WeightPerFoot },
PipeDimensions d => new SeedDimensionsDto { NominalSize = d.NominalSize, Wall = d.Wall, Schedule = d.Schedule },
_ => null
};
private static MaterialDimensions? MapDtoToDimensions(MaterialShape shape, SeedDimensionsDto dto) => shape switch
{
MaterialShape.RoundBar => new RoundBarDimensions { Diameter = dto.Diameter ?? 0 },
MaterialShape.RoundTube => new RoundTubeDimensions { OuterDiameter = dto.OuterDiameter ?? 0, Wall = dto.Wall ?? 0 },
MaterialShape.FlatBar => new FlatBarDimensions { Width = dto.Width ?? 0, Thickness = dto.Thickness ?? 0 },
MaterialShape.SquareBar => new SquareBarDimensions { Size = dto.Size ?? 0 },
MaterialShape.SquareTube => new SquareTubeDimensions { Size = dto.Size ?? 0, Wall = dto.Wall ?? 0 },
MaterialShape.RectangularTube => new RectangularTubeDimensions { Width = dto.Width ?? 0, Height = dto.Height ?? 0, Wall = dto.Wall ?? 0 },
MaterialShape.Angle => new AngleDimensions { Leg1 = dto.Leg1 ?? 0, Leg2 = dto.Leg2 ?? 0, Thickness = dto.Thickness ?? 0 },
MaterialShape.Channel => new ChannelDimensions { Height = dto.Height ?? 0, Flange = dto.Flange ?? 0, Web = dto.Web ?? 0 },
MaterialShape.IBeam => new IBeamDimensions { Height = dto.Height ?? 0, WeightPerFoot = dto.WeightPerFoot ?? 0 },
MaterialShape.Pipe => new PipeDimensions { NominalSize = dto.NominalSize ?? 0, Wall = dto.Wall ?? 0, Schedule = dto.Schedule },
_ => null
};
}

View File

@@ -16,6 +16,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.3" />
</ItemGroup>
</Project>

View File

@@ -47,84 +47,80 @@ public class ApplicationDbContext : DbContext
entity.Property(e => e.CreatedAt).HasDefaultValueSql("GETUTCDATE()");
});
// MaterialDimensions - TPH inheritance
// MaterialDimensions - TPC inheritance (each shape gets its own table, no base table)
modelBuilder.Entity<MaterialDimensions>(entity =>
{
entity.HasKey(e => e.Id);
entity.UseTpcMappingStrategy();
// 1:1 relationship with Material
entity.HasOne(e => e.Material)
.WithOne(m => m.Dimensions)
.HasForeignKey<MaterialDimensions>(e => e.MaterialId)
.OnDelete(DeleteBehavior.Cascade);
// TPH discriminator
entity.HasDiscriminator<string>("DimensionType")
.HasValue<RoundBarDimensions>("RoundBar")
.HasValue<RoundTubeDimensions>("RoundTube")
.HasValue<FlatBarDimensions>("FlatBar")
.HasValue<SquareBarDimensions>("SquareBar")
.HasValue<SquareTubeDimensions>("SquareTube")
.HasValue<RectangularTubeDimensions>("RectangularTube")
.HasValue<AngleDimensions>("Angle")
.HasValue<ChannelDimensions>("Channel")
.HasValue<IBeamDimensions>("IBeam")
.HasValue<PipeDimensions>("Pipe");
});
// Configure each dimension type's properties
modelBuilder.Entity<RoundBarDimensions>(entity =>
{
entity.ToTable("DimRoundBar");
entity.Property(e => e.Diameter).HasPrecision(10, 4);
entity.HasIndex(e => e.Diameter);
});
modelBuilder.Entity<RoundTubeDimensions>(entity =>
{
entity.ToTable("DimRoundTube");
entity.Property(e => e.OuterDiameter).HasPrecision(10, 4);
entity.Property(e => e.Wall).HasColumnName("Wall").HasPrecision(10, 4);
entity.Property(e => e.Wall).HasPrecision(10, 4);
entity.HasIndex(e => e.OuterDiameter);
});
modelBuilder.Entity<FlatBarDimensions>(entity =>
{
entity.Property(e => e.Width).HasColumnName("Width").HasPrecision(10, 4);
entity.Property(e => e.Thickness).HasColumnName("Thickness").HasPrecision(10, 4);
entity.ToTable("DimFlatBar");
entity.Property(e => e.Width).HasPrecision(10, 4);
entity.Property(e => e.Thickness).HasPrecision(10, 4);
entity.HasIndex(e => e.Width);
});
modelBuilder.Entity<SquareBarDimensions>(entity =>
{
entity.Property(e => e.Size).HasColumnName("Size").HasPrecision(10, 4);
entity.ToTable("DimSquareBar");
entity.Property(e => e.Size).HasPrecision(10, 4);
entity.HasIndex(e => e.Size);
});
modelBuilder.Entity<SquareTubeDimensions>(entity =>
{
entity.Property(e => e.Size).HasColumnName("Size").HasPrecision(10, 4);
entity.Property(e => e.Wall).HasColumnName("Wall").HasPrecision(10, 4);
entity.ToTable("DimSquareTube");
entity.Property(e => e.Size).HasPrecision(10, 4);
entity.Property(e => e.Wall).HasPrecision(10, 4);
entity.HasIndex(e => e.Size);
});
modelBuilder.Entity<RectangularTubeDimensions>(entity =>
{
entity.Property(e => e.Width).HasColumnName("Width").HasPrecision(10, 4);
entity.Property(e => e.Height).HasColumnName("Height").HasPrecision(10, 4);
entity.Property(e => e.Wall).HasColumnName("Wall").HasPrecision(10, 4);
entity.ToTable("DimRectangularTube");
entity.Property(e => e.Width).HasPrecision(10, 4);
entity.Property(e => e.Height).HasPrecision(10, 4);
entity.Property(e => e.Wall).HasPrecision(10, 4);
entity.HasIndex(e => e.Width);
});
modelBuilder.Entity<AngleDimensions>(entity =>
{
entity.ToTable("DimAngle");
entity.Property(e => e.Leg1).HasPrecision(10, 4);
entity.Property(e => e.Leg2).HasPrecision(10, 4);
entity.Property(e => e.Thickness).HasColumnName("Thickness").HasPrecision(10, 4);
entity.Property(e => e.Thickness).HasPrecision(10, 4);
entity.HasIndex(e => e.Leg1);
});
modelBuilder.Entity<ChannelDimensions>(entity =>
{
entity.Property(e => e.Height).HasColumnName("Height").HasPrecision(10, 4);
entity.ToTable("DimChannel");
entity.Property(e => e.Height).HasPrecision(10, 4);
entity.Property(e => e.Flange).HasPrecision(10, 4);
entity.Property(e => e.Web).HasPrecision(10, 4);
entity.HasIndex(e => e.Height);
@@ -132,15 +128,17 @@ public class ApplicationDbContext : DbContext
modelBuilder.Entity<IBeamDimensions>(entity =>
{
entity.Property(e => e.Height).HasColumnName("Height").HasPrecision(10, 4);
entity.ToTable("DimIBeam");
entity.Property(e => e.Height).HasPrecision(10, 4);
entity.Property(e => e.WeightPerFoot).HasPrecision(10, 4);
entity.HasIndex(e => e.Height);
});
modelBuilder.Entity<PipeDimensions>(entity =>
{
entity.ToTable("DimPipe");
entity.Property(e => e.NominalSize).HasPrecision(10, 4);
entity.Property(e => e.Wall).HasColumnName("Wall").HasPrecision(10, 4);
entity.Property(e => e.Wall).HasPrecision(10, 4);
entity.Property(e => e.Schedule).HasMaxLength(20);
entity.HasIndex(e => e.NominalSize);
});

View File

@@ -0,0 +1,31 @@
{
"exportedAt": "2026-02-16T17:09:52.843008+00:00",
"suppliers": [
{
"name": "Alro Steel"
}
],
"cuttingTools": [
{
"name": "Bandsaw",
"kerfInches": 0.0625,
"isDefault": true
},
{
"name": "Chop Saw",
"kerfInches": 0.125,
"isDefault": false
},
{
"name": "Cold Cut Saw",
"kerfInches": 0.0625,
"isDefault": false
},
{
"name": "Hacksaw",
"kerfInches": 0.0625,
"isDefault": false
}
],
"materials": []
}

View File

@@ -0,0 +1,962 @@
// <auto-generated />
using System;
using CutList.Web.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace CutList.Web.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20260216183131_MaterialDimensionsTPHtoTPT")]
partial class MaterialDimensionsTPHtoTPT
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.11")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("CutList.Web.Data.Entities.CuttingTool", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<bool>("IsDefault")
.HasColumnType("bit");
b.Property<decimal>("KerfInches")
.HasPrecision(6, 4)
.HasColumnType("decimal(6,4)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.HasKey("Id");
b.ToTable("CuttingTools");
b.HasData(
new
{
Id = 1,
IsActive = true,
IsDefault = true,
KerfInches = 0.0625m,
Name = "Bandsaw"
},
new
{
Id = 2,
IsActive = true,
IsDefault = false,
KerfInches = 0.125m,
Name = "Chop Saw"
},
new
{
Id = 3,
IsActive = true,
IsDefault = false,
KerfInches = 0.0625m,
Name = "Cold Cut Saw"
},
new
{
Id = 4,
IsActive = true,
IsDefault = false,
KerfInches = 0.0625m,
Name = "Hacksaw"
});
});
modelBuilder.Entity("CutList.Web.Data.Entities.Job", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<string>("Customer")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int?>("CuttingToolId")
.HasColumnType("int");
b.Property<string>("JobNumber")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<DateTime?>("LockedAt")
.HasColumnType("datetime2");
b.Property<string>("Name")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Notes")
.HasColumnType("nvarchar(max)");
b.Property<string>("OptimizationResultJson")
.HasColumnType("nvarchar(max)");
b.Property<DateTime?>("OptimizedAt")
.HasColumnType("datetime2");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.HasKey("Id");
b.HasIndex("CuttingToolId");
b.HasIndex("JobNumber")
.IsUnique();
b.ToTable("Jobs");
});
modelBuilder.Entity("CutList.Web.Data.Entities.JobPart", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("JobId")
.HasColumnType("int");
b.Property<decimal>("LengthInches")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<int>("MaterialId")
.HasColumnType("int");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int>("Quantity")
.HasColumnType("int");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("JobId");
b.HasIndex("MaterialId");
b.ToTable("JobParts");
});
modelBuilder.Entity("CutList.Web.Data.Entities.JobStock", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<bool>("IsCustomLength")
.HasColumnType("bit");
b.Property<int>("JobId")
.HasColumnType("int");
b.Property<decimal>("LengthInches")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<int>("MaterialId")
.HasColumnType("int");
b.Property<int>("Priority")
.HasColumnType("int");
b.Property<int>("Quantity")
.HasColumnType("int");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.Property<int?>("StockItemId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("JobId");
b.HasIndex("MaterialId");
b.HasIndex("StockItemId");
b.ToTable("JobStocks");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Material", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<string>("Description")
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<string>("Grade")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<string>("Shape")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Size")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.HasKey("Id");
b.ToTable("Materials");
});
modelBuilder.Entity("CutList.Web.Data.Entities.MaterialDimensions", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("MaterialId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("MaterialId")
.IsUnique();
b.ToTable("MaterialDimensions");
b.UseTptMappingStrategy();
});
modelBuilder.Entity("CutList.Web.Data.Entities.PurchaseItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<int?>("JobId")
.HasColumnType("int");
b.Property<string>("Notes")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<int>("Quantity")
.HasColumnType("int");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<int>("StockItemId")
.HasColumnType("int");
b.Property<int?>("SupplierId")
.HasColumnType("int");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.HasKey("Id");
b.HasIndex("JobId");
b.HasIndex("StockItemId");
b.HasIndex("SupplierId");
b.ToTable("PurchaseItems");
});
modelBuilder.Entity("CutList.Web.Data.Entities.StockItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<decimal>("LengthInches")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<int>("MaterialId")
.HasColumnType("int");
b.Property<string>("Name")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Notes")
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<int>("QuantityOnHand")
.HasColumnType("int");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.HasKey("Id");
b.HasIndex("MaterialId", "LengthInches")
.IsUnique();
b.ToTable("StockItems");
});
modelBuilder.Entity("CutList.Web.Data.Entities.StockTransaction", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<int?>("JobId")
.HasColumnType("int");
b.Property<string>("Notes")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<int>("Quantity")
.HasColumnType("int");
b.Property<int>("StockItemId")
.HasColumnType("int");
b.Property<int?>("SupplierId")
.HasColumnType("int");
b.Property<int>("Type")
.HasColumnType("int");
b.Property<decimal?>("UnitPrice")
.HasPrecision(10, 2)
.HasColumnType("decimal(10,2)");
b.HasKey("Id");
b.HasIndex("JobId");
b.HasIndex("StockItemId");
b.HasIndex("SupplierId");
b.ToTable("StockTransactions");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Supplier", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ContactInfo")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Notes")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("Suppliers");
});
modelBuilder.Entity("CutList.Web.Data.Entities.SupplierOffering", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<string>("Notes")
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<string>("PartNumber")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<decimal?>("Price")
.HasPrecision(10, 2)
.HasColumnType("decimal(10,2)");
b.Property<int>("StockItemId")
.HasColumnType("int");
b.Property<string>("SupplierDescription")
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<int>("SupplierId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("StockItemId");
b.HasIndex("SupplierId", "StockItemId")
.IsUnique();
b.ToTable("SupplierOfferings");
});
modelBuilder.Entity("CutList.Web.Data.Entities.AngleDimensions", b =>
{
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
b.Property<decimal>("Leg1")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<decimal>("Leg2")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<decimal>("Thickness")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.HasIndex("Leg1");
b.ToTable("AngleDimensions");
});
modelBuilder.Entity("CutList.Web.Data.Entities.ChannelDimensions", b =>
{
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
b.Property<decimal>("Flange")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<decimal>("Height")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<decimal>("Web")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.HasIndex("Height");
b.ToTable("ChannelDimensions");
});
modelBuilder.Entity("CutList.Web.Data.Entities.FlatBarDimensions", b =>
{
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
b.Property<decimal>("Thickness")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<decimal>("Width")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.HasIndex("Width");
b.ToTable("FlatBarDimensions");
});
modelBuilder.Entity("CutList.Web.Data.Entities.IBeamDimensions", b =>
{
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
b.Property<decimal>("Height")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<decimal>("WeightPerFoot")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.HasIndex("Height");
b.ToTable("IBeamDimensions");
});
modelBuilder.Entity("CutList.Web.Data.Entities.PipeDimensions", b =>
{
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
b.Property<decimal>("NominalSize")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<string>("Schedule")
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<decimal?>("Wall")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.HasIndex("NominalSize");
b.ToTable("PipeDimensions");
});
modelBuilder.Entity("CutList.Web.Data.Entities.RectangularTubeDimensions", b =>
{
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
b.Property<decimal>("Height")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<decimal>("Wall")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<decimal>("Width")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.HasIndex("Width");
b.ToTable("RectangularTubeDimensions");
});
modelBuilder.Entity("CutList.Web.Data.Entities.RoundBarDimensions", b =>
{
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
b.Property<decimal>("Diameter")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.HasIndex("Diameter");
b.ToTable("RoundBarDimensions");
});
modelBuilder.Entity("CutList.Web.Data.Entities.RoundTubeDimensions", b =>
{
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
b.Property<decimal>("OuterDiameter")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<decimal>("Wall")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.HasIndex("OuterDiameter");
b.ToTable("RoundTubeDimensions");
});
modelBuilder.Entity("CutList.Web.Data.Entities.SquareBarDimensions", b =>
{
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
b.Property<decimal>("Size")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.HasIndex("Size");
b.ToTable("SquareBarDimensions");
});
modelBuilder.Entity("CutList.Web.Data.Entities.SquareTubeDimensions", b =>
{
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
b.Property<decimal>("Size")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<decimal>("Wall")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.HasIndex("Size");
b.ToTable("SquareTubeDimensions");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Job", b =>
{
b.HasOne("CutList.Web.Data.Entities.CuttingTool", "CuttingTool")
.WithMany("Jobs")
.HasForeignKey("CuttingToolId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("CuttingTool");
});
modelBuilder.Entity("CutList.Web.Data.Entities.JobPart", b =>
{
b.HasOne("CutList.Web.Data.Entities.Job", "Job")
.WithMany("Parts")
.HasForeignKey("JobId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
.WithMany("JobParts")
.HasForeignKey("MaterialId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Job");
b.Navigation("Material");
});
modelBuilder.Entity("CutList.Web.Data.Entities.JobStock", b =>
{
b.HasOne("CutList.Web.Data.Entities.Job", "Job")
.WithMany("Stock")
.HasForeignKey("JobId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
.WithMany()
.HasForeignKey("MaterialId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("CutList.Web.Data.Entities.StockItem", "StockItem")
.WithMany()
.HasForeignKey("StockItemId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("Job");
b.Navigation("Material");
b.Navigation("StockItem");
});
modelBuilder.Entity("CutList.Web.Data.Entities.MaterialDimensions", b =>
{
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
.WithOne("Dimensions")
.HasForeignKey("CutList.Web.Data.Entities.MaterialDimensions", "MaterialId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Material");
});
modelBuilder.Entity("CutList.Web.Data.Entities.PurchaseItem", b =>
{
b.HasOne("CutList.Web.Data.Entities.Job", "Job")
.WithMany()
.HasForeignKey("JobId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("CutList.Web.Data.Entities.StockItem", "StockItem")
.WithMany()
.HasForeignKey("StockItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("CutList.Web.Data.Entities.Supplier", "Supplier")
.WithMany()
.HasForeignKey("SupplierId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("Job");
b.Navigation("StockItem");
b.Navigation("Supplier");
});
modelBuilder.Entity("CutList.Web.Data.Entities.StockItem", b =>
{
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
.WithMany("StockItems")
.HasForeignKey("MaterialId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Material");
});
modelBuilder.Entity("CutList.Web.Data.Entities.StockTransaction", b =>
{
b.HasOne("CutList.Web.Data.Entities.Job", "Job")
.WithMany()
.HasForeignKey("JobId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("CutList.Web.Data.Entities.StockItem", "StockItem")
.WithMany("Transactions")
.HasForeignKey("StockItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("CutList.Web.Data.Entities.Supplier", "Supplier")
.WithMany()
.HasForeignKey("SupplierId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("Job");
b.Navigation("StockItem");
b.Navigation("Supplier");
});
modelBuilder.Entity("CutList.Web.Data.Entities.SupplierOffering", b =>
{
b.HasOne("CutList.Web.Data.Entities.StockItem", "StockItem")
.WithMany("SupplierOfferings")
.HasForeignKey("StockItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("CutList.Web.Data.Entities.Supplier", "Supplier")
.WithMany("Offerings")
.HasForeignKey("SupplierId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("StockItem");
b.Navigation("Supplier");
});
modelBuilder.Entity("CutList.Web.Data.Entities.AngleDimensions", b =>
{
b.HasOne("CutList.Web.Data.Entities.MaterialDimensions", null)
.WithOne()
.HasForeignKey("CutList.Web.Data.Entities.AngleDimensions", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("CutList.Web.Data.Entities.ChannelDimensions", b =>
{
b.HasOne("CutList.Web.Data.Entities.MaterialDimensions", null)
.WithOne()
.HasForeignKey("CutList.Web.Data.Entities.ChannelDimensions", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("CutList.Web.Data.Entities.FlatBarDimensions", b =>
{
b.HasOne("CutList.Web.Data.Entities.MaterialDimensions", null)
.WithOne()
.HasForeignKey("CutList.Web.Data.Entities.FlatBarDimensions", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("CutList.Web.Data.Entities.IBeamDimensions", b =>
{
b.HasOne("CutList.Web.Data.Entities.MaterialDimensions", null)
.WithOne()
.HasForeignKey("CutList.Web.Data.Entities.IBeamDimensions", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("CutList.Web.Data.Entities.PipeDimensions", b =>
{
b.HasOne("CutList.Web.Data.Entities.MaterialDimensions", null)
.WithOne()
.HasForeignKey("CutList.Web.Data.Entities.PipeDimensions", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("CutList.Web.Data.Entities.RectangularTubeDimensions", b =>
{
b.HasOne("CutList.Web.Data.Entities.MaterialDimensions", null)
.WithOne()
.HasForeignKey("CutList.Web.Data.Entities.RectangularTubeDimensions", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("CutList.Web.Data.Entities.RoundBarDimensions", b =>
{
b.HasOne("CutList.Web.Data.Entities.MaterialDimensions", null)
.WithOne()
.HasForeignKey("CutList.Web.Data.Entities.RoundBarDimensions", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("CutList.Web.Data.Entities.RoundTubeDimensions", b =>
{
b.HasOne("CutList.Web.Data.Entities.MaterialDimensions", null)
.WithOne()
.HasForeignKey("CutList.Web.Data.Entities.RoundTubeDimensions", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("CutList.Web.Data.Entities.SquareBarDimensions", b =>
{
b.HasOne("CutList.Web.Data.Entities.MaterialDimensions", null)
.WithOne()
.HasForeignKey("CutList.Web.Data.Entities.SquareBarDimensions", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("CutList.Web.Data.Entities.SquareTubeDimensions", b =>
{
b.HasOne("CutList.Web.Data.Entities.MaterialDimensions", null)
.WithOne()
.HasForeignKey("CutList.Web.Data.Entities.SquareTubeDimensions", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("CutList.Web.Data.Entities.CuttingTool", b =>
{
b.Navigation("Jobs");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Job", b =>
{
b.Navigation("Parts");
b.Navigation("Stock");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Material", b =>
{
b.Navigation("Dimensions");
b.Navigation("JobParts");
b.Navigation("StockItems");
});
modelBuilder.Entity("CutList.Web.Data.Entities.StockItem", b =>
{
b.Navigation("SupplierOfferings");
b.Navigation("Transactions");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Supplier", b =>
{
b.Navigation("Offerings");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,353 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CutList.Web.Migrations
{
/// <inheritdoc />
public partial class MaterialDimensionsTPHtoTPT : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
// 1. Create the new TPT tables first (before dropping any columns)
migrationBuilder.CreateTable(
name: "AngleDimensions",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false),
Leg1 = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false),
Leg2 = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false),
Thickness = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AngleDimensions", x => x.Id);
table.ForeignKey(
name: "FK_AngleDimensions_MaterialDimensions_Id",
column: x => x.Id,
principalTable: "MaterialDimensions",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ChannelDimensions",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false),
Height = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false),
Flange = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false),
Web = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ChannelDimensions", x => x.Id);
table.ForeignKey(
name: "FK_ChannelDimensions_MaterialDimensions_Id",
column: x => x.Id,
principalTable: "MaterialDimensions",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "FlatBarDimensions",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false),
Width = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false),
Thickness = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_FlatBarDimensions", x => x.Id);
table.ForeignKey(
name: "FK_FlatBarDimensions_MaterialDimensions_Id",
column: x => x.Id,
principalTable: "MaterialDimensions",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "IBeamDimensions",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false),
Height = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false),
WeightPerFoot = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_IBeamDimensions", x => x.Id);
table.ForeignKey(
name: "FK_IBeamDimensions_MaterialDimensions_Id",
column: x => x.Id,
principalTable: "MaterialDimensions",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "PipeDimensions",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false),
NominalSize = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false),
Wall = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: true),
Schedule = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_PipeDimensions", x => x.Id);
table.ForeignKey(
name: "FK_PipeDimensions_MaterialDimensions_Id",
column: x => x.Id,
principalTable: "MaterialDimensions",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "RectangularTubeDimensions",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false),
Width = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false),
Height = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false),
Wall = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_RectangularTubeDimensions", x => x.Id);
table.ForeignKey(
name: "FK_RectangularTubeDimensions_MaterialDimensions_Id",
column: x => x.Id,
principalTable: "MaterialDimensions",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "RoundBarDimensions",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false),
Diameter = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_RoundBarDimensions", x => x.Id);
table.ForeignKey(
name: "FK_RoundBarDimensions_MaterialDimensions_Id",
column: x => x.Id,
principalTable: "MaterialDimensions",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "RoundTubeDimensions",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false),
OuterDiameter = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false),
Wall = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_RoundTubeDimensions", x => x.Id);
table.ForeignKey(
name: "FK_RoundTubeDimensions_MaterialDimensions_Id",
column: x => x.Id,
principalTable: "MaterialDimensions",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "SquareBarDimensions",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false),
Size = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SquareBarDimensions", x => x.Id);
table.ForeignKey(
name: "FK_SquareBarDimensions_MaterialDimensions_Id",
column: x => x.Id,
principalTable: "MaterialDimensions",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "SquareTubeDimensions",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false),
Size = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false),
Wall = table.Column<decimal>(type: "decimal(10,4)", precision: 10, scale: 4, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SquareTubeDimensions", x => x.Id);
table.ForeignKey(
name: "FK_SquareTubeDimensions_MaterialDimensions_Id",
column: x => x.Id,
principalTable: "MaterialDimensions",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
// 2. Migrate existing data from the TPH table into the new TPT tables
migrationBuilder.Sql(@"
INSERT INTO RoundBarDimensions (Id, Diameter)
SELECT Id, ISNULL(Diameter, 0) FROM MaterialDimensions WHERE DimensionType = 'RoundBar';
INSERT INTO RoundTubeDimensions (Id, OuterDiameter, Wall)
SELECT Id, ISNULL(OuterDiameter, 0), ISNULL(Wall, 0) FROM MaterialDimensions WHERE DimensionType = 'RoundTube';
INSERT INTO FlatBarDimensions (Id, Width, Thickness)
SELECT Id, ISNULL(Width, 0), ISNULL(Thickness, 0) FROM MaterialDimensions WHERE DimensionType = 'FlatBar';
INSERT INTO SquareBarDimensions (Id, Size)
SELECT Id, ISNULL(Size, 0) FROM MaterialDimensions WHERE DimensionType = 'SquareBar';
INSERT INTO SquareTubeDimensions (Id, Size, Wall)
SELECT Id, ISNULL(Size, 0), ISNULL(Wall, 0) FROM MaterialDimensions WHERE DimensionType = 'SquareTube';
INSERT INTO RectangularTubeDimensions (Id, Width, Height, Wall)
SELECT Id, ISNULL(Width, 0), ISNULL(Height, 0), ISNULL(Wall, 0) FROM MaterialDimensions WHERE DimensionType = 'RectangularTube';
INSERT INTO AngleDimensions (Id, Leg1, Leg2, Thickness)
SELECT Id, ISNULL(Leg1, 0), ISNULL(Leg2, 0), ISNULL(Thickness, 0) FROM MaterialDimensions WHERE DimensionType = 'Angle';
INSERT INTO ChannelDimensions (Id, Height, Flange, Web)
SELECT Id, ISNULL(Height, 0), ISNULL(Flange, 0), ISNULL(Web, 0) FROM MaterialDimensions WHERE DimensionType = 'Channel';
INSERT INTO IBeamDimensions (Id, Height, WeightPerFoot)
SELECT Id, ISNULL(Height, 0), ISNULL(WeightPerFoot, 0) FROM MaterialDimensions WHERE DimensionType = 'IBeam';
INSERT INTO PipeDimensions (Id, NominalSize, Wall, Schedule)
SELECT Id, ISNULL(NominalSize, 0), Wall, Schedule FROM MaterialDimensions WHERE DimensionType = 'Pipe';
");
// 3. Now drop the old TPH columns and indexes
migrationBuilder.DropIndex(
name: "IX_MaterialDimensions_Diameter",
table: "MaterialDimensions");
migrationBuilder.DropIndex(
name: "IX_MaterialDimensions_Height",
table: "MaterialDimensions");
migrationBuilder.DropIndex(
name: "IX_MaterialDimensions_Leg1",
table: "MaterialDimensions");
migrationBuilder.DropIndex(
name: "IX_MaterialDimensions_NominalSize",
table: "MaterialDimensions");
migrationBuilder.DropIndex(
name: "IX_MaterialDimensions_OuterDiameter",
table: "MaterialDimensions");
migrationBuilder.DropIndex(
name: "IX_MaterialDimensions_Size",
table: "MaterialDimensions");
migrationBuilder.DropIndex(
name: "IX_MaterialDimensions_Width",
table: "MaterialDimensions");
migrationBuilder.DropColumn(name: "Diameter", table: "MaterialDimensions");
migrationBuilder.DropColumn(name: "DimensionType", table: "MaterialDimensions");
migrationBuilder.DropColumn(name: "Flange", table: "MaterialDimensions");
migrationBuilder.DropColumn(name: "Height", table: "MaterialDimensions");
migrationBuilder.DropColumn(name: "Leg1", table: "MaterialDimensions");
migrationBuilder.DropColumn(name: "Leg2", table: "MaterialDimensions");
migrationBuilder.DropColumn(name: "NominalSize", table: "MaterialDimensions");
migrationBuilder.DropColumn(name: "OuterDiameter", table: "MaterialDimensions");
migrationBuilder.DropColumn(name: "Schedule", table: "MaterialDimensions");
migrationBuilder.DropColumn(name: "Size", table: "MaterialDimensions");
migrationBuilder.DropColumn(name: "Thickness", table: "MaterialDimensions");
migrationBuilder.DropColumn(name: "Wall", table: "MaterialDimensions");
migrationBuilder.DropColumn(name: "Web", table: "MaterialDimensions");
migrationBuilder.DropColumn(name: "WeightPerFoot", table: "MaterialDimensions");
migrationBuilder.DropColumn(name: "Width", table: "MaterialDimensions");
// 4. Create indexes on the new tables
migrationBuilder.CreateIndex(name: "IX_AngleDimensions_Leg1", table: "AngleDimensions", column: "Leg1");
migrationBuilder.CreateIndex(name: "IX_ChannelDimensions_Height", table: "ChannelDimensions", column: "Height");
migrationBuilder.CreateIndex(name: "IX_FlatBarDimensions_Width", table: "FlatBarDimensions", column: "Width");
migrationBuilder.CreateIndex(name: "IX_IBeamDimensions_Height", table: "IBeamDimensions", column: "Height");
migrationBuilder.CreateIndex(name: "IX_PipeDimensions_NominalSize", table: "PipeDimensions", column: "NominalSize");
migrationBuilder.CreateIndex(name: "IX_RectangularTubeDimensions_Width", table: "RectangularTubeDimensions", column: "Width");
migrationBuilder.CreateIndex(name: "IX_RoundBarDimensions_Diameter", table: "RoundBarDimensions", column: "Diameter");
migrationBuilder.CreateIndex(name: "IX_RoundTubeDimensions_OuterDiameter", table: "RoundTubeDimensions", column: "OuterDiameter");
migrationBuilder.CreateIndex(name: "IX_SquareBarDimensions_Size", table: "SquareBarDimensions", column: "Size");
migrationBuilder.CreateIndex(name: "IX_SquareTubeDimensions_Size", table: "SquareTubeDimensions", column: "Size");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
// Re-add the TPH columns
migrationBuilder.AddColumn<string>(name: "DimensionType", table: "MaterialDimensions", type: "nvarchar(21)", maxLength: 21, nullable: false, defaultValue: "");
migrationBuilder.AddColumn<decimal>(name: "Diameter", table: "MaterialDimensions", type: "decimal(10,4)", precision: 10, scale: 4, nullable: true);
migrationBuilder.AddColumn<decimal>(name: "Flange", table: "MaterialDimensions", type: "decimal(10,4)", precision: 10, scale: 4, nullable: true);
migrationBuilder.AddColumn<decimal>(name: "Height", table: "MaterialDimensions", type: "decimal(10,4)", precision: 10, scale: 4, nullable: true);
migrationBuilder.AddColumn<decimal>(name: "Leg1", table: "MaterialDimensions", type: "decimal(10,4)", precision: 10, scale: 4, nullable: true);
migrationBuilder.AddColumn<decimal>(name: "Leg2", table: "MaterialDimensions", type: "decimal(10,4)", precision: 10, scale: 4, nullable: true);
migrationBuilder.AddColumn<decimal>(name: "NominalSize", table: "MaterialDimensions", type: "decimal(10,4)", precision: 10, scale: 4, nullable: true);
migrationBuilder.AddColumn<decimal>(name: "OuterDiameter", table: "MaterialDimensions", type: "decimal(10,4)", precision: 10, scale: 4, nullable: true);
migrationBuilder.AddColumn<string>(name: "Schedule", table: "MaterialDimensions", type: "nvarchar(20)", maxLength: 20, nullable: true);
migrationBuilder.AddColumn<decimal>(name: "Size", table: "MaterialDimensions", type: "decimal(10,4)", precision: 10, scale: 4, nullable: true);
migrationBuilder.AddColumn<decimal>(name: "Thickness", table: "MaterialDimensions", type: "decimal(10,4)", precision: 10, scale: 4, nullable: true);
migrationBuilder.AddColumn<decimal>(name: "Wall", table: "MaterialDimensions", type: "decimal(10,4)", precision: 10, scale: 4, nullable: true);
migrationBuilder.AddColumn<decimal>(name: "Web", table: "MaterialDimensions", type: "decimal(10,4)", precision: 10, scale: 4, nullable: true);
migrationBuilder.AddColumn<decimal>(name: "WeightPerFoot", table: "MaterialDimensions", type: "decimal(10,4)", precision: 10, scale: 4, nullable: true);
migrationBuilder.AddColumn<decimal>(name: "Width", table: "MaterialDimensions", type: "decimal(10,4)", precision: 10, scale: 4, nullable: true);
// Migrate data back to TPH
migrationBuilder.Sql(@"
UPDATE md SET DimensionType = 'RoundBar', Diameter = rb.Diameter FROM MaterialDimensions md INNER JOIN RoundBarDimensions rb ON md.Id = rb.Id;
UPDATE md SET DimensionType = 'RoundTube', OuterDiameter = rt.OuterDiameter, Wall = rt.Wall FROM MaterialDimensions md INNER JOIN RoundTubeDimensions rt ON md.Id = rt.Id;
UPDATE md SET DimensionType = 'FlatBar', Width = fb.Width, Thickness = fb.Thickness FROM MaterialDimensions md INNER JOIN FlatBarDimensions fb ON md.Id = fb.Id;
UPDATE md SET DimensionType = 'SquareBar', Size = sb.Size FROM MaterialDimensions md INNER JOIN SquareBarDimensions sb ON md.Id = sb.Id;
UPDATE md SET DimensionType = 'SquareTube', Size = st.Size, Wall = st.Wall FROM MaterialDimensions md INNER JOIN SquareTubeDimensions st ON md.Id = st.Id;
UPDATE md SET DimensionType = 'RectangularTube', Width = rt.Width, Height = rt.Height, Wall = rt.Wall FROM MaterialDimensions md INNER JOIN RectangularTubeDimensions rt ON md.Id = rt.Id;
UPDATE md SET DimensionType = 'Angle', Leg1 = a.Leg1, Leg2 = a.Leg2, Thickness = a.Thickness FROM MaterialDimensions md INNER JOIN AngleDimensions a ON md.Id = a.Id;
UPDATE md SET DimensionType = 'Channel', Height = c.Height, Flange = c.Flange, Web = c.Web FROM MaterialDimensions md INNER JOIN ChannelDimensions c ON md.Id = c.Id;
UPDATE md SET DimensionType = 'IBeam', Height = ib.Height, WeightPerFoot = ib.WeightPerFoot FROM MaterialDimensions md INNER JOIN IBeamDimensions ib ON md.Id = ib.Id;
UPDATE md SET DimensionType = 'Pipe', NominalSize = p.NominalSize, Wall = p.Wall, Schedule = p.Schedule FROM MaterialDimensions md INNER JOIN PipeDimensions p ON md.Id = p.Id;
");
// Drop TPT tables
migrationBuilder.DropTable(name: "AngleDimensions");
migrationBuilder.DropTable(name: "ChannelDimensions");
migrationBuilder.DropTable(name: "FlatBarDimensions");
migrationBuilder.DropTable(name: "IBeamDimensions");
migrationBuilder.DropTable(name: "PipeDimensions");
migrationBuilder.DropTable(name: "RectangularTubeDimensions");
migrationBuilder.DropTable(name: "RoundBarDimensions");
migrationBuilder.DropTable(name: "RoundTubeDimensions");
migrationBuilder.DropTable(name: "SquareBarDimensions");
migrationBuilder.DropTable(name: "SquareTubeDimensions");
// Re-create TPH indexes
migrationBuilder.CreateIndex(name: "IX_MaterialDimensions_Diameter", table: "MaterialDimensions", column: "Diameter");
migrationBuilder.CreateIndex(name: "IX_MaterialDimensions_Height", table: "MaterialDimensions", column: "Height");
migrationBuilder.CreateIndex(name: "IX_MaterialDimensions_Leg1", table: "MaterialDimensions", column: "Leg1");
migrationBuilder.CreateIndex(name: "IX_MaterialDimensions_NominalSize", table: "MaterialDimensions", column: "NominalSize");
migrationBuilder.CreateIndex(name: "IX_MaterialDimensions_OuterDiameter", table: "MaterialDimensions", column: "OuterDiameter");
migrationBuilder.CreateIndex(name: "IX_MaterialDimensions_Size", table: "MaterialDimensions", column: "Size");
migrationBuilder.CreateIndex(name: "IX_MaterialDimensions_Width", table: "MaterialDimensions", column: "Width");
}
}
}

View File

@@ -0,0 +1,962 @@
// <auto-generated />
using System;
using CutList.Web.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace CutList.Web.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20260216190925_RenameDimensionTables")]
partial class RenameDimensionTables
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.11")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("CutList.Web.Data.Entities.CuttingTool", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<bool>("IsDefault")
.HasColumnType("bit");
b.Property<decimal>("KerfInches")
.HasPrecision(6, 4)
.HasColumnType("decimal(6,4)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.HasKey("Id");
b.ToTable("CuttingTools");
b.HasData(
new
{
Id = 1,
IsActive = true,
IsDefault = true,
KerfInches = 0.0625m,
Name = "Bandsaw"
},
new
{
Id = 2,
IsActive = true,
IsDefault = false,
KerfInches = 0.125m,
Name = "Chop Saw"
},
new
{
Id = 3,
IsActive = true,
IsDefault = false,
KerfInches = 0.0625m,
Name = "Cold Cut Saw"
},
new
{
Id = 4,
IsActive = true,
IsDefault = false,
KerfInches = 0.0625m,
Name = "Hacksaw"
});
});
modelBuilder.Entity("CutList.Web.Data.Entities.Job", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<string>("Customer")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int?>("CuttingToolId")
.HasColumnType("int");
b.Property<string>("JobNumber")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<DateTime?>("LockedAt")
.HasColumnType("datetime2");
b.Property<string>("Name")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Notes")
.HasColumnType("nvarchar(max)");
b.Property<string>("OptimizationResultJson")
.HasColumnType("nvarchar(max)");
b.Property<DateTime?>("OptimizedAt")
.HasColumnType("datetime2");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.HasKey("Id");
b.HasIndex("CuttingToolId");
b.HasIndex("JobNumber")
.IsUnique();
b.ToTable("Jobs");
});
modelBuilder.Entity("CutList.Web.Data.Entities.JobPart", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("JobId")
.HasColumnType("int");
b.Property<decimal>("LengthInches")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<int>("MaterialId")
.HasColumnType("int");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int>("Quantity")
.HasColumnType("int");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("JobId");
b.HasIndex("MaterialId");
b.ToTable("JobParts");
});
modelBuilder.Entity("CutList.Web.Data.Entities.JobStock", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<bool>("IsCustomLength")
.HasColumnType("bit");
b.Property<int>("JobId")
.HasColumnType("int");
b.Property<decimal>("LengthInches")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<int>("MaterialId")
.HasColumnType("int");
b.Property<int>("Priority")
.HasColumnType("int");
b.Property<int>("Quantity")
.HasColumnType("int");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.Property<int?>("StockItemId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("JobId");
b.HasIndex("MaterialId");
b.HasIndex("StockItemId");
b.ToTable("JobStocks");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Material", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<string>("Description")
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<string>("Grade")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<string>("Shape")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Size")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.HasKey("Id");
b.ToTable("Materials");
});
modelBuilder.Entity("CutList.Web.Data.Entities.MaterialDimensions", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("MaterialId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("MaterialId")
.IsUnique();
b.ToTable("DimBase", (string)null);
b.UseTptMappingStrategy();
});
modelBuilder.Entity("CutList.Web.Data.Entities.PurchaseItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<int?>("JobId")
.HasColumnType("int");
b.Property<string>("Notes")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<int>("Quantity")
.HasColumnType("int");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<int>("StockItemId")
.HasColumnType("int");
b.Property<int?>("SupplierId")
.HasColumnType("int");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.HasKey("Id");
b.HasIndex("JobId");
b.HasIndex("StockItemId");
b.HasIndex("SupplierId");
b.ToTable("PurchaseItems");
});
modelBuilder.Entity("CutList.Web.Data.Entities.StockItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<decimal>("LengthInches")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<int>("MaterialId")
.HasColumnType("int");
b.Property<string>("Name")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Notes")
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<int>("QuantityOnHand")
.HasColumnType("int");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.HasKey("Id");
b.HasIndex("MaterialId", "LengthInches")
.IsUnique();
b.ToTable("StockItems");
});
modelBuilder.Entity("CutList.Web.Data.Entities.StockTransaction", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<int?>("JobId")
.HasColumnType("int");
b.Property<string>("Notes")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<int>("Quantity")
.HasColumnType("int");
b.Property<int>("StockItemId")
.HasColumnType("int");
b.Property<int?>("SupplierId")
.HasColumnType("int");
b.Property<int>("Type")
.HasColumnType("int");
b.Property<decimal?>("UnitPrice")
.HasPrecision(10, 2)
.HasColumnType("decimal(10,2)");
b.HasKey("Id");
b.HasIndex("JobId");
b.HasIndex("StockItemId");
b.HasIndex("SupplierId");
b.ToTable("StockTransactions");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Supplier", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ContactInfo")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Notes")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("Suppliers");
});
modelBuilder.Entity("CutList.Web.Data.Entities.SupplierOffering", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<string>("Notes")
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<string>("PartNumber")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<decimal?>("Price")
.HasPrecision(10, 2)
.HasColumnType("decimal(10,2)");
b.Property<int>("StockItemId")
.HasColumnType("int");
b.Property<string>("SupplierDescription")
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<int>("SupplierId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("StockItemId");
b.HasIndex("SupplierId", "StockItemId")
.IsUnique();
b.ToTable("SupplierOfferings");
});
modelBuilder.Entity("CutList.Web.Data.Entities.AngleDimensions", b =>
{
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
b.Property<decimal>("Leg1")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<decimal>("Leg2")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<decimal>("Thickness")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.HasIndex("Leg1");
b.ToTable("DimAngle", (string)null);
});
modelBuilder.Entity("CutList.Web.Data.Entities.ChannelDimensions", b =>
{
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
b.Property<decimal>("Flange")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<decimal>("Height")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<decimal>("Web")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.HasIndex("Height");
b.ToTable("DimChannel", (string)null);
});
modelBuilder.Entity("CutList.Web.Data.Entities.FlatBarDimensions", b =>
{
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
b.Property<decimal>("Thickness")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<decimal>("Width")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.HasIndex("Width");
b.ToTable("DimFlatBar", (string)null);
});
modelBuilder.Entity("CutList.Web.Data.Entities.IBeamDimensions", b =>
{
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
b.Property<decimal>("Height")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<decimal>("WeightPerFoot")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.HasIndex("Height");
b.ToTable("DimIBeam", (string)null);
});
modelBuilder.Entity("CutList.Web.Data.Entities.PipeDimensions", b =>
{
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
b.Property<decimal>("NominalSize")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<string>("Schedule")
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<decimal?>("Wall")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.HasIndex("NominalSize");
b.ToTable("DimPipe", (string)null);
});
modelBuilder.Entity("CutList.Web.Data.Entities.RectangularTubeDimensions", b =>
{
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
b.Property<decimal>("Height")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<decimal>("Wall")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<decimal>("Width")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.HasIndex("Width");
b.ToTable("DimRectangularTube", (string)null);
});
modelBuilder.Entity("CutList.Web.Data.Entities.RoundBarDimensions", b =>
{
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
b.Property<decimal>("Diameter")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.HasIndex("Diameter");
b.ToTable("DimRoundBar", (string)null);
});
modelBuilder.Entity("CutList.Web.Data.Entities.RoundTubeDimensions", b =>
{
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
b.Property<decimal>("OuterDiameter")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<decimal>("Wall")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.HasIndex("OuterDiameter");
b.ToTable("DimRoundTube", (string)null);
});
modelBuilder.Entity("CutList.Web.Data.Entities.SquareBarDimensions", b =>
{
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
b.Property<decimal>("Size")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.HasIndex("Size");
b.ToTable("DimSquareBar", (string)null);
});
modelBuilder.Entity("CutList.Web.Data.Entities.SquareTubeDimensions", b =>
{
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
b.Property<decimal>("Size")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<decimal>("Wall")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.HasIndex("Size");
b.ToTable("DimSquareTube", (string)null);
});
modelBuilder.Entity("CutList.Web.Data.Entities.Job", b =>
{
b.HasOne("CutList.Web.Data.Entities.CuttingTool", "CuttingTool")
.WithMany("Jobs")
.HasForeignKey("CuttingToolId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("CuttingTool");
});
modelBuilder.Entity("CutList.Web.Data.Entities.JobPart", b =>
{
b.HasOne("CutList.Web.Data.Entities.Job", "Job")
.WithMany("Parts")
.HasForeignKey("JobId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
.WithMany("JobParts")
.HasForeignKey("MaterialId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Job");
b.Navigation("Material");
});
modelBuilder.Entity("CutList.Web.Data.Entities.JobStock", b =>
{
b.HasOne("CutList.Web.Data.Entities.Job", "Job")
.WithMany("Stock")
.HasForeignKey("JobId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
.WithMany()
.HasForeignKey("MaterialId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("CutList.Web.Data.Entities.StockItem", "StockItem")
.WithMany()
.HasForeignKey("StockItemId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("Job");
b.Navigation("Material");
b.Navigation("StockItem");
});
modelBuilder.Entity("CutList.Web.Data.Entities.MaterialDimensions", b =>
{
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
.WithOne("Dimensions")
.HasForeignKey("CutList.Web.Data.Entities.MaterialDimensions", "MaterialId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Material");
});
modelBuilder.Entity("CutList.Web.Data.Entities.PurchaseItem", b =>
{
b.HasOne("CutList.Web.Data.Entities.Job", "Job")
.WithMany()
.HasForeignKey("JobId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("CutList.Web.Data.Entities.StockItem", "StockItem")
.WithMany()
.HasForeignKey("StockItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("CutList.Web.Data.Entities.Supplier", "Supplier")
.WithMany()
.HasForeignKey("SupplierId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("Job");
b.Navigation("StockItem");
b.Navigation("Supplier");
});
modelBuilder.Entity("CutList.Web.Data.Entities.StockItem", b =>
{
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
.WithMany("StockItems")
.HasForeignKey("MaterialId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Material");
});
modelBuilder.Entity("CutList.Web.Data.Entities.StockTransaction", b =>
{
b.HasOne("CutList.Web.Data.Entities.Job", "Job")
.WithMany()
.HasForeignKey("JobId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("CutList.Web.Data.Entities.StockItem", "StockItem")
.WithMany("Transactions")
.HasForeignKey("StockItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("CutList.Web.Data.Entities.Supplier", "Supplier")
.WithMany()
.HasForeignKey("SupplierId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("Job");
b.Navigation("StockItem");
b.Navigation("Supplier");
});
modelBuilder.Entity("CutList.Web.Data.Entities.SupplierOffering", b =>
{
b.HasOne("CutList.Web.Data.Entities.StockItem", "StockItem")
.WithMany("SupplierOfferings")
.HasForeignKey("StockItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("CutList.Web.Data.Entities.Supplier", "Supplier")
.WithMany("Offerings")
.HasForeignKey("SupplierId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("StockItem");
b.Navigation("Supplier");
});
modelBuilder.Entity("CutList.Web.Data.Entities.AngleDimensions", b =>
{
b.HasOne("CutList.Web.Data.Entities.MaterialDimensions", null)
.WithOne()
.HasForeignKey("CutList.Web.Data.Entities.AngleDimensions", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("CutList.Web.Data.Entities.ChannelDimensions", b =>
{
b.HasOne("CutList.Web.Data.Entities.MaterialDimensions", null)
.WithOne()
.HasForeignKey("CutList.Web.Data.Entities.ChannelDimensions", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("CutList.Web.Data.Entities.FlatBarDimensions", b =>
{
b.HasOne("CutList.Web.Data.Entities.MaterialDimensions", null)
.WithOne()
.HasForeignKey("CutList.Web.Data.Entities.FlatBarDimensions", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("CutList.Web.Data.Entities.IBeamDimensions", b =>
{
b.HasOne("CutList.Web.Data.Entities.MaterialDimensions", null)
.WithOne()
.HasForeignKey("CutList.Web.Data.Entities.IBeamDimensions", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("CutList.Web.Data.Entities.PipeDimensions", b =>
{
b.HasOne("CutList.Web.Data.Entities.MaterialDimensions", null)
.WithOne()
.HasForeignKey("CutList.Web.Data.Entities.PipeDimensions", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("CutList.Web.Data.Entities.RectangularTubeDimensions", b =>
{
b.HasOne("CutList.Web.Data.Entities.MaterialDimensions", null)
.WithOne()
.HasForeignKey("CutList.Web.Data.Entities.RectangularTubeDimensions", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("CutList.Web.Data.Entities.RoundBarDimensions", b =>
{
b.HasOne("CutList.Web.Data.Entities.MaterialDimensions", null)
.WithOne()
.HasForeignKey("CutList.Web.Data.Entities.RoundBarDimensions", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("CutList.Web.Data.Entities.RoundTubeDimensions", b =>
{
b.HasOne("CutList.Web.Data.Entities.MaterialDimensions", null)
.WithOne()
.HasForeignKey("CutList.Web.Data.Entities.RoundTubeDimensions", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("CutList.Web.Data.Entities.SquareBarDimensions", b =>
{
b.HasOne("CutList.Web.Data.Entities.MaterialDimensions", null)
.WithOne()
.HasForeignKey("CutList.Web.Data.Entities.SquareBarDimensions", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("CutList.Web.Data.Entities.SquareTubeDimensions", b =>
{
b.HasOne("CutList.Web.Data.Entities.MaterialDimensions", null)
.WithOne()
.HasForeignKey("CutList.Web.Data.Entities.SquareTubeDimensions", "Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("CutList.Web.Data.Entities.CuttingTool", b =>
{
b.Navigation("Jobs");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Job", b =>
{
b.Navigation("Parts");
b.Navigation("Stock");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Material", b =>
{
b.Navigation("Dimensions");
b.Navigation("JobParts");
b.Navigation("StockItems");
});
modelBuilder.Entity("CutList.Web.Data.Entities.StockItem", b =>
{
b.Navigation("SupplierOfferings");
b.Navigation("Transactions");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Supplier", b =>
{
b.Navigation("Offerings");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,678 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CutList.Web.Migrations
{
/// <inheritdoc />
public partial class RenameDimensionTables : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_AngleDimensions_MaterialDimensions_Id",
table: "AngleDimensions");
migrationBuilder.DropForeignKey(
name: "FK_ChannelDimensions_MaterialDimensions_Id",
table: "ChannelDimensions");
migrationBuilder.DropForeignKey(
name: "FK_FlatBarDimensions_MaterialDimensions_Id",
table: "FlatBarDimensions");
migrationBuilder.DropForeignKey(
name: "FK_IBeamDimensions_MaterialDimensions_Id",
table: "IBeamDimensions");
migrationBuilder.DropForeignKey(
name: "FK_MaterialDimensions_Materials_MaterialId",
table: "MaterialDimensions");
migrationBuilder.DropForeignKey(
name: "FK_PipeDimensions_MaterialDimensions_Id",
table: "PipeDimensions");
migrationBuilder.DropForeignKey(
name: "FK_RectangularTubeDimensions_MaterialDimensions_Id",
table: "RectangularTubeDimensions");
migrationBuilder.DropForeignKey(
name: "FK_RoundBarDimensions_MaterialDimensions_Id",
table: "RoundBarDimensions");
migrationBuilder.DropForeignKey(
name: "FK_RoundTubeDimensions_MaterialDimensions_Id",
table: "RoundTubeDimensions");
migrationBuilder.DropForeignKey(
name: "FK_SquareBarDimensions_MaterialDimensions_Id",
table: "SquareBarDimensions");
migrationBuilder.DropForeignKey(
name: "FK_SquareTubeDimensions_MaterialDimensions_Id",
table: "SquareTubeDimensions");
migrationBuilder.DropPrimaryKey(
name: "PK_SquareTubeDimensions",
table: "SquareTubeDimensions");
migrationBuilder.DropPrimaryKey(
name: "PK_SquareBarDimensions",
table: "SquareBarDimensions");
migrationBuilder.DropPrimaryKey(
name: "PK_RoundTubeDimensions",
table: "RoundTubeDimensions");
migrationBuilder.DropPrimaryKey(
name: "PK_RoundBarDimensions",
table: "RoundBarDimensions");
migrationBuilder.DropPrimaryKey(
name: "PK_RectangularTubeDimensions",
table: "RectangularTubeDimensions");
migrationBuilder.DropPrimaryKey(
name: "PK_PipeDimensions",
table: "PipeDimensions");
migrationBuilder.DropPrimaryKey(
name: "PK_MaterialDimensions",
table: "MaterialDimensions");
migrationBuilder.DropPrimaryKey(
name: "PK_IBeamDimensions",
table: "IBeamDimensions");
migrationBuilder.DropPrimaryKey(
name: "PK_FlatBarDimensions",
table: "FlatBarDimensions");
migrationBuilder.DropPrimaryKey(
name: "PK_ChannelDimensions",
table: "ChannelDimensions");
migrationBuilder.DropPrimaryKey(
name: "PK_AngleDimensions",
table: "AngleDimensions");
migrationBuilder.RenameTable(
name: "SquareTubeDimensions",
newName: "DimSquareTube");
migrationBuilder.RenameTable(
name: "SquareBarDimensions",
newName: "DimSquareBar");
migrationBuilder.RenameTable(
name: "RoundTubeDimensions",
newName: "DimRoundTube");
migrationBuilder.RenameTable(
name: "RoundBarDimensions",
newName: "DimRoundBar");
migrationBuilder.RenameTable(
name: "RectangularTubeDimensions",
newName: "DimRectangularTube");
migrationBuilder.RenameTable(
name: "PipeDimensions",
newName: "DimPipe");
migrationBuilder.RenameTable(
name: "MaterialDimensions",
newName: "DimBase");
migrationBuilder.RenameTable(
name: "IBeamDimensions",
newName: "DimIBeam");
migrationBuilder.RenameTable(
name: "FlatBarDimensions",
newName: "DimFlatBar");
migrationBuilder.RenameTable(
name: "ChannelDimensions",
newName: "DimChannel");
migrationBuilder.RenameTable(
name: "AngleDimensions",
newName: "DimAngle");
migrationBuilder.RenameIndex(
name: "IX_SquareTubeDimensions_Size",
table: "DimSquareTube",
newName: "IX_DimSquareTube_Size");
migrationBuilder.RenameIndex(
name: "IX_SquareBarDimensions_Size",
table: "DimSquareBar",
newName: "IX_DimSquareBar_Size");
migrationBuilder.RenameIndex(
name: "IX_RoundTubeDimensions_OuterDiameter",
table: "DimRoundTube",
newName: "IX_DimRoundTube_OuterDiameter");
migrationBuilder.RenameIndex(
name: "IX_RoundBarDimensions_Diameter",
table: "DimRoundBar",
newName: "IX_DimRoundBar_Diameter");
migrationBuilder.RenameIndex(
name: "IX_RectangularTubeDimensions_Width",
table: "DimRectangularTube",
newName: "IX_DimRectangularTube_Width");
migrationBuilder.RenameIndex(
name: "IX_PipeDimensions_NominalSize",
table: "DimPipe",
newName: "IX_DimPipe_NominalSize");
migrationBuilder.RenameIndex(
name: "IX_MaterialDimensions_MaterialId",
table: "DimBase",
newName: "IX_DimBase_MaterialId");
migrationBuilder.RenameIndex(
name: "IX_IBeamDimensions_Height",
table: "DimIBeam",
newName: "IX_DimIBeam_Height");
migrationBuilder.RenameIndex(
name: "IX_FlatBarDimensions_Width",
table: "DimFlatBar",
newName: "IX_DimFlatBar_Width");
migrationBuilder.RenameIndex(
name: "IX_ChannelDimensions_Height",
table: "DimChannel",
newName: "IX_DimChannel_Height");
migrationBuilder.RenameIndex(
name: "IX_AngleDimensions_Leg1",
table: "DimAngle",
newName: "IX_DimAngle_Leg1");
migrationBuilder.AddPrimaryKey(
name: "PK_DimSquareTube",
table: "DimSquareTube",
column: "Id");
migrationBuilder.AddPrimaryKey(
name: "PK_DimSquareBar",
table: "DimSquareBar",
column: "Id");
migrationBuilder.AddPrimaryKey(
name: "PK_DimRoundTube",
table: "DimRoundTube",
column: "Id");
migrationBuilder.AddPrimaryKey(
name: "PK_DimRoundBar",
table: "DimRoundBar",
column: "Id");
migrationBuilder.AddPrimaryKey(
name: "PK_DimRectangularTube",
table: "DimRectangularTube",
column: "Id");
migrationBuilder.AddPrimaryKey(
name: "PK_DimPipe",
table: "DimPipe",
column: "Id");
migrationBuilder.AddPrimaryKey(
name: "PK_DimBase",
table: "DimBase",
column: "Id");
migrationBuilder.AddPrimaryKey(
name: "PK_DimIBeam",
table: "DimIBeam",
column: "Id");
migrationBuilder.AddPrimaryKey(
name: "PK_DimFlatBar",
table: "DimFlatBar",
column: "Id");
migrationBuilder.AddPrimaryKey(
name: "PK_DimChannel",
table: "DimChannel",
column: "Id");
migrationBuilder.AddPrimaryKey(
name: "PK_DimAngle",
table: "DimAngle",
column: "Id");
migrationBuilder.AddForeignKey(
name: "FK_DimAngle_DimBase_Id",
table: "DimAngle",
column: "Id",
principalTable: "DimBase",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_DimBase_Materials_MaterialId",
table: "DimBase",
column: "MaterialId",
principalTable: "Materials",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_DimChannel_DimBase_Id",
table: "DimChannel",
column: "Id",
principalTable: "DimBase",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_DimFlatBar_DimBase_Id",
table: "DimFlatBar",
column: "Id",
principalTable: "DimBase",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_DimIBeam_DimBase_Id",
table: "DimIBeam",
column: "Id",
principalTable: "DimBase",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_DimPipe_DimBase_Id",
table: "DimPipe",
column: "Id",
principalTable: "DimBase",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_DimRectangularTube_DimBase_Id",
table: "DimRectangularTube",
column: "Id",
principalTable: "DimBase",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_DimRoundBar_DimBase_Id",
table: "DimRoundBar",
column: "Id",
principalTable: "DimBase",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_DimRoundTube_DimBase_Id",
table: "DimRoundTube",
column: "Id",
principalTable: "DimBase",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_DimSquareBar_DimBase_Id",
table: "DimSquareBar",
column: "Id",
principalTable: "DimBase",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_DimSquareTube_DimBase_Id",
table: "DimSquareTube",
column: "Id",
principalTable: "DimBase",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_DimAngle_DimBase_Id",
table: "DimAngle");
migrationBuilder.DropForeignKey(
name: "FK_DimBase_Materials_MaterialId",
table: "DimBase");
migrationBuilder.DropForeignKey(
name: "FK_DimChannel_DimBase_Id",
table: "DimChannel");
migrationBuilder.DropForeignKey(
name: "FK_DimFlatBar_DimBase_Id",
table: "DimFlatBar");
migrationBuilder.DropForeignKey(
name: "FK_DimIBeam_DimBase_Id",
table: "DimIBeam");
migrationBuilder.DropForeignKey(
name: "FK_DimPipe_DimBase_Id",
table: "DimPipe");
migrationBuilder.DropForeignKey(
name: "FK_DimRectangularTube_DimBase_Id",
table: "DimRectangularTube");
migrationBuilder.DropForeignKey(
name: "FK_DimRoundBar_DimBase_Id",
table: "DimRoundBar");
migrationBuilder.DropForeignKey(
name: "FK_DimRoundTube_DimBase_Id",
table: "DimRoundTube");
migrationBuilder.DropForeignKey(
name: "FK_DimSquareBar_DimBase_Id",
table: "DimSquareBar");
migrationBuilder.DropForeignKey(
name: "FK_DimSquareTube_DimBase_Id",
table: "DimSquareTube");
migrationBuilder.DropPrimaryKey(
name: "PK_DimSquareTube",
table: "DimSquareTube");
migrationBuilder.DropPrimaryKey(
name: "PK_DimSquareBar",
table: "DimSquareBar");
migrationBuilder.DropPrimaryKey(
name: "PK_DimRoundTube",
table: "DimRoundTube");
migrationBuilder.DropPrimaryKey(
name: "PK_DimRoundBar",
table: "DimRoundBar");
migrationBuilder.DropPrimaryKey(
name: "PK_DimRectangularTube",
table: "DimRectangularTube");
migrationBuilder.DropPrimaryKey(
name: "PK_DimPipe",
table: "DimPipe");
migrationBuilder.DropPrimaryKey(
name: "PK_DimIBeam",
table: "DimIBeam");
migrationBuilder.DropPrimaryKey(
name: "PK_DimFlatBar",
table: "DimFlatBar");
migrationBuilder.DropPrimaryKey(
name: "PK_DimChannel",
table: "DimChannel");
migrationBuilder.DropPrimaryKey(
name: "PK_DimBase",
table: "DimBase");
migrationBuilder.DropPrimaryKey(
name: "PK_DimAngle",
table: "DimAngle");
migrationBuilder.RenameTable(
name: "DimSquareTube",
newName: "SquareTubeDimensions");
migrationBuilder.RenameTable(
name: "DimSquareBar",
newName: "SquareBarDimensions");
migrationBuilder.RenameTable(
name: "DimRoundTube",
newName: "RoundTubeDimensions");
migrationBuilder.RenameTable(
name: "DimRoundBar",
newName: "RoundBarDimensions");
migrationBuilder.RenameTable(
name: "DimRectangularTube",
newName: "RectangularTubeDimensions");
migrationBuilder.RenameTable(
name: "DimPipe",
newName: "PipeDimensions");
migrationBuilder.RenameTable(
name: "DimIBeam",
newName: "IBeamDimensions");
migrationBuilder.RenameTable(
name: "DimFlatBar",
newName: "FlatBarDimensions");
migrationBuilder.RenameTable(
name: "DimChannel",
newName: "ChannelDimensions");
migrationBuilder.RenameTable(
name: "DimBase",
newName: "MaterialDimensions");
migrationBuilder.RenameTable(
name: "DimAngle",
newName: "AngleDimensions");
migrationBuilder.RenameIndex(
name: "IX_DimSquareTube_Size",
table: "SquareTubeDimensions",
newName: "IX_SquareTubeDimensions_Size");
migrationBuilder.RenameIndex(
name: "IX_DimSquareBar_Size",
table: "SquareBarDimensions",
newName: "IX_SquareBarDimensions_Size");
migrationBuilder.RenameIndex(
name: "IX_DimRoundTube_OuterDiameter",
table: "RoundTubeDimensions",
newName: "IX_RoundTubeDimensions_OuterDiameter");
migrationBuilder.RenameIndex(
name: "IX_DimRoundBar_Diameter",
table: "RoundBarDimensions",
newName: "IX_RoundBarDimensions_Diameter");
migrationBuilder.RenameIndex(
name: "IX_DimRectangularTube_Width",
table: "RectangularTubeDimensions",
newName: "IX_RectangularTubeDimensions_Width");
migrationBuilder.RenameIndex(
name: "IX_DimPipe_NominalSize",
table: "PipeDimensions",
newName: "IX_PipeDimensions_NominalSize");
migrationBuilder.RenameIndex(
name: "IX_DimIBeam_Height",
table: "IBeamDimensions",
newName: "IX_IBeamDimensions_Height");
migrationBuilder.RenameIndex(
name: "IX_DimFlatBar_Width",
table: "FlatBarDimensions",
newName: "IX_FlatBarDimensions_Width");
migrationBuilder.RenameIndex(
name: "IX_DimChannel_Height",
table: "ChannelDimensions",
newName: "IX_ChannelDimensions_Height");
migrationBuilder.RenameIndex(
name: "IX_DimBase_MaterialId",
table: "MaterialDimensions",
newName: "IX_MaterialDimensions_MaterialId");
migrationBuilder.RenameIndex(
name: "IX_DimAngle_Leg1",
table: "AngleDimensions",
newName: "IX_AngleDimensions_Leg1");
migrationBuilder.AddPrimaryKey(
name: "PK_SquareTubeDimensions",
table: "SquareTubeDimensions",
column: "Id");
migrationBuilder.AddPrimaryKey(
name: "PK_SquareBarDimensions",
table: "SquareBarDimensions",
column: "Id");
migrationBuilder.AddPrimaryKey(
name: "PK_RoundTubeDimensions",
table: "RoundTubeDimensions",
column: "Id");
migrationBuilder.AddPrimaryKey(
name: "PK_RoundBarDimensions",
table: "RoundBarDimensions",
column: "Id");
migrationBuilder.AddPrimaryKey(
name: "PK_RectangularTubeDimensions",
table: "RectangularTubeDimensions",
column: "Id");
migrationBuilder.AddPrimaryKey(
name: "PK_PipeDimensions",
table: "PipeDimensions",
column: "Id");
migrationBuilder.AddPrimaryKey(
name: "PK_IBeamDimensions",
table: "IBeamDimensions",
column: "Id");
migrationBuilder.AddPrimaryKey(
name: "PK_FlatBarDimensions",
table: "FlatBarDimensions",
column: "Id");
migrationBuilder.AddPrimaryKey(
name: "PK_ChannelDimensions",
table: "ChannelDimensions",
column: "Id");
migrationBuilder.AddPrimaryKey(
name: "PK_MaterialDimensions",
table: "MaterialDimensions",
column: "Id");
migrationBuilder.AddPrimaryKey(
name: "PK_AngleDimensions",
table: "AngleDimensions",
column: "Id");
migrationBuilder.AddForeignKey(
name: "FK_AngleDimensions_MaterialDimensions_Id",
table: "AngleDimensions",
column: "Id",
principalTable: "MaterialDimensions",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_ChannelDimensions_MaterialDimensions_Id",
table: "ChannelDimensions",
column: "Id",
principalTable: "MaterialDimensions",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_FlatBarDimensions_MaterialDimensions_Id",
table: "FlatBarDimensions",
column: "Id",
principalTable: "MaterialDimensions",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_IBeamDimensions_MaterialDimensions_Id",
table: "IBeamDimensions",
column: "Id",
principalTable: "MaterialDimensions",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_MaterialDimensions_Materials_MaterialId",
table: "MaterialDimensions",
column: "MaterialId",
principalTable: "Materials",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_PipeDimensions_MaterialDimensions_Id",
table: "PipeDimensions",
column: "Id",
principalTable: "MaterialDimensions",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_RectangularTubeDimensions_MaterialDimensions_Id",
table: "RectangularTubeDimensions",
column: "Id",
principalTable: "MaterialDimensions",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_RoundBarDimensions_MaterialDimensions_Id",
table: "RoundBarDimensions",
column: "Id",
principalTable: "MaterialDimensions",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_RoundTubeDimensions_MaterialDimensions_Id",
table: "RoundTubeDimensions",
column: "Id",
principalTable: "MaterialDimensions",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_SquareBarDimensions_MaterialDimensions_Id",
table: "SquareBarDimensions",
column: "Id",
principalTable: "MaterialDimensions",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_SquareTubeDimensions_MaterialDimensions_Id",
table: "SquareTubeDimensions",
column: "Id",
principalTable: "MaterialDimensions",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
}
}

View File

@@ -0,0 +1,875 @@
// <auto-generated />
using System;
using CutList.Web.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace CutList.Web.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20260216191345_DimensionsTPTtoTPC")]
partial class DimensionsTPTtoTPC
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.11")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.HasSequence("MaterialDimensionsSequence");
modelBuilder.Entity("CutList.Web.Data.Entities.CuttingTool", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<bool>("IsDefault")
.HasColumnType("bit");
b.Property<decimal>("KerfInches")
.HasPrecision(6, 4)
.HasColumnType("decimal(6,4)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.HasKey("Id");
b.ToTable("CuttingTools");
b.HasData(
new
{
Id = 1,
IsActive = true,
IsDefault = true,
KerfInches = 0.0625m,
Name = "Bandsaw"
},
new
{
Id = 2,
IsActive = true,
IsDefault = false,
KerfInches = 0.125m,
Name = "Chop Saw"
},
new
{
Id = 3,
IsActive = true,
IsDefault = false,
KerfInches = 0.0625m,
Name = "Cold Cut Saw"
},
new
{
Id = 4,
IsActive = true,
IsDefault = false,
KerfInches = 0.0625m,
Name = "Hacksaw"
});
});
modelBuilder.Entity("CutList.Web.Data.Entities.Job", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<string>("Customer")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int?>("CuttingToolId")
.HasColumnType("int");
b.Property<string>("JobNumber")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<DateTime?>("LockedAt")
.HasColumnType("datetime2");
b.Property<string>("Name")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Notes")
.HasColumnType("nvarchar(max)");
b.Property<string>("OptimizationResultJson")
.HasColumnType("nvarchar(max)");
b.Property<DateTime?>("OptimizedAt")
.HasColumnType("datetime2");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.HasKey("Id");
b.HasIndex("CuttingToolId");
b.HasIndex("JobNumber")
.IsUnique();
b.ToTable("Jobs");
});
modelBuilder.Entity("CutList.Web.Data.Entities.JobPart", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("JobId")
.HasColumnType("int");
b.Property<decimal>("LengthInches")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<int>("MaterialId")
.HasColumnType("int");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int>("Quantity")
.HasColumnType("int");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("JobId");
b.HasIndex("MaterialId");
b.ToTable("JobParts");
});
modelBuilder.Entity("CutList.Web.Data.Entities.JobStock", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<bool>("IsCustomLength")
.HasColumnType("bit");
b.Property<int>("JobId")
.HasColumnType("int");
b.Property<decimal>("LengthInches")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<int>("MaterialId")
.HasColumnType("int");
b.Property<int>("Priority")
.HasColumnType("int");
b.Property<int>("Quantity")
.HasColumnType("int");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.Property<int?>("StockItemId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("JobId");
b.HasIndex("MaterialId");
b.HasIndex("StockItemId");
b.ToTable("JobStocks");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Material", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<string>("Description")
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<string>("Grade")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<string>("Shape")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Size")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.HasKey("Id");
b.ToTable("Materials");
});
modelBuilder.Entity("CutList.Web.Data.Entities.MaterialDimensions", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValueSql("NEXT VALUE FOR [MaterialDimensionsSequence]");
SqlServerPropertyBuilderExtensions.UseSequence(b.Property<int>("Id"));
b.Property<int>("MaterialId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("MaterialId")
.IsUnique();
b.ToTable((string)null);
b.UseTpcMappingStrategy();
});
modelBuilder.Entity("CutList.Web.Data.Entities.PurchaseItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<int?>("JobId")
.HasColumnType("int");
b.Property<string>("Notes")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<int>("Quantity")
.HasColumnType("int");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<int>("StockItemId")
.HasColumnType("int");
b.Property<int?>("SupplierId")
.HasColumnType("int");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.HasKey("Id");
b.HasIndex("JobId");
b.HasIndex("StockItemId");
b.HasIndex("SupplierId");
b.ToTable("PurchaseItems");
});
modelBuilder.Entity("CutList.Web.Data.Entities.StockItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<decimal>("LengthInches")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<int>("MaterialId")
.HasColumnType("int");
b.Property<string>("Name")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Notes")
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<int>("QuantityOnHand")
.HasColumnType("int");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime2");
b.HasKey("Id");
b.HasIndex("MaterialId", "LengthInches")
.IsUnique();
b.ToTable("StockItems");
});
modelBuilder.Entity("CutList.Web.Data.Entities.StockTransaction", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<int?>("JobId")
.HasColumnType("int");
b.Property<string>("Notes")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<int>("Quantity")
.HasColumnType("int");
b.Property<int>("StockItemId")
.HasColumnType("int");
b.Property<int?>("SupplierId")
.HasColumnType("int");
b.Property<int>("Type")
.HasColumnType("int");
b.Property<decimal?>("UnitPrice")
.HasPrecision(10, 2)
.HasColumnType("decimal(10,2)");
b.HasKey("Id");
b.HasIndex("JobId");
b.HasIndex("StockItemId");
b.HasIndex("SupplierId");
b.ToTable("StockTransactions");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Supplier", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ContactInfo")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Notes")
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.ToTable("Suppliers");
});
modelBuilder.Entity("CutList.Web.Data.Entities.SupplierOffering", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<string>("Notes")
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<string>("PartNumber")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<decimal?>("Price")
.HasPrecision(10, 2)
.HasColumnType("decimal(10,2)");
b.Property<int>("StockItemId")
.HasColumnType("int");
b.Property<string>("SupplierDescription")
.HasMaxLength(255)
.HasColumnType("nvarchar(255)");
b.Property<int>("SupplierId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("StockItemId");
b.HasIndex("SupplierId", "StockItemId")
.IsUnique();
b.ToTable("SupplierOfferings");
});
modelBuilder.Entity("CutList.Web.Data.Entities.AngleDimensions", b =>
{
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
b.Property<decimal>("Leg1")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<decimal>("Leg2")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<decimal>("Thickness")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.HasIndex("Leg1");
b.ToTable("DimAngle", (string)null);
});
modelBuilder.Entity("CutList.Web.Data.Entities.ChannelDimensions", b =>
{
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
b.Property<decimal>("Flange")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<decimal>("Height")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<decimal>("Web")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.HasIndex("Height");
b.ToTable("DimChannel", (string)null);
});
modelBuilder.Entity("CutList.Web.Data.Entities.FlatBarDimensions", b =>
{
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
b.Property<decimal>("Thickness")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<decimal>("Width")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.HasIndex("Width");
b.ToTable("DimFlatBar", (string)null);
});
modelBuilder.Entity("CutList.Web.Data.Entities.IBeamDimensions", b =>
{
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
b.Property<decimal>("Height")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<decimal>("WeightPerFoot")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.HasIndex("Height");
b.ToTable("DimIBeam", (string)null);
});
modelBuilder.Entity("CutList.Web.Data.Entities.PipeDimensions", b =>
{
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
b.Property<decimal>("NominalSize")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<string>("Schedule")
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<decimal?>("Wall")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.HasIndex("NominalSize");
b.ToTable("DimPipe", (string)null);
});
modelBuilder.Entity("CutList.Web.Data.Entities.RectangularTubeDimensions", b =>
{
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
b.Property<decimal>("Height")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<decimal>("Wall")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<decimal>("Width")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.HasIndex("Width");
b.ToTable("DimRectangularTube", (string)null);
});
modelBuilder.Entity("CutList.Web.Data.Entities.RoundBarDimensions", b =>
{
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
b.Property<decimal>("Diameter")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.HasIndex("Diameter");
b.ToTable("DimRoundBar", (string)null);
});
modelBuilder.Entity("CutList.Web.Data.Entities.RoundTubeDimensions", b =>
{
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
b.Property<decimal>("OuterDiameter")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<decimal>("Wall")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.HasIndex("OuterDiameter");
b.ToTable("DimRoundTube", (string)null);
});
modelBuilder.Entity("CutList.Web.Data.Entities.SquareBarDimensions", b =>
{
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
b.Property<decimal>("Size")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.HasIndex("Size");
b.ToTable("DimSquareBar", (string)null);
});
modelBuilder.Entity("CutList.Web.Data.Entities.SquareTubeDimensions", b =>
{
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
b.Property<decimal>("Size")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.Property<decimal>("Wall")
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)");
b.HasIndex("Size");
b.ToTable("DimSquareTube", (string)null);
});
modelBuilder.Entity("CutList.Web.Data.Entities.Job", b =>
{
b.HasOne("CutList.Web.Data.Entities.CuttingTool", "CuttingTool")
.WithMany("Jobs")
.HasForeignKey("CuttingToolId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("CuttingTool");
});
modelBuilder.Entity("CutList.Web.Data.Entities.JobPart", b =>
{
b.HasOne("CutList.Web.Data.Entities.Job", "Job")
.WithMany("Parts")
.HasForeignKey("JobId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
.WithMany("JobParts")
.HasForeignKey("MaterialId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Job");
b.Navigation("Material");
});
modelBuilder.Entity("CutList.Web.Data.Entities.JobStock", b =>
{
b.HasOne("CutList.Web.Data.Entities.Job", "Job")
.WithMany("Stock")
.HasForeignKey("JobId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
.WithMany()
.HasForeignKey("MaterialId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("CutList.Web.Data.Entities.StockItem", "StockItem")
.WithMany()
.HasForeignKey("StockItemId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("Job");
b.Navigation("Material");
b.Navigation("StockItem");
});
modelBuilder.Entity("CutList.Web.Data.Entities.MaterialDimensions", b =>
{
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
.WithOne("Dimensions")
.HasForeignKey("CutList.Web.Data.Entities.MaterialDimensions", "MaterialId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Material");
});
modelBuilder.Entity("CutList.Web.Data.Entities.PurchaseItem", b =>
{
b.HasOne("CutList.Web.Data.Entities.Job", "Job")
.WithMany()
.HasForeignKey("JobId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("CutList.Web.Data.Entities.StockItem", "StockItem")
.WithMany()
.HasForeignKey("StockItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("CutList.Web.Data.Entities.Supplier", "Supplier")
.WithMany()
.HasForeignKey("SupplierId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("Job");
b.Navigation("StockItem");
b.Navigation("Supplier");
});
modelBuilder.Entity("CutList.Web.Data.Entities.StockItem", b =>
{
b.HasOne("CutList.Web.Data.Entities.Material", "Material")
.WithMany("StockItems")
.HasForeignKey("MaterialId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Material");
});
modelBuilder.Entity("CutList.Web.Data.Entities.StockTransaction", b =>
{
b.HasOne("CutList.Web.Data.Entities.Job", "Job")
.WithMany()
.HasForeignKey("JobId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("CutList.Web.Data.Entities.StockItem", "StockItem")
.WithMany("Transactions")
.HasForeignKey("StockItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("CutList.Web.Data.Entities.Supplier", "Supplier")
.WithMany()
.HasForeignKey("SupplierId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("Job");
b.Navigation("StockItem");
b.Navigation("Supplier");
});
modelBuilder.Entity("CutList.Web.Data.Entities.SupplierOffering", b =>
{
b.HasOne("CutList.Web.Data.Entities.StockItem", "StockItem")
.WithMany("SupplierOfferings")
.HasForeignKey("StockItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("CutList.Web.Data.Entities.Supplier", "Supplier")
.WithMany("Offerings")
.HasForeignKey("SupplierId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("StockItem");
b.Navigation("Supplier");
});
modelBuilder.Entity("CutList.Web.Data.Entities.CuttingTool", b =>
{
b.Navigation("Jobs");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Job", b =>
{
b.Navigation("Parts");
b.Navigation("Stock");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Material", b =>
{
b.Navigation("Dimensions");
b.Navigation("JobParts");
b.Navigation("StockItems");
});
modelBuilder.Entity("CutList.Web.Data.Entities.StockItem", b =>
{
b.Navigation("SupplierOfferings");
b.Navigation("Transactions");
});
modelBuilder.Entity("CutList.Web.Data.Entities.Supplier", b =>
{
b.Navigation("Offerings");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,172 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CutList.Web.Migrations
{
/// <inheritdoc />
public partial class DimensionsTPTtoTPC : Migration
{
private static readonly string[] DimTables =
[
"DimAngle", "DimChannel", "DimFlatBar", "DimIBeam", "DimPipe",
"DimRectangularTube", "DimRoundBar", "DimRoundTube", "DimSquareBar", "DimSquareTube"
];
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
// 1. Drop FKs from shape tables to DimBase
foreach (var table in DimTables)
{
migrationBuilder.DropForeignKey(
name: $"FK_{table}_DimBase_Id",
table: table);
}
// 2. Add MaterialId column to each shape table (nullable initially)
foreach (var table in DimTables)
{
migrationBuilder.AddColumn<int>(
name: "MaterialId",
table: table,
type: "int",
nullable: true);
}
// 3. Copy MaterialId from DimBase into each shape table
foreach (var table in DimTables)
{
migrationBuilder.Sql(
$"UPDATE t SET t.MaterialId = b.MaterialId FROM [{table}] t INNER JOIN [DimBase] b ON t.Id = b.Id");
}
// 4. Make MaterialId non-nullable now that data is populated
foreach (var table in DimTables)
{
migrationBuilder.AlterColumn<int>(
name: "MaterialId",
table: table,
type: "int",
nullable: false,
oldClrType: typeof(int),
oldNullable: true);
}
// 5. Drop DimBase
migrationBuilder.DropTable(name: "DimBase");
// 6. Create shared sequence for unique IDs across all shape tables
migrationBuilder.CreateSequence(name: "MaterialDimensionsSequence");
// 7. Switch Id columns to use the sequence
foreach (var table in DimTables)
{
migrationBuilder.AlterColumn<int>(
name: "Id",
table: table,
type: "int",
nullable: false,
defaultValueSql: "NEXT VALUE FOR [MaterialDimensionsSequence]",
oldClrType: typeof(int),
oldType: "int");
}
// 8. Create indexes and FKs for MaterialId on each shape table
foreach (var table in DimTables)
{
migrationBuilder.CreateIndex(
name: $"IX_{table}_MaterialId",
table: table,
column: "MaterialId",
unique: true);
migrationBuilder.AddForeignKey(
name: $"FK_{table}_Materials_MaterialId",
table: table,
column: "MaterialId",
principalTable: "Materials",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
// Drop FKs and indexes from shape tables
foreach (var table in DimTables)
{
migrationBuilder.DropForeignKey(
name: $"FK_{table}_Materials_MaterialId",
table: table);
migrationBuilder.DropIndex(
name: $"IX_{table}_MaterialId",
table: table);
}
// Remove sequence from Id columns
foreach (var table in DimTables)
{
migrationBuilder.AlterColumn<int>(
name: "Id",
table: table,
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int",
oldDefaultValueSql: "NEXT VALUE FOR [MaterialDimensionsSequence]");
}
migrationBuilder.DropSequence(name: "MaterialDimensionsSequence");
// Re-create DimBase
migrationBuilder.CreateTable(
name: "DimBase",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
MaterialId = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_DimBase", x => x.Id);
table.ForeignKey(
name: "FK_DimBase_Materials_MaterialId",
column: x => x.MaterialId,
principalTable: "Materials",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_DimBase_MaterialId",
table: "DimBase",
column: "MaterialId",
unique: true);
// Copy data back to DimBase from all shape tables
foreach (var table in DimTables)
{
migrationBuilder.Sql(
$"SET IDENTITY_INSERT [DimBase] ON; INSERT INTO [DimBase] (Id, MaterialId) SELECT Id, MaterialId FROM [{table}]; SET IDENTITY_INSERT [DimBase] OFF;");
}
// Re-add FKs from shape tables to DimBase and drop MaterialId
foreach (var table in DimTables)
{
migrationBuilder.AddForeignKey(
name: $"FK_{table}_DimBase_Id",
table: table,
column: "Id",
principalTable: "DimBase",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.DropColumn(name: "MaterialId", table: table);
}
}
}
}

View File

@@ -22,6 +22,8 @@ namespace CutList.Web.Migrations
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.HasSequence("MaterialDimensionsSequence");
modelBuilder.Entity("CutList.Web.Data.Entities.CuttingTool", b =>
{
b.Property<int>("Id")
@@ -274,14 +276,10 @@ namespace CutList.Web.Migrations
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
.HasColumnType("int")
.HasDefaultValueSql("NEXT VALUE FOR [MaterialDimensionsSequence]");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("DimensionType")
.IsRequired()
.HasMaxLength(21)
.HasColumnType("nvarchar(21)");
SqlServerPropertyBuilderExtensions.UseSequence(b.Property<int>("Id"));
b.Property<int>("MaterialId")
.HasColumnType("int");
@@ -291,11 +289,9 @@ namespace CutList.Web.Migrations
b.HasIndex("MaterialId")
.IsUnique();
b.ToTable("MaterialDimensions");
b.ToTable((string)null);
b.HasDiscriminator<string>("DimensionType").HasValue("MaterialDimensions");
b.UseTphMappingStrategy();
b.UseTpcMappingStrategy();
});
modelBuilder.Entity("CutList.Web.Data.Entities.PurchaseItem", b =>
@@ -527,14 +523,12 @@ namespace CutList.Web.Migrations
.HasColumnType("decimal(10,4)");
b.Property<decimal>("Thickness")
.ValueGeneratedOnUpdateSometimes()
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)")
.HasColumnName("Thickness");
.HasColumnType("decimal(10,4)");
b.HasIndex("Leg1");
b.HasDiscriminator().HasValue("Angle");
b.ToTable("DimAngle", (string)null);
});
modelBuilder.Entity("CutList.Web.Data.Entities.ChannelDimensions", b =>
@@ -546,10 +540,8 @@ namespace CutList.Web.Migrations
.HasColumnType("decimal(10,4)");
b.Property<decimal>("Height")
.ValueGeneratedOnUpdateSometimes()
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)")
.HasColumnName("Height");
.HasColumnType("decimal(10,4)");
b.Property<decimal>("Web")
.HasPrecision(10, 4)
@@ -557,7 +549,7 @@ namespace CutList.Web.Migrations
b.HasIndex("Height");
b.HasDiscriminator().HasValue("Channel");
b.ToTable("DimChannel", (string)null);
});
modelBuilder.Entity("CutList.Web.Data.Entities.FlatBarDimensions", b =>
@@ -565,20 +557,16 @@ namespace CutList.Web.Migrations
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
b.Property<decimal>("Thickness")
.ValueGeneratedOnUpdateSometimes()
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)")
.HasColumnName("Thickness");
.HasColumnType("decimal(10,4)");
b.Property<decimal>("Width")
.ValueGeneratedOnUpdateSometimes()
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)")
.HasColumnName("Width");
.HasColumnType("decimal(10,4)");
b.HasIndex("Width");
b.HasDiscriminator().HasValue("FlatBar");
b.ToTable("DimFlatBar", (string)null);
});
modelBuilder.Entity("CutList.Web.Data.Entities.IBeamDimensions", b =>
@@ -586,10 +574,8 @@ namespace CutList.Web.Migrations
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
b.Property<decimal>("Height")
.ValueGeneratedOnUpdateSometimes()
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)")
.HasColumnName("Height");
.HasColumnType("decimal(10,4)");
b.Property<decimal>("WeightPerFoot")
.HasPrecision(10, 4)
@@ -597,7 +583,7 @@ namespace CutList.Web.Migrations
b.HasIndex("Height");
b.HasDiscriminator().HasValue("IBeam");
b.ToTable("DimIBeam", (string)null);
});
modelBuilder.Entity("CutList.Web.Data.Entities.PipeDimensions", b =>
@@ -613,14 +599,12 @@ namespace CutList.Web.Migrations
.HasColumnType("nvarchar(20)");
b.Property<decimal?>("Wall")
.ValueGeneratedOnUpdateSometimes()
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)")
.HasColumnName("Wall");
.HasColumnType("decimal(10,4)");
b.HasIndex("NominalSize");
b.HasDiscriminator().HasValue("Pipe");
b.ToTable("DimPipe", (string)null);
});
modelBuilder.Entity("CutList.Web.Data.Entities.RectangularTubeDimensions", b =>
@@ -628,26 +612,20 @@ namespace CutList.Web.Migrations
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
b.Property<decimal>("Height")
.ValueGeneratedOnUpdateSometimes()
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)")
.HasColumnName("Height");
.HasColumnType("decimal(10,4)");
b.Property<decimal>("Wall")
.ValueGeneratedOnUpdateSometimes()
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)")
.HasColumnName("Wall");
.HasColumnType("decimal(10,4)");
b.Property<decimal>("Width")
.ValueGeneratedOnUpdateSometimes()
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)")
.HasColumnName("Width");
.HasColumnType("decimal(10,4)");
b.HasIndex("Width");
b.HasDiscriminator().HasValue("RectangularTube");
b.ToTable("DimRectangularTube", (string)null);
});
modelBuilder.Entity("CutList.Web.Data.Entities.RoundBarDimensions", b =>
@@ -660,7 +638,7 @@ namespace CutList.Web.Migrations
b.HasIndex("Diameter");
b.HasDiscriminator().HasValue("RoundBar");
b.ToTable("DimRoundBar", (string)null);
});
modelBuilder.Entity("CutList.Web.Data.Entities.RoundTubeDimensions", b =>
@@ -672,14 +650,12 @@ namespace CutList.Web.Migrations
.HasColumnType("decimal(10,4)");
b.Property<decimal>("Wall")
.ValueGeneratedOnUpdateSometimes()
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)")
.HasColumnName("Wall");
.HasColumnType("decimal(10,4)");
b.HasIndex("OuterDiameter");
b.HasDiscriminator().HasValue("RoundTube");
b.ToTable("DimRoundTube", (string)null);
});
modelBuilder.Entity("CutList.Web.Data.Entities.SquareBarDimensions", b =>
@@ -687,14 +663,12 @@ namespace CutList.Web.Migrations
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
b.Property<decimal>("Size")
.ValueGeneratedOnUpdateSometimes()
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)")
.HasColumnName("Size");
.HasColumnType("decimal(10,4)");
b.HasIndex("Size");
b.HasDiscriminator().HasValue("SquareBar");
b.ToTable("DimSquareBar", (string)null);
});
modelBuilder.Entity("CutList.Web.Data.Entities.SquareTubeDimensions", b =>
@@ -702,20 +676,16 @@ namespace CutList.Web.Migrations
b.HasBaseType("CutList.Web.Data.Entities.MaterialDimensions");
b.Property<decimal>("Size")
.ValueGeneratedOnUpdateSometimes()
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)")
.HasColumnName("Size");
.HasColumnType("decimal(10,4)");
b.Property<decimal>("Wall")
.ValueGeneratedOnUpdateSometimes()
.HasPrecision(10, 4)
.HasColumnType("decimal(10,4)")
.HasColumnName("Wall");
.HasColumnType("decimal(10,4)");
b.HasIndex("Size");
b.HasDiscriminator().HasValue("SquareTube");
b.ToTable("DimSquareTube", (string)null);
});
modelBuilder.Entity("CutList.Web.Data.Entities.Job", b =>

View File

@@ -10,6 +10,9 @@ builder.Services.AddControllers();
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Add Entity Framework
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
@@ -27,7 +30,12 @@ builder.Services.AddScoped<CatalogService>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
else
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
app.UseHsts();

View File

@@ -0,0 +1,83 @@
# Alro Steel SmartGrid Scraper — Remaining Steps
## Status: Script is READY TO RUN
The scraper at `scripts/AlroCatalog/scrape_alro.py` is complete and tested. Discovery mode confirmed it works correctly against the live site.
## What's Done
1. Script written with correct ASP.NET control IDs (discovered via `--discover` mode)
2. Level 1 (main grid) navigation: working
3. Level 2 (popup grid) navigation: working
4. Level 3 (dims panel) scraping: working — uses cascading dropdowns `ddlDimA``ddlDimB``ddlDimC``ddlLength`
5. Grade filter: 11 common grades (A-36, 1018, 1045, 1144, 12L14, etc.)
6. Size string normalization: "1-1/2\"" matches O'Neal format
7. Progress save/resume: working
8. Discovery mode verified: A-36 Round bars → 27 sizes, 80 items (lengths include "20 FT", "Custom Cut List", "Drop/Remnant" — non-stock entries filtered out in catalog builder)
## Remaining Steps
### Step 1: Run the full scrape
```bash
cd C:\Users\aisaacs\Desktop\Projects\CutList
python scripts/AlroCatalog/scrape_alro.py
```
- This scrapes all 3 categories (Bars, Pipe/Tube, Structural) for 11 filtered grades
- Takes ~30-60 minutes (cascading dropdown selections with 1.5s delay each)
- Progress saved incrementally to `scripts/AlroCatalog/alro-scrape-progress.json`
- If interrupted, resume with `python scripts/AlroCatalog/scrape_alro.py --resume`
- To scrape ALL grades: `python scripts/AlroCatalog/scrape_alro.py --all-grades`
### Step 2: Review output
- Output: `CutList.Web/Data/SeedData/alro-catalog.json`
- Verify material counts, shapes, sizes
- Spot-check dimensions against myalro.com
- Compare shape coverage to O'Neal catalog
### Step 3: Post-scrape adjustments (if needed)
**Dimension mapping for Structural/Pipe shapes**: The `build_size_and_dims()` function handles all shapes but Structural (Angle, Channel, Beam) and Pipe/Tube shapes haven't been tested live yet. After scraping, check the screenshots in `scripts/AlroCatalog/screenshots/` to verify dimension mapping. The first item of each new shape gets a screenshot + HTML dump.
**Known dimension mapping assumptions:**
- Angle: DimA = leg size, DimB = thickness → `"leg1 x leg2 x thickness"` (assumes equal legs)
- Channel: DimA = height, DimB = flange → needs verification
- IBeam: DimA = depth, DimB = weight/ft → `"W{depth} x {wt}"`
- SquareTube: DimA = size, DimB = wall
- RectTube: DimA = width, DimB = height, DimC = wall
- RoundTube: DimA = OD, DimB = wall
- Pipe: DimA = NPS, DimB = schedule
**If dimension mapping is wrong for a shape**: Edit the `build_size_and_dims()` function in `scrape_alro.py` and re-run just the catalog builder:
```python
python -c "
import json
from scripts.AlroCatalog.scrape_alro import build_catalog
data = json.load(open('scripts/AlroCatalog/alro-scrape-progress.json'))
catalog = build_catalog(data['items'])
json.dump(catalog, open('CutList.Web/Data/SeedData/alro-catalog.json', 'w'), indent=2)
"
```
### Step 4: Part numbers (optional future enhancement)
The current scraper captures sizes and lengths but NOT part numbers. To get part numbers, the script would need to:
1. Select DimA + DimB + Length
2. Click the "Next >" button (`btnSearch`)
3. Capture part number from the results panel
4. Click Back
This adds significant time per item. The catalog works without part numbers — the supplierOfferings have empty partNumber/supplierDescription fields.
## Key Files
| File | Purpose |
|------|---------|
| `scripts/AlroCatalog/scrape_alro.py` | The scraper script |
| `scripts/AlroCatalog/alro-scrape-progress.json` | Incremental progress (resume support) |
| `scripts/AlroCatalog/screenshots/` | Discovery HTML/screenshots per shape |
| `CutList.Web/Data/SeedData/alro-catalog.json` | Final output (same schema as oneals-catalog.json) |
| `CutList.Web/Data/SeedData/oneals-catalog.json` | Reference format |
## Grade Filter (editable in script)
Located at line ~50 in `scrape_alro.py`. Current filter:
- A-36, 1018 CF, 1018 HR, 1044 HR, 1045 CF, 1045 HR, 1045 TG&P
- 1144 CF, 1144 HR, 12L14 CF, A311/Stressproof
To add/remove grades, edit the `GRADE_FILTER` set in the script.

View File

@@ -0,0 +1,971 @@
{
"completed": [
[
"Bars",
"A-36",
"ROUND"
]
],
"items": [
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": ".188",
"dim_a_text": "3/16",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Custom Cut List",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": ".188",
"dim_a_text": "3/16",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "20 FT",
"length_inches": 240.0
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": ".250",
"dim_a_text": "1/4",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Custom Cut List",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": ".250",
"dim_a_text": "1/4",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Drop/Remnant",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": ".250",
"dim_a_text": "1/4",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "20 FT",
"length_inches": 240.0
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": ".313",
"dim_a_text": "5/16",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Custom Cut List",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": ".313",
"dim_a_text": "5/16",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Drop/Remnant",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": ".313",
"dim_a_text": "5/16",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "20 FT",
"length_inches": 240.0
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": ".375",
"dim_a_text": "3/8",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Custom Cut List",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": ".375",
"dim_a_text": "3/8",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Drop/Remnant",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": ".375",
"dim_a_text": "3/8",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "20 FT",
"length_inches": 240.0
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": ".438",
"dim_a_text": "7/16",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Custom Cut List",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": ".438",
"dim_a_text": "7/16",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Drop/Remnant",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": ".438",
"dim_a_text": "7/16",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "20 FT",
"length_inches": 240.0
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": ".500",
"dim_a_text": "1/2",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Custom Cut List",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": ".500",
"dim_a_text": "1/2",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Drop/Remnant",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": ".500",
"dim_a_text": "1/2",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "20 FT",
"length_inches": 240.0
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": ".563",
"dim_a_text": "9/16",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Custom Cut List",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": ".563",
"dim_a_text": "9/16",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Drop/Remnant",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": ".563",
"dim_a_text": "9/16",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "20 FT",
"length_inches": 240.0
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": ".625",
"dim_a_text": "5/8",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Custom Cut List",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": ".625",
"dim_a_text": "5/8",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Drop/Remnant",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": ".625",
"dim_a_text": "5/8",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "20 FT",
"length_inches": 240.0
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": ".750",
"dim_a_text": "3/4",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Custom Cut List",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": ".750",
"dim_a_text": "3/4",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Drop/Remnant",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": ".750",
"dim_a_text": "3/4",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "20 FT",
"length_inches": 240.0
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": ".875",
"dim_a_text": "7/8",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Custom Cut List",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": ".875",
"dim_a_text": "7/8",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Drop/Remnant",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": ".875",
"dim_a_text": "7/8",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "20 FT",
"length_inches": 240.0
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "1.000",
"dim_a_text": "1",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Custom Cut List",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "1.000",
"dim_a_text": "1",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Drop/Remnant",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "1.000",
"dim_a_text": "1",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "20 FT",
"length_inches": 240.0
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "1.125",
"dim_a_text": "1 1/8",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Custom Cut List",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "1.125",
"dim_a_text": "1 1/8",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Drop/Remnant",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "1.125",
"dim_a_text": "1 1/8",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "20 FT",
"length_inches": 240.0
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "1.250",
"dim_a_text": "1 1/4",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Custom Cut List",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "1.250",
"dim_a_text": "1 1/4",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Drop/Remnant",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "1.250",
"dim_a_text": "1 1/4",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "20 FT",
"length_inches": 240.0
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "1.375",
"dim_a_text": "1 3/8",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Custom Cut List",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "1.375",
"dim_a_text": "1 3/8",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Drop/Remnant",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "1.375",
"dim_a_text": "1 3/8",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "20 FT",
"length_inches": 240.0
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "1.500",
"dim_a_text": "1 1/2",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Custom Cut List",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "1.500",
"dim_a_text": "1 1/2",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Drop/Remnant",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "1.500",
"dim_a_text": "1 1/2",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "20 FT",
"length_inches": 240.0
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "1.625",
"dim_a_text": "1 5/8",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Custom Cut List",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "1.625",
"dim_a_text": "1 5/8",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Drop/Remnant",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "1.625",
"dim_a_text": "1 5/8",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "20 FT",
"length_inches": 240.0
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "1.750",
"dim_a_text": "1 3/4",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Custom Cut List",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "1.750",
"dim_a_text": "1 3/4",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Drop/Remnant",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "1.750",
"dim_a_text": "1 3/4",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "20 FT",
"length_inches": 240.0
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "1.875",
"dim_a_text": "1 7/8",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Custom Cut List",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "1.875",
"dim_a_text": "1 7/8",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Drop/Remnant",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "1.875",
"dim_a_text": "1 7/8",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "20 FT",
"length_inches": 240.0
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "2.000",
"dim_a_text": "2",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Custom Cut List",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "2.000",
"dim_a_text": "2",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Drop/Remnant",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "2.000",
"dim_a_text": "2",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "20 FT",
"length_inches": 240.0
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "2.125",
"dim_a_text": "2 1/8",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Custom Cut List",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "2.125",
"dim_a_text": "2 1/8",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Drop/Remnant",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "2.125",
"dim_a_text": "2 1/8",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "20 FT",
"length_inches": 240.0
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "2.250",
"dim_a_text": "2 1/4",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Custom Cut List",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "2.250",
"dim_a_text": "2 1/4",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Drop/Remnant",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "2.250",
"dim_a_text": "2 1/4",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "20 FT",
"length_inches": 240.0
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "2.375",
"dim_a_text": "2 3/8",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Custom Cut List",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "2.375",
"dim_a_text": "2 3/8",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Drop/Remnant",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "2.375",
"dim_a_text": "2 3/8",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "20 FT",
"length_inches": 240.0
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "2.500",
"dim_a_text": "2 1/2",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Custom Cut List",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "2.500",
"dim_a_text": "2 1/2",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Drop/Remnant",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "2.500",
"dim_a_text": "2 1/2",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "20 FT",
"length_inches": 240.0
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "2.625",
"dim_a_text": "2 5/8",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Custom Cut List",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "2.625",
"dim_a_text": "2 5/8",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Drop/Remnant",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "2.625",
"dim_a_text": "2 5/8",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "20 FT",
"length_inches": 240.0
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "2.750",
"dim_a_text": "2 3/4",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Custom Cut List",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "2.750",
"dim_a_text": "2 3/4",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Drop/Remnant",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "2.750",
"dim_a_text": "2 3/4",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "20 FT",
"length_inches": 240.0
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "2.875",
"dim_a_text": "2 7/8",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Custom Cut List",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "2.875",
"dim_a_text": "2 7/8",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Drop/Remnant",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "2.875",
"dim_a_text": "2 7/8",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "20 FT",
"length_inches": 240.0
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "3.000",
"dim_a_text": "3",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Custom Cut List",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "3.000",
"dim_a_text": "3",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "Drop/Remnant",
"length_inches": null
},
{
"grade": "A-36",
"shape": "RoundBar",
"dim_a_val": "3.000",
"dim_a_text": "3",
"dim_b_val": null,
"dim_b_text": null,
"dim_c_val": null,
"dim_c_text": null,
"length_text": "20 FT",
"length_inches": 240.0
}
]
}

View File

@@ -0,0 +1,728 @@
#!/usr/bin/env python3
"""
Alro Steel SmartGrid Scraper
Scrapes myalro.com's SmartGrid for Carbon Steel materials and outputs
a catalog JSON matching the O'Neal catalog format.
Usage:
python scrape_alro.py # Scrape filtered grades (edit GRADE_FILTER below)
python scrape_alro.py --all-grades # Scrape ALL grades (slow)
python scrape_alro.py --discover # Scrape first item only, dump HTML/screenshots
python scrape_alro.py --resume # Resume from saved progress
"""
import asyncio
import json
import re
import sys
import logging
from datetime import datetime, timezone
from pathlib import Path
from playwright.async_api import async_playwright, Page, TimeoutError as PwTimeout
from playwright_stealth import Stealth
# ── Logging ──────────────────────────────────────────────────────────
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
datefmt="%H:%M:%S",
)
log = logging.getLogger(__name__)
# ── Paths ────────────────────────────────────────────────────────────
SCRIPT_DIR = Path(__file__).parent.resolve()
OUTPUT_PATH = (SCRIPT_DIR / "../../CutList.Web/Data/SeedData/alro-catalog.json").resolve()
PROGRESS_PATH = SCRIPT_DIR / "alro-scrape-progress.json"
SCREENSHOTS_DIR = SCRIPT_DIR / "screenshots"
# ── Config ───────────────────────────────────────────────────────────
BASE_URL = "https://www.myalro.com/SmartGrid.aspx?PT=Steel&Clear=true"
DELAY = 5 # seconds between postback clicks
TIMEOUT = 15_000 # ms for element waits
CS_ROW = 4 # Carbon Steel row index in main grid
CATEGORIES = ["Bars", "Pipe / Tube", "Structural"]
# ┌─────────────────────────────────────────────────────────────────┐
# │ GRADE FILTER — only these grades will be scraped. │
# │ Use --all-grades flag to override and scrape everything. │
# │ Grade names must match the gpname attribute exactly. │
# └─────────────────────────────────────────────────────────────────┘
GRADE_FILTER = {
# Common structural / general purpose
"A-36",
# Mild steel
"1018 CF",
"1018 HR",
# Medium carbon (shafts, gears, pins)
"1045 CF",
"1045 HR",
"1045 TG&P",
# Free-machining
"1144 CF",
"1144 HR",
"12L14 CF",
# Hot-rolled plate/bar
"1044 HR",
# Stressproof (high-strength shafting)
"A311/Stressproof",
}
# Alro shape column header → our MaterialShape enum
SHAPE_MAP = {
"ROUND": "RoundBar",
"FLAT": "FlatBar",
"SQUARE": "SquareBar",
"ANGLE": "Angle",
"CHANNEL": "Channel",
"BEAM": "IBeam",
"SQ TUBE": "SquareTube",
"SQUARE TUBE": "SquareTube",
"REC TUBE": "RectangularTube",
"RECT TUBE": "RectangularTube",
"RECTANGULAR TUBE": "RectangularTube",
"ROUND TUBE": "RoundTube",
"RND TUBE": "RoundTube",
"PIPE": "Pipe",
}
# ── ASP.NET control IDs ─────────────────────────────────────────
_CP = "ctl00_ContentPlaceHolder1"
_PU = f"{_CP}_pnlPopUP"
ID = dict(
main_grid = f"{_CP}_grdMain",
popup_grid = f"{_PU}_grdPopUp",
popup_window = f"{_PU}_Window",
dims_panel = f"{_PU}_upnlDims",
back_btn = f"{_PU}_btnBack",
# Dimension dropdowns (cascading: A → B → C → Length)
dim_a = f"{_PU}_ddlDimA",
dim_b = f"{_PU}_ddlDimB",
dim_c = f"{_PU}_ddlDimC",
dim_length = f"{_PU}_ddlLength",
btn_next = f"{_PU}_btnSearch",
)
# Postback targets ($ separators)
PB = dict(
main_grid = "ctl00$ContentPlaceHolder1$grdMain",
popup_grid = "ctl00$ContentPlaceHolder1$pnlPopUP$grdPopUp",
back_btn = "ctl00$ContentPlaceHolder1$pnlPopUP$btnBack",
popup = "ctl00$ContentPlaceHolder1$pnlPopUP",
dim_a = "ctl00$ContentPlaceHolder1$pnlPopUP$ddlDimA",
dim_b = "ctl00$ContentPlaceHolder1$pnlPopUP$ddlDimB",
dim_c = "ctl00$ContentPlaceHolder1$pnlPopUP$ddlDimC",
)
# ═══════════════════════════════════════════════════════════════════════
# Utility helpers
# ═══════════════════════════════════════════════════════════════════════
def parse_fraction(s: str) -> float | None:
"""Parse fraction/decimal string → float. '1-1/4' → 1.25, '.250' → 0.25"""
if not s:
return None
s = s.strip().strip('"\'')
# Collapse double spaces from Alro dropdown text ("1 1/4" → "1 1/4")
s = re.sub(r"\s+", " ", s)
if not s:
return None
try:
return float(s)
except ValueError:
pass
# Mixed fraction: "1-1/4" or "1 1/4"
m = re.match(r"^(\d+)[\s-](\d+)/(\d+)$", s)
if m:
return int(m[1]) + int(m[2]) / int(m[3])
m = re.match(r"^(\d+)/(\d+)$", s)
if m:
return int(m[1]) / int(m[2])
m = re.match(r"^(\d+)$", s)
if m:
return float(m[1])
return None
def decimal_to_fraction(value: float) -> str:
"""0.25 → '1/4', 1.25 → '1-1/4', 3.0 → '3'"""
if value <= 0:
return "0"
whole = int(value)
frac = value - whole
if abs(frac) < 0.001:
return str(whole)
from math import gcd
sixteenths = round(frac * 16)
if sixteenths == 16:
return str(whole + 1)
g = gcd(sixteenths, 16)
num, den = sixteenths // g, 16 // g
frac_s = f"{num}/{den}"
return f"{whole}-{frac_s}" if whole else frac_s
def normalize_dim_text(s: str) -> str:
"""Normalize dimension text: '1 1/4''1-1/4', '3/16''3/16'"""
s = re.sub(r"\s+", " ", s.strip())
# "1 1/4" → "1-1/4" (mixed fraction with space → hyphen)
s = re.sub(r"^(\d+)\s+(\d+/\d+)$", r"\1-\2", s)
return s
def parse_length_to_inches(text: str) -> float | None:
"""Parse length string to inches. \"20'\" → 240, \"240\" → 240"""
s = text.strip().upper()
s = re.sub(r"\s*(RL|RANDOM.*|LENGTHS?|EA|EACH|STOCK)\s*", "", s).strip()
m = re.match(r"^(\d+(?:\.\d+)?)\s*['\u2032]", s)
if m:
return float(m[1]) * 12
m = re.match(r"^(\d+(?:\.\d+)?)\s*FT", s)
if m:
return float(m[1]) * 12
m = re.match(r'^(\d+(?:\.\d+)?)\s*"?\s*$', s)
if m:
v = float(m[1])
return v * 12 if v <= 30 else v
return None
# ═══════════════════════════════════════════════════════════════════════
# SmartGrid navigation
# ═══════════════════════════════════════════════════════════════════════
async def wait_for_update(page: Page, timeout: int = TIMEOUT):
"""Wait for ASP.NET partial postback to finish."""
try:
await page.wait_for_load_state("networkidle", timeout=timeout)
except PwTimeout:
log.warning(" networkidle timeout continuing")
await asyncio.sleep(0.5)
async def do_postback(page: Page, target: str, arg: str):
"""Execute a __doPostBack call."""
await page.evaluate(f"__doPostBack('{target}', '{arg}')")
async def click_category(page: Page, category: str) -> bool:
"""Click a category blue-button for Carbon Steel in the main grid."""
log.info(f"Clicking main grid: {category} (row {CS_ROW})")
arg = f"{category}${CS_ROW}"
link = await page.query_selector(
f"#{ID['main_grid']} a[href*=\"'{arg}'\"] img[src*='blue_button']"
)
if not link:
log.error(f" Button not found for {arg}")
return False
parent = await link.evaluate_handle("el => el.parentElement")
await parent.as_element().click()
try:
await page.wait_for_selector(f"#{ID['popup_grid']}", state="visible", timeout=TIMEOUT)
await wait_for_update(page)
return True
except PwTimeout:
log.error(f" Popup did not appear for {category}")
return False
async def scrape_popup_grid(page: Page):
"""Parse the popup grid → [(grade_name, grade_id, shape, row_idx, has_btn)]."""
headers = await page.eval_on_selector_all(
f"#{ID['popup_grid']} tr.DataHeader th",
"els => els.map(el => el.textContent.trim())",
)
log.info(f" Popup columns: {headers}")
rows = await page.query_selector_all(
f"#{ID['popup_grid']} tr.griditemP, #{ID['popup_grid']} tr.gridaltItemP"
)
combos = []
for row_idx, row in enumerate(rows):
first_td = await row.query_selector("td[gpid]")
if not first_td:
continue
gid = (await first_td.get_attribute("gpid") or "").strip()
gname = (await first_td.get_attribute("gpname") or "").strip()
tds = await row.query_selector_all("td")
for col_idx, td in enumerate(tds):
if col_idx == 0:
continue
shape = headers[col_idx] if col_idx < len(headers) else ""
img = await td.query_selector("img[src*='blue_button']")
combos.append((gname, gid, shape, row_idx, img is not None))
active = sum(1 for c in combos if c[4])
log.info(f" {active} active grade/shape combos")
return combos
async def click_shape(page: Page, shape: str, row_idx: int) -> bool:
"""Click a shape button in the popup grid; wait for dims panel."""
arg = f"{shape}${row_idx}"
link = await page.query_selector(
f"#{ID['popup_grid']} a[href*=\"'{arg}'\"] img[src*='blue_button']"
)
if not link:
try:
await do_postback(page, PB["popup_grid"], arg)
except Exception:
log.warning(f" Could not click shape {arg}")
return False
else:
parent = await link.evaluate_handle("el => el.parentElement")
await parent.as_element().click()
try:
# Wait for the DimA dropdown to appear (the real indicator of dims panel loaded)
await page.wait_for_selector(f"#{ID['dim_a']}", state="attached", timeout=TIMEOUT)
await wait_for_update(page)
return True
except PwTimeout:
# Check if panel has any content at all
html = await page.inner_html(f"#{ID['dims_panel']}")
if len(html.strip()) > 50:
await wait_for_update(page)
return True
log.warning(f" Dims panel timeout for {arg}")
return False
async def click_back(page: Page):
"""Click Back to return to the popup grid view."""
try:
await do_postback(page, PB["back_btn"], "")
await wait_for_update(page)
await asyncio.sleep(DELAY)
except Exception as e:
log.warning(f" Back button error: {e}")
async def close_popup(page: Page):
"""Close the popup window and return to the main grid."""
try:
await do_postback(page, PB["popup"], "Close")
await wait_for_update(page)
await asyncio.sleep(DELAY)
except Exception as e:
log.warning(f" Close popup error: {e}")
# ═══════════════════════════════════════════════════════════════════════
# Level 3 — Dimension Panel Scraping
# ═══════════════════════════════════════════════════════════════════════
async def get_select_options(page: Page, sel_id: str):
"""Return [(value, text), ...] for a <select>, excluding placeholders."""
el = await page.query_selector(f"#{sel_id}")
if not el:
return []
# Check if disabled
disabled = await el.get_attribute("disabled")
if disabled:
return []
try:
opts = await page.eval_on_selector(
f"#{sel_id}",
"""el => Array.from(el.options).map(o => ({
v: o.value, t: o.text.trim(), d: o.disabled
}))""",
)
except Exception:
return []
return [
(o["v"], o["t"])
for o in opts
if o["v"] and o["v"] != "-1" and o["t"] and not o["d"]
and o["t"].lower() not in ("- select -", "--select--", "select...", "select", "")
]
async def scrape_dims_panel(page: Page, grade: str, shape_alro: str,
shape_mapped: str, *, save_discovery: bool = False):
"""Main Level 3 extraction. Returns list of raw item dicts."""
items: list[dict] = []
if save_discovery:
SCREENSHOTS_DIR.mkdir(exist_ok=True)
safe = f"{grade}_{shape_alro}".replace(" ", "_").replace("/", "-")
await page.screenshot(path=str(SCREENSHOTS_DIR / f"dims_{safe}.png"), full_page=True)
html = await page.inner_html(f"#{ID['dims_panel']}")
(SCREENSHOTS_DIR / f"dims_{safe}.html").write_text(html, encoding="utf-8")
log.info(f" Discovery saved → screenshots/dims_{safe}.*")
# ── Get DimA options (primary dimension: diameter, width, size, etc.) ──
dim_a_opts = await get_select_options(page, ID["dim_a"])
if not dim_a_opts:
log.warning(f" No DimA options found")
try:
html = await page.inner_html(f"#{ID['dims_panel']}")
if len(html) > 50:
SCREENSHOTS_DIR.mkdir(exist_ok=True)
safe = f"{grade}_{shape_alro}_nodimopts".replace(" ", "_").replace("/", "-")
(SCREENSHOTS_DIR / f"{safe}.html").write_text(html, encoding="utf-8")
except Exception as e:
log.warning(f" Could not dump dims panel: {e}")
return []
log.info(f" DimA: {len(dim_a_opts)} sizes")
for a_val, a_text in dim_a_opts:
# Select DimA → triggers postback → DimB/Length populate
await page.select_option(f"#{ID['dim_a']}", a_val)
await asyncio.sleep(DELAY)
await wait_for_update(page)
# Check if DimB appeared (secondary dimension: thickness, wall, etc.)
dim_b_opts = await get_select_options(page, ID["dim_b"])
if dim_b_opts:
for b_val, b_text in dim_b_opts:
await page.select_option(f"#{ID['dim_b']}", b_val)
await asyncio.sleep(DELAY)
await wait_for_update(page)
# Check for DimC (tertiary — rare)
dim_c_opts = await get_select_options(page, ID["dim_c"])
if dim_c_opts:
for c_val, c_text in dim_c_opts:
await page.select_option(f"#{ID['dim_c']}", c_val)
await asyncio.sleep(DELAY)
await wait_for_update(page)
lengths = await get_select_options(page, ID["dim_length"])
for l_val, l_text in lengths:
items.append(_make_item(
grade, shape_mapped,
a_val, a_text, b_val, b_text, c_val, c_text,
l_text,
))
else:
# No DimC — read lengths
lengths = await get_select_options(page, ID["dim_length"])
for l_val, l_text in lengths:
items.append(_make_item(
grade, shape_mapped,
a_val, a_text, b_val, b_text, None, None,
l_text,
))
else:
# No DimB — just DimA + Length
lengths = await get_select_options(page, ID["dim_length"])
for l_val, l_text in lengths:
items.append(_make_item(
grade, shape_mapped,
a_val, a_text, None, None, None, None,
l_text,
))
return items
def _make_item(grade, shape, a_val, a_text, b_val, b_text, c_val, c_text, l_text):
"""Build a raw item dict from dimension selections."""
return {
"grade": grade,
"shape": shape,
"dim_a_val": a_val, # decimal string like ".500"
"dim_a_text": a_text, # fraction string like "1/2"
"dim_b_val": b_val,
"dim_b_text": b_text,
"dim_c_val": c_val,
"dim_c_text": c_text,
"length_text": l_text,
"length_inches": parse_length_to_inches(l_text),
}
# ═══════════════════════════════════════════════════════════════════════
# Output — build catalog JSON
# ═══════════════════════════════════════════════════════════════════════
def build_size_and_dims(shape: str, item: dict):
"""Return (size_string, dimensions_dict) for a catalog material entry.
Uses the decimal values from dropdown option values for precision,
and fraction text from dropdown option text for display.
"""
# Use the numeric value from the dropdown (e.g. ".500") for precision
a = float(item["dim_a_val"]) if item.get("dim_a_val") else None
b = float(item["dim_b_val"]) if item.get("dim_b_val") else None
c = float(item["dim_c_val"]) if item.get("dim_c_val") else None
a_txt = normalize_dim_text(item.get("dim_a_text") or "")
b_txt = normalize_dim_text(item.get("dim_b_text") or "")
c_txt = normalize_dim_text(item.get("dim_c_text") or "")
if shape == "RoundBar" and a is not None:
return f'{a_txt}"', {"diameter": round(a, 4)}
if shape == "FlatBar":
if a is not None and b is not None:
return (f'{a_txt}" x {b_txt}"',
{"width": round(a, 4), "thickness": round(b, 4)})
if a is not None:
return f'{a_txt}"', {"width": round(a, 4), "thickness": 0}
if shape == "SquareBar" and a is not None:
return f'{a_txt}"', {"size": round(a, 4)}
if shape == "Angle":
if a is not None and b is not None:
return (f'{a_txt}" x {a_txt}" x {b_txt}"',
{"leg1": round(a, 4), "leg2": round(a, 4), "thickness": round(b, 4)})
if a is not None:
return f'{a_txt}"', {"leg1": round(a, 4), "leg2": round(a, 4), "thickness": 0}
if shape == "Channel":
# Channels may use DimA for combined designation or height
if a is not None and b is not None:
return (f'{a_txt}" x {b_txt}"',
{"height": round(a, 4), "flange": round(b, 4), "web": 0})
if a is not None:
return a_txt, {"height": round(a, 4), "flange": 0, "web": 0}
if shape == "IBeam":
# DimA might be the W-designation, DimB the weight/ft
if a is not None and b is not None:
return (f"W{int(a)} x {b}",
{"height": round(a, 4), "weightPerFoot": round(b, 4)})
if a is not None:
return f"W{int(a)}", {"height": round(a, 4), "weightPerFoot": 0}
if shape == "SquareTube":
if a is not None and b is not None:
return (f'{a_txt}" x {b_txt}" wall',
{"size": round(a, 4), "wall": round(b, 4)})
if a is not None:
return f'{a_txt}"', {"size": round(a, 4), "wall": 0}
if shape == "RectangularTube":
if a is not None and b is not None and c is not None:
return (f'{a_txt}" x {b_txt}" x {c_txt}" wall',
{"width": round(a, 4), "height": round(b, 4), "wall": round(c, 4)})
if a is not None and b is not None:
return (f'{a_txt}" x {b_txt}"',
{"width": round(a, 4), "height": round(b, 4), "wall": 0})
if shape == "RoundTube":
if a is not None and b is not None:
return (f'{a_txt}" OD x {b_txt}" wall',
{"outerDiameter": round(a, 4), "wall": round(b, 4)})
if a is not None:
return f'{a_txt}" OD', {"outerDiameter": round(a, 4), "wall": 0}
if shape == "Pipe":
sched = b_txt or c_txt or "40"
if a is not None:
return (f'{a_txt}" NPS Sch {sched}',
{"nominalSize": round(a, 4), "schedule": sched})
# Fallback
return a_txt or "", {}
def build_catalog(scraped: list[dict]) -> dict:
"""Assemble the final catalog JSON from scraped item dicts."""
materials: dict[tuple, dict] = {}
for item in scraped:
shape = item.get("shape", "")
grade = item.get("grade", "")
if not shape or not grade:
continue
size_str, dims = build_size_and_dims(shape, item)
key = (shape, grade, size_str)
if key not in materials:
materials[key] = {
"shape": shape,
"type": "Steel",
"grade": grade,
"size": size_str,
"dimensions": dims,
"stockItems": [],
}
length = item.get("length_inches")
if length and length > 0:
existing = {si["lengthInches"] for si in materials[key]["stockItems"]}
if round(length, 4) not in existing:
materials[key]["stockItems"].append({
"lengthInches": round(length, 4),
"quantityOnHand": 0,
"supplierOfferings": [{
"supplierName": "Alro Steel",
"partNumber": "",
"supplierDescription": "",
}],
})
sorted_mats = sorted(materials.values(), key=lambda m: (m["shape"], m["grade"], m["size"]))
return {
"exportedAt": datetime.now(timezone.utc).isoformat(),
"suppliers": [{"name": "Alro Steel"}],
"cuttingTools": [
{"name": "Bandsaw", "kerfInches": 0.0625, "isDefault": True},
{"name": "Chop Saw", "kerfInches": 0.125, "isDefault": False},
{"name": "Cold Cut Saw", "kerfInches": 0.0625, "isDefault": False},
{"name": "Hacksaw", "kerfInches": 0.0625, "isDefault": False},
],
"materials": sorted_mats,
}
# ═══════════════════════════════════════════════════════════════════════
# Progress management
# ═══════════════════════════════════════════════════════════════════════
def load_progress() -> dict:
if PROGRESS_PATH.exists():
return json.loads(PROGRESS_PATH.read_text(encoding="utf-8"))
return {"completed": [], "items": []}
def save_progress(progress: dict):
PROGRESS_PATH.write_text(json.dumps(progress, indent=2, ensure_ascii=False), encoding="utf-8")
# ═══════════════════════════════════════════════════════════════════════
# Main
# ═══════════════════════════════════════════════════════════════════════
async def main():
discover = "--discover" in sys.argv
resume = "--resume" in sys.argv
all_grades = "--all-grades" in sys.argv
progress = load_progress() if resume else {"completed": [], "items": []}
all_items: list[dict] = progress.get("items", [])
done_keys: set[tuple] = {tuple(k) for k in progress.get("completed", [])}
log.info("Alro Steel SmartGrid Scraper")
if all_grades:
log.info(" Mode: ALL grades")
else:
log.info(f" Filtering to {len(GRADE_FILTER)} grades: {', '.join(sorted(GRADE_FILTER))}")
if resume:
log.info(f" Resuming: {len(done_keys)} combos done, {len(all_items)} items")
if discover:
log.info(" Discovery mode — will scrape first item then stop")
async with Stealth().use_async(async_playwright()) as pw:
browser = await pw.chromium.launch(headless=False)
ctx = await browser.new_context(
viewport={"width": 1280, "height": 900},
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
locale="en-US",
timezone_id="America/Indiana/Indianapolis",
)
page = await ctx.new_page()
log.info(f"Navigating to SmartGrid …")
await page.goto(BASE_URL, wait_until="networkidle", timeout=30_000)
await asyncio.sleep(2)
if not await page.query_selector(f"#{ID['main_grid']}"):
log.error("Main grid not found! Saving screenshot.")
SCREENSHOTS_DIR.mkdir(exist_ok=True)
await page.screenshot(path=str(SCREENSHOTS_DIR / "error_no_grid.png"))
await browser.close()
return
log.info("Main grid loaded")
total_scraped = 0
first_item = True
for category in CATEGORIES:
log.info(f"\n{'=' * 60}")
log.info(f" Category: {category}")
log.info(f"{'=' * 60}")
if not await click_category(page, category):
continue
await asyncio.sleep(DELAY)
combos = await scrape_popup_grid(page)
for grade_name, grade_id, shape_name, row_idx, has_btn in combos:
if not has_btn:
continue
# Grade filter
if not all_grades and grade_name not in GRADE_FILTER:
continue
shape_upper = shape_name.upper().strip()
shape_mapped = SHAPE_MAP.get(shape_upper)
if shape_mapped is None:
log.info(f" Skip unmapped shape: {shape_name}")
continue
combo_key = (category, grade_name, shape_name)
if combo_key in done_keys:
log.info(f" Skip (done): {grade_name} / {shape_name}")
continue
log.info(f"\n -- {grade_name} / {shape_name} -> {shape_mapped} --")
if not await click_shape(page, shape_name, row_idx):
await click_back(page)
await asyncio.sleep(DELAY)
continue
await asyncio.sleep(DELAY)
items = await scrape_dims_panel(
page, grade_name, shape_name, shape_mapped,
save_discovery=first_item or discover,
)
first_item = False
all_items.extend(items)
total_scraped += len(items)
log.info(f" -> {len(items)} items (total {total_scraped})")
done_keys.add(combo_key)
progress["completed"] = [list(k) for k in done_keys]
progress["items"] = all_items
save_progress(progress)
await click_back(page)
await asyncio.sleep(DELAY)
if discover:
log.info("\nDiscovery done. Check: scripts/AlroCatalog/screenshots/")
await browser.close()
return
await close_popup(page)
await asyncio.sleep(DELAY)
await browser.close()
# ── Build output ──
log.info(f"\n{'=' * 60}")
log.info(f"Building catalog from {len(all_items)} items …")
catalog = build_catalog(all_items)
OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
OUTPUT_PATH.write_text(json.dumps(catalog, indent=2, ensure_ascii=False), encoding="utf-8")
log.info(f"Written: {OUTPUT_PATH}")
log.info(f"Materials: {len(catalog['materials'])}")
total_stock = sum(len(m["stockItems"]) for m in catalog["materials"])
log.info(f"Stock items: {total_stock}")
by_shape: dict[str, int] = {}
for m in catalog["materials"]:
by_shape[m["shape"]] = by_shape.get(m["shape"], 0) + 1
for s, n in sorted(by_shape.items()):
log.info(f" {s}: {n}")
if __name__ == "__main__":
asyncio.run(main())