Phase 3: AR/AP aging buckets, PO seeder, Bills vendor fix
- Bills.cs: replace aceHardware/fastenal lookups with grainger/harbor/localSupply to match Phase 1 vendor renames; update all vendor invoice number prefixes - Bills.cs: add 3 AP aging-bucket bills (30-60, 61-90, 90+ days overdue) so all four AP aging buckets are populated for report demos - Invoices.cs: add 3 more overdue invoices (31-60, 61-90, 90+ day AR buckets) alongside the existing 21-day overdue; total now 29 invoices - New SeedDataService.PurchaseOrders.cs: 7 POs — 3 Received (historical), 2 Submitted (in-flight), 2 Draft (pending approval); links to inventory items where available - SeedDataService.cs: wire SeedPurchaseOrdersAsync after Vendors seeder - Remove.cs: add PO + POItem cleanup inside Bills removal block (two-step ID fetch to avoid nested LINQ translation issues) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using PowderCoating.Core.Entities;
|
||||
using PowderCoating.Core.Enums;
|
||||
|
||||
namespace PowderCoating.Infrastructure.Services;
|
||||
|
||||
public partial class SeedDataService
|
||||
{
|
||||
/// <summary>
|
||||
/// Seeds 7 purchase orders across three vendors covering a 3-month window:
|
||||
/// 3 Received (historical), 2 Submitted (in-flight), and 2 Draft (pending approval).
|
||||
/// This gives every PO status a visible example for demo walkthroughs.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Vendors are resolved by partial name match against the company's vendor list — the
|
||||
/// same approach used by <see cref="SeedBillsAsync"/>. PO numbers follow the convention
|
||||
/// <c>PO-YYMM-####</c> stamped at seed time.
|
||||
///
|
||||
/// Received POs link back to the <see cref="Bill"/> that was created after receipt when
|
||||
/// one with a matching vendor invoice number exists; otherwise <c>BillId</c> is left null
|
||||
/// so the PO still seeds cleanly even if bills were skipped.
|
||||
///
|
||||
/// Idempotency: returns 0 immediately if any purchase orders already exist for the company.
|
||||
/// </remarks>
|
||||
/// <param name="company">The tenant company to seed purchase orders for.</param>
|
||||
/// <returns>Number of PO records inserted, or 0 if already seeded.</returns>
|
||||
private async Task<int> SeedPurchaseOrdersAsync(Company company)
|
||||
{
|
||||
var existingCount = await _context.Set<PurchaseOrder>()
|
||||
.IgnoreQueryFilters()
|
||||
.CountAsync(p => p.CompanyId == company.Id && !p.IsDeleted);
|
||||
|
||||
if (existingCount > 0)
|
||||
return 0;
|
||||
|
||||
var vendors = await _context.Set<Vendor>()
|
||||
.IgnoreQueryFilters()
|
||||
.Where(v => v.CompanyId == company.Id && !v.IsDeleted)
|
||||
.ToListAsync();
|
||||
|
||||
if (vendors.Count == 0)
|
||||
return 0;
|
||||
|
||||
var prismatic = vendors.FirstOrDefault(v => v.CompanyName.Contains("Prismatic")) ?? vendors.First();
|
||||
var columbia = vendors.FirstOrDefault(v => v.CompanyName.Contains("Columbia")) ?? vendors.First();
|
||||
var harbor = vendors.FirstOrDefault(v => v.CompanyName.Contains("Harbor")) ?? vendors.First();
|
||||
var grainger = vendors.FirstOrDefault(v => v.CompanyName.Contains("Grainger")) ?? vendors.First();
|
||||
var localSupply = vendors.FirstOrDefault(v => v.CompanyName.Contains("Local")) ?? vendors.First();
|
||||
|
||||
// Resolve inventory item IDs for PO line items (optional — may be null if inventory
|
||||
// wasn't seeded yet; the PO seeds cleanly either way using the Description field).
|
||||
var glossBlack = await _context.Set<InventoryItem>().IgnoreQueryFilters()
|
||||
.FirstOrDefaultAsync(i => i.CompanyId == company.Id && i.SKU.EndsWith("-PWD-GBK-001") && !i.IsDeleted);
|
||||
var matteBlack = await _context.Set<InventoryItem>().IgnoreQueryFilters()
|
||||
.FirstOrDefaultAsync(i => i.CompanyId == company.Id && i.SKU.EndsWith("-PWD-MBK-001") && !i.IsDeleted);
|
||||
var superChrome = await _context.Set<InventoryItem>().IgnoreQueryFilters()
|
||||
.FirstOrDefaultAsync(i => i.CompanyId == company.Id && i.SKU.EndsWith("-PWD-CHR-001") && !i.IsDeleted);
|
||||
var candyRed = await _context.Set<InventoryItem>().IgnoreQueryFilters()
|
||||
.FirstOrDefaultAsync(i => i.CompanyId == company.Id && i.SKU.EndsWith("-PWD-CRD-001") && !i.IsDeleted);
|
||||
var blastMedia = await _context.Set<InventoryItem>().IgnoreQueryFilters()
|
||||
.FirstOrDefaultAsync(i => i.CompanyId == company.Id && i.SKU.EndsWith("-BLM-001") && !i.IsDeleted);
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var pfx = $"PO-{now:yy}{now.Month:D2}-";
|
||||
var seq = 1;
|
||||
var seeded = 0;
|
||||
|
||||
async Task<PurchaseOrder> AddPO(PurchaseOrder po)
|
||||
{
|
||||
po.PoNumber = $"{pfx}{seq++:D4}";
|
||||
po.CompanyId = company.Id;
|
||||
foreach (var item in po.Items)
|
||||
{
|
||||
item.CompanyId = company.Id;
|
||||
item.CreatedAt = po.OrderDate;
|
||||
}
|
||||
await _context.Set<PurchaseOrder>().AddAsync(po);
|
||||
await _context.SaveChangesAsync();
|
||||
seeded++;
|
||||
return po;
|
||||
}
|
||||
|
||||
// ── RECEIVED (historical — tied to bills already in the system) ───────
|
||||
|
||||
// PO-1: Prismatic Powders — powder restock 3 months ago (Received, matches bill PP-77211)
|
||||
await AddPO(new PurchaseOrder
|
||||
{
|
||||
VendorId = prismatic.Id,
|
||||
Status = PurchaseOrderStatus.Received,
|
||||
OrderDate = now.AddDays(-95),
|
||||
ExpectedDeliveryDate = now.AddDays(-80),
|
||||
ReceivedDate = now.AddDays(-82),
|
||||
SubTotal = 1_145.00m,
|
||||
TotalAmount = 1_145.00m,
|
||||
Notes = "Quarterly powder restock — Q1",
|
||||
CreatedAt = now.AddDays(-95),
|
||||
Items =
|
||||
{
|
||||
new PurchaseOrderItem { InventoryItemId = matteBlack?.Id, Description = "Matte Black Powder — 50 lb bags", UnitOfMeasure = "bag", QuantityOrdered = 2, QuantityReceived = 2, UnitCost = 178.00m, LineTotal = 356.00m },
|
||||
new PurchaseOrderItem { InventoryItemId = glossBlack?.Id, Description = "Gloss Black Powder — 50 lb bags", UnitOfMeasure = "bag", QuantityOrdered = 2, QuantityReceived = 2, UnitCost = 165.00m, LineTotal = 330.00m },
|
||||
new PurchaseOrderItem { Description = "Satin Silver Powder — 25 lb bags", UnitOfMeasure = "bag", QuantityOrdered = 2, QuantityReceived = 2, UnitCost = 144.50m, LineTotal = 289.00m },
|
||||
new PurchaseOrderItem { Description = "Masking Tape & Plugs Kit", UnitOfMeasure = "kit", QuantityOrdered = 1, QuantityReceived = 1, UnitCost = 170.00m, LineTotal = 170.00m }
|
||||
}
|
||||
});
|
||||
|
||||
// PO-2: Columbia Coatings — specialty colors 2 months ago (Received, matches bill CC-4401)
|
||||
await AddPO(new PurchaseOrder
|
||||
{
|
||||
VendorId = columbia.Id,
|
||||
Status = PurchaseOrderStatus.Received,
|
||||
OrderDate = now.AddDays(-70),
|
||||
ExpectedDeliveryDate = now.AddDays(-58),
|
||||
ReceivedDate = now.AddDays(-60),
|
||||
SubTotal = 986.00m,
|
||||
TotalAmount = 986.00m,
|
||||
Notes = "Specialty metallic & candy colors order",
|
||||
CreatedAt = now.AddDays(-70),
|
||||
Items =
|
||||
{
|
||||
new PurchaseOrderItem { InventoryItemId = candyRed?.Id, Description = "Candy Red Metallic — 10 lb bags", UnitOfMeasure = "bag", QuantityOrdered = 3, QuantityReceived = 3, UnitCost = 145.00m, LineTotal = 435.00m },
|
||||
new PurchaseOrderItem { InventoryItemId = superChrome?.Id, Description = "Chrome Effect Powder — 10 lb bags", UnitOfMeasure = "bag", QuantityOrdered = 2, QuantityReceived = 2, UnitCost = 168.00m, LineTotal = 336.00m },
|
||||
new PurchaseOrderItem { Description = "Hammertone Bronze — 10 lb bags", UnitOfMeasure = "bag", QuantityOrdered = 1, QuantityReceived = 1, UnitCost = 150.50m, LineTotal = 150.50m },
|
||||
new PurchaseOrderItem { Description = "Ground Straps & Hooks", UnitOfMeasure = "lot", QuantityOrdered = 1, QuantityReceived = 1, UnitCost = 64.50m, LineTotal = 64.50m }
|
||||
}
|
||||
});
|
||||
|
||||
// PO-3: Harbor Freight Tools — consumables 6 weeks ago (Received, matches bill HBF-18822 timing)
|
||||
await AddPO(new PurchaseOrder
|
||||
{
|
||||
VendorId = harbor.Id,
|
||||
Status = PurchaseOrderStatus.Received,
|
||||
OrderDate = now.AddDays(-48),
|
||||
ExpectedDeliveryDate = now.AddDays(-40),
|
||||
ReceivedDate = now.AddDays(-42),
|
||||
SubTotal = 412.50m,
|
||||
TotalAmount = 412.50m,
|
||||
Notes = "Shop consumables & hardware restock",
|
||||
CreatedAt = now.AddDays(-48),
|
||||
Items =
|
||||
{
|
||||
new PurchaseOrderItem { Description = "J-Hook Hangers Assortment", UnitOfMeasure = "pkg", QuantityOrdered = 2, QuantityReceived = 2, UnitCost = 89.75m, LineTotal = 179.50m },
|
||||
new PurchaseOrderItem { Description = "Masking Caps — Mixed (100-pack)", UnitOfMeasure = "box", QuantityOrdered = 2, QuantityReceived = 2, UnitCost = 60.00m, LineTotal = 120.00m },
|
||||
new PurchaseOrderItem { Description = "Wire Brushes & Abrasives", UnitOfMeasure = "lot", QuantityOrdered = 1, QuantityReceived = 1, UnitCost = 113.00m, LineTotal = 113.00m }
|
||||
}
|
||||
});
|
||||
|
||||
// ── SUBMITTED (in-flight — awaiting delivery) ─────────────────────────
|
||||
|
||||
// PO-4: Grainger Industrial Supply — safety equipment & filter replacement (matches open GRG-7714 partial bill)
|
||||
await AddPO(new PurchaseOrder
|
||||
{
|
||||
VendorId = grainger.Id,
|
||||
Status = PurchaseOrderStatus.Submitted,
|
||||
OrderDate = now.AddDays(-14),
|
||||
ExpectedDeliveryDate = now.AddDays(3),
|
||||
SubTotal = 648.00m,
|
||||
TotalAmount = 648.00m,
|
||||
Notes = "Blast room filter replacement + safety restocking",
|
||||
CreatedAt = now.AddDays(-14),
|
||||
Items =
|
||||
{
|
||||
new PurchaseOrderItem { Description = "HEPA Filter Cartridges — 12-pack", UnitOfMeasure = "box", QuantityOrdered = 2, QuantityReceived = 0, UnitCost = 189.00m, LineTotal = 378.00m },
|
||||
new PurchaseOrderItem { Description = "Blast Nozzle Tungsten — 3/8\"", UnitOfMeasure = "ea", QuantityOrdered = 2, QuantityReceived = 0, UnitCost = 85.00m, LineTotal = 170.00m },
|
||||
new PurchaseOrderItem { Description = "Safety Respirators (10-pack)", UnitOfMeasure = "box", QuantityOrdered = 1, QuantityReceived = 0, UnitCost = 100.00m, LineTotal = 100.00m }
|
||||
}
|
||||
});
|
||||
|
||||
// PO-5: Prismatic Powders — current month powder order (matches open bill PP-88530)
|
||||
await AddPO(new PurchaseOrder
|
||||
{
|
||||
VendorId = prismatic.Id,
|
||||
Status = PurchaseOrderStatus.Submitted,
|
||||
OrderDate = now.AddDays(-8),
|
||||
ExpectedDeliveryDate = now.AddDays(7),
|
||||
SubTotal = 1_050.00m,
|
||||
TotalAmount = 1_050.00m,
|
||||
Notes = "June powder restock — Matte Black + seasonal Gloss Red",
|
||||
CreatedAt = now.AddDays(-8),
|
||||
Items =
|
||||
{
|
||||
new PurchaseOrderItem { InventoryItemId = matteBlack?.Id, Description = "Matte Black Powder — 25 lb bags", UnitOfMeasure = "bag", QuantityOrdered = 6, QuantityReceived = 0, UnitCost = 89.00m, LineTotal = 534.00m },
|
||||
new PurchaseOrderItem { Description = "Gloss Red Powder — 10 lb bags", UnitOfMeasure = "bag", QuantityOrdered = 2, QuantityReceived = 0, UnitCost = 132.00m, LineTotal = 264.00m },
|
||||
new PurchaseOrderItem { Description = "Hanging Racks (10-pack)", UnitOfMeasure = "pkg", QuantityOrdered = 2, QuantityReceived = 0, UnitCost = 126.00m, LineTotal = 252.00m }
|
||||
}
|
||||
});
|
||||
|
||||
// ── DRAFT (pending review / approval) ─────────────────────────────────
|
||||
|
||||
// PO-6: Harbor Freight Tools — shop tools pending manager approval
|
||||
await AddPO(new PurchaseOrder
|
||||
{
|
||||
VendorId = harbor.Id,
|
||||
Status = PurchaseOrderStatus.Draft,
|
||||
OrderDate = now.AddDays(-3),
|
||||
ExpectedDeliveryDate = now.AddDays(10),
|
||||
SubTotal = 318.75m,
|
||||
TotalAmount = 318.75m,
|
||||
Notes = "Monthly hardware restock — needs approval before submit",
|
||||
InternalNotes = "Manager review requested: higher than normal month due to extra hooks for upcoming Apex Motorsports batch.",
|
||||
CreatedAt = now.AddDays(-3),
|
||||
Items =
|
||||
{
|
||||
new PurchaseOrderItem { Description = "Hanging Racks & J-Hooks", UnitOfMeasure = "pkg", QuantityOrdered = 1, QuantityReceived = 0, UnitCost = 198.75m, LineTotal = 198.75m },
|
||||
new PurchaseOrderItem { Description = "Masking Caps — Mixed (100-pack)", UnitOfMeasure = "box", QuantityOrdered = 2, QuantityReceived = 0, UnitCost = 60.00m, LineTotal = 120.00m }
|
||||
}
|
||||
});
|
||||
|
||||
// PO-7: Local Industrial Supply — blast media restock (inventory currently at zero)
|
||||
await AddPO(new PurchaseOrder
|
||||
{
|
||||
VendorId = localSupply.Id,
|
||||
Status = PurchaseOrderStatus.Draft,
|
||||
OrderDate = now.AddDays(-1),
|
||||
ExpectedDeliveryDate = now.AddDays(5),
|
||||
SubTotal = 385.00m,
|
||||
TotalAmount = 385.00m,
|
||||
Notes = "URGENT — blast media out of stock, production blocked on Pressure Pot Blaster",
|
||||
InternalNotes = "Rush order requested; call LIS rep for same-week delivery.",
|
||||
CreatedAt = now.AddDays(-1),
|
||||
Items =
|
||||
{
|
||||
new PurchaseOrderItem { InventoryItemId = blastMedia?.Id, Description = "Aluminum Oxide #80 Grit — 100 lb bag", UnitOfMeasure = "bag", QuantityOrdered = 5, QuantityReceived = 0, UnitCost = 77.00m, LineTotal = 385.00m }
|
||||
}
|
||||
});
|
||||
|
||||
return seeded;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user