From 0959af442f530c9502a7f7eac12f18dc7f48c7dd Mon Sep 17 00:00:00 2001 From: Smit Joshi Date: Mon, 10 Aug 2026 11:23:59 +0530 Subject: [PATCH] fix(qif): preserve repeated check references --- README.md | 2 +- statement_normalizer/parsers/qif_parser.py | 5 ++-- tests/test_qif_parser.py | 27 +++++++++++++++++++++- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 06cb513..bf8f82d 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/statement_normalizer/parsers/qif_parser.py b/statement_normalizer/parsers/qif_parser.py index e2da644..d3ee97a 100644 --- a/statement_normalizer/parsers/qif_parser.py +++ b/statement_normalizer/parsers/qif_parser.py @@ -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. @@ -135,7 +135,6 @@ def flush() -> None: amount=amount, description=description, currency=currency, - fitid=record.get("N") or None, source_format="qif", raw=raw, ) diff --git a/tests/test_qif_parser.py b/tests/test_qif_parser.py index 29b7375..0900492 100644 --- a/tests/test_qif_parser.py +++ b/tests/test_qif_parser.py @@ -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 @@ -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.