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_credit_bill;
use FS::cust_pay_batch;
use FS::cust_bill_event;
+use FS::part_pkg;
+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.
AGENTNUM, if specified, means that this invoice will only be sent for customers
-of the specified agent.
+of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
+single agent) or an arrayref of agentnums.
INVOICE_FROM, if specified, overrides the default email invoice From: address.
sub send {
my $self = shift;
my $template = scalar(@_) ? shift : '';
- return 'N/A' if scalar(@_) && $_[0] && $self->cust_main->agentnum != shift;
+ if ( scalar(@_) && $_[0] ) {
+ my $agentnums = ref($_[0]) ? shift : [ shift ];
+ return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
+ }
+
my $invoice_from =
scalar(@_)
? shift
: ( $self->_agent_invoice_from || $conf->config('invoice_from') );
- #my @print_text = $self->print_text('', $template);
my @invoicing_list = $self->cust_main->invoicing_list;
- if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list ) {
- #email
+ $self->email($template, $invoice_from)
+ if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
- #better to notify this person than silence
- @invoicing_list = ($invoice_from) unless @invoicing_list;
+ $self->print($template)
+ if grep { $_ eq 'POST' } @invoicing_list; #postal
- my $error = send_email(
- $self->generate_email(
- 'from' => $invoice_from,
- 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
- #'print_text' => [ @print_text ],
- 'template' => $template,
- )
- );
- die "can't email invoice: $error\n" if $error;
- #die "$error\n" if $error;
+ $self->fax($template)
+ if grep { $_ eq 'FAX' } @invoicing_list; #fax
- }
+ '';
- if ( grep { $_ =~ /^(POST|FAX)$/ } @invoicing_list ) {
- my $lpr_data;
- if ($conf->config('invoice_latex')) {
- $lpr_data = [ $self->print_ps('', $template) ];
- } else {
- $lpr_data = [ $self->print_text('', $template) ];
- }
+}
- if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
- my $lpr = $conf->config('lpr');
- open(LPR, "|$lpr")
- or die "Can't open pipe to $lpr: $!\n";
- print LPR @{$lpr_data};
- close LPR
- or die $! ? "Error closing $lpr: $!\n"
- : "Exit status $? from $lpr\n";
- }
+=item email [ TEMPLATENAME [ , INVOICE_FROM ] ]
- if ( grep { $_ eq 'FAX' } @invoicing_list ) { #fax
- die 'FAX invoice destination not supported with plain text invoices.'
- unless $conf->exists('invoice_latex');
- my $dialstring = $self->cust_main->getfield('fax');
- #Check $dialstring?
- my $error = send_fax(docdata => $lpr_data, dialstring => $dialstring);
- die $error if $error;
- }
+Emails this invoice.
+
+TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
+
+INVOICE_FROM, if specified, overrides the default email invoice From: address.
+
+=cut
+
+sub email {
+ my $self = shift;
+ my $template = scalar(@_) ? shift : '';
+ my $invoice_from =
+ scalar(@_)
+ ? shift
+ : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
+
+ my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
+ $self->cust_main->invoicing_list;
+
+ #better to notify this person than silence
+ @invoicing_list = ($invoice_from) unless @invoicing_list;
+ my $error = send_email(
+ $self->generate_email(
+ 'from' => $invoice_from,
+ 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
+ 'template' => $template,
+ )
+ );
+ die "can't email invoice: $error\n" if $error;
+ #die "$error\n" if $error;
+
+}
+
+=item lpr_data [ TEMPLATENAME ]
+
+Returns the postscript or plaintext for this invoice as an arrayref.
+
+TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
+
+=cut
+
+sub lpr_data {
+ my( $self, $template) = @_;
+ $conf->exists('invoice_latex')
+ ? [ $self->print_ps('', $template) ]
+ : [ $self->print_text('', $template) ];
+}
+
+=item print [ TEMPLATENAME ]
+
+Prints this invoice.
+
+TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
+
+=cut
+
+sub print {
+ my $self = shift;
+ my $template = scalar(@_) ? shift : '';
+
+ my $lpr = $conf->config('lpr');
+
+ 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 ]
+
+Faxes this invoice.
+
+TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
+
+=cut
+
+sub fax {
+ my $self = shift;
+ my $template = scalar(@_) ? shift : '';
+
+ die 'FAX invoice destination not (yet?) supported with plain text invoices.'
+ unless $conf->exists('invoice_latex');
+
+ my $dialstring = $self->cust_main->getfield('fax');
+ #Check $dialstring?
+
+ my $error = send_fax( 'docdata' => $self->lpr_data($template),
+ 'dialstring' => $dialstring,
+ );
+ die $error if $error;
}
) 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 ) {
+ 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 ( $conf->exists('invoice_latexreturnaddress')
- && length($conf->exists('invoice_latexreturnaddress'))
- )
- {
- $returnaddress = join("\n", $conf->config('invoice_latexreturnaddress') );
+ if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
+ $returnaddress = join("\n",
+ $conf->config_orbase('invoice_latexreturnaddress', $template)
+ );
} else {
$returnaddress = '~';
}
'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'} = $conf->exists('invoice_htmlreturnaddress')
- ? join("\n", $conf->config('invoice_htmlreturnaddress') )
- : join("\n", map {
+ 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('invoice_latexreturnaddress')
+ $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 ) {
+ 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.