4 use vars qw( @ISA $DEBUG $me $conf
5 $money_char $date_format $rdate_format $date_format_long );
6 use vars qw( $invoice_lines @buf ); #yuck
7 use Fcntl qw(:flock); #for spool_csv
8 use List::Util qw(min max);
10 use Text::Template 1.20;
12 use String::ShellQuote;
15 use Storable qw( freeze thaw );
17 use FS::UID qw( datasrc );
18 use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
19 use FS::Record qw( qsearch qsearchs dbh );
20 use FS::cust_main_Mixin;
22 use FS::cust_statement;
23 use FS::cust_bill_pkg;
24 use FS::cust_bill_pkg_display;
25 use FS::cust_bill_pkg_detail;
29 use FS::cust_credit_bill;
31 use FS::cust_pay_batch;
32 use FS::cust_bill_event;
35 use FS::cust_bill_pay;
36 use FS::cust_bill_pay_batch;
37 use FS::part_bill_event;
40 use FS::cust_bill_batch;
43 @ISA = qw( FS::cust_main_Mixin FS::Record );
46 $me = '[FS::cust_bill]';
48 #ask FS::UID to run this stuff for us later
49 FS::UID->install_callback( sub {
51 $money_char = $conf->config('money_char') || '$';
52 $date_format = $conf->config('date_format') || '%x'; #/YY
53 $rdate_format = $conf->config('date_format') || '%m/%d/%Y'; #/YYYY
54 $date_format_long = $conf->config('date_format_long') || '%b %o, %Y';
59 FS::cust_bill - Object methods for cust_bill records
65 $record = new FS::cust_bill \%hash;
66 $record = new FS::cust_bill { 'column' => 'value' };
68 $error = $record->insert;
70 $error = $new_record->replace($old_record);
72 $error = $record->delete;
74 $error = $record->check;
76 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
78 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
80 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
82 @cust_pay_objects = $cust_bill->cust_pay;
84 $tax_amount = $record->tax;
86 @lines = $cust_bill->print_text;
87 @lines = $cust_bill->print_text $time;
91 An FS::cust_bill object represents an invoice; a declaration that a customer
92 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
93 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
94 following fields are currently supported:
100 =item invnum - primary key (assigned automatically for new invoices)
102 =item custnum - customer (see L<FS::cust_main>)
104 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
105 L<Time::Local> and L<Date::Parse> for conversion functions.
107 =item charged - amount of this invoice
109 =item invoice_terms - optional terms override for this specific invoice
113 Customer info at invoice generation time
117 =item previous_balance
119 =item billing_balance
127 =item printed - deprecated
135 =item closed - books closed flag, empty or `Y'
137 =item statementnum - invoice aggregation (see L<FS::cust_statement>)
139 =item agent_invid - legacy invoice number
149 Creates a new invoice. To add the invoice to the database, see L<"insert">.
150 Invoices are normally created by calling the bill method of a customer object
151 (see L<FS::cust_main>).
155 sub table { 'cust_bill'; }
157 sub cust_linked { $_[0]->cust_main_custnum; }
158 sub cust_unlinked_msg {
160 "WARNING: can't find cust_main.custnum ". $self->custnum.
161 ' (cust_bill.invnum '. $self->invnum. ')';
166 Adds this invoice to the database ("Posts" the invoice). If there is an error,
167 returns the error, otherwise returns false.
173 warn "$me insert called\n" if $DEBUG;
175 local $SIG{HUP} = 'IGNORE';
176 local $SIG{INT} = 'IGNORE';
177 local $SIG{QUIT} = 'IGNORE';
178 local $SIG{TERM} = 'IGNORE';
179 local $SIG{TSTP} = 'IGNORE';
180 local $SIG{PIPE} = 'IGNORE';
182 my $oldAutoCommit = $FS::UID::AutoCommit;
183 local $FS::UID::AutoCommit = 0;
186 my $error = $self->SUPER::insert;
188 $dbh->rollback if $oldAutoCommit;
192 if ( $self->get('cust_bill_pkg') ) {
193 foreach my $cust_bill_pkg ( @{$self->get('cust_bill_pkg')} ) {
194 $cust_bill_pkg->invnum($self->invnum);
195 my $error = $cust_bill_pkg->insert;
197 $dbh->rollback if $oldAutoCommit;
198 return "can't create invoice line item: $error";
203 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
210 This method now works but you probably shouldn't use it. Instead, apply a
211 credit against the invoice.
213 Using this method to delete invoices outright is really, really bad. There
214 would be no record you ever posted this invoice, and there are no check to
215 make sure charged = 0 or that there are no associated cust_bill_pkg records.
217 Really, don't use it.
223 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
225 local $SIG{HUP} = 'IGNORE';
226 local $SIG{INT} = 'IGNORE';
227 local $SIG{QUIT} = 'IGNORE';
228 local $SIG{TERM} = 'IGNORE';
229 local $SIG{TSTP} = 'IGNORE';
230 local $SIG{PIPE} = 'IGNORE';
232 my $oldAutoCommit = $FS::UID::AutoCommit;
233 local $FS::UID::AutoCommit = 0;
236 foreach my $table (qw(
248 foreach my $linked ( $self->$table() ) {
249 my $error = $linked->delete;
251 $dbh->rollback if $oldAutoCommit;
258 my $error = $self->SUPER::delete(@_);
260 $dbh->rollback if $oldAutoCommit;
264 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
270 =item replace [ OLD_RECORD ]
272 You can, but probably shouldn't modify invoices...
274 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
275 supplied, replaces this record. If there is an error, returns the error,
276 otherwise returns false.
280 #replace can be inherited from Record.pm
282 # replace_check is now the preferred way to #implement replace data checks
283 # (so $object->replace() works without an argument)
286 my( $new, $old ) = ( shift, shift );
287 return "Can't modify closed invoice" if $old->closed =~ /^Y/i;
288 #return "Can't change _date!" unless $old->_date eq $new->_date;
289 return "Can't change _date" unless $old->_date == $new->_date;
290 return "Can't change charged" unless $old->charged == $new->charged
291 || $old->charged == 0;
298 Checks all fields to make sure this is a valid invoice. If there is an error,
299 returns the error, otherwise returns false. Called by the insert and replace
308 $self->ut_numbern('invnum')
309 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
310 || $self->ut_numbern('_date')
311 || $self->ut_money('charged')
312 || $self->ut_numbern('printed')
313 || $self->ut_enum('closed', [ '', 'Y' ])
314 || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
315 || $self->ut_numbern('agent_invid') #varchar?
317 return $error if $error;
319 $self->_date(time) unless $self->_date;
321 $self->printed(0) if $self->printed eq '';
328 Returns the displayed invoice number for this invoice: agent_invid if
329 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
335 if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
336 return $self->agent_invid;
338 return $self->invnum;
344 Returns a list consisting of the total previous balance for this customer,
345 followed by the previous outstanding invoices (as FS::cust_bill objects also).
352 my @cust_bill = sort { $a->_date <=> $b->_date }
353 grep { $_->owed != 0 && $_->_date < $self->_date }
354 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
356 foreach ( @cust_bill ) { $total += $_->owed; }
362 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
369 { 'table' => 'cust_bill_pkg',
370 'hashref' => { 'invnum' => $self->invnum },
371 'order_by' => 'ORDER BY billpkgnum',
376 =item cust_bill_pkg_pkgnum PKGNUM
378 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
383 sub cust_bill_pkg_pkgnum {
384 my( $self, $pkgnum ) = @_;
386 { 'table' => 'cust_bill_pkg',
387 'hashref' => { 'invnum' => $self->invnum,
390 'order_by' => 'ORDER BY billpkgnum',
397 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
404 my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
405 $self->cust_bill_pkg;
407 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
412 Returns true if any of the packages (or their definitions) corresponding to the
413 line items for this invoice have the no_auto flag set.
419 grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
422 =item open_cust_bill_pkg
424 Returns the open line items for this invoice.
426 Note that cust_bill_pkg with both setup and recur fees are returned as two
427 separate line items, each with only one fee.
431 # modeled after cust_main::open_cust_bill
432 sub open_cust_bill_pkg {
435 # grep { $_->owed > 0 } $self->cust_bill_pkg
437 my %other = ( 'recur' => 'setup',
438 'setup' => 'recur', );
440 foreach my $field ( qw( recur setup )) {
441 push @open, map { $_->set( $other{$field}, 0 ); $_; }
442 grep { $_->owed($field) > 0 }
443 $self->cust_bill_pkg;
449 =item cust_bill_event
451 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
455 sub cust_bill_event {
457 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
460 =item num_cust_bill_event
462 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
466 sub num_cust_bill_event {
469 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
470 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
471 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
472 $sth->fetchrow_arrayref->[0];
477 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
481 #false laziness w/cust_pkg.pm
485 'table' => 'cust_event',
486 'addl_from' => 'JOIN part_event USING ( eventpart )',
487 'hashref' => { 'tablenum' => $self->invnum },
488 'extra_sql' => " AND eventtable = 'cust_bill' ",
494 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
498 #false laziness w/cust_pkg.pm
502 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
503 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
504 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
505 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
506 $sth->fetchrow_arrayref->[0];
511 Returns the customer (see L<FS::cust_main>) for this invoice.
517 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
520 =item cust_suspend_if_balance_over AMOUNT
522 Suspends the customer associated with this invoice if the total amount owed on
523 this invoice and all older invoices is greater than the specified amount.
525 Returns a list: an empty list on success or a list of errors.
529 sub cust_suspend_if_balance_over {
530 my( $self, $amount ) = ( shift, shift );
531 my $cust_main = $self->cust_main;
532 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
535 $cust_main->suspend(@_);
541 Depreciated. See the cust_credited method.
543 #Returns a list consisting of the total previous credited (see
544 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
545 #outstanding credits (FS::cust_credit objects).
551 croak "FS::cust_bill->cust_credit depreciated; see ".
552 "FS::cust_bill->cust_credit_bill";
555 #my @cust_credit = sort { $a->_date <=> $b->_date }
556 # grep { $_->credited != 0 && $_->_date < $self->_date }
557 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
559 #foreach (@cust_credit) { $total += $_->credited; }
560 #$total, @cust_credit;
565 Depreciated. See the cust_bill_pay method.
567 #Returns all payments (see L<FS::cust_pay>) for this invoice.
573 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
575 #sort { $a->_date <=> $b->_date }
576 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
582 qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
585 sub cust_bill_pay_batch {
587 qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
592 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
598 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
599 sort { $a->_date <=> $b->_date }
600 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
605 =item cust_credit_bill
607 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
613 map { $_ } #return $self->num_cust_credit_bill unless wantarray;
614 sort { $a->_date <=> $b->_date }
615 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
619 sub cust_credit_bill {
620 shift->cust_credited(@_);
623 =item cust_bill_pay_pkgnum PKGNUM
625 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
626 with matching pkgnum.
630 sub cust_bill_pay_pkgnum {
631 my( $self, $pkgnum ) = @_;
632 map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
633 sort { $a->_date <=> $b->_date }
634 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
640 =item cust_credited_pkgnum PKGNUM
642 =item cust_credit_bill_pkgnum PKGNUM
644 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
645 with matching pkgnum.
649 sub cust_credited_pkgnum {
650 my( $self, $pkgnum ) = @_;
651 map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
652 sort { $a->_date <=> $b->_date }
653 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
659 sub cust_credit_bill_pkgnum {
660 shift->cust_credited_pkgnum(@_);
665 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
672 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
674 foreach (@taxlines) { $total += $_->setup; }
680 Returns the amount owed (still outstanding) on this invoice, which is charged
681 minus all payment applications (see L<FS::cust_bill_pay>) and credit
682 applications (see L<FS::cust_credit_bill>).
688 my $balance = $self->charged;
689 $balance -= $_->amount foreach ( $self->cust_bill_pay );
690 $balance -= $_->amount foreach ( $self->cust_credited );
691 $balance = sprintf( "%.2f", $balance);
692 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
697 my( $self, $pkgnum ) = @_;
699 #my $balance = $self->charged;
701 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
703 $balance -= $_->amount for $self->cust_bill_pay_pkgnum($pkgnum);
704 $balance -= $_->amount for $self->cust_credited_pkgnum($pkgnum);
706 $balance = sprintf( "%.2f", $balance);
707 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
711 =item apply_payments_and_credits [ OPTION => VALUE ... ]
713 Applies unapplied payments and credits to this invoice.
715 A hash of optional arguments may be passed. Currently "manual" is supported.
716 If true, a payment receipt is sent instead of a statement when
717 'payment_receipt_email' configuration option is set.
719 If there is an error, returns the error, otherwise returns false.
723 sub apply_payments_and_credits {
724 my( $self, %options ) = @_;
726 local $SIG{HUP} = 'IGNORE';
727 local $SIG{INT} = 'IGNORE';
728 local $SIG{QUIT} = 'IGNORE';
729 local $SIG{TERM} = 'IGNORE';
730 local $SIG{TSTP} = 'IGNORE';
731 local $SIG{PIPE} = 'IGNORE';
733 my $oldAutoCommit = $FS::UID::AutoCommit;
734 local $FS::UID::AutoCommit = 0;
737 $self->select_for_update; #mutex
739 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
740 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
742 if ( $conf->exists('pkg-balances') ) {
743 # limit @payments & @credits to those w/ a pkgnum grepped from $self
744 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
745 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
746 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
749 while ( $self->owed > 0 and ( @payments || @credits ) ) {
752 if ( @payments && @credits ) {
754 #decide which goes first by weight of top (unapplied) line item
756 my @open_lineitems = $self->open_cust_bill_pkg;
759 max( map { $_->part_pkg->pay_weight || 0 }
764 my $max_credit_weight =
765 max( map { $_->part_pkg->credit_weight || 0 }
771 #if both are the same... payments first? it has to be something
772 if ( $max_pay_weight >= $max_credit_weight ) {
778 } elsif ( @payments ) {
780 } elsif ( @credits ) {
783 die "guru meditation #12 and 35";
787 if ( $app eq 'pay' ) {
789 my $payment = shift @payments;
790 $unapp_amount = $payment->unapplied;
791 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
792 $app->pkgnum( $payment->pkgnum )
793 if $conf->exists('pkg-balances') && $payment->pkgnum;
795 } elsif ( $app eq 'credit' ) {
797 my $credit = shift @credits;
798 $unapp_amount = $credit->credited;
799 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
800 $app->pkgnum( $credit->pkgnum )
801 if $conf->exists('pkg-balances') && $credit->pkgnum;
804 die "guru meditation #12 and 35";
808 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
809 warn "owed_pkgnum ". $app->pkgnum;
810 $owed = $self->owed_pkgnum($app->pkgnum);
814 next unless $owed > 0;
816 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
817 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
819 $app->invnum( $self->invnum );
821 my $error = $app->insert(%options);
823 $dbh->rollback if $oldAutoCommit;
824 return "Error inserting ". $app->table. " record: $error";
826 die $error if $error;
830 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
835 =item generate_email OPTION => VALUE ...
843 sender address, required
847 alternate template name, optional
851 text attachment arrayref, optional
855 email subject, optional
859 notice name instead of "Invoice", optional
863 Returns an argument list to be passed to L<FS::Misc::send_email>.
874 my $me = '[FS::cust_bill::generate_email]';
877 'from' => $args{'from'},
878 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
882 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
883 'template' => $args{'template'},
884 'notice_name' => ( $args{'notice_name'} || 'Invoice' ),
887 my $cust_main = $self->cust_main;
889 if (ref($args{'to'}) eq 'ARRAY') {
890 $return{'to'} = $args{'to'};
892 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
893 $cust_main->invoicing_list
897 if ( $conf->exists('invoice_html') ) {
899 warn "$me creating HTML/text multipart message"
902 $return{'nobody'} = 1;
904 my $alternative = build MIME::Entity
905 'Type' => 'multipart/alternative',
906 'Encoding' => '7bit',
907 'Disposition' => 'inline'
911 if ( $conf->exists('invoice_email_pdf')
912 and scalar($conf->config('invoice_email_pdf_note')) ) {
914 warn "$me using 'invoice_email_pdf_note' in multipart message"
916 $data = [ map { $_ . "\n" }
917 $conf->config('invoice_email_pdf_note')
922 warn "$me not using 'invoice_email_pdf_note' in multipart message"
924 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
925 $data = $args{'print_text'};
927 $data = [ $self->print_text(\%opt) ];
932 $alternative->attach(
933 'Type' => 'text/plain',
934 #'Encoding' => 'quoted-printable',
935 'Encoding' => '7bit',
937 'Disposition' => 'inline',
940 $args{'from'} =~ /\@([\w\.\-]+)/;
941 my $from = $1 || 'example.com';
942 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
945 my $agentnum = $cust_main->agentnum;
946 if ( defined($args{'template'}) && length($args{'template'})
947 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
950 $logo = 'logo_'. $args{'template'}. '.png';
954 my $image_data = $conf->config_binary( $logo, $agentnum);
956 my $image = build MIME::Entity
957 'Type' => 'image/png',
958 'Encoding' => 'base64',
959 'Data' => $image_data,
960 'Filename' => 'logo.png',
961 'Content-ID' => "<$content_id>",
965 if($conf->exists('invoice-barcode')){
966 my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
967 $barcode = build MIME::Entity
968 'Type' => 'image/png',
969 'Encoding' => 'base64',
970 'Data' => $self->invoice_barcode(0),
971 'Filename' => 'barcode.png',
972 'Content-ID' => "<$barcode_content_id>",
974 $opt{'barcode_cid'} = $barcode_content_id;
977 $alternative->attach(
978 'Type' => 'text/html',
979 'Encoding' => 'quoted-printable',
980 'Data' => [ '<html>',
983 ' '. encode_entities($return{'subject'}),
986 ' <body bgcolor="#e8e8e8">',
987 $self->print_html({ 'cid'=>$content_id, %opt }),
991 'Disposition' => 'inline',
992 #'Filename' => 'invoice.pdf',
996 if ( $cust_main->email_csv_cdr ) {
998 push @otherparts, build MIME::Entity
999 'Type' => 'text/csv',
1000 'Encoding' => '7bit',
1001 'Data' => [ map { "$_\n" }
1002 $self->call_details('prepend_billed_number' => 1)
1004 'Disposition' => 'attachment',
1005 'Filename' => 'usage-'. $self->invnum. '.csv',
1010 if ( $conf->exists('invoice_email_pdf') ) {
1015 # multipart/alternative
1021 my $related = build MIME::Entity 'Type' => 'multipart/related',
1022 'Encoding' => '7bit';
1024 #false laziness w/Misc::send_email
1025 $related->head->replace('Content-type',
1026 $related->mime_type.
1027 '; boundary="'. $related->head->multipart_boundary. '"'.
1028 '; type=multipart/alternative'
1031 $related->add_part($alternative);
1033 $related->add_part($image);
1035 my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
1037 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
1041 #no other attachment:
1043 # multipart/alternative
1048 $return{'content-type'} = 'multipart/related';
1049 if($conf->exists('invoice-barcode')){
1050 $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ];
1053 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
1055 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
1056 #$return{'disposition'} = 'inline';
1062 if ( $conf->exists('invoice_email_pdf') ) {
1063 warn "$me creating PDF attachment"
1066 #mime parts arguments a la MIME::Entity->build().
1067 $return{'mimeparts'} = [
1068 { $self->mimebuild_pdf(\%opt) }
1072 if ( $conf->exists('invoice_email_pdf')
1073 and scalar($conf->config('invoice_email_pdf_note')) ) {
1075 warn "$me using 'invoice_email_pdf_note'"
1077 $return{'body'} = [ map { $_ . "\n" }
1078 $conf->config('invoice_email_pdf_note')
1083 warn "$me not using 'invoice_email_pdf_note'"
1085 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1086 $return{'body'} = $args{'print_text'};
1088 $return{'body'} = [ $self->print_text(\%opt) ];
1101 Returns a list suitable for passing to MIME::Entity->build(), representing
1102 this invoice as PDF attachment.
1109 'Type' => 'application/pdf',
1110 'Encoding' => 'base64',
1111 'Data' => [ $self->print_pdf(@_) ],
1112 'Disposition' => 'attachment',
1113 'Filename' => 'invoice-'. $self->invnum. '.pdf',
1117 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1119 Sends this invoice to the destinations configured for this customer: sends
1120 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1122 Options can be passed as a hashref (recommended) or as a list of up to
1123 four values for templatename, agentnum, invoice_from and amount.
1125 I<template>, if specified, is the name of a suffix for alternate invoices.
1127 I<agentnum>, if specified, means that this invoice will only be sent for customers
1128 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
1129 single agent) or an arrayref of agentnums.
1131 I<invoice_from>, if specified, overrides the default email invoice From: address.
1133 I<amount>, if specified, only sends the invoice if the total amount owed on this
1134 invoice and all older invoices is greater than the specified amount.
1136 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1140 sub queueable_send {
1143 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1144 or die "invalid invoice number: " . $opt{invnum};
1146 my @args = ( $opt{template}, $opt{agentnum} );
1147 push @args, $opt{invoice_from}
1148 if exists($opt{invoice_from}) && $opt{invoice_from};
1150 my $error = $self->send( @args );
1151 die $error if $error;
1158 my( $template, $invoice_from, $notice_name );
1160 my $balance_over = 0;
1164 $template = $opt->{'template'} || '';
1165 if ( $agentnums = $opt->{'agentnum'} ) {
1166 $agentnums = [ $agentnums ] unless ref($agentnums);
1168 $invoice_from = $opt->{'invoice_from'};
1169 $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1170 $notice_name = $opt->{'notice_name'};
1172 $template = scalar(@_) ? shift : '';
1173 if ( scalar(@_) && $_[0] ) {
1174 $agentnums = ref($_[0]) ? shift : [ shift ];
1176 $invoice_from = shift if scalar(@_);
1177 $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1180 return 'N/A' unless ! $agentnums
1181 or grep { $_ == $self->cust_main->agentnum } @$agentnums;
1184 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
1186 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1187 $conf->config('invoice_from', $self->cust_main->agentnum );
1190 'template' => $template,
1191 'invoice_from' => $invoice_from,
1192 'notice_name' => ( $notice_name || 'Invoice' ),
1195 my @invoicing_list = $self->cust_main->invoicing_list;
1197 #$self->email_invoice(\%opt)
1199 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
1201 #$self->print_invoice(\%opt)
1203 if grep { $_ eq 'POST' } @invoicing_list; #postal
1205 $self->fax_invoice(\%opt)
1206 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1212 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
1214 Emails this invoice.
1216 Options can be passed as a hashref (recommended) or as a list of up to
1217 two values for templatename and invoice_from.
1219 I<template>, if specified, is the name of a suffix for alternate invoices.
1221 I<invoice_from>, if specified, overrides the default email invoice From: address.
1223 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1227 sub queueable_email {
1230 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1231 or die "invalid invoice number: " . $opt{invnum};
1233 my @args = ( $opt{template} );
1234 push @args, $opt{invoice_from}
1235 if exists($opt{invoice_from}) && $opt{invoice_from};
1237 my $error = $self->email( @args );
1238 die $error if $error;
1242 #sub email_invoice {
1246 my( $template, $invoice_from, $notice_name );
1249 $template = $opt->{'template'} || '';
1250 $invoice_from = $opt->{'invoice_from'};
1251 $notice_name = $opt->{'notice_name'} || 'Invoice';
1253 $template = scalar(@_) ? shift : '';
1254 $invoice_from = shift if scalar(@_);
1255 $notice_name = 'Invoice';
1258 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1259 $conf->config('invoice_from', $self->cust_main->agentnum );
1261 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1262 $self->cust_main->invoicing_list;
1264 if ( ! @invoicing_list ) { #no recipients
1265 if ( $conf->exists('cust_bill-no_recipients-error') ) {
1266 die 'No recipients for customer #'. $self->custnum;
1268 #default: better to notify this person than silence
1269 @invoicing_list = ($invoice_from);
1273 my $subject = $self->email_subject($template);
1275 my $error = send_email(
1276 $self->generate_email(
1277 'from' => $invoice_from,
1278 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1279 'subject' => $subject,
1280 'template' => $template,
1281 'notice_name' => $notice_name,
1284 die "can't email invoice: $error\n" if $error;
1285 #die "$error\n" if $error;
1292 #my $template = scalar(@_) ? shift : '';
1295 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1298 my $cust_main = $self->cust_main;
1299 my $name = $cust_main->name;
1300 my $name_short = $cust_main->name_short;
1301 my $invoice_number = $self->invnum;
1302 my $invoice_date = $self->_date_pretty;
1304 eval qq("$subject");
1307 =item lpr_data HASHREF | [ TEMPLATE ]
1309 Returns the postscript or plaintext for this invoice as an arrayref.
1311 Options can be passed as a hashref (recommended) or as a single optional value
1314 I<template>, if specified, is the name of a suffix for alternate invoices.
1316 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1322 my( $template, $notice_name );
1325 $template = $opt->{'template'} || '';
1326 $notice_name = $opt->{'notice_name'} || 'Invoice';
1328 $template = scalar(@_) ? shift : '';
1329 $notice_name = 'Invoice';
1333 'template' => $template,
1334 'notice_name' => $notice_name,
1337 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1338 [ $self->$method( \%opt ) ];
1341 =item print HASHREF | [ TEMPLATE ]
1343 Prints this invoice.
1345 Options can be passed as a hashref (recommended) or as a single optional
1348 I<template>, if specified, is the name of a suffix for alternate invoices.
1350 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1354 #sub print_invoice {
1357 my( $template, $notice_name );
1360 $template = $opt->{'template'} || '';
1361 $notice_name = $opt->{'notice_name'} || 'Invoice';
1363 $template = scalar(@_) ? shift : '';
1364 $notice_name = 'Invoice';
1368 'template' => $template,
1369 'notice_name' => $notice_name,
1372 if($conf->exists('invoice_print_pdf')) {
1373 # Add the invoice to the current batch.
1374 $self->batch_invoice(\%opt);
1377 do_print $self->lpr_data(\%opt);
1381 =item fax_invoice HASHREF | [ TEMPLATE ]
1385 Options can be passed as a hashref (recommended) or as a single optional
1388 I<template>, if specified, is the name of a suffix for alternate invoices.
1390 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1396 my( $template, $notice_name );
1399 $template = $opt->{'template'} || '';
1400 $notice_name = $opt->{'notice_name'} || 'Invoice';
1402 $template = scalar(@_) ? shift : '';
1403 $notice_name = 'Invoice';
1406 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1407 unless $conf->exists('invoice_latex');
1409 my $dialstring = $self->cust_main->getfield('fax');
1413 'template' => $template,
1414 'notice_name' => $notice_name,
1417 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1418 'dialstring' => $dialstring,
1420 die $error if $error;
1424 =item batch_invoice [ HASHREF ]
1426 Place this invoice into the open batch (see C<FS::bill_batch>). If there
1427 isn't an open batch, one will be created.
1432 my ($self, $opt) = @_;
1433 my $batch = FS::bill_batch->get_open_batch;
1434 my $cust_bill_batch = FS::cust_bill_batch->new({
1435 batchnum => $batch->batchnum,
1436 invnum => $self->invnum,
1438 return $cust_bill_batch->insert($opt);
1441 =item ftp_invoice [ TEMPLATENAME ]
1443 Sends this invoice data via FTP.
1445 TEMPLATENAME is unused?
1451 my $template = scalar(@_) ? shift : '';
1454 'protocol' => 'ftp',
1455 'server' => $conf->config('cust_bill-ftpserver'),
1456 'username' => $conf->config('cust_bill-ftpusername'),
1457 'password' => $conf->config('cust_bill-ftppassword'),
1458 'dir' => $conf->config('cust_bill-ftpdir'),
1459 'format' => $conf->config('cust_bill-ftpformat'),
1463 =item spool_invoice [ TEMPLATENAME ]
1465 Spools this invoice data (see L<FS::spool_csv>)
1467 TEMPLATENAME is unused?
1473 my $template = scalar(@_) ? shift : '';
1476 'format' => $conf->config('cust_bill-spoolformat'),
1477 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1481 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1483 Like B<send>, but only sends the invoice if it is the newest open invoice for
1488 sub send_if_newest {
1493 grep { $_->owed > 0 }
1494 qsearch('cust_bill', {
1495 'custnum' => $self->custnum,
1496 #'_date' => { op=>'>', value=>$self->_date },
1497 'invnum' => { op=>'>', value=>$self->invnum },
1504 =item send_csv OPTION => VALUE, ...
1506 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1510 protocol - currently only "ftp"
1516 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1517 and YYMMDDHHMMSS is a timestamp.
1519 See L</print_csv> for a description of the output format.
1524 my($self, %opt) = @_;
1528 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1529 mkdir $spooldir, 0700 unless -d $spooldir;
1531 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1532 my $file = "$spooldir/$tracctnum.csv";
1534 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1536 open(CSV, ">$file") or die "can't open $file: $!";
1544 if ( $opt{protocol} eq 'ftp' ) {
1545 eval "use Net::FTP;";
1547 $net = Net::FTP->new($opt{server}) or die @$;
1549 die "unknown protocol: $opt{protocol}";
1552 $net->login( $opt{username}, $opt{password} )
1553 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1555 $net->binary or die "can't set binary mode";
1557 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1559 $net->put($file) or die "can't put $file: $!";
1569 Spools CSV invoice data.
1575 =item format - 'default' or 'billco'
1577 =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>).
1579 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1581 =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.
1588 my($self, %opt) = @_;
1590 my $cust_main = $self->cust_main;
1592 if ( $opt{'dest'} ) {
1593 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1594 $cust_main->invoicing_list;
1595 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1596 || ! keys %invoicing_list;
1599 if ( $opt{'balanceover'} ) {
1601 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1604 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1605 mkdir $spooldir, 0700 unless -d $spooldir;
1607 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1611 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1612 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1615 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1617 open(CSV, ">>$file") or die "can't open $file: $!";
1618 flock(CSV, LOCK_EX);
1623 if ( lc($opt{'format'}) eq 'billco' ) {
1625 flock(CSV, LOCK_UN);
1630 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1633 open(CSV,">>$file") or die "can't open $file: $!";
1634 flock(CSV, LOCK_EX);
1640 flock(CSV, LOCK_UN);
1647 =item print_csv OPTION => VALUE, ...
1649 Returns CSV data for this invoice.
1653 format - 'default' or 'billco'
1655 Returns a list consisting of two scalars. The first is a single line of CSV
1656 header information for this invoice. The second is one or more lines of CSV
1657 detail information for this invoice.
1659 If I<format> is not specified or "default", the fields of the CSV file are as
1662 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1666 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1668 B<record_type> is C<cust_bill> for the initial header line only. The
1669 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1670 fields are filled in.
1672 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1673 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1676 =item invnum - invoice number
1678 =item custnum - customer number
1680 =item _date - invoice date
1682 =item charged - total invoice amount
1684 =item first - customer first name
1686 =item last - customer first name
1688 =item company - company name
1690 =item address1 - address line 1
1692 =item address2 - address line 1
1702 =item pkg - line item description
1704 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1706 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1708 =item sdate - start date for recurring fee
1710 =item edate - end date for recurring fee
1714 If I<format> is "billco", the fields of the header CSV file are as follows:
1716 +-------------------------------------------------------------------+
1717 | FORMAT HEADER FILE |
1718 |-------------------------------------------------------------------|
1719 | Field | Description | Name | Type | Width |
1720 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1721 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1722 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1723 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1724 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1725 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1726 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1727 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1728 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1729 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1730 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1731 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1732 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1733 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1734 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1735 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1736 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1737 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1738 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1739 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1740 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1741 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1742 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1743 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1744 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1745 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1746 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1747 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1748 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1749 +-------+-------------------------------+------------+------+-------+
1751 If I<format> is "billco", the fields of the detail CSV file are as follows:
1753 FORMAT FOR DETAIL FILE
1755 Field | Description | Name | Type | Width
1756 1 | N/A-Leave Empty | RC | CHAR | 2
1757 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1758 3 | Account Number | TRACCTNUM | CHAR | 15
1759 4 | Invoice Number | TRINVOICE | CHAR | 15
1760 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1761 6 | Transaction Detail | DETAILS | CHAR | 100
1762 7 | Amount | AMT | NUM* | 9
1763 8 | Line Format Control** | LNCTRL | CHAR | 2
1764 9 | Grouping Code | GROUP | CHAR | 2
1765 10 | User Defined | ACCT CODE | CHAR | 15
1770 my($self, %opt) = @_;
1772 eval "use Text::CSV_XS";
1775 my $cust_main = $self->cust_main;
1777 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1779 if ( lc($opt{'format'}) eq 'billco' ) {
1782 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1784 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1786 my( $previous_balance, @unused ) = $self->previous; #previous balance
1788 my $pmt_cr_applied = 0;
1789 $pmt_cr_applied += $_->{'amount'}
1790 foreach ( $self->_items_payments, $self->_items_credits ) ;
1792 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1795 '', # 1 | N/A-Leave Empty CHAR 2
1796 '', # 2 | N/A-Leave Empty CHAR 15
1797 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1798 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1799 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1800 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1801 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1802 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1803 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1804 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1805 '', # 10 | Ancillary Billing Information CHAR 30
1806 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1807 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1810 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1813 $duedate, # 14 | Bill Due Date CHAR 10
1815 $previous_balance, # 15 | Previous Balance NUM* 9
1816 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1817 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1818 $totaldue, # 18 | Total Amt Due NUM* 9
1819 $totaldue, # 19 | Total Amt Due NUM* 9
1820 '', # 20 | 30 Day Aging NUM* 9
1821 '', # 21 | 60 Day Aging NUM* 9
1822 '', # 22 | 90 Day Aging NUM* 9
1823 'N', # 23 | Y/N CHAR 1
1824 '', # 24 | Remittance automation CHAR 100
1825 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1826 $self->custnum, # 26 | Customer Reference Number CHAR 15
1827 '0', # 27 | Federal Tax*** NUM* 9
1828 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1829 '0', # 29 | Other Taxes & Fees*** NUM* 9
1838 time2str("%x", $self->_date),
1839 sprintf("%.2f", $self->charged),
1840 ( map { $cust_main->getfield($_) }
1841 qw( first last company address1 address2 city state zip country ) ),
1843 ) or die "can't create csv";
1846 my $header = $csv->string. "\n";
1849 if ( lc($opt{'format'}) eq 'billco' ) {
1852 foreach my $item ( $self->_items_pkg ) {
1855 '', # 1 | N/A-Leave Empty CHAR 2
1856 '', # 2 | N/A-Leave Empty CHAR 15
1857 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1858 $self->invnum, # 4 | Invoice Number CHAR 15
1859 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1860 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1861 $item->{'amount'}, # 7 | Amount NUM* 9
1862 '', # 8 | Line Format Control** CHAR 2
1863 '', # 9 | Grouping Code CHAR 2
1864 '', # 10 | User Defined CHAR 15
1867 $detail .= $csv->string. "\n";
1873 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1875 my($pkg, $setup, $recur, $sdate, $edate);
1876 if ( $cust_bill_pkg->pkgnum ) {
1878 ($pkg, $setup, $recur, $sdate, $edate) = (
1879 $cust_bill_pkg->part_pkg->pkg,
1880 ( $cust_bill_pkg->setup != 0
1881 ? sprintf("%.2f", $cust_bill_pkg->setup )
1883 ( $cust_bill_pkg->recur != 0
1884 ? sprintf("%.2f", $cust_bill_pkg->recur )
1886 ( $cust_bill_pkg->sdate
1887 ? time2str("%x", $cust_bill_pkg->sdate)
1889 ($cust_bill_pkg->edate
1890 ?time2str("%x", $cust_bill_pkg->edate)
1894 } else { #pkgnum tax
1895 next unless $cust_bill_pkg->setup != 0;
1896 $pkg = $cust_bill_pkg->desc;
1897 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
1898 ( $sdate, $edate ) = ( '', '' );
1904 ( map { '' } (1..11) ),
1905 ($pkg, $setup, $recur, $sdate, $edate)
1906 ) or die "can't create csv";
1908 $detail .= $csv->string. "\n";
1914 ( $header, $detail );
1920 Pays this invoice with a compliemntary payment. If there is an error,
1921 returns the error, otherwise returns false.
1927 my $cust_pay = new FS::cust_pay ( {
1928 'invnum' => $self->invnum,
1929 'paid' => $self->owed,
1932 'payinfo' => $self->cust_main->payinfo,
1940 Attempts to pay this invoice with a credit card payment via a
1941 Business::OnlinePayment realtime gateway. See
1942 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1943 for supported processors.
1949 $self->realtime_bop( 'CC', @_ );
1954 Attempts to pay this invoice with an electronic check (ACH) payment via a
1955 Business::OnlinePayment realtime gateway. See
1956 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1957 for supported processors.
1963 $self->realtime_bop( 'ECHECK', @_ );
1968 Attempts to pay this invoice with phone bill (LEC) payment via a
1969 Business::OnlinePayment realtime gateway. See
1970 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1971 for supported processors.
1977 $self->realtime_bop( 'LEC', @_ );
1981 my( $self, $method ) = @_;
1983 my $cust_main = $self->cust_main;
1984 my $balance = $cust_main->balance;
1985 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1986 $amount = sprintf("%.2f", $amount);
1987 return "not run (balance $balance)" unless $amount > 0;
1989 my $description = 'Internet Services';
1990 if ( $conf->exists('business-onlinepayment-description') ) {
1991 my $dtempl = $conf->config('business-onlinepayment-description');
1993 my $agent_obj = $cust_main->agent
1994 or die "can't retreive agent for $cust_main (agentnum ".
1995 $cust_main->agentnum. ")";
1996 my $agent = $agent_obj->agent;
1997 my $pkgs = join(', ',
1998 map { $_->part_pkg->pkg }
1999 grep { $_->pkgnum } $self->cust_bill_pkg
2001 $description = eval qq("$dtempl");
2004 $cust_main->realtime_bop($method, $amount,
2005 'description' => $description,
2006 'invnum' => $self->invnum,
2007 #this didn't do what we want, it just calls apply_payments_and_credits
2009 'apply_to_invoice' => 1,
2011 #this changes application behavior: auto payments
2012 #triggered against a specific invoice are now applied
2013 #to that invoice instead of oldest open.
2019 =item batch_card OPTION => VALUE...
2021 Adds a payment for this invoice to the pending credit card batch (see
2022 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2023 runs the payment using a realtime gateway.
2028 my ($self, %options) = @_;
2029 my $cust_main = $self->cust_main;
2031 $options{invnum} = $self->invnum;
2033 $cust_main->batch_card(%options);
2036 sub _agent_template {
2038 $self->cust_main->agent_template;
2041 sub _agent_invoice_from {
2043 $self->cust_main->agent_invoice_from;
2046 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2048 Returns an text invoice, as a list of lines.
2050 Options can be passed as a hashref (recommended) or as a list of time, template
2051 and then any key/value pairs for any other options.
2053 I<time>, if specified, is used to control the printing of overdue messages. The
2054 default is now. It isn't the date of the invoice; that's the `_date' field.
2055 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2056 L<Time::Local> and L<Date::Parse> for conversion functions.
2058 I<template>, if specified, is the name of a suffix for alternate invoices.
2060 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2066 my( $today, $template, %opt );
2068 %opt = %{ shift() };
2069 $today = delete($opt{'time'}) || '';
2070 $template = delete($opt{template}) || '';
2072 ( $today, $template, %opt ) = @_;
2075 my %params = ( 'format' => 'template' );
2076 $params{'time'} = $today if $today;
2077 $params{'template'} = $template if $template;
2078 $params{$_} = $opt{$_}
2079 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2081 $self->print_generic( %params );
2084 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2086 Internal method - returns a filename of a filled-in LaTeX template for this
2087 invoice (Note: add ".tex" to get the actual filename), and a filename of
2088 an associated logo (with the .eps extension included).
2090 See print_ps and print_pdf for methods that return PostScript and PDF output.
2092 Options can be passed as a hashref (recommended) or as a list of time, template
2093 and then any key/value pairs for any other options.
2095 I<time>, if specified, is used to control the printing of overdue messages. The
2096 default is now. It isn't the date of the invoice; that's the `_date' field.
2097 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2098 L<Time::Local> and L<Date::Parse> for conversion functions.
2100 I<template>, if specified, is the name of a suffix for alternate invoices.
2102 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2108 my( $today, $template, %opt );
2110 %opt = %{ shift() };
2111 $today = delete($opt{'time'}) || '';
2112 $template = delete($opt{template}) || '';
2114 ( $today, $template, %opt ) = @_;
2117 my %params = ( 'format' => 'latex' );
2118 $params{'time'} = $today if $today;
2119 $params{'template'} = $template if $template;
2120 $params{$_} = $opt{$_}
2121 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2123 $template ||= $self->_agent_template;
2125 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2126 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2130 ) or die "can't open temp file: $!\n";
2132 my $agentnum = $self->cust_main->agentnum;
2134 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2135 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2136 or die "can't write temp file: $!\n";
2138 print $lh $conf->config_binary('logo.eps', $agentnum)
2139 or die "can't write temp file: $!\n";
2142 $params{'logo_file'} = $lh->filename;
2144 if($conf->exists('invoice-barcode')){
2145 my $png_file = $self->invoice_barcode($dir);
2146 my $eps_file = $png_file;
2147 $eps_file =~ s/\.png$/.eps/g;
2148 $png_file =~ /(barcode.*png)/;
2150 $eps_file =~ /(barcode.*eps)/;
2153 my $curr_dir = cwd();
2155 # after painfuly long experimentation, it was determined that sam2p won't
2156 # accept : and other chars in the path, no matter how hard I tried to
2157 # escape them, hence the chdir (and chdir back, just to be safe)
2158 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
2159 or die "sam2p failed: $!\n";
2163 $params{'barcode_file'} = $eps_file;
2166 my @filled_in = $self->print_generic( %params );
2168 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2172 ) or die "can't open temp file: $!\n";
2173 print $fh join('', @filled_in );
2176 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2177 return ($1, $params{'logo_file'}, $params{'barcode_file'});
2181 =item invoice_barcode DIR_OR_FALSE
2183 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2184 it is taken as the temp directory where the PNG file will be generated and the
2185 PNG file name is returned. Otherwise, the PNG image itself is returned.
2189 sub invoice_barcode {
2190 my ($self, $dir) = (shift,shift);
2192 my $gdbar = new GD::Barcode('Code39',$self->invnum);
2193 die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2194 my $gd = $gdbar->plot(Height => 30);
2197 my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2201 ) or die "can't open temp file: $!\n";
2202 print $bh $gd->png or die "cannot write barcode to file: $!\n";
2203 my $png_file = $bh->filename;
2210 =item print_generic OPTION => VALUE ...
2212 Internal method - returns a filled-in template for this invoice as a scalar.
2214 See print_ps and print_pdf for methods that return PostScript and PDF output.
2216 Non optional options include
2217 format - latex, html, template
2219 Optional options include
2221 template - a value used as a suffix for a configuration template
2223 time - a value used to control the printing of overdue messages. The
2224 default is now. It isn't the date of the invoice; that's the `_date' field.
2225 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2226 L<Time::Local> and L<Date::Parse> for conversion functions.
2230 unsquelch_cdr - overrides any per customer cdr squelching when true
2232 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2236 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2237 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2238 # yes: fixed width (dot matrix) text printing will be borked
2241 my( $self, %params ) = @_;
2242 my $today = $params{today} ? $params{today} : time;
2243 warn "$me print_generic called on $self with suffix $params{template}\n"
2246 my $format = $params{format};
2247 die "Unknown format: $format"
2248 unless $format =~ /^(latex|html|template)$/;
2250 my $cust_main = $self->cust_main;
2251 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2252 unless $cust_main->payname
2253 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2255 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2256 'html' => [ '<%=', '%>' ],
2257 'template' => [ '{', '}' ],
2260 warn "$me print_generic creating template\n"
2263 #create the template
2264 my $template = $params{template} ? $params{template} : $self->_agent_template;
2265 my $templatefile = "invoice_$format";
2266 $templatefile .= "_$template"
2267 if length($template);
2268 my @invoice_template = map "$_\n", $conf->config($templatefile)
2269 or die "cannot load config data $templatefile";
2272 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2273 #change this to a die when the old code is removed
2274 warn "old-style invoice template $templatefile; ".
2275 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2276 $old_latex = 'true';
2277 @invoice_template = _translate_old_latex_format(@invoice_template);
2280 warn "$me print_generic creating T:T object\n"
2283 my $text_template = new Text::Template(
2285 SOURCE => \@invoice_template,
2286 DELIMITERS => $delimiters{$format},
2289 warn "$me print_generic compiling T:T object\n"
2292 $text_template->compile()
2293 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2296 # additional substitution could possibly cause breakage in existing templates
2297 my %convert_maps = (
2299 'notes' => sub { map "$_", @_ },
2300 'footer' => sub { map "$_", @_ },
2301 'smallfooter' => sub { map "$_", @_ },
2302 'returnaddress' => sub { map "$_", @_ },
2303 'coupon' => sub { map "$_", @_ },
2304 'summary' => sub { map "$_", @_ },
2310 s/%%(.*)$/<!-- $1 -->/g;
2311 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2312 s/\\begin\{enumerate\}/<ol>/g;
2314 s/\\end\{enumerate\}/<\/ol>/g;
2315 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2324 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2326 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2331 s/\\\\\*?\s*$/<BR>/;
2332 s/\\hyphenation\{[\w\s\-]+}//;
2337 'coupon' => sub { "" },
2338 'summary' => sub { "" },
2345 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2346 s/\\begin\{enumerate\}//g;
2348 s/\\end\{enumerate\}//g;
2349 s/\\textbf\{(.*)\}/$1/g;
2356 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2358 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2363 s/\\\\\*?\s*$/\n/; # dubious
2364 s/\\hyphenation\{[\w\s\-]+}//;
2368 'coupon' => sub { "" },
2369 'summary' => sub { "" },
2374 # hashes for differing output formats
2375 my %nbsps = ( 'latex' => '~',
2376 'html' => '', # '&nbps;' would be nice
2377 'template' => '', # not used
2379 my $nbsp = $nbsps{$format};
2381 my %escape_functions = ( 'latex' => \&_latex_escape,
2382 'html' => \&_html_escape_nbsp,#\&encode_entities,
2383 'template' => sub { shift },
2385 my $escape_function = $escape_functions{$format};
2386 my $escape_function_nonbsp = ($format eq 'html')
2387 ? \&_html_escape : $escape_function;
2389 my %date_formats = ( 'latex' => $date_format_long,
2390 'html' => $date_format_long,
2393 $date_formats{'html'} =~ s/ / /g;
2395 my $date_format = $date_formats{$format};
2397 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2399 'html' => sub { return '<b>'. shift(). '</b>'
2401 'template' => sub { shift },
2403 my $embolden_function = $embolden_functions{$format};
2405 warn "$me generating template variables\n"
2408 # generate template variables
2411 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2415 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2421 $returnaddress = join("\n",
2422 $conf->config_orbase("invoice_${format}returnaddress", $template)
2425 } elsif ( grep /\S/,
2426 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2428 my $convert_map = $convert_maps{$format}{'returnaddress'};
2431 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2436 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2438 my $convert_map = $convert_maps{$format}{'returnaddress'};
2439 $returnaddress = join( "\n", &$convert_map(
2440 map { s/( {2,})/'~' x length($1)/eg;
2444 ( $conf->config('company_name', $self->cust_main->agentnum),
2445 $conf->config('company_address', $self->cust_main->agentnum),
2452 my $warning = "Couldn't find a return address; ".
2453 "do you need to set the company_address configuration value?";
2455 $returnaddress = $nbsp;
2456 #$returnaddress = $warning;
2460 warn "$me generating invoice data\n"
2463 my $agentnum = $self->cust_main->agentnum;
2465 my %invoice_data = (
2468 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2469 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2470 'returnaddress' => $returnaddress,
2471 'agent' => &$escape_function($cust_main->agent->agent),
2474 'invnum' => $self->invnum,
2475 'date' => time2str($date_format, $self->_date),
2476 'today' => time2str($date_format_long, $today),
2477 'terms' => $self->terms,
2478 'template' => $template, #params{'template'},
2479 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2480 'current_charges' => sprintf("%.2f", $self->charged),
2481 'duedate' => $self->due_date2str($rdate_format), #date_format?
2484 'custnum' => $cust_main->display_custnum,
2485 'agent_custid' => &$escape_function($cust_main->agent_custid),
2486 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2487 payname company address1 address2 city state zip fax
2491 'ship_enable' => $conf->exists('invoice-ship_address'),
2492 'unitprices' => $conf->exists('invoice-unitprice'),
2493 'smallernotes' => $conf->exists('invoice-smallernotes'),
2494 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2495 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2497 #layout info -- would be fancy to calc some of this and bury the template
2499 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2500 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2501 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2502 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2503 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2504 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2505 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2506 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2507 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2508 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2510 # better hang on to conf_dir for a while (for old templates)
2511 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2513 #these are only used when doing paged plaintext
2519 my $min_sdate = 999999999999;
2521 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2522 next unless $cust_bill_pkg->pkgnum > 0;
2523 $min_sdate = $cust_bill_pkg->sdate
2524 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2525 $max_edate = $cust_bill_pkg->edate
2526 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2529 $invoice_data{'bill_period'} = '';
2530 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
2531 . " to " . time2str('%e %h', $max_edate)
2532 if ($max_edate != 0 && $min_sdate != 999999999999);
2534 $invoice_data{finance_section} = '';
2535 if ( $conf->config('finance_pkgclass') ) {
2537 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2538 $invoice_data{finance_section} = $pkg_class->categoryname;
2540 $invoice_data{finance_amount} = '0.00';
2541 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2543 my $countrydefault = $conf->config('countrydefault') || 'US';
2544 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2545 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2546 my $method = $prefix.$_;
2547 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2549 $invoice_data{'ship_country'} = ''
2550 if ( $invoice_data{'ship_country'} eq $countrydefault );
2552 $invoice_data{'cid'} = $params{'cid'}
2555 if ( $cust_main->country eq $countrydefault ) {
2556 $invoice_data{'country'} = '';
2558 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2562 $invoice_data{'address'} = \@address;
2564 $cust_main->payname.
2565 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2566 ? " (P.O. #". $cust_main->payinfo. ")"
2570 push @address, $cust_main->company
2571 if $cust_main->company;
2572 push @address, $cust_main->address1;
2573 push @address, $cust_main->address2
2574 if $cust_main->address2;
2576 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2577 push @address, $invoice_data{'country'}
2578 if $invoice_data{'country'};
2580 while (scalar(@address) < 5);
2582 $invoice_data{'logo_file'} = $params{'logo_file'}
2583 if $params{'logo_file'};
2584 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2585 if $params{'barcode_file'};
2586 $invoice_data{'barcode_img'} = $params{'barcode_img'}
2587 if $params{'barcode_img'};
2588 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2589 if $params{'barcode_cid'};
2591 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2592 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2593 #my $balance_due = $self->owed + $pr_total - $cr_total;
2594 my $balance_due = $self->owed + $pr_total;
2595 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2596 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2597 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2598 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2600 my $summarypage = '';
2601 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2604 $invoice_data{'summarypage'} = $summarypage;
2606 warn "$me substituting variables in notes, footer, smallfooter\n"
2609 foreach my $include (qw( notes footer smallfooter coupon )) {
2611 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2614 if ( $conf->exists($inc_file, $agentnum)
2615 && length( $conf->config($inc_file, $agentnum) ) ) {
2617 @inc_src = $conf->config($inc_file, $agentnum);
2621 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2623 my $convert_map = $convert_maps{$format}{$include};
2625 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2626 s/--\@\]/$delimiters{$format}[1]/g;
2629 &$convert_map( $conf->config($inc_file, $agentnum) );
2633 my $inc_tt = new Text::Template (
2635 SOURCE => [ map "$_\n", @inc_src ],
2636 DELIMITERS => $delimiters{$format},
2637 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2639 unless ( $inc_tt->compile() ) {
2640 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2641 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2645 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2647 $invoice_data{$include} =~ s/\n+$//
2648 if ($format eq 'latex');
2651 $invoice_data{'po_line'} =
2652 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2653 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2656 my %money_chars = ( 'latex' => '',
2657 'html' => $conf->config('money_char') || '$',
2660 my $money_char = $money_chars{$format};
2662 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2663 'html' => $conf->config('money_char') || '$',
2666 my $other_money_char = $other_money_chars{$format};
2667 $invoice_data{'dollar'} = $other_money_char;
2669 my @detail_items = ();
2670 my @total_items = ();
2674 $invoice_data{'detail_items'} = \@detail_items;
2675 $invoice_data{'total_items'} = \@total_items;
2676 $invoice_data{'buf'} = \@buf;
2677 $invoice_data{'sections'} = \@sections;
2679 warn "$me generating sections\n"
2682 my $previous_section = { 'description' => 'Previous Charges',
2683 'subtotal' => $other_money_char.
2684 sprintf('%.2f', $pr_total),
2685 'summarized' => $summarypage ? 'Y' : '',
2687 $previous_section->{posttotal} = '0 / 30 / 60/ 90 days overdue '.
2688 join(' / ', map { $cust_main->balance_date_range(@$_) }
2689 $self->_prior_month30s
2691 if $conf->exists('invoice_include_aging');
2694 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2695 'subtotal' => $taxtotal, # adjusted below
2696 'summarized' => $summarypage ? 'Y' : '',
2698 my $tax_weight = _pkg_category($tax_section->{description})
2699 ? _pkg_category($tax_section->{description})->weight
2701 $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2702 $tax_section->{'sort_weight'} = $tax_weight;
2705 my $adjusttotal = 0;
2706 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2707 'subtotal' => 0, # adjusted below
2708 'summarized' => $summarypage ? 'Y' : '',
2710 my $adjust_weight = _pkg_category($adjust_section->{description})
2711 ? _pkg_category($adjust_section->{description})->weight
2713 $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2714 $adjust_section->{'sort_weight'} = $adjust_weight;
2716 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2717 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2718 $invoice_data{'multisection'} = $multisection;
2719 my $late_sections = [];
2720 my $extra_sections = [];
2721 my $extra_lines = ();
2722 if ( $multisection ) {
2723 ($extra_sections, $extra_lines) =
2724 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2725 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2727 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2729 push @detail_items, @$extra_lines if $extra_lines;
2731 $self->_items_sections( $late_sections, # this could stand a refactor
2733 $escape_function_nonbsp,
2737 if ($conf->exists('svc_phone_sections')) {
2738 my ($phone_sections, $phone_lines) =
2739 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2740 push @{$late_sections}, @$phone_sections;
2741 push @detail_items, @$phone_lines;
2744 push @sections, { 'description' => '', 'subtotal' => '' };
2747 unless ( $conf->exists('disable_previous_balance')
2748 || $conf->exists('previous_balance-summary_only')
2752 warn "$me adding previous balances\n"
2755 foreach my $line_item ( $self->_items_previous ) {
2758 ext_description => [],
2760 $detail->{'ref'} = $line_item->{'pkgnum'};
2761 $detail->{'quantity'} = 1;
2762 $detail->{'section'} = $previous_section;
2763 $detail->{'description'} = &$escape_function($line_item->{'description'});
2764 if ( exists $line_item->{'ext_description'} ) {
2765 @{$detail->{'ext_description'}} = map {
2766 &$escape_function($_);
2767 } @{$line_item->{'ext_description'}};
2769 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2770 $line_item->{'amount'};
2771 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2773 push @detail_items, $detail;
2774 push @buf, [ $detail->{'description'},
2775 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2781 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2782 push @buf, ['','-----------'];
2783 push @buf, [ 'Total Previous Balance',
2784 $money_char. sprintf("%10.2f", $pr_total) ];
2788 if ( $conf->exists('svc_phone-did-summary') ) {
2789 warn "$me adding DID summary\n"
2792 my ($didsummary,$minutes) = $self->_did_summary;
2793 my $didsummary_desc = 'DID Activity Summary (Past 30 days)';
2795 { 'description' => $didsummary_desc,
2796 'ext_description' => [ $didsummary, $minutes ],
2801 foreach my $section (@sections, @$late_sections) {
2803 warn "$me adding section \n". Dumper($section)
2806 # begin some normalization
2807 $section->{'subtotal'} = $section->{'amount'}
2809 && !exists($section->{subtotal})
2810 && exists($section->{amount});
2812 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2813 if ( $invoice_data{finance_section} &&
2814 $section->{'description'} eq $invoice_data{finance_section} );
2816 $section->{'subtotal'} = $other_money_char.
2817 sprintf('%.2f', $section->{'subtotal'})
2820 # continue some normalization
2821 $section->{'amount'} = $section->{'subtotal'}
2825 if ( $section->{'description'} ) {
2826 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2831 warn "$me setting options\n"
2834 my $multilocation = scalar($cust_main->cust_location); #too expensive?
2836 $options{'section'} = $section if $multisection;
2837 $options{'format'} = $format;
2838 $options{'escape_function'} = $escape_function;
2839 $options{'format_function'} = sub { () } unless $unsquelched;
2840 $options{'unsquelched'} = $unsquelched;
2841 $options{'summary_page'} = $summarypage;
2842 $options{'skip_usage'} =
2843 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2844 $options{'multilocation'} = $multilocation;
2845 $options{'multisection'} = $multisection;
2847 warn "$me searching for line items\n"
2850 foreach my $line_item ( $self->_items_pkg(%options) ) {
2852 warn "$me adding line item $line_item\n"
2856 ext_description => [],
2858 $detail->{'ref'} = $line_item->{'pkgnum'};
2859 $detail->{'quantity'} = $line_item->{'quantity'};
2860 $detail->{'section'} = $section;
2861 $detail->{'description'} = &$escape_function($line_item->{'description'});
2862 if ( exists $line_item->{'ext_description'} ) {
2863 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2865 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2866 $line_item->{'amount'};
2867 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2868 $line_item->{'unit_amount'};
2869 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2871 push @detail_items, $detail;
2872 push @buf, ( [ $detail->{'description'},
2873 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2875 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2879 if ( $section->{'description'} ) {
2880 push @buf, ( ['','-----------'],
2881 [ $section->{'description'}. ' sub-total',
2882 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2891 $invoice_data{current_less_finance} =
2892 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2894 if ( $multisection && !$conf->exists('disable_previous_balance')
2895 || $conf->exists('previous_balance-summary_only') )
2897 unshift @sections, $previous_section if $pr_total;
2900 warn "$me adding taxes\n"
2903 foreach my $tax ( $self->_items_tax ) {
2905 $taxtotal += $tax->{'amount'};
2907 my $description = &$escape_function( $tax->{'description'} );
2908 my $amount = sprintf( '%.2f', $tax->{'amount'} );
2910 if ( $multisection ) {
2912 my $money = $old_latex ? '' : $money_char;
2913 push @detail_items, {
2914 ext_description => [],
2917 description => $description,
2918 amount => $money. $amount,
2920 section => $tax_section,
2925 push @total_items, {
2926 'total_item' => $description,
2927 'total_amount' => $other_money_char. $amount,
2932 push @buf,[ $description,
2933 $money_char. $amount,
2940 $total->{'total_item'} = 'Sub-total';
2941 $total->{'total_amount'} =
2942 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2944 if ( $multisection ) {
2945 $tax_section->{'subtotal'} = $other_money_char.
2946 sprintf('%.2f', $taxtotal);
2947 $tax_section->{'pretotal'} = 'New charges sub-total '.
2948 $total->{'total_amount'};
2949 push @sections, $tax_section if $taxtotal;
2951 unshift @total_items, $total;
2954 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2956 push @buf,['','-----------'];
2957 push @buf,[( $conf->exists('disable_previous_balance')
2959 : 'Total New Charges'
2961 $money_char. sprintf("%10.2f",$self->charged) ];
2967 $item = $conf->config('previous_balance-exclude_from_total')
2968 || 'Total New Charges'
2969 if $conf->exists('previous_balance-exclude_from_total');
2970 my $amount = $self->charged +
2971 ( $conf->exists('disable_previous_balance') ||
2972 $conf->exists('previous_balance-exclude_from_total')
2976 $total->{'total_item'} = &$embolden_function($item);
2977 $total->{'total_amount'} =
2978 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
2979 if ( $multisection ) {
2980 if ( $adjust_section->{'sort_weight'} ) {
2981 $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
2982 sprintf("%.2f", ($self->billing_balance || 0) );
2984 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2985 sprintf('%.2f', $self->charged );
2988 push @total_items, $total;
2990 push @buf,['','-----------'];
2993 sprintf( '%10.2f', $amount )
2998 unless ( $conf->exists('disable_previous_balance') ) {
2999 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3002 my $credittotal = 0;
3003 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3006 $total->{'total_item'} = &$escape_function($credit->{'description'});
3007 $credittotal += $credit->{'amount'};
3008 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3009 $adjusttotal += $credit->{'amount'};
3010 if ( $multisection ) {
3011 my $money = $old_latex ? '' : $money_char;
3012 push @detail_items, {
3013 ext_description => [],
3016 description => &$escape_function($credit->{'description'}),
3017 amount => $money. $credit->{'amount'},
3019 section => $adjust_section,
3022 push @total_items, $total;
3026 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3029 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3030 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3034 my $paymenttotal = 0;
3035 foreach my $payment ( $self->_items_payments ) {
3037 $total->{'total_item'} = &$escape_function($payment->{'description'});
3038 $paymenttotal += $payment->{'amount'};
3039 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3040 $adjusttotal += $payment->{'amount'};
3041 if ( $multisection ) {
3042 my $money = $old_latex ? '' : $money_char;
3043 push @detail_items, {
3044 ext_description => [],
3047 description => &$escape_function($payment->{'description'}),
3048 amount => $money. $payment->{'amount'},
3050 section => $adjust_section,
3053 push @total_items, $total;
3055 push @buf, [ $payment->{'description'},
3056 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3059 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3061 if ( $multisection ) {
3062 $adjust_section->{'subtotal'} = $other_money_char.
3063 sprintf('%.2f', $adjusttotal);
3064 push @sections, $adjust_section
3065 unless $adjust_section->{sort_weight};
3070 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3071 $total->{'total_amount'} =
3072 &$embolden_function(
3073 $other_money_char. sprintf('%.2f', $summarypage
3075 $self->billing_balance
3076 : $self->owed + $pr_total
3079 if ( $multisection && !$adjust_section->{sort_weight} ) {
3080 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3081 $total->{'total_amount'};
3083 push @total_items, $total;
3085 push @buf,['','-----------'];
3086 push @buf,[$self->balance_due_msg, $money_char.
3087 sprintf("%10.2f", $balance_due ) ];
3091 if ( $multisection ) {
3092 if ($conf->exists('svc_phone_sections')) {
3094 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3095 $total->{'total_amount'} =
3096 &$embolden_function(
3097 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3099 my $last_section = pop @sections;
3100 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3101 $total->{'total_amount'};
3102 push @sections, $last_section;
3104 push @sections, @$late_sections
3108 my @includelist = ();
3109 push @includelist, 'summary' if $summarypage;
3110 foreach my $include ( @includelist ) {
3112 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3115 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3117 @inc_src = $conf->config($inc_file, $agentnum);
3121 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3123 my $convert_map = $convert_maps{$format}{$include};
3125 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3126 s/--\@\]/$delimiters{$format}[1]/g;
3129 &$convert_map( $conf->config($inc_file, $agentnum) );
3133 my $inc_tt = new Text::Template (
3135 SOURCE => [ map "$_\n", @inc_src ],
3136 DELIMITERS => $delimiters{$format},
3137 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3139 unless ( $inc_tt->compile() ) {
3140 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3141 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3145 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3147 $invoice_data{$include} =~ s/\n+$//
3148 if ($format eq 'latex');
3153 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3154 /invoice_lines\((\d*)\)/;
3155 $invoice_lines += $1 || scalar(@buf);
3158 die "no invoice_lines() functions in template?"
3159 if ( $format eq 'template' && !$wasfunc );
3161 if ($format eq 'template') {
3163 if ( $invoice_lines ) {
3164 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3165 $invoice_data{'total_pages'}++
3166 if scalar(@buf) % $invoice_lines;
3169 #setup subroutine for the template
3170 sub FS::cust_bill::_template::invoice_lines {
3171 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
3173 scalar(@FS::cust_bill::_template::buf)
3174 ? shift @FS::cust_bill::_template::buf
3183 push @collect, split("\n",
3184 $text_template->fill_in( HASH => \%invoice_data,
3185 PACKAGE => 'FS::cust_bill::_template'
3188 $FS::cust_bill::_template::page++;
3190 map "$_\n", @collect;
3192 warn "filling in template for invoice ". $self->invnum. "\n"
3194 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3197 $text_template->fill_in(HASH => \%invoice_data);
3201 # helper routine for generating date ranges
3202 sub _prior_month30s {
3205 [ 1, 2592000 ], # 0-30 days ago
3206 [ 2592000, 5184000 ], # 30-60 days ago
3207 [ 5184000, 7776000 ], # 60-90 days ago
3208 [ 7776000, 0 ], # 90+ days ago
3211 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3212 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3217 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3219 Returns an postscript invoice, as a scalar.
3221 Options can be passed as a hashref (recommended) or as a list of time, template
3222 and then any key/value pairs for any other options.
3224 I<time> an optional value used to control the printing of overdue messages. The
3225 default is now. It isn't the date of the invoice; that's the `_date' field.
3226 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3227 L<Time::Local> and L<Date::Parse> for conversion functions.
3229 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3236 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3237 my $ps = generate_ps($file);
3239 unlink($barcodefile);
3244 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3246 Returns an PDF invoice, as a scalar.
3248 Options can be passed as a hashref (recommended) or as a list of time, template
3249 and then any key/value pairs for any other options.
3251 I<time> an optional value used to control the printing of overdue messages. The
3252 default is now. It isn't the date of the invoice; that's the `_date' field.
3253 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3254 L<Time::Local> and L<Date::Parse> for conversion functions.
3256 I<template>, if specified, is the name of a suffix for alternate invoices.
3258 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3265 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3266 my $pdf = generate_pdf($file);
3268 unlink($barcodefile);
3273 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3275 Returns an HTML invoice, as a scalar.
3277 I<time> an optional value used to control the printing of overdue messages. The
3278 default is now. It isn't the date of the invoice; that's the `_date' field.
3279 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3280 L<Time::Local> and L<Date::Parse> for conversion functions.
3282 I<template>, if specified, is the name of a suffix for alternate invoices.
3284 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3286 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3287 when emailing the invoice as part of a multipart/related MIME email.
3295 %params = %{ shift() };
3297 $params{'time'} = shift;
3298 $params{'template'} = shift;
3299 $params{'cid'} = shift;
3302 $params{'format'} = 'html';
3304 $self->print_generic( %params );
3307 # quick subroutine for print_latex
3309 # There are ten characters that LaTeX treats as special characters, which
3310 # means that they do not simply typeset themselves:
3311 # # $ % & ~ _ ^ \ { }
3313 # TeX ignores blanks following an escaped character; if you want a blank (as
3314 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3318 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3319 $value =~ s/([<>])/\$$1\$/g;
3325 encode_entities($value);
3329 sub _html_escape_nbsp {
3330 my $value = _html_escape(shift);
3331 $value =~ s/ +/ /g;
3335 #utility methods for print_*
3337 sub _translate_old_latex_format {
3338 warn "_translate_old_latex_format called\n"
3345 if ( $line =~ /^%%Detail\s*$/ ) {
3347 push @template, q![@--!,
3348 q! foreach my $_tr_line (@detail_items) {!,
3349 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3350 q! $_tr_line->{'description'} .= !,
3351 q! "\\tabularnewline\n~~".!,
3352 q! join( "\\tabularnewline\n~~",!,
3353 q! @{$_tr_line->{'ext_description'}}!,
3357 while ( ( my $line_item_line = shift )
3358 !~ /^%%EndDetail\s*$/ ) {
3359 $line_item_line =~ s/'/\\'/g; # nice LTS
3360 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3361 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3362 push @template, " \$OUT .= '$line_item_line';";
3365 push @template, '}',
3368 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3370 push @template, '[@--',
3371 ' foreach my $_tr_line (@total_items) {';
3373 while ( ( my $total_item_line = shift )
3374 !~ /^%%EndTotalDetails\s*$/ ) {
3375 $total_item_line =~ s/'/\\'/g; # nice LTS
3376 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3377 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3378 push @template, " \$OUT .= '$total_item_line';";
3381 push @template, '}',
3385 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3386 push @template, $line;
3392 warn "$_\n" foreach @template;
3401 #check for an invoice-specific override
3402 return $self->invoice_terms if $self->invoice_terms;
3404 #check for a customer- specific override
3405 my $cust_main = $self->cust_main;
3406 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3408 #use configured default
3409 $conf->config('invoice_default_terms') || '';
3415 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3416 $duedate = $self->_date() + ( $1 * 86400 );
3423 $self->due_date ? time2str(shift, $self->due_date) : '';
3426 sub balance_due_msg {
3428 my $msg = 'Balance Due';
3429 return $msg unless $self->terms;
3430 if ( $self->due_date ) {
3431 $msg .= ' - Please pay by '. $self->due_date2str($date_format);
3432 } elsif ( $self->terms ) {
3433 $msg .= ' - '. $self->terms;
3438 sub balance_due_date {
3441 if ( $conf->exists('invoice_default_terms')
3442 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3443 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3448 =item invnum_date_pretty
3450 Returns a string with the invoice number and date, for example:
3451 "Invoice #54 (3/20/2008)"
3455 sub invnum_date_pretty {
3457 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3462 Returns a string with the date, for example: "3/20/2008"
3468 time2str($date_format, $self->_date);
3471 use vars qw(%pkg_category_cache);
3472 sub _items_sections {
3475 my $summarypage = shift;
3477 my $extra_sections = shift;
3481 my %late_subtotal = ();
3484 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3487 my $usage = $cust_bill_pkg->usage;
3489 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3490 next if ( $display->summary && $summarypage );
3492 my $section = $display->section;
3493 my $type = $display->type;
3495 $not_tax{$section} = 1
3496 unless $cust_bill_pkg->pkgnum == 0;
3498 if ( $display->post_total && !$summarypage ) {
3499 if (! $type || $type eq 'S') {
3500 $late_subtotal{$section} += $cust_bill_pkg->setup
3501 if $cust_bill_pkg->setup != 0;
3505 $late_subtotal{$section} += $cust_bill_pkg->recur
3506 if $cust_bill_pkg->recur != 0;
3509 if ($type && $type eq 'R') {
3510 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3511 if $cust_bill_pkg->recur != 0;
3514 if ($type && $type eq 'U') {
3515 $late_subtotal{$section} += $usage
3516 unless scalar(@$extra_sections);
3521 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3523 if (! $type || $type eq 'S') {
3524 $subtotal{$section} += $cust_bill_pkg->setup
3525 if $cust_bill_pkg->setup != 0;
3529 $subtotal{$section} += $cust_bill_pkg->recur
3530 if $cust_bill_pkg->recur != 0;
3533 if ($type && $type eq 'R') {
3534 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3535 if $cust_bill_pkg->recur != 0;
3538 if ($type && $type eq 'U') {
3539 $subtotal{$section} += $usage
3540 unless scalar(@$extra_sections);
3549 %pkg_category_cache = ();
3551 push @$late, map { { 'description' => &{$escape}($_),
3552 'subtotal' => $late_subtotal{$_},
3554 'sort_weight' => ( _pkg_category($_)
3555 ? _pkg_category($_)->weight
3558 ((_pkg_category($_) && _pkg_category($_)->condense)
3559 ? $self->_condense_section($format)
3563 sort _sectionsort keys %late_subtotal;
3566 if ( $summarypage ) {
3567 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3568 map { $_->categoryname } qsearch('pkg_category', {});
3569 push @sections, '' if exists($subtotal{''});
3571 @sections = keys %subtotal;
3574 my @early = map { { 'description' => &{$escape}($_),
3575 'subtotal' => $subtotal{$_},
3576 'summarized' => $not_tax{$_} ? '' : 'Y',
3577 'tax_section' => $not_tax{$_} ? '' : 'Y',
3578 'sort_weight' => ( _pkg_category($_)
3579 ? _pkg_category($_)->weight
3582 ((_pkg_category($_) && _pkg_category($_)->condense)
3583 ? $self->_condense_section($format)
3588 push @early, @$extra_sections if $extra_sections;
3590 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3594 #helper subs for above
3597 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3601 my $categoryname = shift;
3602 $pkg_category_cache{$categoryname} ||=
3603 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3606 my %condensed_format = (
3607 'label' => [ qw( Description Qty Amount ) ],
3609 sub { shift->{description} },
3610 sub { shift->{quantity} },
3611 sub { my($href, %opt) = @_;
3612 ($opt{dollar} || ''). $href->{amount};
3615 'align' => [ qw( l r r ) ],
3616 'span' => [ qw( 5 1 1 ) ], # unitprices?
3617 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3620 sub _condense_section {
3621 my ( $self, $format ) = ( shift, shift );
3623 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3624 qw( description_generator
3627 total_line_generator
3632 sub _condensed_generator_defaults {
3633 my ( $self, $format ) = ( shift, shift );
3634 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3643 sub _condensed_header_generator {
3644 my ( $self, $format ) = ( shift, shift );
3646 my ( $f, $prefix, $suffix, $separator, $column ) =
3647 _condensed_generator_defaults($format);
3649 if ($format eq 'latex') {
3650 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3651 $suffix = "\\\\\n\\hline";
3654 sub { my ($d,$a,$s,$w) = @_;
3655 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3657 } elsif ( $format eq 'html' ) {
3658 $prefix = '<th></th>';
3662 sub { my ($d,$a,$s,$w) = @_;
3663 return qq!<th align="$html_align{$a}">$d</th>!;
3671 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3673 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3676 $prefix. join($separator, @result). $suffix;
3681 sub _condensed_description_generator {
3682 my ( $self, $format ) = ( shift, shift );
3684 my ( $f, $prefix, $suffix, $separator, $column ) =
3685 _condensed_generator_defaults($format);
3687 my $money_char = '$';
3688 if ($format eq 'latex') {
3689 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3691 $separator = " & \n";
3693 sub { my ($d,$a,$s,$w) = @_;
3694 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3696 $money_char = '\\dollar';
3697 }elsif ( $format eq 'html' ) {
3698 $prefix = '"><td align="center"></td>';
3702 sub { my ($d,$a,$s,$w) = @_;
3703 return qq!<td align="$html_align{$a}">$d</td>!;
3705 #$money_char = $conf->config('money_char') || '$';
3706 $money_char = ''; # this is madness
3714 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3716 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3718 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3719 map { $f->{$_}->[$i] } qw(align span width)
3723 $prefix. join( $separator, @result ). $suffix;
3728 sub _condensed_total_generator {
3729 my ( $self, $format ) = ( shift, shift );
3731 my ( $f, $prefix, $suffix, $separator, $column ) =
3732 _condensed_generator_defaults($format);
3735 if ($format eq 'latex') {
3738 $separator = " & \n";
3740 sub { my ($d,$a,$s,$w) = @_;
3741 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3743 }elsif ( $format eq 'html' ) {
3747 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3749 sub { my ($d,$a,$s,$w) = @_;
3750 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3759 # my $r = &{$f->{fields}->[$i]}(@args);
3760 # $r .= ' Total' unless $i;
3762 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3764 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3765 map { $f->{$_}->[$i] } qw(align span width)
3769 $prefix. join( $separator, @result ). $suffix;
3774 =item total_line_generator FORMAT
3776 Returns a coderef used for generation of invoice total line items for this
3777 usage_class. FORMAT is either html or latex
3781 # should not be used: will have issues with hash element names (description vs
3782 # total_item and amount vs total_amount -- another array of functions?
3784 sub _condensed_total_line_generator {
3785 my ( $self, $format ) = ( shift, shift );
3787 my ( $f, $prefix, $suffix, $separator, $column ) =
3788 _condensed_generator_defaults($format);
3791 if ($format eq 'latex') {
3794 $separator = " & \n";
3796 sub { my ($d,$a,$s,$w) = @_;
3797 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3799 }elsif ( $format eq 'html' ) {
3803 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3805 sub { my ($d,$a,$s,$w) = @_;
3806 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3815 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3817 &{$column}( &{$f->{fields}->[$i]}(@args),
3818 map { $f->{$_}->[$i] } qw(align span width)
3822 $prefix. join( $separator, @result ). $suffix;
3827 #sub _items_extra_usage_sections {
3829 # my $escape = shift;
3831 # my %sections = ();
3833 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
3834 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3836 # next unless $cust_bill_pkg->pkgnum > 0;
3838 # foreach my $section ( keys %usage_class ) {
3840 # my $usage = $cust_bill_pkg->usage($section);
3842 # next unless $usage && $usage > 0;
3844 # $sections{$section} ||= 0;
3845 # $sections{$section} += $usage;
3851 # map { { 'description' => &{$escape}($_),
3852 # 'subtotal' => $sections{$_},
3853 # 'summarized' => '',
3854 # 'tax_section' => '',
3857 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3861 sub _items_extra_usage_sections {
3870 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3871 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3872 next unless $cust_bill_pkg->pkgnum > 0;
3874 foreach my $classnum ( keys %usage_class ) {
3875 my $section = $usage_class{$classnum}->classname;
3876 $classnums{$section} = $classnum;
3878 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3879 my $amount = $detail->amount;
3880 next unless $amount && $amount > 0;
3882 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
3883 $sections{$section}{amount} += $amount; #subtotal
3884 $sections{$section}{calls}++;
3885 $sections{$section}{duration} += $detail->duration;
3887 my $desc = $detail->regionname;
3888 my $description = $desc;
3889 $description = substr($desc, 0, 50). '...'
3890 if $format eq 'latex' && length($desc) > 50;
3892 $lines{$section}{$desc} ||= {
3893 description => &{$escape}($description),
3894 #pkgpart => $part_pkg->pkgpart,
3895 pkgnum => $cust_bill_pkg->pkgnum,
3900 #unit_amount => $cust_bill_pkg->unitrecur,
3901 quantity => $cust_bill_pkg->quantity,
3902 product_code => 'N/A',
3903 ext_description => [],
3906 $lines{$section}{$desc}{amount} += $amount;
3907 $lines{$section}{$desc}{calls}++;
3908 $lines{$section}{$desc}{duration} += $detail->duration;
3914 my %sectionmap = ();
3915 foreach (keys %sections) {
3916 my $usage_class = $usage_class{$classnums{$_}};
3917 $sectionmap{$_} = { 'description' => &{$escape}($_),
3918 'amount' => $sections{$_}{amount}, #subtotal
3919 'calls' => $sections{$_}{calls},
3920 'duration' => $sections{$_}{duration},
3922 'tax_section' => '',
3923 'sort_weight' => $usage_class->weight,
3924 ( $usage_class->format
3925 ? ( map { $_ => $usage_class->$_($format) }
3926 qw( description_generator header_generator total_generator total_line_generator )
3933 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
3937 foreach my $section ( keys %lines ) {
3938 foreach my $line ( keys %{$lines{$section}} ) {
3939 my $l = $lines{$section}{$line};
3940 $l->{section} = $sectionmap{$section};
3941 $l->{amount} = sprintf( "%.2f", $l->{amount} );
3942 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3947 return(\@sections, \@lines);
3953 my $end = $self->_date;
3954 my $start = $end - 2592000; # 30 days
3955 my $cust_main = $self->cust_main;
3956 my @pkgs = $cust_main->all_pkgs;
3957 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
3960 foreach my $pkg ( @pkgs ) {
3961 my @h_cust_svc = $pkg->h_cust_svc($end);
3962 foreach my $h_cust_svc ( @h_cust_svc ) {
3963 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
3964 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
3966 my $inserted = $h_cust_svc->date_inserted;
3967 my $deleted = $h_cust_svc->date_deleted;
3968 my $phone_inserted = $h_cust_svc->h_svc_x($inserted);
3970 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
3972 # DID either activated or ported in; cannot be both for same DID simultaneously
3973 if ($inserted >= $start && $inserted <= $end && $phone_inserted
3974 && (!$phone_inserted->lnp_status
3975 || $phone_inserted->lnp_status eq ''
3976 || $phone_inserted->lnp_status eq 'native')) {
3979 else { # this one not so clean, should probably move to (h_)svc_phone
3980 my $phone_portedin = qsearchs( 'h_svc_phone',
3981 { 'svcnum' => $h_cust_svc->svcnum,
3982 'lnp_status' => 'portedin' },
3983 FS::h_svc_phone->sql_h_searchs($end),
3985 $num_portedin++ if $phone_portedin;
3988 # DID either deactivated or ported out; cannot be both for same DID simultaneously
3989 if($deleted >= $start && $deleted <= $end && $phone_deleted
3990 && (!$phone_deleted->lnp_status
3991 || $phone_deleted->lnp_status ne 'portingout')) {
3994 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
3995 && $phone_deleted->lnp_status
3996 && $phone_deleted->lnp_status eq 'portingout') {
4000 # increment usage minutes
4001 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end);
4002 foreach my $cdr ( @cdrs ) {
4003 $minutes += $cdr->billsec/60;
4006 # don't look at this service again
4007 push @seen, $h_cust_svc->svcnum;
4011 $minutes = sprintf("%d", $minutes);
4012 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4013 . "$num_deactivated Ported-Out: $num_portedout ",
4014 "Total Minutes: $minutes");
4017 sub _items_svc_phone_sections {
4026 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4027 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4029 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4030 next unless $cust_bill_pkg->pkgnum > 0;
4032 my @header = $cust_bill_pkg->details_header;
4033 next unless scalar(@header);
4035 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4037 my $phonenum = $detail->phonenum;
4038 next unless $phonenum;
4040 my $amount = $detail->amount;
4041 next unless $amount && $amount > 0;
4043 $sections{$phonenum} ||= { 'amount' => 0,
4046 'sort_weight' => -1,
4047 'phonenum' => $phonenum,
4049 $sections{$phonenum}{amount} += $amount; #subtotal
4050 $sections{$phonenum}{calls}++;
4051 $sections{$phonenum}{duration} += $detail->duration;
4053 my $desc = $detail->regionname;
4054 my $description = $desc;
4055 $description = substr($desc, 0, 50). '...'
4056 if $format eq 'latex' && length($desc) > 50;
4058 $lines{$phonenum}{$desc} ||= {
4059 description => &{$escape}($description),
4060 #pkgpart => $part_pkg->pkgpart,
4068 product_code => 'N/A',
4069 ext_description => [],
4072 $lines{$phonenum}{$desc}{amount} += $amount;
4073 $lines{$phonenum}{$desc}{calls}++;
4074 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4076 my $line = $usage_class{$detail->classnum}->classname;
4077 $sections{"$phonenum $line"} ||=
4081 'sort_weight' => $usage_class{$detail->classnum}->weight,
4082 'phonenum' => $phonenum,
4083 'header' => [ @header ],
4085 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4086 $sections{"$phonenum $line"}{calls}++;
4087 $sections{"$phonenum $line"}{duration} += $detail->duration;
4089 $lines{"$phonenum $line"}{$desc} ||= {
4090 description => &{$escape}($description),
4091 #pkgpart => $part_pkg->pkgpart,
4099 product_code => 'N/A',
4100 ext_description => [],
4103 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4104 $lines{"$phonenum $line"}{$desc}{calls}++;
4105 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4106 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4107 $detail->formatted('format' => $format);
4112 my %sectionmap = ();
4113 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4114 foreach ( keys %sections ) {
4115 my @header = @{ $sections{$_}{header} || [] };
4117 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4118 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4119 my $usage_class = $summary ? $simple : $usage_simple;
4120 my $ending = $summary ? ' usage charges' : '';
4123 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4125 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4126 'amount' => $sections{$_}{amount}, #subtotal
4127 'calls' => $sections{$_}{calls},
4128 'duration' => $sections{$_}{duration},
4130 'tax_section' => '',
4131 'phonenum' => $sections{$_}{phonenum},
4132 'sort_weight' => $sections{$_}{sort_weight},
4133 'post_total' => $summary, #inspire pagebreak
4135 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4136 qw( description_generator
4139 total_line_generator
4146 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4147 $a->{sort_weight} <=> $b->{sort_weight}
4152 foreach my $section ( keys %lines ) {
4153 foreach my $line ( keys %{$lines{$section}} ) {
4154 my $l = $lines{$section}{$line};
4155 $l->{section} = $sectionmap{$section};
4156 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4157 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4162 if($conf->exists('phone_usage_class_summary')) {
4163 # this only works with Latex
4167 # after this, we'll have only two sections per DID:
4168 # Calls Summary and Calls Detail
4169 foreach my $section ( @sections ) {
4170 if($section->{'post_total'}) {
4171 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4172 $section->{'total_line_generator'} = sub { '' };
4173 $section->{'total_generator'} = sub { '' };
4174 $section->{'header_generator'} = sub { '' };
4175 $section->{'description_generator'} = '';
4176 push @newsections, $section;
4177 my %calls_detail = %$section;
4178 $calls_detail{'post_total'} = '';
4179 $calls_detail{'sort_weight'} = '';
4180 $calls_detail{'description_generator'} = sub { '' };
4181 $calls_detail{'header_generator'} = sub {
4182 return ' & Date/Time & Called Number & Duration & Price'
4183 if $format eq 'latex';
4186 $calls_detail{'description'} = 'Calls Detail: '
4187 . $section->{'phonenum'};
4188 push @newsections, \%calls_detail;
4192 # after this, each usage class is collapsed/summarized into a single
4193 # line under the Calls Summary section
4194 foreach my $newsection ( @newsections ) {
4195 if($newsection->{'post_total'}) { # this means Calls Summary
4196 foreach my $section ( @sections ) {
4197 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4198 && !$section->{'post_total'});
4199 my $newdesc = $section->{'description'};
4200 my $tn = $section->{'phonenum'};
4201 $newdesc =~ s/$tn//g;
4202 my $line = { ext_description => [],
4206 calls => $section->{'calls'},
4207 section => $newsection,
4208 duration => $section->{'duration'},
4209 description => $newdesc,
4210 amount => sprintf("%.2f",$section->{'amount'}),
4211 product_code => 'N/A',
4213 push @newlines, $line;
4218 # after this, Calls Details is populated with all CDRs
4219 foreach my $newsection ( @newsections ) {
4220 if(!$newsection->{'post_total'}) { # this means Calls Details
4221 foreach my $line ( @lines ) {
4222 next unless (scalar(@{$line->{'ext_description'}}) &&
4223 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4225 my @extdesc = @{$line->{'ext_description'}};
4227 foreach my $extdesc ( @extdesc ) {
4228 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4229 push @newextdesc, $extdesc;
4231 $line->{'ext_description'} = \@newextdesc;
4232 $line->{'section'} = $newsection;
4233 push @newlines, $line;
4238 return(\@newsections, \@newlines);
4241 return(\@sections, \@lines);
4248 #my @display = scalar(@_)
4250 # : qw( _items_previous _items_pkg );
4251 # #: qw( _items_pkg );
4252 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4253 my @display = qw( _items_previous _items_pkg );
4256 foreach my $display ( @display ) {
4257 push @b, $self->$display(@_);
4262 sub _items_previous {
4264 my $cust_main = $self->cust_main;
4265 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4267 foreach ( @pr_cust_bill ) {
4268 my $date = $conf->exists('invoice_show_prior_due_date')
4269 ? 'due '. $_->due_date2str($date_format)
4270 : time2str($date_format, $_->_date);
4272 'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
4273 #'pkgpart' => 'N/A',
4275 'amount' => sprintf("%.2f", $_->owed),
4281 # 'description' => 'Previous Balance',
4282 # #'pkgpart' => 'N/A',
4283 # 'pkgnum' => 'N/A',
4284 # 'amount' => sprintf("%10.2f", $pr_total ),
4285 # 'ext_description' => [ map {
4286 # "Invoice ". $_->invnum.
4287 # " (". time2str("%x",$_->_date). ") ".
4288 # sprintf("%10.2f", $_->owed)
4289 # } @pr_cust_bill ],
4298 warn "$me _items_pkg searching for all package line items\n"
4301 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4303 warn "$me _items_pkg filtering line items\n"
4305 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4307 if ($options{section} && $options{section}->{condensed}) {
4309 warn "$me _items_pkg condensing section\n"
4313 local $Storable::canonical = 1;
4314 foreach ( @items ) {
4316 delete $item->{ref};
4317 delete $item->{ext_description};
4318 my $key = freeze($item);
4319 $itemshash{$key} ||= 0;
4320 $itemshash{$key} ++; # += $item->{quantity};
4322 @items = sort { $a->{description} cmp $b->{description} }
4323 map { my $i = thaw($_);
4324 $i->{quantity} = $itemshash{$_};
4326 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4332 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4339 return 0 unless $a->itemdesc cmp $b->itemdesc;
4340 return -1 if $b->itemdesc eq 'Tax';
4341 return 1 if $a->itemdesc eq 'Tax';
4342 return -1 if $b->itemdesc eq 'Other surcharges';
4343 return 1 if $a->itemdesc eq 'Other surcharges';
4344 $a->itemdesc cmp $b->itemdesc;
4349 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4350 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4353 sub _items_cust_bill_pkg {
4355 my $cust_bill_pkgs = shift;
4358 my $format = $opt{format} || '';
4359 my $escape_function = $opt{escape_function} || sub { shift };
4360 my $format_function = $opt{format_function} || '';
4361 my $unsquelched = $opt{unsquelched} || '';
4362 my $section = $opt{section}->{description} if $opt{section};
4363 my $summary_page = $opt{summary_page} || '';
4364 my $multilocation = $opt{multilocation} || '';
4365 my $multisection = $opt{multisection} || '';
4366 my $discount_show_always = 0;
4369 my ($s, $r, $u) = ( undef, undef, undef );
4370 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4373 warn "$me _items_cust_bill_pkg considering cust_bill_pkg $cust_bill_pkg\n"
4376 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
4377 && $conf->exists('discount-show-always'));
4379 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4380 if ( $_ && !$cust_bill_pkg->hidden ) {
4381 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4382 $_->{amount} =~ s/^\-0\.00$/0.00/;
4383 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4385 unless ( $_->{amount} == 0 && !$discount_show_always );
4390 foreach my $display ( grep { defined($section)
4391 ? $_->section eq $section
4394 #grep { !$_->summary || !$summary_page } # bunk!
4395 grep { !$_->summary || $multisection }
4396 $cust_bill_pkg->cust_bill_pkg_display
4400 warn "$me _items_cust_bill_pkg considering display item $display\n"
4403 my $type = $display->type;
4405 my $desc = $cust_bill_pkg->desc;
4406 $desc = substr($desc, 0, 50). '...'
4407 if $format eq 'latex' && length($desc) > 50;
4409 my %details_opt = ( 'format' => $format,
4410 'escape_function' => $escape_function,
4411 'format_function' => $format_function,
4414 if ( $cust_bill_pkg->pkgnum > 0 ) {
4416 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4419 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4421 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
4423 warn "$me _items_cust_bill_pkg adding setup\n"
4426 my $description = $desc;
4427 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
4430 unless ( $cust_pkg->part_pkg->hide_svc_detail
4431 || $cust_bill_pkg->hidden )
4434 push @d, map &{$escape_function}($_),
4435 $cust_pkg->h_labels_short($self->_date, undef, 'I')
4436 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4438 if ( $multilocation ) {
4439 my $loc = $cust_pkg->location_label;
4440 $loc = substr($loc, 0, 50). '...'
4441 if $format eq 'latex' && length($loc) > 50;
4442 push @d, &{$escape_function}($loc);
4447 push @d, $cust_bill_pkg->details(%details_opt)
4448 if $cust_bill_pkg->recur == 0;
4450 if ( $cust_bill_pkg->hidden ) {
4451 $s->{amount} += $cust_bill_pkg->setup;
4452 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4453 push @{ $s->{ext_description} }, @d;
4456 description => $description,
4457 #pkgpart => $part_pkg->pkgpart,
4458 pkgnum => $cust_bill_pkg->pkgnum,
4459 amount => $cust_bill_pkg->setup,
4460 unit_amount => $cust_bill_pkg->unitsetup,
4461 quantity => $cust_bill_pkg->quantity,
4462 ext_description => \@d,
4468 if ( ( $cust_bill_pkg->recur != 0 || $cust_bill_pkg->setup == 0 ||
4469 ($discount_show_always && $cust_bill_pkg->recur == 0) ) &&
4470 ( !$type || $type eq 'R' || $type eq 'U' )
4474 warn "$me _items_cust_bill_pkg adding recur/usage\n"
4477 my $is_summary = $display->summary;
4478 my $description = ($is_summary && $type && $type eq 'U')
4479 ? "Usage charges" : $desc;
4481 $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4482 " - ". time2str($date_format, $cust_bill_pkg->edate).
4484 unless $conf->exists('disable_line_item_date_ranges');
4488 #at least until cust_bill_pkg has "past" ranges in addition to
4489 #the "future" sdate/edate ones... see #3032
4490 my @dates = ( $self->_date );
4491 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4492 push @dates, $prev->sdate if $prev;
4493 push @dates, undef if !$prev;
4495 unless ( $cust_pkg->part_pkg->hide_svc_detail
4496 || $cust_bill_pkg->itemdesc
4497 || $cust_bill_pkg->hidden
4498 || $is_summary && $type && $type eq 'U' )
4501 warn "$me _items_cust_bill_pkg adding service details\n"
4504 push @d, map &{$escape_function}($_),
4505 $cust_pkg->h_labels_short(@dates, 'I')
4506 #$cust_bill_pkg->edate,
4507 #$cust_bill_pkg->sdate)
4508 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4510 warn "$me _items_cust_bill_pkg done adding service details\n"
4513 if ( $multilocation ) {
4514 my $loc = $cust_pkg->location_label;
4515 $loc = substr($loc, 0, 50). '...'
4516 if $format eq 'latex' && length($loc) > 50;
4517 push @d, &{$escape_function}($loc);
4522 warn "$me _items_cust_bill_pkg adding details\n"
4525 push @d, $cust_bill_pkg->details(%details_opt)
4526 unless ($is_summary || $type && $type eq 'R');
4528 warn "$me _items_cust_bill_pkg calculating amount\n"
4533 $amount = $cust_bill_pkg->recur;
4534 }elsif($type eq 'R') {
4535 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4536 }elsif($type eq 'U') {
4537 $amount = $cust_bill_pkg->usage;
4540 if ( !$type || $type eq 'R' ) {
4542 warn "$me _items_cust_bill_pkg adding recur\n"
4545 if ( $cust_bill_pkg->hidden ) {
4546 $r->{amount} += $amount;
4547 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4548 push @{ $r->{ext_description} }, @d;
4551 description => $description,
4552 #pkgpart => $part_pkg->pkgpart,
4553 pkgnum => $cust_bill_pkg->pkgnum,
4555 unit_amount => $cust_bill_pkg->unitrecur,
4556 quantity => $cust_bill_pkg->quantity,
4557 ext_description => \@d,
4561 } else { # $type eq 'U'
4563 warn "$me _items_cust_bill_pkg adding usage\n"
4566 if ( $cust_bill_pkg->hidden ) {
4567 $u->{amount} += $amount;
4568 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4569 push @{ $u->{ext_description} }, @d;
4572 description => $description,
4573 #pkgpart => $part_pkg->pkgpart,
4574 pkgnum => $cust_bill_pkg->pkgnum,
4576 unit_amount => $cust_bill_pkg->unitrecur,
4577 quantity => $cust_bill_pkg->quantity,
4578 ext_description => \@d,
4584 } # recurring or usage with recurring charge
4586 } else { #pkgnum tax or one-shot line item (??)
4588 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
4591 if ( $cust_bill_pkg->setup != 0 ) {
4593 'description' => $desc,
4594 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
4597 if ( $cust_bill_pkg->recur != 0 ) {
4599 'description' => "$desc (".
4600 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
4601 time2str($date_format, $cust_bill_pkg->edate). ')',
4602 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
4612 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
4615 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4617 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4618 $_->{amount} =~ s/^\-0\.00$/0.00/;
4619 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4621 unless ( $_->{amount} == 0 && !$discount_show_always );
4629 sub _items_credits {
4630 my( $self, %opt ) = @_;
4631 my $trim_len = $opt{'trim_len'} || 60;
4635 foreach ( $self->cust_credited ) {
4637 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4639 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4640 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4641 $reason = " ($reason) " if $reason;
4644 #'description' => 'Credit ref\#'. $_->crednum.
4645 # " (". time2str("%x",$_->cust_credit->_date) .")".
4647 'description' => 'Credit applied '.
4648 time2str($date_format,$_->cust_credit->_date). $reason,
4649 'amount' => sprintf("%.2f",$_->amount),
4657 sub _items_payments {
4661 #get & print payments
4662 foreach ( $self->cust_bill_pay ) {
4664 #something more elaborate if $_->amount ne ->cust_pay->paid ?
4667 'description' => "Payment received ".
4668 time2str($date_format,$_->cust_pay->_date ),
4669 'amount' => sprintf("%.2f", $_->amount )
4677 =item call_details [ OPTION => VALUE ... ]
4679 Returns an array of CSV strings representing the call details for this invoice
4680 The only option available is the boolean prepend_billed_number
4685 my ($self, %opt) = @_;
4687 my $format_function = sub { shift };
4689 if ($opt{prepend_billed_number}) {
4690 $format_function = sub {
4694 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4699 my @details = map { $_->details( 'format_function' => $format_function,
4700 'escape_function' => sub{ return() },
4704 $self->cust_bill_pkg;
4705 my $header = $details[0];
4706 ( $header, grep { $_ ne $header } @details );
4716 =item process_reprint
4720 sub process_reprint {
4721 process_re_X('print', @_);
4724 =item process_reemail
4728 sub process_reemail {
4729 process_re_X('email', @_);
4737 process_re_X('fax', @_);
4745 process_re_X('ftp', @_);
4752 sub process_respool {
4753 process_re_X('spool', @_);
4756 use Storable qw(thaw);
4760 my( $method, $job ) = ( shift, shift );
4761 warn "$me process_re_X $method for job $job\n" if $DEBUG;
4763 my $param = thaw(decode_base64(shift));
4764 warn Dumper($param) if $DEBUG;
4775 my($method, $job, %param ) = @_;
4777 warn "re_X $method for job $job with param:\n".
4778 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
4781 #some false laziness w/search/cust_bill.html
4783 my $orderby = 'ORDER BY cust_bill._date';
4785 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
4787 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4789 my @cust_bill = qsearch( {
4790 #'select' => "cust_bill.*",
4791 'table' => 'cust_bill',
4792 'addl_from' => $addl_from,
4794 'extra_sql' => $extra_sql,
4795 'order_by' => $orderby,
4799 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4801 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
4804 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
4805 foreach my $cust_bill ( @cust_bill ) {
4806 $cust_bill->$method();
4808 if ( $job ) { #progressbar foo
4810 if ( time - $min_sec > $last ) {
4811 my $error = $job->update_statustext(
4812 int( 100 * $num / scalar(@cust_bill) )
4814 die $error if $error;
4825 =head1 CLASS METHODS
4831 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
4836 my ($class, $start, $end) = @_;
4838 $class->paid_sql($start, $end). ' - '.
4839 $class->credited_sql($start, $end);
4844 Returns an SQL fragment to retreive the net amount (charged minus credited).
4849 my ($class, $start, $end) = @_;
4850 'charged - '. $class->credited_sql($start, $end);
4855 Returns an SQL fragment to retreive the amount paid against this invoice.
4860 my ($class, $start, $end) = @_;
4861 $start &&= "AND cust_bill_pay._date <= $start";
4862 $end &&= "AND cust_bill_pay._date > $end";
4863 $start = '' unless defined($start);
4864 $end = '' unless defined($end);
4865 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
4866 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
4871 Returns an SQL fragment to retreive the amount credited against this invoice.
4876 my ($class, $start, $end) = @_;
4877 $start &&= "AND cust_credit_bill._date <= $start";
4878 $end &&= "AND cust_credit_bill._date > $end";
4879 $start = '' unless defined($start);
4880 $end = '' unless defined($end);
4881 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
4882 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
4887 Returns an SQL fragment to retrieve the due date of an invoice.
4888 Currently only supported on PostgreSQL.
4896 cust_bill.invoice_terms,
4897 cust_main.invoice_terms,
4898 \''.($conf->config('invoice_default_terms') || '').'\'
4899 ), E\'Net (\\\\d+)\'
4901 ) * 86400 + cust_bill._date'
4904 =item search_sql_where HASHREF
4906 Class method which returns an SQL WHERE fragment to search for parameters
4907 specified in HASHREF. Valid parameters are
4913 List reference of start date, end date, as UNIX timestamps.
4923 List reference of charged limits (exclusive).
4927 List reference of charged limits (exclusive).
4931 flag, return open invoices only
4935 flag, return net invoices only
4939 =item newest_percust
4943 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
4947 sub search_sql_where {
4948 my($class, $param) = @_;
4950 warn "$me search_sql_where called with params: \n".
4951 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
4957 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
4958 push @search, "cust_main.agentnum = $1";
4962 if ( $param->{_date} ) {
4963 my($beginning, $ending) = @{$param->{_date}};
4965 push @search, "cust_bill._date >= $beginning",
4966 "cust_bill._date < $ending";
4970 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
4971 push @search, "cust_bill.invnum >= $1";
4973 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
4974 push @search, "cust_bill.invnum <= $1";
4978 if ( $param->{charged} ) {
4979 my @charged = ref($param->{charged})
4980 ? @{ $param->{charged} }
4981 : ($param->{charged});
4983 push @search, map { s/^charged/cust_bill.charged/; $_; }
4987 my $owed_sql = FS::cust_bill->owed_sql;
4990 if ( $param->{owed} ) {
4991 my @owed = ref($param->{owed})
4992 ? @{ $param->{owed} }
4994 push @search, map { s/^owed/$owed_sql/; $_; }
4999 push @search, "0 != $owed_sql"
5000 if $param->{'open'};
5001 push @search, '0 != '. FS::cust_bill->net_sql
5005 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5006 if $param->{'days'};
5009 if ( $param->{'newest_percust'} ) {
5011 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5012 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5014 my @newest_where = map { my $x = $_;
5015 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5018 grep ! /^cust_main./, @search;
5019 my $newest_where = scalar(@newest_where)
5020 ? ' AND '. join(' AND ', @newest_where)
5024 push @search, "cust_bill._date = (
5025 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5026 WHERE newest_cust_bill.custnum = cust_bill.custnum
5032 #agent virtualization
5033 my $curuser = $FS::CurrentUser::CurrentUser;
5034 if ( $curuser->username eq 'fs_queue'
5035 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5037 my $newuser = qsearchs('access_user', {
5038 'username' => $username,
5042 $curuser = $newuser;
5044 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5047 push @search, $curuser->agentnums_sql;
5049 join(' AND ', @search );
5061 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5062 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base