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:
@@ -51,6 +51,30 @@ public class InvoicesController : Controller
|
||||
_logoService = logoService;
|
||||
}
|
||||
|
||||
private static readonly string[] StandardPaymentTerms =
|
||||
[
|
||||
"Due on Receipt",
|
||||
"Net 15",
|
||||
"Net 30",
|
||||
"Net 45",
|
||||
"Net 60",
|
||||
"Net 90",
|
||||
"2% 10 Net 30",
|
||||
"COD",
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Builds the payment terms SelectList for Create/Edit views. Always includes the provided
|
||||
/// <paramref name="selectedTerm"/> even if it is a custom value not in the standard list.
|
||||
/// </summary>
|
||||
private static SelectList BuildPaymentTermsSelectList(string? selectedTerm)
|
||||
{
|
||||
var terms = StandardPaymentTerms.ToList();
|
||||
if (!string.IsNullOrWhiteSpace(selectedTerm) && !terms.Contains(selectedTerm, StringComparer.OrdinalIgnoreCase))
|
||||
terms.Insert(0, selectedTerm);
|
||||
return new SelectList(terms, selectedTerm);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// GET: /Invoices
|
||||
// -----------------------------------------------------------------------
|
||||
@@ -328,9 +352,9 @@ public class InvoicesController : Controller
|
||||
var job = await _unitOfWork.Jobs.GetByIdAsync(jobId.Value, false, j => j.Customer, j => j.JobItems);
|
||||
if (job == null) return NotFound();
|
||||
|
||||
// Validate no existing invoice for this job
|
||||
var existing = await _unitOfWork.Invoices.GetForJobAsync(jobId.Value, includeDeleted: true);
|
||||
if (existing != null)
|
||||
// Validate no existing active invoice for this job (voided ones are kept as history)
|
||||
var existing = await _unitOfWork.Invoices.GetForJobAsync(jobId.Value);
|
||||
if (existing != null && existing.Status != InvoiceStatus.Voided)
|
||||
return RedirectToAction(nameof(Details), new { id = existing.Id });
|
||||
|
||||
dto.JobId = job.Id;
|
||||
@@ -383,12 +407,15 @@ public class InvoicesController : Controller
|
||||
});
|
||||
}
|
||||
|
||||
// Track whether there were real job items before any fallback
|
||||
bool hadJobItems = dto.InvoiceItems.Any();
|
||||
|
||||
// If no job items, use job final price as single line.
|
||||
// FinalPrice is always the post-tax total (set by the pricing engine or imported from
|
||||
// an export). Treat it as the agreed total and force TaxPercent = 0 so the invoice
|
||||
// does not apply tax a second time. Without this, imported jobs double-tax because
|
||||
// their FinalPrice already includes the tax that was applied in the source environment.
|
||||
if (!dto.InvoiceItems.Any())
|
||||
if (!hadJobItems)
|
||||
{
|
||||
var defaultRevAccId = defaultRevenueAccount?.Id;
|
||||
dto.InvoiceItems.Add(new CreateInvoiceItemDto
|
||||
@@ -431,6 +458,26 @@ public class InvoicesController : Controller
|
||||
dto.TaxPercent = sourceQuote.TaxPercent;
|
||||
dto.DiscountAmount = sourceQuote.DiscountAmount;
|
||||
}
|
||||
else if (hadJobItems && costs?.ShopSuppliesRate > 0)
|
||||
{
|
||||
// Direct job — no source quote. Derive shop supplies from the items subtotal
|
||||
// using the current company rate. (Quote-sourced jobs read the pre-agreed amount
|
||||
// from the quote snapshot instead; this path only fires when there is no quote.)
|
||||
var itemsSubtotal = dto.InvoiceItems.Sum(i => i.TotalPrice);
|
||||
var shopSuppliesAmount = Math.Round(itemsSubtotal * (costs.ShopSuppliesRate / 100m), 2);
|
||||
if (shopSuppliesAmount > 0.01m)
|
||||
{
|
||||
dto.InvoiceItems.Add(new CreateInvoiceItemDto
|
||||
{
|
||||
Description = $"Shop Supplies ({costs.ShopSuppliesRate:0.##}%)",
|
||||
Quantity = 1,
|
||||
UnitPrice = shopSuppliesAmount,
|
||||
TotalPrice = shopSuppliesAmount,
|
||||
DisplayOrder = order,
|
||||
RevenueAccountId = defaultRevenueAccount?.Id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Override tax to 0 for tax-exempt customers, regardless of company default or quote rate
|
||||
if (job.Customer?.IsTaxExempt == true)
|
||||
@@ -444,7 +491,7 @@ public class InvoicesController : Controller
|
||||
: string.Empty;
|
||||
}
|
||||
|
||||
await PopulateCreateViewBagAsync(currentUser.CompanyId);
|
||||
await PopulateCreateViewBagAsync(currentUser.CompanyId, dto.Terms);
|
||||
ViewBag.GuidedActivation = guidedActivation;
|
||||
return View(dto);
|
||||
}
|
||||
@@ -485,7 +532,7 @@ public class InvoicesController : Controller
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
await PopulateCreateViewBagAsync(currentUser.CompanyId);
|
||||
await PopulateCreateViewBagAsync(currentUser.CompanyId, dto.Terms);
|
||||
ViewBag.GuidedActivation = guidedActivation;
|
||||
return View(dto);
|
||||
}
|
||||
@@ -493,21 +540,32 @@ public class InvoicesController : Controller
|
||||
if (!dto.InvoiceItems.Any())
|
||||
{
|
||||
ModelState.AddModelError("", "Please add at least one line item before saving.");
|
||||
await PopulateCreateViewBagAsync(currentUser.CompanyId);
|
||||
await PopulateCreateViewBagAsync(currentUser.CompanyId, dto.Terms);
|
||||
ViewBag.GuidedActivation = guidedActivation;
|
||||
return View(dto);
|
||||
}
|
||||
|
||||
// Validate no existing invoice for this job before starting the transaction
|
||||
// Validate no existing active invoice for this job before starting the transaction.
|
||||
// Voided invoices are treated as history — clear their JobId FK so the unique index
|
||||
// slot is freed and the new invoice can be saved.
|
||||
if (dto.JobId.HasValue)
|
||||
{
|
||||
var existing = await _unitOfWork.Invoices.GetForJobAsync(dto.JobId.Value, includeDeleted: true);
|
||||
var existing = await _unitOfWork.Invoices.GetForJobAsync(dto.JobId.Value);
|
||||
if (existing != null)
|
||||
{
|
||||
ModelState.AddModelError("", "An invoice already exists for this job.");
|
||||
await PopulateCreateViewBagAsync(currentUser.CompanyId);
|
||||
ViewBag.GuidedActivation = guidedActivation;
|
||||
return View(dto);
|
||||
if (existing.Status != InvoiceStatus.Voided)
|
||||
{
|
||||
ModelState.AddModelError("", "An invoice already exists for this job.");
|
||||
await PopulateCreateViewBagAsync(currentUser.CompanyId, dto.Terms);
|
||||
ViewBag.GuidedActivation = guidedActivation;
|
||||
return View(dto);
|
||||
}
|
||||
|
||||
// Clear the voided invoice's JobId so the unique (CompanyId, JobId) index
|
||||
// allows the new invoice to be inserted.
|
||||
existing.JobId = null;
|
||||
await _unitOfWork.Invoices.UpdateAsync(existing);
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -684,7 +742,7 @@ public class InvoicesController : Controller
|
||||
_logger.LogError(ex, "Error creating invoice");
|
||||
TempData["Error"] = "An error occurred while creating the invoice.";
|
||||
var currentUser = await _userManager.GetUserAsync(User);
|
||||
if (currentUser != null) await PopulateCreateViewBagAsync(currentUser.CompanyId);
|
||||
if (currentUser != null) await PopulateCreateViewBagAsync(currentUser.CompanyId, dto.Terms);
|
||||
ViewBag.GuidedActivation = guidedActivation;
|
||||
return View(dto);
|
||||
}
|
||||
@@ -694,9 +752,9 @@ public class InvoicesController : Controller
|
||||
// GET: /Invoices/Edit/5
|
||||
// -----------------------------------------------------------------------
|
||||
/// <summary>
|
||||
/// Loads the Edit form. Only Draft invoices are editable — any other status redirects to
|
||||
/// Details with an error. Sent/Paid/Voided invoices must be voided and recreated rather
|
||||
/// than edited, to preserve the audit trail for those states.
|
||||
/// Loads the Edit form. Draft, Sent, and Overdue invoices are editable. Paid, PartiallyPaid,
|
||||
/// Voided, and WrittenOff invoices are locked — those statuses represent committed financial
|
||||
/// records that should not be altered after the fact.
|
||||
/// </summary>
|
||||
public async Task<IActionResult> Edit(int? id)
|
||||
{
|
||||
@@ -707,9 +765,9 @@ public class InvoicesController : Controller
|
||||
var invoice = await LoadInvoiceForViewAsync(id.Value);
|
||||
if (invoice == null) return NotFound();
|
||||
|
||||
if (invoice.Status != InvoiceStatus.Draft)
|
||||
if (invoice.Status is not (InvoiceStatus.Draft or InvoiceStatus.Sent or InvoiceStatus.Overdue))
|
||||
{
|
||||
TempData["Error"] = "Only Draft invoices can be edited.";
|
||||
TempData["Error"] = "Only open invoices (Draft, Sent, Overdue) can be edited.";
|
||||
return RedirectToAction(nameof(Details), new { id });
|
||||
}
|
||||
|
||||
@@ -748,6 +806,11 @@ public class InvoicesController : Controller
|
||||
? invoice.Customer.CompanyName
|
||||
: $"{invoice.Customer.ContactFirstName} {invoice.Customer.ContactLastName}".Trim())
|
||||
: string.Empty;
|
||||
ViewBag.InvoiceStatus = invoice.Status;
|
||||
var customerEmail = invoice.Customer?.BillingEmail ?? invoice.Customer?.Email;
|
||||
ViewBag.CanResend = invoice.Status is (InvoiceStatus.Sent or InvoiceStatus.Overdue)
|
||||
&& !string.IsNullOrWhiteSpace(customerEmail);
|
||||
ViewBag.PaymentTermsOptions = BuildPaymentTermsSelectList(dto.Terms);
|
||||
|
||||
return View(dto);
|
||||
}
|
||||
@@ -763,23 +826,23 @@ public class InvoicesController : Controller
|
||||
// POST: /Invoices/Edit/5
|
||||
// -----------------------------------------------------------------------
|
||||
/// <summary>
|
||||
/// Saves edits to a Draft invoice. Line items are replaced via a soft-delete-and-add cycle
|
||||
/// (old items flagged IsDeleted, new items inserted) so the audit trail of what was originally
|
||||
/// on the invoice is preserved in the database. Customer.CurrentBalance is adjusted by the
|
||||
/// delta (newTotal − oldTotal) so outstanding AR stays accurate without recalculating from scratch.
|
||||
/// Only Draft invoices can be edited; guard is checked on both GET and POST.
|
||||
/// Saves edits to an open invoice (Draft, Sent, or Overdue). Line items are replaced via a
|
||||
/// soft-delete-and-add cycle so the original items are preserved in the audit trail.
|
||||
/// Customer.CurrentBalance is adjusted by the delta (newTotal − oldTotal). Status is kept
|
||||
/// as-is (Sent stays Sent) so the customer-facing record remains consistent. If resendToCustomer
|
||||
/// is true and the invoice is Sent/Overdue, a fresh PDF is emailed to the customer.
|
||||
/// </summary>
|
||||
[HttpPost, ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Edit(int id, UpdateInvoiceDto dto)
|
||||
public async Task<IActionResult> Edit(int id, UpdateInvoiceDto dto, bool resendToCustomer = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
var invoice = await LoadInvoiceForViewAsync(id);
|
||||
if (invoice == null) return NotFound();
|
||||
|
||||
if (invoice.Status != InvoiceStatus.Draft)
|
||||
if (invoice.Status is not (InvoiceStatus.Draft or InvoiceStatus.Sent or InvoiceStatus.Overdue))
|
||||
{
|
||||
TempData["Error"] = "Only Draft invoices can be edited.";
|
||||
TempData["Error"] = "Only open invoices (Draft, Sent, Overdue) can be edited.";
|
||||
return RedirectToAction(nameof(Details), new { id });
|
||||
}
|
||||
|
||||
@@ -789,6 +852,7 @@ public class InvoicesController : Controller
|
||||
ViewBag.InvoiceId = invoice.Id;
|
||||
ViewBag.JobNumber = invoice.Job?.JobNumber;
|
||||
ViewBag.CustomerName = invoice.Customer?.CompanyName;
|
||||
ViewBag.PaymentTermsOptions = BuildPaymentTermsSelectList(dto.Terms);
|
||||
return View(dto);
|
||||
}
|
||||
|
||||
@@ -862,6 +926,28 @@ public class InvoicesController : Controller
|
||||
await _unitOfWork.CompleteAsync();
|
||||
|
||||
TempData["Success"] = "Invoice updated successfully.";
|
||||
|
||||
// Optionally re-send the updated invoice PDF to the customer
|
||||
if (resendToCustomer && invoice.Status is (InvoiceStatus.Sent or InvoiceStatus.Overdue))
|
||||
{
|
||||
try
|
||||
{
|
||||
var currentUserForPdf = await _userManager.GetUserAsync(User);
|
||||
var pdfBytes = await BuildInvoicePdfAsync(invoice, invoice.CompanyId);
|
||||
string? paymentUrl = null;
|
||||
if (!string.IsNullOrEmpty(invoice.PaymentLinkToken))
|
||||
paymentUrl = $"{Request.Scheme}://{Request.Host}/pay/{invoice.PaymentLinkToken}";
|
||||
await _notificationService.NotifyInvoiceSentAsync(invoice, pdfBytes, $"Invoice-{invoice.InvoiceNumber}.pdf", paymentUrl);
|
||||
var notifLog = await _unitOfWork.NotificationLogs.GetLatestForInvoiceAsync(id);
|
||||
this.SetNotificationResultToast(notifLog);
|
||||
}
|
||||
catch (Exception notifyEx)
|
||||
{
|
||||
_logger.LogWarning(notifyEx, "Re-send of updated invoice {Id} failed", id);
|
||||
TempData["WarningPermanent"] = "Invoice saved, but re-sending the email failed. You can re-send manually from the invoice details.";
|
||||
}
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(Details), new { id });
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -883,7 +969,7 @@ public class InvoicesController : Controller
|
||||
/// works identically in dev (localhost) and production without config changes.
|
||||
/// </summary>
|
||||
[HttpPost, ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Send(int id)
|
||||
public async Task<IActionResult> Send(int id, string? overrideEmail = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -916,7 +1002,7 @@ public class InvoicesController : Controller
|
||||
try
|
||||
{
|
||||
var pdfBytes = await BuildInvoicePdfAsync(invoice, currentUser!.CompanyId);
|
||||
await _notificationService.NotifyInvoiceSentAsync(invoice, pdfBytes, $"Invoice-{invoice.InvoiceNumber}.pdf", paymentUrl);
|
||||
await _notificationService.NotifyInvoiceSentAsync(invoice, pdfBytes, $"Invoice-{invoice.InvoiceNumber}.pdf", paymentUrl, overrideEmail: overrideEmail?.Trim());
|
||||
pdfAndNotifSucceeded = true;
|
||||
}
|
||||
catch (Exception notifyEx)
|
||||
@@ -1296,7 +1382,7 @@ public class InvoicesController : Controller
|
||||
/// <see cref="BuildInvoicePdfAsync"/> which fetches company branding, template settings,
|
||||
/// and the full invoice DTO in one call, then hands off to IPdfService.
|
||||
/// </summary>
|
||||
public async Task<IActionResult> DownloadPdf(int? id)
|
||||
public async Task<IActionResult> DownloadPdf(int? id, bool inline = false)
|
||||
{
|
||||
if (id == null) return NotFound();
|
||||
|
||||
@@ -1309,7 +1395,17 @@ public class InvoicesController : Controller
|
||||
if (currentUser == null) return Unauthorized();
|
||||
|
||||
var pdfBytes = await BuildInvoicePdfAsync(invoice, currentUser.CompanyId);
|
||||
return File(pdfBytes, "application/pdf", $"Invoice-{invoice.InvoiceNumber}.pdf");
|
||||
var fileName = $"Invoice-{invoice.InvoiceNumber}.pdf";
|
||||
|
||||
if (inline)
|
||||
{
|
||||
// Return with inline content-disposition so the browser renders the PDF
|
||||
// in a new tab, enabling the native print dialog.
|
||||
Response.Headers["Content-Disposition"] = $"inline; filename=\"{fileName}\"";
|
||||
return File(pdfBytes, "application/pdf");
|
||||
}
|
||||
|
||||
return File(pdfBytes, "application/pdf", fileName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -1336,9 +1432,10 @@ public class InvoicesController : Controller
|
||||
var currentUser = await _userManager.GetUserAsync(User);
|
||||
if (currentUser == null) return Unauthorized();
|
||||
|
||||
var existing = await _unitOfWork.Invoices.GetForJobAsync(jobId, includeDeleted: true);
|
||||
var existing = await _unitOfWork.Invoices.GetForJobAsync(jobId);
|
||||
|
||||
if (existing != null)
|
||||
// Voided invoices are kept as history — don't block creation of a new one
|
||||
if (existing != null && existing.Status != InvoiceStatus.Voided)
|
||||
return RedirectToAction(nameof(Details), new { id = existing.Id });
|
||||
|
||||
return RedirectToAction(nameof(Create), new { jobId });
|
||||
@@ -1361,7 +1458,7 @@ public class InvoicesController : Controller
|
||||
/// Details view can show an inline toast with the delivery outcome.
|
||||
/// </summary>
|
||||
[HttpPost, ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> ResendInvoice(int id)
|
||||
public async Task<IActionResult> ResendInvoice(int id, string? overrideEmail = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -1375,11 +1472,21 @@ public class InvoicesController : Controller
|
||||
if (invoice.Status == InvoiceStatus.Voided || invoice.Status == InvoiceStatus.WrittenOff)
|
||||
return Json(new { success = false, message = "Voided invoices cannot be resent." });
|
||||
|
||||
// Validate override email when provided
|
||||
overrideEmail = overrideEmail?.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(overrideEmail) && !overrideEmail.Contains('@'))
|
||||
return Json(new { success = false, message = "The email address provided is not valid." });
|
||||
|
||||
var currentUser = await _userManager.GetUserAsync(User);
|
||||
var recipientName = invoice.Customer?.IsCommercial == true
|
||||
? invoice.Customer.CompanyName ?? "Customer"
|
||||
: $"{invoice.Customer?.ContactFirstName} {invoice.Customer?.ContactLastName}".Trim();
|
||||
var recipientEmail = invoice.Customer?.Email ?? string.Empty;
|
||||
var recipientEmail = !string.IsNullOrWhiteSpace(overrideEmail)
|
||||
? overrideEmail
|
||||
: invoice.Customer?.BillingEmail ?? invoice.Customer?.Email ?? string.Empty;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(recipientEmail))
|
||||
return Json(new { success = false, message = "No email address on file. Please provide an address to send to." });
|
||||
|
||||
byte[]? pdfBytes = null;
|
||||
string? pdfFilename = null;
|
||||
@@ -1393,7 +1500,7 @@ public class InvoicesController : Controller
|
||||
_logger.LogWarning(pdfEx, "PDF generation failed during resend of invoice {Id}; sending without attachment", id);
|
||||
}
|
||||
|
||||
await _notificationService.NotifyInvoiceSentAsync(invoice, pdfBytes, pdfFilename);
|
||||
await _notificationService.NotifyInvoiceSentAsync(invoice, pdfBytes, pdfFilename, overrideEmail: overrideEmail);
|
||||
|
||||
var latestLog = await _unitOfWork.NotificationLogs.GetLatestForInvoiceAsync(id);
|
||||
|
||||
@@ -1403,7 +1510,7 @@ public class InvoicesController : Controller
|
||||
if (latestLog?.Status == NotificationStatus.Skipped)
|
||||
return Json(new { success = false, message = $"{recipientName} has email notifications disabled or no email address on file." });
|
||||
|
||||
return Json(new { success = true, message = $"Invoice resent to {recipientName} ({recipientEmail})." });
|
||||
return Json(new { success = true, message = $"Invoice sent to {recipientEmail}." });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -1500,6 +1607,10 @@ public class InvoicesController : Controller
|
||||
if (invoice.TaxAmount > 0)
|
||||
await _accountBalanceService.DebitAsync(invoice.SalesTaxAccountId, invoice.TaxAmount);
|
||||
|
||||
// Clear the JobId FK before soft-deleting so the unique index slot is freed
|
||||
// and a new invoice can be created for the same job if needed.
|
||||
invoice.JobId = null;
|
||||
await _unitOfWork.Invoices.UpdateAsync(invoice);
|
||||
await _unitOfWork.Invoices.SoftDeleteAsync(id);
|
||||
|
||||
}); // end ExecuteInTransactionAsync
|
||||
@@ -1754,7 +1865,7 @@ public class InvoicesController : Controller
|
||||
/// — Company default tax rate and set of tax-exempt customer IDs for client-side JS to auto-zero tax.
|
||||
/// — Merchandise catalog items serialized as camelCase JSON for the invoice line-item picker modal.
|
||||
/// </summary>
|
||||
private async Task PopulateCreateViewBagAsync(int companyId)
|
||||
private async Task PopulateCreateViewBagAsync(int companyId, string? selectedTerms = null)
|
||||
{
|
||||
var customers = await _unitOfWork.Customers.GetAllAsync();
|
||||
ViewBag.Customers = customers.Where(c => c.IsActive).OrderBy(c => c.CompanyName ?? c.ContactLastName).ToList();
|
||||
@@ -1768,6 +1879,12 @@ public class InvoicesController : Controller
|
||||
.Select(c => c.Id)
|
||||
.ToHashSet();
|
||||
|
||||
// Payment terms dropdown — pre-select selectedTerms if provided, else company default
|
||||
var prefs = await _unitOfWork.CompanyPreferences
|
||||
.FirstOrDefaultAsync(p => p.CompanyId == companyId && !p.IsDeleted);
|
||||
var defaultTerms = selectedTerms ?? prefs?.DefaultPaymentTerms ?? "Net 30";
|
||||
ViewBag.PaymentTermsOptions = BuildPaymentTermsSelectList(defaultTerms);
|
||||
|
||||
// Merchandise items for the invoice merch picker (all active IsMerchandise items)
|
||||
var allMerchItems = await _unitOfWork.CatalogItems.FindAsync(
|
||||
i => i.IsMerchandise && i.IsActive, false, i => i.Category);
|
||||
@@ -1787,7 +1904,9 @@ public class InvoicesController : Controller
|
||||
private async Task PopulateBankAccountsAsync()
|
||||
{
|
||||
var accounts = await _unitOfWork.Accounts.FindAsync(a => a.IsActive
|
||||
&& (a.AccountSubType == AccountSubType.Checking || a.AccountSubType == AccountSubType.Savings));
|
||||
&& (a.AccountSubType == AccountSubType.Cash ||
|
||||
a.AccountSubType == AccountSubType.Checking ||
|
||||
a.AccountSubType == AccountSubType.Savings));
|
||||
ViewBag.BankAccounts = accounts
|
||||
.OrderBy(a => a.AccountNumber)
|
||||
.Select(a => new SelectListItem($"{a.AccountNumber} – {a.Name}", a.Id.ToString()))
|
||||
|
||||
Reference in New Issue
Block a user