Skip to content

Postings

This document specifies the posting model for plain text accounting systems.

Definition

A Posting is a single line within a transaction that records a change to an account's balance. Each posting specifies an account and optionally an amount, cost, and price.

Posting Structure

Posting = {
  account: Account,
  amount: Amount?,
  cost: CostSpec?,
  price: PriceSpec?,
  metadata: Metadata
}

Components

Account

The account affected by this posting:

Assets:Checking
Expenses:Food:Groceries
Income:Salary

Amount

The quantity and commodity added or removed:

100.00 USD
-50 EUR
10 AAPL

Cost (Optional)

Acquisition cost for lot tracking:

{150.00 USD}
{150.00 USD, 2024-01-15}
{150.00 USD, 2024-01-15, "lot-1"}

Price (Optional)

Conversion price for this posting:

@ 1.08 USD
@@ 108 USD

Metadata

Key-value annotations:

2024-01-15 * "Purchase"
  Assets:Stock  10 AAPL {150 USD}
    broker: "Fidelity"
    order-id: "12345"
  Assets:Checking

Basic Syntax

Simple Posting

Assets:Checking  100.00 USD

With Cost

Assets:Stock  10 AAPL {150.00 USD}

With Price

Assets:Euro  100 EUR @ 1.08 USD

With Cost and Price

Assets:Stock  -10 AAPL {150.00 USD} @ 160.00 USD

Amount Inference

Single Elision

One posting per transaction may omit its amount:

2024-01-15 * "Salary"
  Assets:Checking   5000.00 USD
  Income:Salary                    ; Inferred: -5000.00 USD

Inference Rules

The inferred amount is calculated to balance the transaction:

inferred_amount = -sum(other_posting_weights)

Multiple Elisions

At most one posting may omit its amount:

2024-01-15 * "Invalid"
  Assets:Checking                  ; Error: multiple elisions
  Income:Salary

Weight Calculation

Purpose

The "weight" of a posting determines its contribution to transaction balance.

Weight Rules

Posting Type Weight Calculation
Amount only units
With price @ units × price
With total price @@ total_price
With cost {} units × cost
Cost + price units × cost

Examples

100 USD                    ; Weight: 100 USD
100 EUR @ 1.08 USD         ; Weight: 108 USD
100 EUR @@ 108 USD         ; Weight: 108 USD
10 AAPL {150 USD}          ; Weight: 1500 USD
10 AAPL {150 USD} @ 160    ; Weight: 1500 USD (cost used)

Cost Specifications

Augmentation (Buying)

When adding to inventory, cost defines the lot:

2024-01-15 * "Buy stock"
  Assets:Stock    10 AAPL {150.00 USD}
  Assets:Cash    -1500.00 USD

Creates lot: 10 AAPL at 150 USD each, dated 2024-01-15

Reduction (Selling)

When removing from inventory, cost filters matching lots:

2024-03-15 * "Sell stock"
  Assets:Stock   -10 AAPL {150.00 USD}
  Assets:Cash    1600.00 USD

Reduces the lot matching 150 USD cost.

Cost Spec Components

Component Syntax Purpose
Amount 150.00 USD Per-unit cost
Date 2024-01-15 Acquisition date
Label "lot-1" User identifier
Merge * Average cost

Cost Spec Examples

{150 USD}                           ; Cost only
{2024-01-15}                        ; Date only
{"lot-1"}                           ; Label only
{150 USD, 2024-01-15}               ; Cost and date
{150 USD, 2024-01-15, "lot-1"}      ; All components
{*}                                 ; Merge at average
{}                                  ; Match any lot

Price Specifications

Per-Unit Price

100 EUR @ 1.08 USD

Meaning: Each EUR is worth 1.08 USD

Total Price

100 EUR @@ 108 USD

Meaning: All 100 EUR together are worth 108 USD

Price Recording

Prices create implicit price database entries:

2024-01-15 * "Exchange"
  Assets:USD   108 USD
  Assets:EUR  -100 EUR @ 1.08 USD

Records: 2024-01-15: EUR = 1.08 USD

Posting Flags

Transaction Flag Inheritance

Postings inherit the transaction's flag by default:

2024-01-15 * "Cleared transaction"
  Assets:Checking  100 USD     ; Inherits *
  Income:Salary                ; Inherits *

Per-Posting Flags

Some formats allow per-posting flags:

2024-01-15 * "Mixed"
  * Assets:Checking  100 USD   ; Cleared
  ! Income:Salary              ; Pending

Posting Order

Within Transaction

Postings are typically ordered: 1. Asset accounts first 2. Then liability accounts 3. Then equity accounts 4. Then income accounts 5. Then expense accounts last

Formatting Convention

2024-01-15 * "Paycheck"
  Assets:Checking     5000.00 USD    ; Asset first
  Expenses:Tax:Federal  500.00 USD   ; Expense
  Income:Salary                       ; Income last

Balance Checking

Transaction Balance

All postings MUST sum to zero:

sum(posting.weight for posting in transaction.postings) == 0

Tolerance

Small rounding differences may be tolerated:

2024-01-15 * "Currency exchange"
  Assets:USD    108.00 USD
  Assets:EUR   -100.00 EUR @ 1.0799 USD    ; Weight: 107.99
  ; Residual: 0.01 USD (within tolerance)

Balance Error

ERROR: Transaction does not balance
  --> ledger.beancount:42:1
   |
42 | 2024-01-15 * "Unbalanced"
   | ^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = residual: 0.50 USD
   = hint: add posting to balance transaction

Special Postings

Padding Posting

Generated by pad directive:

2024-01-01 pad Assets:Checking Equity:Opening-Balances

; Generates:
;   2024-01-01 * "Padding"
;     Assets:Checking         XXXX USD
;     Equity:Opening-Balances

Auto-Posting

Some plugins generate automatic postings:

; With auto-accounts plugin:
2024-01-15 * "Unknown payee"
  Expenses:Misc  100 USD         ; Auto-opened account
  Assets:Cash

Metadata on Postings

Syntax

2024-01-15 * "Purchase"
  Assets:Stock  10 AAPL {150 USD}
    settlement-date: 2024-01-17
    broker: "Fidelity"
  Assets:Cash  -1500 USD

Indentation

Posting metadata is indented further than the posting:

  Account  Amount
    key: value

Implementation Model

@dataclass
class Posting:
    account: str
    units: Optional[Amount] = None
    cost: Optional[CostSpec] = None
    price: Optional[Amount] = None
    flag: Optional[str] = None
    metadata: Dict[str, Any] = field(default_factory=dict)

    @property
    def weight(self) -> Amount:
        """Calculate the weight for balance checking."""
        if self.units is None:
            raise ValueError("Cannot compute weight of elided posting")

        if self.cost is not None:
            return Amount(
                self.units.number * self.cost.number_per,
                self.cost.currency
            )
        elif self.price is not None:
            return Amount(
                self.units.number * self.price.number,
                self.price.commodity
            )
        else:
            return self.units

Validation

Account Not Opened

ERROR: Account not opened
  --> ledger.beancount:42:3
   |
42 |   Assets:Unknown  100 USD
   |   ^^^^^^^^^^^^^^

Currency Not Allowed

ERROR: Currency not allowed for account
  --> ledger.beancount:42:20
   |
42 |   Assets:Checking  100 EUR
   |                        ^^^
   |
   = allowed: USD

Invalid Cost

ERROR: Cannot specify cost for currency
  --> ledger.beancount:42:20
   |
42 |   Assets:Cash  100 USD {1 EUR}
   |                        ^^^^^^^
   |
   = hint: costs are for commodities held at cost basis

Cross-Format Notes

Feature Beancount Ledger hledger
Cost syntax {100 USD} {=$100} {100 USD}
Price syntax @ 1.08 USD @ $1.08 @ 1.08 USD
Total price @@ 108 USD @@ $108 @@ 108 USD
Posting flags No Yes Yes
Metadata key: value ; key: value ; key: value
Multiple elisions No Yes Yes