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
292 || $new->{'Hash'}{'cc_surcharge_replace_hack'};
298 =item add_cc_surcharge
304 sub add_cc_surcharge {
305 my ($self, $pkgnum, $amount) = (shift, shift, shift);
308 my $cust_bill_pkg = new FS::cust_bill_pkg({
309 'invnum' => $self->invnum,
313 $error = $cust_bill_pkg->insert;
314 return $error if $error;
316 $self->{'Hash'}{'cc_surcharge_replace_hack'} = 1;
317 $self->charged($self->charged+$amount);
318 $error = $self->replace;
319 return $error if $error;
321 $self->apply_payments_and_credits;
327 Checks all fields to make sure this is a valid invoice. If there is an error,
328 returns the error, otherwise returns false. Called by the insert and replace
337 $self->ut_numbern('invnum')
338 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
339 || $self->ut_numbern('_date')
340 || $self->ut_money('charged')
341 || $self->ut_numbern('printed')
342 || $self->ut_enum('closed', [ '', 'Y' ])
343 || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
344 || $self->ut_numbern('agent_invid') #varchar?
346 return $error if $error;
348 $self->_date(time) unless $self->_date;
350 $self->printed(0) if $self->printed eq '';
357 Returns the displayed invoice number for this invoice: agent_invid if
358 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
364 if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
365 return $self->agent_invid;
367 return $self->invnum;
373 Returns a list consisting of the total previous balance for this customer,
374 followed by the previous outstanding invoices (as FS::cust_bill objects also).
381 my @cust_bill = sort { $a->_date <=> $b->_date }
382 grep { $_->owed != 0 && $_->_date < $self->_date }
383 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
385 foreach ( @cust_bill ) { $total += $_->owed; }
391 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
398 { 'table' => 'cust_bill_pkg',
399 'hashref' => { 'invnum' => $self->invnum },
400 'order_by' => 'ORDER BY billpkgnum',
405 =item cust_bill_pkg_pkgnum PKGNUM
407 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
412 sub cust_bill_pkg_pkgnum {
413 my( $self, $pkgnum ) = @_;
415 { 'table' => 'cust_bill_pkg',
416 'hashref' => { 'invnum' => $self->invnum,
419 'order_by' => 'ORDER BY billpkgnum',
426 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
433 my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
434 $self->cust_bill_pkg;
436 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
441 Returns true if any of the packages (or their definitions) corresponding to the
442 line items for this invoice have the no_auto flag set.
448 grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
451 =item open_cust_bill_pkg
453 Returns the open line items for this invoice.
455 Note that cust_bill_pkg with both setup and recur fees are returned as two
456 separate line items, each with only one fee.
460 # modeled after cust_main::open_cust_bill
461 sub open_cust_bill_pkg {
464 # grep { $_->owed > 0 } $self->cust_bill_pkg
466 my %other = ( 'recur' => 'setup',
467 'setup' => 'recur', );
469 foreach my $field ( qw( recur setup )) {
470 push @open, map { $_->set( $other{$field}, 0 ); $_; }
471 grep { $_->owed($field) > 0 }
472 $self->cust_bill_pkg;
478 =item cust_bill_event
480 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
484 sub cust_bill_event {
486 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
489 =item num_cust_bill_event
491 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
495 sub num_cust_bill_event {
498 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
499 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
500 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
501 $sth->fetchrow_arrayref->[0];
506 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
510 #false laziness w/cust_pkg.pm
514 'table' => 'cust_event',
515 'addl_from' => 'JOIN part_event USING ( eventpart )',
516 'hashref' => { 'tablenum' => $self->invnum },
517 'extra_sql' => " AND eventtable = 'cust_bill' ",
523 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
527 #false laziness w/cust_pkg.pm
531 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
532 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
533 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
534 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
535 $sth->fetchrow_arrayref->[0];
540 Returns the customer (see L<FS::cust_main>) for this invoice.
546 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
549 =item cust_suspend_if_balance_over AMOUNT
551 Suspends the customer associated with this invoice if the total amount owed on
552 this invoice and all older invoices is greater than the specified amount.
554 Returns a list: an empty list on success or a list of errors.
558 sub cust_suspend_if_balance_over {
559 my( $self, $amount ) = ( shift, shift );
560 my $cust_main = $self->cust_main;
561 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
564 $cust_main->suspend(@_);
570 Depreciated. See the cust_credited method.
572 #Returns a list consisting of the total previous credited (see
573 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
574 #outstanding credits (FS::cust_credit objects).
580 croak "FS::cust_bill->cust_credit depreciated; see ".
581 "FS::cust_bill->cust_credit_bill";
584 #my @cust_credit = sort { $a->_date <=> $b->_date }
585 # grep { $_->credited != 0 && $_->_date < $self->_date }
586 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
588 #foreach (@cust_credit) { $total += $_->credited; }
589 #$total, @cust_credit;
594 Depreciated. See the cust_bill_pay method.
596 #Returns all payments (see L<FS::cust_pay>) for this invoice.
602 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
604 #sort { $a->_date <=> $b->_date }
605 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
611 qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
614 sub cust_bill_pay_batch {
616 qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
621 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
627 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
628 sort { $a->_date <=> $b->_date }
629 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
634 =item cust_credit_bill
636 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
642 map { $_ } #return $self->num_cust_credit_bill unless wantarray;
643 sort { $a->_date <=> $b->_date }
644 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
648 sub cust_credit_bill {
649 shift->cust_credited(@_);
652 =item cust_bill_pay_pkgnum PKGNUM
654 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
655 with matching pkgnum.
659 sub cust_bill_pay_pkgnum {
660 my( $self, $pkgnum ) = @_;
661 map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
662 sort { $a->_date <=> $b->_date }
663 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
669 =item cust_credited_pkgnum PKGNUM
671 =item cust_credit_bill_pkgnum PKGNUM
673 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
674 with matching pkgnum.
678 sub cust_credited_pkgnum {
679 my( $self, $pkgnum ) = @_;
680 map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
681 sort { $a->_date <=> $b->_date }
682 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
688 sub cust_credit_bill_pkgnum {
689 shift->cust_credited_pkgnum(@_);
694 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
701 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
703 foreach (@taxlines) { $total += $_->setup; }
709 Returns the amount owed (still outstanding) on this invoice, which is charged
710 minus all payment applications (see L<FS::cust_bill_pay>) and credit
711 applications (see L<FS::cust_credit_bill>).
717 my $balance = $self->charged;
718 $balance -= $_->amount foreach ( $self->cust_bill_pay );
719 $balance -= $_->amount foreach ( $self->cust_credited );
720 $balance = sprintf( "%.2f", $balance);
721 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
726 my( $self, $pkgnum ) = @_;
728 #my $balance = $self->charged;
730 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
732 $balance -= $_->amount for $self->cust_bill_pay_pkgnum($pkgnum);
733 $balance -= $_->amount for $self->cust_credited_pkgnum($pkgnum);
735 $balance = sprintf( "%.2f", $balance);
736 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
740 =item apply_payments_and_credits [ OPTION => VALUE ... ]
742 Applies unapplied payments and credits to this invoice.
744 A hash of optional arguments may be passed. Currently "manual" is supported.
745 If true, a payment receipt is sent instead of a statement when
746 'payment_receipt_email' configuration option is set.
748 If there is an error, returns the error, otherwise returns false.
752 sub apply_payments_and_credits {
753 my( $self, %options ) = @_;
755 local $SIG{HUP} = 'IGNORE';
756 local $SIG{INT} = 'IGNORE';
757 local $SIG{QUIT} = 'IGNORE';
758 local $SIG{TERM} = 'IGNORE';
759 local $SIG{TSTP} = 'IGNORE';
760 local $SIG{PIPE} = 'IGNORE';
762 my $oldAutoCommit = $FS::UID::AutoCommit;
763 local $FS::UID::AutoCommit = 0;
766 $self->select_for_update; #mutex
768 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
769 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
771 if ( $conf->exists('pkg-balances') ) {
772 # limit @payments & @credits to those w/ a pkgnum grepped from $self
773 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
774 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
775 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
778 while ( $self->owed > 0 and ( @payments || @credits ) ) {
781 if ( @payments && @credits ) {
783 #decide which goes first by weight of top (unapplied) line item
785 my @open_lineitems = $self->open_cust_bill_pkg;
788 max( map { $_->part_pkg->pay_weight || 0 }
793 my $max_credit_weight =
794 max( map { $_->part_pkg->credit_weight || 0 }
800 #if both are the same... payments first? it has to be something
801 if ( $max_pay_weight >= $max_credit_weight ) {
807 } elsif ( @payments ) {
809 } elsif ( @credits ) {
812 die "guru meditation #12 and 35";
816 if ( $app eq 'pay' ) {
818 my $payment = shift @payments;
819 $unapp_amount = $payment->unapplied;
820 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
821 $app->pkgnum( $payment->pkgnum )
822 if $conf->exists('pkg-balances') && $payment->pkgnum;
824 } elsif ( $app eq 'credit' ) {
826 my $credit = shift @credits;
827 $unapp_amount = $credit->credited;
828 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
829 $app->pkgnum( $credit->pkgnum )
830 if $conf->exists('pkg-balances') && $credit->pkgnum;
833 die "guru meditation #12 and 35";
837 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
838 warn "owed_pkgnum ". $app->pkgnum;
839 $owed = $self->owed_pkgnum($app->pkgnum);
843 next unless $owed > 0;
845 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
846 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
848 $app->invnum( $self->invnum );
850 my $error = $app->insert(%options);
852 $dbh->rollback if $oldAutoCommit;
853 return "Error inserting ". $app->table. " record: $error";
855 die $error if $error;
859 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
864 =item generate_email OPTION => VALUE ...
872 sender address, required
876 alternate template name, optional
880 text attachment arrayref, optional
884 email subject, optional
888 notice name instead of "Invoice", optional
892 Returns an argument list to be passed to L<FS::Misc::send_email>.
903 my $me = '[FS::cust_bill::generate_email]';
906 'from' => $args{'from'},
907 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
911 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
912 'template' => $args{'template'},
913 'notice_name' => ( $args{'notice_name'} || 'Invoice' ),
916 my $cust_main = $self->cust_main;
918 if (ref($args{'to'}) eq 'ARRAY') {
919 $return{'to'} = $args{'to'};
921 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
922 $cust_main->invoicing_list
926 if ( $conf->exists('invoice_html') ) {
928 warn "$me creating HTML/text multipart message"
931 $return{'nobody'} = 1;
933 my $alternative = build MIME::Entity
934 'Type' => 'multipart/alternative',
935 'Encoding' => '7bit',
936 'Disposition' => 'inline'
940 if ( $conf->exists('invoice_email_pdf')
941 and scalar($conf->config('invoice_email_pdf_note')) ) {
943 warn "$me using 'invoice_email_pdf_note' in multipart message"
945 $data = [ map { $_ . "\n" }
946 $conf->config('invoice_email_pdf_note')
951 warn "$me not using 'invoice_email_pdf_note' in multipart message"
953 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
954 $data = $args{'print_text'};
956 $data = [ $self->print_text(\%opt) ];
961 $alternative->attach(
962 'Type' => 'text/plain',
963 #'Encoding' => 'quoted-printable',
964 'Encoding' => '7bit',
966 'Disposition' => 'inline',
969 $args{'from'} =~ /\@([\w\.\-]+)/;
970 my $from = $1 || 'example.com';
971 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
974 my $agentnum = $cust_main->agentnum;
975 if ( defined($args{'template'}) && length($args{'template'})
976 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
979 $logo = 'logo_'. $args{'template'}. '.png';
983 my $image_data = $conf->config_binary( $logo, $agentnum);
985 my $image = build MIME::Entity
986 'Type' => 'image/png',
987 'Encoding' => 'base64',
988 'Data' => $image_data,
989 'Filename' => 'logo.png',
990 'Content-ID' => "<$content_id>",
993 $alternative->attach(
994 'Type' => 'text/html',
995 'Encoding' => 'quoted-printable',
996 'Data' => [ '<html>',
999 ' '. encode_entities($return{'subject'}),
1002 ' <body bgcolor="#e8e8e8">',
1003 $self->print_html({ 'cid'=>$content_id, %opt }),
1007 'Disposition' => 'inline',
1008 #'Filename' => 'invoice.pdf',
1011 my @otherparts = ();
1012 if ( $cust_main->email_csv_cdr ) {
1014 push @otherparts, build MIME::Entity
1015 'Type' => 'text/csv',
1016 'Encoding' => '7bit',
1017 'Data' => [ map { "$_\n" }
1018 $self->call_details('prepend_billed_number' => 1)
1020 'Disposition' => 'attachment',
1021 'Filename' => 'usage-'. $self->invnum. '.csv',
1026 if ( $conf->exists('invoice_email_pdf') ) {
1031 # multipart/alternative
1037 my $related = build MIME::Entity 'Type' => 'multipart/related',
1038 'Encoding' => '7bit';
1040 #false laziness w/Misc::send_email
1041 $related->head->replace('Content-type',
1042 $related->mime_type.
1043 '; boundary="'. $related->head->multipart_boundary. '"'.
1044 '; type=multipart/alternative'
1047 $related->add_part($alternative);
1049 $related->add_part($image);
1051 my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
1053 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
1057 #no other attachment:
1059 # multipart/alternative
1064 $return{'content-type'} = 'multipart/related';
1065 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
1066 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
1067 #$return{'disposition'} = 'inline';
1073 if ( $conf->exists('invoice_email_pdf') ) {
1074 warn "$me creating PDF attachment"
1077 #mime parts arguments a la MIME::Entity->build().
1078 $return{'mimeparts'} = [
1079 { $self->mimebuild_pdf(\%opt) }
1083 if ( $conf->exists('invoice_email_pdf')
1084 and scalar($conf->config('invoice_email_pdf_note')) ) {
1086 warn "$me using 'invoice_email_pdf_note'"
1088 $return{'body'} = [ map { $_ . "\n" }
1089 $conf->config('invoice_email_pdf_note')
1094 warn "$me not using 'invoice_email_pdf_note'"
1096 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1097 $return{'body'} = $args{'print_text'};
1099 $return{'body'} = [ $self->print_text(\%opt) ];
1112 Returns a list suitable for passing to MIME::Entity->build(), representing
1113 this invoice as PDF attachment.
1120 'Type' => 'application/pdf',
1121 'Encoding' => 'base64',
1122 'Data' => [ $self->print_pdf(@_) ],
1123 'Disposition' => 'attachment',
1124 'Filename' => 'invoice-'. $self->invnum. '.pdf',
1128 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1130 Sends this invoice to the destinations configured for this customer: sends
1131 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1133 Options can be passed as a hashref (recommended) or as a list of up to
1134 four values for templatename, agentnum, invoice_from and amount.
1136 I<template>, if specified, is the name of a suffix for alternate invoices.
1138 I<agentnum>, if specified, means that this invoice will only be sent for customers
1139 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
1140 single agent) or an arrayref of agentnums.
1142 I<invoice_from>, if specified, overrides the default email invoice From: address.
1144 I<amount>, if specified, only sends the invoice if the total amount owed on this
1145 invoice and all older invoices is greater than the specified amount.
1147 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1151 sub queueable_send {
1154 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1155 or die "invalid invoice number: " . $opt{invnum};
1157 my @args = ( $opt{template}, $opt{agentnum} );
1158 push @args, $opt{invoice_from}
1159 if exists($opt{invoice_from}) && $opt{invoice_from};
1161 my $error = $self->send( @args );
1162 die $error if $error;
1169 my( $template, $invoice_from, $notice_name );
1171 my $balance_over = 0;
1175 $template = $opt->{'template'} || '';
1176 if ( $agentnums = $opt->{'agentnum'} ) {
1177 $agentnums = [ $agentnums ] unless ref($agentnums);
1179 $invoice_from = $opt->{'invoice_from'};
1180 $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1181 $notice_name = $opt->{'notice_name'};
1183 $template = scalar(@_) ? shift : '';
1184 if ( scalar(@_) && $_[0] ) {
1185 $agentnums = ref($_[0]) ? shift : [ shift ];
1187 $invoice_from = shift if scalar(@_);
1188 $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1191 return 'N/A' unless ! $agentnums
1192 or grep { $_ == $self->cust_main->agentnum } @$agentnums;
1195 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
1197 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1198 $conf->config('invoice_from', $self->cust_main->agentnum );
1201 'template' => $template,
1202 'invoice_from' => $invoice_from,
1203 'notice_name' => ( $notice_name || 'Invoice' ),
1206 my @invoicing_list = $self->cust_main->invoicing_list;
1208 #$self->email_invoice(\%opt)
1210 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
1212 #$self->print_invoice(\%opt)
1214 if grep { $_ eq 'POST' } @invoicing_list; #postal
1216 $self->fax_invoice(\%opt)
1217 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1223 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
1225 Emails this invoice.
1227 Options can be passed as a hashref (recommended) or as a list of up to
1228 two values for templatename and invoice_from.
1230 I<template>, if specified, is the name of a suffix for alternate invoices.
1232 I<invoice_from>, if specified, overrides the default email invoice From: address.
1234 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1238 sub queueable_email {
1241 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1242 or die "invalid invoice number: " . $opt{invnum};
1244 my @args = ( $opt{template} );
1245 push @args, $opt{invoice_from}
1246 if exists($opt{invoice_from}) && $opt{invoice_from};
1248 my $error = $self->email( @args );
1249 die $error if $error;
1253 #sub email_invoice {
1257 my( $template, $invoice_from, $notice_name );
1260 $template = $opt->{'template'} || '';
1261 $invoice_from = $opt->{'invoice_from'};
1262 $notice_name = $opt->{'notice_name'} || 'Invoice';
1264 $template = scalar(@_) ? shift : '';
1265 $invoice_from = shift if scalar(@_);
1266 $notice_name = 'Invoice';
1269 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1270 $conf->config('invoice_from', $self->cust_main->agentnum );
1272 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1273 $self->cust_main->invoicing_list;
1275 if ( ! @invoicing_list ) { #no recipients
1276 if ( $conf->exists('cust_bill-no_recipients-error') ) {
1277 die 'No recipients for customer #'. $self->custnum;
1279 #default: better to notify this person than silence
1280 @invoicing_list = ($invoice_from);
1284 my $subject = $self->email_subject($template);
1286 my $error = send_email(
1287 $self->generate_email(
1288 'from' => $invoice_from,
1289 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1290 'subject' => $subject,
1291 'template' => $template,
1292 'notice_name' => $notice_name,
1295 die "can't email invoice: $error\n" if $error;
1296 #die "$error\n" if $error;
1303 #my $template = scalar(@_) ? shift : '';
1306 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1309 my $cust_main = $self->cust_main;
1310 my $name = $cust_main->name;
1311 my $name_short = $cust_main->name_short;
1312 my $invoice_number = $self->invnum;
1313 my $invoice_date = $self->_date_pretty;
1315 eval qq("$subject");
1318 =item lpr_data HASHREF | [ TEMPLATE ]
1320 Returns the postscript or plaintext for this invoice as an arrayref.
1322 Options can be passed as a hashref (recommended) or as a single optional value
1325 I<template>, if specified, is the name of a suffix for alternate invoices.
1327 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1333 my( $template, $notice_name );
1336 $template = $opt->{'template'} || '';
1337 $notice_name = $opt->{'notice_name'} || 'Invoice';
1339 $template = scalar(@_) ? shift : '';
1340 $notice_name = 'Invoice';
1344 'template' => $template,
1345 'notice_name' => $notice_name,
1348 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1349 [ $self->$method( \%opt ) ];
1352 =item print HASHREF | [ TEMPLATE ]
1354 Prints this invoice.
1356 Options can be passed as a hashref (recommended) or as a single optional
1359 I<template>, if specified, is the name of a suffix for alternate invoices.
1361 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1365 #sub print_invoice {
1368 my( $template, $notice_name );
1371 $template = $opt->{'template'} || '';
1372 $notice_name = $opt->{'notice_name'} || 'Invoice';
1374 $template = scalar(@_) ? shift : '';
1375 $notice_name = 'Invoice';
1379 'template' => $template,
1380 'notice_name' => $notice_name,
1383 if($conf->exists('invoice_print_pdf')) {
1384 # Add the invoice to the current batch.
1385 $self->batch_invoice(\%opt);
1388 do_print $self->lpr_data(\%opt);
1392 =item fax_invoice HASHREF | [ TEMPLATE ]
1396 Options can be passed as a hashref (recommended) or as a single optional
1399 I<template>, if specified, is the name of a suffix for alternate invoices.
1401 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1407 my( $template, $notice_name );
1410 $template = $opt->{'template'} || '';
1411 $notice_name = $opt->{'notice_name'} || 'Invoice';
1413 $template = scalar(@_) ? shift : '';
1414 $notice_name = 'Invoice';
1417 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1418 unless $conf->exists('invoice_latex');
1420 my $dialstring = $self->cust_main->getfield('fax');
1424 'template' => $template,
1425 'notice_name' => $notice_name,
1428 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1429 'dialstring' => $dialstring,
1431 die $error if $error;
1435 =item batch_invoice [ HASHREF ]
1437 Place this invoice into the open batch (see C<FS::bill_batch>). If there
1438 isn't an open batch, one will be created.
1443 my ($self, $opt) = @_;
1444 my $batch = FS::bill_batch->get_open_batch;
1445 my $cust_bill_batch = FS::cust_bill_batch->new({
1446 batchnum => $batch->batchnum,
1447 invnum => $self->invnum,
1449 return $cust_bill_batch->insert($opt);
1452 =item ftp_invoice [ TEMPLATENAME ]
1454 Sends this invoice data via FTP.
1456 TEMPLATENAME is unused?
1462 my $template = scalar(@_) ? shift : '';
1465 'protocol' => 'ftp',
1466 'server' => $conf->config('cust_bill-ftpserver'),
1467 'username' => $conf->config('cust_bill-ftpusername'),
1468 'password' => $conf->config('cust_bill-ftppassword'),
1469 'dir' => $conf->config('cust_bill-ftpdir'),
1470 'format' => $conf->config('cust_bill-ftpformat'),
1474 =item spool_invoice [ TEMPLATENAME ]
1476 Spools this invoice data (see L<FS::spool_csv>)
1478 TEMPLATENAME is unused?
1484 my $template = scalar(@_) ? shift : '';
1487 'format' => $conf->config('cust_bill-spoolformat'),
1488 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1492 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1494 Like B<send>, but only sends the invoice if it is the newest open invoice for
1499 sub send_if_newest {
1504 grep { $_->owed > 0 }
1505 qsearch('cust_bill', {
1506 'custnum' => $self->custnum,
1507 #'_date' => { op=>'>', value=>$self->_date },
1508 'invnum' => { op=>'>', value=>$self->invnum },
1515 =item send_csv OPTION => VALUE, ...
1517 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1521 protocol - currently only "ftp"
1527 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1528 and YYMMDDHHMMSS is a timestamp.
1530 See L</print_csv> for a description of the output format.
1535 my($self, %opt) = @_;
1539 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1540 mkdir $spooldir, 0700 unless -d $spooldir;
1542 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1543 my $file = "$spooldir/$tracctnum.csv";
1545 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1547 open(CSV, ">$file") or die "can't open $file: $!";
1555 if ( $opt{protocol} eq 'ftp' ) {
1556 eval "use Net::FTP;";
1558 $net = Net::FTP->new($opt{server}) or die @$;
1560 die "unknown protocol: $opt{protocol}";
1563 $net->login( $opt{username}, $opt{password} )
1564 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1566 $net->binary or die "can't set binary mode";
1568 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1570 $net->put($file) or die "can't put $file: $!";
1580 Spools CSV invoice data.
1586 =item format - 'default' or 'billco'
1588 =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>).
1590 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1592 =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.
1599 my($self, %opt) = @_;
1601 my $cust_main = $self->cust_main;
1603 if ( $opt{'dest'} ) {
1604 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1605 $cust_main->invoicing_list;
1606 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1607 || ! keys %invoicing_list;
1610 if ( $opt{'balanceover'} ) {
1612 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1615 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1616 mkdir $spooldir, 0700 unless -d $spooldir;
1618 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1622 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1623 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1626 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1628 open(CSV, ">>$file") or die "can't open $file: $!";
1629 flock(CSV, LOCK_EX);
1634 if ( lc($opt{'format'}) eq 'billco' ) {
1636 flock(CSV, LOCK_UN);
1641 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1644 open(CSV,">>$file") or die "can't open $file: $!";
1645 flock(CSV, LOCK_EX);
1651 flock(CSV, LOCK_UN);
1658 =item print_csv OPTION => VALUE, ...
1660 Returns CSV data for this invoice.
1664 format - 'default' or 'billco'
1666 Returns a list consisting of two scalars. The first is a single line of CSV
1667 header information for this invoice. The second is one or more lines of CSV
1668 detail information for this invoice.
1670 If I<format> is not specified or "default", the fields of the CSV file are as
1673 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1677 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1679 B<record_type> is C<cust_bill> for the initial header line only. The
1680 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1681 fields are filled in.
1683 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1684 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1687 =item invnum - invoice number
1689 =item custnum - customer number
1691 =item _date - invoice date
1693 =item charged - total invoice amount
1695 =item first - customer first name
1697 =item last - customer first name
1699 =item company - company name
1701 =item address1 - address line 1
1703 =item address2 - address line 1
1713 =item pkg - line item description
1715 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1717 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1719 =item sdate - start date for recurring fee
1721 =item edate - end date for recurring fee
1725 If I<format> is "billco", the fields of the header CSV file are as follows:
1727 +-------------------------------------------------------------------+
1728 | FORMAT HEADER FILE |
1729 |-------------------------------------------------------------------|
1730 | Field | Description | Name | Type | Width |
1731 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1732 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1733 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1734 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1735 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1736 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1737 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1738 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1739 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1740 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1741 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1742 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1743 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1744 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1745 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1746 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1747 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1748 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1749 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1750 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1751 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1752 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1753 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1754 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1755 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1756 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1757 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1758 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1759 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1760 +-------+-------------------------------+------------+------+-------+
1762 If I<format> is "billco", the fields of the detail CSV file are as follows:
1764 FORMAT FOR DETAIL FILE
1766 Field | Description | Name | Type | Width
1767 1 | N/A-Leave Empty | RC | CHAR | 2
1768 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1769 3 | Account Number | TRACCTNUM | CHAR | 15
1770 4 | Invoice Number | TRINVOICE | CHAR | 15
1771 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1772 6 | Transaction Detail | DETAILS | CHAR | 100
1773 7 | Amount | AMT | NUM* | 9
1774 8 | Line Format Control** | LNCTRL | CHAR | 2
1775 9 | Grouping Code | GROUP | CHAR | 2
1776 10 | User Defined | ACCT CODE | CHAR | 15
1781 my($self, %opt) = @_;
1783 eval "use Text::CSV_XS";
1786 my $cust_main = $self->cust_main;
1788 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1790 if ( lc($opt{'format'}) eq 'billco' ) {
1793 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1795 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1797 my( $previous_balance, @unused ) = $self->previous; #previous balance
1799 my $pmt_cr_applied = 0;
1800 $pmt_cr_applied += $_->{'amount'}
1801 foreach ( $self->_items_payments, $self->_items_credits ) ;
1803 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1806 '', # 1 | N/A-Leave Empty CHAR 2
1807 '', # 2 | N/A-Leave Empty CHAR 15
1808 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1809 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1810 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1811 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1812 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1813 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1814 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1815 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1816 '', # 10 | Ancillary Billing Information CHAR 30
1817 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1818 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1821 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1824 $duedate, # 14 | Bill Due Date CHAR 10
1826 $previous_balance, # 15 | Previous Balance NUM* 9
1827 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1828 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1829 $totaldue, # 18 | Total Amt Due NUM* 9
1830 $totaldue, # 19 | Total Amt Due NUM* 9
1831 '', # 20 | 30 Day Aging NUM* 9
1832 '', # 21 | 60 Day Aging NUM* 9
1833 '', # 22 | 90 Day Aging NUM* 9
1834 'N', # 23 | Y/N CHAR 1
1835 '', # 24 | Remittance automation CHAR 100
1836 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1837 $self->custnum, # 26 | Customer Reference Number CHAR 15
1838 '0', # 27 | Federal Tax*** NUM* 9
1839 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1840 '0', # 29 | Other Taxes & Fees*** NUM* 9
1849 time2str("%x", $self->_date),
1850 sprintf("%.2f", $self->charged),
1851 ( map { $cust_main->getfield($_) }
1852 qw( first last company address1 address2 city state zip country ) ),
1854 ) or die "can't create csv";
1857 my $header = $csv->string. "\n";
1860 if ( lc($opt{'format'}) eq 'billco' ) {
1863 foreach my $item ( $self->_items_pkg ) {
1866 '', # 1 | N/A-Leave Empty CHAR 2
1867 '', # 2 | N/A-Leave Empty CHAR 15
1868 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1869 $self->invnum, # 4 | Invoice Number CHAR 15
1870 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1871 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1872 $item->{'amount'}, # 7 | Amount NUM* 9
1873 '', # 8 | Line Format Control** CHAR 2
1874 '', # 9 | Grouping Code CHAR 2
1875 '', # 10 | User Defined CHAR 15
1878 $detail .= $csv->string. "\n";
1884 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1886 my($pkg, $setup, $recur, $sdate, $edate);
1887 if ( $cust_bill_pkg->pkgnum ) {
1889 ($pkg, $setup, $recur, $sdate, $edate) = (
1890 $cust_bill_pkg->part_pkg->pkg,
1891 ( $cust_bill_pkg->setup != 0
1892 ? sprintf("%.2f", $cust_bill_pkg->setup )
1894 ( $cust_bill_pkg->recur != 0
1895 ? sprintf("%.2f", $cust_bill_pkg->recur )
1897 ( $cust_bill_pkg->sdate
1898 ? time2str("%x", $cust_bill_pkg->sdate)
1900 ($cust_bill_pkg->edate
1901 ?time2str("%x", $cust_bill_pkg->edate)
1905 } else { #pkgnum tax
1906 next unless $cust_bill_pkg->setup != 0;
1907 $pkg = $cust_bill_pkg->desc;
1908 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
1909 ( $sdate, $edate ) = ( '', '' );
1915 ( map { '' } (1..11) ),
1916 ($pkg, $setup, $recur, $sdate, $edate)
1917 ) or die "can't create csv";
1919 $detail .= $csv->string. "\n";
1925 ( $header, $detail );
1931 Pays this invoice with a compliemntary payment. If there is an error,
1932 returns the error, otherwise returns false.
1938 my $cust_pay = new FS::cust_pay ( {
1939 'invnum' => $self->invnum,
1940 'paid' => $self->owed,
1943 'payinfo' => $self->cust_main->payinfo,
1951 Attempts to pay this invoice with a credit card payment via a
1952 Business::OnlinePayment realtime gateway. See
1953 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1954 for supported processors.
1960 $self->realtime_bop( 'CC', @_ );
1965 Attempts to pay this invoice with an electronic check (ACH) payment via a
1966 Business::OnlinePayment realtime gateway. See
1967 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1968 for supported processors.
1974 $self->realtime_bop( 'ECHECK', @_ );
1979 Attempts to pay this invoice with phone bill (LEC) payment via a
1980 Business::OnlinePayment realtime gateway. See
1981 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1982 for supported processors.
1988 $self->realtime_bop( 'LEC', @_ );
1992 my( $self, $method ) = (shift,shift);
1995 my $cust_main = $self->cust_main;
1996 my $balance = $cust_main->balance;
1997 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1998 $amount = sprintf("%.2f", $amount);
1999 return "not run (balance $balance)" unless $amount > 0;
2001 my $description = 'Internet Services';
2002 if ( $conf->exists('business-onlinepayment-description') ) {
2003 my $dtempl = $conf->config('business-onlinepayment-description');
2005 my $agent_obj = $cust_main->agent
2006 or die "can't retreive agent for $cust_main (agentnum ".
2007 $cust_main->agentnum. ")";
2008 my $agent = $agent_obj->agent;
2009 my $pkgs = join(', ',
2010 map { $_->part_pkg->pkg }
2011 grep { $_->pkgnum } $self->cust_bill_pkg
2013 $description = eval qq("$dtempl");
2016 $cust_main->realtime_bop($method, $amount,
2017 'description' => $description,
2018 'invnum' => $self->invnum,
2019 #this didn't do what we want, it just calls apply_payments_and_credits
2021 'apply_to_invoice' => 1,
2024 #this changes application behavior: auto payments
2025 #triggered against a specific invoice are now applied
2026 #to that invoice instead of oldest open.
2032 =item batch_card OPTION => VALUE...
2034 Adds a payment for this invoice to the pending credit card batch (see
2035 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2036 runs the payment using a realtime gateway.
2041 my ($self, %options) = @_;
2042 my $cust_main = $self->cust_main;
2044 $options{invnum} = $self->invnum;
2046 $cust_main->batch_card(%options);
2049 sub _agent_template {
2051 $self->cust_main->agent_template;
2054 sub _agent_invoice_from {
2056 $self->cust_main->agent_invoice_from;
2059 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2061 Returns an text invoice, as a list of lines.
2063 Options can be passed as a hashref (recommended) or as a list of time, template
2064 and then any key/value pairs for any other options.
2066 I<time>, if specified, is used to control the printing of overdue messages. The
2067 default is now. It isn't the date of the invoice; that's the `_date' field.
2068 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2069 L<Time::Local> and L<Date::Parse> for conversion functions.
2071 I<template>, if specified, is the name of a suffix for alternate invoices.
2073 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2079 my( $today, $template, %opt );
2081 %opt = %{ shift() };
2082 $today = delete($opt{'time'}) || '';
2083 $template = delete($opt{template}) || '';
2085 ( $today, $template, %opt ) = @_;
2088 my %params = ( 'format' => 'template' );
2089 $params{'time'} = $today if $today;
2090 $params{'template'} = $template if $template;
2091 $params{$_} = $opt{$_}
2092 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2094 $self->print_generic( %params );
2097 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2099 Internal method - returns a filename of a filled-in LaTeX template for this
2100 invoice (Note: add ".tex" to get the actual filename), and a filename of
2101 an associated logo (with the .eps extension included).
2103 See print_ps and print_pdf for methods that return PostScript and PDF output.
2105 Options can be passed as a hashref (recommended) or as a list of time, template
2106 and then any key/value pairs for any other options.
2108 I<time>, if specified, is used to control the printing of overdue messages. The
2109 default is now. It isn't the date of the invoice; that's the `_date' field.
2110 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2111 L<Time::Local> and L<Date::Parse> for conversion functions.
2113 I<template>, if specified, is the name of a suffix for alternate invoices.
2115 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2121 my( $today, $template, %opt );
2123 %opt = %{ shift() };
2124 $today = delete($opt{'time'}) || '';
2125 $template = delete($opt{template}) || '';
2127 ( $today, $template, %opt ) = @_;
2130 my %params = ( 'format' => 'latex' );
2131 $params{'time'} = $today if $today;
2132 $params{'template'} = $template if $template;
2133 $params{$_} = $opt{$_}
2134 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2136 $template ||= $self->_agent_template;
2138 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2139 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2143 ) or die "can't open temp file: $!\n";
2145 my $agentnum = $self->cust_main->agentnum;
2147 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2148 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2149 or die "can't write temp file: $!\n";
2151 print $lh $conf->config_binary('logo.eps', $agentnum)
2152 or die "can't write temp file: $!\n";
2155 $params{'logo_file'} = $lh->filename;
2157 if($conf->exists('invoice-barcode')){
2158 my $gdbar = new GD::Barcode('Code39',$self->invnum);
2159 die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2160 my $gd = $gdbar->plot(Height => 20);
2161 my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2165 ) or die "can't open temp file: $!\n";
2166 print $bh $gd->png or die "cannot write barcode to file: $!\n";
2168 my $png_file = $bh->filename;
2171 my $eps_file = $png_file;
2172 $eps_file =~ s/\.png$/.eps/g;
2173 $png_file =~ /(barcode.*png)/;
2175 $eps_file =~ /(barcode.*eps)/;
2178 my $curr_dir = cwd();
2180 # after painfuly long experimentation, it was determined that sam2p won't
2181 # accept : and other chars in the path, no matter how hard I tried to
2182 # escape them, hence the chdir (and chdir back, just to be safe)
2183 system('sam2p', $png_file, 'EPS:', $eps_file ) == 0
2184 or die "sam2p failed: $!\n";
2188 $params{'barcode_file'} = $eps_file;
2191 my @filled_in = $self->print_generic( %params );
2193 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2197 ) or die "can't open temp file: $!\n";
2198 print $fh join('', @filled_in );
2201 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2202 return ($1, $params{'logo_file'}, $params{'barcode_file'});
2206 =item print_generic OPTION => VALUE ...
2208 Internal method - returns a filled-in template for this invoice as a scalar.
2210 See print_ps and print_pdf for methods that return PostScript and PDF output.
2212 Non optional options include
2213 format - latex, html, template
2215 Optional options include
2217 template - a value used as a suffix for a configuration template
2219 time - a value used to control the printing of overdue messages. The
2220 default is now. It isn't the date of the invoice; that's the `_date' field.
2221 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2222 L<Time::Local> and L<Date::Parse> for conversion functions.
2226 unsquelch_cdr - overrides any per customer cdr squelching when true
2228 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2232 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2233 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2234 # yes: fixed width (dot matrix) text printing will be borked
2237 my( $self, %params ) = @_;
2238 my $today = $params{today} ? $params{today} : time;
2239 warn "$me print_generic called on $self with suffix $params{template}\n"
2242 my $format = $params{format};
2243 die "Unknown format: $format"
2244 unless $format =~ /^(latex|html|template)$/;
2246 my $cust_main = $self->cust_main;
2247 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2248 unless $cust_main->payname
2249 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2251 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2252 'html' => [ '<%=', '%>' ],
2253 'template' => [ '{', '}' ],
2256 warn "$me print_generic creating template\n"
2259 #create the template
2260 my $template = $params{template} ? $params{template} : $self->_agent_template;
2261 my $templatefile = "invoice_$format";
2262 $templatefile .= "_$template"
2263 if length($template);
2264 my @invoice_template = map "$_\n", $conf->config($templatefile)
2265 or die "cannot load config data $templatefile";
2268 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2269 #change this to a die when the old code is removed
2270 warn "old-style invoice template $templatefile; ".
2271 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2272 $old_latex = 'true';
2273 @invoice_template = _translate_old_latex_format(@invoice_template);
2276 warn "$me print_generic creating T:T object\n"
2279 my $text_template = new Text::Template(
2281 SOURCE => \@invoice_template,
2282 DELIMITERS => $delimiters{$format},
2285 warn "$me print_generic compiling T:T object\n"
2288 $text_template->compile()
2289 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2292 # additional substitution could possibly cause breakage in existing templates
2293 my %convert_maps = (
2295 'notes' => sub { map "$_", @_ },
2296 'footer' => sub { map "$_", @_ },
2297 'smallfooter' => sub { map "$_", @_ },
2298 'returnaddress' => sub { map "$_", @_ },
2299 'coupon' => sub { map "$_", @_ },
2300 'summary' => sub { map "$_", @_ },
2306 s/%%(.*)$/<!-- $1 -->/g;
2307 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2308 s/\\begin\{enumerate\}/<ol>/g;
2310 s/\\end\{enumerate\}/<\/ol>/g;
2311 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2320 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2322 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2327 s/\\\\\*?\s*$/<BR>/;
2328 s/\\hyphenation\{[\w\s\-]+}//;
2333 'coupon' => sub { "" },
2334 'summary' => sub { "" },
2341 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2342 s/\\begin\{enumerate\}//g;
2344 s/\\end\{enumerate\}//g;
2345 s/\\textbf\{(.*)\}/$1/g;
2352 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2354 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2359 s/\\\\\*?\s*$/\n/; # dubious
2360 s/\\hyphenation\{[\w\s\-]+}//;
2364 'coupon' => sub { "" },
2365 'summary' => sub { "" },
2370 # hashes for differing output formats
2371 my %nbsps = ( 'latex' => '~',
2372 'html' => '', # '&nbps;' would be nice
2373 'template' => '', # not used
2375 my $nbsp = $nbsps{$format};
2377 my %escape_functions = ( 'latex' => \&_latex_escape,
2378 'html' => \&_html_escape_nbsp,#\&encode_entities,
2379 'template' => sub { shift },
2381 my $escape_function = $escape_functions{$format};
2382 my $escape_function_nonbsp = ($format eq 'html')
2383 ? \&_html_escape : $escape_function;
2385 my %date_formats = ( 'latex' => $date_format_long,
2386 'html' => $date_format_long,
2389 $date_formats{'html'} =~ s/ / /g;
2391 my $date_format = $date_formats{$format};
2393 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2395 'html' => sub { return '<b>'. shift(). '</b>'
2397 'template' => sub { shift },
2399 my $embolden_function = $embolden_functions{$format};
2401 warn "$me generating template variables\n"
2404 # generate template variables
2407 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2411 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2417 $returnaddress = join("\n",
2418 $conf->config_orbase("invoice_${format}returnaddress", $template)
2421 } elsif ( grep /\S/,
2422 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2424 my $convert_map = $convert_maps{$format}{'returnaddress'};
2427 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2432 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2434 my $convert_map = $convert_maps{$format}{'returnaddress'};
2435 $returnaddress = join( "\n", &$convert_map(
2436 map { s/( {2,})/'~' x length($1)/eg;
2440 ( $conf->config('company_name', $self->cust_main->agentnum),
2441 $conf->config('company_address', $self->cust_main->agentnum),
2448 my $warning = "Couldn't find a return address; ".
2449 "do you need to set the company_address configuration value?";
2451 $returnaddress = $nbsp;
2452 #$returnaddress = $warning;
2456 warn "$me generating invoice data\n"
2459 my $agentnum = $self->cust_main->agentnum;
2461 my %invoice_data = (
2464 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2465 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2466 'returnaddress' => $returnaddress,
2467 'agent' => &$escape_function($cust_main->agent->agent),
2470 'invnum' => $self->invnum,
2471 'date' => time2str($date_format, $self->_date),
2472 'today' => time2str($date_format_long, $today),
2473 'terms' => $self->terms,
2474 'template' => $template, #params{'template'},
2475 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2476 'current_charges' => sprintf("%.2f", $self->charged),
2477 'duedate' => $self->due_date2str($rdate_format), #date_format?
2480 'custnum' => $cust_main->display_custnum,
2481 'agent_custid' => &$escape_function($cust_main->agent_custid),
2482 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2483 payname company address1 address2 city state zip fax
2487 'ship_enable' => $conf->exists('invoice-ship_address'),
2488 'unitprices' => $conf->exists('invoice-unitprice'),
2489 'smallernotes' => $conf->exists('invoice-smallernotes'),
2490 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2491 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2493 #layout info -- would be fancy to calc some of this and bury the template
2495 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2496 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2497 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2498 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2499 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2500 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2501 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2502 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2503 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2504 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2506 # better hang on to conf_dir for a while (for old templates)
2507 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2509 #these are only used when doing paged plaintext
2515 $invoice_data{finance_section} = '';
2516 if ( $conf->config('finance_pkgclass') ) {
2518 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2519 $invoice_data{finance_section} = $pkg_class->categoryname;
2521 $invoice_data{finance_amount} = '0.00';
2522 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2524 my $countrydefault = $conf->config('countrydefault') || 'US';
2525 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2526 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2527 my $method = $prefix.$_;
2528 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2530 $invoice_data{'ship_country'} = ''
2531 if ( $invoice_data{'ship_country'} eq $countrydefault );
2533 $invoice_data{'cid'} = $params{'cid'}
2536 if ( $cust_main->country eq $countrydefault ) {
2537 $invoice_data{'country'} = '';
2539 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2543 $invoice_data{'address'} = \@address;
2545 $cust_main->payname.
2546 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2547 ? " (P.O. #". $cust_main->payinfo. ")"
2551 push @address, $cust_main->company
2552 if $cust_main->company;
2553 push @address, $cust_main->address1;
2554 push @address, $cust_main->address2
2555 if $cust_main->address2;
2557 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2558 push @address, $invoice_data{'country'}
2559 if $invoice_data{'country'};
2561 while (scalar(@address) < 5);
2563 $invoice_data{'logo_file'} = $params{'logo_file'}
2564 if $params{'logo_file'};
2565 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2566 if $params{'barcode_file'};
2568 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2569 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2570 #my $balance_due = $self->owed + $pr_total - $cr_total;
2571 my $balance_due = $self->owed + $pr_total;
2572 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2573 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2574 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2575 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2577 my $summarypage = '';
2578 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2581 $invoice_data{'summarypage'} = $summarypage;
2583 warn "$me substituting variables in notes, footer, smallfooter\n"
2586 foreach my $include (qw( notes footer smallfooter coupon )) {
2588 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2591 if ( $conf->exists($inc_file, $agentnum)
2592 && length( $conf->config($inc_file, $agentnum) ) ) {
2594 @inc_src = $conf->config($inc_file, $agentnum);
2598 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2600 my $convert_map = $convert_maps{$format}{$include};
2602 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2603 s/--\@\]/$delimiters{$format}[1]/g;
2606 &$convert_map( $conf->config($inc_file, $agentnum) );
2610 my $inc_tt = new Text::Template (
2612 SOURCE => [ map "$_\n", @inc_src ],
2613 DELIMITERS => $delimiters{$format},
2614 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2616 unless ( $inc_tt->compile() ) {
2617 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2618 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2622 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2624 $invoice_data{$include} =~ s/\n+$//
2625 if ($format eq 'latex');
2628 $invoice_data{'po_line'} =
2629 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2630 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2633 my %money_chars = ( 'latex' => '',
2634 'html' => $conf->config('money_char') || '$',
2637 my $money_char = $money_chars{$format};
2639 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2640 'html' => $conf->config('money_char') || '$',
2643 my $other_money_char = $other_money_chars{$format};
2644 $invoice_data{'dollar'} = $other_money_char;
2646 my @detail_items = ();
2647 my @total_items = ();
2651 $invoice_data{'detail_items'} = \@detail_items;
2652 $invoice_data{'total_items'} = \@total_items;
2653 $invoice_data{'buf'} = \@buf;
2654 $invoice_data{'sections'} = \@sections;
2656 warn "$me generating sections\n"
2659 my $previous_section = { 'description' => 'Previous Charges',
2660 'subtotal' => $other_money_char.
2661 sprintf('%.2f', $pr_total),
2662 'summarized' => $summarypage ? 'Y' : '',
2664 $previous_section->{posttotal} = '0 / 30 / 60/ 90 days overdue '.
2665 join(' / ', map { $cust_main->balance_date_range(@$_) }
2666 $self->_prior_month30s
2668 if $conf->exists('invoice_include_aging');
2671 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2672 'subtotal' => $taxtotal, # adjusted below
2673 'summarized' => $summarypage ? 'Y' : '',
2675 my $tax_weight = _pkg_category($tax_section->{description})
2676 ? _pkg_category($tax_section->{description})->weight
2678 $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2679 $tax_section->{'sort_weight'} = $tax_weight;
2682 my $adjusttotal = 0;
2683 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2684 'subtotal' => 0, # adjusted below
2685 'summarized' => $summarypage ? 'Y' : '',
2687 my $adjust_weight = _pkg_category($adjust_section->{description})
2688 ? _pkg_category($adjust_section->{description})->weight
2690 $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2691 $adjust_section->{'sort_weight'} = $adjust_weight;
2693 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2694 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2695 $invoice_data{'multisection'} = $multisection;
2696 my $late_sections = [];
2697 my $extra_sections = [];
2698 my $extra_lines = ();
2699 if ( $multisection ) {
2700 ($extra_sections, $extra_lines) =
2701 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2702 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2704 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2706 push @detail_items, @$extra_lines if $extra_lines;
2708 $self->_items_sections( $late_sections, # this could stand a refactor
2710 $escape_function_nonbsp,
2714 if ($conf->exists('svc_phone_sections')) {
2715 my ($phone_sections, $phone_lines) =
2716 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2717 push @{$late_sections}, @$phone_sections;
2718 push @detail_items, @$phone_lines;
2721 push @sections, { 'description' => '', 'subtotal' => '' };
2724 unless ( $conf->exists('disable_previous_balance')
2725 || $conf->exists('previous_balance-summary_only')
2729 warn "$me adding previous balances\n"
2732 foreach my $line_item ( $self->_items_previous ) {
2735 ext_description => [],
2737 $detail->{'ref'} = $line_item->{'pkgnum'};
2738 $detail->{'quantity'} = 1;
2739 $detail->{'section'} = $previous_section;
2740 $detail->{'description'} = &$escape_function($line_item->{'description'});
2741 if ( exists $line_item->{'ext_description'} ) {
2742 @{$detail->{'ext_description'}} = map {
2743 &$escape_function($_);
2744 } @{$line_item->{'ext_description'}};
2746 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2747 $line_item->{'amount'};
2748 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2750 push @detail_items, $detail;
2751 push @buf, [ $detail->{'description'},
2752 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2758 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2759 push @buf, ['','-----------'];
2760 push @buf, [ 'Total Previous Balance',
2761 $money_char. sprintf("%10.2f", $pr_total) ];
2765 if ( $conf->exists('svc_phone-did-summary') ) {
2766 warn "$me adding DID summary\n"
2769 my ($didsummary,$minutes) = $self->_did_summary;
2770 my $didsummary_desc = 'DID Activity Summary (Past 30 days)';
2772 { 'description' => $didsummary_desc,
2773 'ext_description' => [ $didsummary, $minutes ],
2778 foreach my $section (@sections, @$late_sections) {
2780 warn "$me adding section \n". Dumper($section)
2783 # begin some normalization
2784 $section->{'subtotal'} = $section->{'amount'}
2786 && !exists($section->{subtotal})
2787 && exists($section->{amount});
2789 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2790 if ( $invoice_data{finance_section} &&
2791 $section->{'description'} eq $invoice_data{finance_section} );
2793 $section->{'subtotal'} = $other_money_char.
2794 sprintf('%.2f', $section->{'subtotal'})
2797 # continue some normalization
2798 $section->{'amount'} = $section->{'subtotal'}
2802 if ( $section->{'description'} ) {
2803 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2808 warn "$me setting options\n"
2811 my $multilocation = scalar($cust_main->cust_location); #too expensive?
2813 $options{'section'} = $section if $multisection;
2814 $options{'format'} = $format;
2815 $options{'escape_function'} = $escape_function;
2816 $options{'format_function'} = sub { () } unless $unsquelched;
2817 $options{'unsquelched'} = $unsquelched;
2818 $options{'summary_page'} = $summarypage;
2819 $options{'skip_usage'} =
2820 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2821 $options{'multilocation'} = $multilocation;
2822 $options{'multisection'} = $multisection;
2824 warn "$me searching for line items\n"
2827 foreach my $line_item ( $self->_items_pkg(%options) ) {
2829 warn "$me adding line item $line_item\n"
2833 ext_description => [],
2835 $detail->{'ref'} = $line_item->{'pkgnum'};
2836 $detail->{'quantity'} = $line_item->{'quantity'};
2837 $detail->{'section'} = $section;
2838 $detail->{'description'} = &$escape_function($line_item->{'description'});
2839 if ( exists $line_item->{'ext_description'} ) {
2840 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2842 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2843 $line_item->{'amount'};
2844 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2845 $line_item->{'unit_amount'};
2846 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2848 push @detail_items, $detail;
2849 push @buf, ( [ $detail->{'description'},
2850 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2852 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2856 if ( $section->{'description'} ) {
2857 push @buf, ( ['','-----------'],
2858 [ $section->{'description'}. ' sub-total',
2859 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2868 $invoice_data{current_less_finance} =
2869 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2871 if ( $multisection && !$conf->exists('disable_previous_balance')
2872 || $conf->exists('previous_balance-summary_only') )
2874 unshift @sections, $previous_section if $pr_total;
2877 warn "$me adding taxes\n"
2880 foreach my $tax ( $self->_items_tax ) {
2882 $taxtotal += $tax->{'amount'};
2884 my $description = &$escape_function( $tax->{'description'} );
2885 my $amount = sprintf( '%.2f', $tax->{'amount'} );
2887 if ( $multisection ) {
2889 my $money = $old_latex ? '' : $money_char;
2890 push @detail_items, {
2891 ext_description => [],
2894 description => $description,
2895 amount => $money. $amount,
2897 section => $tax_section,
2902 push @total_items, {
2903 'total_item' => $description,
2904 'total_amount' => $other_money_char. $amount,
2909 push @buf,[ $description,
2910 $money_char. $amount,
2917 $total->{'total_item'} = 'Sub-total';
2918 $total->{'total_amount'} =
2919 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2921 if ( $multisection ) {
2922 $tax_section->{'subtotal'} = $other_money_char.
2923 sprintf('%.2f', $taxtotal);
2924 $tax_section->{'pretotal'} = 'New charges sub-total '.
2925 $total->{'total_amount'};
2926 push @sections, $tax_section if $taxtotal;
2928 unshift @total_items, $total;
2931 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2933 push @buf,['','-----------'];
2934 push @buf,[( $conf->exists('disable_previous_balance')
2936 : 'Total New Charges'
2938 $money_char. sprintf("%10.2f",$self->charged) ];
2944 $item = $conf->config('previous_balance-exclude_from_total')
2945 || 'Total New Charges'
2946 if $conf->exists('previous_balance-exclude_from_total');
2947 my $amount = $self->charged +
2948 ( $conf->exists('disable_previous_balance') ||
2949 $conf->exists('previous_balance-exclude_from_total')
2953 $total->{'total_item'} = &$embolden_function($item);
2954 $total->{'total_amount'} =
2955 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
2956 if ( $multisection ) {
2957 if ( $adjust_section->{'sort_weight'} ) {
2958 $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
2959 sprintf("%.2f", ($self->billing_balance || 0) );
2961 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2962 sprintf('%.2f', $self->charged );
2965 push @total_items, $total;
2967 push @buf,['','-----------'];
2970 sprintf( '%10.2f', $amount )
2975 unless ( $conf->exists('disable_previous_balance') ) {
2976 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2979 my $credittotal = 0;
2980 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
2983 $total->{'total_item'} = &$escape_function($credit->{'description'});
2984 $credittotal += $credit->{'amount'};
2985 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2986 $adjusttotal += $credit->{'amount'};
2987 if ( $multisection ) {
2988 my $money = $old_latex ? '' : $money_char;
2989 push @detail_items, {
2990 ext_description => [],
2993 description => &$escape_function($credit->{'description'}),
2994 amount => $money. $credit->{'amount'},
2996 section => $adjust_section,
2999 push @total_items, $total;
3003 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3006 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3007 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3011 my $paymenttotal = 0;
3012 foreach my $payment ( $self->_items_payments ) {
3014 $total->{'total_item'} = &$escape_function($payment->{'description'});
3015 $paymenttotal += $payment->{'amount'};
3016 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3017 $adjusttotal += $payment->{'amount'};
3018 if ( $multisection ) {
3019 my $money = $old_latex ? '' : $money_char;
3020 push @detail_items, {
3021 ext_description => [],
3024 description => &$escape_function($payment->{'description'}),
3025 amount => $money. $payment->{'amount'},
3027 section => $adjust_section,
3030 push @total_items, $total;
3032 push @buf, [ $payment->{'description'},
3033 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3036 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3038 if ( $multisection ) {
3039 $adjust_section->{'subtotal'} = $other_money_char.
3040 sprintf('%.2f', $adjusttotal);
3041 push @sections, $adjust_section
3042 unless $adjust_section->{sort_weight};
3047 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3048 $total->{'total_amount'} =
3049 &$embolden_function(
3050 $other_money_char. sprintf('%.2f', $summarypage
3052 $self->billing_balance
3053 : $self->owed + $pr_total
3056 if ( $multisection && !$adjust_section->{sort_weight} ) {
3057 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3058 $total->{'total_amount'};
3060 push @total_items, $total;
3062 push @buf,['','-----------'];
3063 push @buf,[$self->balance_due_msg, $money_char.
3064 sprintf("%10.2f", $balance_due ) ];
3068 if ( $multisection ) {
3069 if ($conf->exists('svc_phone_sections')) {
3071 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3072 $total->{'total_amount'} =
3073 &$embolden_function(
3074 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3076 my $last_section = pop @sections;
3077 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3078 $total->{'total_amount'};
3079 push @sections, $last_section;
3081 push @sections, @$late_sections
3085 my @includelist = ();
3086 push @includelist, 'summary' if $summarypage;
3087 foreach my $include ( @includelist ) {
3089 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3092 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3094 @inc_src = $conf->config($inc_file, $agentnum);
3098 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3100 my $convert_map = $convert_maps{$format}{$include};
3102 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3103 s/--\@\]/$delimiters{$format}[1]/g;
3106 &$convert_map( $conf->config($inc_file, $agentnum) );
3110 my $inc_tt = new Text::Template (
3112 SOURCE => [ map "$_\n", @inc_src ],
3113 DELIMITERS => $delimiters{$format},
3114 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3116 unless ( $inc_tt->compile() ) {
3117 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3118 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3122 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3124 $invoice_data{$include} =~ s/\n+$//
3125 if ($format eq 'latex');
3130 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3131 /invoice_lines\((\d*)\)/;
3132 $invoice_lines += $1 || scalar(@buf);
3135 die "no invoice_lines() functions in template?"
3136 if ( $format eq 'template' && !$wasfunc );
3138 if ($format eq 'template') {
3140 if ( $invoice_lines ) {
3141 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3142 $invoice_data{'total_pages'}++
3143 if scalar(@buf) % $invoice_lines;
3146 #setup subroutine for the template
3147 sub FS::cust_bill::_template::invoice_lines {
3148 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
3150 scalar(@FS::cust_bill::_template::buf)
3151 ? shift @FS::cust_bill::_template::buf
3160 push @collect, split("\n",
3161 $text_template->fill_in( HASH => \%invoice_data,
3162 PACKAGE => 'FS::cust_bill::_template'
3165 $FS::cust_bill::_template::page++;
3167 map "$_\n", @collect;
3169 warn "filling in template for invoice ". $self->invnum. "\n"
3171 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3174 $text_template->fill_in(HASH => \%invoice_data);
3178 # helper routine for generating date ranges
3179 sub _prior_month30s {
3182 [ 1, 2592000 ], # 0-30 days ago
3183 [ 2592000, 5184000 ], # 30-60 days ago
3184 [ 5184000, 7776000 ], # 60-90 days ago
3185 [ 7776000, 0 ], # 90+ days ago
3188 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3189 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3194 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3196 Returns an postscript invoice, as a scalar.
3198 Options can be passed as a hashref (recommended) or as a list of time, template
3199 and then any key/value pairs for any other options.
3201 I<time> an optional value used to control the printing of overdue messages. The
3202 default is now. It isn't the date of the invoice; that's the `_date' field.
3203 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3204 L<Time::Local> and L<Date::Parse> for conversion functions.
3206 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3213 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3214 my $ps = generate_ps($file);
3216 unlink($barcodefile);
3221 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3223 Returns an PDF invoice, as a scalar.
3225 Options can be passed as a hashref (recommended) or as a list of time, template
3226 and then any key/value pairs for any other options.
3228 I<time> an optional value used to control the printing of overdue messages. The
3229 default is now. It isn't the date of the invoice; that's the `_date' field.
3230 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3231 L<Time::Local> and L<Date::Parse> for conversion functions.
3233 I<template>, if specified, is the name of a suffix for alternate invoices.
3235 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3242 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3243 my $pdf = generate_pdf($file);
3245 unlink($barcodefile);
3250 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3252 Returns an HTML invoice, as a scalar.
3254 I<time> an optional value used to control the printing of overdue messages. The
3255 default is now. It isn't the date of the invoice; that's the `_date' field.
3256 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3257 L<Time::Local> and L<Date::Parse> for conversion functions.
3259 I<template>, if specified, is the name of a suffix for alternate invoices.
3261 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3263 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3264 when emailing the invoice as part of a multipart/related MIME email.
3272 %params = %{ shift() };
3274 $params{'time'} = shift;
3275 $params{'template'} = shift;
3276 $params{'cid'} = shift;
3279 $params{'format'} = 'html';
3281 $self->print_generic( %params );
3284 # quick subroutine for print_latex
3286 # There are ten characters that LaTeX treats as special characters, which
3287 # means that they do not simply typeset themselves:
3288 # # $ % & ~ _ ^ \ { }
3290 # TeX ignores blanks following an escaped character; if you want a blank (as
3291 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3295 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3296 $value =~ s/([<>])/\$$1\$/g;
3302 encode_entities($value);
3306 sub _html_escape_nbsp {
3307 my $value = _html_escape(shift);
3308 $value =~ s/ +/ /g;
3312 #utility methods for print_*
3314 sub _translate_old_latex_format {
3315 warn "_translate_old_latex_format called\n"
3322 if ( $line =~ /^%%Detail\s*$/ ) {
3324 push @template, q![@--!,
3325 q! foreach my $_tr_line (@detail_items) {!,
3326 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3327 q! $_tr_line->{'description'} .= !,
3328 q! "\\tabularnewline\n~~".!,
3329 q! join( "\\tabularnewline\n~~",!,
3330 q! @{$_tr_line->{'ext_description'}}!,
3334 while ( ( my $line_item_line = shift )
3335 !~ /^%%EndDetail\s*$/ ) {
3336 $line_item_line =~ s/'/\\'/g; # nice LTS
3337 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3338 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3339 push @template, " \$OUT .= '$line_item_line';";
3342 push @template, '}',
3345 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3347 push @template, '[@--',
3348 ' foreach my $_tr_line (@total_items) {';
3350 while ( ( my $total_item_line = shift )
3351 !~ /^%%EndTotalDetails\s*$/ ) {
3352 $total_item_line =~ s/'/\\'/g; # nice LTS
3353 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3354 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3355 push @template, " \$OUT .= '$total_item_line';";
3358 push @template, '}',
3362 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3363 push @template, $line;
3369 warn "$_\n" foreach @template;
3378 #check for an invoice-specific override
3379 return $self->invoice_terms if $self->invoice_terms;
3381 #check for a customer- specific override
3382 my $cust_main = $self->cust_main;
3383 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3385 #use configured default
3386 $conf->config('invoice_default_terms') || '';
3392 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3393 $duedate = $self->_date() + ( $1 * 86400 );
3400 $self->due_date ? time2str(shift, $self->due_date) : '';
3403 sub balance_due_msg {
3405 my $msg = 'Balance Due';
3406 return $msg unless $self->terms;
3407 if ( $self->due_date ) {
3408 $msg .= ' - Please pay by '. $self->due_date2str($date_format);
3409 } elsif ( $self->terms ) {
3410 $msg .= ' - '. $self->terms;
3415 sub balance_due_date {
3418 if ( $conf->exists('invoice_default_terms')
3419 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3420 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3425 =item invnum_date_pretty
3427 Returns a string with the invoice number and date, for example:
3428 "Invoice #54 (3/20/2008)"
3432 sub invnum_date_pretty {
3434 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3439 Returns a string with the date, for example: "3/20/2008"
3445 time2str($date_format, $self->_date);
3448 use vars qw(%pkg_category_cache);
3449 sub _items_sections {
3452 my $summarypage = shift;
3454 my $extra_sections = shift;
3458 my %late_subtotal = ();
3461 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3464 my $usage = $cust_bill_pkg->usage;
3466 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3467 next if ( $display->summary && $summarypage );
3469 my $section = $display->section;
3470 my $type = $display->type;
3472 $not_tax{$section} = 1
3473 unless $cust_bill_pkg->pkgnum == 0;
3475 if ( $display->post_total && !$summarypage ) {
3476 if (! $type || $type eq 'S') {
3477 $late_subtotal{$section} += $cust_bill_pkg->setup
3478 if $cust_bill_pkg->setup != 0;
3482 $late_subtotal{$section} += $cust_bill_pkg->recur
3483 if $cust_bill_pkg->recur != 0;
3486 if ($type && $type eq 'R') {
3487 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3488 if $cust_bill_pkg->recur != 0;
3491 if ($type && $type eq 'U') {
3492 $late_subtotal{$section} += $usage
3493 unless scalar(@$extra_sections);
3498 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3500 if (! $type || $type eq 'S') {
3501 $subtotal{$section} += $cust_bill_pkg->setup
3502 if $cust_bill_pkg->setup != 0;
3506 $subtotal{$section} += $cust_bill_pkg->recur
3507 if $cust_bill_pkg->recur != 0;
3510 if ($type && $type eq 'R') {
3511 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3512 if $cust_bill_pkg->recur != 0;
3515 if ($type && $type eq 'U') {
3516 $subtotal{$section} += $usage
3517 unless scalar(@$extra_sections);
3526 %pkg_category_cache = ();
3528 push @$late, map { { 'description' => &{$escape}($_),
3529 'subtotal' => $late_subtotal{$_},
3531 'sort_weight' => ( _pkg_category($_)
3532 ? _pkg_category($_)->weight
3535 ((_pkg_category($_) && _pkg_category($_)->condense)
3536 ? $self->_condense_section($format)
3540 sort _sectionsort keys %late_subtotal;
3543 if ( $summarypage ) {
3544 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3545 map { $_->categoryname } qsearch('pkg_category', {});
3546 push @sections, '' if exists($subtotal{''});
3548 @sections = keys %subtotal;
3551 my @early = map { { 'description' => &{$escape}($_),
3552 'subtotal' => $subtotal{$_},
3553 'summarized' => $not_tax{$_} ? '' : 'Y',
3554 'tax_section' => $not_tax{$_} ? '' : 'Y',
3555 'sort_weight' => ( _pkg_category($_)
3556 ? _pkg_category($_)->weight
3559 ((_pkg_category($_) && _pkg_category($_)->condense)
3560 ? $self->_condense_section($format)
3565 push @early, @$extra_sections if $extra_sections;
3567 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3571 #helper subs for above
3574 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3578 my $categoryname = shift;
3579 $pkg_category_cache{$categoryname} ||=
3580 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3583 my %condensed_format = (
3584 'label' => [ qw( Description Qty Amount ) ],
3586 sub { shift->{description} },
3587 sub { shift->{quantity} },
3588 sub { my($href, %opt) = @_;
3589 ($opt{dollar} || ''). $href->{amount};
3592 'align' => [ qw( l r r ) ],
3593 'span' => [ qw( 5 1 1 ) ], # unitprices?
3594 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3597 sub _condense_section {
3598 my ( $self, $format ) = ( shift, shift );
3600 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3601 qw( description_generator
3604 total_line_generator
3609 sub _condensed_generator_defaults {
3610 my ( $self, $format ) = ( shift, shift );
3611 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3620 sub _condensed_header_generator {
3621 my ( $self, $format ) = ( shift, shift );
3623 my ( $f, $prefix, $suffix, $separator, $column ) =
3624 _condensed_generator_defaults($format);
3626 if ($format eq 'latex') {
3627 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3628 $suffix = "\\\\\n\\hline";
3631 sub { my ($d,$a,$s,$w) = @_;
3632 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3634 } elsif ( $format eq 'html' ) {
3635 $prefix = '<th></th>';
3639 sub { my ($d,$a,$s,$w) = @_;
3640 return qq!<th align="$html_align{$a}">$d</th>!;
3648 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3650 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3653 $prefix. join($separator, @result). $suffix;
3658 sub _condensed_description_generator {
3659 my ( $self, $format ) = ( shift, shift );
3661 my ( $f, $prefix, $suffix, $separator, $column ) =
3662 _condensed_generator_defaults($format);
3664 my $money_char = '$';
3665 if ($format eq 'latex') {
3666 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3668 $separator = " & \n";
3670 sub { my ($d,$a,$s,$w) = @_;
3671 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3673 $money_char = '\\dollar';
3674 }elsif ( $format eq 'html' ) {
3675 $prefix = '"><td align="center"></td>';
3679 sub { my ($d,$a,$s,$w) = @_;
3680 return qq!<td align="$html_align{$a}">$d</td>!;
3682 #$money_char = $conf->config('money_char') || '$';
3683 $money_char = ''; # this is madness
3691 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3693 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3695 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3696 map { $f->{$_}->[$i] } qw(align span width)
3700 $prefix. join( $separator, @result ). $suffix;
3705 sub _condensed_total_generator {
3706 my ( $self, $format ) = ( shift, shift );
3708 my ( $f, $prefix, $suffix, $separator, $column ) =
3709 _condensed_generator_defaults($format);
3712 if ($format eq 'latex') {
3715 $separator = " & \n";
3717 sub { my ($d,$a,$s,$w) = @_;
3718 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3720 }elsif ( $format eq 'html' ) {
3724 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3726 sub { my ($d,$a,$s,$w) = @_;
3727 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3736 # my $r = &{$f->{fields}->[$i]}(@args);
3737 # $r .= ' Total' unless $i;
3739 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3741 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3742 map { $f->{$_}->[$i] } qw(align span width)
3746 $prefix. join( $separator, @result ). $suffix;
3751 =item total_line_generator FORMAT
3753 Returns a coderef used for generation of invoice total line items for this
3754 usage_class. FORMAT is either html or latex
3758 # should not be used: will have issues with hash element names (description vs
3759 # total_item and amount vs total_amount -- another array of functions?
3761 sub _condensed_total_line_generator {
3762 my ( $self, $format ) = ( shift, shift );
3764 my ( $f, $prefix, $suffix, $separator, $column ) =
3765 _condensed_generator_defaults($format);
3768 if ($format eq 'latex') {
3771 $separator = " & \n";
3773 sub { my ($d,$a,$s,$w) = @_;
3774 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3776 }elsif ( $format eq 'html' ) {
3780 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3782 sub { my ($d,$a,$s,$w) = @_;
3783 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3792 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3794 &{$column}( &{$f->{fields}->[$i]}(@args),
3795 map { $f->{$_}->[$i] } qw(align span width)
3799 $prefix. join( $separator, @result ). $suffix;
3804 #sub _items_extra_usage_sections {
3806 # my $escape = shift;
3808 # my %sections = ();
3810 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
3811 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3813 # next unless $cust_bill_pkg->pkgnum > 0;
3815 # foreach my $section ( keys %usage_class ) {
3817 # my $usage = $cust_bill_pkg->usage($section);
3819 # next unless $usage && $usage > 0;
3821 # $sections{$section} ||= 0;
3822 # $sections{$section} += $usage;
3828 # map { { 'description' => &{$escape}($_),
3829 # 'subtotal' => $sections{$_},
3830 # 'summarized' => '',
3831 # 'tax_section' => '',
3834 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3838 sub _items_extra_usage_sections {
3847 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3848 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3849 next unless $cust_bill_pkg->pkgnum > 0;
3851 foreach my $classnum ( keys %usage_class ) {
3852 my $section = $usage_class{$classnum}->classname;
3853 $classnums{$section} = $classnum;
3855 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3856 my $amount = $detail->amount;
3857 next unless $amount && $amount > 0;
3859 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
3860 $sections{$section}{amount} += $amount; #subtotal
3861 $sections{$section}{calls}++;
3862 $sections{$section}{duration} += $detail->duration;
3864 my $desc = $detail->regionname;
3865 my $description = $desc;
3866 $description = substr($desc, 0, 50). '...'
3867 if $format eq 'latex' && length($desc) > 50;
3869 $lines{$section}{$desc} ||= {
3870 description => &{$escape}($description),
3871 #pkgpart => $part_pkg->pkgpart,
3872 pkgnum => $cust_bill_pkg->pkgnum,
3877 #unit_amount => $cust_bill_pkg->unitrecur,
3878 quantity => $cust_bill_pkg->quantity,
3879 product_code => 'N/A',
3880 ext_description => [],
3883 $lines{$section}{$desc}{amount} += $amount;
3884 $lines{$section}{$desc}{calls}++;
3885 $lines{$section}{$desc}{duration} += $detail->duration;
3891 my %sectionmap = ();
3892 foreach (keys %sections) {
3893 my $usage_class = $usage_class{$classnums{$_}};
3894 $sectionmap{$_} = { 'description' => &{$escape}($_),
3895 'amount' => $sections{$_}{amount}, #subtotal
3896 'calls' => $sections{$_}{calls},
3897 'duration' => $sections{$_}{duration},
3899 'tax_section' => '',
3900 'sort_weight' => $usage_class->weight,
3901 ( $usage_class->format
3902 ? ( map { $_ => $usage_class->$_($format) }
3903 qw( description_generator header_generator total_generator total_line_generator )
3910 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
3914 foreach my $section ( keys %lines ) {
3915 foreach my $line ( keys %{$lines{$section}} ) {
3916 my $l = $lines{$section}{$line};
3917 $l->{section} = $sectionmap{$section};
3918 $l->{amount} = sprintf( "%.2f", $l->{amount} );
3919 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3924 return(\@sections, \@lines);
3930 my $end = $self->_date;
3931 my $start = $end - 2592000; # 30 days
3932 my $cust_main = $self->cust_main;
3933 my @pkgs = $cust_main->all_pkgs;
3934 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
3937 foreach my $pkg ( @pkgs ) {
3938 my @h_cust_svc = $pkg->h_cust_svc($end);
3939 foreach my $h_cust_svc ( @h_cust_svc ) {
3940 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
3941 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
3943 my $inserted = $h_cust_svc->date_inserted;
3944 my $deleted = $h_cust_svc->date_deleted;
3945 my $phone_inserted = $h_cust_svc->h_svc_x($inserted);
3947 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
3949 # DID either activated or ported in; cannot be both for same DID simultaneously
3950 if ($inserted >= $start && $inserted <= $end && $phone_inserted
3951 && (!$phone_inserted->lnp_status
3952 || $phone_inserted->lnp_status eq ''
3953 || $phone_inserted->lnp_status eq 'native')) {
3956 else { # this one not so clean, should probably move to (h_)svc_phone
3957 my $phone_portedin = qsearchs( 'h_svc_phone',
3958 { 'svcnum' => $h_cust_svc->svcnum,
3959 'lnp_status' => 'portedin' },
3960 FS::h_svc_phone->sql_h_searchs($end),
3962 $num_portedin++ if $phone_portedin;
3965 # DID either deactivated or ported out; cannot be both for same DID simultaneously
3966 if($deleted >= $start && $deleted <= $end && $phone_deleted
3967 && (!$phone_deleted->lnp_status
3968 || $phone_deleted->lnp_status ne 'portingout')) {
3971 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
3972 && $phone_deleted->lnp_status
3973 && $phone_deleted->lnp_status eq 'portingout') {
3977 # increment usage minutes
3978 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end);
3979 foreach my $cdr ( @cdrs ) {
3980 $minutes += $cdr->billsec/60;
3983 # don't look at this service again
3984 push @seen, $h_cust_svc->svcnum;
3988 $minutes = sprintf("%d", $minutes);
3989 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
3990 . "$num_deactivated Ported-Out: $num_portedout ",
3991 "Total Minutes: $minutes");
3994 sub _items_svc_phone_sections {
4003 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4004 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4006 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4007 next unless $cust_bill_pkg->pkgnum > 0;
4009 my @header = $cust_bill_pkg->details_header;
4010 next unless scalar(@header);
4012 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4014 my $phonenum = $detail->phonenum;
4015 next unless $phonenum;
4017 my $amount = $detail->amount;
4018 next unless $amount && $amount > 0;
4020 $sections{$phonenum} ||= { 'amount' => 0,
4023 'sort_weight' => -1,
4024 'phonenum' => $phonenum,
4026 $sections{$phonenum}{amount} += $amount; #subtotal
4027 $sections{$phonenum}{calls}++;
4028 $sections{$phonenum}{duration} += $detail->duration;
4030 my $desc = $detail->regionname;
4031 my $description = $desc;
4032 $description = substr($desc, 0, 50). '...'
4033 if $format eq 'latex' && length($desc) > 50;
4035 $lines{$phonenum}{$desc} ||= {
4036 description => &{$escape}($description),
4037 #pkgpart => $part_pkg->pkgpart,
4045 product_code => 'N/A',
4046 ext_description => [],
4049 $lines{$phonenum}{$desc}{amount} += $amount;
4050 $lines{$phonenum}{$desc}{calls}++;
4051 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4053 my $line = $usage_class{$detail->classnum}->classname;
4054 $sections{"$phonenum $line"} ||=
4058 'sort_weight' => $usage_class{$detail->classnum}->weight,
4059 'phonenum' => $phonenum,
4060 'header' => [ @header ],
4062 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4063 $sections{"$phonenum $line"}{calls}++;
4064 $sections{"$phonenum $line"}{duration} += $detail->duration;
4066 $lines{"$phonenum $line"}{$desc} ||= {
4067 description => &{$escape}($description),
4068 #pkgpart => $part_pkg->pkgpart,
4076 product_code => 'N/A',
4077 ext_description => [],
4080 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4081 $lines{"$phonenum $line"}{$desc}{calls}++;
4082 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4083 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4084 $detail->formatted('format' => $format);
4089 my %sectionmap = ();
4090 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4091 foreach ( keys %sections ) {
4092 my @header = @{ $sections{$_}{header} || [] };
4094 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4095 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4096 my $usage_class = $summary ? $simple : $usage_simple;
4097 my $ending = $summary ? ' usage charges' : '';
4100 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4102 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4103 'amount' => $sections{$_}{amount}, #subtotal
4104 'calls' => $sections{$_}{calls},
4105 'duration' => $sections{$_}{duration},
4107 'tax_section' => '',
4108 'phonenum' => $sections{$_}{phonenum},
4109 'sort_weight' => $sections{$_}{sort_weight},
4110 'post_total' => $summary, #inspire pagebreak
4112 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4113 qw( description_generator
4116 total_line_generator
4123 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4124 $a->{sort_weight} <=> $b->{sort_weight}
4129 foreach my $section ( keys %lines ) {
4130 foreach my $line ( keys %{$lines{$section}} ) {
4131 my $l = $lines{$section}{$line};
4132 $l->{section} = $sectionmap{$section};
4133 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4134 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4139 return(\@sections, \@lines);
4146 #my @display = scalar(@_)
4148 # : qw( _items_previous _items_pkg );
4149 # #: qw( _items_pkg );
4150 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4151 my @display = qw( _items_previous _items_pkg );
4154 foreach my $display ( @display ) {
4155 push @b, $self->$display(@_);
4160 sub _items_previous {
4162 my $cust_main = $self->cust_main;
4163 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4165 foreach ( @pr_cust_bill ) {
4166 my $date = $conf->exists('invoice_show_prior_due_date')
4167 ? 'due '. $_->due_date2str($date_format)
4168 : time2str($date_format, $_->_date);
4170 'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
4171 #'pkgpart' => 'N/A',
4173 'amount' => sprintf("%.2f", $_->owed),
4179 # 'description' => 'Previous Balance',
4180 # #'pkgpart' => 'N/A',
4181 # 'pkgnum' => 'N/A',
4182 # 'amount' => sprintf("%10.2f", $pr_total ),
4183 # 'ext_description' => [ map {
4184 # "Invoice ". $_->invnum.
4185 # " (". time2str("%x",$_->_date). ") ".
4186 # sprintf("%10.2f", $_->owed)
4187 # } @pr_cust_bill ],
4196 warn "$me _items_pkg searching for all package line items\n"
4199 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4201 warn "$me _items_pkg filtering line items\n"
4203 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4205 if ($options{section} && $options{section}->{condensed}) {
4207 warn "$me _items_pkg condensing section\n"
4211 local $Storable::canonical = 1;
4212 foreach ( @items ) {
4214 delete $item->{ref};
4215 delete $item->{ext_description};
4216 my $key = freeze($item);
4217 $itemshash{$key} ||= 0;
4218 $itemshash{$key} ++; # += $item->{quantity};
4220 @items = sort { $a->{description} cmp $b->{description} }
4221 map { my $i = thaw($_);
4222 $i->{quantity} = $itemshash{$_};
4224 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4230 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4237 return 0 unless $a->itemdesc cmp $b->itemdesc;
4238 return -1 if $b->itemdesc eq 'Tax';
4239 return 1 if $a->itemdesc eq 'Tax';
4240 return -1 if $b->itemdesc eq 'Other surcharges';
4241 return 1 if $a->itemdesc eq 'Other surcharges';
4242 $a->itemdesc cmp $b->itemdesc;
4247 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4248 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4251 sub _items_cust_bill_pkg {
4253 my $cust_bill_pkgs = shift;
4256 my $format = $opt{format} || '';
4257 my $escape_function = $opt{escape_function} || sub { shift };
4258 my $format_function = $opt{format_function} || '';
4259 my $unsquelched = $opt{unsquelched} || '';
4260 my $section = $opt{section}->{description} if $opt{section};
4261 my $summary_page = $opt{summary_page} || '';
4262 my $multilocation = $opt{multilocation} || '';
4263 my $multisection = $opt{multisection} || '';
4264 my $discount_show_always = 0;
4267 my ($s, $r, $u) = ( undef, undef, undef );
4268 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4271 warn "$me _items_cust_bill_pkg considering cust_bill_pkg $cust_bill_pkg\n"
4274 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
4275 && $conf->exists('discount-show-always'));
4277 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4278 if ( $_ && !$cust_bill_pkg->hidden ) {
4279 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4280 $_->{amount} =~ s/^\-0\.00$/0.00/;
4281 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4283 unless ( $_->{amount} == 0 && !$discount_show_always );
4288 foreach my $display ( grep { defined($section)
4289 ? $_->section eq $section
4292 #grep { !$_->summary || !$summary_page } # bunk!
4293 grep { !$_->summary || $multisection }
4294 $cust_bill_pkg->cust_bill_pkg_display
4298 warn "$me _items_cust_bill_pkg considering display item $display\n"
4301 my $type = $display->type;
4303 my $desc = $cust_bill_pkg->desc;
4304 $desc = substr($desc, 0, 50). '...'
4305 if $format eq 'latex' && length($desc) > 50;
4307 my %details_opt = ( 'format' => $format,
4308 'escape_function' => $escape_function,
4309 'format_function' => $format_function,
4312 if ( $cust_bill_pkg->pkgnum > 0 ) {
4314 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4317 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4319 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
4321 warn "$me _items_cust_bill_pkg adding setup\n"
4324 my $description = $desc;
4325 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
4328 unless ( $cust_pkg->part_pkg->hide_svc_detail
4329 || $cust_bill_pkg->hidden )
4332 push @d, map &{$escape_function}($_),
4333 $cust_pkg->h_labels_short($self->_date, undef, 'I')
4334 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4336 if ( $multilocation ) {
4337 my $loc = $cust_pkg->location_label;
4338 $loc = substr($loc, 0, 50). '...'
4339 if $format eq 'latex' && length($loc) > 50;
4340 push @d, &{$escape_function}($loc);
4345 push @d, $cust_bill_pkg->details(%details_opt)
4346 if $cust_bill_pkg->recur == 0;
4348 if ( $cust_bill_pkg->hidden ) {
4349 $s->{amount} += $cust_bill_pkg->setup;
4350 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4351 push @{ $s->{ext_description} }, @d;
4354 description => $description,
4355 #pkgpart => $part_pkg->pkgpart,
4356 pkgnum => $cust_bill_pkg->pkgnum,
4357 amount => $cust_bill_pkg->setup,
4358 unit_amount => $cust_bill_pkg->unitsetup,
4359 quantity => $cust_bill_pkg->quantity,
4360 ext_description => \@d,
4366 if ( ( $cust_bill_pkg->recur != 0 || $cust_bill_pkg->setup == 0 ||
4367 ($discount_show_always && $cust_bill_pkg->recur == 0) ) &&
4368 ( !$type || $type eq 'R' || $type eq 'U' )
4372 warn "$me _items_cust_bill_pkg adding recur/usage\n"
4375 my $is_summary = $display->summary;
4376 my $description = ($is_summary && $type && $type eq 'U')
4377 ? "Usage charges" : $desc;
4379 $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4380 " - ". time2str($date_format, $cust_bill_pkg->edate).
4382 unless $conf->exists('disable_line_item_date_ranges');
4386 #at least until cust_bill_pkg has "past" ranges in addition to
4387 #the "future" sdate/edate ones... see #3032
4388 my @dates = ( $self->_date );
4389 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4390 push @dates, $prev->sdate if $prev;
4391 push @dates, undef if !$prev;
4393 unless ( $cust_pkg->part_pkg->hide_svc_detail
4394 || $cust_bill_pkg->itemdesc
4395 || $cust_bill_pkg->hidden
4396 || $is_summary && $type && $type eq 'U' )
4399 warn "$me _items_cust_bill_pkg adding service details\n"
4402 push @d, map &{$escape_function}($_),
4403 $cust_pkg->h_labels_short(@dates, 'I')
4404 #$cust_bill_pkg->edate,
4405 #$cust_bill_pkg->sdate)
4406 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4408 warn "$me _items_cust_bill_pkg done adding service details\n"
4411 if ( $multilocation ) {
4412 my $loc = $cust_pkg->location_label;
4413 $loc = substr($loc, 0, 50). '...'
4414 if $format eq 'latex' && length($loc) > 50;
4415 push @d, &{$escape_function}($loc);
4420 warn "$me _items_cust_bill_pkg adding details\n"
4423 push @d, $cust_bill_pkg->details(%details_opt)
4424 unless ($is_summary || $type && $type eq 'R');
4426 warn "$me _items_cust_bill_pkg calculating amount\n"
4431 $amount = $cust_bill_pkg->recur;
4432 }elsif($type eq 'R') {
4433 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4434 }elsif($type eq 'U') {
4435 $amount = $cust_bill_pkg->usage;
4438 if ( !$type || $type eq 'R' ) {
4440 warn "$me _items_cust_bill_pkg adding recur\n"
4443 if ( $cust_bill_pkg->hidden ) {
4444 $r->{amount} += $amount;
4445 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4446 push @{ $r->{ext_description} }, @d;
4449 description => $description,
4450 #pkgpart => $part_pkg->pkgpart,
4451 pkgnum => $cust_bill_pkg->pkgnum,
4453 unit_amount => $cust_bill_pkg->unitrecur,
4454 quantity => $cust_bill_pkg->quantity,
4455 ext_description => \@d,
4459 } else { # $type eq 'U'
4461 warn "$me _items_cust_bill_pkg adding usage\n"
4464 if ( $cust_bill_pkg->hidden ) {
4465 $u->{amount} += $amount;
4466 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4467 push @{ $u->{ext_description} }, @d;
4470 description => $description,
4471 #pkgpart => $part_pkg->pkgpart,
4472 pkgnum => $cust_bill_pkg->pkgnum,
4474 unit_amount => $cust_bill_pkg->unitrecur,
4475 quantity => $cust_bill_pkg->quantity,
4476 ext_description => \@d,
4482 } # recurring or usage with recurring charge
4484 } else { #pkgnum tax or one-shot line item (??)
4486 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
4489 if ( $cust_bill_pkg->setup != 0 ) {
4491 'description' => $desc,
4492 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
4495 if ( $cust_bill_pkg->recur != 0 ) {
4497 'description' => "$desc (".
4498 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
4499 time2str($date_format, $cust_bill_pkg->edate). ')',
4500 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
4510 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
4513 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4515 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4516 $_->{amount} =~ s/^\-0\.00$/0.00/;
4517 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4519 unless ( $_->{amount} == 0 && !$discount_show_always );
4527 sub _items_credits {
4528 my( $self, %opt ) = @_;
4529 my $trim_len = $opt{'trim_len'} || 60;
4533 foreach ( $self->cust_credited ) {
4535 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4537 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4538 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4539 $reason = " ($reason) " if $reason;
4542 #'description' => 'Credit ref\#'. $_->crednum.
4543 # " (". time2str("%x",$_->cust_credit->_date) .")".
4545 'description' => 'Credit applied '.
4546 time2str($date_format,$_->cust_credit->_date). $reason,
4547 'amount' => sprintf("%.2f",$_->amount),
4555 sub _items_payments {
4559 #get & print payments
4560 foreach ( $self->cust_bill_pay ) {
4562 #something more elaborate if $_->amount ne ->cust_pay->paid ?
4565 'description' => "Payment received ".
4566 time2str($date_format,$_->cust_pay->_date ),
4567 'amount' => sprintf("%.2f", $_->amount )
4575 =item call_details [ OPTION => VALUE ... ]
4577 Returns an array of CSV strings representing the call details for this invoice
4578 The only option available is the boolean prepend_billed_number
4583 my ($self, %opt) = @_;
4585 my $format_function = sub { shift };
4587 if ($opt{prepend_billed_number}) {
4588 $format_function = sub {
4592 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4597 my @details = map { $_->details( 'format_function' => $format_function,
4598 'escape_function' => sub{ return() },
4602 $self->cust_bill_pkg;
4603 my $header = $details[0];
4604 ( $header, grep { $_ ne $header } @details );
4614 =item process_reprint
4618 sub process_reprint {
4619 process_re_X('print', @_);
4622 =item process_reemail
4626 sub process_reemail {
4627 process_re_X('email', @_);
4635 process_re_X('fax', @_);
4643 process_re_X('ftp', @_);
4650 sub process_respool {
4651 process_re_X('spool', @_);
4654 use Storable qw(thaw);
4658 my( $method, $job ) = ( shift, shift );
4659 warn "$me process_re_X $method for job $job\n" if $DEBUG;
4661 my $param = thaw(decode_base64(shift));
4662 warn Dumper($param) if $DEBUG;
4673 my($method, $job, %param ) = @_;
4675 warn "re_X $method for job $job with param:\n".
4676 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
4679 #some false laziness w/search/cust_bill.html
4681 my $orderby = 'ORDER BY cust_bill._date';
4683 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
4685 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4687 my @cust_bill = qsearch( {
4688 #'select' => "cust_bill.*",
4689 'table' => 'cust_bill',
4690 'addl_from' => $addl_from,
4692 'extra_sql' => $extra_sql,
4693 'order_by' => $orderby,
4697 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4699 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
4702 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
4703 foreach my $cust_bill ( @cust_bill ) {
4704 $cust_bill->$method();
4706 if ( $job ) { #progressbar foo
4708 if ( time - $min_sec > $last ) {
4709 my $error = $job->update_statustext(
4710 int( 100 * $num / scalar(@cust_bill) )
4712 die $error if $error;
4723 =head1 CLASS METHODS
4729 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
4734 my ($class, $start, $end) = @_;
4736 $class->paid_sql($start, $end). ' - '.
4737 $class->credited_sql($start, $end);
4742 Returns an SQL fragment to retreive the net amount (charged minus credited).
4747 my ($class, $start, $end) = @_;
4748 'charged - '. $class->credited_sql($start, $end);
4753 Returns an SQL fragment to retreive the amount paid against this invoice.
4758 my ($class, $start, $end) = @_;
4759 $start &&= "AND cust_bill_pay._date <= $start";
4760 $end &&= "AND cust_bill_pay._date > $end";
4761 $start = '' unless defined($start);
4762 $end = '' unless defined($end);
4763 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
4764 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
4769 Returns an SQL fragment to retreive the amount credited against this invoice.
4774 my ($class, $start, $end) = @_;
4775 $start &&= "AND cust_credit_bill._date <= $start";
4776 $end &&= "AND cust_credit_bill._date > $end";
4777 $start = '' unless defined($start);
4778 $end = '' unless defined($end);
4779 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
4780 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
4785 Returns an SQL fragment to retrieve the due date of an invoice.
4786 Currently only supported on PostgreSQL.
4794 cust_bill.invoice_terms,
4795 cust_main.invoice_terms,
4796 \''.($conf->config('invoice_default_terms') || '').'\'
4797 ), E\'Net (\\\\d+)\'
4799 ) * 86400 + cust_bill._date'
4802 =item search_sql_where HASHREF
4804 Class method which returns an SQL WHERE fragment to search for parameters
4805 specified in HASHREF. Valid parameters are
4811 List reference of start date, end date, as UNIX timestamps.
4821 List reference of charged limits (exclusive).
4825 List reference of charged limits (exclusive).
4829 flag, return open invoices only
4833 flag, return net invoices only
4837 =item newest_percust
4841 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
4845 sub search_sql_where {
4846 my($class, $param) = @_;
4848 warn "$me search_sql_where called with params: \n".
4849 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
4855 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
4856 push @search, "cust_main.agentnum = $1";
4860 if ( $param->{_date} ) {
4861 my($beginning, $ending) = @{$param->{_date}};
4863 push @search, "cust_bill._date >= $beginning",
4864 "cust_bill._date < $ending";
4868 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
4869 push @search, "cust_bill.invnum >= $1";
4871 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
4872 push @search, "cust_bill.invnum <= $1";
4876 if ( $param->{charged} ) {
4877 my @charged = ref($param->{charged})
4878 ? @{ $param->{charged} }
4879 : ($param->{charged});
4881 push @search, map { s/^charged/cust_bill.charged/; $_; }
4885 my $owed_sql = FS::cust_bill->owed_sql;
4888 if ( $param->{owed} ) {
4889 my @owed = ref($param->{owed})
4890 ? @{ $param->{owed} }
4892 push @search, map { s/^owed/$owed_sql/; $_; }
4897 push @search, "0 != $owed_sql"
4898 if $param->{'open'};
4899 push @search, '0 != '. FS::cust_bill->net_sql
4903 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
4904 if $param->{'days'};
4907 if ( $param->{'newest_percust'} ) {
4909 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
4910 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
4912 my @newest_where = map { my $x = $_;
4913 $x =~ s/\bcust_bill\./newest_cust_bill./g;
4916 grep ! /^cust_main./, @search;
4917 my $newest_where = scalar(@newest_where)
4918 ? ' AND '. join(' AND ', @newest_where)
4922 push @search, "cust_bill._date = (
4923 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
4924 WHERE newest_cust_bill.custnum = cust_bill.custnum
4930 #agent virtualization
4931 my $curuser = $FS::CurrentUser::CurrentUser;
4932 if ( $curuser->username eq 'fs_queue'
4933 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
4935 my $newuser = qsearchs('access_user', {
4936 'username' => $username,
4940 $curuser = $newuser;
4942 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
4945 push @search, $curuser->agentnums_sql;
4947 join(' AND ', @search );
4959 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
4960 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base