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
9 use List::Util qw(min max);
11 use Text::Template 1.20;
13 use String::ShellQuote;
16 use Storable qw( freeze thaw );
18 use FS::UID qw( datasrc );
19 use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
20 use FS::Record qw( qsearch qsearchs dbh );
21 use FS::cust_main_Mixin;
23 use FS::cust_statement;
24 use FS::cust_bill_pkg;
25 use FS::cust_bill_pkg_display;
26 use FS::cust_bill_pkg_detail;
30 use FS::cust_credit_bill;
32 use FS::cust_pay_batch;
33 use FS::cust_bill_event;
36 use FS::cust_bill_pay;
37 use FS::cust_bill_pay_batch;
38 use FS::part_bill_event;
41 use FS::cust_bill_batch;
42 use FS::cust_bill_pay_pkg;
43 use FS::cust_credit_bill_pkg;
45 @ISA = qw( FS::cust_main_Mixin FS::Record );
48 $me = '[FS::cust_bill]';
50 #ask FS::UID to run this stuff for us later
51 FS::UID->install_callback( sub {
53 $money_char = $conf->config('money_char') || '$';
54 $date_format = $conf->config('date_format') || '%x'; #/YY
55 $rdate_format = $conf->config('date_format') || '%m/%d/%Y'; #/YYYY
56 $date_format_long = $conf->config('date_format_long') || '%b %o, %Y';
61 FS::cust_bill - Object methods for cust_bill records
67 $record = new FS::cust_bill \%hash;
68 $record = new FS::cust_bill { 'column' => 'value' };
70 $error = $record->insert;
72 $error = $new_record->replace($old_record);
74 $error = $record->delete;
76 $error = $record->check;
78 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
80 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
82 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
84 @cust_pay_objects = $cust_bill->cust_pay;
86 $tax_amount = $record->tax;
88 @lines = $cust_bill->print_text;
89 @lines = $cust_bill->print_text $time;
93 An FS::cust_bill object represents an invoice; a declaration that a customer
94 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
95 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
96 following fields are currently supported:
102 =item invnum - primary key (assigned automatically for new invoices)
104 =item custnum - customer (see L<FS::cust_main>)
106 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
107 L<Time::Local> and L<Date::Parse> for conversion functions.
109 =item charged - amount of this invoice
111 =item invoice_terms - optional terms override for this specific invoice
115 Customer info at invoice generation time
119 =item previous_balance
121 =item billing_balance
129 =item printed - deprecated
137 =item closed - books closed flag, empty or `Y'
139 =item statementnum - invoice aggregation (see L<FS::cust_statement>)
141 =item agent_invid - legacy invoice number
151 Creates a new invoice. To add the invoice to the database, see L<"insert">.
152 Invoices are normally created by calling the bill method of a customer object
153 (see L<FS::cust_main>).
157 sub table { 'cust_bill'; }
159 sub cust_linked { $_[0]->cust_main_custnum; }
160 sub cust_unlinked_msg {
162 "WARNING: can't find cust_main.custnum ". $self->custnum.
163 ' (cust_bill.invnum '. $self->invnum. ')';
168 Adds this invoice to the database ("Posts" the invoice). If there is an error,
169 returns the error, otherwise returns false.
175 warn "$me insert called\n" if $DEBUG;
177 local $SIG{HUP} = 'IGNORE';
178 local $SIG{INT} = 'IGNORE';
179 local $SIG{QUIT} = 'IGNORE';
180 local $SIG{TERM} = 'IGNORE';
181 local $SIG{TSTP} = 'IGNORE';
182 local $SIG{PIPE} = 'IGNORE';
184 my $oldAutoCommit = $FS::UID::AutoCommit;
185 local $FS::UID::AutoCommit = 0;
188 my $error = $self->SUPER::insert;
190 $dbh->rollback if $oldAutoCommit;
194 if ( $self->get('cust_bill_pkg') ) {
195 foreach my $cust_bill_pkg ( @{$self->get('cust_bill_pkg')} ) {
196 $cust_bill_pkg->invnum($self->invnum);
197 my $error = $cust_bill_pkg->insert;
199 $dbh->rollback if $oldAutoCommit;
200 return "can't create invoice line item: $error";
205 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
212 This method now works but you probably shouldn't use it. Instead, apply a
213 credit against the invoice.
215 Using this method to delete invoices outright is really, really bad. There
216 would be no record you ever posted this invoice, and there are no check to
217 make sure charged = 0 or that there are no associated cust_bill_pkg records.
219 Really, don't use it.
225 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
227 local $SIG{HUP} = 'IGNORE';
228 local $SIG{INT} = 'IGNORE';
229 local $SIG{QUIT} = 'IGNORE';
230 local $SIG{TERM} = 'IGNORE';
231 local $SIG{TSTP} = 'IGNORE';
232 local $SIG{PIPE} = 'IGNORE';
234 my $oldAutoCommit = $FS::UID::AutoCommit;
235 local $FS::UID::AutoCommit = 0;
238 foreach my $table (qw(
250 foreach my $linked ( $self->$table() ) {
251 my $error = $linked->delete;
253 $dbh->rollback if $oldAutoCommit;
260 my $error = $self->SUPER::delete(@_);
262 $dbh->rollback if $oldAutoCommit;
266 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
272 =item replace [ OLD_RECORD ]
274 You can, but probably shouldn't modify invoices...
276 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
277 supplied, replaces this record. If there is an error, returns the error,
278 otherwise returns false.
282 #replace can be inherited from Record.pm
284 # replace_check is now the preferred way to #implement replace data checks
285 # (so $object->replace() works without an argument)
288 my( $new, $old ) = ( shift, shift );
289 return "Can't modify closed invoice" if $old->closed =~ /^Y/i;
290 #return "Can't change _date!" unless $old->_date eq $new->_date;
291 return "Can't change _date" unless $old->_date == $new->_date;
292 return "Can't change charged" unless $old->charged == $new->charged
293 || $old->charged == 0;
300 Checks all fields to make sure this is a valid invoice. If there is an error,
301 returns the error, otherwise returns false. Called by the insert and replace
310 $self->ut_numbern('invnum')
311 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
312 || $self->ut_numbern('_date')
313 || $self->ut_money('charged')
314 || $self->ut_numbern('printed')
315 || $self->ut_enum('closed', [ '', 'Y' ])
316 || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
317 || $self->ut_numbern('agent_invid') #varchar?
319 return $error if $error;
321 $self->_date(time) unless $self->_date;
323 $self->printed(0) if $self->printed eq '';
330 Returns the displayed invoice number for this invoice: agent_invid if
331 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
337 if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
338 return $self->agent_invid;
340 return $self->invnum;
346 Returns a list consisting of the total previous balance for this customer,
347 followed by the previous outstanding invoices (as FS::cust_bill objects also).
354 my @cust_bill = sort { $a->_date <=> $b->_date }
355 grep { $_->owed != 0 && $_->_date < $self->_date }
356 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
358 foreach ( @cust_bill ) { $total += $_->owed; }
364 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
371 { 'table' => 'cust_bill_pkg',
372 'hashref' => { 'invnum' => $self->invnum },
373 'order_by' => 'ORDER BY billpkgnum',
378 =item cust_bill_pkg_pkgnum PKGNUM
380 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
385 sub cust_bill_pkg_pkgnum {
386 my( $self, $pkgnum ) = @_;
388 { 'table' => 'cust_bill_pkg',
389 'hashref' => { 'invnum' => $self->invnum,
392 'order_by' => 'ORDER BY billpkgnum',
399 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
406 my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
407 $self->cust_bill_pkg;
409 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
414 Returns true if any of the packages (or their definitions) corresponding to the
415 line items for this invoice have the no_auto flag set.
421 grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
424 =item open_cust_bill_pkg
426 Returns the open line items for this invoice.
428 Note that cust_bill_pkg with both setup and recur fees are returned as two
429 separate line items, each with only one fee.
433 # modeled after cust_main::open_cust_bill
434 sub open_cust_bill_pkg {
437 # grep { $_->owed > 0 } $self->cust_bill_pkg
439 my %other = ( 'recur' => 'setup',
440 'setup' => 'recur', );
442 foreach my $field ( qw( recur setup )) {
443 push @open, map { $_->set( $other{$field}, 0 ); $_; }
444 grep { $_->owed($field) > 0 }
445 $self->cust_bill_pkg;
451 =item cust_bill_event
453 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
457 sub cust_bill_event {
459 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
462 =item num_cust_bill_event
464 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
468 sub num_cust_bill_event {
471 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
472 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
473 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
474 $sth->fetchrow_arrayref->[0];
479 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
483 #false laziness w/cust_pkg.pm
487 'table' => 'cust_event',
488 'addl_from' => 'JOIN part_event USING ( eventpart )',
489 'hashref' => { 'tablenum' => $self->invnum },
490 'extra_sql' => " AND eventtable = 'cust_bill' ",
496 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
500 #false laziness w/cust_pkg.pm
504 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
505 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
506 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
507 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
508 $sth->fetchrow_arrayref->[0];
513 Returns the customer (see L<FS::cust_main>) for this invoice.
519 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
522 =item cust_suspend_if_balance_over AMOUNT
524 Suspends the customer associated with this invoice if the total amount owed on
525 this invoice and all older invoices is greater than the specified amount.
527 Returns a list: an empty list on success or a list of errors.
531 sub cust_suspend_if_balance_over {
532 my( $self, $amount ) = ( shift, shift );
533 my $cust_main = $self->cust_main;
534 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
537 $cust_main->suspend(@_);
543 Depreciated. See the cust_credited method.
545 #Returns a list consisting of the total previous credited (see
546 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
547 #outstanding credits (FS::cust_credit objects).
553 croak "FS::cust_bill->cust_credit depreciated; see ".
554 "FS::cust_bill->cust_credit_bill";
557 #my @cust_credit = sort { $a->_date <=> $b->_date }
558 # grep { $_->credited != 0 && $_->_date < $self->_date }
559 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
561 #foreach (@cust_credit) { $total += $_->credited; }
562 #$total, @cust_credit;
567 Depreciated. See the cust_bill_pay method.
569 #Returns all payments (see L<FS::cust_pay>) for this invoice.
575 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
577 #sort { $a->_date <=> $b->_date }
578 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
584 qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
587 sub cust_bill_pay_batch {
589 qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
594 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
600 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
601 sort { $a->_date <=> $b->_date }
602 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
607 =item cust_credit_bill
609 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
615 map { $_ } #return $self->num_cust_credit_bill unless wantarray;
616 sort { $a->_date <=> $b->_date }
617 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
621 sub cust_credit_bill {
622 shift->cust_credited(@_);
625 #=item cust_bill_pay_pkgnum PKGNUM
627 #Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
628 #with matching pkgnum.
632 #sub cust_bill_pay_pkgnum {
633 # my( $self, $pkgnum ) = @_;
634 # map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
635 # sort { $a->_date <=> $b->_date }
636 # qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
637 # 'pkgnum' => $pkgnum,
642 =item cust_bill_pay_pkg PKGNUM
644 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
645 applied against the matching pkgnum.
649 sub cust_bill_pay_pkg {
650 my( $self, $pkgnum ) = @_;
653 'select' => 'cust_bill_pay_pkg.*',
654 'table' => 'cust_bill_pay_pkg',
655 'addl_from' => ' LEFT JOIN cust_bill_pay USING ( billpaynum ) '.
656 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
657 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
658 " AND cust_bill_pkg.pkgnum = $pkgnum",
663 #=item cust_credited_pkgnum PKGNUM
665 #=item cust_credit_bill_pkgnum PKGNUM
667 #Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
668 #with matching pkgnum.
672 #sub cust_credited_pkgnum {
673 # my( $self, $pkgnum ) = @_;
674 # map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
675 # sort { $a->_date <=> $b->_date }
676 # qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
677 # 'pkgnum' => $pkgnum,
682 #sub cust_credit_bill_pkgnum {
683 # shift->cust_credited_pkgnum(@_);
686 =item cust_credit_bill_pkg PKGNUM
688 Returns all credit applications (see L<FS::cust_credit_bill>) for this invoice
689 applied against the matching pkgnum.
693 sub cust_credit_bill_pkg {
694 my( $self, $pkgnum ) = @_;
697 'select' => 'cust_credit_bill_pkg.*',
698 'table' => 'cust_credit_bill_pkg',
699 'addl_from' => ' LEFT JOIN cust_credit_bill USING ( creditbillnum ) '.
700 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
701 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
702 " AND cust_bill_pkg.pkgnum = $pkgnum",
709 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
716 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
718 foreach (@taxlines) { $total += $_->setup; }
724 Returns the amount owed (still outstanding) on this invoice, which is charged
725 minus all payment applications (see L<FS::cust_bill_pay>) and credit
726 applications (see L<FS::cust_credit_bill>).
732 my $balance = $self->charged;
733 $balance -= $_->amount foreach ( $self->cust_bill_pay );
734 $balance -= $_->amount foreach ( $self->cust_credited );
735 $balance = sprintf( "%.2f", $balance);
736 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
741 my( $self, $pkgnum ) = @_;
743 #my $balance = $self->charged;
745 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
747 $balance -= $_->amount for $self->cust_bill_pay_pkg($pkgnum);
748 $balance -= $_->amount for $self->cust_credit_bill_pkg($pkgnum);
750 $balance = sprintf( "%.2f", $balance);
751 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
755 =item apply_payments_and_credits [ OPTION => VALUE ... ]
757 Applies unapplied payments and credits to this invoice.
759 A hash of optional arguments may be passed. Currently "manual" is supported.
760 If true, a payment receipt is sent instead of a statement when
761 'payment_receipt_email' configuration option is set.
763 If there is an error, returns the error, otherwise returns false.
767 sub apply_payments_and_credits {
768 my( $self, %options ) = @_;
770 local $SIG{HUP} = 'IGNORE';
771 local $SIG{INT} = 'IGNORE';
772 local $SIG{QUIT} = 'IGNORE';
773 local $SIG{TERM} = 'IGNORE';
774 local $SIG{TSTP} = 'IGNORE';
775 local $SIG{PIPE} = 'IGNORE';
777 my $oldAutoCommit = $FS::UID::AutoCommit;
778 local $FS::UID::AutoCommit = 0;
781 $self->select_for_update; #mutex
783 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
784 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
786 if ( $conf->exists('pkg-balances') ) {
787 # limit @payments & @credits to those w/ a pkgnum grepped from $self
788 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
789 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
790 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
793 while ( $self->owed > 0 and ( @payments || @credits ) ) {
796 if ( @payments && @credits ) {
798 #decide which goes first by weight of top (unapplied) line item
800 my @open_lineitems = $self->open_cust_bill_pkg;
803 max( map { $_->part_pkg->pay_weight || 0 }
808 my $max_credit_weight =
809 max( map { $_->part_pkg->credit_weight || 0 }
815 #if both are the same... payments first? it has to be something
816 if ( $max_pay_weight >= $max_credit_weight ) {
822 } elsif ( @payments ) {
824 } elsif ( @credits ) {
827 die "guru meditation #12 and 35";
831 if ( $app eq 'pay' ) {
833 my $payment = shift @payments;
834 $unapp_amount = $payment->unapplied;
835 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
836 $app->pkgnum( $payment->pkgnum )
837 if $conf->exists('pkg-balances') && $payment->pkgnum;
839 } elsif ( $app eq 'credit' ) {
841 my $credit = shift @credits;
842 $unapp_amount = $credit->credited;
843 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
844 $app->pkgnum( $credit->pkgnum )
845 if $conf->exists('pkg-balances') && $credit->pkgnum;
848 die "guru meditation #12 and 35";
852 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
853 warn "owed_pkgnum ". $app->pkgnum;
854 $owed = $self->owed_pkgnum($app->pkgnum);
858 next unless $owed > 0;
860 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
861 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
863 $app->invnum( $self->invnum );
865 my $error = $app->insert(%options);
867 $dbh->rollback if $oldAutoCommit;
868 return "Error inserting ". $app->table. " record: $error";
870 die $error if $error;
874 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
879 =item generate_email OPTION => VALUE ...
887 sender address, required
891 alternate template name, optional
895 text attachment arrayref, optional
899 email subject, optional
903 notice name instead of "Invoice", optional
907 Returns an argument list to be passed to L<FS::Misc::send_email>.
918 my $me = '[FS::cust_bill::generate_email]';
921 'from' => $args{'from'},
922 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
926 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
927 'template' => $args{'template'},
928 'notice_name' => ( $args{'notice_name'} || 'Invoice' ),
931 my $cust_main = $self->cust_main;
933 if (ref($args{'to'}) eq 'ARRAY') {
934 $return{'to'} = $args{'to'};
936 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
937 $cust_main->invoicing_list
941 if ( $conf->exists('invoice_html') ) {
943 warn "$me creating HTML/text multipart message"
946 $return{'nobody'} = 1;
948 my $alternative = build MIME::Entity
949 'Type' => 'multipart/alternative',
950 'Encoding' => '7bit',
951 'Disposition' => 'inline'
955 if ( $conf->exists('invoice_email_pdf')
956 and scalar($conf->config('invoice_email_pdf_note')) ) {
958 warn "$me using 'invoice_email_pdf_note' in multipart message"
960 $data = [ map { $_ . "\n" }
961 $conf->config('invoice_email_pdf_note')
966 warn "$me not using 'invoice_email_pdf_note' in multipart message"
968 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
969 $data = $args{'print_text'};
971 $data = [ $self->print_text(\%opt) ];
976 $alternative->attach(
977 'Type' => 'text/plain',
978 #'Encoding' => 'quoted-printable',
979 'Encoding' => '7bit',
981 'Disposition' => 'inline',
984 $args{'from'} =~ /\@([\w\.\-]+)/;
985 my $from = $1 || 'example.com';
986 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
989 my $agentnum = $cust_main->agentnum;
990 if ( defined($args{'template'}) && length($args{'template'})
991 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
994 $logo = 'logo_'. $args{'template'}. '.png';
998 my $image_data = $conf->config_binary( $logo, $agentnum);
1000 my $image = build MIME::Entity
1001 'Type' => 'image/png',
1002 'Encoding' => 'base64',
1003 'Data' => $image_data,
1004 'Filename' => 'logo.png',
1005 'Content-ID' => "<$content_id>",
1009 if($conf->exists('invoice-barcode')){
1010 my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1011 $barcode = build MIME::Entity
1012 'Type' => 'image/png',
1013 'Encoding' => 'base64',
1014 'Data' => $self->invoice_barcode(0),
1015 'Filename' => 'barcode.png',
1016 'Content-ID' => "<$barcode_content_id>",
1018 $opt{'barcode_cid'} = $barcode_content_id;
1021 $alternative->attach(
1022 'Type' => 'text/html',
1023 'Encoding' => 'quoted-printable',
1024 'Data' => [ '<html>',
1027 ' '. encode_entities($return{'subject'}),
1030 ' <body bgcolor="#e8e8e8">',
1031 $self->print_html({ 'cid'=>$content_id, %opt }),
1035 'Disposition' => 'inline',
1036 #'Filename' => 'invoice.pdf',
1039 my @otherparts = ();
1040 if ( $cust_main->email_csv_cdr ) {
1042 push @otherparts, build MIME::Entity
1043 'Type' => 'text/csv',
1044 'Encoding' => '7bit',
1045 'Data' => [ map { "$_\n" }
1046 $self->call_details('prepend_billed_number' => 1)
1048 'Disposition' => 'attachment',
1049 'Filename' => 'usage-'. $self->invnum. '.csv',
1054 if ( $conf->exists('invoice_email_pdf') ) {
1059 # multipart/alternative
1065 my $related = build MIME::Entity 'Type' => 'multipart/related',
1066 'Encoding' => '7bit';
1068 #false laziness w/Misc::send_email
1069 $related->head->replace('Content-type',
1070 $related->mime_type.
1071 '; boundary="'. $related->head->multipart_boundary. '"'.
1072 '; type=multipart/alternative'
1075 $related->add_part($alternative);
1077 $related->add_part($image);
1079 my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
1081 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
1085 #no other attachment:
1087 # multipart/alternative
1092 $return{'content-type'} = 'multipart/related';
1093 if($conf->exists('invoice-barcode')){
1094 $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ];
1097 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
1099 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
1100 #$return{'disposition'} = 'inline';
1106 if ( $conf->exists('invoice_email_pdf') ) {
1107 warn "$me creating PDF attachment"
1110 #mime parts arguments a la MIME::Entity->build().
1111 $return{'mimeparts'} = [
1112 { $self->mimebuild_pdf(\%opt) }
1116 if ( $conf->exists('invoice_email_pdf')
1117 and scalar($conf->config('invoice_email_pdf_note')) ) {
1119 warn "$me using 'invoice_email_pdf_note'"
1121 $return{'body'} = [ map { $_ . "\n" }
1122 $conf->config('invoice_email_pdf_note')
1127 warn "$me not using 'invoice_email_pdf_note'"
1129 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1130 $return{'body'} = $args{'print_text'};
1132 $return{'body'} = [ $self->print_text(\%opt) ];
1145 Returns a list suitable for passing to MIME::Entity->build(), representing
1146 this invoice as PDF attachment.
1153 'Type' => 'application/pdf',
1154 'Encoding' => 'base64',
1155 'Data' => [ $self->print_pdf(@_) ],
1156 'Disposition' => 'attachment',
1157 'Filename' => 'invoice-'. $self->invnum. '.pdf',
1161 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1163 Sends this invoice to the destinations configured for this customer: sends
1164 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1166 Options can be passed as a hashref (recommended) or as a list of up to
1167 four values for templatename, agentnum, invoice_from and amount.
1169 I<template>, if specified, is the name of a suffix for alternate invoices.
1171 I<agentnum>, if specified, means that this invoice will only be sent for customers
1172 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
1173 single agent) or an arrayref of agentnums.
1175 I<invoice_from>, if specified, overrides the default email invoice From: address.
1177 I<amount>, if specified, only sends the invoice if the total amount owed on this
1178 invoice and all older invoices is greater than the specified amount.
1180 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1184 sub queueable_send {
1187 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1188 or die "invalid invoice number: " . $opt{invnum};
1190 my @args = ( $opt{template}, $opt{agentnum} );
1191 push @args, $opt{invoice_from}
1192 if exists($opt{invoice_from}) && $opt{invoice_from};
1194 my $error = $self->send( @args );
1195 die $error if $error;
1202 my( $template, $invoice_from, $notice_name );
1204 my $balance_over = 0;
1208 $template = $opt->{'template'} || '';
1209 if ( $agentnums = $opt->{'agentnum'} ) {
1210 $agentnums = [ $agentnums ] unless ref($agentnums);
1212 $invoice_from = $opt->{'invoice_from'};
1213 $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1214 $notice_name = $opt->{'notice_name'};
1216 $template = scalar(@_) ? shift : '';
1217 if ( scalar(@_) && $_[0] ) {
1218 $agentnums = ref($_[0]) ? shift : [ shift ];
1220 $invoice_from = shift if scalar(@_);
1221 $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1224 return 'N/A' unless ! $agentnums
1225 or grep { $_ == $self->cust_main->agentnum } @$agentnums;
1228 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
1230 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1231 $conf->config('invoice_from', $self->cust_main->agentnum );
1234 'template' => $template,
1235 'invoice_from' => $invoice_from,
1236 'notice_name' => ( $notice_name || 'Invoice' ),
1239 my @invoicing_list = $self->cust_main->invoicing_list;
1241 #$self->email_invoice(\%opt)
1243 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
1245 #$self->print_invoice(\%opt)
1247 if grep { $_ eq 'POST' } @invoicing_list; #postal
1249 $self->fax_invoice(\%opt)
1250 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1256 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
1258 Emails this invoice.
1260 Options can be passed as a hashref (recommended) or as a list of up to
1261 two values for templatename and invoice_from.
1263 I<template>, if specified, is the name of a suffix for alternate invoices.
1265 I<invoice_from>, if specified, overrides the default email invoice From: address.
1267 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1271 sub queueable_email {
1274 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1275 or die "invalid invoice number: " . $opt{invnum};
1277 my @args = ( $opt{template} );
1278 push @args, $opt{invoice_from}
1279 if exists($opt{invoice_from}) && $opt{invoice_from};
1281 my $error = $self->email( @args );
1282 die $error if $error;
1286 #sub email_invoice {
1290 my( $template, $invoice_from, $notice_name );
1293 $template = $opt->{'template'} || '';
1294 $invoice_from = $opt->{'invoice_from'};
1295 $notice_name = $opt->{'notice_name'} || 'Invoice';
1297 $template = scalar(@_) ? shift : '';
1298 $invoice_from = shift if scalar(@_);
1299 $notice_name = 'Invoice';
1302 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1303 $conf->config('invoice_from', $self->cust_main->agentnum );
1305 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1306 $self->cust_main->invoicing_list;
1308 if ( ! @invoicing_list ) { #no recipients
1309 if ( $conf->exists('cust_bill-no_recipients-error') ) {
1310 die 'No recipients for customer #'. $self->custnum;
1312 #default: better to notify this person than silence
1313 @invoicing_list = ($invoice_from);
1317 my $subject = $self->email_subject($template);
1319 my $error = send_email(
1320 $self->generate_email(
1321 'from' => $invoice_from,
1322 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1323 'subject' => $subject,
1324 'template' => $template,
1325 'notice_name' => $notice_name,
1328 die "can't email invoice: $error\n" if $error;
1329 #die "$error\n" if $error;
1336 #my $template = scalar(@_) ? shift : '';
1339 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1342 my $cust_main = $self->cust_main;
1343 my $name = $cust_main->name;
1344 my $name_short = $cust_main->name_short;
1345 my $invoice_number = $self->invnum;
1346 my $invoice_date = $self->_date_pretty;
1348 eval qq("$subject");
1351 =item lpr_data HASHREF | [ TEMPLATE ]
1353 Returns the postscript or plaintext for this invoice as an arrayref.
1355 Options can be passed as a hashref (recommended) or as a single optional value
1358 I<template>, if specified, is the name of a suffix for alternate invoices.
1360 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1366 my( $template, $notice_name );
1369 $template = $opt->{'template'} || '';
1370 $notice_name = $opt->{'notice_name'} || 'Invoice';
1372 $template = scalar(@_) ? shift : '';
1373 $notice_name = 'Invoice';
1377 'template' => $template,
1378 'notice_name' => $notice_name,
1381 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1382 [ $self->$method( \%opt ) ];
1385 =item print HASHREF | [ TEMPLATE ]
1387 Prints this invoice.
1389 Options can be passed as a hashref (recommended) or as a single optional
1392 I<template>, if specified, is the name of a suffix for alternate invoices.
1394 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1398 #sub print_invoice {
1401 my( $template, $notice_name );
1404 $template = $opt->{'template'} || '';
1405 $notice_name = $opt->{'notice_name'} || 'Invoice';
1407 $template = scalar(@_) ? shift : '';
1408 $notice_name = 'Invoice';
1412 'template' => $template,
1413 'notice_name' => $notice_name,
1416 if($conf->exists('invoice_print_pdf')) {
1417 # Add the invoice to the current batch.
1418 $self->batch_invoice(\%opt);
1421 do_print $self->lpr_data(\%opt);
1425 =item fax_invoice HASHREF | [ TEMPLATE ]
1429 Options can be passed as a hashref (recommended) or as a single optional
1432 I<template>, if specified, is the name of a suffix for alternate invoices.
1434 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1440 my( $template, $notice_name );
1443 $template = $opt->{'template'} || '';
1444 $notice_name = $opt->{'notice_name'} || 'Invoice';
1446 $template = scalar(@_) ? shift : '';
1447 $notice_name = 'Invoice';
1450 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1451 unless $conf->exists('invoice_latex');
1453 my $dialstring = $self->cust_main->getfield('fax');
1457 'template' => $template,
1458 'notice_name' => $notice_name,
1461 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1462 'dialstring' => $dialstring,
1464 die $error if $error;
1468 =item batch_invoice [ HASHREF ]
1470 Place this invoice into the open batch (see C<FS::bill_batch>). If there
1471 isn't an open batch, one will be created.
1476 my ($self, $opt) = @_;
1477 my $batch = FS::bill_batch->get_open_batch;
1478 my $cust_bill_batch = FS::cust_bill_batch->new({
1479 batchnum => $batch->batchnum,
1480 invnum => $self->invnum,
1482 return $cust_bill_batch->insert($opt);
1485 =item ftp_invoice [ TEMPLATENAME ]
1487 Sends this invoice data via FTP.
1489 TEMPLATENAME is unused?
1495 my $template = scalar(@_) ? shift : '';
1498 'protocol' => 'ftp',
1499 'server' => $conf->config('cust_bill-ftpserver'),
1500 'username' => $conf->config('cust_bill-ftpusername'),
1501 'password' => $conf->config('cust_bill-ftppassword'),
1502 'dir' => $conf->config('cust_bill-ftpdir'),
1503 'format' => $conf->config('cust_bill-ftpformat'),
1507 =item spool_invoice [ TEMPLATENAME ]
1509 Spools this invoice data (see L<FS::spool_csv>)
1511 TEMPLATENAME is unused?
1517 my $template = scalar(@_) ? shift : '';
1520 'format' => $conf->config('cust_bill-spoolformat'),
1521 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1525 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1527 Like B<send>, but only sends the invoice if it is the newest open invoice for
1532 sub send_if_newest {
1537 grep { $_->owed > 0 }
1538 qsearch('cust_bill', {
1539 'custnum' => $self->custnum,
1540 #'_date' => { op=>'>', value=>$self->_date },
1541 'invnum' => { op=>'>', value=>$self->invnum },
1548 =item send_csv OPTION => VALUE, ...
1550 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1554 protocol - currently only "ftp"
1560 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1561 and YYMMDDHHMMSS is a timestamp.
1563 See L</print_csv> for a description of the output format.
1568 my($self, %opt) = @_;
1572 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1573 mkdir $spooldir, 0700 unless -d $spooldir;
1575 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1576 my $file = "$spooldir/$tracctnum.csv";
1578 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1580 open(CSV, ">$file") or die "can't open $file: $!";
1588 if ( $opt{protocol} eq 'ftp' ) {
1589 eval "use Net::FTP;";
1591 $net = Net::FTP->new($opt{server}) or die @$;
1593 die "unknown protocol: $opt{protocol}";
1596 $net->login( $opt{username}, $opt{password} )
1597 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1599 $net->binary or die "can't set binary mode";
1601 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1603 $net->put($file) or die "can't put $file: $!";
1613 Spools CSV invoice data.
1619 =item format - 'default' or 'billco'
1621 =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>).
1623 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1625 =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.
1632 my($self, %opt) = @_;
1634 my $cust_main = $self->cust_main;
1636 if ( $opt{'dest'} ) {
1637 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1638 $cust_main->invoicing_list;
1639 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1640 || ! keys %invoicing_list;
1643 if ( $opt{'balanceover'} ) {
1645 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1648 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1649 mkdir $spooldir, 0700 unless -d $spooldir;
1651 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1655 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1656 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1659 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1661 open(CSV, ">>$file") or die "can't open $file: $!";
1662 flock(CSV, LOCK_EX);
1667 if ( lc($opt{'format'}) eq 'billco' ) {
1669 flock(CSV, LOCK_UN);
1674 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1677 open(CSV,">>$file") or die "can't open $file: $!";
1678 flock(CSV, LOCK_EX);
1684 flock(CSV, LOCK_UN);
1691 =item print_csv OPTION => VALUE, ...
1693 Returns CSV data for this invoice.
1697 format - 'default' or 'billco'
1699 Returns a list consisting of two scalars. The first is a single line of CSV
1700 header information for this invoice. The second is one or more lines of CSV
1701 detail information for this invoice.
1703 If I<format> is not specified or "default", the fields of the CSV file are as
1706 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1710 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1712 B<record_type> is C<cust_bill> for the initial header line only. The
1713 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1714 fields are filled in.
1716 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1717 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1720 =item invnum - invoice number
1722 =item custnum - customer number
1724 =item _date - invoice date
1726 =item charged - total invoice amount
1728 =item first - customer first name
1730 =item last - customer first name
1732 =item company - company name
1734 =item address1 - address line 1
1736 =item address2 - address line 1
1746 =item pkg - line item description
1748 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1750 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1752 =item sdate - start date for recurring fee
1754 =item edate - end date for recurring fee
1758 If I<format> is "billco", the fields of the header CSV file are as follows:
1760 +-------------------------------------------------------------------+
1761 | FORMAT HEADER FILE |
1762 |-------------------------------------------------------------------|
1763 | Field | Description | Name | Type | Width |
1764 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1765 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1766 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1767 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1768 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1769 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1770 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1771 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1772 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1773 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1774 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1775 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1776 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1777 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1778 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1779 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1780 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1781 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1782 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1783 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1784 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1785 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1786 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1787 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1788 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1789 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1790 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1791 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1792 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1793 +-------+-------------------------------+------------+------+-------+
1795 If I<format> is "billco", the fields of the detail CSV file are as follows:
1797 FORMAT FOR DETAIL FILE
1799 Field | Description | Name | Type | Width
1800 1 | N/A-Leave Empty | RC | CHAR | 2
1801 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1802 3 | Account Number | TRACCTNUM | CHAR | 15
1803 4 | Invoice Number | TRINVOICE | CHAR | 15
1804 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1805 6 | Transaction Detail | DETAILS | CHAR | 100
1806 7 | Amount | AMT | NUM* | 9
1807 8 | Line Format Control** | LNCTRL | CHAR | 2
1808 9 | Grouping Code | GROUP | CHAR | 2
1809 10 | User Defined | ACCT CODE | CHAR | 15
1814 my($self, %opt) = @_;
1816 eval "use Text::CSV_XS";
1819 my $cust_main = $self->cust_main;
1821 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1823 if ( lc($opt{'format'}) eq 'billco' ) {
1826 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1828 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1830 my( $previous_balance, @unused ) = $self->previous; #previous balance
1832 my $pmt_cr_applied = 0;
1833 $pmt_cr_applied += $_->{'amount'}
1834 foreach ( $self->_items_payments, $self->_items_credits ) ;
1836 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1839 '', # 1 | N/A-Leave Empty CHAR 2
1840 '', # 2 | N/A-Leave Empty CHAR 15
1841 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1842 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1843 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1844 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1845 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1846 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1847 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1848 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1849 '', # 10 | Ancillary Billing Information CHAR 30
1850 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1851 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1854 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1857 $duedate, # 14 | Bill Due Date CHAR 10
1859 $previous_balance, # 15 | Previous Balance NUM* 9
1860 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1861 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1862 $totaldue, # 18 | Total Amt Due NUM* 9
1863 $totaldue, # 19 | Total Amt Due NUM* 9
1864 '', # 20 | 30 Day Aging NUM* 9
1865 '', # 21 | 60 Day Aging NUM* 9
1866 '', # 22 | 90 Day Aging NUM* 9
1867 'N', # 23 | Y/N CHAR 1
1868 '', # 24 | Remittance automation CHAR 100
1869 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1870 $self->custnum, # 26 | Customer Reference Number CHAR 15
1871 '0', # 27 | Federal Tax*** NUM* 9
1872 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1873 '0', # 29 | Other Taxes & Fees*** NUM* 9
1882 time2str("%x", $self->_date),
1883 sprintf("%.2f", $self->charged),
1884 ( map { $cust_main->getfield($_) }
1885 qw( first last company address1 address2 city state zip country ) ),
1887 ) or die "can't create csv";
1890 my $header = $csv->string. "\n";
1893 if ( lc($opt{'format'}) eq 'billco' ) {
1896 foreach my $item ( $self->_items_pkg ) {
1899 '', # 1 | N/A-Leave Empty CHAR 2
1900 '', # 2 | N/A-Leave Empty CHAR 15
1901 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1902 $self->invnum, # 4 | Invoice Number CHAR 15
1903 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1904 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1905 $item->{'amount'}, # 7 | Amount NUM* 9
1906 '', # 8 | Line Format Control** CHAR 2
1907 '', # 9 | Grouping Code CHAR 2
1908 '', # 10 | User Defined CHAR 15
1911 $detail .= $csv->string. "\n";
1917 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1919 my($pkg, $setup, $recur, $sdate, $edate);
1920 if ( $cust_bill_pkg->pkgnum ) {
1922 ($pkg, $setup, $recur, $sdate, $edate) = (
1923 $cust_bill_pkg->part_pkg->pkg,
1924 ( $cust_bill_pkg->setup != 0
1925 ? sprintf("%.2f", $cust_bill_pkg->setup )
1927 ( $cust_bill_pkg->recur != 0
1928 ? sprintf("%.2f", $cust_bill_pkg->recur )
1930 ( $cust_bill_pkg->sdate
1931 ? time2str("%x", $cust_bill_pkg->sdate)
1933 ($cust_bill_pkg->edate
1934 ?time2str("%x", $cust_bill_pkg->edate)
1938 } else { #pkgnum tax
1939 next unless $cust_bill_pkg->setup != 0;
1940 $pkg = $cust_bill_pkg->desc;
1941 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
1942 ( $sdate, $edate ) = ( '', '' );
1948 ( map { '' } (1..11) ),
1949 ($pkg, $setup, $recur, $sdate, $edate)
1950 ) or die "can't create csv";
1952 $detail .= $csv->string. "\n";
1958 ( $header, $detail );
1964 Pays this invoice with a compliemntary payment. If there is an error,
1965 returns the error, otherwise returns false.
1971 my $cust_pay = new FS::cust_pay ( {
1972 'invnum' => $self->invnum,
1973 'paid' => $self->owed,
1976 'payinfo' => $self->cust_main->payinfo,
1984 Attempts to pay this invoice with a credit card payment via a
1985 Business::OnlinePayment realtime gateway. See
1986 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1987 for supported processors.
1993 $self->realtime_bop( 'CC', @_ );
1998 Attempts to pay this invoice with an electronic check (ACH) payment via a
1999 Business::OnlinePayment realtime gateway. See
2000 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2001 for supported processors.
2007 $self->realtime_bop( 'ECHECK', @_ );
2012 Attempts to pay this invoice with phone bill (LEC) payment via a
2013 Business::OnlinePayment realtime gateway. See
2014 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2015 for supported processors.
2021 $self->realtime_bop( 'LEC', @_ );
2025 my( $self, $method ) = @_;
2027 my $cust_main = $self->cust_main;
2028 my $balance = $cust_main->balance;
2029 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2030 $amount = sprintf("%.2f", $amount);
2031 return "not run (balance $balance)" unless $amount > 0;
2033 my $description = 'Internet Services';
2034 if ( $conf->exists('business-onlinepayment-description') ) {
2035 my $dtempl = $conf->config('business-onlinepayment-description');
2037 my $agent_obj = $cust_main->agent
2038 or die "can't retreive agent for $cust_main (agentnum ".
2039 $cust_main->agentnum. ")";
2040 my $agent = $agent_obj->agent;
2041 my $pkgs = join(', ',
2042 map { $_->part_pkg->pkg }
2043 grep { $_->pkgnum } $self->cust_bill_pkg
2045 $description = eval qq("$dtempl");
2048 $cust_main->realtime_bop($method, $amount,
2049 'description' => $description,
2050 'invnum' => $self->invnum,
2051 #this didn't do what we want, it just calls apply_payments_and_credits
2053 'apply_to_invoice' => 1,
2055 #this changes application behavior: auto payments
2056 #triggered against a specific invoice are now applied
2057 #to that invoice instead of oldest open.
2063 =item batch_card OPTION => VALUE...
2065 Adds a payment for this invoice to the pending credit card batch (see
2066 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2067 runs the payment using a realtime gateway.
2072 my ($self, %options) = @_;
2073 my $cust_main = $self->cust_main;
2075 $options{invnum} = $self->invnum;
2077 $cust_main->batch_card(%options);
2080 sub _agent_template {
2082 $self->cust_main->agent_template;
2085 sub _agent_invoice_from {
2087 $self->cust_main->agent_invoice_from;
2090 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2092 Returns an text invoice, as a list of lines.
2094 Options can be passed as a hashref (recommended) or as a list of time, template
2095 and then any key/value pairs for any other options.
2097 I<time>, if specified, is used to control the printing of overdue messages. The
2098 default is now. It isn't the date of the invoice; that's the `_date' field.
2099 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2100 L<Time::Local> and L<Date::Parse> for conversion functions.
2102 I<template>, if specified, is the name of a suffix for alternate invoices.
2104 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2110 my( $today, $template, %opt );
2112 %opt = %{ shift() };
2113 $today = delete($opt{'time'}) || '';
2114 $template = delete($opt{template}) || '';
2116 ( $today, $template, %opt ) = @_;
2119 my %params = ( 'format' => 'template' );
2120 $params{'time'} = $today if $today;
2121 $params{'template'} = $template if $template;
2122 $params{$_} = $opt{$_}
2123 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2125 $self->print_generic( %params );
2128 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2130 Internal method - returns a filename of a filled-in LaTeX template for this
2131 invoice (Note: add ".tex" to get the actual filename), and a filename of
2132 an associated logo (with the .eps extension included).
2134 See print_ps and print_pdf for methods that return PostScript and PDF output.
2136 Options can be passed as a hashref (recommended) or as a list of time, template
2137 and then any key/value pairs for any other options.
2139 I<time>, if specified, is used to control the printing of overdue messages. The
2140 default is now. It isn't the date of the invoice; that's the `_date' field.
2141 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2142 L<Time::Local> and L<Date::Parse> for conversion functions.
2144 I<template>, if specified, is the name of a suffix for alternate invoices.
2146 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2152 my( $today, $template, %opt );
2154 %opt = %{ shift() };
2155 $today = delete($opt{'time'}) || '';
2156 $template = delete($opt{template}) || '';
2158 ( $today, $template, %opt ) = @_;
2161 my %params = ( 'format' => 'latex' );
2162 $params{'time'} = $today if $today;
2163 $params{'template'} = $template if $template;
2164 $params{$_} = $opt{$_}
2165 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2167 $template ||= $self->_agent_template;
2169 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2170 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2174 ) or die "can't open temp file: $!\n";
2176 my $agentnum = $self->cust_main->agentnum;
2178 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2179 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2180 or die "can't write temp file: $!\n";
2182 print $lh $conf->config_binary('logo.eps', $agentnum)
2183 or die "can't write temp file: $!\n";
2186 $params{'logo_file'} = $lh->filename;
2188 if($conf->exists('invoice-barcode')){
2189 my $png_file = $self->invoice_barcode($dir);
2190 my $eps_file = $png_file;
2191 $eps_file =~ s/\.png$/.eps/g;
2192 $png_file =~ /(barcode.*png)/;
2194 $eps_file =~ /(barcode.*eps)/;
2197 my $curr_dir = cwd();
2199 # after painfuly long experimentation, it was determined that sam2p won't
2200 # accept : and other chars in the path, no matter how hard I tried to
2201 # escape them, hence the chdir (and chdir back, just to be safe)
2202 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
2203 or die "sam2p failed: $!\n";
2207 $params{'barcode_file'} = $eps_file;
2210 my @filled_in = $self->print_generic( %params );
2212 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2216 ) or die "can't open temp file: $!\n";
2217 print $fh join('', @filled_in );
2220 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2221 return ($1, $params{'logo_file'}, $params{'barcode_file'});
2225 =item invoice_barcode DIR_OR_FALSE
2227 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2228 it is taken as the temp directory where the PNG file will be generated and the
2229 PNG file name is returned. Otherwise, the PNG image itself is returned.
2233 sub invoice_barcode {
2234 my ($self, $dir) = (shift,shift);
2236 my $gdbar = new GD::Barcode('Code39',$self->invnum);
2237 die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2238 my $gd = $gdbar->plot(Height => 30);
2241 my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2245 ) or die "can't open temp file: $!\n";
2246 print $bh $gd->png or die "cannot write barcode to file: $!\n";
2247 my $png_file = $bh->filename;
2254 =item print_generic OPTION => VALUE ...
2256 Internal method - returns a filled-in template for this invoice as a scalar.
2258 See print_ps and print_pdf for methods that return PostScript and PDF output.
2260 Non optional options include
2261 format - latex, html, template
2263 Optional options include
2265 template - a value used as a suffix for a configuration template
2267 time - a value used to control the printing of overdue messages. The
2268 default is now. It isn't the date of the invoice; that's the `_date' field.
2269 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2270 L<Time::Local> and L<Date::Parse> for conversion functions.
2274 unsquelch_cdr - overrides any per customer cdr squelching when true
2276 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2280 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2281 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2282 # yes: fixed width (dot matrix) text printing will be borked
2285 my( $self, %params ) = @_;
2286 my $today = $params{today} ? $params{today} : time;
2287 warn "$me print_generic called on $self with suffix $params{template}\n"
2290 my $format = $params{format};
2291 die "Unknown format: $format"
2292 unless $format =~ /^(latex|html|template)$/;
2294 my $cust_main = $self->cust_main;
2295 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2296 unless $cust_main->payname
2297 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2299 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2300 'html' => [ '<%=', '%>' ],
2301 'template' => [ '{', '}' ],
2304 warn "$me print_generic creating template\n"
2307 #create the template
2308 my $template = $params{template} ? $params{template} : $self->_agent_template;
2309 my $templatefile = "invoice_$format";
2310 $templatefile .= "_$template"
2311 if length($template);
2312 my @invoice_template = map "$_\n", $conf->config($templatefile)
2313 or die "cannot load config data $templatefile";
2316 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2317 #change this to a die when the old code is removed
2318 warn "old-style invoice template $templatefile; ".
2319 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2320 $old_latex = 'true';
2321 @invoice_template = _translate_old_latex_format(@invoice_template);
2324 warn "$me print_generic creating T:T object\n"
2327 my $text_template = new Text::Template(
2329 SOURCE => \@invoice_template,
2330 DELIMITERS => $delimiters{$format},
2333 warn "$me print_generic compiling T:T object\n"
2336 $text_template->compile()
2337 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2340 # additional substitution could possibly cause breakage in existing templates
2341 my %convert_maps = (
2343 'notes' => sub { map "$_", @_ },
2344 'footer' => sub { map "$_", @_ },
2345 'smallfooter' => sub { map "$_", @_ },
2346 'returnaddress' => sub { map "$_", @_ },
2347 'coupon' => sub { map "$_", @_ },
2348 'summary' => sub { map "$_", @_ },
2354 s/%%(.*)$/<!-- $1 -->/g;
2355 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2356 s/\\begin\{enumerate\}/<ol>/g;
2358 s/\\end\{enumerate\}/<\/ol>/g;
2359 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2368 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2370 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2375 s/\\\\\*?\s*$/<BR>/;
2376 s/\\hyphenation\{[\w\s\-]+}//;
2381 'coupon' => sub { "" },
2382 'summary' => sub { "" },
2389 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2390 s/\\begin\{enumerate\}//g;
2392 s/\\end\{enumerate\}//g;
2393 s/\\textbf\{(.*)\}/$1/g;
2400 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2402 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2407 s/\\\\\*?\s*$/\n/; # dubious
2408 s/\\hyphenation\{[\w\s\-]+}//;
2412 'coupon' => sub { "" },
2413 'summary' => sub { "" },
2418 # hashes for differing output formats
2419 my %nbsps = ( 'latex' => '~',
2420 'html' => '', # '&nbps;' would be nice
2421 'template' => '', # not used
2423 my $nbsp = $nbsps{$format};
2425 my %escape_functions = ( 'latex' => \&_latex_escape,
2426 'html' => \&_html_escape_nbsp,#\&encode_entities,
2427 'template' => sub { shift },
2429 my $escape_function = $escape_functions{$format};
2430 my $escape_function_nonbsp = ($format eq 'html')
2431 ? \&_html_escape : $escape_function;
2433 my %date_formats = ( 'latex' => $date_format_long,
2434 'html' => $date_format_long,
2437 $date_formats{'html'} =~ s/ / /g;
2439 my $date_format = $date_formats{$format};
2441 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2443 'html' => sub { return '<b>'. shift(). '</b>'
2445 'template' => sub { shift },
2447 my $embolden_function = $embolden_functions{$format};
2449 my %newline_tokens = ( 'latex' => '\\\\',
2453 my $newline_token = $newline_tokens{$format};
2455 warn "$me generating template variables\n"
2458 # generate template variables
2461 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2465 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2471 $returnaddress = join("\n",
2472 $conf->config_orbase("invoice_${format}returnaddress", $template)
2475 } elsif ( grep /\S/,
2476 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2478 my $convert_map = $convert_maps{$format}{'returnaddress'};
2481 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2486 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2488 my $convert_map = $convert_maps{$format}{'returnaddress'};
2489 $returnaddress = join( "\n", &$convert_map(
2490 map { s/( {2,})/'~' x length($1)/eg;
2494 ( $conf->config('company_name', $self->cust_main->agentnum),
2495 $conf->config('company_address', $self->cust_main->agentnum),
2502 my $warning = "Couldn't find a return address; ".
2503 "do you need to set the company_address configuration value?";
2505 $returnaddress = $nbsp;
2506 #$returnaddress = $warning;
2510 warn "$me generating invoice data\n"
2513 my $agentnum = $self->cust_main->agentnum;
2515 my %invoice_data = (
2518 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2519 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2520 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
2521 'returnaddress' => $returnaddress,
2522 'agent' => &$escape_function($cust_main->agent->agent),
2525 'invnum' => $self->invnum,
2526 'date' => time2str($date_format, $self->_date),
2527 'today' => time2str($date_format_long, $today),
2528 'terms' => $self->terms,
2529 'template' => $template, #params{'template'},
2530 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2531 'current_charges' => sprintf("%.2f", $self->charged),
2532 'duedate' => $self->due_date2str($rdate_format), #date_format?
2535 'custnum' => $cust_main->display_custnum,
2536 'agent_custid' => &$escape_function($cust_main->agent_custid),
2537 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2538 payname company address1 address2 city state zip fax
2542 'ship_enable' => $conf->exists('invoice-ship_address'),
2543 'unitprices' => $conf->exists('invoice-unitprice'),
2544 'smallernotes' => $conf->exists('invoice-smallernotes'),
2545 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2546 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2548 #layout info -- would be fancy to calc some of this and bury the template
2550 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2551 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2552 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2553 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2554 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2555 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2556 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2557 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2558 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2559 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2561 # better hang on to conf_dir for a while (for old templates)
2562 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2564 #these are only used when doing paged plaintext
2570 my $min_sdate = 999999999999;
2572 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2573 next unless $cust_bill_pkg->pkgnum > 0;
2574 $min_sdate = $cust_bill_pkg->sdate
2575 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2576 $max_edate = $cust_bill_pkg->edate
2577 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2580 $invoice_data{'bill_period'} = '';
2581 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
2582 . " to " . time2str('%e %h', $max_edate)
2583 if ($max_edate != 0 && $min_sdate != 999999999999);
2585 $invoice_data{finance_section} = '';
2586 if ( $conf->config('finance_pkgclass') ) {
2588 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2589 $invoice_data{finance_section} = $pkg_class->categoryname;
2591 $invoice_data{finance_amount} = '0.00';
2592 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2594 my $countrydefault = $conf->config('countrydefault') || 'US';
2595 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2596 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2597 my $method = $prefix.$_;
2598 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2600 $invoice_data{'ship_country'} = ''
2601 if ( $invoice_data{'ship_country'} eq $countrydefault );
2603 $invoice_data{'cid'} = $params{'cid'}
2606 if ( $cust_main->country eq $countrydefault ) {
2607 $invoice_data{'country'} = '';
2609 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2613 $invoice_data{'address'} = \@address;
2615 $cust_main->payname.
2616 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2617 ? " (P.O. #". $cust_main->payinfo. ")"
2621 push @address, $cust_main->company
2622 if $cust_main->company;
2623 push @address, $cust_main->address1;
2624 push @address, $cust_main->address2
2625 if $cust_main->address2;
2627 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2628 push @address, $invoice_data{'country'}
2629 if $invoice_data{'country'};
2631 while (scalar(@address) < 5);
2633 $invoice_data{'logo_file'} = $params{'logo_file'}
2634 if $params{'logo_file'};
2635 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2636 if $params{'barcode_file'};
2637 $invoice_data{'barcode_img'} = $params{'barcode_img'}
2638 if $params{'barcode_img'};
2639 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2640 if $params{'barcode_cid'};
2642 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2643 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2644 #my $balance_due = $self->owed + $pr_total - $cr_total;
2645 my $balance_due = $self->owed + $pr_total;
2646 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2647 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2648 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2649 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2651 my $summarypage = '';
2652 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2655 $invoice_data{'summarypage'} = $summarypage;
2657 warn "$me substituting variables in notes, footer, smallfooter\n"
2660 foreach my $include (qw( notes footer smallfooter coupon )) {
2662 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2665 if ( $conf->exists($inc_file, $agentnum)
2666 && length( $conf->config($inc_file, $agentnum) ) ) {
2668 @inc_src = $conf->config($inc_file, $agentnum);
2672 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2674 my $convert_map = $convert_maps{$format}{$include};
2676 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2677 s/--\@\]/$delimiters{$format}[1]/g;
2680 &$convert_map( $conf->config($inc_file, $agentnum) );
2684 my $inc_tt = new Text::Template (
2686 SOURCE => [ map "$_\n", @inc_src ],
2687 DELIMITERS => $delimiters{$format},
2688 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2690 unless ( $inc_tt->compile() ) {
2691 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2692 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2696 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2698 $invoice_data{$include} =~ s/\n+$//
2699 if ($format eq 'latex');
2702 $invoice_data{'po_line'} =
2703 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2704 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2707 my %money_chars = ( 'latex' => '',
2708 'html' => $conf->config('money_char') || '$',
2711 my $money_char = $money_chars{$format};
2713 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2714 'html' => $conf->config('money_char') || '$',
2717 my $other_money_char = $other_money_chars{$format};
2718 $invoice_data{'dollar'} = $other_money_char;
2720 my @detail_items = ();
2721 my @total_items = ();
2725 $invoice_data{'detail_items'} = \@detail_items;
2726 $invoice_data{'total_items'} = \@total_items;
2727 $invoice_data{'buf'} = \@buf;
2728 $invoice_data{'sections'} = \@sections;
2730 warn "$me generating sections\n"
2733 my $previous_section = { 'description' => 'Previous Charges',
2734 'subtotal' => $other_money_char.
2735 sprintf('%.2f', $pr_total),
2736 'summarized' => $summarypage ? 'Y' : '',
2738 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
2739 join(' / ', map { $cust_main->balance_date_range(@$_) }
2740 $self->_prior_month30s
2742 if $conf->exists('invoice_include_aging');
2745 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2746 'subtotal' => $taxtotal, # adjusted below
2747 'summarized' => $summarypage ? 'Y' : '',
2749 my $tax_weight = _pkg_category($tax_section->{description})
2750 ? _pkg_category($tax_section->{description})->weight
2752 $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2753 $tax_section->{'sort_weight'} = $tax_weight;
2756 my $adjusttotal = 0;
2757 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2758 'subtotal' => 0, # adjusted below
2759 'summarized' => $summarypage ? 'Y' : '',
2761 my $adjust_weight = _pkg_category($adjust_section->{description})
2762 ? _pkg_category($adjust_section->{description})->weight
2764 $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2765 $adjust_section->{'sort_weight'} = $adjust_weight;
2767 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2768 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2769 $invoice_data{'multisection'} = $multisection;
2770 my $late_sections = [];
2771 my $extra_sections = [];
2772 my $extra_lines = ();
2773 if ( $multisection ) {
2774 ($extra_sections, $extra_lines) =
2775 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2776 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2778 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2780 push @detail_items, @$extra_lines if $extra_lines;
2782 $self->_items_sections( $late_sections, # this could stand a refactor
2784 $escape_function_nonbsp,
2788 if ($conf->exists('svc_phone_sections')) {
2789 my ($phone_sections, $phone_lines) =
2790 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2791 push @{$late_sections}, @$phone_sections;
2792 push @detail_items, @$phone_lines;
2795 push @sections, { 'description' => '', 'subtotal' => '' };
2798 unless ( $conf->exists('disable_previous_balance')
2799 || $conf->exists('previous_balance-summary_only')
2803 warn "$me adding previous balances\n"
2806 foreach my $line_item ( $self->_items_previous ) {
2809 ext_description => [],
2811 $detail->{'ref'} = $line_item->{'pkgnum'};
2812 $detail->{'quantity'} = 1;
2813 $detail->{'section'} = $previous_section;
2814 $detail->{'description'} = &$escape_function($line_item->{'description'});
2815 if ( exists $line_item->{'ext_description'} ) {
2816 @{$detail->{'ext_description'}} = map {
2817 &$escape_function($_);
2818 } @{$line_item->{'ext_description'}};
2820 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2821 $line_item->{'amount'};
2822 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2824 push @detail_items, $detail;
2825 push @buf, [ $detail->{'description'},
2826 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2832 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2833 push @buf, ['','-----------'];
2834 push @buf, [ 'Total Previous Balance',
2835 $money_char. sprintf("%10.2f", $pr_total) ];
2839 if ( $conf->exists('svc_phone-did-summary') ) {
2840 warn "$me adding DID summary\n"
2843 my ($didsummary,$minutes) = $self->_did_summary;
2844 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
2846 { 'description' => $didsummary_desc,
2847 'ext_description' => [ $didsummary, $minutes ],
2852 foreach my $section (@sections, @$late_sections) {
2854 warn "$me adding section \n". Dumper($section)
2857 # begin some normalization
2858 $section->{'subtotal'} = $section->{'amount'}
2860 && !exists($section->{subtotal})
2861 && exists($section->{amount});
2863 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2864 if ( $invoice_data{finance_section} &&
2865 $section->{'description'} eq $invoice_data{finance_section} );
2867 $section->{'subtotal'} = $other_money_char.
2868 sprintf('%.2f', $section->{'subtotal'})
2871 # continue some normalization
2872 $section->{'amount'} = $section->{'subtotal'}
2876 if ( $section->{'description'} ) {
2877 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2882 warn "$me setting options\n"
2885 my $multilocation = scalar($cust_main->cust_location); #too expensive?
2887 $options{'section'} = $section if $multisection;
2888 $options{'format'} = $format;
2889 $options{'escape_function'} = $escape_function;
2890 $options{'format_function'} = sub { () } unless $unsquelched;
2891 $options{'unsquelched'} = $unsquelched;
2892 $options{'summary_page'} = $summarypage;
2893 $options{'skip_usage'} =
2894 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2895 $options{'multilocation'} = $multilocation;
2896 $options{'multisection'} = $multisection;
2898 warn "$me searching for line items\n"
2901 foreach my $line_item ( $self->_items_pkg(%options) ) {
2903 warn "$me adding line item $line_item\n"
2907 ext_description => [],
2909 $detail->{'ref'} = $line_item->{'pkgnum'};
2910 $detail->{'quantity'} = $line_item->{'quantity'};
2911 $detail->{'section'} = $section;
2912 $detail->{'description'} = &$escape_function($line_item->{'description'});
2913 if ( exists $line_item->{'ext_description'} ) {
2914 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2916 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2917 $line_item->{'amount'};
2918 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2919 $line_item->{'unit_amount'};
2920 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2922 push @detail_items, $detail;
2923 push @buf, ( [ $detail->{'description'},
2924 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2926 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2930 if ( $section->{'description'} ) {
2931 push @buf, ( ['','-----------'],
2932 [ $section->{'description'}. ' sub-total',
2933 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2942 $invoice_data{current_less_finance} =
2943 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2945 if ( $multisection && !$conf->exists('disable_previous_balance')
2946 || $conf->exists('previous_balance-summary_only') )
2948 unshift @sections, $previous_section if $pr_total;
2951 warn "$me adding taxes\n"
2954 foreach my $tax ( $self->_items_tax ) {
2956 $taxtotal += $tax->{'amount'};
2958 my $description = &$escape_function( $tax->{'description'} );
2959 my $amount = sprintf( '%.2f', $tax->{'amount'} );
2961 if ( $multisection ) {
2963 my $money = $old_latex ? '' : $money_char;
2964 push @detail_items, {
2965 ext_description => [],
2968 description => $description,
2969 amount => $money. $amount,
2971 section => $tax_section,
2976 push @total_items, {
2977 'total_item' => $description,
2978 'total_amount' => $other_money_char. $amount,
2983 push @buf,[ $description,
2984 $money_char. $amount,
2991 $total->{'total_item'} = 'Sub-total';
2992 $total->{'total_amount'} =
2993 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2995 if ( $multisection ) {
2996 $tax_section->{'subtotal'} = $other_money_char.
2997 sprintf('%.2f', $taxtotal);
2998 $tax_section->{'pretotal'} = 'New charges sub-total '.
2999 $total->{'total_amount'};
3000 push @sections, $tax_section if $taxtotal;
3002 unshift @total_items, $total;
3005 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3007 push @buf,['','-----------'];
3008 push @buf,[( $conf->exists('disable_previous_balance')
3010 : 'Total New Charges'
3012 $money_char. sprintf("%10.2f",$self->charged) ];
3018 $item = $conf->config('previous_balance-exclude_from_total')
3019 || 'Total New Charges'
3020 if $conf->exists('previous_balance-exclude_from_total');
3021 my $amount = $self->charged +
3022 ( $conf->exists('disable_previous_balance') ||
3023 $conf->exists('previous_balance-exclude_from_total')
3027 $total->{'total_item'} = &$embolden_function($item);
3028 $total->{'total_amount'} =
3029 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
3030 if ( $multisection ) {
3031 if ( $adjust_section->{'sort_weight'} ) {
3032 $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
3033 sprintf("%.2f", ($self->billing_balance || 0) );
3035 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
3036 sprintf('%.2f', $self->charged );
3039 push @total_items, $total;
3041 push @buf,['','-----------'];
3044 sprintf( '%10.2f', $amount )
3049 unless ( $conf->exists('disable_previous_balance') ) {
3050 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3053 my $credittotal = 0;
3054 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3057 $total->{'total_item'} = &$escape_function($credit->{'description'});
3058 $credittotal += $credit->{'amount'};
3059 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3060 $adjusttotal += $credit->{'amount'};
3061 if ( $multisection ) {
3062 my $money = $old_latex ? '' : $money_char;
3063 push @detail_items, {
3064 ext_description => [],
3067 description => &$escape_function($credit->{'description'}),
3068 amount => $money. $credit->{'amount'},
3070 section => $adjust_section,
3073 push @total_items, $total;
3077 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3080 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3081 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3085 my $paymenttotal = 0;
3086 foreach my $payment ( $self->_items_payments ) {
3088 $total->{'total_item'} = &$escape_function($payment->{'description'});
3089 $paymenttotal += $payment->{'amount'};
3090 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3091 $adjusttotal += $payment->{'amount'};
3092 if ( $multisection ) {
3093 my $money = $old_latex ? '' : $money_char;
3094 push @detail_items, {
3095 ext_description => [],
3098 description => &$escape_function($payment->{'description'}),
3099 amount => $money. $payment->{'amount'},
3101 section => $adjust_section,
3104 push @total_items, $total;
3106 push @buf, [ $payment->{'description'},
3107 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3110 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3112 if ( $multisection ) {
3113 $adjust_section->{'subtotal'} = $other_money_char.
3114 sprintf('%.2f', $adjusttotal);
3115 push @sections, $adjust_section
3116 unless $adjust_section->{sort_weight};
3121 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3122 $total->{'total_amount'} =
3123 &$embolden_function(
3124 $other_money_char. sprintf('%.2f', $summarypage
3126 $self->billing_balance
3127 : $self->owed + $pr_total
3130 if ( $multisection && !$adjust_section->{sort_weight} ) {
3131 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3132 $total->{'total_amount'};
3134 push @total_items, $total;
3136 push @buf,['','-----------'];
3137 push @buf,[$self->balance_due_msg, $money_char.
3138 sprintf("%10.2f", $balance_due ) ];
3141 if ( $conf->exists('previous_balance-show_credit')
3142 and $cust_main->balance < 0 ) {
3143 my $credit_total = {
3144 'total_item' => &$embolden_function($self->credit_balance_msg),
3145 'total_amount' => &$embolden_function(
3146 $other_money_char. sprintf('%.2f', -$cust_main->balance)
3149 if ( $multisection ) {
3150 $adjust_section->{'posttotal'} .= $newline_token .
3151 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3154 push @total_items, $credit_total;
3156 push @buf,['','-----------'];
3157 push @buf,[$self->credit_balance_msg, $money_char.
3158 sprintf("%10.2f", -$cust_main->balance ) ];
3162 if ( $multisection ) {
3163 if ($conf->exists('svc_phone_sections')) {
3165 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3166 $total->{'total_amount'} =
3167 &$embolden_function(
3168 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3170 my $last_section = pop @sections;
3171 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3172 $total->{'total_amount'};
3173 push @sections, $last_section;
3175 push @sections, @$late_sections
3179 my @includelist = ();
3180 push @includelist, 'summary' if $summarypage;
3181 foreach my $include ( @includelist ) {
3183 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3186 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3188 @inc_src = $conf->config($inc_file, $agentnum);
3192 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3194 my $convert_map = $convert_maps{$format}{$include};
3196 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3197 s/--\@\]/$delimiters{$format}[1]/g;
3200 &$convert_map( $conf->config($inc_file, $agentnum) );
3204 my $inc_tt = new Text::Template (
3206 SOURCE => [ map "$_\n", @inc_src ],
3207 DELIMITERS => $delimiters{$format},
3208 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3210 unless ( $inc_tt->compile() ) {
3211 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3212 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3216 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3218 $invoice_data{$include} =~ s/\n+$//
3219 if ($format eq 'latex');
3224 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3225 /invoice_lines\((\d*)\)/;
3226 $invoice_lines += $1 || scalar(@buf);
3229 die "no invoice_lines() functions in template?"
3230 if ( $format eq 'template' && !$wasfunc );
3232 if ($format eq 'template') {
3234 if ( $invoice_lines ) {
3235 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3236 $invoice_data{'total_pages'}++
3237 if scalar(@buf) % $invoice_lines;
3240 #setup subroutine for the template
3241 sub FS::cust_bill::_template::invoice_lines {
3242 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
3244 scalar(@FS::cust_bill::_template::buf)
3245 ? shift @FS::cust_bill::_template::buf
3254 push @collect, split("\n",
3255 $text_template->fill_in( HASH => \%invoice_data,
3256 PACKAGE => 'FS::cust_bill::_template'
3259 $FS::cust_bill::_template::page++;
3261 map "$_\n", @collect;
3263 warn "filling in template for invoice ". $self->invnum. "\n"
3265 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3268 $text_template->fill_in(HASH => \%invoice_data);
3272 # helper routine for generating date ranges
3273 sub _prior_month30s {
3276 [ 1, 2592000 ], # 0-30 days ago
3277 [ 2592000, 5184000 ], # 30-60 days ago
3278 [ 5184000, 7776000 ], # 60-90 days ago
3279 [ 7776000, 0 ], # 90+ days ago
3282 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3283 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3288 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3290 Returns an postscript invoice, as a scalar.
3292 Options can be passed as a hashref (recommended) or as a list of time, template
3293 and then any key/value pairs for any other options.
3295 I<time> an optional value used to control the printing of overdue messages. The
3296 default is now. It isn't the date of the invoice; that's the `_date' field.
3297 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3298 L<Time::Local> and L<Date::Parse> for conversion functions.
3300 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3307 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3308 my $ps = generate_ps($file);
3310 unlink($barcodefile) if $barcodefile;
3315 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3317 Returns an PDF invoice, as a scalar.
3319 Options can be passed as a hashref (recommended) or as a list of time, template
3320 and then any key/value pairs for any other options.
3322 I<time> an optional value used to control the printing of overdue messages. The
3323 default is now. It isn't the date of the invoice; that's the `_date' field.
3324 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3325 L<Time::Local> and L<Date::Parse> for conversion functions.
3327 I<template>, if specified, is the name of a suffix for alternate invoices.
3329 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3336 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3337 my $pdf = generate_pdf($file);
3339 unlink($barcodefile) if $barcodefile;
3344 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3346 Returns an HTML invoice, as a scalar.
3348 I<time> an optional value used to control the printing of overdue messages. The
3349 default is now. It isn't the date of the invoice; that's the `_date' field.
3350 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3351 L<Time::Local> and L<Date::Parse> for conversion functions.
3353 I<template>, if specified, is the name of a suffix for alternate invoices.
3355 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3357 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3358 when emailing the invoice as part of a multipart/related MIME email.
3366 %params = %{ shift() };
3368 $params{'time'} = shift;
3369 $params{'template'} = shift;
3370 $params{'cid'} = shift;
3373 $params{'format'} = 'html';
3375 $self->print_generic( %params );
3378 # quick subroutine for print_latex
3380 # There are ten characters that LaTeX treats as special characters, which
3381 # means that they do not simply typeset themselves:
3382 # # $ % & ~ _ ^ \ { }
3384 # TeX ignores blanks following an escaped character; if you want a blank (as
3385 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3389 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3390 $value =~ s/([<>])/\$$1\$/g;
3396 encode_entities($value);
3400 sub _html_escape_nbsp {
3401 my $value = _html_escape(shift);
3402 $value =~ s/ +/ /g;
3406 #utility methods for print_*
3408 sub _translate_old_latex_format {
3409 warn "_translate_old_latex_format called\n"
3416 if ( $line =~ /^%%Detail\s*$/ ) {
3418 push @template, q![@--!,
3419 q! foreach my $_tr_line (@detail_items) {!,
3420 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3421 q! $_tr_line->{'description'} .= !,
3422 q! "\\tabularnewline\n~~".!,
3423 q! join( "\\tabularnewline\n~~",!,
3424 q! @{$_tr_line->{'ext_description'}}!,
3428 while ( ( my $line_item_line = shift )
3429 !~ /^%%EndDetail\s*$/ ) {
3430 $line_item_line =~ s/'/\\'/g; # nice LTS
3431 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3432 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3433 push @template, " \$OUT .= '$line_item_line';";
3436 push @template, '}',
3439 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3441 push @template, '[@--',
3442 ' foreach my $_tr_line (@total_items) {';
3444 while ( ( my $total_item_line = shift )
3445 !~ /^%%EndTotalDetails\s*$/ ) {
3446 $total_item_line =~ s/'/\\'/g; # nice LTS
3447 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3448 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3449 push @template, " \$OUT .= '$total_item_line';";
3452 push @template, '}',
3456 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3457 push @template, $line;
3463 warn "$_\n" foreach @template;
3472 #check for an invoice-specific override
3473 return $self->invoice_terms if $self->invoice_terms;
3475 #check for a customer- specific override
3476 my $cust_main = $self->cust_main;
3477 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3479 #use configured default
3480 $conf->config('invoice_default_terms') || '';
3486 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3487 $duedate = $self->_date() + ( $1 * 86400 );
3494 $self->due_date ? time2str(shift, $self->due_date) : '';
3497 sub balance_due_msg {
3499 my $msg = 'Balance Due';
3500 return $msg unless $self->terms;
3501 if ( $self->due_date ) {
3502 $msg .= ' - Please pay by '. $self->due_date2str($date_format);
3503 } elsif ( $self->terms ) {
3504 $msg .= ' - '. $self->terms;
3509 sub balance_due_date {
3512 if ( $conf->exists('invoice_default_terms')
3513 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3514 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3519 sub credit_balance_msg { 'Credit Balance Remaining' }
3521 =item invnum_date_pretty
3523 Returns a string with the invoice number and date, for example:
3524 "Invoice #54 (3/20/2008)"
3528 sub invnum_date_pretty {
3530 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3535 Returns a string with the date, for example: "3/20/2008"
3541 time2str($date_format, $self->_date);
3544 use vars qw(%pkg_category_cache);
3545 sub _items_sections {
3548 my $summarypage = shift;
3550 my $extra_sections = shift;
3554 my %late_subtotal = ();
3557 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3560 my $usage = $cust_bill_pkg->usage;
3562 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3563 next if ( $display->summary && $summarypage );
3565 my $section = $display->section;
3566 my $type = $display->type;
3568 $not_tax{$section} = 1
3569 unless $cust_bill_pkg->pkgnum == 0;
3571 if ( $display->post_total && !$summarypage ) {
3572 if (! $type || $type eq 'S') {
3573 $late_subtotal{$section} += $cust_bill_pkg->setup
3574 if $cust_bill_pkg->setup != 0;
3578 $late_subtotal{$section} += $cust_bill_pkg->recur
3579 if $cust_bill_pkg->recur != 0;
3582 if ($type && $type eq 'R') {
3583 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3584 if $cust_bill_pkg->recur != 0;
3587 if ($type && $type eq 'U') {
3588 $late_subtotal{$section} += $usage
3589 unless scalar(@$extra_sections);
3594 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3596 if (! $type || $type eq 'S') {
3597 $subtotal{$section} += $cust_bill_pkg->setup
3598 if $cust_bill_pkg->setup != 0;
3602 $subtotal{$section} += $cust_bill_pkg->recur
3603 if $cust_bill_pkg->recur != 0;
3606 if ($type && $type eq 'R') {
3607 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3608 if $cust_bill_pkg->recur != 0;
3611 if ($type && $type eq 'U') {
3612 $subtotal{$section} += $usage
3613 unless scalar(@$extra_sections);
3622 %pkg_category_cache = ();
3624 push @$late, map { { 'description' => &{$escape}($_),
3625 'subtotal' => $late_subtotal{$_},
3627 'sort_weight' => ( _pkg_category($_)
3628 ? _pkg_category($_)->weight
3631 ((_pkg_category($_) && _pkg_category($_)->condense)
3632 ? $self->_condense_section($format)
3636 sort _sectionsort keys %late_subtotal;
3639 if ( $summarypage ) {
3640 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3641 map { $_->categoryname } qsearch('pkg_category', {});
3642 push @sections, '' if exists($subtotal{''});
3644 @sections = keys %subtotal;
3647 my @early = map { { 'description' => &{$escape}($_),
3648 'subtotal' => $subtotal{$_},
3649 'summarized' => $not_tax{$_} ? '' : 'Y',
3650 'tax_section' => $not_tax{$_} ? '' : 'Y',
3651 'sort_weight' => ( _pkg_category($_)
3652 ? _pkg_category($_)->weight
3655 ((_pkg_category($_) && _pkg_category($_)->condense)
3656 ? $self->_condense_section($format)
3661 push @early, @$extra_sections if $extra_sections;
3663 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3667 #helper subs for above
3670 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3674 my $categoryname = shift;
3675 $pkg_category_cache{$categoryname} ||=
3676 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3679 my %condensed_format = (
3680 'label' => [ qw( Description Qty Amount ) ],
3682 sub { shift->{description} },
3683 sub { shift->{quantity} },
3684 sub { my($href, %opt) = @_;
3685 ($opt{dollar} || ''). $href->{amount};
3688 'align' => [ qw( l r r ) ],
3689 'span' => [ qw( 5 1 1 ) ], # unitprices?
3690 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3693 sub _condense_section {
3694 my ( $self, $format ) = ( shift, shift );
3696 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3697 qw( description_generator
3700 total_line_generator
3705 sub _condensed_generator_defaults {
3706 my ( $self, $format ) = ( shift, shift );
3707 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3716 sub _condensed_header_generator {
3717 my ( $self, $format ) = ( shift, shift );
3719 my ( $f, $prefix, $suffix, $separator, $column ) =
3720 _condensed_generator_defaults($format);
3722 if ($format eq 'latex') {
3723 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3724 $suffix = "\\\\\n\\hline";
3727 sub { my ($d,$a,$s,$w) = @_;
3728 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3730 } elsif ( $format eq 'html' ) {
3731 $prefix = '<th></th>';
3735 sub { my ($d,$a,$s,$w) = @_;
3736 return qq!<th align="$html_align{$a}">$d</th>!;
3744 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3746 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3749 $prefix. join($separator, @result). $suffix;
3754 sub _condensed_description_generator {
3755 my ( $self, $format ) = ( shift, shift );
3757 my ( $f, $prefix, $suffix, $separator, $column ) =
3758 _condensed_generator_defaults($format);
3760 my $money_char = '$';
3761 if ($format eq 'latex') {
3762 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3764 $separator = " & \n";
3766 sub { my ($d,$a,$s,$w) = @_;
3767 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3769 $money_char = '\\dollar';
3770 }elsif ( $format eq 'html' ) {
3771 $prefix = '"><td align="center"></td>';
3775 sub { my ($d,$a,$s,$w) = @_;
3776 return qq!<td align="$html_align{$a}">$d</td>!;
3778 #$money_char = $conf->config('money_char') || '$';
3779 $money_char = ''; # this is madness
3787 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3789 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3791 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3792 map { $f->{$_}->[$i] } qw(align span width)
3796 $prefix. join( $separator, @result ). $suffix;
3801 sub _condensed_total_generator {
3802 my ( $self, $format ) = ( shift, shift );
3804 my ( $f, $prefix, $suffix, $separator, $column ) =
3805 _condensed_generator_defaults($format);
3808 if ($format eq 'latex') {
3811 $separator = " & \n";
3813 sub { my ($d,$a,$s,$w) = @_;
3814 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3816 }elsif ( $format eq 'html' ) {
3820 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3822 sub { my ($d,$a,$s,$w) = @_;
3823 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3832 # my $r = &{$f->{fields}->[$i]}(@args);
3833 # $r .= ' Total' unless $i;
3835 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3837 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3838 map { $f->{$_}->[$i] } qw(align span width)
3842 $prefix. join( $separator, @result ). $suffix;
3847 =item total_line_generator FORMAT
3849 Returns a coderef used for generation of invoice total line items for this
3850 usage_class. FORMAT is either html or latex
3854 # should not be used: will have issues with hash element names (description vs
3855 # total_item and amount vs total_amount -- another array of functions?
3857 sub _condensed_total_line_generator {
3858 my ( $self, $format ) = ( shift, shift );
3860 my ( $f, $prefix, $suffix, $separator, $column ) =
3861 _condensed_generator_defaults($format);
3864 if ($format eq 'latex') {
3867 $separator = " & \n";
3869 sub { my ($d,$a,$s,$w) = @_;
3870 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3872 }elsif ( $format eq 'html' ) {
3876 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3878 sub { my ($d,$a,$s,$w) = @_;
3879 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3888 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3890 &{$column}( &{$f->{fields}->[$i]}(@args),
3891 map { $f->{$_}->[$i] } qw(align span width)
3895 $prefix. join( $separator, @result ). $suffix;
3900 #sub _items_extra_usage_sections {
3902 # my $escape = shift;
3904 # my %sections = ();
3906 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
3907 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3909 # next unless $cust_bill_pkg->pkgnum > 0;
3911 # foreach my $section ( keys %usage_class ) {
3913 # my $usage = $cust_bill_pkg->usage($section);
3915 # next unless $usage && $usage > 0;
3917 # $sections{$section} ||= 0;
3918 # $sections{$section} += $usage;
3924 # map { { 'description' => &{$escape}($_),
3925 # 'subtotal' => $sections{$_},
3926 # 'summarized' => '',
3927 # 'tax_section' => '',
3930 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3934 sub _items_extra_usage_sections {
3943 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3944 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3945 next unless $cust_bill_pkg->pkgnum > 0;
3947 foreach my $classnum ( keys %usage_class ) {
3948 my $section = $usage_class{$classnum}->classname;
3949 $classnums{$section} = $classnum;
3951 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3952 my $amount = $detail->amount;
3953 next unless $amount && $amount > 0;
3955 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
3956 $sections{$section}{amount} += $amount; #subtotal
3957 $sections{$section}{calls}++;
3958 $sections{$section}{duration} += $detail->duration;
3960 my $desc = $detail->regionname;
3961 my $description = $desc;
3962 $description = substr($desc, 0, 50). '...'
3963 if $format eq 'latex' && length($desc) > 50;
3965 $lines{$section}{$desc} ||= {
3966 description => &{$escape}($description),
3967 #pkgpart => $part_pkg->pkgpart,
3968 pkgnum => $cust_bill_pkg->pkgnum,
3973 #unit_amount => $cust_bill_pkg->unitrecur,
3974 quantity => $cust_bill_pkg->quantity,
3975 product_code => 'N/A',
3976 ext_description => [],
3979 $lines{$section}{$desc}{amount} += $amount;
3980 $lines{$section}{$desc}{calls}++;
3981 $lines{$section}{$desc}{duration} += $detail->duration;
3987 my %sectionmap = ();
3988 foreach (keys %sections) {
3989 my $usage_class = $usage_class{$classnums{$_}};
3990 $sectionmap{$_} = { 'description' => &{$escape}($_),
3991 'amount' => $sections{$_}{amount}, #subtotal
3992 'calls' => $sections{$_}{calls},
3993 'duration' => $sections{$_}{duration},
3995 'tax_section' => '',
3996 'sort_weight' => $usage_class->weight,
3997 ( $usage_class->format
3998 ? ( map { $_ => $usage_class->$_($format) }
3999 qw( description_generator header_generator total_generator total_line_generator )
4006 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4010 foreach my $section ( keys %lines ) {
4011 foreach my $line ( keys %{$lines{$section}} ) {
4012 my $l = $lines{$section}{$line};
4013 $l->{section} = $sectionmap{$section};
4014 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4015 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4020 return(\@sections, \@lines);
4026 my $end = $self->_date;
4028 # start at date of previous invoice + 1 second or 0 if no previous invoice
4029 my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4030 $start = 0 if !$start;
4033 my $cust_main = $self->cust_main;
4034 my @pkgs = $cust_main->all_pkgs;
4035 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4038 foreach my $pkg ( @pkgs ) {
4039 my @h_cust_svc = $pkg->h_cust_svc($end);
4040 foreach my $h_cust_svc ( @h_cust_svc ) {
4041 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4042 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4044 my $inserted = $h_cust_svc->date_inserted;
4045 my $deleted = $h_cust_svc->date_deleted;
4046 my $phone_inserted = $h_cust_svc->h_svc_x($inserted);
4048 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4050 # DID either activated or ported in; cannot be both for same DID simultaneously
4051 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4052 && (!$phone_inserted->lnp_status
4053 || $phone_inserted->lnp_status eq ''
4054 || $phone_inserted->lnp_status eq 'native')) {
4057 else { # this one not so clean, should probably move to (h_)svc_phone
4058 my $phone_portedin = qsearchs( 'h_svc_phone',
4059 { 'svcnum' => $h_cust_svc->svcnum,
4060 'lnp_status' => 'portedin' },
4061 FS::h_svc_phone->sql_h_searchs($end),
4063 $num_portedin++ if $phone_portedin;
4066 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4067 if($deleted >= $start && $deleted <= $end && $phone_deleted
4068 && (!$phone_deleted->lnp_status
4069 || $phone_deleted->lnp_status ne 'portingout')) {
4072 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4073 && $phone_deleted->lnp_status
4074 && $phone_deleted->lnp_status eq 'portingout') {
4078 # increment usage minutes
4079 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end);
4080 foreach my $cdr ( @cdrs ) {
4081 $minutes += $cdr->billsec/60;
4084 # don't look at this service again
4085 push @seen, $h_cust_svc->svcnum;
4089 $minutes = sprintf("%d", $minutes);
4090 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4091 . "$num_deactivated Ported-Out: $num_portedout ",
4092 "Total Minutes: $minutes");
4095 sub _items_svc_phone_sections {
4104 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4105 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4107 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4108 next unless $cust_bill_pkg->pkgnum > 0;
4110 my @header = $cust_bill_pkg->details_header;
4111 next unless scalar(@header);
4113 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4115 my $phonenum = $detail->phonenum;
4116 next unless $phonenum;
4118 my $amount = $detail->amount;
4119 next unless $amount && $amount > 0;
4121 $sections{$phonenum} ||= { 'amount' => 0,
4124 'sort_weight' => -1,
4125 'phonenum' => $phonenum,
4127 $sections{$phonenum}{amount} += $amount; #subtotal
4128 $sections{$phonenum}{calls}++;
4129 $sections{$phonenum}{duration} += $detail->duration;
4131 my $desc = $detail->regionname;
4132 my $description = $desc;
4133 $description = substr($desc, 0, 50). '...'
4134 if $format eq 'latex' && length($desc) > 50;
4136 $lines{$phonenum}{$desc} ||= {
4137 description => &{$escape}($description),
4138 #pkgpart => $part_pkg->pkgpart,
4146 product_code => 'N/A',
4147 ext_description => [],
4150 $lines{$phonenum}{$desc}{amount} += $amount;
4151 $lines{$phonenum}{$desc}{calls}++;
4152 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4154 my $line = $usage_class{$detail->classnum}->classname;
4155 $sections{"$phonenum $line"} ||=
4159 'sort_weight' => $usage_class{$detail->classnum}->weight,
4160 'phonenum' => $phonenum,
4161 'header' => [ @header ],
4163 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4164 $sections{"$phonenum $line"}{calls}++;
4165 $sections{"$phonenum $line"}{duration} += $detail->duration;
4167 $lines{"$phonenum $line"}{$desc} ||= {
4168 description => &{$escape}($description),
4169 #pkgpart => $part_pkg->pkgpart,
4177 product_code => 'N/A',
4178 ext_description => [],
4181 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4182 $lines{"$phonenum $line"}{$desc}{calls}++;
4183 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4184 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4185 $detail->formatted('format' => $format);
4190 my %sectionmap = ();
4191 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4192 foreach ( keys %sections ) {
4193 my @header = @{ $sections{$_}{header} || [] };
4195 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4196 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4197 my $usage_class = $summary ? $simple : $usage_simple;
4198 my $ending = $summary ? ' usage charges' : '';
4201 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4203 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4204 'amount' => $sections{$_}{amount}, #subtotal
4205 'calls' => $sections{$_}{calls},
4206 'duration' => $sections{$_}{duration},
4208 'tax_section' => '',
4209 'phonenum' => $sections{$_}{phonenum},
4210 'sort_weight' => $sections{$_}{sort_weight},
4211 'post_total' => $summary, #inspire pagebreak
4213 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4214 qw( description_generator
4217 total_line_generator
4224 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4225 $a->{sort_weight} <=> $b->{sort_weight}
4230 foreach my $section ( keys %lines ) {
4231 foreach my $line ( keys %{$lines{$section}} ) {
4232 my $l = $lines{$section}{$line};
4233 $l->{section} = $sectionmap{$section};
4234 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4235 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4240 if($conf->exists('phone_usage_class_summary')) {
4241 # this only works with Latex
4245 # after this, we'll have only two sections per DID:
4246 # Calls Summary and Calls Detail
4247 foreach my $section ( @sections ) {
4248 if($section->{'post_total'}) {
4249 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4250 $section->{'total_line_generator'} = sub { '' };
4251 $section->{'total_generator'} = sub { '' };
4252 $section->{'header_generator'} = sub { '' };
4253 $section->{'description_generator'} = '';
4254 push @newsections, $section;
4255 my %calls_detail = %$section;
4256 $calls_detail{'post_total'} = '';
4257 $calls_detail{'sort_weight'} = '';
4258 $calls_detail{'description_generator'} = sub { '' };
4259 $calls_detail{'header_generator'} = sub {
4260 return ' & Date/Time & Called Number & Duration & Price'
4261 if $format eq 'latex';
4264 $calls_detail{'description'} = 'Calls Detail: '
4265 . $section->{'phonenum'};
4266 push @newsections, \%calls_detail;
4270 # after this, each usage class is collapsed/summarized into a single
4271 # line under the Calls Summary section
4272 foreach my $newsection ( @newsections ) {
4273 if($newsection->{'post_total'}) { # this means Calls Summary
4274 foreach my $section ( @sections ) {
4275 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4276 && !$section->{'post_total'});
4277 my $newdesc = $section->{'description'};
4278 my $tn = $section->{'phonenum'};
4279 $newdesc =~ s/$tn//g;
4280 my $line = { ext_description => [],
4284 calls => $section->{'calls'},
4285 section => $newsection,
4286 duration => $section->{'duration'},
4287 description => $newdesc,
4288 amount => sprintf("%.2f",$section->{'amount'}),
4289 product_code => 'N/A',
4291 push @newlines, $line;
4296 # after this, Calls Details is populated with all CDRs
4297 foreach my $newsection ( @newsections ) {
4298 if(!$newsection->{'post_total'}) { # this means Calls Details
4299 foreach my $line ( @lines ) {
4300 next unless (scalar(@{$line->{'ext_description'}}) &&
4301 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4303 my @extdesc = @{$line->{'ext_description'}};
4305 foreach my $extdesc ( @extdesc ) {
4306 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4307 push @newextdesc, $extdesc;
4309 $line->{'ext_description'} = \@newextdesc;
4310 $line->{'section'} = $newsection;
4311 push @newlines, $line;
4316 return(\@newsections, \@newlines);
4319 return(\@sections, \@lines);
4326 #my @display = scalar(@_)
4328 # : qw( _items_previous _items_pkg );
4329 # #: qw( _items_pkg );
4330 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4331 my @display = qw( _items_previous _items_pkg );
4334 foreach my $display ( @display ) {
4335 push @b, $self->$display(@_);
4340 sub _items_previous {
4342 my $cust_main = $self->cust_main;
4343 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4345 foreach ( @pr_cust_bill ) {
4346 my $date = $conf->exists('invoice_show_prior_due_date')
4347 ? 'due '. $_->due_date2str($date_format)
4348 : time2str($date_format, $_->_date);
4350 'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
4351 #'pkgpart' => 'N/A',
4353 'amount' => sprintf("%.2f", $_->owed),
4359 # 'description' => 'Previous Balance',
4360 # #'pkgpart' => 'N/A',
4361 # 'pkgnum' => 'N/A',
4362 # 'amount' => sprintf("%10.2f", $pr_total ),
4363 # 'ext_description' => [ map {
4364 # "Invoice ". $_->invnum.
4365 # " (". time2str("%x",$_->_date). ") ".
4366 # sprintf("%10.2f", $_->owed)
4367 # } @pr_cust_bill ],
4376 warn "$me _items_pkg searching for all package line items\n"
4379 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4381 warn "$me _items_pkg filtering line items\n"
4383 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4385 if ($options{section} && $options{section}->{condensed}) {
4387 warn "$me _items_pkg condensing section\n"
4391 local $Storable::canonical = 1;
4392 foreach ( @items ) {
4394 delete $item->{ref};
4395 delete $item->{ext_description};
4396 my $key = freeze($item);
4397 $itemshash{$key} ||= 0;
4398 $itemshash{$key} ++; # += $item->{quantity};
4400 @items = sort { $a->{description} cmp $b->{description} }
4401 map { my $i = thaw($_);
4402 $i->{quantity} = $itemshash{$_};
4404 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4410 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4417 return 0 unless $a->itemdesc cmp $b->itemdesc;
4418 return -1 if $b->itemdesc eq 'Tax';
4419 return 1 if $a->itemdesc eq 'Tax';
4420 return -1 if $b->itemdesc eq 'Other surcharges';
4421 return 1 if $a->itemdesc eq 'Other surcharges';
4422 $a->itemdesc cmp $b->itemdesc;
4427 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4428 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4431 sub _items_cust_bill_pkg {
4433 my $cust_bill_pkgs = shift;
4436 my $format = $opt{format} || '';
4437 my $escape_function = $opt{escape_function} || sub { shift };
4438 my $format_function = $opt{format_function} || '';
4439 my $unsquelched = $opt{unsquelched} || '';
4440 my $section = $opt{section}->{description} if $opt{section};
4441 my $summary_page = $opt{summary_page} || '';
4442 my $multilocation = $opt{multilocation} || '';
4443 my $multisection = $opt{multisection} || '';
4444 my $discount_show_always = 0;
4447 my ($s, $r, $u) = ( undef, undef, undef );
4448 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4451 warn "$me _items_cust_bill_pkg considering cust_bill_pkg $cust_bill_pkg\n"
4454 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
4455 && $conf->exists('discount-show-always'));
4457 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4458 if ( $_ && !$cust_bill_pkg->hidden ) {
4459 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4460 $_->{amount} =~ s/^\-0\.00$/0.00/;
4461 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4463 unless ( $_->{amount} == 0 && !$discount_show_always );
4468 foreach my $display ( grep { defined($section)
4469 ? $_->section eq $section
4472 #grep { !$_->summary || !$summary_page } # bunk!
4473 grep { !$_->summary || $multisection }
4474 $cust_bill_pkg->cust_bill_pkg_display
4478 warn "$me _items_cust_bill_pkg considering display item $display\n"
4481 my $type = $display->type;
4483 my $desc = $cust_bill_pkg->desc;
4484 $desc = substr($desc, 0, 50). '...'
4485 if $format eq 'latex' && length($desc) > 50;
4487 my %details_opt = ( 'format' => $format,
4488 'escape_function' => $escape_function,
4489 'format_function' => $format_function,
4492 if ( $cust_bill_pkg->pkgnum > 0 ) {
4494 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4497 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4499 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
4501 warn "$me _items_cust_bill_pkg adding setup\n"
4504 my $description = $desc;
4505 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
4508 unless ( $cust_pkg->part_pkg->hide_svc_detail
4509 || $cust_bill_pkg->hidden )
4512 push @d, map &{$escape_function}($_),
4513 $cust_pkg->h_labels_short($self->_date, undef, 'I')
4514 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4516 if ( $multilocation ) {
4517 my $loc = $cust_pkg->location_label;
4518 $loc = substr($loc, 0, 50). '...'
4519 if $format eq 'latex' && length($loc) > 50;
4520 push @d, &{$escape_function}($loc);
4525 push @d, $cust_bill_pkg->details(%details_opt)
4526 if $cust_bill_pkg->recur == 0;
4528 if ( $cust_bill_pkg->hidden ) {
4529 $s->{amount} += $cust_bill_pkg->setup;
4530 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4531 push @{ $s->{ext_description} }, @d;
4534 description => $description,
4535 #pkgpart => $part_pkg->pkgpart,
4536 pkgnum => $cust_bill_pkg->pkgnum,
4537 amount => $cust_bill_pkg->setup,
4538 unit_amount => $cust_bill_pkg->unitsetup,
4539 quantity => $cust_bill_pkg->quantity,
4540 ext_description => \@d,
4546 if ( ( $cust_bill_pkg->recur != 0 || $cust_bill_pkg->setup == 0 ||
4547 ($discount_show_always && $cust_bill_pkg->recur == 0) ) &&
4548 ( !$type || $type eq 'R' || $type eq 'U' )
4552 warn "$me _items_cust_bill_pkg adding recur/usage\n"
4555 my $is_summary = $display->summary;
4556 my $description = ($is_summary && $type && $type eq 'U')
4557 ? "Usage charges" : $desc;
4559 $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4560 " - ". time2str($date_format, $cust_bill_pkg->edate).
4562 unless $conf->exists('disable_line_item_date_ranges');
4566 #at least until cust_bill_pkg has "past" ranges in addition to
4567 #the "future" sdate/edate ones... see #3032
4568 my @dates = ( $self->_date );
4569 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4570 push @dates, $prev->sdate if $prev;
4571 push @dates, undef if !$prev;
4573 unless ( $cust_pkg->part_pkg->hide_svc_detail
4574 || $cust_bill_pkg->itemdesc
4575 || $cust_bill_pkg->hidden
4576 || $is_summary && $type && $type eq 'U' )
4579 warn "$me _items_cust_bill_pkg adding service details\n"
4582 push @d, map &{$escape_function}($_),
4583 $cust_pkg->h_labels_short(@dates, 'I')
4584 #$cust_bill_pkg->edate,
4585 #$cust_bill_pkg->sdate)
4586 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4588 warn "$me _items_cust_bill_pkg done adding service details\n"
4591 if ( $multilocation ) {
4592 my $loc = $cust_pkg->location_label;
4593 $loc = substr($loc, 0, 50). '...'
4594 if $format eq 'latex' && length($loc) > 50;
4595 push @d, &{$escape_function}($loc);
4600 unless ( $is_summary ) {
4601 warn "$me _items_cust_bill_pkg adding details\n"
4604 #instead of omitting details entirely in this case (unwanted side
4605 # effects), just omit CDRs
4606 $details_opt{'format_function'} = sub { () }
4607 if $type && $type eq 'R';
4609 push @d, $cust_bill_pkg->details(%details_opt);
4612 warn "$me _items_cust_bill_pkg calculating amount\n"
4617 $amount = $cust_bill_pkg->recur;
4618 } elsif ($type eq 'R') {
4619 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4620 } elsif ($type eq 'U') {
4621 $amount = $cust_bill_pkg->usage;
4624 if ( !$type || $type eq 'R' ) {
4626 warn "$me _items_cust_bill_pkg adding recur\n"
4629 if ( $cust_bill_pkg->hidden ) {
4630 $r->{amount} += $amount;
4631 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4632 push @{ $r->{ext_description} }, @d;
4635 description => $description,
4636 #pkgpart => $part_pkg->pkgpart,
4637 pkgnum => $cust_bill_pkg->pkgnum,
4639 unit_amount => $cust_bill_pkg->unitrecur,
4640 quantity => $cust_bill_pkg->quantity,
4641 ext_description => \@d,
4645 } else { # $type eq 'U'
4647 warn "$me _items_cust_bill_pkg adding usage\n"
4650 if ( $cust_bill_pkg->hidden ) {
4651 $u->{amount} += $amount;
4652 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4653 push @{ $u->{ext_description} }, @d;
4656 description => $description,
4657 #pkgpart => $part_pkg->pkgpart,
4658 pkgnum => $cust_bill_pkg->pkgnum,
4660 unit_amount => $cust_bill_pkg->unitrecur,
4661 quantity => $cust_bill_pkg->quantity,
4662 ext_description => \@d,
4668 } # recurring or usage with recurring charge
4670 } else { #pkgnum tax or one-shot line item (??)
4672 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
4675 if ( $cust_bill_pkg->setup != 0 ) {
4677 'description' => $desc,
4678 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
4681 if ( $cust_bill_pkg->recur != 0 ) {
4683 'description' => "$desc (".
4684 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
4685 time2str($date_format, $cust_bill_pkg->edate). ')',
4686 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
4696 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
4699 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4701 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4702 $_->{amount} =~ s/^\-0\.00$/0.00/;
4703 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4705 unless ( $_->{amount} == 0 && !$discount_show_always );
4713 sub _items_credits {
4714 my( $self, %opt ) = @_;
4715 my $trim_len = $opt{'trim_len'} || 60;
4719 foreach ( $self->cust_credited ) {
4721 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4723 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4724 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4725 $reason = " ($reason) " if $reason;
4728 #'description' => 'Credit ref\#'. $_->crednum.
4729 # " (". time2str("%x",$_->cust_credit->_date) .")".
4731 'description' => 'Credit applied '.
4732 time2str($date_format,$_->cust_credit->_date). $reason,
4733 'amount' => sprintf("%.2f",$_->amount),
4741 sub _items_payments {
4745 #get & print payments
4746 foreach ( $self->cust_bill_pay ) {
4748 #something more elaborate if $_->amount ne ->cust_pay->paid ?
4751 'description' => "Payment received ".
4752 time2str($date_format,$_->cust_pay->_date ),
4753 'amount' => sprintf("%.2f", $_->amount )
4761 =item call_details [ OPTION => VALUE ... ]
4763 Returns an array of CSV strings representing the call details for this invoice
4764 The only option available is the boolean prepend_billed_number
4769 my ($self, %opt) = @_;
4771 my $format_function = sub { shift };
4773 if ($opt{prepend_billed_number}) {
4774 $format_function = sub {
4778 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4783 my @details = map { $_->details( 'format_function' => $format_function,
4784 'escape_function' => sub{ return() },
4788 $self->cust_bill_pkg;
4789 my $header = $details[0];
4790 ( $header, grep { $_ ne $header } @details );
4800 =item process_reprint
4804 sub process_reprint {
4805 process_re_X('print', @_);
4808 =item process_reemail
4812 sub process_reemail {
4813 process_re_X('email', @_);
4821 process_re_X('fax', @_);
4829 process_re_X('ftp', @_);
4836 sub process_respool {
4837 process_re_X('spool', @_);
4840 use Storable qw(thaw);
4844 my( $method, $job ) = ( shift, shift );
4845 warn "$me process_re_X $method for job $job\n" if $DEBUG;
4847 my $param = thaw(decode_base64(shift));
4848 warn Dumper($param) if $DEBUG;
4859 my($method, $job, %param ) = @_;
4861 warn "re_X $method for job $job with param:\n".
4862 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
4865 #some false laziness w/search/cust_bill.html
4867 my $orderby = 'ORDER BY cust_bill._date';
4869 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
4871 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4873 my @cust_bill = qsearch( {
4874 #'select' => "cust_bill.*",
4875 'table' => 'cust_bill',
4876 'addl_from' => $addl_from,
4878 'extra_sql' => $extra_sql,
4879 'order_by' => $orderby,
4883 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4885 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
4888 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
4889 foreach my $cust_bill ( @cust_bill ) {
4890 $cust_bill->$method();
4892 if ( $job ) { #progressbar foo
4894 if ( time - $min_sec > $last ) {
4895 my $error = $job->update_statustext(
4896 int( 100 * $num / scalar(@cust_bill) )
4898 die $error if $error;
4909 =head1 CLASS METHODS
4915 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
4920 my ($class, $start, $end) = @_;
4922 $class->paid_sql($start, $end). ' - '.
4923 $class->credited_sql($start, $end);
4928 Returns an SQL fragment to retreive the net amount (charged minus credited).
4933 my ($class, $start, $end) = @_;
4934 'charged - '. $class->credited_sql($start, $end);
4939 Returns an SQL fragment to retreive the amount paid against this invoice.
4944 my ($class, $start, $end) = @_;
4945 $start &&= "AND cust_bill_pay._date <= $start";
4946 $end &&= "AND cust_bill_pay._date > $end";
4947 $start = '' unless defined($start);
4948 $end = '' unless defined($end);
4949 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
4950 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
4955 Returns an SQL fragment to retreive the amount credited against this invoice.
4960 my ($class, $start, $end) = @_;
4961 $start &&= "AND cust_credit_bill._date <= $start";
4962 $end &&= "AND cust_credit_bill._date > $end";
4963 $start = '' unless defined($start);
4964 $end = '' unless defined($end);
4965 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
4966 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
4971 Returns an SQL fragment to retrieve the due date of an invoice.
4972 Currently only supported on PostgreSQL.
4980 cust_bill.invoice_terms,
4981 cust_main.invoice_terms,
4982 \''.($conf->config('invoice_default_terms') || '').'\'
4983 ), E\'Net (\\\\d+)\'
4985 ) * 86400 + cust_bill._date'
4988 =item search_sql_where HASHREF
4990 Class method which returns an SQL WHERE fragment to search for parameters
4991 specified in HASHREF. Valid parameters are
4997 List reference of start date, end date, as UNIX timestamps.
5007 List reference of charged limits (exclusive).
5011 List reference of charged limits (exclusive).
5015 flag, return open invoices only
5019 flag, return net invoices only
5023 =item newest_percust
5027 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5031 sub search_sql_where {
5032 my($class, $param) = @_;
5034 warn "$me search_sql_where called with params: \n".
5035 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
5041 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5042 push @search, "cust_main.agentnum = $1";
5046 if ( $param->{_date} ) {
5047 my($beginning, $ending) = @{$param->{_date}};
5049 push @search, "cust_bill._date >= $beginning",
5050 "cust_bill._date < $ending";
5054 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5055 push @search, "cust_bill.invnum >= $1";
5057 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5058 push @search, "cust_bill.invnum <= $1";
5062 if ( $param->{charged} ) {
5063 my @charged = ref($param->{charged})
5064 ? @{ $param->{charged} }
5065 : ($param->{charged});
5067 push @search, map { s/^charged/cust_bill.charged/; $_; }
5071 my $owed_sql = FS::cust_bill->owed_sql;
5074 if ( $param->{owed} ) {
5075 my @owed = ref($param->{owed})
5076 ? @{ $param->{owed} }
5078 push @search, map { s/^owed/$owed_sql/; $_; }
5083 push @search, "0 != $owed_sql"
5084 if $param->{'open'};
5085 push @search, '0 != '. FS::cust_bill->net_sql
5089 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5090 if $param->{'days'};
5093 if ( $param->{'newest_percust'} ) {
5095 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5096 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5098 my @newest_where = map { my $x = $_;
5099 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5102 grep ! /^cust_main./, @search;
5103 my $newest_where = scalar(@newest_where)
5104 ? ' AND '. join(' AND ', @newest_where)
5108 push @search, "cust_bill._date = (
5109 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5110 WHERE newest_cust_bill.custnum = cust_bill.custnum
5116 #agent virtualization
5117 my $curuser = $FS::CurrentUser::CurrentUser;
5118 if ( $curuser->username eq 'fs_queue'
5119 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5121 my $newuser = qsearchs('access_user', {
5122 'username' => $username,
5126 $curuser = $newuser;
5128 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5131 push @search, $curuser->agentnums_sql;
5133 join(' AND ', @search );
5145 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5146 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base