2017-02-11 06:33:15 +01:00
|
|
|
|
#!/usr/bin/env perl
|
|
|
|
|
use strict;
|
2017-03-05 14:09:58 +01:00
|
|
|
|
use utf8;
|
2017-02-11 06:33:15 +01:00
|
|
|
|
|
2017-02-21 22:29:13 +01:00
|
|
|
|
my ($db, %OPTS, $ACCOUNT, %targets);
|
2017-02-11 06:33:15 +01:00
|
|
|
|
|
2017-02-17 17:09:59 +01:00
|
|
|
|
package Commands {
|
2017-02-11 06:33:15 +01:00
|
|
|
|
use Text::Wrap qw(wrap);
|
|
|
|
|
use Carp qw(croak);
|
2017-02-17 17:09:59 +01:00
|
|
|
|
use Scalar::Util qw(openhandle);
|
2017-02-11 06:33:15 +01:00
|
|
|
|
|
2017-02-17 17:09:59 +01:00
|
|
|
|
use open qw/:std :encoding(utf8)/;
|
2017-02-11 06:33:15 +01:00
|
|
|
|
|
2017-02-21 22:29:13 +01:00
|
|
|
|
sub import {
|
|
|
|
|
$db = $_[1];
|
|
|
|
|
set_txn_mode(undef, $_[2] // "each");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub cond_txn_all (&);
|
|
|
|
|
sub cond_txn_each (&);
|
|
|
|
|
sub set_txn_mode {
|
|
|
|
|
my (undef, $mode) = @_;
|
|
|
|
|
|
|
|
|
|
my $with_txn = sub (&) { $db->storage->txn_do(shift) };
|
|
|
|
|
my $no_txn = sub (&) { goto &{$_[0]} };
|
|
|
|
|
|
|
|
|
|
no warnings 'redefine';
|
|
|
|
|
if ( $mode eq "all" ) {
|
|
|
|
|
*cond_txn_all = $with_txn;
|
|
|
|
|
*cond_txn_each = $no_txn;
|
|
|
|
|
}
|
|
|
|
|
elsif ( $mode eq "each" ) {
|
|
|
|
|
*cond_txn_all = $no_txn;
|
|
|
|
|
*cond_txn_each = $with_txn;
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
croak "Unsupported txn_mode $mode";
|
|
|
|
|
}
|
2017-02-17 17:09:59 +01:00
|
|
|
|
}
|
2017-02-11 06:33:15 +01:00
|
|
|
|
|
2017-02-21 22:29:13 +01:00
|
|
|
|
sub single_charge { # charge interactively
|
2017-02-11 06:33:15 +01:00
|
|
|
|
my $accounts = $db->resultset("Account");
|
|
|
|
|
|
|
|
|
|
_require_value();
|
|
|
|
|
_require_purpose();
|
|
|
|
|
_provide_date();
|
|
|
|
|
|
|
|
|
|
my $billId = delete $OPTS{'bill-id'}
|
|
|
|
|
and _require_target();
|
|
|
|
|
|
|
|
|
|
my $charger = sub {
|
|
|
|
|
my ($account, $billId) = @_;
|
2017-02-17 17:09:59 +01:00
|
|
|
|
my $what = $billId ? 'add_to_debits' : 'add_to_credits';
|
2017-02-11 06:33:15 +01:00
|
|
|
|
return $account->$what({
|
|
|
|
|
$billId ? (
|
2017-03-05 14:09:58 +01:00
|
|
|
|
billId => sprintf($billId, $account->ID),
|
2017-02-11 06:33:15 +01:00
|
|
|
|
targetCredit => delete( $OPTS{target} ),
|
|
|
|
|
) : (),
|
|
|
|
|
map { $_ => $OPTS{$_} } qw(value purpose date),
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if ( $ACCOUNT ) {
|
|
|
|
|
my $account = $accounts->find({ ID => $ACCOUNT });
|
2017-03-05 14:09:58 +01:00
|
|
|
|
my ($record, $transf_count);
|
2017-02-21 22:29:13 +01:00
|
|
|
|
cond_txn_each {
|
2017-03-05 14:09:58 +01:00
|
|
|
|
$record = $charger->($account, $billId);
|
2017-02-21 22:29:13 +01:00
|
|
|
|
if ( !$billId and my $t = $OPTS{target} ) {
|
2017-03-05 14:09:58 +01:00
|
|
|
|
$transf_count = $db->make_transfers( $record->ID => $t );
|
2017-02-21 22:29:13 +01:00
|
|
|
|
}
|
|
|
|
|
};
|
2017-03-05 14:09:58 +01:00
|
|
|
|
printf("Created %s, transferred %d of %d.\n",
|
|
|
|
|
$billId // $record->credId, $transf_count, $record->value
|
|
|
|
|
);
|
2017-02-11 06:33:15 +01:00
|
|
|
|
}
|
2017-02-17 17:09:59 +01:00
|
|
|
|
|
2017-03-05 14:09:58 +01:00
|
|
|
|
elsif ( $billId =~ s{ @ (\{)? ((?(1)\S+|\w+)) (?(1)\}) }{ %s }xms ) {
|
|
|
|
|
# ^^ regex to consider list of real or pseudonymized account IDs
|
|
|
|
|
# e.g. @Member | @{one,another,...} | @{M{xyz},M{mno}}
|
|
|
|
|
$accounts = $accounts->search({
|
|
|
|
|
$1 ? (ID => { -in => [ split /,/, $2 ] }) : ( type => $2 )
|
|
|
|
|
});
|
2017-02-21 22:29:13 +01:00
|
|
|
|
cond_txn_all {
|
|
|
|
|
while ( my $account = $accounts->next ) {
|
2017-03-05 14:09:58 +01:00
|
|
|
|
cond_txn_each { $charger->( $account, $billId ); }
|
2017-02-21 22:29:13 +01:00
|
|
|
|
}
|
|
|
|
|
};
|
2017-02-11 06:33:15 +01:00
|
|
|
|
}
|
2017-02-17 17:09:59 +01:00
|
|
|
|
|
2017-02-11 06:33:15 +01:00
|
|
|
|
else {
|
2017-02-17 17:09:59 +01:00
|
|
|
|
croak "You haven't specified an account to charge";
|
2017-02-11 06:33:15 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub _require_value {
|
|
|
|
|
until ( $OPTS{value} && $OPTS{value} =~ m{^\d+(\.\d\d)?$} ) {
|
2017-03-05 14:09:58 +01:00
|
|
|
|
print "Charge value in cent (or use decimal point): ";
|
2017-02-11 06:33:15 +01:00
|
|
|
|
chomp( $OPTS{value} = <STDIN> );
|
|
|
|
|
}
|
|
|
|
|
$_ = integer_money_value($_) for $OPTS{value};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub _require_purpose {
|
|
|
|
|
until ( $OPTS{purpose} ) {
|
|
|
|
|
print "Purpose: ";
|
|
|
|
|
chomp( $OPTS{purpose} = <STDIN> );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub _provide_date {
|
|
|
|
|
my ($YEAR, $MONTH, $DAY) = (localtime)[3,4,5];
|
|
|
|
|
$YEAR += 1900; $MONTH++;
|
|
|
|
|
until ( $OPTS{date} && $OPTS{date} =~ m{\d{4}-?\d\d-?\d\d} ) {
|
|
|
|
|
|
|
|
|
|
printf "Full date [YYYY-MM-DD] or year [%d]: ", $YEAR;
|
|
|
|
|
chomp( my $year = <STDIN> );
|
|
|
|
|
if ( !length($year) ) {}
|
|
|
|
|
elsif ( $year =~ m{^(\d{4})-(\d\d)-(\d\d)$} ) {
|
|
|
|
|
($YEAR, $MONTH, $DAY) = ($1, $2, $3);
|
|
|
|
|
goto ASSIGN;
|
|
|
|
|
}
|
|
|
|
|
elsif ( $year =~ m{^(\d\d)?\d\d$} && $year > 0 ) {
|
|
|
|
|
substr $year, 0, 0, 20 if !$1;
|
|
|
|
|
if ( $year ne $YEAR ) {
|
|
|
|
|
$YEAR = $year;
|
|
|
|
|
$_ = '' for $MONTH, $DAY;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else { redo; }
|
|
|
|
|
|
|
|
|
|
printf "Month [%d]: ", $MONTH;
|
|
|
|
|
chomp( my $month = <STDIN> );
|
|
|
|
|
if ( !length( $month ) && $MONTH ) {}
|
|
|
|
|
elsif ( $month !~ m{\D} && $month > 0 && $month < 13 ) {
|
|
|
|
|
if ( $month != $MONTH ) {
|
|
|
|
|
$MONTH = $month;
|
|
|
|
|
$DAY = '';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else { redo; }
|
|
|
|
|
|
|
|
|
|
printf "Day [%d]: ", $DAY;
|
|
|
|
|
chomp( my $day = <STDIN> );
|
|
|
|
|
if ( !length( $day ) ) {}
|
|
|
|
|
elsif ( $day !~ m{\D} && $day > 0 && $month < 31 ) {
|
|
|
|
|
$DAY = $day;
|
|
|
|
|
}
|
|
|
|
|
else { redo; }
|
|
|
|
|
|
|
|
|
|
ASSIGN:
|
|
|
|
|
$OPTS{date} = sprintf "%4d-%02d-%02d", $YEAR, $MONTH, $DAY;
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub _require_target {
|
|
|
|
|
my $t;
|
|
|
|
|
until ( $t = $OPTS{target} and $t =~ m{^\d+$} ) {
|
|
|
|
|
unless ( $t ) {
|
|
|
|
|
print "Target credit id: ";
|
|
|
|
|
chomp( my $t = <STDIN> );
|
|
|
|
|
}
|
|
|
|
|
if ( $t =~ m{ \A \? (\w+) \z } ) {
|
2017-03-05 14:09:58 +01:00
|
|
|
|
$OPTS{target} = select_target_credit_from($t);
|
2017-02-11 06:33:15 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub charge_account {
|
2017-02-21 22:29:13 +01:00
|
|
|
|
goto &single_charge if %OPTS;
|
2017-02-11 06:33:15 +01:00
|
|
|
|
|
2017-02-21 22:29:13 +01:00
|
|
|
|
print "Enter records ...\n" if -t STDIN;
|
2017-02-11 06:33:15 +01:00
|
|
|
|
|
2017-03-05 14:09:58 +01:00
|
|
|
|
local *ARGV = (
|
|
|
|
|
openhandle($_[0])
|
|
|
|
|
// croak "charge_account() expects open file handle to read from"
|
|
|
|
|
) if my $abort_on_exception = exists $_[0];
|
2017-02-17 17:09:59 +01:00
|
|
|
|
|
2017-02-11 06:33:15 +01:00
|
|
|
|
# Possible line formats:
|
|
|
|
|
# -------------------------------------------------------
|
2017-02-21 22:29:13 +01:00
|
|
|
|
#
|
2017-02-17 17:09:59 +01:00
|
|
|
|
# Date Account Var Amount Purpose :: Credit (Payment or target)
|
2017-02-21 22:29:13 +01:00
|
|
|
|
# ^^^ Field may be empty or '+', yet not omitted
|
2017-02-17 17:09:59 +01:00
|
|
|
|
# Date Account Amount TCr ID: Purpose :: Bill
|
2017-02-21 22:29:13 +01:00
|
|
|
|
# ^^^ Field may be empty, yet not omitted
|
|
|
|
|
#
|
|
|
|
|
# Please note: Omit Account if it is specified on commandline.
|
|
|
|
|
#
|
|
|
|
|
|
|
|
|
|
cond_txn_all {
|
|
|
|
|
|
|
|
|
|
while ( defined( my $line = <ARGV> ) ) {
|
|
|
|
|
|
|
|
|
|
_strip_comment_from($line) // next;
|
|
|
|
|
|
2017-03-05 14:09:58 +01:00
|
|
|
|
my ($sep) = $line =~ m{([,; \t|])};
|
|
|
|
|
|
2017-02-21 22:29:13 +01:00
|
|
|
|
my @columns
|
2017-03-05 14:09:58 +01:00
|
|
|
|
= split /[$sep][ ]*/, $line,
|
2017-02-21 22:29:13 +01:00
|
|
|
|
defined($ACCOUNT) ? 4 : 5;
|
|
|
|
|
|
|
|
|
|
splice @columns, 1, 0, $ACCOUNT // ();
|
2017-03-05 14:09:58 +01:00
|
|
|
|
|
|
|
|
|
# Strip quotes from all but ultimate purpose
|
|
|
|
|
for ( @columns[ 0 .. 3 ] ) {
|
|
|
|
|
m{\A(["'])}xms && m{$1\z}xms or next;
|
|
|
|
|
substr $_, 0, 1, '';
|
|
|
|
|
substr $_, -1, 1, '';
|
|
|
|
|
}
|
|
|
|
|
my ($record, $transf_count)
|
|
|
|
|
= eval { cond_txn_each { _inner_charge(@columns) } };
|
|
|
|
|
|
|
|
|
|
if ( !defined $record ) {
|
2017-02-21 22:29:13 +01:00
|
|
|
|
if ( $@ ) {
|
|
|
|
|
die $@ if $abort_on_exception;
|
|
|
|
|
warn $@;
|
2017-03-05 14:09:58 +01:00
|
|
|
|
next;
|
2017-02-21 22:29:13 +01:00
|
|
|
|
}
|
2017-03-05 14:09:58 +01:00
|
|
|
|
else { die '_inner_charge() returned nothing'; }
|
2017-02-21 22:29:13 +01:00
|
|
|
|
};
|
2017-03-05 14:09:58 +01:00
|
|
|
|
|
|
|
|
|
printf("Created %s, transferred %d of %d.\n",
|
|
|
|
|
eval { $record->billId } // $record->credId,
|
|
|
|
|
$transf_count, $record->value
|
|
|
|
|
);
|
2017-02-21 22:29:13 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-02-11 06:33:15 +01:00
|
|
|
|
|
2017-02-21 22:29:13 +01:00
|
|
|
|
sub _inner_charge {
|
|
|
|
|
my ($date, $account, $debit, $credit, $purpose) = @_;
|
2017-02-11 06:33:15 +01:00
|
|
|
|
|
2017-02-21 22:29:13 +01:00
|
|
|
|
$account = $db->resultset("Account")->find($account)
|
|
|
|
|
// croak "There is no account with that name: $account";
|
|
|
|
|
|
|
|
|
|
chomp $credit if !defined $purpose;
|
|
|
|
|
|
2017-03-05 14:09:58 +01:00
|
|
|
|
{ # In order to slurp any consecutive lines take two cases into account:
|
|
|
|
|
# 1) Purpose has not begun next from value on initial line ->
|
|
|
|
|
# Grab every line before the next empty one (must have no length).
|
|
|
|
|
# Consider any quotes to be literal.
|
|
|
|
|
# 2) Grab everything until closing quote is recognized.
|
|
|
|
|
# This is the case when the quote is the same as the opening one,
|
|
|
|
|
# it in not doubled and before a space or the end of line.
|
|
|
|
|
|
|
|
|
|
my ($quot, $end, $has_more, $line);
|
|
|
|
|
|
|
|
|
|
$DB::single = 1;
|
|
|
|
|
|
|
|
|
|
if ( defined $purpose ) {
|
|
|
|
|
$has_more = $purpose !~ s{ \A (?<quot> ["'] ) }{}xms
|
|
|
|
|
&& $purpose =~ m{ : \s* \z }xms;
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
$has_more = 1;
|
|
|
|
|
($purpose = <ARGV>) =~ s{^\s+}{};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
while (1) { # Single iteration, either early closed or redone
|
|
|
|
|
|
|
|
|
|
$has_more
|
|
|
|
|
or # we are inside region wrapped by identical quotes:
|
|
|
|
|
($quot = $+{quot}) .. ( $end = $purpose =~ s{
|
|
|
|
|
\G # from position of last iteration
|
|
|
|
|
(?<![$quot\\]) # must not be preseded by escape or
|
|
|
|
|
($quot) # same quote
|
|
|
|
|
(?=\s|\z) # before space or end of line
|
|
|
|
|
}{}goxms
|
|
|
|
|
)
|
|
|
|
|
or last # because there are no consecutive lines.
|
|
|
|
|
;
|
|
|
|
|
|
|
|
|
|
last if (!$+{quot} && $end) # we reach closing quote
|
|
|
|
|
|| !defined($line = <ARGV>) # there is no next line
|
|
|
|
|
|| ($has_more && $line eq $/) # empty line
|
|
|
|
|
;
|
|
|
|
|
|
2017-02-21 22:29:13 +01:00
|
|
|
|
$line =~ s{^\s+}{};
|
2017-03-05 14:09:58 +01:00
|
|
|
|
$line =~ s{$quot{2}}{$quot}g if $quot; # merge consecutive quotes
|
|
|
|
|
$purpose .= $line;
|
|
|
|
|
|
2017-02-21 22:29:13 +01:00
|
|
|
|
}
|
2017-03-05 14:09:58 +01:00
|
|
|
|
|
|
|
|
|
chomp $purpose;
|
2017-02-21 22:29:13 +01:00
|
|
|
|
_strip_comment_from($purpose);
|
2017-03-05 14:09:58 +01:00
|
|
|
|
|
2017-02-21 22:29:13 +01:00
|
|
|
|
}
|
2017-02-11 06:33:15 +01:00
|
|
|
|
|
2017-03-05 14:09:58 +01:00
|
|
|
|
my ($record, @transfer, $category);
|
|
|
|
|
|
|
|
|
|
if ( $purpose =~ s{ \s* -C\[ ([^\r\t\n\]]+) \] \z }{}xms ) {
|
|
|
|
|
$category = $db->resultset("Category")->search({
|
|
|
|
|
$1 =~ /^\d+$/ ? (ID => $1) : (label => { -like => "$1%" })
|
|
|
|
|
})->single->ID;
|
|
|
|
|
}
|
2017-02-11 06:33:15 +01:00
|
|
|
|
|
2017-03-05 14:09:58 +01:00
|
|
|
|
if ( $debit =~ /\b\d/ ) {
|
2017-02-11 06:33:15 +01:00
|
|
|
|
|
2017-02-21 22:29:13 +01:00
|
|
|
|
my $tgt = $credit;
|
2017-03-05 14:09:58 +01:00
|
|
|
|
my $billId = $purpose =~ s{ \A ([[:graph:]]+?) : \s* }{}xms
|
2017-02-21 22:29:13 +01:00
|
|
|
|
? $1
|
|
|
|
|
: croak qq{Missing ID in debit purpose: $purpose}
|
|
|
|
|
;
|
2017-02-11 06:33:15 +01:00
|
|
|
|
|
2017-02-21 22:29:13 +01:00
|
|
|
|
if ( $purpose =~ s{ (?: \t | \s* << \s* ) (.+?) \r? \n? \z }{}xms ) {
|
|
|
|
|
push @transfer, [
|
|
|
|
|
map { /\D/ ? $targets{ $_ } : $_ } split /,\s*/, $1
|
|
|
|
|
] => $billId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( my ($var, $op, $assigned)
|
2017-03-05 14:09:58 +01:00
|
|
|
|
= $tgt =~ m{ \A ([A-Za-z]\w+) ([?=]) (\w+) \z }xms
|
2017-02-21 22:29:13 +01:00
|
|
|
|
) {
|
2017-02-11 06:33:15 +01:00
|
|
|
|
|
2017-02-21 22:29:13 +01:00
|
|
|
|
if ( defined( my $c = $targets{$var} ) ) {
|
|
|
|
|
croak "$var already assigned to $c";
|
2017-02-11 06:33:15 +01:00
|
|
|
|
}
|
2017-02-21 22:29:13 +01:00
|
|
|
|
|
|
|
|
|
if ( $op eq q{=} ) {
|
|
|
|
|
$tgt = $targets{ $var } = $assigned;
|
2017-02-11 06:33:15 +01:00
|
|
|
|
}
|
2017-02-21 22:29:13 +01:00
|
|
|
|
else {
|
|
|
|
|
$tgt = select_target_credit_from($assigned, $var);
|
2017-02-11 06:33:15 +01:00
|
|
|
|
}
|
2017-02-17 17:09:59 +01:00
|
|
|
|
|
2017-02-21 22:29:13 +01:00
|
|
|
|
}
|
|
|
|
|
elsif ( $tgt =~ /^[a-z]/i ) {
|
|
|
|
|
$tgt = $targets{ $tgt }
|
|
|
|
|
// croak "Target credit not assigned: $tgt!\n";
|
2017-02-11 06:33:15 +01:00
|
|
|
|
}
|
2017-03-05 14:09:58 +01:00
|
|
|
|
else { $tgt = undef; }
|
2017-02-17 17:09:59 +01:00
|
|
|
|
|
2017-03-05 14:09:58 +01:00
|
|
|
|
$record = $account->add_to_debits({
|
2017-02-21 22:29:13 +01:00
|
|
|
|
billId => $billId,
|
|
|
|
|
date => $date,
|
|
|
|
|
purpose => $purpose,
|
|
|
|
|
value => integer_money_value($debit),
|
|
|
|
|
targetCredit => $tgt,
|
2017-03-05 14:09:58 +01:00
|
|
|
|
category => $category
|
2017-02-21 22:29:13 +01:00
|
|
|
|
});
|
2017-02-17 17:09:59 +01:00
|
|
|
|
|
2017-02-21 22:29:13 +01:00
|
|
|
|
}
|
2017-02-17 17:09:59 +01:00
|
|
|
|
|
2017-03-05 14:09:58 +01:00
|
|
|
|
elsif ( $credit =~ /\b\d/ ) {
|
2017-02-17 17:09:59 +01:00
|
|
|
|
|
2017-02-21 22:29:13 +01:00
|
|
|
|
my $tgt = $purpose =~ s{ \s* >> \s* (.+) \r? \n? \z }{}xms && $1;
|
2017-02-17 17:09:59 +01:00
|
|
|
|
|
2017-02-21 22:29:13 +01:00
|
|
|
|
my $c = $account->add_to_credits({
|
|
|
|
|
date => $date,
|
|
|
|
|
account => $account,
|
|
|
|
|
purpose => $purpose,
|
2017-03-05 14:09:58 +01:00
|
|
|
|
value => integer_money_value($credit),
|
|
|
|
|
category => $category
|
2017-02-21 22:29:13 +01:00
|
|
|
|
});
|
2017-02-17 17:09:59 +01:00
|
|
|
|
|
2017-02-21 22:29:13 +01:00
|
|
|
|
if ( $debit =~ /^[a-z]/i ) { $targets{ $debit } = $c->credId; }
|
|
|
|
|
elsif ( length $debit && $debit ne q{+} ) {
|
|
|
|
|
croak "Invalid field input: $debit neither number nor name";
|
2017-02-11 06:33:15 +01:00
|
|
|
|
}
|
2017-02-21 22:29:13 +01:00
|
|
|
|
|
|
|
|
|
push @transfer, $c->credId => $tgt if $tgt;
|
|
|
|
|
|
2017-03-05 14:09:58 +01:00
|
|
|
|
$record = $c;
|
2017-02-11 06:33:15 +01:00
|
|
|
|
}
|
|
|
|
|
|
2017-02-21 22:29:13 +01:00
|
|
|
|
else {
|
|
|
|
|
croak "Cannot be parsed: $debit $credit\n";
|
2017-02-11 06:33:15 +01:00
|
|
|
|
}
|
2017-02-21 22:29:13 +01:00
|
|
|
|
|
2017-03-05 14:09:58 +01:00
|
|
|
|
return $record, @transfer ? $db->make_transfers(@transfer) : 0;
|
2017-02-21 22:29:13 +01:00
|
|
|
|
|
2017-02-11 06:33:15 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub _strip_comment_from {
|
|
|
|
|
my $comment;
|
|
|
|
|
$comment .= $1 while $_[0] =~ s{ \s+ (?<!\\) \# (.*) $ }{}gxm;
|
|
|
|
|
$comment =~ s{\\#}{#}gxms;
|
|
|
|
|
return( $comment or length($_[0]) ? q{} : undef );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub integer_money_value {
|
2017-03-05 14:09:58 +01:00
|
|
|
|
|
2017-02-11 06:33:15 +01:00
|
|
|
|
my $val = shift;
|
2017-03-05 14:09:58 +01:00
|
|
|
|
|
|
|
|
|
# Strip currency signs and space.
|
|
|
|
|
$val =~ s{ \A [^0-9]+ }{}xms;
|
|
|
|
|
$val =~ s{ [^0-9]+ \z }{}xms;
|
|
|
|
|
|
|
|
|
|
# Strip cent to finally add to the integer
|
|
|
|
|
my $cent = $val =~ s{ [,.] ([0-9]{2})? \z }{}xms ? $1 || 0 : undef;
|
|
|
|
|
|
|
|
|
|
# Strip number formatting separators
|
|
|
|
|
$val =~ s{ (?<=[0-9]) [ _.,'’] (?=[0-9]) }{}gxms;
|
|
|
|
|
|
|
|
|
|
die "Not a positive money value (cent): $val" if $val =~ /[^0-9]/;
|
|
|
|
|
|
|
|
|
|
($val *= 100) += $cent if defined $cent;
|
2017-02-21 22:29:13 +01:00
|
|
|
|
return $val;
|
2017-02-11 06:33:15 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub select_target_credit_from {
|
2017-02-17 17:09:59 +01:00
|
|
|
|
my ($account, $var) = @_;
|
2017-02-11 06:33:15 +01:00
|
|
|
|
|
|
|
|
|
croak "Interactive target credit selection is not possible"
|
|
|
|
|
if !( -t STDIN );
|
|
|
|
|
|
2017-02-17 17:09:59 +01:00
|
|
|
|
print "Please select target credit to reference by $var:\n",
|
|
|
|
|
"-------------------------------------------------\n";
|
|
|
|
|
|
2017-02-11 06:33:15 +01:00
|
|
|
|
$account = $db->resultset("Account")->find($account);
|
|
|
|
|
my $credits = $account->credits->search({}, {
|
|
|
|
|
join => 'income',
|
|
|
|
|
group_by => ['income.targetCredit'],
|
|
|
|
|
having => \[ 'ifnull(sum(income.paid),0) = me.value' ],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
croak $account->ID
|
|
|
|
|
. " has no credits to receive settlements of debt"
|
|
|
|
|
if !$credits->count;
|
|
|
|
|
|
|
|
|
|
while ( my $c = $credits->next ) {
|
|
|
|
|
printf "%d) %s %s\n", $c->credId, $c->date, $c->purpose;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
while (1) {
|
|
|
|
|
print "Which credit do you want to target? ";
|
|
|
|
|
chomp( my $input = <STDIN> );
|
2017-02-17 17:09:59 +01:00
|
|
|
|
return $targets{ $var } = $input if $credits->find($input);
|
2017-02-11 06:33:15 +01:00
|
|
|
|
warn "Credit ID $input is not a listed option.\n";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub make_transfers {
|
|
|
|
|
print "Make transfers ...\n";
|
|
|
|
|
|
|
|
|
|
if ( ! -t STDIN ) {
|
2017-02-17 17:09:59 +01:00
|
|
|
|
croak "Input expected from interactive terminal";
|
2017-02-11 06:33:15 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
my $search_opts = { order_by => { -asc => ['date'] } };
|
|
|
|
|
my $avcredits = $db->resultset("AvailableCredits")->search(
|
|
|
|
|
{ $OPTS{source} ? (
|
|
|
|
|
credId => { -in => TrsrDB::expand_ids($OPTS{source}) }
|
|
|
|
|
) : ()
|
|
|
|
|
},
|
|
|
|
|
$search_opts
|
|
|
|
|
);
|
|
|
|
|
my $transfers = $db->resultset("Transfer");
|
|
|
|
|
|
|
|
|
|
CREDIT:
|
|
|
|
|
while ( my $c = $avcredits->next ) {
|
|
|
|
|
|
|
|
|
|
printf " * [%d] %s has %d available in credit %d) %s\n",
|
|
|
|
|
$c->date, $c->account->ID, $c->difference, $c->credId, $c->purpose;
|
|
|
|
|
|
|
|
|
|
my $suggestions = $c->suggested_to_pay->search(
|
|
|
|
|
{ $OPTS{target} ? (
|
|
|
|
|
billId => { -in => TrsrDB::expand_ids($OPTS{target}) }
|
|
|
|
|
) : ()
|
|
|
|
|
},
|
|
|
|
|
$search_opts
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
my $last_input = q{ };
|
|
|
|
|
|
2017-02-21 22:29:13 +01:00
|
|
|
|
cond_txn_all {
|
|
|
|
|
|
|
|
|
|
DEBIT:
|
|
|
|
|
while ( my $s = $suggestions->next ) {
|
|
|
|
|
printf " - [%s] %s: %s? (%s)\b\b", $s->date, $s->billId,
|
|
|
|
|
$s->purpose, $last_input;
|
|
|
|
|
chomp( my $input = <STDIN> );
|
|
|
|
|
$input = $last_input if !length($input);
|
|
|
|
|
if ( lc($input) =~ /^y(es)?$/i ) {
|
|
|
|
|
$transfers->create({
|
|
|
|
|
credId => $c->credId, billId => $s->billId
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
elsif ( !$input || lc($input) !~ /^no?$/i ) {
|
|
|
|
|
print "Input invalid!\n";
|
|
|
|
|
redo DEBIT;
|
|
|
|
|
}
|
|
|
|
|
$last_input = $input;
|
2017-02-11 06:33:15 +01:00
|
|
|
|
}
|
|
|
|
|
|
2017-02-21 22:29:13 +01:00
|
|
|
|
}
|
2017-02-11 06:33:15 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub balance_status {
|
|
|
|
|
my ($with_report) = @_;
|
|
|
|
|
print "Get balance status ...\n";
|
|
|
|
|
my $results = $db->resultset("Balance");
|
|
|
|
|
my $header = "\tavailbl\tearned\tspent\tpromise\tarrears\teven_until\n";
|
|
|
|
|
print "ID", $header unless $with_report;
|
|
|
|
|
while ( my $r = $results->next ) {
|
|
|
|
|
my %val = $r->get_columns();
|
|
|
|
|
if ($with_report) { _balance_report( $val{ID} ) }
|
2017-03-05 14:09:58 +01:00
|
|
|
|
print join "\t", $with_report ? "Balance:\n$header" : $val{ID},
|
|
|
|
|
map({ sprintf '%7d', $val{$_} } qw{
|
|
|
|
|
available earned spent promised arrears
|
|
|
|
|
}), $val{even_until};
|
2017-02-11 06:33:15 +01:00
|
|
|
|
print "\n";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub _balance_report {
|
|
|
|
|
my ($id) = @_;
|
|
|
|
|
print "Report of account $id:\n", "=" x 76, "\n";
|
|
|
|
|
my $rs = $db->resultset("Account")->find($id)->report();
|
|
|
|
|
my ($i, $state) = (0, 1);
|
|
|
|
|
|
|
|
|
|
my $c = 0;
|
|
|
|
|
|
|
|
|
|
my $print_subline = sub {
|
|
|
|
|
my $row = shift;
|
|
|
|
|
$row->{value} /= 100;
|
|
|
|
|
my $tab = " " x (2 + length $c);
|
|
|
|
|
print wrap( $row->{value} > 0 ? "$c. " : $tab, $tab, sprintf(
|
|
|
|
|
"\t%+f\t%s @ %s\n", @{$row}{qw/value purpose date/}
|
|
|
|
|
));
|
|
|
|
|
};
|
|
|
|
|
my $print_remainder = sub {
|
|
|
|
|
my $c = $db->resultset("AvailableCredits")->find($c) // return;
|
|
|
|
|
print " " x length( $c->credId ), " (+", $c->difference,
|
|
|
|
|
" left available.)";
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
while ( my $row = $rs->next ) {
|
|
|
|
|
$i++;
|
|
|
|
|
my %d = $row->get_columns();
|
|
|
|
|
if ( $c && !defined $d{credId} ) {
|
|
|
|
|
print "--- Current arrears: ---\n";
|
|
|
|
|
$c = 0;
|
|
|
|
|
redo;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$d{credId} //= 0;
|
|
|
|
|
if ( $d{value} > 0 ) {
|
|
|
|
|
$print_remainder->() if $c;
|
|
|
|
|
$c = $d{credId};
|
|
|
|
|
}
|
|
|
|
|
elsif( $d{credId} != $c ) {
|
|
|
|
|
die "Unexpected transfer for credId $d{credId}!\n";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$print_subline->(\%d);
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( !$i ) {
|
|
|
|
|
print "Account is evened out, alright.\n";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-02-17 17:09:59 +01:00
|
|
|
|
} # End of package Commands
|
|
|
|
|
|
|
|
|
|
return 1 if caller;
|
|
|
|
|
|
2017-02-11 06:33:15 +01:00
|
|
|
|
package main;
|
|
|
|
|
|
2017-02-17 17:09:59 +01:00
|
|
|
|
use Getopt::Long;
|
2017-03-05 14:09:58 +01:00
|
|
|
|
use FindBin qw($Bin);
|
|
|
|
|
use lib $Bin;
|
|
|
|
|
BEGIN {
|
|
|
|
|
if (!( $ARGV[0] eq 'makedb'
|
|
|
|
|
|| $ARGV[0] eq 'server'
|
|
|
|
|
|| $ARGV[0] eq 'sql'
|
|
|
|
|
)) {
|
|
|
|
|
require TrsrDB;
|
|
|
|
|
TrsrDB->import(\$db);
|
|
|
|
|
}
|
|
|
|
|
elsif ( $ARGV[0] eq 'makedb' && -e $ENV{TRSRDB_SQLITE_FILE} ) {
|
|
|
|
|
die "$ENV{TRSRDB_SQLITE_FILE} does exist.\n";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Commands::set_txn_mode(undef, "each");
|
2017-02-17 17:09:59 +01:00
|
|
|
|
|
|
|
|
|
%OPTS = (
|
|
|
|
|
'account' => \$ACCOUNT,
|
2017-02-21 22:29:13 +01:00
|
|
|
|
'target-credit' => \%targets,
|
|
|
|
|
'txn-mode' => \&Commands::set_txn_mode,
|
2017-02-17 17:09:59 +01:00
|
|
|
|
);
|
|
|
|
|
GetOptions( \%OPTS,
|
2017-02-21 22:29:13 +01:00
|
|
|
|
'txn-mode=s', 'account|a=s', 'target|t=s', 'target-credit|tcr=i', 'source|s=s',
|
2017-02-17 17:09:59 +01:00
|
|
|
|
'date|d=s', 'value|v=i', 'purpose|p=s', 'bill-id|b=s'
|
|
|
|
|
);
|
2017-02-21 22:29:13 +01:00
|
|
|
|
delete @OPTS{'account', 'target-credit', 'txn-mode'};
|
2017-02-17 17:09:59 +01:00
|
|
|
|
|
|
|
|
|
my %dispatcher = (
|
|
|
|
|
charge => \&Commands::charge_account,
|
|
|
|
|
ct => sub {
|
|
|
|
|
Commands::charge_account();
|
|
|
|
|
Commands::make_transfers();
|
|
|
|
|
},
|
|
|
|
|
cts => sub {
|
|
|
|
|
Commands::charge_account();
|
|
|
|
|
Commands::make_transfers();
|
|
|
|
|
Commands::balance_status();
|
|
|
|
|
},
|
|
|
|
|
ctr => sub {
|
|
|
|
|
Commands::charge_account();
|
|
|
|
|
Commands::make_transfers();
|
|
|
|
|
Commands::balance_status(1);
|
|
|
|
|
},
|
2017-03-05 14:09:58 +01:00
|
|
|
|
makedb => sub {
|
|
|
|
|
chdir($Bin);
|
|
|
|
|
exec q[cat schema/tables.sql schema/*/*.sql]
|
|
|
|
|
. q[| sqlite3 $TRSRDB_SQLITE_FILE];
|
|
|
|
|
},
|
2017-02-17 17:09:59 +01:00
|
|
|
|
report => sub {
|
|
|
|
|
Commands::balance_status(1);
|
|
|
|
|
},
|
|
|
|
|
server => sub {
|
|
|
|
|
exec qw{morbo server -w TrsrDB -w templates/};
|
|
|
|
|
},
|
|
|
|
|
sql => sub {
|
2017-02-21 22:29:13 +01:00
|
|
|
|
exec "sqlite3",
|
|
|
|
|
-cmd => "PRAGMA foreign_keys=ON; PRAGMA recursive_triggers=ON;",
|
|
|
|
|
'-line', $ENV{TRSRDB_SQLITE_FILE};
|
2017-02-17 17:09:59 +01:00
|
|
|
|
},
|
|
|
|
|
status => \&Commands::balance_status,
|
|
|
|
|
transfer => \&Commands::make_transfers,
|
|
|
|
|
tr => sub {
|
|
|
|
|
Commands::make_transfers();
|
|
|
|
|
Commands::balance_status(1);
|
|
|
|
|
},
|
|
|
|
|
ts => sub {
|
|
|
|
|
Commands::make_transfers();
|
|
|
|
|
Commands::balance_status();
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
2017-02-11 06:33:15 +01:00
|
|
|
|
my $cmd = shift;
|
|
|
|
|
if ( my $sref = $dispatcher{ $cmd } ) {
|
|
|
|
|
$sref->();
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
die qq{Command not supported: $cmd\n};
|
|
|
|
|
}
|
|
|
|
|
|