4 use vars qw( @ISA $DEBUG $me $conf
5 $money_char $date_format $rdate_format $date_format_long );
6 use vars qw( $invoice_lines @buf ); #yuck
7 use Fcntl qw(:flock); #for spool_csv
8 use List::Util qw(min max);
10 use Text::Template 1.20;
12 use String::ShellQuote;
15 use Storable qw( freeze thaw );
17 use FS::UID qw( datasrc );
18 use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
19 use FS::Record qw( qsearch qsearchs dbh );
20 use FS::cust_main_Mixin;
22 use FS::cust_statement;
23 use FS::cust_bill_pkg;
24 use FS::cust_bill_pkg_display;
25 use FS::cust_bill_pkg_detail;
29 use FS::cust_credit_bill;
31 use FS::cust_pay_batch;
32 use FS::cust_bill_event;
35 use FS::cust_bill_pay;
36 use FS::cust_bill_pay_batch;
37 use FS::part_bill_event;
40 use FS::cust_bill_batch;
43 @ISA = qw( FS::cust_main_Mixin FS::Record );
46 $me = '[FS::cust_bill]';
48 #ask FS::UID to run this stuff for us later
49 FS::UID->install_callback( sub {
51 $money_char = $conf->config('money_char') || '$';
52 $date_format = $conf->config('date_format') || '%x'; #/YY
53 $rdate_format = $conf->config('date_format') || '%m/%d/%Y'; #/YYYY
54 $date_format_long = $conf->config('date_format_long') || '%b %o, %Y';
59 FS::cust_bill - Object methods for cust_bill records
65 $record = new FS::cust_bill \%hash;
66 $record = new FS::cust_bill { 'column' => 'value' };
68 $error = $record->insert;
70 $error = $new_record->replace($old_record);
72 $error = $record->delete;
74 $error = $record->check;
76 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
78 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
80 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
82 @cust_pay_objects = $cust_bill->cust_pay;
84 $tax_amount = $record->tax;
86 @lines = $cust_bill->print_text;
87 @lines = $cust_bill->print_text $time;
91 An FS::cust_bill object represents an invoice; a declaration that a customer
92 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
93 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
94 following fields are currently supported:
100 =item invnum - primary key (assigned automatically for new invoices)
102 =item custnum - customer (see L<FS::cust_main>)
104 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
105 L<Time::Local> and L<Date::Parse> for conversion functions.
107 =item charged - amount of this invoice
109 =item invoice_terms - optional terms override for this specific invoice
113 Customer info at invoice generation time
117 =item previous_balance
119 =item billing_balance
127 =item printed - deprecated
135 =item closed - books closed flag, empty or `Y'
137 =item statementnum - invoice aggregation (see L<FS::cust_statement>)
139 =item agent_invid - legacy invoice number
149 Creates a new invoice. To add the invoice to the database, see L<"insert">.
150 Invoices are normally created by calling the bill method of a customer object
151 (see L<FS::cust_main>).
155 sub table { 'cust_bill'; }
157 sub cust_linked { $_[0]->cust_main_custnum; }
158 sub cust_unlinked_msg {
160 "WARNING: can't find cust_main.custnum ". $self->custnum.
161 ' (cust_bill.invnum '. $self->invnum. ')';
166 Adds this invoice to the database ("Posts" the invoice). If there is an error,
167 returns the error, otherwise returns false.
173 warn "$me insert called\n" if $DEBUG;
175 local $SIG{HUP} = 'IGNORE';
176 local $SIG{INT} = 'IGNORE';
177 local $SIG{QUIT} = 'IGNORE';
178 local $SIG{TERM} = 'IGNORE';
179 local $SIG{TSTP} = 'IGNORE';
180 local $SIG{PIPE} = 'IGNORE';
182 my $oldAutoCommit = $FS::UID::AutoCommit;
183 local $FS::UID::AutoCommit = 0;
186 my $error = $self->SUPER::insert;
188 $dbh->rollback if $oldAutoCommit;
192 if ( $self->get('cust_bill_pkg') ) {
193 foreach my $cust_bill_pkg ( @{$self->get('cust_bill_pkg')} ) {
194 $cust_bill_pkg->invnum($self->invnum);
195 my $error = $cust_bill_pkg->insert;
197 $dbh->rollback if $oldAutoCommit;
198 return "can't create invoice line item: $error";
203 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
210 This method now works but you probably shouldn't use it. Instead, apply a
211 credit against the invoice.
213 Using this method to delete invoices outright is really, really bad. There
214 would be no record you ever posted this invoice, and there are no check to
215 make sure charged = 0 or that there are no associated cust_bill_pkg records.
217 Really, don't use it.
223 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
225 local $SIG{HUP} = 'IGNORE';
226 local $SIG{INT} = 'IGNORE';
227 local $SIG{QUIT} = 'IGNORE';
228 local $SIG{TERM} = 'IGNORE';
229 local $SIG{TSTP} = 'IGNORE';
230 local $SIG{PIPE} = 'IGNORE';
232 my $oldAutoCommit = $FS::UID::AutoCommit;
233 local $FS::UID::AutoCommit = 0;
236 foreach my $table (qw(
248 foreach my $linked ( $self->$table() ) {
249 my $error = $linked->delete;
251 $dbh->rollback if $oldAutoCommit;
258 my $error = $self->SUPER::delete(@_);
260 $dbh->rollback if $oldAutoCommit;
264 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
270 =item replace [ OLD_RECORD ]
272 You can, but probably shouldn't modify invoices...
274 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
275 supplied, replaces this record. If there is an error, returns the error,
276 otherwise returns false.
280 #replace can be inherited from Record.pm
282 # replace_check is now the preferred way to #implement replace data checks
283 # (so $object->replace() works without an argument)
286 my( $new, $old ) = ( shift, shift );
287 return "Can't modify closed invoice" if $old->closed =~ /^Y/i;
288 #return "Can't change _date!" unless $old->_date eq $new->_date;
289 return "Can't change _date" unless $old->_date == $new->_date;
290 return "Can't change charged" unless $old->charged == $new->charged
291 || $old->charged == 0
292 || $new->{'Hash'}{'cc_surcharge_replace_hack'};
298 =item add_cc_surcharge
304 sub add_cc_surcharge {
305 my ($self, $pkgnum, $amount) = (shift, shift, shift);
308 my $cust_bill_pkg = new FS::cust_bill_pkg({
309 'invnum' => $self->invnum,
313 $error = $cust_bill_pkg->insert;
314 return $error if $error;
316 $self->{'Hash'}{'cc_surcharge_replace_hack'} = 1;
317 $self->charged($self->charged+$amount);
318 $error = $self->replace;
319 return $error if $error;
321 $self->apply_payments_and_credits;
327 Checks all fields to make sure this is a valid invoice. If there is an error,
328 returns the error, otherwise returns false. Called by the insert and replace
337 $self->ut_numbern('invnum')
338 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
339 || $self->ut_numbern('_date')
340 || $self->ut_money('charged')
341 || $self->ut_numbern('printed')
342 || $self->ut_enum('closed', [ '', 'Y' ])
343 || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
344 || $self->ut_numbern('agent_invid') #varchar?
346 return $error if $error;
348 $self->_date(time) unless $self->_date;
350 $self->printed(0) if $self->printed eq '';
357 Returns the displayed invoice number for this invoice: agent_invid if
358 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
364 if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
365 return $self->agent_invid;
367 return $self->invnum;
373 Returns a list consisting of the total previous balance for this customer,
374 followed by the previous outstanding invoices (as FS::cust_bill objects also).
381 my @cust_bill = sort { $a->_date <=> $b->_date }
382 grep { $_->owed != 0 && $_->_date < $self->_date }
383 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
385 foreach ( @cust_bill ) { $total += $_->owed; }
391 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
398 { 'table' => 'cust_bill_pkg',
399 'hashref' => { 'invnum' => $self->invnum },
400 'order_by' => 'ORDER BY billpkgnum',
405 =item cust_bill_pkg_pkgnum PKGNUM
407 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
412 sub cust_bill_pkg_pkgnum {
413 my( $self, $pkgnum ) = @_;
415 { 'table' => 'cust_bill_pkg',
416 'hashref' => { 'invnum' => $self->invnum,
419 'order_by' => 'ORDER BY billpkgnum',
426 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
433 my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
434 $self->cust_bill_pkg;
436 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
441 Returns true if any of the packages (or their definitions) corresponding to the
442 line items for this invoice have the no_auto flag set.
448 grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
451 =item open_cust_bill_pkg
453 Returns the open line items for this invoice.
455 Note that cust_bill_pkg with both setup and recur fees are returned as two
456 separate line items, each with only one fee.
460 # modeled after cust_main::open_cust_bill
461 sub open_cust_bill_pkg {
464 # grep { $_->owed > 0 } $self->cust_bill_pkg
466 my %other = ( 'recur' => 'setup',
467 'setup' => 'recur', );
469 foreach my $field ( qw( recur setup )) {
470 push @open, map { $_->set( $other{$field}, 0 ); $_; }
471 grep { $_->owed($field) > 0 }
472 $self->cust_bill_pkg;
478 =item cust_bill_event
480 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
484 sub cust_bill_event {
486 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
489 =item num_cust_bill_event
491 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
495 sub num_cust_bill_event {
498 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
499 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
500 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
501 $sth->fetchrow_arrayref->[0];
506 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
510 #false laziness w/cust_pkg.pm
514 'table' => 'cust_event',
515 'addl_from' => 'JOIN part_event USING ( eventpart )',
516 'hashref' => { 'tablenum' => $self->invnum },
517 'extra_sql' => " AND eventtable = 'cust_bill' ",
523 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
527 #false laziness w/cust_pkg.pm
531 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
532 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
533 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
534 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
535 $sth->fetchrow_arrayref->[0];
540 Returns the customer (see L<FS::cust_main>) for this invoice.
546 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
549 =item cust_suspend_if_balance_over AMOUNT
551 Suspends the customer associated with this invoice if the total amount owed on
552 this invoice and all older invoices is greater than the specified amount.
554 Returns a list: an empty list on success or a list of errors.
558 sub cust_suspend_if_balance_over {
559 my( $self, $amount ) = ( shift, shift );
560 my $cust_main = $self->cust_main;
561 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
564 $cust_main->suspend(@_);
570 Depreciated. See the cust_credited method.
572 #Returns a list consisting of the total previous credited (see
573 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
574 #outstanding credits (FS::cust_credit objects).
580 croak "FS::cust_bill->cust_credit depreciated; see ".
581 "FS::cust_bill->cust_credit_bill";
584 #my @cust_credit = sort { $a->_date <=> $b->_date }
585 # grep { $_->credited != 0 && $_->_date < $self->_date }
586 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
588 #foreach (@cust_credit) { $total += $_->credited; }
589 #$total, @cust_credit;
594 Depreciated. See the cust_bill_pay method.
596 #Returns all payments (see L<FS::cust_pay>) for this invoice.
602 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
604 #sort { $a->_date <=> $b->_date }
605 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
611 qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
614 sub cust_bill_pay_batch {
616 qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
621 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
627 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
628 sort { $a->_date <=> $b->_date }
629 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
634 =item cust_credit_bill
636 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
642 map { $_ } #return $self->num_cust_credit_bill unless wantarray;
643 sort { $a->_date <=> $b->_date }
644 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
648 sub cust_credit_bill {
649 shift->cust_credited(@_);
652 =item cust_bill_pay_pkgnum PKGNUM
654 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
655 with matching pkgnum.
659 sub cust_bill_pay_pkgnum {
660 my( $self, $pkgnum ) = @_;
661 map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
662 sort { $a->_date <=> $b->_date }
663 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
669 =item cust_credited_pkgnum PKGNUM
671 =item cust_credit_bill_pkgnum PKGNUM
673 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
674 with matching pkgnum.
678 sub cust_credited_pkgnum {
679 my( $self, $pkgnum ) = @_;
680 map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
681 sort { $a->_date <=> $b->_date }
682 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
688 sub cust_credit_bill_pkgnum {
689 shift->cust_credited_pkgnum(@_);
694 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
701 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
703 foreach (@taxlines) { $total += $_->setup; }
709 Returns the amount owed (still outstanding) on this invoice, which is charged
710 minus all payment applications (see L<FS::cust_bill_pay>) and credit
711 applications (see L<FS::cust_credit_bill>).
717 my $balance = $self->charged;
718 $balance -= $_->amount foreach ( $self->cust_bill_pay );
719 $balance -= $_->amount foreach ( $self->cust_credited );
720 $balance = sprintf( "%.2f", $balance);
721 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
726 my( $self, $pkgnum ) = @_;
728 #my $balance = $self->charged;
730 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
732 $balance -= $_->amount for $self->cust_bill_pay_pkgnum($pkgnum);
733 $balance -= $_->amount for $self->cust_credited_pkgnum($pkgnum);
735 $balance = sprintf( "%.2f", $balance);
736 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
740 =item apply_payments_and_credits [ OPTION => VALUE ... ]
742 Applies unapplied payments and credits to this invoice.
744 A hash of optional arguments may be passed. Currently "manual" is supported.
745 If true, a payment receipt is sent instead of a statement when
746 'payment_receipt_email' configuration option is set.
748 If there is an error, returns the error, otherwise returns false.
752 sub apply_payments_and_credits {
753 my( $self, %options ) = @_;
755 local $SIG{HUP} = 'IGNORE';
756 local $SIG{INT} = 'IGNORE';
757 local $SIG{QUIT} = 'IGNORE';
758 local $SIG{TERM} = 'IGNORE';
759 local $SIG{TSTP} = 'IGNORE';
760 local $SIG{PIPE} = 'IGNORE';
762 my $oldAutoCommit = $FS::UID::AutoCommit;
763 local $FS::UID::AutoCommit = 0;
766 $self->select_for_update; #mutex
768 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
769 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
771 if ( $conf->exists('pkg-balances') ) {
772 # limit @payments & @credits to those w/ a pkgnum grepped from $self
773 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
774 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
775 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
778 while ( $self->owed > 0 and ( @payments || @credits ) ) {
781 if ( @payments && @credits ) {
783 #decide which goes first by weight of top (unapplied) line item
785 my @open_lineitems = $self->open_cust_bill_pkg;
788 max( map { $_->part_pkg->pay_weight || 0 }
793 my $max_credit_weight =
794 max( map { $_->part_pkg->credit_weight || 0 }
800 #if both are the same... payments first? it has to be something
801 if ( $max_pay_weight >= $max_credit_weight ) {
807 } elsif ( @payments ) {
809 } elsif ( @credits ) {
812 die "guru meditation #12 and 35";
816 if ( $app eq 'pay' ) {
818 my $payment = shift @payments;
819 $unapp_amount = $payment->unapplied;
820 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
821 $app->pkgnum( $payment->pkgnum )
822 if $conf->exists('pkg-balances') && $payment->pkgnum;
824 } elsif ( $app eq 'credit' ) {
826 my $credit = shift @credits;
827 $unapp_amount = $credit->credited;
828 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
829 $app->pkgnum( $credit->pkgnum )
830 if $conf->exists('pkg-balances') && $credit->pkgnum;
833 die "guru meditation #12 and 35";
837 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
838 warn "owed_pkgnum ". $app->pkgnum;
839 $owed = $self->owed_pkgnum($app->pkgnum);
843 next unless $owed > 0;
845 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
846 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
848 $app->invnum( $self->invnum );
850 my $error = $app->insert(%options);
852 $dbh->rollback if $oldAutoCommit;
853 return "Error inserting ". $app->table. " record: $error";
855 die $error if $error;
859 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
864 =item generate_email OPTION => VALUE ...
872 sender address, required
876 alternate template name, optional
880 text attachment arrayref, optional
884 email subject, optional
888 notice name instead of "Invoice", optional
892 Returns an argument list to be passed to L<FS::Misc::send_email>.
903 my $me = '[FS::cust_bill::generate_email]';
906 'from' => $args{'from'},
907 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
911 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
912 'template' => $args{'template'},
913 'notice_name' => ( $args{'notice_name'} || 'Invoice' ),
914 'no_coupon' => $args{'no_coupon'},
917 my $cust_main = $self->cust_main;
919 if (ref($args{'to'}) eq 'ARRAY') {
920 $return{'to'} = $args{'to'};
922 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
923 $cust_main->invoicing_list
927 if ( $conf->exists('invoice_html') ) {
929 warn "$me creating HTML/text multipart message"
932 $return{'nobody'} = 1;
934 my $alternative = build MIME::Entity
935 'Type' => 'multipart/alternative',
936 'Encoding' => '7bit',
937 'Disposition' => 'inline'
941 if ( $conf->exists('invoice_email_pdf')
942 and scalar($conf->config('invoice_email_pdf_note')) ) {
944 warn "$me using 'invoice_email_pdf_note' in multipart message"
946 $data = [ map { $_ . "\n" }
947 $conf->config('invoice_email_pdf_note')
952 warn "$me not using 'invoice_email_pdf_note' in multipart message"
954 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
955 $data = $args{'print_text'};
957 $data = [ $self->print_text(\%opt) ];
962 $alternative->attach(
963 'Type' => 'text/plain',
964 #'Encoding' => 'quoted-printable',
965 'Encoding' => '7bit',
967 'Disposition' => 'inline',
970 $args{'from'} =~ /\@([\w\.\-]+)/;
971 my $from = $1 || 'example.com';
972 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
975 my $agentnum = $cust_main->agentnum;
976 if ( defined($args{'template'}) && length($args{'template'})
977 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
980 $logo = 'logo_'. $args{'template'}. '.png';
984 my $image_data = $conf->config_binary( $logo, $agentnum);
986 my $image = build MIME::Entity
987 'Type' => 'image/png',
988 'Encoding' => 'base64',
989 'Data' => $image_data,
990 'Filename' => 'logo.png',
991 'Content-ID' => "<$content_id>",
995 if($conf->exists('invoice-barcode')){
996 my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
997 $barcode = build MIME::Entity
998 'Type' => 'image/png',
999 'Encoding' => 'base64',
1000 'Data' => $self->invoice_barcode(0),
1001 'Filename' => 'barcode.png',
1002 'Content-ID' => "<$barcode_content_id>",
1004 $opt{'barcode_cid'} = $barcode_content_id;
1007 $alternative->attach(
1008 'Type' => 'text/html',
1009 'Encoding' => 'quoted-printable',
1010 'Data' => [ '<html>',
1013 ' '. encode_entities($return{'subject'}),
1016 ' <body bgcolor="#e8e8e8">',
1017 $self->print_html({ 'cid'=>$content_id, %opt }),
1021 'Disposition' => 'inline',
1022 #'Filename' => 'invoice.pdf',
1025 my @otherparts = ();
1026 if ( $cust_main->email_csv_cdr ) {
1028 push @otherparts, build MIME::Entity
1029 'Type' => 'text/csv',
1030 'Encoding' => '7bit',
1031 'Data' => [ map { "$_\n" }
1032 $self->call_details('prepend_billed_number' => 1)
1034 'Disposition' => 'attachment',
1035 'Filename' => 'usage-'. $self->invnum. '.csv',
1040 if ( $conf->exists('invoice_email_pdf') ) {
1045 # multipart/alternative
1051 my $related = build MIME::Entity 'Type' => 'multipart/related',
1052 'Encoding' => '7bit';
1054 #false laziness w/Misc::send_email
1055 $related->head->replace('Content-type',
1056 $related->mime_type.
1057 '; boundary="'. $related->head->multipart_boundary. '"'.
1058 '; type=multipart/alternative'
1061 $related->add_part($alternative);
1063 $related->add_part($image);
1065 my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
1067 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
1071 #no other attachment:
1073 # multipart/alternative
1078 $return{'content-type'} = 'multipart/related';
1079 if($conf->exists('invoice-barcode')){
1080 $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ];
1083 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
1085 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
1086 #$return{'disposition'} = 'inline';
1092 if ( $conf->exists('invoice_email_pdf') ) {
1093 warn "$me creating PDF attachment"
1096 #mime parts arguments a la MIME::Entity->build().
1097 $return{'mimeparts'} = [
1098 { $self->mimebuild_pdf(\%opt) }
1102 if ( $conf->exists('invoice_email_pdf')
1103 and scalar($conf->config('invoice_email_pdf_note')) ) {
1105 warn "$me using 'invoice_email_pdf_note'"
1107 $return{'body'} = [ map { $_ . "\n" }
1108 $conf->config('invoice_email_pdf_note')
1113 warn "$me not using 'invoice_email_pdf_note'"
1115 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1116 $return{'body'} = $args{'print_text'};
1118 $return{'body'} = [ $self->print_text(\%opt) ];
1131 Returns a list suitable for passing to MIME::Entity->build(), representing
1132 this invoice as PDF attachment.
1139 'Type' => 'application/pdf',
1140 'Encoding' => 'base64',
1141 'Data' => [ $self->print_pdf(@_) ],
1142 'Disposition' => 'attachment',
1143 'Filename' => 'invoice-'. $self->invnum. '.pdf',
1147 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1149 Sends this invoice to the destinations configured for this customer: sends
1150 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1152 Options can be passed as a hashref (recommended) or as a list of up to
1153 four values for templatename, agentnum, invoice_from and amount.
1155 I<template>, if specified, is the name of a suffix for alternate invoices.
1157 I<agentnum>, if specified, means that this invoice will only be sent for customers
1158 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
1159 single agent) or an arrayref of agentnums.
1161 I<invoice_from>, if specified, overrides the default email invoice From: address.
1163 I<amount>, if specified, only sends the invoice if the total amount owed on this
1164 invoice and all older invoices is greater than the specified amount.
1166 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1170 sub queueable_send {
1173 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1174 or die "invalid invoice number: " . $opt{invnum};
1176 my @args = ( $opt{template}, $opt{agentnum} );
1177 push @args, $opt{invoice_from}
1178 if exists($opt{invoice_from}) && $opt{invoice_from};
1180 my $error = $self->send( @args );
1181 die $error if $error;
1188 my( $template, $invoice_from, $notice_name );
1190 my $balance_over = 0;
1194 $template = $opt->{'template'} || '';
1195 if ( $agentnums = $opt->{'agentnum'} ) {
1196 $agentnums = [ $agentnums ] unless ref($agentnums);
1198 $invoice_from = $opt->{'invoice_from'};
1199 $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1200 $notice_name = $opt->{'notice_name'};
1202 $template = scalar(@_) ? shift : '';
1203 if ( scalar(@_) && $_[0] ) {
1204 $agentnums = ref($_[0]) ? shift : [ shift ];
1206 $invoice_from = shift if scalar(@_);
1207 $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1210 return 'N/A' unless ! $agentnums
1211 or grep { $_ == $self->cust_main->agentnum } @$agentnums;
1214 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
1216 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1217 $conf->config('invoice_from', $self->cust_main->agentnum );
1220 'template' => $template,
1221 'invoice_from' => $invoice_from,
1222 'notice_name' => ( $notice_name || 'Invoice' ),
1225 my @invoicing_list = $self->cust_main->invoicing_list;
1227 #$self->email_invoice(\%opt)
1229 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
1231 #$self->print_invoice(\%opt)
1233 if grep { $_ eq 'POST' } @invoicing_list; #postal
1235 $self->fax_invoice(\%opt)
1236 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1242 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
1244 Emails this invoice.
1246 Options can be passed as a hashref (recommended) or as a list of up to
1247 two values for templatename and invoice_from.
1249 I<template>, if specified, is the name of a suffix for alternate invoices.
1251 I<invoice_from>, if specified, overrides the default email invoice From: address.
1253 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1257 sub queueable_email {
1260 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1261 or die "invalid invoice number: " . $opt{invnum};
1263 my %args = ( 'template' => $opt{template} );
1264 $args{$_} = $opt{$_}
1265 foreach grep { exists($opt{$_}) && $opt{$_} }
1266 qw( invoice_from notice_name no_coupon );
1268 my $error = $self->email( \%args );
1269 die $error if $error;
1273 #sub email_invoice {
1277 my( $template, $invoice_from, $notice_name, $no_coupon );
1280 $template = $opt->{'template'} || '';
1281 $invoice_from = $opt->{'invoice_from'};
1282 $notice_name = $opt->{'notice_name'} || 'Invoice';
1283 $no_coupon = $opt->{'no_coupon'} || 0;
1285 $template = scalar(@_) ? shift : '';
1286 $invoice_from = shift if scalar(@_);
1287 $notice_name = 'Invoice';
1291 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1292 $conf->config('invoice_from', $self->cust_main->agentnum );
1294 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1295 $self->cust_main->invoicing_list;
1297 if ( ! @invoicing_list ) { #no recipients
1298 if ( $conf->exists('cust_bill-no_recipients-error') ) {
1299 die 'No recipients for customer #'. $self->custnum;
1301 #default: better to notify this person than silence
1302 @invoicing_list = ($invoice_from);
1306 my $subject = $self->email_subject($template);
1308 my $error = send_email(
1309 $self->generate_email(
1310 'from' => $invoice_from,
1311 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1312 'subject' => $subject,
1313 'template' => $template,
1314 'notice_name' => $notice_name,
1315 'no_coupon' => $no_coupon,
1318 die "can't email invoice: $error\n" if $error;
1319 #die "$error\n" if $error;
1326 #my $template = scalar(@_) ? shift : '';
1329 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1332 my $cust_main = $self->cust_main;
1333 my $name = $cust_main->name;
1334 my $name_short = $cust_main->name_short;
1335 my $invoice_number = $self->invnum;
1336 my $invoice_date = $self->_date_pretty;
1338 eval qq("$subject");
1341 =item lpr_data HASHREF | [ TEMPLATE ]
1343 Returns the postscript or plaintext for this invoice as an arrayref.
1345 Options can be passed as a hashref (recommended) or as a single optional value
1348 I<template>, if specified, is the name of a suffix for alternate invoices.
1350 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1356 my( $template, $notice_name );
1359 $template = $opt->{'template'} || '';
1360 $notice_name = $opt->{'notice_name'} || 'Invoice';
1362 $template = scalar(@_) ? shift : '';
1363 $notice_name = 'Invoice';
1367 'template' => $template,
1368 'notice_name' => $notice_name,
1371 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1372 [ $self->$method( \%opt ) ];
1375 =item print HASHREF | [ TEMPLATE ]
1377 Prints this invoice.
1379 Options can be passed as a hashref (recommended) or as a single optional
1382 I<template>, if specified, is the name of a suffix for alternate invoices.
1384 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1388 #sub print_invoice {
1391 my( $template, $notice_name );
1394 $template = $opt->{'template'} || '';
1395 $notice_name = $opt->{'notice_name'} || 'Invoice';
1397 $template = scalar(@_) ? shift : '';
1398 $notice_name = 'Invoice';
1402 'template' => $template,
1403 'notice_name' => $notice_name,
1406 if($conf->exists('invoice_print_pdf')) {
1407 # Add the invoice to the current batch.
1408 $self->batch_invoice(\%opt);
1411 do_print $self->lpr_data(\%opt);
1415 =item fax_invoice HASHREF | [ TEMPLATE ]
1419 Options can be passed as a hashref (recommended) or as a single optional
1422 I<template>, if specified, is the name of a suffix for alternate invoices.
1424 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1430 my( $template, $notice_name );
1433 $template = $opt->{'template'} || '';
1434 $notice_name = $opt->{'notice_name'} || 'Invoice';
1436 $template = scalar(@_) ? shift : '';
1437 $notice_name = 'Invoice';
1440 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1441 unless $conf->exists('invoice_latex');
1443 my $dialstring = $self->cust_main->getfield('fax');
1447 'template' => $template,
1448 'notice_name' => $notice_name,
1451 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1452 'dialstring' => $dialstring,
1454 die $error if $error;
1458 =item batch_invoice [ HASHREF ]
1460 Place this invoice into the open batch (see C<FS::bill_batch>). If there
1461 isn't an open batch, one will be created.
1466 my ($self, $opt) = @_;
1467 my $batch = FS::bill_batch->get_open_batch;
1468 my $cust_bill_batch = FS::cust_bill_batch->new({
1469 batchnum => $batch->batchnum,
1470 invnum => $self->invnum,
1472 return $cust_bill_batch->insert($opt);
1475 =item ftp_invoice [ TEMPLATENAME ]
1477 Sends this invoice data via FTP.
1479 TEMPLATENAME is unused?
1485 my $template = scalar(@_) ? shift : '';
1488 'protocol' => 'ftp',
1489 'server' => $conf->config('cust_bill-ftpserver'),
1490 'username' => $conf->config('cust_bill-ftpusername'),
1491 'password' => $conf->config('cust_bill-ftppassword'),
1492 'dir' => $conf->config('cust_bill-ftpdir'),
1493 'format' => $conf->config('cust_bill-ftpformat'),
1497 =item spool_invoice [ TEMPLATENAME ]
1499 Spools this invoice data (see L<FS::spool_csv>)
1501 TEMPLATENAME is unused?
1507 my $template = scalar(@_) ? shift : '';
1510 'format' => $conf->config('cust_bill-spoolformat'),
1511 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1515 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1517 Like B<send>, but only sends the invoice if it is the newest open invoice for
1522 sub send_if_newest {
1527 grep { $_->owed > 0 }
1528 qsearch('cust_bill', {
1529 'custnum' => $self->custnum,
1530 #'_date' => { op=>'>', value=>$self->_date },
1531 'invnum' => { op=>'>', value=>$self->invnum },
1538 =item send_csv OPTION => VALUE, ...
1540 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1544 protocol - currently only "ftp"
1550 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1551 and YYMMDDHHMMSS is a timestamp.
1553 See L</print_csv> for a description of the output format.
1558 my($self, %opt) = @_;
1562 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1563 mkdir $spooldir, 0700 unless -d $spooldir;
1565 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1566 my $file = "$spooldir/$tracctnum.csv";
1568 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1570 open(CSV, ">$file") or die "can't open $file: $!";
1578 if ( $opt{protocol} eq 'ftp' ) {
1579 eval "use Net::FTP;";
1581 $net = Net::FTP->new($opt{server}) or die @$;
1583 die "unknown protocol: $opt{protocol}";
1586 $net->login( $opt{username}, $opt{password} )
1587 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1589 $net->binary or die "can't set binary mode";
1591 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1593 $net->put($file) or die "can't put $file: $!";
1603 Spools CSV invoice data.
1609 =item format - 'default' or 'billco'
1611 =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>).
1613 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1615 =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.
1622 my($self, %opt) = @_;
1624 my $cust_main = $self->cust_main;
1626 if ( $opt{'dest'} ) {
1627 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1628 $cust_main->invoicing_list;
1629 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1630 || ! keys %invoicing_list;
1633 if ( $opt{'balanceover'} ) {
1635 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1638 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1639 mkdir $spooldir, 0700 unless -d $spooldir;
1641 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1645 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1646 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1649 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1651 open(CSV, ">>$file") or die "can't open $file: $!";
1652 flock(CSV, LOCK_EX);
1657 if ( lc($opt{'format'}) eq 'billco' ) {
1659 flock(CSV, LOCK_UN);
1664 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1667 open(CSV,">>$file") or die "can't open $file: $!";
1668 flock(CSV, LOCK_EX);
1674 flock(CSV, LOCK_UN);
1681 =item print_csv OPTION => VALUE, ...
1683 Returns CSV data for this invoice.
1687 format - 'default' or 'billco'
1689 Returns a list consisting of two scalars. The first is a single line of CSV
1690 header information for this invoice. The second is one or more lines of CSV
1691 detail information for this invoice.
1693 If I<format> is not specified or "default", the fields of the CSV file are as
1696 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1700 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1702 B<record_type> is C<cust_bill> for the initial header line only. The
1703 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1704 fields are filled in.
1706 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1707 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1710 =item invnum - invoice number
1712 =item custnum - customer number
1714 =item _date - invoice date
1716 =item charged - total invoice amount
1718 =item first - customer first name
1720 =item last - customer first name
1722 =item company - company name
1724 =item address1 - address line 1
1726 =item address2 - address line 1
1736 =item pkg - line item description
1738 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1740 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1742 =item sdate - start date for recurring fee
1744 =item edate - end date for recurring fee
1748 If I<format> is "billco", the fields of the header CSV file are as follows:
1750 +-------------------------------------------------------------------+
1751 | FORMAT HEADER FILE |
1752 |-------------------------------------------------------------------|
1753 | Field | Description | Name | Type | Width |
1754 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1755 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1756 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1757 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1758 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1759 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1760 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1761 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1762 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1763 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1764 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1765 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1766 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1767 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1768 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1769 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1770 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1771 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1772 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1773 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1774 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1775 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1776 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1777 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1778 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1779 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1780 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1781 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1782 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1783 +-------+-------------------------------+------------+------+-------+
1785 If I<format> is "billco", the fields of the detail CSV file are as follows:
1787 FORMAT FOR DETAIL FILE
1789 Field | Description | Name | Type | Width
1790 1 | N/A-Leave Empty | RC | CHAR | 2
1791 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1792 3 | Account Number | TRACCTNUM | CHAR | 15
1793 4 | Invoice Number | TRINVOICE | CHAR | 15
1794 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1795 6 | Transaction Detail | DETAILS | CHAR | 100
1796 7 | Amount | AMT | NUM* | 9
1797 8 | Line Format Control** | LNCTRL | CHAR | 2
1798 9 | Grouping Code | GROUP | CHAR | 2
1799 10 | User Defined | ACCT CODE | CHAR | 15
1804 my($self, %opt) = @_;
1806 eval "use Text::CSV_XS";
1809 my $cust_main = $self->cust_main;
1811 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1813 if ( lc($opt{'format'}) eq 'billco' ) {
1816 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1818 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1820 my( $previous_balance, @unused ) = $self->previous; #previous balance
1822 my $pmt_cr_applied = 0;
1823 $pmt_cr_applied += $_->{'amount'}
1824 foreach ( $self->_items_payments, $self->_items_credits ) ;
1826 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1829 '', # 1 | N/A-Leave Empty CHAR 2
1830 '', # 2 | N/A-Leave Empty CHAR 15
1831 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1832 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1833 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1834 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1835 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1836 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1837 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1838 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1839 '', # 10 | Ancillary Billing Information CHAR 30
1840 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1841 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1844 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1847 $duedate, # 14 | Bill Due Date CHAR 10
1849 $previous_balance, # 15 | Previous Balance NUM* 9
1850 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1851 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1852 $totaldue, # 18 | Total Amt Due NUM* 9
1853 $totaldue, # 19 | Total Amt Due NUM* 9
1854 '', # 20 | 30 Day Aging NUM* 9
1855 '', # 21 | 60 Day Aging NUM* 9
1856 '', # 22 | 90 Day Aging NUM* 9
1857 'N', # 23 | Y/N CHAR 1
1858 '', # 24 | Remittance automation CHAR 100
1859 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1860 $self->custnum, # 26 | Customer Reference Number CHAR 15
1861 '0', # 27 | Federal Tax*** NUM* 9
1862 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1863 '0', # 29 | Other Taxes & Fees*** NUM* 9
1872 time2str("%x", $self->_date),
1873 sprintf("%.2f", $self->charged),
1874 ( map { $cust_main->getfield($_) }
1875 qw( first last company address1 address2 city state zip country ) ),
1877 ) or die "can't create csv";
1880 my $header = $csv->string. "\n";
1883 if ( lc($opt{'format'}) eq 'billco' ) {
1886 foreach my $item ( $self->_items_pkg ) {
1889 '', # 1 | N/A-Leave Empty CHAR 2
1890 '', # 2 | N/A-Leave Empty CHAR 15
1891 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1892 $self->invnum, # 4 | Invoice Number CHAR 15
1893 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1894 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1895 $item->{'amount'}, # 7 | Amount NUM* 9
1896 '', # 8 | Line Format Control** CHAR 2
1897 '', # 9 | Grouping Code CHAR 2
1898 '', # 10 | User Defined CHAR 15
1901 $detail .= $csv->string. "\n";
1907 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1909 my($pkg, $setup, $recur, $sdate, $edate);
1910 if ( $cust_bill_pkg->pkgnum ) {
1912 ($pkg, $setup, $recur, $sdate, $edate) = (
1913 $cust_bill_pkg->part_pkg->pkg,
1914 ( $cust_bill_pkg->setup != 0
1915 ? sprintf("%.2f", $cust_bill_pkg->setup )
1917 ( $cust_bill_pkg->recur != 0
1918 ? sprintf("%.2f", $cust_bill_pkg->recur )
1920 ( $cust_bill_pkg->sdate
1921 ? time2str("%x", $cust_bill_pkg->sdate)
1923 ($cust_bill_pkg->edate
1924 ?time2str("%x", $cust_bill_pkg->edate)
1928 } else { #pkgnum tax
1929 next unless $cust_bill_pkg->setup != 0;
1930 $pkg = $cust_bill_pkg->desc;
1931 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
1932 ( $sdate, $edate ) = ( '', '' );
1938 ( map { '' } (1..11) ),
1939 ($pkg, $setup, $recur, $sdate, $edate)
1940 ) or die "can't create csv";
1942 $detail .= $csv->string. "\n";
1948 ( $header, $detail );
1954 Pays this invoice with a compliemntary payment. If there is an error,
1955 returns the error, otherwise returns false.
1961 my $cust_pay = new FS::cust_pay ( {
1962 'invnum' => $self->invnum,
1963 'paid' => $self->owed,
1966 'payinfo' => $self->cust_main->payinfo,
1974 Attempts to pay this invoice with a credit card payment via a
1975 Business::OnlinePayment realtime gateway. See
1976 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1977 for supported processors.
1983 $self->realtime_bop( 'CC', @_ );
1988 Attempts to pay this invoice with an electronic check (ACH) payment via a
1989 Business::OnlinePayment realtime gateway. See
1990 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1991 for supported processors.
1997 $self->realtime_bop( 'ECHECK', @_ );
2002 Attempts to pay this invoice with phone bill (LEC) payment via a
2003 Business::OnlinePayment realtime gateway. See
2004 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2005 for supported processors.
2011 $self->realtime_bop( 'LEC', @_ );
2015 my( $self, $method ) = (shift,shift);
2018 my $cust_main = $self->cust_main;
2019 my $balance = $cust_main->balance;
2020 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2021 $amount = sprintf("%.2f", $amount);
2022 return "not run (balance $balance)" unless $amount > 0;
2024 my $description = 'Internet Services';
2025 if ( $conf->exists('business-onlinepayment-description') ) {
2026 my $dtempl = $conf->config('business-onlinepayment-description');
2028 my $agent_obj = $cust_main->agent
2029 or die "can't retreive agent for $cust_main (agentnum ".
2030 $cust_main->agentnum. ")";
2031 my $agent = $agent_obj->agent;
2032 my $pkgs = join(', ',
2033 map { $_->part_pkg->pkg }
2034 grep { $_->pkgnum } $self->cust_bill_pkg
2036 $description = eval qq("$dtempl");
2039 $cust_main->realtime_bop($method, $amount,
2040 'description' => $description,
2041 'invnum' => $self->invnum,
2042 #this didn't do what we want, it just calls apply_payments_and_credits
2044 'apply_to_invoice' => 1,
2047 #this changes application behavior: auto payments
2048 #triggered against a specific invoice are now applied
2049 #to that invoice instead of oldest open.
2055 =item batch_card OPTION => VALUE...
2057 Adds a payment for this invoice to the pending credit card batch (see
2058 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2059 runs the payment using a realtime gateway.
2064 my ($self, %options) = @_;
2065 my $cust_main = $self->cust_main;
2067 $options{invnum} = $self->invnum;
2069 $cust_main->batch_card(%options);
2072 sub _agent_template {
2074 $self->cust_main->agent_template;
2077 sub _agent_invoice_from {
2079 $self->cust_main->agent_invoice_from;
2082 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2084 Returns an text invoice, as a list of lines.
2086 Options can be passed as a hashref (recommended) or as a list of time, template
2087 and then any key/value pairs for any other options.
2089 I<time>, if specified, is used to control the printing of overdue messages. The
2090 default is now. It isn't the date of the invoice; that's the `_date' field.
2091 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2092 L<Time::Local> and L<Date::Parse> for conversion functions.
2094 I<template>, if specified, is the name of a suffix for alternate invoices.
2096 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2102 my( $today, $template, %opt );
2104 %opt = %{ shift() };
2105 $today = delete($opt{'time'}) || '';
2106 $template = delete($opt{template}) || '';
2108 ( $today, $template, %opt ) = @_;
2111 my %params = ( 'format' => 'template' );
2112 $params{'time'} = $today if $today;
2113 $params{'template'} = $template if $template;
2114 $params{$_} = $opt{$_}
2115 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2117 $self->print_generic( %params );
2120 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2122 Internal method - returns a filename of a filled-in LaTeX template for this
2123 invoice (Note: add ".tex" to get the actual filename), and a filename of
2124 an associated logo (with the .eps extension included).
2126 See print_ps and print_pdf for methods that return PostScript and PDF output.
2128 Options can be passed as a hashref (recommended) or as a list of time, template
2129 and then any key/value pairs for any other options.
2131 I<time>, if specified, is used to control the printing of overdue messages. The
2132 default is now. It isn't the date of the invoice; that's the `_date' field.
2133 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2134 L<Time::Local> and L<Date::Parse> for conversion functions.
2136 I<template>, if specified, is the name of a suffix for alternate invoices.
2138 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2144 my( $today, $template, %opt );
2146 %opt = %{ shift() };
2147 $today = delete($opt{'time'}) || '';
2148 $template = delete($opt{template}) || '';
2150 ( $today, $template, %opt ) = @_;
2153 my %params = ( 'format' => 'latex' );
2154 $params{'time'} = $today if $today;
2155 $params{'template'} = $template if $template;
2156 $params{$_} = $opt{$_}
2157 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2159 $template ||= $self->_agent_template;
2161 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2162 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2166 ) or die "can't open temp file: $!\n";
2168 my $agentnum = $self->cust_main->agentnum;
2170 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2171 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2172 or die "can't write temp file: $!\n";
2174 print $lh $conf->config_binary('logo.eps', $agentnum)
2175 or die "can't write temp file: $!\n";
2178 $params{'logo_file'} = $lh->filename;
2180 if($conf->exists('invoice-barcode')){
2181 my $png_file = $self->invoice_barcode($dir);
2182 my $eps_file = $png_file;
2183 $eps_file =~ s/\.png$/.eps/g;
2184 $png_file =~ /(barcode.*png)/;
2186 $eps_file =~ /(barcode.*eps)/;
2189 my $curr_dir = cwd();
2191 # after painfuly long experimentation, it was determined that sam2p won't
2192 # accept : and other chars in the path, no matter how hard I tried to
2193 # escape them, hence the chdir (and chdir back, just to be safe)
2194 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
2195 or die "sam2p failed: $!\n";
2199 $params{'barcode_file'} = $eps_file;
2202 my @filled_in = $self->print_generic( %params );
2204 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2208 ) or die "can't open temp file: $!\n";
2209 print $fh join('', @filled_in );
2212 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2213 return ($1, $params{'logo_file'}, $params{'barcode_file'});
2217 =item invoice_barcode DIR_OR_FALSE
2219 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2220 it is taken as the temp directory where the PNG file will be generated and the
2221 PNG file name is returned. Otherwise, the PNG image itself is returned.
2225 sub invoice_barcode {
2226 my ($self, $dir) = (shift,shift);
2228 my $gdbar = new GD::Barcode('Code39',$self->invnum);
2229 die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2230 my $gd = $gdbar->plot(Height => 30);
2233 my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2237 ) or die "can't open temp file: $!\n";
2238 print $bh $gd->png or die "cannot write barcode to file: $!\n";
2239 my $png_file = $bh->filename;
2246 =item print_generic OPTION => VALUE ...
2248 Internal method - returns a filled-in template for this invoice as a scalar.
2250 See print_ps and print_pdf for methods that return PostScript and PDF output.
2252 Non optional options include
2253 format - latex, html, template
2255 Optional options include
2257 template - a value used as a suffix for a configuration template
2259 time - a value used to control the printing of overdue messages. The
2260 default is now. It isn't the date of the invoice; that's the `_date' field.
2261 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2262 L<Time::Local> and L<Date::Parse> for conversion functions.
2266 unsquelch_cdr - overrides any per customer cdr squelching when true
2268 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2272 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2273 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2274 # yes: fixed width (dot matrix) text printing will be borked
2277 my( $self, %params ) = @_;
2278 my $today = $params{today} ? $params{today} : time;
2279 warn "$me print_generic called on $self with suffix $params{template}\n"
2282 my $format = $params{format};
2283 die "Unknown format: $format"
2284 unless $format =~ /^(latex|html|template)$/;
2286 my $cust_main = $self->cust_main;
2287 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2288 unless $cust_main->payname
2289 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2291 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2292 'html' => [ '<%=', '%>' ],
2293 'template' => [ '{', '}' ],
2296 warn "$me print_generic creating template\n"
2299 #create the template
2300 my $template = $params{template} ? $params{template} : $self->_agent_template;
2301 my $templatefile = "invoice_$format";
2302 $templatefile .= "_$template"
2303 if length($template) && $conf->exists($templatefile."_$template");
2304 my @invoice_template = map "$_\n", $conf->config($templatefile)
2305 or die "cannot load config data $templatefile";
2308 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2309 #change this to a die when the old code is removed
2310 warn "old-style invoice template $templatefile; ".
2311 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2312 $old_latex = 'true';
2313 @invoice_template = _translate_old_latex_format(@invoice_template);
2316 warn "$me print_generic creating T:T object\n"
2319 my $text_template = new Text::Template(
2321 SOURCE => \@invoice_template,
2322 DELIMITERS => $delimiters{$format},
2325 warn "$me print_generic compiling T:T object\n"
2328 $text_template->compile()
2329 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2332 # additional substitution could possibly cause breakage in existing templates
2333 my %convert_maps = (
2335 'notes' => sub { map "$_", @_ },
2336 'footer' => sub { map "$_", @_ },
2337 'smallfooter' => sub { map "$_", @_ },
2338 'returnaddress' => sub { map "$_", @_ },
2339 'coupon' => sub { map "$_", @_ },
2340 'summary' => sub { map "$_", @_ },
2346 s/%%(.*)$/<!-- $1 -->/g;
2347 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2348 s/\\begin\{enumerate\}/<ol>/g;
2350 s/\\end\{enumerate\}/<\/ol>/g;
2351 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2360 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2362 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2367 s/\\\\\*?\s*$/<BR>/;
2368 s/\\hyphenation\{[\w\s\-]+}//;
2373 'coupon' => sub { "" },
2374 'summary' => sub { "" },
2381 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2382 s/\\begin\{enumerate\}//g;
2384 s/\\end\{enumerate\}//g;
2385 s/\\textbf\{(.*)\}/$1/g;
2392 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2394 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2399 s/\\\\\*?\s*$/\n/; # dubious
2400 s/\\hyphenation\{[\w\s\-]+}//;
2404 'coupon' => sub { "" },
2405 'summary' => sub { "" },
2410 # hashes for differing output formats
2411 my %nbsps = ( 'latex' => '~',
2412 'html' => '', # '&nbps;' would be nice
2413 'template' => '', # not used
2415 my $nbsp = $nbsps{$format};
2417 my %escape_functions = ( 'latex' => \&_latex_escape,
2418 'html' => \&_html_escape_nbsp,#\&encode_entities,
2419 'template' => sub { shift },
2421 my $escape_function = $escape_functions{$format};
2422 my $escape_function_nonbsp = ($format eq 'html')
2423 ? \&_html_escape : $escape_function;
2425 my %date_formats = ( 'latex' => $date_format_long,
2426 'html' => $date_format_long,
2429 $date_formats{'html'} =~ s/ / /g;
2431 my $date_format = $date_formats{$format};
2433 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2435 'html' => sub { return '<b>'. shift(). '</b>'
2437 'template' => sub { shift },
2439 my $embolden_function = $embolden_functions{$format};
2441 my %newline_tokens = ( 'latex' => '\\\\',
2445 my $newline_token = $newline_tokens{$format};
2447 warn "$me generating template variables\n"
2450 # generate template variables
2453 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2457 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2463 $returnaddress = join("\n",
2464 $conf->config_orbase("invoice_${format}returnaddress", $template)
2467 } elsif ( grep /\S/,
2468 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2470 my $convert_map = $convert_maps{$format}{'returnaddress'};
2473 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2478 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2480 my $convert_map = $convert_maps{$format}{'returnaddress'};
2481 $returnaddress = join( "\n", &$convert_map(
2482 map { s/( {2,})/'~' x length($1)/eg;
2486 ( $conf->config('company_name', $self->cust_main->agentnum),
2487 $conf->config('company_address', $self->cust_main->agentnum),
2494 my $warning = "Couldn't find a return address; ".
2495 "do you need to set the company_address configuration value?";
2497 $returnaddress = $nbsp;
2498 #$returnaddress = $warning;
2502 warn "$me generating invoice data\n"
2505 my $agentnum = $self->cust_main->agentnum;
2507 my %invoice_data = (
2510 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2511 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2512 'returnaddress' => $returnaddress,
2513 'agent' => &$escape_function($cust_main->agent->agent),
2516 'invnum' => $self->invnum,
2517 'date' => time2str($date_format, $self->_date),
2518 'today' => time2str($date_format_long, $today),
2519 'terms' => $self->terms,
2520 'template' => $template, #params{'template'},
2521 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2522 'current_charges' => sprintf("%.2f", $self->charged),
2523 'duedate' => $self->due_date2str($rdate_format), #date_format?
2526 'custnum' => $cust_main->display_custnum,
2527 'agent_custid' => &$escape_function($cust_main->agent_custid),
2528 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2529 payname company address1 address2 city state zip fax
2533 'ship_enable' => $conf->exists('invoice-ship_address'),
2534 'unitprices' => $conf->exists('invoice-unitprice'),
2535 'smallernotes' => $conf->exists('invoice-smallernotes'),
2536 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2537 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2539 #layout info -- would be fancy to calc some of this and bury the template
2541 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2542 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2543 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2544 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2545 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2546 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2547 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2548 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2549 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2550 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2552 # better hang on to conf_dir for a while (for old templates)
2553 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2555 #these are only used when doing paged plaintext
2561 my $min_sdate = 999999999999;
2563 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2564 next unless $cust_bill_pkg->pkgnum > 0;
2565 $min_sdate = $cust_bill_pkg->sdate
2566 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2567 $max_edate = $cust_bill_pkg->edate
2568 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2571 $invoice_data{'bill_period'} = '';
2572 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
2573 . " to " . time2str('%e %h', $max_edate)
2574 if ($max_edate != 0 && $min_sdate != 999999999999);
2576 $invoice_data{finance_section} = '';
2577 if ( $conf->config('finance_pkgclass') ) {
2579 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2580 $invoice_data{finance_section} = $pkg_class->categoryname;
2582 $invoice_data{finance_amount} = '0.00';
2583 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2585 my $countrydefault = $conf->config('countrydefault') || 'US';
2586 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2587 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2588 my $method = $prefix.$_;
2589 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2591 $invoice_data{'ship_country'} = ''
2592 if ( $invoice_data{'ship_country'} eq $countrydefault );
2594 $invoice_data{'cid'} = $params{'cid'}
2597 if ( $cust_main->country eq $countrydefault ) {
2598 $invoice_data{'country'} = '';
2600 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2604 $invoice_data{'address'} = \@address;
2606 $cust_main->payname.
2607 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2608 ? " (P.O. #". $cust_main->payinfo. ")"
2612 push @address, $cust_main->company
2613 if $cust_main->company;
2614 push @address, $cust_main->address1;
2615 push @address, $cust_main->address2
2616 if $cust_main->address2;
2618 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2619 push @address, $invoice_data{'country'}
2620 if $invoice_data{'country'};
2622 while (scalar(@address) < 5);
2624 $invoice_data{'logo_file'} = $params{'logo_file'}
2625 if $params{'logo_file'};
2626 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2627 if $params{'barcode_file'};
2628 $invoice_data{'barcode_img'} = $params{'barcode_img'}
2629 if $params{'barcode_img'};
2630 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2631 if $params{'barcode_cid'};
2633 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2634 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2635 #my $balance_due = $self->owed + $pr_total - $cr_total;
2636 my $balance_due = $self->owed + $pr_total;
2637 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2638 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2639 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2640 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2642 my $summarypage = '';
2643 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2646 $invoice_data{'summarypage'} = $summarypage;
2648 warn "$me substituting variables in notes, footer, smallfooter\n"
2651 my @include = (qw( notes footer smallfooter ));
2652 push @include, 'coupon' unless $params{'no_coupon'};
2653 foreach my $include (@include) {
2655 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2658 if ( $conf->exists($inc_file, $agentnum)
2659 && length( $conf->config($inc_file, $agentnum) ) ) {
2661 @inc_src = $conf->config($inc_file, $agentnum);
2665 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2667 my $convert_map = $convert_maps{$format}{$include};
2669 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2670 s/--\@\]/$delimiters{$format}[1]/g;
2673 &$convert_map( $conf->config($inc_file, $agentnum) );
2677 my $inc_tt = new Text::Template (
2679 SOURCE => [ map "$_\n", @inc_src ],
2680 DELIMITERS => $delimiters{$format},
2681 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2683 unless ( $inc_tt->compile() ) {
2684 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2685 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2689 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2691 $invoice_data{$include} =~ s/\n+$//
2692 if ($format eq 'latex');
2695 $invoice_data{'po_line'} =
2696 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2697 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2700 my %money_chars = ( 'latex' => '',
2701 'html' => $conf->config('money_char') || '$',
2704 my $money_char = $money_chars{$format};
2706 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2707 'html' => $conf->config('money_char') || '$',
2710 my $other_money_char = $other_money_chars{$format};
2711 $invoice_data{'dollar'} = $other_money_char;
2713 my @detail_items = ();
2714 my @total_items = ();
2718 $invoice_data{'detail_items'} = \@detail_items;
2719 $invoice_data{'total_items'} = \@total_items;
2720 $invoice_data{'buf'} = \@buf;
2721 $invoice_data{'sections'} = \@sections;
2723 warn "$me generating sections\n"
2726 my $previous_section = { 'description' => 'Previous Charges',
2727 'subtotal' => $other_money_char.
2728 sprintf('%.2f', $pr_total),
2729 'summarized' => $summarypage ? 'Y' : '',
2731 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
2732 join(' / ', map { $cust_main->balance_date_range(@$_) }
2733 $self->_prior_month30s
2735 if $conf->exists('invoice_include_aging');
2738 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2739 'subtotal' => $taxtotal, # adjusted below
2740 'summarized' => $summarypage ? 'Y' : '',
2742 my $tax_weight = _pkg_category($tax_section->{description})
2743 ? _pkg_category($tax_section->{description})->weight
2745 $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2746 $tax_section->{'sort_weight'} = $tax_weight;
2749 my $adjusttotal = 0;
2750 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2751 'subtotal' => 0, # adjusted below
2752 'summarized' => $summarypage ? 'Y' : '',
2754 my $adjust_weight = _pkg_category($adjust_section->{description})
2755 ? _pkg_category($adjust_section->{description})->weight
2757 $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2758 $adjust_section->{'sort_weight'} = $adjust_weight;
2760 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2761 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2762 $invoice_data{'multisection'} = $multisection;
2763 my $late_sections = [];
2764 my $extra_sections = [];
2765 my $extra_lines = ();
2766 if ( $multisection ) {
2767 ($extra_sections, $extra_lines) =
2768 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2769 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2771 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2773 push @detail_items, @$extra_lines if $extra_lines;
2775 $self->_items_sections( $late_sections, # this could stand a refactor
2777 $escape_function_nonbsp,
2781 if ($conf->exists('svc_phone_sections')) {
2782 my ($phone_sections, $phone_lines) =
2783 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2784 push @{$late_sections}, @$phone_sections;
2785 push @detail_items, @$phone_lines;
2788 push @sections, { 'description' => '', 'subtotal' => '' };
2791 unless ( $conf->exists('disable_previous_balance')
2792 || $conf->exists('previous_balance-summary_only')
2796 warn "$me adding previous balances\n"
2799 foreach my $line_item ( $self->_items_previous ) {
2802 ext_description => [],
2804 $detail->{'ref'} = $line_item->{'pkgnum'};
2805 $detail->{'quantity'} = 1;
2806 $detail->{'section'} = $previous_section;
2807 $detail->{'description'} = &$escape_function($line_item->{'description'});
2808 if ( exists $line_item->{'ext_description'} ) {
2809 @{$detail->{'ext_description'}} = map {
2810 &$escape_function($_);
2811 } @{$line_item->{'ext_description'}};
2813 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2814 $line_item->{'amount'};
2815 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2817 push @detail_items, $detail;
2818 push @buf, [ $detail->{'description'},
2819 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2825 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2826 push @buf, ['','-----------'];
2827 push @buf, [ 'Total Previous Balance',
2828 $money_char. sprintf("%10.2f", $pr_total) ];
2832 if ( $conf->exists('svc_phone-did-summary') ) {
2833 warn "$me adding DID summary\n"
2836 my ($didsummary,$minutes) = $self->_did_summary;
2837 my $didsummary_desc = 'DID Activity Summary (Past 30 days)';
2839 { 'description' => $didsummary_desc,
2840 'ext_description' => [ $didsummary, $minutes ],
2845 foreach my $section (@sections, @$late_sections) {
2847 warn "$me adding section \n". Dumper($section)
2850 # begin some normalization
2851 $section->{'subtotal'} = $section->{'amount'}
2853 && !exists($section->{subtotal})
2854 && exists($section->{amount});
2856 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2857 if ( $invoice_data{finance_section} &&
2858 $section->{'description'} eq $invoice_data{finance_section} );
2860 $section->{'subtotal'} = $other_money_char.
2861 sprintf('%.2f', $section->{'subtotal'})
2864 # continue some normalization
2865 $section->{'amount'} = $section->{'subtotal'}
2869 if ( $section->{'description'} ) {
2870 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2875 warn "$me setting options\n"
2878 my $multilocation = scalar($cust_main->cust_location); #too expensive?
2880 $options{'section'} = $section if $multisection;
2881 $options{'format'} = $format;
2882 $options{'escape_function'} = $escape_function;
2883 $options{'format_function'} = sub { () } unless $unsquelched;
2884 $options{'unsquelched'} = $unsquelched;
2885 $options{'summary_page'} = $summarypage;
2886 $options{'skip_usage'} =
2887 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2888 $options{'multilocation'} = $multilocation;
2889 $options{'multisection'} = $multisection;
2891 warn "$me searching for line items\n"
2894 foreach my $line_item ( $self->_items_pkg(%options) ) {
2896 warn "$me adding line item $line_item\n"
2900 ext_description => [],
2902 $detail->{'ref'} = $line_item->{'pkgnum'};
2903 $detail->{'quantity'} = $line_item->{'quantity'};
2904 $detail->{'section'} = $section;
2905 $detail->{'description'} = &$escape_function($line_item->{'description'});
2906 if ( exists $line_item->{'ext_description'} ) {
2907 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2909 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2910 $line_item->{'amount'};
2911 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2912 $line_item->{'unit_amount'};
2913 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2915 push @detail_items, $detail;
2916 push @buf, ( [ $detail->{'description'},
2917 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2919 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2923 if ( $section->{'description'} ) {
2924 push @buf, ( ['','-----------'],
2925 [ $section->{'description'}. ' sub-total',
2926 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2935 $invoice_data{current_less_finance} =
2936 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2938 if ( $multisection && !$conf->exists('disable_previous_balance')
2939 || $conf->exists('previous_balance-summary_only') )
2941 unshift @sections, $previous_section if $pr_total;
2944 warn "$me adding taxes\n"
2947 foreach my $tax ( $self->_items_tax ) {
2949 $taxtotal += $tax->{'amount'};
2951 my $description = &$escape_function( $tax->{'description'} );
2952 my $amount = sprintf( '%.2f', $tax->{'amount'} );
2954 if ( $multisection ) {
2956 my $money = $old_latex ? '' : $money_char;
2957 push @detail_items, {
2958 ext_description => [],
2961 description => $description,
2962 amount => $money. $amount,
2964 section => $tax_section,
2969 push @total_items, {
2970 'total_item' => $description,
2971 'total_amount' => $other_money_char. $amount,
2976 push @buf,[ $description,
2977 $money_char. $amount,
2984 $total->{'total_item'} = 'Sub-total';
2985 $total->{'total_amount'} =
2986 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2988 if ( $multisection ) {
2989 $tax_section->{'subtotal'} = $other_money_char.
2990 sprintf('%.2f', $taxtotal);
2991 $tax_section->{'pretotal'} = 'New charges sub-total '.
2992 $total->{'total_amount'};
2993 push @sections, $tax_section if $taxtotal;
2995 unshift @total_items, $total;
2998 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3000 push @buf,['','-----------'];
3001 push @buf,[( $conf->exists('disable_previous_balance')
3003 : 'Total New Charges'
3005 $money_char. sprintf("%10.2f",$self->charged) ];
3011 $item = $conf->config('previous_balance-exclude_from_total')
3012 || 'Total New Charges'
3013 if $conf->exists('previous_balance-exclude_from_total');
3014 my $amount = $self->charged +
3015 ( $conf->exists('disable_previous_balance') ||
3016 $conf->exists('previous_balance-exclude_from_total')
3020 $total->{'total_item'} = &$embolden_function($item);
3021 $total->{'total_amount'} =
3022 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
3023 if ( $multisection ) {
3024 if ( $adjust_section->{'sort_weight'} ) {
3025 $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
3026 sprintf("%.2f", ($self->billing_balance || 0) );
3028 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
3029 sprintf('%.2f', $self->charged );
3032 push @total_items, $total;
3034 push @buf,['','-----------'];
3037 sprintf( '%10.2f', $amount )
3042 unless ( $conf->exists('disable_previous_balance') ) {
3043 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3046 my $credittotal = 0;
3047 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3050 $total->{'total_item'} = &$escape_function($credit->{'description'});
3051 $credittotal += $credit->{'amount'};
3052 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3053 $adjusttotal += $credit->{'amount'};
3054 if ( $multisection ) {
3055 my $money = $old_latex ? '' : $money_char;
3056 push @detail_items, {
3057 ext_description => [],
3060 description => &$escape_function($credit->{'description'}),
3061 amount => $money. $credit->{'amount'},
3063 section => $adjust_section,
3066 push @total_items, $total;
3070 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3073 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3074 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3078 my $paymenttotal = 0;
3079 foreach my $payment ( $self->_items_payments ) {
3081 $total->{'total_item'} = &$escape_function($payment->{'description'});
3082 $paymenttotal += $payment->{'amount'};
3083 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3084 $adjusttotal += $payment->{'amount'};
3085 if ( $multisection ) {
3086 my $money = $old_latex ? '' : $money_char;
3087 push @detail_items, {
3088 ext_description => [],
3091 description => &$escape_function($payment->{'description'}),
3092 amount => $money. $payment->{'amount'},
3094 section => $adjust_section,
3097 push @total_items, $total;
3099 push @buf, [ $payment->{'description'},
3100 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3103 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3105 if ( $multisection ) {
3106 $adjust_section->{'subtotal'} = $other_money_char.
3107 sprintf('%.2f', $adjusttotal);
3108 push @sections, $adjust_section
3109 unless $adjust_section->{sort_weight};
3114 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3115 $total->{'total_amount'} =
3116 &$embolden_function(
3117 $other_money_char. sprintf('%.2f', $summarypage
3119 $self->billing_balance
3120 : $self->owed + $pr_total
3123 if ( $multisection && !$adjust_section->{sort_weight} ) {
3124 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3125 $total->{'total_amount'};
3127 push @total_items, $total;
3129 push @buf,['','-----------'];
3130 push @buf,[$self->balance_due_msg, $money_char.
3131 sprintf("%10.2f", $balance_due ) ];
3134 if ( $conf->exists('previous_balance-show_credit')
3135 and $cust_main->balance < 0 ) {
3136 my $credit_total = {
3137 'total_item' => &$embolden_function($self->credit_balance_msg),
3138 'total_amount' => &$embolden_function(
3139 $other_money_char. sprintf('%.2f', -$cust_main->balance)
3142 if ( $multisection ) {
3143 $adjust_section->{'posttotal'} .= $newline_token .
3144 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3147 push @total_items, $credit_total;
3149 push @buf,['','-----------'];
3150 push @buf,[$self->credit_balance_msg, $money_char.
3151 sprintf("%10.2f", -$cust_main->balance ) ];
3155 if ( $multisection ) {
3156 if ($conf->exists('svc_phone_sections')) {
3158 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3159 $total->{'total_amount'} =
3160 &$embolden_function(
3161 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3163 my $last_section = pop @sections;
3164 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3165 $total->{'total_amount'};
3166 push @sections, $last_section;
3168 push @sections, @$late_sections
3172 my @includelist = ();
3173 push @includelist, 'summary' if $summarypage;
3174 foreach my $include ( @includelist ) {
3176 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3179 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3181 @inc_src = $conf->config($inc_file, $agentnum);
3185 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3187 my $convert_map = $convert_maps{$format}{$include};
3189 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3190 s/--\@\]/$delimiters{$format}[1]/g;
3193 &$convert_map( $conf->config($inc_file, $agentnum) );
3197 my $inc_tt = new Text::Template (
3199 SOURCE => [ map "$_\n", @inc_src ],
3200 DELIMITERS => $delimiters{$format},
3201 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3203 unless ( $inc_tt->compile() ) {
3204 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3205 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3209 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3211 $invoice_data{$include} =~ s/\n+$//
3212 if ($format eq 'latex');
3217 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3218 /invoice_lines\((\d*)\)/;
3219 $invoice_lines += $1 || scalar(@buf);
3222 die "no invoice_lines() functions in template?"
3223 if ( $format eq 'template' && !$wasfunc );
3225 if ($format eq 'template') {
3227 if ( $invoice_lines ) {
3228 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3229 $invoice_data{'total_pages'}++
3230 if scalar(@buf) % $invoice_lines;
3233 #setup subroutine for the template
3234 sub FS::cust_bill::_template::invoice_lines {
3235 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
3237 scalar(@FS::cust_bill::_template::buf)
3238 ? shift @FS::cust_bill::_template::buf
3247 push @collect, split("\n",
3248 $text_template->fill_in( HASH => \%invoice_data,
3249 PACKAGE => 'FS::cust_bill::_template'
3252 $FS::cust_bill::_template::page++;
3254 map "$_\n", @collect;
3256 warn "filling in template for invoice ". $self->invnum. "\n"
3258 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3261 $text_template->fill_in(HASH => \%invoice_data);
3265 # helper routine for generating date ranges
3266 sub _prior_month30s {
3269 [ 1, 2592000 ], # 0-30 days ago
3270 [ 2592000, 5184000 ], # 30-60 days ago
3271 [ 5184000, 7776000 ], # 60-90 days ago
3272 [ 7776000, 0 ], # 90+ days ago
3275 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3276 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3281 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3283 Returns an postscript invoice, as a scalar.
3285 Options can be passed as a hashref (recommended) or as a list of time, template
3286 and then any key/value pairs for any other options.
3288 I<time> an optional value used to control the printing of overdue messages. The
3289 default is now. It isn't the date of the invoice; that's the `_date' field.
3290 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3291 L<Time::Local> and L<Date::Parse> for conversion functions.
3293 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3300 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3301 my $ps = generate_ps($file);
3303 unlink($barcodefile);
3308 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3310 Returns an PDF invoice, as a scalar.
3312 Options can be passed as a hashref (recommended) or as a list of time, template
3313 and then any key/value pairs for any other options.
3315 I<time> an optional value used to control the printing of overdue messages. The
3316 default is now. It isn't the date of the invoice; that's the `_date' field.
3317 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3318 L<Time::Local> and L<Date::Parse> for conversion functions.
3320 I<template>, if specified, is the name of a suffix for alternate invoices.
3322 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3329 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3330 my $pdf = generate_pdf($file);
3332 unlink($barcodefile);
3337 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3339 Returns an HTML invoice, as a scalar.
3341 I<time> an optional value used to control the printing of overdue messages. The
3342 default is now. It isn't the date of the invoice; that's the `_date' field.
3343 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3344 L<Time::Local> and L<Date::Parse> for conversion functions.
3346 I<template>, if specified, is the name of a suffix for alternate invoices.
3348 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3350 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3351 when emailing the invoice as part of a multipart/related MIME email.
3359 %params = %{ shift() };
3361 $params{'time'} = shift;
3362 $params{'template'} = shift;
3363 $params{'cid'} = shift;
3366 $params{'format'} = 'html';
3368 $self->print_generic( %params );
3371 # quick subroutine for print_latex
3373 # There are ten characters that LaTeX treats as special characters, which
3374 # means that they do not simply typeset themselves:
3375 # # $ % & ~ _ ^ \ { }
3377 # TeX ignores blanks following an escaped character; if you want a blank (as
3378 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3382 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3383 $value =~ s/([<>])/\$$1\$/g;
3389 encode_entities($value);
3393 sub _html_escape_nbsp {
3394 my $value = _html_escape(shift);
3395 $value =~ s/ +/ /g;
3399 #utility methods for print_*
3401 sub _translate_old_latex_format {
3402 warn "_translate_old_latex_format called\n"
3409 if ( $line =~ /^%%Detail\s*$/ ) {
3411 push @template, q![@--!,
3412 q! foreach my $_tr_line (@detail_items) {!,
3413 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3414 q! $_tr_line->{'description'} .= !,
3415 q! "\\tabularnewline\n~~".!,
3416 q! join( "\\tabularnewline\n~~",!,
3417 q! @{$_tr_line->{'ext_description'}}!,
3421 while ( ( my $line_item_line = shift )
3422 !~ /^%%EndDetail\s*$/ ) {
3423 $line_item_line =~ s/'/\\'/g; # nice LTS
3424 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3425 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3426 push @template, " \$OUT .= '$line_item_line';";
3429 push @template, '}',
3432 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3434 push @template, '[@--',
3435 ' foreach my $_tr_line (@total_items) {';
3437 while ( ( my $total_item_line = shift )
3438 !~ /^%%EndTotalDetails\s*$/ ) {
3439 $total_item_line =~ s/'/\\'/g; # nice LTS
3440 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3441 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3442 push @template, " \$OUT .= '$total_item_line';";
3445 push @template, '}',
3449 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3450 push @template, $line;
3456 warn "$_\n" foreach @template;
3465 #check for an invoice-specific override
3466 return $self->invoice_terms if $self->invoice_terms;
3468 #check for a customer- specific override
3469 my $cust_main = $self->cust_main;
3470 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3472 #use configured default
3473 $conf->config('invoice_default_terms') || '';
3479 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3480 $duedate = $self->_date() + ( $1 * 86400 );
3487 $self->due_date ? time2str(shift, $self->due_date) : '';
3490 sub balance_due_msg {
3492 my $msg = 'Balance Due';
3493 return $msg unless $self->terms;
3494 if ( $self->due_date ) {
3495 $msg .= ' - Please pay by '. $self->due_date2str($date_format);
3496 } elsif ( $self->terms ) {
3497 $msg .= ' - '. $self->terms;
3502 sub balance_due_date {
3505 if ( $conf->exists('invoice_default_terms')
3506 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3507 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3512 sub credit_balance_msg { 'Credit Balance Remaining' }
3514 =item invnum_date_pretty
3516 Returns a string with the invoice number and date, for example:
3517 "Invoice #54 (3/20/2008)"
3521 sub invnum_date_pretty {
3523 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3528 Returns a string with the date, for example: "3/20/2008"
3534 time2str($date_format, $self->_date);
3537 use vars qw(%pkg_category_cache);
3538 sub _items_sections {
3541 my $summarypage = shift;
3543 my $extra_sections = shift;
3547 my %late_subtotal = ();
3550 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3553 my $usage = $cust_bill_pkg->usage;
3555 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3556 next if ( $display->summary && $summarypage );
3558 my $section = $display->section;
3559 my $type = $display->type;
3561 $not_tax{$section} = 1
3562 unless $cust_bill_pkg->pkgnum == 0;
3564 if ( $display->post_total && !$summarypage ) {
3565 if (! $type || $type eq 'S') {
3566 $late_subtotal{$section} += $cust_bill_pkg->setup
3567 if $cust_bill_pkg->setup != 0;
3571 $late_subtotal{$section} += $cust_bill_pkg->recur
3572 if $cust_bill_pkg->recur != 0;
3575 if ($type && $type eq 'R') {
3576 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3577 if $cust_bill_pkg->recur != 0;
3580 if ($type && $type eq 'U') {
3581 $late_subtotal{$section} += $usage
3582 unless scalar(@$extra_sections);
3587 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3589 if (! $type || $type eq 'S') {
3590 $subtotal{$section} += $cust_bill_pkg->setup
3591 if $cust_bill_pkg->setup != 0;
3595 $subtotal{$section} += $cust_bill_pkg->recur
3596 if $cust_bill_pkg->recur != 0;
3599 if ($type && $type eq 'R') {
3600 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3601 if $cust_bill_pkg->recur != 0;
3604 if ($type && $type eq 'U') {
3605 $subtotal{$section} += $usage
3606 unless scalar(@$extra_sections);
3615 %pkg_category_cache = ();
3617 push @$late, map { { 'description' => &{$escape}($_),
3618 'subtotal' => $late_subtotal{$_},
3620 'sort_weight' => ( _pkg_category($_)
3621 ? _pkg_category($_)->weight
3624 ((_pkg_category($_) && _pkg_category($_)->condense)
3625 ? $self->_condense_section($format)
3629 sort _sectionsort keys %late_subtotal;
3632 if ( $summarypage ) {
3633 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3634 map { $_->categoryname } qsearch('pkg_category', {});
3635 push @sections, '' if exists($subtotal{''});
3637 @sections = keys %subtotal;
3640 my @early = map { { 'description' => &{$escape}($_),
3641 'subtotal' => $subtotal{$_},
3642 'summarized' => $not_tax{$_} ? '' : 'Y',
3643 'tax_section' => $not_tax{$_} ? '' : 'Y',
3644 'sort_weight' => ( _pkg_category($_)
3645 ? _pkg_category($_)->weight
3648 ((_pkg_category($_) && _pkg_category($_)->condense)
3649 ? $self->_condense_section($format)
3654 push @early, @$extra_sections if $extra_sections;
3656 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3660 #helper subs for above
3663 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3667 my $categoryname = shift;
3668 $pkg_category_cache{$categoryname} ||=
3669 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3672 my %condensed_format = (
3673 'label' => [ qw( Description Qty Amount ) ],
3675 sub { shift->{description} },
3676 sub { shift->{quantity} },
3677 sub { my($href, %opt) = @_;
3678 ($opt{dollar} || ''). $href->{amount};
3681 'align' => [ qw( l r r ) ],
3682 'span' => [ qw( 5 1 1 ) ], # unitprices?
3683 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3686 sub _condense_section {
3687 my ( $self, $format ) = ( shift, shift );
3689 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3690 qw( description_generator
3693 total_line_generator
3698 sub _condensed_generator_defaults {
3699 my ( $self, $format ) = ( shift, shift );
3700 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3709 sub _condensed_header_generator {
3710 my ( $self, $format ) = ( shift, shift );
3712 my ( $f, $prefix, $suffix, $separator, $column ) =
3713 _condensed_generator_defaults($format);
3715 if ($format eq 'latex') {
3716 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3717 $suffix = "\\\\\n\\hline";
3720 sub { my ($d,$a,$s,$w) = @_;
3721 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3723 } elsif ( $format eq 'html' ) {
3724 $prefix = '<th></th>';
3728 sub { my ($d,$a,$s,$w) = @_;
3729 return qq!<th align="$html_align{$a}">$d</th>!;
3737 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3739 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3742 $prefix. join($separator, @result). $suffix;
3747 sub _condensed_description_generator {
3748 my ( $self, $format ) = ( shift, shift );
3750 my ( $f, $prefix, $suffix, $separator, $column ) =
3751 _condensed_generator_defaults($format);
3753 my $money_char = '$';
3754 if ($format eq 'latex') {
3755 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3757 $separator = " & \n";
3759 sub { my ($d,$a,$s,$w) = @_;
3760 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3762 $money_char = '\\dollar';
3763 }elsif ( $format eq 'html' ) {
3764 $prefix = '"><td align="center"></td>';
3768 sub { my ($d,$a,$s,$w) = @_;
3769 return qq!<td align="$html_align{$a}">$d</td>!;
3771 #$money_char = $conf->config('money_char') || '$';
3772 $money_char = ''; # this is madness
3780 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3782 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3784 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3785 map { $f->{$_}->[$i] } qw(align span width)
3789 $prefix. join( $separator, @result ). $suffix;
3794 sub _condensed_total_generator {
3795 my ( $self, $format ) = ( shift, shift );
3797 my ( $f, $prefix, $suffix, $separator, $column ) =
3798 _condensed_generator_defaults($format);
3801 if ($format eq 'latex') {
3804 $separator = " & \n";
3806 sub { my ($d,$a,$s,$w) = @_;
3807 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3809 }elsif ( $format eq 'html' ) {
3813 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3815 sub { my ($d,$a,$s,$w) = @_;
3816 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3825 # my $r = &{$f->{fields}->[$i]}(@args);
3826 # $r .= ' Total' unless $i;
3828 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3830 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3831 map { $f->{$_}->[$i] } qw(align span width)
3835 $prefix. join( $separator, @result ). $suffix;
3840 =item total_line_generator FORMAT
3842 Returns a coderef used for generation of invoice total line items for this
3843 usage_class. FORMAT is either html or latex
3847 # should not be used: will have issues with hash element names (description vs
3848 # total_item and amount vs total_amount -- another array of functions?
3850 sub _condensed_total_line_generator {
3851 my ( $self, $format ) = ( shift, shift );
3853 my ( $f, $prefix, $suffix, $separator, $column ) =
3854 _condensed_generator_defaults($format);
3857 if ($format eq 'latex') {
3860 $separator = " & \n";
3862 sub { my ($d,$a,$s,$w) = @_;
3863 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3865 }elsif ( $format eq 'html' ) {
3869 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3871 sub { my ($d,$a,$s,$w) = @_;
3872 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3881 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3883 &{$column}( &{$f->{fields}->[$i]}(@args),
3884 map { $f->{$_}->[$i] } qw(align span width)
3888 $prefix. join( $separator, @result ). $suffix;
3893 #sub _items_extra_usage_sections {
3895 # my $escape = shift;
3897 # my %sections = ();
3899 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
3900 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3902 # next unless $cust_bill_pkg->pkgnum > 0;
3904 # foreach my $section ( keys %usage_class ) {
3906 # my $usage = $cust_bill_pkg->usage($section);
3908 # next unless $usage && $usage > 0;
3910 # $sections{$section} ||= 0;
3911 # $sections{$section} += $usage;
3917 # map { { 'description' => &{$escape}($_),
3918 # 'subtotal' => $sections{$_},
3919 # 'summarized' => '',
3920 # 'tax_section' => '',
3923 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3927 sub _items_extra_usage_sections {
3936 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3937 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3938 next unless $cust_bill_pkg->pkgnum > 0;
3940 foreach my $classnum ( keys %usage_class ) {
3941 my $section = $usage_class{$classnum}->classname;
3942 $classnums{$section} = $classnum;
3944 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3945 my $amount = $detail->amount;
3946 next unless $amount && $amount > 0;
3948 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
3949 $sections{$section}{amount} += $amount; #subtotal
3950 $sections{$section}{calls}++;
3951 $sections{$section}{duration} += $detail->duration;
3953 my $desc = $detail->regionname;
3954 my $description = $desc;
3955 $description = substr($desc, 0, 50). '...'
3956 if $format eq 'latex' && length($desc) > 50;
3958 $lines{$section}{$desc} ||= {
3959 description => &{$escape}($description),
3960 #pkgpart => $part_pkg->pkgpart,
3961 pkgnum => $cust_bill_pkg->pkgnum,
3966 #unit_amount => $cust_bill_pkg->unitrecur,
3967 quantity => $cust_bill_pkg->quantity,
3968 product_code => 'N/A',
3969 ext_description => [],
3972 $lines{$section}{$desc}{amount} += $amount;
3973 $lines{$section}{$desc}{calls}++;
3974 $lines{$section}{$desc}{duration} += $detail->duration;
3980 my %sectionmap = ();
3981 foreach (keys %sections) {
3982 my $usage_class = $usage_class{$classnums{$_}};
3983 $sectionmap{$_} = { 'description' => &{$escape}($_),
3984 'amount' => $sections{$_}{amount}, #subtotal
3985 'calls' => $sections{$_}{calls},
3986 'duration' => $sections{$_}{duration},
3988 'tax_section' => '',
3989 'sort_weight' => $usage_class->weight,
3990 ( $usage_class->format
3991 ? ( map { $_ => $usage_class->$_($format) }
3992 qw( description_generator header_generator total_generator total_line_generator )
3999 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4003 foreach my $section ( keys %lines ) {
4004 foreach my $line ( keys %{$lines{$section}} ) {
4005 my $l = $lines{$section}{$line};
4006 $l->{section} = $sectionmap{$section};
4007 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4008 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4013 return(\@sections, \@lines);
4019 my $end = $self->_date;
4020 my $start = $end - 2592000; # 30 days
4021 my $cust_main = $self->cust_main;
4022 my @pkgs = $cust_main->all_pkgs;
4023 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4026 foreach my $pkg ( @pkgs ) {
4027 my @h_cust_svc = $pkg->h_cust_svc($end);
4028 foreach my $h_cust_svc ( @h_cust_svc ) {
4029 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4030 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4032 my $inserted = $h_cust_svc->date_inserted;
4033 my $deleted = $h_cust_svc->date_deleted;
4034 my $phone_inserted = $h_cust_svc->h_svc_x($inserted);
4036 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4038 # DID either activated or ported in; cannot be both for same DID simultaneously
4039 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4040 && (!$phone_inserted->lnp_status
4041 || $phone_inserted->lnp_status eq ''
4042 || $phone_inserted->lnp_status eq 'native')) {
4045 else { # this one not so clean, should probably move to (h_)svc_phone
4046 my $phone_portedin = qsearchs( 'h_svc_phone',
4047 { 'svcnum' => $h_cust_svc->svcnum,
4048 'lnp_status' => 'portedin' },
4049 FS::h_svc_phone->sql_h_searchs($end),
4051 $num_portedin++ if $phone_portedin;
4054 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4055 if($deleted >= $start && $deleted <= $end && $phone_deleted
4056 && (!$phone_deleted->lnp_status
4057 || $phone_deleted->lnp_status ne 'portingout')) {
4060 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4061 && $phone_deleted->lnp_status
4062 && $phone_deleted->lnp_status eq 'portingout') {
4066 # increment usage minutes
4067 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end);
4068 foreach my $cdr ( @cdrs ) {
4069 $minutes += $cdr->billsec/60;
4072 # don't look at this service again
4073 push @seen, $h_cust_svc->svcnum;
4077 $minutes = sprintf("%d", $minutes);
4078 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4079 . "$num_deactivated Ported-Out: $num_portedout ",
4080 "Total Minutes: $minutes");
4083 sub _items_svc_phone_sections {
4092 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4093 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4095 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4096 next unless $cust_bill_pkg->pkgnum > 0;
4098 my @header = $cust_bill_pkg->details_header;
4099 next unless scalar(@header);
4101 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4103 my $phonenum = $detail->phonenum;
4104 next unless $phonenum;
4106 my $amount = $detail->amount;
4107 next unless $amount && $amount > 0;
4109 $sections{$phonenum} ||= { 'amount' => 0,
4112 'sort_weight' => -1,
4113 'phonenum' => $phonenum,
4115 $sections{$phonenum}{amount} += $amount; #subtotal
4116 $sections{$phonenum}{calls}++;
4117 $sections{$phonenum}{duration} += $detail->duration;
4119 my $desc = $detail->regionname;
4120 my $description = $desc;
4121 $description = substr($desc, 0, 50). '...'
4122 if $format eq 'latex' && length($desc) > 50;
4124 $lines{$phonenum}{$desc} ||= {
4125 description => &{$escape}($description),
4126 #pkgpart => $part_pkg->pkgpart,
4134 product_code => 'N/A',
4135 ext_description => [],
4138 $lines{$phonenum}{$desc}{amount} += $amount;
4139 $lines{$phonenum}{$desc}{calls}++;
4140 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4142 my $line = $usage_class{$detail->classnum}->classname;
4143 $sections{"$phonenum $line"} ||=
4147 'sort_weight' => $usage_class{$detail->classnum}->weight,
4148 'phonenum' => $phonenum,
4149 'header' => [ @header ],
4151 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4152 $sections{"$phonenum $line"}{calls}++;
4153 $sections{"$phonenum $line"}{duration} += $detail->duration;
4155 $lines{"$phonenum $line"}{$desc} ||= {
4156 description => &{$escape}($description),
4157 #pkgpart => $part_pkg->pkgpart,
4165 product_code => 'N/A',
4166 ext_description => [],
4169 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4170 $lines{"$phonenum $line"}{$desc}{calls}++;
4171 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4172 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4173 $detail->formatted('format' => $format);
4178 my %sectionmap = ();
4179 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4180 foreach ( keys %sections ) {
4181 my @header = @{ $sections{$_}{header} || [] };
4183 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4184 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4185 my $usage_class = $summary ? $simple : $usage_simple;
4186 my $ending = $summary ? ' usage charges' : '';
4189 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4191 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4192 'amount' => $sections{$_}{amount}, #subtotal
4193 'calls' => $sections{$_}{calls},
4194 'duration' => $sections{$_}{duration},
4196 'tax_section' => '',
4197 'phonenum' => $sections{$_}{phonenum},
4198 'sort_weight' => $sections{$_}{sort_weight},
4199 'post_total' => $summary, #inspire pagebreak
4201 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4202 qw( description_generator
4205 total_line_generator
4212 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4213 $a->{sort_weight} <=> $b->{sort_weight}
4218 foreach my $section ( keys %lines ) {
4219 foreach my $line ( keys %{$lines{$section}} ) {
4220 my $l = $lines{$section}{$line};
4221 $l->{section} = $sectionmap{$section};
4222 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4223 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4228 if($conf->exists('phone_usage_class_summary')) {
4229 # this only works with Latex
4233 # after this, we'll have only two sections per DID:
4234 # Calls Summary and Calls Detail
4235 foreach my $section ( @sections ) {
4236 if($section->{'post_total'}) {
4237 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4238 $section->{'total_line_generator'} = sub { '' };
4239 $section->{'total_generator'} = sub { '' };
4240 $section->{'header_generator'} = sub { '' };
4241 $section->{'description_generator'} = '';
4242 push @newsections, $section;
4243 my %calls_detail = %$section;
4244 $calls_detail{'post_total'} = '';
4245 $calls_detail{'sort_weight'} = '';
4246 $calls_detail{'description_generator'} = sub { '' };
4247 $calls_detail{'header_generator'} = sub {
4248 return ' & Date/Time & Called Number & Duration & Price'
4249 if $format eq 'latex';
4252 $calls_detail{'description'} = 'Calls Detail: '
4253 . $section->{'phonenum'};
4254 push @newsections, \%calls_detail;
4258 # after this, each usage class is collapsed/summarized into a single
4259 # line under the Calls Summary section
4260 foreach my $newsection ( @newsections ) {
4261 if($newsection->{'post_total'}) { # this means Calls Summary
4262 foreach my $section ( @sections ) {
4263 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4264 && !$section->{'post_total'});
4265 my $newdesc = $section->{'description'};
4266 my $tn = $section->{'phonenum'};
4267 $newdesc =~ s/$tn//g;
4268 my $line = { ext_description => [],
4272 calls => $section->{'calls'},
4273 section => $newsection,
4274 duration => $section->{'duration'},
4275 description => $newdesc,
4276 amount => sprintf("%.2f",$section->{'amount'}),
4277 product_code => 'N/A',
4279 push @newlines, $line;
4284 # after this, Calls Details is populated with all CDRs
4285 foreach my $newsection ( @newsections ) {
4286 if(!$newsection->{'post_total'}) { # this means Calls Details
4287 foreach my $line ( @lines ) {
4288 next unless (scalar(@{$line->{'ext_description'}}) &&
4289 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4291 my @extdesc = @{$line->{'ext_description'}};
4293 foreach my $extdesc ( @extdesc ) {
4294 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4295 push @newextdesc, $extdesc;
4297 $line->{'ext_description'} = \@newextdesc;
4298 $line->{'section'} = $newsection;
4299 push @newlines, $line;
4304 return(\@newsections, \@newlines);
4307 return(\@sections, \@lines);
4314 #my @display = scalar(@_)
4316 # : qw( _items_previous _items_pkg );
4317 # #: qw( _items_pkg );
4318 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4319 my @display = qw( _items_previous _items_pkg );
4322 foreach my $display ( @display ) {
4323 push @b, $self->$display(@_);
4328 sub _items_previous {
4330 my $cust_main = $self->cust_main;
4331 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4333 foreach ( @pr_cust_bill ) {
4334 my $date = $conf->exists('invoice_show_prior_due_date')
4335 ? 'due '. $_->due_date2str($date_format)
4336 : time2str($date_format, $_->_date);
4338 'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
4339 #'pkgpart' => 'N/A',
4341 'amount' => sprintf("%.2f", $_->owed),
4347 # 'description' => 'Previous Balance',
4348 # #'pkgpart' => 'N/A',
4349 # 'pkgnum' => 'N/A',
4350 # 'amount' => sprintf("%10.2f", $pr_total ),
4351 # 'ext_description' => [ map {
4352 # "Invoice ". $_->invnum.
4353 # " (". time2str("%x",$_->_date). ") ".
4354 # sprintf("%10.2f", $_->owed)
4355 # } @pr_cust_bill ],
4364 warn "$me _items_pkg searching for all package line items\n"
4367 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4369 warn "$me _items_pkg filtering line items\n"
4371 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4373 if ($options{section} && $options{section}->{condensed}) {
4375 warn "$me _items_pkg condensing section\n"
4379 local $Storable::canonical = 1;
4380 foreach ( @items ) {
4382 delete $item->{ref};
4383 delete $item->{ext_description};
4384 my $key = freeze($item);
4385 $itemshash{$key} ||= 0;
4386 $itemshash{$key} ++; # += $item->{quantity};
4388 @items = sort { $a->{description} cmp $b->{description} }
4389 map { my $i = thaw($_);
4390 $i->{quantity} = $itemshash{$_};
4392 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4398 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4405 return 0 unless $a->itemdesc cmp $b->itemdesc;
4406 return -1 if $b->itemdesc eq 'Tax';
4407 return 1 if $a->itemdesc eq 'Tax';
4408 return -1 if $b->itemdesc eq 'Other surcharges';
4409 return 1 if $a->itemdesc eq 'Other surcharges';
4410 $a->itemdesc cmp $b->itemdesc;
4415 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4416 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4419 sub _items_cust_bill_pkg {
4421 my $cust_bill_pkgs = shift;
4424 my $format = $opt{format} || '';
4425 my $escape_function = $opt{escape_function} || sub { shift };
4426 my $format_function = $opt{format_function} || '';
4427 my $unsquelched = $opt{unsquelched} || '';
4428 my $section = $opt{section}->{description} if $opt{section};
4429 my $summary_page = $opt{summary_page} || '';
4430 my $multilocation = $opt{multilocation} || '';
4431 my $multisection = $opt{multisection} || '';
4432 my $discount_show_always = 0;
4435 my ($s, $r, $u) = ( undef, undef, undef );
4436 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4439 warn "$me _items_cust_bill_pkg considering cust_bill_pkg $cust_bill_pkg\n"
4442 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
4443 && $conf->exists('discount-show-always'));
4445 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4446 if ( $_ && !$cust_bill_pkg->hidden ) {
4447 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4448 $_->{amount} =~ s/^\-0\.00$/0.00/;
4449 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4451 unless ( $_->{amount} == 0 && !$discount_show_always );
4456 foreach my $display ( grep { defined($section)
4457 ? $_->section eq $section
4460 #grep { !$_->summary || !$summary_page } # bunk!
4461 grep { !$_->summary || $multisection }
4462 $cust_bill_pkg->cust_bill_pkg_display
4466 warn "$me _items_cust_bill_pkg considering display item $display\n"
4469 my $type = $display->type;
4471 my $desc = $cust_bill_pkg->desc;
4472 $desc = substr($desc, 0, 50). '...'
4473 if $format eq 'latex' && length($desc) > 50;
4475 my %details_opt = ( 'format' => $format,
4476 'escape_function' => $escape_function,
4477 'format_function' => $format_function,
4480 if ( $cust_bill_pkg->pkgnum > 0 ) {
4482 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4485 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4487 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
4489 warn "$me _items_cust_bill_pkg adding setup\n"
4492 my $description = $desc;
4493 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
4496 unless ( $cust_pkg->part_pkg->hide_svc_detail
4497 || $cust_bill_pkg->hidden )
4500 push @d, map &{$escape_function}($_),
4501 $cust_pkg->h_labels_short($self->_date, undef, 'I')
4502 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4504 if ( $multilocation ) {
4505 my $loc = $cust_pkg->location_label;
4506 $loc = substr($loc, 0, 50). '...'
4507 if $format eq 'latex' && length($loc) > 50;
4508 push @d, &{$escape_function}($loc);
4513 push @d, $cust_bill_pkg->details(%details_opt)
4514 if $cust_bill_pkg->recur == 0;
4516 if ( $cust_bill_pkg->hidden ) {
4517 $s->{amount} += $cust_bill_pkg->setup;
4518 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4519 push @{ $s->{ext_description} }, @d;
4522 description => $description,
4523 #pkgpart => $part_pkg->pkgpart,
4524 pkgnum => $cust_bill_pkg->pkgnum,
4525 amount => $cust_bill_pkg->setup,
4526 unit_amount => $cust_bill_pkg->unitsetup,
4527 quantity => $cust_bill_pkg->quantity,
4528 ext_description => \@d,
4534 if ( ( $cust_bill_pkg->recur != 0 || $cust_bill_pkg->setup == 0 ||
4535 ($discount_show_always && $cust_bill_pkg->recur == 0) ) &&
4536 ( !$type || $type eq 'R' || $type eq 'U' )
4540 warn "$me _items_cust_bill_pkg adding recur/usage\n"
4543 my $is_summary = $display->summary;
4544 my $description = ($is_summary && $type && $type eq 'U')
4545 ? "Usage charges" : $desc;
4547 $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4548 " - ". time2str($date_format, $cust_bill_pkg->edate).
4550 unless $conf->exists('disable_line_item_date_ranges');
4554 #at least until cust_bill_pkg has "past" ranges in addition to
4555 #the "future" sdate/edate ones... see #3032
4556 my @dates = ( $self->_date );
4557 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4558 push @dates, $prev->sdate if $prev;
4559 push @dates, undef if !$prev;
4561 unless ( $cust_pkg->part_pkg->hide_svc_detail
4562 || $cust_bill_pkg->itemdesc
4563 || $cust_bill_pkg->hidden
4564 || $is_summary && $type && $type eq 'U' )
4567 warn "$me _items_cust_bill_pkg adding service details\n"
4570 push @d, map &{$escape_function}($_),
4571 $cust_pkg->h_labels_short(@dates, 'I')
4572 #$cust_bill_pkg->edate,
4573 #$cust_bill_pkg->sdate)
4574 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4576 warn "$me _items_cust_bill_pkg done adding service details\n"
4579 if ( $multilocation ) {
4580 my $loc = $cust_pkg->location_label;
4581 $loc = substr($loc, 0, 50). '...'
4582 if $format eq 'latex' && length($loc) > 50;
4583 push @d, &{$escape_function}($loc);
4588 unless ( $is_summary ) {
4589 warn "$me _items_cust_bill_pkg adding details\n"
4592 #instead of omitting details entirely in this case (unwanted side
4593 # effects), just omit CDRs
4594 $details_opt{'format_function'} = sub { () }
4595 if $type && $type eq 'R';
4597 push @d, $cust_bill_pkg->details(%details_opt);
4600 warn "$me _items_cust_bill_pkg calculating amount\n"
4605 $amount = $cust_bill_pkg->recur;
4606 } elsif ($type eq 'R') {
4607 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4608 } elsif ($type eq 'U') {
4609 $amount = $cust_bill_pkg->usage;
4612 if ( !$type || $type eq 'R' ) {
4614 warn "$me _items_cust_bill_pkg adding recur\n"
4617 if ( $cust_bill_pkg->hidden ) {
4618 $r->{amount} += $amount;
4619 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4620 push @{ $r->{ext_description} }, @d;
4623 description => $description,
4624 #pkgpart => $part_pkg->pkgpart,
4625 pkgnum => $cust_bill_pkg->pkgnum,
4627 unit_amount => $cust_bill_pkg->unitrecur,
4628 quantity => $cust_bill_pkg->quantity,
4629 ext_description => \@d,
4633 } else { # $type eq 'U'
4635 warn "$me _items_cust_bill_pkg adding usage\n"
4638 if ( $cust_bill_pkg->hidden ) {
4639 $u->{amount} += $amount;
4640 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4641 push @{ $u->{ext_description} }, @d;
4644 description => $description,
4645 #pkgpart => $part_pkg->pkgpart,
4646 pkgnum => $cust_bill_pkg->pkgnum,
4648 unit_amount => $cust_bill_pkg->unitrecur,
4649 quantity => $cust_bill_pkg->quantity,
4650 ext_description => \@d,
4656 } # recurring or usage with recurring charge
4658 } else { #pkgnum tax or one-shot line item (??)
4660 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
4663 if ( $cust_bill_pkg->setup != 0 ) {
4665 'description' => $desc,
4666 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
4669 if ( $cust_bill_pkg->recur != 0 ) {
4671 'description' => "$desc (".
4672 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
4673 time2str($date_format, $cust_bill_pkg->edate). ')',
4674 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
4684 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
4687 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4689 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4690 $_->{amount} =~ s/^\-0\.00$/0.00/;
4691 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4693 unless ( $_->{amount} == 0 && !$discount_show_always );
4701 sub _items_credits {
4702 my( $self, %opt ) = @_;
4703 my $trim_len = $opt{'trim_len'} || 60;
4707 foreach ( $self->cust_credited ) {
4709 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4711 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4712 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4713 $reason = " ($reason) " if $reason;
4716 #'description' => 'Credit ref\#'. $_->crednum.
4717 # " (". time2str("%x",$_->cust_credit->_date) .")".
4719 'description' => 'Credit applied '.
4720 time2str($date_format,$_->cust_credit->_date). $reason,
4721 'amount' => sprintf("%.2f",$_->amount),
4729 sub _items_payments {
4733 #get & print payments
4734 foreach ( $self->cust_bill_pay ) {
4736 #something more elaborate if $_->amount ne ->cust_pay->paid ?
4739 'description' => "Payment received ".
4740 time2str($date_format,$_->cust_pay->_date ),
4741 'amount' => sprintf("%.2f", $_->amount )
4749 =item call_details [ OPTION => VALUE ... ]
4751 Returns an array of CSV strings representing the call details for this invoice
4752 The only option available is the boolean prepend_billed_number
4757 my ($self, %opt) = @_;
4759 my $format_function = sub { shift };
4761 if ($opt{prepend_billed_number}) {
4762 $format_function = sub {
4766 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4771 my @details = map { $_->details( 'format_function' => $format_function,
4772 'escape_function' => sub{ return() },
4776 $self->cust_bill_pkg;
4777 my $header = $details[0];
4778 ( $header, grep { $_ ne $header } @details );
4788 =item process_reprint
4792 sub process_reprint {
4793 process_re_X('print', @_);
4796 =item process_reemail
4800 sub process_reemail {
4801 process_re_X('email', @_);
4809 process_re_X('fax', @_);
4817 process_re_X('ftp', @_);
4824 sub process_respool {
4825 process_re_X('spool', @_);
4828 use Storable qw(thaw);
4832 my( $method, $job ) = ( shift, shift );
4833 warn "$me process_re_X $method for job $job\n" if $DEBUG;
4835 my $param = thaw(decode_base64(shift));
4836 warn Dumper($param) if $DEBUG;
4847 my($method, $job, %param ) = @_;
4849 warn "re_X $method for job $job with param:\n".
4850 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
4853 #some false laziness w/search/cust_bill.html
4855 my $orderby = 'ORDER BY cust_bill._date';
4857 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
4859 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4861 my @cust_bill = qsearch( {
4862 #'select' => "cust_bill.*",
4863 'table' => 'cust_bill',
4864 'addl_from' => $addl_from,
4866 'extra_sql' => $extra_sql,
4867 'order_by' => $orderby,
4871 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4873 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
4876 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
4877 foreach my $cust_bill ( @cust_bill ) {
4878 $cust_bill->$method();
4880 if ( $job ) { #progressbar foo
4882 if ( time - $min_sec > $last ) {
4883 my $error = $job->update_statustext(
4884 int( 100 * $num / scalar(@cust_bill) )
4886 die $error if $error;
4897 =head1 CLASS METHODS
4903 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
4908 my ($class, $start, $end) = @_;
4910 $class->paid_sql($start, $end). ' - '.
4911 $class->credited_sql($start, $end);
4916 Returns an SQL fragment to retreive the net amount (charged minus credited).
4921 my ($class, $start, $end) = @_;
4922 'charged - '. $class->credited_sql($start, $end);
4927 Returns an SQL fragment to retreive the amount paid against this invoice.
4932 my ($class, $start, $end) = @_;
4933 $start &&= "AND cust_bill_pay._date <= $start";
4934 $end &&= "AND cust_bill_pay._date > $end";
4935 $start = '' unless defined($start);
4936 $end = '' unless defined($end);
4937 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
4938 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
4943 Returns an SQL fragment to retreive the amount credited against this invoice.
4948 my ($class, $start, $end) = @_;
4949 $start &&= "AND cust_credit_bill._date <= $start";
4950 $end &&= "AND cust_credit_bill._date > $end";
4951 $start = '' unless defined($start);
4952 $end = '' unless defined($end);
4953 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
4954 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
4959 Returns an SQL fragment to retrieve the due date of an invoice.
4960 Currently only supported on PostgreSQL.
4968 cust_bill.invoice_terms,
4969 cust_main.invoice_terms,
4970 \''.($conf->config('invoice_default_terms') || '').'\'
4971 ), E\'Net (\\\\d+)\'
4973 ) * 86400 + cust_bill._date'
4976 =item search_sql_where HASHREF
4978 Class method which returns an SQL WHERE fragment to search for parameters
4979 specified in HASHREF. Valid parameters are
4985 List reference of start date, end date, as UNIX timestamps.
4995 List reference of charged limits (exclusive).
4999 List reference of charged limits (exclusive).
5003 flag, return open invoices only
5007 flag, return net invoices only
5011 =item newest_percust
5015 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5019 sub search_sql_where {
5020 my($class, $param) = @_;
5022 warn "$me search_sql_where called with params: \n".
5023 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
5029 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5030 push @search, "cust_main.agentnum = $1";
5034 if ( $param->{_date} ) {
5035 my($beginning, $ending) = @{$param->{_date}};
5037 push @search, "cust_bill._date >= $beginning",
5038 "cust_bill._date < $ending";
5042 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5043 push @search, "cust_bill.invnum >= $1";
5045 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5046 push @search, "cust_bill.invnum <= $1";
5050 if ( $param->{charged} ) {
5051 my @charged = ref($param->{charged})
5052 ? @{ $param->{charged} }
5053 : ($param->{charged});
5055 push @search, map { s/^charged/cust_bill.charged/; $_; }
5059 my $owed_sql = FS::cust_bill->owed_sql;
5062 if ( $param->{owed} ) {
5063 my @owed = ref($param->{owed})
5064 ? @{ $param->{owed} }
5066 push @search, map { s/^owed/$owed_sql/; $_; }
5071 push @search, "0 != $owed_sql"
5072 if $param->{'open'};
5073 push @search, '0 != '. FS::cust_bill->net_sql
5077 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5078 if $param->{'days'};
5081 if ( $param->{'newest_percust'} ) {
5083 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5084 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5086 my @newest_where = map { my $x = $_;
5087 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5090 grep ! /^cust_main./, @search;
5091 my $newest_where = scalar(@newest_where)
5092 ? ' AND '. join(' AND ', @newest_where)
5096 push @search, "cust_bill._date = (
5097 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5098 WHERE newest_cust_bill.custnum = cust_bill.custnum
5104 #agent virtualization
5105 my $curuser = $FS::CurrentUser::CurrentUser;
5106 if ( $curuser->username eq 'fs_queue'
5107 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5109 my $newuser = qsearchs('access_user', {
5110 'username' => $username,
5114 $curuser = $newuser;
5116 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5119 push @search, $curuser->agentnums_sql;
5121 join(' AND ', @search );
5133 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5134 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base