1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
//! Auto-generate Open directives for accounts used without explicit open.
use crate::types::{
DirectiveData, DirectiveWrapper, OpenData, PluginInput, PluginOp, PluginOutput,
};
use super::super::NativePlugin;
/// Plugin that auto-generates Open directives for accounts used without explicit open.
pub struct AutoAccountsPlugin;
impl NativePlugin for AutoAccountsPlugin {
fn name(&self) -> &'static str {
"auto_accounts"
}
fn description(&self) -> &'static str {
"Auto-generate Open directives for used accounts"
}
/// Synthesizes `Open` directives the early validator needs to
/// see — must run pre-booking to suppress spurious E1001 errors
/// on accounts the plugin will auto-create.
fn is_synth(&self) -> bool {
true
}
fn process(&self, input: PluginInput) -> PluginOutput {
use std::collections::{HashMap, HashSet};
let mut opened_accounts: HashSet<String> = HashSet::new();
let mut account_first_use: HashMap<String, String> = HashMap::new(); // account -> earliest date
// First pass: find all open directives and EARLIEST use of each account
// (directives may not be in date order in the input)
for wrapper in &input.directives {
match &wrapper.data {
DirectiveData::Open(data) => {
opened_accounts.insert(data.account.clone());
}
DirectiveData::Transaction(txn) => {
for posting in &txn.postings {
account_first_use
.entry(posting.account.clone())
.and_modify(|existing| {
if wrapper.date < *existing {
existing.clone_from(&wrapper.date);
}
})
.or_insert_with(|| wrapper.date.clone());
}
}
DirectiveData::Balance(data) => {
account_first_use
.entry(data.account.clone())
.and_modify(|existing| {
if wrapper.date < *existing {
existing.clone_from(&wrapper.date);
}
})
.or_insert_with(|| wrapper.date.clone());
}
DirectiveData::Pad(data) => {
account_first_use
.entry(data.account.clone())
.and_modify(|existing| {
if wrapper.date < *existing {
existing.clone_from(&wrapper.date);
}
})
.or_insert_with(|| wrapper.date.clone());
account_first_use
.entry(data.source_account.clone())
.and_modify(|existing| {
if wrapper.date < *existing {
existing.clone_from(&wrapper.date);
}
})
.or_insert_with(|| wrapper.date.clone());
}
_ => {}
}
}
// Generate open directives for accounts without explicit open
// Sort accounts for deterministic ordering (matches Python beancount behavior)
let mut accounts_to_open: Vec<_> = account_first_use
.iter()
.filter(|(account, _)| !opened_accounts.contains(*account))
.collect();
accounts_to_open.sort_by_key(|(account, _)| *account);
// Start with Keep ops for every input directive (preserves spans).
let mut ops: Vec<PluginOp> = (0..input.directives.len()).map(PluginOp::Keep).collect();
// Insert synthesized Open directives for accounts without explicit open.
for (index, (account, date)) in accounts_to_open.into_iter().enumerate() {
ops.push(PluginOp::Insert(DirectiveWrapper {
directive_type: "open".to_string(),
date: date.clone(),
filename: Some("<auto_accounts>".to_string()),
lineno: Some(index as u32), // Use index as lineno for deterministic sorting
data: DirectiveData::Open(OpenData {
account: account.clone(),
currencies: vec![],
booking: None,
metadata: vec![],
}),
}));
}
// Final ordering is the loader's responsibility — it re-sorts
// directives after the plugin pass.
PluginOutput {
ops,
errors: Vec::new(),
}
}
}