# ABSTRACT: A tool to convert HomeBank files to Ledger format
-use warnings FATAL => 'all'; # temp fatal all
+use warnings;
use strict;
use App::HomeBank2Ledger::Formatter;
use Getopt::Long 2.38 qw(GetOptionsFromArray);
use Pod::Usage;
-our $VERSION = '0.001'; # VERSION
+our $VERSION = '0.009'; # VERSION
my %ACCOUNT_TYPES = ( # map HomeBank account types to Ledger accounts
bank => 'Assets:Bank',
if ($opts->{manual}) {
pod2usage(-exitval => 0, -verbose => 2);
}
+ if (!$opts->{input}) {
+ print STDERR "Input file is required.\n";
+ exit(1);
+ }
my $homebank = File::HomeBank->new(file => $opts->{input});
my $homebank = shift;
my $opts = shift || {};
+ my $default_account_income = 'Income:Unknown';
+ my $default_account_expenses = 'Expenses:Unknown';
+
my $ledger = App::HomeBank2Ledger::Ledger->new;
my $transactions = $homebank->sorted_transactions;
my $accounts = $homebank->accounts;
my $categories = $homebank->categories;
+ my @budget;
# determine full Ledger account names
for my $account (@$accounts) {
my $type = $category->{flags}{income} ? 'Income' : 'Expenses';
my $full_name = $homebank->full_category_name($category->{key});
$category->{ledger_name} = "${type}:${full_name}";
+
+ if ($opts->{budget} && $category->{flags}{budget}) {
+ for my $month_num ($category->{flags}{custom} ? (1 .. 12) : 0) {
+ my $amount = $category->{budget_amounts}[$month_num] || 0;
+ next if !$amount && !$category->{flags}{forced};
+
+ $budget[$month_num]{$category->{ledger_name}} = $amount;
+ }
+ }
}
# handle renaming and marking excluded accounts
$item->{excluded} = 1 if $item->{ledger_name} =~ /$re/;
}
}
+ while (my ($re, $replacement) = each %{$opts->{rename_accounts}}) {
+ $default_account_income =~ s/$re/$replacement/;
+ $default_account_expenses =~ s/$re/$replacement/;
+ }
my $has_initial_balance = grep { $_->{initial} && !$_->{excluded} } @$accounts;
if ($opts->{accounts}) {
my @accounts = map { $_->{ledger_name} } grep { !$_->{excluded} } @$accounts, @$categories;
- push @accounts, $opts->{default_account};
+ push @accounts, $default_account_income if !grep { $_ eq $default_account_income } @accounts;
+ push @accounts, $default_account_expenses if !grep { $_ eq $default_account_expenses } @accounts;
push @accounts, $OPENING_BALANCES_ACCOUNT if $has_initial_balance;
$ledger->add_accounts(@accounts);
$ledger->add_commodities($commodity) if $opts->{commodities};
}
+ my $first_date;
if ($has_initial_balance) {
# transactions are sorted, so the first transaction is the oldest
- my $first_date = $opts->{opening_date} || $transactions->[0]{date};
+ $first_date = $opts->{opening_date} || $transactions->[0]{date};
if ($first_date !~ /^\d{4}-\d{2}-\d{2}$/) {
die "Opening date must be in the form YYYY-MM-DD.\n";
}
});
}
+ if ($opts->{budget}) {
+ my ($first_year) = $first_date =~ /^(\d{4})/;
+
+ for my $month_num (0 .. 12) {
+ next if !$budget[$month_num];
+
+ my $payee = 'Monthly';
+ if (0 < $month_num) {
+ my $year = $first_year;
+ $year += 1 if sprintf('%04d-%02d-99', $first_year, $month_num) lt $first_date;
+ my $date = sprintf('%04d-%02d', $year, $month_num);
+ $payee = "Every 12 months from ${date}";
+ }
+ # my @MONTHS = qw(ALL Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
+ # $payee = "Monthly this $MONTHS[$month_num]" if 0 < $month_num;
+
+ my @postings;
+
+ for my $account (sort keys %{$budget[$month_num]}) {
+ my $amount = $budget[$month_num]{$account};
+ push @postings, {
+ account => $account,
+ amount => -$amount,
+ commodity => $commodities{$homebank->base_currency},
+ }
+ }
+ push @postings, {
+ account => 'Assets',
+ };
+
+ $ledger->add_transactions({
+ date => '~',
+ payee => $payee,
+ postings => \@postings,
+ });
+ }
+ }
+
my %seen;
TRANSACTION:
my $account = $homebank->find_account_by_key($transaction->{account});
my $amount = $transaction->{amount};
my $status = $STATUS_SYMBOLS{$transaction->{status} || ''} || '';
- my $paymode = $transaction->{paymode} || ''; # internaltransfer
my $memo = $transaction->{wording} || '';
my $payee = $homebank->find_payee_by_key($transaction->{payee});
my $tags = _split_tags($transaction->{tags});
+ my $date = $transaction->{date};
my @postings;
push @postings, {
+ date => $date,
account => $account->{ledger_name},
amount => $amount,
commodity => $commodities{$account->{currency}},
payee => $payee->{name},
- memo => $memo,
+ note => $memo,
status => $status,
tags => $tags,
};
- if ($paymode eq 'internaltransfer') {
+ if ($transaction->{dst_account}) { # is an internal transfer
my $paired_transaction = $homebank->find_transaction_transfer_pair($transaction);
my $dst_account = $homebank->find_account_by_key($transaction->{dst_account});
$seen{$transaction->{transfer_key}}++ if $transaction->{transfer_key};
$seen{$paired_transaction->{transfer_key}}++ if $paired_transaction->{transfer_key};
+ my $paired_date = $paired_transaction && $paired_transaction->{date};
my $paired_payee = $homebank->find_payee_by_key($paired_transaction->{payee});
push @postings, {
+ date => $paired_date,
account => $dst_account->{ledger_name},
amount => $paired_transaction->{amount} || -$transaction->{amount},
commodity => $commodities{$dst_account->{currency}},
payee => $paired_payee->{name},
- memo => $paired_transaction->{wording} || '',
+ note => $paired_transaction->{wording} || '',
status => $STATUS_SYMBOLS{$paired_transaction->{status} || ''} || $status,
tags => _split_tags($paired_transaction->{tags}),
};
my @categories = split(/\|\|/, $transaction->{split_category} || '');
for (my $i = 0; $amounts[$i]; ++$i) {
- my $amount = -$amounts[$i];
- my $category = $homebank->find_category_by_key($categories[$i]);
- my $memo = $memos[$i] || '';
- my $other_account = $category ? $category->{ledger_name} : $opts->{default_account};
+ my $amount = -$amounts[$i];
+ my $category = $homebank->find_category_by_key($categories[$i]);
+ my $memo = $memos[$i] || '';
+ my $other_account = $category ? $category->{ledger_name}
+ : $amount < 0 ? $default_account_income
+ : $default_account_expenses;
push @postings, {
account => $other_account,
commodity => $commodities{$account->{currency}},
amount => $amount,
payee => $payee->{name},
- memo => $memo,
+ note => $memo,
status => $status,
tags => $tags,
};
}
}
- else { # with or without category
- my $category = $homebank->find_category_by_key($transaction->{category});
- my $other_account = $category ? $category->{ledger_name} : $opts->{default_account};
+ else { # normal transaction with or without category
+ my $amount = -$transaction->{amount};
+ my $category = $homebank->find_category_by_key($transaction->{category});
+ my $other_account = $category ? $category->{ledger_name}
+ : $amount < 0 ? $default_account_income
+ : $default_account_expenses;
+
push @postings, {
account => $other_account,
commodity => $commodities{$account->{currency}},
- amount => -$transaction->{amount},
+ amount => $amount,
payee => $payee->{name},
- memo => $memo,
+ note => $memo,
status => $status,
tags => $tags,
};
}
$ledger->add_transactions({
- date => $transaction->{date},
+ date => $date,
payee => $payee->{name},
memo => $memo,
postings => \@postings,
payees => 1,
tags => 1,
commodities => 1,
+ budget => 1,
opening_date => '',
- default_account => 'Expenses:No Category',
rename_accounts => {},
exclude_accounts => [],
);
'payees!' => \$opts{payees},
'tags!' => \$opts{tags},
'commodities!' => \$opts{commodities},
+ 'budget!' => \$opts{budget},
'opening-date=s' => \$opts{opening_date},
- 'default-account=s' => \$opts{default_account},
'rename-account|r=s' => \%{$opts{rename_accounts}},
'exclude-account|x=s' => \@{$opts{exclude_accounts}},
) or pod2usage(-exitval => 1, -verbose => 99, -sections => [qw(SYNOPSIS OPTIONS)]);
- $opts{input} = shift @args if !$opts{input};
- if (!$opts{input}) {
- print STDERR "Input file is required.\n";
- exit(1);
- }
+ $opts{input} = shift @args if !$opts{input};
+ $opts{budget} = 0 if lc($opts{format}) ne 'ledger';
return \%opts;
}
=head1 VERSION
-version 0.001
+version 0.009
=head1 SYNOPSIS