Fix invoice re-creation after void; add payment terms selector and shop supplies line

- Voided invoices no longer block creating a new invoice for the same job: voided invoice's
  JobId FK is cleared so the unique index slot is freed for the replacement
- Invoice Details view shows voided invoices as history rather than hiding them
- Payment terms: standardized SelectList (Due on Receipt, Net 15/30/45/60/90, 2% 10 Net 30,
  COD) with custom-term preservation; invoice-due-date.js auto-updates Due Date on term change
- Shop supplies on direct (no-quote) jobs: InvoicesController derives the shop supplies line
  from the company rate when the job has no source quote to read the pre-agreed amount from
- Job entity: ShopSuppliesAmount + ShopSuppliesPercent fields preserved through job lifecycle
- Migration: AddShopSuppliesAmountToJob

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-08 20:47:34 -04:00
parent fc35fd123c
commit 3278152d83
10 changed files with 9972 additions and 74 deletions
@@ -45,6 +45,12 @@ public class JobDto
public decimal QuotedPrice { get; set; }
public decimal FinalPrice { get; set; }
public decimal ShopSuppliesAmount { get; set; }
public decimal ShopSuppliesPercent { get; set; }
public bool IsRushJob { get; set; }
public string DiscountType { get; set; } = "None";
public decimal DiscountValue { get; set; }
public string? DiscountReason { get; set; }
public string? CustomerPO { get; set; }
public string? SpecialInstructions { get; set; }
public string? InternalNotes { get; set; }
@@ -24,7 +24,9 @@ public class InvoiceProfile : Profile
? s.Customer.CompanyName
: $"{s.Customer.ContactFirstName} {s.Customer.ContactLastName}".Trim())
: string.Empty))
.ForMember(d => d.CustomerEmail, o => o.MapFrom(s => s.Customer != null ? s.Customer.Email : null))
.ForMember(d => d.CustomerEmail, o => o.MapFrom(s => s.Customer != null
? (s.Customer.BillingEmail ?? s.Customer.Email)
: null))
.ForMember(d => d.CustomerPhone, o => o.MapFrom(s => s.Customer != null ? s.Customer.Phone : null))
.ForMember(d => d.CustomerNotifyByEmail, o => o.MapFrom(s => s.Customer == null || s.Customer.NotifyByEmail))
.ForMember(d => d.PreparedByName, o => o.MapFrom(s => s.PreparedBy != null