Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ clean schema.
| MT940 | `.sta` `.940` `.mt940` | `D` / `C` mark on `:61:` | `:25:` | content hash | `:60F:` currency |
| CAMT.053 | `.xml` (sniffed) | `CdtDbtInd` (`DBIT`/`CRDT`) | IBAN / `Othr` id | `AcctSvcrRef` | `Amt@Ccy` |
| CAMT.052 | `.xml` (sniffed) | `CdtDbtInd` (`DBIT`/`CRDT`) | IBAN / `Othr` id | `AcctSvcrRef` | `Amt@Ccy` |
| QIF | `.qif` | sign of `T` / `U` amount | – | content hash (`N` ref hint) | `--currency` default |
| QIF | `.qif` | sign of `T` / `U` amount | – | content hash | `--currency` default |
| Text / PDF-text | `.txt` | sign / column heuristic | – | content hash | `--currency` default |

All amounts are normalized to one sign convention (debits negative, credits
Expand Down
5 changes: 2 additions & 3 deletions statement_normalizer/parsers/qif_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@
some dialects and is used only as a fallback.
* ``P`` payee, and ``M`` memo. We join them into the description, mirroring how
the OFX parser combines ``NAME`` + ``MEMO``.
* ``N`` the check or reference number, preserved on ``raw`` and used as a dedup
hint when present.
* ``N`` the check or reference number, preserved on ``raw`` for traceability.
It is not a bank-assigned unique id, so it is not used as a ``FITID``.
* ``L`` the Quicken category/transfer line, preserved on ``raw`` for
traceability.

Expand Down Expand Up @@ -135,7 +135,6 @@ def flush() -> None:
amount=amount,
description=description,
currency=currency,
fitid=record.get("N") or None,
source_format="qif",
raw=raw,
)
Expand Down
27 changes: 26 additions & 1 deletion tests/test_qif_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ def test_qif_basic(fixture_path):
assert gas.txn_type == TxnType.DEBIT
# Payee + memo joined, mirroring the OFX parser's NAME + MEMO behavior.
assert gas.description == "GAS STATION 4471 Fuel regular unleaded"
assert gas.fitid == "1021" # N (check/ref number) preserved as a dedup hint
assert gas.fitid is None # QIF N is a check/reference value, not a unique FITID.
assert gas.raw["N"] == "1021"
assert gas.raw["L"] == "Auto:Fuel" # category retained on raw


Expand Down Expand Up @@ -58,6 +59,30 @@ def test_qif_end_to_end_via_normalize(fixture_path):
assert len(stmt.transactions) == 4


def test_qif_repeated_reference_does_not_drop_transactions():
text = (
"!Type:Bank\n"
"D01/02/2024\n"
"T-20.00\n"
"NATM\n"
"PCASH WITHDRAWAL\n"
"^\n"
"D01/05/2024\n"
"T-55.00\n"
"NATM\n"
"PCASH WITHDRAWAL\n"
"^\n"
)

stmt = normalize_bytes(text, fmt="qif")

assert [txn.amount for txn in stmt.transactions] == [
Decimal("-20.00"),
Decimal("-55.00"),
]
assert [txn.raw["N"] for txn in stmt.transactions] == ["ATM", "ATM"]


def test_qif_skips_non_statement_sections():
# An !Account list block and a category list should not yield transactions;
# only the !Type:Bank records do.
Expand Down