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
@@ -156,3 +156,47 @@ public class Expense : BaseEntity
public virtual Account PaymentAccount { get; set; } = null!;
public virtual Job? Job { get; set; }
}
/// <summary>
/// Manual double-entry journal entry. Lines must balance (sum of debits == sum of credits)
/// before posting. Once posted the entry is immutable — use Reverse to correct it.
/// Entry numbering follows the pattern JE-YYMM-#### scoped per company.
/// </summary>
public class JournalEntry : BaseEntity
{
public string EntryNumber { get; set; } = string.Empty;
public DateTime EntryDate { get; set; } = DateTime.UtcNow;
public string? Reference { get; set; }
public string? Description { get; set; }
public JournalEntryStatus Status { get; set; } = JournalEntryStatus.Draft;
/// <summary>True if this entry was machine-generated as a reversal of another entry.</summary>
public bool IsReversal { get; set; } = false;
/// <summary>FK to the original entry being reversed. Null for normal entries.</summary>
public int? ReversalOfId { get; set; }
public DateTime? PostedAt { get; set; }
public string? PostedBy { get; set; }
// Navigation
public virtual ICollection<JournalEntryLine> Lines { get; set; } = new List<JournalEntryLine>();
public virtual JournalEntry? ReversalOf { get; set; }
}
/// <summary>
/// One debit or credit line within a <see cref="JournalEntry"/>. Either DebitAmount or CreditAmount
/// should be non-zero per line (not both). LineOrder controls display sequence.
/// </summary>
public class JournalEntryLine : BaseEntity
{
public int JournalEntryId { get; set; }
public int AccountId { get; set; }
public decimal DebitAmount { get; set; }
public decimal CreditAmount { get; set; }
public string? Description { get; set; }
public int LineOrder { get; set; }
// Navigation
public virtual JournalEntry JournalEntry { get; set; } = null!;
public virtual Account Account { get; set; } = null!;
}