Skip to main content

datasynth_group/
standalone.rs

1//! Standalone single-process generation — Task 9.2.
2//!
3//! [`generate_standalone`] runs the full v5.0 pipeline — manifest +
4//! shards + aggregate — in one call, without spawning a subprocess
5//! per phase.  It is the in-process equivalent of:
6//!
7//! ```text
8//! datasynth-data group manifest --config group.yaml --out manifest.json
9//! datasynth-data group shard    --manifest manifest.json --shard $SHARD_ID --out ./out
10//! datasynth-data group aggregate --manifest manifest.json --shards-dir ./out --out ./out
11//! ```
12//!
13//! ...with manifest persistence at `out_dir/manifest.json` for
14//! debuggability and for parity with the multi-step CLI flow.
15//!
16//! # Memory caveat — orchestrator runs are heavy
17//!
18//! `run_shard` drives [`datasynth_runtime::EnhancedOrchestrator::generate`]
19//! end-to-end for every entity in the shard.  Each Mini-Acme entity
20//! peaks at **~17 GiB RSS** for ~15 minutes; running all five entities
21//! sequentially takes 60–90 minutes on a single host.
22//!
23//! When [`StandaloneOptions::parallel_shards = true`] (the default),
24//! the driver uses [`rayon`] to schedule shards concurrently.  Peak
25//! RSS scales linearly with the number of shards in flight — N shards
26//! × 17 GiB ≈ 17·N GiB.  This is fine on the XXL Azure VM (256 GiB)
27//! but will OOM a 32 GiB workstation in seconds.  The associated
28//! `tests/standalone_e2e.rs` is `#[ignore]`d for exactly this reason —
29//! mirror the pattern in [`crate::shard::runner::run_shard`]
30//! integration tests.
31//!
32//! For the determinism harness (`tests/property/determinism.rs`)
33//! callers should pass `parallel_shards: false` so two runs over the
34//! same input produce byte-identical archives without the rayon
35//! scheduler's non-deterministic interleaving in flight (writes to
36//! disk are still deterministic per-shard because the runner's per-
37//! entity output writer is sync, but the scheduler may flush in a
38//! different order).
39//!
40//! # File layout
41//!
42//! After a successful run:
43//!
44//! ```text
45//! {out_dir}/
46//!   ├── manifest.json
47//!   ├── entities/
48//!   │   ├── ENTITY_A/        ← per-shard runner output (verbatim)
49//!   │   ├── ENTITY_B/
50//!   │   └── ...
51//!   ├── shard_summary.json   ← from each run_shard call (overwrites
52//!   │                         per-shard; the last shard's summary
53//!   │                         persists.  Each ShardSummary is also
54//!   │                         captured in StandaloneSummary.shard_summaries)
55//!   ├── consolidated/        ← from run_aggregate (Chunk 6/7/8 outputs)
56//!   └── ic_eliminations/     ← from run_aggregate (coverage report)
57//! ```
58//!
59//! Note: `shard_summary.json` is not race-free across shards — the
60//! runner writes it per-shard at `{out_dir}/shard_summary.json`, which
61//! means the last shard wins.  This is a known v5.0 limitation; the
62//! per-shard summaries are reliably available in
63//! [`StandaloneSummary::shard_summaries`].
64
65use std::fs;
66use std::path::{Path, PathBuf};
67
68use rayon::prelude::*;
69use serde::{Deserialize, Serialize};
70
71use crate::aggregate::driver::{run_aggregate, AggregateOptions, AggregateSummary};
72use crate::config::GroupConfig;
73use crate::errors::{GroupError, GroupResult};
74use crate::manifest::builder::build_manifest;
75use crate::shard::runner::{run_shard_with_opening_balances, ShardSummary};
76
77// ── Public types ──────────────────────────────────────────────────────────────
78
79/// Knobs the caller can supply to tune [`generate_standalone`]
80/// behaviour.
81///
82/// All fields default to "sensible production": no prior period,
83/// fail-fast on missing shards, parallel shard execution.  Override
84/// `parallel_shards = false` for determinism harnesses.
85#[derive(Debug, Clone)]
86pub struct StandaloneOptions {
87    /// Forwarded verbatim to
88    /// [`crate::aggregate::driver::AggregateOptions::prior_period_aggregate`].
89    pub prior_period_aggregate: Option<PathBuf>,
90    /// Forwarded verbatim to
91    /// [`crate::aggregate::driver::AggregateOptions::tolerate_missing_shards`].
92    pub tolerate_missing_shards: bool,
93    /// When `true`, run shards in parallel via [`rayon`].  Defaults to
94    /// `true`.  Set to `false` for determinism harnesses (sequential
95    /// shard execution removes the scheduler's interleaving from the
96    /// output trace).
97    pub parallel_shards: bool,
98    /// Forwarded verbatim to
99    /// [`crate::aggregate::driver::AggregateOptions::cgu_test_inputs`]
100    /// — the per-period CGU goodwill impairment test inputs.  Empty
101    /// by default; an engagement that wants annual IAS 36 § 10
102    /// impairment testing supplies one entry per CGU under test.
103    pub cgu_test_inputs: Vec<crate::aggregate::cgu_impairment::CguTestInputs>,
104    /// **v5.5.2** — Forwarded verbatim to
105    /// [`crate::aggregate::driver::AggregateOptions::cpi_series_by_currency`].
106    /// Per-currency CPI series for IAS 29 § 12 indexed restatement.
107    /// Empty by default — single-period engagements without
108    /// hyperinflationary subsidiaries see no behaviour change.  When
109    /// the chain runner forwards a non-empty map, every period applies
110    /// the same series; vary by period at the library level if the
111    /// engagement requires per-period CPI overrides.
112    pub cpi_series_by_currency: std::collections::BTreeMap<
113        String,
114        datasynth_core::models::hyperinflation::GeneralPriceIndex,
115    >,
116    /// **v5.3** — Per-entity opening-balance carryover from a prior
117    /// period.  When non-empty, the shard runner pre-populates each
118    /// matching entity's `ShardContext.opening_balances` so the
119    /// orchestrator's Phase 3b uses these BS positions instead of
120    /// generating fresh openings.  Empty by default — single-period
121    /// engagements see no behaviour change.
122    ///
123    /// **v5.31 C2 (#157)** — auto-populated by
124    /// [`generate_standalone_chain`] from the prior period's
125    /// `entities/{code}/period_close/trial_balances.json` via
126    /// [`crate::aggregate::opening_balance::read_prior_period_closing_tbs`]
127    /// followed by
128    /// [`datasynth_generators::balance::project_closing_to_opening`].
129    /// The projection (vs the legacy `extract_opening_balances`) is
130    /// what absorbs prior-period net income into Retained Earnings —
131    /// the orchestrator emits `Adjusted` TBs (not `PostClosing`), so
132    /// dropping P&L without absorbing it would silently lose the
133    /// period's earnings on the chain hand-off.
134    ///
135    /// Callers using `generate_standalone` directly can populate this
136    /// manually when they want to drive multi-period continuity
137    /// without the chain helper.
138    pub entity_opening_balances: std::collections::BTreeMap<
139        String,
140        Vec<datasynth_core::models::balance::EntityOpeningBalance>,
141    >,
142    /// **v5.31 C2 (#157)** — accounting framework used to project the
143    /// prior period's closing TB onto next-period opening balances.
144    /// Selects which account code holds Retained Earnings (US GAAP
145    /// `"3200"`, SKR03/04 `"2970"`, IFRS varies).  Only consulted by
146    /// [`generate_standalone_chain`] when computing carryover; the
147    /// per-period generation itself reads framework off the config.
148    /// Defaults to `"us_gaap"`.  Must match how the prior period was
149    /// generated — mismatches mean net income lands in the wrong
150    /// account.
151    pub closing_to_opening_framework: String,
152}
153
154impl Default for StandaloneOptions {
155    fn default() -> Self {
156        Self {
157            prior_period_aggregate: None,
158            tolerate_missing_shards: false,
159            parallel_shards: true,
160            cgu_test_inputs: Vec::new(),
161            entity_opening_balances: std::collections::BTreeMap::new(),
162            cpi_series_by_currency: std::collections::BTreeMap::new(),
163            closing_to_opening_framework: "us_gaap".to_string(),
164        }
165    }
166}
167
168/// Top-level result returned by [`generate_standalone`].
169///
170/// `shard_summaries` is one entry per shard in
171/// `manifest.shard_plan.shards`, in the order the manifest declares
172/// them (rayon scheduling does not affect the result ordering — we
173/// sort post-collect).
174#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
175pub struct StandaloneSummary {
176    /// Path to the persisted manifest at `{out_dir}/manifest.json`.
177    pub manifest_path: PathBuf,
178    /// Per-shard summaries from `run_shard`, one per shard in the
179    /// manifest's [`crate::manifest::shard_plan::ShardPlan`].
180    pub shard_summaries: Vec<ShardSummary>,
181    /// Aggregate-phase summary from [`run_aggregate`].
182    pub aggregate: AggregateSummary,
183}
184
185// ── Public API ────────────────────────────────────────────────────────────────
186
187/// Drive the full v5.0 pipeline (manifest → shards → aggregate) in one
188/// call.
189///
190/// 1. Build the manifest from `cfg`.
191/// 2. Persist it to `{out_dir}/manifest.json`.
192/// 3. Run every shard in `manifest.shard_plan.shards` (parallel or
193///    sequential per `opts.parallel_shards`), writing per-entity
194///    archives under `{out_dir}/entities/{code}/`.
195/// 4. Run the aggregate-phase driver on `out_dir` (which now contains
196///    every shard's output) and emit consolidated FS artefacts.
197/// 5. Return a [`StandaloneSummary`] linking the manifest, every
198///    shard's summary, and the aggregate summary.
199///
200/// # Errors
201///
202/// - [`GroupError::Manifest`] / [`GroupError::Config`] propagated from
203///   [`build_manifest`].
204/// - [`GroupError::Shard`] propagated from any `run_shard` failure
205///   (orchestrator construction, generation, or per-entity output).
206/// - [`GroupError::Aggregate`] / [`GroupError::Io`] /
207///   [`GroupError::Serde`] propagated from [`run_aggregate`].
208/// - [`GroupError::Io`] if the manifest cannot be persisted.
209pub fn generate_standalone(
210    cfg: &GroupConfig,
211    out_dir: &Path,
212    opts: &StandaloneOptions,
213) -> GroupResult<StandaloneSummary> {
214    // ── 1. Manifest ─────────────────────────────────────────────────
215    let manifest = build_manifest(cfg)?;
216
217    // ── 2. Persist manifest at out_dir/manifest.json ────────────────
218    fs::create_dir_all(out_dir).map_err(GroupError::Io)?;
219    let manifest_path = out_dir.join("manifest.json");
220    let mut manifest_json = serde_json::to_string_pretty(&manifest)?;
221    manifest_json.push('\n');
222    fs::write(&manifest_path, manifest_json).map_err(GroupError::Io)?;
223
224    // ── 3. Run every shard ──────────────────────────────────────────
225    //
226    // Capture the shard ids in declaration order from
227    // `shard_plan.shards`, then dispatch via rayon (parallel) or a
228    // plain map (sequential).  Both code paths materialise into a
229    // `Vec<ShardSummary>` in declaration order so callers receive a
230    // deterministic ordering regardless of scheduler interleaving.
231    let shard_ids: Vec<String> = manifest
232        .shard_plan
233        .shards
234        .iter()
235        .map(|s| s.shard_id.clone())
236        .collect();
237
238    let shard_summaries: Vec<ShardSummary> = if opts.parallel_shards {
239        // rayon's par_iter preserves the input order in the collected
240        // Vec, so the result is still declaration-ordered.
241        shard_ids
242            .par_iter()
243            .map(|sid| {
244                run_shard_with_opening_balances(
245                    &manifest,
246                    sid,
247                    out_dir,
248                    &opts.entity_opening_balances,
249                )
250            })
251            .collect::<GroupResult<Vec<_>>>()?
252    } else {
253        let mut out: Vec<ShardSummary> = Vec::with_capacity(shard_ids.len());
254        for sid in &shard_ids {
255            out.push(run_shard_with_opening_balances(
256                &manifest,
257                sid,
258                out_dir,
259                &opts.entity_opening_balances,
260            )?);
261        }
262        out
263    };
264
265    // ── 4. Aggregate phase ──────────────────────────────────────────
266    //
267    // The runner writes every per-entity archive under
268    // `{out_dir}/entities/{code}/`, so `shards_dir == out_dir` for the
269    // aggregate driver.
270    let agg_opts = AggregateOptions {
271        prior_period_aggregate: opts.prior_period_aggregate.clone(),
272        tolerate_missing_shards: opts.tolerate_missing_shards,
273        cgu_test_inputs: opts.cgu_test_inputs.clone(),
274        cpi_series_by_currency: opts.cpi_series_by_currency.clone(),
275    };
276    let aggregate = run_aggregate(&manifest, out_dir, out_dir, &agg_opts)?;
277
278    Ok(StandaloneSummary {
279        manifest_path,
280        shard_summaries,
281        aggregate,
282    })
283}
284
285// ── Multi-period chain helper (v5.3) ──────────────────────────────────────────
286
287/// One period in a multi-period engagement chain.  Carries the
288/// period-specific config + the output subdirectory the chain
289/// runner uses to scope this period's archive.
290///
291/// `out_subdir` is interpreted relative to the chain's `base_out_dir`
292/// (the second parameter of [`generate_standalone_chain`]) — typically
293/// something like `"2024_Q1"` or `"period_001"`.  Subdirs must be
294/// unique within a chain or [`generate_standalone_chain`] returns
295/// [`GroupError::Config`].
296#[derive(Debug, Clone, Serialize, Deserialize)]
297pub struct PeriodChainSpec {
298    /// Period config (start_date / length / fiscal_year_end) for this
299    /// period.  Replaces [`GroupConfig::period`] when the chain runner
300    /// invokes the per-period [`generate_standalone`] call.
301    pub period: crate::config::PeriodConfig,
302    /// Output subdirectory under `base_out_dir`.  Must be unique
303    /// within the chain.
304    pub out_subdir: String,
305}
306
307/// Drive the full v5.0 pipeline once per period, chaining the
308/// aggregate-phase prior-period plumbing automatically.
309///
310/// For each period in `periods`:
311///
312/// 1. Clone `base_cfg`, override its `.period` with the spec's
313///    period config.  All other fields (ownership, fx, audit, tax,
314///    cgu, output) flow through unchanged.
315/// 2. Compute `out_dir = base_out_dir.join(spec.out_subdir)`.
316/// 3. For period 0, use `opts` as-is — caller's
317///    `prior_period_aggregate` is preserved (lets engagements seed
318///    from an externally produced archive).
319/// 4. For periods 1..N, override `opts.prior_period_aggregate` to
320///    point to the previous period's `out_dir` so opening NCI / CTA /
321///    equity-method values flow forward automatically.
322/// 5. Invoke [`generate_standalone`] and append the [`StandaloneSummary`]
323///    to the result vector.
324///
325/// On per-period failure the chain stops; outputs already written for
326/// prior periods are preserved on disk.
327///
328/// # Companion to [`crate::aggregate::run_aggregate_chain`]
329///
330/// `run_aggregate_chain` (PR #150) handles the same prior-period
331/// stitching at the aggregate-only layer (i.e. when shard archives
332/// already exist on disk).  This helper extends the pattern to the
333/// full pipeline — it generates the data per period **and** chains
334/// the aggregate plumbing.
335///
336/// # Errors
337///
338/// - [`GroupError::Config`] if `periods` is empty.
339/// - [`GroupError::Config`] if any two `out_subdir` values collide.
340/// - Any error from the underlying [`generate_standalone`] call.
341///
342/// # Caveat — orchestrator opening balances
343///
344/// **v5.3** — This helper now threads **both** the aggregate-phase
345/// prior-period plumbing (opening NCI / CTA / equity-method carrying
346/// values) **and** the orchestrator-side opening-balance carryover
347/// (entity-level opening TB = prior period's closing TB).  Between
348/// periods N and N+1:
349///
350/// 1. After period N's `generate_standalone` returns, walk
351///    `period_N_out_dir/entities/{code}/period_close/trial_balances.json`
352///    via [`crate::aggregate::opening_balance::read_prior_period_closing_tbs`]
353///    for every entity in the manifest.
354/// 2. Project each closing TB onto its BS positions via
355///    [`crate::aggregate::opening_balance::extract_opening_balances`].
356/// 3. Convert the group-side `OpeningBalance` records into core-side
357///    `EntityOpeningBalance` records (the runtime-friendly carrier
358///    `ShardContext.opening_balances` consumes).
359/// 4. Populate period-(N+1)'s `StandaloneOptions.entity_opening_balances`
360///    with the per-entity carry-forwards.  The shard runner installs
361///    these into each entity's `ShardContext`, and the orchestrator's
362///    Phase 3b uses them in place of the industry-mix opening
363///    generator.
364///
365/// On the first period (idx == 0) and when the entity has no prior
366/// closing TB on disk (e.g. it was a generated entity that wasn't
367/// persisted), the carryover is silently skipped — that entity's
368/// orchestrator falls through to fresh generator output.
369pub fn generate_standalone_chain(
370    base_cfg: &GroupConfig,
371    periods: Vec<PeriodChainSpec>,
372    base_out_dir: &Path,
373    opts: &StandaloneOptions,
374) -> GroupResult<Vec<StandaloneSummary>> {
375    if periods.is_empty() {
376        return Err(GroupError::Config(
377            "generate_standalone_chain: periods must be non-empty".to_string(),
378        ));
379    }
380
381    // Reject duplicate out_subdir values up front — running two
382    // periods into the same directory would produce a corrupt archive
383    // (the second period's outputs would silently overwrite the first).
384    let mut seen: std::collections::BTreeSet<&str> = std::collections::BTreeSet::new();
385    for spec in &periods {
386        if !seen.insert(spec.out_subdir.as_str()) {
387            return Err(GroupError::Config(format!(
388                "generate_standalone_chain: duplicate out_subdir `{}` — every \
389                 period must have a unique output directory",
390                spec.out_subdir,
391            )));
392        }
393    }
394
395    let mut summaries: Vec<StandaloneSummary> = Vec::with_capacity(periods.len());
396    let mut prior_out: Option<PathBuf> = None;
397
398    for (idx, spec) in periods.into_iter().enumerate() {
399        let mut cfg = base_cfg.clone();
400        cfg.period = spec.period;
401
402        let out_dir = base_out_dir.join(&spec.out_subdir);
403
404        let mut period_opts = opts.clone();
405        if idx > 0 {
406            period_opts.prior_period_aggregate = prior_out.clone();
407            // **v5.3** — auto-thread the orchestrator-side opening-
408            // balance carryover from the prior period.  Walk the
409            // manifest to know which entity codes to ask for, then
410            // load + project each entity's closing TB into runtime-
411            // friendly EntityOpeningBalance records.  Entities whose
412            // closing TB doesn't exist on disk are silently skipped
413            // (best-effort — the orchestrator's industry-mix
414            // generator handles them).
415            if let Some(prior) = &prior_out {
416                let entity_codes: Vec<String> = base_cfg
417                    .ownership
418                    .entities
419                    .iter()
420                    .map(|e| e.code.clone())
421                    .collect();
422                let closing_tbs = crate::aggregate::opening_balance::read_prior_period_closing_tbs(
423                    prior,
424                    &entity_codes,
425                )?;
426                // **v5.31 C2 (#157)** — project each closing TB onto its
427                // opening positions via `project_closing_to_opening`,
428                // which absorbs the period's net income into Retained
429                // Earnings. The legacy `extract_opening_balances` used
430                // to live here, but it silently dropped P&L lines
431                // without absorbing them — correct only when the
432                // closing TB is already PostClosing (zero P&L). The
433                // orchestrator emits `Adjusted` TBs (still showing
434                // period P&L), so the legacy path lost net income on
435                // the chain hand-off.
436                let mut openings_by_entity: std::collections::BTreeMap<
437                    String,
438                    Vec<datasynth_core::models::balance::EntityOpeningBalance>,
439                > = std::collections::BTreeMap::new();
440                for (code, tb) in &closing_tbs {
441                    let core_openings = datasynth_generators::balance::project_closing_to_opening(
442                        tb,
443                        &opts.closing_to_opening_framework,
444                    );
445                    if !core_openings.is_empty() {
446                        openings_by_entity.insert(code.clone(), core_openings);
447                    }
448                }
449                period_opts.entity_opening_balances = openings_by_entity;
450            }
451        }
452
453        let summary = generate_standalone(&cfg, &out_dir, &period_opts)?;
454        prior_out = Some(out_dir);
455        summaries.push(summary);
456    }
457
458    Ok(summaries)
459}
460
461// ── Tests ──────────────────────────────────────────────────────────────────────
462
463#[cfg(test)]
464mod tests {
465    use super::*;
466    use chrono::NaiveDate;
467    use std::path::PathBuf;
468
469    #[test]
470    fn period_chain_spec_field_shape() {
471        // Pin the public API shape — refactors that hide or rename
472        // fields will break the build.
473        let spec = PeriodChainSpec {
474            period: crate::config::PeriodConfig {
475                start_date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
476                length: crate::config::PeriodLength::Quarterly,
477                fiscal_year_end: None,
478            },
479            out_subdir: "2024_Q1".to_string(),
480        };
481        assert_eq!(spec.out_subdir, "2024_Q1");
482        assert_eq!(spec.period.length, crate::config::PeriodLength::Quarterly);
483    }
484
485    #[test]
486    fn empty_periods_rejected() {
487        let cfg = sample_min_config();
488        let err = generate_standalone_chain(
489            &cfg,
490            Vec::new(),
491            &PathBuf::from("/tmp/never"),
492            &StandaloneOptions::default(),
493        )
494        .unwrap_err();
495        assert!(format!("{err}").contains("must be non-empty"));
496    }
497
498    #[test]
499    fn duplicate_out_subdir_rejected() {
500        let cfg = sample_min_config();
501        let p1 = PeriodChainSpec {
502            period: crate::config::PeriodConfig {
503                start_date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
504                length: crate::config::PeriodLength::Quarterly,
505                fiscal_year_end: None,
506            },
507            out_subdir: "DUP".to_string(),
508        };
509        let p2 = PeriodChainSpec {
510            period: crate::config::PeriodConfig {
511                start_date: NaiveDate::from_ymd_opt(2024, 4, 1).unwrap(),
512                length: crate::config::PeriodLength::Quarterly,
513                fiscal_year_end: None,
514            },
515            out_subdir: "DUP".to_string(),
516        };
517        let err = generate_standalone_chain(
518            &cfg,
519            vec![p1, p2],
520            &PathBuf::from("/tmp/never"),
521            &StandaloneOptions::default(),
522        )
523        .unwrap_err();
524        assert!(format!("{err}").contains("duplicate out_subdir"));
525    }
526
527    #[test]
528    fn standalone_options_default_has_empty_entity_opening_balances() {
529        // v5.3 carryover field defaults to empty — single-period
530        // engagements see no behaviour change.
531        let opts = StandaloneOptions::default();
532        assert!(opts.entity_opening_balances.is_empty());
533    }
534
535    #[test]
536    fn standalone_options_can_carry_per_entity_openings() {
537        use datasynth_core::models::balance::{AccountType, EntityOpeningBalance};
538        use rust_decimal::Decimal;
539        let mut opts = StandaloneOptions::default();
540        opts.entity_opening_balances.insert(
541            "SUB".to_string(),
542            vec![EntityOpeningBalance {
543                account_code: "1000".to_string(),
544                account_type: AccountType::Asset,
545                debit: Decimal::from(50_000),
546                credit: Decimal::ZERO,
547            }],
548        );
549        assert_eq!(opts.entity_opening_balances.len(), 1);
550        assert_eq!(opts.entity_opening_balances["SUB"][0].account_code, "1000");
551    }
552
553    /// Helper — minimum-viable GroupConfig for shape-only tests.
554    /// Doesn't reach the orchestrator (early-validation tests bail
555    /// before generate_standalone is called).
556    fn sample_min_config() -> GroupConfig {
557        GroupConfig {
558            id: "TEST".to_string(),
559            name: None,
560            presentation_currency: "CHF".to_string(),
561            period: crate::config::PeriodConfig {
562                start_date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
563                length: crate::config::PeriodLength::Quarterly,
564                fiscal_year_end: None,
565            },
566            seed: 42,
567            defaults: serde_yaml::Value::Null,
568            scoping_profiles: Default::default(),
569            ownership: crate::config::OwnershipConfig {
570                parent_entity_code: "P".to_string(),
571                entities: Vec::new(),
572                generated: Vec::new(),
573                entities_from: None,
574            },
575            intercompany: Default::default(),
576            fx: crate::config::FxConfig {
577                base_currency: "CHF".to_string(),
578                rate_source: Default::default(),
579                rates: Default::default(),
580                policy: crate::config::FxPolicyConfig {
581                    balance_sheet: crate::config::FxRateBasis::Closing,
582                    income_statement: crate::config::FxRateBasis::Average,
583                    equity: crate::config::FxRateBasis::Historical,
584                },
585            },
586            audit: Default::default(),
587            tax: Default::default(),
588            cgu: Default::default(),
589            output: Default::default(),
590            fleet: None,
591        }
592    }
593}