Reconcile account type/sub-type in QBO + CSV imports
Closes the remaining type/sub-type-consistency vector (create/edit + IIF were already covered). The sign convention keys off sub-type, so a mismatched pair posts with the wrong debit/credit sign. - New AccountClassification.DefaultSubTypeForType(type). - QBO import: QBO Type is reliable but DetailType frequently isn't mappable and fell back to AccountSubType.Other (an expense-range sub-type) — so an unmapped Liability/Equity/Revenue account would have posted debit-normal. Now reconciles the sub-type to the type when they disagree. - CSV import: type and sub-type came from independent columns; now derives the type from the sub-type (sub-type authoritative, matching create/edit). - IIF import already returns consistent (type, sub-type) tuples — unchanged. Build clean; 293 unit tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -28,4 +28,21 @@ public static class AccountClassification
|
|||||||
// All expense sub-types (enum values >= 50) and any future additions default to Expense.
|
// All expense sub-types (enum values >= 50) and any future additions default to Expense.
|
||||||
_ => AccountType.Expense
|
_ => AccountType.Expense
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a sensible generic <see cref="AccountSubType"/> for a given <see cref="AccountType"/>.
|
||||||
|
/// Used by importers (e.g. QuickBooks) to reconcile a sub-type back to its parent type when the
|
||||||
|
/// source's detail-type couldn't be mapped to a specific sub-type — without this, an unmapped
|
||||||
|
/// liability/equity/revenue account would fall back to <c>Other</c> (an expense-range sub-type)
|
||||||
|
/// and post with the wrong debit/credit sign, since the sign convention keys off sub-type.
|
||||||
|
/// </summary>
|
||||||
|
public static AccountSubType DefaultSubTypeForType(AccountType type) => type switch
|
||||||
|
{
|
||||||
|
AccountType.Asset => AccountSubType.OtherCurrentAsset,
|
||||||
|
AccountType.Liability => AccountSubType.OtherCurrentLiability,
|
||||||
|
AccountType.Equity => AccountSubType.OwnersEquity,
|
||||||
|
AccountType.Revenue => AccountSubType.OtherIncome,
|
||||||
|
AccountType.CostOfGoods => AccountSubType.CostOfGoodsSold,
|
||||||
|
_ => AccountSubType.Other
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2866,6 +2866,10 @@ public class CsvImportService : ICsvImportService
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sub-type is authoritative (matches account create/edit): derive the parent type
|
||||||
|
// from it so a mismatched CSV pair can't post with the wrong debit/credit sign.
|
||||||
|
accountType = AccountClassification.TypeForSubType(accountSubType);
|
||||||
|
|
||||||
DateTime? openingBalanceDate = null;
|
DateTime? openingBalanceDate = null;
|
||||||
if (!string.IsNullOrWhiteSpace(record.OpeningBalanceDate)
|
if (!string.IsNullOrWhiteSpace(record.OpeningBalanceDate)
|
||||||
&& DateTime.TryParse(record.OpeningBalanceDate, out var parsedDate))
|
&& DateTime.TryParse(record.OpeningBalanceDate, out var parsedDate))
|
||||||
|
|||||||
@@ -565,7 +565,14 @@ public class QuickBooksOnlineService
|
|||||||
var parentName = name.Contains(':') ? name.Substring(0, name.LastIndexOf(':')).Trim() : null;
|
var parentName = name.Contains(':') ? name.Substring(0, name.LastIndexOf(':')).Trim() : null;
|
||||||
|
|
||||||
result.TotalRecords++;
|
result.TotalRecords++;
|
||||||
rows.Add((displayName, parentName, number, desc, MapQboAccountType(typeStr), MapQboDetailType(detailType)));
|
// QBO Type (high-level) is reliable; DetailType often isn't mappable and falls back to
|
||||||
|
// Other. Reconcile so sub-type's parent always matches the type — otherwise an unmapped
|
||||||
|
// liability/equity/revenue would get an expense-range sub-type and post with the wrong sign.
|
||||||
|
var acctType = MapQboAccountType(typeStr);
|
||||||
|
var subType = MapQboDetailType(detailType);
|
||||||
|
if (AccountClassification.TypeForSubType(subType) != acctType)
|
||||||
|
subType = AccountClassification.DefaultSubTypeForType(acctType);
|
||||||
|
rows.Add((displayName, parentName, number, desc, acctType, subType));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass 1: upsert every account WITHOUT parent links so they all get IDs.
|
// Pass 1: upsert every account WITHOUT parent links so they all get IDs.
|
||||||
|
|||||||
Reference in New Issue
Block a user