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>",
964 $alternative->attach(
965 'Type' => 'text/html',
966 'Encoding' => 'quoted-printable',
967 'Data' => [ '<html>',
970 ' '. encode_entities($return{'subject'}),
973 ' <body bgcolor="#e8e8e8">',
974 $self->print_html({ 'cid'=>$content_id, %opt }),
978 'Disposition' => 'inline',
979 #'Filename' => 'invoice.pdf',
983 if ( $cust_main->email_csv_cdr ) {
985 push @otherparts, build MIME::Entity
986 'Type' => 'text/csv',
987 'Encoding' => '7bit',
988 'Data' => [ map { "$_\n" }
989 $self->call_details('prepend_billed_number' => 1)
991 'Disposition' => 'attachment',
992 'Filename' => 'usage-'. $self->invnum. '.csv',
997 if ( $conf->exists('invoice_email_pdf') ) {
1002 # multipart/alternative
1008 my $related = build MIME::Entity 'Type' => 'multipart/related',
1009 'Encoding' => '7bit';
1011 #false laziness w/Misc::send_email
1012 $related->head->replace('Content-type',
1013 $related->mime_type.
1014 '; boundary="'. $related->head->multipart_boundary. '"'.
1015 '; type=multipart/alternative'
1018 $related->add_part($alternative);
1020 $related->add_part($image);
1022 my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
1024 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
1028 #no other attachment:
1030 # multipart/alternative
1035 $return{'content-type'} = 'multipart/related';
1036 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
1037 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
1038 #$return{'disposition'} = 'inline';
1044 if ( $conf->exists('invoice_email_pdf') ) {
1045 warn "$me creating PDF attachment"
1048 #mime parts arguments a la MIME::Entity->build().
1049 $return{'mimeparts'} = [
1050 { $self->mimebuild_pdf(\%opt) }
1054 if ( $conf->exists('invoice_email_pdf')
1055 and scalar($conf->config('invoice_email_pdf_note')) ) {
1057 warn "$me using 'invoice_email_pdf_note'"
1059 $return{'body'} = [ map { $_ . "\n" }
1060 $conf->config('invoice_email_pdf_note')
1065 warn "$me not using 'invoice_email_pdf_note'"
1067 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1068 $return{'body'} = $args{'print_text'};
1070 $return{'body'} = [ $self->print_text(\%opt) ];
1083 Returns a list suitable for passing to MIME::Entity->build(), representing
1084 this invoice as PDF attachment.
1091 'Type' => 'application/pdf',
1092 'Encoding' => 'base64',
1093 'Data' => [ $self->print_pdf(@_) ],
1094 'Disposition' => 'attachment',
1095 'Filename' => 'invoice-'. $self->invnum. '.pdf',
1099 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1101 Sends this invoice to the destinations configured for this customer: sends
1102 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1104 Options can be passed as a hashref (recommended) or as a list of up to
1105 four values for templatename, agentnum, invoice_from and amount.
1107 I<template>, if specified, is the name of a suffix for alternate invoices.
1109 I<agentnum>, if specified, means that this invoice will only be sent for customers
1110 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
1111 single agent) or an arrayref of agentnums.
1113 I<invoice_from>, if specified, overrides the default email invoice From: address.
1115 I<amount>, if specified, only sends the invoice if the total amount owed on this
1116 invoice and all older invoices is greater than the specified amount.
1118 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1122 sub queueable_send {
1125 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1126 or die "invalid invoice number: " . $opt{invnum};
1128 my @args = ( $opt{template}, $opt{agentnum} );
1129 push @args, $opt{invoice_from}
1130 if exists($opt{invoice_from}) && $opt{invoice_from};
1132 my $error = $self->send( @args );
1133 die $error if $error;
1140 my( $template, $invoice_from, $notice_name );
1142 my $balance_over = 0;
1146 $template = $opt->{'template'} || '';
1147 if ( $agentnums = $opt->{'agentnum'} ) {
1148 $agentnums = [ $agentnums ] unless ref($agentnums);
1150 $invoice_from = $opt->{'invoice_from'};
1151 $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1152 $notice_name = $opt->{'notice_name'};
1154 $template = scalar(@_) ? shift : '';
1155 if ( scalar(@_) && $_[0] ) {
1156 $agentnums = ref($_[0]) ? shift : [ shift ];
1158 $invoice_from = shift if scalar(@_);
1159 $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1162 return 'N/A' unless ! $agentnums
1163 or grep { $_ == $self->cust_main->agentnum } @$agentnums;
1166 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
1168 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1169 $conf->config('invoice_from', $self->cust_main->agentnum );
1172 'template' => $template,
1173 'invoice_from' => $invoice_from,
1174 'notice_name' => ( $notice_name || 'Invoice' ),
1177 my @invoicing_list = $self->cust_main->invoicing_list;
1179 #$self->email_invoice(\%opt)
1181 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
1183 #$self->print_invoice(\%opt)
1185 if grep { $_ eq 'POST' } @invoicing_list; #postal
1187 $self->fax_invoice(\%opt)
1188 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1194 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
1196 Emails this invoice.
1198 Options can be passed as a hashref (recommended) or as a list of up to
1199 two values for templatename and invoice_from.
1201 I<template>, if specified, is the name of a suffix for alternate invoices.
1203 I<invoice_from>, if specified, overrides the default email invoice From: address.
1205 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1209 sub queueable_email {
1212 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1213 or die "invalid invoice number: " . $opt{invnum};
1215 my @args = ( $opt{template} );
1216 push @args, $opt{invoice_from}
1217 if exists($opt{invoice_from}) && $opt{invoice_from};
1219 my $error = $self->email( @args );
1220 die $error if $error;
1224 #sub email_invoice {
1228 my( $template, $invoice_from, $notice_name );
1231 $template = $opt->{'template'} || '';
1232 $invoice_from = $opt->{'invoice_from'};
1233 $notice_name = $opt->{'notice_name'} || 'Invoice';
1235 $template = scalar(@_) ? shift : '';
1236 $invoice_from = shift if scalar(@_);
1237 $notice_name = 'Invoice';
1240 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1241 $conf->config('invoice_from', $self->cust_main->agentnum );
1243 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1244 $self->cust_main->invoicing_list;
1246 if ( ! @invoicing_list ) { #no recipients
1247 if ( $conf->exists('cust_bill-no_recipients-error') ) {
1248 die 'No recipients for customer #'. $self->custnum;
1250 #default: better to notify this person than silence
1251 @invoicing_list = ($invoice_from);
1255 my $subject = $self->email_subject($template);
1257 my $error = send_email(
1258 $self->generate_email(
1259 'from' => $invoice_from,
1260 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1261 'subject' => $subject,
1262 'template' => $template,
1263 'notice_name' => $notice_name,
1266 die "can't email invoice: $error\n" if $error;
1267 #die "$error\n" if $error;
1274 #my $template = scalar(@_) ? shift : '';
1277 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1280 my $cust_main = $self->cust_main;
1281 my $name = $cust_main->name;
1282 my $name_short = $cust_main->name_short;
1283 my $invoice_number = $self->invnum;
1284 my $invoice_date = $self->_date_pretty;
1286 eval qq("$subject");
1289 =item lpr_data HASHREF | [ TEMPLATE ]
1291 Returns the postscript or plaintext for this invoice as an arrayref.
1293 Options can be passed as a hashref (recommended) or as a single optional value
1296 I<template>, if specified, is the name of a suffix for alternate invoices.
1298 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1304 my( $template, $notice_name );
1307 $template = $opt->{'template'} || '';
1308 $notice_name = $opt->{'notice_name'} || 'Invoice';
1310 $template = scalar(@_) ? shift : '';
1311 $notice_name = 'Invoice';
1315 'template' => $template,
1316 'notice_name' => $notice_name,
1319 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1320 [ $self->$method( \%opt ) ];
1323 =item print HASHREF | [ TEMPLATE ]
1325 Prints this invoice.
1327 Options can be passed as a hashref (recommended) or as a single optional
1330 I<template>, if specified, is the name of a suffix for alternate invoices.
1332 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1336 #sub print_invoice {
1339 my( $template, $notice_name );
1342 $template = $opt->{'template'} || '';
1343 $notice_name = $opt->{'notice_name'} || 'Invoice';
1345 $template = scalar(@_) ? shift : '';
1346 $notice_name = 'Invoice';
1350 'template' => $template,
1351 'notice_name' => $notice_name,
1354 if($conf->exists('invoice_print_pdf')) {
1355 # Add the invoice to the current batch.
1356 $self->batch_invoice(\%opt);
1359 do_print $self->lpr_data(\%opt);
1363 =item fax_invoice HASHREF | [ TEMPLATE ]
1367 Options can be passed as a hashref (recommended) or as a single optional
1370 I<template>, if specified, is the name of a suffix for alternate invoices.
1372 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1378 my( $template, $notice_name );
1381 $template = $opt->{'template'} || '';
1382 $notice_name = $opt->{'notice_name'} || 'Invoice';
1384 $template = scalar(@_) ? shift : '';
1385 $notice_name = 'Invoice';
1388 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1389 unless $conf->exists('invoice_latex');
1391 my $dialstring = $self->cust_main->getfield('fax');
1395 'template' => $template,
1396 'notice_name' => $notice_name,
1399 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1400 'dialstring' => $dialstring,
1402 die $error if $error;
1406 =item batch_invoice [ HASHREF ]
1408 Place this invoice into the open batch (see C<FS::bill_batch>). If there
1409 isn't an open batch, one will be created.
1414 my ($self, $opt) = @_;
1415 my $batch = FS::bill_batch->get_open_batch;
1416 my $cust_bill_batch = FS::cust_bill_batch->new({
1417 batchnum => $batch->batchnum,
1418 invnum => $self->invnum,
1420 return $cust_bill_batch->insert($opt);
1423 =item ftp_invoice [ TEMPLATENAME ]
1425 Sends this invoice data via FTP.
1427 TEMPLATENAME is unused?
1433 my $template = scalar(@_) ? shift : '';
1436 'protocol' => 'ftp',
1437 'server' => $conf->config('cust_bill-ftpserver'),
1438 'username' => $conf->config('cust_bill-ftpusername'),
1439 'password' => $conf->config('cust_bill-ftppassword'),
1440 'dir' => $conf->config('cust_bill-ftpdir'),
1441 'format' => $conf->config('cust_bill-ftpformat'),
1445 =item spool_invoice [ TEMPLATENAME ]
1447 Spools this invoice data (see L<FS::spool_csv>)
1449 TEMPLATENAME is unused?
1455 my $template = scalar(@_) ? shift : '';
1458 'format' => $conf->config('cust_bill-spoolformat'),
1459 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1463 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1465 Like B<send>, but only sends the invoice if it is the newest open invoice for
1470 sub send_if_newest {
1475 grep { $_->owed > 0 }
1476 qsearch('cust_bill', {
1477 'custnum' => $self->custnum,
1478 #'_date' => { op=>'>', value=>$self->_date },
1479 'invnum' => { op=>'>', value=>$self->invnum },
1486 =item send_csv OPTION => VALUE, ...
1488 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1492 protocol - currently only "ftp"
1498 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1499 and YYMMDDHHMMSS is a timestamp.
1501 See L</print_csv> for a description of the output format.
1506 my($self, %opt) = @_;
1510 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1511 mkdir $spooldir, 0700 unless -d $spooldir;
1513 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1514 my $file = "$spooldir/$tracctnum.csv";
1516 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1518 open(CSV, ">$file") or die "can't open $file: $!";
1526 if ( $opt{protocol} eq 'ftp' ) {
1527 eval "use Net::FTP;";
1529 $net = Net::FTP->new($opt{server}) or die @$;
1531 die "unknown protocol: $opt{protocol}";
1534 $net->login( $opt{username}, $opt{password} )
1535 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1537 $net->binary or die "can't set binary mode";
1539 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1541 $net->put($file) or die "can't put $file: $!";
1551 Spools CSV invoice data.
1557 =item format - 'default' or 'billco'
1559 =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>).
1561 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1563 =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.
1570 my($self, %opt) = @_;
1572 my $cust_main = $self->cust_main;
1574 if ( $opt{'dest'} ) {
1575 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1576 $cust_main->invoicing_list;
1577 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1578 || ! keys %invoicing_list;
1581 if ( $opt{'balanceover'} ) {
1583 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1586 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1587 mkdir $spooldir, 0700 unless -d $spooldir;
1589 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1593 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1594 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1597 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1599 open(CSV, ">>$file") or die "can't open $file: $!";
1600 flock(CSV, LOCK_EX);
1605 if ( lc($opt{'format'}) eq 'billco' ) {
1607 flock(CSV, LOCK_UN);
1612 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1615 open(CSV,">>$file") or die "can't open $file: $!";
1616 flock(CSV, LOCK_EX);
1622 flock(CSV, LOCK_UN);
1629 =item print_csv OPTION => VALUE, ...
1631 Returns CSV data for this invoice.
1635 format - 'default' or 'billco'
1637 Returns a list consisting of two scalars. The first is a single line of CSV
1638 header information for this invoice. The second is one or more lines of CSV
1639 detail information for this invoice.
1641 If I<format> is not specified or "default", the fields of the CSV file are as
1644 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1648 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1650 B<record_type> is C<cust_bill> for the initial header line only. The
1651 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1652 fields are filled in.
1654 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1655 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1658 =item invnum - invoice number
1660 =item custnum - customer number
1662 =item _date - invoice date
1664 =item charged - total invoice amount
1666 =item first - customer first name
1668 =item last - customer first name
1670 =item company - company name
1672 =item address1 - address line 1
1674 =item address2 - address line 1
1684 =item pkg - line item description
1686 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1688 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1690 =item sdate - start date for recurring fee
1692 =item edate - end date for recurring fee
1696 If I<format> is "billco", the fields of the header CSV file are as follows:
1698 +-------------------------------------------------------------------+
1699 | FORMAT HEADER FILE |
1700 |-------------------------------------------------------------------|
1701 | Field | Description | Name | Type | Width |
1702 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1703 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1704 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1705 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1706 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1707 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1708 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1709 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1710 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1711 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1712 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1713 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1714 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1715 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1716 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1717 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1718 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1719 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1720 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1721 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1722 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1723 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1724 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1725 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1726 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1727 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1728 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1729 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1730 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1731 +-------+-------------------------------+------------+------+-------+
1733 If I<format> is "billco", the fields of the detail CSV file are as follows:
1735 FORMAT FOR DETAIL FILE
1737 Field | Description | Name | Type | Width
1738 1 | N/A-Leave Empty | RC | CHAR | 2
1739 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1740 3 | Account Number | TRACCTNUM | CHAR | 15
1741 4 | Invoice Number | TRINVOICE | CHAR | 15
1742 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1743 6 | Transaction Detail | DETAILS | CHAR | 100
1744 7 | Amount | AMT | NUM* | 9
1745 8 | Line Format Control** | LNCTRL | CHAR | 2
1746 9 | Grouping Code | GROUP | CHAR | 2
1747 10 | User Defined | ACCT CODE | CHAR | 15
1752 my($self, %opt) = @_;
1754 eval "use Text::CSV_XS";
1757 my $cust_main = $self->cust_main;
1759 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1761 if ( lc($opt{'format'}) eq 'billco' ) {
1764 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1766 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1768 my( $previous_balance, @unused ) = $self->previous; #previous balance
1770 my $pmt_cr_applied = 0;
1771 $pmt_cr_applied += $_->{'amount'}
1772 foreach ( $self->_items_payments, $self->_items_credits ) ;
1774 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1777 '', # 1 | N/A-Leave Empty CHAR 2
1778 '', # 2 | N/A-Leave Empty CHAR 15
1779 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1780 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1781 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1782 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1783 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1784 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1785 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1786 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1787 '', # 10 | Ancillary Billing Information CHAR 30
1788 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1789 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1792 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1795 $duedate, # 14 | Bill Due Date CHAR 10
1797 $previous_balance, # 15 | Previous Balance NUM* 9
1798 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1799 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1800 $totaldue, # 18 | Total Amt Due NUM* 9
1801 $totaldue, # 19 | Total Amt Due NUM* 9
1802 '', # 20 | 30 Day Aging NUM* 9
1803 '', # 21 | 60 Day Aging NUM* 9
1804 '', # 22 | 90 Day Aging NUM* 9
1805 'N', # 23 | Y/N CHAR 1
1806 '', # 24 | Remittance automation CHAR 100
1807 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1808 $self->custnum, # 26 | Customer Reference Number CHAR 15
1809 '0', # 27 | Federal Tax*** NUM* 9
1810 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1811 '0', # 29 | Other Taxes & Fees*** NUM* 9
1820 time2str("%x", $self->_date),
1821 sprintf("%.2f", $self->charged),
1822 ( map { $cust_main->getfield($_) }
1823 qw( first last company address1 address2 city state zip country ) ),
1825 ) or die "can't create csv";
1828 my $header = $csv->string. "\n";
1831 if ( lc($opt{'format'}) eq 'billco' ) {
1834 foreach my $item ( $self->_items_pkg ) {
1837 '', # 1 | N/A-Leave Empty CHAR 2
1838 '', # 2 | N/A-Leave Empty CHAR 15
1839 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1840 $self->invnum, # 4 | Invoice Number CHAR 15
1841 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1842 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1843 $item->{'amount'}, # 7 | Amount NUM* 9
1844 '', # 8 | Line Format Control** CHAR 2
1845 '', # 9 | Grouping Code CHAR 2
1846 '', # 10 | User Defined CHAR 15
1849 $detail .= $csv->string. "\n";
1855 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1857 my($pkg, $setup, $recur, $sdate, $edate);
1858 if ( $cust_bill_pkg->pkgnum ) {
1860 ($pkg, $setup, $recur, $sdate, $edate) = (
1861 $cust_bill_pkg->part_pkg->pkg,
1862 ( $cust_bill_pkg->setup != 0
1863 ? sprintf("%.2f", $cust_bill_pkg->setup )
1865 ( $cust_bill_pkg->recur != 0
1866 ? sprintf("%.2f", $cust_bill_pkg->recur )
1868 ( $cust_bill_pkg->sdate
1869 ? time2str("%x", $cust_bill_pkg->sdate)
1871 ($cust_bill_pkg->edate
1872 ?time2str("%x", $cust_bill_pkg->edate)
1876 } else { #pkgnum tax
1877 next unless $cust_bill_pkg->setup != 0;
1878 $pkg = $cust_bill_pkg->desc;
1879 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
1880 ( $sdate, $edate ) = ( '', '' );
1886 ( map { '' } (1..11) ),
1887 ($pkg, $setup, $recur, $sdate, $edate)
1888 ) or die "can't create csv";
1890 $detail .= $csv->string. "\n";
1896 ( $header, $detail );
1902 Pays this invoice with a compliemntary payment. If there is an error,
1903 returns the error, otherwise returns false.
1909 my $cust_pay = new FS::cust_pay ( {
1910 'invnum' => $self->invnum,
1911 'paid' => $self->owed,
1914 'payinfo' => $self->cust_main->payinfo,
1922 Attempts to pay this invoice with a credit card payment via a
1923 Business::OnlinePayment realtime gateway. See
1924 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1925 for supported processors.
1931 $self->realtime_bop( 'CC', @_ );
1936 Attempts to pay this invoice with an electronic check (ACH) payment via a
1937 Business::OnlinePayment realtime gateway. See
1938 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1939 for supported processors.
1945 $self->realtime_bop( 'ECHECK', @_ );
1950 Attempts to pay this invoice with phone bill (LEC) payment via a
1951 Business::OnlinePayment realtime gateway. See
1952 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1953 for supported processors.
1959 $self->realtime_bop( 'LEC', @_ );
1963 my( $self, $method ) = @_;
1965 my $cust_main = $self->cust_main;
1966 my $balance = $cust_main->balance;
1967 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1968 $amount = sprintf("%.2f", $amount);
1969 return "not run (balance $balance)" unless $amount > 0;
1971 my $description = 'Internet Services';
1972 if ( $conf->exists('business-onlinepayment-description') ) {
1973 my $dtempl = $conf->config('business-onlinepayment-description');
1975 my $agent_obj = $cust_main->agent
1976 or die "can't retreive agent for $cust_main (agentnum ".
1977 $cust_main->agentnum. ")";
1978 my $agent = $agent_obj->agent;
1979 my $pkgs = join(', ',
1980 map { $_->part_pkg->pkg }
1981 grep { $_->pkgnum } $self->cust_bill_pkg
1983 $description = eval qq("$dtempl");
1986 $cust_main->realtime_bop($method, $amount,
1987 'description' => $description,
1988 'invnum' => $self->invnum,
1989 #this didn't do what we want, it just calls apply_payments_and_credits
1991 'apply_to_invoice' => 1,
1993 #this changes application behavior: auto payments
1994 #triggered against a specific invoice are now applied
1995 #to that invoice instead of oldest open.
2001 =item batch_card OPTION => VALUE...
2003 Adds a payment for this invoice to the pending credit card batch (see
2004 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2005 runs the payment using a realtime gateway.
2010 my ($self, %options) = @_;
2011 my $cust_main = $self->cust_main;
2013 $options{invnum} = $self->invnum;
2015 $cust_main->batch_card(%options);
2018 sub _agent_template {
2020 $self->cust_main->agent_template;
2023 sub _agent_invoice_from {
2025 $self->cust_main->agent_invoice_from;
2028 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2030 Returns an text invoice, as a list of lines.
2032 Options can be passed as a hashref (recommended) or as a list of time, template
2033 and then any key/value pairs for any other options.
2035 I<time>, if specified, is used to control the printing of overdue messages. The
2036 default is now. It isn't the date of the invoice; that's the `_date' field.
2037 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2038 L<Time::Local> and L<Date::Parse> for conversion functions.
2040 I<template>, if specified, is the name of a suffix for alternate invoices.
2042 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2048 my( $today, $template, %opt );
2050 %opt = %{ shift() };
2051 $today = delete($opt{'time'}) || '';
2052 $template = delete($opt{template}) || '';
2054 ( $today, $template, %opt ) = @_;
2057 my %params = ( 'format' => 'template' );
2058 $params{'time'} = $today if $today;
2059 $params{'template'} = $template if $template;
2060 $params{$_} = $opt{$_}
2061 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2063 $self->print_generic( %params );
2066 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2068 Internal method - returns a filename of a filled-in LaTeX template for this
2069 invoice (Note: add ".tex" to get the actual filename), and a filename of
2070 an associated logo (with the .eps extension included).
2072 See print_ps and print_pdf for methods that return PostScript and PDF output.
2074 Options can be passed as a hashref (recommended) or as a list of time, template
2075 and then any key/value pairs for any other options.
2077 I<time>, if specified, is used to control the printing of overdue messages. The
2078 default is now. It isn't the date of the invoice; that's the `_date' field.
2079 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2080 L<Time::Local> and L<Date::Parse> for conversion functions.
2082 I<template>, if specified, is the name of a suffix for alternate invoices.
2084 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2090 my( $today, $template, %opt );
2092 %opt = %{ shift() };
2093 $today = delete($opt{'time'}) || '';
2094 $template = delete($opt{template}) || '';
2096 ( $today, $template, %opt ) = @_;
2099 my %params = ( 'format' => 'latex' );
2100 $params{'time'} = $today if $today;
2101 $params{'template'} = $template if $template;
2102 $params{$_} = $opt{$_}
2103 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2105 $template ||= $self->_agent_template;
2107 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2108 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2112 ) or die "can't open temp file: $!\n";
2114 my $agentnum = $self->cust_main->agentnum;
2116 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2117 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2118 or die "can't write temp file: $!\n";
2120 print $lh $conf->config_binary('logo.eps', $agentnum)
2121 or die "can't write temp file: $!\n";
2124 $params{'logo_file'} = $lh->filename;
2126 if($conf->exists('invoice-barcode')){
2127 my $gdbar = new GD::Barcode('Code39',$self->invnum);
2128 die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2129 my $gd = $gdbar->plot(Height => 20);
2130 my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2134 ) or die "can't open temp file: $!\n";
2135 print $bh $gd->png or die "cannot write barcode to file: $!\n";
2137 my $png_file = $bh->filename;
2140 my $eps_file = $png_file;
2141 $eps_file =~ s/\.png$/.eps/g;
2142 $png_file =~ /(barcode.*png)/;
2144 $eps_file =~ /(barcode.*eps)/;
2147 my $curr_dir = cwd();
2149 # after painfuly long experimentation, it was determined that sam2p won't
2150 # accept : and other chars in the path, no matter how hard I tried to
2151 # escape them, hence the chdir (and chdir back, just to be safe)
2152 system('sam2p', $png_file, 'EPS:', $eps_file ) == 0
2153 or die "sam2p failed: $!\n";
2157 $params{'barcode_file'} = $eps_file;
2160 my @filled_in = $self->print_generic( %params );
2162 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2166 ) or die "can't open temp file: $!\n";
2167 print $fh join('', @filled_in );
2170 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2171 return ($1, $params{'logo_file'}, $params{'barcode_file'});
2175 =item print_generic OPTION => VALUE ...
2177 Internal method - returns a filled-in template for this invoice as a scalar.
2179 See print_ps and print_pdf for methods that return PostScript and PDF output.
2181 Non optional options include
2182 format - latex, html, template
2184 Optional options include
2186 template - a value used as a suffix for a configuration template
2188 time - a value used to control the printing of overdue messages. The
2189 default is now. It isn't the date of the invoice; that's the `_date' field.
2190 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2191 L<Time::Local> and L<Date::Parse> for conversion functions.
2195 unsquelch_cdr - overrides any per customer cdr squelching when true
2197 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2201 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2202 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2203 # yes: fixed width (dot matrix) text printing will be borked
2206 my( $self, %params ) = @_;
2207 my $today = $params{today} ? $params{today} : time;
2208 warn "$me print_generic called on $self with suffix $params{template}\n"
2211 my $format = $params{format};
2212 die "Unknown format: $format"
2213 unless $format =~ /^(latex|html|template)$/;
2215 my $cust_main = $self->cust_main;
2216 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2217 unless $cust_main->payname
2218 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2220 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2221 'html' => [ '<%=', '%>' ],
2222 'template' => [ '{', '}' ],
2225 warn "$me print_generic creating template\n"
2228 #create the template
2229 my $template = $params{template} ? $params{template} : $self->_agent_template;
2230 my $templatefile = "invoice_$format";
2231 $templatefile .= "_$template"
2232 if length($template);
2233 my @invoice_template = map "$_\n", $conf->config($templatefile)
2234 or die "cannot load config data $templatefile";
2237 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2238 #change this to a die when the old code is removed
2239 warn "old-style invoice template $templatefile; ".
2240 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2241 $old_latex = 'true';
2242 @invoice_template = _translate_old_latex_format(@invoice_template);
2245 warn "$me print_generic creating T:T object\n"
2248 my $text_template = new Text::Template(
2250 SOURCE => \@invoice_template,
2251 DELIMITERS => $delimiters{$format},
2254 warn "$me print_generic compiling T:T object\n"
2257 $text_template->compile()
2258 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2261 # additional substitution could possibly cause breakage in existing templates
2262 my %convert_maps = (
2264 'notes' => sub { map "$_", @_ },
2265 'footer' => sub { map "$_", @_ },
2266 'smallfooter' => sub { map "$_", @_ },
2267 'returnaddress' => sub { map "$_", @_ },
2268 'coupon' => sub { map "$_", @_ },
2269 'summary' => sub { map "$_", @_ },
2275 s/%%(.*)$/<!-- $1 -->/g;
2276 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2277 s/\\begin\{enumerate\}/<ol>/g;
2279 s/\\end\{enumerate\}/<\/ol>/g;
2280 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2289 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2291 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2296 s/\\\\\*?\s*$/<BR>/;
2297 s/\\hyphenation\{[\w\s\-]+}//;
2302 'coupon' => sub { "" },
2303 'summary' => sub { "" },
2310 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2311 s/\\begin\{enumerate\}//g;
2313 s/\\end\{enumerate\}//g;
2314 s/\\textbf\{(.*)\}/$1/g;
2321 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2323 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2328 s/\\\\\*?\s*$/\n/; # dubious
2329 s/\\hyphenation\{[\w\s\-]+}//;
2333 'coupon' => sub { "" },
2334 'summary' => sub { "" },
2339 # hashes for differing output formats
2340 my %nbsps = ( 'latex' => '~',
2341 'html' => '', # '&nbps;' would be nice
2342 'template' => '', # not used
2344 my $nbsp = $nbsps{$format};
2346 my %escape_functions = ( 'latex' => \&_latex_escape,
2347 'html' => \&_html_escape_nbsp,#\&encode_entities,
2348 'template' => sub { shift },
2350 my $escape_function = $escape_functions{$format};
2351 my $escape_function_nonbsp = ($format eq 'html')
2352 ? \&_html_escape : $escape_function;
2354 my %date_formats = ( 'latex' => $date_format_long,
2355 'html' => $date_format_long,
2358 $date_formats{'html'} =~ s/ / /g;
2360 my $date_format = $date_formats{$format};
2362 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2364 'html' => sub { return '<b>'. shift(). '</b>'
2366 'template' => sub { shift },
2368 my $embolden_function = $embolden_functions{$format};
2370 warn "$me generating template variables\n"
2373 # generate template variables
2376 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2380 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2386 $returnaddress = join("\n",
2387 $conf->config_orbase("invoice_${format}returnaddress", $template)
2390 } elsif ( grep /\S/,
2391 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2393 my $convert_map = $convert_maps{$format}{'returnaddress'};
2396 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2401 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2403 my $convert_map = $convert_maps{$format}{'returnaddress'};
2404 $returnaddress = join( "\n", &$convert_map(
2405 map { s/( {2,})/'~' x length($1)/eg;
2409 ( $conf->config('company_name', $self->cust_main->agentnum),
2410 $conf->config('company_address', $self->cust_main->agentnum),
2417 my $warning = "Couldn't find a return address; ".
2418 "do you need to set the company_address configuration value?";
2420 $returnaddress = $nbsp;
2421 #$returnaddress = $warning;
2425 warn "$me generating invoice data\n"
2428 my $agentnum = $self->cust_main->agentnum;
2430 my %invoice_data = (
2433 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2434 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2435 'returnaddress' => $returnaddress,
2436 'agent' => &$escape_function($cust_main->agent->agent),
2439 'invnum' => $self->invnum,
2440 'date' => time2str($date_format, $self->_date),
2441 'today' => time2str($date_format_long, $today),
2442 'terms' => $self->terms,
2443 'template' => $template, #params{'template'},
2444 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2445 'current_charges' => sprintf("%.2f", $self->charged),
2446 'duedate' => $self->due_date2str($rdate_format), #date_format?
2449 'custnum' => $cust_main->display_custnum,
2450 'agent_custid' => &$escape_function($cust_main->agent_custid),
2451 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2452 payname company address1 address2 city state zip fax
2456 'ship_enable' => $conf->exists('invoice-ship_address'),
2457 'unitprices' => $conf->exists('invoice-unitprice'),
2458 'smallernotes' => $conf->exists('invoice-smallernotes'),
2459 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2460 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2462 #layout info -- would be fancy to calc some of this and bury the template
2464 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2465 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2466 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2467 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2468 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2469 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2470 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2471 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2472 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2473 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2475 # better hang on to conf_dir for a while (for old templates)
2476 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2478 #these are only used when doing paged plaintext
2484 $invoice_data{finance_section} = '';
2485 if ( $conf->config('finance_pkgclass') ) {
2487 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2488 $invoice_data{finance_section} = $pkg_class->categoryname;
2490 $invoice_data{finance_amount} = '0.00';
2491 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2493 my $countrydefault = $conf->config('countrydefault') || 'US';
2494 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2495 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2496 my $method = $prefix.$_;
2497 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2499 $invoice_data{'ship_country'} = ''
2500 if ( $invoice_data{'ship_country'} eq $countrydefault );
2502 $invoice_data{'cid'} = $params{'cid'}
2505 if ( $cust_main->country eq $countrydefault ) {
2506 $invoice_data{'country'} = '';
2508 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2512 $invoice_data{'address'} = \@address;
2514 $cust_main->payname.
2515 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2516 ? " (P.O. #". $cust_main->payinfo. ")"
2520 push @address, $cust_main->company
2521 if $cust_main->company;
2522 push @address, $cust_main->address1;
2523 push @address, $cust_main->address2
2524 if $cust_main->address2;
2526 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2527 push @address, $invoice_data{'country'}
2528 if $invoice_data{'country'};
2530 while (scalar(@address) < 5);
2532 $invoice_data{'logo_file'} = $params{'logo_file'}
2533 if $params{'logo_file'};
2534 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2535 if $params{'barcode_file'};
2537 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2538 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2539 #my $balance_due = $self->owed + $pr_total - $cr_total;
2540 my $balance_due = $self->owed + $pr_total;
2541 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2542 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2543 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2544 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2546 my $summarypage = '';
2547 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2550 $invoice_data{'summarypage'} = $summarypage;
2552 warn "$me substituting variables in notes, footer, smallfooter\n"
2555 foreach my $include (qw( notes footer smallfooter coupon )) {
2557 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2560 if ( $conf->exists($inc_file, $agentnum)
2561 && length( $conf->config($inc_file, $agentnum) ) ) {
2563 @inc_src = $conf->config($inc_file, $agentnum);
2567 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2569 my $convert_map = $convert_maps{$format}{$include};
2571 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2572 s/--\@\]/$delimiters{$format}[1]/g;
2575 &$convert_map( $conf->config($inc_file, $agentnum) );
2579 my $inc_tt = new Text::Template (
2581 SOURCE => [ map "$_\n", @inc_src ],
2582 DELIMITERS => $delimiters{$format},
2583 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2585 unless ( $inc_tt->compile() ) {
2586 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2587 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2591 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2593 $invoice_data{$include} =~ s/\n+$//
2594 if ($format eq 'latex');
2597 $invoice_data{'po_line'} =
2598 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2599 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2602 my %money_chars = ( 'latex' => '',
2603 'html' => $conf->config('money_char') || '$',
2606 my $money_char = $money_chars{$format};
2608 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2609 'html' => $conf->config('money_char') || '$',
2612 my $other_money_char = $other_money_chars{$format};
2613 $invoice_data{'dollar'} = $other_money_char;
2615 my @detail_items = ();
2616 my @total_items = ();
2620 $invoice_data{'detail_items'} = \@detail_items;
2621 $invoice_data{'total_items'} = \@total_items;
2622 $invoice_data{'buf'} = \@buf;
2623 $invoice_data{'sections'} = \@sections;
2625 warn "$me generating sections\n"
2628 my $previous_section = { 'description' => 'Previous Charges',
2629 'subtotal' => $other_money_char.
2630 sprintf('%.2f', $pr_total),
2631 'summarized' => $summarypage ? 'Y' : '',
2633 $previous_section->{posttotal} = '0 / 30 / 60/ 90 days overdue '.
2634 join(' / ', map { $cust_main->balance_date_range(@$_) }
2635 $self->_prior_month30s
2637 if $conf->exists('invoice_include_aging');
2640 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2641 'subtotal' => $taxtotal, # adjusted below
2642 'summarized' => $summarypage ? 'Y' : '',
2644 my $tax_weight = _pkg_category($tax_section->{description})
2645 ? _pkg_category($tax_section->{description})->weight
2647 $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2648 $tax_section->{'sort_weight'} = $tax_weight;
2651 my $adjusttotal = 0;
2652 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2653 'subtotal' => 0, # adjusted below
2654 'summarized' => $summarypage ? 'Y' : '',
2656 my $adjust_weight = _pkg_category($adjust_section->{description})
2657 ? _pkg_category($adjust_section->{description})->weight
2659 $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2660 $adjust_section->{'sort_weight'} = $adjust_weight;
2662 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2663 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2664 $invoice_data{'multisection'} = $multisection;
2665 my $late_sections = [];
2666 my $extra_sections = [];
2667 my $extra_lines = ();
2668 if ( $multisection ) {
2669 ($extra_sections, $extra_lines) =
2670 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2671 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2673 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2675 push @detail_items, @$extra_lines if $extra_lines;
2677 $self->_items_sections( $late_sections, # this could stand a refactor
2679 $escape_function_nonbsp,
2683 if ($conf->exists('svc_phone_sections')) {
2684 my ($phone_sections, $phone_lines) =
2685 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2686 push @{$late_sections}, @$phone_sections;
2687 push @detail_items, @$phone_lines;
2690 push @sections, { 'description' => '', 'subtotal' => '' };
2693 unless ( $conf->exists('disable_previous_balance')
2694 || $conf->exists('previous_balance-summary_only')
2698 warn "$me adding previous balances\n"
2701 foreach my $line_item ( $self->_items_previous ) {
2704 ext_description => [],
2706 $detail->{'ref'} = $line_item->{'pkgnum'};
2707 $detail->{'quantity'} = 1;
2708 $detail->{'section'} = $previous_section;
2709 $detail->{'description'} = &$escape_function($line_item->{'description'});
2710 if ( exists $line_item->{'ext_description'} ) {
2711 @{$detail->{'ext_description'}} = map {
2712 &$escape_function($_);
2713 } @{$line_item->{'ext_description'}};
2715 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2716 $line_item->{'amount'};
2717 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2719 push @detail_items, $detail;
2720 push @buf, [ $detail->{'description'},
2721 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2727 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2728 push @buf, ['','-----------'];
2729 push @buf, [ 'Total Previous Balance',
2730 $money_char. sprintf("%10.2f", $pr_total) ];
2734 if ( $conf->exists('svc_phone-did-summary') ) {
2735 warn "$me adding DID summary\n"
2738 my ($didsummary,$minutes) = $self->_did_summary;
2739 my $didsummary_desc = 'DID Activity Summary (Past 30 days)';
2741 { 'description' => $didsummary_desc,
2742 'ext_description' => [ $didsummary, $minutes ],
2747 foreach my $section (@sections, @$late_sections) {
2749 warn "$me adding section \n". Dumper($section)
2752 # begin some normalization
2753 $section->{'subtotal'} = $section->{'amount'}
2755 && !exists($section->{subtotal})
2756 && exists($section->{amount});
2758 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2759 if ( $invoice_data{finance_section} &&
2760 $section->{'description'} eq $invoice_data{finance_section} );
2762 $section->{'subtotal'} = $other_money_char.
2763 sprintf('%.2f', $section->{'subtotal'})
2766 # continue some normalization
2767 $section->{'amount'} = $section->{'subtotal'}
2771 if ( $section->{'description'} ) {
2772 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2777 warn "$me setting options\n"
2780 my $multilocation = scalar($cust_main->cust_location); #too expensive?
2782 $options{'section'} = $section if $multisection;
2783 $options{'format'} = $format;
2784 $options{'escape_function'} = $escape_function;
2785 $options{'format_function'} = sub { () } unless $unsquelched;
2786 $options{'unsquelched'} = $unsquelched;
2787 $options{'summary_page'} = $summarypage;
2788 $options{'skip_usage'} =
2789 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2790 $options{'multilocation'} = $multilocation;
2791 $options{'multisection'} = $multisection;
2793 warn "$me searching for line items\n"
2796 foreach my $line_item ( $self->_items_pkg(%options) ) {
2798 warn "$me adding line item $line_item\n"
2802 ext_description => [],
2804 $detail->{'ref'} = $line_item->{'pkgnum'};
2805 $detail->{'quantity'} = $line_item->{'quantity'};
2806 $detail->{'section'} = $section;
2807 $detail->{'description'} = &$escape_function($line_item->{'description'});
2808 if ( exists $line_item->{'ext_description'} ) {
2809 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2811 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2812 $line_item->{'amount'};
2813 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2814 $line_item->{'unit_amount'};
2815 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2817 push @detail_items, $detail;
2818 push @buf, ( [ $detail->{'description'},
2819 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2821 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2825 if ( $section->{'description'} ) {
2826 push @buf, ( ['','-----------'],
2827 [ $section->{'description'}. ' sub-total',
2828 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2837 $invoice_data{current_less_finance} =
2838 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2840 if ( $multisection && !$conf->exists('disable_previous_balance')
2841 || $conf->exists('previous_balance-summary_only') )
2843 unshift @sections, $previous_section if $pr_total;
2846 warn "$me adding taxes\n"
2849 foreach my $tax ( $self->_items_tax ) {
2851 $taxtotal += $tax->{'amount'};
2853 my $description = &$escape_function( $tax->{'description'} );
2854 my $amount = sprintf( '%.2f', $tax->{'amount'} );
2856 if ( $multisection ) {
2858 my $money = $old_latex ? '' : $money_char;
2859 push @detail_items, {
2860 ext_description => [],
2863 description => $description,
2864 amount => $money. $amount,
2866 section => $tax_section,
2871 push @total_items, {
2872 'total_item' => $description,
2873 'total_amount' => $other_money_char. $amount,
2878 push @buf,[ $description,
2879 $money_char. $amount,
2886 $total->{'total_item'} = 'Sub-total';
2887 $total->{'total_amount'} =
2888 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2890 if ( $multisection ) {
2891 $tax_section->{'subtotal'} = $other_money_char.
2892 sprintf('%.2f', $taxtotal);
2893 $tax_section->{'pretotal'} = 'New charges sub-total '.
2894 $total->{'total_amount'};
2895 push @sections, $tax_section if $taxtotal;
2897 unshift @total_items, $total;
2900 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2902 push @buf,['','-----------'];
2903 push @buf,[( $conf->exists('disable_previous_balance')
2905 : 'Total New Charges'
2907 $money_char. sprintf("%10.2f",$self->charged) ];
2913 $item = $conf->config('previous_balance-exclude_from_total')
2914 || 'Total New Charges'
2915 if $conf->exists('previous_balance-exclude_from_total');
2916 my $amount = $self->charged +
2917 ( $conf->exists('disable_previous_balance') ||
2918 $conf->exists('previous_balance-exclude_from_total')
2922 $total->{'total_item'} = &$embolden_function($item);
2923 $total->{'total_amount'} =
2924 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
2925 if ( $multisection ) {
2926 if ( $adjust_section->{'sort_weight'} ) {
2927 $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
2928 sprintf("%.2f", ($self->billing_balance || 0) );
2930 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2931 sprintf('%.2f', $self->charged );
2934 push @total_items, $total;
2936 push @buf,['','-----------'];
2939 sprintf( '%10.2f', $amount )
2944 unless ( $conf->exists('disable_previous_balance') ) {
2945 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2948 my $credittotal = 0;
2949 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
2952 $total->{'total_item'} = &$escape_function($credit->{'description'});
2953 $credittotal += $credit->{'amount'};
2954 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2955 $adjusttotal += $credit->{'amount'};
2956 if ( $multisection ) {
2957 my $money = $old_latex ? '' : $money_char;
2958 push @detail_items, {
2959 ext_description => [],
2962 description => &$escape_function($credit->{'description'}),
2963 amount => $money. $credit->{'amount'},
2965 section => $adjust_section,
2968 push @total_items, $total;
2972 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2975 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
2976 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
2980 my $paymenttotal = 0;
2981 foreach my $payment ( $self->_items_payments ) {
2983 $total->{'total_item'} = &$escape_function($payment->{'description'});
2984 $paymenttotal += $payment->{'amount'};
2985 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2986 $adjusttotal += $payment->{'amount'};
2987 if ( $multisection ) {
2988 my $money = $old_latex ? '' : $money_char;
2989 push @detail_items, {
2990 ext_description => [],
2993 description => &$escape_function($payment->{'description'}),
2994 amount => $money. $payment->{'amount'},
2996 section => $adjust_section,
2999 push @total_items, $total;
3001 push @buf, [ $payment->{'description'},
3002 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3005 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3007 if ( $multisection ) {
3008 $adjust_section->{'subtotal'} = $other_money_char.
3009 sprintf('%.2f', $adjusttotal);
3010 push @sections, $adjust_section
3011 unless $adjust_section->{sort_weight};
3016 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3017 $total->{'total_amount'} =
3018 &$embolden_function(
3019 $other_money_char. sprintf('%.2f', $summarypage
3021 $self->billing_balance
3022 : $self->owed + $pr_total
3025 if ( $multisection && !$adjust_section->{sort_weight} ) {
3026 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3027 $total->{'total_amount'};
3029 push @total_items, $total;
3031 push @buf,['','-----------'];
3032 push @buf,[$self->balance_due_msg, $money_char.
3033 sprintf("%10.2f", $balance_due ) ];
3037 if ( $multisection ) {
3038 if ($conf->exists('svc_phone_sections')) {
3040 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3041 $total->{'total_amount'} =
3042 &$embolden_function(
3043 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3045 my $last_section = pop @sections;
3046 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3047 $total->{'total_amount'};
3048 push @sections, $last_section;
3050 push @sections, @$late_sections
3054 my @includelist = ();
3055 push @includelist, 'summary' if $summarypage;
3056 foreach my $include ( @includelist ) {
3058 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3061 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3063 @inc_src = $conf->config($inc_file, $agentnum);
3067 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3069 my $convert_map = $convert_maps{$format}{$include};
3071 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3072 s/--\@\]/$delimiters{$format}[1]/g;
3075 &$convert_map( $conf->config($inc_file, $agentnum) );
3079 my $inc_tt = new Text::Template (
3081 SOURCE => [ map "$_\n", @inc_src ],
3082 DELIMITERS => $delimiters{$format},
3083 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3085 unless ( $inc_tt->compile() ) {
3086 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3087 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3091 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3093 $invoice_data{$include} =~ s/\n+$//
3094 if ($format eq 'latex');
3099 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3100 /invoice_lines\((\d*)\)/;
3101 $invoice_lines += $1 || scalar(@buf);
3104 die "no invoice_lines() functions in template?"
3105 if ( $format eq 'template' && !$wasfunc );
3107 if ($format eq 'template') {
3109 if ( $invoice_lines ) {
3110 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3111 $invoice_data{'total_pages'}++
3112 if scalar(@buf) % $invoice_lines;
3115 #setup subroutine for the template
3116 sub FS::cust_bill::_template::invoice_lines {
3117 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
3119 scalar(@FS::cust_bill::_template::buf)
3120 ? shift @FS::cust_bill::_template::buf
3129 push @collect, split("\n",
3130 $text_template->fill_in( HASH => \%invoice_data,
3131 PACKAGE => 'FS::cust_bill::_template'
3134 $FS::cust_bill::_template::page++;
3136 map "$_\n", @collect;
3138 warn "filling in template for invoice ". $self->invnum. "\n"
3140 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3143 $text_template->fill_in(HASH => \%invoice_data);
3147 # helper routine for generating date ranges
3148 sub _prior_month30s {
3151 [ 1, 2592000 ], # 0-30 days ago
3152 [ 2592000, 5184000 ], # 30-60 days ago
3153 [ 5184000, 7776000 ], # 60-90 days ago
3154 [ 7776000, 0 ], # 90+ days ago
3157 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3158 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3163 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3165 Returns an postscript invoice, as a scalar.
3167 Options can be passed as a hashref (recommended) or as a list of time, template
3168 and then any key/value pairs for any other options.
3170 I<time> an optional value used to control the printing of overdue messages. The
3171 default is now. It isn't the date of the invoice; that's the `_date' field.
3172 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3173 L<Time::Local> and L<Date::Parse> for conversion functions.
3175 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3182 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3183 my $ps = generate_ps($file);
3185 unlink($barcodefile);
3190 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3192 Returns an PDF invoice, as a scalar.
3194 Options can be passed as a hashref (recommended) or as a list of time, template
3195 and then any key/value pairs for any other options.
3197 I<time> an optional value used to control the printing of overdue messages. The
3198 default is now. It isn't the date of the invoice; that's the `_date' field.
3199 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3200 L<Time::Local> and L<Date::Parse> for conversion functions.
3202 I<template>, if specified, is the name of a suffix for alternate invoices.
3204 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3211 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3212 my $pdf = generate_pdf($file);
3214 unlink($barcodefile);
3219 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3221 Returns an HTML invoice, as a scalar.
3223 I<time> an optional value used to control the printing of overdue messages. The
3224 default is now. It isn't the date of the invoice; that's the `_date' field.
3225 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3226 L<Time::Local> and L<Date::Parse> for conversion functions.
3228 I<template>, if specified, is the name of a suffix for alternate invoices.
3230 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3232 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3233 when emailing the invoice as part of a multipart/related MIME email.
3241 %params = %{ shift() };
3243 $params{'time'} = shift;
3244 $params{'template'} = shift;
3245 $params{'cid'} = shift;
3248 $params{'format'} = 'html';
3250 $self->print_generic( %params );
3253 # quick subroutine for print_latex
3255 # There are ten characters that LaTeX treats as special characters, which
3256 # means that they do not simply typeset themselves:
3257 # # $ % & ~ _ ^ \ { }
3259 # TeX ignores blanks following an escaped character; if you want a blank (as
3260 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3264 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3265 $value =~ s/([<>])/\$$1\$/g;
3271 encode_entities($value);
3275 sub _html_escape_nbsp {
3276 my $value = _html_escape(shift);
3277 $value =~ s/ +/ /g;
3281 #utility methods for print_*
3283 sub _translate_old_latex_format {
3284 warn "_translate_old_latex_format called\n"
3291 if ( $line =~ /^%%Detail\s*$/ ) {
3293 push @template, q![@--!,
3294 q! foreach my $_tr_line (@detail_items) {!,
3295 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3296 q! $_tr_line->{'description'} .= !,
3297 q! "\\tabularnewline\n~~".!,
3298 q! join( "\\tabularnewline\n~~",!,
3299 q! @{$_tr_line->{'ext_description'}}!,
3303 while ( ( my $line_item_line = shift )
3304 !~ /^%%EndDetail\s*$/ ) {
3305 $line_item_line =~ s/'/\\'/g; # nice LTS
3306 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3307 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3308 push @template, " \$OUT .= '$line_item_line';";
3311 push @template, '}',
3314 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3316 push @template, '[@--',
3317 ' foreach my $_tr_line (@total_items) {';
3319 while ( ( my $total_item_line = shift )
3320 !~ /^%%EndTotalDetails\s*$/ ) {
3321 $total_item_line =~ s/'/\\'/g; # nice LTS
3322 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3323 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3324 push @template, " \$OUT .= '$total_item_line';";
3327 push @template, '}',
3331 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3332 push @template, $line;
3338 warn "$_\n" foreach @template;
3347 #check for an invoice-specific override
3348 return $self->invoice_terms if $self->invoice_terms;
3350 #check for a customer- specific override
3351 my $cust_main = $self->cust_main;
3352 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3354 #use configured default
3355 $conf->config('invoice_default_terms') || '';
3361 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3362 $duedate = $self->_date() + ( $1 * 86400 );
3369 $self->due_date ? time2str(shift, $self->due_date) : '';
3372 sub balance_due_msg {
3374 my $msg = 'Balance Due';
3375 return $msg unless $self->terms;
3376 if ( $self->due_date ) {
3377 $msg .= ' - Please pay by '. $self->due_date2str($date_format);
3378 } elsif ( $self->terms ) {
3379 $msg .= ' - '. $self->terms;
3384 sub balance_due_date {
3387 if ( $conf->exists('invoice_default_terms')
3388 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3389 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3394 =item invnum_date_pretty
3396 Returns a string with the invoice number and date, for example:
3397 "Invoice #54 (3/20/2008)"
3401 sub invnum_date_pretty {
3403 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3408 Returns a string with the date, for example: "3/20/2008"
3414 time2str($date_format, $self->_date);
3417 use vars qw(%pkg_category_cache);
3418 sub _items_sections {
3421 my $summarypage = shift;
3423 my $extra_sections = shift;
3427 my %late_subtotal = ();
3430 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3433 my $usage = $cust_bill_pkg->usage;
3435 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3436 next if ( $display->summary && $summarypage );
3438 my $section = $display->section;
3439 my $type = $display->type;
3441 $not_tax{$section} = 1
3442 unless $cust_bill_pkg->pkgnum == 0;
3444 if ( $display->post_total && !$summarypage ) {
3445 if (! $type || $type eq 'S') {
3446 $late_subtotal{$section} += $cust_bill_pkg->setup
3447 if $cust_bill_pkg->setup != 0;
3451 $late_subtotal{$section} += $cust_bill_pkg->recur
3452 if $cust_bill_pkg->recur != 0;
3455 if ($type && $type eq 'R') {
3456 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3457 if $cust_bill_pkg->recur != 0;
3460 if ($type && $type eq 'U') {
3461 $late_subtotal{$section} += $usage
3462 unless scalar(@$extra_sections);
3467 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3469 if (! $type || $type eq 'S') {
3470 $subtotal{$section} += $cust_bill_pkg->setup
3471 if $cust_bill_pkg->setup != 0;
3475 $subtotal{$section} += $cust_bill_pkg->recur
3476 if $cust_bill_pkg->recur != 0;
3479 if ($type && $type eq 'R') {
3480 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3481 if $cust_bill_pkg->recur != 0;
3484 if ($type && $type eq 'U') {
3485 $subtotal{$section} += $usage
3486 unless scalar(@$extra_sections);
3495 %pkg_category_cache = ();
3497 push @$late, map { { 'description' => &{$escape}($_),
3498 'subtotal' => $late_subtotal{$_},
3500 'sort_weight' => ( _pkg_category($_)
3501 ? _pkg_category($_)->weight
3504 ((_pkg_category($_) && _pkg_category($_)->condense)
3505 ? $self->_condense_section($format)
3509 sort _sectionsort keys %late_subtotal;
3512 if ( $summarypage ) {
3513 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3514 map { $_->categoryname } qsearch('pkg_category', {});
3515 push @sections, '' if exists($subtotal{''});
3517 @sections = keys %subtotal;
3520 my @early = map { { 'description' => &{$escape}($_),
3521 'subtotal' => $subtotal{$_},
3522 'summarized' => $not_tax{$_} ? '' : 'Y',
3523 'tax_section' => $not_tax{$_} ? '' : 'Y',
3524 'sort_weight' => ( _pkg_category($_)
3525 ? _pkg_category($_)->weight
3528 ((_pkg_category($_) && _pkg_category($_)->condense)
3529 ? $self->_condense_section($format)
3534 push @early, @$extra_sections if $extra_sections;
3536 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3540 #helper subs for above
3543 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3547 my $categoryname = shift;
3548 $pkg_category_cache{$categoryname} ||=
3549 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3552 my %condensed_format = (
3553 'label' => [ qw( Description Qty Amount ) ],
3555 sub { shift->{description} },
3556 sub { shift->{quantity} },
3557 sub { my($href, %opt) = @_;
3558 ($opt{dollar} || ''). $href->{amount};
3561 'align' => [ qw( l r r ) ],
3562 'span' => [ qw( 5 1 1 ) ], # unitprices?
3563 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3566 sub _condense_section {
3567 my ( $self, $format ) = ( shift, shift );
3569 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3570 qw( description_generator
3573 total_line_generator
3578 sub _condensed_generator_defaults {
3579 my ( $self, $format ) = ( shift, shift );
3580 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3589 sub _condensed_header_generator {
3590 my ( $self, $format ) = ( shift, shift );
3592 my ( $f, $prefix, $suffix, $separator, $column ) =
3593 _condensed_generator_defaults($format);
3595 if ($format eq 'latex') {
3596 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3597 $suffix = "\\\\\n\\hline";
3600 sub { my ($d,$a,$s,$w) = @_;
3601 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3603 } elsif ( $format eq 'html' ) {
3604 $prefix = '<th></th>';
3608 sub { my ($d,$a,$s,$w) = @_;
3609 return qq!<th align="$html_align{$a}">$d</th>!;
3617 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3619 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3622 $prefix. join($separator, @result). $suffix;
3627 sub _condensed_description_generator {
3628 my ( $self, $format ) = ( shift, shift );
3630 my ( $f, $prefix, $suffix, $separator, $column ) =
3631 _condensed_generator_defaults($format);
3633 my $money_char = '$';
3634 if ($format eq 'latex') {
3635 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3637 $separator = " & \n";
3639 sub { my ($d,$a,$s,$w) = @_;
3640 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3642 $money_char = '\\dollar';
3643 }elsif ( $format eq 'html' ) {
3644 $prefix = '"><td align="center"></td>';
3648 sub { my ($d,$a,$s,$w) = @_;
3649 return qq!<td align="$html_align{$a}">$d</td>!;
3651 #$money_char = $conf->config('money_char') || '$';
3652 $money_char = ''; # this is madness
3660 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3662 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3664 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3665 map { $f->{$_}->[$i] } qw(align span width)
3669 $prefix. join( $separator, @result ). $suffix;
3674 sub _condensed_total_generator {
3675 my ( $self, $format ) = ( shift, shift );
3677 my ( $f, $prefix, $suffix, $separator, $column ) =
3678 _condensed_generator_defaults($format);
3681 if ($format eq 'latex') {
3684 $separator = " & \n";
3686 sub { my ($d,$a,$s,$w) = @_;
3687 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3689 }elsif ( $format eq 'html' ) {
3693 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3695 sub { my ($d,$a,$s,$w) = @_;
3696 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3705 # my $r = &{$f->{fields}->[$i]}(@args);
3706 # $r .= ' Total' unless $i;
3708 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3710 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3711 map { $f->{$_}->[$i] } qw(align span width)
3715 $prefix. join( $separator, @result ). $suffix;
3720 =item total_line_generator FORMAT
3722 Returns a coderef used for generation of invoice total line items for this
3723 usage_class. FORMAT is either html or latex
3727 # should not be used: will have issues with hash element names (description vs
3728 # total_item and amount vs total_amount -- another array of functions?
3730 sub _condensed_total_line_generator {
3731 my ( $self, $format ) = ( shift, shift );
3733 my ( $f, $prefix, $suffix, $separator, $column ) =
3734 _condensed_generator_defaults($format);
3737 if ($format eq 'latex') {
3740 $separator = " & \n";
3742 sub { my ($d,$a,$s,$w) = @_;
3743 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3745 }elsif ( $format eq 'html' ) {
3749 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3751 sub { my ($d,$a,$s,$w) = @_;
3752 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3761 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3763 &{$column}( &{$f->{fields}->[$i]}(@args),
3764 map { $f->{$_}->[$i] } qw(align span width)
3768 $prefix. join( $separator, @result ). $suffix;
3773 #sub _items_extra_usage_sections {
3775 # my $escape = shift;
3777 # my %sections = ();
3779 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
3780 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3782 # next unless $cust_bill_pkg->pkgnum > 0;
3784 # foreach my $section ( keys %usage_class ) {
3786 # my $usage = $cust_bill_pkg->usage($section);
3788 # next unless $usage && $usage > 0;
3790 # $sections{$section} ||= 0;
3791 # $sections{$section} += $usage;
3797 # map { { 'description' => &{$escape}($_),
3798 # 'subtotal' => $sections{$_},
3799 # 'summarized' => '',
3800 # 'tax_section' => '',
3803 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3807 sub _items_extra_usage_sections {
3816 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3817 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3818 next unless $cust_bill_pkg->pkgnum > 0;
3820 foreach my $classnum ( keys %usage_class ) {
3821 my $section = $usage_class{$classnum}->classname;
3822 $classnums{$section} = $classnum;
3824 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3825 my $amount = $detail->amount;
3826 next unless $amount && $amount > 0;
3828 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
3829 $sections{$section}{amount} += $amount; #subtotal
3830 $sections{$section}{calls}++;
3831 $sections{$section}{duration} += $detail->duration;
3833 my $desc = $detail->regionname;
3834 my $description = $desc;
3835 $description = substr($desc, 0, 50). '...'
3836 if $format eq 'latex' && length($desc) > 50;
3838 $lines{$section}{$desc} ||= {
3839 description => &{$escape}($description),
3840 #pkgpart => $part_pkg->pkgpart,
3841 pkgnum => $cust_bill_pkg->pkgnum,
3846 #unit_amount => $cust_bill_pkg->unitrecur,
3847 quantity => $cust_bill_pkg->quantity,
3848 product_code => 'N/A',
3849 ext_description => [],
3852 $lines{$section}{$desc}{amount} += $amount;
3853 $lines{$section}{$desc}{calls}++;
3854 $lines{$section}{$desc}{duration} += $detail->duration;
3860 my %sectionmap = ();
3861 foreach (keys %sections) {
3862 my $usage_class = $usage_class{$classnums{$_}};
3863 $sectionmap{$_} = { 'description' => &{$escape}($_),
3864 'amount' => $sections{$_}{amount}, #subtotal
3865 'calls' => $sections{$_}{calls},
3866 'duration' => $sections{$_}{duration},
3868 'tax_section' => '',
3869 'sort_weight' => $usage_class->weight,
3870 ( $usage_class->format
3871 ? ( map { $_ => $usage_class->$_($format) }
3872 qw( description_generator header_generator total_generator total_line_generator )
3879 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
3883 foreach my $section ( keys %lines ) {
3884 foreach my $line ( keys %{$lines{$section}} ) {
3885 my $l = $lines{$section}{$line};
3886 $l->{section} = $sectionmap{$section};
3887 $l->{amount} = sprintf( "%.2f", $l->{amount} );
3888 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3893 return(\@sections, \@lines);
3899 my $end = $self->_date;
3900 my $start = $end - 2592000; # 30 days
3901 my $cust_main = $self->cust_main;
3902 my @pkgs = $cust_main->all_pkgs;
3903 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
3906 foreach my $pkg ( @pkgs ) {
3907 my @h_cust_svc = $pkg->h_cust_svc($end);
3908 foreach my $h_cust_svc ( @h_cust_svc ) {
3909 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
3910 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
3912 my $inserted = $h_cust_svc->date_inserted;
3913 my $deleted = $h_cust_svc->date_deleted;
3914 my $phone_inserted = $h_cust_svc->h_svc_x($inserted);
3916 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
3918 # DID either activated or ported in; cannot be both for same DID simultaneously
3919 if ($inserted >= $start && $inserted <= $end && $phone_inserted
3920 && (!$phone_inserted->lnp_status
3921 || $phone_inserted->lnp_status eq ''
3922 || $phone_inserted->lnp_status eq 'native')) {
3925 else { # this one not so clean, should probably move to (h_)svc_phone
3926 my $phone_portedin = qsearchs( 'h_svc_phone',
3927 { 'svcnum' => $h_cust_svc->svcnum,
3928 'lnp_status' => 'portedin' },
3929 FS::h_svc_phone->sql_h_searchs($end),
3931 $num_portedin++ if $phone_portedin;
3934 # DID either deactivated or ported out; cannot be both for same DID simultaneously
3935 if($deleted >= $start && $deleted <= $end && $phone_deleted
3936 && (!$phone_deleted->lnp_status
3937 || $phone_deleted->lnp_status ne 'portingout')) {
3940 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
3941 && $phone_deleted->lnp_status
3942 && $phone_deleted->lnp_status eq 'portingout') {
3946 # increment usage minutes
3947 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end);
3948 foreach my $cdr ( @cdrs ) {
3949 $minutes += $cdr->billsec/60;
3952 # don't look at this service again
3953 push @seen, $h_cust_svc->svcnum;
3957 $minutes = sprintf("%d", $minutes);
3958 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
3959 . "$num_deactivated Ported-Out: $num_portedout ",
3960 "Total Minutes: $minutes");
3963 sub _items_svc_phone_sections {
3972 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3973 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
3975 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3976 next unless $cust_bill_pkg->pkgnum > 0;
3978 my @header = $cust_bill_pkg->details_header;
3979 next unless scalar(@header);
3981 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
3983 my $phonenum = $detail->phonenum;
3984 next unless $phonenum;
3986 my $amount = $detail->amount;
3987 next unless $amount && $amount > 0;
3989 $sections{$phonenum} ||= { 'amount' => 0,
3992 'sort_weight' => -1,
3993 'phonenum' => $phonenum,
3995 $sections{$phonenum}{amount} += $amount; #subtotal
3996 $sections{$phonenum}{calls}++;
3997 $sections{$phonenum}{duration} += $detail->duration;
3999 my $desc = $detail->regionname;
4000 my $description = $desc;
4001 $description = substr($desc, 0, 50). '...'
4002 if $format eq 'latex' && length($desc) > 50;
4004 $lines{$phonenum}{$desc} ||= {
4005 description => &{$escape}($description),
4006 #pkgpart => $part_pkg->pkgpart,
4014 product_code => 'N/A',
4015 ext_description => [],
4018 $lines{$phonenum}{$desc}{amount} += $amount;
4019 $lines{$phonenum}{$desc}{calls}++;
4020 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4022 my $line = $usage_class{$detail->classnum}->classname;
4023 $sections{"$phonenum $line"} ||=
4027 'sort_weight' => $usage_class{$detail->classnum}->weight,
4028 'phonenum' => $phonenum,
4029 'header' => [ @header ],
4031 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4032 $sections{"$phonenum $line"}{calls}++;
4033 $sections{"$phonenum $line"}{duration} += $detail->duration;
4035 $lines{"$phonenum $line"}{$desc} ||= {
4036 description => &{$escape}($description),
4037 #pkgpart => $part_pkg->pkgpart,
4045 product_code => 'N/A',
4046 ext_description => [],
4049 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4050 $lines{"$phonenum $line"}{$desc}{calls}++;
4051 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4052 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4053 $detail->formatted('format' => $format);
4058 my %sectionmap = ();
4059 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4060 foreach ( keys %sections ) {
4061 my @header = @{ $sections{$_}{header} || [] };
4063 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4064 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4065 my $usage_class = $summary ? $simple : $usage_simple;
4066 my $ending = $summary ? ' usage charges' : '';
4069 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4071 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4072 'amount' => $sections{$_}{amount}, #subtotal
4073 'calls' => $sections{$_}{calls},
4074 'duration' => $sections{$_}{duration},
4076 'tax_section' => '',
4077 'phonenum' => $sections{$_}{phonenum},
4078 'sort_weight' => $sections{$_}{sort_weight},
4079 'post_total' => $summary, #inspire pagebreak
4081 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4082 qw( description_generator
4085 total_line_generator
4092 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4093 $a->{sort_weight} <=> $b->{sort_weight}
4098 foreach my $section ( keys %lines ) {
4099 foreach my $line ( keys %{$lines{$section}} ) {
4100 my $l = $lines{$section}{$line};
4101 $l->{section} = $sectionmap{$section};
4102 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4103 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4108 return(\@sections, \@lines);
4115 #my @display = scalar(@_)
4117 # : qw( _items_previous _items_pkg );
4118 # #: qw( _items_pkg );
4119 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4120 my @display = qw( _items_previous _items_pkg );
4123 foreach my $display ( @display ) {
4124 push @b, $self->$display(@_);
4129 sub _items_previous {
4131 my $cust_main = $self->cust_main;
4132 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4134 foreach ( @pr_cust_bill ) {
4135 my $date = $conf->exists('invoice_show_prior_due_date')
4136 ? 'due '. $_->due_date2str($date_format)
4137 : time2str($date_format, $_->_date);
4139 'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
4140 #'pkgpart' => 'N/A',
4142 'amount' => sprintf("%.2f", $_->owed),
4148 # 'description' => 'Previous Balance',
4149 # #'pkgpart' => 'N/A',
4150 # 'pkgnum' => 'N/A',
4151 # 'amount' => sprintf("%10.2f", $pr_total ),
4152 # 'ext_description' => [ map {
4153 # "Invoice ". $_->invnum.
4154 # " (". time2str("%x",$_->_date). ") ".
4155 # sprintf("%10.2f", $_->owed)
4156 # } @pr_cust_bill ],
4165 warn "$me _items_pkg searching for all package line items\n"
4168 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4170 warn "$me _items_pkg filtering line items\n"
4172 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4174 if ($options{section} && $options{section}->{condensed}) {
4176 warn "$me _items_pkg condensing section\n"
4180 local $Storable::canonical = 1;
4181 foreach ( @items ) {
4183 delete $item->{ref};
4184 delete $item->{ext_description};
4185 my $key = freeze($item);
4186 $itemshash{$key} ||= 0;
4187 $itemshash{$key} ++; # += $item->{quantity};
4189 @items = sort { $a->{description} cmp $b->{description} }
4190 map { my $i = thaw($_);
4191 $i->{quantity} = $itemshash{$_};
4193 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4199 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4206 return 0 unless $a->itemdesc cmp $b->itemdesc;
4207 return -1 if $b->itemdesc eq 'Tax';
4208 return 1 if $a->itemdesc eq 'Tax';
4209 return -1 if $b->itemdesc eq 'Other surcharges';
4210 return 1 if $a->itemdesc eq 'Other surcharges';
4211 $a->itemdesc cmp $b->itemdesc;
4216 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4217 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4220 sub _items_cust_bill_pkg {
4222 my $cust_bill_pkgs = shift;
4225 my $format = $opt{format} || '';
4226 my $escape_function = $opt{escape_function} || sub { shift };
4227 my $format_function = $opt{format_function} || '';
4228 my $unsquelched = $opt{unsquelched} || '';
4229 my $section = $opt{section}->{description} if $opt{section};
4230 my $summary_page = $opt{summary_page} || '';
4231 my $multilocation = $opt{multilocation} || '';
4232 my $multisection = $opt{multisection} || '';
4233 my $discount_show_always = 0;
4236 my ($s, $r, $u) = ( undef, undef, undef );
4237 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4240 warn "$me _items_cust_bill_pkg considering cust_bill_pkg $cust_bill_pkg\n"
4243 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
4244 && $conf->exists('discount-show-always'));
4246 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4247 if ( $_ && !$cust_bill_pkg->hidden ) {
4248 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4249 $_->{amount} =~ s/^\-0\.00$/0.00/;
4250 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4252 unless ( $_->{amount} == 0 && !$discount_show_always );
4257 foreach my $display ( grep { defined($section)
4258 ? $_->section eq $section
4261 #grep { !$_->summary || !$summary_page } # bunk!
4262 grep { !$_->summary || $multisection }
4263 $cust_bill_pkg->cust_bill_pkg_display
4267 warn "$me _items_cust_bill_pkg considering display item $display\n"
4270 my $type = $display->type;
4272 my $desc = $cust_bill_pkg->desc;
4273 $desc = substr($desc, 0, 50). '...'
4274 if $format eq 'latex' && length($desc) > 50;
4276 my %details_opt = ( 'format' => $format,
4277 'escape_function' => $escape_function,
4278 'format_function' => $format_function,
4281 if ( $cust_bill_pkg->pkgnum > 0 ) {
4283 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4286 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4288 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
4290 warn "$me _items_cust_bill_pkg adding setup\n"
4293 my $description = $desc;
4294 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
4297 unless ( $cust_pkg->part_pkg->hide_svc_detail
4298 || $cust_bill_pkg->hidden )
4301 push @d, map &{$escape_function}($_),
4302 $cust_pkg->h_labels_short($self->_date, undef, 'I')
4303 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4305 if ( $multilocation ) {
4306 my $loc = $cust_pkg->location_label;
4307 $loc = substr($loc, 0, 50). '...'
4308 if $format eq 'latex' && length($loc) > 50;
4309 push @d, &{$escape_function}($loc);
4314 push @d, $cust_bill_pkg->details(%details_opt)
4315 if $cust_bill_pkg->recur == 0;
4317 if ( $cust_bill_pkg->hidden ) {
4318 $s->{amount} += $cust_bill_pkg->setup;
4319 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4320 push @{ $s->{ext_description} }, @d;
4323 description => $description,
4324 #pkgpart => $part_pkg->pkgpart,
4325 pkgnum => $cust_bill_pkg->pkgnum,
4326 amount => $cust_bill_pkg->setup,
4327 unit_amount => $cust_bill_pkg->unitsetup,
4328 quantity => $cust_bill_pkg->quantity,
4329 ext_description => \@d,
4335 if ( ( $cust_bill_pkg->recur != 0 || $cust_bill_pkg->setup == 0 ||
4336 ($discount_show_always && $cust_bill_pkg->recur == 0) ) &&
4337 ( !$type || $type eq 'R' || $type eq 'U' )
4341 warn "$me _items_cust_bill_pkg adding recur/usage\n"
4344 my $is_summary = $display->summary;
4345 my $description = ($is_summary && $type && $type eq 'U')
4346 ? "Usage charges" : $desc;
4348 $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4349 " - ". time2str($date_format, $cust_bill_pkg->edate).
4351 unless $conf->exists('disable_line_item_date_ranges');
4355 #at least until cust_bill_pkg has "past" ranges in addition to
4356 #the "future" sdate/edate ones... see #3032
4357 my @dates = ( $self->_date );
4358 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4359 push @dates, $prev->sdate if $prev;
4360 push @dates, undef if !$prev;
4362 unless ( $cust_pkg->part_pkg->hide_svc_detail
4363 || $cust_bill_pkg->itemdesc
4364 || $cust_bill_pkg->hidden
4365 || $is_summary && $type && $type eq 'U' )
4368 warn "$me _items_cust_bill_pkg adding service details\n"
4371 push @d, map &{$escape_function}($_),
4372 $cust_pkg->h_labels_short(@dates, 'I')
4373 #$cust_bill_pkg->edate,
4374 #$cust_bill_pkg->sdate)
4375 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4377 warn "$me _items_cust_bill_pkg done adding service details\n"
4380 if ( $multilocation ) {
4381 my $loc = $cust_pkg->location_label;
4382 $loc = substr($loc, 0, 50). '...'
4383 if $format eq 'latex' && length($loc) > 50;
4384 push @d, &{$escape_function}($loc);
4389 warn "$me _items_cust_bill_pkg adding details\n"
4392 push @d, $cust_bill_pkg->details(%details_opt)
4393 unless ($is_summary || $type && $type eq 'R');
4395 warn "$me _items_cust_bill_pkg calculating amount\n"
4400 $amount = $cust_bill_pkg->recur;
4401 }elsif($type eq 'R') {
4402 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4403 }elsif($type eq 'U') {
4404 $amount = $cust_bill_pkg->usage;
4407 if ( !$type || $type eq 'R' ) {
4409 warn "$me _items_cust_bill_pkg adding recur\n"
4412 if ( $cust_bill_pkg->hidden ) {
4413 $r->{amount} += $amount;
4414 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4415 push @{ $r->{ext_description} }, @d;
4418 description => $description,
4419 #pkgpart => $part_pkg->pkgpart,
4420 pkgnum => $cust_bill_pkg->pkgnum,
4422 unit_amount => $cust_bill_pkg->unitrecur,
4423 quantity => $cust_bill_pkg->quantity,
4424 ext_description => \@d,
4428 } else { # $type eq 'U'
4430 warn "$me _items_cust_bill_pkg adding usage\n"
4433 if ( $cust_bill_pkg->hidden ) {
4434 $u->{amount} += $amount;
4435 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4436 push @{ $u->{ext_description} }, @d;
4439 description => $description,
4440 #pkgpart => $part_pkg->pkgpart,
4441 pkgnum => $cust_bill_pkg->pkgnum,
4443 unit_amount => $cust_bill_pkg->unitrecur,
4444 quantity => $cust_bill_pkg->quantity,
4445 ext_description => \@d,
4451 } # recurring or usage with recurring charge
4453 } else { #pkgnum tax or one-shot line item (??)
4455 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
4458 if ( $cust_bill_pkg->setup != 0 ) {
4460 'description' => $desc,
4461 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
4464 if ( $cust_bill_pkg->recur != 0 ) {
4466 'description' => "$desc (".
4467 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
4468 time2str($date_format, $cust_bill_pkg->edate). ')',
4469 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
4479 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
4482 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4484 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4485 $_->{amount} =~ s/^\-0\.00$/0.00/;
4486 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4488 unless ( $_->{amount} == 0 && !$discount_show_always );
4496 sub _items_credits {
4497 my( $self, %opt ) = @_;
4498 my $trim_len = $opt{'trim_len'} || 60;
4502 foreach ( $self->cust_credited ) {
4504 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4506 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4507 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4508 $reason = " ($reason) " if $reason;
4511 #'description' => 'Credit ref\#'. $_->crednum.
4512 # " (". time2str("%x",$_->cust_credit->_date) .")".
4514 'description' => 'Credit applied '.
4515 time2str($date_format,$_->cust_credit->_date). $reason,
4516 'amount' => sprintf("%.2f",$_->amount),
4524 sub _items_payments {
4528 #get & print payments
4529 foreach ( $self->cust_bill_pay ) {
4531 #something more elaborate if $_->amount ne ->cust_pay->paid ?
4534 'description' => "Payment received ".
4535 time2str($date_format,$_->cust_pay->_date ),
4536 'amount' => sprintf("%.2f", $_->amount )
4544 =item call_details [ OPTION => VALUE ... ]
4546 Returns an array of CSV strings representing the call details for this invoice
4547 The only option available is the boolean prepend_billed_number
4552 my ($self, %opt) = @_;
4554 my $format_function = sub { shift };
4556 if ($opt{prepend_billed_number}) {
4557 $format_function = sub {
4561 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4566 my @details = map { $_->details( 'format_function' => $format_function,
4567 'escape_function' => sub{ return() },
4571 $self->cust_bill_pkg;
4572 my $header = $details[0];
4573 ( $header, grep { $_ ne $header } @details );
4583 =item process_reprint
4587 sub process_reprint {
4588 process_re_X('print', @_);
4591 =item process_reemail
4595 sub process_reemail {
4596 process_re_X('email', @_);
4604 process_re_X('fax', @_);
4612 process_re_X('ftp', @_);
4619 sub process_respool {
4620 process_re_X('spool', @_);
4623 use Storable qw(thaw);
4627 my( $method, $job ) = ( shift, shift );
4628 warn "$me process_re_X $method for job $job\n" if $DEBUG;
4630 my $param = thaw(decode_base64(shift));
4631 warn Dumper($param) if $DEBUG;
4642 my($method, $job, %param ) = @_;
4644 warn "re_X $method for job $job with param:\n".
4645 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
4648 #some false laziness w/search/cust_bill.html
4650 my $orderby = 'ORDER BY cust_bill._date';
4652 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
4654 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4656 my @cust_bill = qsearch( {
4657 #'select' => "cust_bill.*",
4658 'table' => 'cust_bill',
4659 'addl_from' => $addl_from,
4661 'extra_sql' => $extra_sql,
4662 'order_by' => $orderby,
4666 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4668 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
4671 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
4672 foreach my $cust_bill ( @cust_bill ) {
4673 $cust_bill->$method();
4675 if ( $job ) { #progressbar foo
4677 if ( time - $min_sec > $last ) {
4678 my $error = $job->update_statustext(
4679 int( 100 * $num / scalar(@cust_bill) )
4681 die $error if $error;
4692 =head1 CLASS METHODS
4698 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
4703 my ($class, $start, $end) = @_;
4705 $class->paid_sql($start, $end). ' - '.
4706 $class->credited_sql($start, $end);
4711 Returns an SQL fragment to retreive the net amount (charged minus credited).
4716 my ($class, $start, $end) = @_;
4717 'charged - '. $class->credited_sql($start, $end);
4722 Returns an SQL fragment to retreive the amount paid against this invoice.
4727 my ($class, $start, $end) = @_;
4728 $start &&= "AND cust_bill_pay._date <= $start";
4729 $end &&= "AND cust_bill_pay._date > $end";
4730 $start = '' unless defined($start);
4731 $end = '' unless defined($end);
4732 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
4733 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
4738 Returns an SQL fragment to retreive the amount credited against this invoice.
4743 my ($class, $start, $end) = @_;
4744 $start &&= "AND cust_credit_bill._date <= $start";
4745 $end &&= "AND cust_credit_bill._date > $end";
4746 $start = '' unless defined($start);
4747 $end = '' unless defined($end);
4748 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
4749 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
4754 Returns an SQL fragment to retrieve the due date of an invoice.
4755 Currently only supported on PostgreSQL.
4763 cust_bill.invoice_terms,
4764 cust_main.invoice_terms,
4765 \''.($conf->config('invoice_default_terms') || '').'\'
4766 ), E\'Net (\\\\d+)\'
4768 ) * 86400 + cust_bill._date'
4771 =item search_sql_where HASHREF
4773 Class method which returns an SQL WHERE fragment to search for parameters
4774 specified in HASHREF. Valid parameters are
4780 List reference of start date, end date, as UNIX timestamps.
4790 List reference of charged limits (exclusive).
4794 List reference of charged limits (exclusive).
4798 flag, return open invoices only
4802 flag, return net invoices only
4806 =item newest_percust
4810 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
4814 sub search_sql_where {
4815 my($class, $param) = @_;
4817 warn "$me search_sql_where called with params: \n".
4818 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
4824 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
4825 push @search, "cust_main.agentnum = $1";
4829 if ( $param->{_date} ) {
4830 my($beginning, $ending) = @{$param->{_date}};
4832 push @search, "cust_bill._date >= $beginning",
4833 "cust_bill._date < $ending";
4837 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
4838 push @search, "cust_bill.invnum >= $1";
4840 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
4841 push @search, "cust_bill.invnum <= $1";
4845 if ( $param->{charged} ) {
4846 my @charged = ref($param->{charged})
4847 ? @{ $param->{charged} }
4848 : ($param->{charged});
4850 push @search, map { s/^charged/cust_bill.charged/; $_; }
4854 my $owed_sql = FS::cust_bill->owed_sql;
4857 if ( $param->{owed} ) {
4858 my @owed = ref($param->{owed})
4859 ? @{ $param->{owed} }
4861 push @search, map { s/^owed/$owed_sql/; $_; }
4866 push @search, "0 != $owed_sql"
4867 if $param->{'open'};
4868 push @search, '0 != '. FS::cust_bill->net_sql
4872 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
4873 if $param->{'days'};
4876 if ( $param->{'newest_percust'} ) {
4878 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
4879 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
4881 my @newest_where = map { my $x = $_;
4882 $x =~ s/\bcust_bill\./newest_cust_bill./g;
4885 grep ! /^cust_main./, @search;
4886 my $newest_where = scalar(@newest_where)
4887 ? ' AND '. join(' AND ', @newest_where)
4891 push @search, "cust_bill._date = (
4892 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
4893 WHERE newest_cust_bill.custnum = cust_bill.custnum
4899 #agent virtualization
4900 my $curuser = $FS::CurrentUser::CurrentUser;
4901 if ( $curuser->username eq 'fs_queue'
4902 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
4904 my $newuser = qsearchs('access_user', {
4905 'username' => $username,
4909 $curuser = $newuser;
4911 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
4914 push @search, $curuser->agentnums_sql;
4916 join(' AND ', @search );
4928 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
4929 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base