Metadata-Version: 2.5
Name: fava-intransit
Version: 0.1.0
Summary: Fava extension: Sankey diagram and consistency checks for a transit account
Author: gio
License: MIT
License-File: LICENSE
Keywords: beancount,fava,plaintextaccounting,sankey
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Web Environment
Classifier: Intended Audience :: End Users/Desktop
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Office/Business :: Financial :: Accounting
Requires-Python: >=3.10
Requires-Dist: beancount>=3
Requires-Dist: fava>=1.30
Provides-Extra: dev
Requires-Dist: build; extra == 'dev'
Requires-Dist: pytest>=7; extra == 'dev'
Requires-Dist: twine; extra == 'dev'
Description-Content-Type: text/markdown

# fava-intransit

A [Fava](https://beancount.github.io/fava/) extension that maps the flows through a
transit account, and tells you where the chain is broken.

Money moved between two institutions is booked twice, once on each side, against a
transit account such as `Assets:Transfer:InTransit`:

```beancount
2020-02-01 * "transfer out to Beta"
  Assets:Bank:Alpha:Checking  -1000.00 EUR
  Assets:Transfer:InTransit

2020-02-04 * "transfer in from Alpha"
  Assets:Bank:Beta:Checking    1000.00 EUR
  Assets:Transfer:InTransit
```

The two legs come from two different statements and are usually imported days apart,
so the transit account is where half-finished transfers pile up. This extension pairs
the legs up and reports what is left over:

- **unmatched legs** — money that left an institution and never arrived, or arrived
  without having been sent. Usually a statement you have not imported yet;
- **transfers that skip the transit account** — the opposite mistake, one institution
  booked straight against another.

Both are drawn as a **Sankey diagram** of institution-to-institution flows, so a broken
chain shows up as a red ribbon ending on a dedicated *Unmatched* node instead of as a
line in a list.

## Install

```bash
pip install fava-intransit
```

## Enable

Add a `custom` directive to your ledger:

```beancount
2010-01-01 custom "fava-extension" "fava_intransit" "{
  'transit_account': 'Assets:Transfer:InTransit',
  'days': 5
}"
```

Every key is optional — with `"{}"` (or no config string at all) the defaults above
apply. The report then appears in Fava's sidebar as **In Transit**.

| key | default | meaning |
| --- | --- | --- |
| `transit_account` | `Assets:Transfer:InTransit` | the account transfers pass through |
| `days` | `5` | how far apart two legs of the same transfer may be dated |
| `institution_level` | `3` | which account component names the institution: with `3`, `Assets:Bank:ING:ContoCorrente` is `ING` |
| `institutions` | `{}` | account prefix → name, when the level rule is not enough |

`days` can also be changed from the report itself, without editing the ledger.

## Which accounts are checked

The set of accounts that are *supposed* to settle through the transit account is
derived from the ledger, not configured: an account joins it as soon as it has been
booked against the transit account even once.

That is what keeps payment wallets out of the report. A wallet topped up by direct
debit is meant to face the bank account directly, and a naive check over every
`Assets:Bank:*` account would flag every single top-up.

## Command line

The same analysis, as text, for use from a Makefile or at the end of an import script:

```bash
fava-intransit main.beancount --days 5
```

```python
from fava_intransit.core import Config, analyze

analysis = analyze(entries, Config(transit_account="Assets:Transfer:InTransit"))
print(len(analysis.unmatched), "legs without a counterpart")
```

`fava_intransit.core` does no I/O: it takes entries you have already loaded, which is
also how the Fava extension avoids parsing the ledger a second time on every request.

## Notes

- Pairing is greedy first-fit on (opposite amount, same currency, within `days`).
- Pairing always runs over the **whole** ledger. Fava's time filter narrows the diagram
  and the tables afterwards, so that a transfer straddling the edge of the filtered
  period is not reported as unmatched.
- One diagram per currency.
- No JavaScript and no bundled assets: the Sankey layout is computed in Python and
  emitted as plain SVG, themed with Fava's own CSS variables.

## Releasing

```bash
rm -rf dist
python -m build
twine check dist/*
twine upload dist/*
```

Bump `version` in `pyproject.toml` first and tag the commit: PyPI refuses to
overwrite a version that already exists, and deleting one does not free the
number either. Try it on TestPyPI (`twine upload -r testpypi dist/*`) when in
doubt - the package page is the README, and a published version cannot be
amended.

## License

MIT
