Phase C: Add Manual Journal Entries (double-entry GL)

- JournalEntry + JournalEntryLine entities with Draft/Posted/Reversed lifecycle
- JournalEntryStatus enum (Draft, Posted, Reversed)
- Migration AddJournalEntries: two new tables with self-referencing reversal FK
- IUnitOfWork/UnitOfWork wired with JournalEntries + JournalEntryLines repos
- ApplicationDbContext: DbSets, tenant query filters, reversal FK config
- LedgerService: JE lines added as 10th source in GetAccountLedgerAsync and ComputePriorBalanceAsync
- JournalEntriesController: Index (All/Draft/Posted tabs), Create, Details, Post, Reverse, Delete
- Views: Index, Create (dynamic balanced line grid with running debit/credit totals), Details
- journal-entry-create.js: dynamic line management with balance indicator
- Nav: Journal Entries added to Finance section in _Layout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-09 23:56:03 -04:00
parent 0afb474c3e
commit a33687f7bd
15 changed files with 11017 additions and 3 deletions
@@ -324,6 +324,11 @@ public class ApplicationDbContext : IdentityDbContext<ApplicationUser>, IDataPro
/// <summary>Ad-hoc expense records (non-bill spending); tenant-filtered with soft delete.</summary>
public DbSet<Expense> Expenses { get; set; }
/// <summary>Manual double-entry journal entries (Draft/Posted/Reversed lifecycle); tenant-filtered with soft delete.</summary>
public DbSet<JournalEntry> JournalEntries { get; set; }
/// <summary>Individual debit/credit lines within a journal entry; soft-delete only (access controlled through parent JournalEntry).</summary>
public DbSet<JournalEntryLine> JournalEntryLines { get; set; }
// Job Templates
/// <summary>Reusable job templates that pre-populate job items, coats, and prep services on job creation.</summary>
public DbSet<JobTemplate> JobTemplates { get; set; }
@@ -614,6 +619,11 @@ public class ApplicationDbContext : IdentityDbContext<ApplicationUser>, IDataPro
modelBuilder.Entity<Expense>().HasQueryFilter(e =>
!e.IsDeleted && (IsPlatformAdmin || e.CompanyId == CurrentCompanyId));
// Journal Entries: tenant-filtered; lines use soft-delete only (child rows)
modelBuilder.Entity<JournalEntry>().HasQueryFilter(e =>
!e.IsDeleted && (IsPlatformAdmin || e.CompanyId == CurrentCompanyId));
modelBuilder.Entity<JournalEntryLine>().HasQueryFilter(e => !e.IsDeleted);
// Purchase Orders
modelBuilder.Entity<PurchaseOrder>().HasQueryFilter(e =>
!e.IsDeleted && (IsPlatformAdmin || e.CompanyId == CurrentCompanyId));
@@ -633,6 +643,13 @@ public class ApplicationDbContext : IdentityDbContext<ApplicationUser>, IDataPro
.HasForeignKey(a => a.ParentAccountId)
.OnDelete(DeleteBehavior.Restrict);
// JournalEntry self-referencing reversal link
modelBuilder.Entity<JournalEntry>()
.HasOne(je => je.ReversalOf)
.WithMany()
.HasForeignKey(je => je.ReversalOfId)
.OnDelete(DeleteBehavior.Restrict);
// Vendor → DefaultExpenseAccount (no cascade)
modelBuilder.Entity<Vendor>()
.HasOne(s => s.DefaultExpenseAccount)