233 lines
6.5 KiB
Perl
233 lines
6.5 KiB
Perl
package RevBank::Cart::Entry;
|
|
|
|
use v5.32;
|
|
use warnings;
|
|
use experimental 'signatures'; # stable since v5.36
|
|
|
|
use Carp qw(carp croak);
|
|
use RevBank::Users;
|
|
use List::Util ();
|
|
use Scalar::Util ();
|
|
|
|
# Workaround for @_ in signatured subs being experimental and controversial
|
|
my $NONE = \do { my $dummy };
|
|
sub _arg_provided($a) {
|
|
return 1 if not ref $a;
|
|
return Scalar::Util::refaddr($a) != Scalar::Util::refaddr($NONE)
|
|
}
|
|
|
|
sub new($class, $amount, $description, $attributes = {}) {
|
|
$amount = RevBank::Amount->parse_string($amount) if not ref $amount;
|
|
|
|
my $self = {
|
|
quantity => 1,
|
|
amount => $amount, # negative = pay, positive = add money
|
|
description => $description,
|
|
attributes => { %$attributes },
|
|
user => undef,
|
|
contras => [],
|
|
caller => List::Util::first(sub { !/^RevBank::Cart/ }, map { (caller $_)[3] } 1..10)
|
|
|| (caller 1)[3],
|
|
highlight => 1,
|
|
};
|
|
|
|
return bless $self, $class;
|
|
}
|
|
|
|
sub add_contra($self, $user, $amount, $description, $display = undef) {
|
|
# $display should be given for either ALL or NONE of the contras,
|
|
# with the exception of contras with $amount == 0.00;
|
|
|
|
$amount = RevBank::Amount->parse_string($amount) if not ref $amount;
|
|
$user = RevBank::Users::assert_user($user);
|
|
|
|
$description =~ s/\$you/$self->{user}/g if defined $self->{user};
|
|
|
|
push @{ $self->{contras} }, {
|
|
user => $user,
|
|
amount => $amount, # should usually have opposite sign (+/-)
|
|
description => $description, # contra user's perspective
|
|
display => $display, # interactive user's perspective
|
|
highlight => 1,
|
|
};
|
|
|
|
$self->attribute('changed', 1);
|
|
|
|
return $self; # for method chaining
|
|
}
|
|
|
|
sub has_attribute($self, $key) {
|
|
return (
|
|
exists $self->{attributes}->{$key}
|
|
and defined $self->{attributes}->{$key}
|
|
);
|
|
}
|
|
|
|
sub attribute($self, $key, $new = $NONE) {
|
|
my $ref = \$self->{attributes}->{$key};
|
|
$$ref = $new if _arg_provided($new);
|
|
return $$ref;
|
|
}
|
|
|
|
sub amount($self, $new = undef) {
|
|
my $ref = \$self->{amount};
|
|
if (defined $new) {
|
|
$new = RevBank::Amount->parse_string($new) if not ref $new;
|
|
$$ref = $new;
|
|
$self->attribute('changed', 1);
|
|
$self->{highlight_amount} = 1;
|
|
}
|
|
|
|
return $$ref;
|
|
}
|
|
|
|
sub quantity($self, $new = undef) {
|
|
my $ref = \$self->{quantity};
|
|
if (defined $new) {
|
|
$new >= 0 or croak "Quantity must be positive";
|
|
$$ref = $new;
|
|
$self->attribute('changed', 1);
|
|
$self->{highlight_quantity} = 1;
|
|
}
|
|
|
|
return $$ref;
|
|
}
|
|
|
|
sub multiplied($self) {
|
|
return $self->{quantity} != 1;
|
|
}
|
|
|
|
sub contras($self) {
|
|
# Shallow copy suffices for now, because there is no depth.
|
|
return map +{ %$_ }, @{ $self->{contras} };
|
|
}
|
|
|
|
sub delete_contras($self) {
|
|
$self->{contras} = [];
|
|
}
|
|
|
|
my $HI = "\e[37;1m";
|
|
my $LO = "\e[2m";
|
|
my $END = "\e[0m";
|
|
|
|
sub as_printable($self) {
|
|
my @s;
|
|
|
|
# Normally, the implied sign is "+", and an "-" is only added for negative
|
|
# numbers. Here, the implied sign is "-", and a "+" is only added for
|
|
# positive numbers.
|
|
my $q = $self->{quantity};
|
|
push @s, sprintf "%s%-4s%s" . "%s%8s%s" . " " . "%s%s%s",
|
|
($self->{highlight} || $self->{highlight_quantity} ? $HI : $LO),
|
|
($q > 1 || $self->{highlight_quantity} ? "${q}x" : ""),
|
|
($self->{highlight} ? "" : $END),
|
|
|
|
($self->{highlight} || $self->{highlight_amount} ? $HI : $LO),
|
|
$self->{amount}->string_flipped,
|
|
($self->{highlight} ? "" : $END),
|
|
|
|
($self->{highlight} ? $HI : $LO),
|
|
$self->{description},
|
|
$END;
|
|
|
|
for my $c (@{ $self->{contras} }) {
|
|
my $description;
|
|
my $amount = $self->{amount};
|
|
my $hidden = RevBank::Users::is_hidden($c->{user});
|
|
my $fromto = $c->{amount}->cents < 0 ? "<-" : "->";
|
|
$fromto .= " $c->{user}";
|
|
|
|
if ($c->{display}) {
|
|
$description =
|
|
$hidden
|
|
? ($ENV{REVBANK_DEBUG} ? "($fromto:) $c->{display}" : $c->{display})
|
|
: "$fromto: $c->{display}";
|
|
|
|
$amount *= -1;
|
|
} elsif ($hidden) {
|
|
next unless $ENV{REVBANK_DEBUG};
|
|
$description = "($fromto: $c->{description})";
|
|
} else {
|
|
$description = $fromto;
|
|
}
|
|
push @s, sprintf(
|
|
"%s%15s %s%s",
|
|
($self->{highlight} || $c->{highlight} ? $HI : $LO),
|
|
($self->{amount} > 0 ? $c->{amount}->string_flipped("") : $c->{amount}->string),
|
|
$description,
|
|
$END,
|
|
);
|
|
delete $c->{highlight};
|
|
}
|
|
delete $self->@{qw(highlight highlight_quantity highlight_amount)};
|
|
|
|
return @s;
|
|
}
|
|
|
|
sub as_loggable($self) {
|
|
croak "Loggable called before set_user" if not defined $self->{user};
|
|
|
|
my $quantity = $self->{quantity};
|
|
|
|
my @s;
|
|
for ($self, @{ $self->{contras} }) {
|
|
my $total = $quantity * $_->{amount};
|
|
|
|
my $description =
|
|
$quantity == 1
|
|
? $_->{description}
|
|
: sprintf("%s [%sx %s]", $_->{description}, $quantity, $_->{amount}->abs);
|
|
|
|
push @s, sprintf(
|
|
"%-12s %4s %3d %6s # %s",
|
|
$_->{user},
|
|
($total->cents > 0 ? 'GAIN' : $total->cents < 0 ? 'LOSE' : '===='),
|
|
$quantity,
|
|
$total->abs,
|
|
$description
|
|
);
|
|
}
|
|
|
|
return @s;
|
|
}
|
|
|
|
sub user($self, $new = undef) {
|
|
if (defined $new) {
|
|
croak "User can only be set once" if defined $self->{user};
|
|
|
|
$self->{user} = $new;
|
|
$_->{description} =~ s/\$you/$new/g for $self, @{ $self->{contras} };
|
|
}
|
|
|
|
return $self->{user};
|
|
}
|
|
|
|
sub sanity_check($self) {
|
|
my @contras = $self->contras;
|
|
|
|
my $sum = RevBank::Amount->new(
|
|
List::Util::sum(map $_->{amount}->cents, $self, @contras)
|
|
);
|
|
|
|
if ($sum != 0) {
|
|
local $ENV{REVBANK_DEBUG} = 1;
|
|
my $message = join("\n",
|
|
"BUG! (probably in $self->{caller})",
|
|
"Unbalanced transactions are not possible in double-entry bookkeeping.",
|
|
$self->as_printable,
|
|
(
|
|
!@contras
|
|
? "Use \$entry->add_contra to balance the transaction."
|
|
: abs($sum) == 2 * abs($self->{amount})
|
|
? "Contras for positive value should be negative values and vice versa."
|
|
: ()
|
|
),
|
|
);
|
|
RevBank::Plugins::call_hooks("log_error", "UNBALANCED ENTRY $message");
|
|
croak $message;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
1;
|