X-Git-Url: https://git.dogcows.com/gitweb?p=chaz%2Fhomebank2ledger;a=blobdiff_plain;f=lib%2FApp%2FHomeBank2Ledger%2FFormatter%2FLedger.pm;h=bd7245944f6b47c2740df330503bb714da698978;hp=677facb02e72a5924617ca4e726424921d9497f7;hb=dcc6bb38342f788f336b8be59b552bcb84e25afe;hpb=2cb8cd6b61921ef8f051f9066411deb4c829b68d diff --git a/lib/App/HomeBank2Ledger/Formatter/Ledger.pm b/lib/App/HomeBank2Ledger/Formatter/Ledger.pm index 677facb..bd72459 100644 --- a/lib/App/HomeBank2Ledger/Formatter/Ledger.pm +++ b/lib/App/HomeBank2Ledger/Formatter/Ledger.pm @@ -11,6 +11,7 @@ L =cut +use v5.10.1; # defined-or use warnings; use strict; @@ -24,6 +25,7 @@ my %STATUS_SYMBOLS = ( cleared => '*', pending => '!', ); +my $SYMBOL = qr![^\s\d.,;:?\!\-+*/^&|=\<\>\[\]\(\)\{\}\@]+!; sub _croak { require Carp; Carp::croak(@_) } @@ -49,7 +51,8 @@ sub format { Get formatted header. For example, - ; Converted from finances.xhb using homebank2ledger 0.001 + ; Name: My Finances + ; File: path/to/finances.xhb =cut @@ -61,9 +64,9 @@ sub format_header { 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, ''; @@ -206,16 +209,18 @@ sub _format_transaction { my $account_width = $self->account_width; - my $date = $transaction->{date}; - my $status = $transaction->{status}; - my $payee = $self->_format_string($transaction->{payee} || ''); - my $memo = $self->_format_string($transaction->{memo} || ''); - my @postings = @{$transaction->{postings}}; + my $date = $transaction->{date} or _croak 'Transaction date is required'; + my $aux_date = $transaction->{aux_date} || $transaction->{effective_date} || ''; + my $status = $transaction->{status} // ''; + my $code = $transaction->{code}; + my $payee = $self->_format_string($transaction->{payee}); + my $note = $self->_format_string($transaction->{note} // $transaction->{memo}); + my @postings = @{$transaction->{postings} || _croak 'At least one transaction posting is required'}; my @out; # figure out the Ledger transaction status - my $status_symbol = $STATUS_SYMBOLS{$status || ''}; + my $status_symbol = $STATUS_SYMBOLS{$status}; if (!$status_symbol) { my %posting_statuses = map { ($_->{status} || '') => 1 } @postings; if (keys(%posting_statuses) == 1) { @@ -224,13 +229,30 @@ sub _format_transaction { } } - $payee =~ s/(?: )|\t;/ ;/g; # don't turn into a memo + $aux_date = '' if $date eq $aux_date; + $code =~ s/[\(\)]+// if defined $code; + $payee =~ s/(?: )|\t;/ ;/g if defined $payee; # don't turn into a note + + my $has_code = defined $code && $code ne ''; + my $has_payee = defined $payee && $payee ne ''; + my $has_note = defined $note && $note ne ''; - push @out, sprintf('%s%s%s%s', $date, - $status_symbol && " ${status_symbol}", - $payee && " $payee", - $memo && " ; $memo", + push @out, join('', $date, + $aux_date && "=${aux_date}", + $status_symbol && " ${status_symbol}", + $has_code && " (${code})", + $has_payee && " ${payee}", + $has_note && $has_payee && " ; ${note}", ); + if ($has_note && !$has_payee) { + push @out, " ; ${note}"; + } + + my $metadata = $transaction->{metadata} || {}; + for my $key (sort keys %$metadata) { + my $value = $self->_format_string($metadata->{$key}) ; + push @out, " ; ${key}: ${value}"; + } for my $posting (@postings) { my @line; @@ -243,13 +265,59 @@ sub _format_transaction { 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}); + my $lot = $posting->{lot} || {}; + if (my $lot_price = $lot->{price} // $posting->{lot_price}) { + my $is_fixed = $lot_price->{fixed} // $posting->{lot_fixed}; + my $fixed_symbol = $is_fixed ? '=' : ''; + push @line, " {${fixed_symbol}", + $self->_format_amount($lot_price->{amount}, $lot_price->{commodity}), + '}'; + } + if (my $lot_date = $lot->{date} // $posting->{lot_date}) { + push @line, " [${lot_date}]"; + } + if (my $lot_note = $self->_format_string($lot->{note} // $posting->{lot_note} // '')) { + $lot_note =~ s/[\(\)]+//; # cleanup + $lot_note =~ s/^\@+//; + push @line, " (${lot_note})" if $lot_note; + } + 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}); + } + } + my $posting_date = $posting->{date} || ''; + my $posting_aux_date = $posting->{aux_date} || ''; + my $posting_note = $self->_format_string($posting->{note} // $posting->{memo} // ''); + $posting_date = '' if $posting_date eq $date; + $posting_aux_date = '' if $posting_aux_date eq $aux_date; + $posting_note = '' if $has_note && $posting_note eq $note; + my $has_posting_note = defined $posting_note && $posting_note ne ''; + if ($posting_date || $posting_aux_date || $has_posting_note) { + if ($posting_date || $posting_aux_date) { + $posting_note = sprintf('[%s%s]%s', + $posting_date, + $posting_aux_date && "=${posting_aux_date}", + $has_posting_note && " ${posting_note}", + ); + } + push @line, " ; ${posting_note}"; + } push @out, join('', @line); - if (my $posting_payee = $posting->{payee}) { - $posting_payee = $self->_format_string($posting_payee); - push @out, " ; Payee: $posting_payee" if $posting_payee ne $payee; + my $metadata = $posting->{metadata} || {}; + for my $key (sort keys %$metadata) { + my $value = $self->_format_string($metadata->{$key}); + push @out, " ; ${key}: ${value}"; + } + + if (my $posting_payee = $self->_format_string($posting->{payee} // '')) { + push @out, " ; Payee: $posting_payee" if !$has_payee || $posting_payee ne $payee; } if (my @tags = @{$posting->{tags} || []}) { @@ -264,11 +332,25 @@ sub _format_transaction { sub _format_string { my $self = shift; - my $str = shift; + my $str = shift // return; $str =~ s/\v//g; return $str; } +sub _quote_string { + my $self = shift; + my $str = shift; + $str =~ s/"/\\"/g; + return "\"$str\""; +} + +sub _format_symbol { + my $self = shift; + my $str = shift; + return $self->_quote_string($str) if $str !~ /^${SYMBOL}$/; + return $str; +} + sub _format_amount { my $self = shift; my $amount = shift; @@ -276,10 +358,15 @@ sub _format_amount { my $format = "\% .$commodity->{frac}f"; my ($whole, $fraction) = split(/\./, sprintf($format, $amount)); + $fraction ||= 0; - my $num = join($commodity->{dchar}, commify($whole, $commodity->{gchar}), $fraction); + my $num = commify($whole, $commodity->{gchar}); + if ($commodity->{frac}) { + $num .= $commodity->{dchar} . $fraction; + } - $num = $commodity->{syprf} ? "$commodity->{symbol} $num" : "$num $commodity->{symbol}"; + my $symbol = $self->_format_symbol($commodity->{symbol}); + $num = $commodity->{syprf} ? "$symbol $num" : "$num $symbol"; return $num; }