use strict;
use vars qw( @ISA $DEBUG $conf $money_char );
use vars qw( $invoice_lines @buf ); #yuck
+use IPC::Run3;
use Date::Format;
use Text::Template 1.20;
use File::Temp 0.14;
use HTML::Entities;
use Locale::Country;
use FS::UID qw( datasrc );
-use FS::Record qw( qsearch qsearchs );
use FS::Misc qw( send_email send_fax );
+use FS::Record qw( qsearch qsearchs );
+use FS::cust_main_Mixin;
use FS::cust_main;
use FS::cust_bill_pkg;
use FS::cust_credit;
use FS::cust_bill_pay;
use FS::part_bill_event;
-@ISA = qw( FS::Record );
+@ISA = qw( FS::cust_main_Mixin FS::Record );
$DEBUG = 0;
sub table { 'cust_bill'; }
+sub cust_linked { $_[0]->cust_main_custnum; }
+sub cust_unlinked_msg {
+ my $self = shift;
+ "WARNING: can't find cust_main.custnum ". $self->custnum.
+ ' (cust_bill.invnum '. $self->invnum. ')';
+}
+
=item insert
Adds this invoice to the database ("Posts" the invoice). If there is an error,
$args{'from'} =~ /\@([\w\.\-]+)/ or $1 = 'example.com';
my $content_id = join('.', rand()*(2**32), $$, time). "\@$1";
+ my $path = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
+ my $file;
+ if ( defined($args{'_template'}) && length($args{'_template'})
+ && -e "$path/logo_". $args{'_template'}. ".png"
+ )
+ {
+ $file = "$path/logo_". $args{'_template'}. ".png";
+ } else {
+ $file = "$path/logo.png";
+ }
+
my $image = build MIME::Entity
'Type' => 'image/png',
'Encoding' => 'base64',
- 'Path' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc/logo.png",
+ 'Path' => $file,
'Filename' => 'logo.png',
'Content-ID' => "<$content_id>",
;
=item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
-Sends this invoice to the destinations configured for this customer: send
-emails or print. See L<FS::cust_main_invoice>.
+Sends this invoice to the destinations configured for this customer: sends
+email, prints and/or faxes. See L<FS::cust_main_invoice>.
TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
=item lpr_data [ TEMPLATENAME ]
-Returns the postscript or plaintext for this invoice.
+Returns the postscript or plaintext for this invoice as an arrayref.
TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
my $template = scalar(@_) ? shift : '';
my $lpr = $conf->config('lpr');
- open(LPR, "|$lpr")
- or die "Can't open pipe to $lpr: $!\n";
- print LPR @{ $self->lpr_data($template) };
- close LPR
- or die $! ? "Error closing $lpr: $!\n"
- : "Exit status $? from $lpr\n";
+
+ my $outerr = '';
+ run3 $lpr, $self->lpr_data($template), \$outerr, \$outerr;
+ if ( $? ) {
+ $outerr = ": $outerr" if length($outerr);
+ die "Error from $lpr (exit status ". ($?>>8). ")$outerr\n";
+ }
+
}
=item fax [ TEMPLATENAME ]
) or die "can't create csv";
print CSV $csv->string. "\n";
- #new charges (false laziness w/print_text)
+ #new charges (false laziness w/print_text and _items stuff)
foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
my($pkg, $setup, $recur, $sdate, $edate);
'plan' => 'send_agent',
'plandata' => { 'op' => '~',
'value' => "(^|\n)agentnum ".
+ '([0-9]*, )*'.
$self->cust_main->agentnum.
+ '(, [0-9]*)*'.
"(\n|\$)",
},
},
=cut
-#still some false laziness w/print_text
+#still some false laziness w/_items stuff (and send_csv)
sub print_text {
my( $self, $today, $template ) = @_;
( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
) {
- if ( $cust_bill_pkg->pkgnum > 0 ) {
+ my $desc = $cust_bill_pkg->desc;
- my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
- my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
- my $pkg = $part_pkg->pkg;
+ if ( $cust_bill_pkg->pkgnum > 0 ) {
if ( $cust_bill_pkg->setup != 0 ) {
- my $description = $pkg;
+ my $description = $desc;
$description .= ' Setup' if $cust_bill_pkg->recur != 0;
push @buf, [ $description,
$money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
push @buf,
map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
- $cust_pkg->h_labels($self->_date);
+ $cust_bill_pkg->cust_pkg->h_labels($self->_date);
}
if ( $cust_bill_pkg->recur != 0 ) {
push @buf, [
- "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
- time2str("%x", $cust_bill_pkg->edate) . ")",
+ "$desc (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
+ time2str("%x", $cust_bill_pkg->edate) . ")",
$money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
];
push @buf,
map { [ " ". $_->[0]. ": ". $_->[1], '' ] }
- $cust_pkg->h_labels($cust_bill_pkg->edate, $cust_bill_pkg->sdate);
+ $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
+ $cust_bill_pkg->sdate );
}
push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
} else { #pkgnum tax or one-shot line item
- my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
- ? ( $cust_bill_pkg->itemdesc || 'Tax' )
- : 'Tax';
+
if ( $cust_bill_pkg->setup != 0 ) {
- push @buf, [ $itemdesc,
+ push @buf, [ $desc,
$money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
}
if ( $cust_bill_pkg->recur != 0 ) {
- push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - "
- . time2str("%x", $cust_bill_pkg->edate). ")",
+ push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
+ . time2str("%x", $cust_bill_pkg->edate). ")",
$money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
];
}
+
}
+
}
push @buf,['','-----------'];
=cut
-#still some false laziness w/print_text
+#still some false laziness w/print_text (mostly print_text should use _items stuff though)
sub print_latex {
my( $self, $today, $template ) = @_;
}
my $returnaddress;
- if ( length($conf->config_orbase('invoice_latexreturnaddress', $template) ) {
+ if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
$returnaddress = join("\n",
$conf->config_orbase('invoice_latexreturnaddress', $template)
);
'city' => _latex_escape($cust_main->city),
'state' => _latex_escape($cust_main->state),
'zip' => _latex_escape($cust_main->zip),
- 'footer' => join("\n", $conf->config('invoice_latexfooter') ),
- 'smallfooter' => join("\n", $conf->config('invoice_latexsmallfooter') ),
+ 'footer' => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
+ 'smallfooter' => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
'returnaddress' => $returnaddress,
'quantity' => 1,
'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
'terms' => $conf->config('invoice_default_terms')
|| 'Payable upon receipt',
'cid' => $cid,
+ 'template' => $template,
# 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
);
- $invoice_data{'returnaddress'} =
- length( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
- ? join("\n", $conf->config('invoice_htmlreturnaddress', $template) )
- : join("\n", map {
- s/~/ /g;
- s/\\\\\*?\s*$/<BR>/;
- s/\\hyphenation\{[\w\s\-]+\}//;
- $_;
- }
- $conf->config_orbase('invoice_latexreturnaddress', $template)
- );
+ if (
+ defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
+ && length( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
+ ) {
+ $invoice_data{'returnaddress'} =
+ join("\n", $conf->config('invoice_htmlreturnaddress', $template) );
+ } else {
+ $invoice_data{'returnaddress'} =
+ join("\n", map {
+ s/~/ /g;
+ s/\\\\\*?\s*$/<BR>/;
+ s/\\hyphenation\{[\w\s\-]+\}//;
+ $_;
+ }
+ $conf->config_orbase( 'invoice_latexreturnaddress',
+ $template
+ )
+ );
+ }
my $countrydefault = $conf->config('countrydefault') || 'US';
if ( $cust_main->country eq $countrydefault ) {
encode_entities(code2country($cust_main->country));
}
- $invoice_data{'notes'} =
- length($conf->config_orbase('invoice_htmlnotes', $template))
- ? join("\n", $conf->config_orbase('invoice_htmlnotes', $template) )
- : join("\n", map {
- s/%%(.*)$/<!-- $1 -->/;
- s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
- s/\\begin\{enumerate\}/<ol>/;
- s/\\item / <li>/;
- s/\\end\{enumerate\}/<\/ol>/;
- s/\\textbf\{(.*)\}/<b>$1<\/b>/;
- $_;
- }
- $conf->config_orbase('invoice_latexnotes', $template)
- );
+ if (
+ defined( $conf->config_orbase('invoice_htmlnotes', $template) )
+ && length( $conf->config_orbase('invoice_htmlnotes', $template) )
+ ) {
+ $invoice_data{'notes'} =
+ join("\n", $conf->config_orbase('invoice_htmlnotes', $template) );
+ } else {
+ $invoice_data{'notes'} =
+ join("\n", map {
+ s/%%(.*)$/<!-- $1 -->/;
+ s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
+ s/\\begin\{enumerate\}/<ol>/;
+ s/\\item / <li>/;
+ s/\\end\{enumerate\}/<\/ol>/;
+ s/\\textbf\{(.*)\}/<b>$1<\/b>/;
+ $_;
+ }
+ $conf->config_orbase('invoice_latexnotes', $template)
+ );
+ }
# #do variable substitutions in notes
# $invoice_data{'notes'} =
# $conf->config_orbase('invoice_latexnotes', $suffix)
# );
- $invoice_data{'footer'} = $conf->exists('invoice_htmlfooter')
- ? join("\n", $conf->config('invoice_htmlfooter') )
- : join("\n", map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; }
- $conf->config('invoice_latexfooter')
+ if (
+ defined( $conf->config_orbase('invoice_htmlfooter', $template) )
+ && length( $conf->config_orbase('invoice_htmlfooter', $template) )
+ ) {
+ $invoice_data{'footer'} =
+ join("\n", $conf->config_orbase('invoice_htmlfooter', $template) );
+ } else {
+ $invoice_data{'footer'} =
+ join("\n", map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; }
+ $conf->config_orbase('invoice_latexfooter', $template)
);
+ }
$invoice_data{'po_line'} =
( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
my @b = ();
foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
- if ( $cust_bill_pkg->pkgnum > 0 ) {
+ my $desc = $cust_bill_pkg->desc;
- my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
- my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
- my $pkg = $part_pkg->pkg;
+ if ( $cust_bill_pkg->pkgnum > 0 ) {
if ( $cust_bill_pkg->setup != 0 ) {
- my $description = $pkg;
+ my $description = $desc;
$description .= ' Setup' if $cust_bill_pkg->recur != 0;
- my @d = $cust_pkg->h_labels_short($self->_date);
+ my @d = $cust_bill_pkg->cust_pkg->h_labels_short($self->_date);
push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
push @b, {
description => $description,
#pkgpart => $part_pkg->pkgpart,
- pkgnum => $cust_pkg->pkgnum,
+ pkgnum => $cust_bill_pkg->pkgnum,
amount => sprintf("%.2f", $cust_bill_pkg->setup),
ext_description => \@d,
};
if ( $cust_bill_pkg->recur != 0 ) {
push @b, {
- description => "$pkg (" .
+ description => "$desc (" .
time2str('%x', $cust_bill_pkg->sdate). ' - '.
time2str('%x', $cust_bill_pkg->edate). ')',
#pkgpart => $part_pkg->pkgpart,
- pkgnum => $cust_pkg->pkgnum,
+ pkgnum => $cust_bill_pkg->pkgnum,
amount => sprintf("%.2f", $cust_bill_pkg->recur),
- ext_description => [ $cust_pkg->h_labels_short($cust_bill_pkg->edate,
- $cust_bill_pkg->sdate),
- $cust_bill_pkg->details,
- ],
+ ext_description =>
+ [ $cust_bill_pkg->cust_pkg->h_labels_short( $cust_bill_pkg->edate,
+ $cust_bill_pkg->sdate),
+ $cust_bill_pkg->details,
+ ],
};
}
} else { #pkgnum tax or one-shot line item (??)
- my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
- ? ( $cust_bill_pkg->itemdesc || 'Tax' )
- : 'Tax';
if ( $cust_bill_pkg->setup != 0 ) {
push @b, {
- 'description' => $itemdesc,
+ 'description' => $desc,
'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
};
}
if ( $cust_bill_pkg->recur != 0 ) {
push @b, {
- 'description' => "$itemdesc (".
+ 'description' => "$desc (".
time2str("%x", $cust_bill_pkg->sdate). ' - '.
time2str("%x", $cust_bill_pkg->edate). ')',
'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
=back
+=head1 SUBROUTINES
+
+=over 4
+
+=item reprint
+
+=cut
+
+sub process_reprint {
+ process_re_X('print', @_);
+}
+
+=item reemail
+
+=cut
+
+sub process_reemail {
+ process_re_X('email', @_);
+}
+
+=item refax
+
+=cut
+
+sub process_refax {
+ process_re_X('fax', @_);
+}
+
+use Storable qw(thaw);
+use Data::Dumper;
+use MIME::Base64;
+sub process_re_X {
+ my( $method, $job ) = ( shift, shift );
+
+ my $param = thaw(decode_base64(shift));
+ warn Dumper($param) if $DEBUG;
+
+ re_X(
+ $method,
+ $job,
+ %$param,
+ );
+
+}
+
+sub re_X {
+ my($method, $job, %param ) = @_;
+# [ 'begin', 'end', 'agentnum', 'open', 'days', 'newest_percust' ],
+
+ #some false laziness w/search/cust_bill.html
+ my $distinct = '';
+ my $orderby = 'ORDER BY cust_bill._date';
+
+ my @where;
+
+ if ( $param{'begin'} =~ /^(\d+)$/ ) {
+ push @where, "cust_bill._date >= $1";
+ }
+ if ( $param{'end'} =~ /^(\d+)$/ ) {
+ push @where, "cust_bill._date < $1";
+ }
+ if ( $param{'agentnum'} =~ /^(\d+)$/ ) {
+ push @where, "cust_main.agentnum = $1";
+ }
+
+ my $owed =
+ "charged - ( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
+ WHERE cust_bill_pay.invnum = cust_bill.invnum )
+ - ( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
+ WHERE cust_credit_bill.invnum = cust_bill.invnum )";
+
+ push @where, "0 != $owed"
+ if $param{'open'};
+
+ push @where, "cust_bill._date < ". (time-86400*$param{'days'})
+ if $param{'days'};
+
+ my $extra_sql = scalar(@where) ? 'WHERE '. join(' AND ', @where) : '';
+
+ my $addl_from = 'left join cust_main using ( custnum )';
+
+ if ( $param{'newest_percust'} ) {
+ $distinct = 'DISTINCT ON ( cust_bill.custnum )';
+ $orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
+ #$count_query = "SELECT COUNT(DISTINCT cust_bill.custnum), 'N/A', 'N/A'";
+ }
+
+ my @cust_bill = qsearch( 'cust_bill',
+ {},
+ "$distinct cust_bill.*",
+ $extra_sql,
+ '',
+ $addl_from
+ );
+
+ my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
+ foreach my $cust_bill ( @cust_bill ) {
+ $cust_bill->$method();
+
+ if ( $job ) { #progressbar foo
+ $num++;
+ if ( time - $min_sec > $last ) {
+ my $error = $job->update_statustext(
+ int( 100 * $num / scalar(@cust_bill) )
+ );
+ die $error if $error;
+ $last = time;
+ }
+ }
+
+ }
+
+}
+
+=back
+
=head1 BUGS
The delete method.