4 use vars qw( @ISA $DEBUG $me $conf
5 $money_char $date_format $rdate_format $date_format_long );
6 use vars qw( $invoice_lines @buf ); #yuck
7 use Fcntl qw(:flock); #for spool_csv
8 use List::Util qw(min max);
10 use Text::Template 1.20;
12 use String::ShellQuote;
15 use Storable qw( freeze thaw );
17 use FS::UID qw( datasrc );
18 use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
19 use FS::Record qw( qsearch qsearchs dbh );
20 use FS::cust_main_Mixin;
22 use FS::cust_statement;
23 use FS::cust_bill_pkg;
24 use FS::cust_bill_pkg_display;
25 use FS::cust_bill_pkg_detail;
29 use FS::cust_credit_bill;
31 use FS::cust_pay_batch;
32 use FS::cust_bill_event;
35 use FS::cust_bill_pay;
36 use FS::cust_bill_pay_batch;
37 use FS::part_bill_event;
40 use FS::cust_bill_batch;
43 @ISA = qw( FS::cust_main_Mixin FS::Record );
46 $me = '[FS::cust_bill]';
48 #ask FS::UID to run this stuff for us later
49 FS::UID->install_callback( sub {
51 $money_char = $conf->config('money_char') || '$';
52 $date_format = $conf->config('date_format') || '%x'; #/YY
53 $rdate_format = $conf->config('date_format') || '%m/%d/%Y'; #/YYYY
54 $date_format_long = $conf->config('date_format_long') || '%b %o, %Y';
59 FS::cust_bill - Object methods for cust_bill records
65 $record = new FS::cust_bill \%hash;
66 $record = new FS::cust_bill { 'column' => 'value' };
68 $error = $record->insert;
70 $error = $new_record->replace($old_record);
72 $error = $record->delete;
74 $error = $record->check;
76 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
78 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
80 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
82 @cust_pay_objects = $cust_bill->cust_pay;
84 $tax_amount = $record->tax;
86 @lines = $cust_bill->print_text;
87 @lines = $cust_bill->print_text $time;
91 An FS::cust_bill object represents an invoice; a declaration that a customer
92 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
93 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
94 following fields are currently supported:
100 =item invnum - primary key (assigned automatically for new invoices)
102 =item custnum - customer (see L<FS::cust_main>)
104 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
105 L<Time::Local> and L<Date::Parse> for conversion functions.
107 =item charged - amount of this invoice
109 =item invoice_terms - optional terms override for this specific invoice
113 Customer info at invoice generation time
117 =item previous_balance
119 =item billing_balance
127 =item printed - deprecated
135 =item closed - books closed flag, empty or `Y'
137 =item statementnum - invoice aggregation (see L<FS::cust_statement>)
139 =item agent_invid - legacy invoice number
149 Creates a new invoice. To add the invoice to the database, see L<"insert">.
150 Invoices are normally created by calling the bill method of a customer object
151 (see L<FS::cust_main>).
155 sub table { 'cust_bill'; }
157 sub cust_linked { $_[0]->cust_main_custnum; }
158 sub cust_unlinked_msg {
160 "WARNING: can't find cust_main.custnum ". $self->custnum.
161 ' (cust_bill.invnum '. $self->invnum. ')';
166 Adds this invoice to the database ("Posts" the invoice). If there is an error,
167 returns the error, otherwise returns false.
173 warn "$me insert called\n" if $DEBUG;
175 local $SIG{HUP} = 'IGNORE';
176 local $SIG{INT} = 'IGNORE';
177 local $SIG{QUIT} = 'IGNORE';
178 local $SIG{TERM} = 'IGNORE';
179 local $SIG{TSTP} = 'IGNORE';
180 local $SIG{PIPE} = 'IGNORE';
182 my $oldAutoCommit = $FS::UID::AutoCommit;
183 local $FS::UID::AutoCommit = 0;
186 my $error = $self->SUPER::insert;
188 $dbh->rollback if $oldAutoCommit;
192 if ( $self->get('cust_bill_pkg') ) {
193 foreach my $cust_bill_pkg ( @{$self->get('cust_bill_pkg')} ) {
194 $cust_bill_pkg->invnum($self->invnum);
195 my $error = $cust_bill_pkg->insert;
197 $dbh->rollback if $oldAutoCommit;
198 return "can't create invoice line item: $error";
203 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
210 This method now works but you probably shouldn't use it. Instead, apply a
211 credit against the invoice.
213 Using this method to delete invoices outright is really, really bad. There
214 would be no record you ever posted this invoice, and there are no check to
215 make sure charged = 0 or that there are no associated cust_bill_pkg records.
217 Really, don't use it.
223 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
225 local $SIG{HUP} = 'IGNORE';
226 local $SIG{INT} = 'IGNORE';
227 local $SIG{QUIT} = 'IGNORE';
228 local $SIG{TERM} = 'IGNORE';
229 local $SIG{TSTP} = 'IGNORE';
230 local $SIG{PIPE} = 'IGNORE';
232 my $oldAutoCommit = $FS::UID::AutoCommit;
233 local $FS::UID::AutoCommit = 0;
236 foreach my $table (qw(
248 foreach my $linked ( $self->$table() ) {
249 my $error = $linked->delete;
251 $dbh->rollback if $oldAutoCommit;
258 my $error = $self->SUPER::delete(@_);
260 $dbh->rollback if $oldAutoCommit;
264 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
270 =item replace [ OLD_RECORD ]
272 You can, but probably shouldn't modify invoices...
274 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
275 supplied, replaces this record. If there is an error, returns the error,
276 otherwise returns false.
280 #replace can be inherited from Record.pm
282 # replace_check is now the preferred way to #implement replace data checks
283 # (so $object->replace() works without an argument)
286 my( $new, $old ) = ( shift, shift );
287 return "Can't modify closed invoice" if $old->closed =~ /^Y/i;
288 #return "Can't change _date!" unless $old->_date eq $new->_date;
289 return "Can't change _date" unless $old->_date == $new->_date;
290 return "Can't change charged" unless $old->charged == $new->charged
291 || $old->charged == 0;
298 Checks all fields to make sure this is a valid invoice. If there is an error,
299 returns the error, otherwise returns false. Called by the insert and replace
308 $self->ut_numbern('invnum')
309 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
310 || $self->ut_numbern('_date')
311 || $self->ut_money('charged')
312 || $self->ut_numbern('printed')
313 || $self->ut_enum('closed', [ '', 'Y' ])
314 || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
315 || $self->ut_numbern('agent_invid') #varchar?
317 return $error if $error;
319 $self->_date(time) unless $self->_date;
321 $self->printed(0) if $self->printed eq '';
328 Returns the displayed invoice number for this invoice: agent_invid if
329 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
335 if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
336 return $self->agent_invid;
338 return $self->invnum;
344 Returns a list consisting of the total previous balance for this customer,
345 followed by the previous outstanding invoices (as FS::cust_bill objects also).
352 my @cust_bill = sort { $a->_date <=> $b->_date }
353 grep { $_->owed != 0 && $_->_date < $self->_date }
354 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
356 foreach ( @cust_bill ) { $total += $_->owed; }
362 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
369 { 'table' => 'cust_bill_pkg',
370 'hashref' => { 'invnum' => $self->invnum },
371 'order_by' => 'ORDER BY billpkgnum',
376 =item cust_bill_pkg_pkgnum PKGNUM
378 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
383 sub cust_bill_pkg_pkgnum {
384 my( $self, $pkgnum ) = @_;
386 { 'table' => 'cust_bill_pkg',
387 'hashref' => { 'invnum' => $self->invnum,
390 'order_by' => 'ORDER BY billpkgnum',
397 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
404 my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
405 $self->cust_bill_pkg;
407 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
412 Returns true if any of the packages (or their definitions) corresponding to the
413 line items for this invoice have the no_auto flag set.
419 grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
422 =item open_cust_bill_pkg
424 Returns the open line items for this invoice.
426 Note that cust_bill_pkg with both setup and recur fees are returned as two
427 separate line items, each with only one fee.
431 # modeled after cust_main::open_cust_bill
432 sub open_cust_bill_pkg {
435 # grep { $_->owed > 0 } $self->cust_bill_pkg
437 my %other = ( 'recur' => 'setup',
438 'setup' => 'recur', );
440 foreach my $field ( qw( recur setup )) {
441 push @open, map { $_->set( $other{$field}, 0 ); $_; }
442 grep { $_->owed($field) > 0 }
443 $self->cust_bill_pkg;
449 =item cust_bill_event
451 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
455 sub cust_bill_event {
457 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
460 =item num_cust_bill_event
462 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
466 sub num_cust_bill_event {
469 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
470 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
471 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
472 $sth->fetchrow_arrayref->[0];
477 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
481 #false laziness w/cust_pkg.pm
485 'table' => 'cust_event',
486 'addl_from' => 'JOIN part_event USING ( eventpart )',
487 'hashref' => { 'tablenum' => $self->invnum },
488 'extra_sql' => " AND eventtable = 'cust_bill' ",
494 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
498 #false laziness w/cust_pkg.pm
502 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
503 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
504 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
505 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
506 $sth->fetchrow_arrayref->[0];
511 Returns the customer (see L<FS::cust_main>) for this invoice.
517 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
520 =item cust_suspend_if_balance_over AMOUNT
522 Suspends the customer associated with this invoice if the total amount owed on
523 this invoice and all older invoices is greater than the specified amount.
525 Returns a list: an empty list on success or a list of errors.
529 sub cust_suspend_if_balance_over {
530 my( $self, $amount ) = ( shift, shift );
531 my $cust_main = $self->cust_main;
532 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
535 $cust_main->suspend(@_);
541 Depreciated. See the cust_credited method.
543 #Returns a list consisting of the total previous credited (see
544 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
545 #outstanding credits (FS::cust_credit objects).
551 croak "FS::cust_bill->cust_credit depreciated; see ".
552 "FS::cust_bill->cust_credit_bill";
555 #my @cust_credit = sort { $a->_date <=> $b->_date }
556 # grep { $_->credited != 0 && $_->_date < $self->_date }
557 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
559 #foreach (@cust_credit) { $total += $_->credited; }
560 #$total, @cust_credit;
565 Depreciated. See the cust_bill_pay method.
567 #Returns all payments (see L<FS::cust_pay>) for this invoice.
573 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
575 #sort { $a->_date <=> $b->_date }
576 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
582 qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
585 sub cust_bill_pay_batch {
587 qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
592 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
598 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
599 sort { $a->_date <=> $b->_date }
600 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
605 =item cust_credit_bill
607 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
613 map { $_ } #return $self->num_cust_credit_bill unless wantarray;
614 sort { $a->_date <=> $b->_date }
615 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
619 sub cust_credit_bill {
620 shift->cust_credited(@_);
623 =item cust_bill_pay_pkgnum PKGNUM
625 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
626 with matching pkgnum.
630 sub cust_bill_pay_pkgnum {
631 my( $self, $pkgnum ) = @_;
632 map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
633 sort { $a->_date <=> $b->_date }
634 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
640 =item cust_credited_pkgnum PKGNUM
642 =item cust_credit_bill_pkgnum PKGNUM
644 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
645 with matching pkgnum.
649 sub cust_credited_pkgnum {
650 my( $self, $pkgnum ) = @_;
651 map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
652 sort { $a->_date <=> $b->_date }
653 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
659 sub cust_credit_bill_pkgnum {
660 shift->cust_credited_pkgnum(@_);
665 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
672 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
674 foreach (@taxlines) { $total += $_->setup; }
680 Returns the amount owed (still outstanding) on this invoice, which is charged
681 minus all payment applications (see L<FS::cust_bill_pay>) and credit
682 applications (see L<FS::cust_credit_bill>).
688 my $balance = $self->charged;
689 $balance -= $_->amount foreach ( $self->cust_bill_pay );
690 $balance -= $_->amount foreach ( $self->cust_credited );
691 $balance = sprintf( "%.2f", $balance);
692 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
697 my( $self, $pkgnum ) = @_;
699 #my $balance = $self->charged;
701 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
703 $balance -= $_->amount for $self->cust_bill_pay_pkgnum($pkgnum);
704 $balance -= $_->amount for $self->cust_credited_pkgnum($pkgnum);
706 $balance = sprintf( "%.2f", $balance);
707 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
711 =item apply_payments_and_credits [ OPTION => VALUE ... ]
713 Applies unapplied payments and credits to this invoice.
715 A hash of optional arguments may be passed. Currently "manual" is supported.
716 If true, a payment receipt is sent instead of a statement when
717 'payment_receipt_email' configuration option is set.
719 If there is an error, returns the error, otherwise returns false.
723 sub apply_payments_and_credits {
724 my( $self, %options ) = @_;
726 local $SIG{HUP} = 'IGNORE';
727 local $SIG{INT} = 'IGNORE';
728 local $SIG{QUIT} = 'IGNORE';
729 local $SIG{TERM} = 'IGNORE';
730 local $SIG{TSTP} = 'IGNORE';
731 local $SIG{PIPE} = 'IGNORE';
733 my $oldAutoCommit = $FS::UID::AutoCommit;
734 local $FS::UID::AutoCommit = 0;
737 $self->select_for_update; #mutex
739 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
740 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
742 if ( $conf->exists('pkg-balances') ) {
743 # limit @payments & @credits to those w/ a pkgnum grepped from $self
744 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
745 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
746 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
749 while ( $self->owed > 0 and ( @payments || @credits ) ) {
752 if ( @payments && @credits ) {
754 #decide which goes first by weight of top (unapplied) line item
756 my @open_lineitems = $self->open_cust_bill_pkg;
759 max( map { $_->part_pkg->pay_weight || 0 }
764 my $max_credit_weight =
765 max( map { $_->part_pkg->credit_weight || 0 }
771 #if both are the same... payments first? it has to be something
772 if ( $max_pay_weight >= $max_credit_weight ) {
778 } elsif ( @payments ) {
780 } elsif ( @credits ) {
783 die "guru meditation #12 and 35";
787 if ( $app eq 'pay' ) {
789 my $payment = shift @payments;
790 $unapp_amount = $payment->unapplied;
791 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
792 $app->pkgnum( $payment->pkgnum )
793 if $conf->exists('pkg-balances') && $payment->pkgnum;
795 } elsif ( $app eq 'credit' ) {
797 my $credit = shift @credits;
798 $unapp_amount = $credit->credited;
799 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
800 $app->pkgnum( $credit->pkgnum )
801 if $conf->exists('pkg-balances') && $credit->pkgnum;
804 die "guru meditation #12 and 35";
808 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
809 warn "owed_pkgnum ". $app->pkgnum;
810 $owed = $self->owed_pkgnum($app->pkgnum);
814 next unless $owed > 0;
816 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
817 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
819 $app->invnum( $self->invnum );
821 my $error = $app->insert(%options);
823 $dbh->rollback if $oldAutoCommit;
824 return "Error inserting ". $app->table. " record: $error";
826 die $error if $error;
830 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
835 =item generate_email OPTION => VALUE ...
843 sender address, required
847 alternate template name, optional
851 text attachment arrayref, optional
855 email subject, optional
859 notice name instead of "Invoice", optional
863 Returns an argument list to be passed to L<FS::Misc::send_email>.
874 my $me = '[FS::cust_bill::generate_email]';
877 'from' => $args{'from'},
878 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
882 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
883 'template' => $args{'template'},
884 'notice_name' => ( $args{'notice_name'} || 'Invoice' ),
887 my $cust_main = $self->cust_main;
889 if (ref($args{'to'}) eq 'ARRAY') {
890 $return{'to'} = $args{'to'};
892 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
893 $cust_main->invoicing_list
897 if ( $conf->exists('invoice_html') ) {
899 warn "$me creating HTML/text multipart message"
902 $return{'nobody'} = 1;
904 my $alternative = build MIME::Entity
905 'Type' => 'multipart/alternative',
906 'Encoding' => '7bit',
907 'Disposition' => 'inline'
911 if ( $conf->exists('invoice_email_pdf')
912 and scalar($conf->config('invoice_email_pdf_note')) ) {
914 warn "$me using 'invoice_email_pdf_note' in multipart message"
916 $data = [ map { $_ . "\n" }
917 $conf->config('invoice_email_pdf_note')
922 warn "$me not using 'invoice_email_pdf_note' in multipart message"
924 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
925 $data = $args{'print_text'};
927 $data = [ $self->print_text(\%opt) ];
932 $alternative->attach(
933 'Type' => 'text/plain',
934 #'Encoding' => 'quoted-printable',
935 'Encoding' => '7bit',
937 'Disposition' => 'inline',
940 $args{'from'} =~ /\@([\w\.\-]+)/;
941 my $from = $1 || 'example.com';
942 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
945 my $agentnum = $cust_main->agentnum;
946 if ( defined($args{'template'}) && length($args{'template'})
947 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
950 $logo = 'logo_'. $args{'template'}. '.png';
954 my $image_data = $conf->config_binary( $logo, $agentnum);
956 my $image = build MIME::Entity
957 'Type' => 'image/png',
958 'Encoding' => 'base64',
959 'Data' => $image_data,
960 'Filename' => 'logo.png',
961 'Content-ID' => "<$content_id>",
965 if($conf->exists('invoice-barcode')){
966 my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
967 $barcode = build MIME::Entity
968 'Type' => 'image/png',
969 'Encoding' => 'base64',
970 'Data' => $self->invoice_barcode(0),
971 'Filename' => 'barcode.png',
972 'Content-ID' => "<$barcode_content_id>",
974 $opt{'barcode_cid'} = $barcode_content_id;
977 $alternative->attach(
978 'Type' => 'text/html',
979 'Encoding' => 'quoted-printable',
980 'Data' => [ '<html>',
983 ' '. encode_entities($return{'subject'}),
986 ' <body bgcolor="#e8e8e8">',
987 $self->print_html({ 'cid'=>$content_id, %opt }),
991 'Disposition' => 'inline',
992 #'Filename' => 'invoice.pdf',
996 if ( $cust_main->email_csv_cdr ) {
998 push @otherparts, build MIME::Entity
999 'Type' => 'text/csv',
1000 'Encoding' => '7bit',
1001 'Data' => [ map { "$_\n" }
1002 $self->call_details('prepend_billed_number' => 1)
1004 'Disposition' => 'attachment',
1005 'Filename' => 'usage-'. $self->invnum. '.csv',
1010 if ( $conf->exists('invoice_email_pdf') ) {
1015 # multipart/alternative
1021 my $related = build MIME::Entity 'Type' => 'multipart/related',
1022 'Encoding' => '7bit';
1024 #false laziness w/Misc::send_email
1025 $related->head->replace('Content-type',
1026 $related->mime_type.
1027 '; boundary="'. $related->head->multipart_boundary. '"'.
1028 '; type=multipart/alternative'
1031 $related->add_part($alternative);
1033 $related->add_part($image);
1035 my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
1037 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
1041 #no other attachment:
1043 # multipart/alternative
1048 $return{'content-type'} = 'multipart/related';
1049 if($conf->exists('invoice-barcode')){
1050 $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ];
1053 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
1055 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
1056 #$return{'disposition'} = 'inline';
1062 if ( $conf->exists('invoice_email_pdf') ) {
1063 warn "$me creating PDF attachment"
1066 #mime parts arguments a la MIME::Entity->build().
1067 $return{'mimeparts'} = [
1068 { $self->mimebuild_pdf(\%opt) }
1072 if ( $conf->exists('invoice_email_pdf')
1073 and scalar($conf->config('invoice_email_pdf_note')) ) {
1075 warn "$me using 'invoice_email_pdf_note'"
1077 $return{'body'} = [ map { $_ . "\n" }
1078 $conf->config('invoice_email_pdf_note')
1083 warn "$me not using 'invoice_email_pdf_note'"
1085 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1086 $return{'body'} = $args{'print_text'};
1088 $return{'body'} = [ $self->print_text(\%opt) ];
1101 Returns a list suitable for passing to MIME::Entity->build(), representing
1102 this invoice as PDF attachment.
1109 'Type' => 'application/pdf',
1110 'Encoding' => 'base64',
1111 'Data' => [ $self->print_pdf(@_) ],
1112 'Disposition' => 'attachment',
1113 'Filename' => 'invoice-'. $self->invnum. '.pdf',
1117 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1119 Sends this invoice to the destinations configured for this customer: sends
1120 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1122 Options can be passed as a hashref (recommended) or as a list of up to
1123 four values for templatename, agentnum, invoice_from and amount.
1125 I<template>, if specified, is the name of a suffix for alternate invoices.
1127 I<agentnum>, if specified, means that this invoice will only be sent for customers
1128 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
1129 single agent) or an arrayref of agentnums.
1131 I<invoice_from>, if specified, overrides the default email invoice From: address.
1133 I<amount>, if specified, only sends the invoice if the total amount owed on this
1134 invoice and all older invoices is greater than the specified amount.
1136 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1140 sub queueable_send {
1143 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1144 or die "invalid invoice number: " . $opt{invnum};
1146 my @args = ( $opt{template}, $opt{agentnum} );
1147 push @args, $opt{invoice_from}
1148 if exists($opt{invoice_from}) && $opt{invoice_from};
1150 my $error = $self->send( @args );
1151 die $error if $error;
1158 my( $template, $invoice_from, $notice_name );
1160 my $balance_over = 0;
1164 $template = $opt->{'template'} || '';
1165 if ( $agentnums = $opt->{'agentnum'} ) {
1166 $agentnums = [ $agentnums ] unless ref($agentnums);
1168 $invoice_from = $opt->{'invoice_from'};
1169 $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1170 $notice_name = $opt->{'notice_name'};
1172 $template = scalar(@_) ? shift : '';
1173 if ( scalar(@_) && $_[0] ) {
1174 $agentnums = ref($_[0]) ? shift : [ shift ];
1176 $invoice_from = shift if scalar(@_);
1177 $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1180 return 'N/A' unless ! $agentnums
1181 or grep { $_ == $self->cust_main->agentnum } @$agentnums;
1184 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
1186 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1187 $conf->config('invoice_from', $self->cust_main->agentnum );
1190 'template' => $template,
1191 'invoice_from' => $invoice_from,
1192 'notice_name' => ( $notice_name || 'Invoice' ),
1195 my @invoicing_list = $self->cust_main->invoicing_list;
1197 #$self->email_invoice(\%opt)
1199 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
1201 #$self->print_invoice(\%opt)
1203 if grep { $_ eq 'POST' } @invoicing_list; #postal
1205 $self->fax_invoice(\%opt)
1206 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1212 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
1214 Emails this invoice.
1216 Options can be passed as a hashref (recommended) or as a list of up to
1217 two values for templatename and invoice_from.
1219 I<template>, if specified, is the name of a suffix for alternate invoices.
1221 I<invoice_from>, if specified, overrides the default email invoice From: address.
1223 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1227 sub queueable_email {
1230 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1231 or die "invalid invoice number: " . $opt{invnum};
1233 my @args = ( $opt{template} );
1234 push @args, $opt{invoice_from}
1235 if exists($opt{invoice_from}) && $opt{invoice_from};
1237 my $error = $self->email( @args );
1238 die $error if $error;
1242 #sub email_invoice {
1246 my( $template, $invoice_from, $notice_name );
1249 $template = $opt->{'template'} || '';
1250 $invoice_from = $opt->{'invoice_from'};
1251 $notice_name = $opt->{'notice_name'} || 'Invoice';
1253 $template = scalar(@_) ? shift : '';
1254 $invoice_from = shift if scalar(@_);
1255 $notice_name = 'Invoice';
1258 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1259 $conf->config('invoice_from', $self->cust_main->agentnum );
1261 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1262 $self->cust_main->invoicing_list;
1264 if ( ! @invoicing_list ) { #no recipients
1265 if ( $conf->exists('cust_bill-no_recipients-error') ) {
1266 die 'No recipients for customer #'. $self->custnum;
1268 #default: better to notify this person than silence
1269 @invoicing_list = ($invoice_from);
1273 my $subject = $self->email_subject($template);
1275 my $error = send_email(
1276 $self->generate_email(
1277 'from' => $invoice_from,
1278 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1279 'subject' => $subject,
1280 'template' => $template,
1281 'notice_name' => $notice_name,
1284 die "can't email invoice: $error\n" if $error;
1285 #die "$error\n" if $error;
1292 #my $template = scalar(@_) ? shift : '';
1295 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1298 my $cust_main = $self->cust_main;
1299 my $name = $cust_main->name;
1300 my $name_short = $cust_main->name_short;
1301 my $invoice_number = $self->invnum;
1302 my $invoice_date = $self->_date_pretty;
1304 eval qq("$subject");
1307 =item lpr_data HASHREF | [ TEMPLATE ]
1309 Returns the postscript or plaintext for this invoice as an arrayref.
1311 Options can be passed as a hashref (recommended) or as a single optional value
1314 I<template>, if specified, is the name of a suffix for alternate invoices.
1316 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1322 my( $template, $notice_name );
1325 $template = $opt->{'template'} || '';
1326 $notice_name = $opt->{'notice_name'} || 'Invoice';
1328 $template = scalar(@_) ? shift : '';
1329 $notice_name = 'Invoice';
1333 'template' => $template,
1334 'notice_name' => $notice_name,
1337 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1338 [ $self->$method( \%opt ) ];
1341 =item print HASHREF | [ TEMPLATE ]
1343 Prints this invoice.
1345 Options can be passed as a hashref (recommended) or as a single optional
1348 I<template>, if specified, is the name of a suffix for alternate invoices.
1350 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1354 #sub print_invoice {
1357 my( $template, $notice_name );
1360 $template = $opt->{'template'} || '';
1361 $notice_name = $opt->{'notice_name'} || 'Invoice';
1363 $template = scalar(@_) ? shift : '';
1364 $notice_name = 'Invoice';
1368 'template' => $template,
1369 'notice_name' => $notice_name,
1372 if($conf->exists('invoice_print_pdf')) {
1373 # Add the invoice to the current batch.
1374 $self->batch_invoice(\%opt);
1377 do_print $self->lpr_data(\%opt);
1381 =item fax_invoice HASHREF | [ TEMPLATE ]
1385 Options can be passed as a hashref (recommended) or as a single optional
1388 I<template>, if specified, is the name of a suffix for alternate invoices.
1390 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1396 my( $template, $notice_name );
1399 $template = $opt->{'template'} || '';
1400 $notice_name = $opt->{'notice_name'} || 'Invoice';
1402 $template = scalar(@_) ? shift : '';
1403 $notice_name = 'Invoice';
1406 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1407 unless $conf->exists('invoice_latex');
1409 my $dialstring = $self->cust_main->getfield('fax');
1413 'template' => $template,
1414 'notice_name' => $notice_name,
1417 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1418 'dialstring' => $dialstring,
1420 die $error if $error;
1424 =item batch_invoice [ HASHREF ]
1426 Place this invoice into the open batch (see C<FS::bill_batch>). If there
1427 isn't an open batch, one will be created.
1432 my ($self, $opt) = @_;
1433 my $batch = FS::bill_batch->get_open_batch;
1434 my $cust_bill_batch = FS::cust_bill_batch->new({
1435 batchnum => $batch->batchnum,
1436 invnum => $self->invnum,
1438 return $cust_bill_batch->insert($opt);
1441 =item ftp_invoice [ TEMPLATENAME ]
1443 Sends this invoice data via FTP.
1445 TEMPLATENAME is unused?
1451 my $template = scalar(@_) ? shift : '';
1454 'protocol' => 'ftp',
1455 'server' => $conf->config('cust_bill-ftpserver'),
1456 'username' => $conf->config('cust_bill-ftpusername'),
1457 'password' => $conf->config('cust_bill-ftppassword'),
1458 'dir' => $conf->config('cust_bill-ftpdir'),
1459 'format' => $conf->config('cust_bill-ftpformat'),
1463 =item spool_invoice [ TEMPLATENAME ]
1465 Spools this invoice data (see L<FS::spool_csv>)
1467 TEMPLATENAME is unused?
1473 my $template = scalar(@_) ? shift : '';
1476 'format' => $conf->config('cust_bill-spoolformat'),
1477 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1481 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1483 Like B<send>, but only sends the invoice if it is the newest open invoice for
1488 sub send_if_newest {
1493 grep { $_->owed > 0 }
1494 qsearch('cust_bill', {
1495 'custnum' => $self->custnum,
1496 #'_date' => { op=>'>', value=>$self->_date },
1497 'invnum' => { op=>'>', value=>$self->invnum },
1504 =item send_csv OPTION => VALUE, ...
1506 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1510 protocol - currently only "ftp"
1516 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1517 and YYMMDDHHMMSS is a timestamp.
1519 See L</print_csv> for a description of the output format.
1524 my($self, %opt) = @_;
1528 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1529 mkdir $spooldir, 0700 unless -d $spooldir;
1531 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1532 my $file = "$spooldir/$tracctnum.csv";
1534 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1536 open(CSV, ">$file") or die "can't open $file: $!";
1544 if ( $opt{protocol} eq 'ftp' ) {
1545 eval "use Net::FTP;";
1547 $net = Net::FTP->new($opt{server}) or die @$;
1549 die "unknown protocol: $opt{protocol}";
1552 $net->login( $opt{username}, $opt{password} )
1553 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1555 $net->binary or die "can't set binary mode";
1557 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1559 $net->put($file) or die "can't put $file: $!";
1569 Spools CSV invoice data.
1575 =item format - 'default' or 'billco'
1577 =item dest - if set (to POST, EMAIL or FAX), only sends spools invoices if the customer has the corresponding invoice destinations set (see L<FS::cust_main_invoice>).
1579 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1581 =item balanceover - if set, only spools the invoice if the total amount owed on this invoice and all older invoices is greater than the specified amount.
1588 my($self, %opt) = @_;
1590 my $cust_main = $self->cust_main;
1592 if ( $opt{'dest'} ) {
1593 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1594 $cust_main->invoicing_list;
1595 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1596 || ! keys %invoicing_list;
1599 if ( $opt{'balanceover'} ) {
1601 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1604 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1605 mkdir $spooldir, 0700 unless -d $spooldir;
1607 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1611 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1612 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1615 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1617 open(CSV, ">>$file") or die "can't open $file: $!";
1618 flock(CSV, LOCK_EX);
1623 if ( lc($opt{'format'}) eq 'billco' ) {
1625 flock(CSV, LOCK_UN);
1630 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1633 open(CSV,">>$file") or die "can't open $file: $!";
1634 flock(CSV, LOCK_EX);
1640 flock(CSV, LOCK_UN);
1647 =item print_csv OPTION => VALUE, ...
1649 Returns CSV data for this invoice.
1653 format - 'default' or 'billco'
1655 Returns a list consisting of two scalars. The first is a single line of CSV
1656 header information for this invoice. The second is one or more lines of CSV
1657 detail information for this invoice.
1659 If I<format> is not specified or "default", the fields of the CSV file are as
1662 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1666 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1668 B<record_type> is C<cust_bill> for the initial header line only. The
1669 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1670 fields are filled in.
1672 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1673 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1676 =item invnum - invoice number
1678 =item custnum - customer number
1680 =item _date - invoice date
1682 =item charged - total invoice amount
1684 =item first - customer first name
1686 =item last - customer first name
1688 =item company - company name
1690 =item address1 - address line 1
1692 =item address2 - address line 1
1702 =item pkg - line item description
1704 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1706 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1708 =item sdate - start date for recurring fee
1710 =item edate - end date for recurring fee
1714 If I<format> is "billco", the fields of the header CSV file are as follows:
1716 +-------------------------------------------------------------------+
1717 | FORMAT HEADER FILE |
1718 |-------------------------------------------------------------------|
1719 | Field | Description | Name | Type | Width |
1720 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1721 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1722 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1723 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1724 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1725 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1726 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1727 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1728 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1729 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1730 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1731 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1732 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1733 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1734 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1735 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1736 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1737 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1738 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1739 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1740 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1741 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1742 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1743 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1744 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1745 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1746 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1747 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1748 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1749 +-------+-------------------------------+------------+------+-------+
1751 If I<format> is "billco", the fields of the detail CSV file are as follows:
1753 FORMAT FOR DETAIL FILE
1755 Field | Description | Name | Type | Width
1756 1 | N/A-Leave Empty | RC | CHAR | 2
1757 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1758 3 | Account Number | TRACCTNUM | CHAR | 15
1759 4 | Invoice Number | TRINVOICE | CHAR | 15
1760 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1761 6 | Transaction Detail | DETAILS | CHAR | 100
1762 7 | Amount | AMT | NUM* | 9
1763 8 | Line Format Control** | LNCTRL | CHAR | 2
1764 9 | Grouping Code | GROUP | CHAR | 2
1765 10 | User Defined | ACCT CODE | CHAR | 15
1770 my($self, %opt) = @_;
1772 eval "use Text::CSV_XS";
1775 my $cust_main = $self->cust_main;
1777 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1779 if ( lc($opt{'format'}) eq 'billco' ) {
1782 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1784 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1786 my( $previous_balance, @unused ) = $self->previous; #previous balance
1788 my $pmt_cr_applied = 0;
1789 $pmt_cr_applied += $_->{'amount'}
1790 foreach ( $self->_items_payments, $self->_items_credits ) ;
1792 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1795 '', # 1 | N/A-Leave Empty CHAR 2
1796 '', # 2 | N/A-Leave Empty CHAR 15
1797 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1798 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1799 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1800 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1801 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1802 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1803 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1804 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1805 '', # 10 | Ancillary Billing Information CHAR 30
1806 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1807 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1810 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1813 $duedate, # 14 | Bill Due Date CHAR 10
1815 $previous_balance, # 15 | Previous Balance NUM* 9
1816 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1817 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1818 $totaldue, # 18 | Total Amt Due NUM* 9
1819 $totaldue, # 19 | Total Amt Due NUM* 9
1820 '', # 20 | 30 Day Aging NUM* 9
1821 '', # 21 | 60 Day Aging NUM* 9
1822 '', # 22 | 90 Day Aging NUM* 9
1823 'N', # 23 | Y/N CHAR 1
1824 '', # 24 | Remittance automation CHAR 100
1825 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1826 $self->custnum, # 26 | Customer Reference Number CHAR 15
1827 '0', # 27 | Federal Tax*** NUM* 9
1828 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1829 '0', # 29 | Other Taxes & Fees*** NUM* 9
1838 time2str("%x", $self->_date),
1839 sprintf("%.2f", $self->charged),
1840 ( map { $cust_main->getfield($_) }
1841 qw( first last company address1 address2 city state zip country ) ),
1843 ) or die "can't create csv";
1846 my $header = $csv->string. "\n";
1849 if ( lc($opt{'format'}) eq 'billco' ) {
1852 foreach my $item ( $self->_items_pkg ) {
1855 '', # 1 | N/A-Leave Empty CHAR 2
1856 '', # 2 | N/A-Leave Empty CHAR 15
1857 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1858 $self->invnum, # 4 | Invoice Number CHAR 15
1859 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1860 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1861 $item->{'amount'}, # 7 | Amount NUM* 9
1862 '', # 8 | Line Format Control** CHAR 2
1863 '', # 9 | Grouping Code CHAR 2
1864 '', # 10 | User Defined CHAR 15
1867 $detail .= $csv->string. "\n";
1873 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1875 my($pkg, $setup, $recur, $sdate, $edate);
1876 if ( $cust_bill_pkg->pkgnum ) {
1878 ($pkg, $setup, $recur, $sdate, $edate) = (
1879 $cust_bill_pkg->part_pkg->pkg,
1880 ( $cust_bill_pkg->setup != 0
1881 ? sprintf("%.2f", $cust_bill_pkg->setup )
1883 ( $cust_bill_pkg->recur != 0
1884 ? sprintf("%.2f", $cust_bill_pkg->recur )
1886 ( $cust_bill_pkg->sdate
1887 ? time2str("%x", $cust_bill_pkg->sdate)
1889 ($cust_bill_pkg->edate
1890 ?time2str("%x", $cust_bill_pkg->edate)
1894 } else { #pkgnum tax
1895 next unless $cust_bill_pkg->setup != 0;
1896 $pkg = $cust_bill_pkg->desc;
1897 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
1898 ( $sdate, $edate ) = ( '', '' );
1904 ( map { '' } (1..11) ),
1905 ($pkg, $setup, $recur, $sdate, $edate)
1906 ) or die "can't create csv";
1908 $detail .= $csv->string. "\n";
1914 ( $header, $detail );
1920 Pays this invoice with a compliemntary payment. If there is an error,
1921 returns the error, otherwise returns false.
1927 my $cust_pay = new FS::cust_pay ( {
1928 'invnum' => $self->invnum,
1929 'paid' => $self->owed,
1932 'payinfo' => $self->cust_main->payinfo,
1940 Attempts to pay this invoice with a credit card payment via a
1941 Business::OnlinePayment realtime gateway. See
1942 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1943 for supported processors.
1949 $self->realtime_bop( 'CC', @_ );
1954 Attempts to pay this invoice with an electronic check (ACH) payment via a
1955 Business::OnlinePayment realtime gateway. See
1956 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1957 for supported processors.
1963 $self->realtime_bop( 'ECHECK', @_ );
1968 Attempts to pay this invoice with phone bill (LEC) payment via a
1969 Business::OnlinePayment realtime gateway. See
1970 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1971 for supported processors.
1977 $self->realtime_bop( 'LEC', @_ );
1981 my( $self, $method ) = @_;
1983 my $cust_main = $self->cust_main;
1984 my $balance = $cust_main->balance;
1985 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1986 $amount = sprintf("%.2f", $amount);
1987 return "not run (balance $balance)" unless $amount > 0;
1989 my $description = 'Internet Services';
1990 if ( $conf->exists('business-onlinepayment-description') ) {
1991 my $dtempl = $conf->config('business-onlinepayment-description');
1993 my $agent_obj = $cust_main->agent
1994 or die "can't retreive agent for $cust_main (agentnum ".
1995 $cust_main->agentnum. ")";
1996 my $agent = $agent_obj->agent;
1997 my $pkgs = join(', ',
1998 map { $_->part_pkg->pkg }
1999 grep { $_->pkgnum } $self->cust_bill_pkg
2001 $description = eval qq("$dtempl");
2004 $cust_main->realtime_bop($method, $amount,
2005 'description' => $description,
2006 'invnum' => $self->invnum,
2007 #this didn't do what we want, it just calls apply_payments_and_credits
2009 'apply_to_invoice' => 1,
2011 #this changes application behavior: auto payments
2012 #triggered against a specific invoice are now applied
2013 #to that invoice instead of oldest open.
2019 =item batch_card OPTION => VALUE...
2021 Adds a payment for this invoice to the pending credit card batch (see
2022 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2023 runs the payment using a realtime gateway.
2028 my ($self, %options) = @_;
2029 my $cust_main = $self->cust_main;
2031 $options{invnum} = $self->invnum;
2033 $cust_main->batch_card(%options);
2036 sub _agent_template {
2038 $self->cust_main->agent_template;
2041 sub _agent_invoice_from {
2043 $self->cust_main->agent_invoice_from;
2046 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2048 Returns an text invoice, as a list of lines.
2050 Options can be passed as a hashref (recommended) or as a list of time, template
2051 and then any key/value pairs for any other options.
2053 I<time>, if specified, is used to control the printing of overdue messages. The
2054 default is now. It isn't the date of the invoice; that's the `_date' field.
2055 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2056 L<Time::Local> and L<Date::Parse> for conversion functions.
2058 I<template>, if specified, is the name of a suffix for alternate invoices.
2060 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2066 my( $today, $template, %opt );
2068 %opt = %{ shift() };
2069 $today = delete($opt{'time'}) || '';
2070 $template = delete($opt{template}) || '';
2072 ( $today, $template, %opt ) = @_;
2075 my %params = ( 'format' => 'template' );
2076 $params{'time'} = $today if $today;
2077 $params{'template'} = $template if $template;
2078 $params{$_} = $opt{$_}
2079 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2081 $self->print_generic( %params );
2084 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2086 Internal method - returns a filename of a filled-in LaTeX template for this
2087 invoice (Note: add ".tex" to get the actual filename), and a filename of
2088 an associated logo (with the .eps extension included).
2090 See print_ps and print_pdf for methods that return PostScript and PDF output.
2092 Options can be passed as a hashref (recommended) or as a list of time, template
2093 and then any key/value pairs for any other options.
2095 I<time>, if specified, is used to control the printing of overdue messages. The
2096 default is now. It isn't the date of the invoice; that's the `_date' field.
2097 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2098 L<Time::Local> and L<Date::Parse> for conversion functions.
2100 I<template>, if specified, is the name of a suffix for alternate invoices.
2102 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2108 my( $today, $template, %opt );
2110 %opt = %{ shift() };
2111 $today = delete($opt{'time'}) || '';
2112 $template = delete($opt{template}) || '';
2114 ( $today, $template, %opt ) = @_;
2117 my %params = ( 'format' => 'latex' );
2118 $params{'time'} = $today if $today;
2119 $params{'template'} = $template if $template;
2120 $params{$_} = $opt{$_}
2121 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2123 $template ||= $self->_agent_template;
2125 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2126 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2130 ) or die "can't open temp file: $!\n";
2132 my $agentnum = $self->cust_main->agentnum;
2134 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2135 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2136 or die "can't write temp file: $!\n";
2138 print $lh $conf->config_binary('logo.eps', $agentnum)
2139 or die "can't write temp file: $!\n";
2142 $params{'logo_file'} = $lh->filename;
2144 if($conf->exists('invoice-barcode')){
2145 my $png_file = $self->invoice_barcode($dir);
2146 my $eps_file = $png_file;
2147 $eps_file =~ s/\.png$/.eps/g;
2148 $png_file =~ /(barcode.*png)/;
2150 $eps_file =~ /(barcode.*eps)/;
2153 my $curr_dir = cwd();
2155 # after painfuly long experimentation, it was determined that sam2p won't
2156 # accept : and other chars in the path, no matter how hard I tried to
2157 # escape them, hence the chdir (and chdir back, just to be safe)
2158 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
2159 or die "sam2p failed: $!\n";
2163 $params{'barcode_file'} = $eps_file;
2166 my @filled_in = $self->print_generic( %params );
2168 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2172 ) or die "can't open temp file: $!\n";
2173 print $fh join('', @filled_in );
2176 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2177 return ($1, $params{'logo_file'}, $params{'barcode_file'});
2181 =item invoice_barcode DIR_OR_FALSE
2183 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2184 it is taken as the temp directory where the PNG file will be generated and the
2185 PNG file name is returned. Otherwise, the PNG image itself is returned.
2189 sub invoice_barcode {
2190 my ($self, $dir) = (shift,shift);
2192 my $gdbar = new GD::Barcode('Code39',$self->invnum);
2193 die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2194 my $gd = $gdbar->plot(Height => 30);
2197 my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2201 ) or die "can't open temp file: $!\n";
2202 print $bh $gd->png or die "cannot write barcode to file: $!\n";
2203 my $png_file = $bh->filename;
2210 =item print_generic OPTION => VALUE ...
2212 Internal method - returns a filled-in template for this invoice as a scalar.
2214 See print_ps and print_pdf for methods that return PostScript and PDF output.
2216 Non optional options include
2217 format - latex, html, template
2219 Optional options include
2221 template - a value used as a suffix for a configuration template
2223 time - a value used to control the printing of overdue messages. The
2224 default is now. It isn't the date of the invoice; that's the `_date' field.
2225 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2226 L<Time::Local> and L<Date::Parse> for conversion functions.
2230 unsquelch_cdr - overrides any per customer cdr squelching when true
2232 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2236 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2237 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2238 # yes: fixed width (dot matrix) text printing will be borked
2241 my( $self, %params ) = @_;
2242 my $today = $params{today} ? $params{today} : time;
2243 warn "$me print_generic called on $self with suffix $params{template}\n"
2246 my $format = $params{format};
2247 die "Unknown format: $format"
2248 unless $format =~ /^(latex|html|template)$/;
2250 my $cust_main = $self->cust_main;
2251 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2252 unless $cust_main->payname
2253 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2255 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2256 'html' => [ '<%=', '%>' ],
2257 'template' => [ '{', '}' ],
2260 warn "$me print_generic creating template\n"
2263 #create the template
2264 my $template = $params{template} ? $params{template} : $self->_agent_template;
2265 my $templatefile = "invoice_$format";
2266 $templatefile .= "_$template"
2267 if length($template);
2268 my @invoice_template = map "$_\n", $conf->config($templatefile)
2269 or die "cannot load config data $templatefile";
2272 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2273 #change this to a die when the old code is removed
2274 warn "old-style invoice template $templatefile; ".
2275 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2276 $old_latex = 'true';
2277 @invoice_template = _translate_old_latex_format(@invoice_template);
2280 warn "$me print_generic creating T:T object\n"
2283 my $text_template = new Text::Template(
2285 SOURCE => \@invoice_template,
2286 DELIMITERS => $delimiters{$format},
2289 warn "$me print_generic compiling T:T object\n"
2292 $text_template->compile()
2293 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2296 # additional substitution could possibly cause breakage in existing templates
2297 my %convert_maps = (
2299 'notes' => sub { map "$_", @_ },
2300 'footer' => sub { map "$_", @_ },
2301 'smallfooter' => sub { map "$_", @_ },
2302 'returnaddress' => sub { map "$_", @_ },
2303 'coupon' => sub { map "$_", @_ },
2304 'summary' => sub { map "$_", @_ },
2310 s/%%(.*)$/<!-- $1 -->/g;
2311 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2312 s/\\begin\{enumerate\}/<ol>/g;
2314 s/\\end\{enumerate\}/<\/ol>/g;
2315 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2324 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2326 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2331 s/\\\\\*?\s*$/<BR>/;
2332 s/\\hyphenation\{[\w\s\-]+}//;
2337 'coupon' => sub { "" },
2338 'summary' => sub { "" },
2345 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2346 s/\\begin\{enumerate\}//g;
2348 s/\\end\{enumerate\}//g;
2349 s/\\textbf\{(.*)\}/$1/g;
2356 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2358 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2363 s/\\\\\*?\s*$/\n/; # dubious
2364 s/\\hyphenation\{[\w\s\-]+}//;
2368 'coupon' => sub { "" },
2369 'summary' => sub { "" },
2374 # hashes for differing output formats
2375 my %nbsps = ( 'latex' => '~',
2376 'html' => '', # '&nbps;' would be nice
2377 'template' => '', # not used
2379 my $nbsp = $nbsps{$format};
2381 my %escape_functions = ( 'latex' => \&_latex_escape,
2382 'html' => \&_html_escape_nbsp,#\&encode_entities,
2383 'template' => sub { shift },
2385 my $escape_function = $escape_functions{$format};
2386 my $escape_function_nonbsp = ($format eq 'html')
2387 ? \&_html_escape : $escape_function;
2389 my %date_formats = ( 'latex' => $date_format_long,
2390 'html' => $date_format_long,
2393 $date_formats{'html'} =~ s/ / /g;
2395 my $date_format = $date_formats{$format};
2397 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2399 'html' => sub { return '<b>'. shift(). '</b>'
2401 'template' => sub { shift },
2403 my $embolden_function = $embolden_functions{$format};
2405 warn "$me generating template variables\n"
2408 # generate template variables
2411 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2415 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2421 $returnaddress = join("\n",
2422 $conf->config_orbase("invoice_${format}returnaddress", $template)
2425 } elsif ( grep /\S/,
2426 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2428 my $convert_map = $convert_maps{$format}{'returnaddress'};
2431 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2436 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2438 my $convert_map = $convert_maps{$format}{'returnaddress'};
2439 $returnaddress = join( "\n", &$convert_map(
2440 map { s/( {2,})/'~' x length($1)/eg;
2444 ( $conf->config('company_name', $self->cust_main->agentnum),
2445 $conf->config('company_address', $self->cust_main->agentnum),
2452 my $warning = "Couldn't find a return address; ".
2453 "do you need to set the company_address configuration value?";
2455 $returnaddress = $nbsp;
2456 #$returnaddress = $warning;
2460 warn "$me generating invoice data\n"
2463 my $agentnum = $self->cust_main->agentnum;
2465 my %invoice_data = (
2468 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2469 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2470 'returnaddress' => $returnaddress,
2471 'agent' => &$escape_function($cust_main->agent->agent),
2474 'invnum' => $self->invnum,
2475 'date' => time2str($date_format, $self->_date),
2476 'today' => time2str($date_format_long, $today),
2477 'terms' => $self->terms,
2478 'template' => $template, #params{'template'},
2479 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2480 'current_charges' => sprintf("%.2f", $self->charged),
2481 'duedate' => $self->due_date2str($rdate_format), #date_format?
2484 'custnum' => $cust_main->display_custnum,
2485 'agent_custid' => &$escape_function($cust_main->agent_custid),
2486 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2487 payname company address1 address2 city state zip fax
2491 'ship_enable' => $conf->exists('invoice-ship_address'),
2492 'unitprices' => $conf->exists('invoice-unitprice'),
2493 'smallernotes' => $conf->exists('invoice-smallernotes'),
2494 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2495 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2497 #layout info -- would be fancy to calc some of this and bury the template
2499 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2500 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2501 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2502 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2503 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2504 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2505 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2506 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2507 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2508 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2510 # better hang on to conf_dir for a while (for old templates)
2511 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2513 #these are only used when doing paged plaintext
2519 $invoice_data{finance_section} = '';
2520 if ( $conf->config('finance_pkgclass') ) {
2522 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2523 $invoice_data{finance_section} = $pkg_class->categoryname;
2525 $invoice_data{finance_amount} = '0.00';
2526 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2528 my $countrydefault = $conf->config('countrydefault') || 'US';
2529 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2530 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2531 my $method = $prefix.$_;
2532 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2534 $invoice_data{'ship_country'} = ''
2535 if ( $invoice_data{'ship_country'} eq $countrydefault );
2537 $invoice_data{'cid'} = $params{'cid'}
2540 if ( $cust_main->country eq $countrydefault ) {
2541 $invoice_data{'country'} = '';
2543 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2547 $invoice_data{'address'} = \@address;
2549 $cust_main->payname.
2550 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2551 ? " (P.O. #". $cust_main->payinfo. ")"
2555 push @address, $cust_main->company
2556 if $cust_main->company;
2557 push @address, $cust_main->address1;
2558 push @address, $cust_main->address2
2559 if $cust_main->address2;
2561 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2562 push @address, $invoice_data{'country'}
2563 if $invoice_data{'country'};
2565 while (scalar(@address) < 5);
2567 $invoice_data{'logo_file'} = $params{'logo_file'}
2568 if $params{'logo_file'};
2569 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2570 if $params{'barcode_file'};
2571 $invoice_data{'barcode_img'} = $params{'barcode_img'}
2572 if $params{'barcode_img'};
2573 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2574 if $params{'barcode_cid'};
2576 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2577 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2578 #my $balance_due = $self->owed + $pr_total - $cr_total;
2579 my $balance_due = $self->owed + $pr_total;
2580 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2581 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2582 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2583 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2585 my $summarypage = '';
2586 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2589 $invoice_data{'summarypage'} = $summarypage;
2591 warn "$me substituting variables in notes, footer, smallfooter\n"
2594 foreach my $include (qw( notes footer smallfooter coupon )) {
2596 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2599 if ( $conf->exists($inc_file, $agentnum)
2600 && length( $conf->config($inc_file, $agentnum) ) ) {
2602 @inc_src = $conf->config($inc_file, $agentnum);
2606 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2608 my $convert_map = $convert_maps{$format}{$include};
2610 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2611 s/--\@\]/$delimiters{$format}[1]/g;
2614 &$convert_map( $conf->config($inc_file, $agentnum) );
2618 my $inc_tt = new Text::Template (
2620 SOURCE => [ map "$_\n", @inc_src ],
2621 DELIMITERS => $delimiters{$format},
2622 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2624 unless ( $inc_tt->compile() ) {
2625 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2626 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2630 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2632 $invoice_data{$include} =~ s/\n+$//
2633 if ($format eq 'latex');
2636 $invoice_data{'po_line'} =
2637 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2638 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2641 my %money_chars = ( 'latex' => '',
2642 'html' => $conf->config('money_char') || '$',
2645 my $money_char = $money_chars{$format};
2647 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2648 'html' => $conf->config('money_char') || '$',
2651 my $other_money_char = $other_money_chars{$format};
2652 $invoice_data{'dollar'} = $other_money_char;
2654 my @detail_items = ();
2655 my @total_items = ();
2659 $invoice_data{'detail_items'} = \@detail_items;
2660 $invoice_data{'total_items'} = \@total_items;
2661 $invoice_data{'buf'} = \@buf;
2662 $invoice_data{'sections'} = \@sections;
2664 warn "$me generating sections\n"
2667 my $previous_section = { 'description' => 'Previous Charges',
2668 'subtotal' => $other_money_char.
2669 sprintf('%.2f', $pr_total),
2670 'summarized' => $summarypage ? 'Y' : '',
2672 $previous_section->{posttotal} = '0 / 30 / 60/ 90 days overdue '.
2673 join(' / ', map { $cust_main->balance_date_range(@$_) }
2674 $self->_prior_month30s
2676 if $conf->exists('invoice_include_aging');
2679 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2680 'subtotal' => $taxtotal, # adjusted below
2681 'summarized' => $summarypage ? 'Y' : '',
2683 my $tax_weight = _pkg_category($tax_section->{description})
2684 ? _pkg_category($tax_section->{description})->weight
2686 $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2687 $tax_section->{'sort_weight'} = $tax_weight;
2690 my $adjusttotal = 0;
2691 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2692 'subtotal' => 0, # adjusted below
2693 'summarized' => $summarypage ? 'Y' : '',
2695 my $adjust_weight = _pkg_category($adjust_section->{description})
2696 ? _pkg_category($adjust_section->{description})->weight
2698 $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2699 $adjust_section->{'sort_weight'} = $adjust_weight;
2701 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2702 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2703 $invoice_data{'multisection'} = $multisection;
2704 my $late_sections = [];
2705 my $extra_sections = [];
2706 my $extra_lines = ();
2707 if ( $multisection ) {
2708 ($extra_sections, $extra_lines) =
2709 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2710 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2712 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2714 push @detail_items, @$extra_lines if $extra_lines;
2716 $self->_items_sections( $late_sections, # this could stand a refactor
2718 $escape_function_nonbsp,
2722 if ($conf->exists('svc_phone_sections')) {
2723 my ($phone_sections, $phone_lines) =
2724 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2725 push @{$late_sections}, @$phone_sections;
2726 push @detail_items, @$phone_lines;
2729 push @sections, { 'description' => '', 'subtotal' => '' };
2732 unless ( $conf->exists('disable_previous_balance')
2733 || $conf->exists('previous_balance-summary_only')
2737 warn "$me adding previous balances\n"
2740 foreach my $line_item ( $self->_items_previous ) {
2743 ext_description => [],
2745 $detail->{'ref'} = $line_item->{'pkgnum'};
2746 $detail->{'quantity'} = 1;
2747 $detail->{'section'} = $previous_section;
2748 $detail->{'description'} = &$escape_function($line_item->{'description'});
2749 if ( exists $line_item->{'ext_description'} ) {
2750 @{$detail->{'ext_description'}} = map {
2751 &$escape_function($_);
2752 } @{$line_item->{'ext_description'}};
2754 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2755 $line_item->{'amount'};
2756 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2758 push @detail_items, $detail;
2759 push @buf, [ $detail->{'description'},
2760 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2766 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2767 push @buf, ['','-----------'];
2768 push @buf, [ 'Total Previous Balance',
2769 $money_char. sprintf("%10.2f", $pr_total) ];
2773 if ( $conf->exists('svc_phone-did-summary') ) {
2774 warn "$me adding DID summary\n"
2777 my ($didsummary,$minutes) = $self->_did_summary;
2778 my $didsummary_desc = 'DID Activity Summary (Past 30 days)';
2780 { 'description' => $didsummary_desc,
2781 'ext_description' => [ $didsummary, $minutes ],
2786 foreach my $section (@sections, @$late_sections) {
2788 warn "$me adding section \n". Dumper($section)
2791 # begin some normalization
2792 $section->{'subtotal'} = $section->{'amount'}
2794 && !exists($section->{subtotal})
2795 && exists($section->{amount});
2797 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2798 if ( $invoice_data{finance_section} &&
2799 $section->{'description'} eq $invoice_data{finance_section} );
2801 $section->{'subtotal'} = $other_money_char.
2802 sprintf('%.2f', $section->{'subtotal'})
2805 # continue some normalization
2806 $section->{'amount'} = $section->{'subtotal'}
2810 if ( $section->{'description'} ) {
2811 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2816 warn "$me setting options\n"
2819 my $multilocation = scalar($cust_main->cust_location); #too expensive?
2821 $options{'section'} = $section if $multisection;
2822 $options{'format'} = $format;
2823 $options{'escape_function'} = $escape_function;
2824 $options{'format_function'} = sub { () } unless $unsquelched;
2825 $options{'unsquelched'} = $unsquelched;
2826 $options{'summary_page'} = $summarypage;
2827 $options{'skip_usage'} =
2828 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2829 $options{'multilocation'} = $multilocation;
2830 $options{'multisection'} = $multisection;
2832 warn "$me searching for line items\n"
2835 foreach my $line_item ( $self->_items_pkg(%options) ) {
2837 warn "$me adding line item $line_item\n"
2841 ext_description => [],
2843 $detail->{'ref'} = $line_item->{'pkgnum'};
2844 $detail->{'quantity'} = $line_item->{'quantity'};
2845 $detail->{'section'} = $section;
2846 $detail->{'description'} = &$escape_function($line_item->{'description'});
2847 if ( exists $line_item->{'ext_description'} ) {
2848 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2850 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2851 $line_item->{'amount'};
2852 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2853 $line_item->{'unit_amount'};
2854 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2856 push @detail_items, $detail;
2857 push @buf, ( [ $detail->{'description'},
2858 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2860 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2864 if ( $section->{'description'} ) {
2865 push @buf, ( ['','-----------'],
2866 [ $section->{'description'}. ' sub-total',
2867 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2876 $invoice_data{current_less_finance} =
2877 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2879 if ( $multisection && !$conf->exists('disable_previous_balance')
2880 || $conf->exists('previous_balance-summary_only') )
2882 unshift @sections, $previous_section if $pr_total;
2885 warn "$me adding taxes\n"
2888 foreach my $tax ( $self->_items_tax ) {
2890 $taxtotal += $tax->{'amount'};
2892 my $description = &$escape_function( $tax->{'description'} );
2893 my $amount = sprintf( '%.2f', $tax->{'amount'} );
2895 if ( $multisection ) {
2897 my $money = $old_latex ? '' : $money_char;
2898 push @detail_items, {
2899 ext_description => [],
2902 description => $description,
2903 amount => $money. $amount,
2905 section => $tax_section,
2910 push @total_items, {
2911 'total_item' => $description,
2912 'total_amount' => $other_money_char. $amount,
2917 push @buf,[ $description,
2918 $money_char. $amount,
2925 $total->{'total_item'} = 'Sub-total';
2926 $total->{'total_amount'} =
2927 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2929 if ( $multisection ) {
2930 $tax_section->{'subtotal'} = $other_money_char.
2931 sprintf('%.2f', $taxtotal);
2932 $tax_section->{'pretotal'} = 'New charges sub-total '.
2933 $total->{'total_amount'};
2934 push @sections, $tax_section if $taxtotal;
2936 unshift @total_items, $total;
2939 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2941 push @buf,['','-----------'];
2942 push @buf,[( $conf->exists('disable_previous_balance')
2944 : 'Total New Charges'
2946 $money_char. sprintf("%10.2f",$self->charged) ];
2952 $item = $conf->config('previous_balance-exclude_from_total')
2953 || 'Total New Charges'
2954 if $conf->exists('previous_balance-exclude_from_total');
2955 my $amount = $self->charged +
2956 ( $conf->exists('disable_previous_balance') ||
2957 $conf->exists('previous_balance-exclude_from_total')
2961 $total->{'total_item'} = &$embolden_function($item);
2962 $total->{'total_amount'} =
2963 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
2964 if ( $multisection ) {
2965 if ( $adjust_section->{'sort_weight'} ) {
2966 $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
2967 sprintf("%.2f", ($self->billing_balance || 0) );
2969 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2970 sprintf('%.2f', $self->charged );
2973 push @total_items, $total;
2975 push @buf,['','-----------'];
2978 sprintf( '%10.2f', $amount )
2983 unless ( $conf->exists('disable_previous_balance') ) {
2984 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2987 my $credittotal = 0;
2988 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
2991 $total->{'total_item'} = &$escape_function($credit->{'description'});
2992 $credittotal += $credit->{'amount'};
2993 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2994 $adjusttotal += $credit->{'amount'};
2995 if ( $multisection ) {
2996 my $money = $old_latex ? '' : $money_char;
2997 push @detail_items, {
2998 ext_description => [],
3001 description => &$escape_function($credit->{'description'}),
3002 amount => $money. $credit->{'amount'},
3004 section => $adjust_section,
3007 push @total_items, $total;
3011 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3014 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3015 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3019 my $paymenttotal = 0;
3020 foreach my $payment ( $self->_items_payments ) {
3022 $total->{'total_item'} = &$escape_function($payment->{'description'});
3023 $paymenttotal += $payment->{'amount'};
3024 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3025 $adjusttotal += $payment->{'amount'};
3026 if ( $multisection ) {
3027 my $money = $old_latex ? '' : $money_char;
3028 push @detail_items, {
3029 ext_description => [],
3032 description => &$escape_function($payment->{'description'}),
3033 amount => $money. $payment->{'amount'},
3035 section => $adjust_section,
3038 push @total_items, $total;
3040 push @buf, [ $payment->{'description'},
3041 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3044 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3046 if ( $multisection ) {
3047 $adjust_section->{'subtotal'} = $other_money_char.
3048 sprintf('%.2f', $adjusttotal);
3049 push @sections, $adjust_section
3050 unless $adjust_section->{sort_weight};
3055 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3056 $total->{'total_amount'} =
3057 &$embolden_function(
3058 $other_money_char. sprintf('%.2f', $summarypage
3060 $self->billing_balance
3061 : $self->owed + $pr_total
3064 if ( $multisection && !$adjust_section->{sort_weight} ) {
3065 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3066 $total->{'total_amount'};
3068 push @total_items, $total;
3070 push @buf,['','-----------'];
3071 push @buf,[$self->balance_due_msg, $money_char.
3072 sprintf("%10.2f", $balance_due ) ];
3076 if ( $multisection ) {
3077 if ($conf->exists('svc_phone_sections')) {
3079 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3080 $total->{'total_amount'} =
3081 &$embolden_function(
3082 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3084 my $last_section = pop @sections;
3085 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3086 $total->{'total_amount'};
3087 push @sections, $last_section;
3089 push @sections, @$late_sections
3093 my @includelist = ();
3094 push @includelist, 'summary' if $summarypage;
3095 foreach my $include ( @includelist ) {
3097 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3100 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3102 @inc_src = $conf->config($inc_file, $agentnum);
3106 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3108 my $convert_map = $convert_maps{$format}{$include};
3110 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3111 s/--\@\]/$delimiters{$format}[1]/g;
3114 &$convert_map( $conf->config($inc_file, $agentnum) );
3118 my $inc_tt = new Text::Template (
3120 SOURCE => [ map "$_\n", @inc_src ],
3121 DELIMITERS => $delimiters{$format},
3122 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3124 unless ( $inc_tt->compile() ) {
3125 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3126 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3130 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3132 $invoice_data{$include} =~ s/\n+$//
3133 if ($format eq 'latex');
3138 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3139 /invoice_lines\((\d*)\)/;
3140 $invoice_lines += $1 || scalar(@buf);
3143 die "no invoice_lines() functions in template?"
3144 if ( $format eq 'template' && !$wasfunc );
3146 if ($format eq 'template') {
3148 if ( $invoice_lines ) {
3149 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3150 $invoice_data{'total_pages'}++
3151 if scalar(@buf) % $invoice_lines;
3154 #setup subroutine for the template
3155 sub FS::cust_bill::_template::invoice_lines {
3156 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
3158 scalar(@FS::cust_bill::_template::buf)
3159 ? shift @FS::cust_bill::_template::buf
3168 push @collect, split("\n",
3169 $text_template->fill_in( HASH => \%invoice_data,
3170 PACKAGE => 'FS::cust_bill::_template'
3173 $FS::cust_bill::_template::page++;
3175 map "$_\n", @collect;
3177 warn "filling in template for invoice ". $self->invnum. "\n"
3179 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3182 $text_template->fill_in(HASH => \%invoice_data);
3186 # helper routine for generating date ranges
3187 sub _prior_month30s {
3190 [ 1, 2592000 ], # 0-30 days ago
3191 [ 2592000, 5184000 ], # 30-60 days ago
3192 [ 5184000, 7776000 ], # 60-90 days ago
3193 [ 7776000, 0 ], # 90+ days ago
3196 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3197 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3202 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3204 Returns an postscript invoice, as a scalar.
3206 Options can be passed as a hashref (recommended) or as a list of time, template
3207 and then any key/value pairs for any other options.
3209 I<time> an optional value used to control the printing of overdue messages. The
3210 default is now. It isn't the date of the invoice; that's the `_date' field.
3211 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3212 L<Time::Local> and L<Date::Parse> for conversion functions.
3214 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3221 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3222 my $ps = generate_ps($file);
3224 unlink($barcodefile);
3229 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3231 Returns an PDF invoice, as a scalar.
3233 Options can be passed as a hashref (recommended) or as a list of time, template
3234 and then any key/value pairs for any other options.
3236 I<time> an optional value used to control the printing of overdue messages. The
3237 default is now. It isn't the date of the invoice; that's the `_date' field.
3238 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3239 L<Time::Local> and L<Date::Parse> for conversion functions.
3241 I<template>, if specified, is the name of a suffix for alternate invoices.
3243 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3250 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3251 my $pdf = generate_pdf($file);
3253 unlink($barcodefile);
3258 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3260 Returns an HTML invoice, as a scalar.
3262 I<time> an optional value used to control the printing of overdue messages. The
3263 default is now. It isn't the date of the invoice; that's the `_date' field.
3264 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3265 L<Time::Local> and L<Date::Parse> for conversion functions.
3267 I<template>, if specified, is the name of a suffix for alternate invoices.
3269 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3271 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3272 when emailing the invoice as part of a multipart/related MIME email.
3280 %params = %{ shift() };
3282 $params{'time'} = shift;
3283 $params{'template'} = shift;
3284 $params{'cid'} = shift;
3287 $params{'format'} = 'html';
3289 $self->print_generic( %params );
3292 # quick subroutine for print_latex
3294 # There are ten characters that LaTeX treats as special characters, which
3295 # means that they do not simply typeset themselves:
3296 # # $ % & ~ _ ^ \ { }
3298 # TeX ignores blanks following an escaped character; if you want a blank (as
3299 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3303 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3304 $value =~ s/([<>])/\$$1\$/g;
3310 encode_entities($value);
3314 sub _html_escape_nbsp {
3315 my $value = _html_escape(shift);
3316 $value =~ s/ +/ /g;
3320 #utility methods for print_*
3322 sub _translate_old_latex_format {
3323 warn "_translate_old_latex_format called\n"
3330 if ( $line =~ /^%%Detail\s*$/ ) {
3332 push @template, q![@--!,
3333 q! foreach my $_tr_line (@detail_items) {!,
3334 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3335 q! $_tr_line->{'description'} .= !,
3336 q! "\\tabularnewline\n~~".!,
3337 q! join( "\\tabularnewline\n~~",!,
3338 q! @{$_tr_line->{'ext_description'}}!,
3342 while ( ( my $line_item_line = shift )
3343 !~ /^%%EndDetail\s*$/ ) {
3344 $line_item_line =~ s/'/\\'/g; # nice LTS
3345 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3346 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3347 push @template, " \$OUT .= '$line_item_line';";
3350 push @template, '}',
3353 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3355 push @template, '[@--',
3356 ' foreach my $_tr_line (@total_items) {';
3358 while ( ( my $total_item_line = shift )
3359 !~ /^%%EndTotalDetails\s*$/ ) {
3360 $total_item_line =~ s/'/\\'/g; # nice LTS
3361 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3362 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3363 push @template, " \$OUT .= '$total_item_line';";
3366 push @template, '}',
3370 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3371 push @template, $line;
3377 warn "$_\n" foreach @template;
3386 #check for an invoice-specific override
3387 return $self->invoice_terms if $self->invoice_terms;
3389 #check for a customer- specific override
3390 my $cust_main = $self->cust_main;
3391 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3393 #use configured default
3394 $conf->config('invoice_default_terms') || '';
3400 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3401 $duedate = $self->_date() + ( $1 * 86400 );
3408 $self->due_date ? time2str(shift, $self->due_date) : '';
3411 sub balance_due_msg {
3413 my $msg = 'Balance Due';
3414 return $msg unless $self->terms;
3415 if ( $self->due_date ) {
3416 $msg .= ' - Please pay by '. $self->due_date2str($date_format);
3417 } elsif ( $self->terms ) {
3418 $msg .= ' - '. $self->terms;
3423 sub balance_due_date {
3426 if ( $conf->exists('invoice_default_terms')
3427 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3428 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3433 =item invnum_date_pretty
3435 Returns a string with the invoice number and date, for example:
3436 "Invoice #54 (3/20/2008)"
3440 sub invnum_date_pretty {
3442 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3447 Returns a string with the date, for example: "3/20/2008"
3453 time2str($date_format, $self->_date);
3456 use vars qw(%pkg_category_cache);
3457 sub _items_sections {
3460 my $summarypage = shift;
3462 my $extra_sections = shift;
3466 my %late_subtotal = ();
3469 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3472 my $usage = $cust_bill_pkg->usage;
3474 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3475 next if ( $display->summary && $summarypage );
3477 my $section = $display->section;
3478 my $type = $display->type;
3480 $not_tax{$section} = 1
3481 unless $cust_bill_pkg->pkgnum == 0;
3483 if ( $display->post_total && !$summarypage ) {
3484 if (! $type || $type eq 'S') {
3485 $late_subtotal{$section} += $cust_bill_pkg->setup
3486 if $cust_bill_pkg->setup != 0;
3490 $late_subtotal{$section} += $cust_bill_pkg->recur
3491 if $cust_bill_pkg->recur != 0;
3494 if ($type && $type eq 'R') {
3495 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3496 if $cust_bill_pkg->recur != 0;
3499 if ($type && $type eq 'U') {
3500 $late_subtotal{$section} += $usage
3501 unless scalar(@$extra_sections);
3506 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3508 if (! $type || $type eq 'S') {
3509 $subtotal{$section} += $cust_bill_pkg->setup
3510 if $cust_bill_pkg->setup != 0;
3514 $subtotal{$section} += $cust_bill_pkg->recur
3515 if $cust_bill_pkg->recur != 0;
3518 if ($type && $type eq 'R') {
3519 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3520 if $cust_bill_pkg->recur != 0;
3523 if ($type && $type eq 'U') {
3524 $subtotal{$section} += $usage
3525 unless scalar(@$extra_sections);
3534 %pkg_category_cache = ();
3536 push @$late, map { { 'description' => &{$escape}($_),
3537 'subtotal' => $late_subtotal{$_},
3539 'sort_weight' => ( _pkg_category($_)
3540 ? _pkg_category($_)->weight
3543 ((_pkg_category($_) && _pkg_category($_)->condense)
3544 ? $self->_condense_section($format)
3548 sort _sectionsort keys %late_subtotal;
3551 if ( $summarypage ) {
3552 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3553 map { $_->categoryname } qsearch('pkg_category', {});
3554 push @sections, '' if exists($subtotal{''});
3556 @sections = keys %subtotal;
3559 my @early = map { { 'description' => &{$escape}($_),
3560 'subtotal' => $subtotal{$_},
3561 'summarized' => $not_tax{$_} ? '' : 'Y',
3562 'tax_section' => $not_tax{$_} ? '' : 'Y',
3563 'sort_weight' => ( _pkg_category($_)
3564 ? _pkg_category($_)->weight
3567 ((_pkg_category($_) && _pkg_category($_)->condense)
3568 ? $self->_condense_section($format)
3573 push @early, @$extra_sections if $extra_sections;
3575 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3579 #helper subs for above
3582 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3586 my $categoryname = shift;
3587 $pkg_category_cache{$categoryname} ||=
3588 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3591 my %condensed_format = (
3592 'label' => [ qw( Description Qty Amount ) ],
3594 sub { shift->{description} },
3595 sub { shift->{quantity} },
3596 sub { my($href, %opt) = @_;
3597 ($opt{dollar} || ''). $href->{amount};
3600 'align' => [ qw( l r r ) ],
3601 'span' => [ qw( 5 1 1 ) ], # unitprices?
3602 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3605 sub _condense_section {
3606 my ( $self, $format ) = ( shift, shift );
3608 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3609 qw( description_generator
3612 total_line_generator
3617 sub _condensed_generator_defaults {
3618 my ( $self, $format ) = ( shift, shift );
3619 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3628 sub _condensed_header_generator {
3629 my ( $self, $format ) = ( shift, shift );
3631 my ( $f, $prefix, $suffix, $separator, $column ) =
3632 _condensed_generator_defaults($format);
3634 if ($format eq 'latex') {
3635 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3636 $suffix = "\\\\\n\\hline";
3639 sub { my ($d,$a,$s,$w) = @_;
3640 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3642 } elsif ( $format eq 'html' ) {
3643 $prefix = '<th></th>';
3647 sub { my ($d,$a,$s,$w) = @_;
3648 return qq!<th align="$html_align{$a}">$d</th>!;
3656 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3658 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3661 $prefix. join($separator, @result). $suffix;
3666 sub _condensed_description_generator {
3667 my ( $self, $format ) = ( shift, shift );
3669 my ( $f, $prefix, $suffix, $separator, $column ) =
3670 _condensed_generator_defaults($format);
3672 my $money_char = '$';
3673 if ($format eq 'latex') {
3674 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3676 $separator = " & \n";
3678 sub { my ($d,$a,$s,$w) = @_;
3679 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3681 $money_char = '\\dollar';
3682 }elsif ( $format eq 'html' ) {
3683 $prefix = '"><td align="center"></td>';
3687 sub { my ($d,$a,$s,$w) = @_;
3688 return qq!<td align="$html_align{$a}">$d</td>!;
3690 #$money_char = $conf->config('money_char') || '$';
3691 $money_char = ''; # this is madness
3699 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3701 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3703 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3704 map { $f->{$_}->[$i] } qw(align span width)
3708 $prefix. join( $separator, @result ). $suffix;
3713 sub _condensed_total_generator {
3714 my ( $self, $format ) = ( shift, shift );
3716 my ( $f, $prefix, $suffix, $separator, $column ) =
3717 _condensed_generator_defaults($format);
3720 if ($format eq 'latex') {
3723 $separator = " & \n";
3725 sub { my ($d,$a,$s,$w) = @_;
3726 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3728 }elsif ( $format eq 'html' ) {
3732 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3734 sub { my ($d,$a,$s,$w) = @_;
3735 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3744 # my $r = &{$f->{fields}->[$i]}(@args);
3745 # $r .= ' Total' unless $i;
3747 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3749 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3750 map { $f->{$_}->[$i] } qw(align span width)
3754 $prefix. join( $separator, @result ). $suffix;
3759 =item total_line_generator FORMAT
3761 Returns a coderef used for generation of invoice total line items for this
3762 usage_class. FORMAT is either html or latex
3766 # should not be used: will have issues with hash element names (description vs
3767 # total_item and amount vs total_amount -- another array of functions?
3769 sub _condensed_total_line_generator {
3770 my ( $self, $format ) = ( shift, shift );
3772 my ( $f, $prefix, $suffix, $separator, $column ) =
3773 _condensed_generator_defaults($format);
3776 if ($format eq 'latex') {
3779 $separator = " & \n";
3781 sub { my ($d,$a,$s,$w) = @_;
3782 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3784 }elsif ( $format eq 'html' ) {
3788 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3790 sub { my ($d,$a,$s,$w) = @_;
3791 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3800 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3802 &{$column}( &{$f->{fields}->[$i]}(@args),
3803 map { $f->{$_}->[$i] } qw(align span width)
3807 $prefix. join( $separator, @result ). $suffix;
3812 #sub _items_extra_usage_sections {
3814 # my $escape = shift;
3816 # my %sections = ();
3818 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
3819 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3821 # next unless $cust_bill_pkg->pkgnum > 0;
3823 # foreach my $section ( keys %usage_class ) {
3825 # my $usage = $cust_bill_pkg->usage($section);
3827 # next unless $usage && $usage > 0;
3829 # $sections{$section} ||= 0;
3830 # $sections{$section} += $usage;
3836 # map { { 'description' => &{$escape}($_),
3837 # 'subtotal' => $sections{$_},
3838 # 'summarized' => '',
3839 # 'tax_section' => '',
3842 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3846 sub _items_extra_usage_sections {
3855 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3856 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3857 next unless $cust_bill_pkg->pkgnum > 0;
3859 foreach my $classnum ( keys %usage_class ) {
3860 my $section = $usage_class{$classnum}->classname;
3861 $classnums{$section} = $classnum;
3863 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3864 my $amount = $detail->amount;
3865 next unless $amount && $amount > 0;
3867 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
3868 $sections{$section}{amount} += $amount; #subtotal
3869 $sections{$section}{calls}++;
3870 $sections{$section}{duration} += $detail->duration;
3872 my $desc = $detail->regionname;
3873 my $description = $desc;
3874 $description = substr($desc, 0, 50). '...'
3875 if $format eq 'latex' && length($desc) > 50;
3877 $lines{$section}{$desc} ||= {
3878 description => &{$escape}($description),
3879 #pkgpart => $part_pkg->pkgpart,
3880 pkgnum => $cust_bill_pkg->pkgnum,
3885 #unit_amount => $cust_bill_pkg->unitrecur,
3886 quantity => $cust_bill_pkg->quantity,
3887 product_code => 'N/A',
3888 ext_description => [],
3891 $lines{$section}{$desc}{amount} += $amount;
3892 $lines{$section}{$desc}{calls}++;
3893 $lines{$section}{$desc}{duration} += $detail->duration;
3899 my %sectionmap = ();
3900 foreach (keys %sections) {
3901 my $usage_class = $usage_class{$classnums{$_}};
3902 $sectionmap{$_} = { 'description' => &{$escape}($_),
3903 'amount' => $sections{$_}{amount}, #subtotal
3904 'calls' => $sections{$_}{calls},
3905 'duration' => $sections{$_}{duration},
3907 'tax_section' => '',
3908 'sort_weight' => $usage_class->weight,
3909 ( $usage_class->format
3910 ? ( map { $_ => $usage_class->$_($format) }
3911 qw( description_generator header_generator total_generator total_line_generator )
3918 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
3922 foreach my $section ( keys %lines ) {
3923 foreach my $line ( keys %{$lines{$section}} ) {
3924 my $l = $lines{$section}{$line};
3925 $l->{section} = $sectionmap{$section};
3926 $l->{amount} = sprintf( "%.2f", $l->{amount} );
3927 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3932 return(\@sections, \@lines);
3938 my $end = $self->_date;
3939 my $start = $end - 2592000; # 30 days
3940 my $cust_main = $self->cust_main;
3941 my @pkgs = $cust_main->all_pkgs;
3942 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
3945 foreach my $pkg ( @pkgs ) {
3946 my @h_cust_svc = $pkg->h_cust_svc($end);
3947 foreach my $h_cust_svc ( @h_cust_svc ) {
3948 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
3949 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
3951 my $inserted = $h_cust_svc->date_inserted;
3952 my $deleted = $h_cust_svc->date_deleted;
3953 my $phone_inserted = $h_cust_svc->h_svc_x($inserted);
3955 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
3957 # DID either activated or ported in; cannot be both for same DID simultaneously
3958 if ($inserted >= $start && $inserted <= $end && $phone_inserted
3959 && (!$phone_inserted->lnp_status
3960 || $phone_inserted->lnp_status eq ''
3961 || $phone_inserted->lnp_status eq 'native')) {
3964 else { # this one not so clean, should probably move to (h_)svc_phone
3965 my $phone_portedin = qsearchs( 'h_svc_phone',
3966 { 'svcnum' => $h_cust_svc->svcnum,
3967 'lnp_status' => 'portedin' },
3968 FS::h_svc_phone->sql_h_searchs($end),
3970 $num_portedin++ if $phone_portedin;
3973 # DID either deactivated or ported out; cannot be both for same DID simultaneously
3974 if($deleted >= $start && $deleted <= $end && $phone_deleted
3975 && (!$phone_deleted->lnp_status
3976 || $phone_deleted->lnp_status ne 'portingout')) {
3979 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
3980 && $phone_deleted->lnp_status
3981 && $phone_deleted->lnp_status eq 'portingout') {
3985 # increment usage minutes
3986 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end);
3987 foreach my $cdr ( @cdrs ) {
3988 $minutes += $cdr->billsec/60;
3991 # don't look at this service again
3992 push @seen, $h_cust_svc->svcnum;
3996 $minutes = sprintf("%d", $minutes);
3997 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
3998 . "$num_deactivated Ported-Out: $num_portedout ",
3999 "Total Minutes: $minutes");
4002 sub _items_svc_phone_sections {
4011 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4012 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4014 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4015 next unless $cust_bill_pkg->pkgnum > 0;
4017 my @header = $cust_bill_pkg->details_header;
4018 next unless scalar(@header);
4020 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4022 my $phonenum = $detail->phonenum;
4023 next unless $phonenum;
4025 my $amount = $detail->amount;
4026 next unless $amount && $amount > 0;
4028 $sections{$phonenum} ||= { 'amount' => 0,
4031 'sort_weight' => -1,
4032 'phonenum' => $phonenum,
4034 $sections{$phonenum}{amount} += $amount; #subtotal
4035 $sections{$phonenum}{calls}++;
4036 $sections{$phonenum}{duration} += $detail->duration;
4038 my $desc = $detail->regionname;
4039 my $description = $desc;
4040 $description = substr($desc, 0, 50). '...'
4041 if $format eq 'latex' && length($desc) > 50;
4043 $lines{$phonenum}{$desc} ||= {
4044 description => &{$escape}($description),
4045 #pkgpart => $part_pkg->pkgpart,
4053 product_code => 'N/A',
4054 ext_description => [],
4057 $lines{$phonenum}{$desc}{amount} += $amount;
4058 $lines{$phonenum}{$desc}{calls}++;
4059 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4061 my $line = $usage_class{$detail->classnum}->classname;
4062 $sections{"$phonenum $line"} ||=
4066 'sort_weight' => $usage_class{$detail->classnum}->weight,
4067 'phonenum' => $phonenum,
4068 'header' => [ @header ],
4070 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4071 $sections{"$phonenum $line"}{calls}++;
4072 $sections{"$phonenum $line"}{duration} += $detail->duration;
4074 $lines{"$phonenum $line"}{$desc} ||= {
4075 description => &{$escape}($description),
4076 #pkgpart => $part_pkg->pkgpart,
4084 product_code => 'N/A',
4085 ext_description => [],
4088 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4089 $lines{"$phonenum $line"}{$desc}{calls}++;
4090 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4091 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4092 $detail->formatted('format' => $format);
4097 my %sectionmap = ();
4098 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4099 foreach ( keys %sections ) {
4100 my @header = @{ $sections{$_}{header} || [] };
4102 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4103 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4104 my $usage_class = $summary ? $simple : $usage_simple;
4105 my $ending = $summary ? ' usage charges' : '';
4108 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4110 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4111 'amount' => $sections{$_}{amount}, #subtotal
4112 'calls' => $sections{$_}{calls},
4113 'duration' => $sections{$_}{duration},
4115 'tax_section' => '',
4116 'phonenum' => $sections{$_}{phonenum},
4117 'sort_weight' => $sections{$_}{sort_weight},
4118 'post_total' => $summary, #inspire pagebreak
4120 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4121 qw( description_generator
4124 total_line_generator
4131 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4132 $a->{sort_weight} <=> $b->{sort_weight}
4137 foreach my $section ( keys %lines ) {
4138 foreach my $line ( keys %{$lines{$section}} ) {
4139 my $l = $lines{$section}{$line};
4140 $l->{section} = $sectionmap{$section};
4141 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4142 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4147 return(\@sections, \@lines);
4154 #my @display = scalar(@_)
4156 # : qw( _items_previous _items_pkg );
4157 # #: qw( _items_pkg );
4158 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4159 my @display = qw( _items_previous _items_pkg );
4162 foreach my $display ( @display ) {
4163 push @b, $self->$display(@_);
4168 sub _items_previous {
4170 my $cust_main = $self->cust_main;
4171 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4173 foreach ( @pr_cust_bill ) {
4174 my $date = $conf->exists('invoice_show_prior_due_date')
4175 ? 'due '. $_->due_date2str($date_format)
4176 : time2str($date_format, $_->_date);
4178 'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
4179 #'pkgpart' => 'N/A',
4181 'amount' => sprintf("%.2f", $_->owed),
4187 # 'description' => 'Previous Balance',
4188 # #'pkgpart' => 'N/A',
4189 # 'pkgnum' => 'N/A',
4190 # 'amount' => sprintf("%10.2f", $pr_total ),
4191 # 'ext_description' => [ map {
4192 # "Invoice ". $_->invnum.
4193 # " (". time2str("%x",$_->_date). ") ".
4194 # sprintf("%10.2f", $_->owed)
4195 # } @pr_cust_bill ],
4204 warn "$me _items_pkg searching for all package line items\n"
4207 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4209 warn "$me _items_pkg filtering line items\n"
4211 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4213 if ($options{section} && $options{section}->{condensed}) {
4215 warn "$me _items_pkg condensing section\n"
4219 local $Storable::canonical = 1;
4220 foreach ( @items ) {
4222 delete $item->{ref};
4223 delete $item->{ext_description};
4224 my $key = freeze($item);
4225 $itemshash{$key} ||= 0;
4226 $itemshash{$key} ++; # += $item->{quantity};
4228 @items = sort { $a->{description} cmp $b->{description} }
4229 map { my $i = thaw($_);
4230 $i->{quantity} = $itemshash{$_};
4232 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4238 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4245 return 0 unless $a->itemdesc cmp $b->itemdesc;
4246 return -1 if $b->itemdesc eq 'Tax';
4247 return 1 if $a->itemdesc eq 'Tax';
4248 return -1 if $b->itemdesc eq 'Other surcharges';
4249 return 1 if $a->itemdesc eq 'Other surcharges';
4250 $a->itemdesc cmp $b->itemdesc;
4255 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4256 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4259 sub _items_cust_bill_pkg {
4261 my $cust_bill_pkgs = shift;
4264 my $format = $opt{format} || '';
4265 my $escape_function = $opt{escape_function} || sub { shift };
4266 my $format_function = $opt{format_function} || '';
4267 my $unsquelched = $opt{unsquelched} || '';
4268 my $section = $opt{section}->{description} if $opt{section};
4269 my $summary_page = $opt{summary_page} || '';
4270 my $multilocation = $opt{multilocation} || '';
4271 my $multisection = $opt{multisection} || '';
4272 my $discount_show_always = 0;
4275 my ($s, $r, $u) = ( undef, undef, undef );
4276 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4279 warn "$me _items_cust_bill_pkg considering cust_bill_pkg $cust_bill_pkg\n"
4282 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
4283 && $conf->exists('discount-show-always'));
4285 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4286 if ( $_ && !$cust_bill_pkg->hidden ) {
4287 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4288 $_->{amount} =~ s/^\-0\.00$/0.00/;
4289 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4291 unless ( $_->{amount} == 0 && !$discount_show_always );
4296 foreach my $display ( grep { defined($section)
4297 ? $_->section eq $section
4300 #grep { !$_->summary || !$summary_page } # bunk!
4301 grep { !$_->summary || $multisection }
4302 $cust_bill_pkg->cust_bill_pkg_display
4306 warn "$me _items_cust_bill_pkg considering display item $display\n"
4309 my $type = $display->type;
4311 my $desc = $cust_bill_pkg->desc;
4312 $desc = substr($desc, 0, 50). '...'
4313 if $format eq 'latex' && length($desc) > 50;
4315 my %details_opt = ( 'format' => $format,
4316 'escape_function' => $escape_function,
4317 'format_function' => $format_function,
4320 if ( $cust_bill_pkg->pkgnum > 0 ) {
4322 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4325 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4327 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
4329 warn "$me _items_cust_bill_pkg adding setup\n"
4332 my $description = $desc;
4333 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
4336 unless ( $cust_pkg->part_pkg->hide_svc_detail
4337 || $cust_bill_pkg->hidden )
4340 push @d, map &{$escape_function}($_),
4341 $cust_pkg->h_labels_short($self->_date, undef, 'I')
4342 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4344 if ( $multilocation ) {
4345 my $loc = $cust_pkg->location_label;
4346 $loc = substr($loc, 0, 50). '...'
4347 if $format eq 'latex' && length($loc) > 50;
4348 push @d, &{$escape_function}($loc);
4353 push @d, $cust_bill_pkg->details(%details_opt)
4354 if $cust_bill_pkg->recur == 0;
4356 if ( $cust_bill_pkg->hidden ) {
4357 $s->{amount} += $cust_bill_pkg->setup;
4358 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4359 push @{ $s->{ext_description} }, @d;
4362 description => $description,
4363 #pkgpart => $part_pkg->pkgpart,
4364 pkgnum => $cust_bill_pkg->pkgnum,
4365 amount => $cust_bill_pkg->setup,
4366 unit_amount => $cust_bill_pkg->unitsetup,
4367 quantity => $cust_bill_pkg->quantity,
4368 ext_description => \@d,
4374 if ( ( $cust_bill_pkg->recur != 0 || $cust_bill_pkg->setup == 0 ||
4375 ($discount_show_always && $cust_bill_pkg->recur == 0) ) &&
4376 ( !$type || $type eq 'R' || $type eq 'U' )
4380 warn "$me _items_cust_bill_pkg adding recur/usage\n"
4383 my $is_summary = $display->summary;
4384 my $description = ($is_summary && $type && $type eq 'U')
4385 ? "Usage charges" : $desc;
4387 $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4388 " - ". time2str($date_format, $cust_bill_pkg->edate).
4390 unless $conf->exists('disable_line_item_date_ranges');
4394 #at least until cust_bill_pkg has "past" ranges in addition to
4395 #the "future" sdate/edate ones... see #3032
4396 my @dates = ( $self->_date );
4397 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4398 push @dates, $prev->sdate if $prev;
4399 push @dates, undef if !$prev;
4401 unless ( $cust_pkg->part_pkg->hide_svc_detail
4402 || $cust_bill_pkg->itemdesc
4403 || $cust_bill_pkg->hidden
4404 || $is_summary && $type && $type eq 'U' )
4407 warn "$me _items_cust_bill_pkg adding service details\n"
4410 push @d, map &{$escape_function}($_),
4411 $cust_pkg->h_labels_short(@dates, 'I')
4412 #$cust_bill_pkg->edate,
4413 #$cust_bill_pkg->sdate)
4414 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4416 warn "$me _items_cust_bill_pkg done adding service details\n"
4419 if ( $multilocation ) {
4420 my $loc = $cust_pkg->location_label;
4421 $loc = substr($loc, 0, 50). '...'
4422 if $format eq 'latex' && length($loc) > 50;
4423 push @d, &{$escape_function}($loc);
4428 warn "$me _items_cust_bill_pkg adding details\n"
4431 push @d, $cust_bill_pkg->details(%details_opt)
4432 unless ($is_summary || $type && $type eq 'R');
4434 warn "$me _items_cust_bill_pkg calculating amount\n"
4439 $amount = $cust_bill_pkg->recur;
4440 }elsif($type eq 'R') {
4441 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4442 }elsif($type eq 'U') {
4443 $amount = $cust_bill_pkg->usage;
4446 if ( !$type || $type eq 'R' ) {
4448 warn "$me _items_cust_bill_pkg adding recur\n"
4451 if ( $cust_bill_pkg->hidden ) {
4452 $r->{amount} += $amount;
4453 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4454 push @{ $r->{ext_description} }, @d;
4457 description => $description,
4458 #pkgpart => $part_pkg->pkgpart,
4459 pkgnum => $cust_bill_pkg->pkgnum,
4461 unit_amount => $cust_bill_pkg->unitrecur,
4462 quantity => $cust_bill_pkg->quantity,
4463 ext_description => \@d,
4467 } else { # $type eq 'U'
4469 warn "$me _items_cust_bill_pkg adding usage\n"
4472 if ( $cust_bill_pkg->hidden ) {
4473 $u->{amount} += $amount;
4474 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4475 push @{ $u->{ext_description} }, @d;
4478 description => $description,
4479 #pkgpart => $part_pkg->pkgpart,
4480 pkgnum => $cust_bill_pkg->pkgnum,
4482 unit_amount => $cust_bill_pkg->unitrecur,
4483 quantity => $cust_bill_pkg->quantity,
4484 ext_description => \@d,
4490 } # recurring or usage with recurring charge
4492 } else { #pkgnum tax or one-shot line item (??)
4494 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
4497 if ( $cust_bill_pkg->setup != 0 ) {
4499 'description' => $desc,
4500 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
4503 if ( $cust_bill_pkg->recur != 0 ) {
4505 'description' => "$desc (".
4506 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
4507 time2str($date_format, $cust_bill_pkg->edate). ')',
4508 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
4518 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
4521 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4523 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4524 $_->{amount} =~ s/^\-0\.00$/0.00/;
4525 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4527 unless ( $_->{amount} == 0 && !$discount_show_always );
4535 sub _items_credits {
4536 my( $self, %opt ) = @_;
4537 my $trim_len = $opt{'trim_len'} || 60;
4541 foreach ( $self->cust_credited ) {
4543 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4545 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4546 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4547 $reason = " ($reason) " if $reason;
4550 #'description' => 'Credit ref\#'. $_->crednum.
4551 # " (". time2str("%x",$_->cust_credit->_date) .")".
4553 'description' => 'Credit applied '.
4554 time2str($date_format,$_->cust_credit->_date). $reason,
4555 'amount' => sprintf("%.2f",$_->amount),
4563 sub _items_payments {
4567 #get & print payments
4568 foreach ( $self->cust_bill_pay ) {
4570 #something more elaborate if $_->amount ne ->cust_pay->paid ?
4573 'description' => "Payment received ".
4574 time2str($date_format,$_->cust_pay->_date ),
4575 'amount' => sprintf("%.2f", $_->amount )
4583 =item call_details [ OPTION => VALUE ... ]
4585 Returns an array of CSV strings representing the call details for this invoice
4586 The only option available is the boolean prepend_billed_number
4591 my ($self, %opt) = @_;
4593 my $format_function = sub { shift };
4595 if ($opt{prepend_billed_number}) {
4596 $format_function = sub {
4600 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4605 my @details = map { $_->details( 'format_function' => $format_function,
4606 'escape_function' => sub{ return() },
4610 $self->cust_bill_pkg;
4611 my $header = $details[0];
4612 ( $header, grep { $_ ne $header } @details );
4622 =item process_reprint
4626 sub process_reprint {
4627 process_re_X('print', @_);
4630 =item process_reemail
4634 sub process_reemail {
4635 process_re_X('email', @_);
4643 process_re_X('fax', @_);
4651 process_re_X('ftp', @_);
4658 sub process_respool {
4659 process_re_X('spool', @_);
4662 use Storable qw(thaw);
4666 my( $method, $job ) = ( shift, shift );
4667 warn "$me process_re_X $method for job $job\n" if $DEBUG;
4669 my $param = thaw(decode_base64(shift));
4670 warn Dumper($param) if $DEBUG;
4681 my($method, $job, %param ) = @_;
4683 warn "re_X $method for job $job with param:\n".
4684 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
4687 #some false laziness w/search/cust_bill.html
4689 my $orderby = 'ORDER BY cust_bill._date';
4691 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
4693 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4695 my @cust_bill = qsearch( {
4696 #'select' => "cust_bill.*",
4697 'table' => 'cust_bill',
4698 'addl_from' => $addl_from,
4700 'extra_sql' => $extra_sql,
4701 'order_by' => $orderby,
4705 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4707 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
4710 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
4711 foreach my $cust_bill ( @cust_bill ) {
4712 $cust_bill->$method();
4714 if ( $job ) { #progressbar foo
4716 if ( time - $min_sec > $last ) {
4717 my $error = $job->update_statustext(
4718 int( 100 * $num / scalar(@cust_bill) )
4720 die $error if $error;
4731 =head1 CLASS METHODS
4737 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
4742 my ($class, $start, $end) = @_;
4744 $class->paid_sql($start, $end). ' - '.
4745 $class->credited_sql($start, $end);
4750 Returns an SQL fragment to retreive the net amount (charged minus credited).
4755 my ($class, $start, $end) = @_;
4756 'charged - '. $class->credited_sql($start, $end);
4761 Returns an SQL fragment to retreive the amount paid against this invoice.
4766 my ($class, $start, $end) = @_;
4767 $start &&= "AND cust_bill_pay._date <= $start";
4768 $end &&= "AND cust_bill_pay._date > $end";
4769 $start = '' unless defined($start);
4770 $end = '' unless defined($end);
4771 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
4772 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
4777 Returns an SQL fragment to retreive the amount credited against this invoice.
4782 my ($class, $start, $end) = @_;
4783 $start &&= "AND cust_credit_bill._date <= $start";
4784 $end &&= "AND cust_credit_bill._date > $end";
4785 $start = '' unless defined($start);
4786 $end = '' unless defined($end);
4787 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
4788 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
4793 Returns an SQL fragment to retrieve the due date of an invoice.
4794 Currently only supported on PostgreSQL.
4802 cust_bill.invoice_terms,
4803 cust_main.invoice_terms,
4804 \''.($conf->config('invoice_default_terms') || '').'\'
4805 ), E\'Net (\\\\d+)\'
4807 ) * 86400 + cust_bill._date'
4810 =item search_sql_where HASHREF
4812 Class method which returns an SQL WHERE fragment to search for parameters
4813 specified in HASHREF. Valid parameters are
4819 List reference of start date, end date, as UNIX timestamps.
4829 List reference of charged limits (exclusive).
4833 List reference of charged limits (exclusive).
4837 flag, return open invoices only
4841 flag, return net invoices only
4845 =item newest_percust
4849 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
4853 sub search_sql_where {
4854 my($class, $param) = @_;
4856 warn "$me search_sql_where called with params: \n".
4857 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
4863 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
4864 push @search, "cust_main.agentnum = $1";
4868 if ( $param->{_date} ) {
4869 my($beginning, $ending) = @{$param->{_date}};
4871 push @search, "cust_bill._date >= $beginning",
4872 "cust_bill._date < $ending";
4876 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
4877 push @search, "cust_bill.invnum >= $1";
4879 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
4880 push @search, "cust_bill.invnum <= $1";
4884 if ( $param->{charged} ) {
4885 my @charged = ref($param->{charged})
4886 ? @{ $param->{charged} }
4887 : ($param->{charged});
4889 push @search, map { s/^charged/cust_bill.charged/; $_; }
4893 my $owed_sql = FS::cust_bill->owed_sql;
4896 if ( $param->{owed} ) {
4897 my @owed = ref($param->{owed})
4898 ? @{ $param->{owed} }
4900 push @search, map { s/^owed/$owed_sql/; $_; }
4905 push @search, "0 != $owed_sql"
4906 if $param->{'open'};
4907 push @search, '0 != '. FS::cust_bill->net_sql
4911 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
4912 if $param->{'days'};
4915 if ( $param->{'newest_percust'} ) {
4917 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
4918 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
4920 my @newest_where = map { my $x = $_;
4921 $x =~ s/\bcust_bill\./newest_cust_bill./g;
4924 grep ! /^cust_main./, @search;
4925 my $newest_where = scalar(@newest_where)
4926 ? ' AND '. join(' AND ', @newest_where)
4930 push @search, "cust_bill._date = (
4931 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
4932 WHERE newest_cust_bill.custnum = cust_bill.custnum
4938 #agent virtualization
4939 my $curuser = $FS::CurrentUser::CurrentUser;
4940 if ( $curuser->username eq 'fs_queue'
4941 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
4943 my $newuser = qsearchs('access_user', {
4944 'username' => $username,
4948 $curuser = $newuser;
4950 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
4953 push @search, $curuser->agentnums_sql;
4955 join(' AND ', @search );
4967 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
4968 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base