Metadata-Version: 2.5
Name: beancount-zakat
Version: 1.0.0
Summary: Zakat calculation for Beancount ledgers, with a Fava dashboard and a standalone CLI
Project-URL: Homepage, https://github.com/WildeBeast2521/beancount-zakat
Project-URL: Documentation, https://github.com/WildeBeast2521/beancount-zakat#readme
Project-URL: Source, https://github.com/WildeBeast2521/beancount-zakat
Project-URL: Issues, https://github.com/WildeBeast2521/beancount-zakat/issues
Project-URL: Changelog, https://github.com/WildeBeast2521/beancount-zakat/blob/main/CHANGELOG.md
Author: WildeBeast2521
License-Expression: MIT
License-File: LICENSE
Keywords: accounting,beancount,fava,islamic-finance,zakat
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Console
Classifier: Environment :: Web Environment
Classifier: Intended Audience :: End Users/Desktop
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3 :: Only
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
Classifier: Topic :: Religion
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: beancount<4,>=3.0
Requires-Dist: hijridate<3,>=2.5
Provides-Extra: dev
Requires-Dist: fava<2,>=1.30; extra == 'dev'
Requires-Dist: mypy>=1.11; extra == 'dev'
Requires-Dist: pytest-cov>=5; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Provides-Extra: fava
Requires-Dist: fava<2,>=1.30; extra == 'fava'
Description-Content-Type: text/markdown

# beancount-zakat

[![CI](https://github.com/WildeBeast2521/beancount-zakat/actions/workflows/ci.yml/badge.svg)](https://github.com/WildeBeast2521/beancount-zakat/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/beancount-zakat.svg)](https://pypi.org/project/beancount-zakat/)
[![Python versions](https://img.shields.io/pypi/pyversions/beancount-zakat.svg)](https://pypi.org/project/beancount-zakat/)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

Zakat calculation for [Beancount](https://beancount.github.io/) ledgers, with a
[Fava](https://beancount.github.io/fava/) dashboard and a standalone CLI.

Gold and silver nisab are calculated **independently** and presented side by
side. Every figure traces back to the accounts, prices, periods and transactions
that produced it.

> **Informational only.** See the [disclaimer](#disclaimer).

---

## Contents

- [What it does](#what-it-does)
- [Scope](#scope)
- [Installation](#installation)
- [Setting up your ledger](#setting-up-your-ledger)
- [The Fava dashboard](#the-fava-dashboard)
- [The CLI](#the-cli)
- [CSV export](#csv-export)
- [Configuration](#configuration)
- [How the calculation works](#how-the-calculation-works)
- [Assumptions and limitations](#assumptions-and-limitations)
- [Troubleshooting](#troubleshooting)
- [Privacy](#privacy)
- [How this is tested](#how-this-is-tested)
- [Contributing](#contributing)
- [License](#license)
- [Disclaimer](#disclaimer)

---

## What it does

- Reads **every entry** loaded by your root ledger, `include`d files and all.
- Selects accounts purely from `beancount_zakat:` metadata on `Open` directives.
  No configuration file, no account list to keep in sync.
- Reconstructs net zakatable wealth over time, re-valuing holdings in other
  commodities whenever their prices move.
- Runs a **layered / marginal hawl** model: every distinct wealth level becomes
  a slice with its own independent holding period.
- Treats the nisab as the **moving threshold** it is — it tracks the metal
  price, so it differs on almost every year of your history.
- Computes gold and silver independently and never adds them together.
- Reports a **signed** balance: positive is owed, negative is paid in excess.
- Keeps every figure exact. No floating-point drift anywhere in the arithmetic.
- Defaults the cutoff to today, so a quiet ledger keeps accruing hawl.

## Scope

### What it covers

- **Cash, bank balances and any other account you tag as an asset.**
- **Holdings in other commodities** — gold, silver, a foreign currency — valued
  from the `price` directives already in your ledger.
- **Stock bought for resale.** Shares or inventory held as trade goods, bought
  with the intention of selling on, are counted at market value like any other
  commodity holding.
- **Debts you tag as liabilities**, deducted from the total.
- **Zakat already paid**, including refunds and correcting reversals.
- **Both bases.** Gold and silver are calculated independently across your whole
  history, from the first tagged posting to the report date.

### What it does not cover

- **Zakat al-fitr** — the per-person charge at the end of Ramadan. Different
  basis, different timing.
- **Agricultural produce, livestock and *rikaz***. These carry their own rates
  and thresholds and are not modelled at all.

### What is yours to decide

- **Which accounts count.** Nothing is inferred from an account name — an account
  takes part only if you tag it. Excluding a personal-use asset means simply not
  tagging it.
- **Which debts are deductible.** Same mechanism, same reasoning. Scholars
  differ sharply on mortgages and future instalments; tagging is where you
  express your position.
- **Whether a shareholding is trade goods or a long-term investment.** The tool
  cannot tell them apart, and will not guess from an account name. Tag the first,
  leave the second alone.
- **Which basis to follow.** Gold and silver are alternatives. Both are shown so
  you can compare them; they are never added together.
- **The nisab weights**, if the authority you follow publishes different gram
  equivalents. They are configurable.
- **Whether this tool's method matches your position.** It accrues liability in
  proportion to time held once the hawl is met, which is a choice and not a
  consensus — see [How the calculation works](#how-the-calculation-works).

## Installation

Requires Python 3.10 or newer, and works with Beancount 3.x. The dashboard needs
Fava 1.30 or newer.

```bash
# Calculation engine and CLI only (does not install Fava):
pip install beancount-zakat

# With the Fava dashboard:
pip install 'beancount-zakat[fava]'
```

## Setting up your ledger

### 1. Tag your accounts

The `beancount_zakat` metadata key on an `Open` directive is the **only** thing
that puts an account in scope. Three roles are recognised:

| Value | Meaning |
|---|---|
| `"asset"` | Counts positively towards zakatable wealth |
| `"liability"` | Counts with its natural (negative) Beancount sign |
| `"expense"` | Postings here are zakat payments |

```beancount
2020-01-01 open Assets:Bank:Savings       PKR
  beancount_zakat: "asset"

2020-01-01 open Assets:Cash               PKR
  beancount_zakat: "asset"

2020-01-01 open Liabilities:Loan-Payable  PKR
  beancount_zakat: "liability"

2020-01-01 open Expenses:Donations:Zakat  PKR
  beancount_zakat: "expense"

;; Untagged, so ignored entirely — this is how you exclude a personal-use asset.
2020-01-01 open Assets:Vehicle            PKR
```

Classification is **exact**: tagging `Assets:Bank` does not pull in
`Assets:Bank:Savings`. Tag each account you want included. Metadata works in
`include`d files just as well as in the root ledger.

Because the tags live in the ledger, your configuration travels with the data it
describes and cannot drift out of step with it.

### 2. Record metal prices

Prices come from ordinary `price` directives in your own ledger. `GLDTOLA` and
`SLVTOLA` are understood out of the box as prices **per tola**:

```beancount
2026-01-01 price GLDTOLA  280000.00 PKR
2026-01-01 price SLVTOLA    3300.00 PKR
```

You only need a directive when the price actually moves — a day with no price of
its own reuses the last known price. A price older than 90 days is still used but
is flagged as stale.

To quote per gram, or to use different symbols, declare them explicitly. The unit
is required, never guessed:

```beancount
2020-01-01 custom "fava-extension" "beancount_zakat.fava_extension" "{
  'metal_commodities': {'XAUGRAM': ['gold', 'gram'],
                        'XAGGRAM': ['silver', 'gram']},
}"
```

If no price exists at or before the report date, the report says so prominently.
Missing valuation data never becomes a silent zero.

### 3. Register the dashboard

```beancount
2020-01-01 custom "fava-extension" "beancount_zakat.fava_extension" "{}"
```

The module path is `beancount_zakat.fava_extension`, not `beancount_zakat`.

A complete, entirely synthetic example lives in
[`examples/ledger/main.beancount`](examples/ledger/main.beancount), covering
assets, a liability, payments and a refund, gold and silver prices, a holding in
another commodity, `include`d files, a nisab break and recovery, and a quiet tail
with no recent transactions.

## The Fava dashboard

```bash
fava examples/ledger/main.beancount
```

Then open the **Zakat** report. Six tabs:

1. **Overview** — side-by-side gold and silver cards showing lifetime liability,
   payments and the signed balance; data-quality status; accounts in scope.
2. **Yearly Summary** — one row per Hijri year, reconciling exactly to the totals.
3. **Wealth & Nisab** — two charts: a **stacked** one showing how net wealth is
   distributed across your accounts, then net wealth on its own against both
   moving thresholds. Plus the full wealth timeline, every change to either nisab
   with the price behind it, and account composition.
4. **Calculation Detail** — gold and silver in separate, switchable sections.
   Each carries one chart of net wealth against **that one** threshold with the
   below-nisab stretches shaded, a **hawl timeline** showing every marginal slice
   as a band (green = complete, amber = running but short of a year, grey = reset
   by falling below the nisab), and a table giving the nisab **range** in force
   during each period plus a plain-language reason.
5. **Payments** — signed payment detail with a running total.
6. **About Zakat & Methodology** — what zakat is, how this tool models it, and
   where its choices are choices rather than consensus.

Notes:

- **Fava's time filter sets the report cutoff.** A time filter ending 2026-10-01
  gives you the position as at 2026-10-01, inclusive. The filter moves the *end*
  only; the timeline always starts at inception, because hawl has to be measured
  from when wealth was actually acquired. With no filter the cutoff is today.
- **One chart stacks by account**, the first on Wealth & Nisab. Every tagged
  account is its own band: anything held stacks up from the zero line, anything
  owed hangs below it, so the gap between the two fronts is net zakatable wealth,
  with the net line drawn over the top. An overdrawn asset sits on the negative
  side too. Each account keeps the same colour wherever it appears. The nisab is
  deliberately absent here — a stack front is a gross figure, and the threshold
  applies to the net. Beyond eight accounts a side the smallest are pooled into a
  single *Other* band; the tables stay complete.
- **Every other chart is net wealth against a threshold** — both nisabs on Wealth
  & Nisab, one per basis on Calculation Detail with the hawl resets shaded.
- **Charts are interactive.** Switch any account or line off and what is left
  re-stacks and rescales; narrow the window with the date control at the top right
  (presets, or explicit from/to dates); hover — or tap, on a touch screen —
  anywhere on the plot for a crosshair readout of everything visible at that date,
  including the running stack total. The charts are drawn to fit the window, so
  they use the width the screen offers and stay legible on a phone.
- **Charts are presentation only.** The bands are the same balances the
  calculation used, drawn as their parts; nothing on a chart feeds back into a
  figure.
- **Works offline and with JavaScript off.** Charts are rendered by the server as
  images and re-drawn in the browser only to make them interactive. Nothing is
  fetched from a CDN.
- **It looks like Fava, because Fava styles it.** The report inherits Fava's
  theme rather than approximating it, including the light/dark choice you make in
  Fava itself, even when that contradicts your operating system.
- Tabs are keyboard-navigable, and the selected tab is kept in the URL so it can
  be bookmarked, linked to, and reached with the browser's back button.
- No status is signalled by colour alone: every chip carries a word and a glyph.

### Screenshots

Captured from a live Fava server running the example ledger; the full set for
every tab, on desktop and mobile, in both themes, is in
[`docs/screenshots/`](docs/screenshots).

| | |
|---|---|
| ![Overview](docs/screenshots/desktop-light-overview.png) | ![Wealth & Nisab](docs/screenshots/desktop-dark-wealth.png) |
| Overview, light theme | Wealth & Nisab, dark theme |
| ![Calculation Detail](docs/screenshots/desktop-light-detail.png) | ![Payments](docs/screenshots/desktop-dark-payments.png) |
| Calculation Detail — per-basis chart and hawl timeline | Payments, dark theme |

## The CLI

The CLI does **not** require Fava.

```bash
beancount-zakat examples/ledger/main.beancount
```

```
usage: beancount-zakat [-h] [--as-of YYYY-MM-DD] [--csv PATH]
                       [--basis {both,gold,silver}] [--width N] [--quiet]
                       [--version]
                       ledger
```

| Option | Meaning |
|---|---|
| `--as-of YYYY-MM-DD` | Report cutoff. Default today. Nothing later affects the result. |
| `--csv PATH` | Also write the CSV set to a directory, or a `.zip` if `PATH` ends in `.zip`. |
| `--basis` | Which detail tables to print. |
| `--width N` | Force an output width instead of detecting the terminal. |
| `--quiet` | Summary and reconciliation only. |

Output degrades gracefully on narrow terminals — below about 100 columns tables
become stacked label/value blocks rather than being truncated.

Examples:

```bash
# Position at a historical date, reproducibly
beancount-zakat ledger.beancount --as-of 2024-06-01

# Just the headline figures
beancount-zakat ledger.beancount --quiet

# Full report plus a CSV bundle
beancount-zakat ledger.beancount --csv ./zakat-2026.zip
```

**Exit codes:** `0` success (warnings may still be printed) · `1` a validation
error makes a result untrustworthy · `2` usage error, or the ledger could not be
loaded.

## CSV export

`--csv PATH` writes seven files. Schema details in
[`docs/csv-schema.md`](docs/csv-schema.md).

| File | Contents |
|---|---|
| `metadata.csv` | Report-level facts, both bases' headline figures, assumptions |
| `warnings.csv` | Every validation finding, with severity |
| `nisab_history.csv` | Every change to either threshold, with the price behind it |
| `yearly_summary.csv` | One row per Hijri reporting year |
| `detail_gold.csv` | Gold marginal slices and holding periods |
| `detail_silver.csv` | Silver marginal slices and holding periods |
| `payments.csv` | Signed payment detail with a running total |

Monetary columns are **exact decimal strings** — no grouping, no currency symbol,
no locale — so a spreadsheet reads them as numbers rather than text. UTF-8,
RFC 4180 line endings, stable column names and row order.

From the dashboard, the Yearly Summary, Calculation Detail and Payments tabs each
offer a CSV download, plus a "Download everything (ZIP)" link.

## Configuration

Account roles come from ledger metadata. Everything else is optional, has a
sensible default, and is set on the `fava-extension` directive.

| Option | Default | Meaning |
|---|---|---|
| `zakat_rate` | `0.025` | Write `'2.5%'` or `0.025`. A bare number above 1 is rejected, not guessed. |
| `gold_nisab_grams` | `87.48` | Gold nisab weight |
| `silver_nisab_grams` | `612.36` | Silver nisab weight |
| `nisab_gold_tola` / `nisab_silver_tola` | `7.5` / `52.5` | Same thing in tola |
| `metal_commodities` | `GLDTOLA`, `SLVTOLA` per tola | `{symbol: [metal, unit]}`, unit is `tola` or `gram` |
| `price_staleness_days` | `90` | When to flag a carried-forward price as stale |

**Precedence**, lowest to highest: built-in defaults → ledger `Open` metadata →
`fava-extension` options and CLI flags.

Account roles are merged as a **union, independently per role**, so declaring one
role never suppresses discovery of another. An account claimed by two roles
produces a warning, and the higher-precedence source wins.

Unknown option keys, invalid roles, bad account names, negative or absurd rates,
and ambiguous commodity units are all reported rather than silently ignored.

## How the calculation works

The full step-by-step is on the dashboard's **About Zakat & Methodology** tab.
In brief:

1. Replay postings into a net-zakatable-wealth timeline.
   Net wealth is `sum(assets) + sum(liabilities)` — liabilities carry their
   natural negative Beancount sign, so debt is subtracted by plain addition.
2. Take a snapshot on every posting date **and** every relevant price date, so
   holdings are re-valued when prices move.
3. Every distinct positive wealth level becomes a **marginal slice**
   (`level − previous level`) with its own independent hawl.
4. A slice's period runs while wealth stays at or above that slice's level **and**
   total wealth stays at or above the nisab. Dropping below either ends the
   period; recovery starts a fresh one. Each period is judged on its own, and
   elapsed time is never carried across a reset.
5. Once a period reaches one full lunar year (`354.36708` days):

   ```
   zakat_due = marginal_amount × elapsed_lunar_years × 2.5%
   ```

   **This tool's position** is that the hawl is the *condition* that makes wealth
   zakatable, not a restriction limiting liability to whole years; once met,
   liability accrues in proportion to the time held. A slice held 3.34 lunar
   years is charged 8.35% of that slice.
6. Steps 3–5 run again, unchanged, against the other basis's nisab.
7. Historical liability is computed first; payments are subtracted afterwards to
   give a signed balance.

> **This layered pro-rata model is a choice, not a scholarly consensus.** Most
> zakat guidance has you value your wealth once a year on your zakat anniversary
> and pay 2.5% of it, treating the hawl as a gate that either opens or does not.
> This tool treats it as a gate *and* accrues liability in proportion to elapsed
> time thereafter, which suits paying accumulated zakat in Ramadan across however
> many years are outstanding. Discuss the *method*, not just the numbers, with
> your scholar.

**The nisab is a moving threshold.** It tracks the metal price, so it differs on
almost every year of your history. No single figure is presented anywhere as "the"
nisab: the dashboard lists every change with the price behind it, and the
calculation detail shows the range in force during each period.

Two notions of "lunar year" are kept strictly apart. The calculation measures
hawl only with the mean lunar year of `354.36708` days; the Umm al-Qura calendar
is used only to *label* reporting years, so it can never move a zakat amount.
Labelling covers Hijri 1343–1500 (1924-08-01 to 2077-11-16); a date outside that
range is refused rather than silently approximated.

## Assumptions and limitations

- **The layered pro-rata model is this tool's construction**, not a consensus
  position — see the note above and the About tab.
- **Each slice keeps its own hawl.** Newly acquired wealth begins a fresh holding
  period rather than being folded into an existing one.
- **No personal zakat anniversary is modelled.** The tool works from continuous
  holding periods, not from a date you nominate each year.
- **Gram equivalents for the nisab vary between institutions.** This tool uses
  87.48 g / 612.36 g (7.5 / 52.5 tola); others publish 85 g / 595 g, roughly a 3%
  difference. Override them if your authority differs.
- **Payments are not allocated per basis.** A payment reduces both balances,
  because it is one real-world event viewed two ways.
- **Prices are taken on trust from your ledger.** A stale price is flagged but
  still used; a missing one is an error, never a silent zero.

## Troubleshooting

**"No accounts are marked for zakat"** — no `Open` directive carries
`beancount_zakat` metadata. Note the value must be a quoted string:
`beancount_zakat: "asset"`, not `beancount_zakat: asset`.

**"No gold price is available"** — add a `price` directive for `GLDTOLA` (or
whatever symbol you configured) dated on or before your report date. Prices dated
*after* the cutoff are deliberately ignored.

**"No price for XYZ ... could not be valued"** — a tagged account holds a
commodity with no price path to your operating currency. Add a `price` directive
for it.

**A slice shows "not running" for a long stretch** — wealth was below that
basis's nisab, so the hawl was reset and none of that time counts. The shaded
band on the Calculation Detail chart shows exactly when. This is per basis: a
reset under gold says nothing about silver.

**The report is slow on a large ledger** — the work grows with the number of
distinct wealth levels rather than the number of transactions, so a long history
of small changes is the expensive case. A synthetic 1,500-transaction ledger
takes about two seconds. The dashboard caches the result, so only the first
render after an edit pays for it.

**Registration fails / the report does not appear** — check the module path is
`beancount_zakat.fava_extension`, and that `pip install 'beancount-zakat[fava]'`
put the package on the same interpreter that runs Fava.

## Privacy

- **Nothing leaves your machine.** No network calls at runtime, no telemetry, no
  CDN — charts are drawn from data already on the page.
- **Nothing is written unless you ask.** The only files produced are the CSV
  exports you request.
- **No ledger content in this repository.** Every fixture and example is
  synthetic.
- **No server tracebacks in the browser.** Errors go to the Fava log; the page
  shows a safe, actionable message instead.

## How this is tested

Every release is checked on Python 3.10 through 3.13, on Linux, macOS and
Windows. Beyond ordinary unit tests, the suite pins the things that would be easy
to break quietly:

- **The figures reconcile.** Yearly rows add up to the lifetime total exactly,
  not approximately, and the charts add up to the same net wealth the calculation
  used.
- **Randomised histories.** The invariants are checked against thousands of
  randomly generated wealth timelines, not only the ones somebody thought to
  write by hand.
- **Whole ledgers**, with included files, price directives, refunds, nisab breaks
  and long quiet stretches.
- **The engine really does run without Fava**, which is checked rather than
  asserted.
- **The dashboard in a real browser**, desktop and mobile, in both themes: no
  errors, no broken layout, keyboard navigation and chart interaction exercised
  end to end. The screenshots above come from that run.

## Contributing

Bug reports and pull requests are welcome.

```bash
git clone https://github.com/WildeBeast2521/beancount-zakat
cd beancount-zakat
python -m venv .venv && . .venv/bin/activate
pip install -r requirements-dev.txt
pytest
```

**Never attach a real ledger** to an issue or a pull request. Every fixture here
is synthetic; reduce the problem to made-up account names and round numbers.

[CONTRIBUTING.md](CONTRIBUTING.md) has the full workflow.
[`docs/architecture.md`](docs/architecture.md) explains how the package is put
together and why the calculation makes the choices it does — read it before
changing anything in `src/`. [SECURITY.md](SECURITY.md) covers reporting a
vulnerability privately.

## License

`beancount-zakat` is released under the [MIT](LICENSE) license.

## Disclaimer

> This tool is for informational and record-keeping purposes only. Zakat rulings
> can vary by school of jurisprudence, asset type, debt treatment, personal
> circumstances, locality, and scholar. The software may contain errors and does
> not constitute religious, legal, tax, accounting, or financial advice. Verify
> the inputs and calculations and consult a qualified Islamic scholar and
> appropriate professional before acting.
