pub struct Entry<const P: u8 = 2> { /* private fields */ }Expand description
An entry draft: what a caller submits to be recorded.
Builders are infallible: an invalid key, kind, account path, dimension or
memo is kept as a BuildError (see build_errors)
and reported when the draft is recorded. P is inferred from the amounts
or fixed by the type (Entry alone means Entry<2>).
use doubleentry::{Entry, amount, currency::EUR, error::Field};
use time::macros::date;
let draft: Entry = Entry::new("invoice-2026-0001", date!(2026 - 03 - 15))
.debit("Assets:Cash", amount!("1190.00"), EUR)
.credit("Income:Sales", amount!("1190.00"), EUR)
.memo("Rechnung 4711");
assert!(draft.fingerprint().is_ok());
let broken: Entry = Entry::new("", date!(2026 - 03 - 15)).debit("Assets::Cash", amount!("1.00"), EUR);
let fields: Vec<Field> = broken.build_errors().iter().map(|e| e.field).collect();
assert_eq!(fields, [Field::Key, Field::Posting(0)]);A hold action is not a builder choice (holds have their own constructors):
use doubleentry::{Entry, record::HoldAction};
let _ = Entry::<2>::new("k", time::macros::date!(2026 - 01 - 01))
.hold_action(HoldAction::Open { expires: None });Implementations§
Source§impl<const P: u8> Entry<P>
impl<const P: u8> Entry<P>
Sourcepub const MAX_POSTINGS: usize = 1024
pub const MAX_POSTINGS: usize = 1024
The most postings in one entry.
Sourcepub const MAX_DOCUMENTS: usize = 16
pub const MAX_DOCUMENTS: usize = 16
The most documents on one entry.
Sourcepub const MAX_MEMO_BYTES: usize = 4096
pub const MAX_MEMO_BYTES: usize = 4096
The most bytes of memo text.
Sourcepub fn new<K>(key: K, booking_date: Date) -> Self
pub fn new<K>(key: K, booking_date: Date) -> Self
A draft under an idempotency key, booked on a date.
Sourcepub fn value_date(self, date: Date) -> Self
pub fn value_date(self, date: Date) -> Self
With a value date (defaults to the booking date).
Sourcepub fn captured_at(self, at: CaptureTime) -> Self
pub fn captured_at(self, at: CaptureTime) -> Self
With the time the caller captured the entry.
Sourcepub fn memo(self, text: impl Into<String>) -> Self
pub fn memo(self, text: impl Into<String>) -> Self
With a memo: 1–4 096 bytes, tabs and line breaks allowed, no other
control character (an invalid memo is rejected as 108 InvalidMemo).
Its salt is supplied when the entry is recorded. The memo is the only
free-text field (see the module docs).
Sourcepub fn memo_salted(self, text: impl Into<String>, salt: MemoSalt) -> Self
pub fn memo_salted(self, text: impl Into<String>, salt: MemoSalt) -> Self
With a memo and the salt of its commitment.
Sourcepub fn debit<A>(self, account: A, amount: Amount<P>, currency: Currency) -> Self
pub fn debit<A>(self, account: A, amount: Amount<P>, currency: Currency) -> Self
With a debit posting.
Sourcepub fn credit<A>(
self,
account: A,
amount: Amount<P>,
currency: Currency,
) -> Self
pub fn credit<A>( self, account: A, amount: Amount<P>, currency: Currency, ) -> Self
With a credit posting.
Sourcepub fn open_hold<K>(key: K, booking_date: Date) -> Self
pub fn open_hold<K>(key: K, booking_date: Date) -> Self
A hold: a reservation of one amount from one account to another,
resolved later exactly once. Add exactly one debit and one credit of
the same amount and currency; on this draft they are Pending postings
(anything else is refused with code 140). Optionally it
expires.
The hold’s id is the id of its entry (Recorded::id),
which its resolution names. While it is open, its amount counts against
the balance limits of both accounts.
use doubleentry::{Entry, Layer, amount, date, currency::EUR};
let hold: Entry = Entry::open_hold("auth-4711", date!("2026-11-20"))
.expires(date!("2026-11-30"))
.debit("Liabilities:Wallets:alice", amount!("100.00"), EUR)
.credit("Liabilities:Card:clearing", amount!("100.00"), EUR);
assert!(hold.postings().iter().all(|p| p.get_layer() == Layer::Pending));Sourcepub fn expires(self, on: Date) -> Self
pub fn expires(self, on: Date) -> Self
The first day a hold may be expired (expire_hold)
and the first day it can no longer be posted. It must lie after the
hold’s booking date (code 141). Only a hold expires: on any other draft
this changes nothing.
use doubleentry::{Entry, date, record::HoldAction};
let hold: Entry = Entry::open_hold("auth-4711", date!("2026-11-20")).expires(date!("2026-11-30"));
assert_eq!(hold.get_hold(), Some(&HoldAction::Open { expires: Some(date!("2026-11-30")) }));
let entry: Entry = Entry::new("sale-1", date!("2026-11-20")).expires(date!("2026-11-30"));
assert_eq!(entry.get_hold(), None);Sourcepub fn post_hold<K>(
key: K,
booking_date: Date,
hold: EntryId,
amount: PostAmount<P>,
) -> Self
pub fn post_hold<K>( key: K, booking_date: Date, hold: EntryId, amount: PostAmount<P>, ) -> Self
Posts (captures) the hold hold: all of it, or part with the rest
released in the same entry. The draft carries no postings — the rule
core derives them from the hold: the two Pending legs released, then
the posted amount settled between the hold’s two accounts. A post must
be booked before the hold’s expiry date.
use doubleentry::{AccountKind, Entry, Journal, Layer, LedgerId, PostAmount, amount, date, currency::EUR};
let mut books = Journal::new(LedgerId::new("example.com/payments")?);
books.declare_currency(EUR, 2, date!("2026-01-01"))?;
books.open("Liabilities:Wallets:alice", AccountKind::Liability, date!("2026-01-01"))?;
books.open("Liabilities:Card:clearing", AccountKind::Liability, date!("2026-01-01"))?;
books.define_months(2026, 11..=11)?;
let hold = books.record(
Entry::open_hold("auth-4711", date!("2026-11-20"))
.debit("Liabilities:Wallets:alice", amount!("100.00"), EUR)
.credit("Liabilities:Card:clearing", amount!("100.00"), EUR),
)?;
let capture = books.record(Entry::post_hold(
"capture-4711",
date!("2026-11-22"),
hold.id,
PostAmount::Partial(amount!("60.00")),
))?;
let postings = books.entry(capture.id).map(|entry| entry.parts().postings.clone()).unwrap_or_default();
let legs: Vec<_> = postings.iter().map(|p| (p.layer, p.side, p.amount)).collect();
use doubleentry::Side::{Credit, Debit};
assert_eq!(legs, [
(Layer::Pending, Credit, amount!("100.00")), // the reservation released
(Layer::Pending, Debit, amount!("100.00")),
(Layer::Settled, Debit, amount!("60.00")), // the capture settled
(Layer::Settled, Credit, amount!("60.00")),
]);Sourcepub fn void_hold<K>(key: K, booking_date: Date, hold: EntryId) -> Self
pub fn void_hold<K>(key: K, booking_date: Date, hold: EntryId) -> Self
Voids (releases) the hold hold on any day from its booking date on,
even after its expiry date or after one of its accounts closed. The
core derives the two Pending release postings.
use doubleentry::{Entry, EntryId, date, record::HoldAction};
let hold = EntryId::from_bytes([7; 16]);
let void: Entry = Entry::void_hold("void-4711", date!("2026-11-21"), hold);
assert_eq!(void.get_hold(), Some(&HoldAction::Void { hold }));
assert!(void.postings().is_empty());Sourcepub fn expire_hold<K>(key: K, booking_date: Date, hold: EntryId) -> Self
pub fn expire_hold<K>(key: K, booking_date: Date, hold: EntryId) -> Self
Expires the hold hold: releases it like a void, booked on or after
its expiry date. Nothing expires by itself — a scheduler lists the
due holds (holds_due) and records an expiry for each.
use doubleentry::{Entry, EntryId, date, record::HoldAction};
let hold = EntryId::from_bytes([7; 16]);
let expiry: Entry = Entry::expire_hold("expire-4711", date!("2026-11-30"), hold);
assert_eq!(expiry.get_hold(), Some(&HoldAction::Expire { hold }));Sourcepub fn reverses(self, target: EntryId) -> Self
pub fn reverses(self, target: EntryId) -> Self
Marks the draft as the reversal of an earlier entry.
Sourcepub fn rate(self, rate: Rate) -> Self
pub fn rate(self, rate: Rate) -> Self
With an exchange rate; rates are kept sorted by (base, quote) and a second rate for the same pair replaces the first.
Sourcepub fn key(&self) -> Option<&IdempotencyKey>
pub fn key(&self) -> Option<&IdempotencyKey>
The idempotency key; None when it was invalid.
Sourcepub const fn booking_date(&self) -> Date
pub const fn booking_date(&self) -> Date
The booking date.
Sourcepub fn get_value_date(&self) -> Date
pub fn get_value_date(&self) -> Date
The value date (the booking date unless set).
Sourcepub const fn get_captured_at(&self) -> Option<CaptureTime>
pub const fn get_captured_at(&self) -> Option<CaptureTime>
The capture time.
Sourcepub const fn get_reverses(&self) -> Option<EntryId>
pub const fn get_reverses(&self) -> Option<EntryId>
The entry this one reverses.
Sourcepub const fn get_hold(&self) -> Option<&HoldAction<P>>
pub const fn get_hold(&self) -> Option<&HoldAction<P>>
The hold action.
Sourcepub fn build_errors(&self) -> &[BuildError]
pub fn build_errors(&self) -> &[BuildError]
The builder arguments that failed validation, in the order given.
Sourcepub fn encoded_len(&self) -> usize
pub fn encoded_len(&self) -> usize
The length of the entry leaf this draft becomes, without allocating,
to check against MAX_LEAF_BYTES
before submitting. Meaningful only without build errors. A hold
resolution’s derived postings (two to four) are not counted.
Sourcepub fn fingerprint(&self) -> Result<Fingerprint, BuildError>
pub fn fingerprint(&self) -> Result<Fingerprint, BuildError>
The draft’s Fingerprint: a tagged SHA-256 of the entry body as
submitted — accounts as given (path or handle), the memo as text, no
salt — so it depends on nothing but the draft.
§Errors
The first BuildError, while any exists.
Trait Implementations§
Source§impl<'de, const P: u8> Deserialize<'de> for Entry<P>
Available on crate feature serde only.
impl<'de, const P: u8> Deserialize<'de> for Entry<P>
serde only.Source§fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error>
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error>
impl<const P: u8> Eq for Entry<P>
impl<const P: u8> StructuralPartialEq for Entry<P>
Auto Trait Implementations§
impl<const P: u8> Freeze for Entry<P>
impl<const P: u8> RefUnwindSafe for Entry<P>
impl<const P: u8> Send for Entry<P>
impl<const P: u8> Sync for Entry<P>
impl<const P: u8> Unpin for Entry<P>
impl<const P: u8> UnsafeUnpin for Entry<P>
impl<const P: u8> UnwindSafe for Entry<P>
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> DeserializeOwned for Twhere
T: for<'de> Deserialize<'de>,
Source§impl<Q, K> Equivalent<K> for Q
impl<Q, K> Equivalent<K> for Q
Source§fn equivalent(&self, key: &K) -> bool
fn equivalent(&self, key: &K) -> bool
key and return true if they are equal.Source§impl<Q, K> Equivalent<K> for Q
impl<Q, K> Equivalent<K> for Q
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more