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
294 || $new->{'Hash'}{'cc_surcharge_replace_hack'};
300 =item add_cc_surcharge
306 sub add_cc_surcharge {
307 my ($self, $pkgnum, $amount) = (shift, shift, shift);
310 my $cust_bill_pkg = new FS::cust_bill_pkg({
311 'invnum' => $self->invnum,
315 $error = $cust_bill_pkg->insert;
316 return $error if $error;
318 $self->{'Hash'}{'cc_surcharge_replace_hack'} = 1;
319 $self->charged($self->charged+$amount);
320 $error = $self->replace;
321 return $error if $error;
323 $self->apply_payments_and_credits;
329 Checks all fields to make sure this is a valid invoice. If there is an error,
330 returns the error, otherwise returns false. Called by the insert and replace
339 $self->ut_numbern('invnum')
340 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
341 || $self->ut_numbern('_date')
342 || $self->ut_money('charged')
343 || $self->ut_numbern('printed')
344 || $self->ut_enum('closed', [ '', 'Y' ])
345 || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
346 || $self->ut_numbern('agent_invid') #varchar?
348 return $error if $error;
350 $self->_date(time) unless $self->_date;
352 $self->printed(0) if $self->printed eq '';
359 Returns the displayed invoice number for this invoice: agent_invid if
360 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
366 if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
367 return $self->agent_invid;
369 return $self->invnum;
375 Returns a list consisting of the total previous balance for this customer,
376 followed by the previous outstanding invoices (as FS::cust_bill objects also).
383 my @cust_bill = sort { $a->_date <=> $b->_date }
384 grep { $_->owed != 0 && $_->_date < $self->_date }
385 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
387 foreach ( @cust_bill ) { $total += $_->owed; }
393 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
400 { 'table' => 'cust_bill_pkg',
401 'hashref' => { 'invnum' => $self->invnum },
402 'order_by' => 'ORDER BY billpkgnum',
407 =item cust_bill_pkg_pkgnum PKGNUM
409 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
414 sub cust_bill_pkg_pkgnum {
415 my( $self, $pkgnum ) = @_;
417 { 'table' => 'cust_bill_pkg',
418 'hashref' => { 'invnum' => $self->invnum,
421 'order_by' => 'ORDER BY billpkgnum',
428 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
435 my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
436 $self->cust_bill_pkg;
438 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
443 Returns true if any of the packages (or their definitions) corresponding to the
444 line items for this invoice have the no_auto flag set.
450 grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
453 =item open_cust_bill_pkg
455 Returns the open line items for this invoice.
457 Note that cust_bill_pkg with both setup and recur fees are returned as two
458 separate line items, each with only one fee.
462 # modeled after cust_main::open_cust_bill
463 sub open_cust_bill_pkg {
466 # grep { $_->owed > 0 } $self->cust_bill_pkg
468 my %other = ( 'recur' => 'setup',
469 'setup' => 'recur', );
471 foreach my $field ( qw( recur setup )) {
472 push @open, map { $_->set( $other{$field}, 0 ); $_; }
473 grep { $_->owed($field) > 0 }
474 $self->cust_bill_pkg;
480 =item cust_bill_event
482 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
486 sub cust_bill_event {
488 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
491 =item num_cust_bill_event
493 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
497 sub num_cust_bill_event {
500 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
501 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
502 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
503 $sth->fetchrow_arrayref->[0];
508 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
512 #false laziness w/cust_pkg.pm
516 'table' => 'cust_event',
517 'addl_from' => 'JOIN part_event USING ( eventpart )',
518 'hashref' => { 'tablenum' => $self->invnum },
519 'extra_sql' => " AND eventtable = 'cust_bill' ",
525 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
529 #false laziness w/cust_pkg.pm
533 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
534 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
535 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
536 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
537 $sth->fetchrow_arrayref->[0];
542 Returns the customer (see L<FS::cust_main>) for this invoice.
548 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
551 =item cust_suspend_if_balance_over AMOUNT
553 Suspends the customer associated with this invoice if the total amount owed on
554 this invoice and all older invoices is greater than the specified amount.
556 Returns a list: an empty list on success or a list of errors.
560 sub cust_suspend_if_balance_over {
561 my( $self, $amount ) = ( shift, shift );
562 my $cust_main = $self->cust_main;
563 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
566 $cust_main->suspend(@_);
572 Depreciated. See the cust_credited method.
574 #Returns a list consisting of the total previous credited (see
575 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
576 #outstanding credits (FS::cust_credit objects).
582 croak "FS::cust_bill->cust_credit depreciated; see ".
583 "FS::cust_bill->cust_credit_bill";
586 #my @cust_credit = sort { $a->_date <=> $b->_date }
587 # grep { $_->credited != 0 && $_->_date < $self->_date }
588 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
590 #foreach (@cust_credit) { $total += $_->credited; }
591 #$total, @cust_credit;
596 Depreciated. See the cust_bill_pay method.
598 #Returns all payments (see L<FS::cust_pay>) for this invoice.
604 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
606 #sort { $a->_date <=> $b->_date }
607 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
613 qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
616 sub cust_bill_pay_batch {
618 qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
623 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
629 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
630 sort { $a->_date <=> $b->_date }
631 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
636 =item cust_credit_bill
638 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
644 map { $_ } #return $self->num_cust_credit_bill unless wantarray;
645 sort { $a->_date <=> $b->_date }
646 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
650 sub cust_credit_bill {
651 shift->cust_credited(@_);
654 #=item cust_bill_pay_pkgnum PKGNUM
656 #Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
657 #with matching pkgnum.
661 #sub cust_bill_pay_pkgnum {
662 # my( $self, $pkgnum ) = @_;
663 # map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
664 # sort { $a->_date <=> $b->_date }
665 # qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
666 # 'pkgnum' => $pkgnum,
671 =item cust_bill_pay_pkg PKGNUM
673 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
674 applied against the matching pkgnum.
678 sub cust_bill_pay_pkg {
679 my( $self, $pkgnum ) = @_;
682 'select' => 'cust_bill_pay_pkg.*',
683 'table' => 'cust_bill_pay_pkg',
684 'addl_from' => ' LEFT JOIN cust_bill_pay USING ( billpaynum ) '.
685 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
686 'hashref' => { 'invnum' => $self->invnum,
693 #=item cust_credited_pkgnum PKGNUM
695 #=item cust_credit_bill_pkgnum PKGNUM
697 #Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
698 #with matching pkgnum.
702 #sub cust_credited_pkgnum {
703 # my( $self, $pkgnum ) = @_;
704 # map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
705 # sort { $a->_date <=> $b->_date }
706 # qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
707 # 'pkgnum' => $pkgnum,
712 #sub cust_credit_bill_pkgnum {
713 # shift->cust_credited_pkgnum(@_);
716 =item cust_credit_bill_pkg PKGNUM
718 Returns all credit applications (see L<FS::cust_credit_bill>) for this invoice
719 applied against the matching pkgnum.
723 sub cust_credit_bill_pkg {
724 my( $self, $pkgnum ) = @_;
727 'select' => 'cust_credit_bill_pkg.*',
728 'table' => 'cust_credit_bill_pkg',
729 'addl_from' => ' LEFT JOIN cust_credit_bill USING ( creditbillnum ) '.
730 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
731 'hashref' => { 'invnum' => $self->invnum,
740 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
747 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
749 foreach (@taxlines) { $total += $_->setup; }
755 Returns the amount owed (still outstanding) on this invoice, which is charged
756 minus all payment applications (see L<FS::cust_bill_pay>) and credit
757 applications (see L<FS::cust_credit_bill>).
763 my $balance = $self->charged;
764 $balance -= $_->amount foreach ( $self->cust_bill_pay );
765 $balance -= $_->amount foreach ( $self->cust_credited );
766 $balance = sprintf( "%.2f", $balance);
767 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
772 my( $self, $pkgnum ) = @_;
774 #my $balance = $self->charged;
776 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
778 $balance -= $_->amount for $self->cust_bill_pay_pkg($pkgnum);
779 $balance -= $_->amount for $self->cust_credit_bill_pkg($pkgnum);
781 $balance = sprintf( "%.2f", $balance);
782 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
786 =item apply_payments_and_credits [ OPTION => VALUE ... ]
788 Applies unapplied payments and credits to this invoice.
790 A hash of optional arguments may be passed. Currently "manual" is supported.
791 If true, a payment receipt is sent instead of a statement when
792 'payment_receipt_email' configuration option is set.
794 If there is an error, returns the error, otherwise returns false.
798 sub apply_payments_and_credits {
799 my( $self, %options ) = @_;
801 local $SIG{HUP} = 'IGNORE';
802 local $SIG{INT} = 'IGNORE';
803 local $SIG{QUIT} = 'IGNORE';
804 local $SIG{TERM} = 'IGNORE';
805 local $SIG{TSTP} = 'IGNORE';
806 local $SIG{PIPE} = 'IGNORE';
808 my $oldAutoCommit = $FS::UID::AutoCommit;
809 local $FS::UID::AutoCommit = 0;
812 $self->select_for_update; #mutex
814 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
815 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
817 if ( $conf->exists('pkg-balances') ) {
818 # limit @payments & @credits to those w/ a pkgnum grepped from $self
819 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
820 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
821 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
824 while ( $self->owed > 0 and ( @payments || @credits ) ) {
827 if ( @payments && @credits ) {
829 #decide which goes first by weight of top (unapplied) line item
831 my @open_lineitems = $self->open_cust_bill_pkg;
834 max( map { $_->part_pkg->pay_weight || 0 }
839 my $max_credit_weight =
840 max( map { $_->part_pkg->credit_weight || 0 }
846 #if both are the same... payments first? it has to be something
847 if ( $max_pay_weight >= $max_credit_weight ) {
853 } elsif ( @payments ) {
855 } elsif ( @credits ) {
858 die "guru meditation #12 and 35";
862 if ( $app eq 'pay' ) {
864 my $payment = shift @payments;
865 $unapp_amount = $payment->unapplied;
866 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
867 $app->pkgnum( $payment->pkgnum )
868 if $conf->exists('pkg-balances') && $payment->pkgnum;
870 } elsif ( $app eq 'credit' ) {
872 my $credit = shift @credits;
873 $unapp_amount = $credit->credited;
874 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
875 $app->pkgnum( $credit->pkgnum )
876 if $conf->exists('pkg-balances') && $credit->pkgnum;
879 die "guru meditation #12 and 35";
883 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
884 warn "owed_pkgnum ". $app->pkgnum;
885 $owed = $self->owed_pkgnum($app->pkgnum);
889 next unless $owed > 0;
891 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
892 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
894 $app->invnum( $self->invnum );
896 my $error = $app->insert(%options);
898 $dbh->rollback if $oldAutoCommit;
899 return "Error inserting ". $app->table. " record: $error";
901 die $error if $error;
905 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
910 =item generate_email OPTION => VALUE ...
918 sender address, required
922 alternate template name, optional
926 text attachment arrayref, optional
930 email subject, optional
934 notice name instead of "Invoice", optional
938 Returns an argument list to be passed to L<FS::Misc::send_email>.
949 my $me = '[FS::cust_bill::generate_email]';
952 'from' => $args{'from'},
953 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
957 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
958 'template' => $args{'template'},
959 'notice_name' => ( $args{'notice_name'} || 'Invoice' ),
960 'no_coupon' => $args{'no_coupon'},
963 my $cust_main = $self->cust_main;
965 if (ref($args{'to'}) eq 'ARRAY') {
966 $return{'to'} = $args{'to'};
968 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
969 $cust_main->invoicing_list
973 if ( $conf->exists('invoice_html') ) {
975 warn "$me creating HTML/text multipart message"
978 $return{'nobody'} = 1;
980 my $alternative = build MIME::Entity
981 'Type' => 'multipart/alternative',
982 'Encoding' => '7bit',
983 'Disposition' => 'inline'
987 if ( $conf->exists('invoice_email_pdf')
988 and scalar($conf->config('invoice_email_pdf_note')) ) {
990 warn "$me using 'invoice_email_pdf_note' in multipart message"
992 $data = [ map { $_ . "\n" }
993 $conf->config('invoice_email_pdf_note')
998 warn "$me not using 'invoice_email_pdf_note' in multipart message"
1000 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1001 $data = $args{'print_text'};
1003 $data = [ $self->print_text(\%opt) ];
1008 $alternative->attach(
1009 'Type' => 'text/plain',
1010 #'Encoding' => 'quoted-printable',
1011 'Encoding' => '7bit',
1013 'Disposition' => 'inline',
1016 $args{'from'} =~ /\@([\w\.\-]+)/;
1017 my $from = $1 || 'example.com';
1018 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1021 my $agentnum = $cust_main->agentnum;
1022 if ( defined($args{'template'}) && length($args{'template'})
1023 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
1026 $logo = 'logo_'. $args{'template'}. '.png';
1030 my $image_data = $conf->config_binary( $logo, $agentnum);
1032 my $image = build MIME::Entity
1033 'Type' => 'image/png',
1034 'Encoding' => 'base64',
1035 'Data' => $image_data,
1036 'Filename' => 'logo.png',
1037 'Content-ID' => "<$content_id>",
1041 if($conf->exists('invoice-barcode')){
1042 my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1043 $barcode = build MIME::Entity
1044 'Type' => 'image/png',
1045 'Encoding' => 'base64',
1046 'Data' => $self->invoice_barcode(0),
1047 'Filename' => 'barcode.png',
1048 'Content-ID' => "<$barcode_content_id>",
1050 $opt{'barcode_cid'} = $barcode_content_id;
1053 $alternative->attach(
1054 'Type' => 'text/html',
1055 'Encoding' => 'quoted-printable',
1056 'Data' => [ '<html>',
1059 ' '. encode_entities($return{'subject'}),
1062 ' <body bgcolor="#e8e8e8">',
1063 $self->print_html({ 'cid'=>$content_id, %opt }),
1067 'Disposition' => 'inline',
1068 #'Filename' => 'invoice.pdf',
1071 my @otherparts = ();
1072 if ( $cust_main->email_csv_cdr ) {
1074 push @otherparts, build MIME::Entity
1075 'Type' => 'text/csv',
1076 'Encoding' => '7bit',
1077 'Data' => [ map { "$_\n" }
1078 $self->call_details('prepend_billed_number' => 1)
1080 'Disposition' => 'attachment',
1081 'Filename' => 'usage-'. $self->invnum. '.csv',
1086 if ( $conf->exists('invoice_email_pdf') ) {
1091 # multipart/alternative
1097 my $related = build MIME::Entity 'Type' => 'multipart/related',
1098 'Encoding' => '7bit';
1100 #false laziness w/Misc::send_email
1101 $related->head->replace('Content-type',
1102 $related->mime_type.
1103 '; boundary="'. $related->head->multipart_boundary. '"'.
1104 '; type=multipart/alternative'
1107 $related->add_part($alternative);
1109 $related->add_part($image);
1111 my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
1113 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
1117 #no other attachment:
1119 # multipart/alternative
1124 $return{'content-type'} = 'multipart/related';
1125 if($conf->exists('invoice-barcode')){
1126 $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ];
1129 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
1131 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
1132 #$return{'disposition'} = 'inline';
1138 if ( $conf->exists('invoice_email_pdf') ) {
1139 warn "$me creating PDF attachment"
1142 #mime parts arguments a la MIME::Entity->build().
1143 $return{'mimeparts'} = [
1144 { $self->mimebuild_pdf(\%opt) }
1148 if ( $conf->exists('invoice_email_pdf')
1149 and scalar($conf->config('invoice_email_pdf_note')) ) {
1151 warn "$me using 'invoice_email_pdf_note'"
1153 $return{'body'} = [ map { $_ . "\n" }
1154 $conf->config('invoice_email_pdf_note')
1159 warn "$me not using 'invoice_email_pdf_note'"
1161 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1162 $return{'body'} = $args{'print_text'};
1164 $return{'body'} = [ $self->print_text(\%opt) ];
1177 Returns a list suitable for passing to MIME::Entity->build(), representing
1178 this invoice as PDF attachment.
1185 'Type' => 'application/pdf',
1186 'Encoding' => 'base64',
1187 'Data' => [ $self->print_pdf(@_) ],
1188 'Disposition' => 'attachment',
1189 'Filename' => 'invoice-'. $self->invnum. '.pdf',
1193 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1195 Sends this invoice to the destinations configured for this customer: sends
1196 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1198 Options can be passed as a hashref (recommended) or as a list of up to
1199 four values for templatename, agentnum, invoice_from and amount.
1201 I<template>, if specified, is the name of a suffix for alternate invoices.
1203 I<agentnum>, if specified, means that this invoice will only be sent for customers
1204 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
1205 single agent) or an arrayref of agentnums.
1207 I<invoice_from>, if specified, overrides the default email invoice From: address.
1209 I<amount>, if specified, only sends the invoice if the total amount owed on this
1210 invoice and all older invoices is greater than the specified amount.
1212 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1216 sub queueable_send {
1219 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1220 or die "invalid invoice number: " . $opt{invnum};
1222 my @args = ( $opt{template}, $opt{agentnum} );
1223 push @args, $opt{invoice_from}
1224 if exists($opt{invoice_from}) && $opt{invoice_from};
1226 my $error = $self->send( @args );
1227 die $error if $error;
1234 my( $template, $invoice_from, $notice_name );
1236 my $balance_over = 0;
1240 $template = $opt->{'template'} || '';
1241 if ( $agentnums = $opt->{'agentnum'} ) {
1242 $agentnums = [ $agentnums ] unless ref($agentnums);
1244 $invoice_from = $opt->{'invoice_from'};
1245 $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1246 $notice_name = $opt->{'notice_name'};
1248 $template = scalar(@_) ? shift : '';
1249 if ( scalar(@_) && $_[0] ) {
1250 $agentnums = ref($_[0]) ? shift : [ shift ];
1252 $invoice_from = shift if scalar(@_);
1253 $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1256 return 'N/A' unless ! $agentnums
1257 or grep { $_ == $self->cust_main->agentnum } @$agentnums;
1260 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
1262 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1263 $conf->config('invoice_from', $self->cust_main->agentnum );
1266 'template' => $template,
1267 'invoice_from' => $invoice_from,
1268 'notice_name' => ( $notice_name || 'Invoice' ),
1271 my @invoicing_list = $self->cust_main->invoicing_list;
1273 #$self->email_invoice(\%opt)
1275 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
1277 #$self->print_invoice(\%opt)
1279 if grep { $_ eq 'POST' } @invoicing_list; #postal
1281 $self->fax_invoice(\%opt)
1282 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1288 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
1290 Emails this invoice.
1292 Options can be passed as a hashref (recommended) or as a list of up to
1293 two values for templatename and invoice_from.
1295 I<template>, if specified, is the name of a suffix for alternate invoices.
1297 I<invoice_from>, if specified, overrides the default email invoice From: address.
1299 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1303 sub queueable_email {
1306 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1307 or die "invalid invoice number: " . $opt{invnum};
1309 my %args = ( 'template' => $opt{template} );
1310 $args{$_} = $opt{$_}
1311 foreach grep { exists($opt{$_}) && $opt{$_} }
1312 qw( invoice_from notice_name no_coupon );
1314 my $error = $self->email( \%args );
1315 die $error if $error;
1319 #sub email_invoice {
1323 my( $template, $invoice_from, $notice_name, $no_coupon );
1326 $template = $opt->{'template'} || '';
1327 $invoice_from = $opt->{'invoice_from'};
1328 $notice_name = $opt->{'notice_name'} || 'Invoice';
1329 $no_coupon = $opt->{'no_coupon'} || 0;
1331 $template = scalar(@_) ? shift : '';
1332 $invoice_from = shift if scalar(@_);
1333 $notice_name = 'Invoice';
1337 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1338 $conf->config('invoice_from', $self->cust_main->agentnum );
1340 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1341 $self->cust_main->invoicing_list;
1343 if ( ! @invoicing_list ) { #no recipients
1344 if ( $conf->exists('cust_bill-no_recipients-error') ) {
1345 die 'No recipients for customer #'. $self->custnum;
1347 #default: better to notify this person than silence
1348 @invoicing_list = ($invoice_from);
1352 my $subject = $self->email_subject($template);
1354 my $error = send_email(
1355 $self->generate_email(
1356 'from' => $invoice_from,
1357 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1358 'subject' => $subject,
1359 'template' => $template,
1360 'notice_name' => $notice_name,
1361 'no_coupon' => $no_coupon,
1364 die "can't email invoice: $error\n" if $error;
1365 #die "$error\n" if $error;
1372 #my $template = scalar(@_) ? shift : '';
1375 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1378 my $cust_main = $self->cust_main;
1379 my $name = $cust_main->name;
1380 my $name_short = $cust_main->name_short;
1381 my $invoice_number = $self->invnum;
1382 my $invoice_date = $self->_date_pretty;
1384 eval qq("$subject");
1387 =item lpr_data HASHREF | [ TEMPLATE ]
1389 Returns the postscript or plaintext for this invoice as an arrayref.
1391 Options can be passed as a hashref (recommended) or as a single optional value
1394 I<template>, if specified, is the name of a suffix for alternate invoices.
1396 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1402 my( $template, $notice_name );
1405 $template = $opt->{'template'} || '';
1406 $notice_name = $opt->{'notice_name'} || 'Invoice';
1408 $template = scalar(@_) ? shift : '';
1409 $notice_name = 'Invoice';
1413 'template' => $template,
1414 'notice_name' => $notice_name,
1417 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1418 [ $self->$method( \%opt ) ];
1421 =item print HASHREF | [ TEMPLATE ]
1423 Prints this invoice.
1425 Options can be passed as a hashref (recommended) or as a single optional
1428 I<template>, if specified, is the name of a suffix for alternate invoices.
1430 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1434 #sub print_invoice {
1437 my( $template, $notice_name );
1440 $template = $opt->{'template'} || '';
1441 $notice_name = $opt->{'notice_name'} || 'Invoice';
1443 $template = scalar(@_) ? shift : '';
1444 $notice_name = 'Invoice';
1448 'template' => $template,
1449 'notice_name' => $notice_name,
1452 if($conf->exists('invoice_print_pdf')) {
1453 # Add the invoice to the current batch.
1454 $self->batch_invoice(\%opt);
1457 do_print $self->lpr_data(\%opt);
1461 =item fax_invoice HASHREF | [ TEMPLATE ]
1465 Options can be passed as a hashref (recommended) or as a single optional
1468 I<template>, if specified, is the name of a suffix for alternate invoices.
1470 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1476 my( $template, $notice_name );
1479 $template = $opt->{'template'} || '';
1480 $notice_name = $opt->{'notice_name'} || 'Invoice';
1482 $template = scalar(@_) ? shift : '';
1483 $notice_name = 'Invoice';
1486 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1487 unless $conf->exists('invoice_latex');
1489 my $dialstring = $self->cust_main->getfield('fax');
1493 'template' => $template,
1494 'notice_name' => $notice_name,
1497 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1498 'dialstring' => $dialstring,
1500 die $error if $error;
1504 =item batch_invoice [ HASHREF ]
1506 Place this invoice into the open batch (see C<FS::bill_batch>). If there
1507 isn't an open batch, one will be created.
1512 my ($self, $opt) = @_;
1513 my $batch = FS::bill_batch->get_open_batch;
1514 my $cust_bill_batch = FS::cust_bill_batch->new({
1515 batchnum => $batch->batchnum,
1516 invnum => $self->invnum,
1518 return $cust_bill_batch->insert($opt);
1521 =item ftp_invoice [ TEMPLATENAME ]
1523 Sends this invoice data via FTP.
1525 TEMPLATENAME is unused?
1531 my $template = scalar(@_) ? shift : '';
1534 'protocol' => 'ftp',
1535 'server' => $conf->config('cust_bill-ftpserver'),
1536 'username' => $conf->config('cust_bill-ftpusername'),
1537 'password' => $conf->config('cust_bill-ftppassword'),
1538 'dir' => $conf->config('cust_bill-ftpdir'),
1539 'format' => $conf->config('cust_bill-ftpformat'),
1543 =item spool_invoice [ TEMPLATENAME ]
1545 Spools this invoice data (see L<FS::spool_csv>)
1547 TEMPLATENAME is unused?
1553 my $template = scalar(@_) ? shift : '';
1556 'format' => $conf->config('cust_bill-spoolformat'),
1557 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1561 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1563 Like B<send>, but only sends the invoice if it is the newest open invoice for
1568 sub send_if_newest {
1573 grep { $_->owed > 0 }
1574 qsearch('cust_bill', {
1575 'custnum' => $self->custnum,
1576 #'_date' => { op=>'>', value=>$self->_date },
1577 'invnum' => { op=>'>', value=>$self->invnum },
1584 =item send_csv OPTION => VALUE, ...
1586 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1590 protocol - currently only "ftp"
1596 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1597 and YYMMDDHHMMSS is a timestamp.
1599 See L</print_csv> for a description of the output format.
1604 my($self, %opt) = @_;
1608 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1609 mkdir $spooldir, 0700 unless -d $spooldir;
1611 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1612 my $file = "$spooldir/$tracctnum.csv";
1614 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1616 open(CSV, ">$file") or die "can't open $file: $!";
1624 if ( $opt{protocol} eq 'ftp' ) {
1625 eval "use Net::FTP;";
1627 $net = Net::FTP->new($opt{server}) or die @$;
1629 die "unknown protocol: $opt{protocol}";
1632 $net->login( $opt{username}, $opt{password} )
1633 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1635 $net->binary or die "can't set binary mode";
1637 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1639 $net->put($file) or die "can't put $file: $!";
1649 Spools CSV invoice data.
1655 =item format - 'default' or 'billco'
1657 =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>).
1659 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1661 =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.
1668 my($self, %opt) = @_;
1670 my $cust_main = $self->cust_main;
1672 if ( $opt{'dest'} ) {
1673 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1674 $cust_main->invoicing_list;
1675 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1676 || ! keys %invoicing_list;
1679 if ( $opt{'balanceover'} ) {
1681 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1684 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1685 mkdir $spooldir, 0700 unless -d $spooldir;
1687 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1691 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1692 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1695 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1697 open(CSV, ">>$file") or die "can't open $file: $!";
1698 flock(CSV, LOCK_EX);
1703 if ( lc($opt{'format'}) eq 'billco' ) {
1705 flock(CSV, LOCK_UN);
1710 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1713 open(CSV,">>$file") or die "can't open $file: $!";
1714 flock(CSV, LOCK_EX);
1720 flock(CSV, LOCK_UN);
1727 =item print_csv OPTION => VALUE, ...
1729 Returns CSV data for this invoice.
1733 format - 'default' or 'billco'
1735 Returns a list consisting of two scalars. The first is a single line of CSV
1736 header information for this invoice. The second is one or more lines of CSV
1737 detail information for this invoice.
1739 If I<format> is not specified or "default", the fields of the CSV file are as
1742 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1746 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1748 B<record_type> is C<cust_bill> for the initial header line only. The
1749 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1750 fields are filled in.
1752 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1753 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1756 =item invnum - invoice number
1758 =item custnum - customer number
1760 =item _date - invoice date
1762 =item charged - total invoice amount
1764 =item first - customer first name
1766 =item last - customer first name
1768 =item company - company name
1770 =item address1 - address line 1
1772 =item address2 - address line 1
1782 =item pkg - line item description
1784 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1786 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1788 =item sdate - start date for recurring fee
1790 =item edate - end date for recurring fee
1794 If I<format> is "billco", the fields of the header CSV file are as follows:
1796 +-------------------------------------------------------------------+
1797 | FORMAT HEADER FILE |
1798 |-------------------------------------------------------------------|
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 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1803 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1804 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1805 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1806 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1807 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1808 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1809 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1810 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1811 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1812 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1813 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1814 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1815 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1816 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1817 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1818 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1819 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1820 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1821 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1822 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1823 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1824 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1825 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1826 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1827 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1828 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1829 +-------+-------------------------------+------------+------+-------+
1831 If I<format> is "billco", the fields of the detail CSV file are as follows:
1833 FORMAT FOR DETAIL FILE
1835 Field | Description | Name | Type | Width
1836 1 | N/A-Leave Empty | RC | CHAR | 2
1837 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1838 3 | Account Number | TRACCTNUM | CHAR | 15
1839 4 | Invoice Number | TRINVOICE | CHAR | 15
1840 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1841 6 | Transaction Detail | DETAILS | CHAR | 100
1842 7 | Amount | AMT | NUM* | 9
1843 8 | Line Format Control** | LNCTRL | CHAR | 2
1844 9 | Grouping Code | GROUP | CHAR | 2
1845 10 | User Defined | ACCT CODE | CHAR | 15
1850 my($self, %opt) = @_;
1852 eval "use Text::CSV_XS";
1855 my $cust_main = $self->cust_main;
1857 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1859 if ( lc($opt{'format'}) eq 'billco' ) {
1862 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1864 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1866 my( $previous_balance, @unused ) = $self->previous; #previous balance
1868 my $pmt_cr_applied = 0;
1869 $pmt_cr_applied += $_->{'amount'}
1870 foreach ( $self->_items_payments, $self->_items_credits ) ;
1872 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1875 '', # 1 | N/A-Leave Empty CHAR 2
1876 '', # 2 | N/A-Leave Empty CHAR 15
1877 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1878 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1879 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1880 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1881 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1882 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1883 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1884 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1885 '', # 10 | Ancillary Billing Information CHAR 30
1886 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1887 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1890 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1893 $duedate, # 14 | Bill Due Date CHAR 10
1895 $previous_balance, # 15 | Previous Balance NUM* 9
1896 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1897 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1898 $totaldue, # 18 | Total Amt Due NUM* 9
1899 $totaldue, # 19 | Total Amt Due NUM* 9
1900 '', # 20 | 30 Day Aging NUM* 9
1901 '', # 21 | 60 Day Aging NUM* 9
1902 '', # 22 | 90 Day Aging NUM* 9
1903 'N', # 23 | Y/N CHAR 1
1904 '', # 24 | Remittance automation CHAR 100
1905 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1906 $self->custnum, # 26 | Customer Reference Number CHAR 15
1907 '0', # 27 | Federal Tax*** NUM* 9
1908 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1909 '0', # 29 | Other Taxes & Fees*** NUM* 9
1918 time2str("%x", $self->_date),
1919 sprintf("%.2f", $self->charged),
1920 ( map { $cust_main->getfield($_) }
1921 qw( first last company address1 address2 city state zip country ) ),
1923 ) or die "can't create csv";
1926 my $header = $csv->string. "\n";
1929 if ( lc($opt{'format'}) eq 'billco' ) {
1932 foreach my $item ( $self->_items_pkg ) {
1935 '', # 1 | N/A-Leave Empty CHAR 2
1936 '', # 2 | N/A-Leave Empty CHAR 15
1937 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1938 $self->invnum, # 4 | Invoice Number CHAR 15
1939 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1940 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1941 $item->{'amount'}, # 7 | Amount NUM* 9
1942 '', # 8 | Line Format Control** CHAR 2
1943 '', # 9 | Grouping Code CHAR 2
1944 '', # 10 | User Defined CHAR 15
1947 $detail .= $csv->string. "\n";
1953 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1955 my($pkg, $setup, $recur, $sdate, $edate);
1956 if ( $cust_bill_pkg->pkgnum ) {
1958 ($pkg, $setup, $recur, $sdate, $edate) = (
1959 $cust_bill_pkg->part_pkg->pkg,
1960 ( $cust_bill_pkg->setup != 0
1961 ? sprintf("%.2f", $cust_bill_pkg->setup )
1963 ( $cust_bill_pkg->recur != 0
1964 ? sprintf("%.2f", $cust_bill_pkg->recur )
1966 ( $cust_bill_pkg->sdate
1967 ? time2str("%x", $cust_bill_pkg->sdate)
1969 ($cust_bill_pkg->edate
1970 ?time2str("%x", $cust_bill_pkg->edate)
1974 } else { #pkgnum tax
1975 next unless $cust_bill_pkg->setup != 0;
1976 $pkg = $cust_bill_pkg->desc;
1977 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
1978 ( $sdate, $edate ) = ( '', '' );
1984 ( map { '' } (1..11) ),
1985 ($pkg, $setup, $recur, $sdate, $edate)
1986 ) or die "can't create csv";
1988 $detail .= $csv->string. "\n";
1994 ( $header, $detail );
2000 Pays this invoice with a compliemntary payment. If there is an error,
2001 returns the error, otherwise returns false.
2007 my $cust_pay = new FS::cust_pay ( {
2008 'invnum' => $self->invnum,
2009 'paid' => $self->owed,
2012 'payinfo' => $self->cust_main->payinfo,
2020 Attempts to pay this invoice with a credit card payment via a
2021 Business::OnlinePayment realtime gateway. See
2022 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2023 for supported processors.
2029 $self->realtime_bop( 'CC', @_ );
2034 Attempts to pay this invoice with an electronic check (ACH) payment via a
2035 Business::OnlinePayment realtime gateway. See
2036 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2037 for supported processors.
2043 $self->realtime_bop( 'ECHECK', @_ );
2048 Attempts to pay this invoice with phone bill (LEC) payment via a
2049 Business::OnlinePayment realtime gateway. See
2050 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2051 for supported processors.
2057 $self->realtime_bop( 'LEC', @_ );
2061 my( $self, $method ) = (shift,shift);
2064 my $cust_main = $self->cust_main;
2065 my $balance = $cust_main->balance;
2066 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2067 $amount = sprintf("%.2f", $amount);
2068 return "not run (balance $balance)" unless $amount > 0;
2070 my $description = 'Internet Services';
2071 if ( $conf->exists('business-onlinepayment-description') ) {
2072 my $dtempl = $conf->config('business-onlinepayment-description');
2074 my $agent_obj = $cust_main->agent
2075 or die "can't retreive agent for $cust_main (agentnum ".
2076 $cust_main->agentnum. ")";
2077 my $agent = $agent_obj->agent;
2078 my $pkgs = join(', ',
2079 map { $_->part_pkg->pkg }
2080 grep { $_->pkgnum } $self->cust_bill_pkg
2082 $description = eval qq("$dtempl");
2085 $cust_main->realtime_bop($method, $amount,
2086 'description' => $description,
2087 'invnum' => $self->invnum,
2088 #this didn't do what we want, it just calls apply_payments_and_credits
2090 'apply_to_invoice' => 1,
2093 #this changes application behavior: auto payments
2094 #triggered against a specific invoice are now applied
2095 #to that invoice instead of oldest open.
2101 =item batch_card OPTION => VALUE...
2103 Adds a payment for this invoice to the pending credit card batch (see
2104 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2105 runs the payment using a realtime gateway.
2110 my ($self, %options) = @_;
2111 my $cust_main = $self->cust_main;
2113 $options{invnum} = $self->invnum;
2115 $cust_main->batch_card(%options);
2118 sub _agent_template {
2120 $self->cust_main->agent_template;
2123 sub _agent_invoice_from {
2125 $self->cust_main->agent_invoice_from;
2128 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2130 Returns an text invoice, as a list of lines.
2132 Options can be passed as a hashref (recommended) or as a list of time, template
2133 and then any key/value pairs for any other options.
2135 I<time>, if specified, is used to control the printing of overdue messages. The
2136 default is now. It isn't the date of the invoice; that's the `_date' field.
2137 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2138 L<Time::Local> and L<Date::Parse> for conversion functions.
2140 I<template>, if specified, is the name of a suffix for alternate invoices.
2142 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2148 my( $today, $template, %opt );
2150 %opt = %{ shift() };
2151 $today = delete($opt{'time'}) || '';
2152 $template = delete($opt{template}) || '';
2154 ( $today, $template, %opt ) = @_;
2157 my %params = ( 'format' => 'template' );
2158 $params{'time'} = $today if $today;
2159 $params{'template'} = $template if $template;
2160 $params{$_} = $opt{$_}
2161 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2163 $self->print_generic( %params );
2166 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2168 Internal method - returns a filename of a filled-in LaTeX template for this
2169 invoice (Note: add ".tex" to get the actual filename), and a filename of
2170 an associated logo (with the .eps extension included).
2172 See print_ps and print_pdf for methods that return PostScript and PDF output.
2174 Options can be passed as a hashref (recommended) or as a list of time, template
2175 and then any key/value pairs for any other options.
2177 I<time>, if specified, is used to control the printing of overdue messages. The
2178 default is now. It isn't the date of the invoice; that's the `_date' field.
2179 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2180 L<Time::Local> and L<Date::Parse> for conversion functions.
2182 I<template>, if specified, is the name of a suffix for alternate invoices.
2184 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2190 my( $today, $template, %opt );
2192 %opt = %{ shift() };
2193 $today = delete($opt{'time'}) || '';
2194 $template = delete($opt{template}) || '';
2196 ( $today, $template, %opt ) = @_;
2199 my %params = ( 'format' => 'latex' );
2200 $params{'time'} = $today if $today;
2201 $params{'template'} = $template if $template;
2202 $params{$_} = $opt{$_}
2203 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2205 $template ||= $self->_agent_template;
2207 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2208 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2212 ) or die "can't open temp file: $!\n";
2214 my $agentnum = $self->cust_main->agentnum;
2216 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2217 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2218 or die "can't write temp file: $!\n";
2220 print $lh $conf->config_binary('logo.eps', $agentnum)
2221 or die "can't write temp file: $!\n";
2224 $params{'logo_file'} = $lh->filename;
2226 if($conf->exists('invoice-barcode')){
2227 my $png_file = $self->invoice_barcode($dir);
2228 my $eps_file = $png_file;
2229 $eps_file =~ s/\.png$/.eps/g;
2230 $png_file =~ /(barcode.*png)/;
2232 $eps_file =~ /(barcode.*eps)/;
2235 my $curr_dir = cwd();
2237 # after painfuly long experimentation, it was determined that sam2p won't
2238 # accept : and other chars in the path, no matter how hard I tried to
2239 # escape them, hence the chdir (and chdir back, just to be safe)
2240 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
2241 or die "sam2p failed: $!\n";
2245 $params{'barcode_file'} = $eps_file;
2248 my @filled_in = $self->print_generic( %params );
2250 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2254 ) or die "can't open temp file: $!\n";
2255 print $fh join('', @filled_in );
2258 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2259 return ($1, $params{'logo_file'}, $params{'barcode_file'});
2263 =item invoice_barcode DIR_OR_FALSE
2265 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2266 it is taken as the temp directory where the PNG file will be generated and the
2267 PNG file name is returned. Otherwise, the PNG image itself is returned.
2271 sub invoice_barcode {
2272 my ($self, $dir) = (shift,shift);
2274 my $gdbar = new GD::Barcode('Code39',$self->invnum);
2275 die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2276 my $gd = $gdbar->plot(Height => 30);
2279 my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2283 ) or die "can't open temp file: $!\n";
2284 print $bh $gd->png or die "cannot write barcode to file: $!\n";
2285 my $png_file = $bh->filename;
2292 =item print_generic OPTION => VALUE ...
2294 Internal method - returns a filled-in template for this invoice as a scalar.
2296 See print_ps and print_pdf for methods that return PostScript and PDF output.
2298 Non optional options include
2299 format - latex, html, template
2301 Optional options include
2303 template - a value used as a suffix for a configuration template
2305 time - a value used to control the printing of overdue messages. The
2306 default is now. It isn't the date of the invoice; that's the `_date' field.
2307 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2308 L<Time::Local> and L<Date::Parse> for conversion functions.
2312 unsquelch_cdr - overrides any per customer cdr squelching when true
2314 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2318 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2319 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2320 # yes: fixed width (dot matrix) text printing will be borked
2323 my( $self, %params ) = @_;
2324 my $today = $params{today} ? $params{today} : time;
2325 warn "$me print_generic called on $self with suffix $params{template}\n"
2328 my $format = $params{format};
2329 die "Unknown format: $format"
2330 unless $format =~ /^(latex|html|template)$/;
2332 my $cust_main = $self->cust_main;
2333 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2334 unless $cust_main->payname
2335 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2337 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2338 'html' => [ '<%=', '%>' ],
2339 'template' => [ '{', '}' ],
2342 warn "$me print_generic creating template\n"
2345 #create the template
2346 my $template = $params{template} ? $params{template} : $self->_agent_template;
2347 my $templatefile = "invoice_$format";
2348 $templatefile .= "_$template"
2349 if length($template) && $conf->exists($templatefile."_$template");
2350 my @invoice_template = map "$_\n", $conf->config($templatefile)
2351 or die "cannot load config data $templatefile";
2354 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2355 #change this to a die when the old code is removed
2356 warn "old-style invoice template $templatefile; ".
2357 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2358 $old_latex = 'true';
2359 @invoice_template = _translate_old_latex_format(@invoice_template);
2362 warn "$me print_generic creating T:T object\n"
2365 my $text_template = new Text::Template(
2367 SOURCE => \@invoice_template,
2368 DELIMITERS => $delimiters{$format},
2371 warn "$me print_generic compiling T:T object\n"
2374 $text_template->compile()
2375 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2378 # additional substitution could possibly cause breakage in existing templates
2379 my %convert_maps = (
2381 'notes' => sub { map "$_", @_ },
2382 'footer' => sub { map "$_", @_ },
2383 'smallfooter' => sub { map "$_", @_ },
2384 'returnaddress' => sub { map "$_", @_ },
2385 'coupon' => sub { map "$_", @_ },
2386 'summary' => sub { map "$_", @_ },
2392 s/%%(.*)$/<!-- $1 -->/g;
2393 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2394 s/\\begin\{enumerate\}/<ol>/g;
2396 s/\\end\{enumerate\}/<\/ol>/g;
2397 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2406 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2408 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2413 s/\\\\\*?\s*$/<BR>/;
2414 s/\\hyphenation\{[\w\s\-]+}//;
2419 'coupon' => sub { "" },
2420 'summary' => sub { "" },
2427 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2428 s/\\begin\{enumerate\}//g;
2430 s/\\end\{enumerate\}//g;
2431 s/\\textbf\{(.*)\}/$1/g;
2438 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2440 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2445 s/\\\\\*?\s*$/\n/; # dubious
2446 s/\\hyphenation\{[\w\s\-]+}//;
2450 'coupon' => sub { "" },
2451 'summary' => sub { "" },
2456 # hashes for differing output formats
2457 my %nbsps = ( 'latex' => '~',
2458 'html' => '', # '&nbps;' would be nice
2459 'template' => '', # not used
2461 my $nbsp = $nbsps{$format};
2463 my %escape_functions = ( 'latex' => \&_latex_escape,
2464 'html' => \&_html_escape_nbsp,#\&encode_entities,
2465 'template' => sub { shift },
2467 my $escape_function = $escape_functions{$format};
2468 my $escape_function_nonbsp = ($format eq 'html')
2469 ? \&_html_escape : $escape_function;
2471 my %date_formats = ( 'latex' => $date_format_long,
2472 'html' => $date_format_long,
2475 $date_formats{'html'} =~ s/ / /g;
2477 my $date_format = $date_formats{$format};
2479 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2481 'html' => sub { return '<b>'. shift(). '</b>'
2483 'template' => sub { shift },
2485 my $embolden_function = $embolden_functions{$format};
2487 my %newline_tokens = ( 'latex' => '\\\\',
2491 my $newline_token = $newline_tokens{$format};
2493 warn "$me generating template variables\n"
2496 # generate template variables
2499 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2503 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2509 $returnaddress = join("\n",
2510 $conf->config_orbase("invoice_${format}returnaddress", $template)
2513 } elsif ( grep /\S/,
2514 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2516 my $convert_map = $convert_maps{$format}{'returnaddress'};
2519 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2524 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2526 my $convert_map = $convert_maps{$format}{'returnaddress'};
2527 $returnaddress = join( "\n", &$convert_map(
2528 map { s/( {2,})/'~' x length($1)/eg;
2532 ( $conf->config('company_name', $self->cust_main->agentnum),
2533 $conf->config('company_address', $self->cust_main->agentnum),
2540 my $warning = "Couldn't find a return address; ".
2541 "do you need to set the company_address configuration value?";
2543 $returnaddress = $nbsp;
2544 #$returnaddress = $warning;
2548 warn "$me generating invoice data\n"
2551 my $agentnum = $self->cust_main->agentnum;
2553 my %invoice_data = (
2556 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2557 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2558 'returnaddress' => $returnaddress,
2559 'agent' => &$escape_function($cust_main->agent->agent),
2562 'invnum' => $self->invnum,
2563 'date' => time2str($date_format, $self->_date),
2564 'today' => time2str($date_format_long, $today),
2565 'terms' => $self->terms,
2566 'template' => $template, #params{'template'},
2567 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2568 'current_charges' => sprintf("%.2f", $self->charged),
2569 'duedate' => $self->due_date2str($rdate_format), #date_format?
2572 'custnum' => $cust_main->display_custnum,
2573 'agent_custid' => &$escape_function($cust_main->agent_custid),
2574 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2575 payname company address1 address2 city state zip fax
2579 'ship_enable' => $conf->exists('invoice-ship_address'),
2580 'unitprices' => $conf->exists('invoice-unitprice'),
2581 'smallernotes' => $conf->exists('invoice-smallernotes'),
2582 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2583 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2585 #layout info -- would be fancy to calc some of this and bury the template
2587 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2588 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2589 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2590 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2591 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2592 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2593 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2594 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2595 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2596 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2598 # better hang on to conf_dir for a while (for old templates)
2599 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2601 #these are only used when doing paged plaintext
2607 my $min_sdate = 999999999999;
2609 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2610 next unless $cust_bill_pkg->pkgnum > 0;
2611 $min_sdate = $cust_bill_pkg->sdate
2612 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2613 $max_edate = $cust_bill_pkg->edate
2614 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2617 $invoice_data{'bill_period'} = '';
2618 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
2619 . " to " . time2str('%e %h', $max_edate)
2620 if ($max_edate != 0 && $min_sdate != 999999999999);
2622 $invoice_data{finance_section} = '';
2623 if ( $conf->config('finance_pkgclass') ) {
2625 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2626 $invoice_data{finance_section} = $pkg_class->categoryname;
2628 $invoice_data{finance_amount} = '0.00';
2629 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2631 my $countrydefault = $conf->config('countrydefault') || 'US';
2632 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2633 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2634 my $method = $prefix.$_;
2635 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2637 $invoice_data{'ship_country'} = ''
2638 if ( $invoice_data{'ship_country'} eq $countrydefault );
2640 $invoice_data{'cid'} = $params{'cid'}
2643 if ( $cust_main->country eq $countrydefault ) {
2644 $invoice_data{'country'} = '';
2646 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2650 $invoice_data{'address'} = \@address;
2652 $cust_main->payname.
2653 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2654 ? " (P.O. #". $cust_main->payinfo. ")"
2658 push @address, $cust_main->company
2659 if $cust_main->company;
2660 push @address, $cust_main->address1;
2661 push @address, $cust_main->address2
2662 if $cust_main->address2;
2664 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2665 push @address, $invoice_data{'country'}
2666 if $invoice_data{'country'};
2668 while (scalar(@address) < 5);
2670 $invoice_data{'logo_file'} = $params{'logo_file'}
2671 if $params{'logo_file'};
2672 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2673 if $params{'barcode_file'};
2674 $invoice_data{'barcode_img'} = $params{'barcode_img'}
2675 if $params{'barcode_img'};
2676 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2677 if $params{'barcode_cid'};
2679 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2680 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2681 #my $balance_due = $self->owed + $pr_total - $cr_total;
2682 my $balance_due = $self->owed + $pr_total;
2683 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2684 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2685 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2686 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2688 my $summarypage = '';
2689 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2692 $invoice_data{'summarypage'} = $summarypage;
2694 warn "$me substituting variables in notes, footer, smallfooter\n"
2697 my @include = (qw( notes footer smallfooter ));
2698 push @include, 'coupon' unless $params{'no_coupon'};
2699 foreach my $include (@include) {
2701 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2704 if ( $conf->exists($inc_file, $agentnum)
2705 && length( $conf->config($inc_file, $agentnum) ) ) {
2707 @inc_src = $conf->config($inc_file, $agentnum);
2711 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2713 my $convert_map = $convert_maps{$format}{$include};
2715 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2716 s/--\@\]/$delimiters{$format}[1]/g;
2719 &$convert_map( $conf->config($inc_file, $agentnum) );
2723 my $inc_tt = new Text::Template (
2725 SOURCE => [ map "$_\n", @inc_src ],
2726 DELIMITERS => $delimiters{$format},
2727 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2729 unless ( $inc_tt->compile() ) {
2730 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2731 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2735 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2737 $invoice_data{$include} =~ s/\n+$//
2738 if ($format eq 'latex');
2741 $invoice_data{'po_line'} =
2742 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2743 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2746 my %money_chars = ( 'latex' => '',
2747 'html' => $conf->config('money_char') || '$',
2750 my $money_char = $money_chars{$format};
2752 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2753 'html' => $conf->config('money_char') || '$',
2756 my $other_money_char = $other_money_chars{$format};
2757 $invoice_data{'dollar'} = $other_money_char;
2759 my @detail_items = ();
2760 my @total_items = ();
2764 $invoice_data{'detail_items'} = \@detail_items;
2765 $invoice_data{'total_items'} = \@total_items;
2766 $invoice_data{'buf'} = \@buf;
2767 $invoice_data{'sections'} = \@sections;
2769 warn "$me generating sections\n"
2772 my $previous_section = { 'description' => 'Previous Charges',
2773 'subtotal' => $other_money_char.
2774 sprintf('%.2f', $pr_total),
2775 'summarized' => $summarypage ? 'Y' : '',
2777 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
2778 join(' / ', map { $cust_main->balance_date_range(@$_) }
2779 $self->_prior_month30s
2781 if $conf->exists('invoice_include_aging');
2784 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2785 'subtotal' => $taxtotal, # adjusted below
2786 'summarized' => $summarypage ? 'Y' : '',
2788 my $tax_weight = _pkg_category($tax_section->{description})
2789 ? _pkg_category($tax_section->{description})->weight
2791 $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2792 $tax_section->{'sort_weight'} = $tax_weight;
2795 my $adjusttotal = 0;
2796 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2797 'subtotal' => 0, # adjusted below
2798 'summarized' => $summarypage ? 'Y' : '',
2800 my $adjust_weight = _pkg_category($adjust_section->{description})
2801 ? _pkg_category($adjust_section->{description})->weight
2803 $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2804 $adjust_section->{'sort_weight'} = $adjust_weight;
2806 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2807 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2808 $invoice_data{'multisection'} = $multisection;
2809 my $late_sections = [];
2810 my $extra_sections = [];
2811 my $extra_lines = ();
2812 if ( $multisection ) {
2813 ($extra_sections, $extra_lines) =
2814 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2815 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2817 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2819 push @detail_items, @$extra_lines if $extra_lines;
2821 $self->_items_sections( $late_sections, # this could stand a refactor
2823 $escape_function_nonbsp,
2827 if ($conf->exists('svc_phone_sections')) {
2828 my ($phone_sections, $phone_lines) =
2829 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2830 push @{$late_sections}, @$phone_sections;
2831 push @detail_items, @$phone_lines;
2834 push @sections, { 'description' => '', 'subtotal' => '' };
2837 unless ( $conf->exists('disable_previous_balance')
2838 || $conf->exists('previous_balance-summary_only')
2842 warn "$me adding previous balances\n"
2845 foreach my $line_item ( $self->_items_previous ) {
2848 ext_description => [],
2850 $detail->{'ref'} = $line_item->{'pkgnum'};
2851 $detail->{'quantity'} = 1;
2852 $detail->{'section'} = $previous_section;
2853 $detail->{'description'} = &$escape_function($line_item->{'description'});
2854 if ( exists $line_item->{'ext_description'} ) {
2855 @{$detail->{'ext_description'}} = map {
2856 &$escape_function($_);
2857 } @{$line_item->{'ext_description'}};
2859 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2860 $line_item->{'amount'};
2861 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2863 push @detail_items, $detail;
2864 push @buf, [ $detail->{'description'},
2865 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2871 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2872 push @buf, ['','-----------'];
2873 push @buf, [ 'Total Previous Balance',
2874 $money_char. sprintf("%10.2f", $pr_total) ];
2878 if ( $conf->exists('svc_phone-did-summary') ) {
2879 warn "$me adding DID summary\n"
2882 my ($didsummary,$minutes) = $self->_did_summary;
2883 my $didsummary_desc = 'DID Activity Summary (Past 30 days)';
2885 { 'description' => $didsummary_desc,
2886 'ext_description' => [ $didsummary, $minutes ],
2891 foreach my $section (@sections, @$late_sections) {
2893 warn "$me adding section \n". Dumper($section)
2896 # begin some normalization
2897 $section->{'subtotal'} = $section->{'amount'}
2899 && !exists($section->{subtotal})
2900 && exists($section->{amount});
2902 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2903 if ( $invoice_data{finance_section} &&
2904 $section->{'description'} eq $invoice_data{finance_section} );
2906 $section->{'subtotal'} = $other_money_char.
2907 sprintf('%.2f', $section->{'subtotal'})
2910 # continue some normalization
2911 $section->{'amount'} = $section->{'subtotal'}
2915 if ( $section->{'description'} ) {
2916 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2921 warn "$me setting options\n"
2924 my $multilocation = scalar($cust_main->cust_location); #too expensive?
2926 $options{'section'} = $section if $multisection;
2927 $options{'format'} = $format;
2928 $options{'escape_function'} = $escape_function;
2929 $options{'format_function'} = sub { () } unless $unsquelched;
2930 $options{'unsquelched'} = $unsquelched;
2931 $options{'summary_page'} = $summarypage;
2932 $options{'skip_usage'} =
2933 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2934 $options{'multilocation'} = $multilocation;
2935 $options{'multisection'} = $multisection;
2937 warn "$me searching for line items\n"
2940 foreach my $line_item ( $self->_items_pkg(%options) ) {
2942 warn "$me adding line item $line_item\n"
2946 ext_description => [],
2948 $detail->{'ref'} = $line_item->{'pkgnum'};
2949 $detail->{'quantity'} = $line_item->{'quantity'};
2950 $detail->{'section'} = $section;
2951 $detail->{'description'} = &$escape_function($line_item->{'description'});
2952 if ( exists $line_item->{'ext_description'} ) {
2953 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2955 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2956 $line_item->{'amount'};
2957 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2958 $line_item->{'unit_amount'};
2959 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2961 push @detail_items, $detail;
2962 push @buf, ( [ $detail->{'description'},
2963 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2965 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2969 if ( $section->{'description'} ) {
2970 push @buf, ( ['','-----------'],
2971 [ $section->{'description'}. ' sub-total',
2972 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2981 $invoice_data{current_less_finance} =
2982 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2984 if ( $multisection && !$conf->exists('disable_previous_balance')
2985 || $conf->exists('previous_balance-summary_only') )
2987 unshift @sections, $previous_section if $pr_total;
2990 warn "$me adding taxes\n"
2993 foreach my $tax ( $self->_items_tax ) {
2995 $taxtotal += $tax->{'amount'};
2997 my $description = &$escape_function( $tax->{'description'} );
2998 my $amount = sprintf( '%.2f', $tax->{'amount'} );
3000 if ( $multisection ) {
3002 my $money = $old_latex ? '' : $money_char;
3003 push @detail_items, {
3004 ext_description => [],
3007 description => $description,
3008 amount => $money. $amount,
3010 section => $tax_section,
3015 push @total_items, {
3016 'total_item' => $description,
3017 'total_amount' => $other_money_char. $amount,
3022 push @buf,[ $description,
3023 $money_char. $amount,
3030 $total->{'total_item'} = 'Sub-total';
3031 $total->{'total_amount'} =
3032 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
3034 if ( $multisection ) {
3035 $tax_section->{'subtotal'} = $other_money_char.
3036 sprintf('%.2f', $taxtotal);
3037 $tax_section->{'pretotal'} = 'New charges sub-total '.
3038 $total->{'total_amount'};
3039 push @sections, $tax_section if $taxtotal;
3041 unshift @total_items, $total;
3044 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3046 push @buf,['','-----------'];
3047 push @buf,[( $conf->exists('disable_previous_balance')
3049 : 'Total New Charges'
3051 $money_char. sprintf("%10.2f",$self->charged) ];
3057 $item = $conf->config('previous_balance-exclude_from_total')
3058 || 'Total New Charges'
3059 if $conf->exists('previous_balance-exclude_from_total');
3060 my $amount = $self->charged +
3061 ( $conf->exists('disable_previous_balance') ||
3062 $conf->exists('previous_balance-exclude_from_total')
3066 $total->{'total_item'} = &$embolden_function($item);
3067 $total->{'total_amount'} =
3068 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
3069 if ( $multisection ) {
3070 if ( $adjust_section->{'sort_weight'} ) {
3071 $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
3072 sprintf("%.2f", ($self->billing_balance || 0) );
3074 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
3075 sprintf('%.2f', $self->charged );
3078 push @total_items, $total;
3080 push @buf,['','-----------'];
3083 sprintf( '%10.2f', $amount )
3088 unless ( $conf->exists('disable_previous_balance') ) {
3089 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3092 my $credittotal = 0;
3093 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3096 $total->{'total_item'} = &$escape_function($credit->{'description'});
3097 $credittotal += $credit->{'amount'};
3098 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3099 $adjusttotal += $credit->{'amount'};
3100 if ( $multisection ) {
3101 my $money = $old_latex ? '' : $money_char;
3102 push @detail_items, {
3103 ext_description => [],
3106 description => &$escape_function($credit->{'description'}),
3107 amount => $money. $credit->{'amount'},
3109 section => $adjust_section,
3112 push @total_items, $total;
3116 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3119 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3120 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3124 my $paymenttotal = 0;
3125 foreach my $payment ( $self->_items_payments ) {
3127 $total->{'total_item'} = &$escape_function($payment->{'description'});
3128 $paymenttotal += $payment->{'amount'};
3129 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3130 $adjusttotal += $payment->{'amount'};
3131 if ( $multisection ) {
3132 my $money = $old_latex ? '' : $money_char;
3133 push @detail_items, {
3134 ext_description => [],
3137 description => &$escape_function($payment->{'description'}),
3138 amount => $money. $payment->{'amount'},
3140 section => $adjust_section,
3143 push @total_items, $total;
3145 push @buf, [ $payment->{'description'},
3146 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3149 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3151 if ( $multisection ) {
3152 $adjust_section->{'subtotal'} = $other_money_char.
3153 sprintf('%.2f', $adjusttotal);
3154 push @sections, $adjust_section
3155 unless $adjust_section->{sort_weight};
3160 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3161 $total->{'total_amount'} =
3162 &$embolden_function(
3163 $other_money_char. sprintf('%.2f', $summarypage
3165 $self->billing_balance
3166 : $self->owed + $pr_total
3169 if ( $multisection && !$adjust_section->{sort_weight} ) {
3170 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3171 $total->{'total_amount'};
3173 push @total_items, $total;
3175 push @buf,['','-----------'];
3176 push @buf,[$self->balance_due_msg, $money_char.
3177 sprintf("%10.2f", $balance_due ) ];
3180 if ( $conf->exists('previous_balance-show_credit')
3181 and $cust_main->balance < 0 ) {
3182 my $credit_total = {
3183 'total_item' => &$embolden_function($self->credit_balance_msg),
3184 'total_amount' => &$embolden_function(
3185 $other_money_char. sprintf('%.2f', -$cust_main->balance)
3188 if ( $multisection ) {
3189 $adjust_section->{'posttotal'} .= $newline_token .
3190 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3193 push @total_items, $credit_total;
3195 push @buf,['','-----------'];
3196 push @buf,[$self->credit_balance_msg, $money_char.
3197 sprintf("%10.2f", -$cust_main->balance ) ];
3201 if ( $multisection ) {
3202 if ($conf->exists('svc_phone_sections')) {
3204 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3205 $total->{'total_amount'} =
3206 &$embolden_function(
3207 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3209 my $last_section = pop @sections;
3210 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3211 $total->{'total_amount'};
3212 push @sections, $last_section;
3214 push @sections, @$late_sections
3218 my @includelist = ();
3219 push @includelist, 'summary' if $summarypage;
3220 foreach my $include ( @includelist ) {
3222 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3225 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3227 @inc_src = $conf->config($inc_file, $agentnum);
3231 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3233 my $convert_map = $convert_maps{$format}{$include};
3235 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3236 s/--\@\]/$delimiters{$format}[1]/g;
3239 &$convert_map( $conf->config($inc_file, $agentnum) );
3243 my $inc_tt = new Text::Template (
3245 SOURCE => [ map "$_\n", @inc_src ],
3246 DELIMITERS => $delimiters{$format},
3247 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3249 unless ( $inc_tt->compile() ) {
3250 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3251 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3255 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3257 $invoice_data{$include} =~ s/\n+$//
3258 if ($format eq 'latex');
3263 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3264 /invoice_lines\((\d*)\)/;
3265 $invoice_lines += $1 || scalar(@buf);
3268 die "no invoice_lines() functions in template?"
3269 if ( $format eq 'template' && !$wasfunc );
3271 if ($format eq 'template') {
3273 if ( $invoice_lines ) {
3274 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3275 $invoice_data{'total_pages'}++
3276 if scalar(@buf) % $invoice_lines;
3279 #setup subroutine for the template
3280 sub FS::cust_bill::_template::invoice_lines {
3281 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
3283 scalar(@FS::cust_bill::_template::buf)
3284 ? shift @FS::cust_bill::_template::buf
3293 push @collect, split("\n",
3294 $text_template->fill_in( HASH => \%invoice_data,
3295 PACKAGE => 'FS::cust_bill::_template'
3298 $FS::cust_bill::_template::page++;
3300 map "$_\n", @collect;
3302 warn "filling in template for invoice ". $self->invnum. "\n"
3304 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3307 $text_template->fill_in(HASH => \%invoice_data);
3311 # helper routine for generating date ranges
3312 sub _prior_month30s {
3315 [ 1, 2592000 ], # 0-30 days ago
3316 [ 2592000, 5184000 ], # 30-60 days ago
3317 [ 5184000, 7776000 ], # 60-90 days ago
3318 [ 7776000, 0 ], # 90+ days ago
3321 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3322 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3327 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3329 Returns an postscript invoice, as a scalar.
3331 Options can be passed as a hashref (recommended) or as a list of time, template
3332 and then any key/value pairs for any other options.
3334 I<time> an optional value used to control the printing of overdue messages. The
3335 default is now. It isn't the date of the invoice; that's the `_date' field.
3336 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3337 L<Time::Local> and L<Date::Parse> for conversion functions.
3339 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3346 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3347 my $ps = generate_ps($file);
3349 unlink($barcodefile);
3354 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3356 Returns an PDF invoice, as a scalar.
3358 Options can be passed as a hashref (recommended) or as a list of time, template
3359 and then any key/value pairs for any other options.
3361 I<time> an optional value used to control the printing of overdue messages. The
3362 default is now. It isn't the date of the invoice; that's the `_date' field.
3363 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3364 L<Time::Local> and L<Date::Parse> for conversion functions.
3366 I<template>, if specified, is the name of a suffix for alternate invoices.
3368 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3375 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3376 my $pdf = generate_pdf($file);
3378 unlink($barcodefile);
3383 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3385 Returns an HTML invoice, as a scalar.
3387 I<time> an optional value used to control the printing of overdue messages. The
3388 default is now. It isn't the date of the invoice; that's the `_date' field.
3389 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3390 L<Time::Local> and L<Date::Parse> for conversion functions.
3392 I<template>, if specified, is the name of a suffix for alternate invoices.
3394 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3396 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3397 when emailing the invoice as part of a multipart/related MIME email.
3405 %params = %{ shift() };
3407 $params{'time'} = shift;
3408 $params{'template'} = shift;
3409 $params{'cid'} = shift;
3412 $params{'format'} = 'html';
3414 $self->print_generic( %params );
3417 # quick subroutine for print_latex
3419 # There are ten characters that LaTeX treats as special characters, which
3420 # means that they do not simply typeset themselves:
3421 # # $ % & ~ _ ^ \ { }
3423 # TeX ignores blanks following an escaped character; if you want a blank (as
3424 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3428 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3429 $value =~ s/([<>])/\$$1\$/g;
3435 encode_entities($value);
3439 sub _html_escape_nbsp {
3440 my $value = _html_escape(shift);
3441 $value =~ s/ +/ /g;
3445 #utility methods for print_*
3447 sub _translate_old_latex_format {
3448 warn "_translate_old_latex_format called\n"
3455 if ( $line =~ /^%%Detail\s*$/ ) {
3457 push @template, q![@--!,
3458 q! foreach my $_tr_line (@detail_items) {!,
3459 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3460 q! $_tr_line->{'description'} .= !,
3461 q! "\\tabularnewline\n~~".!,
3462 q! join( "\\tabularnewline\n~~",!,
3463 q! @{$_tr_line->{'ext_description'}}!,
3467 while ( ( my $line_item_line = shift )
3468 !~ /^%%EndDetail\s*$/ ) {
3469 $line_item_line =~ s/'/\\'/g; # nice LTS
3470 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3471 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3472 push @template, " \$OUT .= '$line_item_line';";
3475 push @template, '}',
3478 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3480 push @template, '[@--',
3481 ' foreach my $_tr_line (@total_items) {';
3483 while ( ( my $total_item_line = shift )
3484 !~ /^%%EndTotalDetails\s*$/ ) {
3485 $total_item_line =~ s/'/\\'/g; # nice LTS
3486 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3487 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3488 push @template, " \$OUT .= '$total_item_line';";
3491 push @template, '}',
3495 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3496 push @template, $line;
3502 warn "$_\n" foreach @template;
3511 #check for an invoice-specific override
3512 return $self->invoice_terms if $self->invoice_terms;
3514 #check for a customer- specific override
3515 my $cust_main = $self->cust_main;
3516 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3518 #use configured default
3519 $conf->config('invoice_default_terms') || '';
3525 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3526 $duedate = $self->_date() + ( $1 * 86400 );
3533 $self->due_date ? time2str(shift, $self->due_date) : '';
3536 sub balance_due_msg {
3538 my $msg = 'Balance Due';
3539 return $msg unless $self->terms;
3540 if ( $self->due_date ) {
3541 $msg .= ' - Please pay by '. $self->due_date2str($date_format);
3542 } elsif ( $self->terms ) {
3543 $msg .= ' - '. $self->terms;
3548 sub balance_due_date {
3551 if ( $conf->exists('invoice_default_terms')
3552 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3553 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3558 sub credit_balance_msg { 'Credit Balance Remaining' }
3560 =item invnum_date_pretty
3562 Returns a string with the invoice number and date, for example:
3563 "Invoice #54 (3/20/2008)"
3567 sub invnum_date_pretty {
3569 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3574 Returns a string with the date, for example: "3/20/2008"
3580 time2str($date_format, $self->_date);
3583 use vars qw(%pkg_category_cache);
3584 sub _items_sections {
3587 my $summarypage = shift;
3589 my $extra_sections = shift;
3593 my %late_subtotal = ();
3596 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3599 my $usage = $cust_bill_pkg->usage;
3601 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3602 next if ( $display->summary && $summarypage );
3604 my $section = $display->section;
3605 my $type = $display->type;
3607 $not_tax{$section} = 1
3608 unless $cust_bill_pkg->pkgnum == 0;
3610 if ( $display->post_total && !$summarypage ) {
3611 if (! $type || $type eq 'S') {
3612 $late_subtotal{$section} += $cust_bill_pkg->setup
3613 if $cust_bill_pkg->setup != 0;
3617 $late_subtotal{$section} += $cust_bill_pkg->recur
3618 if $cust_bill_pkg->recur != 0;
3621 if ($type && $type eq 'R') {
3622 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3623 if $cust_bill_pkg->recur != 0;
3626 if ($type && $type eq 'U') {
3627 $late_subtotal{$section} += $usage
3628 unless scalar(@$extra_sections);
3633 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3635 if (! $type || $type eq 'S') {
3636 $subtotal{$section} += $cust_bill_pkg->setup
3637 if $cust_bill_pkg->setup != 0;
3641 $subtotal{$section} += $cust_bill_pkg->recur
3642 if $cust_bill_pkg->recur != 0;
3645 if ($type && $type eq 'R') {
3646 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3647 if $cust_bill_pkg->recur != 0;
3650 if ($type && $type eq 'U') {
3651 $subtotal{$section} += $usage
3652 unless scalar(@$extra_sections);
3661 %pkg_category_cache = ();
3663 push @$late, map { { 'description' => &{$escape}($_),
3664 'subtotal' => $late_subtotal{$_},
3666 'sort_weight' => ( _pkg_category($_)
3667 ? _pkg_category($_)->weight
3670 ((_pkg_category($_) && _pkg_category($_)->condense)
3671 ? $self->_condense_section($format)
3675 sort _sectionsort keys %late_subtotal;
3678 if ( $summarypage ) {
3679 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3680 map { $_->categoryname } qsearch('pkg_category', {});
3681 push @sections, '' if exists($subtotal{''});
3683 @sections = keys %subtotal;
3686 my @early = map { { 'description' => &{$escape}($_),
3687 'subtotal' => $subtotal{$_},
3688 'summarized' => $not_tax{$_} ? '' : 'Y',
3689 'tax_section' => $not_tax{$_} ? '' : 'Y',
3690 'sort_weight' => ( _pkg_category($_)
3691 ? _pkg_category($_)->weight
3694 ((_pkg_category($_) && _pkg_category($_)->condense)
3695 ? $self->_condense_section($format)
3700 push @early, @$extra_sections if $extra_sections;
3702 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3706 #helper subs for above
3709 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3713 my $categoryname = shift;
3714 $pkg_category_cache{$categoryname} ||=
3715 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3718 my %condensed_format = (
3719 'label' => [ qw( Description Qty Amount ) ],
3721 sub { shift->{description} },
3722 sub { shift->{quantity} },
3723 sub { my($href, %opt) = @_;
3724 ($opt{dollar} || ''). $href->{amount};
3727 'align' => [ qw( l r r ) ],
3728 'span' => [ qw( 5 1 1 ) ], # unitprices?
3729 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3732 sub _condense_section {
3733 my ( $self, $format ) = ( shift, shift );
3735 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3736 qw( description_generator
3739 total_line_generator
3744 sub _condensed_generator_defaults {
3745 my ( $self, $format ) = ( shift, shift );
3746 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3755 sub _condensed_header_generator {
3756 my ( $self, $format ) = ( shift, shift );
3758 my ( $f, $prefix, $suffix, $separator, $column ) =
3759 _condensed_generator_defaults($format);
3761 if ($format eq 'latex') {
3762 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3763 $suffix = "\\\\\n\\hline";
3766 sub { my ($d,$a,$s,$w) = @_;
3767 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3769 } elsif ( $format eq 'html' ) {
3770 $prefix = '<th></th>';
3774 sub { my ($d,$a,$s,$w) = @_;
3775 return qq!<th align="$html_align{$a}">$d</th>!;
3783 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3785 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3788 $prefix. join($separator, @result). $suffix;
3793 sub _condensed_description_generator {
3794 my ( $self, $format ) = ( shift, shift );
3796 my ( $f, $prefix, $suffix, $separator, $column ) =
3797 _condensed_generator_defaults($format);
3799 my $money_char = '$';
3800 if ($format eq 'latex') {
3801 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3803 $separator = " & \n";
3805 sub { my ($d,$a,$s,$w) = @_;
3806 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3808 $money_char = '\\dollar';
3809 }elsif ( $format eq 'html' ) {
3810 $prefix = '"><td align="center"></td>';
3814 sub { my ($d,$a,$s,$w) = @_;
3815 return qq!<td align="$html_align{$a}">$d</td>!;
3817 #$money_char = $conf->config('money_char') || '$';
3818 $money_char = ''; # this is madness
3826 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3828 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3830 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3831 map { $f->{$_}->[$i] } qw(align span width)
3835 $prefix. join( $separator, @result ). $suffix;
3840 sub _condensed_total_generator {
3841 my ( $self, $format ) = ( shift, shift );
3843 my ( $f, $prefix, $suffix, $separator, $column ) =
3844 _condensed_generator_defaults($format);
3847 if ($format eq 'latex') {
3850 $separator = " & \n";
3852 sub { my ($d,$a,$s,$w) = @_;
3853 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3855 }elsif ( $format eq 'html' ) {
3859 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3861 sub { my ($d,$a,$s,$w) = @_;
3862 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3871 # my $r = &{$f->{fields}->[$i]}(@args);
3872 # $r .= ' Total' unless $i;
3874 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3876 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3877 map { $f->{$_}->[$i] } qw(align span width)
3881 $prefix. join( $separator, @result ). $suffix;
3886 =item total_line_generator FORMAT
3888 Returns a coderef used for generation of invoice total line items for this
3889 usage_class. FORMAT is either html or latex
3893 # should not be used: will have issues with hash element names (description vs
3894 # total_item and amount vs total_amount -- another array of functions?
3896 sub _condensed_total_line_generator {
3897 my ( $self, $format ) = ( shift, shift );
3899 my ( $f, $prefix, $suffix, $separator, $column ) =
3900 _condensed_generator_defaults($format);
3903 if ($format eq 'latex') {
3906 $separator = " & \n";
3908 sub { my ($d,$a,$s,$w) = @_;
3909 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3911 }elsif ( $format eq 'html' ) {
3915 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3917 sub { my ($d,$a,$s,$w) = @_;
3918 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3927 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3929 &{$column}( &{$f->{fields}->[$i]}(@args),
3930 map { $f->{$_}->[$i] } qw(align span width)
3934 $prefix. join( $separator, @result ). $suffix;
3939 #sub _items_extra_usage_sections {
3941 # my $escape = shift;
3943 # my %sections = ();
3945 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
3946 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3948 # next unless $cust_bill_pkg->pkgnum > 0;
3950 # foreach my $section ( keys %usage_class ) {
3952 # my $usage = $cust_bill_pkg->usage($section);
3954 # next unless $usage && $usage > 0;
3956 # $sections{$section} ||= 0;
3957 # $sections{$section} += $usage;
3963 # map { { 'description' => &{$escape}($_),
3964 # 'subtotal' => $sections{$_},
3965 # 'summarized' => '',
3966 # 'tax_section' => '',
3969 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3973 sub _items_extra_usage_sections {
3982 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3983 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3984 next unless $cust_bill_pkg->pkgnum > 0;
3986 foreach my $classnum ( keys %usage_class ) {
3987 my $section = $usage_class{$classnum}->classname;
3988 $classnums{$section} = $classnum;
3990 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3991 my $amount = $detail->amount;
3992 next unless $amount && $amount > 0;
3994 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
3995 $sections{$section}{amount} += $amount; #subtotal
3996 $sections{$section}{calls}++;
3997 $sections{$section}{duration} += $detail->duration;
3999 my $desc = $detail->regionname;
4000 my $description = $desc;
4001 $description = substr($desc, 0, 50). '...'
4002 if $format eq 'latex' && length($desc) > 50;
4004 $lines{$section}{$desc} ||= {
4005 description => &{$escape}($description),
4006 #pkgpart => $part_pkg->pkgpart,
4007 pkgnum => $cust_bill_pkg->pkgnum,
4012 #unit_amount => $cust_bill_pkg->unitrecur,
4013 quantity => $cust_bill_pkg->quantity,
4014 product_code => 'N/A',
4015 ext_description => [],
4018 $lines{$section}{$desc}{amount} += $amount;
4019 $lines{$section}{$desc}{calls}++;
4020 $lines{$section}{$desc}{duration} += $detail->duration;
4026 my %sectionmap = ();
4027 foreach (keys %sections) {
4028 my $usage_class = $usage_class{$classnums{$_}};
4029 $sectionmap{$_} = { 'description' => &{$escape}($_),
4030 'amount' => $sections{$_}{amount}, #subtotal
4031 'calls' => $sections{$_}{calls},
4032 'duration' => $sections{$_}{duration},
4034 'tax_section' => '',
4035 'sort_weight' => $usage_class->weight,
4036 ( $usage_class->format
4037 ? ( map { $_ => $usage_class->$_($format) }
4038 qw( description_generator header_generator total_generator total_line_generator )
4045 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4049 foreach my $section ( keys %lines ) {
4050 foreach my $line ( keys %{$lines{$section}} ) {
4051 my $l = $lines{$section}{$line};
4052 $l->{section} = $sectionmap{$section};
4053 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4054 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4059 return(\@sections, \@lines);
4065 my $end = $self->_date;
4066 my $start = $end - 2592000; # 30 days
4067 my $cust_main = $self->cust_main;
4068 my @pkgs = $cust_main->all_pkgs;
4069 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4072 foreach my $pkg ( @pkgs ) {
4073 my @h_cust_svc = $pkg->h_cust_svc($end);
4074 foreach my $h_cust_svc ( @h_cust_svc ) {
4075 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4076 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4078 my $inserted = $h_cust_svc->date_inserted;
4079 my $deleted = $h_cust_svc->date_deleted;
4080 my $phone_inserted = $h_cust_svc->h_svc_x($inserted);
4082 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4084 # DID either activated or ported in; cannot be both for same DID simultaneously
4085 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4086 && (!$phone_inserted->lnp_status
4087 || $phone_inserted->lnp_status eq ''
4088 || $phone_inserted->lnp_status eq 'native')) {
4091 else { # this one not so clean, should probably move to (h_)svc_phone
4092 my $phone_portedin = qsearchs( 'h_svc_phone',
4093 { 'svcnum' => $h_cust_svc->svcnum,
4094 'lnp_status' => 'portedin' },
4095 FS::h_svc_phone->sql_h_searchs($end),
4097 $num_portedin++ if $phone_portedin;
4100 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4101 if($deleted >= $start && $deleted <= $end && $phone_deleted
4102 && (!$phone_deleted->lnp_status
4103 || $phone_deleted->lnp_status ne 'portingout')) {
4106 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4107 && $phone_deleted->lnp_status
4108 && $phone_deleted->lnp_status eq 'portingout') {
4112 # increment usage minutes
4113 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end);
4114 foreach my $cdr ( @cdrs ) {
4115 $minutes += $cdr->billsec/60;
4118 # don't look at this service again
4119 push @seen, $h_cust_svc->svcnum;
4123 $minutes = sprintf("%d", $minutes);
4124 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4125 . "$num_deactivated Ported-Out: $num_portedout ",
4126 "Total Minutes: $minutes");
4129 sub _items_svc_phone_sections {
4138 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4139 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4141 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4142 next unless $cust_bill_pkg->pkgnum > 0;
4144 my @header = $cust_bill_pkg->details_header;
4145 next unless scalar(@header);
4147 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4149 my $phonenum = $detail->phonenum;
4150 next unless $phonenum;
4152 my $amount = $detail->amount;
4153 next unless $amount && $amount > 0;
4155 $sections{$phonenum} ||= { 'amount' => 0,
4158 'sort_weight' => -1,
4159 'phonenum' => $phonenum,
4161 $sections{$phonenum}{amount} += $amount; #subtotal
4162 $sections{$phonenum}{calls}++;
4163 $sections{$phonenum}{duration} += $detail->duration;
4165 my $desc = $detail->regionname;
4166 my $description = $desc;
4167 $description = substr($desc, 0, 50). '...'
4168 if $format eq 'latex' && length($desc) > 50;
4170 $lines{$phonenum}{$desc} ||= {
4171 description => &{$escape}($description),
4172 #pkgpart => $part_pkg->pkgpart,
4180 product_code => 'N/A',
4181 ext_description => [],
4184 $lines{$phonenum}{$desc}{amount} += $amount;
4185 $lines{$phonenum}{$desc}{calls}++;
4186 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4188 my $line = $usage_class{$detail->classnum}->classname;
4189 $sections{"$phonenum $line"} ||=
4193 'sort_weight' => $usage_class{$detail->classnum}->weight,
4194 'phonenum' => $phonenum,
4195 'header' => [ @header ],
4197 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4198 $sections{"$phonenum $line"}{calls}++;
4199 $sections{"$phonenum $line"}{duration} += $detail->duration;
4201 $lines{"$phonenum $line"}{$desc} ||= {
4202 description => &{$escape}($description),
4203 #pkgpart => $part_pkg->pkgpart,
4211 product_code => 'N/A',
4212 ext_description => [],
4215 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4216 $lines{"$phonenum $line"}{$desc}{calls}++;
4217 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4218 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4219 $detail->formatted('format' => $format);
4224 my %sectionmap = ();
4225 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4226 foreach ( keys %sections ) {
4227 my @header = @{ $sections{$_}{header} || [] };
4229 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4230 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4231 my $usage_class = $summary ? $simple : $usage_simple;
4232 my $ending = $summary ? ' usage charges' : '';
4235 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4237 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4238 'amount' => $sections{$_}{amount}, #subtotal
4239 'calls' => $sections{$_}{calls},
4240 'duration' => $sections{$_}{duration},
4242 'tax_section' => '',
4243 'phonenum' => $sections{$_}{phonenum},
4244 'sort_weight' => $sections{$_}{sort_weight},
4245 'post_total' => $summary, #inspire pagebreak
4247 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4248 qw( description_generator
4251 total_line_generator
4258 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4259 $a->{sort_weight} <=> $b->{sort_weight}
4264 foreach my $section ( keys %lines ) {
4265 foreach my $line ( keys %{$lines{$section}} ) {
4266 my $l = $lines{$section}{$line};
4267 $l->{section} = $sectionmap{$section};
4268 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4269 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4274 if($conf->exists('phone_usage_class_summary')) {
4275 # this only works with Latex
4279 # after this, we'll have only two sections per DID:
4280 # Calls Summary and Calls Detail
4281 foreach my $section ( @sections ) {
4282 if($section->{'post_total'}) {
4283 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4284 $section->{'total_line_generator'} = sub { '' };
4285 $section->{'total_generator'} = sub { '' };
4286 $section->{'header_generator'} = sub { '' };
4287 $section->{'description_generator'} = '';
4288 push @newsections, $section;
4289 my %calls_detail = %$section;
4290 $calls_detail{'post_total'} = '';
4291 $calls_detail{'sort_weight'} = '';
4292 $calls_detail{'description_generator'} = sub { '' };
4293 $calls_detail{'header_generator'} = sub {
4294 return ' & Date/Time & Called Number & Duration & Price'
4295 if $format eq 'latex';
4298 $calls_detail{'description'} = 'Calls Detail: '
4299 . $section->{'phonenum'};
4300 push @newsections, \%calls_detail;
4304 # after this, each usage class is collapsed/summarized into a single
4305 # line under the Calls Summary section
4306 foreach my $newsection ( @newsections ) {
4307 if($newsection->{'post_total'}) { # this means Calls Summary
4308 foreach my $section ( @sections ) {
4309 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4310 && !$section->{'post_total'});
4311 my $newdesc = $section->{'description'};
4312 my $tn = $section->{'phonenum'};
4313 $newdesc =~ s/$tn//g;
4314 my $line = { ext_description => [],
4318 calls => $section->{'calls'},
4319 section => $newsection,
4320 duration => $section->{'duration'},
4321 description => $newdesc,
4322 amount => sprintf("%.2f",$section->{'amount'}),
4323 product_code => 'N/A',
4325 push @newlines, $line;
4330 # after this, Calls Details is populated with all CDRs
4331 foreach my $newsection ( @newsections ) {
4332 if(!$newsection->{'post_total'}) { # this means Calls Details
4333 foreach my $line ( @lines ) {
4334 next unless (scalar(@{$line->{'ext_description'}}) &&
4335 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4337 my @extdesc = @{$line->{'ext_description'}};
4339 foreach my $extdesc ( @extdesc ) {
4340 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4341 push @newextdesc, $extdesc;
4343 $line->{'ext_description'} = \@newextdesc;
4344 $line->{'section'} = $newsection;
4345 push @newlines, $line;
4350 return(\@newsections, \@newlines);
4353 return(\@sections, \@lines);
4360 #my @display = scalar(@_)
4362 # : qw( _items_previous _items_pkg );
4363 # #: qw( _items_pkg );
4364 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4365 my @display = qw( _items_previous _items_pkg );
4368 foreach my $display ( @display ) {
4369 push @b, $self->$display(@_);
4374 sub _items_previous {
4376 my $cust_main = $self->cust_main;
4377 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4379 foreach ( @pr_cust_bill ) {
4380 my $date = $conf->exists('invoice_show_prior_due_date')
4381 ? 'due '. $_->due_date2str($date_format)
4382 : time2str($date_format, $_->_date);
4384 'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
4385 #'pkgpart' => 'N/A',
4387 'amount' => sprintf("%.2f", $_->owed),
4393 # 'description' => 'Previous Balance',
4394 # #'pkgpart' => 'N/A',
4395 # 'pkgnum' => 'N/A',
4396 # 'amount' => sprintf("%10.2f", $pr_total ),
4397 # 'ext_description' => [ map {
4398 # "Invoice ". $_->invnum.
4399 # " (". time2str("%x",$_->_date). ") ".
4400 # sprintf("%10.2f", $_->owed)
4401 # } @pr_cust_bill ],
4410 warn "$me _items_pkg searching for all package line items\n"
4413 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4415 warn "$me _items_pkg filtering line items\n"
4417 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4419 if ($options{section} && $options{section}->{condensed}) {
4421 warn "$me _items_pkg condensing section\n"
4425 local $Storable::canonical = 1;
4426 foreach ( @items ) {
4428 delete $item->{ref};
4429 delete $item->{ext_description};
4430 my $key = freeze($item);
4431 $itemshash{$key} ||= 0;
4432 $itemshash{$key} ++; # += $item->{quantity};
4434 @items = sort { $a->{description} cmp $b->{description} }
4435 map { my $i = thaw($_);
4436 $i->{quantity} = $itemshash{$_};
4438 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4444 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4451 return 0 unless $a->itemdesc cmp $b->itemdesc;
4452 return -1 if $b->itemdesc eq 'Tax';
4453 return 1 if $a->itemdesc eq 'Tax';
4454 return -1 if $b->itemdesc eq 'Other surcharges';
4455 return 1 if $a->itemdesc eq 'Other surcharges';
4456 $a->itemdesc cmp $b->itemdesc;
4461 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4462 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4465 sub _items_cust_bill_pkg {
4467 my $cust_bill_pkgs = shift;
4470 my $format = $opt{format} || '';
4471 my $escape_function = $opt{escape_function} || sub { shift };
4472 my $format_function = $opt{format_function} || '';
4473 my $unsquelched = $opt{unsquelched} || '';
4474 my $section = $opt{section}->{description} if $opt{section};
4475 my $summary_page = $opt{summary_page} || '';
4476 my $multilocation = $opt{multilocation} || '';
4477 my $multisection = $opt{multisection} || '';
4478 my $discount_show_always = 0;
4481 my ($s, $r, $u) = ( undef, undef, undef );
4482 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4485 warn "$me _items_cust_bill_pkg considering cust_bill_pkg $cust_bill_pkg\n"
4488 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
4489 && $conf->exists('discount-show-always'));
4491 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4492 if ( $_ && !$cust_bill_pkg->hidden ) {
4493 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4494 $_->{amount} =~ s/^\-0\.00$/0.00/;
4495 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4497 unless ( $_->{amount} == 0 && !$discount_show_always );
4502 foreach my $display ( grep { defined($section)
4503 ? $_->section eq $section
4506 #grep { !$_->summary || !$summary_page } # bunk!
4507 grep { !$_->summary || $multisection }
4508 $cust_bill_pkg->cust_bill_pkg_display
4512 warn "$me _items_cust_bill_pkg considering display item $display\n"
4515 my $type = $display->type;
4517 my $desc = $cust_bill_pkg->desc;
4518 $desc = substr($desc, 0, 50). '...'
4519 if $format eq 'latex' && length($desc) > 50;
4521 my %details_opt = ( 'format' => $format,
4522 'escape_function' => $escape_function,
4523 'format_function' => $format_function,
4526 if ( $cust_bill_pkg->pkgnum > 0 ) {
4528 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4531 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4533 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
4535 warn "$me _items_cust_bill_pkg adding setup\n"
4538 my $description = $desc;
4539 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
4542 unless ( $cust_pkg->part_pkg->hide_svc_detail
4543 || $cust_bill_pkg->hidden )
4546 push @d, map &{$escape_function}($_),
4547 $cust_pkg->h_labels_short($self->_date, undef, 'I')
4548 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4550 if ( $multilocation ) {
4551 my $loc = $cust_pkg->location_label;
4552 $loc = substr($loc, 0, 50). '...'
4553 if $format eq 'latex' && length($loc) > 50;
4554 push @d, &{$escape_function}($loc);
4559 push @d, $cust_bill_pkg->details(%details_opt)
4560 if $cust_bill_pkg->recur == 0;
4562 if ( $cust_bill_pkg->hidden ) {
4563 $s->{amount} += $cust_bill_pkg->setup;
4564 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4565 push @{ $s->{ext_description} }, @d;
4568 description => $description,
4569 #pkgpart => $part_pkg->pkgpart,
4570 pkgnum => $cust_bill_pkg->pkgnum,
4571 amount => $cust_bill_pkg->setup,
4572 unit_amount => $cust_bill_pkg->unitsetup,
4573 quantity => $cust_bill_pkg->quantity,
4574 ext_description => \@d,
4580 if ( ( $cust_bill_pkg->recur != 0 || $cust_bill_pkg->setup == 0 ||
4581 ($discount_show_always && $cust_bill_pkg->recur == 0) ) &&
4582 ( !$type || $type eq 'R' || $type eq 'U' )
4586 warn "$me _items_cust_bill_pkg adding recur/usage\n"
4589 my $is_summary = $display->summary;
4590 my $description = ($is_summary && $type && $type eq 'U')
4591 ? "Usage charges" : $desc;
4593 $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4594 " - ". time2str($date_format, $cust_bill_pkg->edate).
4596 unless $conf->exists('disable_line_item_date_ranges');
4600 #at least until cust_bill_pkg has "past" ranges in addition to
4601 #the "future" sdate/edate ones... see #3032
4602 my @dates = ( $self->_date );
4603 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4604 push @dates, $prev->sdate if $prev;
4605 push @dates, undef if !$prev;
4607 unless ( $cust_pkg->part_pkg->hide_svc_detail
4608 || $cust_bill_pkg->itemdesc
4609 || $cust_bill_pkg->hidden
4610 || $is_summary && $type && $type eq 'U' )
4613 warn "$me _items_cust_bill_pkg adding service details\n"
4616 push @d, map &{$escape_function}($_),
4617 $cust_pkg->h_labels_short(@dates, 'I')
4618 #$cust_bill_pkg->edate,
4619 #$cust_bill_pkg->sdate)
4620 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4622 warn "$me _items_cust_bill_pkg done adding service details\n"
4625 if ( $multilocation ) {
4626 my $loc = $cust_pkg->location_label;
4627 $loc = substr($loc, 0, 50). '...'
4628 if $format eq 'latex' && length($loc) > 50;
4629 push @d, &{$escape_function}($loc);
4634 unless ( $is_summary ) {
4635 warn "$me _items_cust_bill_pkg adding details\n"
4638 #instead of omitting details entirely in this case (unwanted side
4639 # effects), just omit CDRs
4640 $details_opt{'format_function'} = sub { () }
4641 if $type && $type eq 'R';
4643 push @d, $cust_bill_pkg->details(%details_opt);
4646 warn "$me _items_cust_bill_pkg calculating amount\n"
4651 $amount = $cust_bill_pkg->recur;
4652 } elsif ($type eq 'R') {
4653 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4654 } elsif ($type eq 'U') {
4655 $amount = $cust_bill_pkg->usage;
4658 if ( !$type || $type eq 'R' ) {
4660 warn "$me _items_cust_bill_pkg adding recur\n"
4663 if ( $cust_bill_pkg->hidden ) {
4664 $r->{amount} += $amount;
4665 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4666 push @{ $r->{ext_description} }, @d;
4669 description => $description,
4670 #pkgpart => $part_pkg->pkgpart,
4671 pkgnum => $cust_bill_pkg->pkgnum,
4673 unit_amount => $cust_bill_pkg->unitrecur,
4674 quantity => $cust_bill_pkg->quantity,
4675 ext_description => \@d,
4679 } else { # $type eq 'U'
4681 warn "$me _items_cust_bill_pkg adding usage\n"
4684 if ( $cust_bill_pkg->hidden ) {
4685 $u->{amount} += $amount;
4686 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4687 push @{ $u->{ext_description} }, @d;
4690 description => $description,
4691 #pkgpart => $part_pkg->pkgpart,
4692 pkgnum => $cust_bill_pkg->pkgnum,
4694 unit_amount => $cust_bill_pkg->unitrecur,
4695 quantity => $cust_bill_pkg->quantity,
4696 ext_description => \@d,
4702 } # recurring or usage with recurring charge
4704 } else { #pkgnum tax or one-shot line item (??)
4706 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
4709 if ( $cust_bill_pkg->setup != 0 ) {
4711 'description' => $desc,
4712 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
4715 if ( $cust_bill_pkg->recur != 0 ) {
4717 'description' => "$desc (".
4718 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
4719 time2str($date_format, $cust_bill_pkg->edate). ')',
4720 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
4730 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
4733 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4735 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4736 $_->{amount} =~ s/^\-0\.00$/0.00/;
4737 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4739 unless ( $_->{amount} == 0 && !$discount_show_always );
4747 sub _items_credits {
4748 my( $self, %opt ) = @_;
4749 my $trim_len = $opt{'trim_len'} || 60;
4753 foreach ( $self->cust_credited ) {
4755 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4757 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4758 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4759 $reason = " ($reason) " if $reason;
4762 #'description' => 'Credit ref\#'. $_->crednum.
4763 # " (". time2str("%x",$_->cust_credit->_date) .")".
4765 'description' => 'Credit applied '.
4766 time2str($date_format,$_->cust_credit->_date). $reason,
4767 'amount' => sprintf("%.2f",$_->amount),
4775 sub _items_payments {
4779 #get & print payments
4780 foreach ( $self->cust_bill_pay ) {
4782 #something more elaborate if $_->amount ne ->cust_pay->paid ?
4785 'description' => "Payment received ".
4786 time2str($date_format,$_->cust_pay->_date ),
4787 'amount' => sprintf("%.2f", $_->amount )
4795 =item call_details [ OPTION => VALUE ... ]
4797 Returns an array of CSV strings representing the call details for this invoice
4798 The only option available is the boolean prepend_billed_number
4803 my ($self, %opt) = @_;
4805 my $format_function = sub { shift };
4807 if ($opt{prepend_billed_number}) {
4808 $format_function = sub {
4812 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4817 my @details = map { $_->details( 'format_function' => $format_function,
4818 'escape_function' => sub{ return() },
4822 $self->cust_bill_pkg;
4823 my $header = $details[0];
4824 ( $header, grep { $_ ne $header } @details );
4834 =item process_reprint
4838 sub process_reprint {
4839 process_re_X('print', @_);
4842 =item process_reemail
4846 sub process_reemail {
4847 process_re_X('email', @_);
4855 process_re_X('fax', @_);
4863 process_re_X('ftp', @_);
4870 sub process_respool {
4871 process_re_X('spool', @_);
4874 use Storable qw(thaw);
4878 my( $method, $job ) = ( shift, shift );
4879 warn "$me process_re_X $method for job $job\n" if $DEBUG;
4881 my $param = thaw(decode_base64(shift));
4882 warn Dumper($param) if $DEBUG;
4893 my($method, $job, %param ) = @_;
4895 warn "re_X $method for job $job with param:\n".
4896 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
4899 #some false laziness w/search/cust_bill.html
4901 my $orderby = 'ORDER BY cust_bill._date';
4903 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
4905 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4907 my @cust_bill = qsearch( {
4908 #'select' => "cust_bill.*",
4909 'table' => 'cust_bill',
4910 'addl_from' => $addl_from,
4912 'extra_sql' => $extra_sql,
4913 'order_by' => $orderby,
4917 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4919 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
4922 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
4923 foreach my $cust_bill ( @cust_bill ) {
4924 $cust_bill->$method();
4926 if ( $job ) { #progressbar foo
4928 if ( time - $min_sec > $last ) {
4929 my $error = $job->update_statustext(
4930 int( 100 * $num / scalar(@cust_bill) )
4932 die $error if $error;
4943 =head1 CLASS METHODS
4949 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
4954 my ($class, $start, $end) = @_;
4956 $class->paid_sql($start, $end). ' - '.
4957 $class->credited_sql($start, $end);
4962 Returns an SQL fragment to retreive the net amount (charged minus credited).
4967 my ($class, $start, $end) = @_;
4968 'charged - '. $class->credited_sql($start, $end);
4973 Returns an SQL fragment to retreive the amount paid against this invoice.
4978 my ($class, $start, $end) = @_;
4979 $start &&= "AND cust_bill_pay._date <= $start";
4980 $end &&= "AND cust_bill_pay._date > $end";
4981 $start = '' unless defined($start);
4982 $end = '' unless defined($end);
4983 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
4984 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
4989 Returns an SQL fragment to retreive the amount credited against this invoice.
4994 my ($class, $start, $end) = @_;
4995 $start &&= "AND cust_credit_bill._date <= $start";
4996 $end &&= "AND cust_credit_bill._date > $end";
4997 $start = '' unless defined($start);
4998 $end = '' unless defined($end);
4999 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5000 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
5005 Returns an SQL fragment to retrieve the due date of an invoice.
5006 Currently only supported on PostgreSQL.
5014 cust_bill.invoice_terms,
5015 cust_main.invoice_terms,
5016 \''.($conf->config('invoice_default_terms') || '').'\'
5017 ), E\'Net (\\\\d+)\'
5019 ) * 86400 + cust_bill._date'
5022 =item search_sql_where HASHREF
5024 Class method which returns an SQL WHERE fragment to search for parameters
5025 specified in HASHREF. Valid parameters are
5031 List reference of start date, end date, as UNIX timestamps.
5041 List reference of charged limits (exclusive).
5045 List reference of charged limits (exclusive).
5049 flag, return open invoices only
5053 flag, return net invoices only
5057 =item newest_percust
5061 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5065 sub search_sql_where {
5066 my($class, $param) = @_;
5068 warn "$me search_sql_where called with params: \n".
5069 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
5075 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5076 push @search, "cust_main.agentnum = $1";
5080 if ( $param->{_date} ) {
5081 my($beginning, $ending) = @{$param->{_date}};
5083 push @search, "cust_bill._date >= $beginning",
5084 "cust_bill._date < $ending";
5088 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5089 push @search, "cust_bill.invnum >= $1";
5091 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5092 push @search, "cust_bill.invnum <= $1";
5096 if ( $param->{charged} ) {
5097 my @charged = ref($param->{charged})
5098 ? @{ $param->{charged} }
5099 : ($param->{charged});
5101 push @search, map { s/^charged/cust_bill.charged/; $_; }
5105 my $owed_sql = FS::cust_bill->owed_sql;
5108 if ( $param->{owed} ) {
5109 my @owed = ref($param->{owed})
5110 ? @{ $param->{owed} }
5112 push @search, map { s/^owed/$owed_sql/; $_; }
5117 push @search, "0 != $owed_sql"
5118 if $param->{'open'};
5119 push @search, '0 != '. FS::cust_bill->net_sql
5123 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5124 if $param->{'days'};
5127 if ( $param->{'newest_percust'} ) {
5129 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5130 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5132 my @newest_where = map { my $x = $_;
5133 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5136 grep ! /^cust_main./, @search;
5137 my $newest_where = scalar(@newest_where)
5138 ? ' AND '. join(' AND ', @newest_where)
5142 push @search, "cust_bill._date = (
5143 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5144 WHERE newest_cust_bill.custnum = cust_bill.custnum
5150 #agent virtualization
5151 my $curuser = $FS::CurrentUser::CurrentUser;
5152 if ( $curuser->username eq 'fs_queue'
5153 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5155 my $newuser = qsearchs('access_user', {
5156 'username' => $username,
5160 $curuser = $newuser;
5162 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5165 push @search, $curuser->agentnums_sql;
5167 join(' AND ', @search );
5179 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5180 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base