Add an importer for Fidor account statements

korrat
Jun 27, 2023, 1:43 PM
PCHAKXNMERZROCHMO6GWWES5JSN6MK4O2UAABLPETYYSRICWXTHAC

Dependencies

  • [2] WS3UUOV3 Extract commodity type into separate crate
  • [3] 6A5YLGWV Add an importer for the VR-Bank CSV format
  • [4] NSWL54NM allow deriving pretty printing config from a set of directives
  • [5] T2S6UAVJ Include builder methods on beancount-types
  • [6] QRIJE4AQ add a simple pretty printer for beancount directives
  • [7] YDK6X6PP add a library of important types for beancount
  • [8] SJ6AFVZL remove const configuration in favor of runtime config
  • [9] 5S4MZHL5 pretty print decimals using icu
  • [10] 2JBFREZG enable additional warnings
  • [11] VM4ZH5WD Handle transaction metadata when pretty printing
  • [12] 6MYJDQ3I Handle price directives
  • [13] R524JUUE Implement metadata & price directives
  • [*] KB7Y4PJI Implement importers for Amazon accounts
  • [*] I2P2FTLE add basic parser for german decimals
  • [*] SLTVZLYX Upgrade dependencies
  • [*] RCS5VP3A Add an importer for PayPal account statements

Change contents

  • edit in importers/vr-bank/src/lib.rs at line 4
    [3.106][3.106:130]()
    use core::fmt::Display;
  • edit in importers/vr-bank/src/lib.rs at line 288
    [3.7852][3.7852:9105]()
    #[derive(Clone, Copy, Debug)]
    enum TransactionKind {
    DepositFee,
    Distribution,
    ForeignFees,
    Purchase,
    Reversal,
    Sale,
    }
    impl Display for TransactionKind {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
    f.write_str(match self {
    TransactionKind::DepositFee => "Sale To Cover Deposit Fees",
    TransactionKind::Distribution => "Reinvestment of Distribution",
    TransactionKind::ForeignFees => "Sale To Cover Foreign Fees",
    TransactionKind::Purchase => "Purchase",
    TransactionKind::Reversal => "Reversal of Purchase",
    TransactionKind::Sale => "Sale",
    })
    }
    }
    impl FromStr for TransactionKind {
    type Err = Error;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
    let value = match s {
    "Ertragsausschüttung" => Self::Distribution,
    "Fremde Gebühren" => Self::ForeignFees,
    "Kauf" => Self::Purchase,
    "Storno wegen Rücklastschrift" => Self::Reversal,
    "Verkauf" => Self::Sale,
    "Verkauf wg. Depotgebühr UID mit Postbox" => Self::DepositFee,
    other => todo!("unsupported transaction kind {other:?}"),
    };
    Ok(value)
    }
    }
  • file addition: fidor (d--r------)
    [15.1]
  • file addition: src (d--r------)
    [0.1]
  • file addition: lib.rs (---r------)
    [0.20]
    #![warn(clippy::all, clippy::nursery, clippy::pedantic)]
    extern crate alloc;
    use core::fmt;
    use core::fmt::Display;
    use core::fmt::Formatter;
    use core::ops::Index;
    use core::str::FromStr;
    use beancount_importers_framework::error::ImporterBuilderError;
    use beancount_importers_framework::error::UninitializedFieldSnafu;
    use beancount_types::common_keys;
    use beancount_types::Account;
    use beancount_types::AccountTemplate;
    use beancount_types::Amount;
    use beancount_types::Commodity;
    use beancount_types::CostBasis;
    use beancount_types::CostSpec;
    use beancount_types::Directive;
    use beancount_types::Seg;
    use beancount_types::Transaction;
    use derive_builder::Builder;
    use hashbrown::HashMap;
    use miette::Diagnostic;
    use miette::IntoDiagnostic as _;
    use rust_decimal::Decimal;
    use serde::Deserialize;
    use snafu::Backtrace;
    use snafu::OptionExt as _;
    use snafu::Snafu;
    use time::macros::format_description;
    use time::Date;
    // TODO documentation
    // TODO balance assertions
    // TODO metadata + linking
    // TODO adjust accounts
    #[derive(Builder, Clone, Debug, Deserialize)]
    #[builder(
    build_fn(error = "ImporterBuilderError", skip),
    name = "ImporterBuilder"
    )]
    pub struct Config {
    #[builder(setter(into), try_setter)]
    pub account: AccountTemplate<TemplateSelector>,
    #[builder(setter(into), try_setter)]
    pub commodity: Commodity,
    #[builder(setter(into), try_setter)]
    pub express_trade_recipient: AccountTemplate<TemplateSelector>,
    #[builder(setter(into), try_setter)]
    pub express_trade_fees: AccountTemplate<TemplateSelector>,
    #[builder(setter(into), try_setter)]
    pub fallback_account: Account,
    #[builder(setter(into), try_setter)]
    pub fee_account: AccountTemplate<TemplateSelector>,
    #[builder(setter(into, strip_option))]
    pub fee_payee: Option<String>,
    #[builder(field(type = "HashMap<String, Account>"))]
    pub known_ibans: HashMap<String, Account>,
    #[builder(setter(into), try_setter)]
    pub savings_bonds_account: AccountTemplate<TemplateSelector>,
    #[builder(setter(into), try_setter)]
    pub savings_bonds_commodity: Commodity,
    }
    #[derive(Debug, Diagnostic, Snafu)]
    pub enum Error {}
    #[derive(Debug, Deserialize)]
    pub struct Importer {
    #[serde(flatten)]
    config: Config,
    #[serde(default = "csv::Importer::semicolon_delimited", skip_deserializing)]
    importer: csv::Importer,
    }
    impl Importer {
    const NAME: &str = "fidor/transactions";
    #[must_use]
    pub fn builder() -> ImporterBuilder {
    ImporterBuilder::default()
    }
    #[must_use]
    pub fn new(config: Config) -> Self {
    let importer = csv::Importer::semicolon_delimited();
    Self { config, importer }
    }
    }
    impl Importer {
    fn build_transfer_transaction(
    &self,
    record: &csv::Record,
    transaction: &mut Transaction,
    prefix: &str,
    ) {
    let (payee, iban) = record[2]
    .strip_prefix(prefix)
    .and_then(|rest| {
    let (recipient_name, rest) = rest.split_once(", IBAN ")?;
    let recipient_iban = rest.split_once(", ").map_or(rest, |(iban, _)| iban);
    Some((recipient_name.trim(), recipient_iban.trim()))
    })
    .expect("structured narration");
    let opposite_account = self
    .config
    .known_ibans
    .get(iban)
    .unwrap_or(&self.config.fallback_account);
    transaction
    .set_payee(payee)
    .build_posting(opposite_account, |_posting| {});
    }
    }
    impl beancount_importers_framework::ImporterProtocol for Importer {
    type Error = miette::Report;
    fn account(&self, _file: &camino::Utf8Path) -> Result<Account, Self::Error> {
    Ok(self.config.account.base().to_owned())
    }
    fn date(&self, file: &camino::Utf8Path) -> Option<Result<Date, Self::Error>> {
    self.importer
    .date(file, self)
    .map(|result| result.map_err(Self::Error::from))
    }
    fn extract(
    &self,
    file: &camino::Utf8Path,
    existing: &[Directive],
    ) -> Result<Vec<Directive>, Self::Error> {
    self.importer
    .extract(file, existing, self)
    .map_err(Self::Error::from)
    }
    fn filename(&self, _file: &camino::Utf8Path) -> Option<Result<String, Self::Error>> {
    Some(Ok(String::from("transactions.csv")))
    }
    fn identify(&self, file: &camino::Utf8Path) -> Result<bool, Self::Error> {
    const EXPECTED_HEADERS: &[&str] = &["Datum", "Beschreibung", "Beschreibung2", "Wert"];
    self.importer
    .identify(file, EXPECTED_HEADERS)
    .into_diagnostic()
    }
    fn name(&self) -> &'static str {
    Self::NAME
    }
    fn typetag_deserialize(&self) {}
    }
    impl csv::RecordImporter for Importer {
    type Error = Error;
    fn date(&self, record: &csv::Record) -> Option<Result<Date, Self::Error>> {
    Some(parse_transaction_date(record))
    }
    fn extract(
    &self,
    _existing: &[Directive],
    record: &csv::Record,
    ) -> Result<Vec<Directive>, Self::Error> {
    let date = parse_transaction_date(record)?;
    let commodity = self.config.commodity;
    let mut transaction = Transaction::on(date);
    let context = TemplateContext {};
    let account = self.config.account.render(&context);
    let transaction_kind = TransactionKind::try_from(&record[1])?;
    let amount = {
    let amount = &record[3];
    let amount = german_decimal::parse(amount).map_err(|_| -> Error { todo!() })?;
    Amount::new(amount, commodity)
    };
    transaction
    .set_narration(format!("{transaction_kind}"))
    .build_posting(account, |posting| {
    posting.set_amount(amount);
    });
    match transaction_kind {
    TransactionKind::AccountManagementCharge | TransactionKind::ActivityBonus => {
    if let Some(ref payee) = self.config.fee_payee {
    transaction.set_payee(payee);
    }
    transaction.build_posting(self.config.fee_account.render(&context), |_posting| {});
    }
    TransactionKind::Credit => {
    self.build_transfer_transaction(record, &mut transaction, "Absender ");
    }
    TransactionKind::Remittance => {
    self.build_transfer_transaction(record, &mut transaction, "Empfaenger ");
    }
    TransactionKind::PurchaseFee { .. } => {
    if let Some(payee) = &self.config.fee_payee {
    transaction.set_payee(payee);
    }
    transaction.build_posting(
    self.config.express_trade_fees.render(&context),
    |_posting| {},
    );
    }
    TransactionKind::SendFriendsMoney {
    purchase_id,
    recipient,
    } => {
    transaction
    .set_payee(recipient)
    .add_meta(common_keys::TRANSACTION_ID, purchase_id)
    .build_posting(
    self.config.express_trade_recipient.render(&context),
    |_posting| {},
    );
    }
    TransactionKind::SavingsBondCreation { id } => {
    transaction.build_posting(
    self.config.savings_bonds_account.render(&context),
    |posting| {
    let mut cost = CostSpec::from(CostBasis::PerUnit(-amount));
    cost.set_label(id);
    posting
    .set_amount(Amount::new(
    Decimal::ONE,
    self.config.savings_bonds_commodity,
    ))
    .set_cost(cost);
    },
    );
    }
    TransactionKind::SavingsBondPayout { id } => {
    transaction.build_posting(
    self.config.savings_bonds_account.render(&context),
    |posting| {
    let amount =
    Amount::new(-Decimal::ONE, self.config.savings_bonds_commodity);
    posting.set_amount(amount).set_cost(CostSpec::labelled(id));
    },
    );
    }
    }
    Ok(vec![Directive::from(transaction)])
    }
    }
    impl ImporterBuilder {
    pub fn build(&mut self) -> Result<Importer, ImporterBuilderError> {
    let config = Config {
    account: self.account.clone().context(UninitializedFieldSnafu {
    field: "account",
    importer: Importer::NAME,
    })?,
    commodity: self.commodity.context(UninitializedFieldSnafu {
    field: "commodity",
    importer: Importer::NAME,
    })?,
    express_trade_recipient: self.express_trade_recipient.clone().context(
    UninitializedFieldSnafu {
    field: "express_trade_recipient",
    importer: Importer::NAME,
    },
    )?,
    express_trade_fees: self.express_trade_fees.clone().context(
    UninitializedFieldSnafu {
    field: "express_trade_fees",
    importer: Importer::NAME,
    },
    )?,
    fallback_account: self
    .fallback_account
    .clone()
    .context(UninitializedFieldSnafu {
    field: "fallback_account",
    importer: Importer::NAME,
    })?,
    fee_account: self.fee_account.clone().context(UninitializedFieldSnafu {
    field: "fee_account",
    importer: Importer::NAME,
    })?,
    fee_payee: self.fee_payee.clone().context(UninitializedFieldSnafu {
    field: "fee_payee",
    importer: Importer::NAME,
    })?,
    known_ibans: self.known_ibans.clone(),
    savings_bonds_account: self.savings_bonds_account.clone().context(
    UninitializedFieldSnafu {
    field: "savings_bonds_account",
    importer: Importer::NAME,
    },
    )?,
    savings_bonds_commodity: self.savings_bonds_commodity.context(
    UninitializedFieldSnafu {
    field: "savings_bonds_commodity",
    importer: Importer::NAME,
    },
    )?,
    };
    Ok(Importer::new(config))
    }
    pub fn clear_known_ibans(&mut self) -> &mut Self {
    self.known_ibans.clear();
    self
    }
    pub fn try_add_known_iban<A>(
    &mut self,
    iban: impl Into<String>,
    account: A,
    ) -> Result<&mut Self, A::Error>
    where
    A: TryInto<Account>,
    {
    self.known_ibans.insert(iban.into(), account.try_into()?);
    Ok(self)
    }
    }
    #[derive(Debug)]
    struct TemplateContext {}
    impl Index<&TemplateSelector> for TemplateContext {
    type Output = Seg;
    fn index(&self, selector: &TemplateSelector) -> &Self::Output {
    match *selector {}
    }
    }
    #[derive(Clone, Copy, Debug)]
    pub enum TemplateSelector {}
    impl FromStr for TemplateSelector {
    type Err = TemplateSelectorError;
    fn from_str(selector: &str) -> Result<Self, Self::Err> {
    TemplateSelectorSnafu { selector }.fail()
    }
    }
    #[derive(Debug, Diagnostic, Snafu)]
    pub struct TemplateSelectorError {
    selector: String,
    backtrace: Backtrace,
    }
    #[derive(Clone, Copy, Debug)]
    enum TransactionKind<'i> {
    AccountManagementCharge,
    ActivityBonus,
    Credit,
    Remittance,
    PurchaseFee {
    purchase_id: &'i str,
    },
    SendFriendsMoney {
    purchase_id: &'i str,
    recipient: &'i str,
    },
    SavingsBondCreation {
    id: &'i str,
    },
    SavingsBondPayout {
    id: &'i str,
    },
    }
    impl Display for TransactionKind<'_> {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
    match self {
    TransactionKind::AccountManagementCharge => f.write_str("Account Management Charge"),
    TransactionKind::ActivityBonus => f.write_str("Activity Bonus"),
    TransactionKind::Credit => f.write_str("Incoming transfer"),
    TransactionKind::Remittance => f.write_str("Outgoing transfer"),
    TransactionKind::PurchaseFee { purchase_id } => {
    write!(f, "Express trade fee for {purchase_id}")
    }
    TransactionKind::SendFriendsMoney { purchase_id, .. } => {
    write!(f, "Express trade purchase {purchase_id}")
    }
    TransactionKind::SavingsBondCreation { id } => {
    write!(f, "Savings bond purchase {id}")
    }
    TransactionKind::SavingsBondPayout { id } => write!(f, "Savings bond payout {id}"),
    }
    }
    }
    impl<'i> TryFrom<&'i str> for TransactionKind<'i> {
    type Error = Error;
    fn try_from(s: &'i str) -> Result<Self, Self::Error> {
    match s {
    "Aktivitaetsbonus" => return Ok(Self::ActivityBonus),
    "Kontofuehrung" => return Ok(Self::AccountManagementCharge),
    "Ueberweisung" => return Ok(Self::Remittance),
    _ => {}
    }
    if s.starts_with("Ueberweisung") {
    return Ok(Self::Remittance);
    } else if s.starts_with("Gutschrift") {
    return Ok(Self::Credit);
    } else if let Some(rest) = s.strip_prefix("Sparbrief ") {
    let (id, _) = rest.split_once(", ").expect("structured narration format");
    return Ok(Self::SavingsBondCreation { id });
    } else if let Some(rest) = s.strip_prefix("Auszahlung Sparbrief ") {
    let (id, _) = rest.split_once(", ").expect("structured narration format");
    return Ok(Self::SavingsBondPayout { id });
    } else if let Some(purchase_id) = s.strip_prefix("Kaeufergebuehr ") {
    return Ok(Self::PurchaseFee { purchase_id });
    } else if let Some(rest) = s.strip_prefix(r#""Freunden Geld senden" an Benutzer "#) {
    let (recipient, purchase_id) = rest.split_once(" ").expect("structured narration");
    return Ok(Self::SendFriendsMoney {
    purchase_id,
    recipient,
    });
    }
    todo!("unsupported transaction kind {s:?}")
    }
    }
    fn parse_transaction_date(record: &csv::Record) -> Result<Date, Error> {
    use time::format_description::FormatItem;
    const DATE_COLUMN_INDEX: usize = 0;
    const DATE_FORMAT: &[FormatItem] = format_description!("[day].[month].[year]");
    Date::parse(&record[DATE_COLUMN_INDEX], DATE_FORMAT).map_err(|_| todo!())
    }
  • file addition: Cargo.toml (---r------)
    [0.1]
    [package]
    name = "beancount-importer-fidor"
    edition.workspace = true
    publish.workspace = true
    rust-version.workspace = true
    version.workspace = true
    [dependencies]
    # Workspace dependencies
    beancount-importers-framework.path = "../../framework"
    beancount-types.path = "../../common/beancount-types"
    csv.path = "../csv"
    german-decimal.path = "../../common/german-decimal"
    # Inherited dependencies
    camino.workspace = true
    derive_builder.workspace = true
    hashbrown.workspace = true
    miette.workspace = true
    rust_decimal.workspace = true
    serde.workspace = true
    snafu.workspace = true
    time.workspace = true
  • replacement in common/beancount-types/src/transaction.rs at line 15
    [4.125][4.125:147]()
    use crate::CostBasis;
    [4.125]
    [4.20]
    use crate::CostSpec;
  • replacement in common/beancount-types/src/transaction.rs at line 30
    [4.400][4.400:433]()
    pub cost: Option<CostBasis>,
    [4.400]
    [4.433]
    pub cost: Option<CostSpec>,
  • replacement in common/beancount-types/src/transaction.rs at line 98
    [4.1444][4.1444:1539]()
    pub fn set_cost(&mut self, cost: CostBasis) -> &mut Self {
    self.cost = Some(cost);
    [4.1444]
    [4.1539]
    pub fn set_cost(&mut self, cost: impl Into<CostSpec>) -> &mut Self {
    self.cost = Some(cost.into());
  • replacement in common/beancount-types/src/transaction.rs at line 118
    [4.3585][4.568:590](),[4.568][4.568:590]()
    let Posting {
    [4.3585]
    [4.590]
    let Self {
  • replacement in common/beancount-types/src/transaction.rs at line 351
    [4.2312][4.3766:3838]()
    Flag::Complete => '*',
    Flag::Incomplete => '!',
    [4.2312]
    [4.2406]
    Self::Complete => '*',
    Self::Incomplete => '!',
  • edit in common/beancount-types/src/lib.rs at line 15
    [4.4225]
    [4.10628]
    pub use crate::cost::CostSpec;
  • edit in common/beancount-types/src/cost.rs at line 5
    [4.4768]
    [4.7652]
    use beancount_commodity::Commodity;
    use rust_decimal::Decimal;
    use time::Date;
  • edit in common/beancount-types/src/cost.rs at line 17
    [4.7770]
    [4.7770]
    PerUnitAndTotal {
    per_unit: Decimal,
    total: Decimal,
    commodity: Commodity,
    },
  • edit in common/beancount-types/src/cost.rs at line 23
    [4.7772]
    [4.7772]
    impl CostBasis {
    #[must_use]
    pub const fn commodity(&self) -> Option<Commodity> {
    match self {
    Self::Empty => None,
    Self::PerUnit(amount) | Self::Total(amount) => Some(amount.commodity),
    Self::PerUnitAndTotal { commodity, .. } => Some(*commodity),
    }
    }
  • edit in common/beancount-types/src/cost.rs at line 34
    [4.7773]
    [4.7773]
    #[must_use]
    pub const fn per_unit(&self) -> Option<Amount> {
    match self {
    Self::Empty | Self::Total(_) => None,
    Self::PerUnit(amount) => Some(*amount),
    Self::PerUnitAndTotal {
    per_unit,
    commodity,
    ..
    } => Some(Amount::new(*per_unit, *commodity)),
    }
    }
    #[must_use]
    pub const fn total(&self) -> Option<Amount> {
    match self {
    Self::Empty | Self::PerUnit(_) => None,
    Self::Total(amount) => Some(*amount),
    Self::PerUnitAndTotal {
    total, commodity, ..
    } => Some(Amount::new(*total, *commodity)),
    }
    }
    }
  • edit in common/beancount-types/src/cost.rs at line 65
    [4.8072]
    [4.8072]
    Self::PerUnitAndTotal {
    per_unit,
    total,
    commodity,
    } => write!(f, "{{{per_unit} # {total} {commodity}}}"),
  • edit in common/beancount-types/src/cost.rs at line 73
    [4.8090]
    #[derive(Clone, Debug, Eq, Hash, PartialEq)]
    pub struct CostSpec {
    pub basis: CostBasis,
    pub date: Option<Date>,
    pub label: Option<String>,
    }
    impl CostSpec {
    #[must_use]
    pub const fn dated(date: Date) -> Self {
    Self {
    basis: CostBasis::Empty,
    date: Some(date),
    label: None,
    }
    }
    #[must_use]
    pub const fn empty() -> Self {
    Self {
    basis: CostBasis::Empty,
    date: None,
    label: None,
    }
    }
    #[must_use]
    pub const fn from_per_unit(per_unit: Decimal, commodity: Commodity) -> Self {
    Self {
    basis: CostBasis::PerUnit(Amount::new(per_unit, commodity)),
    date: None,
    label: None,
    }
    }
    #[must_use]
    pub const fn from_per_unit_and_total(
    per_unit: Decimal,
    total: Decimal,
    commodity: Commodity,
    ) -> Self {
    Self {
    basis: CostBasis::PerUnitAndTotal {
    per_unit,
    total,
    commodity,
    },
    date: None,
    label: None,
    }
    }
    #[must_use]
    pub const fn from_total(total: Decimal, commodity: Commodity) -> Self {
    Self {
    basis: CostBasis::Total(Amount::new(total, commodity)),
    date: None,
    label: None,
    }
    }
    #[must_use]
    pub fn labelled(label: impl Into<String>) -> Self {
    Self {
    basis: CostBasis::Empty,
    date: None,
    label: Some(label.into()),
    }
    }
    }
    impl CostSpec {
    pub fn set_label(&mut self, label: impl Into<String>) -> &mut Self {
    self.label = Some(label.into());
    self
    }
    }
    impl CostSpec {
    #![allow(clippy::inline_always)]
    delegate::delegate! {
    to self.basis {
    #[must_use]
    pub const fn commodity(&self) -> Option<Commodity>;
    #[must_use]
    pub const fn per_unit(&self) -> Option<Amount>;
    #[must_use]
    pub const fn total(&self) -> Option<Amount>;
    }
    }
    }
    impl Display for CostSpec {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
    let mut had_output = false;
    let Self { basis, date, label } = self;
    f.write_str("{")?;
    match basis {
    CostBasis::Empty => {}
    CostBasis::PerUnit(amount) => {
    write!(f, "{amount}")?;
    had_output = true;
    }
    CostBasis::Total(amount) => {
    write!(f, "# {amount}")?;
    had_output = true;
    }
    CostBasis::PerUnitAndTotal {
    per_unit,
    total,
    commodity,
    } => {
    write!(f, "{per_unit} # {total} {commodity}")?;
    had_output = true;
    }
    }
    if let Some(date) = date {
    if had_output {
    f.write_str(", ")?;
    }
    write!(f, "{date}")?;
    }
    if let Some(label) = label {
    if had_output {
    f.write_str(", ")?;
    }
    write!(f, "{label:?}")?;
    }
    f.write_str("}")
    }
    }
    impl From<CostBasis> for CostSpec {
    fn from(basis: CostBasis) -> Self {
    Self {
    basis,
    date: None,
    label: None,
    }
    }
    }
  • replacement in common/beancount-pretty-printer/src/lib.rs at line 8
    [4.146][4.146:178]()
    use beancount_types::CostBasis;
    [4.146]
    [4.178]
    use beancount_types::CostSpec;
  • replacement in common/beancount-pretty-printer/src/lib.rs at line 235
    [4.3636][4.3636:3707]()
    fn print_cost(&mut self, cost: &Option<CostBasis>) -> Result<()> {
    [4.3636]
    [4.3707]
    fn print_cost(&mut self, cost: &Option<CostSpec>) -> Result<()> {
  • edit in Cargo.toml at line 14
    [17.241]
    [18.15703]
    "importers/fidor",
  • edit in Cargo.lock at line 250
    [2.2524]
    [2.2524]
    ]
    [[package]]
    name = "beancount-importer-fidor"
    version = "0.0.0-dev.0"
    dependencies = [
    "beancount-importers-framework",
    "beancount-types",
    "camino",
    "csv 0.0.0-dev.0",
    "derive_builder",
    "german-decimal",
    "hashbrown 0.14.0",
    "miette",
    "rust_decimal",
    "serde",
    "snafu",
    "time 0.3.22",