4 use vars qw( @ISA $DEBUG $me $conf
5 $money_char $date_format $rdate_format $date_format_long );
6 use vars qw( $invoice_lines @buf ); #yuck
7 use Fcntl qw(:flock); #for spool_csv
8 use List::Util qw(min max);
10 use Text::Template 1.20;
12 use String::ShellQuote;
15 use Storable qw( freeze thaw );
17 use FS::UID qw( datasrc );
18 use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
19 use FS::Record qw( qsearch qsearchs dbh );
20 use FS::cust_main_Mixin;
22 use FS::cust_statement;
23 use FS::cust_bill_pkg;
24 use FS::cust_bill_pkg_display;
25 use FS::cust_bill_pkg_detail;
29 use FS::cust_credit_bill;
31 use FS::cust_pay_batch;
32 use FS::cust_bill_event;
35 use FS::cust_bill_pay;
36 use FS::cust_bill_pay_batch;
37 use FS::part_bill_event;
40 use FS::cust_bill_batch;
43 @ISA = qw( FS::cust_main_Mixin FS::Record );
46 $me = '[FS::cust_bill]';
48 #ask FS::UID to run this stuff for us later
49 FS::UID->install_callback( sub {
51 $money_char = $conf->config('money_char') || '$';
52 $date_format = $conf->config('date_format') || '%x'; #/YY
53 $rdate_format = $conf->config('date_format') || '%m/%d/%Y'; #/YYYY
54 $date_format_long = $conf->config('date_format_long') || '%b %o, %Y';
59 FS::cust_bill - Object methods for cust_bill records
65 $record = new FS::cust_bill \%hash;
66 $record = new FS::cust_bill { 'column' => 'value' };
68 $error = $record->insert;
70 $error = $new_record->replace($old_record);
72 $error = $record->delete;
74 $error = $record->check;
76 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
78 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
80 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
82 @cust_pay_objects = $cust_bill->cust_pay;
84 $tax_amount = $record->tax;
86 @lines = $cust_bill->print_text;
87 @lines = $cust_bill->print_text $time;
91 An FS::cust_bill object represents an invoice; a declaration that a customer
92 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
93 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
94 following fields are currently supported:
100 =item invnum - primary key (assigned automatically for new invoices)
102 =item custnum - customer (see L<FS::cust_main>)
104 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
105 L<Time::Local> and L<Date::Parse> for conversion functions.
107 =item charged - amount of this invoice
109 =item invoice_terms - optional terms override for this specific invoice
113 Customer info at invoice generation time
117 =item previous_balance
119 =item billing_balance
127 =item printed - deprecated
135 =item closed - books closed flag, empty or `Y'
137 =item statementnum - invoice aggregation (see L<FS::cust_statement>)
139 =item agent_invid - legacy invoice number
149 Creates a new invoice. To add the invoice to the database, see L<"insert">.
150 Invoices are normally created by calling the bill method of a customer object
151 (see L<FS::cust_main>).
155 sub table { 'cust_bill'; }
157 sub cust_linked { $_[0]->cust_main_custnum; }
158 sub cust_unlinked_msg {
160 "WARNING: can't find cust_main.custnum ". $self->custnum.
161 ' (cust_bill.invnum '. $self->invnum. ')';
166 Adds this invoice to the database ("Posts" the invoice). If there is an error,
167 returns the error, otherwise returns false.
173 warn "$me insert called\n" if $DEBUG;
175 local $SIG{HUP} = 'IGNORE';
176 local $SIG{INT} = 'IGNORE';
177 local $SIG{QUIT} = 'IGNORE';
178 local $SIG{TERM} = 'IGNORE';
179 local $SIG{TSTP} = 'IGNORE';
180 local $SIG{PIPE} = 'IGNORE';
182 my $oldAutoCommit = $FS::UID::AutoCommit;
183 local $FS::UID::AutoCommit = 0;
186 my $error = $self->SUPER::insert;
188 $dbh->rollback if $oldAutoCommit;
192 if ( $self->get('cust_bill_pkg') ) {
193 foreach my $cust_bill_pkg ( @{$self->get('cust_bill_pkg')} ) {
194 $cust_bill_pkg->invnum($self->invnum);
195 my $error = $cust_bill_pkg->insert;
197 $dbh->rollback if $oldAutoCommit;
198 return "can't create invoice line item: $error";
203 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
210 This method now works but you probably shouldn't use it. Instead, apply a
211 credit against the invoice.
213 Using this method to delete invoices outright is really, really bad. There
214 would be no record you ever posted this invoice, and there are no check to
215 make sure charged = 0 or that there are no associated cust_bill_pkg records.
217 Really, don't use it.
223 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
225 local $SIG{HUP} = 'IGNORE';
226 local $SIG{INT} = 'IGNORE';
227 local $SIG{QUIT} = 'IGNORE';
228 local $SIG{TERM} = 'IGNORE';
229 local $SIG{TSTP} = 'IGNORE';
230 local $SIG{PIPE} = 'IGNORE';
232 my $oldAutoCommit = $FS::UID::AutoCommit;
233 local $FS::UID::AutoCommit = 0;
236 foreach my $table (qw(
248 foreach my $linked ( $self->$table() ) {
249 my $error = $linked->delete;
251 $dbh->rollback if $oldAutoCommit;
258 my $error = $self->SUPER::delete(@_);
260 $dbh->rollback if $oldAutoCommit;
264 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
270 =item replace [ OLD_RECORD ]
272 You can, but probably shouldn't modify invoices...
274 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
275 supplied, replaces this record. If there is an error, returns the error,
276 otherwise returns false.
280 #replace can be inherited from Record.pm
282 # replace_check is now the preferred way to #implement replace data checks
283 # (so $object->replace() works without an argument)
286 my( $new, $old ) = ( shift, shift );
287 return "Can't modify closed invoice" if $old->closed =~ /^Y/i;
288 #return "Can't change _date!" unless $old->_date eq $new->_date;
289 return "Can't change _date" unless $old->_date == $new->_date;
290 return "Can't change charged" unless $old->charged == $new->charged
291 || $old->charged == 0
292 || $new->{'Hash'}{'cc_surcharge_replace_hack'};
298 =item add_cc_surcharge
304 sub add_cc_surcharge {
305 my ($self, $pkgnum, $amount) = (shift, shift, shift);
308 my $cust_bill_pkg = new FS::cust_bill_pkg({
309 'invnum' => $self->invnum,
313 $error = $cust_bill_pkg->insert;
314 return $error if $error;
316 $self->{'Hash'}{'cc_surcharge_replace_hack'} = 1;
317 $self->charged($self->charged+$amount);
318 $error = $self->replace;
319 return $error if $error;
321 $self->apply_payments_and_credits;
327 Checks all fields to make sure this is a valid invoice. If there is an error,
328 returns the error, otherwise returns false. Called by the insert and replace
337 $self->ut_numbern('invnum')
338 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
339 || $self->ut_numbern('_date')
340 || $self->ut_money('charged')
341 || $self->ut_numbern('printed')
342 || $self->ut_enum('closed', [ '', 'Y' ])
343 || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
344 || $self->ut_numbern('agent_invid') #varchar?
346 return $error if $error;
348 $self->_date(time) unless $self->_date;
350 $self->printed(0) if $self->printed eq '';
357 Returns the displayed invoice number for this invoice: agent_invid if
358 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
364 if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
365 return $self->agent_invid;
367 return $self->invnum;
373 Returns a list consisting of the total previous balance for this customer,
374 followed by the previous outstanding invoices (as FS::cust_bill objects also).
381 my @cust_bill = sort { $a->_date <=> $b->_date }
382 grep { $_->owed != 0 && $_->_date < $self->_date }
383 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
385 foreach ( @cust_bill ) { $total += $_->owed; }
391 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
398 { 'table' => 'cust_bill_pkg',
399 'hashref' => { 'invnum' => $self->invnum },
400 'order_by' => 'ORDER BY billpkgnum',
405 =item cust_bill_pkg_pkgnum PKGNUM
407 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
412 sub cust_bill_pkg_pkgnum {
413 my( $self, $pkgnum ) = @_;
415 { 'table' => 'cust_bill_pkg',
416 'hashref' => { 'invnum' => $self->invnum,
419 'order_by' => 'ORDER BY billpkgnum',
426 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
433 my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
434 $self->cust_bill_pkg;
436 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
441 Returns true if any of the packages (or their definitions) corresponding to the
442 line items for this invoice have the no_auto flag set.
448 grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
451 =item open_cust_bill_pkg
453 Returns the open line items for this invoice.
455 Note that cust_bill_pkg with both setup and recur fees are returned as two
456 separate line items, each with only one fee.
460 # modeled after cust_main::open_cust_bill
461 sub open_cust_bill_pkg {
464 # grep { $_->owed > 0 } $self->cust_bill_pkg
466 my %other = ( 'recur' => 'setup',
467 'setup' => 'recur', );
469 foreach my $field ( qw( recur setup )) {
470 push @open, map { $_->set( $other{$field}, 0 ); $_; }
471 grep { $_->owed($field) > 0 }
472 $self->cust_bill_pkg;
478 =item cust_bill_event
480 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
484 sub cust_bill_event {
486 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
489 =item num_cust_bill_event
491 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
495 sub num_cust_bill_event {
498 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
499 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
500 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
501 $sth->fetchrow_arrayref->[0];
506 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
510 #false laziness w/cust_pkg.pm
514 'table' => 'cust_event',
515 'addl_from' => 'JOIN part_event USING ( eventpart )',
516 'hashref' => { 'tablenum' => $self->invnum },
517 'extra_sql' => " AND eventtable = 'cust_bill' ",
523 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
527 #false laziness w/cust_pkg.pm
531 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
532 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
533 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
534 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
535 $sth->fetchrow_arrayref->[0];
540 Returns the customer (see L<FS::cust_main>) for this invoice.
546 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
549 =item cust_suspend_if_balance_over AMOUNT
551 Suspends the customer associated with this invoice if the total amount owed on
552 this invoice and all older invoices is greater than the specified amount.
554 Returns a list: an empty list on success or a list of errors.
558 sub cust_suspend_if_balance_over {
559 my( $self, $amount ) = ( shift, shift );
560 my $cust_main = $self->cust_main;
561 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
564 $cust_main->suspend(@_);
570 Depreciated. See the cust_credited method.
572 #Returns a list consisting of the total previous credited (see
573 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
574 #outstanding credits (FS::cust_credit objects).
580 croak "FS::cust_bill->cust_credit depreciated; see ".
581 "FS::cust_bill->cust_credit_bill";
584 #my @cust_credit = sort { $a->_date <=> $b->_date }
585 # grep { $_->credited != 0 && $_->_date < $self->_date }
586 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
588 #foreach (@cust_credit) { $total += $_->credited; }
589 #$total, @cust_credit;
594 Depreciated. See the cust_bill_pay method.
596 #Returns all payments (see L<FS::cust_pay>) for this invoice.
602 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
604 #sort { $a->_date <=> $b->_date }
605 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
611 qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
614 sub cust_bill_pay_batch {
616 qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
621 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
627 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
628 sort { $a->_date <=> $b->_date }
629 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
634 =item cust_credit_bill
636 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
642 map { $_ } #return $self->num_cust_credit_bill unless wantarray;
643 sort { $a->_date <=> $b->_date }
644 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
648 sub cust_credit_bill {
649 shift->cust_credited(@_);
652 =item cust_bill_pay_pkgnum PKGNUM
654 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
655 with matching pkgnum.
659 sub cust_bill_pay_pkgnum {
660 my( $self, $pkgnum ) = @_;
661 map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
662 sort { $a->_date <=> $b->_date }
663 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
669 =item cust_credited_pkgnum PKGNUM
671 =item cust_credit_bill_pkgnum PKGNUM
673 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
674 with matching pkgnum.
678 sub cust_credited_pkgnum {
679 my( $self, $pkgnum ) = @_;
680 map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
681 sort { $a->_date <=> $b->_date }
682 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
688 sub cust_credit_bill_pkgnum {
689 shift->cust_credited_pkgnum(@_);
694 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
701 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
703 foreach (@taxlines) { $total += $_->setup; }
709 Returns the amount owed (still outstanding) on this invoice, which is charged
710 minus all payment applications (see L<FS::cust_bill_pay>) and credit
711 applications (see L<FS::cust_credit_bill>).
717 my $balance = $self->charged;
718 $balance -= $_->amount foreach ( $self->cust_bill_pay );
719 $balance -= $_->amount foreach ( $self->cust_credited );
720 $balance = sprintf( "%.2f", $balance);
721 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
726 my( $self, $pkgnum ) = @_;
728 #my $balance = $self->charged;
730 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
732 $balance -= $_->amount for $self->cust_bill_pay_pkgnum($pkgnum);
733 $balance -= $_->amount for $self->cust_credited_pkgnum($pkgnum);
735 $balance = sprintf( "%.2f", $balance);
736 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
740 =item apply_payments_and_credits [ OPTION => VALUE ... ]
742 Applies unapplied payments and credits to this invoice.
744 A hash of optional arguments may be passed. Currently "manual" is supported.
745 If true, a payment receipt is sent instead of a statement when
746 'payment_receipt_email' configuration option is set.
748 If there is an error, returns the error, otherwise returns false.
752 sub apply_payments_and_credits {
753 my( $self, %options ) = @_;
755 local $SIG{HUP} = 'IGNORE';
756 local $SIG{INT} = 'IGNORE';
757 local $SIG{QUIT} = 'IGNORE';
758 local $SIG{TERM} = 'IGNORE';
759 local $SIG{TSTP} = 'IGNORE';
760 local $SIG{PIPE} = 'IGNORE';
762 my $oldAutoCommit = $FS::UID::AutoCommit;
763 local $FS::UID::AutoCommit = 0;
766 $self->select_for_update; #mutex
768 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
769 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
771 if ( $conf->exists('pkg-balances') ) {
772 # limit @payments & @credits to those w/ a pkgnum grepped from $self
773 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
774 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
775 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
778 while ( $self->owed > 0 and ( @payments || @credits ) ) {
781 if ( @payments && @credits ) {
783 #decide which goes first by weight of top (unapplied) line item
785 my @open_lineitems = $self->open_cust_bill_pkg;
788 max( map { $_->part_pkg->pay_weight || 0 }
793 my $max_credit_weight =
794 max( map { $_->part_pkg->credit_weight || 0 }
800 #if both are the same... payments first? it has to be something
801 if ( $max_pay_weight >= $max_credit_weight ) {
807 } elsif ( @payments ) {
809 } elsif ( @credits ) {
812 die "guru meditation #12 and 35";
816 if ( $app eq 'pay' ) {
818 my $payment = shift @payments;
819 $unapp_amount = $payment->unapplied;
820 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
821 $app->pkgnum( $payment->pkgnum )
822 if $conf->exists('pkg-balances') && $payment->pkgnum;
824 } elsif ( $app eq 'credit' ) {
826 my $credit = shift @credits;
827 $unapp_amount = $credit->credited;
828 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
829 $app->pkgnum( $credit->pkgnum )
830 if $conf->exists('pkg-balances') && $credit->pkgnum;
833 die "guru meditation #12 and 35";
837 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
838 warn "owed_pkgnum ". $app->pkgnum;
839 $owed = $self->owed_pkgnum($app->pkgnum);
843 next unless $owed > 0;
845 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
846 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
848 $app->invnum( $self->invnum );
850 my $error = $app->insert(%options);
852 $dbh->rollback if $oldAutoCommit;
853 return "Error inserting ". $app->table. " record: $error";
855 die $error if $error;
859 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
864 =item generate_email OPTION => VALUE ...
872 sender address, required
876 alternate template name, optional
880 text attachment arrayref, optional
884 email subject, optional
888 notice name instead of "Invoice", optional
892 Returns an argument list to be passed to L<FS::Misc::send_email>.
903 my $me = '[FS::cust_bill::generate_email]';
906 'from' => $args{'from'},
907 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
911 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
912 'template' => $args{'template'},
913 'notice_name' => ( $args{'notice_name'} || 'Invoice' ),
916 my $cust_main = $self->cust_main;
918 if (ref($args{'to'}) eq 'ARRAY') {
919 $return{'to'} = $args{'to'};
921 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
922 $cust_main->invoicing_list
926 if ( $conf->exists('invoice_html') ) {
928 warn "$me creating HTML/text multipart message"
931 $return{'nobody'} = 1;
933 my $alternative = build MIME::Entity
934 'Type' => 'multipart/alternative',
935 'Encoding' => '7bit',
936 'Disposition' => 'inline'
940 if ( $conf->exists('invoice_email_pdf')
941 and scalar($conf->config('invoice_email_pdf_note')) ) {
943 warn "$me using 'invoice_email_pdf_note' in multipart message"
945 $data = [ map { $_ . "\n" }
946 $conf->config('invoice_email_pdf_note')
951 warn "$me not using 'invoice_email_pdf_note' in multipart message"
953 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
954 $data = $args{'print_text'};
956 $data = [ $self->print_text(\%opt) ];
961 $alternative->attach(
962 'Type' => 'text/plain',
963 #'Encoding' => 'quoted-printable',
964 'Encoding' => '7bit',
966 'Disposition' => 'inline',
969 $args{'from'} =~ /\@([\w\.\-]+)/;
970 my $from = $1 || 'example.com';
971 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
974 my $agentnum = $cust_main->agentnum;
975 if ( defined($args{'template'}) && length($args{'template'})
976 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
979 $logo = 'logo_'. $args{'template'}. '.png';
983 my $image_data = $conf->config_binary( $logo, $agentnum);
985 my $image = build MIME::Entity
986 'Type' => 'image/png',
987 'Encoding' => 'base64',
988 'Data' => $image_data,
989 'Filename' => 'logo.png',
990 'Content-ID' => "<$content_id>",
994 if($conf->exists('invoice-barcode')){
995 my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
996 $barcode = build MIME::Entity
997 'Type' => 'image/png',
998 'Encoding' => 'base64',
999 'Data' => $self->invoice_barcode(0),
1000 'Filename' => 'barcode.png',
1001 'Content-ID' => "<$barcode_content_id>",
1003 $opt{'barcode_cid'} = $barcode_content_id;
1006 $alternative->attach(
1007 'Type' => 'text/html',
1008 'Encoding' => 'quoted-printable',
1009 'Data' => [ '<html>',
1012 ' '. encode_entities($return{'subject'}),
1015 ' <body bgcolor="#e8e8e8">',
1016 $self->print_html({ 'cid'=>$content_id, %opt }),
1020 'Disposition' => 'inline',
1021 #'Filename' => 'invoice.pdf',
1024 my @otherparts = ();
1025 if ( $cust_main->email_csv_cdr ) {
1027 push @otherparts, build MIME::Entity
1028 'Type' => 'text/csv',
1029 'Encoding' => '7bit',
1030 'Data' => [ map { "$_\n" }
1031 $self->call_details('prepend_billed_number' => 1)
1033 'Disposition' => 'attachment',
1034 'Filename' => 'usage-'. $self->invnum. '.csv',
1039 if ( $conf->exists('invoice_email_pdf') ) {
1044 # multipart/alternative
1050 my $related = build MIME::Entity 'Type' => 'multipart/related',
1051 'Encoding' => '7bit';
1053 #false laziness w/Misc::send_email
1054 $related->head->replace('Content-type',
1055 $related->mime_type.
1056 '; boundary="'. $related->head->multipart_boundary. '"'.
1057 '; type=multipart/alternative'
1060 $related->add_part($alternative);
1062 $related->add_part($image);
1064 my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
1066 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
1070 #no other attachment:
1072 # multipart/alternative
1077 $return{'content-type'} = 'multipart/related';
1078 if($conf->exists('invoice-barcode')){
1079 $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ];
1082 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
1084 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
1085 #$return{'disposition'} = 'inline';
1091 if ( $conf->exists('invoice_email_pdf') ) {
1092 warn "$me creating PDF attachment"
1095 #mime parts arguments a la MIME::Entity->build().
1096 $return{'mimeparts'} = [
1097 { $self->mimebuild_pdf(\%opt) }
1101 if ( $conf->exists('invoice_email_pdf')
1102 and scalar($conf->config('invoice_email_pdf_note')) ) {
1104 warn "$me using 'invoice_email_pdf_note'"
1106 $return{'body'} = [ map { $_ . "\n" }
1107 $conf->config('invoice_email_pdf_note')
1112 warn "$me not using 'invoice_email_pdf_note'"
1114 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1115 $return{'body'} = $args{'print_text'};
1117 $return{'body'} = [ $self->print_text(\%opt) ];
1130 Returns a list suitable for passing to MIME::Entity->build(), representing
1131 this invoice as PDF attachment.
1138 'Type' => 'application/pdf',
1139 'Encoding' => 'base64',
1140 'Data' => [ $self->print_pdf(@_) ],
1141 'Disposition' => 'attachment',
1142 'Filename' => 'invoice-'. $self->invnum. '.pdf',
1146 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1148 Sends this invoice to the destinations configured for this customer: sends
1149 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1151 Options can be passed as a hashref (recommended) or as a list of up to
1152 four values for templatename, agentnum, invoice_from and amount.
1154 I<template>, if specified, is the name of a suffix for alternate invoices.
1156 I<agentnum>, if specified, means that this invoice will only be sent for customers
1157 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
1158 single agent) or an arrayref of agentnums.
1160 I<invoice_from>, if specified, overrides the default email invoice From: address.
1162 I<amount>, if specified, only sends the invoice if the total amount owed on this
1163 invoice and all older invoices is greater than the specified amount.
1165 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1169 sub queueable_send {
1172 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1173 or die "invalid invoice number: " . $opt{invnum};
1175 my @args = ( $opt{template}, $opt{agentnum} );
1176 push @args, $opt{invoice_from}
1177 if exists($opt{invoice_from}) && $opt{invoice_from};
1179 my $error = $self->send( @args );
1180 die $error if $error;
1187 my( $template, $invoice_from, $notice_name );
1189 my $balance_over = 0;
1193 $template = $opt->{'template'} || '';
1194 if ( $agentnums = $opt->{'agentnum'} ) {
1195 $agentnums = [ $agentnums ] unless ref($agentnums);
1197 $invoice_from = $opt->{'invoice_from'};
1198 $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1199 $notice_name = $opt->{'notice_name'};
1201 $template = scalar(@_) ? shift : '';
1202 if ( scalar(@_) && $_[0] ) {
1203 $agentnums = ref($_[0]) ? shift : [ shift ];
1205 $invoice_from = shift if scalar(@_);
1206 $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1209 return 'N/A' unless ! $agentnums
1210 or grep { $_ == $self->cust_main->agentnum } @$agentnums;
1213 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
1215 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1216 $conf->config('invoice_from', $self->cust_main->agentnum );
1219 'template' => $template,
1220 'invoice_from' => $invoice_from,
1221 'notice_name' => ( $notice_name || 'Invoice' ),
1224 my @invoicing_list = $self->cust_main->invoicing_list;
1226 #$self->email_invoice(\%opt)
1228 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
1230 #$self->print_invoice(\%opt)
1232 if grep { $_ eq 'POST' } @invoicing_list; #postal
1234 $self->fax_invoice(\%opt)
1235 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1241 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
1243 Emails this invoice.
1245 Options can be passed as a hashref (recommended) or as a list of up to
1246 two values for templatename and invoice_from.
1248 I<template>, if specified, is the name of a suffix for alternate invoices.
1250 I<invoice_from>, if specified, overrides the default email invoice From: address.
1252 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1256 sub queueable_email {
1259 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1260 or die "invalid invoice number: " . $opt{invnum};
1262 my @args = ( $opt{template} );
1263 push @args, $opt{invoice_from}
1264 if exists($opt{invoice_from}) && $opt{invoice_from};
1266 my $error = $self->email( @args );
1267 die $error if $error;
1271 #sub email_invoice {
1275 my( $template, $invoice_from, $notice_name );
1278 $template = $opt->{'template'} || '';
1279 $invoice_from = $opt->{'invoice_from'};
1280 $notice_name = $opt->{'notice_name'} || 'Invoice';
1282 $template = scalar(@_) ? shift : '';
1283 $invoice_from = shift if scalar(@_);
1284 $notice_name = 'Invoice';
1287 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1288 $conf->config('invoice_from', $self->cust_main->agentnum );
1290 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1291 $self->cust_main->invoicing_list;
1293 if ( ! @invoicing_list ) { #no recipients
1294 if ( $conf->exists('cust_bill-no_recipients-error') ) {
1295 die 'No recipients for customer #'. $self->custnum;
1297 #default: better to notify this person than silence
1298 @invoicing_list = ($invoice_from);
1302 my $subject = $self->email_subject($template);
1304 my $error = send_email(
1305 $self->generate_email(
1306 'from' => $invoice_from,
1307 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1308 'subject' => $subject,
1309 'template' => $template,
1310 'notice_name' => $notice_name,
1313 die "can't email invoice: $error\n" if $error;
1314 #die "$error\n" if $error;
1321 #my $template = scalar(@_) ? shift : '';
1324 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1327 my $cust_main = $self->cust_main;
1328 my $name = $cust_main->name;
1329 my $name_short = $cust_main->name_short;
1330 my $invoice_number = $self->invnum;
1331 my $invoice_date = $self->_date_pretty;
1333 eval qq("$subject");
1336 =item lpr_data HASHREF | [ TEMPLATE ]
1338 Returns the postscript or plaintext for this invoice as an arrayref.
1340 Options can be passed as a hashref (recommended) or as a single optional value
1343 I<template>, if specified, is the name of a suffix for alternate invoices.
1345 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1351 my( $template, $notice_name );
1354 $template = $opt->{'template'} || '';
1355 $notice_name = $opt->{'notice_name'} || 'Invoice';
1357 $template = scalar(@_) ? shift : '';
1358 $notice_name = 'Invoice';
1362 'template' => $template,
1363 'notice_name' => $notice_name,
1366 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1367 [ $self->$method( \%opt ) ];
1370 =item print HASHREF | [ TEMPLATE ]
1372 Prints this invoice.
1374 Options can be passed as a hashref (recommended) or as a single optional
1377 I<template>, if specified, is the name of a suffix for alternate invoices.
1379 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1383 #sub print_invoice {
1386 my( $template, $notice_name );
1389 $template = $opt->{'template'} || '';
1390 $notice_name = $opt->{'notice_name'} || 'Invoice';
1392 $template = scalar(@_) ? shift : '';
1393 $notice_name = 'Invoice';
1397 'template' => $template,
1398 'notice_name' => $notice_name,
1401 if($conf->exists('invoice_print_pdf')) {
1402 # Add the invoice to the current batch.
1403 $self->batch_invoice(\%opt);
1406 do_print $self->lpr_data(\%opt);
1410 =item fax_invoice HASHREF | [ TEMPLATE ]
1414 Options can be passed as a hashref (recommended) or as a single optional
1417 I<template>, if specified, is the name of a suffix for alternate invoices.
1419 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1425 my( $template, $notice_name );
1428 $template = $opt->{'template'} || '';
1429 $notice_name = $opt->{'notice_name'} || 'Invoice';
1431 $template = scalar(@_) ? shift : '';
1432 $notice_name = 'Invoice';
1435 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1436 unless $conf->exists('invoice_latex');
1438 my $dialstring = $self->cust_main->getfield('fax');
1442 'template' => $template,
1443 'notice_name' => $notice_name,
1446 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1447 'dialstring' => $dialstring,
1449 die $error if $error;
1453 =item batch_invoice [ HASHREF ]
1455 Place this invoice into the open batch (see C<FS::bill_batch>). If there
1456 isn't an open batch, one will be created.
1461 my ($self, $opt) = @_;
1462 my $batch = FS::bill_batch->get_open_batch;
1463 my $cust_bill_batch = FS::cust_bill_batch->new({
1464 batchnum => $batch->batchnum,
1465 invnum => $self->invnum,
1467 return $cust_bill_batch->insert($opt);
1470 =item ftp_invoice [ TEMPLATENAME ]
1472 Sends this invoice data via FTP.
1474 TEMPLATENAME is unused?
1480 my $template = scalar(@_) ? shift : '';
1483 'protocol' => 'ftp',
1484 'server' => $conf->config('cust_bill-ftpserver'),
1485 'username' => $conf->config('cust_bill-ftpusername'),
1486 'password' => $conf->config('cust_bill-ftppassword'),
1487 'dir' => $conf->config('cust_bill-ftpdir'),
1488 'format' => $conf->config('cust_bill-ftpformat'),
1492 =item spool_invoice [ TEMPLATENAME ]
1494 Spools this invoice data (see L<FS::spool_csv>)
1496 TEMPLATENAME is unused?
1502 my $template = scalar(@_) ? shift : '';
1505 'format' => $conf->config('cust_bill-spoolformat'),
1506 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1510 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1512 Like B<send>, but only sends the invoice if it is the newest open invoice for
1517 sub send_if_newest {
1522 grep { $_->owed > 0 }
1523 qsearch('cust_bill', {
1524 'custnum' => $self->custnum,
1525 #'_date' => { op=>'>', value=>$self->_date },
1526 'invnum' => { op=>'>', value=>$self->invnum },
1533 =item send_csv OPTION => VALUE, ...
1535 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1539 protocol - currently only "ftp"
1545 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1546 and YYMMDDHHMMSS is a timestamp.
1548 See L</print_csv> for a description of the output format.
1553 my($self, %opt) = @_;
1557 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1558 mkdir $spooldir, 0700 unless -d $spooldir;
1560 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1561 my $file = "$spooldir/$tracctnum.csv";
1563 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1565 open(CSV, ">$file") or die "can't open $file: $!";
1573 if ( $opt{protocol} eq 'ftp' ) {
1574 eval "use Net::FTP;";
1576 $net = Net::FTP->new($opt{server}) or die @$;
1578 die "unknown protocol: $opt{protocol}";
1581 $net->login( $opt{username}, $opt{password} )
1582 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1584 $net->binary or die "can't set binary mode";
1586 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1588 $net->put($file) or die "can't put $file: $!";
1598 Spools CSV invoice data.
1604 =item format - 'default' or 'billco'
1606 =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>).
1608 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1610 =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.
1617 my($self, %opt) = @_;
1619 my $cust_main = $self->cust_main;
1621 if ( $opt{'dest'} ) {
1622 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1623 $cust_main->invoicing_list;
1624 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1625 || ! keys %invoicing_list;
1628 if ( $opt{'balanceover'} ) {
1630 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1633 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1634 mkdir $spooldir, 0700 unless -d $spooldir;
1636 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1640 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1641 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1644 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1646 open(CSV, ">>$file") or die "can't open $file: $!";
1647 flock(CSV, LOCK_EX);
1652 if ( lc($opt{'format'}) eq 'billco' ) {
1654 flock(CSV, LOCK_UN);
1659 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1662 open(CSV,">>$file") or die "can't open $file: $!";
1663 flock(CSV, LOCK_EX);
1669 flock(CSV, LOCK_UN);
1676 =item print_csv OPTION => VALUE, ...
1678 Returns CSV data for this invoice.
1682 format - 'default' or 'billco'
1684 Returns a list consisting of two scalars. The first is a single line of CSV
1685 header information for this invoice. The second is one or more lines of CSV
1686 detail information for this invoice.
1688 If I<format> is not specified or "default", the fields of the CSV file are as
1691 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1695 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1697 B<record_type> is C<cust_bill> for the initial header line only. The
1698 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1699 fields are filled in.
1701 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1702 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1705 =item invnum - invoice number
1707 =item custnum - customer number
1709 =item _date - invoice date
1711 =item charged - total invoice amount
1713 =item first - customer first name
1715 =item last - customer first name
1717 =item company - company name
1719 =item address1 - address line 1
1721 =item address2 - address line 1
1731 =item pkg - line item description
1733 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1735 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1737 =item sdate - start date for recurring fee
1739 =item edate - end date for recurring fee
1743 If I<format> is "billco", the fields of the header CSV file are as follows:
1745 +-------------------------------------------------------------------+
1746 | FORMAT HEADER FILE |
1747 |-------------------------------------------------------------------|
1748 | Field | Description | Name | Type | Width |
1749 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1750 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1751 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1752 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1753 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1754 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1755 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1756 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1757 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1758 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1759 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1760 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1761 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1762 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1763 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1764 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1765 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1766 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1767 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1768 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1769 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1770 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1771 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1772 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1773 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1774 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1775 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1776 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1777 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1778 +-------+-------------------------------+------------+------+-------+
1780 If I<format> is "billco", the fields of the detail CSV file are as follows:
1782 FORMAT FOR DETAIL FILE
1784 Field | Description | Name | Type | Width
1785 1 | N/A-Leave Empty | RC | CHAR | 2
1786 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1787 3 | Account Number | TRACCTNUM | CHAR | 15
1788 4 | Invoice Number | TRINVOICE | CHAR | 15
1789 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1790 6 | Transaction Detail | DETAILS | CHAR | 100
1791 7 | Amount | AMT | NUM* | 9
1792 8 | Line Format Control** | LNCTRL | CHAR | 2
1793 9 | Grouping Code | GROUP | CHAR | 2
1794 10 | User Defined | ACCT CODE | CHAR | 15
1799 my($self, %opt) = @_;
1801 eval "use Text::CSV_XS";
1804 my $cust_main = $self->cust_main;
1806 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1808 if ( lc($opt{'format'}) eq 'billco' ) {
1811 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1813 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1815 my( $previous_balance, @unused ) = $self->previous; #previous balance
1817 my $pmt_cr_applied = 0;
1818 $pmt_cr_applied += $_->{'amount'}
1819 foreach ( $self->_items_payments, $self->_items_credits ) ;
1821 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1824 '', # 1 | N/A-Leave Empty CHAR 2
1825 '', # 2 | N/A-Leave Empty CHAR 15
1826 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1827 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1828 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1829 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1830 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1831 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1832 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1833 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1834 '', # 10 | Ancillary Billing Information CHAR 30
1835 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1836 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1839 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1842 $duedate, # 14 | Bill Due Date CHAR 10
1844 $previous_balance, # 15 | Previous Balance NUM* 9
1845 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1846 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1847 $totaldue, # 18 | Total Amt Due NUM* 9
1848 $totaldue, # 19 | Total Amt Due NUM* 9
1849 '', # 20 | 30 Day Aging NUM* 9
1850 '', # 21 | 60 Day Aging NUM* 9
1851 '', # 22 | 90 Day Aging NUM* 9
1852 'N', # 23 | Y/N CHAR 1
1853 '', # 24 | Remittance automation CHAR 100
1854 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1855 $self->custnum, # 26 | Customer Reference Number CHAR 15
1856 '0', # 27 | Federal Tax*** NUM* 9
1857 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1858 '0', # 29 | Other Taxes & Fees*** NUM* 9
1867 time2str("%x", $self->_date),
1868 sprintf("%.2f", $self->charged),
1869 ( map { $cust_main->getfield($_) }
1870 qw( first last company address1 address2 city state zip country ) ),
1872 ) or die "can't create csv";
1875 my $header = $csv->string. "\n";
1878 if ( lc($opt{'format'}) eq 'billco' ) {
1881 foreach my $item ( $self->_items_pkg ) {
1884 '', # 1 | N/A-Leave Empty CHAR 2
1885 '', # 2 | N/A-Leave Empty CHAR 15
1886 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1887 $self->invnum, # 4 | Invoice Number CHAR 15
1888 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1889 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1890 $item->{'amount'}, # 7 | Amount NUM* 9
1891 '', # 8 | Line Format Control** CHAR 2
1892 '', # 9 | Grouping Code CHAR 2
1893 '', # 10 | User Defined CHAR 15
1896 $detail .= $csv->string. "\n";
1902 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1904 my($pkg, $setup, $recur, $sdate, $edate);
1905 if ( $cust_bill_pkg->pkgnum ) {
1907 ($pkg, $setup, $recur, $sdate, $edate) = (
1908 $cust_bill_pkg->part_pkg->pkg,
1909 ( $cust_bill_pkg->setup != 0
1910 ? sprintf("%.2f", $cust_bill_pkg->setup )
1912 ( $cust_bill_pkg->recur != 0
1913 ? sprintf("%.2f", $cust_bill_pkg->recur )
1915 ( $cust_bill_pkg->sdate
1916 ? time2str("%x", $cust_bill_pkg->sdate)
1918 ($cust_bill_pkg->edate
1919 ?time2str("%x", $cust_bill_pkg->edate)
1923 } else { #pkgnum tax
1924 next unless $cust_bill_pkg->setup != 0;
1925 $pkg = $cust_bill_pkg->desc;
1926 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
1927 ( $sdate, $edate ) = ( '', '' );
1933 ( map { '' } (1..11) ),
1934 ($pkg, $setup, $recur, $sdate, $edate)
1935 ) or die "can't create csv";
1937 $detail .= $csv->string. "\n";
1943 ( $header, $detail );
1949 Pays this invoice with a compliemntary payment. If there is an error,
1950 returns the error, otherwise returns false.
1956 my $cust_pay = new FS::cust_pay ( {
1957 'invnum' => $self->invnum,
1958 'paid' => $self->owed,
1961 'payinfo' => $self->cust_main->payinfo,
1969 Attempts to pay this invoice with a credit card payment via a
1970 Business::OnlinePayment realtime gateway. See
1971 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1972 for supported processors.
1978 $self->realtime_bop( 'CC', @_ );
1983 Attempts to pay this invoice with an electronic check (ACH) payment via a
1984 Business::OnlinePayment realtime gateway. See
1985 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1986 for supported processors.
1992 $self->realtime_bop( 'ECHECK', @_ );
1997 Attempts to pay this invoice with phone bill (LEC) payment via a
1998 Business::OnlinePayment realtime gateway. See
1999 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2000 for supported processors.
2006 $self->realtime_bop( 'LEC', @_ );
2010 my( $self, $method ) = (shift,shift);
2013 my $cust_main = $self->cust_main;
2014 my $balance = $cust_main->balance;
2015 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2016 $amount = sprintf("%.2f", $amount);
2017 return "not run (balance $balance)" unless $amount > 0;
2019 my $description = 'Internet Services';
2020 if ( $conf->exists('business-onlinepayment-description') ) {
2021 my $dtempl = $conf->config('business-onlinepayment-description');
2023 my $agent_obj = $cust_main->agent
2024 or die "can't retreive agent for $cust_main (agentnum ".
2025 $cust_main->agentnum. ")";
2026 my $agent = $agent_obj->agent;
2027 my $pkgs = join(', ',
2028 map { $_->part_pkg->pkg }
2029 grep { $_->pkgnum } $self->cust_bill_pkg
2031 $description = eval qq("$dtempl");
2034 $cust_main->realtime_bop($method, $amount,
2035 'description' => $description,
2036 'invnum' => $self->invnum,
2037 #this didn't do what we want, it just calls apply_payments_and_credits
2039 'apply_to_invoice' => 1,
2042 #this changes application behavior: auto payments
2043 #triggered against a specific invoice are now applied
2044 #to that invoice instead of oldest open.
2050 =item batch_card OPTION => VALUE...
2052 Adds a payment for this invoice to the pending credit card batch (see
2053 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2054 runs the payment using a realtime gateway.
2059 my ($self, %options) = @_;
2060 my $cust_main = $self->cust_main;
2062 $options{invnum} = $self->invnum;
2064 $cust_main->batch_card(%options);
2067 sub _agent_template {
2069 $self->cust_main->agent_template;
2072 sub _agent_invoice_from {
2074 $self->cust_main->agent_invoice_from;
2077 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2079 Returns an text invoice, as a list of lines.
2081 Options can be passed as a hashref (recommended) or as a list of time, template
2082 and then any key/value pairs for any other options.
2084 I<time>, if specified, is used to control the printing of overdue messages. The
2085 default is now. It isn't the date of the invoice; that's the `_date' field.
2086 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2087 L<Time::Local> and L<Date::Parse> for conversion functions.
2089 I<template>, if specified, is the name of a suffix for alternate invoices.
2091 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2097 my( $today, $template, %opt );
2099 %opt = %{ shift() };
2100 $today = delete($opt{'time'}) || '';
2101 $template = delete($opt{template}) || '';
2103 ( $today, $template, %opt ) = @_;
2106 my %params = ( 'format' => 'template' );
2107 $params{'time'} = $today if $today;
2108 $params{'template'} = $template if $template;
2109 $params{$_} = $opt{$_}
2110 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2112 $self->print_generic( %params );
2115 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2117 Internal method - returns a filename of a filled-in LaTeX template for this
2118 invoice (Note: add ".tex" to get the actual filename), and a filename of
2119 an associated logo (with the .eps extension included).
2121 See print_ps and print_pdf for methods that return PostScript and PDF output.
2123 Options can be passed as a hashref (recommended) or as a list of time, template
2124 and then any key/value pairs for any other options.
2126 I<time>, if specified, is used to control the printing of overdue messages. The
2127 default is now. It isn't the date of the invoice; that's the `_date' field.
2128 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2129 L<Time::Local> and L<Date::Parse> for conversion functions.
2131 I<template>, if specified, is the name of a suffix for alternate invoices.
2133 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2139 my( $today, $template, %opt );
2141 %opt = %{ shift() };
2142 $today = delete($opt{'time'}) || '';
2143 $template = delete($opt{template}) || '';
2145 ( $today, $template, %opt ) = @_;
2148 my %params = ( 'format' => 'latex' );
2149 $params{'time'} = $today if $today;
2150 $params{'template'} = $template if $template;
2151 $params{$_} = $opt{$_}
2152 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2154 $template ||= $self->_agent_template;
2156 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2157 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2161 ) or die "can't open temp file: $!\n";
2163 my $agentnum = $self->cust_main->agentnum;
2165 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2166 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2167 or die "can't write temp file: $!\n";
2169 print $lh $conf->config_binary('logo.eps', $agentnum)
2170 or die "can't write temp file: $!\n";
2173 $params{'logo_file'} = $lh->filename;
2175 if($conf->exists('invoice-barcode')){
2176 my $png_file = $self->invoice_barcode($dir);
2177 my $eps_file = $png_file;
2178 $eps_file =~ s/\.png$/.eps/g;
2179 $png_file =~ /(barcode.*png)/;
2181 $eps_file =~ /(barcode.*eps)/;
2184 my $curr_dir = cwd();
2186 # after painfuly long experimentation, it was determined that sam2p won't
2187 # accept : and other chars in the path, no matter how hard I tried to
2188 # escape them, hence the chdir (and chdir back, just to be safe)
2189 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
2190 or die "sam2p failed: $!\n";
2194 $params{'barcode_file'} = $eps_file;
2197 my @filled_in = $self->print_generic( %params );
2199 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2203 ) or die "can't open temp file: $!\n";
2204 print $fh join('', @filled_in );
2207 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2208 return ($1, $params{'logo_file'}, $params{'barcode_file'});
2212 =item invoice_barcode DIR_OR_FALSE
2214 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2215 it is taken as the temp directory where the PNG file will be generated and the
2216 PNG file name is returned. Otherwise, the PNG image itself is returned.
2220 sub invoice_barcode {
2221 my ($self, $dir) = (shift,shift);
2223 my $gdbar = new GD::Barcode('Code39',$self->invnum);
2224 die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2225 my $gd = $gdbar->plot(Height => 30);
2228 my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2232 ) or die "can't open temp file: $!\n";
2233 print $bh $gd->png or die "cannot write barcode to file: $!\n";
2234 my $png_file = $bh->filename;
2241 =item print_generic OPTION => VALUE ...
2243 Internal method - returns a filled-in template for this invoice as a scalar.
2245 See print_ps and print_pdf for methods that return PostScript and PDF output.
2247 Non optional options include
2248 format - latex, html, template
2250 Optional options include
2252 template - a value used as a suffix for a configuration template
2254 time - a value used to control the printing of overdue messages. The
2255 default is now. It isn't the date of the invoice; that's the `_date' field.
2256 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2257 L<Time::Local> and L<Date::Parse> for conversion functions.
2261 unsquelch_cdr - overrides any per customer cdr squelching when true
2263 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2267 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2268 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2269 # yes: fixed width (dot matrix) text printing will be borked
2272 my( $self, %params ) = @_;
2273 my $today = $params{today} ? $params{today} : time;
2274 warn "$me print_generic called on $self with suffix $params{template}\n"
2277 my $format = $params{format};
2278 die "Unknown format: $format"
2279 unless $format =~ /^(latex|html|template)$/;
2281 my $cust_main = $self->cust_main;
2282 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2283 unless $cust_main->payname
2284 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2286 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2287 'html' => [ '<%=', '%>' ],
2288 'template' => [ '{', '}' ],
2291 warn "$me print_generic creating template\n"
2294 #create the template
2295 my $template = $params{template} ? $params{template} : $self->_agent_template;
2296 my $templatefile = "invoice_$format";
2297 $templatefile .= "_$template"
2298 if length($template);
2299 my @invoice_template = map "$_\n", $conf->config($templatefile)
2300 or die "cannot load config data $templatefile";
2303 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2304 #change this to a die when the old code is removed
2305 warn "old-style invoice template $templatefile; ".
2306 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2307 $old_latex = 'true';
2308 @invoice_template = _translate_old_latex_format(@invoice_template);
2311 warn "$me print_generic creating T:T object\n"
2314 my $text_template = new Text::Template(
2316 SOURCE => \@invoice_template,
2317 DELIMITERS => $delimiters{$format},
2320 warn "$me print_generic compiling T:T object\n"
2323 $text_template->compile()
2324 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2327 # additional substitution could possibly cause breakage in existing templates
2328 my %convert_maps = (
2330 'notes' => sub { map "$_", @_ },
2331 'footer' => sub { map "$_", @_ },
2332 'smallfooter' => sub { map "$_", @_ },
2333 'returnaddress' => sub { map "$_", @_ },
2334 'coupon' => sub { map "$_", @_ },
2335 'summary' => sub { map "$_", @_ },
2341 s/%%(.*)$/<!-- $1 -->/g;
2342 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2343 s/\\begin\{enumerate\}/<ol>/g;
2345 s/\\end\{enumerate\}/<\/ol>/g;
2346 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2355 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2357 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2362 s/\\\\\*?\s*$/<BR>/;
2363 s/\\hyphenation\{[\w\s\-]+}//;
2368 'coupon' => sub { "" },
2369 'summary' => sub { "" },
2376 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2377 s/\\begin\{enumerate\}//g;
2379 s/\\end\{enumerate\}//g;
2380 s/\\textbf\{(.*)\}/$1/g;
2387 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2389 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2394 s/\\\\\*?\s*$/\n/; # dubious
2395 s/\\hyphenation\{[\w\s\-]+}//;
2399 'coupon' => sub { "" },
2400 'summary' => sub { "" },
2405 # hashes for differing output formats
2406 my %nbsps = ( 'latex' => '~',
2407 'html' => '', # '&nbps;' would be nice
2408 'template' => '', # not used
2410 my $nbsp = $nbsps{$format};
2412 my %escape_functions = ( 'latex' => \&_latex_escape,
2413 'html' => \&_html_escape_nbsp,#\&encode_entities,
2414 'template' => sub { shift },
2416 my $escape_function = $escape_functions{$format};
2417 my $escape_function_nonbsp = ($format eq 'html')
2418 ? \&_html_escape : $escape_function;
2420 my %date_formats = ( 'latex' => $date_format_long,
2421 'html' => $date_format_long,
2424 $date_formats{'html'} =~ s/ / /g;
2426 my $date_format = $date_formats{$format};
2428 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2430 'html' => sub { return '<b>'. shift(). '</b>'
2432 'template' => sub { shift },
2434 my $embolden_function = $embolden_functions{$format};
2436 warn "$me generating template variables\n"
2439 # generate template variables
2442 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2446 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2452 $returnaddress = join("\n",
2453 $conf->config_orbase("invoice_${format}returnaddress", $template)
2456 } elsif ( grep /\S/,
2457 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2459 my $convert_map = $convert_maps{$format}{'returnaddress'};
2462 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2467 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2469 my $convert_map = $convert_maps{$format}{'returnaddress'};
2470 $returnaddress = join( "\n", &$convert_map(
2471 map { s/( {2,})/'~' x length($1)/eg;
2475 ( $conf->config('company_name', $self->cust_main->agentnum),
2476 $conf->config('company_address', $self->cust_main->agentnum),
2483 my $warning = "Couldn't find a return address; ".
2484 "do you need to set the company_address configuration value?";
2486 $returnaddress = $nbsp;
2487 #$returnaddress = $warning;
2491 warn "$me generating invoice data\n"
2494 my $agentnum = $self->cust_main->agentnum;
2496 my %invoice_data = (
2499 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2500 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2501 'returnaddress' => $returnaddress,
2502 'agent' => &$escape_function($cust_main->agent->agent),
2505 'invnum' => $self->invnum,
2506 'date' => time2str($date_format, $self->_date),
2507 'today' => time2str($date_format_long, $today),
2508 'terms' => $self->terms,
2509 'template' => $template, #params{'template'},
2510 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2511 'current_charges' => sprintf("%.2f", $self->charged),
2512 'duedate' => $self->due_date2str($rdate_format), #date_format?
2515 'custnum' => $cust_main->display_custnum,
2516 'agent_custid' => &$escape_function($cust_main->agent_custid),
2517 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2518 payname company address1 address2 city state zip fax
2522 'ship_enable' => $conf->exists('invoice-ship_address'),
2523 'unitprices' => $conf->exists('invoice-unitprice'),
2524 'smallernotes' => $conf->exists('invoice-smallernotes'),
2525 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2526 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2528 #layout info -- would be fancy to calc some of this and bury the template
2530 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2531 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2532 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2533 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2534 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2535 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2536 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2537 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2538 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2539 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2541 # better hang on to conf_dir for a while (for old templates)
2542 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2544 #these are only used when doing paged plaintext
2550 $invoice_data{finance_section} = '';
2551 if ( $conf->config('finance_pkgclass') ) {
2553 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2554 $invoice_data{finance_section} = $pkg_class->categoryname;
2556 $invoice_data{finance_amount} = '0.00';
2557 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2559 my $countrydefault = $conf->config('countrydefault') || 'US';
2560 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2561 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2562 my $method = $prefix.$_;
2563 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2565 $invoice_data{'ship_country'} = ''
2566 if ( $invoice_data{'ship_country'} eq $countrydefault );
2568 $invoice_data{'cid'} = $params{'cid'}
2571 if ( $cust_main->country eq $countrydefault ) {
2572 $invoice_data{'country'} = '';
2574 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2578 $invoice_data{'address'} = \@address;
2580 $cust_main->payname.
2581 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2582 ? " (P.O. #". $cust_main->payinfo. ")"
2586 push @address, $cust_main->company
2587 if $cust_main->company;
2588 push @address, $cust_main->address1;
2589 push @address, $cust_main->address2
2590 if $cust_main->address2;
2592 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2593 push @address, $invoice_data{'country'}
2594 if $invoice_data{'country'};
2596 while (scalar(@address) < 5);
2598 $invoice_data{'logo_file'} = $params{'logo_file'}
2599 if $params{'logo_file'};
2600 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2601 if $params{'barcode_file'};
2602 $invoice_data{'barcode_img'} = $params{'barcode_img'}
2603 if $params{'barcode_img'};
2604 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2605 if $params{'barcode_cid'};
2607 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2608 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2609 #my $balance_due = $self->owed + $pr_total - $cr_total;
2610 my $balance_due = $self->owed + $pr_total;
2611 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2612 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2613 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2614 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2616 my $summarypage = '';
2617 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2620 $invoice_data{'summarypage'} = $summarypage;
2622 warn "$me substituting variables in notes, footer, smallfooter\n"
2625 foreach my $include (qw( notes footer smallfooter coupon )) {
2627 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2630 if ( $conf->exists($inc_file, $agentnum)
2631 && length( $conf->config($inc_file, $agentnum) ) ) {
2633 @inc_src = $conf->config($inc_file, $agentnum);
2637 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2639 my $convert_map = $convert_maps{$format}{$include};
2641 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2642 s/--\@\]/$delimiters{$format}[1]/g;
2645 &$convert_map( $conf->config($inc_file, $agentnum) );
2649 my $inc_tt = new Text::Template (
2651 SOURCE => [ map "$_\n", @inc_src ],
2652 DELIMITERS => $delimiters{$format},
2653 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2655 unless ( $inc_tt->compile() ) {
2656 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2657 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2661 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2663 $invoice_data{$include} =~ s/\n+$//
2664 if ($format eq 'latex');
2667 $invoice_data{'po_line'} =
2668 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2669 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2672 my %money_chars = ( 'latex' => '',
2673 'html' => $conf->config('money_char') || '$',
2676 my $money_char = $money_chars{$format};
2678 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2679 'html' => $conf->config('money_char') || '$',
2682 my $other_money_char = $other_money_chars{$format};
2683 $invoice_data{'dollar'} = $other_money_char;
2685 my @detail_items = ();
2686 my @total_items = ();
2690 $invoice_data{'detail_items'} = \@detail_items;
2691 $invoice_data{'total_items'} = \@total_items;
2692 $invoice_data{'buf'} = \@buf;
2693 $invoice_data{'sections'} = \@sections;
2695 warn "$me generating sections\n"
2698 my $previous_section = { 'description' => 'Previous Charges',
2699 'subtotal' => $other_money_char.
2700 sprintf('%.2f', $pr_total),
2701 'summarized' => $summarypage ? 'Y' : '',
2703 $previous_section->{posttotal} = '0 / 30 / 60/ 90 days overdue '.
2704 join(' / ', map { $cust_main->balance_date_range(@$_) }
2705 $self->_prior_month30s
2707 if $conf->exists('invoice_include_aging');
2710 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2711 'subtotal' => $taxtotal, # adjusted below
2712 'summarized' => $summarypage ? 'Y' : '',
2714 my $tax_weight = _pkg_category($tax_section->{description})
2715 ? _pkg_category($tax_section->{description})->weight
2717 $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2718 $tax_section->{'sort_weight'} = $tax_weight;
2721 my $adjusttotal = 0;
2722 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2723 'subtotal' => 0, # adjusted below
2724 'summarized' => $summarypage ? 'Y' : '',
2726 my $adjust_weight = _pkg_category($adjust_section->{description})
2727 ? _pkg_category($adjust_section->{description})->weight
2729 $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2730 $adjust_section->{'sort_weight'} = $adjust_weight;
2732 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2733 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2734 $invoice_data{'multisection'} = $multisection;
2735 my $late_sections = [];
2736 my $extra_sections = [];
2737 my $extra_lines = ();
2738 if ( $multisection ) {
2739 ($extra_sections, $extra_lines) =
2740 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2741 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2743 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2745 push @detail_items, @$extra_lines if $extra_lines;
2747 $self->_items_sections( $late_sections, # this could stand a refactor
2749 $escape_function_nonbsp,
2753 if ($conf->exists('svc_phone_sections')) {
2754 my ($phone_sections, $phone_lines) =
2755 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2756 push @{$late_sections}, @$phone_sections;
2757 push @detail_items, @$phone_lines;
2760 push @sections, { 'description' => '', 'subtotal' => '' };
2763 unless ( $conf->exists('disable_previous_balance')
2764 || $conf->exists('previous_balance-summary_only')
2768 warn "$me adding previous balances\n"
2771 foreach my $line_item ( $self->_items_previous ) {
2774 ext_description => [],
2776 $detail->{'ref'} = $line_item->{'pkgnum'};
2777 $detail->{'quantity'} = 1;
2778 $detail->{'section'} = $previous_section;
2779 $detail->{'description'} = &$escape_function($line_item->{'description'});
2780 if ( exists $line_item->{'ext_description'} ) {
2781 @{$detail->{'ext_description'}} = map {
2782 &$escape_function($_);
2783 } @{$line_item->{'ext_description'}};
2785 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2786 $line_item->{'amount'};
2787 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2789 push @detail_items, $detail;
2790 push @buf, [ $detail->{'description'},
2791 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2797 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2798 push @buf, ['','-----------'];
2799 push @buf, [ 'Total Previous Balance',
2800 $money_char. sprintf("%10.2f", $pr_total) ];
2804 if ( $conf->exists('svc_phone-did-summary') ) {
2805 warn "$me adding DID summary\n"
2808 my ($didsummary,$minutes) = $self->_did_summary;
2809 my $didsummary_desc = 'DID Activity Summary (Past 30 days)';
2811 { 'description' => $didsummary_desc,
2812 'ext_description' => [ $didsummary, $minutes ],
2817 foreach my $section (@sections, @$late_sections) {
2819 warn "$me adding section \n". Dumper($section)
2822 # begin some normalization
2823 $section->{'subtotal'} = $section->{'amount'}
2825 && !exists($section->{subtotal})
2826 && exists($section->{amount});
2828 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2829 if ( $invoice_data{finance_section} &&
2830 $section->{'description'} eq $invoice_data{finance_section} );
2832 $section->{'subtotal'} = $other_money_char.
2833 sprintf('%.2f', $section->{'subtotal'})
2836 # continue some normalization
2837 $section->{'amount'} = $section->{'subtotal'}
2841 if ( $section->{'description'} ) {
2842 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2847 warn "$me setting options\n"
2850 my $multilocation = scalar($cust_main->cust_location); #too expensive?
2852 $options{'section'} = $section if $multisection;
2853 $options{'format'} = $format;
2854 $options{'escape_function'} = $escape_function;
2855 $options{'format_function'} = sub { () } unless $unsquelched;
2856 $options{'unsquelched'} = $unsquelched;
2857 $options{'summary_page'} = $summarypage;
2858 $options{'skip_usage'} =
2859 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2860 $options{'multilocation'} = $multilocation;
2861 $options{'multisection'} = $multisection;
2863 warn "$me searching for line items\n"
2866 foreach my $line_item ( $self->_items_pkg(%options) ) {
2868 warn "$me adding line item $line_item\n"
2872 ext_description => [],
2874 $detail->{'ref'} = $line_item->{'pkgnum'};
2875 $detail->{'quantity'} = $line_item->{'quantity'};
2876 $detail->{'section'} = $section;
2877 $detail->{'description'} = &$escape_function($line_item->{'description'});
2878 if ( exists $line_item->{'ext_description'} ) {
2879 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2881 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2882 $line_item->{'amount'};
2883 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2884 $line_item->{'unit_amount'};
2885 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2887 push @detail_items, $detail;
2888 push @buf, ( [ $detail->{'description'},
2889 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2891 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2895 if ( $section->{'description'} ) {
2896 push @buf, ( ['','-----------'],
2897 [ $section->{'description'}. ' sub-total',
2898 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2907 $invoice_data{current_less_finance} =
2908 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2910 if ( $multisection && !$conf->exists('disable_previous_balance')
2911 || $conf->exists('previous_balance-summary_only') )
2913 unshift @sections, $previous_section if $pr_total;
2916 warn "$me adding taxes\n"
2919 foreach my $tax ( $self->_items_tax ) {
2921 $taxtotal += $tax->{'amount'};
2923 my $description = &$escape_function( $tax->{'description'} );
2924 my $amount = sprintf( '%.2f', $tax->{'amount'} );
2926 if ( $multisection ) {
2928 my $money = $old_latex ? '' : $money_char;
2929 push @detail_items, {
2930 ext_description => [],
2933 description => $description,
2934 amount => $money. $amount,
2936 section => $tax_section,
2941 push @total_items, {
2942 'total_item' => $description,
2943 'total_amount' => $other_money_char. $amount,
2948 push @buf,[ $description,
2949 $money_char. $amount,
2956 $total->{'total_item'} = 'Sub-total';
2957 $total->{'total_amount'} =
2958 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2960 if ( $multisection ) {
2961 $tax_section->{'subtotal'} = $other_money_char.
2962 sprintf('%.2f', $taxtotal);
2963 $tax_section->{'pretotal'} = 'New charges sub-total '.
2964 $total->{'total_amount'};
2965 push @sections, $tax_section if $taxtotal;
2967 unshift @total_items, $total;
2970 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2972 push @buf,['','-----------'];
2973 push @buf,[( $conf->exists('disable_previous_balance')
2975 : 'Total New Charges'
2977 $money_char. sprintf("%10.2f",$self->charged) ];
2983 $item = $conf->config('previous_balance-exclude_from_total')
2984 || 'Total New Charges'
2985 if $conf->exists('previous_balance-exclude_from_total');
2986 my $amount = $self->charged +
2987 ( $conf->exists('disable_previous_balance') ||
2988 $conf->exists('previous_balance-exclude_from_total')
2992 $total->{'total_item'} = &$embolden_function($item);
2993 $total->{'total_amount'} =
2994 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
2995 if ( $multisection ) {
2996 if ( $adjust_section->{'sort_weight'} ) {
2997 $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
2998 sprintf("%.2f", ($self->billing_balance || 0) );
3000 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
3001 sprintf('%.2f', $self->charged );
3004 push @total_items, $total;
3006 push @buf,['','-----------'];
3009 sprintf( '%10.2f', $amount )
3014 unless ( $conf->exists('disable_previous_balance') ) {
3015 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3018 my $credittotal = 0;
3019 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3022 $total->{'total_item'} = &$escape_function($credit->{'description'});
3023 $credittotal += $credit->{'amount'};
3024 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3025 $adjusttotal += $credit->{'amount'};
3026 if ( $multisection ) {
3027 my $money = $old_latex ? '' : $money_char;
3028 push @detail_items, {
3029 ext_description => [],
3032 description => &$escape_function($credit->{'description'}),
3033 amount => $money. $credit->{'amount'},
3035 section => $adjust_section,
3038 push @total_items, $total;
3042 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3045 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3046 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3050 my $paymenttotal = 0;
3051 foreach my $payment ( $self->_items_payments ) {
3053 $total->{'total_item'} = &$escape_function($payment->{'description'});
3054 $paymenttotal += $payment->{'amount'};
3055 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3056 $adjusttotal += $payment->{'amount'};
3057 if ( $multisection ) {
3058 my $money = $old_latex ? '' : $money_char;
3059 push @detail_items, {
3060 ext_description => [],
3063 description => &$escape_function($payment->{'description'}),
3064 amount => $money. $payment->{'amount'},
3066 section => $adjust_section,
3069 push @total_items, $total;
3071 push @buf, [ $payment->{'description'},
3072 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3075 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3077 if ( $multisection ) {
3078 $adjust_section->{'subtotal'} = $other_money_char.
3079 sprintf('%.2f', $adjusttotal);
3080 push @sections, $adjust_section
3081 unless $adjust_section->{sort_weight};
3086 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3087 $total->{'total_amount'} =
3088 &$embolden_function(
3089 $other_money_char. sprintf('%.2f', $summarypage
3091 $self->billing_balance
3092 : $self->owed + $pr_total
3095 if ( $multisection && !$adjust_section->{sort_weight} ) {
3096 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3097 $total->{'total_amount'};
3099 push @total_items, $total;
3101 push @buf,['','-----------'];
3102 push @buf,[$self->balance_due_msg, $money_char.
3103 sprintf("%10.2f", $balance_due ) ];
3107 if ( $multisection ) {
3108 if ($conf->exists('svc_phone_sections')) {
3110 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3111 $total->{'total_amount'} =
3112 &$embolden_function(
3113 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3115 my $last_section = pop @sections;
3116 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3117 $total->{'total_amount'};
3118 push @sections, $last_section;
3120 push @sections, @$late_sections
3124 my @includelist = ();
3125 push @includelist, 'summary' if $summarypage;
3126 foreach my $include ( @includelist ) {
3128 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3131 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3133 @inc_src = $conf->config($inc_file, $agentnum);
3137 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3139 my $convert_map = $convert_maps{$format}{$include};
3141 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3142 s/--\@\]/$delimiters{$format}[1]/g;
3145 &$convert_map( $conf->config($inc_file, $agentnum) );
3149 my $inc_tt = new Text::Template (
3151 SOURCE => [ map "$_\n", @inc_src ],
3152 DELIMITERS => $delimiters{$format},
3153 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3155 unless ( $inc_tt->compile() ) {
3156 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3157 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3161 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3163 $invoice_data{$include} =~ s/\n+$//
3164 if ($format eq 'latex');
3169 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3170 /invoice_lines\((\d*)\)/;
3171 $invoice_lines += $1 || scalar(@buf);
3174 die "no invoice_lines() functions in template?"
3175 if ( $format eq 'template' && !$wasfunc );
3177 if ($format eq 'template') {
3179 if ( $invoice_lines ) {
3180 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3181 $invoice_data{'total_pages'}++
3182 if scalar(@buf) % $invoice_lines;
3185 #setup subroutine for the template
3186 sub FS::cust_bill::_template::invoice_lines {
3187 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
3189 scalar(@FS::cust_bill::_template::buf)
3190 ? shift @FS::cust_bill::_template::buf
3199 push @collect, split("\n",
3200 $text_template->fill_in( HASH => \%invoice_data,
3201 PACKAGE => 'FS::cust_bill::_template'
3204 $FS::cust_bill::_template::page++;
3206 map "$_\n", @collect;
3208 warn "filling in template for invoice ". $self->invnum. "\n"
3210 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3213 $text_template->fill_in(HASH => \%invoice_data);
3217 # helper routine for generating date ranges
3218 sub _prior_month30s {
3221 [ 1, 2592000 ], # 0-30 days ago
3222 [ 2592000, 5184000 ], # 30-60 days ago
3223 [ 5184000, 7776000 ], # 60-90 days ago
3224 [ 7776000, 0 ], # 90+ days ago
3227 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3228 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3233 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3235 Returns an postscript invoice, as a scalar.
3237 Options can be passed as a hashref (recommended) or as a list of time, template
3238 and then any key/value pairs for any other options.
3240 I<time> an optional value used to control the printing of overdue messages. The
3241 default is now. It isn't the date of the invoice; that's the `_date' field.
3242 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3243 L<Time::Local> and L<Date::Parse> for conversion functions.
3245 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3252 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3253 my $ps = generate_ps($file);
3255 unlink($barcodefile);
3260 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3262 Returns an PDF invoice, as a scalar.
3264 Options can be passed as a hashref (recommended) or as a list of time, template
3265 and then any key/value pairs for any other options.
3267 I<time> an optional value used to control the printing of overdue messages. The
3268 default is now. It isn't the date of the invoice; that's the `_date' field.
3269 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3270 L<Time::Local> and L<Date::Parse> for conversion functions.
3272 I<template>, if specified, is the name of a suffix for alternate invoices.
3274 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3281 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3282 my $pdf = generate_pdf($file);
3284 unlink($barcodefile);
3289 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3291 Returns an HTML invoice, as a scalar.
3293 I<time> an optional value used to control the printing of overdue messages. The
3294 default is now. It isn't the date of the invoice; that's the `_date' field.
3295 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3296 L<Time::Local> and L<Date::Parse> for conversion functions.
3298 I<template>, if specified, is the name of a suffix for alternate invoices.
3300 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3302 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3303 when emailing the invoice as part of a multipart/related MIME email.
3311 %params = %{ shift() };
3313 $params{'time'} = shift;
3314 $params{'template'} = shift;
3315 $params{'cid'} = shift;
3318 $params{'format'} = 'html';
3320 $self->print_generic( %params );
3323 # quick subroutine for print_latex
3325 # There are ten characters that LaTeX treats as special characters, which
3326 # means that they do not simply typeset themselves:
3327 # # $ % & ~ _ ^ \ { }
3329 # TeX ignores blanks following an escaped character; if you want a blank (as
3330 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3334 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3335 $value =~ s/([<>])/\$$1\$/g;
3341 encode_entities($value);
3345 sub _html_escape_nbsp {
3346 my $value = _html_escape(shift);
3347 $value =~ s/ +/ /g;
3351 #utility methods for print_*
3353 sub _translate_old_latex_format {
3354 warn "_translate_old_latex_format called\n"
3361 if ( $line =~ /^%%Detail\s*$/ ) {
3363 push @template, q![@--!,
3364 q! foreach my $_tr_line (@detail_items) {!,
3365 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3366 q! $_tr_line->{'description'} .= !,
3367 q! "\\tabularnewline\n~~".!,
3368 q! join( "\\tabularnewline\n~~",!,
3369 q! @{$_tr_line->{'ext_description'}}!,
3373 while ( ( my $line_item_line = shift )
3374 !~ /^%%EndDetail\s*$/ ) {
3375 $line_item_line =~ s/'/\\'/g; # nice LTS
3376 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3377 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3378 push @template, " \$OUT .= '$line_item_line';";
3381 push @template, '}',
3384 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3386 push @template, '[@--',
3387 ' foreach my $_tr_line (@total_items) {';
3389 while ( ( my $total_item_line = shift )
3390 !~ /^%%EndTotalDetails\s*$/ ) {
3391 $total_item_line =~ s/'/\\'/g; # nice LTS
3392 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3393 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3394 push @template, " \$OUT .= '$total_item_line';";
3397 push @template, '}',
3401 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3402 push @template, $line;
3408 warn "$_\n" foreach @template;
3417 #check for an invoice-specific override
3418 return $self->invoice_terms if $self->invoice_terms;
3420 #check for a customer- specific override
3421 my $cust_main = $self->cust_main;
3422 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3424 #use configured default
3425 $conf->config('invoice_default_terms') || '';
3431 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3432 $duedate = $self->_date() + ( $1 * 86400 );
3439 $self->due_date ? time2str(shift, $self->due_date) : '';
3442 sub balance_due_msg {
3444 my $msg = 'Balance Due';
3445 return $msg unless $self->terms;
3446 if ( $self->due_date ) {
3447 $msg .= ' - Please pay by '. $self->due_date2str($date_format);
3448 } elsif ( $self->terms ) {
3449 $msg .= ' - '. $self->terms;
3454 sub balance_due_date {
3457 if ( $conf->exists('invoice_default_terms')
3458 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3459 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3464 =item invnum_date_pretty
3466 Returns a string with the invoice number and date, for example:
3467 "Invoice #54 (3/20/2008)"
3471 sub invnum_date_pretty {
3473 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3478 Returns a string with the date, for example: "3/20/2008"
3484 time2str($date_format, $self->_date);
3487 use vars qw(%pkg_category_cache);
3488 sub _items_sections {
3491 my $summarypage = shift;
3493 my $extra_sections = shift;
3497 my %late_subtotal = ();
3500 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3503 my $usage = $cust_bill_pkg->usage;
3505 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3506 next if ( $display->summary && $summarypage );
3508 my $section = $display->section;
3509 my $type = $display->type;
3511 $not_tax{$section} = 1
3512 unless $cust_bill_pkg->pkgnum == 0;
3514 if ( $display->post_total && !$summarypage ) {
3515 if (! $type || $type eq 'S') {
3516 $late_subtotal{$section} += $cust_bill_pkg->setup
3517 if $cust_bill_pkg->setup != 0;
3521 $late_subtotal{$section} += $cust_bill_pkg->recur
3522 if $cust_bill_pkg->recur != 0;
3525 if ($type && $type eq 'R') {
3526 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3527 if $cust_bill_pkg->recur != 0;
3530 if ($type && $type eq 'U') {
3531 $late_subtotal{$section} += $usage
3532 unless scalar(@$extra_sections);
3537 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3539 if (! $type || $type eq 'S') {
3540 $subtotal{$section} += $cust_bill_pkg->setup
3541 if $cust_bill_pkg->setup != 0;
3545 $subtotal{$section} += $cust_bill_pkg->recur
3546 if $cust_bill_pkg->recur != 0;
3549 if ($type && $type eq 'R') {
3550 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3551 if $cust_bill_pkg->recur != 0;
3554 if ($type && $type eq 'U') {
3555 $subtotal{$section} += $usage
3556 unless scalar(@$extra_sections);
3565 %pkg_category_cache = ();
3567 push @$late, map { { 'description' => &{$escape}($_),
3568 'subtotal' => $late_subtotal{$_},
3570 'sort_weight' => ( _pkg_category($_)
3571 ? _pkg_category($_)->weight
3574 ((_pkg_category($_) && _pkg_category($_)->condense)
3575 ? $self->_condense_section($format)
3579 sort _sectionsort keys %late_subtotal;
3582 if ( $summarypage ) {
3583 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3584 map { $_->categoryname } qsearch('pkg_category', {});
3585 push @sections, '' if exists($subtotal{''});
3587 @sections = keys %subtotal;
3590 my @early = map { { 'description' => &{$escape}($_),
3591 'subtotal' => $subtotal{$_},
3592 'summarized' => $not_tax{$_} ? '' : 'Y',
3593 'tax_section' => $not_tax{$_} ? '' : 'Y',
3594 'sort_weight' => ( _pkg_category($_)
3595 ? _pkg_category($_)->weight
3598 ((_pkg_category($_) && _pkg_category($_)->condense)
3599 ? $self->_condense_section($format)
3604 push @early, @$extra_sections if $extra_sections;
3606 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3610 #helper subs for above
3613 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3617 my $categoryname = shift;
3618 $pkg_category_cache{$categoryname} ||=
3619 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3622 my %condensed_format = (
3623 'label' => [ qw( Description Qty Amount ) ],
3625 sub { shift->{description} },
3626 sub { shift->{quantity} },
3627 sub { my($href, %opt) = @_;
3628 ($opt{dollar} || ''). $href->{amount};
3631 'align' => [ qw( l r r ) ],
3632 'span' => [ qw( 5 1 1 ) ], # unitprices?
3633 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3636 sub _condense_section {
3637 my ( $self, $format ) = ( shift, shift );
3639 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3640 qw( description_generator
3643 total_line_generator
3648 sub _condensed_generator_defaults {
3649 my ( $self, $format ) = ( shift, shift );
3650 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3659 sub _condensed_header_generator {
3660 my ( $self, $format ) = ( shift, shift );
3662 my ( $f, $prefix, $suffix, $separator, $column ) =
3663 _condensed_generator_defaults($format);
3665 if ($format eq 'latex') {
3666 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3667 $suffix = "\\\\\n\\hline";
3670 sub { my ($d,$a,$s,$w) = @_;
3671 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3673 } elsif ( $format eq 'html' ) {
3674 $prefix = '<th></th>';
3678 sub { my ($d,$a,$s,$w) = @_;
3679 return qq!<th align="$html_align{$a}">$d</th>!;
3687 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3689 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3692 $prefix. join($separator, @result). $suffix;
3697 sub _condensed_description_generator {
3698 my ( $self, $format ) = ( shift, shift );
3700 my ( $f, $prefix, $suffix, $separator, $column ) =
3701 _condensed_generator_defaults($format);
3703 my $money_char = '$';
3704 if ($format eq 'latex') {
3705 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3707 $separator = " & \n";
3709 sub { my ($d,$a,$s,$w) = @_;
3710 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3712 $money_char = '\\dollar';
3713 }elsif ( $format eq 'html' ) {
3714 $prefix = '"><td align="center"></td>';
3718 sub { my ($d,$a,$s,$w) = @_;
3719 return qq!<td align="$html_align{$a}">$d</td>!;
3721 #$money_char = $conf->config('money_char') || '$';
3722 $money_char = ''; # this is madness
3730 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3732 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3734 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3735 map { $f->{$_}->[$i] } qw(align span width)
3739 $prefix. join( $separator, @result ). $suffix;
3744 sub _condensed_total_generator {
3745 my ( $self, $format ) = ( shift, shift );
3747 my ( $f, $prefix, $suffix, $separator, $column ) =
3748 _condensed_generator_defaults($format);
3751 if ($format eq 'latex') {
3754 $separator = " & \n";
3756 sub { my ($d,$a,$s,$w) = @_;
3757 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3759 }elsif ( $format eq 'html' ) {
3763 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3765 sub { my ($d,$a,$s,$w) = @_;
3766 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3775 # my $r = &{$f->{fields}->[$i]}(@args);
3776 # $r .= ' Total' unless $i;
3778 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3780 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3781 map { $f->{$_}->[$i] } qw(align span width)
3785 $prefix. join( $separator, @result ). $suffix;
3790 =item total_line_generator FORMAT
3792 Returns a coderef used for generation of invoice total line items for this
3793 usage_class. FORMAT is either html or latex
3797 # should not be used: will have issues with hash element names (description vs
3798 # total_item and amount vs total_amount -- another array of functions?
3800 sub _condensed_total_line_generator {
3801 my ( $self, $format ) = ( shift, shift );
3803 my ( $f, $prefix, $suffix, $separator, $column ) =
3804 _condensed_generator_defaults($format);
3807 if ($format eq 'latex') {
3810 $separator = " & \n";
3812 sub { my ($d,$a,$s,$w) = @_;
3813 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3815 }elsif ( $format eq 'html' ) {
3819 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3821 sub { my ($d,$a,$s,$w) = @_;
3822 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3831 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3833 &{$column}( &{$f->{fields}->[$i]}(@args),
3834 map { $f->{$_}->[$i] } qw(align span width)
3838 $prefix. join( $separator, @result ). $suffix;
3843 #sub _items_extra_usage_sections {
3845 # my $escape = shift;
3847 # my %sections = ();
3849 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
3850 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3852 # next unless $cust_bill_pkg->pkgnum > 0;
3854 # foreach my $section ( keys %usage_class ) {
3856 # my $usage = $cust_bill_pkg->usage($section);
3858 # next unless $usage && $usage > 0;
3860 # $sections{$section} ||= 0;
3861 # $sections{$section} += $usage;
3867 # map { { 'description' => &{$escape}($_),
3868 # 'subtotal' => $sections{$_},
3869 # 'summarized' => '',
3870 # 'tax_section' => '',
3873 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3877 sub _items_extra_usage_sections {
3886 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3887 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3888 next unless $cust_bill_pkg->pkgnum > 0;
3890 foreach my $classnum ( keys %usage_class ) {
3891 my $section = $usage_class{$classnum}->classname;
3892 $classnums{$section} = $classnum;
3894 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3895 my $amount = $detail->amount;
3896 next unless $amount && $amount > 0;
3898 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
3899 $sections{$section}{amount} += $amount; #subtotal
3900 $sections{$section}{calls}++;
3901 $sections{$section}{duration} += $detail->duration;
3903 my $desc = $detail->regionname;
3904 my $description = $desc;
3905 $description = substr($desc, 0, 50). '...'
3906 if $format eq 'latex' && length($desc) > 50;
3908 $lines{$section}{$desc} ||= {
3909 description => &{$escape}($description),
3910 #pkgpart => $part_pkg->pkgpart,
3911 pkgnum => $cust_bill_pkg->pkgnum,
3916 #unit_amount => $cust_bill_pkg->unitrecur,
3917 quantity => $cust_bill_pkg->quantity,
3918 product_code => 'N/A',
3919 ext_description => [],
3922 $lines{$section}{$desc}{amount} += $amount;
3923 $lines{$section}{$desc}{calls}++;
3924 $lines{$section}{$desc}{duration} += $detail->duration;
3930 my %sectionmap = ();
3931 foreach (keys %sections) {
3932 my $usage_class = $usage_class{$classnums{$_}};
3933 $sectionmap{$_} = { 'description' => &{$escape}($_),
3934 'amount' => $sections{$_}{amount}, #subtotal
3935 'calls' => $sections{$_}{calls},
3936 'duration' => $sections{$_}{duration},
3938 'tax_section' => '',
3939 'sort_weight' => $usage_class->weight,
3940 ( $usage_class->format
3941 ? ( map { $_ => $usage_class->$_($format) }
3942 qw( description_generator header_generator total_generator total_line_generator )
3949 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
3953 foreach my $section ( keys %lines ) {
3954 foreach my $line ( keys %{$lines{$section}} ) {
3955 my $l = $lines{$section}{$line};
3956 $l->{section} = $sectionmap{$section};
3957 $l->{amount} = sprintf( "%.2f", $l->{amount} );
3958 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3963 return(\@sections, \@lines);
3969 my $end = $self->_date;
3970 my $start = $end - 2592000; # 30 days
3971 my $cust_main = $self->cust_main;
3972 my @pkgs = $cust_main->all_pkgs;
3973 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
3976 foreach my $pkg ( @pkgs ) {
3977 my @h_cust_svc = $pkg->h_cust_svc($end);
3978 foreach my $h_cust_svc ( @h_cust_svc ) {
3979 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
3980 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
3982 my $inserted = $h_cust_svc->date_inserted;
3983 my $deleted = $h_cust_svc->date_deleted;
3984 my $phone_inserted = $h_cust_svc->h_svc_x($inserted);
3986 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
3988 # DID either activated or ported in; cannot be both for same DID simultaneously
3989 if ($inserted >= $start && $inserted <= $end && $phone_inserted
3990 && (!$phone_inserted->lnp_status
3991 || $phone_inserted->lnp_status eq ''
3992 || $phone_inserted->lnp_status eq 'native')) {
3995 else { # this one not so clean, should probably move to (h_)svc_phone
3996 my $phone_portedin = qsearchs( 'h_svc_phone',
3997 { 'svcnum' => $h_cust_svc->svcnum,
3998 'lnp_status' => 'portedin' },
3999 FS::h_svc_phone->sql_h_searchs($end),
4001 $num_portedin++ if $phone_portedin;
4004 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4005 if($deleted >= $start && $deleted <= $end && $phone_deleted
4006 && (!$phone_deleted->lnp_status
4007 || $phone_deleted->lnp_status ne 'portingout')) {
4010 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4011 && $phone_deleted->lnp_status
4012 && $phone_deleted->lnp_status eq 'portingout') {
4016 # increment usage minutes
4017 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end);
4018 foreach my $cdr ( @cdrs ) {
4019 $minutes += $cdr->billsec/60;
4022 # don't look at this service again
4023 push @seen, $h_cust_svc->svcnum;
4027 $minutes = sprintf("%d", $minutes);
4028 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4029 . "$num_deactivated Ported-Out: $num_portedout ",
4030 "Total Minutes: $minutes");
4033 sub _items_svc_phone_sections {
4042 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4043 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4045 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4046 next unless $cust_bill_pkg->pkgnum > 0;
4048 my @header = $cust_bill_pkg->details_header;
4049 next unless scalar(@header);
4051 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4053 my $phonenum = $detail->phonenum;
4054 next unless $phonenum;
4056 my $amount = $detail->amount;
4057 next unless $amount && $amount > 0;
4059 $sections{$phonenum} ||= { 'amount' => 0,
4062 'sort_weight' => -1,
4063 'phonenum' => $phonenum,
4065 $sections{$phonenum}{amount} += $amount; #subtotal
4066 $sections{$phonenum}{calls}++;
4067 $sections{$phonenum}{duration} += $detail->duration;
4069 my $desc = $detail->regionname;
4070 my $description = $desc;
4071 $description = substr($desc, 0, 50). '...'
4072 if $format eq 'latex' && length($desc) > 50;
4074 $lines{$phonenum}{$desc} ||= {
4075 description => &{$escape}($description),
4076 #pkgpart => $part_pkg->pkgpart,
4084 product_code => 'N/A',
4085 ext_description => [],
4088 $lines{$phonenum}{$desc}{amount} += $amount;
4089 $lines{$phonenum}{$desc}{calls}++;
4090 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4092 my $line = $usage_class{$detail->classnum}->classname;
4093 $sections{"$phonenum $line"} ||=
4097 'sort_weight' => $usage_class{$detail->classnum}->weight,
4098 'phonenum' => $phonenum,
4099 'header' => [ @header ],
4101 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4102 $sections{"$phonenum $line"}{calls}++;
4103 $sections{"$phonenum $line"}{duration} += $detail->duration;
4105 $lines{"$phonenum $line"}{$desc} ||= {
4106 description => &{$escape}($description),
4107 #pkgpart => $part_pkg->pkgpart,
4115 product_code => 'N/A',
4116 ext_description => [],
4119 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4120 $lines{"$phonenum $line"}{$desc}{calls}++;
4121 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4122 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4123 $detail->formatted('format' => $format);
4128 my %sectionmap = ();
4129 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4130 foreach ( keys %sections ) {
4131 my @header = @{ $sections{$_}{header} || [] };
4133 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4134 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4135 my $usage_class = $summary ? $simple : $usage_simple;
4136 my $ending = $summary ? ' usage charges' : '';
4139 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4141 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4142 'amount' => $sections{$_}{amount}, #subtotal
4143 'calls' => $sections{$_}{calls},
4144 'duration' => $sections{$_}{duration},
4146 'tax_section' => '',
4147 'phonenum' => $sections{$_}{phonenum},
4148 'sort_weight' => $sections{$_}{sort_weight},
4149 'post_total' => $summary, #inspire pagebreak
4151 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4152 qw( description_generator
4155 total_line_generator
4162 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4163 $a->{sort_weight} <=> $b->{sort_weight}
4168 foreach my $section ( keys %lines ) {
4169 foreach my $line ( keys %{$lines{$section}} ) {
4170 my $l = $lines{$section}{$line};
4171 $l->{section} = $sectionmap{$section};
4172 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4173 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4178 return(\@sections, \@lines);
4185 #my @display = scalar(@_)
4187 # : qw( _items_previous _items_pkg );
4188 # #: qw( _items_pkg );
4189 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4190 my @display = qw( _items_previous _items_pkg );
4193 foreach my $display ( @display ) {
4194 push @b, $self->$display(@_);
4199 sub _items_previous {
4201 my $cust_main = $self->cust_main;
4202 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4204 foreach ( @pr_cust_bill ) {
4205 my $date = $conf->exists('invoice_show_prior_due_date')
4206 ? 'due '. $_->due_date2str($date_format)
4207 : time2str($date_format, $_->_date);
4209 'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
4210 #'pkgpart' => 'N/A',
4212 'amount' => sprintf("%.2f", $_->owed),
4218 # 'description' => 'Previous Balance',
4219 # #'pkgpart' => 'N/A',
4220 # 'pkgnum' => 'N/A',
4221 # 'amount' => sprintf("%10.2f", $pr_total ),
4222 # 'ext_description' => [ map {
4223 # "Invoice ". $_->invnum.
4224 # " (". time2str("%x",$_->_date). ") ".
4225 # sprintf("%10.2f", $_->owed)
4226 # } @pr_cust_bill ],
4235 warn "$me _items_pkg searching for all package line items\n"
4238 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4240 warn "$me _items_pkg filtering line items\n"
4242 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4244 if ($options{section} && $options{section}->{condensed}) {
4246 warn "$me _items_pkg condensing section\n"
4250 local $Storable::canonical = 1;
4251 foreach ( @items ) {
4253 delete $item->{ref};
4254 delete $item->{ext_description};
4255 my $key = freeze($item);
4256 $itemshash{$key} ||= 0;
4257 $itemshash{$key} ++; # += $item->{quantity};
4259 @items = sort { $a->{description} cmp $b->{description} }
4260 map { my $i = thaw($_);
4261 $i->{quantity} = $itemshash{$_};
4263 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4269 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4276 return 0 unless $a->itemdesc cmp $b->itemdesc;
4277 return -1 if $b->itemdesc eq 'Tax';
4278 return 1 if $a->itemdesc eq 'Tax';
4279 return -1 if $b->itemdesc eq 'Other surcharges';
4280 return 1 if $a->itemdesc eq 'Other surcharges';
4281 $a->itemdesc cmp $b->itemdesc;
4286 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4287 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4290 sub _items_cust_bill_pkg {
4292 my $cust_bill_pkgs = shift;
4295 my $format = $opt{format} || '';
4296 my $escape_function = $opt{escape_function} || sub { shift };
4297 my $format_function = $opt{format_function} || '';
4298 my $unsquelched = $opt{unsquelched} || '';
4299 my $section = $opt{section}->{description} if $opt{section};
4300 my $summary_page = $opt{summary_page} || '';
4301 my $multilocation = $opt{multilocation} || '';
4302 my $multisection = $opt{multisection} || '';
4303 my $discount_show_always = 0;
4306 my ($s, $r, $u) = ( undef, undef, undef );
4307 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4310 warn "$me _items_cust_bill_pkg considering cust_bill_pkg $cust_bill_pkg\n"
4313 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
4314 && $conf->exists('discount-show-always'));
4316 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4317 if ( $_ && !$cust_bill_pkg->hidden ) {
4318 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4319 $_->{amount} =~ s/^\-0\.00$/0.00/;
4320 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4322 unless ( $_->{amount} == 0 && !$discount_show_always );
4327 foreach my $display ( grep { defined($section)
4328 ? $_->section eq $section
4331 #grep { !$_->summary || !$summary_page } # bunk!
4332 grep { !$_->summary || $multisection }
4333 $cust_bill_pkg->cust_bill_pkg_display
4337 warn "$me _items_cust_bill_pkg considering display item $display\n"
4340 my $type = $display->type;
4342 my $desc = $cust_bill_pkg->desc;
4343 $desc = substr($desc, 0, 50). '...'
4344 if $format eq 'latex' && length($desc) > 50;
4346 my %details_opt = ( 'format' => $format,
4347 'escape_function' => $escape_function,
4348 'format_function' => $format_function,
4351 if ( $cust_bill_pkg->pkgnum > 0 ) {
4353 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4356 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4358 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
4360 warn "$me _items_cust_bill_pkg adding setup\n"
4363 my $description = $desc;
4364 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
4367 unless ( $cust_pkg->part_pkg->hide_svc_detail
4368 || $cust_bill_pkg->hidden )
4371 push @d, map &{$escape_function}($_),
4372 $cust_pkg->h_labels_short($self->_date, undef, 'I')
4373 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4375 if ( $multilocation ) {
4376 my $loc = $cust_pkg->location_label;
4377 $loc = substr($loc, 0, 50). '...'
4378 if $format eq 'latex' && length($loc) > 50;
4379 push @d, &{$escape_function}($loc);
4384 push @d, $cust_bill_pkg->details(%details_opt)
4385 if $cust_bill_pkg->recur == 0;
4387 if ( $cust_bill_pkg->hidden ) {
4388 $s->{amount} += $cust_bill_pkg->setup;
4389 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4390 push @{ $s->{ext_description} }, @d;
4393 description => $description,
4394 #pkgpart => $part_pkg->pkgpart,
4395 pkgnum => $cust_bill_pkg->pkgnum,
4396 amount => $cust_bill_pkg->setup,
4397 unit_amount => $cust_bill_pkg->unitsetup,
4398 quantity => $cust_bill_pkg->quantity,
4399 ext_description => \@d,
4405 if ( ( $cust_bill_pkg->recur != 0 || $cust_bill_pkg->setup == 0 ||
4406 ($discount_show_always && $cust_bill_pkg->recur == 0) ) &&
4407 ( !$type || $type eq 'R' || $type eq 'U' )
4411 warn "$me _items_cust_bill_pkg adding recur/usage\n"
4414 my $is_summary = $display->summary;
4415 my $description = ($is_summary && $type && $type eq 'U')
4416 ? "Usage charges" : $desc;
4418 $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4419 " - ". time2str($date_format, $cust_bill_pkg->edate).
4421 unless $conf->exists('disable_line_item_date_ranges');
4425 #at least until cust_bill_pkg has "past" ranges in addition to
4426 #the "future" sdate/edate ones... see #3032
4427 my @dates = ( $self->_date );
4428 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4429 push @dates, $prev->sdate if $prev;
4430 push @dates, undef if !$prev;
4432 unless ( $cust_pkg->part_pkg->hide_svc_detail
4433 || $cust_bill_pkg->itemdesc
4434 || $cust_bill_pkg->hidden
4435 || $is_summary && $type && $type eq 'U' )
4438 warn "$me _items_cust_bill_pkg adding service details\n"
4441 push @d, map &{$escape_function}($_),
4442 $cust_pkg->h_labels_short(@dates, 'I')
4443 #$cust_bill_pkg->edate,
4444 #$cust_bill_pkg->sdate)
4445 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4447 warn "$me _items_cust_bill_pkg done adding service details\n"
4450 if ( $multilocation ) {
4451 my $loc = $cust_pkg->location_label;
4452 $loc = substr($loc, 0, 50). '...'
4453 if $format eq 'latex' && length($loc) > 50;
4454 push @d, &{$escape_function}($loc);
4459 warn "$me _items_cust_bill_pkg adding details\n"
4462 push @d, $cust_bill_pkg->details(%details_opt)
4463 unless ($is_summary || $type && $type eq 'R');
4465 warn "$me _items_cust_bill_pkg calculating amount\n"
4470 $amount = $cust_bill_pkg->recur;
4471 }elsif($type eq 'R') {
4472 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4473 }elsif($type eq 'U') {
4474 $amount = $cust_bill_pkg->usage;
4477 if ( !$type || $type eq 'R' ) {
4479 warn "$me _items_cust_bill_pkg adding recur\n"
4482 if ( $cust_bill_pkg->hidden ) {
4483 $r->{amount} += $amount;
4484 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4485 push @{ $r->{ext_description} }, @d;
4488 description => $description,
4489 #pkgpart => $part_pkg->pkgpart,
4490 pkgnum => $cust_bill_pkg->pkgnum,
4492 unit_amount => $cust_bill_pkg->unitrecur,
4493 quantity => $cust_bill_pkg->quantity,
4494 ext_description => \@d,
4498 } else { # $type eq 'U'
4500 warn "$me _items_cust_bill_pkg adding usage\n"
4503 if ( $cust_bill_pkg->hidden ) {
4504 $u->{amount} += $amount;
4505 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4506 push @{ $u->{ext_description} }, @d;
4509 description => $description,
4510 #pkgpart => $part_pkg->pkgpart,
4511 pkgnum => $cust_bill_pkg->pkgnum,
4513 unit_amount => $cust_bill_pkg->unitrecur,
4514 quantity => $cust_bill_pkg->quantity,
4515 ext_description => \@d,
4521 } # recurring or usage with recurring charge
4523 } else { #pkgnum tax or one-shot line item (??)
4525 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
4528 if ( $cust_bill_pkg->setup != 0 ) {
4530 'description' => $desc,
4531 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
4534 if ( $cust_bill_pkg->recur != 0 ) {
4536 'description' => "$desc (".
4537 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
4538 time2str($date_format, $cust_bill_pkg->edate). ')',
4539 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
4549 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
4552 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4554 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4555 $_->{amount} =~ s/^\-0\.00$/0.00/;
4556 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4558 unless ( $_->{amount} == 0 && !$discount_show_always );
4566 sub _items_credits {
4567 my( $self, %opt ) = @_;
4568 my $trim_len = $opt{'trim_len'} || 60;
4572 foreach ( $self->cust_credited ) {
4574 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4576 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4577 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4578 $reason = " ($reason) " if $reason;
4581 #'description' => 'Credit ref\#'. $_->crednum.
4582 # " (". time2str("%x",$_->cust_credit->_date) .")".
4584 'description' => 'Credit applied '.
4585 time2str($date_format,$_->cust_credit->_date). $reason,
4586 'amount' => sprintf("%.2f",$_->amount),
4594 sub _items_payments {
4598 #get & print payments
4599 foreach ( $self->cust_bill_pay ) {
4601 #something more elaborate if $_->amount ne ->cust_pay->paid ?
4604 'description' => "Payment received ".
4605 time2str($date_format,$_->cust_pay->_date ),
4606 'amount' => sprintf("%.2f", $_->amount )
4614 =item call_details [ OPTION => VALUE ... ]
4616 Returns an array of CSV strings representing the call details for this invoice
4617 The only option available is the boolean prepend_billed_number
4622 my ($self, %opt) = @_;
4624 my $format_function = sub { shift };
4626 if ($opt{prepend_billed_number}) {
4627 $format_function = sub {
4631 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4636 my @details = map { $_->details( 'format_function' => $format_function,
4637 'escape_function' => sub{ return() },
4641 $self->cust_bill_pkg;
4642 my $header = $details[0];
4643 ( $header, grep { $_ ne $header } @details );
4653 =item process_reprint
4657 sub process_reprint {
4658 process_re_X('print', @_);
4661 =item process_reemail
4665 sub process_reemail {
4666 process_re_X('email', @_);
4674 process_re_X('fax', @_);
4682 process_re_X('ftp', @_);
4689 sub process_respool {
4690 process_re_X('spool', @_);
4693 use Storable qw(thaw);
4697 my( $method, $job ) = ( shift, shift );
4698 warn "$me process_re_X $method for job $job\n" if $DEBUG;
4700 my $param = thaw(decode_base64(shift));
4701 warn Dumper($param) if $DEBUG;
4712 my($method, $job, %param ) = @_;
4714 warn "re_X $method for job $job with param:\n".
4715 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
4718 #some false laziness w/search/cust_bill.html
4720 my $orderby = 'ORDER BY cust_bill._date';
4722 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
4724 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4726 my @cust_bill = qsearch( {
4727 #'select' => "cust_bill.*",
4728 'table' => 'cust_bill',
4729 'addl_from' => $addl_from,
4731 'extra_sql' => $extra_sql,
4732 'order_by' => $orderby,
4736 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4738 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
4741 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
4742 foreach my $cust_bill ( @cust_bill ) {
4743 $cust_bill->$method();
4745 if ( $job ) { #progressbar foo
4747 if ( time - $min_sec > $last ) {
4748 my $error = $job->update_statustext(
4749 int( 100 * $num / scalar(@cust_bill) )
4751 die $error if $error;
4762 =head1 CLASS METHODS
4768 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
4773 my ($class, $start, $end) = @_;
4775 $class->paid_sql($start, $end). ' - '.
4776 $class->credited_sql($start, $end);
4781 Returns an SQL fragment to retreive the net amount (charged minus credited).
4786 my ($class, $start, $end) = @_;
4787 'charged - '. $class->credited_sql($start, $end);
4792 Returns an SQL fragment to retreive the amount paid against this invoice.
4797 my ($class, $start, $end) = @_;
4798 $start &&= "AND cust_bill_pay._date <= $start";
4799 $end &&= "AND cust_bill_pay._date > $end";
4800 $start = '' unless defined($start);
4801 $end = '' unless defined($end);
4802 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
4803 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
4808 Returns an SQL fragment to retreive the amount credited against this invoice.
4813 my ($class, $start, $end) = @_;
4814 $start &&= "AND cust_credit_bill._date <= $start";
4815 $end &&= "AND cust_credit_bill._date > $end";
4816 $start = '' unless defined($start);
4817 $end = '' unless defined($end);
4818 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
4819 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
4824 Returns an SQL fragment to retrieve the due date of an invoice.
4825 Currently only supported on PostgreSQL.
4833 cust_bill.invoice_terms,
4834 cust_main.invoice_terms,
4835 \''.($conf->config('invoice_default_terms') || '').'\'
4836 ), E\'Net (\\\\d+)\'
4838 ) * 86400 + cust_bill._date'
4841 =item search_sql_where HASHREF
4843 Class method which returns an SQL WHERE fragment to search for parameters
4844 specified in HASHREF. Valid parameters are
4850 List reference of start date, end date, as UNIX timestamps.
4860 List reference of charged limits (exclusive).
4864 List reference of charged limits (exclusive).
4868 flag, return open invoices only
4872 flag, return net invoices only
4876 =item newest_percust
4880 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
4884 sub search_sql_where {
4885 my($class, $param) = @_;
4887 warn "$me search_sql_where called with params: \n".
4888 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
4894 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
4895 push @search, "cust_main.agentnum = $1";
4899 if ( $param->{_date} ) {
4900 my($beginning, $ending) = @{$param->{_date}};
4902 push @search, "cust_bill._date >= $beginning",
4903 "cust_bill._date < $ending";
4907 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
4908 push @search, "cust_bill.invnum >= $1";
4910 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
4911 push @search, "cust_bill.invnum <= $1";
4915 if ( $param->{charged} ) {
4916 my @charged = ref($param->{charged})
4917 ? @{ $param->{charged} }
4918 : ($param->{charged});
4920 push @search, map { s/^charged/cust_bill.charged/; $_; }
4924 my $owed_sql = FS::cust_bill->owed_sql;
4927 if ( $param->{owed} ) {
4928 my @owed = ref($param->{owed})
4929 ? @{ $param->{owed} }
4931 push @search, map { s/^owed/$owed_sql/; $_; }
4936 push @search, "0 != $owed_sql"
4937 if $param->{'open'};
4938 push @search, '0 != '. FS::cust_bill->net_sql
4942 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
4943 if $param->{'days'};
4946 if ( $param->{'newest_percust'} ) {
4948 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
4949 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
4951 my @newest_where = map { my $x = $_;
4952 $x =~ s/\bcust_bill\./newest_cust_bill./g;
4955 grep ! /^cust_main./, @search;
4956 my $newest_where = scalar(@newest_where)
4957 ? ' AND '. join(' AND ', @newest_where)
4961 push @search, "cust_bill._date = (
4962 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
4963 WHERE newest_cust_bill.custnum = cust_bill.custnum
4969 #agent virtualization
4970 my $curuser = $FS::CurrentUser::CurrentUser;
4971 if ( $curuser->username eq 'fs_queue'
4972 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
4974 my $newuser = qsearchs('access_user', {
4975 'username' => $username,
4979 $curuser = $newuser;
4981 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
4984 push @search, $curuser->agentnums_sql;
4986 join(' AND ', @search );
4998 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
4999 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base