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 'returnaddress' => $returnaddress,
2521 'agent' => &$escape_function($cust_main->agent->agent),
2524 'invnum' => $self->invnum,
2525 'date' => time2str($date_format, $self->_date),
2526 'today' => time2str($date_format_long, $today),
2527 'terms' => $self->terms,
2528 'template' => $template, #params{'template'},
2529 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2530 'current_charges' => sprintf("%.2f", $self->charged),
2531 'duedate' => $self->due_date2str($rdate_format), #date_format?
2534 'custnum' => $cust_main->display_custnum,
2535 'agent_custid' => &$escape_function($cust_main->agent_custid),
2536 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2537 payname company address1 address2 city state zip fax
2541 'ship_enable' => $conf->exists('invoice-ship_address'),
2542 'unitprices' => $conf->exists('invoice-unitprice'),
2543 'smallernotes' => $conf->exists('invoice-smallernotes'),
2544 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2545 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2547 #layout info -- would be fancy to calc some of this and bury the template
2549 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2550 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2551 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2552 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2553 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2554 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2555 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2556 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2557 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2558 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2560 # better hang on to conf_dir for a while (for old templates)
2561 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2563 #these are only used when doing paged plaintext
2569 my $min_sdate = 999999999999;
2571 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2572 next unless $cust_bill_pkg->pkgnum > 0;
2573 $min_sdate = $cust_bill_pkg->sdate
2574 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2575 $max_edate = $cust_bill_pkg->edate
2576 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2579 $invoice_data{'bill_period'} = '';
2580 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
2581 . " to " . time2str('%e %h', $max_edate)
2582 if ($max_edate != 0 && $min_sdate != 999999999999);
2584 $invoice_data{finance_section} = '';
2585 if ( $conf->config('finance_pkgclass') ) {
2587 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2588 $invoice_data{finance_section} = $pkg_class->categoryname;
2590 $invoice_data{finance_amount} = '0.00';
2591 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2593 my $countrydefault = $conf->config('countrydefault') || 'US';
2594 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2595 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2596 my $method = $prefix.$_;
2597 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2599 $invoice_data{'ship_country'} = ''
2600 if ( $invoice_data{'ship_country'} eq $countrydefault );
2602 $invoice_data{'cid'} = $params{'cid'}
2605 if ( $cust_main->country eq $countrydefault ) {
2606 $invoice_data{'country'} = '';
2608 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2612 $invoice_data{'address'} = \@address;
2614 $cust_main->payname.
2615 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2616 ? " (P.O. #". $cust_main->payinfo. ")"
2620 push @address, $cust_main->company
2621 if $cust_main->company;
2622 push @address, $cust_main->address1;
2623 push @address, $cust_main->address2
2624 if $cust_main->address2;
2626 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2627 push @address, $invoice_data{'country'}
2628 if $invoice_data{'country'};
2630 while (scalar(@address) < 5);
2632 $invoice_data{'logo_file'} = $params{'logo_file'}
2633 if $params{'logo_file'};
2634 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2635 if $params{'barcode_file'};
2636 $invoice_data{'barcode_img'} = $params{'barcode_img'}
2637 if $params{'barcode_img'};
2638 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2639 if $params{'barcode_cid'};
2641 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2642 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2643 #my $balance_due = $self->owed + $pr_total - $cr_total;
2644 my $balance_due = $self->owed + $pr_total;
2645 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2646 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2647 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2648 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2650 my $summarypage = '';
2651 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2654 $invoice_data{'summarypage'} = $summarypage;
2656 warn "$me substituting variables in notes, footer, smallfooter\n"
2659 foreach my $include (qw( notes footer smallfooter coupon )) {
2661 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2664 if ( $conf->exists($inc_file, $agentnum)
2665 && length( $conf->config($inc_file, $agentnum) ) ) {
2667 @inc_src = $conf->config($inc_file, $agentnum);
2671 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2673 my $convert_map = $convert_maps{$format}{$include};
2675 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2676 s/--\@\]/$delimiters{$format}[1]/g;
2679 &$convert_map( $conf->config($inc_file, $agentnum) );
2683 my $inc_tt = new Text::Template (
2685 SOURCE => [ map "$_\n", @inc_src ],
2686 DELIMITERS => $delimiters{$format},
2687 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2689 unless ( $inc_tt->compile() ) {
2690 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2691 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2695 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2697 $invoice_data{$include} =~ s/\n+$//
2698 if ($format eq 'latex');
2701 $invoice_data{'po_line'} =
2702 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2703 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2706 my %money_chars = ( 'latex' => '',
2707 'html' => $conf->config('money_char') || '$',
2710 my $money_char = $money_chars{$format};
2712 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2713 'html' => $conf->config('money_char') || '$',
2716 my $other_money_char = $other_money_chars{$format};
2717 $invoice_data{'dollar'} = $other_money_char;
2719 my @detail_items = ();
2720 my @total_items = ();
2724 $invoice_data{'detail_items'} = \@detail_items;
2725 $invoice_data{'total_items'} = \@total_items;
2726 $invoice_data{'buf'} = \@buf;
2727 $invoice_data{'sections'} = \@sections;
2729 warn "$me generating sections\n"
2732 my $previous_section = { 'description' => 'Previous Charges',
2733 'subtotal' => $other_money_char.
2734 sprintf('%.2f', $pr_total),
2735 'summarized' => $summarypage ? 'Y' : '',
2737 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
2738 join(' / ', map { $cust_main->balance_date_range(@$_) }
2739 $self->_prior_month30s
2741 if $conf->exists('invoice_include_aging');
2744 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2745 'subtotal' => $taxtotal, # adjusted below
2746 'summarized' => $summarypage ? 'Y' : '',
2748 my $tax_weight = _pkg_category($tax_section->{description})
2749 ? _pkg_category($tax_section->{description})->weight
2751 $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2752 $tax_section->{'sort_weight'} = $tax_weight;
2755 my $adjusttotal = 0;
2756 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2757 'subtotal' => 0, # adjusted below
2758 'summarized' => $summarypage ? 'Y' : '',
2760 my $adjust_weight = _pkg_category($adjust_section->{description})
2761 ? _pkg_category($adjust_section->{description})->weight
2763 $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2764 $adjust_section->{'sort_weight'} = $adjust_weight;
2766 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2767 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2768 $invoice_data{'multisection'} = $multisection;
2769 my $late_sections = [];
2770 my $extra_sections = [];
2771 my $extra_lines = ();
2772 if ( $multisection ) {
2773 ($extra_sections, $extra_lines) =
2774 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2775 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2777 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2779 push @detail_items, @$extra_lines if $extra_lines;
2781 $self->_items_sections( $late_sections, # this could stand a refactor
2783 $escape_function_nonbsp,
2787 if ($conf->exists('svc_phone_sections')) {
2788 my ($phone_sections, $phone_lines) =
2789 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2790 push @{$late_sections}, @$phone_sections;
2791 push @detail_items, @$phone_lines;
2794 push @sections, { 'description' => '', 'subtotal' => '' };
2797 unless ( $conf->exists('disable_previous_balance')
2798 || $conf->exists('previous_balance-summary_only')
2802 warn "$me adding previous balances\n"
2805 foreach my $line_item ( $self->_items_previous ) {
2808 ext_description => [],
2810 $detail->{'ref'} = $line_item->{'pkgnum'};
2811 $detail->{'quantity'} = 1;
2812 $detail->{'section'} = $previous_section;
2813 $detail->{'description'} = &$escape_function($line_item->{'description'});
2814 if ( exists $line_item->{'ext_description'} ) {
2815 @{$detail->{'ext_description'}} = map {
2816 &$escape_function($_);
2817 } @{$line_item->{'ext_description'}};
2819 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2820 $line_item->{'amount'};
2821 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2823 push @detail_items, $detail;
2824 push @buf, [ $detail->{'description'},
2825 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2831 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2832 push @buf, ['','-----------'];
2833 push @buf, [ 'Total Previous Balance',
2834 $money_char. sprintf("%10.2f", $pr_total) ];
2838 if ( $conf->exists('svc_phone-did-summary') ) {
2839 warn "$me adding DID summary\n"
2842 my ($didsummary,$minutes) = $self->_did_summary;
2843 my $didsummary_desc = 'DID Activity Summary (Past 30 days)';
2845 { 'description' => $didsummary_desc,
2846 'ext_description' => [ $didsummary, $minutes ],
2851 foreach my $section (@sections, @$late_sections) {
2853 warn "$me adding section \n". Dumper($section)
2856 # begin some normalization
2857 $section->{'subtotal'} = $section->{'amount'}
2859 && !exists($section->{subtotal})
2860 && exists($section->{amount});
2862 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2863 if ( $invoice_data{finance_section} &&
2864 $section->{'description'} eq $invoice_data{finance_section} );
2866 $section->{'subtotal'} = $other_money_char.
2867 sprintf('%.2f', $section->{'subtotal'})
2870 # continue some normalization
2871 $section->{'amount'} = $section->{'subtotal'}
2875 if ( $section->{'description'} ) {
2876 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2881 warn "$me setting options\n"
2884 my $multilocation = scalar($cust_main->cust_location); #too expensive?
2886 $options{'section'} = $section if $multisection;
2887 $options{'format'} = $format;
2888 $options{'escape_function'} = $escape_function;
2889 $options{'format_function'} = sub { () } unless $unsquelched;
2890 $options{'unsquelched'} = $unsquelched;
2891 $options{'summary_page'} = $summarypage;
2892 $options{'skip_usage'} =
2893 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2894 $options{'multilocation'} = $multilocation;
2895 $options{'multisection'} = $multisection;
2897 warn "$me searching for line items\n"
2900 foreach my $line_item ( $self->_items_pkg(%options) ) {
2902 warn "$me adding line item $line_item\n"
2906 ext_description => [],
2908 $detail->{'ref'} = $line_item->{'pkgnum'};
2909 $detail->{'quantity'} = $line_item->{'quantity'};
2910 $detail->{'section'} = $section;
2911 $detail->{'description'} = &$escape_function($line_item->{'description'});
2912 if ( exists $line_item->{'ext_description'} ) {
2913 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2915 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2916 $line_item->{'amount'};
2917 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2918 $line_item->{'unit_amount'};
2919 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2921 push @detail_items, $detail;
2922 push @buf, ( [ $detail->{'description'},
2923 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2925 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2929 if ( $section->{'description'} ) {
2930 push @buf, ( ['','-----------'],
2931 [ $section->{'description'}. ' sub-total',
2932 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2941 $invoice_data{current_less_finance} =
2942 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2944 if ( $multisection && !$conf->exists('disable_previous_balance')
2945 || $conf->exists('previous_balance-summary_only') )
2947 unshift @sections, $previous_section if $pr_total;
2950 warn "$me adding taxes\n"
2953 foreach my $tax ( $self->_items_tax ) {
2955 $taxtotal += $tax->{'amount'};
2957 my $description = &$escape_function( $tax->{'description'} );
2958 my $amount = sprintf( '%.2f', $tax->{'amount'} );
2960 if ( $multisection ) {
2962 my $money = $old_latex ? '' : $money_char;
2963 push @detail_items, {
2964 ext_description => [],
2967 description => $description,
2968 amount => $money. $amount,
2970 section => $tax_section,
2975 push @total_items, {
2976 'total_item' => $description,
2977 'total_amount' => $other_money_char. $amount,
2982 push @buf,[ $description,
2983 $money_char. $amount,
2990 $total->{'total_item'} = 'Sub-total';
2991 $total->{'total_amount'} =
2992 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2994 if ( $multisection ) {
2995 $tax_section->{'subtotal'} = $other_money_char.
2996 sprintf('%.2f', $taxtotal);
2997 $tax_section->{'pretotal'} = 'New charges sub-total '.
2998 $total->{'total_amount'};
2999 push @sections, $tax_section if $taxtotal;
3001 unshift @total_items, $total;
3004 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3006 push @buf,['','-----------'];
3007 push @buf,[( $conf->exists('disable_previous_balance')
3009 : 'Total New Charges'
3011 $money_char. sprintf("%10.2f",$self->charged) ];
3017 $item = $conf->config('previous_balance-exclude_from_total')
3018 || 'Total New Charges'
3019 if $conf->exists('previous_balance-exclude_from_total');
3020 my $amount = $self->charged +
3021 ( $conf->exists('disable_previous_balance') ||
3022 $conf->exists('previous_balance-exclude_from_total')
3026 $total->{'total_item'} = &$embolden_function($item);
3027 $total->{'total_amount'} =
3028 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
3029 if ( $multisection ) {
3030 if ( $adjust_section->{'sort_weight'} ) {
3031 $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
3032 sprintf("%.2f", ($self->billing_balance || 0) );
3034 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
3035 sprintf('%.2f', $self->charged );
3038 push @total_items, $total;
3040 push @buf,['','-----------'];
3043 sprintf( '%10.2f', $amount )
3048 unless ( $conf->exists('disable_previous_balance') ) {
3049 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3052 my $credittotal = 0;
3053 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3056 $total->{'total_item'} = &$escape_function($credit->{'description'});
3057 $credittotal += $credit->{'amount'};
3058 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3059 $adjusttotal += $credit->{'amount'};
3060 if ( $multisection ) {
3061 my $money = $old_latex ? '' : $money_char;
3062 push @detail_items, {
3063 ext_description => [],
3066 description => &$escape_function($credit->{'description'}),
3067 amount => $money. $credit->{'amount'},
3069 section => $adjust_section,
3072 push @total_items, $total;
3076 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3079 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3080 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3084 my $paymenttotal = 0;
3085 foreach my $payment ( $self->_items_payments ) {
3087 $total->{'total_item'} = &$escape_function($payment->{'description'});
3088 $paymenttotal += $payment->{'amount'};
3089 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3090 $adjusttotal += $payment->{'amount'};
3091 if ( $multisection ) {
3092 my $money = $old_latex ? '' : $money_char;
3093 push @detail_items, {
3094 ext_description => [],
3097 description => &$escape_function($payment->{'description'}),
3098 amount => $money. $payment->{'amount'},
3100 section => $adjust_section,
3103 push @total_items, $total;
3105 push @buf, [ $payment->{'description'},
3106 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3109 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3111 if ( $multisection ) {
3112 $adjust_section->{'subtotal'} = $other_money_char.
3113 sprintf('%.2f', $adjusttotal);
3114 push @sections, $adjust_section
3115 unless $adjust_section->{sort_weight};
3120 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3121 $total->{'total_amount'} =
3122 &$embolden_function(
3123 $other_money_char. sprintf('%.2f', $summarypage
3125 $self->billing_balance
3126 : $self->owed + $pr_total
3129 if ( $multisection && !$adjust_section->{sort_weight} ) {
3130 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3131 $total->{'total_amount'};
3133 push @total_items, $total;
3135 push @buf,['','-----------'];
3136 push @buf,[$self->balance_due_msg, $money_char.
3137 sprintf("%10.2f", $balance_due ) ];
3140 if ( $conf->exists('previous_balance-show_credit')
3141 and $cust_main->balance < 0 ) {
3142 my $credit_total = {
3143 'total_item' => &$embolden_function($self->credit_balance_msg),
3144 'total_amount' => &$embolden_function(
3145 $other_money_char. sprintf('%.2f', -$cust_main->balance)
3148 if ( $multisection ) {
3149 $adjust_section->{'posttotal'} .= $newline_token .
3150 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3153 push @total_items, $credit_total;
3155 push @buf,['','-----------'];
3156 push @buf,[$self->credit_balance_msg, $money_char.
3157 sprintf("%10.2f", -$cust_main->balance ) ];
3161 if ( $multisection ) {
3162 if ($conf->exists('svc_phone_sections')) {
3164 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3165 $total->{'total_amount'} =
3166 &$embolden_function(
3167 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3169 my $last_section = pop @sections;
3170 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3171 $total->{'total_amount'};
3172 push @sections, $last_section;
3174 push @sections, @$late_sections
3178 my @includelist = ();
3179 push @includelist, 'summary' if $summarypage;
3180 foreach my $include ( @includelist ) {
3182 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3185 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3187 @inc_src = $conf->config($inc_file, $agentnum);
3191 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3193 my $convert_map = $convert_maps{$format}{$include};
3195 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3196 s/--\@\]/$delimiters{$format}[1]/g;
3199 &$convert_map( $conf->config($inc_file, $agentnum) );
3203 my $inc_tt = new Text::Template (
3205 SOURCE => [ map "$_\n", @inc_src ],
3206 DELIMITERS => $delimiters{$format},
3207 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3209 unless ( $inc_tt->compile() ) {
3210 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3211 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3215 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3217 $invoice_data{$include} =~ s/\n+$//
3218 if ($format eq 'latex');
3223 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3224 /invoice_lines\((\d*)\)/;
3225 $invoice_lines += $1 || scalar(@buf);
3228 die "no invoice_lines() functions in template?"
3229 if ( $format eq 'template' && !$wasfunc );
3231 if ($format eq 'template') {
3233 if ( $invoice_lines ) {
3234 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3235 $invoice_data{'total_pages'}++
3236 if scalar(@buf) % $invoice_lines;
3239 #setup subroutine for the template
3240 sub FS::cust_bill::_template::invoice_lines {
3241 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
3243 scalar(@FS::cust_bill::_template::buf)
3244 ? shift @FS::cust_bill::_template::buf
3253 push @collect, split("\n",
3254 $text_template->fill_in( HASH => \%invoice_data,
3255 PACKAGE => 'FS::cust_bill::_template'
3258 $FS::cust_bill::_template::page++;
3260 map "$_\n", @collect;
3262 warn "filling in template for invoice ". $self->invnum. "\n"
3264 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3267 $text_template->fill_in(HASH => \%invoice_data);
3271 # helper routine for generating date ranges
3272 sub _prior_month30s {
3275 [ 1, 2592000 ], # 0-30 days ago
3276 [ 2592000, 5184000 ], # 30-60 days ago
3277 [ 5184000, 7776000 ], # 60-90 days ago
3278 [ 7776000, 0 ], # 90+ days ago
3281 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3282 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3287 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3289 Returns an postscript invoice, as a scalar.
3291 Options can be passed as a hashref (recommended) or as a list of time, template
3292 and then any key/value pairs for any other options.
3294 I<time> an optional value used to control the printing of overdue messages. The
3295 default is now. It isn't the date of the invoice; that's the `_date' field.
3296 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3297 L<Time::Local> and L<Date::Parse> for conversion functions.
3299 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3306 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3307 my $ps = generate_ps($file);
3309 unlink($barcodefile) if $barcodefile;
3314 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3316 Returns an PDF invoice, as a scalar.
3318 Options can be passed as a hashref (recommended) or as a list of time, template
3319 and then any key/value pairs for any other options.
3321 I<time> an optional value used to control the printing of overdue messages. The
3322 default is now. It isn't the date of the invoice; that's the `_date' field.
3323 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3324 L<Time::Local> and L<Date::Parse> for conversion functions.
3326 I<template>, if specified, is the name of a suffix for alternate invoices.
3328 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3335 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3336 my $pdf = generate_pdf($file);
3338 unlink($barcodefile) if $barcodefile;
3343 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3345 Returns an HTML invoice, as a scalar.
3347 I<time> an optional value used to control the printing of overdue messages. The
3348 default is now. It isn't the date of the invoice; that's the `_date' field.
3349 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3350 L<Time::Local> and L<Date::Parse> for conversion functions.
3352 I<template>, if specified, is the name of a suffix for alternate invoices.
3354 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3356 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3357 when emailing the invoice as part of a multipart/related MIME email.
3365 %params = %{ shift() };
3367 $params{'time'} = shift;
3368 $params{'template'} = shift;
3369 $params{'cid'} = shift;
3372 $params{'format'} = 'html';
3374 $self->print_generic( %params );
3377 # quick subroutine for print_latex
3379 # There are ten characters that LaTeX treats as special characters, which
3380 # means that they do not simply typeset themselves:
3381 # # $ % & ~ _ ^ \ { }
3383 # TeX ignores blanks following an escaped character; if you want a blank (as
3384 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3388 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3389 $value =~ s/([<>])/\$$1\$/g;
3395 encode_entities($value);
3399 sub _html_escape_nbsp {
3400 my $value = _html_escape(shift);
3401 $value =~ s/ +/ /g;
3405 #utility methods for print_*
3407 sub _translate_old_latex_format {
3408 warn "_translate_old_latex_format called\n"
3415 if ( $line =~ /^%%Detail\s*$/ ) {
3417 push @template, q![@--!,
3418 q! foreach my $_tr_line (@detail_items) {!,
3419 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3420 q! $_tr_line->{'description'} .= !,
3421 q! "\\tabularnewline\n~~".!,
3422 q! join( "\\tabularnewline\n~~",!,
3423 q! @{$_tr_line->{'ext_description'}}!,
3427 while ( ( my $line_item_line = shift )
3428 !~ /^%%EndDetail\s*$/ ) {
3429 $line_item_line =~ s/'/\\'/g; # nice LTS
3430 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3431 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3432 push @template, " \$OUT .= '$line_item_line';";
3435 push @template, '}',
3438 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3440 push @template, '[@--',
3441 ' foreach my $_tr_line (@total_items) {';
3443 while ( ( my $total_item_line = shift )
3444 !~ /^%%EndTotalDetails\s*$/ ) {
3445 $total_item_line =~ s/'/\\'/g; # nice LTS
3446 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3447 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3448 push @template, " \$OUT .= '$total_item_line';";
3451 push @template, '}',
3455 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3456 push @template, $line;
3462 warn "$_\n" foreach @template;
3471 #check for an invoice-specific override
3472 return $self->invoice_terms if $self->invoice_terms;
3474 #check for a customer- specific override
3475 my $cust_main = $self->cust_main;
3476 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3478 #use configured default
3479 $conf->config('invoice_default_terms') || '';
3485 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3486 $duedate = $self->_date() + ( $1 * 86400 );
3493 $self->due_date ? time2str(shift, $self->due_date) : '';
3496 sub balance_due_msg {
3498 my $msg = 'Balance Due';
3499 return $msg unless $self->terms;
3500 if ( $self->due_date ) {
3501 $msg .= ' - Please pay by '. $self->due_date2str($date_format);
3502 } elsif ( $self->terms ) {
3503 $msg .= ' - '. $self->terms;
3508 sub balance_due_date {
3511 if ( $conf->exists('invoice_default_terms')
3512 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3513 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3518 sub credit_balance_msg { 'Credit Balance Remaining' }
3520 =item invnum_date_pretty
3522 Returns a string with the invoice number and date, for example:
3523 "Invoice #54 (3/20/2008)"
3527 sub invnum_date_pretty {
3529 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3534 Returns a string with the date, for example: "3/20/2008"
3540 time2str($date_format, $self->_date);
3543 use vars qw(%pkg_category_cache);
3544 sub _items_sections {
3547 my $summarypage = shift;
3549 my $extra_sections = shift;
3553 my %late_subtotal = ();
3556 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3559 my $usage = $cust_bill_pkg->usage;
3561 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3562 next if ( $display->summary && $summarypage );
3564 my $section = $display->section;
3565 my $type = $display->type;
3567 $not_tax{$section} = 1
3568 unless $cust_bill_pkg->pkgnum == 0;
3570 if ( $display->post_total && !$summarypage ) {
3571 if (! $type || $type eq 'S') {
3572 $late_subtotal{$section} += $cust_bill_pkg->setup
3573 if $cust_bill_pkg->setup != 0;
3577 $late_subtotal{$section} += $cust_bill_pkg->recur
3578 if $cust_bill_pkg->recur != 0;
3581 if ($type && $type eq 'R') {
3582 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3583 if $cust_bill_pkg->recur != 0;
3586 if ($type && $type eq 'U') {
3587 $late_subtotal{$section} += $usage
3588 unless scalar(@$extra_sections);
3593 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3595 if (! $type || $type eq 'S') {
3596 $subtotal{$section} += $cust_bill_pkg->setup
3597 if $cust_bill_pkg->setup != 0;
3601 $subtotal{$section} += $cust_bill_pkg->recur
3602 if $cust_bill_pkg->recur != 0;
3605 if ($type && $type eq 'R') {
3606 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3607 if $cust_bill_pkg->recur != 0;
3610 if ($type && $type eq 'U') {
3611 $subtotal{$section} += $usage
3612 unless scalar(@$extra_sections);
3621 %pkg_category_cache = ();
3623 push @$late, map { { 'description' => &{$escape}($_),
3624 'subtotal' => $late_subtotal{$_},
3626 'sort_weight' => ( _pkg_category($_)
3627 ? _pkg_category($_)->weight
3630 ((_pkg_category($_) && _pkg_category($_)->condense)
3631 ? $self->_condense_section($format)
3635 sort _sectionsort keys %late_subtotal;
3638 if ( $summarypage ) {
3639 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3640 map { $_->categoryname } qsearch('pkg_category', {});
3641 push @sections, '' if exists($subtotal{''});
3643 @sections = keys %subtotal;
3646 my @early = map { { 'description' => &{$escape}($_),
3647 'subtotal' => $subtotal{$_},
3648 'summarized' => $not_tax{$_} ? '' : 'Y',
3649 'tax_section' => $not_tax{$_} ? '' : 'Y',
3650 'sort_weight' => ( _pkg_category($_)
3651 ? _pkg_category($_)->weight
3654 ((_pkg_category($_) && _pkg_category($_)->condense)
3655 ? $self->_condense_section($format)
3660 push @early, @$extra_sections if $extra_sections;
3662 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3666 #helper subs for above
3669 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3673 my $categoryname = shift;
3674 $pkg_category_cache{$categoryname} ||=
3675 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3678 my %condensed_format = (
3679 'label' => [ qw( Description Qty Amount ) ],
3681 sub { shift->{description} },
3682 sub { shift->{quantity} },
3683 sub { my($href, %opt) = @_;
3684 ($opt{dollar} || ''). $href->{amount};
3687 'align' => [ qw( l r r ) ],
3688 'span' => [ qw( 5 1 1 ) ], # unitprices?
3689 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3692 sub _condense_section {
3693 my ( $self, $format ) = ( shift, shift );
3695 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3696 qw( description_generator
3699 total_line_generator
3704 sub _condensed_generator_defaults {
3705 my ( $self, $format ) = ( shift, shift );
3706 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3715 sub _condensed_header_generator {
3716 my ( $self, $format ) = ( shift, shift );
3718 my ( $f, $prefix, $suffix, $separator, $column ) =
3719 _condensed_generator_defaults($format);
3721 if ($format eq 'latex') {
3722 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3723 $suffix = "\\\\\n\\hline";
3726 sub { my ($d,$a,$s,$w) = @_;
3727 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3729 } elsif ( $format eq 'html' ) {
3730 $prefix = '<th></th>';
3734 sub { my ($d,$a,$s,$w) = @_;
3735 return qq!<th align="$html_align{$a}">$d</th>!;
3743 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3745 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3748 $prefix. join($separator, @result). $suffix;
3753 sub _condensed_description_generator {
3754 my ( $self, $format ) = ( shift, shift );
3756 my ( $f, $prefix, $suffix, $separator, $column ) =
3757 _condensed_generator_defaults($format);
3759 my $money_char = '$';
3760 if ($format eq 'latex') {
3761 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3763 $separator = " & \n";
3765 sub { my ($d,$a,$s,$w) = @_;
3766 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3768 $money_char = '\\dollar';
3769 }elsif ( $format eq 'html' ) {
3770 $prefix = '"><td align="center"></td>';
3774 sub { my ($d,$a,$s,$w) = @_;
3775 return qq!<td align="$html_align{$a}">$d</td>!;
3777 #$money_char = $conf->config('money_char') || '$';
3778 $money_char = ''; # this is madness
3786 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3788 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3790 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3791 map { $f->{$_}->[$i] } qw(align span width)
3795 $prefix. join( $separator, @result ). $suffix;
3800 sub _condensed_total_generator {
3801 my ( $self, $format ) = ( shift, shift );
3803 my ( $f, $prefix, $suffix, $separator, $column ) =
3804 _condensed_generator_defaults($format);
3807 if ($format eq 'latex') {
3810 $separator = " & \n";
3812 sub { my ($d,$a,$s,$w) = @_;
3813 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3815 }elsif ( $format eq 'html' ) {
3819 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3821 sub { my ($d,$a,$s,$w) = @_;
3822 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3831 # my $r = &{$f->{fields}->[$i]}(@args);
3832 # $r .= ' Total' unless $i;
3834 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3836 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3837 map { $f->{$_}->[$i] } qw(align span width)
3841 $prefix. join( $separator, @result ). $suffix;
3846 =item total_line_generator FORMAT
3848 Returns a coderef used for generation of invoice total line items for this
3849 usage_class. FORMAT is either html or latex
3853 # should not be used: will have issues with hash element names (description vs
3854 # total_item and amount vs total_amount -- another array of functions?
3856 sub _condensed_total_line_generator {
3857 my ( $self, $format ) = ( shift, shift );
3859 my ( $f, $prefix, $suffix, $separator, $column ) =
3860 _condensed_generator_defaults($format);
3863 if ($format eq 'latex') {
3866 $separator = " & \n";
3868 sub { my ($d,$a,$s,$w) = @_;
3869 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3871 }elsif ( $format eq 'html' ) {
3875 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3877 sub { my ($d,$a,$s,$w) = @_;
3878 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3887 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3889 &{$column}( &{$f->{fields}->[$i]}(@args),
3890 map { $f->{$_}->[$i] } qw(align span width)
3894 $prefix. join( $separator, @result ). $suffix;
3899 #sub _items_extra_usage_sections {
3901 # my $escape = shift;
3903 # my %sections = ();
3905 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
3906 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3908 # next unless $cust_bill_pkg->pkgnum > 0;
3910 # foreach my $section ( keys %usage_class ) {
3912 # my $usage = $cust_bill_pkg->usage($section);
3914 # next unless $usage && $usage > 0;
3916 # $sections{$section} ||= 0;
3917 # $sections{$section} += $usage;
3923 # map { { 'description' => &{$escape}($_),
3924 # 'subtotal' => $sections{$_},
3925 # 'summarized' => '',
3926 # 'tax_section' => '',
3929 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3933 sub _items_extra_usage_sections {
3942 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3943 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3944 next unless $cust_bill_pkg->pkgnum > 0;
3946 foreach my $classnum ( keys %usage_class ) {
3947 my $section = $usage_class{$classnum}->classname;
3948 $classnums{$section} = $classnum;
3950 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3951 my $amount = $detail->amount;
3952 next unless $amount && $amount > 0;
3954 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
3955 $sections{$section}{amount} += $amount; #subtotal
3956 $sections{$section}{calls}++;
3957 $sections{$section}{duration} += $detail->duration;
3959 my $desc = $detail->regionname;
3960 my $description = $desc;
3961 $description = substr($desc, 0, 50). '...'
3962 if $format eq 'latex' && length($desc) > 50;
3964 $lines{$section}{$desc} ||= {
3965 description => &{$escape}($description),
3966 #pkgpart => $part_pkg->pkgpart,
3967 pkgnum => $cust_bill_pkg->pkgnum,
3972 #unit_amount => $cust_bill_pkg->unitrecur,
3973 quantity => $cust_bill_pkg->quantity,
3974 product_code => 'N/A',
3975 ext_description => [],
3978 $lines{$section}{$desc}{amount} += $amount;
3979 $lines{$section}{$desc}{calls}++;
3980 $lines{$section}{$desc}{duration} += $detail->duration;
3986 my %sectionmap = ();
3987 foreach (keys %sections) {
3988 my $usage_class = $usage_class{$classnums{$_}};
3989 $sectionmap{$_} = { 'description' => &{$escape}($_),
3990 'amount' => $sections{$_}{amount}, #subtotal
3991 'calls' => $sections{$_}{calls},
3992 'duration' => $sections{$_}{duration},
3994 'tax_section' => '',
3995 'sort_weight' => $usage_class->weight,
3996 ( $usage_class->format
3997 ? ( map { $_ => $usage_class->$_($format) }
3998 qw( description_generator header_generator total_generator total_line_generator )
4005 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4009 foreach my $section ( keys %lines ) {
4010 foreach my $line ( keys %{$lines{$section}} ) {
4011 my $l = $lines{$section}{$line};
4012 $l->{section} = $sectionmap{$section};
4013 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4014 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4019 return(\@sections, \@lines);
4025 my $end = $self->_date;
4026 my $start = $end - 2592000; # 30 days
4027 my $cust_main = $self->cust_main;
4028 my @pkgs = $cust_main->all_pkgs;
4029 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4032 foreach my $pkg ( @pkgs ) {
4033 my @h_cust_svc = $pkg->h_cust_svc($end);
4034 foreach my $h_cust_svc ( @h_cust_svc ) {
4035 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4036 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4038 my $inserted = $h_cust_svc->date_inserted;
4039 my $deleted = $h_cust_svc->date_deleted;
4040 my $phone_inserted = $h_cust_svc->h_svc_x($inserted);
4042 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4044 # DID either activated or ported in; cannot be both for same DID simultaneously
4045 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4046 && (!$phone_inserted->lnp_status
4047 || $phone_inserted->lnp_status eq ''
4048 || $phone_inserted->lnp_status eq 'native')) {
4051 else { # this one not so clean, should probably move to (h_)svc_phone
4052 my $phone_portedin = qsearchs( 'h_svc_phone',
4053 { 'svcnum' => $h_cust_svc->svcnum,
4054 'lnp_status' => 'portedin' },
4055 FS::h_svc_phone->sql_h_searchs($end),
4057 $num_portedin++ if $phone_portedin;
4060 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4061 if($deleted >= $start && $deleted <= $end && $phone_deleted
4062 && (!$phone_deleted->lnp_status
4063 || $phone_deleted->lnp_status ne 'portingout')) {
4066 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4067 && $phone_deleted->lnp_status
4068 && $phone_deleted->lnp_status eq 'portingout') {
4072 # increment usage minutes
4073 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end);
4074 foreach my $cdr ( @cdrs ) {
4075 $minutes += $cdr->billsec/60;
4078 # don't look at this service again
4079 push @seen, $h_cust_svc->svcnum;
4083 $minutes = sprintf("%d", $minutes);
4084 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4085 . "$num_deactivated Ported-Out: $num_portedout ",
4086 "Total Minutes: $minutes");
4089 sub _items_svc_phone_sections {
4098 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4099 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4101 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4102 next unless $cust_bill_pkg->pkgnum > 0;
4104 my @header = $cust_bill_pkg->details_header;
4105 next unless scalar(@header);
4107 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4109 my $phonenum = $detail->phonenum;
4110 next unless $phonenum;
4112 my $amount = $detail->amount;
4113 next unless $amount && $amount > 0;
4115 $sections{$phonenum} ||= { 'amount' => 0,
4118 'sort_weight' => -1,
4119 'phonenum' => $phonenum,
4121 $sections{$phonenum}{amount} += $amount; #subtotal
4122 $sections{$phonenum}{calls}++;
4123 $sections{$phonenum}{duration} += $detail->duration;
4125 my $desc = $detail->regionname;
4126 my $description = $desc;
4127 $description = substr($desc, 0, 50). '...'
4128 if $format eq 'latex' && length($desc) > 50;
4130 $lines{$phonenum}{$desc} ||= {
4131 description => &{$escape}($description),
4132 #pkgpart => $part_pkg->pkgpart,
4140 product_code => 'N/A',
4141 ext_description => [],
4144 $lines{$phonenum}{$desc}{amount} += $amount;
4145 $lines{$phonenum}{$desc}{calls}++;
4146 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4148 my $line = $usage_class{$detail->classnum}->classname;
4149 $sections{"$phonenum $line"} ||=
4153 'sort_weight' => $usage_class{$detail->classnum}->weight,
4154 'phonenum' => $phonenum,
4155 'header' => [ @header ],
4157 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4158 $sections{"$phonenum $line"}{calls}++;
4159 $sections{"$phonenum $line"}{duration} += $detail->duration;
4161 $lines{"$phonenum $line"}{$desc} ||= {
4162 description => &{$escape}($description),
4163 #pkgpart => $part_pkg->pkgpart,
4171 product_code => 'N/A',
4172 ext_description => [],
4175 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4176 $lines{"$phonenum $line"}{$desc}{calls}++;
4177 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4178 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4179 $detail->formatted('format' => $format);
4184 my %sectionmap = ();
4185 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4186 foreach ( keys %sections ) {
4187 my @header = @{ $sections{$_}{header} || [] };
4189 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4190 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4191 my $usage_class = $summary ? $simple : $usage_simple;
4192 my $ending = $summary ? ' usage charges' : '';
4195 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4197 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4198 'amount' => $sections{$_}{amount}, #subtotal
4199 'calls' => $sections{$_}{calls},
4200 'duration' => $sections{$_}{duration},
4202 'tax_section' => '',
4203 'phonenum' => $sections{$_}{phonenum},
4204 'sort_weight' => $sections{$_}{sort_weight},
4205 'post_total' => $summary, #inspire pagebreak
4207 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4208 qw( description_generator
4211 total_line_generator
4218 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4219 $a->{sort_weight} <=> $b->{sort_weight}
4224 foreach my $section ( keys %lines ) {
4225 foreach my $line ( keys %{$lines{$section}} ) {
4226 my $l = $lines{$section}{$line};
4227 $l->{section} = $sectionmap{$section};
4228 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4229 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4234 if($conf->exists('phone_usage_class_summary')) {
4235 # this only works with Latex
4239 # after this, we'll have only two sections per DID:
4240 # Calls Summary and Calls Detail
4241 foreach my $section ( @sections ) {
4242 if($section->{'post_total'}) {
4243 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4244 $section->{'total_line_generator'} = sub { '' };
4245 $section->{'total_generator'} = sub { '' };
4246 $section->{'header_generator'} = sub { '' };
4247 $section->{'description_generator'} = '';
4248 push @newsections, $section;
4249 my %calls_detail = %$section;
4250 $calls_detail{'post_total'} = '';
4251 $calls_detail{'sort_weight'} = '';
4252 $calls_detail{'description_generator'} = sub { '' };
4253 $calls_detail{'header_generator'} = sub {
4254 return ' & Date/Time & Called Number & Duration & Price'
4255 if $format eq 'latex';
4258 $calls_detail{'description'} = 'Calls Detail: '
4259 . $section->{'phonenum'};
4260 push @newsections, \%calls_detail;
4264 # after this, each usage class is collapsed/summarized into a single
4265 # line under the Calls Summary section
4266 foreach my $newsection ( @newsections ) {
4267 if($newsection->{'post_total'}) { # this means Calls Summary
4268 foreach my $section ( @sections ) {
4269 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4270 && !$section->{'post_total'});
4271 my $newdesc = $section->{'description'};
4272 my $tn = $section->{'phonenum'};
4273 $newdesc =~ s/$tn//g;
4274 my $line = { ext_description => [],
4278 calls => $section->{'calls'},
4279 section => $newsection,
4280 duration => $section->{'duration'},
4281 description => $newdesc,
4282 amount => sprintf("%.2f",$section->{'amount'}),
4283 product_code => 'N/A',
4285 push @newlines, $line;
4290 # after this, Calls Details is populated with all CDRs
4291 foreach my $newsection ( @newsections ) {
4292 if(!$newsection->{'post_total'}) { # this means Calls Details
4293 foreach my $line ( @lines ) {
4294 next unless (scalar(@{$line->{'ext_description'}}) &&
4295 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4297 my @extdesc = @{$line->{'ext_description'}};
4299 foreach my $extdesc ( @extdesc ) {
4300 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4301 push @newextdesc, $extdesc;
4303 $line->{'ext_description'} = \@newextdesc;
4304 $line->{'section'} = $newsection;
4305 push @newlines, $line;
4310 return(\@newsections, \@newlines);
4313 return(\@sections, \@lines);
4320 #my @display = scalar(@_)
4322 # : qw( _items_previous _items_pkg );
4323 # #: qw( _items_pkg );
4324 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4325 my @display = qw( _items_previous _items_pkg );
4328 foreach my $display ( @display ) {
4329 push @b, $self->$display(@_);
4334 sub _items_previous {
4336 my $cust_main = $self->cust_main;
4337 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4339 foreach ( @pr_cust_bill ) {
4340 my $date = $conf->exists('invoice_show_prior_due_date')
4341 ? 'due '. $_->due_date2str($date_format)
4342 : time2str($date_format, $_->_date);
4344 'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
4345 #'pkgpart' => 'N/A',
4347 'amount' => sprintf("%.2f", $_->owed),
4353 # 'description' => 'Previous Balance',
4354 # #'pkgpart' => 'N/A',
4355 # 'pkgnum' => 'N/A',
4356 # 'amount' => sprintf("%10.2f", $pr_total ),
4357 # 'ext_description' => [ map {
4358 # "Invoice ". $_->invnum.
4359 # " (". time2str("%x",$_->_date). ") ".
4360 # sprintf("%10.2f", $_->owed)
4361 # } @pr_cust_bill ],
4370 warn "$me _items_pkg searching for all package line items\n"
4373 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4375 warn "$me _items_pkg filtering line items\n"
4377 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4379 if ($options{section} && $options{section}->{condensed}) {
4381 warn "$me _items_pkg condensing section\n"
4385 local $Storable::canonical = 1;
4386 foreach ( @items ) {
4388 delete $item->{ref};
4389 delete $item->{ext_description};
4390 my $key = freeze($item);
4391 $itemshash{$key} ||= 0;
4392 $itemshash{$key} ++; # += $item->{quantity};
4394 @items = sort { $a->{description} cmp $b->{description} }
4395 map { my $i = thaw($_);
4396 $i->{quantity} = $itemshash{$_};
4398 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4404 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4411 return 0 unless $a->itemdesc cmp $b->itemdesc;
4412 return -1 if $b->itemdesc eq 'Tax';
4413 return 1 if $a->itemdesc eq 'Tax';
4414 return -1 if $b->itemdesc eq 'Other surcharges';
4415 return 1 if $a->itemdesc eq 'Other surcharges';
4416 $a->itemdesc cmp $b->itemdesc;
4421 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4422 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4425 sub _items_cust_bill_pkg {
4427 my $cust_bill_pkgs = shift;
4430 my $format = $opt{format} || '';
4431 my $escape_function = $opt{escape_function} || sub { shift };
4432 my $format_function = $opt{format_function} || '';
4433 my $unsquelched = $opt{unsquelched} || '';
4434 my $section = $opt{section}->{description} if $opt{section};
4435 my $summary_page = $opt{summary_page} || '';
4436 my $multilocation = $opt{multilocation} || '';
4437 my $multisection = $opt{multisection} || '';
4438 my $discount_show_always = 0;
4441 my ($s, $r, $u) = ( undef, undef, undef );
4442 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4445 warn "$me _items_cust_bill_pkg considering cust_bill_pkg $cust_bill_pkg\n"
4448 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
4449 && $conf->exists('discount-show-always'));
4451 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4452 if ( $_ && !$cust_bill_pkg->hidden ) {
4453 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4454 $_->{amount} =~ s/^\-0\.00$/0.00/;
4455 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4457 unless ( $_->{amount} == 0 && !$discount_show_always );
4462 foreach my $display ( grep { defined($section)
4463 ? $_->section eq $section
4466 #grep { !$_->summary || !$summary_page } # bunk!
4467 grep { !$_->summary || $multisection }
4468 $cust_bill_pkg->cust_bill_pkg_display
4472 warn "$me _items_cust_bill_pkg considering display item $display\n"
4475 my $type = $display->type;
4477 my $desc = $cust_bill_pkg->desc;
4478 $desc = substr($desc, 0, 50). '...'
4479 if $format eq 'latex' && length($desc) > 50;
4481 my %details_opt = ( 'format' => $format,
4482 'escape_function' => $escape_function,
4483 'format_function' => $format_function,
4486 if ( $cust_bill_pkg->pkgnum > 0 ) {
4488 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4491 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4493 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
4495 warn "$me _items_cust_bill_pkg adding setup\n"
4498 my $description = $desc;
4499 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
4502 unless ( $cust_pkg->part_pkg->hide_svc_detail
4503 || $cust_bill_pkg->hidden )
4506 push @d, map &{$escape_function}($_),
4507 $cust_pkg->h_labels_short($self->_date, undef, 'I')
4508 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4510 if ( $multilocation ) {
4511 my $loc = $cust_pkg->location_label;
4512 $loc = substr($loc, 0, 50). '...'
4513 if $format eq 'latex' && length($loc) > 50;
4514 push @d, &{$escape_function}($loc);
4519 push @d, $cust_bill_pkg->details(%details_opt)
4520 if $cust_bill_pkg->recur == 0;
4522 if ( $cust_bill_pkg->hidden ) {
4523 $s->{amount} += $cust_bill_pkg->setup;
4524 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4525 push @{ $s->{ext_description} }, @d;
4528 description => $description,
4529 #pkgpart => $part_pkg->pkgpart,
4530 pkgnum => $cust_bill_pkg->pkgnum,
4531 amount => $cust_bill_pkg->setup,
4532 unit_amount => $cust_bill_pkg->unitsetup,
4533 quantity => $cust_bill_pkg->quantity,
4534 ext_description => \@d,
4540 if ( ( $cust_bill_pkg->recur != 0 || $cust_bill_pkg->setup == 0 ||
4541 ($discount_show_always && $cust_bill_pkg->recur == 0) ) &&
4542 ( !$type || $type eq 'R' || $type eq 'U' )
4546 warn "$me _items_cust_bill_pkg adding recur/usage\n"
4549 my $is_summary = $display->summary;
4550 my $description = ($is_summary && $type && $type eq 'U')
4551 ? "Usage charges" : $desc;
4553 $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4554 " - ". time2str($date_format, $cust_bill_pkg->edate).
4556 unless $conf->exists('disable_line_item_date_ranges');
4560 #at least until cust_bill_pkg has "past" ranges in addition to
4561 #the "future" sdate/edate ones... see #3032
4562 my @dates = ( $self->_date );
4563 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4564 push @dates, $prev->sdate if $prev;
4565 push @dates, undef if !$prev;
4567 unless ( $cust_pkg->part_pkg->hide_svc_detail
4568 || $cust_bill_pkg->itemdesc
4569 || $cust_bill_pkg->hidden
4570 || $is_summary && $type && $type eq 'U' )
4573 warn "$me _items_cust_bill_pkg adding service details\n"
4576 push @d, map &{$escape_function}($_),
4577 $cust_pkg->h_labels_short(@dates, 'I')
4578 #$cust_bill_pkg->edate,
4579 #$cust_bill_pkg->sdate)
4580 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4582 warn "$me _items_cust_bill_pkg done adding service details\n"
4585 if ( $multilocation ) {
4586 my $loc = $cust_pkg->location_label;
4587 $loc = substr($loc, 0, 50). '...'
4588 if $format eq 'latex' && length($loc) > 50;
4589 push @d, &{$escape_function}($loc);
4594 unless ( $is_summary ) {
4595 warn "$me _items_cust_bill_pkg adding details\n"
4598 #instead of omitting details entirely in this case (unwanted side
4599 # effects), just omit CDRs
4600 $details_opt{'format_function'} = sub { () }
4601 if $type && $type eq 'R';
4603 push @d, $cust_bill_pkg->details(%details_opt);
4606 warn "$me _items_cust_bill_pkg calculating amount\n"
4611 $amount = $cust_bill_pkg->recur;
4612 } elsif ($type eq 'R') {
4613 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4614 } elsif ($type eq 'U') {
4615 $amount = $cust_bill_pkg->usage;
4618 if ( !$type || $type eq 'R' ) {
4620 warn "$me _items_cust_bill_pkg adding recur\n"
4623 if ( $cust_bill_pkg->hidden ) {
4624 $r->{amount} += $amount;
4625 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4626 push @{ $r->{ext_description} }, @d;
4629 description => $description,
4630 #pkgpart => $part_pkg->pkgpart,
4631 pkgnum => $cust_bill_pkg->pkgnum,
4633 unit_amount => $cust_bill_pkg->unitrecur,
4634 quantity => $cust_bill_pkg->quantity,
4635 ext_description => \@d,
4639 } else { # $type eq 'U'
4641 warn "$me _items_cust_bill_pkg adding usage\n"
4644 if ( $cust_bill_pkg->hidden ) {
4645 $u->{amount} += $amount;
4646 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4647 push @{ $u->{ext_description} }, @d;
4650 description => $description,
4651 #pkgpart => $part_pkg->pkgpart,
4652 pkgnum => $cust_bill_pkg->pkgnum,
4654 unit_amount => $cust_bill_pkg->unitrecur,
4655 quantity => $cust_bill_pkg->quantity,
4656 ext_description => \@d,
4662 } # recurring or usage with recurring charge
4664 } else { #pkgnum tax or one-shot line item (??)
4666 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
4669 if ( $cust_bill_pkg->setup != 0 ) {
4671 'description' => $desc,
4672 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
4675 if ( $cust_bill_pkg->recur != 0 ) {
4677 'description' => "$desc (".
4678 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
4679 time2str($date_format, $cust_bill_pkg->edate). ')',
4680 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
4690 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
4693 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4695 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4696 $_->{amount} =~ s/^\-0\.00$/0.00/;
4697 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4699 unless ( $_->{amount} == 0 && !$discount_show_always );
4707 sub _items_credits {
4708 my( $self, %opt ) = @_;
4709 my $trim_len = $opt{'trim_len'} || 60;
4713 foreach ( $self->cust_credited ) {
4715 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4717 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4718 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4719 $reason = " ($reason) " if $reason;
4722 #'description' => 'Credit ref\#'. $_->crednum.
4723 # " (". time2str("%x",$_->cust_credit->_date) .")".
4725 'description' => 'Credit applied '.
4726 time2str($date_format,$_->cust_credit->_date). $reason,
4727 'amount' => sprintf("%.2f",$_->amount),
4735 sub _items_payments {
4739 #get & print payments
4740 foreach ( $self->cust_bill_pay ) {
4742 #something more elaborate if $_->amount ne ->cust_pay->paid ?
4745 'description' => "Payment received ".
4746 time2str($date_format,$_->cust_pay->_date ),
4747 'amount' => sprintf("%.2f", $_->amount )
4755 =item call_details [ OPTION => VALUE ... ]
4757 Returns an array of CSV strings representing the call details for this invoice
4758 The only option available is the boolean prepend_billed_number
4763 my ($self, %opt) = @_;
4765 my $format_function = sub { shift };
4767 if ($opt{prepend_billed_number}) {
4768 $format_function = sub {
4772 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4777 my @details = map { $_->details( 'format_function' => $format_function,
4778 'escape_function' => sub{ return() },
4782 $self->cust_bill_pkg;
4783 my $header = $details[0];
4784 ( $header, grep { $_ ne $header } @details );
4794 =item process_reprint
4798 sub process_reprint {
4799 process_re_X('print', @_);
4802 =item process_reemail
4806 sub process_reemail {
4807 process_re_X('email', @_);
4815 process_re_X('fax', @_);
4823 process_re_X('ftp', @_);
4830 sub process_respool {
4831 process_re_X('spool', @_);
4834 use Storable qw(thaw);
4838 my( $method, $job ) = ( shift, shift );
4839 warn "$me process_re_X $method for job $job\n" if $DEBUG;
4841 my $param = thaw(decode_base64(shift));
4842 warn Dumper($param) if $DEBUG;
4853 my($method, $job, %param ) = @_;
4855 warn "re_X $method for job $job with param:\n".
4856 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
4859 #some false laziness w/search/cust_bill.html
4861 my $orderby = 'ORDER BY cust_bill._date';
4863 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
4865 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4867 my @cust_bill = qsearch( {
4868 #'select' => "cust_bill.*",
4869 'table' => 'cust_bill',
4870 'addl_from' => $addl_from,
4872 'extra_sql' => $extra_sql,
4873 'order_by' => $orderby,
4877 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4879 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
4882 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
4883 foreach my $cust_bill ( @cust_bill ) {
4884 $cust_bill->$method();
4886 if ( $job ) { #progressbar foo
4888 if ( time - $min_sec > $last ) {
4889 my $error = $job->update_statustext(
4890 int( 100 * $num / scalar(@cust_bill) )
4892 die $error if $error;
4903 =head1 CLASS METHODS
4909 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
4914 my ($class, $start, $end) = @_;
4916 $class->paid_sql($start, $end). ' - '.
4917 $class->credited_sql($start, $end);
4922 Returns an SQL fragment to retreive the net amount (charged minus credited).
4927 my ($class, $start, $end) = @_;
4928 'charged - '. $class->credited_sql($start, $end);
4933 Returns an SQL fragment to retreive the amount paid against this invoice.
4938 my ($class, $start, $end) = @_;
4939 $start &&= "AND cust_bill_pay._date <= $start";
4940 $end &&= "AND cust_bill_pay._date > $end";
4941 $start = '' unless defined($start);
4942 $end = '' unless defined($end);
4943 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
4944 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
4949 Returns an SQL fragment to retreive the amount credited against this invoice.
4954 my ($class, $start, $end) = @_;
4955 $start &&= "AND cust_credit_bill._date <= $start";
4956 $end &&= "AND cust_credit_bill._date > $end";
4957 $start = '' unless defined($start);
4958 $end = '' unless defined($end);
4959 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
4960 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
4965 Returns an SQL fragment to retrieve the due date of an invoice.
4966 Currently only supported on PostgreSQL.
4974 cust_bill.invoice_terms,
4975 cust_main.invoice_terms,
4976 \''.($conf->config('invoice_default_terms') || '').'\'
4977 ), E\'Net (\\\\d+)\'
4979 ) * 86400 + cust_bill._date'
4982 =item search_sql_where HASHREF
4984 Class method which returns an SQL WHERE fragment to search for parameters
4985 specified in HASHREF. Valid parameters are
4991 List reference of start date, end date, as UNIX timestamps.
5001 List reference of charged limits (exclusive).
5005 List reference of charged limits (exclusive).
5009 flag, return open invoices only
5013 flag, return net invoices only
5017 =item newest_percust
5021 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5025 sub search_sql_where {
5026 my($class, $param) = @_;
5028 warn "$me search_sql_where called with params: \n".
5029 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
5035 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5036 push @search, "cust_main.agentnum = $1";
5040 if ( $param->{_date} ) {
5041 my($beginning, $ending) = @{$param->{_date}};
5043 push @search, "cust_bill._date >= $beginning",
5044 "cust_bill._date < $ending";
5048 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5049 push @search, "cust_bill.invnum >= $1";
5051 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5052 push @search, "cust_bill.invnum <= $1";
5056 if ( $param->{charged} ) {
5057 my @charged = ref($param->{charged})
5058 ? @{ $param->{charged} }
5059 : ($param->{charged});
5061 push @search, map { s/^charged/cust_bill.charged/; $_; }
5065 my $owed_sql = FS::cust_bill->owed_sql;
5068 if ( $param->{owed} ) {
5069 my @owed = ref($param->{owed})
5070 ? @{ $param->{owed} }
5072 push @search, map { s/^owed/$owed_sql/; $_; }
5077 push @search, "0 != $owed_sql"
5078 if $param->{'open'};
5079 push @search, '0 != '. FS::cust_bill->net_sql
5083 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5084 if $param->{'days'};
5087 if ( $param->{'newest_percust'} ) {
5089 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5090 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5092 my @newest_where = map { my $x = $_;
5093 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5096 grep ! /^cust_main./, @search;
5097 my $newest_where = scalar(@newest_where)
5098 ? ' AND '. join(' AND ', @newest_where)
5102 push @search, "cust_bill._date = (
5103 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5104 WHERE newest_cust_bill.custnum = cust_bill.custnum
5110 #agent virtualization
5111 my $curuser = $FS::CurrentUser::CurrentUser;
5112 if ( $curuser->username eq 'fs_queue'
5113 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5115 my $newuser = qsearchs('access_user', {
5116 'username' => $username,
5120 $curuser = $newuser;
5122 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5125 push @search, $curuser->agentnums_sql;
5127 join(' AND ', @search );
5139 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5140 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base