Preserve accounting linkages through CSV export/import round-trip

The data export was silently dropping account linkages, so an export->import
(used to copy prod data) lost which bank account each payment hit, all invoice
line items, and any bill/deposit/journal-entry detail. Diagnosed while cleaning
up a company's books from a copied dataset. Now every accounting linkage travels
by account/vendor/customer/invoice number — matching how expenses already worked
— so a round-trip preserves GL attribution instead of dropping it.

Payments: add DepositAccountNumber to the export + DTO; import resolves it back
to DepositAccountId so payments post to the right bank account on recalc.

Invoices: were header-only (re-imported invoices had 0 line items). Add a new
invoice_items CSV (one row per line, carrying RevenueAccountNumber) with export,
idempotent import, UI cards, and a template.

Bills / Deposits / Journal Entries: were not exported at all. Add full-fidelity
export + import including line-item children — bills + bill line items (vendor
by name, AP account + per-line expense account by number), deposits (customer +
bank account + applied invoice), and journal entries + JE lines (account by
number, debit/credit). 5 DTOs, 5 importers, 5 exporters + actions, all added to
the all_data export zip, plus 10 import/export UI cards.

Shared RunCsvImport helper added for the new import endpoints. All linkages
resolve by stable business keys (numbers/names), never internal IDs, so the
files round-trip across databases. Build clean; 278 unit tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-18 18:49:39 -04:00
parent f752abad86
commit f54b945053
11 changed files with 1543 additions and 20 deletions
@@ -0,0 +1,31 @@
using CsvHelper.Configuration.Attributes;
namespace PowderCoating.Application.DTOs.Import;
/// <summary>
/// DTO for importing journal entry lines from CSV. Column names match the native journal-entry-lines
/// export (ExportJournalEntryLinesCsv). Lines are matched to their parent entry by EntryNumber and the
/// account is resolved from AccountNumber (required — a JE line is meaningless without its account).
/// Either DebitAmount or CreditAmount is non-zero per line, not both.
/// </summary>
public class JournalEntryLineImportDto
{
[Name("EntryNumber")]
public string? EntryNumber { get; set; }
/// <summary>Account number (Chart of Accounts) this line debits or credits. Required.</summary>
[Name("AccountNumber")]
public string? AccountNumber { get; set; }
[Name("DebitAmount")]
public decimal DebitAmount { get; set; }
[Name("CreditAmount")]
public decimal CreditAmount { get; set; }
[Name("Description")]
public string? Description { get; set; }
[Name("LineOrder")]
public int LineOrder { get; set; }
}