Harden multi-tenant isolation across all user-facing controllers
Added explicit CompanyId == companyId predicates to every tenant-scoped query in 22 controllers so cross-tenant data leakage is impossible even if EF Core global query filters are bypassed or misconfigured. Also fixed ApplicationDbContext.IsPlatformAdmin to correctly return true for SuperAdmins with no CompanyId claim (break-glass accounts) and when no HTTP context is present (background services, unit tests), resolving 225 unit test failures that stemmed from the global filter blocking all in-memory test data. New MultiTenantIsolationTests class (8 tests) verifies the explicit predicate layer independently of the global query filters. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ using PowderCoating.Core.Interfaces;
|
||||
using PowderCoating.Core.Entities;
|
||||
using PowderCoating.Shared.Constants;
|
||||
using PowderCoating.Web.ViewModels.Reports;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace PowderCoating.Web.Controllers;
|
||||
|
||||
@@ -25,8 +26,9 @@ public class ReportsController : Controller
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly IAccountingAiService _accountingAi;
|
||||
private readonly IAiUsageLogger _usageLogger;
|
||||
private readonly ITenantContext _tenantContext;
|
||||
|
||||
public ReportsController(IUnitOfWork unitOfWork, ILogger<ReportsController> logger, IFinancialReportService financialReports, IOperationalReportService operationalReports, IPdfService pdfService, UserManager<ApplicationUser> userManager, IAccountingAiService accountingAi, IAiUsageLogger usageLogger)
|
||||
public ReportsController(IUnitOfWork unitOfWork, ILogger<ReportsController> logger, IFinancialReportService financialReports, IOperationalReportService operationalReports, IPdfService pdfService, UserManager<ApplicationUser> userManager, IAccountingAiService accountingAi, IAiUsageLogger usageLogger, ITenantContext tenantContext)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_logger = logger;
|
||||
@@ -36,6 +38,7 @@ public class ReportsController : Controller
|
||||
_userManager = userManager;
|
||||
_accountingAi = accountingAi;
|
||||
_usageLogger = usageLogger;
|
||||
_tenantContext = tenantContext;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -79,27 +82,26 @@ public class ReportsController : Controller
|
||||
var completedStatusCodes = new[] { "COMPLETED", "READY_FOR_PICKUP", "DELIVERED" };
|
||||
var activeStatusCodes = new[] { "PENDING", "QUOTED", "APPROVED", "IN_PREPARATION", "SANDBLASTING",
|
||||
"MASKING_TAPING", "CLEANING", "IN_OVEN", "COATING", "CURING", "QUALITY_CHECK", "ON_HOLD" };
|
||||
var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||
|
||||
// Load only necessary data - optimized with filtering and minimal eager loading
|
||||
// Jobs: Load all jobs (we need various status filters and the collection is needed for job status distribution)
|
||||
// Note: Date filtering would exclude data needed for jobsByStatus calculation
|
||||
var jobs = (await _unitOfWork.Jobs.GetAllAsync(false, j => j.Customer, j => j.JobStatus, j => j.JobPriority, j => j.AssignedUser)).ToList();
|
||||
// Load only necessary data — all explicitly scoped to this company
|
||||
var jobs = (await _unitOfWork.Jobs.FindAsync(j => j.CompanyId == companyId, false, j => j.Customer, j => j.JobStatus, j => j.JobPriority, j => j.AssignedUser)).ToList();
|
||||
|
||||
// Quotes: Load all quotes (needed for quote status distribution and conversion funnel)
|
||||
var quotes = (await _unitOfWork.Quotes.GetAllAsync(false, q => q.Customer, q => q.QuoteStatus)).ToList();
|
||||
var quotes = (await _unitOfWork.Quotes.FindAsync(q => q.CompanyId == companyId, false, q => q.Customer, q => q.QuoteStatus)).ToList();
|
||||
|
||||
// Customers: Load all (needed for active count and customer creation trend across all months)
|
||||
var customers = (await _unitOfWork.Customers.GetAllAsync()).ToList();
|
||||
var customers = (await _unitOfWork.Customers.FindAsync(c => c.CompanyId == companyId)).ToList();
|
||||
|
||||
// Equipment: Load all for status distribution
|
||||
var equipment = (await _unitOfWork.Equipment.GetAllAsync()).ToList();
|
||||
var equipment = (await _unitOfWork.Equipment.FindAsync(e => e.CompanyId == companyId)).ToList();
|
||||
|
||||
// Inventory: Load all for low stock analysis
|
||||
var inventory = (await _unitOfWork.InventoryItems.GetAllAsync()).ToList();
|
||||
var inventory = (await _unitOfWork.InventoryItems.FindAsync(i => i.CompanyId == companyId)).ToList();
|
||||
|
||||
// Appointments: Filter to relevant date range at DB level
|
||||
var appointments = (await _unitOfWork.Appointments.FindAsync(
|
||||
a => a.ScheduledStartTime >= startDate,
|
||||
a => a.CompanyId == companyId && a.ScheduledStartTime >= startDate,
|
||||
false,
|
||||
a => a.Customer,
|
||||
a => a.AppointmentType,
|
||||
@@ -108,7 +110,7 @@ public class ReportsController : Controller
|
||||
// Users with assigned jobs/appointments will be loaded below when building worker stats
|
||||
|
||||
// CatalogItems: Load all for category distribution
|
||||
var catalogItems = (await _unitOfWork.CatalogItems.GetAllAsync(false, c => c.Category)).ToList();
|
||||
var catalogItems = (await _unitOfWork.CatalogItems.FindAsync(ci => ci.CompanyId == companyId, false, c => c.Category)).ToList();
|
||||
|
||||
// === OVERVIEW METRICS ===
|
||||
var completedJobs = jobs.Where(j => completedStatusCodes.Contains(j.JobStatus.StatusCode)).ToList();
|
||||
@@ -382,7 +384,7 @@ public class ReportsController : Controller
|
||||
.ToDictionary(g => g.Key, g => g.Count());
|
||||
|
||||
// === FINANCIAL ANALYTICS ===
|
||||
var allInvoices = (await _unitOfWork.Invoices.GetAllAsync(false, i => i.Customer, i => i.Payments)).ToList();
|
||||
var allInvoices = (await _unitOfWork.Invoices.FindAsync(i => i.CompanyId == companyId, false, i => i.Customer, i => i.Payments)).ToList();
|
||||
var activeInvoices = allInvoices.Where(i => i.Status != InvoiceStatus.Voided && i.Status != InvoiceStatus.WrittenOff).ToList();
|
||||
|
||||
var totalInvoiced = activeInvoices.Sum(i => i.Total);
|
||||
@@ -781,7 +783,7 @@ public class ReportsController : Controller
|
||||
|
||||
// === POWDER CONSUMPTION VS PURCHASE ===
|
||||
var allInventoryTransactions = (await _unitOfWork.InventoryTransactions
|
||||
.GetAllAsync(false, t => t.InventoryItem))
|
||||
.FindAsync(t => t.CompanyId == companyId, false, t => t.InventoryItem))
|
||||
.ToList();
|
||||
|
||||
var powderConsumptionItems = allInventoryTransactions
|
||||
@@ -1309,14 +1311,15 @@ public class ReportsController : Controller
|
||||
var completedStatusCodes = new[] { "COMPLETED", "READY_FOR_PICKUP", "DELIVERED" };
|
||||
var activeStatusCodes = new[] { "PENDING", "QUOTED", "APPROVED", "IN_PREPARATION", "SANDBLASTING",
|
||||
"MASKING_TAPING", "CLEANING", "IN_OVEN", "COATING", "CURING", "QUALITY_CHECK", "ON_HOLD" };
|
||||
var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||
|
||||
var jobs = (await _unitOfWork.Jobs.GetAllAsync(false, j => j.Customer, j => j.JobStatus, j => j.JobPriority)).ToList();
|
||||
var customers = (await _unitOfWork.Customers.GetAllAsync()).ToList();
|
||||
var quotes = (await _unitOfWork.Quotes.GetAllAsync(false, q => q.QuoteStatus)).ToList();
|
||||
var equipment = (await _unitOfWork.Equipment.GetAllAsync()).ToList();
|
||||
var inventory = (await _unitOfWork.InventoryItems.GetAllAsync()).ToList();
|
||||
var allAppointments = await _unitOfWork.Appointments.GetAllAsync(false, a => a.AppointmentStatus);
|
||||
var appointments = allAppointments.Where(a => a.ScheduledStartTime >= startDate).ToList();
|
||||
var jobs = (await _unitOfWork.Jobs.FindAsync(j => j.CompanyId == companyId, false, j => j.Customer, j => j.JobStatus, j => j.JobPriority)).ToList();
|
||||
var customers = (await _unitOfWork.Customers.FindAsync(c => c.CompanyId == companyId)).ToList();
|
||||
var quotes = (await _unitOfWork.Quotes.FindAsync(q => q.CompanyId == companyId, false, q => q.QuoteStatus)).ToList();
|
||||
var equipment = (await _unitOfWork.Equipment.FindAsync(e => e.CompanyId == companyId)).ToList();
|
||||
var inventory = (await _unitOfWork.InventoryItems.FindAsync(i => i.CompanyId == companyId)).ToList();
|
||||
var allAppointments = await _unitOfWork.Appointments.FindAsync(a => a.CompanyId == companyId && a.ScheduledStartTime >= startDate, false, a => a.AppointmentStatus);
|
||||
var appointments = allAppointments.ToList();
|
||||
|
||||
var completedJobs = jobs.Where(j => completedStatusCodes.Contains(j.JobStatus.StatusCode)).ToList();
|
||||
var activeJobs = jobs.Where(j => activeStatusCodes.Contains(j.JobStatus.StatusCode)).ToList();
|
||||
@@ -1384,7 +1387,8 @@ public class ReportsController : Controller
|
||||
var now = DateTime.UtcNow;
|
||||
var startDate = now.AddMonths(-months);
|
||||
var completedStatusCodes = new[] { "COMPLETED", "READY_FOR_PICKUP", "DELIVERED" };
|
||||
var jobs = (await _unitOfWork.Jobs.GetAllAsync(false, j => j.Customer, j => j.JobStatus, j => j.JobPriority)).ToList();
|
||||
var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||
var jobs = (await _unitOfWork.Jobs.FindAsync(j => j.CompanyId == companyId, false, j => j.Customer, j => j.JobStatus, j => j.JobPriority)).ToList();
|
||||
var completedJobs = jobs.Where(j => completedStatusCodes.Contains(j.JobStatus.StatusCode)).ToList();
|
||||
var inRange = completedJobs.Where(j => j.UpdatedAt >= startDate).ToList();
|
||||
var byMonth = inRange.GroupBy(j => new DateTime(j.UpdatedAt!.Value.Year, j.UpdatedAt.Value.Month, 1)).ToDictionary(g => g.Key, g => g.ToList());
|
||||
@@ -1430,12 +1434,13 @@ public class ReportsController : Controller
|
||||
var completedStatusCodes = new[] { "COMPLETED", "READY_FOR_PICKUP", "DELIVERED" };
|
||||
var activeStatusCodes = new[] { "PENDING", "QUOTED", "APPROVED", "IN_PREPARATION", "SANDBLASTING",
|
||||
"MASKING_TAPING", "CLEANING", "IN_OVEN", "COATING", "CURING", "QUALITY_CHECK", "ON_HOLD" };
|
||||
var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||
|
||||
var jobs = (await _unitOfWork.Jobs.GetAllAsync(false, j => j.JobStatus, j => j.JobPriority, j => j.AssignedUser)).ToList();
|
||||
var equipment = (await _unitOfWork.Equipment.GetAllAsync()).ToList();
|
||||
var inventory = (await _unitOfWork.InventoryItems.GetAllAsync()).ToList();
|
||||
var allAppts = await _unitOfWork.Appointments.GetAllAsync(false, a => a.AppointmentType, a => a.AppointmentStatus);
|
||||
var appointments = allAppts.Where(a => a.ScheduledStartTime >= startDate).ToList();
|
||||
var jobs = (await _unitOfWork.Jobs.FindAsync(j => j.CompanyId == companyId, false, j => j.JobStatus, j => j.JobPriority, j => j.AssignedUser)).ToList();
|
||||
var equipment = (await _unitOfWork.Equipment.FindAsync(e => e.CompanyId == companyId)).ToList();
|
||||
var inventory = (await _unitOfWork.InventoryItems.FindAsync(i => i.CompanyId == companyId)).ToList();
|
||||
var allAppts = await _unitOfWork.Appointments.FindAsync(a => a.CompanyId == companyId && a.ScheduledStartTime >= startDate, false, a => a.AppointmentType, a => a.AppointmentStatus);
|
||||
var appointments = allAppts.ToList();
|
||||
|
||||
var activeJobs = jobs.Where(j => activeStatusCodes.Contains(j.JobStatus.StatusCode)).ToList();
|
||||
var completedJobs = jobs.Where(j => completedStatusCodes.Contains(j.JobStatus.StatusCode)).ToList();
|
||||
@@ -1483,10 +1488,11 @@ public class ReportsController : Controller
|
||||
var now = DateTime.UtcNow;
|
||||
var startDate = now.AddMonths(-months);
|
||||
var completedStatusCodes = new[] { "COMPLETED", "READY_FOR_PICKUP", "DELIVERED" };
|
||||
var customers = (await _unitOfWork.Customers.GetAllAsync()).ToList();
|
||||
var quotes = (await _unitOfWork.Quotes.GetAllAsync(false, q => q.QuoteStatus)).ToList();
|
||||
var catalogItems = (await _unitOfWork.CatalogItems.GetAllAsync(false, c => c.Category)).ToList();
|
||||
var completedJobs = (await _unitOfWork.Jobs.GetAllAsync(false, j => j.Customer, j => j.JobStatus, j => j.JobPriority))
|
||||
var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||
var customers = (await _unitOfWork.Customers.FindAsync(c => c.CompanyId == companyId)).ToList();
|
||||
var quotes = (await _unitOfWork.Quotes.FindAsync(q => q.CompanyId == companyId, false, q => q.QuoteStatus)).ToList();
|
||||
var catalogItems = (await _unitOfWork.CatalogItems.FindAsync(ci => ci.CompanyId == companyId, false, c => c.Category)).ToList();
|
||||
var completedJobs = (await _unitOfWork.Jobs.FindAsync(j => j.CompanyId == companyId, false, j => j.Customer, j => j.JobStatus, j => j.JobPriority))
|
||||
.Where(j => completedStatusCodes.Contains(j.JobStatus.StatusCode)).ToList();
|
||||
|
||||
var customersByMonth = customers.Where(c => c.CreatedAt >= startDate).GroupBy(c => new DateTime(c.CreatedAt.Year, c.CreatedAt.Month, 1)).ToDictionary(g => g.Key, g => g.Count());
|
||||
@@ -1523,7 +1529,8 @@ public class ReportsController : Controller
|
||||
if (!AllowAccounting()) return RedirectToAction(nameof(Landing));
|
||||
var now = DateTime.UtcNow;
|
||||
var today = DateTime.Today;
|
||||
var allInvoices = (await _unitOfWork.Invoices.GetAllAsync(false, i => i.Customer, i => i.Payments)).ToList();
|
||||
var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||
var allInvoices = (await _unitOfWork.Invoices.FindAsync(i => i.CompanyId == companyId, false, i => i.Customer, i => i.Payments)).ToList();
|
||||
var activeInvoices = allInvoices.Where(i => i.Status != InvoiceStatus.Voided && i.Status != InvoiceStatus.WrittenOff).ToList();
|
||||
var outstandingInvoices = activeInvoices.Where(i => i.Status != InvoiceStatus.Paid && i.Total > i.AmountPaid).ToList();
|
||||
var overdueInvoices = activeInvoices.Where(i => i.Status != InvoiceStatus.Paid && i.DueDate.HasValue && i.DueDate.Value < today).ToList();
|
||||
@@ -1574,7 +1581,8 @@ public class ReportsController : Controller
|
||||
|
||||
var monthLabels = new List<string>(); var monthlyBillsPaid = new List<decimal>(); var monthlyDirectExpenses = new List<decimal>();
|
||||
// Also load collected payments for P&L comparison
|
||||
var allInvoices = (await _unitOfWork.Invoices.GetAllAsync(false, i => i.Payments)).ToList();
|
||||
var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||
var allInvoices = (await _unitOfWork.Invoices.FindAsync(i => i.CompanyId == companyId, false, i => i.Payments)).ToList();
|
||||
var paymentsByMonth = allInvoices.SelectMany(i => i.Payments.Where(p => !p.IsDeleted)).GroupBy(p => new DateTime(p.PaymentDate.Year, p.PaymentDate.Month, 1)).ToDictionary(g => g.Key, g => g.ToList());
|
||||
var plRevenue = new List<decimal>(); var plExpenses = new List<decimal>(); var plNet = new List<decimal>();
|
||||
for (var i = months - 1; i >= 0; i--)
|
||||
@@ -1609,8 +1617,10 @@ public class ReportsController : Controller
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var startDate = now.AddMonths(-months);
|
||||
var powderTransactions = (await _unitOfWork.InventoryTransactions.GetAllAsync(false, t => t.InventoryItem))
|
||||
.Where(t => t.TransactionType == InventoryTransactionType.JobUsage && t.TransactionDate >= startDate).ToList();
|
||||
var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||
var powderTransactions = (await _unitOfWork.InventoryTransactions.FindAsync(
|
||||
t => t.CompanyId == companyId && t.TransactionType == InventoryTransactionType.JobUsage && t.TransactionDate >= startDate, false, t => t.InventoryItem))
|
||||
.ToList();
|
||||
|
||||
var topColors = powderTransactions.Where(t => t.InventoryItem != null).GroupBy(t => t.InventoryItemId)
|
||||
.Select(g => new PowderUsageByColorItem { InventoryItemId = g.Key, ColorName = g.First().InventoryItem!.ColorName ?? g.First().InventoryItem.Name, ColorCode = g.First().InventoryItem!.ColorCode, SKU = g.First().InventoryItem!.SKU, Manufacturer = g.First().InventoryItem!.Manufacturer, TotalLbsUsed = g.Sum(t => Math.Abs(t.Quantity)), TotalCost = g.Sum(t => Math.Abs(t.TotalCost)), JobCount = g.Where(t => !string.IsNullOrEmpty(t.Reference)).Select(t => t.Reference).Distinct().Count() })
|
||||
@@ -1631,7 +1641,8 @@ public class ReportsController : Controller
|
||||
/// <summary>Sales by Customer report — all active (non-voided) invoices grouped by customer, sorted by total invoiced.</summary>
|
||||
public async Task<IActionResult> SalesByCustomer(int months = 6)
|
||||
{
|
||||
var allInvoices = (await _unitOfWork.Invoices.GetAllAsync(false, i => i.Customer, i => i.Payments)).ToList();
|
||||
var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||
var allInvoices = (await _unitOfWork.Invoices.FindAsync(i => i.CompanyId == companyId, false, i => i.Customer, i => i.Payments)).ToList();
|
||||
var activeInvoices = allInvoices.Where(i => i.Status != InvoiceStatus.Voided && i.Status != InvoiceStatus.WrittenOff).ToList();
|
||||
var items = activeInvoices.Where(i => i.Customer != null)
|
||||
.GroupBy(i => new { i.CustomerId, Name = i.Customer!.IsCommercial ? i.Customer.CompanyName : $"{i.Customer.ContactFirstName} {i.Customer.ContactLastName}".Trim(), i.Customer.IsCommercial })
|
||||
@@ -1650,8 +1661,9 @@ public class ReportsController : Controller
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var completedStatusCodes = new[] { "COMPLETED", "READY_FOR_PICKUP", "DELIVERED" };
|
||||
var customers = (await _unitOfWork.Customers.GetAllAsync()).ToList();
|
||||
var completedJobs = (await _unitOfWork.Jobs.GetAllAsync(false, j => j.JobStatus)).Where(j => completedStatusCodes.Contains(j.JobStatus.StatusCode)).ToList();
|
||||
var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||
var customers = (await _unitOfWork.Customers.FindAsync(c => c.CompanyId == companyId)).ToList();
|
||||
var completedJobs = (await _unitOfWork.Jobs.FindAsync(j => j.CompanyId == companyId, false, j => j.JobStatus)).Where(j => completedStatusCodes.Contains(j.JobStatus.StatusCode)).ToList();
|
||||
var items = customers.Where(c => c.IsActive).Select(c =>
|
||||
{
|
||||
var cJobs = completedJobs.Where(j => j.CustomerId == c.Id).ToList();
|
||||
@@ -1682,7 +1694,8 @@ public class ReportsController : Controller
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var completedStatusCodes = new[] { "COMPLETED", "READY_FOR_PICKUP", "DELIVERED" };
|
||||
var completedJobs = (await _unitOfWork.Jobs.GetAllAsync(false, j => j.JobStatus)).Where(j => completedStatusCodes.Contains(j.JobStatus.StatusCode) && j.CompletedDate.HasValue).ToList();
|
||||
var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||
var completedJobs = (await _unitOfWork.Jobs.FindAsync(j => j.CompanyId == companyId, false, j => j.JobStatus)).Where(j => completedStatusCodes.Contains(j.JobStatus.StatusCode) && j.CompletedDate.HasValue).ToList();
|
||||
var allStatusHistory = await _operationalReports.GetAllJobStatusHistoryAsync();
|
||||
var historyByJob = allStatusHistory.GroupBy(h => h.JobId).ToDictionary(g => g.Key, g => g.OrderBy(h => h.ChangedDate).ToList());
|
||||
var statusDisplayOrder = new[] { "PENDING", "QUOTED", "APPROVED", "IN_PREPARATION", "SANDBLASTING", "MASKING_TAPING", "CLEANING", "IN_OVEN", "COATING", "CURING", "QUALITY_CHECK" };
|
||||
@@ -1720,7 +1733,8 @@ public class ReportsController : Controller
|
||||
var now = DateTime.UtcNow;
|
||||
var today = DateTime.Today;
|
||||
var activeStatusCodes = new[] { "PENDING", "QUOTED", "APPROVED", "IN_PREPARATION", "SANDBLASTING", "MASKING_TAPING", "CLEANING", "IN_OVEN", "COATING", "CURING", "QUALITY_CHECK", "ON_HOLD" };
|
||||
var activeJobs = (await _unitOfWork.Jobs.GetAllAsync(false, j => j.Customer, j => j.JobStatus, j => j.JobPriority)).Where(j => activeStatusCodes.Contains(j.JobStatus.StatusCode)).ToList();
|
||||
var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||
var activeJobs = (await _unitOfWork.Jobs.FindAsync(j => j.CompanyId == companyId, false, j => j.Customer, j => j.JobStatus, j => j.JobPriority)).Where(j => activeStatusCodes.Contains(j.JobStatus.StatusCode)).ToList();
|
||||
var items = activeJobs.Select(j => new JobStatusAgingItem
|
||||
{
|
||||
JobId = j.Id, JobNumber = j.JobNumber, CustomerName = j.Customer?.IsCommercial == true ? j.Customer.CompanyName ?? "Unknown" : $"{j.Customer?.ContactFirstName} {j.Customer?.ContactLastName}".Trim(),
|
||||
@@ -1740,7 +1754,8 @@ public class ReportsController : Controller
|
||||
{
|
||||
if (!AllowAccounting()) return RedirectToAction(nameof(Landing));
|
||||
var today = DateTime.Today;
|
||||
var allInvoices = (await _unitOfWork.Invoices.GetAllAsync(false, i => i.Customer, i => i.Payments)).ToList();
|
||||
var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||
var allInvoices = (await _unitOfWork.Invoices.FindAsync(i => i.CompanyId == companyId, false, i => i.Customer, i => i.Payments)).ToList();
|
||||
var items = allInvoices.Where(i => i.Customer != null && i.Status != InvoiceStatus.Voided && i.Status != InvoiceStatus.WrittenOff && i.Status != InvoiceStatus.Paid)
|
||||
.Select(i =>
|
||||
{
|
||||
@@ -1758,7 +1773,8 @@ public class ReportsController : Controller
|
||||
/// </summary>
|
||||
public async Task<IActionResult> PowderConsumption(int months = 6)
|
||||
{
|
||||
var allTx = (await _unitOfWork.InventoryTransactions.GetAllAsync(false, t => t.InventoryItem)).ToList();
|
||||
var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||
var allTx = (await _unitOfWork.InventoryTransactions.FindAsync(t => t.CompanyId == companyId, false, t => t.InventoryItem)).ToList();
|
||||
var items = allTx.Where(t => t.InventoryItem != null)
|
||||
.GroupBy(t => new { t.InventoryItemId, t.InventoryItem!.Name, t.InventoryItem.SKU, t.InventoryItem.ColorName, t.InventoryItem.ColorCode, t.InventoryItem.Manufacturer })
|
||||
.Select(g => new PowderConsumptionItem { InventoryItemId = g.Key.InventoryItemId, ItemName = g.Key.Name, SKU = g.Key.SKU, ColorName = g.Key.ColorName, ColorCode = g.Key.ColorCode, Manufacturer = g.Key.Manufacturer, TotalPurchasedLbs = g.Where(t => t.TransactionType == InventoryTransactionType.Purchase || t.TransactionType == InventoryTransactionType.Initial).Sum(t => t.Quantity), TotalConsumedLbs = g.Where(t => t.TransactionType == InventoryTransactionType.JobUsage || t.TransactionType == InventoryTransactionType.Waste).Sum(t => Math.Abs(t.Quantity)), PurchaseCount = g.Count(t => t.TransactionType == InventoryTransactionType.Purchase), UsageJobCount = g.Where(t => t.TransactionType == InventoryTransactionType.JobUsage && !string.IsNullOrEmpty(t.Reference)).Select(t => t.Reference).Distinct().Count() })
|
||||
@@ -1776,8 +1792,9 @@ public class ReportsController : Controller
|
||||
public async Task<IActionResult> InventoryTurnover(int months = 6)
|
||||
{
|
||||
var daysInPeriod = months * 30.0;
|
||||
var inventory = (await _unitOfWork.InventoryItems.GetAllAsync()).ToList();
|
||||
var allTx = (await _unitOfWork.InventoryTransactions.GetAllAsync(false, t => t.InventoryItem)).ToList();
|
||||
var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||
var inventory = (await _unitOfWork.InventoryItems.FindAsync(i => i.CompanyId == companyId)).ToList();
|
||||
var allTx = (await _unitOfWork.InventoryTransactions.FindAsync(t => t.CompanyId == companyId, false, t => t.InventoryItem)).ToList();
|
||||
var items = inventory.Where(i => i.IsActive).Select(i =>
|
||||
{
|
||||
var iTx = allTx.Where(t => t.InventoryItemId == i.Id).ToList();
|
||||
@@ -1835,8 +1852,9 @@ public class ReportsController : Controller
|
||||
var now = DateTime.UtcNow;
|
||||
var today = DateTime.Today;
|
||||
|
||||
var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||
// Load invoices for AR data
|
||||
var allInvoices = (await _unitOfWork.Invoices.GetAllAsync(false, i => i.Customer, i => i.Payments)).ToList();
|
||||
var allInvoices = (await _unitOfWork.Invoices.FindAsync(i => i.CompanyId == companyId, false, i => i.Customer, i => i.Payments)).ToList();
|
||||
var activeInvoices = allInvoices.Where(i => i.Status != InvoiceStatus.Voided && i.Status != InvoiceStatus.WrittenOff).ToList();
|
||||
var outstandingInvoices = activeInvoices.Where(i => i.BalanceDue > 0 && i.Status != InvoiceStatus.Paid).ToList();
|
||||
|
||||
@@ -1930,13 +1948,14 @@ public class ReportsController : Controller
|
||||
var companyName = await GetCompanyNameAsync();
|
||||
var today = DateTime.Today;
|
||||
|
||||
var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||
// Open AR invoices
|
||||
var openInvoices = (await _unitOfWork.Invoices.GetAllAsync(false, i => i.Customer, i => i.Payments))
|
||||
var openInvoices = (await _unitOfWork.Invoices.FindAsync(i => i.CompanyId == companyId, false, i => i.Customer, i => i.Payments))
|
||||
.Where(i => i.BalanceDue > 0 && i.Status != InvoiceStatus.Voided && i.Status != InvoiceStatus.WrittenOff && i.Status != InvoiceStatus.Paid)
|
||||
.ToList();
|
||||
|
||||
// Compute avg days to pay per customer from paid invoices
|
||||
var paidInvoices = (await _unitOfWork.Invoices.GetAllAsync(false, i => i.Payments))
|
||||
var paidInvoices = (await _unitOfWork.Invoices.FindAsync(i => i.CompanyId == companyId, false, i => i.Payments))
|
||||
.Where(i => i.Status == InvoiceStatus.Paid && i.InvoiceDate != default)
|
||||
.ToList();
|
||||
var avgDaysByCustomer = paidInvoices
|
||||
@@ -2137,7 +2156,8 @@ public class ReportsController : Controller
|
||||
var companyName = await GetCompanyNameAsync();
|
||||
var today = DateTime.Today;
|
||||
|
||||
var allInvoices = (await _unitOfWork.Invoices.GetAllAsync(false, i => i.Customer, i => i.Payments)).ToList();
|
||||
var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||
var allInvoices = (await _unitOfWork.Invoices.FindAsync(i => i.CompanyId == companyId, false, i => i.Customer, i => i.Payments)).ToList();
|
||||
var activeInvoices = allInvoices.Where(i =>
|
||||
i.Status != InvoiceStatus.Voided &&
|
||||
i.Status != InvoiceStatus.WrittenOff).ToList();
|
||||
@@ -2256,8 +2276,9 @@ public class ReportsController : Controller
|
||||
var companyName = await GetCompanyNameAsync();
|
||||
var now = DateTime.UtcNow;
|
||||
var startOfYear = new DateTime(now.Year, 1, 1);
|
||||
var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
|
||||
|
||||
var allInvoices = (await _unitOfWork.Invoices.GetAllAsync(false, i => i.Customer, i => i.Payments))
|
||||
var allInvoices = (await _unitOfWork.Invoices.FindAsync(i => i.CompanyId == companyId, false, i => i.Customer, i => i.Payments))
|
||||
.Where(i => i.Status != InvoiceStatus.Voided && i.Status != InvoiceStatus.WrittenOff)
|
||||
.ToList();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user