supporters/scripts/t-shirt-label-print.plx
Bradley M. Kuhn ca8348d84b Rework various aspects of shirt label printing
The primary change here is to add output such that we can get CSV
output that can be later used in post-processing to upload the data
to  third-party shipping services.

Various other changes are present as well, including changes to the
logic related to which t-shirt to give when no specific type is
requested.
2022-11-21 17:31:17 -08:00

315 lines
13 KiB
Perl

#!/usr/bin/perl
# License: AGPL-3.0-or-later
use strict;
use warnings;
use autodie qw(open close chdir);
use DBI;
use Encode qw(encode decode);
use Text::CSV; # libtext-csv-perl in Debian
use YAML::XS qw(LoadFile);
use LaTeX::Encode;
use Supporters;
use utf8;
use IPC::Shareable;
require 'bean-query-daemon-lib.pl';
binmode STDOUT, ":utf8";
BeancountQueryInitialize();
if (@ARGV < 8 or @ARGV > 9) {
print STDERR "usage: $0 <SUPPORTERS_SQLITE_DB_FILE> <GIVING_LIMIT> <T-SHIRT-STYLE> <SIZE_COUNTS> <ID_IN_USA_FILE> <MONTHLY_SEARCH_REGEX> <ANNUAL_SEARCH_REGEX> <VERBOSE>\n";
exit 1;
}
my($SUPPORTERS_SQLITE_DB_FILE, $GIVING_LIMIT, $T_SHIRT_STYLE, $SIZE_COUNT_FILE, $ID_IN_USA_FILE, $MONTHLY_SEARCH_REGEX, $ANNUAL_SEARCH_REGEX, $VERBOSE) = @ARGV;
$VERBOSE = 0 if not defined $VERBOSE;
open(my $idsInUSAFH, "<", $ID_IN_USA_FILE);
my %idsKnownToBeInUSA;
while (my $idInUSA = <$idsInUSAFH>) {
chomp $idInUSA;
$idsKnownToBeInUSA{$idInUSA} = 1;
}
my(@usaRows, @intRows);
my($sizeCounts);
$sizeCounts = LoadFile $SIZE_COUNT_FILE if (defined $SIZE_COUNT_FILE and -r $SIZE_COUNT_FILE);
my $dbh = DBI->connect("dbi:SQLite:dbname=$SUPPORTERS_SQLITE_DB_FILE", "", "",
{ RaiseError => 1, sqlite_unicode => 1 })
or die $DBI::errstr;
my $fileName = BeancountQuerySubmit(<<SUPPORTESR_DB_READ
SELECT ANY_META("program") as program, date, ANY_META("entity") as entity, NUMBER(COST(position))
WHERE account ~ "^Income" AND ANY_META("project") ~ "Conservancy"
AND ANY_META("program") ~ "Conservancy:Supporters:" AND NOT ANY_META("entity") ~ "^\\s*\$"
SUPPORTESR_DB_READ
, 'text');
open(my $fh, "<", $fileName);
my $sp = new Supporters($dbh, $fh, { monthly => $MONTHLY_SEARCH_REGEX, annual => $ANNUAL_SEARCH_REGEX});
my(@supporterIds) = $sp->findDonor({});
my $overallCount = 0;
my %lines;
my @typeList;
my @oldTypeList;
if ($T_SHIRT_STYLE eq 'ONLY-fy2021design') {
@typeList = qw/t-shirt-fy2021design-0/;
} elsif ($T_SHIRT_STYLE eq 'fy2021design') {
@typeList = qw/t-shirt-fy2021design-0 t-shirt-fy2018design-0 t-shirt-vintage-0/;
@oldTypeList = qw/t-shirt-0 t-shirt-1 t-shirt-extra-0 t-shirt-extra-1/;
} elsif ($T_SHIRT_STYLE eq 'fy2018design') {
@typeList = qw/t-shirt-fy2018design-0 t-shirt-vintage-0/;
@oldTypeList = qw/t-shirt-0 t-shirt-1 t-shirt-extra-0 t-shirt-extra-1/;
} elsif ($T_SHIRT_STYLE == 0) {
@typeList = qw/t-shirt-0 t-shirt-extra-0/;
} elsif ($T_SHIRT_STYLE == 1) {
@oldTypeList = qw/t-shirt-0 t-shirt-extra-0/;
@typeList = qw/t-shirt-1 t-shirt-extra-1/;
} else {
die "Unknown t-shirt style given: $T_SHIRT_STYLE";
}
my %requestData;
my $totalSent = 0;
foreach my $id (@supporterIds) {
my $lastGaveDate = $sp->donorLastGave($id);
my $totalDonated = $sp->donorTotalGaveInPeriod(donorId => $id);
$lastGaveDate = "0000-01-01" if not defined $lastGaveDate;
my $ledgerEntityId = $sp->getLedgerEntityId($id);
foreach my $type (@typeList, @oldTypeList) {
my $request = $sp->getRequest({ donorId => $id, requestType => $type,
ignoreHeldRequests => 1, ignoreFulfilledRequests => 1 });
if (defined $request and defined $request->{requestType}) {
$requestData{$id}{lastGaveDate} = $lastGaveDate unless defined $requestData{$id}{lastGaveDate};
$requestData{$id}{oldestShirtDate} = '9999-12-31' unless defined $requestData{$id}{oldestShirtDate};
$requestData{$id}{forSortingbySize} = "" unless defined $requestData{$id}{forSortingbySize};
$requestData{$id}{shirts}{$request->{requestType}} = $request;
$requestData{$id}{forSortingbySize} = $request->{requestConfiguration}
if ($requestData{$id}{forSortingbySize} eq "" and defined $request->{requestConfiguration});
$requestData{$id}{oldestShirtDate} = $request->{requestDate} if ($request->{requestDate} lt $requestData{$id}{oldestShirtDate});
$requestData{$id}{total} = $totalDonated;
}
}
}
sub sortFunction($$) { return (($requestData{$_[1]}{total} <=> $requestData{$_[0]}{total}) or
($requestData{$_[1]}{lastGaveDate} cmp $requestData{$_[0]}{lastGaveDate}) or
($requestData{$_[0]}{oldestShirtDate} cmp $requestData{$_[1]}{oldestShirtDate}) or
($requestData{$_[0]}{forSortingbySize} cmp $requestData{$_[1]}{forSortingbySize}) or
($_[0] <=> $_[1]));
}
my %need;
foreach my $id (sort { sortFunction($a, $b); } keys %requestData) {
next if $id == 20 or $id == 70 or $id == 670 or $id == 34;
my $email = $sp->getPreferredEmailAddress($id);
my @emails;
if (not defined $email) {
my(%emails) = $sp->getEmailAddresses($id);
my $bestDate = '0001-01-01';
foreach my $possibleEmail (keys %emails) {
push(@emails, $possibleEmail);
print STDERR "$emails{$email}{date_encountered} gt $bestDate\n";
if ($emails{$possibleEmail}{date_encountered} gt $bestDate) {
$email = $possibleEmail; $bestDate = $emails{$possibleEmail}{date_encountered};
}
}
} else {
@emails = qw/$email/;
}
print STDERR "Supporter $id has no email address\n" if not defined $email;
my($specificRequest2018, $specificRequest2021) = (0, 0);
my $request = $sp->getRequest({ donorId => $id, requestType => 't-shirt-fy2018design-0' });
$specificRequest2018 = (defined $request);
$request = $sp->getRequest({ donorId => $id, requestType => 't-shirt-fy2021design-0' });
$specificRequest2021 = (defined $request);
my $bestPostal;
my $remainingQualifyingDonations = $requestData{$id}{total};
next if $remainingQualifyingDonations < 60.00; # Must have given at least $60 to get a shirt.
my $outputSoFar = "$id:\n" . " oldest request: $requestData{$id}{oldestShirtDate}\n" .
" lastGave: $requestData{$id}{lastGaveDate}\n" .
" total: " . sprintf('%8.2f', $requestData{$id}{total}) ."\n" .
" emails: " . join(", ", @emails) . "\n" .
" email_for_ship: " . $email . "\n" .
" shirts: $id,";
my @shirtTypes;
my $requestDates = "";
my($finalOutputType, $finalSize);
foreach my $type (sort { $requestData{$id}{shirts}{$a}{requestDate} cmp $requestData{$id}{shirts}{$b}{requestDate} } keys %{$requestData{$id}{shirts}}) {
my $outputType = $type;
my $size = $requestData{$id}{shirts}{$type}{requestConfiguration};
die "$id $type request has no size!" unless defined $size;
last if $remainingQualifyingDonations < 60.00;
$remainingQualifyingDonations -= 60.00;
if ($type !~ /(2018|2021|vintage)/) {
$outputType = 'any';
} elsif ($type =~ /2018/) {
$outputType = '2018design';
} elsif ($type =~ /2021/) {
$outputType = '2021design';
} elsif ($type =~ /vintage/) {
$outputType = 'vintage';
}
#if ($wantedOrGotBoth2018And2021) {
# if ($type =~ /2018/) {
# if ($type =~ /2018/) {
# $outputType = "2018design";
# } elsif ($type =~ /2021/) {
# $outputType = "2021design";
# } elsif ($type =~ /vint/i) {
# $outputType = "vintage";
# } else {
# $outputType = "any";
# }
# } elsif ($type !~ /vint/i) {
# $outputType = "2021design";
# } else {
# $outputType = "vintage";
# }
$size = "Standard$size" if $size =~ /^Ladies/;
# The commented out code below is designed to handle the sizeCounts and make best guess about what
# shirt to send.
die "Supporter $id: $outputType: $type: $size: invalid size \"$size\""
unless ($outputType ne 'any' and defined $sizeCounts->{$outputType}{$size})
or ($outputType eq 'any' and defined $sizeCounts->{vintage}{$size});
if ($outputType eq 'vintage' and $sizeCounts->{vintage}{$size} <= 0
and defined $sizeCounts->{'vintage-green'} and $sizeCounts->{'vintage-green'}{$size} > 0) {
$outputType = 'vintage-green';
} elsif ($outputType eq 'any' and $specificRequest2021 and $specificRequest2018) {
$outputType = 'vintage';
} elsif ($outputType eq 'any' and $specificRequest2018) {
$outputType = '2021design';
} elsif ($outputType eq 'any' and $specificRequest2021) {
$outputType = '2018design';
} elsif ($outputType eq 'any') {
foreach my $reType (qw/2021design 2018design vintage-green vintage/) {
if ($sizeCounts->{$reType}{$size} > 0) {
print "$id: bkuhn Using $outputType request as $reType\n";
$outputType = $reType;
last;
}
}
}
if ($outputType ne 'any' and $sizeCounts->{$outputType}{$size} < 0) {
die "Somehow size count for $outputType, $size got to be less than zero!!!";
} elsif ($outputType eq 'any' or $sizeCounts->{$outputType}{$size} == 0) {
$need{$outputType}{$size} = 0 unless defined $need{$outputType}{$size};
$need{$outputType}{$size}++;
next;
}
$bestPostal = $sp->getBestPostalAddress($id);
unless (defined $bestPostal) {
warn "Supporter $id: unable to find best postal address!";
next;
}
my $likelyUSA = 1
if ($idsKnownToBeInUSA{$id} or ($bestPostal =~ /United\s*States|USA|,\s*\S+\s+\d{5,5}(\s+|$)|,\s*\S+\s+\d{5,5}\-\d{4,4}(\s+|$)/mi
and (not $bestPostal =~ /Saudi\s*Arabia|France|Sweden|Uruguay|Bulgaria|Indonesia|Switzerland|Spain|Brasil|Brazil|Estonia|Germany|Bosnia|Herzegovina|Italy|Czech|Finland|Korea|Ireland|Israel/im)));
if (not $likelyUSA and $bestPostal =~ /Russia/im) {
warn "Support $id: CANNOT CURRENTLY SHIP TO RUSSIA";
next;
}
# # Continue on only if we have sizes left
$sizeCounts->{$outputType}{$size}--;
$totalSent++;
if ($requestDates ne "") {
$requestDates .= ", ";
$outputSoFar .= ",";
}
$outputSoFar .= "${size}_$outputType";
push(@shirtTypes, $type);
$requestDates .= $requestData{$id}{shirts}{$type}{requestDate};
# Order ID (required),Order Date,Order Value,Requested Service,Ship To - Name,Ship To - Company,Ship To - Address 1,Ship To - Address 2,Ship To - Address 3,Ship To - State/Province,Ship To - City,Ship To - Postal Code,Ship To - Country,Ship To - Phone,Ship To - Email,Total Weight in Oz,Dimensions - Length,Dimensions - Width,Dimensions - Height,Notes - From Customer,Notes - Internal,Gift Wrap?,Gift Message
my $orderID = $outputType . '_' . $size . '_' . $requestData{$id}{shirts}{$type}{requestDate} . '_' . sprintf("%4.4d", $id);
$orderID =~ s/\s*//;
my $outputAddress = join(" ", split /\n/, $bestPostal);
$outputAddress = $bestPostal;
if ($likelyUSA) {
push(@usaRows, [ $orderID, $requestData{$id}{shirts}{$type}{requestDate},
sprintf('%s_%s_%4.4d', $size, $outputType, $id), $size, $email,
"USA", $outputAddress ] );
} else {
push(@intRows, [ $orderID, $requestData{$id}{shirts}{$type}{requestDate},
sprintf('%s_%s_%4.4d', $size, $outputType, $id), $size, $email,
"International", $outputAddress ] );
}
}
next if $requestDates eq "";
my $shirtTypes;
foreach my $type (sort { $a cmp $b } @shirtTypes) {
$shirtTypes .= "," if defined $shirtTypes;
$shirtTypes = " SHIRT_TYPES: " if not defined $shirtTypes;
$shirtTypes .= $type;
}
print "#" x 75;
print "\n$outputSoFar\n$shirtTypes\n";
print "\n Request Dates: $requestDates\n";
print " LedgerEntityId: ", $sp->getLedgerEntityId($id), "\n";
print " Display name: \"", $sp->getDisplayName($id), "\"\n";
print " Postal Address:\n$bestPostal\n";
}
BeancountQueryComplete();
my $totalNeed = 0;
my %subtotals;
if (defined $sizeCounts) {
print "FINAL INVENTORY\n";
foreach my $type (sort { $a cmp $b } keys %$sizeCounts) {
print "$type:\n";
foreach my $size (sort { $a cmp $b } keys %{$sizeCounts->{$type}}) {
print " $size: $sizeCounts->{$type}{$size}\n";
}
}
print "NEED INVENTORY\n";
foreach my $type (sort { $a cmp $b } keys %need) {
print "$type:\n";
$subtotals{$type} = 0;
foreach my $size (sort { $a cmp $b } keys %{$need{$type}}) {
print " $size: $need{$type}{$size}\n";
$totalNeed += $need{$type}{$size};
$subtotals{$type} += $need{$type}{$size}
}
}
}
print "TOTAL SENT: $totalSent ";
if (defined $sizeCounts) {
print "\n\nTOTAL NEED: $totalNeed\n";
foreach my $type (sort { $a cmp $b } keys %need) {
print " ... includes subtotal of $type: $subtotals{$type}\n";
}
}
open my $usaFH, ">:encoding(utf8)", "usa.csv";
open my $intFH, ">:encoding(utf8)", "international.csv";
my $usaCSV = Text::CSV->new({ binary => 1, quote_char => '"', allow_whitespace => 1, always_quote => 1,
sep_char => ',', escape_char => "\\", auto_diag => 2, diag_verbose => 2 });
my $intCSV = Text::CSV->new({ binary => 1, quote_char => '"', allow_whitespace => 1, always_quote => 1,
sep_char => ',', escape_char => "\\", auto_diag => 2, diag_verbose => 2 });
my @headers = qw/orderId orderDate myReference size email usaOrInt formattedAddress/;
$usaCSV->say($usaFH, \@headers);
$intCSV->say($intFH, \@headers);
$usaCSV->say($usaFH, $_) for @usaRows; close $usaFH;
$intCSV->say($intFH, $_) for @intRows; close $intFH;
###############################################################################
#
# Local variables:
# compile-command: "perl -c t-shirts-label-print.plx"
# End: