Revision history for App-HomeBank2Ledger.
+0.005 2019-08-17 16:26:33-06:00 MST7MDT
+ * Add --budget option for converting HomeBank budget to Ledger.
+ * Support quoting commodities when needed.
+ * The formatter now adds posting notes when they differ from the transaction
+ memo and avoids printing posting payees if not different from the
+ transaction payee.
+ * Fix bug which could add duplicate ":Unknown" accounts.
+
0.004 2019-06-17 23:28:10-06:00 MST7MDT
* Remove --default-account option. Instead the default account(s) can be
customized using the --rename-account option, like this:
"XML::Entities" : "0",
"XML::Parser::Lite" : "0",
"parent" : "0",
+ "perl" : "v5.10.1",
"strict" : "0",
"warnings" : "0"
}
"File::Spec" : "0",
"IO::Handle" : "0",
"IPC::Open3" : "0",
- "Test::More" : "0",
- "perl" : "5.006"
+ "Test::More" : "0"
}
}
},
"provides" : {
"App::HomeBank2Ledger" : {
"file" : "lib/App/HomeBank2Ledger.pm",
- "version" : "0.004"
+ "version" : "0.005"
},
"App::HomeBank2Ledger::Formatter" : {
"file" : "lib/App/HomeBank2Ledger/Formatter.pm",
- "version" : "0.004"
+ "version" : "0.005"
},
"App::HomeBank2Ledger::Formatter::Beancount" : {
"file" : "lib/App/HomeBank2Ledger/Formatter/Beancount.pm",
- "version" : "0.004"
+ "version" : "0.005"
},
"App::HomeBank2Ledger::Formatter::Ledger" : {
"file" : "lib/App/HomeBank2Ledger/Formatter/Ledger.pm",
- "version" : "0.004"
+ "version" : "0.005"
},
"App::HomeBank2Ledger::Ledger" : {
"file" : "lib/App/HomeBank2Ledger/Ledger.pm",
- "version" : "0.004"
+ "version" : "0.005"
},
"App::HomeBank2Ledger::Util" : {
"file" : "lib/App/HomeBank2Ledger/Util.pm",
- "version" : "0.004"
+ "version" : "0.005"
},
"File::HomeBank" : {
"file" : "lib/File/HomeBank.pm",
- "version" : "0.004"
+ "version" : "0.005"
}
},
"release_status" : "stable",
"web" : "https://github.com/chazmcgarvey/homebank2ledger"
}
},
- "version" : "0.004",
+ "version" : "0.005",
"x_authority" : "cpan:CCM",
"x_generated_by_perl" : "v5.28.0",
"x_serialization_backend" : "Cpanel::JSON::XS version 4.08"
IO::Handle: '0'
IPC::Open3: '0'
Test::More: '0'
- perl: '5.006'
configure_requires:
ExtUtils::MakeMaker: '0'
dynamic_config: 0
provides:
App::HomeBank2Ledger:
file: lib/App/HomeBank2Ledger.pm
- version: '0.004'
+ version: '0.005'
App::HomeBank2Ledger::Formatter:
file: lib/App/HomeBank2Ledger/Formatter.pm
- version: '0.004'
+ version: '0.005'
App::HomeBank2Ledger::Formatter::Beancount:
file: lib/App/HomeBank2Ledger/Formatter/Beancount.pm
- version: '0.004'
+ version: '0.005'
App::HomeBank2Ledger::Formatter::Ledger:
file: lib/App/HomeBank2Ledger/Formatter/Ledger.pm
- version: '0.004'
+ version: '0.005'
App::HomeBank2Ledger::Ledger:
file: lib/App/HomeBank2Ledger/Ledger.pm
- version: '0.004'
+ version: '0.005'
App::HomeBank2Ledger::Util:
file: lib/App/HomeBank2Ledger/Util.pm
- version: '0.004'
+ version: '0.005'
File::HomeBank:
file: lib/File/HomeBank.pm
- version: '0.004'
+ version: '0.005'
requires:
Carp: '0'
Exporter: '0'
XML::Entities: '0'
XML::Parser::Lite: '0'
parent: '0'
+ perl: v5.10.1
strict: '0'
warnings: '0'
resources:
bugtracker: https://github.com/chazmcgarvey/homebank2ledger/issues
homepage: https://github.com/chazmcgarvey/homebank2ledger
repository: https://github.com/chazmcgarvey/homebank2ledger.git
-version: '0.004'
+version: '0.005'
x_authority: cpan:CCM
x_generated_by_perl: v5.28.0
x_serialization_backend: 'YAML::Tiny version 1.73'
use strict;
use warnings;
-use 5.006;
+use 5.010001;
use ExtUtils::MakeMaker;
"bin/homebank2ledger"
],
"LICENSE" => "mit",
- "MIN_PERL_VERSION" => "5.006",
+ "MIN_PERL_VERSION" => "5.010001",
"NAME" => "App::HomeBank2Ledger",
"PREREQ_PM" => {
"Carp" => 0,
"IPC::Open3" => 0,
"Test::More" => 0
},
- "VERSION" => "0.004",
+ "VERSION" => "0.005",
"test" => {
"TESTS" => "t/*.t"
}
VERSION
- version 0.004
+ version 0.005
SYNOPSIS
Defaults to enabled; use --no-commodities to disable.
+ --budget
+
+ Enables budget transactions.
+
+ Budget transactions are only supported by the Ledger format (for now).
+ This option is silently ignored otherwise.
+
+ Defaults to enabled; use --no-budget to disable.
+
--opening-date DATE
Specify the opening date for the "opening balances" transaction. This
* I didn't intend to make this a releasable robust product, so it's
lacking tests.
- * Budgets and scheduled transactions are not (yet) converted.
+ * Scheduled transactions are not (yet) converted.
* There are some minor formatting tweaks I will make (e.g.
consolidate transaction tags and payees)
use App::HomeBank2Ledger;
-our $VERSION = '0.004'; # VERSION
+our $VERSION = '0.005'; # VERSION
App::HomeBank2Ledger->main(@ARGV);
=head1 VERSION
-version 0.004
+version 0.005
=head1 SYNOPSIS
Defaults to enabled; use C<--no-commodities> to disable.
+=head2 --budget
+
+Enables budget transactions.
+
+Budget transactions are only supported by the Ledger format (for now). This option is silently
+ignored otherwise.
+
+Defaults to enabled; use C<--no-budget> to disable.
+
=head2 --opening-date DATE
Specify the opening date for the "opening balances" transaction. This transaction is created (if
=item *
-Budgets and scheduled transactions are not (yet) converted.
+Scheduled transactions are not (yet) converted.
=item *
# 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.004'; # VERSION
+our $VERSION = '0.005'; # VERSION
my %ACCOUNT_TYPES = ( # map HomeBank account types to Ledger accounts
bank => 'Assets:Bank',
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
if ($opts->{accounts}) {
my @accounts = map { $_->{ledger_name} } grep { !$_->{excluded} } @$accounts, @$categories;
- push @accounts, $default_account_income, $default_account_expenses;
+ 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:
amount => $amount,
commodity => $commodities{$account->{currency}},
payee => $payee->{name},
- memo => $memo,
+ note => $memo,
status => $status,
tags => $tags,
};
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}),
};
commodity => $commodities{$account->{currency}},
amount => $amount,
payee => $payee->{name},
- memo => $memo,
+ note => $memo,
status => $status,
tags => $tags,
};
commodity => $commodities{$account->{currency}},
amount => $amount,
payee => $payee->{name},
- memo => $memo,
+ note => $memo,
status => $status,
tags => $tags,
};
payees => 1,
tags => 1,
commodities => 1,
+ budget => 1,
opening_date => '',
rename_accounts => {},
exclude_accounts => [],
'payees!' => \$opts{payees},
'tags!' => \$opts{tags},
'commodities!' => \$opts{commodities},
+ 'budget!' => \$opts{budget},
'opening-date=s' => \$opts{opening_date},
'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};
+ $opts{input} = shift @args if !$opts{input};
+ $opts{budget} = 0 if lc($opts{format}) ne 'ledger';
return \%opts;
}
=head1 VERSION
-version 0.004
+version 0.005
=head1 SYNOPSIS
use Module::Pluggable search_path => [__PACKAGE__],
sub_name => 'available_formatters';
-our $VERSION = '0.004'; # VERSION
+our $VERSION = '0.005'; # VERSION
sub _croak { require Carp; Carp::croak(@_) }
=head1 VERSION
-version 0.004
+version 0.005
=head1 SYNOPSIS
# ABSTRACT: Beancount formatter
+use v5.10.1; # defined-or
use warnings;
use strict;
use parent 'App::HomeBank2Ledger::Formatter';
-our $VERSION = '0.004'; # VERSION
+our $VERSION = '0.005'; # VERSION
my %STATUS_SYMBOLS = (
cleared => '*',
my $ledger = shift;
my @out = (
- $self->_format_header,
- $self->_format_accounts($ledger),
- $self->_format_commodities($ledger),
- # $self->_format_payees,
- # $self->_format_tags,
- $self->_format_transactions($ledger),
+ $self->format_header,
+ $self->format_accounts($ledger),
+ $self->format_commodities($ledger),
+ # $self->format_payees,
+ # $self->format_tags,
+ $self->format_transactions($ledger),
);
return join($/, map { rtrim($_) } @out);
}
-sub _format_header {
+
+sub format_header {
my $self = shift;
my @out;
if (my $name = $self->name) {
push @out, "; Name: $name";
}
-
- my $file = $self->file;
- push @out, "; Converted from ${file} using homebank2ledger ${VERSION}";
+ if (my $file = $self->file) {
+ push @out, "; File: $file";
+ }
push @out, '';
return @out;
}
-sub _format_accounts {
+
+sub format_accounts {
my $self = shift;
my $ledger = shift;
return @out;
}
-sub _format_commodities {
+
+sub format_commodities {
my $self = shift;
my $ledger = shift;
return @out;
}
-sub _format_transactions {
+
+sub format_transactions {
my $self = shift;
my $ledger = shift;
push @line, ($posting_status_symbol ? " $posting_status_symbol " : ' ');
push @line, sprintf("\%-${account_width}s", $account);
push @line, ' ';
- push @line, $self->_format_amount($posting->{amount}, $posting->{commodity}) if defined $posting->{amount};
+ if (defined $posting->{amount}) {
+ push @line, $self->_format_amount($posting->{amount}, $posting->{commodity});
+ my $lot_price = $posting->{lot_price};
+ my $lot_date = $posting->{lot_date};
+ my $lot_ref = $posting->{lot_ref};
+ if ($lot_price || $lot_date || $lot_ref) {
+ push @line, ' {',
+ join(', ',
+ $lot_price ? $self->_format_amount($lot_price->{amount}, $lot_price->{commodity}) : (),
+ $lot_date ? $lot_date : (),
+ $lot_ref ? $self->_format_string($lot_ref) : (),
+ ),
+ '}';
+ }
+ if (my $cost = $posting->{total_cost} // $posting->{cost}) {
+ my $is_total = defined $posting->{total_cost};
+ my $cost_symbol = $is_total ? '@@' : '@';
+ push @line, ' ', $cost_symbol, ' ',
+ $self->_format_amount($cost->{amount}, $cost->{commodity});
+ }
+ }
push @out, join('', @line);
}
=head1 VERSION
-version 0.004
+version 0.005
=head1 DESCRIPTION
This is a formatter for L<Beancount|http://furius.ca/beancount/>.
+=head1 METHODS
+
+=head2 format_header
+
+ @lines = $formatter->format_header;
+
+Get formatted header. For example,
+
+ ; Name: My Finances
+ ; File: path/to/finances.xhb
+
+=head2 format_accounts
+
+ @lines = $formatter->format_accounts($ledger);
+
+Get formatted accounts. For example,
+
+ 2003-02-14 open Assets:Bank:Credit-Union:Savings
+ 2003-02-14 open Assets:Bank:Credit-Union:Checking
+ ...
+
+=head2 format_commodities
+
+ @lines = $formatter->format_commodities($ledger);
+
+Get formattted commodities. For example,
+
+ 2003-02-14 commodity USD
+ name: "US Dollar"
+ ...
+
+=head2 format_transactions
+
+ @lines = $formatter->format_transactions($ledger);
+
+Get formatted transactions. For example,
+
+ 2003-02-14 * "Opening Balance"
+ Assets:Bank:Credit-Union:Savings 458.21 USD
+ Assets:Bank:Credit-Union:Checking 194.17 USD
+ Equity:Opening-Balances
+
+ ...
+
=head1 SEE ALSO
L<App::HomeBank2Ledger::Formatter>
# ABSTRACT: Ledger formatter
+use v5.10.1; # defined-or
use warnings;
use strict;
use parent 'App::HomeBank2Ledger::Formatter';
-our $VERSION = '0.004'; # VERSION
+our $VERSION = '0.005'; # VERSION
my %STATUS_SYMBOLS = (
cleared => '*',
my $ledger = shift;
my @out = (
- $self->_format_header,
- $self->_format_accounts($ledger),
- $self->_format_commodities($ledger),
- $self->_format_payees($ledger),
- $self->_format_tags($ledger),
- $self->_format_transactions($ledger),
+ $self->format_header,
+ $self->format_accounts($ledger),
+ $self->format_commodities($ledger),
+ $self->format_payees($ledger),
+ $self->format_tags($ledger),
+ $self->format_transactions($ledger),
);
return join($/, map { rtrim($_) } @out);
}
-sub _format_header {
+
+sub format_header {
my $self = shift;
my @out;
if (my $name = $self->name) {
push @out, "; Name: $name";
}
-
- my $file = $self->file;
- push @out, "; Converted from ${file} using homebank2ledger ${VERSION}";
+ if (my $file = $self->file) {
+ push @out, "; File: $file";
+ }
push @out, '';
return @out;
}
-sub _format_accounts {
+
+sub format_accounts {
my $self = shift;
my $ledger = shift;
return @out;
}
-sub _format_commodities {
+
+sub format_commodities {
my $self = shift;
my $ledger = shift;
return @out;
}
-sub _format_payees {
+
+sub format_payees {
my $self = shift;
my $ledger = shift;
return @out;
}
-sub _format_tags {
+
+sub format_tags {
my $self = shift;
my $ledger = shift;
return @out;
}
-sub _format_transactions {
+
+sub format_transactions {
my $self = shift;
my $ledger = shift;
push @line, ($posting_status_symbol ? " $posting_status_symbol " : ' ');
push @line, sprintf("\%-${account_width}s", $posting->{account});
push @line, ' ';
- push @line, $self->_format_amount($posting->{amount}, $posting->{commodity}) if defined $posting->{amount};
+ if (defined $posting->{amount}) {
+ push @line, $self->_format_amount($posting->{amount}, $posting->{commodity});
+ if (my $price = $posting->{lot_price}) {
+ my $is_fixed = $posting->{lot_fixed};
+ my $fixed_symbol = $is_fixed ? '=' : '';
+ push @line, " {${fixed_symbol}",
+ $self->_format_amount($price->{amount}, $price->{commodity}),
+ '}';
+ }
+ if (my $lot_date = $posting->{lot_date}) {
+ push @line, " [$posting->{lot_date}]";
+ }
+ if (my $cost = $posting->{total_cost} // $posting->{cost}) {
+ my $is_total = defined $posting->{total_cost};
+ my $cost_symbol = $is_total ? '@@' : '@';
+ push @line, ' ', $cost_symbol, ' ',
+ $self->_format_amount($cost->{amount}, $cost->{commodity});
+ }
+ }
+ if (my $note = $posting->{note}) {
+ $note = $self->_format_string($note);
+ push @line, " ; $note" if $note ne $memo;
+ }
push @out, join('', @line);
- if (my $payee = $posting->{payee}) {
- push @out, ' ; Payee: '.$self->_format_string($payee);
+ if (my $posting_payee = $posting->{payee}) {
+ $posting_payee = $self->_format_string($posting_payee);
+ push @out, " ; Payee: $posting_payee" if $posting_payee ne $payee;
}
if (my @tags = @{$posting->{tags} || []}) {
- push @out, " ; :".join(':', @tags).":";
+ push @out, ' ; :'.join(':', @tags).':';
}
}
return $str;
}
+sub _quote_string {
+ my $self = shift;
+ my $str = shift;
+ $str =~ s/"/\\"/g;
+ return "\"$str\"";
+}
+
sub _format_amount {
my $self = shift;
my $amount = shift;
my $num = join($commodity->{dchar}, commify($whole, $commodity->{gchar}), $fraction);
- $num = $commodity->{syprf} ? "$commodity->{symbol} $num" : "$num $commodity->{symbol}";
+ my $symbol = $commodity->{symbol};
+ $symbol = $self->_quote_string($symbol) if $symbol =~ /[0-9\s]/;
+
+ $num = $commodity->{syprf} ? "$symbol $num" : "$num $symbol";
return $num;
}
=head1 VERSION
-version 0.004
+version 0.005
=head1 DESCRIPTION
This is a formatter for L<Ledger|https://www.ledger-cli.org/>.
+=head1 METHODS
+
+=head2 format_header
+
+ @lines = $formatter->format_header;
+
+Get formatted header. For example,
+
+ ; Name: My Finances
+ ; File: path/to/finances.xhb
+
+=head2 format_accounts
+
+ @lines = $formatter->format_accounts($ledger);
+
+Get formatted accounts. For example,
+
+ account Assets:Bank:Credit Union:Savings
+ account Assets:Bank:Credit Union:Checking
+ ...
+
+=head2 format_commodities
+
+ @lines = $formatter->format_commodities($ledger);
+
+Get formattted commodities. For example,
+
+ commodity $
+ note US Dollar
+ format $ 1,000.00
+ alias USD
+ ...
+
+=head2 format_payees
+
+ @lines = $formatter->format_payees($ledger);
+
+Get formatted payees. For example,
+
+ payee 180 Tacos
+ ...
+
+=head2 format_tags
+
+ @lines = $formatter->format_tags($ledger);
+
+Get formatted tags. For example,
+
+ tag yapc
+ ...
+
+=head2 format_transactions
+
+ @lines = $formatter->format_transactions($ledger);
+
+Get formatted transactions. For example,
+
+ 2003-02-14 * Opening Balance
+ Assets:Bank:Credit Union:Savings $ 458.21
+ Assets:Bank:Credit Union:Checking $ 194.17
+ Equity:Opening Balances
+
+ ...
+
=head1 SEE ALSO
L<App::HomeBank2Ledger::Formatter>
use warnings;
use strict;
-our $VERSION = '0.004'; # VERSION
+our $VERSION = '0.005'; # VERSION
sub new {
=head1 VERSION
-version 0.004
+version 0.005
=head1 SYNOPSIS
use Exporter qw(import);
-our $VERSION = '0.004'; # VERSION
+our $VERSION = '0.005'; # VERSION
our @EXPORT_OK = qw(commify rtrim);
=head1 VERSION
-version 0.004
+version 0.005
=head1 FUNCTIONS
use XML::Entities;
use XML::Parser::Lite;
-our $VERSION = '0.004'; # VERSION
+our $VERSION = '0.005'; # VERSION
our @EXPORT_OK = qw(parse_string parse_file);
$attr{flags}{$name} = $flags & (1 << $shift) ? 1 : 0;
}
+ for my $bnum (0 .. 12) {
+ $attr{budget_amounts}[$bnum] = delete $attr{"b$bnum"} if $attr{"b$bnum"};
+ }
+
push @categories, \%attr;
}
elsif ($node eq 'ope') { # transaction
=head1 VERSION
-version 0.004
+version 0.005
=head1 SYNOPSIS
'XML::Entities' => '0',
'XML::Parser::Lite' => '0',
'parent' => '0',
+ 'perl' => 'v5.10.1',
'strict' => '0',
'warnings' => '0'
}
'File::Spec' => '0',
'IO::Handle' => '0',
'IPC::Open3' => '0',
- 'Test::More' => '0',
- 'perl' => '5.006'
+ 'Test::More' => '0'
}
}
};