4 use vars qw( @ISA $DEBUG $me $conf $money_char );
5 use vars qw( $invoice_lines @buf ); #yuck
6 use Fcntl qw(:flock); #for spool_csv
7 use List::Util qw(min max);
9 use Text::Template 1.20;
11 use String::ShellQuote;
14 use FS::UID qw( datasrc );
15 use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
16 use FS::Record qw( qsearch qsearchs dbh );
17 use FS::cust_main_Mixin;
19 use FS::cust_bill_pkg;
20 use FS::cust_bill_pkg_display;
24 use FS::cust_credit_bill;
26 use FS::cust_pay_batch;
27 use FS::cust_bill_event;
30 use FS::cust_bill_pay;
31 use FS::cust_bill_pay_batch;
32 use FS::part_bill_event;
35 @ISA = qw( FS::cust_main_Mixin FS::Record );
38 $me = '[FS::cust_bill]';
40 #ask FS::UID to run this stuff for us later
41 FS::UID->install_callback( sub {
43 $money_char = $conf->config('money_char') || '$';
48 FS::cust_bill - Object methods for cust_bill records
54 $record = new FS::cust_bill \%hash;
55 $record = new FS::cust_bill { 'column' => 'value' };
57 $error = $record->insert;
59 $error = $new_record->replace($old_record);
61 $error = $record->delete;
63 $error = $record->check;
65 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
67 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
69 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
71 @cust_pay_objects = $cust_bill->cust_pay;
73 $tax_amount = $record->tax;
75 @lines = $cust_bill->print_text;
76 @lines = $cust_bill->print_text $time;
80 An FS::cust_bill object represents an invoice; a declaration that a customer
81 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
82 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
83 following fields are currently supported:
87 =item invnum - primary key (assigned automatically for new invoices)
89 =item custnum - customer (see L<FS::cust_main>)
91 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
92 L<Time::Local> and L<Date::Parse> for conversion functions.
94 =item charged - amount of this invoice
96 =item printed - deprecated
98 =item closed - books closed flag, empty or `Y'
108 Creates a new invoice. To add the invoice to the database, see L<"insert">.
109 Invoices are normally created by calling the bill method of a customer object
110 (see L<FS::cust_main>).
114 sub table { 'cust_bill'; }
116 sub cust_linked { $_[0]->cust_main_custnum; }
117 sub cust_unlinked_msg {
119 "WARNING: can't find cust_main.custnum ". $self->custnum.
120 ' (cust_bill.invnum '. $self->invnum. ')';
125 Adds this invoice to the database ("Posts" the invoice). If there is an error,
126 returns the error, otherwise returns false.
130 This method now works but you probably shouldn't use it. Instead, apply a
131 credit against the invoice.
133 Using this method to delete invoices outright is really, really bad. There
134 would be no record you ever posted this invoice, and there are no check to
135 make sure charged = 0 or that there are no associated cust_bill_pkg records.
137 Really, don't use it.
143 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
144 $self->SUPER::delete(@_);
147 =item replace OLD_RECORD
149 Replaces the OLD_RECORD with this one in the database. If there is an error,
150 returns the error, otherwise returns false.
152 Only printed may be changed. printed is normally updated by calling the
153 collect method of a customer object (see L<FS::cust_main>).
157 #replace can be inherited from Record.pm
159 # replace_check is now the preferred way to #implement replace data checks
160 # (so $object->replace() works without an argument)
163 my( $new, $old ) = ( shift, shift );
164 return "Can't change custnum!" unless $old->custnum == $new->custnum;
165 #return "Can't change _date!" unless $old->_date eq $new->_date;
166 return "Can't change _date!" unless $old->_date == $new->_date;
167 return "Can't change charged!" unless $old->charged == $new->charged
168 || $old->charged == 0;
175 Checks all fields to make sure this is a valid invoice. If there is an error,
176 returns the error, otherwise returns false. Called by the insert and replace
185 $self->ut_numbern('invnum')
186 || $self->ut_number('custnum')
187 || $self->ut_numbern('_date')
188 || $self->ut_money('charged')
189 || $self->ut_numbern('printed')
190 || $self->ut_enum('closed', [ '', 'Y' ])
192 return $error if $error;
194 return "Unknown customer"
195 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
197 $self->_date(time) unless $self->_date;
199 $self->printed(0) if $self->printed eq '';
206 Returns a list consisting of the total previous balance for this customer,
207 followed by the previous outstanding invoices (as FS::cust_bill objects also).
214 my @cust_bill = sort { $a->_date <=> $b->_date }
215 grep { $_->owed != 0 && $_->_date < $self->_date }
216 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
218 foreach ( @cust_bill ) { $total += $_->owed; }
224 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
231 { 'table' => 'cust_bill_pkg',
232 'hashref' => { 'invnum' => $self->invnum },
233 'order_by' => 'ORDER BY billpkgnum',
240 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
247 my @cust_pkg = map { $_->cust_pkg } $self->cust_bill_pkg;
249 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
252 =item open_cust_bill_pkg
254 Returns the open line items for this invoice.
256 Note that cust_bill_pkg with both setup and recur fees are returned as two
257 separate line items, each with only one fee.
261 # modeled after cust_main::open_cust_bill
262 sub open_cust_bill_pkg {
265 # grep { $_->owed > 0 } $self->cust_bill_pkg
267 my %other = ( 'recur' => 'setup',
268 'setup' => 'recur', );
270 foreach my $field ( qw( recur setup )) {
271 push @open, map { $_->set( $other{$field}, 0 ); $_; }
272 grep { $_->owed($field) > 0 }
273 $self->cust_bill_pkg;
279 =item cust_bill_event
281 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
285 sub cust_bill_event {
287 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
290 =item num_cust_bill_event
292 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
296 sub num_cust_bill_event {
299 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
300 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
301 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
302 $sth->fetchrow_arrayref->[0];
307 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
311 #false laziness w/cust_pkg.pm
315 'table' => 'cust_event',
316 'addl_from' => 'JOIN part_event USING ( eventpart )',
317 'hashref' => { 'tablenum' => $self->invnum },
318 'extra_sql' => " AND eventtable = 'cust_bill' ",
324 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
328 #false laziness w/cust_pkg.pm
332 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
333 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
334 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
335 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
336 $sth->fetchrow_arrayref->[0];
341 Returns the customer (see L<FS::cust_main>) for this invoice.
347 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
350 =item cust_suspend_if_balance_over AMOUNT
352 Suspends the customer associated with this invoice if the total amount owed on
353 this invoice and all older invoices is greater than the specified amount.
355 Returns a list: an empty list on success or a list of errors.
359 sub cust_suspend_if_balance_over {
360 my( $self, $amount ) = ( shift, shift );
361 my $cust_main = $self->cust_main;
362 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
365 $cust_main->suspend(@_);
371 Depreciated. See the cust_credited method.
373 #Returns a list consisting of the total previous credited (see
374 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
375 #outstanding credits (FS::cust_credit objects).
381 croak "FS::cust_bill->cust_credit depreciated; see ".
382 "FS::cust_bill->cust_credit_bill";
385 #my @cust_credit = sort { $a->_date <=> $b->_date }
386 # grep { $_->credited != 0 && $_->_date < $self->_date }
387 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
389 #foreach (@cust_credit) { $total += $_->credited; }
390 #$total, @cust_credit;
395 Depreciated. See the cust_bill_pay method.
397 #Returns all payments (see L<FS::cust_pay>) for this invoice.
403 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
405 #sort { $a->_date <=> $b->_date }
406 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
412 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
418 sort { $a->_date <=> $b->_date }
419 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
424 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
430 sort { $a->_date <=> $b->_date }
431 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
437 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
444 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
446 foreach (@taxlines) { $total += $_->setup; }
452 Returns the amount owed (still outstanding) on this invoice, which is charged
453 minus all payment applications (see L<FS::cust_bill_pay>) and credit
454 applications (see L<FS::cust_credit_bill>).
460 my $balance = $self->charged;
461 $balance -= $_->amount foreach ( $self->cust_bill_pay );
462 $balance -= $_->amount foreach ( $self->cust_credited );
463 $balance = sprintf( "%.2f", $balance);
464 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
468 =item apply_payments_and_credits
472 sub apply_payments_and_credits {
475 local $SIG{HUP} = 'IGNORE';
476 local $SIG{INT} = 'IGNORE';
477 local $SIG{QUIT} = 'IGNORE';
478 local $SIG{TERM} = 'IGNORE';
479 local $SIG{TSTP} = 'IGNORE';
480 local $SIG{PIPE} = 'IGNORE';
482 my $oldAutoCommit = $FS::UID::AutoCommit;
483 local $FS::UID::AutoCommit = 0;
486 $self->select_for_update; #mutex
488 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
489 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
491 while ( $self->owed > 0 and ( @payments || @credits ) ) {
494 if ( @payments && @credits ) {
496 #decide which goes first by weight of top (unapplied) line item
498 my @open_lineitems = $self->open_cust_bill_pkg;
501 max( map { $_->part_pkg->pay_weight || 0 }
506 my $max_credit_weight =
507 max( map { $_->part_pkg->credit_weight || 0 }
513 #if both are the same... payments first? it has to be something
514 if ( $max_pay_weight >= $max_credit_weight ) {
520 } elsif ( @payments ) {
522 } elsif ( @credits ) {
525 die "guru meditation #12 and 35";
528 if ( $app eq 'pay' ) {
530 my $payment = shift @payments;
532 $app = new FS::cust_bill_pay {
533 'paynum' => $payment->paynum,
534 'amount' => sprintf('%.2f', min( $payment->unapplied, $self->owed ) ),
537 } elsif ( $app eq 'credit' ) {
539 my $credit = shift @credits;
541 $app = new FS::cust_credit_bill {
542 'crednum' => $credit->crednum,
543 'amount' => sprintf('%.2f', min( $credit->credited, $self->owed ) ),
547 die "guru meditation #12 and 35";
550 $app->invnum( $self->invnum );
552 my $error = $app->insert;
554 $dbh->rollback if $oldAutoCommit;
555 return "Error inserting ". $app->table. " record: $error";
557 die $error if $error;
561 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
566 =item generate_email OPTION => VALUE ...
574 sender address, required
578 alternate template name, optional
582 text attachment arrayref, optional
586 email subject, optional
590 Returns an argument list to be passed to L<FS::Misc::send_email>.
601 my $me = '[FS::cust_bill::generate_email]';
604 'from' => $args{'from'},
605 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
608 my %cdrs = ( 'unsquelch_cdr' => $conf->exists('voip-cdr_email') );
610 if (ref($args{'to'}) eq 'ARRAY') {
611 $return{'to'} = $args{'to'};
613 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
614 $self->cust_main->invoicing_list
618 if ( $conf->exists('invoice_html') ) {
620 warn "$me creating HTML/text multipart message"
623 $return{'nobody'} = 1;
625 my $alternative = build MIME::Entity
626 'Type' => 'multipart/alternative',
627 'Encoding' => '7bit',
628 'Disposition' => 'inline'
632 if ( $conf->exists('invoice_email_pdf')
633 and scalar($conf->config('invoice_email_pdf_note')) ) {
635 warn "$me using 'invoice_email_pdf_note' in multipart message"
637 $data = [ map { $_ . "\n" }
638 $conf->config('invoice_email_pdf_note')
643 warn "$me not using 'invoice_email_pdf_note' in multipart message"
645 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
646 $data = $args{'print_text'};
648 $data = [ $self->print_text('', $args{'template'}, %cdrs) ];
653 $alternative->attach(
654 'Type' => 'text/plain',
655 #'Encoding' => 'quoted-printable',
656 'Encoding' => '7bit',
658 'Disposition' => 'inline',
661 $args{'from'} =~ /\@([\w\.\-]+)/;
662 my $from = $1 || 'example.com';
663 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
666 my $agentnum = $self->cust_main->agentnum;
667 if ( defined($args{'template'}) && length($args{'template'})
668 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
671 $logo = 'logo_'. $args{'template'}. '.png';
675 my $image_data = $conf->config_binary( $logo, $agentnum);
677 my $image = build MIME::Entity
678 'Type' => 'image/png',
679 'Encoding' => 'base64',
680 'Data' => $image_data,
681 'Filename' => 'logo.png',
682 'Content-ID' => "<$content_id>",
685 $alternative->attach(
686 'Type' => 'text/html',
687 'Encoding' => 'quoted-printable',
688 'Data' => [ '<html>',
691 ' '. encode_entities($return{'subject'}),
694 ' <body bgcolor="#e8e8e8">',
695 $self->print_html({ time => '',
696 template => $args{'template'},
703 'Disposition' => 'inline',
704 #'Filename' => 'invoice.pdf',
708 if ( $self->cust_main->email_csv_cdr ) {
710 push @otherparts, build MIME::Entity
711 'Type' => 'text/csv',
712 'Encoding' => '7bit',
713 'Data' => [ map { "$_\n" } $self->call_details ],
714 'Disposition' => 'attachment',
719 if ( $conf->exists('invoice_email_pdf') ) {
724 # multipart/alternative
730 my $related = build MIME::Entity 'Type' => 'multipart/related',
731 'Encoding' => '7bit';
733 #false laziness w/Misc::send_email
734 $related->head->replace('Content-type',
736 '; boundary="'. $related->head->multipart_boundary. '"'.
737 '; type=multipart/alternative'
740 $related->add_part($alternative);
742 $related->add_part($image);
744 my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'}, %cdrs);
746 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
750 #no other attachment:
752 # multipart/alternative
757 $return{'content-type'} = 'multipart/related';
758 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
759 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
760 #$return{'disposition'} = 'inline';
766 if ( $conf->exists('invoice_email_pdf') ) {
767 warn "$me creating PDF attachment"
770 #mime parts arguments a la MIME::Entity->build().
771 $return{'mimeparts'} = [
772 { $self->mimebuild_pdf('', $args{'template'}, %cdrs) }
776 if ( $conf->exists('invoice_email_pdf')
777 and scalar($conf->config('invoice_email_pdf_note')) ) {
779 warn "$me using 'invoice_email_pdf_note'"
781 $return{'body'} = [ map { $_ . "\n" }
782 $conf->config('invoice_email_pdf_note')
787 warn "$me not using 'invoice_email_pdf_note'"
789 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
790 $return{'body'} = $args{'print_text'};
792 $return{'body'} = [ $self->print_text('', $args{'template'}, %cdrs) ];
805 Returns a list suitable for passing to MIME::Entity->build(), representing
806 this invoice as PDF attachment.
813 'Type' => 'application/pdf',
814 'Encoding' => 'base64',
815 'Data' => [ $self->print_pdf(@_) ],
816 'Disposition' => 'attachment',
817 'Filename' => 'invoice-'. $self->invnum. '.pdf',
821 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
823 Sends this invoice to the destinations configured for this customer: sends
824 email, prints and/or faxes. See L<FS::cust_main_invoice>.
826 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
828 AGENTNUM, if specified, means that this invoice will only be sent for customers
829 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
830 single agent) or an arrayref of agentnums.
832 INVOICE_FROM, if specified, overrides the default email invoice From: address.
834 AMOUNT, if specified, only sends the invoice if the total amount owed on this
835 invoice and all older invoices is greater than the specified amount.
842 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
843 or die "invalid invoice number: " . $opt{invnum};
845 my @args = ( $opt{template}, $opt{agentnum} );
846 push @args, $opt{invoice_from}
847 if exists($opt{invoice_from}) && $opt{invoice_from};
849 my $error = $self->send( @args );
850 die $error if $error;
856 my $template = scalar(@_) ? shift : '';
857 if ( scalar(@_) && $_[0] ) {
858 my $agentnums = ref($_[0]) ? shift : [ shift ];
859 return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
865 : ( $self->_agent_invoice_from || #XXX should go away
866 $conf->config('invoice_from', $self->cust_main->agentnum )
869 my $balance_over = ( scalar(@_) && $_[0] !~ /^\s*$/ ) ? shift : 0;
872 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
874 my @invoicing_list = $self->cust_main->invoicing_list;
876 #$self->email_invoice($template, $invoice_from)
877 $self->email($template, $invoice_from)
878 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
880 #$self->print_invoice($template)
881 $self->print($template)
882 if grep { $_ eq 'POST' } @invoicing_list; #postal
884 $self->fax_invoice($template)
885 if grep { $_ eq 'FAX' } @invoicing_list; #fax
891 =item email [ TEMPLATENAME [ , INVOICE_FROM ] ]
895 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
897 INVOICE_FROM, if specified, overrides the default email invoice From: address.
901 sub queueable_email {
904 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
905 or die "invalid invoice number: " . $opt{invnum};
907 my @args = ( $opt{template} );
908 push @args, $opt{invoice_from}
909 if exists($opt{invoice_from}) && $opt{invoice_from};
911 my $error = $self->email( @args );
912 die $error if $error;
919 my $template = scalar(@_) ? shift : '';
923 : ( $self->_agent_invoice_from || #XXX should go away
924 $conf->config('invoice_from', $self->cust_main->agentnum )
928 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
929 $self->cust_main->invoicing_list;
931 #better to notify this person than silence
932 @invoicing_list = ($invoice_from) unless @invoicing_list;
934 my $subject = $self->email_subject($template);
936 my $error = send_email(
937 $self->generate_email(
938 'from' => $invoice_from,
939 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
940 'subject' => $subject,
941 'template' => $template,
944 die "can't email invoice: $error\n" if $error;
945 #die "$error\n" if $error;
952 #my $template = scalar(@_) ? shift : '';
955 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
958 my $cust_main = $self->cust_main;
959 my $name = $cust_main->name;
960 my $name_short = $cust_main->name_short;
961 my $invoice_number = $self->invnum;
962 my $invoice_date = $self->_date_pretty;
967 =item lpr_data [ TEMPLATENAME ]
969 Returns the postscript or plaintext for this invoice as an arrayref.
971 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
976 my( $self, $template) = @_;
977 $conf->exists('invoice_latex')
978 ? [ $self->print_ps('', $template) ]
979 : [ $self->print_text('', $template) ];
982 =item print [ TEMPLATENAME ]
986 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
993 my $template = scalar(@_) ? shift : '';
995 do_print $self->lpr_data($template);
998 =item fax_invoice [ TEMPLATENAME ]
1002 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
1008 my $template = scalar(@_) ? shift : '';
1010 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1011 unless $conf->exists('invoice_latex');
1013 my $dialstring = $self->cust_main->getfield('fax');
1016 my $error = send_fax( 'docdata' => $self->lpr_data($template),
1017 'dialstring' => $dialstring,
1019 die $error if $error;
1023 =item ftp_invoice [ TEMPLATENAME ]
1025 Sends this invoice data via FTP.
1027 TEMPLATENAME is unused?
1033 my $template = scalar(@_) ? shift : '';
1036 'protocol' => 'ftp',
1037 'server' => $conf->config('cust_bill-ftpserver'),
1038 'username' => $conf->config('cust_bill-ftpusername'),
1039 'password' => $conf->config('cust_bill-ftppassword'),
1040 'dir' => $conf->config('cust_bill-ftpdir'),
1041 'format' => $conf->config('cust_bill-ftpformat'),
1045 =item spool_invoice [ TEMPLATENAME ]
1047 Spools this invoice data (see L<FS::spool_csv>)
1049 TEMPLATENAME is unused?
1055 my $template = scalar(@_) ? shift : '';
1058 'format' => $conf->config('cust_bill-spoolformat'),
1059 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1063 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1065 Like B<send>, but only sends the invoice if it is the newest open invoice for
1070 sub send_if_newest {
1075 grep { $_->owed > 0 }
1076 qsearch('cust_bill', {
1077 'custnum' => $self->custnum,
1078 #'_date' => { op=>'>', value=>$self->_date },
1079 'invnum' => { op=>'>', value=>$self->invnum },
1086 =item send_csv OPTION => VALUE, ...
1088 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1092 protocol - currently only "ftp"
1098 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1099 and YYMMDDHHMMSS is a timestamp.
1101 See L</print_csv> for a description of the output format.
1106 my($self, %opt) = @_;
1110 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1111 mkdir $spooldir, 0700 unless -d $spooldir;
1113 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1114 my $file = "$spooldir/$tracctnum.csv";
1116 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1118 open(CSV, ">$file") or die "can't open $file: $!";
1126 if ( $opt{protocol} eq 'ftp' ) {
1127 eval "use Net::FTP;";
1129 $net = Net::FTP->new($opt{server}) or die @$;
1131 die "unknown protocol: $opt{protocol}";
1134 $net->login( $opt{username}, $opt{password} )
1135 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1137 $net->binary or die "can't set binary mode";
1139 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1141 $net->put($file) or die "can't put $file: $!";
1151 Spools CSV invoice data.
1157 =item format - 'default' or 'billco'
1159 =item dest - if set (to POST, EMAIL or FAX), only sends spools invoices if the customer has the corresponding invoice destinations set (see L<FS::cust_main_invoice>).
1161 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1163 =item balanceover - if set, only spools the invoice if the total amount owed on this invoice and all older invoices is greater than the specified amount.
1170 my($self, %opt) = @_;
1172 my $cust_main = $self->cust_main;
1174 if ( $opt{'dest'} ) {
1175 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1176 $cust_main->invoicing_list;
1177 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1178 || ! keys %invoicing_list;
1181 if ( $opt{'balanceover'} ) {
1183 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1186 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1187 mkdir $spooldir, 0700 unless -d $spooldir;
1189 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1193 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1194 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1197 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1199 open(CSV, ">>$file") or die "can't open $file: $!";
1200 flock(CSV, LOCK_EX);
1205 if ( lc($opt{'format'}) eq 'billco' ) {
1207 flock(CSV, LOCK_UN);
1212 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1215 open(CSV,">>$file") or die "can't open $file: $!";
1216 flock(CSV, LOCK_EX);
1222 flock(CSV, LOCK_UN);
1229 =item print_csv OPTION => VALUE, ...
1231 Returns CSV data for this invoice.
1235 format - 'default' or 'billco'
1237 Returns a list consisting of two scalars. The first is a single line of CSV
1238 header information for this invoice. The second is one or more lines of CSV
1239 detail information for this invoice.
1241 If I<format> is not specified or "default", the fields of the CSV file are as
1244 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1248 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1250 B<record_type> is C<cust_bill> for the initial header line only. The
1251 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1252 fields are filled in.
1254 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1255 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1258 =item invnum - invoice number
1260 =item custnum - customer number
1262 =item _date - invoice date
1264 =item charged - total invoice amount
1266 =item first - customer first name
1268 =item last - customer first name
1270 =item company - company name
1272 =item address1 - address line 1
1274 =item address2 - address line 1
1284 =item pkg - line item description
1286 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1288 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1290 =item sdate - start date for recurring fee
1292 =item edate - end date for recurring fee
1296 If I<format> is "billco", the fields of the header CSV file are as follows:
1298 +-------------------------------------------------------------------+
1299 | FORMAT HEADER FILE |
1300 |-------------------------------------------------------------------|
1301 | Field | Description | Name | Type | Width |
1302 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1303 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1304 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1305 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1306 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1307 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1308 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1309 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1310 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1311 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1312 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1313 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1314 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1315 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1316 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1317 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1318 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1319 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1320 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1321 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1322 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1323 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1324 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1325 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1326 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1327 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1328 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1329 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1330 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1331 +-------+-------------------------------+------------+------+-------+
1333 If I<format> is "billco", the fields of the detail CSV file are as follows:
1335 FORMAT FOR DETAIL FILE
1337 Field | Description | Name | Type | Width
1338 1 | N/A-Leave Empty | RC | CHAR | 2
1339 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1340 3 | Account Number | TRACCTNUM | CHAR | 15
1341 4 | Invoice Number | TRINVOICE | CHAR | 15
1342 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1343 6 | Transaction Detail | DETAILS | CHAR | 100
1344 7 | Amount | AMT | NUM* | 9
1345 8 | Line Format Control** | LNCTRL | CHAR | 2
1346 9 | Grouping Code | GROUP | CHAR | 2
1347 10 | User Defined | ACCT CODE | CHAR | 15
1352 my($self, %opt) = @_;
1354 eval "use Text::CSV_XS";
1357 my $cust_main = $self->cust_main;
1359 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1361 if ( lc($opt{'format'}) eq 'billco' ) {
1364 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1366 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1368 my( $previous_balance, @unused ) = $self->previous; #previous balance
1370 my $pmt_cr_applied = 0;
1371 $pmt_cr_applied += $_->{'amount'}
1372 foreach ( $self->_items_payments, $self->_items_credits ) ;
1374 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1377 '', # 1 | N/A-Leave Empty CHAR 2
1378 '', # 2 | N/A-Leave Empty CHAR 15
1379 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1380 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1381 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1382 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1383 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1384 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1385 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1386 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1387 '', # 10 | Ancillary Billing Information CHAR 30
1388 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1389 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1392 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1395 $duedate, # 14 | Bill Due Date CHAR 10
1397 $previous_balance, # 15 | Previous Balance NUM* 9
1398 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1399 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1400 $totaldue, # 18 | Total Amt Due NUM* 9
1401 $totaldue, # 19 | Total Amt Due NUM* 9
1402 '', # 20 | 30 Day Aging NUM* 9
1403 '', # 21 | 60 Day Aging NUM* 9
1404 '', # 22 | 90 Day Aging NUM* 9
1405 'N', # 23 | Y/N CHAR 1
1406 '', # 24 | Remittance automation CHAR 100
1407 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1408 $self->custnum, # 26 | Customer Reference Number CHAR 15
1409 '0', # 27 | Federal Tax*** NUM* 9
1410 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1411 '0', # 29 | Other Taxes & Fees*** NUM* 9
1420 time2str("%x", $self->_date),
1421 sprintf("%.2f", $self->charged),
1422 ( map { $cust_main->getfield($_) }
1423 qw( first last company address1 address2 city state zip country ) ),
1425 ) or die "can't create csv";
1428 my $header = $csv->string. "\n";
1431 if ( lc($opt{'format'}) eq 'billco' ) {
1434 foreach my $item ( $self->_items_pkg ) {
1437 '', # 1 | N/A-Leave Empty CHAR 2
1438 '', # 2 | N/A-Leave Empty CHAR 15
1439 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1440 $self->invnum, # 4 | Invoice Number CHAR 15
1441 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1442 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1443 $item->{'amount'}, # 7 | Amount NUM* 9
1444 '', # 8 | Line Format Control** CHAR 2
1445 '', # 9 | Grouping Code CHAR 2
1446 '', # 10 | User Defined CHAR 15
1449 $detail .= $csv->string. "\n";
1455 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1457 my($pkg, $setup, $recur, $sdate, $edate);
1458 if ( $cust_bill_pkg->pkgnum ) {
1460 ($pkg, $setup, $recur, $sdate, $edate) = (
1461 $cust_bill_pkg->part_pkg->pkg,
1462 ( $cust_bill_pkg->setup != 0
1463 ? sprintf("%.2f", $cust_bill_pkg->setup )
1465 ( $cust_bill_pkg->recur != 0
1466 ? sprintf("%.2f", $cust_bill_pkg->recur )
1468 ( $cust_bill_pkg->sdate
1469 ? time2str("%x", $cust_bill_pkg->sdate)
1471 ($cust_bill_pkg->edate
1472 ?time2str("%x", $cust_bill_pkg->edate)
1476 } else { #pkgnum tax
1477 next unless $cust_bill_pkg->setup != 0;
1478 $pkg = $cust_bill_pkg->desc;
1479 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
1480 ( $sdate, $edate ) = ( '', '' );
1486 ( map { '' } (1..11) ),
1487 ($pkg, $setup, $recur, $sdate, $edate)
1488 ) or die "can't create csv";
1490 $detail .= $csv->string. "\n";
1496 ( $header, $detail );
1502 Pays this invoice with a compliemntary payment. If there is an error,
1503 returns the error, otherwise returns false.
1509 my $cust_pay = new FS::cust_pay ( {
1510 'invnum' => $self->invnum,
1511 'paid' => $self->owed,
1514 'payinfo' => $self->cust_main->payinfo,
1522 Attempts to pay this invoice with a credit card payment via a
1523 Business::OnlinePayment realtime gateway. See
1524 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1525 for supported processors.
1531 $self->realtime_bop( 'CC', @_ );
1536 Attempts to pay this invoice with an electronic check (ACH) payment via a
1537 Business::OnlinePayment realtime gateway. See
1538 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1539 for supported processors.
1545 $self->realtime_bop( 'ECHECK', @_ );
1550 Attempts to pay this invoice with phone bill (LEC) payment via a
1551 Business::OnlinePayment realtime gateway. See
1552 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1553 for supported processors.
1559 $self->realtime_bop( 'LEC', @_ );
1563 my( $self, $method ) = @_;
1565 my $cust_main = $self->cust_main;
1566 my $balance = $cust_main->balance;
1567 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1568 $amount = sprintf("%.2f", $amount);
1569 return "not run (balance $balance)" unless $amount > 0;
1571 my $description = 'Internet Services';
1572 if ( $conf->exists('business-onlinepayment-description') ) {
1573 my $dtempl = $conf->config('business-onlinepayment-description');
1575 my $agent_obj = $cust_main->agent
1576 or die "can't retreive agent for $cust_main (agentnum ".
1577 $cust_main->agentnum. ")";
1578 my $agent = $agent_obj->agent;
1579 my $pkgs = join(', ',
1580 map { $_->part_pkg->pkg }
1581 grep { $_->pkgnum } $self->cust_bill_pkg
1583 $description = eval qq("$dtempl");
1586 $cust_main->realtime_bop($method, $amount,
1587 'description' => $description,
1588 'invnum' => $self->invnum,
1593 =item batch_card OPTION => VALUE...
1595 Adds a payment for this invoice to the pending credit card batch (see
1596 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1597 runs the payment using a realtime gateway.
1602 my ($self, %options) = @_;
1603 my $cust_main = $self->cust_main;
1605 $options{invnum} = $self->invnum;
1607 $cust_main->batch_card(%options);
1610 sub _agent_template {
1612 $self->cust_main->agent_template;
1615 sub _agent_invoice_from {
1617 $self->cust_main->agent_invoice_from;
1620 =item print_text [ TIME [ , TEMPLATE ] ]
1622 Returns an text invoice, as a list of lines.
1624 TIME an optional value used to control the printing of overdue messages. The
1625 default is now. It isn't the date of the invoice; that's the `_date' field.
1626 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1627 L<Time::Local> and L<Date::Parse> for conversion functions.
1632 my( $self, $today, $template, %opt ) = @_;
1634 my %params = ( 'format' => 'template' );
1635 $params{'time'} = $today if $today;
1636 $params{'template'} = $template if $template;
1637 $params{'unsquelch_cdr'} = $opt{'unsquelch_cdr'} if $opt{'unsquelch_cdr'};
1639 $self->print_generic( %params );
1642 =item print_latex [ TIME [ , TEMPLATE ] ]
1644 Internal method - returns a filename of a filled-in LaTeX template for this
1645 invoice (Note: add ".tex" to get the actual filename), and a filename of
1646 an associated logo (with the .eps extension included).
1648 See print_ps and print_pdf for methods that return PostScript and PDF output.
1650 TIME an optional value used to control the printing of overdue messages. The
1651 default is now. It isn't the date of the invoice; that's the `_date' field.
1652 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1653 L<Time::Local> and L<Date::Parse> for conversion functions.
1658 my( $self, $today, $template, %opt ) = @_;
1660 my %params = ( 'format' => 'latex' );
1661 $params{'time'} = $today if $today;
1662 $params{'template'} = $template if $template;
1663 $params{'unsquelch_cdr'} = $opt{'unsquelch_cdr'} if $opt{'unsquelch_cdr'};
1665 $template ||= $self->_agent_template;
1667 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
1668 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1672 ) or die "can't open temp file: $!\n";
1674 my $agentnum = $self->cust_main->agentnum;
1676 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
1677 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
1678 or die "can't write temp file: $!\n";
1680 print $lh $conf->config_binary('logo.eps', $agentnum)
1681 or die "can't write temp file: $!\n";
1684 $params{'logo_file'} = $lh->filename;
1686 my @filled_in = $self->print_generic( %params );
1688 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1692 ) or die "can't open temp file: $!\n";
1693 print $fh join('', @filled_in );
1696 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1697 return ($1, $params{'logo_file'});
1701 =item print_generic OPTIONS_HASH
1703 Internal method - returns a filled-in template for this invoice as a scalar.
1705 See print_ps and print_pdf for methods that return PostScript and PDF output.
1707 Non optional options include
1708 format - latex, html, template
1710 Optional options include
1712 template - a value used as a suffix for a configuration template
1714 time - a value used to control the printing of overdue messages. The
1715 default is now. It isn't the date of the invoice; that's the `_date' field.
1716 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1717 L<Time::Local> and L<Date::Parse> for conversion functions.
1721 unsquelch_cdr - overrides any per customer cdr squelching when true
1725 #what's with all the sprintf('%10.2f')'s in here? will it cause any
1726 # (alignment?) problems to change them all to '%.2f' ?
1729 my( $self, %params ) = @_;
1730 my $today = $params{today} ? $params{today} : time;
1731 warn "FS::cust_bill::print_generic called on $self with suffix $params{template}\n"
1734 my $format = $params{format};
1735 die "Unknown format: $format"
1736 unless $format =~ /^(latex|html|template)$/;
1738 my $cust_main = $self->cust_main;
1739 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1740 unless $cust_main->payname
1741 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
1743 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
1744 'html' => [ '<%=', '%>' ],
1745 'template' => [ '{', '}' ],
1748 #create the template
1749 my $template = $params{template} ? $params{template} : $self->_agent_template;
1750 my $templatefile = "invoice_$format";
1751 $templatefile .= "_$template"
1752 if length($template);
1753 my @invoice_template = map "$_\n", $conf->config($templatefile)
1754 or die "cannot load config data $templatefile";
1757 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
1758 #change this to a die when the old code is removed
1759 warn "old-style invoice template $templatefile; ".
1760 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1761 $old_latex = 'true';
1762 @invoice_template = _translate_old_latex_format(@invoice_template);
1765 my $text_template = new Text::Template(
1767 SOURCE => \@invoice_template,
1768 DELIMITERS => $delimiters{$format},
1771 $text_template->compile()
1772 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
1775 # additional substitution could possibly cause breakage in existing templates
1776 my %convert_maps = (
1778 'notes' => sub { map "$_", @_ },
1779 'footer' => sub { map "$_", @_ },
1780 'smallfooter' => sub { map "$_", @_ },
1781 'returnaddress' => sub { map "$_", @_ },
1782 'coupon' => sub { map "$_", @_ },
1788 s/%%(.*)$/<!-- $1 -->/g;
1789 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
1790 s/\\begin\{enumerate\}/<ol>/g;
1792 s/\\end\{enumerate\}/<\/ol>/g;
1793 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
1802 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1804 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1809 s/\\\\\*?\s*$/<BR>/;
1810 s/\\hyphenation\{[\w\s\-]+}//;
1815 'coupon' => sub { "" },
1822 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
1823 s/\\begin\{enumerate\}//g;
1825 s/\\end\{enumerate\}//g;
1826 s/\\textbf\{(.*)\}/$1/g;
1833 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
1835 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
1840 s/\\\\\*?\s*$/\n/; # dubious
1841 s/\\hyphenation\{[\w\s\-]+}//;
1845 'coupon' => sub { "" },
1850 # hashes for differing output formats
1851 my %nbsps = ( 'latex' => '~',
1852 'html' => '', # '&nbps;' would be nice
1853 'template' => '', # not used
1855 my $nbsp = $nbsps{$format};
1857 my %escape_functions = ( 'latex' => \&_latex_escape,
1858 'html' => \&encode_entities,
1859 'template' => sub { shift },
1861 my $escape_function = $escape_functions{$format};
1863 my %date_formats = ( 'latex' => '%b %o, %Y',
1864 'html' => '%b %o, %Y',
1867 my $date_format = $date_formats{$format};
1869 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
1871 'html' => sub { return '<b>'. shift(). '</b>'
1873 'template' => sub { shift },
1875 my $embolden_function = $embolden_functions{$format};
1878 # generate template variables
1881 defined( $conf->config_orbase( "invoice_${format}returnaddress",
1885 && length( $conf->config_orbase( "invoice_${format}returnaddress",
1891 $returnaddress = join("\n",
1892 $conf->config_orbase("invoice_${format}returnaddress", $template)
1895 } elsif ( grep /\S/,
1896 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
1898 my $convert_map = $convert_maps{$format}{'returnaddress'};
1901 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
1906 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
1908 my $convert_map = $convert_maps{$format}{'returnaddress'};
1909 $returnaddress = join( "\n", &$convert_map(
1910 map { s/( {2,})/'~' x length($1)/eg;
1914 ( $conf->config('company_name', $self->cust_main->agentnum),
1915 $conf->config('company_address', $self->cust_main->agentnum),
1922 my $warning = "Couldn't find a return address; ".
1923 "do you need to set the company_address configuration value?";
1925 $returnaddress = $nbsp;
1926 #$returnaddress = $warning;
1930 my %invoice_data = (
1931 'company_name' => scalar( $conf->config('company_name', $self->cust_main->agentnum) ),
1932 'company_address' => join("\n", $conf->config('company_address', $self->cust_main->agentnum) ). "\n",
1933 'custnum' => $cust_main->display_custnum,
1934 'invnum' => $self->invnum,
1935 'date' => time2str($date_format, $self->_date),
1936 'today' => time2str('%b %o, %Y', $today),
1937 'agent' => &$escape_function($cust_main->agent->agent),
1938 'agent_custid' => &$escape_function($cust_main->agent_custid),
1939 'payname' => &$escape_function($cust_main->payname),
1940 'company' => &$escape_function($cust_main->company),
1941 'address1' => &$escape_function($cust_main->address1),
1942 'address2' => &$escape_function($cust_main->address2),
1943 'city' => &$escape_function($cust_main->city),
1944 'state' => &$escape_function($cust_main->state),
1945 'zip' => &$escape_function($cust_main->zip),
1946 'fax' => &$escape_function($cust_main->fax),
1947 'returnaddress' => $returnaddress,
1949 'terms' => $self->terms,
1950 'template' => $template, #params{'template'},
1951 #'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1952 # better hang on to conf_dir for a while
1953 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1956 'current_charges' => sprintf("%.2f", $self->charged),
1957 'duedate' => $self->due_date2str('%m/%d/%Y'), #date_format?
1958 'ship_enable' => $conf->exists('invoice-ship_address'),
1959 'unitprices' => $conf->exists('invoice-unitprice'),
1962 my $countrydefault = $conf->config('countrydefault') || 'US';
1963 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
1964 foreach ( qw( contact company address1 address2 city state zip country fax) ){
1965 my $method = $prefix.$_;
1966 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
1968 $invoice_data{'ship_country'} = ''
1969 if ( $invoice_data{'ship_country'} eq $countrydefault );
1971 $invoice_data{'cid'} = $params{'cid'}
1974 if ( $cust_main->country eq $countrydefault ) {
1975 $invoice_data{'country'} = '';
1977 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
1981 $invoice_data{'address'} = \@address;
1983 $cust_main->payname.
1984 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1985 ? " (P.O. #". $cust_main->payinfo. ")"
1989 push @address, $cust_main->company
1990 if $cust_main->company;
1991 push @address, $cust_main->address1;
1992 push @address, $cust_main->address2
1993 if $cust_main->address2;
1995 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1996 push @address, $invoice_data{'country'}
1997 if $invoice_data{'country'};
1999 while (scalar(@address) < 5);
2001 $invoice_data{'logo_file'} = $params{'logo_file'}
2002 if $params{'logo_file'};
2004 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2005 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2006 #my $balance_due = $self->owed + $pr_total - $cr_total;
2007 my $balance_due = $self->owed + $pr_total;
2008 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2009 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2011 my $agentnum = $self->cust_main->agentnum;
2013 #do variable substitution in notes, footer, smallfooter
2014 foreach my $include (qw( notes footer smallfooter coupon )) {
2016 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2019 if ( $conf->exists($inc_file, $agentnum)
2020 && length( $conf->config($inc_file, $agentnum) ) ) {
2022 @inc_src = $conf->config($inc_file, $agentnum);
2026 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2028 my $convert_map = $convert_maps{$format}{$include};
2030 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2031 s/--\@\]/$delimiters{$format}[1]/g;
2034 &$convert_map( $conf->config($inc_file, $agentnum) );
2038 my $inc_tt = new Text::Template (
2040 SOURCE => [ map "$_\n", @inc_src ],
2041 DELIMITERS => $delimiters{$format},
2042 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2044 unless ( $inc_tt->compile() ) {
2045 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2046 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2050 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2052 $invoice_data{$include} =~ s/\n+$//
2053 if ($format eq 'latex');
2056 $invoice_data{'po_line'} =
2057 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2058 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2061 my %money_chars = ( 'latex' => '',
2062 'html' => $conf->config('money_char') || '$',
2065 my $money_char = $money_chars{$format};
2067 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2068 'html' => $conf->config('money_char') || '$',
2071 my $other_money_char = $other_money_chars{$format};
2073 my @detail_items = ();
2074 my @total_items = ();
2078 $invoice_data{'detail_items'} = \@detail_items;
2079 $invoice_data{'total_items'} = \@total_items;
2080 $invoice_data{'buf'} = \@buf;
2081 $invoice_data{'sections'} = \@sections;
2083 my $previous_section = { 'description' => 'Previous Charges',
2084 'subtotal' => $other_money_char.
2085 sprintf('%.2f', $pr_total),
2089 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2090 'subtotal' => $taxtotal }; # adjusted below
2092 my $adjusttotal = 0;
2093 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2094 'subtotal' => 0 }; # adjusted below
2096 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2097 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2098 my $late_sections = [];
2099 if ( $multisection ) {
2100 push @sections, $self->_items_sections( $late_sections );
2102 push @sections, { 'description' => '', 'subtotal' => '' };
2105 unless ( $conf->exists('disable_previous_balance')
2106 || $conf->exists('previous_balance-summary_only')
2110 foreach my $line_item ( $self->_items_previous ) {
2113 ext_description => [],
2115 $detail->{'ref'} = $line_item->{'pkgnum'};
2116 $detail->{'quantity'} = 1;
2117 $detail->{'section'} = $previous_section;
2118 $detail->{'description'} = &$escape_function($line_item->{'description'});
2119 if ( exists $line_item->{'ext_description'} ) {
2120 @{$detail->{'ext_description'}} = map {
2121 &$escape_function($_);
2122 } @{$line_item->{'ext_description'}};
2124 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2125 $line_item->{'amount'};
2126 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2128 push @detail_items, $detail;
2129 push @buf, [ $detail->{'description'},
2130 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2136 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2137 push @buf, ['','-----------'];
2138 push @buf, [ 'Total Previous Balance',
2139 $money_char. sprintf("%10.2f", $pr_total) ];
2143 foreach my $section (@sections, @$late_sections) {
2145 $section->{'subtotal'} = $other_money_char.
2146 sprintf('%.2f', $section->{'subtotal'})
2149 if ( $section->{'description'} ) {
2150 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2156 $options{'section'} = $section if $multisection;
2157 $options{'format'} = $format;
2158 $options{'escape_function'} = $escape_function;
2159 $options{'format_function'} = sub { () } unless $unsquelched;
2160 $options{'unsquelched'} = $unsquelched;
2162 foreach my $line_item ( $self->_items_pkg(%options) ) {
2164 ext_description => [],
2166 $detail->{'ref'} = $line_item->{'pkgnum'};
2167 $detail->{'quantity'} = $line_item->{'quantity'};
2168 $detail->{'section'} = $section;
2169 $detail->{'description'} = &$escape_function($line_item->{'description'});
2170 if ( exists $line_item->{'ext_description'} ) {
2171 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2173 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2174 $line_item->{'amount'};
2175 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2176 $line_item->{'unit_amount'};
2177 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2179 push @detail_items, $detail;
2180 push @buf, ( [ $detail->{'description'},
2181 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2183 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2187 if ( $section->{'description'} ) {
2188 push @buf, ( ['','-----------'],
2189 [ $section->{'description'}. ' sub-total',
2190 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2199 if ( $multisection && !$conf->exists('disable_previous_balance') ) {
2200 unshift @sections, $previous_section if $pr_total;
2203 foreach my $tax ( $self->_items_tax ) {
2205 $taxtotal += $tax->{'amount'};
2207 my $description = &$escape_function( $tax->{'description'} );
2208 my $amount = sprintf( '%.2f', $tax->{'amount'} );
2210 if ( $multisection ) {
2212 my $money = $old_latex ? '' : $money_char;
2213 push @detail_items, {
2214 ext_description => [],
2217 description => $description,
2218 amount => $money. $amount,
2220 section => $tax_section,
2225 push @total_items, {
2226 'total_item' => $description,
2227 'total_amount' => $other_money_char. $amount,
2232 push @buf,[ $description,
2233 $money_char. $amount,
2240 $total->{'total_item'} = 'Sub-total';
2241 $total->{'total_amount'} =
2242 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2244 if ( $multisection ) {
2245 $tax_section->{'subtotal'} = $other_money_char.
2246 sprintf('%.2f', $taxtotal);
2247 $tax_section->{'pretotal'} = 'New charges sub-total '.
2248 $total->{'total_amount'};
2249 push @sections, $tax_section if $taxtotal;
2251 unshift @total_items, $total;
2254 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2256 push @buf,['','-----------'];
2257 push @buf,[( $conf->exists('disable_previous_balance')
2259 : 'Total New Charges'
2261 $money_char. sprintf("%10.2f",$self->charged) ];
2266 $total->{'total_item'} = &$embolden_function('Total');
2267 $total->{'total_amount'} =
2268 &$embolden_function(
2271 $self->charged + ( $conf->exists('disable_previous_balance')
2277 if ( $multisection ) {
2278 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2279 sprintf('%.2f', $self->charged );
2281 push @total_items, $total;
2283 push @buf,['','-----------'];
2284 push @buf,['Total Charges',
2286 sprintf( '%10.2f', $self->charged +
2287 ( $conf->exists('disable_previous_balance')
2296 unless ( $conf->exists('disable_previous_balance') ) {
2297 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2300 my $credittotal = 0;
2301 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
2304 $total->{'total_item'} = &$escape_function($credit->{'description'});
2305 $credittotal += $credit->{'amount'};
2306 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2307 $adjusttotal += $credit->{'amount'};
2308 if ( $multisection ) {
2309 my $money = $old_latex ? '' : $money_char;
2310 push @detail_items, {
2311 ext_description => [],
2314 description => &$escape_function($credit->{'description'}),
2315 amount => $money. $credit->{'amount'},
2317 section => $adjust_section,
2320 push @total_items, $total;
2324 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2327 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
2328 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
2332 my $paymenttotal = 0;
2333 foreach my $payment ( $self->_items_payments ) {
2335 $total->{'total_item'} = &$escape_function($payment->{'description'});
2336 $paymenttotal += $payment->{'amount'};
2337 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2338 $adjusttotal += $payment->{'amount'};
2339 if ( $multisection ) {
2340 my $money = $old_latex ? '' : $money_char;
2341 push @detail_items, {
2342 ext_description => [],
2345 description => &$escape_function($payment->{'description'}),
2346 amount => $money. $payment->{'amount'},
2348 section => $adjust_section,
2351 push @total_items, $total;
2353 push @buf, [ $payment->{'description'},
2354 $money_char. sprintf("%10.2f", $payment->{'amount'}),
2357 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2359 if ( $multisection ) {
2360 $adjust_section->{'subtotal'} = $other_money_char.
2361 sprintf('%.2f', $adjusttotal);
2362 push @sections, $adjust_section;
2367 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2368 $total->{'total_amount'} =
2369 &$embolden_function(
2370 $other_money_char. sprintf('%.2f', $self->owed + $pr_total )
2372 if ( $multisection ) {
2373 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2374 $total->{'total_amount'};
2376 push @total_items, $total;
2378 push @buf,['','-----------'];
2379 push @buf,[$self->balance_due_msg, $money_char.
2380 sprintf("%10.2f", $balance_due ) ];
2384 if ( $multisection ) {
2385 push @sections, @$late_sections
2391 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
2392 /invoice_lines\((\d*)\)/;
2393 $invoice_lines += $1 || scalar(@buf);
2396 die "no invoice_lines() functions in template?"
2397 if ( $format eq 'template' && !$wasfunc );
2399 if ($format eq 'template') {
2401 if ( $invoice_lines ) {
2402 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
2403 $invoice_data{'total_pages'}++
2404 if scalar(@buf) % $invoice_lines;
2407 #setup subroutine for the template
2408 sub FS::cust_bill::_template::invoice_lines {
2409 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
2411 scalar(@FS::cust_bill::_template::buf)
2412 ? shift @FS::cust_bill::_template::buf
2421 push @collect, split("\n",
2422 $text_template->fill_in( HASH => \%invoice_data,
2423 PACKAGE => 'FS::cust_bill::_template'
2426 $FS::cust_bill::_template::page++;
2428 map "$_\n", @collect;
2430 warn "filling in template for invoice ". $self->invnum. "\n"
2432 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
2435 $text_template->fill_in(HASH => \%invoice_data);
2439 =item print_ps [ TIME [ , TEMPLATE ] ]
2441 Returns an postscript invoice, as a scalar.
2443 TIME an optional value used to control the printing of overdue messages. The
2444 default is now. It isn't the date of the invoice; that's the `_date' field.
2445 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2446 L<Time::Local> and L<Date::Parse> for conversion functions.
2453 my ($file, $lfile) = $self->print_latex(@_);
2454 my $ps = generate_ps($file);
2460 =item print_pdf [ TIME [ , TEMPLATE ] ]
2462 Returns an PDF invoice, as a scalar.
2464 TIME an optional value used to control the printing of overdue messages. The
2465 default is now. It isn't the date of the invoice; that's the `_date' field.
2466 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2467 L<Time::Local> and L<Date::Parse> for conversion functions.
2474 my ($file, $lfile) = $self->print_latex(@_);
2475 my $pdf = generate_pdf($file);
2481 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2483 Returns an HTML invoice, as a scalar.
2485 TIME an optional value used to control the printing of overdue messages. The
2486 default is now. It isn't the date of the invoice; that's the `_date' field.
2487 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2488 L<Time::Local> and L<Date::Parse> for conversion functions.
2490 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2491 when emailing the invoice as part of a multipart/related MIME email.
2499 %params = %{ shift() };
2501 $params{'time'} = shift;
2502 $params{'template'} = shift;
2503 $params{'cid'} = shift;
2506 $params{'format'} = 'html';
2508 $self->print_generic( %params );
2511 # quick subroutine for print_latex
2513 # There are ten characters that LaTeX treats as special characters, which
2514 # means that they do not simply typeset themselves:
2515 # # $ % & ~ _ ^ \ { }
2517 # TeX ignores blanks following an escaped character; if you want a blank (as
2518 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
2522 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2523 $value =~ s/([<>])/\$$1\$/g;
2527 #utility methods for print_*
2529 sub _translate_old_latex_format {
2530 warn "_translate_old_latex_format called\n"
2537 if ( $line =~ /^%%Detail\s*$/ ) {
2539 push @template, q![@--!,
2540 q! foreach my $_tr_line (@detail_items) {!,
2541 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
2542 q! $_tr_line->{'description'} .= !,
2543 q! "\\tabularnewline\n~~".!,
2544 q! join( "\\tabularnewline\n~~",!,
2545 q! @{$_tr_line->{'ext_description'}}!,
2549 while ( ( my $line_item_line = shift )
2550 !~ /^%%EndDetail\s*$/ ) {
2551 $line_item_line =~ s/'/\\'/g; # nice LTS
2552 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
2553 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2554 push @template, " \$OUT .= '$line_item_line';";
2557 push @template, '}',
2560 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
2562 push @template, '[@--',
2563 ' foreach my $_tr_line (@total_items) {';
2565 while ( ( my $total_item_line = shift )
2566 !~ /^%%EndTotalDetails\s*$/ ) {
2567 $total_item_line =~ s/'/\\'/g; # nice LTS
2568 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
2569 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2570 push @template, " \$OUT .= '$total_item_line';";
2573 push @template, '}',
2577 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
2578 push @template, $line;
2584 warn "$_\n" foreach @template;
2593 #check for an invoice- specific override (eventually)
2595 #check for a customer- specific override
2596 return $self->cust_main->invoice_terms
2597 if $self->cust_main->invoice_terms;
2599 #use configured default
2600 $conf->config('invoice_default_terms') || '';
2606 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
2607 $duedate = $self->_date() + ( $1 * 86400 );
2614 $self->due_date ? time2str(shift, $self->due_date) : '';
2617 sub balance_due_msg {
2619 my $msg = 'Balance Due';
2620 return $msg unless $self->terms;
2621 if ( $self->due_date ) {
2622 $msg .= ' - Please pay by '. $self->due_date2str('%x');
2623 } elsif ( $self->terms ) {
2624 $msg .= ' - '. $self->terms;
2629 sub balance_due_date {
2632 if ( $conf->exists('invoice_default_terms')
2633 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
2634 $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
2639 =item invnum_date_pretty
2641 Returns a string with the invoice number and date, for example:
2642 "Invoice #54 (3/20/2008)"
2646 sub invnum_date_pretty {
2648 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
2653 Returns a string with the date, for example: "3/20/2008"
2659 time2str('%x', $self->_date);
2662 sub _items_sections {
2669 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
2672 if ( $cust_bill_pkg->pkgnum > 0 ) {
2673 my $usage = $cust_bill_pkg->usage;
2675 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
2676 my $desc = $display->section;
2677 my $type = $display->type;
2679 if ( $display->post_total ) {
2680 if (! $type || $type eq 'S') {
2681 $l{$desc} += $cust_bill_pkg->setup
2682 if ( $cust_bill_pkg->setup != 0 );
2686 $l{$desc} += $cust_bill_pkg->recur
2687 if ( $cust_bill_pkg->recur != 0 );
2690 if ($type && $type eq 'R') {
2691 $l{$desc} += $cust_bill_pkg->recur - $usage
2692 if ( $cust_bill_pkg->recur != 0 );
2695 if ($type && $type eq 'U') {
2696 $l{$desc} += $usage;
2700 if (! $type || $type eq 'S') {
2701 $s{$desc} += $cust_bill_pkg->setup
2702 if ( $cust_bill_pkg->setup != 0 );
2706 $s{$desc} += $cust_bill_pkg->recur
2707 if ( $cust_bill_pkg->recur != 0 );
2710 if ($type && $type eq 'R') {
2711 $s{$desc} += $cust_bill_pkg->recur - $usage
2712 if ( $cust_bill_pkg->recur != 0 );
2715 if ($type && $type eq 'U') {
2716 $s{$desc} += $usage;
2727 push @$late, map { { 'description' => $_,
2728 'subtotal' => $l{$_},
2732 map { {'description' => $_, 'subtotal' => $s{$_}} } sort keys %s;
2739 #my @display = scalar(@_)
2741 # : qw( _items_previous _items_pkg );
2742 # #: qw( _items_pkg );
2743 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2744 my @display = qw( _items_previous _items_pkg );
2747 foreach my $display ( @display ) {
2748 push @b, $self->$display(@_);
2753 sub _items_previous {
2755 my $cust_main = $self->cust_main;
2756 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2758 foreach ( @pr_cust_bill ) {
2760 'description' => 'Previous Balance, Invoice #'. $_->invnum.
2761 ' ('. time2str('%x',$_->_date). ')',
2762 #'pkgpart' => 'N/A',
2764 'amount' => sprintf("%.2f", $_->owed),
2770 # 'description' => 'Previous Balance',
2771 # #'pkgpart' => 'N/A',
2772 # 'pkgnum' => 'N/A',
2773 # 'amount' => sprintf("%10.2f", $pr_total ),
2774 # 'ext_description' => [ map {
2775 # "Invoice ". $_->invnum.
2776 # " (". time2str("%x",$_->_date). ") ".
2777 # sprintf("%10.2f", $_->owed)
2778 # } @pr_cust_bill ],
2785 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2786 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2790 return 0 unless $a cmp $b;
2791 return -1 if $b eq 'Tax';
2792 return 1 if $a eq 'Tax';
2793 return -1 if $b eq 'Other surcharges';
2794 return 1 if $a eq 'Other surcharges';
2800 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2801 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2804 sub _items_cust_bill_pkg {
2806 my $cust_bill_pkg = shift;
2809 my $format = $opt{format} || '';
2810 my $escape_function = $opt{escape_function} || sub { shift };
2811 my $format_function = $opt{format_function} || '';
2812 my $unsquelched = $opt{unsquelched} || '';
2813 my $section = $opt{section}->{description} if $opt{section};
2816 my ($s, $r, $u) = ( undef, undef, undef );
2817 foreach my $cust_bill_pkg ( @$cust_bill_pkg )
2820 foreach ( $s, $r, $u ) {
2821 if ( $_ && !$cust_bill_pkg->hidden ) {
2822 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2823 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2829 foreach my $display ( grep { defined($section)
2830 ? $_->section eq $section
2833 $cust_bill_pkg->cust_bill_pkg_display
2837 my $type = $display->type;
2839 my $cust_pkg = $cust_bill_pkg->cust_pkg;
2841 my $desc = $cust_bill_pkg->desc;
2842 $desc = substr($desc, 0, 50). '...'
2843 if $format eq 'latex' && length($desc) > 50;
2845 my %details_opt = ( 'format' => $format,
2846 'escape_function' => $escape_function,
2847 'format_function' => $format_function,
2850 if ( $cust_bill_pkg->pkgnum > 0 ) {
2852 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
2854 my $description = $desc;
2855 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2858 push @d, map &{$escape_function}($_),
2859 $cust_pkg->h_labels_short($self->_date)
2860 unless $cust_pkg->part_pkg->hide_svc_detail
2861 || $cust_bill_pkg->hidden;
2862 push @d, $cust_bill_pkg->details(%details_opt)
2863 if $cust_bill_pkg->recur == 0;
2865 if ( $cust_bill_pkg->hidden ) {
2866 $s->{amount} += $cust_bill_pkg->setup;
2867 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
2868 push @{ $s->{ext_description} }, @d;
2871 description => $description,
2872 #pkgpart => $part_pkg->pkgpart,
2873 pkgnum => $cust_bill_pkg->pkgnum,
2874 amount => $cust_bill_pkg->setup,
2875 unit_amount => $cust_bill_pkg->unitsetup,
2876 quantity => $cust_bill_pkg->quantity,
2877 ext_description => \@d,
2883 if ( $cust_bill_pkg->recur != 0 &&
2884 ( !$type || $type eq 'R' || $type eq 'U' )
2888 my $is_summary = $display->summary;
2889 my $description = $is_summary ? "Usage charges" : $desc;
2891 unless ( $conf->exists('disable_line_item_date_ranges') ) {
2892 $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
2893 " - ". time2str("%x", $cust_bill_pkg->edate). ")";
2898 #at least until cust_bill_pkg has "past" ranges in addition to
2899 #the "future" sdate/edate ones... see #3032
2900 my @dates = ( $self->_date );
2901 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
2902 push @dates, $prev->sdate if $prev;
2904 push @d, map &{$escape_function}($_),
2905 $cust_pkg->h_labels_short(@dates)
2906 #$cust_bill_pkg->edate,
2907 #$cust_bill_pkg->sdate)
2908 unless $cust_pkg->part_pkg->hide_svc_detail
2909 || $cust_bill_pkg->itemdesc
2910 || $cust_bill_pkg->hidden
2913 push @d, $cust_bill_pkg->details(%details_opt)
2914 unless ($is_summary || $type && $type eq 'R');
2918 $amount = $cust_bill_pkg->recur;
2919 }elsif($type eq 'R') {
2920 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
2921 }elsif($type eq 'U') {
2922 $amount = $cust_bill_pkg->usage;
2925 if ( !$type || $type eq 'R' ) {
2927 if ( $cust_bill_pkg->hidden ) {
2928 $r->{amount} += $amount;
2929 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
2930 push @{ $r->{ext_description} }, @d;
2933 description => $description,
2934 #pkgpart => $part_pkg->pkgpart,
2935 pkgnum => $cust_bill_pkg->pkgnum,
2937 unit_amount => $cust_bill_pkg->unitrecur,
2938 quantity => $cust_bill_pkg->quantity,
2939 ext_description => \@d,
2943 } elsif ( $amount ) { # && $type eq 'U'
2945 if ( $cust_bill_pkg->hidden ) {
2946 $u->{amount} += $amount;
2947 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
2948 push @{ $u->{ext_description} }, @d;
2951 description => $description,
2952 #pkgpart => $part_pkg->pkgpart,
2953 pkgnum => $cust_bill_pkg->pkgnum,
2955 unit_amount => $cust_bill_pkg->unitrecur,
2956 quantity => $cust_bill_pkg->quantity,
2957 ext_description => \@d,
2963 } # recurring or usage with recurring charge
2965 } else { #pkgnum tax or one-shot line item (??)
2967 if ( $cust_bill_pkg->setup != 0 ) {
2969 'description' => $desc,
2970 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
2973 if ( $cust_bill_pkg->recur != 0 ) {
2975 'description' => "$desc (".
2976 time2str("%x", $cust_bill_pkg->sdate). ' - '.
2977 time2str("%x", $cust_bill_pkg->edate). ')',
2978 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
2988 foreach ( $s, $r, $u ) {
2990 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
2991 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
3000 sub _items_credits {
3001 my( $self, %opt ) = @_;
3002 my $trim_len = $opt{'trim_len'} || 60;
3006 foreach ( $self->cust_credited ) {
3008 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
3010 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
3011 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
3012 $reason = " ($reason) " if $reason;
3015 #'description' => 'Credit ref\#'. $_->crednum.
3016 # " (". time2str("%x",$_->cust_credit->_date) .")".
3018 'description' => 'Credit applied '.
3019 time2str("%x",$_->cust_credit->_date). $reason,
3020 'amount' => sprintf("%.2f",$_->amount),
3028 sub _items_payments {
3032 #get & print payments
3033 foreach ( $self->cust_bill_pay ) {
3035 #something more elaborate if $_->amount ne ->cust_pay->paid ?
3038 'description' => "Payment received ".
3039 time2str("%x",$_->cust_pay->_date ),
3040 'amount' => sprintf("%.2f", $_->amount )
3050 Returns an array of CSV strings representing the call details for this invoice
3056 map { $_->details( 'format_function' => sub{ shift },
3057 'escape_function' => sub{ return() },
3061 $self->cust_bill_pkg;
3071 =item process_reprint
3075 sub process_reprint {
3076 process_re_X('print', @_);
3079 =item process_reemail
3083 sub process_reemail {
3084 process_re_X('email', @_);
3092 process_re_X('fax', @_);
3100 process_re_X('ftp', @_);
3107 sub process_respool {
3108 process_re_X('spool', @_);
3111 use Storable qw(thaw);
3115 my( $method, $job ) = ( shift, shift );
3116 warn "$me process_re_X $method for job $job\n" if $DEBUG;
3118 my $param = thaw(decode_base64(shift));
3119 warn Dumper($param) if $DEBUG;
3130 my($method, $job, %param ) = @_;
3132 warn "re_X $method for job $job with param:\n".
3133 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
3136 #some false laziness w/search/cust_bill.html
3138 my $orderby = 'ORDER BY cust_bill._date';
3140 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
3142 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
3144 my @cust_bill = qsearch( {
3145 #'select' => "cust_bill.*",
3146 'table' => 'cust_bill',
3147 'addl_from' => $addl_from,
3149 'extra_sql' => $extra_sql,
3150 'order_by' => $orderby,
3154 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
3156 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
3159 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
3160 foreach my $cust_bill ( @cust_bill ) {
3161 $cust_bill->$method();
3163 if ( $job ) { #progressbar foo
3165 if ( time - $min_sec > $last ) {
3166 my $error = $job->update_statustext(
3167 int( 100 * $num / scalar(@cust_bill) )
3169 die $error if $error;
3180 =head1 CLASS METHODS
3186 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
3192 'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
3197 Returns an SQL fragment to retreive the net amount (charged minus credited).
3203 'charged - '. $class->credited_sql;
3208 Returns an SQL fragment to retreive the amount paid against this invoice.
3214 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
3215 WHERE cust_bill.invnum = cust_bill_pay.invnum )";
3220 Returns an SQL fragment to retreive the amount credited against this invoice.
3226 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
3227 WHERE cust_bill.invnum = cust_credit_bill.invnum )";
3230 =item search_sql HASHREF
3232 Class method which returns an SQL WHERE fragment to search for parameters
3233 specified in HASHREF. Valid parameters are
3239 Epoch date (UNIX timestamp) setting a lower bound for _date values
3243 Epoch date (UNIX timestamp) setting an upper bound for _date values
3257 =item newest_percust
3261 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
3266 my($class, $param) = @_;
3268 warn "$me search_sql called with params: \n".
3269 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
3274 if ( $param->{'begin'} =~ /^(\d+)$/ ) {
3275 push @search, "cust_bill._date >= $1";
3277 if ( $param->{'end'} =~ /^(\d+)$/ ) {
3278 push @search, "cust_bill._date < $1";
3280 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
3281 push @search, "cust_bill.invnum >= $1";
3283 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
3284 push @search, "cust_bill.invnum <= $1";
3286 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
3287 push @search, "cust_main.agentnum = $1";
3290 push @search, '0 != '. FS::cust_bill->owed_sql
3291 if $param->{'open'};
3293 push @search, '0 != '. FS::cust_bill->net_sql
3296 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
3297 if $param->{'days'};
3299 if ( $param->{'newest_percust'} ) {
3301 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
3302 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
3304 my @newest_where = map { my $x = $_;
3305 $x =~ s/\bcust_bill\./newest_cust_bill./g;
3308 grep ! /^cust_main./, @search;
3309 my $newest_where = scalar(@newest_where)
3310 ? ' AND '. join(' AND ', @newest_where)
3314 push @search, "cust_bill._date = (
3315 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
3316 WHERE newest_cust_bill.custnum = cust_bill.custnum
3322 my $curuser = $FS::CurrentUser::CurrentUser;
3323 if ( $curuser->username eq 'fs_queue'
3324 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
3326 my $newuser = qsearchs('access_user', {
3327 'username' => $username,
3331 $curuser = $newuser;
3333 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
3337 push @search, $curuser->agentnums_sql;
3339 join(' AND ', @search );
3351 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3352 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base