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 my %newline_tokens = ( 'latex' => '\\\\',
2409 my $newline_token = $newline_tokens{$format};
2411 warn "$me generating template variables\n"
2414 # generate template variables
2417 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2421 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2427 $returnaddress = join("\n",
2428 $conf->config_orbase("invoice_${format}returnaddress", $template)
2431 } elsif ( grep /\S/,
2432 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2434 my $convert_map = $convert_maps{$format}{'returnaddress'};
2437 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2442 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2444 my $convert_map = $convert_maps{$format}{'returnaddress'};
2445 $returnaddress = join( "\n", &$convert_map(
2446 map { s/( {2,})/'~' x length($1)/eg;
2450 ( $conf->config('company_name', $self->cust_main->agentnum),
2451 $conf->config('company_address', $self->cust_main->agentnum),
2458 my $warning = "Couldn't find a return address; ".
2459 "do you need to set the company_address configuration value?";
2461 $returnaddress = $nbsp;
2462 #$returnaddress = $warning;
2466 warn "$me generating invoice data\n"
2469 my $agentnum = $self->cust_main->agentnum;
2471 my %invoice_data = (
2474 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2475 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2476 'returnaddress' => $returnaddress,
2477 'agent' => &$escape_function($cust_main->agent->agent),
2480 'invnum' => $self->invnum,
2481 'date' => time2str($date_format, $self->_date),
2482 'today' => time2str($date_format_long, $today),
2483 'terms' => $self->terms,
2484 'template' => $template, #params{'template'},
2485 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2486 'current_charges' => sprintf("%.2f", $self->charged),
2487 'duedate' => $self->due_date2str($rdate_format), #date_format?
2490 'custnum' => $cust_main->display_custnum,
2491 'agent_custid' => &$escape_function($cust_main->agent_custid),
2492 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2493 payname company address1 address2 city state zip fax
2497 'ship_enable' => $conf->exists('invoice-ship_address'),
2498 'unitprices' => $conf->exists('invoice-unitprice'),
2499 'smallernotes' => $conf->exists('invoice-smallernotes'),
2500 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2501 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2503 #layout info -- would be fancy to calc some of this and bury the template
2505 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2506 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2507 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2508 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2509 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2510 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2511 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2512 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2513 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2514 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2516 # better hang on to conf_dir for a while (for old templates)
2517 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2519 #these are only used when doing paged plaintext
2525 my $min_sdate = 999999999999;
2527 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2528 next unless $cust_bill_pkg->pkgnum > 0;
2529 $min_sdate = $cust_bill_pkg->sdate
2530 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2531 $max_edate = $cust_bill_pkg->edate
2532 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2535 $invoice_data{'bill_period'} = '';
2536 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
2537 . " to " . time2str('%e %h', $max_edate)
2538 if ($max_edate != 0 && $min_sdate != 999999999999);
2540 $invoice_data{finance_section} = '';
2541 if ( $conf->config('finance_pkgclass') ) {
2543 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2544 $invoice_data{finance_section} = $pkg_class->categoryname;
2546 $invoice_data{finance_amount} = '0.00';
2547 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2549 my $countrydefault = $conf->config('countrydefault') || 'US';
2550 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2551 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2552 my $method = $prefix.$_;
2553 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2555 $invoice_data{'ship_country'} = ''
2556 if ( $invoice_data{'ship_country'} eq $countrydefault );
2558 $invoice_data{'cid'} = $params{'cid'}
2561 if ( $cust_main->country eq $countrydefault ) {
2562 $invoice_data{'country'} = '';
2564 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2568 $invoice_data{'address'} = \@address;
2570 $cust_main->payname.
2571 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2572 ? " (P.O. #". $cust_main->payinfo. ")"
2576 push @address, $cust_main->company
2577 if $cust_main->company;
2578 push @address, $cust_main->address1;
2579 push @address, $cust_main->address2
2580 if $cust_main->address2;
2582 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2583 push @address, $invoice_data{'country'}
2584 if $invoice_data{'country'};
2586 while (scalar(@address) < 5);
2588 $invoice_data{'logo_file'} = $params{'logo_file'}
2589 if $params{'logo_file'};
2590 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2591 if $params{'barcode_file'};
2592 $invoice_data{'barcode_img'} = $params{'barcode_img'}
2593 if $params{'barcode_img'};
2594 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2595 if $params{'barcode_cid'};
2597 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2598 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2599 #my $balance_due = $self->owed + $pr_total - $cr_total;
2600 my $balance_due = $self->owed + $pr_total;
2601 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2602 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2603 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2604 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2606 my $summarypage = '';
2607 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2610 $invoice_data{'summarypage'} = $summarypage;
2612 warn "$me substituting variables in notes, footer, smallfooter\n"
2615 foreach my $include (qw( notes footer smallfooter coupon )) {
2617 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2620 if ( $conf->exists($inc_file, $agentnum)
2621 && length( $conf->config($inc_file, $agentnum) ) ) {
2623 @inc_src = $conf->config($inc_file, $agentnum);
2627 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2629 my $convert_map = $convert_maps{$format}{$include};
2631 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2632 s/--\@\]/$delimiters{$format}[1]/g;
2635 &$convert_map( $conf->config($inc_file, $agentnum) );
2639 my $inc_tt = new Text::Template (
2641 SOURCE => [ map "$_\n", @inc_src ],
2642 DELIMITERS => $delimiters{$format},
2643 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2645 unless ( $inc_tt->compile() ) {
2646 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2647 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2651 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2653 $invoice_data{$include} =~ s/\n+$//
2654 if ($format eq 'latex');
2657 $invoice_data{'po_line'} =
2658 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2659 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2662 my %money_chars = ( 'latex' => '',
2663 'html' => $conf->config('money_char') || '$',
2666 my $money_char = $money_chars{$format};
2668 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2669 'html' => $conf->config('money_char') || '$',
2672 my $other_money_char = $other_money_chars{$format};
2673 $invoice_data{'dollar'} = $other_money_char;
2675 my @detail_items = ();
2676 my @total_items = ();
2680 $invoice_data{'detail_items'} = \@detail_items;
2681 $invoice_data{'total_items'} = \@total_items;
2682 $invoice_data{'buf'} = \@buf;
2683 $invoice_data{'sections'} = \@sections;
2685 warn "$me generating sections\n"
2688 my $previous_section = { 'description' => 'Previous Charges',
2689 'subtotal' => $other_money_char.
2690 sprintf('%.2f', $pr_total),
2691 'summarized' => $summarypage ? 'Y' : '',
2693 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
2694 join(' / ', map { $cust_main->balance_date_range(@$_) }
2695 $self->_prior_month30s
2697 if $conf->exists('invoice_include_aging');
2700 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2701 'subtotal' => $taxtotal, # adjusted below
2702 'summarized' => $summarypage ? 'Y' : '',
2704 my $tax_weight = _pkg_category($tax_section->{description})
2705 ? _pkg_category($tax_section->{description})->weight
2707 $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2708 $tax_section->{'sort_weight'} = $tax_weight;
2711 my $adjusttotal = 0;
2712 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2713 'subtotal' => 0, # adjusted below
2714 'summarized' => $summarypage ? 'Y' : '',
2716 my $adjust_weight = _pkg_category($adjust_section->{description})
2717 ? _pkg_category($adjust_section->{description})->weight
2719 $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2720 $adjust_section->{'sort_weight'} = $adjust_weight;
2722 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2723 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2724 $invoice_data{'multisection'} = $multisection;
2725 my $late_sections = [];
2726 my $extra_sections = [];
2727 my $extra_lines = ();
2728 if ( $multisection ) {
2729 ($extra_sections, $extra_lines) =
2730 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2731 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2733 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2735 push @detail_items, @$extra_lines if $extra_lines;
2737 $self->_items_sections( $late_sections, # this could stand a refactor
2739 $escape_function_nonbsp,
2743 if ($conf->exists('svc_phone_sections')) {
2744 my ($phone_sections, $phone_lines) =
2745 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2746 push @{$late_sections}, @$phone_sections;
2747 push @detail_items, @$phone_lines;
2750 push @sections, { 'description' => '', 'subtotal' => '' };
2753 unless ( $conf->exists('disable_previous_balance')
2754 || $conf->exists('previous_balance-summary_only')
2758 warn "$me adding previous balances\n"
2761 foreach my $line_item ( $self->_items_previous ) {
2764 ext_description => [],
2766 $detail->{'ref'} = $line_item->{'pkgnum'};
2767 $detail->{'quantity'} = 1;
2768 $detail->{'section'} = $previous_section;
2769 $detail->{'description'} = &$escape_function($line_item->{'description'});
2770 if ( exists $line_item->{'ext_description'} ) {
2771 @{$detail->{'ext_description'}} = map {
2772 &$escape_function($_);
2773 } @{$line_item->{'ext_description'}};
2775 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2776 $line_item->{'amount'};
2777 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2779 push @detail_items, $detail;
2780 push @buf, [ $detail->{'description'},
2781 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2787 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2788 push @buf, ['','-----------'];
2789 push @buf, [ 'Total Previous Balance',
2790 $money_char. sprintf("%10.2f", $pr_total) ];
2794 if ( $conf->exists('svc_phone-did-summary') ) {
2795 warn "$me adding DID summary\n"
2798 my ($didsummary,$minutes) = $self->_did_summary;
2799 my $didsummary_desc = 'DID Activity Summary (Past 30 days)';
2801 { 'description' => $didsummary_desc,
2802 'ext_description' => [ $didsummary, $minutes ],
2807 foreach my $section (@sections, @$late_sections) {
2809 warn "$me adding section \n". Dumper($section)
2812 # begin some normalization
2813 $section->{'subtotal'} = $section->{'amount'}
2815 && !exists($section->{subtotal})
2816 && exists($section->{amount});
2818 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2819 if ( $invoice_data{finance_section} &&
2820 $section->{'description'} eq $invoice_data{finance_section} );
2822 $section->{'subtotal'} = $other_money_char.
2823 sprintf('%.2f', $section->{'subtotal'})
2826 # continue some normalization
2827 $section->{'amount'} = $section->{'subtotal'}
2831 if ( $section->{'description'} ) {
2832 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2837 warn "$me setting options\n"
2840 my $multilocation = scalar($cust_main->cust_location); #too expensive?
2842 $options{'section'} = $section if $multisection;
2843 $options{'format'} = $format;
2844 $options{'escape_function'} = $escape_function;
2845 $options{'format_function'} = sub { () } unless $unsquelched;
2846 $options{'unsquelched'} = $unsquelched;
2847 $options{'summary_page'} = $summarypage;
2848 $options{'skip_usage'} =
2849 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2850 $options{'multilocation'} = $multilocation;
2851 $options{'multisection'} = $multisection;
2853 warn "$me searching for line items\n"
2856 foreach my $line_item ( $self->_items_pkg(%options) ) {
2858 warn "$me adding line item $line_item\n"
2862 ext_description => [],
2864 $detail->{'ref'} = $line_item->{'pkgnum'};
2865 $detail->{'quantity'} = $line_item->{'quantity'};
2866 $detail->{'section'} = $section;
2867 $detail->{'description'} = &$escape_function($line_item->{'description'});
2868 if ( exists $line_item->{'ext_description'} ) {
2869 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2871 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2872 $line_item->{'amount'};
2873 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2874 $line_item->{'unit_amount'};
2875 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2877 push @detail_items, $detail;
2878 push @buf, ( [ $detail->{'description'},
2879 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2881 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2885 if ( $section->{'description'} ) {
2886 push @buf, ( ['','-----------'],
2887 [ $section->{'description'}. ' sub-total',
2888 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2897 $invoice_data{current_less_finance} =
2898 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2900 if ( $multisection && !$conf->exists('disable_previous_balance')
2901 || $conf->exists('previous_balance-summary_only') )
2903 unshift @sections, $previous_section if $pr_total;
2906 warn "$me adding taxes\n"
2909 foreach my $tax ( $self->_items_tax ) {
2911 $taxtotal += $tax->{'amount'};
2913 my $description = &$escape_function( $tax->{'description'} );
2914 my $amount = sprintf( '%.2f', $tax->{'amount'} );
2916 if ( $multisection ) {
2918 my $money = $old_latex ? '' : $money_char;
2919 push @detail_items, {
2920 ext_description => [],
2923 description => $description,
2924 amount => $money. $amount,
2926 section => $tax_section,
2931 push @total_items, {
2932 'total_item' => $description,
2933 'total_amount' => $other_money_char. $amount,
2938 push @buf,[ $description,
2939 $money_char. $amount,
2946 $total->{'total_item'} = 'Sub-total';
2947 $total->{'total_amount'} =
2948 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2950 if ( $multisection ) {
2951 $tax_section->{'subtotal'} = $other_money_char.
2952 sprintf('%.2f', $taxtotal);
2953 $tax_section->{'pretotal'} = 'New charges sub-total '.
2954 $total->{'total_amount'};
2955 push @sections, $tax_section if $taxtotal;
2957 unshift @total_items, $total;
2960 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2962 push @buf,['','-----------'];
2963 push @buf,[( $conf->exists('disable_previous_balance')
2965 : 'Total New Charges'
2967 $money_char. sprintf("%10.2f",$self->charged) ];
2973 $item = $conf->config('previous_balance-exclude_from_total')
2974 || 'Total New Charges'
2975 if $conf->exists('previous_balance-exclude_from_total');
2976 my $amount = $self->charged +
2977 ( $conf->exists('disable_previous_balance') ||
2978 $conf->exists('previous_balance-exclude_from_total')
2982 $total->{'total_item'} = &$embolden_function($item);
2983 $total->{'total_amount'} =
2984 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
2985 if ( $multisection ) {
2986 if ( $adjust_section->{'sort_weight'} ) {
2987 $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
2988 sprintf("%.2f", ($self->billing_balance || 0) );
2990 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2991 sprintf('%.2f', $self->charged );
2994 push @total_items, $total;
2996 push @buf,['','-----------'];
2999 sprintf( '%10.2f', $amount )
3004 unless ( $conf->exists('disable_previous_balance') ) {
3005 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3008 my $credittotal = 0;
3009 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3012 $total->{'total_item'} = &$escape_function($credit->{'description'});
3013 $credittotal += $credit->{'amount'};
3014 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3015 $adjusttotal += $credit->{'amount'};
3016 if ( $multisection ) {
3017 my $money = $old_latex ? '' : $money_char;
3018 push @detail_items, {
3019 ext_description => [],
3022 description => &$escape_function($credit->{'description'}),
3023 amount => $money. $credit->{'amount'},
3025 section => $adjust_section,
3028 push @total_items, $total;
3032 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3035 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3036 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3040 my $paymenttotal = 0;
3041 foreach my $payment ( $self->_items_payments ) {
3043 $total->{'total_item'} = &$escape_function($payment->{'description'});
3044 $paymenttotal += $payment->{'amount'};
3045 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3046 $adjusttotal += $payment->{'amount'};
3047 if ( $multisection ) {
3048 my $money = $old_latex ? '' : $money_char;
3049 push @detail_items, {
3050 ext_description => [],
3053 description => &$escape_function($payment->{'description'}),
3054 amount => $money. $payment->{'amount'},
3056 section => $adjust_section,
3059 push @total_items, $total;
3061 push @buf, [ $payment->{'description'},
3062 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3065 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3067 if ( $multisection ) {
3068 $adjust_section->{'subtotal'} = $other_money_char.
3069 sprintf('%.2f', $adjusttotal);
3070 push @sections, $adjust_section
3071 unless $adjust_section->{sort_weight};
3076 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3077 $total->{'total_amount'} =
3078 &$embolden_function(
3079 $other_money_char. sprintf('%.2f', $summarypage
3081 $self->billing_balance
3082 : $self->owed + $pr_total
3085 if ( $multisection && !$adjust_section->{sort_weight} ) {
3086 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3087 $total->{'total_amount'};
3089 push @total_items, $total;
3091 push @buf,['','-----------'];
3092 push @buf,[$self->balance_due_msg, $money_char.
3093 sprintf("%10.2f", $balance_due ) ];
3096 if ( $conf->exists('previous_balance-show_credit')
3097 and $cust_main->balance < 0 ) {
3098 my $credit_total = {
3099 'total_item' => &$embolden_function($self->credit_balance_msg),
3100 'total_amount' => &$embolden_function(
3101 $other_money_char. sprintf('%.2f', -$cust_main->balance)
3104 if ( $multisection ) {
3105 $adjust_section->{'posttotal'} .= $newline_token .
3106 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3109 push @total_items, $credit_total;
3111 push @buf,['','-----------'];
3112 push @buf,[$self->credit_balance_msg, $money_char.
3113 sprintf("%10.2f", -$cust_main->balance ) ];
3117 if ( $multisection ) {
3118 if ($conf->exists('svc_phone_sections')) {
3120 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3121 $total->{'total_amount'} =
3122 &$embolden_function(
3123 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3125 my $last_section = pop @sections;
3126 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3127 $total->{'total_amount'};
3128 push @sections, $last_section;
3130 push @sections, @$late_sections
3134 my @includelist = ();
3135 push @includelist, 'summary' if $summarypage;
3136 foreach my $include ( @includelist ) {
3138 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3141 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3143 @inc_src = $conf->config($inc_file, $agentnum);
3147 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3149 my $convert_map = $convert_maps{$format}{$include};
3151 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3152 s/--\@\]/$delimiters{$format}[1]/g;
3155 &$convert_map( $conf->config($inc_file, $agentnum) );
3159 my $inc_tt = new Text::Template (
3161 SOURCE => [ map "$_\n", @inc_src ],
3162 DELIMITERS => $delimiters{$format},
3163 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3165 unless ( $inc_tt->compile() ) {
3166 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3167 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3171 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3173 $invoice_data{$include} =~ s/\n+$//
3174 if ($format eq 'latex');
3179 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3180 /invoice_lines\((\d*)\)/;
3181 $invoice_lines += $1 || scalar(@buf);
3184 die "no invoice_lines() functions in template?"
3185 if ( $format eq 'template' && !$wasfunc );
3187 if ($format eq 'template') {
3189 if ( $invoice_lines ) {
3190 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3191 $invoice_data{'total_pages'}++
3192 if scalar(@buf) % $invoice_lines;
3195 #setup subroutine for the template
3196 sub FS::cust_bill::_template::invoice_lines {
3197 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
3199 scalar(@FS::cust_bill::_template::buf)
3200 ? shift @FS::cust_bill::_template::buf
3209 push @collect, split("\n",
3210 $text_template->fill_in( HASH => \%invoice_data,
3211 PACKAGE => 'FS::cust_bill::_template'
3214 $FS::cust_bill::_template::page++;
3216 map "$_\n", @collect;
3218 warn "filling in template for invoice ". $self->invnum. "\n"
3220 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3223 $text_template->fill_in(HASH => \%invoice_data);
3227 # helper routine for generating date ranges
3228 sub _prior_month30s {
3231 [ 1, 2592000 ], # 0-30 days ago
3232 [ 2592000, 5184000 ], # 30-60 days ago
3233 [ 5184000, 7776000 ], # 60-90 days ago
3234 [ 7776000, 0 ], # 90+ days ago
3237 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3238 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3243 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3245 Returns an postscript invoice, as a scalar.
3247 Options can be passed as a hashref (recommended) or as a list of time, template
3248 and then any key/value pairs for any other options.
3250 I<time> an optional value used to control the printing of overdue messages. The
3251 default is now. It isn't the date of the invoice; that's the `_date' field.
3252 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3253 L<Time::Local> and L<Date::Parse> for conversion functions.
3255 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3262 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3263 my $ps = generate_ps($file);
3265 unlink($barcodefile);
3270 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3272 Returns an PDF invoice, as a scalar.
3274 Options can be passed as a hashref (recommended) or as a list of time, template
3275 and then any key/value pairs for any other options.
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)
3291 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3292 my $pdf = generate_pdf($file);
3294 unlink($barcodefile);
3299 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3301 Returns an HTML invoice, as a scalar.
3303 I<time> an optional value used to control the printing of overdue messages. The
3304 default is now. It isn't the date of the invoice; that's the `_date' field.
3305 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3306 L<Time::Local> and L<Date::Parse> for conversion functions.
3308 I<template>, if specified, is the name of a suffix for alternate invoices.
3310 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3312 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3313 when emailing the invoice as part of a multipart/related MIME email.
3321 %params = %{ shift() };
3323 $params{'time'} = shift;
3324 $params{'template'} = shift;
3325 $params{'cid'} = shift;
3328 $params{'format'} = 'html';
3330 $self->print_generic( %params );
3333 # quick subroutine for print_latex
3335 # There are ten characters that LaTeX treats as special characters, which
3336 # means that they do not simply typeset themselves:
3337 # # $ % & ~ _ ^ \ { }
3339 # TeX ignores blanks following an escaped character; if you want a blank (as
3340 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3344 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3345 $value =~ s/([<>])/\$$1\$/g;
3351 encode_entities($value);
3355 sub _html_escape_nbsp {
3356 my $value = _html_escape(shift);
3357 $value =~ s/ +/ /g;
3361 #utility methods for print_*
3363 sub _translate_old_latex_format {
3364 warn "_translate_old_latex_format called\n"
3371 if ( $line =~ /^%%Detail\s*$/ ) {
3373 push @template, q![@--!,
3374 q! foreach my $_tr_line (@detail_items) {!,
3375 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3376 q! $_tr_line->{'description'} .= !,
3377 q! "\\tabularnewline\n~~".!,
3378 q! join( "\\tabularnewline\n~~",!,
3379 q! @{$_tr_line->{'ext_description'}}!,
3383 while ( ( my $line_item_line = shift )
3384 !~ /^%%EndDetail\s*$/ ) {
3385 $line_item_line =~ s/'/\\'/g; # nice LTS
3386 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3387 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3388 push @template, " \$OUT .= '$line_item_line';";
3391 push @template, '}',
3394 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3396 push @template, '[@--',
3397 ' foreach my $_tr_line (@total_items) {';
3399 while ( ( my $total_item_line = shift )
3400 !~ /^%%EndTotalDetails\s*$/ ) {
3401 $total_item_line =~ s/'/\\'/g; # nice LTS
3402 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3403 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3404 push @template, " \$OUT .= '$total_item_line';";
3407 push @template, '}',
3411 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3412 push @template, $line;
3418 warn "$_\n" foreach @template;
3427 #check for an invoice-specific override
3428 return $self->invoice_terms if $self->invoice_terms;
3430 #check for a customer- specific override
3431 my $cust_main = $self->cust_main;
3432 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3434 #use configured default
3435 $conf->config('invoice_default_terms') || '';
3441 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3442 $duedate = $self->_date() + ( $1 * 86400 );
3449 $self->due_date ? time2str(shift, $self->due_date) : '';
3452 sub balance_due_msg {
3454 my $msg = 'Balance Due';
3455 return $msg unless $self->terms;
3456 if ( $self->due_date ) {
3457 $msg .= ' - Please pay by '. $self->due_date2str($date_format);
3458 } elsif ( $self->terms ) {
3459 $msg .= ' - '. $self->terms;
3464 sub balance_due_date {
3467 if ( $conf->exists('invoice_default_terms')
3468 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3469 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3474 sub credit_balance_msg { 'Credit Balance Remaining' }
3476 =item invnum_date_pretty
3478 Returns a string with the invoice number and date, for example:
3479 "Invoice #54 (3/20/2008)"
3483 sub invnum_date_pretty {
3485 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3490 Returns a string with the date, for example: "3/20/2008"
3496 time2str($date_format, $self->_date);
3499 use vars qw(%pkg_category_cache);
3500 sub _items_sections {
3503 my $summarypage = shift;
3505 my $extra_sections = shift;
3509 my %late_subtotal = ();
3512 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3515 my $usage = $cust_bill_pkg->usage;
3517 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3518 next if ( $display->summary && $summarypage );
3520 my $section = $display->section;
3521 my $type = $display->type;
3523 $not_tax{$section} = 1
3524 unless $cust_bill_pkg->pkgnum == 0;
3526 if ( $display->post_total && !$summarypage ) {
3527 if (! $type || $type eq 'S') {
3528 $late_subtotal{$section} += $cust_bill_pkg->setup
3529 if $cust_bill_pkg->setup != 0;
3533 $late_subtotal{$section} += $cust_bill_pkg->recur
3534 if $cust_bill_pkg->recur != 0;
3537 if ($type && $type eq 'R') {
3538 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3539 if $cust_bill_pkg->recur != 0;
3542 if ($type && $type eq 'U') {
3543 $late_subtotal{$section} += $usage
3544 unless scalar(@$extra_sections);
3549 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3551 if (! $type || $type eq 'S') {
3552 $subtotal{$section} += $cust_bill_pkg->setup
3553 if $cust_bill_pkg->setup != 0;
3557 $subtotal{$section} += $cust_bill_pkg->recur
3558 if $cust_bill_pkg->recur != 0;
3561 if ($type && $type eq 'R') {
3562 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3563 if $cust_bill_pkg->recur != 0;
3566 if ($type && $type eq 'U') {
3567 $subtotal{$section} += $usage
3568 unless scalar(@$extra_sections);
3577 %pkg_category_cache = ();
3579 push @$late, map { { 'description' => &{$escape}($_),
3580 'subtotal' => $late_subtotal{$_},
3582 'sort_weight' => ( _pkg_category($_)
3583 ? _pkg_category($_)->weight
3586 ((_pkg_category($_) && _pkg_category($_)->condense)
3587 ? $self->_condense_section($format)
3591 sort _sectionsort keys %late_subtotal;
3594 if ( $summarypage ) {
3595 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3596 map { $_->categoryname } qsearch('pkg_category', {});
3597 push @sections, '' if exists($subtotal{''});
3599 @sections = keys %subtotal;
3602 my @early = map { { 'description' => &{$escape}($_),
3603 'subtotal' => $subtotal{$_},
3604 'summarized' => $not_tax{$_} ? '' : 'Y',
3605 'tax_section' => $not_tax{$_} ? '' : 'Y',
3606 'sort_weight' => ( _pkg_category($_)
3607 ? _pkg_category($_)->weight
3610 ((_pkg_category($_) && _pkg_category($_)->condense)
3611 ? $self->_condense_section($format)
3616 push @early, @$extra_sections if $extra_sections;
3618 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3622 #helper subs for above
3625 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3629 my $categoryname = shift;
3630 $pkg_category_cache{$categoryname} ||=
3631 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3634 my %condensed_format = (
3635 'label' => [ qw( Description Qty Amount ) ],
3637 sub { shift->{description} },
3638 sub { shift->{quantity} },
3639 sub { my($href, %opt) = @_;
3640 ($opt{dollar} || ''). $href->{amount};
3643 'align' => [ qw( l r r ) ],
3644 'span' => [ qw( 5 1 1 ) ], # unitprices?
3645 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3648 sub _condense_section {
3649 my ( $self, $format ) = ( shift, shift );
3651 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3652 qw( description_generator
3655 total_line_generator
3660 sub _condensed_generator_defaults {
3661 my ( $self, $format ) = ( shift, shift );
3662 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3671 sub _condensed_header_generator {
3672 my ( $self, $format ) = ( shift, shift );
3674 my ( $f, $prefix, $suffix, $separator, $column ) =
3675 _condensed_generator_defaults($format);
3677 if ($format eq 'latex') {
3678 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3679 $suffix = "\\\\\n\\hline";
3682 sub { my ($d,$a,$s,$w) = @_;
3683 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3685 } elsif ( $format eq 'html' ) {
3686 $prefix = '<th></th>';
3690 sub { my ($d,$a,$s,$w) = @_;
3691 return qq!<th align="$html_align{$a}">$d</th>!;
3699 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3701 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3704 $prefix. join($separator, @result). $suffix;
3709 sub _condensed_description_generator {
3710 my ( $self, $format ) = ( shift, shift );
3712 my ( $f, $prefix, $suffix, $separator, $column ) =
3713 _condensed_generator_defaults($format);
3715 my $money_char = '$';
3716 if ($format eq 'latex') {
3717 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3719 $separator = " & \n";
3721 sub { my ($d,$a,$s,$w) = @_;
3722 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3724 $money_char = '\\dollar';
3725 }elsif ( $format eq 'html' ) {
3726 $prefix = '"><td align="center"></td>';
3730 sub { my ($d,$a,$s,$w) = @_;
3731 return qq!<td align="$html_align{$a}">$d</td>!;
3733 #$money_char = $conf->config('money_char') || '$';
3734 $money_char = ''; # this is madness
3742 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3744 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3746 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3747 map { $f->{$_}->[$i] } qw(align span width)
3751 $prefix. join( $separator, @result ). $suffix;
3756 sub _condensed_total_generator {
3757 my ( $self, $format ) = ( shift, shift );
3759 my ( $f, $prefix, $suffix, $separator, $column ) =
3760 _condensed_generator_defaults($format);
3763 if ($format eq 'latex') {
3766 $separator = " & \n";
3768 sub { my ($d,$a,$s,$w) = @_;
3769 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3771 }elsif ( $format eq 'html' ) {
3775 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3777 sub { my ($d,$a,$s,$w) = @_;
3778 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3787 # my $r = &{$f->{fields}->[$i]}(@args);
3788 # $r .= ' Total' unless $i;
3790 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3792 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3793 map { $f->{$_}->[$i] } qw(align span width)
3797 $prefix. join( $separator, @result ). $suffix;
3802 =item total_line_generator FORMAT
3804 Returns a coderef used for generation of invoice total line items for this
3805 usage_class. FORMAT is either html or latex
3809 # should not be used: will have issues with hash element names (description vs
3810 # total_item and amount vs total_amount -- another array of functions?
3812 sub _condensed_total_line_generator {
3813 my ( $self, $format ) = ( shift, shift );
3815 my ( $f, $prefix, $suffix, $separator, $column ) =
3816 _condensed_generator_defaults($format);
3819 if ($format eq 'latex') {
3822 $separator = " & \n";
3824 sub { my ($d,$a,$s,$w) = @_;
3825 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3827 }elsif ( $format eq 'html' ) {
3831 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3833 sub { my ($d,$a,$s,$w) = @_;
3834 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3843 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3845 &{$column}( &{$f->{fields}->[$i]}(@args),
3846 map { $f->{$_}->[$i] } qw(align span width)
3850 $prefix. join( $separator, @result ). $suffix;
3855 #sub _items_extra_usage_sections {
3857 # my $escape = shift;
3859 # my %sections = ();
3861 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
3862 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3864 # next unless $cust_bill_pkg->pkgnum > 0;
3866 # foreach my $section ( keys %usage_class ) {
3868 # my $usage = $cust_bill_pkg->usage($section);
3870 # next unless $usage && $usage > 0;
3872 # $sections{$section} ||= 0;
3873 # $sections{$section} += $usage;
3879 # map { { 'description' => &{$escape}($_),
3880 # 'subtotal' => $sections{$_},
3881 # 'summarized' => '',
3882 # 'tax_section' => '',
3885 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3889 sub _items_extra_usage_sections {
3898 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3899 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3900 next unless $cust_bill_pkg->pkgnum > 0;
3902 foreach my $classnum ( keys %usage_class ) {
3903 my $section = $usage_class{$classnum}->classname;
3904 $classnums{$section} = $classnum;
3906 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3907 my $amount = $detail->amount;
3908 next unless $amount && $amount > 0;
3910 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
3911 $sections{$section}{amount} += $amount; #subtotal
3912 $sections{$section}{calls}++;
3913 $sections{$section}{duration} += $detail->duration;
3915 my $desc = $detail->regionname;
3916 my $description = $desc;
3917 $description = substr($desc, 0, 50). '...'
3918 if $format eq 'latex' && length($desc) > 50;
3920 $lines{$section}{$desc} ||= {
3921 description => &{$escape}($description),
3922 #pkgpart => $part_pkg->pkgpart,
3923 pkgnum => $cust_bill_pkg->pkgnum,
3928 #unit_amount => $cust_bill_pkg->unitrecur,
3929 quantity => $cust_bill_pkg->quantity,
3930 product_code => 'N/A',
3931 ext_description => [],
3934 $lines{$section}{$desc}{amount} += $amount;
3935 $lines{$section}{$desc}{calls}++;
3936 $lines{$section}{$desc}{duration} += $detail->duration;
3942 my %sectionmap = ();
3943 foreach (keys %sections) {
3944 my $usage_class = $usage_class{$classnums{$_}};
3945 $sectionmap{$_} = { 'description' => &{$escape}($_),
3946 'amount' => $sections{$_}{amount}, #subtotal
3947 'calls' => $sections{$_}{calls},
3948 'duration' => $sections{$_}{duration},
3950 'tax_section' => '',
3951 'sort_weight' => $usage_class->weight,
3952 ( $usage_class->format
3953 ? ( map { $_ => $usage_class->$_($format) }
3954 qw( description_generator header_generator total_generator total_line_generator )
3961 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
3965 foreach my $section ( keys %lines ) {
3966 foreach my $line ( keys %{$lines{$section}} ) {
3967 my $l = $lines{$section}{$line};
3968 $l->{section} = $sectionmap{$section};
3969 $l->{amount} = sprintf( "%.2f", $l->{amount} );
3970 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3975 return(\@sections, \@lines);
3981 my $end = $self->_date;
3982 my $start = $end - 2592000; # 30 days
3983 my $cust_main = $self->cust_main;
3984 my @pkgs = $cust_main->all_pkgs;
3985 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
3988 foreach my $pkg ( @pkgs ) {
3989 my @h_cust_svc = $pkg->h_cust_svc($end);
3990 foreach my $h_cust_svc ( @h_cust_svc ) {
3991 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
3992 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
3994 my $inserted = $h_cust_svc->date_inserted;
3995 my $deleted = $h_cust_svc->date_deleted;
3996 my $phone_inserted = $h_cust_svc->h_svc_x($inserted);
3998 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4000 # DID either activated or ported in; cannot be both for same DID simultaneously
4001 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4002 && (!$phone_inserted->lnp_status
4003 || $phone_inserted->lnp_status eq ''
4004 || $phone_inserted->lnp_status eq 'native')) {
4007 else { # this one not so clean, should probably move to (h_)svc_phone
4008 my $phone_portedin = qsearchs( 'h_svc_phone',
4009 { 'svcnum' => $h_cust_svc->svcnum,
4010 'lnp_status' => 'portedin' },
4011 FS::h_svc_phone->sql_h_searchs($end),
4013 $num_portedin++ if $phone_portedin;
4016 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4017 if($deleted >= $start && $deleted <= $end && $phone_deleted
4018 && (!$phone_deleted->lnp_status
4019 || $phone_deleted->lnp_status ne 'portingout')) {
4022 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4023 && $phone_deleted->lnp_status
4024 && $phone_deleted->lnp_status eq 'portingout') {
4028 # increment usage minutes
4029 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end);
4030 foreach my $cdr ( @cdrs ) {
4031 $minutes += $cdr->billsec/60;
4034 # don't look at this service again
4035 push @seen, $h_cust_svc->svcnum;
4039 $minutes = sprintf("%d", $minutes);
4040 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4041 . "$num_deactivated Ported-Out: $num_portedout ",
4042 "Total Minutes: $minutes");
4045 sub _items_svc_phone_sections {
4054 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4055 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4057 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4058 next unless $cust_bill_pkg->pkgnum > 0;
4060 my @header = $cust_bill_pkg->details_header;
4061 next unless scalar(@header);
4063 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4065 my $phonenum = $detail->phonenum;
4066 next unless $phonenum;
4068 my $amount = $detail->amount;
4069 next unless $amount && $amount > 0;
4071 $sections{$phonenum} ||= { 'amount' => 0,
4074 'sort_weight' => -1,
4075 'phonenum' => $phonenum,
4077 $sections{$phonenum}{amount} += $amount; #subtotal
4078 $sections{$phonenum}{calls}++;
4079 $sections{$phonenum}{duration} += $detail->duration;
4081 my $desc = $detail->regionname;
4082 my $description = $desc;
4083 $description = substr($desc, 0, 50). '...'
4084 if $format eq 'latex' && length($desc) > 50;
4086 $lines{$phonenum}{$desc} ||= {
4087 description => &{$escape}($description),
4088 #pkgpart => $part_pkg->pkgpart,
4096 product_code => 'N/A',
4097 ext_description => [],
4100 $lines{$phonenum}{$desc}{amount} += $amount;
4101 $lines{$phonenum}{$desc}{calls}++;
4102 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4104 my $line = $usage_class{$detail->classnum}->classname;
4105 $sections{"$phonenum $line"} ||=
4109 'sort_weight' => $usage_class{$detail->classnum}->weight,
4110 'phonenum' => $phonenum,
4111 'header' => [ @header ],
4113 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4114 $sections{"$phonenum $line"}{calls}++;
4115 $sections{"$phonenum $line"}{duration} += $detail->duration;
4117 $lines{"$phonenum $line"}{$desc} ||= {
4118 description => &{$escape}($description),
4119 #pkgpart => $part_pkg->pkgpart,
4127 product_code => 'N/A',
4128 ext_description => [],
4131 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4132 $lines{"$phonenum $line"}{$desc}{calls}++;
4133 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4134 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4135 $detail->formatted('format' => $format);
4140 my %sectionmap = ();
4141 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4142 foreach ( keys %sections ) {
4143 my @header = @{ $sections{$_}{header} || [] };
4145 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4146 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4147 my $usage_class = $summary ? $simple : $usage_simple;
4148 my $ending = $summary ? ' usage charges' : '';
4151 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4153 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4154 'amount' => $sections{$_}{amount}, #subtotal
4155 'calls' => $sections{$_}{calls},
4156 'duration' => $sections{$_}{duration},
4158 'tax_section' => '',
4159 'phonenum' => $sections{$_}{phonenum},
4160 'sort_weight' => $sections{$_}{sort_weight},
4161 'post_total' => $summary, #inspire pagebreak
4163 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4164 qw( description_generator
4167 total_line_generator
4174 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4175 $a->{sort_weight} <=> $b->{sort_weight}
4180 foreach my $section ( keys %lines ) {
4181 foreach my $line ( keys %{$lines{$section}} ) {
4182 my $l = $lines{$section}{$line};
4183 $l->{section} = $sectionmap{$section};
4184 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4185 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4190 if($conf->exists('phone_usage_class_summary')) {
4191 # this only works with Latex
4195 # after this, we'll have only two sections per DID:
4196 # Calls Summary and Calls Detail
4197 foreach my $section ( @sections ) {
4198 if($section->{'post_total'}) {
4199 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4200 $section->{'total_line_generator'} = sub { '' };
4201 $section->{'total_generator'} = sub { '' };
4202 $section->{'header_generator'} = sub { '' };
4203 $section->{'description_generator'} = '';
4204 push @newsections, $section;
4205 my %calls_detail = %$section;
4206 $calls_detail{'post_total'} = '';
4207 $calls_detail{'sort_weight'} = '';
4208 $calls_detail{'description_generator'} = sub { '' };
4209 $calls_detail{'header_generator'} = sub {
4210 return ' & Date/Time & Called Number & Duration & Price'
4211 if $format eq 'latex';
4214 $calls_detail{'description'} = 'Calls Detail: '
4215 . $section->{'phonenum'};
4216 push @newsections, \%calls_detail;
4220 # after this, each usage class is collapsed/summarized into a single
4221 # line under the Calls Summary section
4222 foreach my $newsection ( @newsections ) {
4223 if($newsection->{'post_total'}) { # this means Calls Summary
4224 foreach my $section ( @sections ) {
4225 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4226 && !$section->{'post_total'});
4227 my $newdesc = $section->{'description'};
4228 my $tn = $section->{'phonenum'};
4229 $newdesc =~ s/$tn//g;
4230 my $line = { ext_description => [],
4234 calls => $section->{'calls'},
4235 section => $newsection,
4236 duration => $section->{'duration'},
4237 description => $newdesc,
4238 amount => sprintf("%.2f",$section->{'amount'}),
4239 product_code => 'N/A',
4241 push @newlines, $line;
4246 # after this, Calls Details is populated with all CDRs
4247 foreach my $newsection ( @newsections ) {
4248 if(!$newsection->{'post_total'}) { # this means Calls Details
4249 foreach my $line ( @lines ) {
4250 next unless (scalar(@{$line->{'ext_description'}}) &&
4251 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4253 my @extdesc = @{$line->{'ext_description'}};
4255 foreach my $extdesc ( @extdesc ) {
4256 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4257 push @newextdesc, $extdesc;
4259 $line->{'ext_description'} = \@newextdesc;
4260 $line->{'section'} = $newsection;
4261 push @newlines, $line;
4266 return(\@newsections, \@newlines);
4269 return(\@sections, \@lines);
4276 #my @display = scalar(@_)
4278 # : qw( _items_previous _items_pkg );
4279 # #: qw( _items_pkg );
4280 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4281 my @display = qw( _items_previous _items_pkg );
4284 foreach my $display ( @display ) {
4285 push @b, $self->$display(@_);
4290 sub _items_previous {
4292 my $cust_main = $self->cust_main;
4293 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4295 foreach ( @pr_cust_bill ) {
4296 my $date = $conf->exists('invoice_show_prior_due_date')
4297 ? 'due '. $_->due_date2str($date_format)
4298 : time2str($date_format, $_->_date);
4300 'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
4301 #'pkgpart' => 'N/A',
4303 'amount' => sprintf("%.2f", $_->owed),
4309 # 'description' => 'Previous Balance',
4310 # #'pkgpart' => 'N/A',
4311 # 'pkgnum' => 'N/A',
4312 # 'amount' => sprintf("%10.2f", $pr_total ),
4313 # 'ext_description' => [ map {
4314 # "Invoice ". $_->invnum.
4315 # " (". time2str("%x",$_->_date). ") ".
4316 # sprintf("%10.2f", $_->owed)
4317 # } @pr_cust_bill ],
4326 warn "$me _items_pkg searching for all package line items\n"
4329 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4331 warn "$me _items_pkg filtering line items\n"
4333 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4335 if ($options{section} && $options{section}->{condensed}) {
4337 warn "$me _items_pkg condensing section\n"
4341 local $Storable::canonical = 1;
4342 foreach ( @items ) {
4344 delete $item->{ref};
4345 delete $item->{ext_description};
4346 my $key = freeze($item);
4347 $itemshash{$key} ||= 0;
4348 $itemshash{$key} ++; # += $item->{quantity};
4350 @items = sort { $a->{description} cmp $b->{description} }
4351 map { my $i = thaw($_);
4352 $i->{quantity} = $itemshash{$_};
4354 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4360 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4367 return 0 unless $a->itemdesc cmp $b->itemdesc;
4368 return -1 if $b->itemdesc eq 'Tax';
4369 return 1 if $a->itemdesc eq 'Tax';
4370 return -1 if $b->itemdesc eq 'Other surcharges';
4371 return 1 if $a->itemdesc eq 'Other surcharges';
4372 $a->itemdesc cmp $b->itemdesc;
4377 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4378 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4381 sub _items_cust_bill_pkg {
4383 my $cust_bill_pkgs = shift;
4386 my $format = $opt{format} || '';
4387 my $escape_function = $opt{escape_function} || sub { shift };
4388 my $format_function = $opt{format_function} || '';
4389 my $unsquelched = $opt{unsquelched} || '';
4390 my $section = $opt{section}->{description} if $opt{section};
4391 my $summary_page = $opt{summary_page} || '';
4392 my $multilocation = $opt{multilocation} || '';
4393 my $multisection = $opt{multisection} || '';
4394 my $discount_show_always = 0;
4397 my ($s, $r, $u) = ( undef, undef, undef );
4398 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4401 warn "$me _items_cust_bill_pkg considering cust_bill_pkg $cust_bill_pkg\n"
4404 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
4405 && $conf->exists('discount-show-always'));
4407 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4408 if ( $_ && !$cust_bill_pkg->hidden ) {
4409 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4410 $_->{amount} =~ s/^\-0\.00$/0.00/;
4411 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4413 unless ( $_->{amount} == 0 && !$discount_show_always );
4418 foreach my $display ( grep { defined($section)
4419 ? $_->section eq $section
4422 #grep { !$_->summary || !$summary_page } # bunk!
4423 grep { !$_->summary || $multisection }
4424 $cust_bill_pkg->cust_bill_pkg_display
4428 warn "$me _items_cust_bill_pkg considering display item $display\n"
4431 my $type = $display->type;
4433 my $desc = $cust_bill_pkg->desc;
4434 $desc = substr($desc, 0, 50). '...'
4435 if $format eq 'latex' && length($desc) > 50;
4437 my %details_opt = ( 'format' => $format,
4438 'escape_function' => $escape_function,
4439 'format_function' => $format_function,
4442 if ( $cust_bill_pkg->pkgnum > 0 ) {
4444 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4447 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4449 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
4451 warn "$me _items_cust_bill_pkg adding setup\n"
4454 my $description = $desc;
4455 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
4458 unless ( $cust_pkg->part_pkg->hide_svc_detail
4459 || $cust_bill_pkg->hidden )
4462 push @d, map &{$escape_function}($_),
4463 $cust_pkg->h_labels_short($self->_date, undef, 'I')
4464 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4466 if ( $multilocation ) {
4467 my $loc = $cust_pkg->location_label;
4468 $loc = substr($loc, 0, 50). '...'
4469 if $format eq 'latex' && length($loc) > 50;
4470 push @d, &{$escape_function}($loc);
4475 push @d, $cust_bill_pkg->details(%details_opt)
4476 if $cust_bill_pkg->recur == 0;
4478 if ( $cust_bill_pkg->hidden ) {
4479 $s->{amount} += $cust_bill_pkg->setup;
4480 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4481 push @{ $s->{ext_description} }, @d;
4484 description => $description,
4485 #pkgpart => $part_pkg->pkgpart,
4486 pkgnum => $cust_bill_pkg->pkgnum,
4487 amount => $cust_bill_pkg->setup,
4488 unit_amount => $cust_bill_pkg->unitsetup,
4489 quantity => $cust_bill_pkg->quantity,
4490 ext_description => \@d,
4496 if ( ( $cust_bill_pkg->recur != 0 || $cust_bill_pkg->setup == 0 ||
4497 ($discount_show_always && $cust_bill_pkg->recur == 0) ) &&
4498 ( !$type || $type eq 'R' || $type eq 'U' )
4502 warn "$me _items_cust_bill_pkg adding recur/usage\n"
4505 my $is_summary = $display->summary;
4506 my $description = ($is_summary && $type && $type eq 'U')
4507 ? "Usage charges" : $desc;
4509 $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4510 " - ". time2str($date_format, $cust_bill_pkg->edate).
4512 unless $conf->exists('disable_line_item_date_ranges');
4516 #at least until cust_bill_pkg has "past" ranges in addition to
4517 #the "future" sdate/edate ones... see #3032
4518 my @dates = ( $self->_date );
4519 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4520 push @dates, $prev->sdate if $prev;
4521 push @dates, undef if !$prev;
4523 unless ( $cust_pkg->part_pkg->hide_svc_detail
4524 || $cust_bill_pkg->itemdesc
4525 || $cust_bill_pkg->hidden
4526 || $is_summary && $type && $type eq 'U' )
4529 warn "$me _items_cust_bill_pkg adding service details\n"
4532 push @d, map &{$escape_function}($_),
4533 $cust_pkg->h_labels_short(@dates, 'I')
4534 #$cust_bill_pkg->edate,
4535 #$cust_bill_pkg->sdate)
4536 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4538 warn "$me _items_cust_bill_pkg done adding service details\n"
4541 if ( $multilocation ) {
4542 my $loc = $cust_pkg->location_label;
4543 $loc = substr($loc, 0, 50). '...'
4544 if $format eq 'latex' && length($loc) > 50;
4545 push @d, &{$escape_function}($loc);
4550 unless ( $is_summary ) {
4551 warn "$me _items_cust_bill_pkg adding details\n"
4554 #instead of omitting details entirely in this case (unwanted side
4555 # effects), just omit CDRs
4556 $details_opt{'format_function'} = sub { () }
4557 if $type && $type eq 'R';
4559 push @d, $cust_bill_pkg->details(%details_opt);
4562 warn "$me _items_cust_bill_pkg calculating amount\n"
4567 $amount = $cust_bill_pkg->recur;
4568 } elsif ($type eq 'R') {
4569 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4570 } elsif ($type eq 'U') {
4571 $amount = $cust_bill_pkg->usage;
4574 if ( !$type || $type eq 'R' ) {
4576 warn "$me _items_cust_bill_pkg adding recur\n"
4579 if ( $cust_bill_pkg->hidden ) {
4580 $r->{amount} += $amount;
4581 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4582 push @{ $r->{ext_description} }, @d;
4585 description => $description,
4586 #pkgpart => $part_pkg->pkgpart,
4587 pkgnum => $cust_bill_pkg->pkgnum,
4589 unit_amount => $cust_bill_pkg->unitrecur,
4590 quantity => $cust_bill_pkg->quantity,
4591 ext_description => \@d,
4595 } else { # $type eq 'U'
4597 warn "$me _items_cust_bill_pkg adding usage\n"
4600 if ( $cust_bill_pkg->hidden ) {
4601 $u->{amount} += $amount;
4602 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4603 push @{ $u->{ext_description} }, @d;
4606 description => $description,
4607 #pkgpart => $part_pkg->pkgpart,
4608 pkgnum => $cust_bill_pkg->pkgnum,
4610 unit_amount => $cust_bill_pkg->unitrecur,
4611 quantity => $cust_bill_pkg->quantity,
4612 ext_description => \@d,
4618 } # recurring or usage with recurring charge
4620 } else { #pkgnum tax or one-shot line item (??)
4622 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
4625 if ( $cust_bill_pkg->setup != 0 ) {
4627 'description' => $desc,
4628 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
4631 if ( $cust_bill_pkg->recur != 0 ) {
4633 'description' => "$desc (".
4634 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
4635 time2str($date_format, $cust_bill_pkg->edate). ')',
4636 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
4646 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
4649 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4651 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4652 $_->{amount} =~ s/^\-0\.00$/0.00/;
4653 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4655 unless ( $_->{amount} == 0 && !$discount_show_always );
4663 sub _items_credits {
4664 my( $self, %opt ) = @_;
4665 my $trim_len = $opt{'trim_len'} || 60;
4669 foreach ( $self->cust_credited ) {
4671 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4673 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4674 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4675 $reason = " ($reason) " if $reason;
4678 #'description' => 'Credit ref\#'. $_->crednum.
4679 # " (". time2str("%x",$_->cust_credit->_date) .")".
4681 'description' => 'Credit applied '.
4682 time2str($date_format,$_->cust_credit->_date). $reason,
4683 'amount' => sprintf("%.2f",$_->amount),
4691 sub _items_payments {
4695 #get & print payments
4696 foreach ( $self->cust_bill_pay ) {
4698 #something more elaborate if $_->amount ne ->cust_pay->paid ?
4701 'description' => "Payment received ".
4702 time2str($date_format,$_->cust_pay->_date ),
4703 'amount' => sprintf("%.2f", $_->amount )
4711 =item call_details [ OPTION => VALUE ... ]
4713 Returns an array of CSV strings representing the call details for this invoice
4714 The only option available is the boolean prepend_billed_number
4719 my ($self, %opt) = @_;
4721 my $format_function = sub { shift };
4723 if ($opt{prepend_billed_number}) {
4724 $format_function = sub {
4728 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4733 my @details = map { $_->details( 'format_function' => $format_function,
4734 'escape_function' => sub{ return() },
4738 $self->cust_bill_pkg;
4739 my $header = $details[0];
4740 ( $header, grep { $_ ne $header } @details );
4750 =item process_reprint
4754 sub process_reprint {
4755 process_re_X('print', @_);
4758 =item process_reemail
4762 sub process_reemail {
4763 process_re_X('email', @_);
4771 process_re_X('fax', @_);
4779 process_re_X('ftp', @_);
4786 sub process_respool {
4787 process_re_X('spool', @_);
4790 use Storable qw(thaw);
4794 my( $method, $job ) = ( shift, shift );
4795 warn "$me process_re_X $method for job $job\n" if $DEBUG;
4797 my $param = thaw(decode_base64(shift));
4798 warn Dumper($param) if $DEBUG;
4809 my($method, $job, %param ) = @_;
4811 warn "re_X $method for job $job with param:\n".
4812 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
4815 #some false laziness w/search/cust_bill.html
4817 my $orderby = 'ORDER BY cust_bill._date';
4819 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
4821 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4823 my @cust_bill = qsearch( {
4824 #'select' => "cust_bill.*",
4825 'table' => 'cust_bill',
4826 'addl_from' => $addl_from,
4828 'extra_sql' => $extra_sql,
4829 'order_by' => $orderby,
4833 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4835 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
4838 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
4839 foreach my $cust_bill ( @cust_bill ) {
4840 $cust_bill->$method();
4842 if ( $job ) { #progressbar foo
4844 if ( time - $min_sec > $last ) {
4845 my $error = $job->update_statustext(
4846 int( 100 * $num / scalar(@cust_bill) )
4848 die $error if $error;
4859 =head1 CLASS METHODS
4865 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
4870 my ($class, $start, $end) = @_;
4872 $class->paid_sql($start, $end). ' - '.
4873 $class->credited_sql($start, $end);
4878 Returns an SQL fragment to retreive the net amount (charged minus credited).
4883 my ($class, $start, $end) = @_;
4884 'charged - '. $class->credited_sql($start, $end);
4889 Returns an SQL fragment to retreive the amount paid against this invoice.
4894 my ($class, $start, $end) = @_;
4895 $start &&= "AND cust_bill_pay._date <= $start";
4896 $end &&= "AND cust_bill_pay._date > $end";
4897 $start = '' unless defined($start);
4898 $end = '' unless defined($end);
4899 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
4900 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
4905 Returns an SQL fragment to retreive the amount credited against this invoice.
4910 my ($class, $start, $end) = @_;
4911 $start &&= "AND cust_credit_bill._date <= $start";
4912 $end &&= "AND cust_credit_bill._date > $end";
4913 $start = '' unless defined($start);
4914 $end = '' unless defined($end);
4915 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
4916 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
4921 Returns an SQL fragment to retrieve the due date of an invoice.
4922 Currently only supported on PostgreSQL.
4930 cust_bill.invoice_terms,
4931 cust_main.invoice_terms,
4932 \''.($conf->config('invoice_default_terms') || '').'\'
4933 ), E\'Net (\\\\d+)\'
4935 ) * 86400 + cust_bill._date'
4938 =item search_sql_where HASHREF
4940 Class method which returns an SQL WHERE fragment to search for parameters
4941 specified in HASHREF. Valid parameters are
4947 List reference of start date, end date, as UNIX timestamps.
4957 List reference of charged limits (exclusive).
4961 List reference of charged limits (exclusive).
4965 flag, return open invoices only
4969 flag, return net invoices only
4973 =item newest_percust
4977 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
4981 sub search_sql_where {
4982 my($class, $param) = @_;
4984 warn "$me search_sql_where called with params: \n".
4985 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
4991 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
4992 push @search, "cust_main.agentnum = $1";
4996 if ( $param->{_date} ) {
4997 my($beginning, $ending) = @{$param->{_date}};
4999 push @search, "cust_bill._date >= $beginning",
5000 "cust_bill._date < $ending";
5004 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5005 push @search, "cust_bill.invnum >= $1";
5007 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5008 push @search, "cust_bill.invnum <= $1";
5012 if ( $param->{charged} ) {
5013 my @charged = ref($param->{charged})
5014 ? @{ $param->{charged} }
5015 : ($param->{charged});
5017 push @search, map { s/^charged/cust_bill.charged/; $_; }
5021 my $owed_sql = FS::cust_bill->owed_sql;
5024 if ( $param->{owed} ) {
5025 my @owed = ref($param->{owed})
5026 ? @{ $param->{owed} }
5028 push @search, map { s/^owed/$owed_sql/; $_; }
5033 push @search, "0 != $owed_sql"
5034 if $param->{'open'};
5035 push @search, '0 != '. FS::cust_bill->net_sql
5039 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5040 if $param->{'days'};
5043 if ( $param->{'newest_percust'} ) {
5045 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5046 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5048 my @newest_where = map { my $x = $_;
5049 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5052 grep ! /^cust_main./, @search;
5053 my $newest_where = scalar(@newest_where)
5054 ? ' AND '. join(' AND ', @newest_where)
5058 push @search, "cust_bill._date = (
5059 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5060 WHERE newest_cust_bill.custnum = cust_bill.custnum
5066 #agent virtualization
5067 my $curuser = $FS::CurrentUser::CurrentUser;
5068 if ( $curuser->username eq 'fs_queue'
5069 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5071 my $newuser = qsearchs('access_user', {
5072 'username' => $username,
5076 $curuser = $newuser;
5078 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5081 push @search, $curuser->agentnums_sql;
5083 join(' AND ', @search );
5095 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5096 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base